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
12 changes: 10 additions & 2 deletions Tools/i18n_audit.py
Original file line number Diff line number Diff line change
Expand Up @@ -1234,8 +1234,16 @@ def catalog_summary() -> None:
for v in strings.values():
if v.get("shouldTranslate") is False:
continue
state = (v.get("localizations", {}).get(lang) or {}).get("stringUnit", {}).get("state")
if state != "translated":
# Via `_is_translated`, NOT a bare `localizations[lang].stringUnit.state` read: a
# pluralised entry keeps its units under `variations.plural.<category>.stringUnit`, so the
# flat lookup returns None and scores a fully-translated plural as a gap. That is the exact
# trap `_string_units` was written for, and this summary was the one caller still falling
# into it — reporting de/es/fr/pt-PT missing=4 and pl missing=5 on the Strand catalog when
# every one of those entries was translated in every form. Worse than a wrong number: it
# sent a reader to re-translate strings that were already done, and it made Polish look
# like the worst-covered language precisely BECAUSE it correctly carries one/few/many/other
# where the others need only a flat unit.
if not _is_translated(v, lang):
missing += 1
line += f" {lang} missing={missing}"
print(" ", line)
Expand Down
8 changes: 4 additions & 4 deletions Tools/i18n_extra_locale_baseline.txt
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ NOOPWatch/Localizable.xcstrings:zh-Hant 0
NOOPWatchComplications/Localizable.xcstrings:it 25
NOOPWatchComplications/Localizable.xcstrings:zh-Hans 25
NOOPWatchComplications/Localizable.xcstrings:zh-Hant 25
Strand/Resources/Localizable.xcstrings:it 181
Strand/Resources/Localizable.xcstrings:ru 158
Strand/Resources/Localizable.xcstrings:zh-Hans 131
Strand/Resources/Localizable.xcstrings:zh-Hant 181
Strand/Resources/Localizable.xcstrings:it 179
Strand/Resources/Localizable.xcstrings:ru 156
Strand/Resources/Localizable.xcstrings:zh-Hans 129
Strand/Resources/Localizable.xcstrings:zh-Hant 179
values-zh/strings.xml 49
66 changes: 66 additions & 0 deletions Tools/test_i18n_audit.py
Original file line number Diff line number Diff line change
Expand Up @@ -434,3 +434,69 @@ def test_two_word_brand_is_flagged(self):

if __name__ == "__main__":
unittest.main()


class CatalogSummaryPluralCoverage(unittest.TestCase):
"""The human-readable coverage summary must count plural entries the same way the gate does.

`catalog_summary` used to read `localizations[lang].stringUnit.state` directly. A pluralised entry
keeps its units under `variations.plural.<category>.stringUnit`, so that flat lookup returned None
and scored a fully-translated plural as a gap — it reported de/es/fr/pt-PT missing=4 and pl missing=5
on the Strand catalog when every one of those was translated in every form, and it made Polish look
worst-covered precisely because it correctly carries one/few/many/other.
"""

def _summary(self, catalog: dict) -> str:
import contextlib
import io
saved_catalogs, saved_load = ia.CATALOGS, ia.load_catalog
try:
ia.CATALOGS = [((), ia.ROOT / "fixture" / "Localizable.xcstrings")]
ia.load_catalog = lambda _path: catalog
buf = io.StringIO()
with contextlib.redirect_stdout(buf):
ia.catalog_summary()
return buf.getvalue()
finally:
ia.CATALOGS, ia.load_catalog = saved_catalogs, saved_load

@staticmethod
def _plural(states: dict[str, str]) -> dict:
return {"variations": {"plural": {
cat: {"stringUnit": {"state": st, "value": f"{cat} form"}} for cat, st in states.items()
}}}

def test_fully_translated_plural_is_not_a_gap(self):
cat = {"strings": {"%lld days": {"localizations": {
lang: self._plural({"one": "translated", "other": "translated"})
for lang in ("de", "es", "fr", "pt-PT")
}}}}
out = self._summary(cat)
for lang in ("de", "es", "fr", "pt-PT"):
self.assertIn(f"{lang} missing=0", out)

def test_polish_extra_plural_categories_are_not_penalised(self):
"""one/few/many/other is Polish being handled correctly, not five gaps."""
cat = {"strings": {"%lld days": {"localizations": {
"pl": self._plural({c: "translated" for c in ("one", "few", "many", "other")}),
}}}}
self.assertIn("pl missing=0", self._summary(cat))

def test_partially_translated_plural_is_still_a_gap(self):
"""The fix must not over-correct: one untranslated category still counts."""
cat = {"strings": {"%lld days": {"localizations": {
"de": self._plural({"one": "translated", "other": "new"}),
}}}}
self.assertIn("de missing=1", self._summary(cat))

def test_flat_string_unit_still_counted(self):
"""Non-plural entries keep working exactly as before."""
cat = {"strings": {
"Hello": {"localizations": {"de": {"stringUnit": {"state": "translated", "value": "Hallo"}}}},
"Bye": {"localizations": {"de": {"stringUnit": {"state": "new", "value": ""}}}},
}}
self.assertIn("de missing=1", self._summary(cat))

def test_should_translate_false_is_skipped(self):
cat = {"strings": {"NOOP": {"shouldTranslate": False, "localizations": {}}}}
self.assertIn("de missing=0", self._summary(cat))
Loading