diff --git a/core/frontend/services/theme-engine/i18n/i18n.js b/core/frontend/services/theme-engine/i18n/i18n.js index 3d904bf802..e3bd144e9b 100644 --- a/core/frontend/services/theme-engine/i18n/i18n.js +++ b/core/frontend/services/theme-engine/i18n/i18n.js @@ -1,73 +1,312 @@ const errors = require('@tryghost/errors'); -const i18n = require('../../../../shared/i18n'); +const fs = require('fs-extra'); +const path = require('path'); +const MessageFormat = require('intl-messageformat'); +const jp = require('jsonpath'); +const isString = require('lodash/isString'); +const isObject = require('lodash/isObject'); +const isEqual = require('lodash/isEqual'); +const isNil = require('lodash/isNil'); +const merge = require('lodash/merge'); +const get = require('lodash/get'); -class ThemeI18n extends i18n.I18n { +class I18n { /** * @param {objec} [options] - * @param {string} basePath - the base path for the translation directory (e.g. where themes live) + * @param {string} basePath - the base path to the translations directory * @param {string} [locale] - a locale string + * @param {{dot|fulltext}} [stringMode] - which mode our translation keys use + * @param {{object}} [logging] - logging method */ constructor(options = {}) { - super(options); - // We don't care what gets passed in, themes use fulltext mode - this._stringMode = 'fulltext'; + this._basePath = options.basePath || __dirname; + this._locale = options.locale || this.defaultLocale(); + this._stringMode = options.stringMode || 'dot'; + this._logging = options.logging || console; + + this._strings = null; } /** - * Setup i18n support for themes: - * - Load correct language file into memory - * - * @param {object} options - * @param {String} options.activeTheme - name of the currently loaded theme - * @param {String} options.locale - name of the currently loaded locale - * + * BasePath getter & setter used for testing */ - init({activeTheme, locale} = {}) { - // This function is called during theme initialization, and when switching language or theme. - this._locale = locale || this._locale; - this._activetheme = activeTheme || this._activetheme; + set basePath(basePath) { + this._basePath = basePath; + } - super.init(); + /** + * Need to call init after this + */ + get basePath() { + return this._basePath; + } + + /** + * English is our default locale + */ + defaultLocale() { + return 'en'; + } + + supportedLocales() { + return [this.defaultLocale()]; + } + + /** + * Exporting the current locale (e.g. "en") to make it available for other files as well, + * such as core/frontend/helpers/date.js and core/frontend/helpers/lang.js + */ + locale() { + return this._locale; + } + + /** + * Helper method to find and compile the given data context with a proper string resource. + * + * @param {string} translationPath Path within the JSON language file to desired string (ie: "errors.init.jsNotBuilt") + * @param {object} [bindings] + * @returns {string} + */ + t(translationPath, bindings) { + let string; + let msg; + + string = this._findString(translationPath); + + // If the path returns an array (as in the case with anything that has multiple paragraphs such as emails), then + // loop through them and return an array of translated/formatted strings. Otherwise, just return the normal + // translated/formatted string. + if (Array.isArray(string)) { + msg = []; + string.forEach(function (s) { + msg.push(this._formatMessage(s, bindings)); + }); + } else { + msg = this._formatMessage(string, bindings); + } + + return msg; + } + + /** + * Setup i18n support: + * - Load proper language file into memory + */ + init() { + this._strings = this._loadStrings(); + + this._initializeIntl(); + } + + /** + * Attempt to load strings from a file + * + * @param {sting} [locale] + * @returns {object} strings + */ + _loadStrings(locale) { + locale = locale || this.locale(); + + try { + return this._readTranslationsFile(locale); + } catch (err) { + if (err.code === 'ENOENT') { + this._handleMissingFileError(locale, err); + + if (locale !== this.defaultLocale()) { + this._handleFallbackToDefault(); + return this._loadStrings(this.defaultLocale()); + } + } else if (err instanceof SyntaxError) { + this._handleInvalidFileError(locale, err); + } else { + throw err; + } + + // At this point we've done all we can and strings must be an object + return {}; + } + } + + /** + * Do the lookup within the JSON file using jsonpath + * + * @param {String} msgPath + */ + _getCandidateString(msgPath) { + // Our default string mode is "dot" for dot-notation, e.g. $.something.like.this used in the backend + // Both jsonpath's dot-notation and bracket-notation start with '$' E.g.: $.store.book.title or $['store']['book']['title'] + // While bracket-notation allows any Unicode characters in keys (i.e. for themes / fulltext mode) E.g. $['Read more'] + // dot-notation allows only word characters in keys for backend messages (that is \w or [A-Za-z0-9_] in RegExp) + let jsonPath = `$.${msgPath}`; + let fallback = null; + + if (this._stringMode === 'fulltext') { + jsonPath = jp.stringify(['$', msgPath]); + // In fulltext mode we can use the passed string as a fallback + fallback = msgPath; + } + + try { + return jp.value(this._strings, jsonPath) || fallback; + } catch (err) { + this._handleInvalidKeyError(msgPath, err); + } + } + + /** + * Parse JSON file for matching locale, returns string giving path. + * + * @param {string} msgPath Path with in the JSON language file to desired string (ie: "errors.init.jsNotBuilt") + * @returns {string} + */ + _findString(msgPath, opts) { + const options = merge({log: true}, opts || {}); + let candidateString; + let matchingString; + + // no path? no string + if (msgPath.length === 0 || !isString(msgPath)) { + this._handleEmptyKeyError(); + return ''; + } + + // If not in memory, load translations for core + if (isNil(this._strings)) { + this._handleUninitialisedError(msgPath); + } + + candidateString = this._getCandidateString(msgPath); + + matchingString = candidateString || {}; + + if (isObject(matchingString) || isEqual(matchingString, {})) { + if (options.log) { + this._handleMissingKeyError(msgPath); + } + + matchingString = this._fallbackError(); + } + + return matchingString; } _translationFileDirs() { - return [this.basePath, this._activetheme, 'locales']; + return [this.basePath]; + } + + // If we are passed a locale, use that, else use this.locale + _translationFileName(locale) { + return `${locale || this.locale()}.json`; + } + + /** + * Read the translations file + * Error handling to be done by consumer + * + * @param {string} locale + */ + _readTranslationsFile(locale) { + const filePath = path.join(...this._translationFileDirs(), this._translationFileName(locale)); + const content = fs.readFileSync(filePath); + return JSON.parse(content); + } + + /** + * Format the string using the correct locale and applying any bindings + * @param {String} string + * @param {Object} bindings + */ + _formatMessage(string, bindings) { + let currentLocale = this.locale(); + let msg = new MessageFormat(string, currentLocale); + + try { + msg = msg.format(bindings); + } catch (err) { + this._handleFormatError(err); + + // fallback + msg = new MessageFormat(this._fallbackError(), currentLocale); + msg = msg.format(); + } + + return msg; + } + + /** + * [Private] Setup i18n support: + * - Polyfill node.js if it does not have Intl support or support for a particular locale + */ + _initializeIntl() { + let hasBuiltInLocaleData; + let IntlPolyfill; + + if (global.Intl) { + // Determine if the built-in `Intl` has the locale data we need. + hasBuiltInLocaleData = this.supportedLocales().every(function (locale) { + return Intl.NumberFormat.supportedLocalesOf(locale)[0] === locale && + Intl.DateTimeFormat.supportedLocalesOf(locale)[0] === locale; + }); + if (!hasBuiltInLocaleData) { + // `Intl` exists, but it doesn't have the data we need, so load the + // polyfill and replace the constructors with need with the polyfill's. + IntlPolyfill = require('intl'); + Intl.NumberFormat = IntlPolyfill.NumberFormat; + Intl.DateTimeFormat = IntlPolyfill.DateTimeFormat; + } + } else { + // No `Intl`, so use and load the polyfill. + global.Intl = require('intl'); + } } _handleUninitialisedError(key) { - throw new errors.IncorrectUsageError({message: `Theme translation was used before it was initialised with key ${key}`}); + this._logging.warn(`i18n was used before it was initialised with key ${key}`); + this.init(); + } + + _handleFormatError(err) { + this._logging.error(err.message); } _handleFallbackToDefault() { - this._logging.warn(`Theme translations falling back to locales/${this.defaultLocale()}.json.`); + this._logging.warn(`i18n is falling back to ${this.defaultLocale()}.json.`); } _handleMissingFileError(locale) { - if (locale !== this.defaultLocale()) { - this._logging.warn(`Theme translations file locales/${locale}.json not found.`); - } + this._logging.warn(`i18n was unable to find ${locale}.json.`); } _handleInvalidFileError(locale, err) { this._logging.error(new errors.IncorrectUsageError({ err, - message: `Theme translations unable to parse locales/${locale}.json. Please check that it is valid JSON.` + message: `i18n was unable to parse ${locale}.json. Please check that it is valid JSON.` })); } _handleEmptyKeyError() { - this._logging.warn('Theme translations {{t}} helper called without a translation key.'); + this._logging.warn('i18n.t() was called without a key'); } - _handleMissingKeyError() { - // This case cannot be reached in themes as we use the key as the fallback + _handleMissingKeyError(key) { + this._logging.error(new errors.IncorrectUsageError({ + message: `i18n.t() was called with a key that could not be found: ${key}` + })); } _handleInvalidKeyError(key, err) { throw new errors.IncorrectUsageError({ err, - message: `Theme translations {{t}} helper called with an invalid translation key: ${key}` + message: `i18n.t() called with an invalid key: ${key}` }); } + + /** + * A really basic error for if everything goes wrong + */ + _fallbackError() { + return get(this._strings, 'errors.errors.anErrorOccurred', 'An error occurred'); + } } -module.exports = ThemeI18n; +module.exports = I18n; diff --git a/core/frontend/services/theme-engine/i18n/index.js b/core/frontend/services/theme-engine/i18n/index.js index f2e767fff4..d3efd7330c 100644 --- a/core/frontend/services/theme-engine/i18n/index.js +++ b/core/frontend/services/theme-engine/i18n/index.js @@ -1,7 +1,7 @@ const config = require('../../../../shared/config'); const logging = require('@tryghost/logging'); -const ThemeI18n = require('./i18n'); +const ThemeI18n = require('./theme-i18n'); module.exports = new ThemeI18n({logging, basePath: config.getContentPath('themes')}); module.exports.ThemeI18n = ThemeI18n; diff --git a/core/frontend/services/theme-engine/i18n/theme-i18n.js b/core/frontend/services/theme-engine/i18n/theme-i18n.js new file mode 100644 index 0000000000..8bff052e5c --- /dev/null +++ b/core/frontend/services/theme-engine/i18n/theme-i18n.js @@ -0,0 +1,73 @@ +const errors = require('@tryghost/errors'); +const I18n = require('./i18n'); + +class ThemeI18n extends I18n { + /** + * @param {objec} [options] + * @param {string} basePath - the base path for the translation directory (e.g. where themes live) + * @param {string} [locale] - a locale string + */ + constructor(options = {}) { + super(options); + // We don't care what gets passed in, themes use fulltext mode + this._stringMode = 'fulltext'; + } + + /** + * Setup i18n support for themes: + * - Load correct language file into memory + * + * @param {object} options + * @param {String} options.activeTheme - name of the currently loaded theme + * @param {String} options.locale - name of the currently loaded locale + * + */ + init({activeTheme, locale} = {}) { + // This function is called during theme initialization, and when switching language or theme. + this._locale = locale || this._locale; + this._activetheme = activeTheme || this._activetheme; + + super.init(); + } + + _translationFileDirs() { + return [this.basePath, this._activetheme, 'locales']; + } + + _handleUninitialisedError(key) { + throw new errors.IncorrectUsageError({message: `Theme translation was used before it was initialised with key ${key}`}); + } + + _handleFallbackToDefault() { + this._logging.warn(`Theme translations falling back to locales/${this.defaultLocale()}.json.`); + } + + _handleMissingFileError(locale) { + if (locale !== this.defaultLocale()) { + this._logging.warn(`Theme translations file locales/${locale}.json not found.`); + } + } + _handleInvalidFileError(locale, err) { + this._logging.error(new errors.IncorrectUsageError({ + err, + message: `Theme translations unable to parse locales/${locale}.json. Please check that it is valid JSON.` + })); + } + + _handleEmptyKeyError() { + this._logging.warn('Theme translations {{t}} helper called without a translation key.'); + } + + _handleMissingKeyError() { + // This case cannot be reached in themes as we use the key as the fallback + } + + _handleInvalidKeyError(key, err) { + throw new errors.IncorrectUsageError({ + err, + message: `Theme translations {{t}} helper called with an invalid translation key: ${key}` + }); + } +} + +module.exports = ThemeI18n; diff --git a/core/shared/i18n/i18n.js b/core/shared/i18n/i18n.js deleted file mode 100644 index e3bd144e9b..0000000000 --- a/core/shared/i18n/i18n.js +++ /dev/null @@ -1,312 +0,0 @@ -const errors = require('@tryghost/errors'); -const fs = require('fs-extra'); -const path = require('path'); -const MessageFormat = require('intl-messageformat'); -const jp = require('jsonpath'); -const isString = require('lodash/isString'); -const isObject = require('lodash/isObject'); -const isEqual = require('lodash/isEqual'); -const isNil = require('lodash/isNil'); -const merge = require('lodash/merge'); -const get = require('lodash/get'); - -class I18n { - /** - * @param {objec} [options] - * @param {string} basePath - the base path to the translations directory - * @param {string} [locale] - a locale string - * @param {{dot|fulltext}} [stringMode] - which mode our translation keys use - * @param {{object}} [logging] - logging method - */ - constructor(options = {}) { - this._basePath = options.basePath || __dirname; - this._locale = options.locale || this.defaultLocale(); - this._stringMode = options.stringMode || 'dot'; - this._logging = options.logging || console; - - this._strings = null; - } - - /** - * BasePath getter & setter used for testing - */ - set basePath(basePath) { - this._basePath = basePath; - } - - /** - * Need to call init after this - */ - get basePath() { - return this._basePath; - } - - /** - * English is our default locale - */ - defaultLocale() { - return 'en'; - } - - supportedLocales() { - return [this.defaultLocale()]; - } - - /** - * Exporting the current locale (e.g. "en") to make it available for other files as well, - * such as core/frontend/helpers/date.js and core/frontend/helpers/lang.js - */ - locale() { - return this._locale; - } - - /** - * Helper method to find and compile the given data context with a proper string resource. - * - * @param {string} translationPath Path within the JSON language file to desired string (ie: "errors.init.jsNotBuilt") - * @param {object} [bindings] - * @returns {string} - */ - t(translationPath, bindings) { - let string; - let msg; - - string = this._findString(translationPath); - - // If the path returns an array (as in the case with anything that has multiple paragraphs such as emails), then - // loop through them and return an array of translated/formatted strings. Otherwise, just return the normal - // translated/formatted string. - if (Array.isArray(string)) { - msg = []; - string.forEach(function (s) { - msg.push(this._formatMessage(s, bindings)); - }); - } else { - msg = this._formatMessage(string, bindings); - } - - return msg; - } - - /** - * Setup i18n support: - * - Load proper language file into memory - */ - init() { - this._strings = this._loadStrings(); - - this._initializeIntl(); - } - - /** - * Attempt to load strings from a file - * - * @param {sting} [locale] - * @returns {object} strings - */ - _loadStrings(locale) { - locale = locale || this.locale(); - - try { - return this._readTranslationsFile(locale); - } catch (err) { - if (err.code === 'ENOENT') { - this._handleMissingFileError(locale, err); - - if (locale !== this.defaultLocale()) { - this._handleFallbackToDefault(); - return this._loadStrings(this.defaultLocale()); - } - } else if (err instanceof SyntaxError) { - this._handleInvalidFileError(locale, err); - } else { - throw err; - } - - // At this point we've done all we can and strings must be an object - return {}; - } - } - - /** - * Do the lookup within the JSON file using jsonpath - * - * @param {String} msgPath - */ - _getCandidateString(msgPath) { - // Our default string mode is "dot" for dot-notation, e.g. $.something.like.this used in the backend - // Both jsonpath's dot-notation and bracket-notation start with '$' E.g.: $.store.book.title or $['store']['book']['title'] - // While bracket-notation allows any Unicode characters in keys (i.e. for themes / fulltext mode) E.g. $['Read more'] - // dot-notation allows only word characters in keys for backend messages (that is \w or [A-Za-z0-9_] in RegExp) - let jsonPath = `$.${msgPath}`; - let fallback = null; - - if (this._stringMode === 'fulltext') { - jsonPath = jp.stringify(['$', msgPath]); - // In fulltext mode we can use the passed string as a fallback - fallback = msgPath; - } - - try { - return jp.value(this._strings, jsonPath) || fallback; - } catch (err) { - this._handleInvalidKeyError(msgPath, err); - } - } - - /** - * Parse JSON file for matching locale, returns string giving path. - * - * @param {string} msgPath Path with in the JSON language file to desired string (ie: "errors.init.jsNotBuilt") - * @returns {string} - */ - _findString(msgPath, opts) { - const options = merge({log: true}, opts || {}); - let candidateString; - let matchingString; - - // no path? no string - if (msgPath.length === 0 || !isString(msgPath)) { - this._handleEmptyKeyError(); - return ''; - } - - // If not in memory, load translations for core - if (isNil(this._strings)) { - this._handleUninitialisedError(msgPath); - } - - candidateString = this._getCandidateString(msgPath); - - matchingString = candidateString || {}; - - if (isObject(matchingString) || isEqual(matchingString, {})) { - if (options.log) { - this._handleMissingKeyError(msgPath); - } - - matchingString = this._fallbackError(); - } - - return matchingString; - } - - _translationFileDirs() { - return [this.basePath]; - } - - // If we are passed a locale, use that, else use this.locale - _translationFileName(locale) { - return `${locale || this.locale()}.json`; - } - - /** - * Read the translations file - * Error handling to be done by consumer - * - * @param {string} locale - */ - _readTranslationsFile(locale) { - const filePath = path.join(...this._translationFileDirs(), this._translationFileName(locale)); - const content = fs.readFileSync(filePath); - return JSON.parse(content); - } - - /** - * Format the string using the correct locale and applying any bindings - * @param {String} string - * @param {Object} bindings - */ - _formatMessage(string, bindings) { - let currentLocale = this.locale(); - let msg = new MessageFormat(string, currentLocale); - - try { - msg = msg.format(bindings); - } catch (err) { - this._handleFormatError(err); - - // fallback - msg = new MessageFormat(this._fallbackError(), currentLocale); - msg = msg.format(); - } - - return msg; - } - - /** - * [Private] Setup i18n support: - * - Polyfill node.js if it does not have Intl support or support for a particular locale - */ - _initializeIntl() { - let hasBuiltInLocaleData; - let IntlPolyfill; - - if (global.Intl) { - // Determine if the built-in `Intl` has the locale data we need. - hasBuiltInLocaleData = this.supportedLocales().every(function (locale) { - return Intl.NumberFormat.supportedLocalesOf(locale)[0] === locale && - Intl.DateTimeFormat.supportedLocalesOf(locale)[0] === locale; - }); - if (!hasBuiltInLocaleData) { - // `Intl` exists, but it doesn't have the data we need, so load the - // polyfill and replace the constructors with need with the polyfill's. - IntlPolyfill = require('intl'); - Intl.NumberFormat = IntlPolyfill.NumberFormat; - Intl.DateTimeFormat = IntlPolyfill.DateTimeFormat; - } - } else { - // No `Intl`, so use and load the polyfill. - global.Intl = require('intl'); - } - } - - _handleUninitialisedError(key) { - this._logging.warn(`i18n was used before it was initialised with key ${key}`); - this.init(); - } - - _handleFormatError(err) { - this._logging.error(err.message); - } - - _handleFallbackToDefault() { - this._logging.warn(`i18n is falling back to ${this.defaultLocale()}.json.`); - } - - _handleMissingFileError(locale) { - this._logging.warn(`i18n was unable to find ${locale}.json.`); - } - _handleInvalidFileError(locale, err) { - this._logging.error(new errors.IncorrectUsageError({ - err, - message: `i18n was unable to parse ${locale}.json. Please check that it is valid JSON.` - })); - } - - _handleEmptyKeyError() { - this._logging.warn('i18n.t() was called without a key'); - } - - _handleMissingKeyError(key) { - this._logging.error(new errors.IncorrectUsageError({ - message: `i18n.t() was called with a key that could not be found: ${key}` - })); - } - - _handleInvalidKeyError(key, err) { - throw new errors.IncorrectUsageError({ - err, - message: `i18n.t() called with an invalid key: ${key}` - }); - } - - /** - * A really basic error for if everything goes wrong - */ - _fallbackError() { - return get(this._strings, 'errors.errors.anErrorOccurred', 'An error occurred'); - } -} - -module.exports = I18n; diff --git a/core/shared/i18n/index.js b/core/shared/i18n/index.js deleted file mode 100644 index 837a97e2fe..0000000000 --- a/core/shared/i18n/index.js +++ /dev/null @@ -1,6 +0,0 @@ -const path = require('path'); -const logging = require('@tryghost/logging'); -const I18n = require('./i18n'); - -module.exports = new I18n({logging, basePath: path.join(__dirname, 'translations')}); -module.exports.I18n = I18n; diff --git a/test/unit/frontend/services/theme-engine/i18n.test.js b/test/unit/frontend/services/theme-engine/i18n.test.js index 58b3af62fe..c7ea171ebb 100644 --- a/test/unit/frontend/services/theme-engine/i18n.test.js +++ b/test/unit/frontend/services/theme-engine/i18n.test.js @@ -1,10 +1,90 @@ const should = require('should'); +const sinon = require('sinon'); -const ThemeI18n = require('../../../../../core/frontend/services/theme-engine/i18n').ThemeI18n; +const I18n = require('../../../../../core/frontend/services/theme-engine/i18n/i18n'); -describe('ThemeI18n Class Behaviour', function () { +const logging = { + warn: sinon.stub(), + error: sinon.stub() +}; + +describe('I18n Class Behaviour', function () { it('defaults to en', function () { - const i18n = new ThemeI18n(); + const i18n = new I18n({logging}); i18n.locale().should.eql('en'); }); + + it('can have a different locale set', function () { + const i18n = new I18n({locale: 'fr', logging}); + i18n.locale().should.eql('fr'); + }); + + describe('file loading behaviour', function () { + it('will fallback to en file correctly without changing locale', function () { + const i18n = new I18n({locale: 'fr', logging}); + + let fileSpy = sinon.spy(i18n, '_readTranslationsFile'); + + i18n.locale().should.eql('fr'); + i18n.init(); + + i18n.locale().should.eql('fr'); + fileSpy.calledTwice.should.be.true(); + fileSpy.secondCall.args[0].should.eql('en'); + }); + }); + + describe('translation key dot notation (default behaviour)', function () { + const fakeStrings = { + test: {string: {path: 'I am correct'}} + }; + let i18n; + + beforeEach(function initBasicI18n() { + i18n = new I18n({logging}); + sinon.stub(i18n, '_loadStrings').returns(fakeStrings); + i18n.init(); + }); + + it('correctly loads strings', function () { + i18n._strings.should.eql(fakeStrings); + }); + + it('correctly uses dot notation', function () { + i18n.t('test.string.path').should.eql('I am correct'); + }); + + it('uses key fallback correctly', function () { + i18n.t('unknown.string').should.eql('An error occurred'); + }); + + it('errors for invalid strings', function () { + should(function () { + i18n.t('unknown string'); + }).throw('i18n.t() called with an invalid key: unknown string'); + }); + }); + + describe('translation key fulltext notation (theme behaviour)', function () { + const fakeStrings = {'Full text': 'I am correct'}; + let i18n; + + beforeEach(function initFulltextI18n() { + i18n = new I18n({stringMode: 'fulltext', logging}); + sinon.stub(i18n, '_loadStrings').returns(fakeStrings); + i18n.init(); + }); + + it('correctly loads strings', function () { + i18n._strings.should.eql(fakeStrings); + }); + + it('correctly uses fulltext with bracket notation', function () { + i18n.t('Full text').should.eql('I am correct'); + }); + + it('uses key fallback correctly', function () { + i18n.t('unknown string').should.eql('unknown string'); + }); + }); }); diff --git a/test/unit/frontend/services/theme-engine/theme-i18n.test.js b/test/unit/frontend/services/theme-engine/theme-i18n.test.js new file mode 100644 index 0000000000..58b3af62fe --- /dev/null +++ b/test/unit/frontend/services/theme-engine/theme-i18n.test.js @@ -0,0 +1,10 @@ +const should = require('should'); + +const ThemeI18n = require('../../../../../core/frontend/services/theme-engine/i18n').ThemeI18n; + +describe('ThemeI18n Class Behaviour', function () { + it('defaults to en', function () { + const i18n = new ThemeI18n(); + i18n.locale().should.eql('en'); + }); +}); diff --git a/test/unit/shared/i18n.test.js b/test/unit/shared/i18n.test.js deleted file mode 100644 index bc87b57208..0000000000 --- a/test/unit/shared/i18n.test.js +++ /dev/null @@ -1,90 +0,0 @@ -const should = require('should'); -const sinon = require('sinon'); - -const I18n = require('../../../core/shared/i18n').I18n; - -const logging = { - warn: sinon.stub(), - error: sinon.stub() -}; - -describe('I18n Class Behaviour', function () { - it('defaults to en', function () { - const i18n = new I18n({logging}); - i18n.locale().should.eql('en'); - }); - - it('can have a different locale set', function () { - const i18n = new I18n({locale: 'fr', logging}); - i18n.locale().should.eql('fr'); - }); - - describe('file loading behaviour', function () { - it('will fallback to en file correctly without changing locale', function () { - const i18n = new I18n({locale: 'fr', logging}); - - let fileSpy = sinon.spy(i18n, '_readTranslationsFile'); - - i18n.locale().should.eql('fr'); - i18n.init(); - - i18n.locale().should.eql('fr'); - fileSpy.calledTwice.should.be.true(); - fileSpy.secondCall.args[0].should.eql('en'); - }); - }); - - describe('translation key dot notation (default behaviour)', function () { - const fakeStrings = { - test: {string: {path: 'I am correct'}} - }; - let i18n; - - beforeEach(function initBasicI18n() { - i18n = new I18n({logging}); - sinon.stub(i18n, '_loadStrings').returns(fakeStrings); - i18n.init(); - }); - - it('correctly loads strings', function () { - i18n._strings.should.eql(fakeStrings); - }); - - it('correctly uses dot notation', function () { - i18n.t('test.string.path').should.eql('I am correct'); - }); - - it('uses key fallback correctly', function () { - i18n.t('unknown.string').should.eql('An error occurred'); - }); - - it('errors for invalid strings', function () { - should(function () { - i18n.t('unknown string'); - }).throw('i18n.t() called with an invalid key: unknown string'); - }); - }); - - describe('translation key fulltext notation (theme behaviour)', function () { - const fakeStrings = {'Full text': 'I am correct'}; - let i18n; - - beforeEach(function initFulltextI18n() { - i18n = new I18n({stringMode: 'fulltext', logging}); - sinon.stub(i18n, '_loadStrings').returns(fakeStrings); - i18n.init(); - }); - - it('correctly loads strings', function () { - i18n._strings.should.eql(fakeStrings); - }); - - it('correctly uses fulltext with bracket notation', function () { - i18n.t('Full text').should.eql('I am correct'); - }); - - it('uses key fallback correctly', function () { - i18n.t('unknown string').should.eql('unknown string'); - }); - }); -});