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/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;
+}