This is an automated email from the ASF dual-hosted git repository.
bbovenzi pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow.git
The following commit(s) were added to refs/heads/main by this push:
new 86c64598200 UI: Fix Portuguese and Hungarian labels that show a raw
placeholder (#75964)
86c64598200 is described below
commit 86c64598200741452d1472a1f680dbb9caa72c61
Author: Pushkal Gupta <[email protected]>
AuthorDate: Sat Oct 31 00:03:32 2026 +0530
UI: Fix Portuguese and Hungarian labels that show a raw placeholder (#75964)
Co-authored-by: Brent Bovenzi <[email protected]>
---
.../airflow/ui/public/i18n/locales/hu/hitl.json | 2 +-
.../airflow/ui/public/i18n/locales/pt/common.json | 8 +--
.../src/airflow/ui/src/i18n/locales.test.ts | 78 ++++++++++++++++++++++
3 files changed, 83 insertions(+), 5 deletions(-)
diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/hu/hitl.json
b/airflow-core/src/airflow/ui/public/i18n/locales/hu/hitl.json
index d6823b17fef..1e3cd1393ac 100644
--- a/airflow-core/src/airflow/ui/public/i18n/locales/hu/hitl.json
+++ b/airflow-core/src/airflow/ui/public/i18n/locales/hu/hitl.json
@@ -20,7 +20,7 @@
"optionsLabel": "Lehetőségek",
"received": "Válasz beérkezett: ",
"respond": "Válaszadás",
- "responded_by_user_name": "Válaszolta: {{userName}}",
+ "responded_by_user_name": "Válaszolta (felhasználónév)",
"success": "A(z) {{taskId}} feladatpéldányhoz tartozó válasz sikeres",
"title": "Emberi feladat példány - {{taskId}}"
},
diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/pt/common.json
b/airflow-core/src/airflow/ui/public/i18n/locales/pt/common.json
index bc583409766..2396620372f 100644
--- a/airflow-core/src/airflow/ui/public/i18n/locales/pt/common.json
+++ b/airflow-core/src/airflow/ui/public/i18n/locales/pt/common.json
@@ -123,7 +123,7 @@
"expand": {
"collapse": "Recolher",
"expand": "Expandir",
- "hotkey": "Pressione {{hotkey}} para expandir ou recolher",
+ "hotkey": "e",
"tooltip": "Pressione {{hotkey}} para expandir ou recolher a secção"
},
"expandAllExtra": "Expandir todos os extra json",
@@ -216,7 +216,7 @@
"signedInAs": "Sessão iniciada como",
"source": {
"hide": "Ocultar",
- "hotkey": "Pressione {{hotkey}} para ocultar/mostrar",
+ "hotkey": "s",
"show": "Mostrar"
},
"sourceAssetEvent_many": "Eventos de Asset de Origem",
@@ -308,7 +308,7 @@
},
"timestamp": {
"hide": "Ocultar",
- "hotkey": "Pressione {{hotkey}} para ocultar/mostrar",
+ "hotkey": "t",
"show": "Mostrar"
},
"timezone": "Fuso Horário",
@@ -360,7 +360,7 @@
"tryNumber": "Número de Tentativas",
"user": "Usuário",
"wrap": {
- "hotkey": "Pressione {{hotkey}} para expandir ou recolher",
+ "hotkey": "w",
"tooltip": "Pressione {{hotkey}} para expandir ou recolher a secção",
"unwrap": "Expandir",
"wrap": "Recolher"
diff --git a/airflow-core/src/airflow/ui/src/i18n/locales.test.ts
b/airflow-core/src/airflow/ui/src/i18n/locales.test.ts
index 54fc2ccd76c..d8b41c0d889 100644
--- a/airflow-core/src/airflow/ui/src/i18n/locales.test.ts
+++ b/airflow-core/src/airflow/ui/src/i18n/locales.test.ts
@@ -157,3 +157,81 @@ describe("locale counter placeholders", () => {
expect(instance.t("dag:logs.search.matchCount", { total: 5
})).toBe("{{current, number}} of 5");
});
});
+const PLACEHOLDER = /\{\{\s*(?<name>[A-Za-z0-9_$]+)/gu;
+const PLURAL_SUFFIX = /_(?:zero|one|two|few|many|other)$/u;
+const PLURAL_CATEGORIES = ["_other", "_one", "_many", "_few", "_two", "_zero",
""];
+
+const placeholdersOf = (value: string): Set<string> =>
+ new Set([...value.matchAll(PLACEHOLDER)].map((match) => match.groups?.name
?? ""));
+
+const stringsOf = (lng: string): Map<string, string> =>
+ new Map(
+ Object.entries((resources[lng] ?? {}) as Record<string,
unknown>).flatMap(([ns, bundle]) =>
+ flattenStrings(bundle, ns),
+ ),
+ );
+
+// Plural categories differ by language, so a translated `foo_many` may have
no en key of its own
+// category to compare against and has to fall back to whichever form en does
define.
+const enCounterpart = (en: Map<string, string>, key: string): [string, string]
| undefined => {
+ const exact = en.get(key);
+
+ if (exact !== undefined) {
+ return [key, exact];
+ }
+
+ const base = key.replace(PLURAL_SUFFIX, "");
+
+ for (const category of PLURAL_CATEGORIES) {
+ const value = en.get(`${base}${category}`);
+
+ if (value !== undefined) {
+ return [`${base}${category}`, value];
+ }
+ }
+
+ return undefined;
+};
+
+// A translation may only interpolate the variables its en original
interpolates. The call site
+// passes one fixed set, so a locale that invents a variable shows the user a
raw "{{...}}" — as
+// the counter test above pins — and one that drops an interpolated value
loses it silently.
+const findVariableMismatch = (en: Map<string, string>, key: string, value:
string): string | undefined => {
+ const counterpart = enCounterpart(en, key);
+
+ if (counterpart === undefined) {
+ return undefined;
+ }
+
+ const [enKey, original] = counterpart;
+ const expectedNames = placeholdersOf(original);
+ const actualNames = placeholdersOf(value);
+
+ // i18next only resolves a plural form when `count` was passed, so it is
always available.
+ if (PLURAL_SUFFIX.test(key) || PLURAL_SUFFIX.test(enKey)) {
+ expectedNames.delete("count");
+ actualNames.delete("count");
+ }
+
+ const invented = [...actualNames].filter((name) => !expectedNames.has(name));
+ const dropped = [...expectedNames].filter((name) => !actualNames.has(name));
+
+ return invented.length > 0 || dropped.length > 0
+ ? `${key}: invented [${invented.join(", ")}], dropped [${dropped.join(",
")}]`
+ : undefined;
+};
+
+describe("locale interpolation variables", () => {
+ const en = stringsOf("en");
+ const translations = Object.keys(resources)
+ .filter((lng) => lng !== "en")
+ .sort();
+
+ it.each(translations)("interpolates only the variables en does, in %s",
(lng) => {
+ const offenders = [...stringsOf(lng)]
+ .map(([key, value]) => findVariableMismatch(en, key, value))
+ .filter((offender) => offender !== undefined);
+
+ expect(offenders).toEqual([]);
+ });
+});