from __future__ import annotations
from typing import Dict, Optional, Set, Tuple, cast
import os
import codecs
from functools import partial
import logging
from itertools import zip_longest
from compare_locales.parser import getParser
from compare_locales.plurals import get_plural
import fluent.syntax.ast as FTL
from fluent.syntax.parser import FluentParser
from fluent.syntax.serializer import FluentSerializer
from .changesets import Changes
from .errors import UnreadableReferenceError
from .evaluator import Evaluator
from .merge import merge_resource
from .transforms import Source
class InternalContext:
dependencies: Dict[Tuple[str, str], Set[Tuple[str, Source]]] = {}
localization_dir: str
reference_dir: str
def __init__(self, lang, enforce_translated=False):
self.fluent_parser = FluentParser(with_spans=False)
self.fluent_serializer = FluentSerializer()
self.plural_categories = get_plural(lang)
if self.plural_categories is None:
logger = logging.getLogger("migrate")
logger.warning(
f'Plural rule for "{lang}" is not defined in "compare-locales"'
)
self.plural_categories = ("one", "other")
self.enforce_translated = enforce_translated
self.reference_resources = {}
self.localization_resources = {}
self.target_resources = {}
self.transforms = {}
self.evaluator = Evaluator(self)
def read_ftl_resource(self, path: str):
f = codecs.open(path, "r", "utf8")
try:
contents = f.read()
except UnicodeDecodeError as err:
logger = logging.getLogger("migrate")
logger.warning(f"Unable to read file {path}: {err}")
raise err
finally:
f.close()
ast = self.fluent_parser.parse(contents)
annots = [
annot
for entry in ast.body
if isinstance(entry, FTL.Junk)
for annot in entry.annotations
]
if len(annots):
logger = logging.getLogger("migrate")
for annot in annots:
msg = annot.message
logger.warning(f"Syntax error in {path}: {msg}")
return ast
def read_legacy_resource(self, path: str):
parser = getParser(path)
parser.readFile(path)
return {
entity.key: entity.val
for entity in parser
if entity.localized or self.enforce_translated
}
def read_reference_ftl(self, path: str):
fullpath = os.path.join(self.reference_dir, path)
try:
return self.read_ftl_resource(fullpath)
except OSError:
error_message = f"Missing reference file: {fullpath}"
logging.getLogger("migrate").error(error_message)
raise UnreadableReferenceError(error_message)
except UnicodeDecodeError as err:
error_message = f"Error reading file {fullpath}: {err}"
logging.getLogger("migrate").error(error_message)
raise UnreadableReferenceError(error_message)
def read_localization_ftl(self, path: str):
fullpath = os.path.join(self.localization_dir, path)
try:
return self.read_ftl_resource(fullpath)
except OSError:
logger = logging.getLogger("migrate")
logger.info(
"Localization file {} does not exist and "
"it will be created".format(path)
)
return FTL.Resource()
except UnicodeDecodeError:
logger = logging.getLogger("migrate")
logger.warning(
"Localization file {} has broken encoding. "
"It will be re-created and some translations "
"may be lost".format(path)
)
return FTL.Resource()
def maybe_add_localization(self, path: str):
try:
fullpath = os.path.join(self.localization_dir, path)
if not fullpath.endswith(".ftl"):
collection = self.read_legacy_resource(fullpath)
else:
collection = self.read_ftl_resource(fullpath)
except OSError:
logger = logging.getLogger("migrate")
logger.warning(f"Missing localization file: {path}")
else:
self.localization_resources[path] = collection
def get_legacy_source(self, path: str, key: str):
resource = self.localization_resources[path]
return resource.get(key, None)
def get_fluent_source_pattern(self, path: str, key: str):
resource = self.localization_resources[path]
msg_key, _, attr_key = key.partition(".")
found = None
for entry in resource.body:
if isinstance(entry, (FTL.Message, FTL.Term)):
if entry.id.name == msg_key:
found = entry
break
if found is None:
return None
if not attr_key:
return found.value
for attribute in found.attributes:
if attribute.id.name == attr_key:
return attribute.value
return None
def messages_equal(self, res1, res2):
def message_id(message):
"Return the message's identifer name for sorting purposes."
return message.id.name
messages1 = sorted(
(
entry
for entry in res1.body
if isinstance(entry, FTL.Message) or isinstance(entry, FTL.Term)
),
key=message_id,
)
messages2 = sorted(
(
entry
for entry in res2.body
if isinstance(entry, FTL.Message) or isinstance(entry, FTL.Term)
),
key=message_id,
)
for msg1, msg2 in zip_longest(messages1, messages2):
if msg1 is None or msg2 is None:
return False
if not msg1.equals(msg2):
return False
return True
def merge_changeset(
self,
changeset: Optional[Changes] = None,
known_translations: Optional[Changes] = None,
):
if changeset is None:
changeset = {
(path, key)
for path, strings in self.localization_resources.items()
if not path.endswith(".ftl")
for key in strings.keys()
}
if known_translations is None:
known_translations = changeset
for path, reference in self.reference_resources.items():
current = self.target_resources[path]
transforms = self.transforms.get(path, [])
in_changeset = partial(
self.in_changeset, changeset, known_translations, path
)
snapshot = merge_resource(
self, reference, current, transforms, in_changeset
)
if self.messages_equal(current, snapshot):
continue
self.target_resources[path] = snapshot
yield path, snapshot
def in_changeset(
self, changeset: Changes, known_translations: Changes, path: str, ident
) -> bool:
message_deps = self.dependencies.get((path, ident), None)
if message_deps is None:
return False
if len(message_deps) == 0:
return True
active_deps = cast(bool, message_deps & changeset)
available_deps = message_deps & known_translations
return active_deps and message_deps == available_deps
def serialize_changeset(
self, changeset: Changes, known_translations: Optional[Changes] = None
):
return {
path: self.fluent_serializer.serialize(snapshot)
for path, snapshot in self.merge_changeset(changeset, known_translations)
}
def evaluate(self, node):
return self.evaluator.visit(node)
logging.basicConfig()