This is an automated email from the ASF dual-hosted git repository.

vatsrahul1001 pushed a commit to branch v3-4-test
in repository https://gitbox.apache.org/repos/asf/airflow.git


The following commit(s) were added to refs/heads/v3-4-test by this push:
     new 9987f4021c8 [v3-4-test] UI: Fix Portuguese and Hungarian labels that 
show a raw placeholder (#75964) (#76080)
9987f4021c8 is described below

commit 9987f4021c852df1dbeca2040f7e1c308652c78b
Author: github-actions[bot] 
<41898282+github-actions[bot]@users.noreply.github.com>
AuthorDate: Sat Oct 31 12:44:48 2026 +0530

    [v3-4-test] UI: Fix Portuguese and Hungarian labels that show a raw 
placeholder (#75964) (#76080)
    
    (cherry picked from commit 86c64598200741452d1472a1f680dbb9caa72c61)
    
    Co-authored-by: Pushkal Gupta 
<[email protected]>
    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([]);
+  });
+});

Reply via email to