1# coding=utf8 2 3import os 4import logging 5import argparse 6from contextlib import contextmanager 7import importlib 8import sys 9 10import hglib 11import six 12 13from fluent.migrate.context import MigrationContext 14from fluent.migrate.errors import MigrationError 15from fluent.migrate.changesets import convert_blame_to_changesets 16from fluent.migrate.blame import Blame 17 18 19@contextmanager 20def dont_write_bytecode(): 21 _dont_write_bytecode = sys.dont_write_bytecode 22 sys.dont_write_bytecode = True 23 yield 24 sys.dont_write_bytecode = _dont_write_bytecode 25 26 27class Migrator(object): 28 def __init__(self, locale, reference_dir, localization_dir, dry_run): 29 self.locale = locale 30 self.reference_dir = reference_dir 31 self.localization_dir = localization_dir 32 self.dry_run = dry_run 33 self._client = None 34 35 @property 36 def client(self): 37 if self._client is None: 38 self._client = hglib.open(self.localization_dir, 'utf-8') 39 return self._client 40 41 def close(self): 42 # close hglib.client, if we cached one. 43 if self._client is not None: 44 self._client.close() 45 46 def run(self, migration): 47 print('\nRunning migration {} for {}'.format( 48 migration.__name__, self.locale)) 49 50 # For each migration create a new context. 51 ctx = MigrationContext( 52 self.locale, self.reference_dir, self.localization_dir 53 ) 54 55 try: 56 # Add the migration spec. 57 migration.migrate(ctx) 58 except MigrationError as e: 59 print(' Skipping migration {} for {}:\n {}'.format( 60 migration.__name__, self.locale, e)) 61 return 62 63 # Keep track of how many changesets we're committing. 64 index = 0 65 description_template = migration.migrate.__doc__ 66 67 # Annotate localization files used as sources by this migration 68 # to preserve attribution of translations. 69 files = ctx.localization_resources.keys() 70 blame = Blame(self.client).attribution(files) 71 changesets = convert_blame_to_changesets(blame) 72 known_legacy_translations = set() 73 74 for changeset in changesets: 75 snapshot = self.snapshot( 76 ctx, changeset['changes'], known_legacy_translations 77 ) 78 if not snapshot: 79 continue 80 self.serialize_changeset(snapshot) 81 index += 1 82 self.commit_changeset( 83 description_template, changeset['author'], index 84 ) 85 86 def snapshot(self, ctx, changes_in_changeset, known_legacy_translations): 87 '''Run the migration for the changeset, with the set of 88 this and all prior legacy translations. 89 ''' 90 known_legacy_translations.update(changes_in_changeset) 91 return ctx.serialize_changeset( 92 changes_in_changeset, 93 known_legacy_translations 94 ) 95 96 def serialize_changeset(self, snapshot): 97 '''Write serialized FTL files to disk.''' 98 for path, content in six.iteritems(snapshot): 99 fullpath = os.path.join(self.localization_dir, path) 100 print(' Writing to {}'.format(fullpath)) 101 if not self.dry_run: 102 fulldir = os.path.dirname(fullpath) 103 if not os.path.isdir(fulldir): 104 os.makedirs(fulldir) 105 with open(fullpath, 'wb') as f: 106 f.write(content.encode('utf8')) 107 f.close() 108 109 def commit_changeset( 110 self, description_template, author, index 111 ): 112 message = description_template.format( 113 index=index, 114 author=author 115 ) 116 117 print(' Committing changeset: {}'.format(message)) 118 if self.dry_run: 119 return 120 try: 121 self.client.commit( 122 message, user=author.encode('utf-8'), addremove=True 123 ) 124 except hglib.error.CommandError as err: 125 print(' WARNING: hg commit failed ({})'.format(err)) 126 127 128def main(locale, reference_dir, localization_dir, migrations, dry_run): 129 """Run migrations and commit files with the result.""" 130 migrator = Migrator(locale, reference_dir, localization_dir, dry_run) 131 132 for migration in migrations: 133 migrator.run(migration) 134 135 migrator.close() 136 137 138def cli(): 139 parser = argparse.ArgumentParser( 140 description='Migrate translations to FTL.' 141 ) 142 parser.add_argument( 143 'migrations', metavar='MIGRATION', type=str, nargs='+', 144 help='migrations to run (Python modules)' 145 ) 146 parser.add_argument( 147 '--locale', '--lang', type=str, 148 help='target locale code (--lang is deprecated)' 149 ) 150 parser.add_argument( 151 '--reference-dir', type=str, 152 help='directory with reference FTL files' 153 ) 154 parser.add_argument( 155 '--localization-dir', type=str, 156 help='directory for localization files' 157 ) 158 parser.add_argument( 159 '--dry-run', action='store_true', 160 help='do not write to disk nor commit any changes' 161 ) 162 parser.set_defaults(dry_run=False) 163 164 logger = logging.getLogger('migrate') 165 logger.setLevel(logging.INFO) 166 167 args = parser.parse_args() 168 169 # Don't byte-compile migrations. 170 # They're not our code, and infrequently run 171 with dont_write_bytecode(): 172 migrations = map(importlib.import_module, args.migrations) 173 174 main( 175 locale=args.locale, 176 reference_dir=args.reference_dir, 177 localization_dir=args.localization_dir, 178 migrations=migrations, 179 dry_run=args.dry_run 180 ) 181 182 183if __name__ == '__main__': 184 cli() 185