From 05c44ae2a609470d4f11c256fc664996bc73eb85 Mon Sep 17 00:00:00 2001 From: Francesco Lodolo Date: Thu, 30 Oct 2025 18:55:08 +0100 Subject: [PATCH 1/3] Serialize Android plurals has a flat string, following Fluent syntax --- tests/test_string_projectconfig.py | 10 +- .../app/src/main/res/values/strings.xml | 96 ++++++++++--------- tmx_products/functions.py | 53 +++++++++- 3 files changed, 111 insertions(+), 48 deletions(-) diff --git a/tests/test_string_projectconfig.py b/tests/test_string_projectconfig.py index 29bb407..4ad7c82 100644 --- a/tests/test_string_projectconfig.py +++ b/tests/test_string_projectconfig.py @@ -21,9 +21,17 @@ def testGetAndroidStrings(self): strings_locale = extraction.translations self.assertEqual(len(strings_locale), 11) self.assertEqual(len(strings_locale["it"]), 6) - self.assertEqual(len(strings_locale["en-US"]), 17) + self.assertEqual(len(strings_locale["en-US"]), 18) self.assertEqual(len(strings_locale["es-ES"]), 5) + # Check plurals + self.assertEqual( + strings_locale["en-US"][ + "test/MozillaReality/FirefoxReality/app/src/main/res/values/strings.xml:close_tabs_plural" + ], + "[one] Close %d tab\n*[other] Close %d tabs", + ) + # Check escapes self.assertEqual( strings_locale["it"][ diff --git a/tests/testfiles/android/MozillaReality/FirefoxReality/app/src/main/res/values/strings.xml b/tests/testfiles/android/MozillaReality/FirefoxReality/app/src/main/res/values/strings.xml index 0e6f193..3e6e66f 100644 --- a/tests/testfiles/android/MozillaReality/FirefoxReality/app/src/main/res/values/strings.xml +++ b/tests/testfiles/android/MozillaReality/FirefoxReality/app/src/main/res/values/strings.xml @@ -1,55 +1,61 @@ - - - - ENTER - - - GO - - - SEARCH - - - SEND - - - NEXT - - + + + ENTER + + + GO + + + SEARCH + + + SEND + + + NEXT + + - DONE + --> + DONE - - space + + space - - Allow + + Allow - - Don’t Allow + + Don’t Allow - - Enable + + Enable - - Enabled + + Enabled + + + + Close %d tab + Close %d tabs + diff --git a/tmx_products/functions.py b/tmx_products/functions.py index 1416a6b..dc17428 100644 --- a/tmx_products/functions.py +++ b/tmx_products/functions.py @@ -2,10 +2,19 @@ import os from configparser import ConfigParser +from typing import Union from moz.l10n.formats import Format from moz.l10n.message import serialize_message -from moz.l10n.model import Entry, Message, Resource +from moz.l10n.model import ( + CatchallKey, + Entry, + Expression, + Message, + Pattern, + Resource, + SelectMessage, +) def get_config() -> str: @@ -112,6 +121,37 @@ def get_entry_value(value: Message) -> str: return entry_value + def pattern_to_string(pattern: Pattern) -> str: + parts = [] + for node in pattern: + if isinstance(node, str): + parts.append(node) + elif isinstance(node, Expression): + attrs = node.attributes + src = (attrs or {}).get("source") + if src: + parts.append(src) + else: + if node.function == "integer": + parts.append("%d") + elif node.function == "number": + parts.append("%s") + else: + parts.append("{expr}") + else: + parts.append(str(node)) + return "".join(parts) + + def serialize_select_variants(entry: Entry) -> str: + msg: SelectMessage = entry.value + lines: list[str] = [] + for key_tuple, pattern in msg.variants.items(): + key: Union[str, CatchallKey] = key_tuple[0] if key_tuple else "other" + default = "*" if isinstance(key, CatchallKey) else "" + label: str | None = key.value if isinstance(key, CatchallKey) else str(key) + lines.append(f"{default}[{label}] {pattern_to_string(pattern)}") + return "\n".join(lines) + try: for section in resource.sections: for entry in section.entries: @@ -130,7 +170,16 @@ def get_entry_value(value: Message) -> str: attr_id = f"{string_id}.{attribute}" storage[attr_id] = get_entry_value(attr_value) else: - storage[string_id] = get_entry_value(entry.value) + if resource.format == Format.android: + # If it's a plural string in Android, each variant + # is stored within the message, following a format + # similar to Fluent. + if hasattr(entry.value, "variants"): + storage[string_id] = serialize_select_variants(entry) + else: + storage[string_id] = get_entry_value(entry.value) + else: + storage[string_id] = get_entry_value(entry.value) except Exception as e: print(f"Error parsing file: {filename}") print(e) From d68441b3dbf706977927ee7e4b1d78044459c1ae Mon Sep 17 00:00:00 2001 From: Francesco Lodolo Date: Fri, 31 Oct 2025 06:45:25 +0100 Subject: [PATCH 2/3] Simplify code --- tmx_products/functions.py | 28 ++++------------------------ 1 file changed, 4 insertions(+), 24 deletions(-) diff --git a/tmx_products/functions.py b/tmx_products/functions.py index dc17428..a3bf625 100644 --- a/tmx_products/functions.py +++ b/tmx_products/functions.py @@ -9,9 +9,8 @@ from moz.l10n.model import ( CatchallKey, Entry, - Expression, Message, - Pattern, + PatternMessage, Resource, SelectMessage, ) @@ -121,27 +120,6 @@ def get_entry_value(value: Message) -> str: return entry_value - def pattern_to_string(pattern: Pattern) -> str: - parts = [] - for node in pattern: - if isinstance(node, str): - parts.append(node) - elif isinstance(node, Expression): - attrs = node.attributes - src = (attrs or {}).get("source") - if src: - parts.append(src) - else: - if node.function == "integer": - parts.append("%d") - elif node.function == "number": - parts.append("%s") - else: - parts.append("{expr}") - else: - parts.append(str(node)) - return "".join(parts) - def serialize_select_variants(entry: Entry) -> str: msg: SelectMessage = entry.value lines: list[str] = [] @@ -149,7 +127,9 @@ def serialize_select_variants(entry: Entry) -> str: key: Union[str, CatchallKey] = key_tuple[0] if key_tuple else "other" default = "*" if isinstance(key, CatchallKey) else "" label: str | None = key.value if isinstance(key, CatchallKey) else str(key) - lines.append(f"{default}[{label}] {pattern_to_string(pattern)}") + lines.append( + f"{default}[{label}] {serialize_message(resource.format, PatternMessage(pattern))}" + ) return "\n".join(lines) try: From ab7aa0aa87f09924da87c330aff0a66c752678f6 Mon Sep 17 00:00:00 2001 From: Francesco Lodolo Date: Fri, 31 Oct 2025 06:47:24 +0100 Subject: [PATCH 3/3] Add 3.13 and 3.14 to testing --- .github/workflows/tests.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 5b69020..dfc7c54 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -40,7 +40,8 @@ jobs: python-versions: - '3.11' - '3.12' - - '3.12' + - '3.13' + - '3.14' steps: - name: Check out repository uses: actions/checkout@v5