From d71fa224b3bdc33299361093003f0ae6eed4b3fc Mon Sep 17 00:00:00 2001
From: Christoph Reiter <reiter.christoph@gmail.com>
Date: Thu, 10 Jun 2021 16:00:44 +0200
Subject: [PATCH] common: Add an i18next API for overriding translations

Create a new instance with createInstance() and call setOverrides()
with override data to override translations.

See the tests for some examples.
---
 packages/common/i18next.js      | 49 +++++++++++++++++++++++++++++----
 packages/common/test/i18next.js | 21 ++++++++++++++
 2 files changed, 65 insertions(+), 5 deletions(-)

diff --git a/packages/common/i18next.js b/packages/common/i18next.js
index 26d78a3f..6130fa25 100644
--- a/packages/common/i18next.js
+++ b/packages/common/i18next.js
@@ -42,6 +42,17 @@ export function humanFileSize(bytes, si = false) {
     return bytes.toFixed(1)+' '+units[u];
 }
 
+/**
+ * @param {string} namespace The namespace to override
+ * @returns {string} The new namespace name
+ */
+function getOverrideNamespace(namespace) {
+    // This just needs to be different to the namespace, make it special
+    // so it's clear what it is used for in case it ends up in some error
+    // message or something
+    return '--' + namespace + '-override';
+}
+
 /**
  * Creates a new i18next instance that is fully initialized.
  *
@@ -54,16 +65,18 @@ export function humanFileSize(bytes, si = false) {
  * @returns {i18next.i18n} A new independent i18next instance
  */
 export function createInstance(languages, lng, fallback, namespace) {
-    if (namespace === undefined)
+    if (namespace === undefined) {
         namespace = 'translation';
+    }
+    let overrideNamespace = getOverrideNamespace(namespace);
 
     var options = {
         lng: lng,
         fallbackLng: fallback,
         debug: false,
-        ns: [namespace + '-override'],
-        defaultNS: namespace + '-override',
-        fallbackNS: [namespace],
+        ns: [overrideNamespace, namespace],
+        defaultNS: namespace,
+        fallbackNS: namespace,
         initImmediate: false, // Don't init async
         resources: {},
     };
@@ -77,4 +90,30 @@ export function createInstance(languages, lng, fallback, namespace) {
     console.assert(i18n.isInitialized);
 
     return i18n;
-}
\ No newline at end of file
+}
+
+/**
+ * Sets translation overrides for the given i18next instance. Any previously
+ * applied overrides will be removed first. So calling this with an empty overrides
+ * object is equal to removing all overrides.
+ *
+ * @param {i18next.i18n} i18n - The i18next instance
+ * @param {object} overrides - The override data in the following format: "{language: {namespace: {key: value}}}"
+ */
+export function setOverrides(i18n, overrides) {
+    // We add a special namespace which gets used with priority and falls back
+    // to the original one. This way we an change the overrides at runtime
+    // and can even remove them.
+    let namespace = i18n.options.fallbackNS;
+    let overrideNamespace = getOverrideNamespace(namespace);
+    let hasOverrides = false;
+    for(let lng of i18n.languages) {
+        i18n.removeResourceBundle(lng, overrideNamespace);
+        if (overrides[lng] === undefined || overrides[lng][namespace] === undefined)
+            continue;
+        let resources = overrides[lng][namespace];
+        hasOverrides = true;
+        i18n.addResourceBundle(lng, overrideNamespace, resources);
+    }
+    i18n.setDefaultNamespace(hasOverrides ? overrideNamespace : namespace);
+}
diff --git a/packages/common/test/i18next.js b/packages/common/test/i18next.js
index 3b75700e..ef116042 100644
--- a/packages/common/test/i18next.js
+++ b/packages/common/test/i18next.js
@@ -47,4 +47,25 @@ suite('i18next', () => {
         assert.equal(i18next.numberFormat(inst, 1.25), '1.25');
         assert.equal(i18next.numberFormat(inst, 1234), '1,234');
     });
+
+    test('overrides', () => {
+        let namespace = 'ns';
+        var inst = i18next.createInstance({en:  {foo: 'bar'}}, 'en', 'en', namespace);
+        assert.equal(inst.t('foo'), 'bar');
+        assert.equal(inst.t('quux'), 'quux');
+        i18next.setOverrides(inst, {en: {[namespace]: {quux: 'bla'}}});
+        assert.equal(inst.t('quux'), 'bla');
+        assert.equal(inst.t('foo'), 'bar');
+        i18next.setOverrides(inst, {en: {[namespace]: {}}});
+        assert.equal(inst.t('quux'), 'quux');
+        assert.equal(inst.t('foo'), 'bar');
+        i18next.setOverrides(inst, {en: {[namespace]: {foo: 'hmm'}}});
+        assert.equal(inst.t('foo'), 'hmm');
+        i18next.setOverrides(inst, {en: {[namespace]: {quux: 'bla'}}});
+        assert.equal(inst.t('foo'), 'bar');
+        assert.equal(inst.t('quux'), 'bla');
+        i18next.setOverrides(inst, {});
+        assert.equal(inst.t('foo'), 'bar');
+        assert.equal(inst.t('quux'), 'quux');
+    });
 });
-- 
GitLab