import os import logging import argparse from contextlib import contextmanager import importlib import sys import hglib from fluent.migrate.context import MigrationContext from fluent.migrate.errors import MigrationError from fluent.migrate.changesets import convert_blame_to_changesets from fluent.migrate.blame import Blame @contextmanager def dont_write_bytecode(): _dont_write_bytecode = sys.dont_write_bytecode sys.dont_write_bytecode = True yield sys.dont_write_bytecode = _dont_write_bytecode class Migrator: def __init__(self, locale, reference_dir, localization_dir, dry_run): self.locale = locale self.reference_dir = reference_dir self.localization_dir = localization_dir self.dry_run = dry_run self._client = None @property def client(self): if self._client is None: self._client = hglib.open(self.localization_dir, 'utf-8') return self._client def close(self): # close hglib.client, if we cached one. if self._client is not None: self._client.close() def run(self, migration): print('\nRunning migration {} for {}'.format( migration.__name__, self.locale)) # For each migration create a new context. ctx = MigrationContext( self.locale, self.reference_dir, self.localization_dir ) try: # Add the migration spec. migration.migrate(ctx) except MigrationError as e: print(' Skipping migration {} for {}:\n {}'.format( migration.__name__, self.locale, e)) return # Keep track of how many changesets we're committing. index = 0 description_template = migration.migrate.__doc__ # Annotate localization files used as sources by this migration # to preserve attribution of translations. files = ctx.localization_resources.keys() blame = Blame(self.client).attribution(files) changesets = convert_blame_to_changesets(blame) known_legacy_translations = set() for changeset in changesets: snapshot = self.snapshot( ctx, changeset['changes'], known_legacy_translations ) if not snapshot: continue self.serialize_changeset(snapshot) index += 1 self.commit_changeset( description_template, changeset['author'], index ) def snapshot(self, ctx, changes_in_changeset, known_legacy_translations): '''Run the migration for the changeset, with the set of this and all prior legacy translations. ''' known_legacy_translations.update(changes_in_changeset) return ctx.serialize_changeset( changes_in_changeset, known_legacy_translations ) def serialize_changeset(self, snapshot): '''Write serialized FTL files to disk.''' for path, content in snapshot.items(): fullpath = os.path.join(self.localization_dir, path) print(f' Writing to {fullpath}') if not self.dry_run: fulldir = os.path.dirname(fullpath) if not os.path.isdir(fulldir): os.makedirs(fulldir) with open(fullpath, 'wb') as f: f.write(content.encode('utf8')) f.close() def commit_changeset( self, description_template, author, index ): message = description_template.format( index=index, author=author ) print(f' Committing changeset: {message}') if self.dry_run: return try: self.client.commit( message, user=author.encode('utf-8'), addremove=True ) except hglib.error.CommandError as err: print(f' WARNING: hg commit failed ({err})') def main(locale, reference_dir, localization_dir, migrations, dry_run): """Run migrations and commit files with the result.""" migrator = Migrator(locale, reference_dir, localization_dir, dry_run) for migration in migrations: migrator.run(migration) migrator.close() def cli(): parser = argparse.ArgumentParser( description='Migrate translations to FTL.' ) parser.add_argument( 'migrations', metavar='MIGRATION', type=str, nargs='+', help='migrations to run (Python modules)' ) parser.add_argument( '--locale', '--lang', type=str, help='target locale code (--lang is deprecated)' ) parser.add_argument( '--reference-dir', type=str, help='directory with reference FTL files' ) parser.add_argument( '--localization-dir', type=str, help='directory for localization files' ) parser.add_argument( '--dry-run', action='store_true', help='do not write to disk nor commit any changes' ) parser.set_defaults(dry_run=False) logger = logging.getLogger('migrate') logger.setLevel(logging.INFO) args = parser.parse_args() # Don't byte-compile migrations. # They're not our code, and infrequently run with dont_write_bytecode(): migrations = map(importlib.import_module, args.migrations) main( locale=args.locale, reference_dir=args.reference_dir, localization_dir=args.localization_dir, migrations=migrations, dry_run=args.dry_run ) if __name__ == '__main__': cli()