diff --git a/src/locale.js b/src/locale.js index 68cec84..9ece6fd 100644 --- a/src/locale.js +++ b/src/locale.js @@ -9,7 +9,7 @@ /* globals console */ -import { translate } from './translation-service'; +import { _translate } from './translation-service'; const RTL_LANGUAGE_CODES = [ 'ar', 'fa', 'he', 'ku', 'ug' ]; @@ -76,25 +76,48 @@ export default class Locale { this.contentLanguageDirection = getLanguageDirection( this.contentLanguage ); /** - * Translates the given string to the {@link #uiLanguage}. This method is also available in - * {@link module:core/editor/editor~Editor#t} and {@link module:ui/view~View#t}. + * Translates the given message to the {@link #uiLanguage}. This method is also available in + * {@link module:core/editor/editor~Editor#t Editor} and {@link module:ui/view~View#t View}. * - * The strings may contain placeholders (`%`) for values which are passed as the second argument. - * `` is the index in the `values` array. + * This method's context is statically bound to the `Locale` instance and **always should be called as a function**: * - * editor.t( 'Created file "%0" in %1ms.', [ fileName, timeTaken ] ); + * const t = locale.t; + * t( 'Label' ); * - * This method's context is statically bound to Locale instance, - * so it can be called as a function: + * The message can be either a string or an object implementing the {@link module:utils/translation-service~Message} interface. * - * const t = this.t; - * t( 'Label' ); + * The message may contain placeholders (`%`) for value(s) that are passed as a `values` parameter. + * For an array of values the `%` will be changed to an element of that array at the given index. + * For a single value passed as the second argument, only the `%0` placeholders will be changed to the provided value. + * + * t( 'Created file "%0" in %1ms.', [ fileName, timeTaken ] ); + * t( 'Created file "%0", fileName ); + * + * The message supports plural forms. To specify the plural form, use the `plural` property. Singular or plural form + * will be chosen depending on the first value from the passed `values`. The value of the `plural` property is used + * as a default plural translation when the translation for the target language is missing. + * + * t( { string: 'Add a space', plural: 'Add %0 spaces' }, 1 ); // 'Add a space' for the English language. + * t( { string: 'Add a space', plural: 'Add %0 spaces' }, 5 ); // 'Add 5 spaces' for the English language. + * t( { string: '%1 a space', plural: '%1 %0 spaces' }, [ 2, 'Add' ] ); // 'Add 2 spaces' for the English language. + * + * t( { string: 'Add a space', plural: 'Add %0 spaces' }, 1 ); // 'Dodaj spację' for the Polish language. + * t( { string: 'Add a space', plural: 'Add %0 spaces' }, 5 ); // 'Dodaj 5 spacji' for the Polish language. + * t( { string: '%1 a space', plural: '%1 %0 spaces' }, [ 2, 'Add' ] ); // 'Dodaj 2 spacje' for the Polish language. + * + * * The message should provide an id using the `id` property when the message strings are not unique and their + * translations should be different. + * + * translate( 'en', { string: 'image', id: 'ADD_IMAGE' } ); + * translate( 'en', { string: 'image', id: 'AN_IMAGE' } ); * * @method #t - * @param {String} str The string to translate. - * @param {String[]} [values] Values that should be used to interpolate the string. + * @param {String|module:utils/translation-service~Message} message A message that will be localized (translated). + * @param {String|Number|Array.} [values] A value or an array of values that will fill message placeholders. + * For messages supporting plural forms the first value will determine the plural form. + * @returns {String} */ - this.t = ( ...args ) => this._t( ...args ); + this.t = ( message, values ) => this._t( message, values ); } /** @@ -122,23 +145,38 @@ export default class Locale { } /** - * Base for the {@link #t} method. + * An unbound version of the {@link #t} method. * * @private + * @param {String|module:utils/translation-service~Message} message + * @param {Number|String|Array.} [values] + * @returns {String} */ - _t( str, values ) { - let translatedString = translate( this.uiLanguage, str ); + _t( message, values = [] ) { + if ( !Array.isArray( values ) ) { + values = [ values ]; + } - if ( values ) { - translatedString = translatedString.replace( /%(\d+)/g, ( match, index ) => { - return ( index < values.length ) ? values[ index ] : match; - } ); + if ( typeof message === 'string' ) { + message = { string: message }; } - return translatedString; + const hasPluralForm = !!message.plural; + const quantity = hasPluralForm ? values[ 0 ] : 1; + + const translatedString = _translate( this.uiLanguage, message, quantity ); + + return interpolateString( translatedString, values ); } } +// Fills the `%0, %1, ...` string placeholders with values. +function interpolateString( string, values ) { + return string.replace( /%(\d+)/g, ( match, index ) => { + return ( index < values.length ) ? values[ index ] : match; + } ); +} + // Helps determine whether a language is LTR or RTL. // // @param {String} language The ISO 639-1 language code. diff --git a/src/translation-service.js b/src/translation-service.js index c2855f1..eea910a 100644 --- a/src/translation-service.js +++ b/src/translation-service.js @@ -9,62 +9,144 @@ * @module utils/translation-service */ +import CKEditorError from './ckeditorerror'; + /* istanbul ignore else */ if ( !window.CKEDITOR_TRANSLATIONS ) { window.CKEDITOR_TRANSLATIONS = {}; } /** - * Adds translations to existing ones. - * These translations will later be available for the {@link module:utils/translation-service~translate `translate()`} function. + * Adds translations to existing ones or overrides the existing translations. These translations will later + * be available for the {@link module:utils/locale~Locale#t `t()`} function. + * + * The `translations` is an object which consists of a `messageId: translation` pairs. Note that the message id can be + * either constructed from the message string or from the message id if it was passed + * (this happens rarely and mostly for short messages or messages with placeholders). + * Since the editor displays only the message string, the message id can be found either in the source code or in the + * built translations for another language. * * add( 'pl', { - * 'OK': 'OK', - * 'Cancel [context: reject]': 'Anuluj' + * 'Cancel': 'Anuluj', + * 'IMAGE': 'obraz', // Note that the `IMAGE` comes from the message id, while the string can be `image`. * } ); * + * If the message is supposed to support various plural forms, make sure to provide an array with the singular form and all plural forms: + * + * add( 'pl', { + * 'Add space': [ 'Dodaj spację', 'Dodaj %0 spacje', 'Dodaj %0 spacji' ] + * } ); + * + * You should also specify the third argument (the `getPluralForm` function) that will be used to determine the plural form if no + * language file was loaded for that language. All language files coming from CKEditor 5 sources will have this option set, so + * these plural form rules will be reused by other translations added to the registered languages. The `getPluralForm` function + * can return either a boolean or a number. + * + * add( 'en', { + * // ... Translations. + * }, n => n !== 1 ); + * add( 'pl', { + * // ... Translations. + * }, n => n == 1 ? 0 : n % 10 >= 2 && n % 10 <= 4 && ( n % 100 < 10 || n % 100 >= 20 ) ? 1 : 2 ); + * + * All translations extend the global `window.CKEDITOR_TRANSLATIONS` object. An example of this object can be found below: + * + * { + * pl: { + * dictionary: { + * 'Cancel': 'Anuluj', + * 'Add space': [ 'Dodaj spację', 'Dodaj %0 spacje', 'Dodaj %0 spacji' ] + * }, + * // A function that returns the plural form index. + * getPluralForm: n => n !==1 + * } + * // other languages. + * } + * * If you cannot import this function from this module (e.g. because you use a CKEditor 5 build), then you can * still add translations by extending the global `window.CKEDITOR_TRANSLATIONS` object by using a function like * the one below: * - * function addTranslations( language, translations ) { + * function addTranslations( language, translations, getPluralForm ) { * if ( !window.CKEDITOR_TRANSLATIONS ) { * window.CKEDITOR_TRANSLATIONS = {}; * } + + * if ( !window.CKEDITOR_TRANSLATIONS[ language ] ) { + * window.CKEDITOR_TRANSLATIONS[ language ] = {}; + * } * - * const dictionary = window.CKEDITOR_TRANSLATIONS[ language ] || ( window.CKEDITOR_TRANSLATIONS[ language ] = {} ); + * const languageTranslations = window.CKEDITOR_TRANSLATIONS[ language ]; + * + * languageTranslations.dictionary = languageTranslations.dictionary || {}; + * languageTranslations.getPluralForm = getPluralForm || languageTranslations.getPluralForm; * * // Extend the dictionary for the given language. - * Object.assign( dictionary, translations ); + * Object.assign( languageTranslations.dictionary, translations ); * } * * @param {String} language Target language. - * @param {Object.} translations Translations which will be added to the dictionary. + * @param {Object.} translations An object with translations which will be added to the dictionary. + * For each message id the value should be either a translation or an array of translations if the message + * should support plural forms. + * @param {Function} getPluralForm A function that returns the plural form index (a number). */ -export function add( language, translations ) { - const dictionary = window.CKEDITOR_TRANSLATIONS[ language ] || ( window.CKEDITOR_TRANSLATIONS[ language ] = {} ); +export function add( language, translations, getPluralForm ) { + if ( !window.CKEDITOR_TRANSLATIONS[ language ] ) { + window.CKEDITOR_TRANSLATIONS[ language ] = {}; + } + + const languageTranslations = window.CKEDITOR_TRANSLATIONS[ language ]; + + languageTranslations.dictionary = languageTranslations.dictionary || {}; + languageTranslations.getPluralForm = getPluralForm || languageTranslations.getPluralForm; - Object.assign( dictionary, translations ); + Object.assign( languageTranslations.dictionary, translations ); } /** - * Translates string if the translation of the string was previously added to the dictionary. - * See {@link module:utils/translation-service Translation Service}. - * This happens in a multi-language mode were translation modules are created by the bundler. + * **Note:** this method is internal, use {@link module:utils/locale~Locale#t the `t()` function} instead to translate + * editor UI parts. + * + * This function is responsible for translating messages to the specified language. It uses perviously added translations + * by {@link module:utils/translation-service~add} (a translations dictionary and and the `getPluralForm` function + * to provide accurate translations of plural forms). * * When no translation is defined in the dictionary or the dictionary doesn't exist this function returns - * the original string without the `'[context: ]'` (happens in development and single-language modes). + * the original message string or message plural depending on the number of elements. + * + * translate( 'pl', { string: 'Cancel' } ); // 'Cancel' + * + * The third optional argument is the number of elements, based on which the single form or one of plural forms + * should be picked when the message is supposed to support various plural forms. * - * In a single-language mode (when values passed to `t()` were replaced with target language strings) the dictionary - * is left empty, so this function will return the original strings always. + * translate( 'en', { string: 'Add a space', plural: 'Add %0 spaces' }, 1 ); // 'Add a space' + * translate( 'en', { string: 'Add a space', plural: 'Add %0 spaces' }, 3 ); // 'Add %0 spaces' * - * translate( 'pl', 'Cancel [context: reject]' ); + * The message should provide an id using the `id` property when the message strings are not unique and their + * translations should be different. * + * translate( 'en', { string: 'image', id: 'ADD_IMAGE' } ); + * translate( 'en', { string: 'image', id: 'AN_IMAGE' } ); + * + * @protected * @param {String} language Target language. - * @param {String} translationKey String that will be translated. + * @param {module:utils/translation-service~Message|String} message A message that will be translated. + * @param {Number} [quantity] A number of elements for which a plural form should be picked from the target language dictionary. * @returns {String} Translated sentence. */ -export function translate( language, translationKey ) { +export function _translate( language, message, quantity = 1 ) { + if ( typeof quantity !== 'number' ) { + /** + * The incorrect value has been passed to the `translation` function. This probably was caused + * by the incorrect message interpolation of a plural form. Note that for messages supporting plural forms + * the second argument of the `t()` function should always be a number or an array with number as the first element. + * + * @error translation-service-quantity-not-a-number + */ + throw new CKEditorError( 'translation-service-quantity-not-a-number: Expecting `quantity` to be a number.', null, { quantity } ); + } + const numberOfLanguages = getNumberOfLanguages(); if ( numberOfLanguages === 1 ) { @@ -73,14 +155,28 @@ export function translate( language, translationKey ) { language = Object.keys( window.CKEDITOR_TRANSLATIONS )[ 0 ]; } - if ( numberOfLanguages === 0 || !hasTranslation( language, translationKey ) ) { - return translationKey.replace( / \[context: [^\]]+\]$/, '' ); + const messageId = message.id || message.string; + + if ( numberOfLanguages === 0 || !hasTranslation( language, messageId ) ) { + if ( quantity !== 1 ) { + // Return the default plural form that was passed in the `message.plural` parameter. + return message.plural; + } + + return message.string; } - const dictionary = window.CKEDITOR_TRANSLATIONS[ language ]; + const dictionary = window.CKEDITOR_TRANSLATIONS[ language ].dictionary; + const getPluralForm = window.CKEDITOR_TRANSLATIONS[ language ].getPluralForm || ( n => n === 1 ? 0 : 1 ); - // In case of missing translations we still need to cut off the `[context: ]` parts. - return dictionary[ translationKey ].replace( / \[context: [^\]]+\]$/, '' ); + if ( typeof dictionary[ messageId ] === 'string' ) { + return dictionary[ messageId ]; + } + + const pluralFormIndex = Number( getPluralForm( quantity ) ); + + // Note: The `translate` function is not responsible for replacing `%0, %1, ...` with values. + return dictionary[ messageId ][ pluralFormIndex ]; } /** @@ -93,13 +189,28 @@ export function _clear() { } // Checks whether the dictionary exists and translation in that dictionary exists. -function hasTranslation( language, translationKey ) { +function hasTranslation( language, messageId ) { return ( - ( language in window.CKEDITOR_TRANSLATIONS ) && - ( translationKey in window.CKEDITOR_TRANSLATIONS[ language ] ) + !!window.CKEDITOR_TRANSLATIONS[ language ] && + !!window.CKEDITOR_TRANSLATIONS[ language ].dictionary[ messageId ] ); } function getNumberOfLanguages() { return Object.keys( window.CKEDITOR_TRANSLATIONS ).length; } + +/** + * The internationalization message interface. A message that implements this interface can be passed to the `t()` function + * to be translated to the target ui language. + * + * @typedef {Object} module:utils/translation-service~Message + * + * @property {String} string The message string to translate. Acts as a default translation if the translation for given language + * is not defined. When the message is supposed to support plural forms then the string should be the English singular form of the message. + * @property {String} [id] The message id. If passed then the message id is taken from this property instead of the `message.string`. + * This property is useful when various messages share the same message string. E.g. `editor` string in `in the editor` and `my editor` + * sentences. + * @property {String} [plural] The plural form of the message. This property should be skipped when a message is not supposed + * to support plural forms. Otherwise it should always be set to a string with the English plural form of the message. + */ diff --git a/tests/locale.js b/tests/locale.js index 5ef5236..56f1be8 100644 --- a/tests/locale.js +++ b/tests/locale.js @@ -6,15 +6,15 @@ /* globals console */ import Locale from '../src/locale'; +import { + add as addTranslations, + _clear as clearTranslations +} from '../src/translation-service'; +import { expectToThrowCKEditorError } from './_utils/utils'; describe( 'Locale', () => { - let locale; - - beforeEach( () => { - locale = new Locale(); - } ); - afterEach( () => { + clearTranslations(); sinon.restore(); } ); @@ -115,42 +115,115 @@ describe( 'Locale', () => { } ); describe( 't', () => { - it( 'has the context bound', () => { + let locale; + + beforeEach( () => { + // eslint-disable-next-line no-nested-ternary + const getPolishPluralForm = n => n == 1 ? 0 : n % 10 >= 2 && n % 10 <= 4 && ( n % 100 < 10 || n % 100 >= 20 ) ? 1 : 2; + + addTranslations( 'pl', { + 'foo': 'foo_pl', + 'bar': [ 'bar_pl_0', '%0 bar_pl_1', '%0 bar_pl_2' ] + }, getPolishPluralForm ); + + addTranslations( 'de', { + 'foo': 'foo_de', + 'bar': [ 'bar_de_0', '%0 bar_de_1', '%0 bar_de_2' ] + } ); + + locale = new Locale( { + uiLanguage: 'pl', + contentLanguage: 'de' + } ); + } ); + + it( 'should translate a message to the target ui language', () => { const t = locale.t; - expect( t( 'Foo' ) ).to.equal( 'Foo' ); + expect( t( 'foo' ) ).to.equal( 'foo_pl' ); } ); - it( 'interpolates 1 value', () => { + it( 'should translate a message using the message id if it was passed', () => { const t = locale.t; - expect( t( '%0 - %0', [ 'foo' ] ) ).to.equal( 'foo - foo' ); + addTranslations( 'pl', { + 'ADD_IMAGE': 'obrazek', + 'image': 'foo' + } ); + + expect( t( { string: 'image', id: 'ADD_IMAGE' } ) ).to.equal( 'obrazek' ); } ); - it( 'interpolates 3 values', () => { + it( 'should translate a message supporting plural forms', () => { const t = locale.t; - expect( t( '%1 - %0 - %2', [ 'a', 'b', 'c' ] ) ).to.equal( 'b - a - c' ); + expect( t( { string: 'bar', plural: '%0 bars' }, [ 1 ] ), 1 ).to.equal( 'bar_pl_0' ); + expect( t( { string: 'bar', plural: '%0 bars' }, [ 2 ] ), 2 ).to.equal( '2 bar_pl_1' ); + expect( t( { string: 'bar', plural: '%0 bars' }, [ 5 ] ), 3 ).to.equal( '5 bar_pl_2' ); } ); - // Those test make sure that if %0 is really to be used, then it's going to work. - // It'd be a super rare case if one would need to use %0 and at the same time interpolate something. - it( 'does not interpolate placeholders if values not passed', () => { + it( 'should translate a message supporting plural forms with a message id if it was passed', () => { const t = locale.t; - expect( t( '%1 - %0 - %2' ) ).to.equal( '%1 - %0 - %2' ); + addTranslations( 'pl', { + 'ADD_SPACE': [ '%1 spację', '%1 %0 spacje', '%1 %0 spacji' ], + 'Add': 'Dodaj', + 'Remove': 'Usuń' + } ); + + const addOrRemoveSpaceMessage = { string: '%1 a space', plural: '%1 %0 spaces', id: 'ADD_SPACE' }; + + expect( t( addOrRemoveSpaceMessage, [ 1, t( 'Add' ) ] ), 1 ).to.equal( 'Dodaj spację' ); + expect( t( addOrRemoveSpaceMessage, [ 2, t( 'Remove' ) ] ), 2 ).to.equal( 'Usuń 2 spacje' ); + expect( t( addOrRemoveSpaceMessage, [ 5, t( 'Add' ) ] ), 3 ).to.equal( 'Dodaj 5 spacji' ); } ); - it( 'does not interpolate those placeholders for which values has not been passed', () => { + it( 'should interpolate a message with provided values', () => { const t = locale.t; + expect( t( '%0 - %0', [ 'foo' ] ) ).to.equal( 'foo - foo' ); + expect( t( '%1 - %0 - %2', [ 'a', 'b', 'c' ] ) ).to.equal( 'b - a - c' ); + + // Those test make sure that if %0 is really to be used, then it's going to work. + // It'd be a super rare case if one would need to use %0 and at the same time interpolate something. + expect( t( '%1 - %0 - %2' ) ).to.equal( '%1 - %0 - %2' ); expect( t( '%1 - %0 - %2', [ 'a' ] ) ).to.equal( '%1 - a - %2' ); } ); + + it( 'should interpolate a message with a provided value (shorthand version)', () => { + const t = locale.t; + + expect( t( 'Add %0', 'space' ) ).to.equal( 'Add space' ); + expect( t( 'Remove %0 %1', 'spaces' ) ).to.equal( 'Remove spaces %1' ); + + expect( t( '%0 bar %0', 'foo' ) ).to.equal( 'foo bar foo' ); + } ); + + it( 'should throw an error when a value used to determine the plural version is not a number', () => { + const t = locale.t; + + expectToThrowCKEditorError( () => { + t( { string: 'Add space', plural: 'Add %0 spaces' }, 'space' ); + }, /translation-service-quantity-not-a-number:/, null, { quantity: 'space' } ); + + expectToThrowCKEditorError( () => { + t( { string: 'Add space', plural: 'Add %0 spaces' }, [ 'space' ] ); + }, /translation-service-quantity-not-a-number:/, null, { quantity: 'space' } ); + + expect( () => { + t( { string: 'Add space', plural: 'Add %0 spaces' }, [ 3 ] ); + t( { string: 'Add space', plural: 'Add %0 spaces' }, 3 ); + t( { string: 'Add %1', plural: 'Add %0 %1' }, [ 3, 'spaces' ] ); + t( { string: 'Add %0' }, [ 'space' ] ); + t( { string: 'Add %0' }, 'space' ); + } ).to.not.throw(); + } ); } ); describe( 'language()', () => { it( 'should return #uiLanguage', () => { const stub = sinon.stub( console, 'warn' ); + const locale = new Locale(); expect( locale.language ).to.equal( locale.uiLanguage ); sinon.assert.calledWithMatch( stub, 'locale-deprecated-language-property' ); @@ -158,6 +231,7 @@ describe( 'Locale', () => { it( 'should warn about deprecation', () => { const stub = sinon.stub( console, 'warn' ); + const locale = new Locale(); expect( locale.language ).to.equal( 'en' ); sinon.assert.calledWithMatch( stub, 'locale-deprecated-language-property' ); diff --git a/tests/translation-service.js b/tests/translation-service.js index 1e66327..c51a446 100644 --- a/tests/translation-service.js +++ b/tests/translation-service.js @@ -3,74 +3,152 @@ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license */ -import { translate, add, _clear } from '../src/translation-service'; +import { _translate, add, _clear } from '../src/translation-service'; describe( 'translation-service', () => { afterEach( () => { _clear(); } ); - it( 'should return english string if no translation exists', () => { - const translation = translate( 'pl', 'Bold' ); + describe( 'add()', () => { + it( 'should merge translation added several times', () => { + add( 'pl', { 'foo': 'foo_pl' } ); + add( 'pl', { 'bar': 'bar_pl' } ); - expect( translation ).to.be.equal( 'Bold' ); - } ); + const translatedFoo = _translate( 'pl', { string: 'foo' } ); + const translatedBar = _translate( 'pl', { string: 'bar' } ); + + expect( translatedFoo ).to.equal( 'foo_pl' ); + expect( translatedBar ).to.equal( 'bar_pl' ); + } ); - it( 'should return english string without context if no translation exists', () => { - const translation = translate( 'pl', 'Bold [context: bold]' ); + it( 'should overwrite previously added translations for the same message ids', () => { + add( 'pl', { 'foo': 'First' } ); + add( 'pl', { 'foo': 'Second' } ); - expect( translation ).to.be.equal( 'Bold' ); - } ); + const translatedFoo = _translate( 'pl', { string: 'foo' } ); - it( 'should return translation if the translation for the concrete language is defined', () => { - add( 'pl', { - 'OK': 'OK', - 'Cancel [context: reject]': 'Anuluj' + expect( translatedFoo ).to.equal( 'Second' ); } ); - const translation = translate( 'pl', 'Cancel [context: reject]' ); + it( 'should set the plural form function if it is provided', () => { + add( 'pl', { + 'Add space': [ 'Dodaj spację', 'Dodaj %0 spacje', 'Dodaj %0 spacji' ] + } ); - expect( translation ).to.be.equal( 'Anuluj' ); + // eslint-disable-next-line no-nested-ternary + add( 'pl', {}, n => n == 1 ? 0 : n % 10 >= 2 && n % 10 <= 4 && ( n % 100 < 10 || n % 100 >= 20 ) ? 1 : 2 ); + + expect( _translate( 'pl', { string: 'Add space' }, 0 ) ).to.equal( 'Dodaj %0 spacji' ); + expect( _translate( 'pl', { string: 'Add space' }, 1 ) ).to.equal( 'Dodaj spację' ); + expect( _translate( 'pl', { string: 'Add space' }, 3 ) ).to.equal( 'Dodaj %0 spacje' ); + expect( _translate( 'pl', { string: 'Add space' }, 13 ) ).to.equal( 'Dodaj %0 spacji' ); + } ); } ); - it( 'should return english string without context if the translations for the concrete language exist, ' + + describe( '_translate()', () => { + it( 'should return translated messages when translations are defined', () => { + add( 'pl', { + 'OK': 'OK', + 'Cancel': 'Anuluj' + } ); + + add( 'en_US', { + 'OK': 'OK', + 'Cancel': 'Cancel' + } ); + + const translatedCancelPL = _translate( 'pl', { string: 'Cancel' } ); + const translatedCancelEN = _translate( 'en', { string: 'Cancel' } ); + + expect( translatedCancelPL ).to.be.equal( 'Anuluj' ); + expect( translatedCancelEN ).to.be.equal( 'Cancel' ); + } ); + + it( 'should return the original message string if no translation exists for the given message', () => { + const translatedBold = _translate( 'pl', { string: 'Bold' } ); + + expect( translatedBold ).to.be.equal( 'Bold' ); + } ); + + it( 'should return the correct plural form of english message if no translation exists for the given message', () => { + const addSpaces = _translate( 'pl', { string: 'Add a space', plural: 'Add %0 spaces' }, 3 ); + const addASpace = _translate( 'pl', { string: 'Add a space', plural: 'Add %0 spaces' }, 1 ); + + expect( addSpaces ).to.be.equal( 'Add %0 spaces' ); + expect( addASpace ).to.be.equal( 'Add a space' ); + } ); + + it( 'should return the original message string if a translation for the target language does not exist' + 'but translation doesn\'t', () => { - add( 'pl', { - 'OK': 'OK', - 'Cancel [context: reject]': 'Anuluj' + add( 'pl', { + 'OK': 'OK', + 'Cancel': 'Anuluj' + } ); + + const translatedBold = _translate( 'pl', { string: 'Bold' } ); + + expect( translatedBold ).to.be.equal( 'Bold' ); } ); - const translation = translate( 'pl', 'Bold [context: bold]' ); + it( 'should return a translated message when only one language is provided', () => { + add( 'pl', { + 'OK': 'OK', + 'Cancel': 'Anuluj' + } ); - expect( translation ).to.be.equal( 'Bold' ); - } ); + const translatedCancel = _translate( 'de', { string: 'Cancel' } ); - it( 'should use provided language if only one is provided', () => { - add( 'pl', { - 'OK': 'OK', - 'Cancel [context: reject]': 'Anuluj' + expect( translatedCancel ).to.be.equal( 'Anuluj' ); } ); - const translation = translate( 'de', 'Cancel [context: reject]' ); + it( 'should return a translated message based on message id when it was passed', () => { + add( 'pl', { + 'ADD_IMAGE': 'obraz' + } ); - expect( translation ).to.be.equal( 'Anuluj' ); - } ); + const translatedFooBar = _translate( 'pl', { string: 'image', id: 'ADD_IMAGE' } ); - it( 'should be able to merge translations', () => { - add( 'pl', { - 'OK': 'OK', - 'Cancel [context: reject]': 'Anuluj' + expect( translatedFooBar ).to.equal( 'obraz' ); } ); - add( 'en_US', { - 'OK': 'OK', - 'Cancel [context: reject]': 'Cancel' + it( 'should return the correct plural form of the message based on the provided function', () => { + add( 'pl', { + 'Add space': [ 'Dodaj spację', 'Dodaj %0 spacje', 'Dodaj %0 spacji' ], + 'Cancel': 'Anuluj' + // eslint-disable-next-line no-nested-ternary + }, n => n == 1 ? 0 : n % 10 >= 2 && n % 10 <= 4 && ( n % 100 < 10 || n % 100 >= 20 ) ? 1 : 2 ); + + expect( _translate( 'pl', { string: 'Add space' }, 0 ) ).to.equal( 'Dodaj %0 spacji' ); + expect( _translate( 'pl', { string: 'Add space' }, 1 ) ).to.equal( 'Dodaj spację' ); + expect( _translate( 'pl', { string: 'Add space' }, 3 ) ).to.equal( 'Dodaj %0 spacje' ); + expect( _translate( 'pl', { string: 'Add space' }, 13 ) ).to.equal( 'Dodaj %0 spacji' ); } ); - const translationPL = translate( 'pl', 'Cancel [context: reject]' ); - const translationEN = translate( 'en', 'Cancel [context: reject]' ); + it( 'should return a plural form based on rules for English if no function to determine the plural form was provided', () => { + add( 'pl', { + 'Add space': [ 'Dodaj spację', 'Dodaj %0 spacje', 'Dodaj %0 spacji' ], + 'Cancel': 'Anuluj' + } ); - expect( translationPL ).to.be.equal( 'Anuluj' ); - expect( translationEN ).to.be.equal( 'Cancel' ); + expect( _translate( 'pl', { string: 'Add space' }, 1 ) ).to.equal( 'Dodaj spację' ); + + expect( _translate( 'pl', { string: 'Add space' }, 0 ) ).to.equal( 'Dodaj %0 spacje' ); + expect( _translate( 'pl', { string: 'Add space' }, 3 ) ).to.equal( 'Dodaj %0 spacje' ); + expect( _translate( 'pl', { string: 'Add space' }, 13 ) ).to.equal( 'Dodaj %0 spacje' ); + } ); + + it( 'should support a plural form rule that returns a boolean', () => { + add( 'pl', { + 'Add space': [ 'Dodaj spację', 'Dodaj %0 spacje' ], + 'Cancel': 'Anuluj' + }, n => n !== 1 ); + + expect( _translate( 'pl', { string: 'Add space' }, 1 ) ).to.equal( 'Dodaj spację' ); + + expect( _translate( 'pl', { string: 'Add space' }, 0 ) ).to.equal( 'Dodaj %0 spacje' ); + expect( _translate( 'pl', { string: 'Add space' }, 3 ) ).to.equal( 'Dodaj %0 spacje' ); + expect( _translate( 'pl', { string: 'Add space' }, 13 ) ).to.equal( 'Dodaj %0 spacje' ); + } ); } ); } );