lib/l10n_utils/management/commands/process_ftl.py (127 lines of code) (raw):

# This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at https://mozilla.org/MPL/2.0/. import shutil from subprocess import CalledProcessError from django.conf import settings from django.core.management.base import CommandError from fluent.syntax.parser import FluentParser, ParseError from lib.l10n_utils.fluent import fluent_l10n, get_metadata, write_metadata from ._ftl_repo_base import FTLRepoCommand GIT_COMMIT_EMAIL = "meao-bots+mozmarrobot@mozilla.com" GIT_COMMIT_NAME = "MozMEAO Bot" class NoisyFluentParser(FluentParser): """A parser that will raise exceptions. The one from fluent.syntax doesn't raise exceptions, but will return instances of fluent.syntax.ast.Junk instead. """ def get_entry_or_junk(self, ps): """Allow the ParseError to bubble up""" entry = self.get_entry(ps) ps.expect_line_end() return entry class Command(FTLRepoCommand): help = "Processes .ftl files from l10n team for use in bedrock" parser = None def add_arguments(self, parser): super().add_arguments(parser) parser.add_argument("--push", action="store_true", dest="push", default=False, help="Push the changes to the MEAO Fluent files repo.") def handle(self, *args, **options): super().handle(*args, **options) self.parser = NoisyFluentParser() self.update_fluent_files() self.update_l10n_team_files() no_errors = self.copy_ftl_files() self.set_activation() self.copy_configs() if options["push"]: changes = self.commit_changes() if changes: self.push_changes() if not no_errors: raise CommandError("Some errors were discovered in some .ftl files and they were not updated. See above for details.") def config_fluent_repo(self): """Set user config so that committing will work""" self.meao_repo.git("config", "user.email", GIT_COMMIT_EMAIL) self.meao_repo.git("config", "user.name", GIT_COMMIT_NAME) def commit_changes(self): self.config_fluent_repo() self.meao_repo.git("add", ".") try: self.meao_repo.git("commit", "-m", "Update files from l10n repo") except CalledProcessError: self.stdout.write("No changes to commit") return False self.stdout.write("Committed changes to local repo") return True def push_changes(self): try: self.meao_repo.git("push", self.git_push_url, "HEAD:master") except CalledProcessError: raise CommandError(f"There was a problem pushing to {self.meao_repo.remote_url}") commit = self.meao_repo.git("rev-parse", "--short", "HEAD") self.stdout.write(f"Pushed {commit} to {self.meao_repo.remote_url}") @property def git_push_url(self): if not settings.FLUENT_REPO_AUTH: raise CommandError("Git push authentication not configured") return self.meao_repo.remote_url_auth(settings.FLUENT_REPO_AUTH) def _copy_file(self, filepath): relative_filepath = filepath.relative_to(self.l10n_repo.path) to_filepath = self.meao_repo.path.joinpath(relative_filepath) to_filepath.parent.mkdir(parents=True, exist_ok=True) shutil.copy2(str(filepath), str(to_filepath)) self.stdout.write(".", ending="") self.stdout.flush() def copy_configs(self): count = 0 for filepath in self.l10n_repo.path.rglob("*.toml"): self._copy_file(filepath) count += 1 self.stdout.write(f"\nCopied {count} .toml files") def copy_ftl_files(self): count = 0 errors = [] for filepath in self.l10n_repo.path.rglob("*.ftl"): if not self.lint_ftl_file(filepath): errors.append(filepath.relative_to(self.l10n_repo.path)) continue self._copy_file(filepath) count += 1 self.stdout.write(f"\nCopied {count} .ftl files") if errors: self.stdout.write("The following files had parse errors and were not copied:") for fpath in errors: self.stdout.write(f"- {fpath}") return False return True def lint_ftl_file(self, filepath): with filepath.open() as ftl: try: self.parser.parse(ftl.read()) except ParseError: return False return True def set_activation(self): updated_ftl = set() modified, _ = self.meao_repo.modified_files() for fname in modified: if not fname.endswith(".ftl"): continue locale, ftl_name = fname.split("/", 1) updated_ftl.add(ftl_name) for ftl_name in updated_ftl: self.calculate_activation(ftl_name) def calculate_activation(self, ftl_file): translations = self.meao_repo.path.glob(f"*/{ftl_file}") metadata = get_metadata(ftl_file) active_locales = metadata.get("active_locales", []) inactive_locales = metadata.get("inactive_locales", []) percent_required = metadata.get("percent_required", settings.FLUENT_DEFAULT_PERCENT_REQUIRED) all_locales = {str(x.relative_to(self.meao_repo.path)).split("/", 1)[0] for x in translations} locales_to_check = all_locales.difference(["en"], active_locales, inactive_locales) new_activations = [] for locale in locales_to_check: l10n = fluent_l10n([locale, "en"], [ftl_file]) if not l10n.has_required_messages: continue percent_trans = l10n.percent_translated if percent_trans < percent_required: continue new_activations.append(locale) if new_activations: active_locales.extend(new_activations) metadata["active_locales"] = sorted(active_locales) write_metadata(ftl_file, metadata) self.stdout.write(f"Activated {len(new_activations)} new locales for {ftl_file}")