Skip to content

Commit 0ac7d09

Browse files
committed
Add plural per-language forms in check-translations script
Different languages have different plural forms. Our script should take the original English forms and convert them into the right plural forms for the language. Also noticed that sorting order is slightly different than the one that eslint uses. The "eslint" sorting order is now used when generating missing keys.
1 parent 0c2e416 commit 0ac7d09

File tree

1 file changed

+85
-11
lines changed

1 file changed

+85
-11
lines changed

dev/i18n/check_translations_completeness.py

Lines changed: 85 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,19 @@
4444
Path(__file__).parents[2] / "airflow-core" / "src" / "airflow" / "ui" / "public" / "i18n" / "locales"
4545
)
4646

47+
# Plural suffixes per language (expand as needed)
48+
PLURAL_SUFFIXES = {
49+
"en": ["_one", "_other"],
50+
"pl": ["_one", "_few", "_many", "_other"],
51+
"de": ["_one", "_other"],
52+
"fr": ["_one", "_other"],
53+
"nl": ["_one", "_other"],
54+
"ar": ["_zero", "_one", "_two", "_few", "_many", "_other"],
55+
"he": ["_one", "_other"],
56+
"ko": ["_other"],
57+
"zh-TW": ["_other"],
58+
}
59+
4760

4861
class LocaleSummary(NamedTuple):
4962
"""
@@ -84,6 +97,30 @@ class LocaleKeySet(NamedTuple):
8497
keys: set[str] | None
8598

8699

100+
def get_plural_base(key: str, suffixes: list[str]) -> str | None:
101+
for suffix in suffixes:
102+
if key.endswith(suffix):
103+
return key[: -len(suffix)]
104+
return None
105+
106+
107+
def expand_plural_keys(keys: set[str], lang: str) -> set[str]:
108+
"""
109+
For a set of keys, expand all plural bases to include all required suffixes for the language.
110+
"""
111+
suffixes = PLURAL_SUFFIXES.get(lang, ["_one", "_other"])
112+
base_to_suffixes: dict[str, set[str]] = {}
113+
for key in keys:
114+
base = get_plural_base(key, suffixes)
115+
if base:
116+
base_to_suffixes.setdefault(base, set()).add(key[len(base) :])
117+
expanded = set(keys)
118+
for base in base_to_suffixes.keys():
119+
for suffix in suffixes:
120+
expanded.add(base + suffix)
121+
return expanded
122+
123+
87124
def get_locale_files() -> list[LocaleFiles]:
88125
return [
89126
LocaleFiles(
@@ -127,33 +164,34 @@ def compare_keys(
127164
for filename in all_files:
128165
key_sets: list[LocaleKeySet] = []
129166
for lf in locale_files:
167+
keys = set()
130168
if filename in lf.files:
131169
path = LOCALES_DIR / lf.locale / filename
132170
try:
133171
data = load_json(path)
134172
keys = set(flatten_keys(data))
135173
except Exception as e:
136174
print(f"Error loading {path}: {e}")
137-
keys = set()
138-
else:
139-
keys = None
140175
key_sets.append(LocaleKeySet(locale=lf.locale, keys=keys))
141176
keys_by_locale = {ks.locale: ks.keys for ks in key_sets}
142177
en_keys = keys_by_locale.get("en", set()) or set()
178+
# Expand English keys for all required plural forms in each language
179+
expanded_en_keys = {lang: expand_plural_keys(en_keys, lang) for lang in keys_by_locale.keys()}
143180
missing_keys: dict[str, list[str]] = {}
144181
extra_keys: dict[str, list[str]] = {}
145182
missing_counts[filename] = {}
146183
for ks in key_sets:
147184
if ks.locale == "en":
148185
continue
186+
required_keys = expanded_en_keys.get(ks.locale, en_keys)
149187
if ks.keys is None:
150-
missing_keys[ks.locale] = list(en_keys)
188+
missing_keys[ks.locale] = list(required_keys)
151189
extra_keys[ks.locale] = []
152-
missing_counts[filename][ks.locale] = len(en_keys)
190+
missing_counts[filename][ks.locale] = len(required_keys)
153191
else:
154-
missing = list(en_keys - ks.keys)
192+
missing = list(required_keys - ks.keys)
155193
missing_keys[ks.locale] = missing
156-
extra_keys[ks.locale] = list(ks.keys - en_keys)
194+
extra_keys[ks.locale] = list(ks.keys - required_keys)
157195
missing_counts[filename][ks.locale] = len(missing)
158196
summary[filename] = LocaleSummary(missing_keys=missing_keys, extra_keys=extra_keys)
159197
return summary, missing_counts
@@ -429,9 +467,11 @@ def add_missing_translations(language: str, summary: dict[str, LocaleSummary], c
429467
Add missing translations for the selected language.
430468
431469
It does it by copying them from English and prefixing with 'TODO: translate:'.
470+
Ensures all required plural forms for the language are added.
432471
"""
472+
suffixes = PLURAL_SUFFIXES.get(language, ["_one", "_other"])
433473
for filename, diff in summary.items():
434-
missing_keys = diff.missing_keys.get(language, [])
474+
missing_keys = set(diff.missing_keys.get(language, []))
435475
if not missing_keys:
436476
continue
437477
en_path = LOCALES_DIR / "en" / filename
@@ -447,10 +487,27 @@ def add_missing_translations(language: str, summary: dict[str, LocaleSummary], c
447487
console.print(f"[yellow]Failed to load {language} file {language}: {e}[/yellow]")
448488
lang_data = {} # Start with an empty dict if the file doesn't exist
449489

450-
# Helper to recursively add missing keys
490+
# Helper to recursively add missing keys, including plural forms
451491
def add_keys(src, dst, prefix=""):
452492
for k, v in src.items():
453493
full_key = f"{prefix}.{k}" if prefix else k
494+
base = get_plural_base(full_key, suffixes)
495+
if base and any(full_key == base + s for s in suffixes):
496+
# Add all plural forms if any are missing
497+
for suffix in suffixes:
498+
plural_key = base + suffix
499+
plural_path = plural_key.split(".")
500+
# Traverse dst to the right place
501+
d = dst
502+
for part in plural_path[:-1]:
503+
d = d.setdefault(part, {})
504+
if plural_key in missing_keys:
505+
if isinstance(v, dict):
506+
d[plural_path[-1]] = {}
507+
add_keys(v, d[plural_path[-1]], ".".join(plural_path[:-1]))
508+
else:
509+
d[plural_path[-1]] = f"TODO: translate: {v}"
510+
continue
454511
if full_key in missing_keys:
455512
if isinstance(v, dict):
456513
dst[k] = {}
@@ -464,10 +521,27 @@ def add_keys(src, dst, prefix=""):
464521
add_keys(v, dst[k], full_key)
465522

466523
add_keys(en_data, lang_data)
467-
# Write back to file, preserving order
524+
525+
# Write back to file, preserving order and using eslint-style key sorting
526+
def eslint_key_sort(obj):
527+
if isinstance(obj, dict):
528+
# Sort keys: numbers first, then uppercase, then lowercase, then others (eslint default)
529+
def sort_key(k):
530+
if k.isdigit():
531+
return (0, int(k))
532+
if k and k[0].isupper():
533+
return (1, k)
534+
if k and k[0].islower():
535+
return (2, k)
536+
return (3, k)
537+
538+
return {k: eslint_key_sort(obj[k]) for k in sorted(obj, key=sort_key)}
539+
return obj
540+
541+
lang_data = eslint_key_sort(lang_data)
468542
lang_path.parent.mkdir(parents=True, exist_ok=True)
469543
with open(lang_path, "w", encoding="utf-8") as f:
470-
json.dump(lang_data, f, ensure_ascii=False, indent=2, sort_keys=True)
544+
json.dump(lang_data, f, ensure_ascii=False, indent=2)
471545
f.write("\n") # Ensure newline at the end of the file
472546
console.print(f"[green]Added missing translations to {lang_path}[/green]")
473547

0 commit comments

Comments
 (0)