Skip to content
This repository has been archived by the owner on Jun 26, 2020. It is now read-only.

Commit

Permalink
Merge 9fe6f52 into 2902b30
Browse files Browse the repository at this point in the history
  • Loading branch information
ma2ciek committed Apr 22, 2020
2 parents 2902b30 + 9fe6f52 commit dd8ba23
Show file tree
Hide file tree
Showing 4 changed files with 393 additions and 106 deletions.
80 changes: 59 additions & 21 deletions src/locale.js
Original file line number Diff line number Diff line change
Expand Up @@ -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' ];

Expand Down Expand Up @@ -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 (`%<index>`) for values which are passed as the second argument.
* `<index>` 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 (`%<index>`) for value(s) that are passed as a `values` parameter.
* For an array of values the `%<index>` 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. Therefore, it should be a number.
*
* 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.<String|Number>} [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 );
}

/**
Expand Down Expand Up @@ -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.<Number|String>} [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.
Expand Down
153 changes: 125 additions & 28 deletions src/translation-service.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,62 +9,130 @@
* @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 either 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 );
*
* 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 languageTranslations = window.CKEDITOR_TRANSLATIONS[ language ];
*
* const dictionary = window.CKEDITOR_TRANSLATIONS[ language ] || ( 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.<String, String>} translations Translations which will be added to the dictionary.
* @param {Object.<String,*>} 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 ];

Object.assign( dictionary, translations );
languageTranslations.dictionary = languageTranslations.dictionary || {};
languageTranslations.getPluralForm = getPluralForm || languageTranslations.getPluralForm;

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'
*
* 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.
* 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.
*
* translate( 'pl', 'Cancel [context: reject]' );
* 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'
*
* 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 ) {
Expand All @@ -73,14 +141,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 ];
}

/**
Expand All @@ -93,13 +175,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} 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.
*/

0 comments on commit dd8ba23

Please sign in to comment.