diff --git a/karma.conf.js b/karma.conf.js index 681afdd..de9e608 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -9,14 +9,7 @@ module.exports = config => { files: [ './bin/test.min.js', { - pattern: 'test-assets/json/*.json', - watched: false, - included: false, - served: true, - nocache: false - }, - { - pattern: 'test-assets/l10n/de-DE/*.json', + pattern: 'test-assets/**/*.*', watched: false, included: false, served: true, diff --git a/src/l10n/resource_manager.js b/src/l10n/resource_manager.js new file mode 100644 index 0000000..44b8dee --- /dev/null +++ b/src/l10n/resource_manager.js @@ -0,0 +1,178 @@ +goog.module('clulib.l10n.ResourceManager'); + +const ResourceBundle = goog.require('clulib.l10n.ResourceBundle'); + +/** + * Manages multiple ResourceBundles for multiple locales. + * + * Loads json localization files via pattern `baseUrl/locale/id.json`. + */ +class ResourceManager { + /** + * @param {Array} bundleIds + * @param {Array} locales + * @param {string} baseUrl + */ + constructor (bundleIds, locales, baseUrl) { + /** + * @type {Array} + * @private + */ + this.bundleIds_ = bundleIds; + + /** + * @type {Array} + * @private + */ + this.locales_ = locales; + + /** + * @type {string} + * @private + */ + this.baseUrl_ = baseUrl; + + /** + * @type {?string} + * @private + */ + this.currentLocale_ = null; + + /** + * @type {Map>} + * @private + */ + this.bundles_ = new Map(); + + locales.forEach(locale => { + const localeMap = new Map(); + + bundleIds.forEach(bundleId => { + localeMap.set(bundleId, new ResourceBundle(bundleId, locale, baseUrl)); + }); + + this.bundles_.set(locale, localeMap); + }); + } + + /** + * @returns {?string} + */ + get currentLocale () { + return this.currentLocale_; + } + + /** + * Checks if the locale is valid for this ResourceManager + * + * @param {string} locale + * @returns {boolean} + */ + isLocaleValid (locale) { + return this.locales_.includes(locale); + } + + // eslint-disable-next-line valid-jsdoc + /** + * Changes the current locale, loads the resource files if necessary. + * + * @param {string} locale + * @returns {Promise} + */ + async changeLocale (locale) { + if (!this.isLocaleValid(locale)) + throw new Error(`Locale '${locale}' is not registered with this ResourceManager.`); + + const bundleLoaders = Array.from(this.bundles_.get(locale).values()) + .map(bundle => bundle.load()); + + await Promise.all(bundleLoaders); + + this.currentLocale_ = locale; + } + + /** + * Gets a specific bundle for the current locale. + * + * @param {string} bundleId + * @returns {ResourceBundle} + */ + getBundle (bundleId) { + if (this.currentLocale_ == null) + throw new Error('No locale set!'); + + return this.bundles_.get(/** @type {!string} */ (this.currentLocale_)).get(bundleId); + } + + /** + * Checks if a bundle contains a localization key. + * + * @param {string} bundleId + * @param {string} key + * @returns {boolean} + */ + contains (bundleId, key) { + return this.getBundle(bundleId).contains(key); + } + + /** + * Gets an object from the json localization file of the current bundle. + * + * @param {string} bundleId + * @param {string} key + * @returns {Object} + */ + getObject (bundleId, key) { + return this.getBundle(bundleId).getObject(key); + } + + /** + * Gets an array from the json localization file of the current bundle. + * + * @param {string} bundleId + * @param {string} key + * @returns {Array} + */ + getArray (bundleId, key) { + return this.getBundle(bundleId).getArray(key); + } + + /** + * Gets a boolean from the json localization file of the current bundle. + * + * @param {string} bundleId + * @param {string} key + * @returns {boolean} + */ + getBoolean (bundleId, key) { + return this.getBundle(bundleId).getBoolean(key); + } + + /** + * Gets a number from the json localization file of the current bundle. + * + * @param {string} bundleId + * @param {string} key + * @returns {number} + */ + getNumber (bundleId, key) { + return this.getBundle(bundleId).getNumber(key); + } + + /** + * Gets a string from the json localization file of the current bundle. + * + * Takes an optional `replaceObject` which will replace placeholder keys in the string. + * An object with key `foo` will replace all placeholders `{foo}`. + * + * @param {string} bundleId + * @param {string} key + * @param {Object=} replaceObject + * @returns {string} + */ + getString (bundleId, key, replaceObject = null) { + return this.getBundle(bundleId).getString(key, replaceObject); + } +} + +exports = ResourceManager; diff --git a/test-assets/l10n/de-DE/test.json b/test-assets/l10n/de-DE/test.json index 34af819..e37d2b5 100644 --- a/test-assets/l10n/de-DE/test.json +++ b/test-assets/l10n/de-DE/test.json @@ -1,16 +1,16 @@ { - "string": "Hello, world!", + "string": "Hallo, Welt!", "array": [ 0, 1, 2 ], "object": { - "one": 1, - "two": 2, - "three": 3 + "eins": 1, + "zwei": 2, + "drei": 3 }, "number": 10, "boolean": false, - "string-placeholder": "Hello {to}, my name is {from}." + "string-placeholder": "Hallo {to}, mein name ist {from}." } diff --git a/test-assets/l10n/en-GB/test.json b/test-assets/l10n/en-GB/test.json new file mode 100644 index 0000000..34af819 --- /dev/null +++ b/test-assets/l10n/en-GB/test.json @@ -0,0 +1,16 @@ +{ + "string": "Hello, world!", + "array": [ + 0, + 1, + 2 + ], + "object": { + "one": 1, + "two": 2, + "three": 3 + }, + "number": 10, + "boolean": false, + "string-placeholder": "Hello {to}, my name is {from}." +} diff --git a/test/l10n/resource_bundle_spec.js b/test/l10n/resource_bundle_spec.js index 801e976..be4d727 100644 --- a/test/l10n/resource_bundle_spec.js +++ b/test/l10n/resource_bundle_spec.js @@ -21,11 +21,11 @@ exports = function () { expect(bundle.getArray('array')).toEqual([0, 1, 2]); expect(bundle.getBoolean('boolean')).toBe(false); expect(bundle.getNumber('number')).toBe(10); - expect(bundle.getString('string')).toBe('Hello, world!'); - expect(bundle.getObject('object')).toEqual({'one': 1, 'two': 2, 'three': 3}); + expect(bundle.getString('string')).toBe('Hallo, Welt!'); + expect(bundle.getObject('object')).toEqual({'eins': 1, 'zwei': 2, 'drei': 3}); expect(bundle.getString('string-placeholder', {'from': 'Bob', 'to': 'Max'})) - .toBe('Hello Max, my name is Bob.'); + .toBe('Hallo Max, mein name ist Bob.'); }); }); }; diff --git a/test/l10n/resource_manager_spec.js b/test/l10n/resource_manager_spec.js new file mode 100644 index 0000000..3e1aabb --- /dev/null +++ b/test/l10n/resource_manager_spec.js @@ -0,0 +1,42 @@ +goog.module('test.clulib.l10n.ResourceManager'); + +const ResourceManager = goog.require('clulib.l10n.ResourceManager'); +const env = goog.require('testing.environment'); + +const base = `${env.basePath}/test-assets/l10n`; + +exports = function () { + describe('clulib.l10n.ResourceManager', () => { + it('should load resource files in different languages', async () => { + const manager = new ResourceManager(['test'], ['de-DE', 'en-GB'], base); + + expect(manager.currentLocale).toBe(null); + + await manager.changeLocale('de-DE'); + + expect(manager.contains('test', 'string-placeholder')).toBe(true); + + expect(manager.getArray('test', 'array')).toEqual([0, 1, 2]); + expect(manager.getBoolean('test', 'boolean')).toBe(false); + expect(manager.getNumber('test', 'number')).toBe(10); + expect(manager.getString('test', 'string')).toBe('Hallo, Welt!'); + expect(manager.getObject('test', 'object')).toEqual({'eins': 1, 'zwei': 2, 'drei': 3}); + + expect(manager.getString('test', 'string-placeholder', {'from': 'Bob', 'to': 'Max'})) + .toBe('Hallo Max, mein name ist Bob.'); + + await manager.changeLocale('en-GB'); + + expect(manager.contains('test', 'string-placeholder')).toBe(true); + + expect(manager.getArray('test', 'array')).toEqual([0, 1, 2]); + expect(manager.getBoolean('test', 'boolean')).toBe(false); + expect(manager.getNumber('test', 'number')).toBe(10); + expect(manager.getString('test', 'string')).toBe('Hello, world!'); + expect(manager.getObject('test', 'object')).toEqual({'one': 1, 'two': 2, 'three': 3}); + + expect(manager.getString('test', 'string-placeholder', {'from': 'Bob', 'to': 'Max'})) + .toBe('Hello Max, my name is Bob.'); + }); + }); +}; diff --git a/test/test_main.js b/test/test_main.js index c355e7c..31854b5 100644 --- a/test/test_main.js +++ b/test/test_main.js @@ -7,6 +7,7 @@ const collectionsMain = goog.require('test.clulib.collections'); const domMain = goog.require('test.clulib.dom'); const functionsMain = goog.require('test.clulib.functions'); const resourceBundleMain = goog.require('test.clulib.l10n.ResourceBundle'); +const resourceManagerMain = goog.require('test.clulib.l10n.ResourceManager'); const httpRequestMain = goog.require('test.clulib.net.http_request'); const mathMain = goog.require('test.clulib.math'); @@ -17,5 +18,6 @@ collectionsMain(); domMain(); functionsMain(); resourceBundleMain(); +resourceManagerMain(); httpRequestMain(); mathMain();