diff --git a/package.json b/package.json index 5d05a494..c7b1f1dd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "openstack-uicore-foundation", - "version": "5.0.60", + "version": "5.0.62", "description": "ui reactjs components for openstack marketing site", "main": "lib/openstack-uicore-foundation.js", "scripts": { 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)); +};