Add i18n vendor overlays and dynamic bundling
- Add opt-in vendor overlays under src/env/locales/<env>
(and optional variant), merged on top of base locales at runtime.
- Auto-discover and bundle all base locale JSON files
in src/locales/.
- Example: move dump type labels under pageDumps.dumpTypes;
read vendor-only dump labels from overlays.
- Docs: update i18n guidelines and env README (formatting fixes).
- Tests: add focused unit tests for overlays and locale aliases.
Tested:
- Unit: i18n.locale-alias.spec.js, i18n.vendor.spec.js (passing)
- Manual: Verified dynamic locale discovery and overlay merge in UI
Change-Id: I8eae2bfec0e9622bafdafac3168dbf96650e8ae8
Signed-off-by: jason westover <jwestover@nvidia.com>
diff --git a/src/env/locales/README.md b/src/env/locales/README.md
new file mode 100644
index 0000000..90fae61
--- /dev/null
+++ b/src/env/locales/README.md
@@ -0,0 +1,64 @@
+# Vendor locale overlays
+
+This directory contains environment/vendor-specific translation bundles that are
+merged on top of the base, vendor-neutral locales in `src/locales/`.
+
+## Structure
+
+```text
+src/env/locales/
+ <vendor>/
+ en-US.json
+ ka-GE.json
+ ru-RU.json
+ zh-CN.json
+ <vendor-variant>/
+ en-US.json # optional, only when variant overrides vendor root
+ ka-GE.json # optional
+ ru-RU.json # optional
+ zh-CN.json # optional
+```
+
+Examples:
+
+- Shared vendor folder: `src/env/locales/nvidia/`
+- Variant folder: `src/env/locales/nvidia-gb/`
+
+## Merge order at runtime
+
+1. Base locales from `src/locales/` (auto-discovered)
+2. Vendor root overlays (e.g., `src/env/locales/nvidia/`)
+3. Variant overlays (e.g., `src/env/locales/nvidia-gb/`)
+
+Variant keys overwrite vendor root keys on conflict.
+
+## Guidelines
+
+- Keep `src/locales/` vendor‑neutral. Put vendor‑specific strings here.
+- Prefer the vendor root folder when multiple projects share strings to avoid
+ duplication.
+- Only add a variant folder if it truly needs to override vendor root strings.
+- File names must match locale codes (e.g., `en-US.json`, `ru-RU.json`,
+ `zh-CN.json`, `ka-GE.json`).
+- Use 4‑space indentation; alphabetize object keys for readability.
+- JSON must be valid (no trailing commas or comments).
+
+## Environment selection
+
+The active environment is selected by `VUE_APP_ENV_NAME` (e.g., `nvidia`,
+`nvidia-gb`). See the `.env.*` files at the repo root (e.g., `.env.nvidia-gb`).
+
+## Bundling
+
+- All JSON files under `src/locales/` are bundled automatically.
+- Matching overlays under `src/env/locales/<env>` are also bundled and merged at
+ app start.
+
+## Testing
+
+Focused unit tests exist for overlays and fallback:
+
+- `npm run test:unit -- i18n.vendor.spec.js`
+- `npm run test:unit -- i18n.locale-alias.spec.js`
+
+These verify vendor root → variant merge behavior and locale alias handling.
diff --git a/src/env/locales/nvidia/en-US.json b/src/env/locales/nvidia/en-US.json
new file mode 100644
index 0000000..23d8057
--- /dev/null
+++ b/src/env/locales/nvidia/en-US.json
@@ -0,0 +1,11 @@
+{
+ "pageDumps": {
+ "dumpTypes": {
+ "bmcDump": "BMC dump",
+ "hmcDump": "HMC dump",
+ "systemBmcDump": "System [BMC] dump (disruptive)",
+ "systemHgxDump": "System [HGX] dump (disruptive)",
+ "systemDump": "System [BMC] dump (disruptive)"
+ }
+ }
+}
diff --git a/src/env/locales/nvidia/ka-GE.json b/src/env/locales/nvidia/ka-GE.json
new file mode 100644
index 0000000..a6e53a9
--- /dev/null
+++ b/src/env/locales/nvidia/ka-GE.json
@@ -0,0 +1,11 @@
+{
+ "pageDumps": {
+ "dumpTypes": {
+ "bmcDump": "BMC-ის დამპი",
+ "hmcDump": "HMC დამპი",
+ "systemBmcDump": "სისტემის [BMC] დამპი (დროებით წყვეტს სისტემის მუშაობას)",
+ "systemHgxDump": "სისტემის [HGX] დამპი (დროებით წყვეტს სისტემის მუშაობას)",
+ "systemDump": "სისტემის [BMC] დამპი (დროებით წყვეტს სისტემის მუშაობას)"
+ }
+ }
+}
diff --git a/src/env/locales/nvidia/ru-RU.json b/src/env/locales/nvidia/ru-RU.json
new file mode 100644
index 0000000..b6c67b0
--- /dev/null
+++ b/src/env/locales/nvidia/ru-RU.json
@@ -0,0 +1,11 @@
+{
+ "pageDumps": {
+ "dumpTypes": {
+ "bmcDump": "BMC дамп",
+ "hmcDump": "HMC дамп",
+ "systemBmcDump": "Системный [BMC] дамп (разрушительный)",
+ "systemHgxDump": "Системный [HGX] дамп (разрушительный)",
+ "systemDump": "Системный [BMC] дамп (разрушительный)"
+ }
+ }
+}
diff --git a/src/env/locales/nvidia/zh-CN.json b/src/env/locales/nvidia/zh-CN.json
new file mode 100644
index 0000000..1071123
--- /dev/null
+++ b/src/env/locales/nvidia/zh-CN.json
@@ -0,0 +1,11 @@
+{
+ "pageDumps": {
+ "dumpTypes": {
+ "bmcDump": "BMC 转储",
+ "hmcDump": "HMC 转储",
+ "systemBmcDump": "系统 [BMC] 转储(破坏性)",
+ "systemHgxDump": "系统 [HGX] 转储(破坏性)",
+ "systemDump": "系统 [BMC] 转储(破坏性)"
+ }
+ }
+}
diff --git a/src/i18n.js b/src/i18n.js
index 2580784..65c7d1f 100644
--- a/src/i18n.js
+++ b/src/i18n.js
@@ -1,29 +1,94 @@
import { createI18n } from 'vue-i18n';
+import { deepMerge } from './utilities/objectUtils';
-import en_us from './locales/en-US.json';
-import ru_ru from './locales/ru-RU.json';
-import ka_ge from './locales/ka-GE.json';
-
-function loadLocaleMessages() {
- const messages = {
- 'en-US': en_us,
- 'ka-GE': ka_ge,
- 'ru-RU': ru_ru,
- };
+export function loadBaseLocaleMessages() {
+ const context = require.context(
+ './locales',
+ true,
+ /[A-Za-z0-9-_,\s]+\.json$/i,
+ );
+ const messages = {};
+ context.keys().forEach((key) => {
+ const match = key.match(/([A-Za-z0-9-_]+)\.json$/i);
+ if (!match) return;
+ const locale = match[1];
+ const mod = context(key);
+ messages[locale] = mod && mod.default ? mod.default : mod;
+ });
return messages;
}
-const i18n = createI18n({
- // Get default locale from local storage
- locale: window.localStorage.getItem('storedLanguage'),
- // Locales that don't exist will fallback to English
- fallbackLocale: 'en-US',
- // Falling back to fallbackLocale generates two console warnings
- // Silent fallback suppresses console warnings when using fallback
- silentFallbackWarn: true,
- messages: loadLocaleMessages(),
- globalInjection: false,
- legacy: false,
-});
+export function loadEnvLocaleMessages(envName) {
+ if (!envName) return {};
+ const envMessages = {};
+ const envLocales = require.context(
+ './env/locales',
+ true,
+ /[A-Za-z0-9-_,\s]+\.json$/i,
+ );
+ const vendorRoot = String(envName).split('-')[0];
+ const candidates =
+ vendorRoot && vendorRoot !== envName ? [vendorRoot, envName] : [envName];
+ candidates.forEach((candidate) => {
+ envLocales.keys().forEach((key) => {
+ if (!key.includes(`/${candidate}/`)) return;
+ const localeMatch = key.match(/([A-Za-z0-9-_]+)\.json$/i);
+ if (!localeMatch) return;
+ const locale = localeMatch[1];
+ const mod = envLocales(key);
+ const bundle = mod && mod.default ? mod.default : mod;
+ envMessages[locale] = deepMerge(envMessages[locale] || {}, bundle);
+ });
+ });
+ return envMessages;
+}
-export default i18n;
+export function createI18nInstance(
+ envName,
+ locale,
+ loadEnv = loadEnvLocaleMessages,
+ loadBase = loadBaseLocaleMessages,
+) {
+ const base = loadBase();
+ const env = loadEnv(envName);
+ const messages = { ...base };
+ Object.keys(env).forEach((loc) => {
+ messages[loc] = deepMerge(base[loc] || {}, env[loc]);
+ });
+
+ const addAlias = (alias, target) => {
+ if (!messages[alias] && messages[target])
+ messages[alias] = messages[target];
+ };
+ addAlias('en', 'en-US');
+ addAlias('ru', 'ru-RU');
+ addAlias('zh', 'zh-CN');
+ addAlias('ka', 'ka-GE');
+
+ const normalize = (val) => {
+ if (!val) return undefined;
+ const s = String(val);
+ if (s === 'en') return 'en-US';
+ if (s === 'ru') return 'ru-RU';
+ if (s === 'zh') return 'zh-CN';
+ if (s === 'ka') return 'ka-GE';
+ return s;
+ };
+
+ return createI18n({
+ locale: normalize(locale),
+ // Locales that don't exist will fallback to English
+ fallbackLocale: 'en-US',
+ // Falling back to fallbackLocale generates two console warnings
+ // Silent fallback suppresses console warnings when using fallback
+ silentFallbackWarn: true,
+ messages,
+ globalInjection: false,
+ legacy: false,
+ });
+}
+
+const envName = process.env.VUE_APP_ENV_NAME;
+// Get default locale from local storage
+const stored = window.localStorage.getItem('storedLanguage');
+export default createI18nInstance(envName, stored);
diff --git a/src/locales/en-US.json b/src/locales/en-US.json
index e64287d..091d7d9 100644
--- a/src/locales/en-US.json
+++ b/src/locales/en-US.json
@@ -220,11 +220,13 @@
"pageDumps": {
"dumpsAvailableOnBmc": "Dumps available on BMC",
"initiateDump": "Initiate dump",
- "form": {
+ "dumpTypes": {
"bmcDump": "BMC dump",
+ "systemDump": "System dump (disruptive)"
+ },
+ "form": {
"initiateDump": "Initiate dump",
"selectDumpType": "Select dump type",
- "systemDump": "System dump (disruptive)",
"systemDumpInfo": "System dumps will be offloaded to the operating system and will not appear in the table below."
},
"modal": {
diff --git a/src/locales/ka-GE.json b/src/locales/ka-GE.json
index 7427d4f..93425af 100644
--- a/src/locales/ka-GE.json
+++ b/src/locales/ka-GE.json
@@ -220,11 +220,13 @@
"pageDumps": {
"dumpsAvailableOnBmc": "BMC-ზე ხელმისაწვდომი დამპები",
"initiateDump": "დამპის შექმნა",
- "form": {
+ "dumpTypes": {
"bmcDump": "BMC-ის დამპი",
+ "systemDump": "სისტემის დამპი (დროებით წყვეტს სისტემის მუშაობას)"
+ },
+ "form": {
"initiateDump": "დამპის შექმნა",
"selectDumpType": "აირჩით დამპის ტიპი",
- "systemDump": "სისტემის დამპი (დროებით წყვეტს სისტემის მუშაობას)",
"systemDumpInfo": "სისტემის დამპები გადაეცემა ოპერაციულ სისტემას და ქვემოთ ცხრილში არ გამოჩნდება"
},
"modal": {
diff --git a/src/locales/ru-RU.json b/src/locales/ru-RU.json
index 2d7779f..4df30b3 100644
--- a/src/locales/ru-RU.json
+++ b/src/locales/ru-RU.json
@@ -220,11 +220,13 @@
"pageDumps": {
"dumpsAvailableOnBmc": "Дампы доступные на BMC",
"initiateDump": "Создать дамп",
- "form": {
+ "dumpTypes": {
"bmcDump": "Дамп BMC",
+ "systemDump": "Системный дамп (прерывающий работу)"
+ },
+ "form": {
"initiateDump": "Создать дамп",
"selectDumpType": "Выбрать тип дампа",
- "systemDump": "Системный дамп (прерывающий работу)",
"systemDumpInfo": "Системные дампы будут выгружены в операционную систему и не появятся в таблице ниже."
},
"modal": {
diff --git a/src/utilities/objectUtils.js b/src/utilities/objectUtils.js
new file mode 100644
index 0000000..8d4586b
--- /dev/null
+++ b/src/utilities/objectUtils.js
@@ -0,0 +1,31 @@
+/**
+ * Deeply merge two plain objects (or arrays) without mutating the inputs.
+ *
+ * Rules:
+ * - Arrays from the source replace arrays on the target.
+ * - Plain objects are merged recursively.
+ * - Primitive values from the source overwrite the target.
+ */
+export function deepMerge(target, source) {
+ if (typeof target !== 'object' || target === null) return source;
+ if (typeof source !== 'object' || source === null) return target;
+ const output = Array.isArray(target) ? target.slice() : { ...target };
+ Object.keys(source).forEach((key) => {
+ const sourceValue = source[key];
+ const targetValue = output[key];
+ if (Array.isArray(sourceValue)) {
+ output[key] = sourceValue.slice();
+ } else if (
+ typeof sourceValue === 'object' &&
+ sourceValue !== null &&
+ typeof targetValue === 'object' &&
+ targetValue !== null &&
+ !Array.isArray(targetValue)
+ ) {
+ output[key] = deepMerge(targetValue, sourceValue);
+ } else {
+ output[key] = sourceValue;
+ }
+ });
+ return output;
+}
diff --git a/src/views/Logs/Dumps/DumpsForm.vue b/src/views/Logs/Dumps/DumpsForm.vue
index 7e61b96..17257d1 100644
--- a/src/views/Logs/Dumps/DumpsForm.vue
+++ b/src/views/Logs/Dumps/DumpsForm.vue
@@ -55,8 +55,11 @@
$t: useI18n().t,
selectedDumpType: null,
dumpTypeOptions: [
- { value: 'bmc', text: i18n.global.t('pageDumps.form.bmcDump') },
- { value: 'system', text: i18n.global.t('pageDumps.form.systemDump') },
+ { value: 'bmc', text: i18n.global.t('pageDumps.dumpTypes.bmcDump') },
+ {
+ value: 'system',
+ text: i18n.global.t('pageDumps.dumpTypes.systemDump'),
+ },
],
};
},