Skip to content

Commit dfd1870

Browse files
committed
Use MF2 syntax for xcode & xliff strings
1 parent 737a932 commit dfd1870

File tree

22 files changed

+291
-274
lines changed

22 files changed

+291
-274
lines changed

pontoon/base/migrations/0100_android_as_mf2.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,11 @@
33

44
from moz.l10n.formats.android import android_parse_message, android_serialize_message
55
from moz.l10n.formats.mf2 import mf2_parse_message, mf2_serialize_message
6+
from moz.l10n.model import PatternMessage
67

78
from django.db import migrations
89

9-
from pontoon.base.simple_preview import android_simple_preview
10+
from pontoon.base.simple_preview import preview_placeholder
1011

1112

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

3435

36+
def android_simple_preview(msg: PatternMessage) -> str:
37+
return "".join(preview_placeholder(part) for part in msg.pattern)
38+
39+
3540
def mf2_tm_changed(tm):
3641
changed = False
3742
src_prev = tm.source
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
from html import escape, unescape
2+
3+
from moz.l10n.formats.mf2 import mf2_parse_message, mf2_serialize_message
4+
from moz.l10n.formats.xliff import xliff_parse_message, xliff_serialize_message
5+
from moz.l10n.message import message_from_json
6+
7+
from django.db import migrations
8+
9+
10+
def mf2_entity_changed(entity):
11+
db_source = entity.string
12+
msg = message_from_json(entity.value)
13+
mf2_source = mf2_serialize_message(msg)
14+
if mf2_source == db_source:
15+
return False
16+
entity.string = mf2_source
17+
return True
18+
19+
20+
def mf2_translation_changed(translation, is_xcode: bool):
21+
db_source = translation.string
22+
msg = xliff_parse_message(escape(db_source), is_xcode=is_xcode)
23+
mf2_source = mf2_serialize_message(msg)
24+
if mf2_source == db_source:
25+
return False
26+
translation.string = mf2_source
27+
return True
28+
29+
30+
def xliff_as_mf2(apps, schema_editor):
31+
Entity = apps.get_model("base", "Entity")
32+
Translation = apps.get_model("base", "Translation")
33+
34+
entities = Entity.objects.filter(resource__format__in=["xliff", "xcode"])
35+
ent_fixed = [e for e in entities if mf2_entity_changed(e)]
36+
n = Entity.objects.bulk_update(ent_fixed, ["string"], batch_size=10_000)
37+
print(f" ({n} entities)", end="", flush=True)
38+
39+
translations = Translation.objects.filter(
40+
entity__resource__format="xcode"
41+
).select_related("entity")
42+
trans_fixed = [t for t in translations if mf2_translation_changed(t, True)]
43+
n = Translation.objects.bulk_update(trans_fixed, ["string"], batch_size=10_000)
44+
print(f" ({n} xcode translations)", end="", flush=True)
45+
46+
translations = Translation.objects.filter(
47+
entity__resource__format="xliff"
48+
).select_related("entity")
49+
trans_fixed = [t for t in translations if mf2_translation_changed(t, False)]
50+
n = Translation.objects.bulk_update(trans_fixed, ["string"], batch_size=10_000)
51+
print(f" ({n} xliff translations)", end="", flush=True)
52+
53+
54+
def xliff_string_changed(obj):
55+
mf2_source = obj.string
56+
msg = mf2_parse_message(mf2_source)
57+
string = unescape(xliff_serialize_message(msg))
58+
if string == mf2_source:
59+
return False
60+
obj.string = string
61+
return True
62+
63+
64+
def mf2_as_xliff(apps, schema_editor):
65+
Entity = apps.get_model("base", "Entity")
66+
entities = Entity.objects.filter(resource__format__in=["xliff", "xcode"])
67+
ent_fixed = [e for e in entities if xliff_string_changed(e)]
68+
n = Entity.objects.bulk_update(ent_fixed, ["meta", "string"], batch_size=10_000)
69+
print(f" ({n} entities)", end="", flush=True)
70+
71+
Translation = apps.get_model("base", "Translation")
72+
translations = Translation.objects.filter(
73+
entity__resource__format__in=["xliff", "xcode"]
74+
)
75+
trans_fixed = [t for t in translations if xliff_string_changed(t)]
76+
n = Translation.objects.bulk_update(trans_fixed, ["string"], batch_size=10_000)
77+
print(f" ({n} translations)", end="", flush=True)
78+
79+
80+
class Migration(migrations.Migration):
81+
dependencies = [("base", "0101_webext_as_mf2")]
82+
operations = [migrations.RunPython(xliff_as_mf2, reverse_code=mf2_as_xliff)]

pontoon/base/simple_preview.py

