Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion pontoon/base/migrations/0100_android_as_mf2.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@

from moz.l10n.formats.android import android_parse_message, android_serialize_message
from moz.l10n.formats.mf2 import mf2_parse_message, mf2_serialize_message
from moz.l10n.model import PatternMessage

from django.db import migrations

from pontoon.base.simple_preview import android_simple_preview
from pontoon.base.simple_preview import preview_placeholder


android_nl = compile(r"\s*\n\s*")
Expand All @@ -32,6 +33,10 @@ def mf2_string_changed(obj):
return True


def android_simple_preview(msg: PatternMessage) -> str:
return "".join(preview_placeholder(part) for part in msg.pattern)


def mf2_tm_changed(tm):
changed = False
src_prev = tm.source
Expand Down
82 changes: 82 additions & 0 deletions pontoon/base/migrations/0103_xliff_as_mf2.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
from html import escape, unescape

from moz.l10n.formats.mf2 import mf2_parse_message, mf2_serialize_message
from moz.l10n.formats.xliff import xliff_parse_message, xliff_serialize_message
from moz.l10n.message import message_from_json

from django.db import migrations


def mf2_entity_changed(entity):
db_source = entity.string
msg = message_from_json(entity.value)
mf2_source = mf2_serialize_message(msg)
if mf2_source == db_source:
return False
entity.string = mf2_source
return True


def mf2_translation_changed(translation, is_xcode: bool):
db_source = translation.string
msg = xliff_parse_message(escape(db_source), is_xcode=is_xcode)
mf2_source = mf2_serialize_message(msg)
if mf2_source == db_source:
return False
translation.string = mf2_source
return True


def xliff_as_mf2(apps, schema_editor):
Entity = apps.get_model("base", "Entity")
Translation = apps.get_model("base", "Translation")

entities = Entity.objects.filter(resource__format__in=["xliff", "xcode"])
ent_fixed = [e for e in entities if mf2_entity_changed(e)]
n = Entity.objects.bulk_update(ent_fixed, ["string"], batch_size=10_000)
print(f" ({n} entities)", end="", flush=True)

translations = Translation.objects.filter(
entity__resource__format="xcode"
).select_related("entity")
trans_fixed = [t for t in translations if mf2_translation_changed(t, True)]
n = Translation.objects.bulk_update(trans_fixed, ["string"], batch_size=10_000)
print(f" ({n} xcode translations)", end="", flush=True)

translations = Translation.objects.filter(
entity__resource__format="xliff"
).select_related("entity")
trans_fixed = [t for t in translations if mf2_translation_changed(t, False)]
n = Translation.objects.bulk_update(trans_fixed, ["string"], batch_size=10_000)
print(f" ({n} xliff translations)", end="", flush=True)


def xliff_string_changed(obj):
mf2_source = obj.string
msg = mf2_parse_message(mf2_source)
string = unescape(xliff_serialize_message(msg))
if string == mf2_source:
return False
obj.string = string
return True


def mf2_as_xliff(apps, schema_editor):
Entity = apps.get_model("base", "Entity")
entities = Entity.objects.filter(resource__format__in=["xliff", "xcode"])
ent_fixed = [e for e in entities if xliff_string_changed(e)]
n = Entity.objects.bulk_update(ent_fixed, ["meta", "string"], batch_size=10_000)
print(f" ({n} entities)", end="", flush=True)

Translation = apps.get_model("base", "Translation")
translations = Translation.objects.filter(
entity__resource__format__in=["xliff", "xcode"]
)
trans_fixed = [t for t in translations if xliff_string_changed(t)]
n = Translation.objects.bulk_update(trans_fixed, ["string"], batch_size=10_000)
print(f" ({n} translations)", end="", flush=True)


class Migration(migrations.Migration):
dependencies = [("base", "0102_add_pretranslators_group")]
operations = [migrations.RunPython(xliff_as_mf2, reverse_code=mf2_as_xliff)]
93 changes: 45 additions & 48 deletions pontoon/base/simple_preview.py
Original file line number Diff line number Diff line change
@@ -1,24 +1,21 @@
from json import dumps

from moz.l10n.formats import Format
from moz.l10n.formats.fluent import fluent_parse_entry
from moz.l10n.formats.fluent import fluent_parse_entry, fluent_serialize_message
from moz.l10n.formats.mf2 import mf2_parse_message, mf2_serialize_message
from moz.l10n.message import serialize_message
from moz.l10n.model import (
CatchallKey,
Expression,
Markup,
Message,
Pattern,
PatternMessage,
SelectMessage,
VariableRef,
)

