44
44
Path (__file__ ).parents [2 ] / "airflow-core" / "src" / "airflow" / "ui" / "public" / "i18n" / "locales"
45
45
)
46
46
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
+
47
60
48
61
class LocaleSummary (NamedTuple ):
49
62
"""
@@ -84,6 +97,30 @@ class LocaleKeySet(NamedTuple):
84
97
keys : set [str ] | None
85
98
86
99
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
+
87
124
def get_locale_files () -> list [LocaleFiles ]:
88
125
return [
89
126
LocaleFiles (
@@ -127,33 +164,34 @@ def compare_keys(
127
164
for filename in all_files :
128
165
key_sets : list [LocaleKeySet ] = []
129
166
for lf in locale_files :
167
+ keys = set ()
130
168
if filename in lf .files :
131
169
path = LOCALES_DIR / lf .locale / filename
132
170
try :
133
171
data = load_json (path )
134
172
keys = set (flatten_keys (data ))
135
173
except Exception as e :
136
174
print (f"Error loading { path } : { e } " )
137
- keys = set ()
138
- else :
139
- keys = None
140
175
key_sets .append (LocaleKeySet (locale = lf .locale , keys = keys ))
141
176
keys_by_locale = {ks .locale : ks .keys for ks in key_sets }
142
177
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 ()}
143
180
missing_keys : dict [str , list [str ]] = {}
144
181
extra_keys : dict [str , list [str ]] = {}
145
182
missing_counts [filename ] = {}
146
183
for ks in key_sets :
147
184
if ks .locale == "en" :
148
185
continue
186
+ required_keys = expanded_en_keys .get (ks .locale , en_keys )
149
187
if ks .keys is None :
150
- missing_keys [ks .locale ] = list (en_keys )
188
+ missing_keys [ks .locale ] = list (required_keys )
151
189
extra_keys [ks .locale ] = []
152
- missing_counts [filename ][ks .locale ] = len (en_keys )
190
+ missing_counts [filename ][ks .locale ] = len (required_keys )
153
191
else :
154
- missing = list (en_keys - ks .keys )
192
+ missing = list (required_keys - ks .keys )
155
193
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 )
157
195
missing_counts [filename ][ks .locale ] = len (missing )
158
196
summary [filename ] = LocaleSummary (missing_keys = missing_keys , extra_keys = extra_keys )
159
197
return summary , missing_counts
@@ -429,9 +467,11 @@ def add_missing_translations(language: str, summary: dict[str, LocaleSummary], c
429
467
Add missing translations for the selected language.
430
468
431
469
It does it by copying them from English and prefixing with 'TODO: translate:'.
470
+ Ensures all required plural forms for the language are added.
432
471
"""
472
+ suffixes = PLURAL_SUFFIXES .get (language , ["_one" , "_other" ])
433
473
for filename , diff in summary .items ():
434
- missing_keys = diff .missing_keys .get (language , [])
474
+ missing_keys = set ( diff .missing_keys .get (language , []) )
435
475
if not missing_keys :
436
476
continue
437
477
en_path = LOCALES_DIR / "en" / filename
@@ -447,10 +487,27 @@ def add_missing_translations(language: str, summary: dict[str, LocaleSummary], c
447
487
console .print (f"[yellow]Failed to load { language } file { language } : { e } [/yellow]" )
448
488
lang_data = {} # Start with an empty dict if the file doesn't exist
449
489
450
- # Helper to recursively add missing keys
490
+ # Helper to recursively add missing keys, including plural forms
451
491
def add_keys (src , dst , prefix = "" ):
452
492
for k , v in src .items ():
453
493
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
454
511
if full_key in missing_keys :
455
512
if isinstance (v , dict ):
456
513
dst [k ] = {}
@@ -464,10 +521,27 @@ def add_keys(src, dst, prefix=""):
464
521
add_keys (v , dst [k ], full_key )
465
522
466
523
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 )
468
542
lang_path .parent .mkdir (parents = True , exist_ok = True )
469
543
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 )
471
545
f .write ("\n " ) # Ensure newline at the end of the file
472
546
console .print (f"[green]Added missing translations to { lang_path } [/green]" )
473
547
0 commit comments