diff --git a/client/app/assets/i18n/de.json b/client/app/assets/i18n/de.json index 929919b2..6db33317 100644 --- a/client/app/assets/i18n/de.json +++ b/client/app/assets/i18n/de.json @@ -3,6 +3,7 @@ "joinRoomInfo": "Du kannst auch einen bereits existierenden Raum betreten. Frage den Ersteller für den Link.", "createNewRoom": "Erstelle einen neuen Raum", "join": "Beitreten", + "joinRoomname": "%{roomName} beitreten", "name": "Name...", "addStory": "Story hinzufügen", "newRound": "Neue Runde", diff --git a/client/app/assets/i18n/en.json b/client/app/assets/i18n/en.json index 31f99acb..8dd090bc 100644 --- a/client/app/assets/i18n/en.json +++ b/client/app/assets/i18n/en.json @@ -3,6 +3,7 @@ "joinRoomInfo": "You can also join an existing room by link. Ask the creator of the room for the address.", "createNewRoom": "Create new room", "join": "Join", + "joinRoomname": "Join %{roomName}", "name": "Name...", "addStory": "Add Story", "newRound": "New Round", diff --git a/client/app/services/clientActionReducer.js b/client/app/services/clientActionReducer.js index 8cf73843..4c730847 100644 --- a/client/app/services/clientActionReducer.js +++ b/client/app/services/clientActionReducer.js @@ -1,5 +1,4 @@ import clientSettingsStore from '../store/clientSettingsStore'; -import translator from './translator'; import { TOGGLE_BACKLOG, TOGGLE_USER_MENU, @@ -86,7 +85,8 @@ export default function clientActionReducer(state, action) { case SET_LANGUAGE: { const language = action.language; clientSettingsStore.setPresetLanguage(language); - return {...state, language, translator: (key) => translator(key, language)}; + state.setLanguage(language); + return {...state, language}; } case LOCATION_CHANGED: { diff --git a/client/app/services/translator.js b/client/app/services/translator.js index ec97fdfd..5657aecc 100644 --- a/client/app/services/translator.js +++ b/client/app/services/translator.js @@ -1,32 +1,14 @@ -import log from 'loglevel'; +import Polyglot from 'node-polyglot'; -import translationsEN from '../assets/i18n/en.json'; -import translationsDE from '../assets/i18n/de.json'; +export default function translatorFactory(dictionary, language) { + const polyglot = new Polyglot({phrases: dictionary[language]}); -const dictionary = { - en: translationsEN, - de: translationsDE -}; + return { + t: translatorFunction, + setLanguage: (lang) => polyglot.replace(dictionary[lang]) + }; -/** - * Performs a lookup of a translation with the given key for the given language. - * - * @param translationKey - * @param language - * @returns {string} - */ -export default function lookup(translationKey, language) { - const translations = dictionary[language]; - - if (!translations) { - log.error(`Unknown language '${language}'`); - return `!!!${translationKey}!!!`; - } - - const translation = translations[translationKey]; - if (!translation) { - return `!!!${translationKey}!!!`; + function translatorFunction(translationKey, data) { + return polyglot.t(translationKey, {...data, _: `!!!${translationKey}!!!`}); } - - return translation; } diff --git a/client/app/store/initialState.js b/client/app/store/initialState.js index bac72d3e..bc733e8e 100644 --- a/client/app/store/initialState.js +++ b/client/app/store/initialState.js @@ -1,9 +1,20 @@ import clientSettingsStore from './clientSettingsStore'; -import translator from '../services/translator'; +import translatorFactory from '../services/translator'; + +import translationsEN from '../../app/assets/i18n/en.json'; +import translationsDE from '../../app/assets/i18n/de.json'; const DEFAULT_LANGUAGE = 'en'; const userLanguage = clientSettingsStore.getPresetLanguage(); +const {t, setLanguage} = translatorFactory( + { + en: translationsEN, + de: translationsDE + }, + userLanguage || DEFAULT_LANGUAGE +); + /** * The initial state that is loaded into the redux store on (client) application load. */ @@ -30,7 +41,8 @@ const INITIAL_STATE = { actionLog: [], // will contain human readable "log messages" of actions that did take place in the current room pendingCommands: {}, // will contain pending commands (commands for which no event is received yet) language: userLanguage || DEFAULT_LANGUAGE, - translator: (key) => translator(key, userLanguage || DEFAULT_LANGUAGE) + translator: t, + setLanguage }; export default INITIAL_STATE; diff --git a/client/package-lock.json b/client/package-lock.json index d1d99957..b03a547f 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -3847,7 +3847,6 @@ "version": "1.1.3", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", "integrity": "sha1-z4jabL7ib+bbcJT2HYcMvYTO6fE=", - "dev": true, "requires": { "object-keys": "^1.0.12" } @@ -4206,7 +4205,6 @@ "version": "1.17.5", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.5.tgz", "integrity": "sha512-BR9auzDbySxOcfog0tLECW8l28eRGpDpU3Dm3Hp4q/N+VtLTmyj4EUN088XZWQDW/hzj6sYRDXeOFsaAODKvpg==", - "dev": true, "requires": { "es-to-primitive": "^1.2.1", "function-bind": "^1.1.1", @@ -4225,7 +4223,6 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", - "dev": true, "requires": { "is-callable": "^1.1.4", "is-date-object": "^1.0.1", @@ -5153,6 +5150,14 @@ "integrity": "sha1-j6jPBBGhoxr9B7BtKQK7n8gVoTM=", "dev": true }, + "for-each": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", + "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", + "requires": { + "is-callable": "^1.1.3" + } + }, "for-in": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", @@ -5235,8 +5240,7 @@ "function-bind": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha1-pWiZ0+o8m6uHS7l3O3xe3pL0iV0=", - "dev": true + "integrity": "sha1-pWiZ0+o8m6uHS7l3O3xe3pL0iV0=" }, "functional-red-black-tree": { "version": "1.0.1", @@ -5411,7 +5415,6 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", "integrity": "sha1-ci18v8H2qoJB8W3YFOAR4fQeh5Y=", - "dev": true, "requires": { "function-bind": "^1.1.1" } @@ -5465,8 +5468,7 @@ "has-symbols": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.1.tgz", - "integrity": "sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==", - "dev": true + "integrity": "sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==" }, "has-value": { "version": "1.0.0", @@ -6000,8 +6002,7 @@ "is-callable": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.5.tgz", - "integrity": "sha512-ESKv5sMCJB2jnHTWZ3O5itG+O128Hsus4K4Qh1h2/cgn2vbgnLSVqfV46AeJA9D5EeeLa9w81KUXMtn34zhX+Q==", - "dev": true + "integrity": "sha512-ESKv5sMCJB2jnHTWZ3O5itG+O128Hsus4K4Qh1h2/cgn2vbgnLSVqfV46AeJA9D5EeeLa9w81KUXMtn34zhX+Q==" }, "is-ci": { "version": "2.0.0", @@ -6041,8 +6042,7 @@ "is-date-object": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.2.tgz", - "integrity": "sha512-USlDT524woQ08aoZFzh3/Z6ch9Y/EWXEHQ/AaRN0SkKq4t2Jw2R2339tSXmwuVoY7LLlBCbOIlx2myP/L5zk0g==", - "dev": true + "integrity": "sha512-USlDT524woQ08aoZFzh3/Z6ch9Y/EWXEHQ/AaRN0SkKq4t2Jw2R2339tSXmwuVoY7LLlBCbOIlx2myP/L5zk0g==" }, "is-descriptor": { "version": "0.1.6", @@ -6152,7 +6152,6 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.5.tgz", "integrity": "sha512-vlKW17SNq44owv5AQR3Cq0bQPEb8+kF3UKZ2fiZNOWtztYE5i0CzCZxFDwO58qAOWtxdBRVO/V5Qin1wjCqFYQ==", - "dev": true, "requires": { "has": "^1.0.3" } @@ -6173,7 +6172,6 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.3.tgz", "integrity": "sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ==", - "dev": true, "requires": { "has-symbols": "^1.0.1" } @@ -7999,8 +7997,7 @@ "js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" }, "js-yaml": { "version": "3.14.0", @@ -8284,7 +8281,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "dev": true, "requires": { "js-tokens": "^3.0.0 || ^4.0.0" } @@ -8813,6 +8809,17 @@ } } }, + "node-polyglot": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/node-polyglot/-/node-polyglot-2.4.0.tgz", + "integrity": "sha512-KRzKwzMWm3wSAjOSop7/WwNyzaMkCe9ddkwXTQsIZEJmvEnqy/bCqLpAVw6xBszKfy4iLdYVA0d83L+cIkYPbA==", + "requires": { + "for-each": "^0.3.3", + "has": "^1.0.3", + "string.prototype.trim": "^1.1.2", + "warning": "^4.0.3" + } + }, "node-releases": { "version": "1.1.57", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.57.tgz", @@ -8910,8 +8917,7 @@ "object-inspect": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.7.0.tgz", - "integrity": "sha512-a7pEHdh1xKIAgTySUGgLMx/xwDZskN1Ud6egYYN3EdRW4ZMPNEDUTF+hwy2LUC+Bl+SyLXANnwz/jyh/qutKUw==", - "dev": true + "integrity": "sha512-a7pEHdh1xKIAgTySUGgLMx/xwDZskN1Ud6egYYN3EdRW4ZMPNEDUTF+hwy2LUC+Bl+SyLXANnwz/jyh/qutKUw==" }, "object-is": { "version": "1.1.2", @@ -8926,8 +8932,7 @@ "object-keys": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", - "dev": true + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==" }, "object-visit": { "version": "1.0.1", @@ -8942,7 +8947,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.0.tgz", "integrity": "sha1-lovxEA15Vrs8oIbwBvhGs7xACNo=", - "dev": true, "requires": { "define-properties": "^1.1.2", "function-bind": "^1.1.1", @@ -11515,11 +11519,20 @@ "side-channel": "^1.0.2" } }, + "string.prototype.trim": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.1.tgz", + "integrity": "sha512-MjGFEeqixw47dAMFMtgUro/I0+wNqZB5GKXGt1fFr24u3TzDXCPu7J9Buppzoe3r/LqkSDLDDJzE15RGWDGAVw==", + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.0-next.1", + "function-bind": "^1.1.1" + } + }, "string.prototype.trimend": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.1.tgz", "integrity": "sha512-LRPxFUaTtpqYsTeNKaFOw3R4bxIzWOnbQ837QfBylo8jIxtcbK/A/sMV7Q+OAV/vWo+7s25pOE10KYSjaSO06g==", - "dev": true, "requires": { "define-properties": "^1.1.3", "es-abstract": "^1.17.5" @@ -11529,7 +11542,6 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/string.prototype.trimleft/-/string.prototype.trimleft-2.1.2.tgz", "integrity": "sha512-gCA0tza1JBvqr3bfAIFJGqfdRTyPae82+KTnm3coDXkZN9wnuW3HjGgN386D7hfv5CHQYCI022/rJPVlqXyHSw==", - "dev": true, "requires": { "define-properties": "^1.1.3", "es-abstract": "^1.17.5", @@ -11540,7 +11552,6 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/string.prototype.trimright/-/string.prototype.trimright-2.1.2.tgz", "integrity": "sha512-ZNRQ7sY3KroTaYjRS6EbNiiHrOkjihL9aQE/8gfQ4DtAC/aEBRHFJa44OmoWxGGqXuJlfKkZW4WcXErGr+9ZFg==", - "dev": true, "requires": { "define-properties": "^1.1.3", "es-abstract": "^1.17.5", @@ -11551,7 +11562,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.1.tgz", "integrity": "sha512-XxZn+QpvrBI1FOcg6dIpxUPgWCPuNXvMD72aaRaUQv1eD4e/Qy8i/hFTe0BUmD60p/QA6bh1avmuPTfNjqVWRw==", - "dev": true, "requires": { "define-properties": "^1.1.3", "es-abstract": "^1.17.5" @@ -12444,6 +12454,14 @@ "makeerror": "1.0.x" } }, + "warning": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz", + "integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==", + "requires": { + "loose-envify": "^1.0.0" + } + }, "watchpack": { "version": "1.6.1", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-1.6.1.tgz", diff --git a/client/package.json b/client/package.json index 4c6b7696..12f134d1 100644 --- a/client/package.json +++ b/client/package.json @@ -26,6 +26,7 @@ }, "dependencies": {}, "devDependencies": { + "node-polyglot": "^2.4.0", "@babel/core": "^7.10.1", "@babel/preset-env": "^7.10.1", "@babel/preset-react": "7.10.1", diff --git a/client/test/unit/translatorTest.js b/client/test/unit/translatorTest.js new file mode 100644 index 00000000..d60789b3 --- /dev/null +++ b/client/test/unit/translatorTest.js @@ -0,0 +1,40 @@ +import translatorFactory from '../../app/services/translator'; + +import translationsEN from '../../app/assets/i18n/en.json'; +import translationsDE from '../../app/assets/i18n/de.json'; + +const dictionary = { + en: translationsEN, + de: translationsDE +}; + +test('simple', () => { + const {t, setLanguage} = translatorFactory(dictionary, 'de'); + + let translated = t('username'); + expect(translated).toBe('Benutzername'); + + setLanguage('en'); + + translated = t('username'); + expect(translated).toBe('Username'); +}); + +test('unknown key', () => { + const {t} = translatorFactory(dictionary, 'de'); + + let translated = t('notKnown'); + expect(translated).toBe('!!!notKnown!!!'); +}); + +test('interpolation', () => { + const {t, setLanguage} = translatorFactory(dictionary, 'de'); + + let translated = t('joinRoomname', {roomName: 'Custom-Room'}); + expect(translated).toBe('Custom-Room beitreten'); + + setLanguage('en'); + + translated = t('joinRoomname', {roomName: 'Custom-Room'}); + expect(translated).toBe('Join Custom-Room'); +});