from pontoon.base.models import Resource


def get_simple_preview(format: str, string: str):
def get_simple_preview(format: str, msg: str | Message | Pattern) -> str:
"""
Flatten a message entry as a simple string.

Expand All @@ -27,58 +24,58 @@ def get_simple_preview(format: str, string: str):
For Fluent, selects the value if it's not empty,
or the first non-empty attribute.
"""
try:
match format:
case Resource.Format.FLUENT:
entry = fluent_parse_entry(string, with_linepos=False)
if not entry.value.is_empty():
msg = entry.value
else:
msg = next(
if format == Resource.Format.FLUENT:
if isinstance(msg, str):
try:
entry = fluent_parse_entry(msg, with_linepos=False)
msg = (
entry.value
if not entry.value.is_empty()
else next(
prop
for prop in entry.properties.values()
if not prop.is_empty()
)
msg = as_pattern_message(msg)
return serialize_message(Format.fluent, msg)
)
except Exception:
return msg
pattern = as_simple_pattern(msg)
return fluent_serialize_message(PatternMessage(pattern))

case Resource.Format.ANDROID:
msg = mf2_parse_message(string)
return android_simple_preview(msg)

case Resource.Format.GETTEXT | Resource.Format.WEBEXT:
msg = mf2_parse_message(string)
msg = as_pattern_message(msg)
return serialize_message(None, msg)
except Exception:
pass
return string


def as_pattern_message(msg: Message) -> PatternMessage:
if isinstance(msg, SelectMessage):
default_pattern = next(
pattern
for keys, pattern in msg.variants.items()
if all(isinstance(key, CatchallKey) for key in keys)
)
return PatternMessage(default_pattern)
else:
if format in (
Resource.Format.ANDROID,
Resource.Format.GETTEXT,
Resource.Format.WEBEXT,
Resource.Format.XCODE,
Resource.Format.XLIFF,
):
if isinstance(msg, str):
try:
msg = mf2_parse_message(msg)
except Exception:
return msg
elif isinstance(msg, str):
return msg


def android_simple_preview(msg: Message | Pattern) -> str:
"""
Matches the JS androidEditPattern() from translate/src/utils/message/android.ts
"""
preview = ""
pattern = msg if isinstance(msg, list) else as_pattern_message(msg).pattern
for part in pattern:
preview += android_placeholder_preview(part)
for part in as_simple_pattern(msg):
preview += preview_placeholder(part)
return preview


def android_placeholder_preview(part: str | Expression | Markup) -> str:
def as_simple_pattern(msg: Message | Pattern) -> Pattern:
if isinstance(msg, list):
return msg
if isinstance(msg, PatternMessage):
return msg.pattern
return next(
pattern
for keys, pattern in msg.variants.items()
if all(isinstance(key, CatchallKey) for key in keys)
)


def preview_placeholder(part: str | Expression | Markup) -> str:
if isinstance(part, str):
return part
if isinstance(ps := part.attributes.get("source", None), str):
Expand All @@ -88,12 +85,12 @@ def android_placeholder_preview(part: str | Expression | Markup) -> str:
return part.arg
elif part.function == "entity" and isinstance(part.arg, VariableRef):
return part.arg.name
elif part.kind == "open":
elif part.kind in ("open", "standalone"):
res = "<" + part.name
for name, val in part.options.items():
valstr = dumps(val) if isinstance(val, str) else "$" + val.name
res += f" {name}={valstr}"
res += ">"
res += ">" if part.kind == "open" else " />"
return res
elif part.kind == "close" and not part.options:
return f"</{part.name}>"
Expand Down
12 changes: 7 additions & 5 deletions pontoon/checks/libraries/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from moz.l10n.model import CatchallKey, Pattern, PatternMessage, SelectMessage

from pontoon.base.models import Entity, Resource
from pontoon.base.simple_preview import android_simple_preview
from pontoon.base.simple_preview import get_simple_preview

from . import compare_locales, translate_toolkit
from .custom import run_custom_checks
Expand Down Expand Up @@ -75,19 +75,21 @@ def run_checks(
case Resource.Format.ANDROID:
src_msg = mf2_parse_message(entity.string)
tgt_msg = mf2_parse_message(string)
src0 = android_simple_preview(src_msg)
src0 = get_simple_preview(res_format, src_msg)
if isinstance(src_msg, SelectMessage) and isinstance(
tgt_msg, SelectMessage
):
for keys, pattern in tgt_msg.variants.items():
src = (
android_simple_preview(src_msg.variants[keys])
get_simple_preview(res_format, src_msg.variants[keys])
if keys == ("one",) and keys in src_msg.variants
else src0
)
tt_patterns.append((src, android_simple_preview(pattern)))
tt_patterns.append(
(src, get_simple_preview(res_format, pattern))
)
else:
tt_patterns.append((src0, android_simple_preview(tgt_msg)))
tt_patterns.append((src0, get_simple_preview(res_format, tgt_msg)))

case Resource.Format.GETTEXT:
src_msg = mf2_parse_message(entity.string)
Expand Down
11 changes: 4 additions & 7 deletions pontoon/checks/libraries/custom.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,7 @@
)

from pontoon.base.models import Entity, Resource
from pontoon.base.simple_preview import (
android_placeholder_preview,
android_simple_preview,
)
from pontoon.base.simple_preview import get_simple_preview, preview_placeholder


parser = FluentParser()
Expand Down Expand Up @@ -81,7 +78,7 @@ def run_custom_checks(entity: Entity, string: str) -> dict[str, list[str]]:
for el in pattern
if not isinstance(el, str)
)
orig_ps = {android_placeholder_preview(ph) for ph in orig_ph_iter}
orig_ps = {preview_placeholder(ph) for ph in orig_ph_iter}
except ValueError:
orig_msg = None
orig_ps = set()
Expand All @@ -99,11 +96,11 @@ def run_custom_checks(entity: Entity, string: str) -> dict[str, list[str]]:
try:
for pattern in patterns:
android_msg = android_parse_message(
escape(android_simple_preview(pattern))
escape(get_simple_preview(Resource.Format.ANDROID, pattern))
)
for el in android_msg.pattern:
if not isinstance(el, str):
ps = android_placeholder_preview(el)
ps = preview_placeholder(el)
if ps in orig_ps:
found_ps.add(ps)
else:
Expand Down
4 changes: 4 additions & 0 deletions pontoon/pretranslation/pretranslate.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@ def get_pretranslation(
Resource.Format.ANDROID,
Resource.Format.GETTEXT,
Resource.Format.WEBEXT,
Resource.Format.XCODE,
Resource.Format.XLIFF,
}:
format = Format.mf2
msg = parse_message(format, entity.string)
Expand Down Expand Up @@ -90,6 +92,8 @@ def __init__(self, entity: Entity, locale: Locale, preserve_placeables: bool):
Resource.Format.ANDROID
| Resource.Format.GETTEXT
| Resource.Format.WEBEXT
| Resource.Format.XCODE
| Resource.Format.XLIFF
):
self.format = Format.mf2
case _:
Expand Down
4 changes: 2 additions & 2 deletions pontoon/sync/core/translations_from_repo.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
from pontoon.checks.utils import bulk_run_checks
from pontoon.sync.core.checkout import Checkout, Checkouts
from pontoon.sync.core.paths import UploadPaths
from pontoon.sync.formats import as_vcs_translations
from pontoon.sync.formats import as_repo_translations


log = logging.getLogger(__name__)
Expand Down Expand Up @@ -162,7 +162,7 @@ def find_db_updates(
translated_resources[db_path].add(locale.pk)
translations.update(
((db_path, tx.key, locale.pk), (tx.string, tx.fuzzy))
for tx in as_vcs_translations(l10n_res)
for tx in as_repo_translations(l10n_res)
)
except Exception as error:
scope = f"[{project.slug}:{db_path}, {locale.code}]"
Expand Down
2 changes: 1 addition & 1 deletion pontoon/sync/core/translations_to_repo.py
Original file line number Diff line number Diff line change
Expand Up @@ -347,7 +347,7 @@ def set_translation(
return False

match format:
case Format.android | Format.gettext | Format.webext:
case Format.android | Format.gettext | Format.webext | Format.xliff:
msg = parse_message(Format.mf2, tx.string)
if isinstance(entry.value, SelectMessage):
entry.value.variants = (
Expand Down
Loading
Loading