From a4df6812a6fae72fccff2a98efc87d1643f5afc3 Mon Sep 17 00:00:00 2001 From: romanetar Date: Fri, 11 Sep 2026 10:29:20 +0200 Subject: [PATCH 1/5] Fix: fall back to English for untranslated i18n keys Non english bundles are partial: zh.json and es.json only cover errors/general, while en.json has 18 sections. Since i18n-react renders the raw key when a lookup misses, components such as SponsorOrderGrid showed literal strings like "sponsor_order_grid.code" to any user whose browser language is zh or es. Deep merge the language bundle on top of English so missing keys fall back instead of leaking the key, and default to English for unsupported languages, which previously left the text table undefined and broke every translation. Drop the try/catch around setTexts: it never fired, as i18n-react's setTexts only assigns, so its English fallback was dead code. Co-Authored-By: Claude Opus 5 (1M context) --- src/i18n/__tests__/i18n.test.js | 50 +++++++++++++++++++++++++++++++++ src/i18n/i18n.js | 23 +++++++++------ 2 files changed, 65 insertions(+), 8 deletions(-) create mode 100644 src/i18n/__tests__/i18n.test.js diff --git a/src/i18n/__tests__/i18n.test.js b/src/i18n/__tests__/i18n.test.js new file mode 100644 index 00000000..9f767b17 --- /dev/null +++ b/src/i18n/__tests__/i18n.test.js @@ -0,0 +1,50 @@ +import en from "../en.json"; +import zh from "../zh.json"; + +const loadI18n = async (language) => { + jest.resetModules(); + jest.doMock("../../utils/methods", () => ({ + getCurrentUserLanguage: () => language + })); + const T = (await import("i18n-react")).default; + const i18n = await import("../i18n"); + return { T, ...i18n }; +}; + +describe("i18n lib translations", () => { + afterEach(() => { + jest.dontMock("../../utils/methods"); + }); + + it("uses the matching bundle when the key is translated", async () => { + const { T } = await loadI18n("zh-CN"); + expect(T.translate("general.save")).toBe(zh.general.save); + }); + + it("falls back to English for keys missing from a partial bundle", async () => { + const { T } = await loadI18n("zh-CN"); + // zh.json only translates errors/general, so this would otherwise render + // the raw key + expect(T.translate("sponsor_order_grid.code")).toBe( + en.sponsor_order_grid.code + ); + }); + + it("falls back to English for unsupported languages", async () => { + const { T } = await loadI18n("fr-FR"); + expect(T.translate("general.save")).toBe(en.general.save); + expect(T.translate("sponsor_order_grid.code")).toBe( + en.sponsor_order_grid.code + ); + }); + + it("lets consumer texts win but keeps the lib fallbacks in setAppTexts", async () => { + const { T, setAppTexts } = await loadI18n("zh-CN"); + setAppTexts({ sponsor_order_grid: { code: "Custom Code" } }); + expect(T.translate("sponsor_order_grid.code")).toBe("Custom Code"); + expect(T.translate("sponsor_order_grid.type")).toBe( + en.sponsor_order_grid.type + ); + expect(T.translate("general.save")).toBe(zh.general.save); + }); +}); diff --git a/src/i18n/i18n.js b/src/i18n/i18n.js index 81f0903c..9fd68598 100644 --- a/src/i18n/i18n.js +++ b/src/i18n/i18n.js @@ -22,11 +22,19 @@ if (language.length > 2) { language = language.split("_")[0]; } -try { - T.setTexts(resources[language]); -} catch (e) { - T.setTexts(resources['en']); -} +/** + * Returns the lib translations for the given language, deep-merged on top of + * English. Non english bundles are partial (they only translate a subset of the + * keys), so without this merge any key missing from them would be rendered as + * the raw key (i18n-react returns the key itself when it is not found). + * Unsupported languages fall back to English entirely. + * + * @param {string} lang + * @returns {object} + */ +const getLibTexts = (lang) => merge({}, resources['en'], resources[lang] || {}); + +T.setTexts(getLibTexts(language)); /** * Call this instead of T.setTexts() in consumer apps. @@ -37,6 +45,5 @@ try { * @param {object} customTexts - your app's translation object */ export const setAppTexts = (customTexts = {}) => { - const libTexts = resources[language] || resources['en']; - T.setTexts(merge({}, libTexts, customTexts)); -}; \ No newline at end of file + T.setTexts(merge({}, getLibTexts(language), customTexts)); +}; From 227703d8bf2a6d078010714feac48aeaa42f84c3 Mon Sep 17 00:00:00 2001 From: romanetar Date: Fri, 11 Sep 2026 10:40:11 +0200 Subject: [PATCH 2/5] 5.0.61 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 5d05a494..37908eb1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "openstack-uicore-foundation", - "version": "5.0.60", + "version": "5.0.61", "description": "ui reactjs components for openstack marketing site", "main": "lib/openstack-uicore-foundation.js", "scripts": { From 835d89c60aa592694b71c32f4dedd2dfe2066a0d Mon Sep 17 00:00:00 2001 From: romanetar Date: Fri, 11 Sep 2026 10:42:52 +0200 Subject: [PATCH 3/5] v5.0.62-beta.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 37908eb1..6be64076 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "openstack-uicore-foundation", - "version": "5.0.61", + "version": "5.0.62-beta.0", "description": "ui reactjs components for openstack marketing site", "main": "lib/openstack-uicore-foundation.js", "scripts": { From 4c6cf5394247a171ebb7f4e1baef424b64bb434b Mon Sep 17 00:00:00 2001 From: romanetar Date: Fri, 11 Sep 2026 10:48:25 +0200 Subject: [PATCH 4/5] v5.0.62-beta.1 Co-Authored-By: Claude Opus 5 (1M context) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 6be64076..3cf321ff 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "openstack-uicore-foundation", - "version": "5.0.62-beta.0", + "version": "5.0.62-beta.1", "description": "ui reactjs components for openstack marketing site", "main": "lib/openstack-uicore-foundation.js", "scripts": { From 67ae261f7c524ff016d4843613c0b391f4c0ca93 Mon Sep 17 00:00:00 2001 From: romanetar Date: Fri, 11 Sep 2026 14:59:19 +0200 Subject: [PATCH 5/5] 5.0.62 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 3cf321ff..c7b1f1dd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "openstack-uicore-foundation", - "version": "5.0.62-beta.1", + "version": "5.0.62", "description": "ui reactjs components for openstack marketing site", "main": "lib/openstack-uicore-foundation.js", "scripts": {