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'),
+        },
       ],
     };
   },