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
10 changes: 4 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,12 +120,10 @@ python <tools>/scripts/localization.py --operation import \

The importer derives identity/runtime metadata from the extension manifest,
requires all 14 configured locales, and validates every table before copying.
Each required real-language manifest/runtime table must contain translation
evidence: a nonempty target differing from English, or a target explicitly marked
translated/final/signed-off. Individual untranslated strings are allowed;
pseudo-localization is checked for coverage but is not real-language evidence.
Missing, empty, incomplete, or wholly untranslated handbacks fail explicitly.
Do not publish while waiting for service onboarding or real translated output.
Untranslated strings and wholly untranslated tables are allowed: users see the
English text until translated handbacks arrive. Translation state does not gate
release packaging. Missing, empty, malformed, or incomplete handbacks still fail
explicitly, including missing runtime tables when runtime localization is declared.

Generated root `package.nls.<locale>.json` and declared runtime locale files should
be ignored by Git but included by VSCE. Keep English `package.nls.json` tracked.
Expand Down
21 changes: 3 additions & 18 deletions scripts/localization.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@
"zh-tw",
)
LOCALE_ALIASES = {"zh-hans": "zh-cn", "zh-hant": "zh-tw"}
TRANSLATED_STATES = {"translated", "final", "signed-off"}


def inside(root: pathlib.Path, value: str) -> pathlib.Path:
Expand Down Expand Up @@ -115,9 +114,9 @@ def export_english(


def validate_handback(files: list[pathlib.Path], runtime: bool) -> None:
"""Require coverage and translation evidence in each required locale table."""
"""Require complete, nonempty tables while allowing untranslated strings."""
kinds = ("package", "bundle") if runtime else ("package",)
tables = {}
tables = set()
for path in files:
document = ET.parse(path)
for table in document.findall(".//{*}file"):
Expand All @@ -132,31 +131,17 @@ def validate_handback(files: list[pathlib.Path], runtime: bool) -> None:
units = table.findall(".//{*}trans-unit")
if not units:
raise ValueError(f"Empty translation table: {locale}/{kind}")
translated = False
for unit in units:
source = unit.find("{*}source")
target = unit.find("{*}target")
if source is None or not "".join(source.itertext()).strip():
raise ValueError(f"Missing English source in {locale}/{kind}")
if target is not None:
text = "".join(target.itertext()).strip()
translated |= bool(text) and (
text != "".join(source.itertext()).strip()
or target.get("state") in TRANSLATED_STATES
)
tables[key] = translated
tables.add(key)
for locale in LOCALES:
for kind in kinds:
if (locale, kind) not in tables:
raise ValueError(
f"Incomplete translation handback: missing {locale}/{kind}"
)
if locale != "qps-ploc" and not tables[locale, kind]:
raise ValueError(
f"Translation handback is not ready: {locale}/{kind} is wholly "
"untranslated. Wait for translated output from the "
"localization service."
)


def import_translations(
Expand Down
27 changes: 11 additions & 16 deletions scripts/localization_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@


class LocalizationTests(unittest.TestCase):
"""Exercise generic exports and fully staged, translation-ready imports."""
"""Exercise generic exports and fully staged imports with English fallback."""

def setUp(self):
self.temporary = tempfile.TemporaryDirectory(prefix="shared localization ")
Expand Down Expand Up @@ -176,19 +176,13 @@ def test_manifest_only_import(self):
self.assertEqual(len(list(self.root.glob("package.nls.*.json"))), 14)
self.assertFalse((self.root / "l10n").exists())

def test_readiness_checked_per_table(self):
def test_wholly_untranslated_table_is_allowed(self):
for kind in ("package", "bundle"):
with self.subTest(kind=kind):
self.handbacks(untranslated=("de", kind))
existing = self.root / "package.nls.de.json"
existing.write_text('{"description":"keep"}', encoding="utf-8")
with self.assertRaisesRegex(ValueError, f"de/{kind} is wholly"):
localization.import_translations(
self.root, self.translations, self.output
)
self.assertEqual(existing.read_text(), '{"description":"keep"}')
self.assertFalse(self.output.exists())
self.calls.assert_not_called()
localization.validate_handback(
sorted(self.translations.rglob("*.xlf")), runtime=True
)

def test_partial_translation_is_allowed(self):
files = self.handbacks()
Expand All @@ -200,12 +194,13 @@ def test_source_identical_marked_translated_is_allowed(self):
files = self.handbacks(same=True, state=state)
localization.validate_handback(files, runtime=True)

def test_all_new_english_is_rejected(self):
files = self.handbacks(same=True)
with self.assertRaisesRegex(ValueError, "wholly untranslated"):
localization.validate_handback(files, runtime=True)
def test_all_new_english_is_allowed(self):
self.handbacks(same=True)
localization.import_translations(self.root, self.translations, self.output)
self.assertEqual(len(list(self.root.glob("package.nls.*.json"))), 14)
self.assertEqual(len(list((self.root / "l10n").glob("*.json"))), 14)

def test_pseudo_localization_is_not_translation_evidence(self):
def test_untranslated_pseudo_localization_is_allowed(self):
files = self.handbacks(untranslated=("qps-ploc", "package"))
localization.validate_handback(files, runtime=True)

Expand Down
Loading