Lines changed: 45 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,21 @@
11
from json import dumps
22

3-
from moz.l10n.formats import Format
4-
from moz.l10n.formats.fluent import fluent_parse_entry
3+
from moz.l10n.formats.fluent import fluent_parse_entry, fluent_serialize_message
54
from moz.l10n.formats.mf2 import mf2_parse_message, mf2_serialize_message
6-
from moz.l10n.message import serialize_message
75
from moz.l10n.model import (
86
CatchallKey,
97
Expression,
108
Markup,
119
Message,
1210
Pattern,
1311
PatternMessage,
14-
SelectMessage,
1512
VariableRef,
1613
)
1714

1815
from pontoon.base.models import Resource
1916

2017

21-
def get_simple_preview(format: str, string: str):
18+
def get_simple_preview(format: str, msg: str | Message | Pattern) -> str:
2219
"""
2320
Flatten a message entry as a simple string.
2421
@@ -27,58 +24,58 @@ def get_simple_preview(format: str, string: str):
2724
For Fluent, selects the value if it's not empty,
2825
or the first non-empty attribute.
2926
"""
30-
try:
31-
match format:
32-
case Resource.Format.FLUENT:
33-
entry = fluent_parse_entry(string, with_linepos=False)
34-
if not entry.value.is_empty():
35-
msg = entry.value
36-
else:
37-
msg = next(
27+
if format == Resource.Format.FLUENT:
28+
if isinstance(msg, str):
29+
try:
30+
entry = fluent_parse_entry(msg, with_linepos=False)
31+
msg = (
32+
entry.value
33+
if not entry.value.is_empty()
34+
else next(
3835
prop
3936
for prop in entry.properties.values()
4037
if not prop.is_empty()
4138
)
42-
msg = as_pattern_message(msg)
43-
return serialize_message(Format.fluent, msg)
39+
)
40+
except Exception:
41+
return msg
42+
pattern = as_simple_pattern(msg)
43+
return fluent_serialize_message(PatternMessage(pattern))
4444

45-
case Resource.Format.ANDROID:
46-
msg = mf2_parse_message(string)
47-
return android_simple_preview(msg)
48-
49-
case Resource.Format.GETTEXT | Resource.Format.WEBEXT:
50-
msg = mf2_parse_message(string)
51-
msg = as_pattern_message(msg)
52-
return serialize_message(None, msg)
53-
except Exception:
54-
pass
55-
return string
56-
57-
58-
def as_pattern_message(msg: Message) -> PatternMessage:
59-
if isinstance(msg, SelectMessage):
60-
default_pattern = next(
61-
pattern
62-
for keys, pattern in msg.variants.items()
63-
if all(isinstance(key, CatchallKey) for key in keys)
64-
)
65-
return PatternMessage(default_pattern)
66-
else:
45+
if format in (
46+
Resource.Format.ANDROID,
47+
Resource.Format.GETTEXT,
48+
Resource.Format.WEBEXT,
49+
Resource.Format.XCODE,
50+
Resource.Format.XLIFF,
51+
):
52+
if isinstance(msg, str):
53+
try:
54+
msg = mf2_parse_message(msg)
55+
except Exception:
56+
return msg
57+
elif isinstance(msg, str):
6758
return msg
6859

69-
70-
def android_simple_preview(msg: Message | Pattern) -> str:
71-
"""
72-
Matches the JS androidEditPattern() from translate/src/utils/message/android.ts
73-
"""
7460
preview = ""
75-
pattern = msg if isinstance(msg, list) else as_pattern_message(msg).pattern
76-
for part in pattern:
77-
preview += android_placeholder_preview(part)
61+
for part in as_simple_pattern(msg):
62+
preview += preview_placeholder(part)
7863
return preview
7964

8065

81-
def android_placeholder_preview(part: str | Expression | Markup) -> str:
66+
def as_simple_pattern(msg: Message | Pattern) -> Pattern:
67+
if isinstance(msg, list):
68+
return msg
69+
if isinstance(msg, PatternMessage):
70+
return msg.pattern
71+
return next(
72+
pattern
73+
for keys, pattern in msg.variants.items()
74+
if all(isinstance(key, CatchallKey) for key in keys)
75+
)
76+
77+
78+
def preview_placeholder(part: str | Expression | Markup) -> str:
8279
if isinstance(part, str):
8380
return part
8481
if isinstance(ps := part.attributes.get("source", None), str):
@@ -88,12 +85,12 @@ def android_placeholder_preview(part: str | Expression | Markup) -> str:
8885
return part.arg
8986
elif part.function == "entity" and isinstance(part.arg, VariableRef):
9087
return part.arg.name
91-
elif part.kind == "open":
88+
elif part.kind in ("open", "standalone"):
9289
res = "<" + part.name
9390
for name, val in part.options.items():
9491
valstr = dumps(val) if isinstance(val, str) else "$" + val.name
9592
res += f" {name}={valstr}"
96-
res += ">"
93+
res += ">" if part.kind == "open" else " />"
9794
return res
9895
elif part.kind == "close" and not part.options:
9996
return f"</{part.name}>"

pontoon/checks/libraries/__init__.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from moz.l10n.model import CatchallKey, Pattern, PatternMessage, SelectMessage
44

55
from pontoon.base.models import Entity, Resource
6-
from pontoon.base.simple_preview import android_simple_preview
6+
from pontoon.base.simple_preview import get_simple_preview
77

88
from . import compare_locales, translate_toolkit
99
from .custom import run_custom_checks
@@ -75,19 +75,21 @@ def run_checks(
7575
case Resource.Format.ANDROID:
7676
src_msg = mf2_parse_message(entity.string)
7777
tgt_msg = mf2_parse_message(string)
78-
src0 = android_simple_preview(src_msg)
78+
src0 = get_simple_preview(res_format, src_msg)
7979
if isinstance(src_msg, SelectMessage) and isinstance(
8080
tgt_msg, SelectMessage
8181
):
8282
for keys, pattern in tgt_msg.variants.items():
8383
src = (
84-
android_simple_preview(src_msg.variants[keys])
84+
get_simple_preview(res_format, src_msg.variants[keys])
8585
if keys == ("one",) and keys in src_msg.variants
8686
else src0
8787
)
88-
tt_patterns.append((src, android_simple_preview(pattern)))
88+
tt_patterns.append(
89+
(src, get_simple_preview(res_format, pattern))
90+
)
8991
else:
90-
tt_patterns.append((src0, android_simple_preview(tgt_msg)))
92+
tt_patterns.append((src0, get_simple_preview(res_format, tgt_msg)))
9193

9294
case Resource.Format.GETTEXT:
9395
src_msg = mf2_parse_message(entity.string)

pontoon/checks/libraries/custom.py

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,7 @@
1717
)
1818

1919
from pontoon.base.models import Entity, Resource
20-
from pontoon.base.simple_preview import (
21-
android_placeholder_preview,
22-
android_simple_preview,
23-
)
20+
from pontoon.base.simple_preview import get_simple_preview, preview_placeholder
2421

2522

2623
parser = FluentParser()
@@ -81,7 +78,7 @@ def run_custom_checks(entity: Entity, string: str) -> dict[str, list[str]]:
8178
for el in pattern
8279
if not isinstance(el, str)
8380
)
84-
orig_ps = {android_placeholder_preview(ph) for ph in orig_ph_iter}
81+
orig_ps = {preview_placeholder(ph) for ph in orig_ph_iter}
8582
except ValueError:
8683
orig_msg = None
8784
orig_ps = set()
@@ -99,11 +96,11 @@ def run_custom_checks(entity: Entity, string: str) -> dict[str, list[str]]:
9996
try:
10097
for pattern in patterns:
10198
android_msg = android_parse_message(
102-
escape(android_simple_preview(pattern))
99+
escape(get_simple_preview(Resource.Format.ANDROID, pattern))
103100
)
104101
for el in android_msg.pattern:
105102
if not isinstance(el, str):
106-
ps = android_placeholder_preview(el)
103+
ps = preview_placeholder(el)
107104
if ps in orig_ps:
108105
found_ps.add(ps)
109106
else:

pontoon/pretranslation/pretranslate.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,8 @@ def get_pretranslation(
6262
Resource.Format.ANDROID,
6363
Resource.Format.GETTEXT,
6464
Resource.Format.WEBEXT,
65+
Resource.Format.XCODE,
66+
Resource.Format.XLIFF,
6567
}:
6668
format = Format.mf2
6769
msg = parse_message(format, entity.string)
@@ -90,6 +92,8 @@ def __init__(self, entity: Entity, locale: Locale, preserve_placeables: bool):
9092
Resource.Format.ANDROID
9193
| Resource.Format.GETTEXT
9294
| Resource.Format.WEBEXT
95+
| Resource.Format.XCODE
96+
| Resource.Format.XLIFF
9397
):
9498
self.format = Format.mf2
9599
case _:

pontoon/sync/core/translations_to_repo.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -347,7 +347,7 @@ def set_translation(
347347
return False
348348

349349
match format:
350-
case Format.android | Format.gettext | Format.webext:
350+
case Format.android | Format.gettext | Format.webext | Format.xliff:
351351
msg = parse_message(Format.mf2, tx.string)
352352
if isinstance(entry.value, SelectMessage):
353353
entry.value.variants = (

0 commit comments

Comments
 (0)