from __future__ import annotations from typing import List, Set, Tuple, cast import logging import fluent.syntax.ast as FTL from fluent.migrate.util import fold from .transforms import Source from .util import get_message, skeleton from .errors import ( EmptyLocalizationError, UnreadableReferenceError, ) from ._context import InternalContext __all__ = [ "EmptyLocalizationError", "UnreadableReferenceError", "MigrationContext", ] class MigrationContext(InternalContext): """Stateful context for merging translation resources. `MigrationContext` must be configured with the target locale and the directory locations of the input data. The transformation takes four types of input data: - The en-US FTL reference files which will be used as templates for message order, comments and sections. If the reference_dir is None, the migration will create Messages and Terms in the order given by the transforms. - The current FTL files for the given locale. - A list of `FTL.Message` or `FTL.Term` objects some of whose nodes are special helper or transform nodes: helpers: VARIABLE_REFERENCE, MESSAGE_REFERENCE, TERM_REFERENCE transforms: COPY, REPLACE_IN_TEXT, REPLACE, PLURALS, CONCAT fluent value helper: COPY_PATTERN The legacy (DTD, properties) translation files are deduced by the dependencies in the transforms. The translations from these files will be read from the localization_dir and transformed into FTL and merged into the existing FTL files for the given language. """ def __init__( self, locale: str, reference_dir: str, localization_dir: str, enforce_translated=False, ): super().__init__( locale, enforce_translated=enforce_translated, ) self.locale = locale # Paths to directories with input data, relative to CWD. self.reference_dir = reference_dir self.localization_dir = localization_dir self.dependencies = {} """ A dict whose keys are `(path, key)` tuples corresponding to target FTL translations, and values are sets of `(path, key)` tuples corresponding to localized entities which will be migrated. """ def add_transforms( self, target: str, reference: str, transforms: List[FTL.Message | FTL.Term] ): """Define transforms for target using reference as template. `target` is a path of the destination FTL file relative to the localization directory. `reference` is a path to the template FTL file relative to the reference directory. Each transform is an extended FTL node with `Transform` nodes as some values. Transforms are stored in their lazy AST form until `merge_changeset` is called, at which point they are evaluated to real FTL nodes with migrated translations. Each transform is scanned for `Source` nodes which will be used to build the list of dependencies for the transformed message. For transforms that merely copy legacy messages or Fluent patterns, using `fluent.migrate.helpers.transforms_from` is recommended. """ def get_sources(acc, cur): if isinstance(cur, Source): acc.add((cur.path, cur.key)) return acc if self.reference_dir is None: # Add skeletons to resource body for each transform # if there's no reference. reference_ast = self.reference_resources.get(target) if reference_ast is None: reference_ast = FTL.Resource() reference_ast.body.extend(skeleton(transform) for transform in transforms) else: reference_ast = self.read_reference_ftl(reference) self.reference_resources[target] = reference_ast for node in transforms: ident = cast(str, node.id.name) # Scan `node` for `Source` nodes and collect the information they # store into a set of dependencies. dependencies = cast(Set[Tuple[str, Source]], fold(get_sources, node, set())) # Set these sources as dependencies for the current transform. self.dependencies[(target, ident)] = dependencies # The target Fluent message should exist in the reference file. If # it doesn't, it's probably a typo. # Of course, only if we're having a reference. if self.reference_dir is None: continue if get_message(reference_ast.body, ident) is None: logger = logging.getLogger("migrate") logger.warning( '{} "{}" was not found in {}'.format( type(node).__name__, ident, reference ) ) # Keep track of localization resource paths which were defined as # sources in the transforms. expected_paths = set() # Read all legacy translation files defined in Source transforms. This # may fail but a single missing legacy resource doesn't mean that the # migration can't succeed. for dependencies in self.dependencies.values(): for path in {path for path, _ in dependencies}: expected_paths.add(path) self.maybe_add_localization(path) # However, if all legacy resources are missing, bail out early. There # are no translations to migrate. We'd also get errors in hg annotate. if len(expected_paths) > 0 and len(self.localization_resources) == 0: error_message = "No localization files were found" logging.getLogger("migrate").error(error_message) raise EmptyLocalizationError(error_message) # Add the current transforms to any other transforms added earlier for # this path. path_transforms = self.transforms.setdefault(target, []) path_transforms += transforms if target not in self.target_resources: target_ast = self.read_localization_ftl(target) self.target_resources[target] = target_ast