@ckeditor/ckeditor5-utils
Version:
Miscellaneous utilities used by CKEditor 5.
210 lines (208 loc) • 9.12 kB
JavaScript
/**
* @license Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
*/
import CKEditorError from './ckeditorerror.js';
import global from './dom/global.js';
import { merge } from 'es-toolkit/compat';
/* istanbul ignore else -- @preserve */
if (!global.window.CKEDITOR_TRANSLATIONS) {
global.window.CKEDITOR_TRANSLATIONS = {};
}
/**
* 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 `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.
*
* ```ts
* add( 'pl', {
* '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:
*
* ```ts
* 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.
*
* ```ts
* 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:
*
* ```ts
* {
* 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 ? 0 : n % 10 >= 2 && n % 10 <= 4 && ( n % 100 < 10 || n % 100 >= 20 ) ? 1 : 2 );
* }
* // Other languages.
* }
* ```
*
* If you cannot import this function from this module (e.g. because you use a CKEditor 5 build), you can
* still add translations by extending the global `window.CKEDITOR_TRANSLATIONS` object by using a function like
* the one below:
*
* ```ts
* function addTranslations( language, translations, getPluralForm ) {
* if ( !global.window.CKEDITOR_TRANSLATIONS ) {
* global.window.CKEDITOR_TRANSLATIONS = {};
* }
* if ( !global.window.CKEDITOR_TRANSLATIONS[ language ] ) {
* global.window.CKEDITOR_TRANSLATIONS[ language ] = {};
* }
*
* const languageTranslations = global.window.CKEDITOR_TRANSLATIONS[ language ];
*
* languageTranslations.dictionary = languageTranslations.dictionary || {};
* languageTranslations.getPluralForm = getPluralForm || languageTranslations.getPluralForm;
*
* // Extend the dictionary for the given language.
* Object.assign( languageTranslations.dictionary, translations );
* }
* ```
*
* @param language Target language.
* @param 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 getPluralForm A function that returns the plural form index (a number).
*/
export function add(language, translations, getPluralForm) {
if (!global.window.CKEDITOR_TRANSLATIONS[language]) {
global.window.CKEDITOR_TRANSLATIONS[language] = {};
}
const languageTranslations = global.window.CKEDITOR_TRANSLATIONS[language];
languageTranslations.dictionary = languageTranslations.dictionary || {};
languageTranslations.getPluralForm = getPluralForm || languageTranslations.getPluralForm;
Object.assign(languageTranslations.dictionary, translations);
}
/**
* **Note:** This method is internal, use {@link module:utils/locale~Locale#t the `t()` function} instead to translate
* the editor UI parts.
*
* This function is responsible for translating messages to the specified language. It uses translations added perviously
* by {@link module:utils/translation-service~add} (a translations dictionary and the `getPluralForm()` function
* to provide accurate translations of plural forms).
*
* When no translation is defined in the dictionary or the dictionary does not exist, this function returns
* the original message string or the message plural depending on the number of elements.
*
* ```ts
* translate( 'pl', { string: 'Cancel' } ); // 'Cancel'
* ```
*
* The third optional argument is the number of elements, based on which the single form or one of the plural forms
* should be picked when the message is supposed to support various plural forms.
*
* ```ts
* 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.
*
* ```ts
* translate( 'en', { string: 'image', id: 'ADD_IMAGE' } );
* translate( 'en', { string: 'image', id: 'AN_IMAGE' } );
* ```
*
* @internal
* @param language Target language.
* @param message A message that will be translated.
* @param quantity The number of elements for which a plural form should be picked from the target language dictionary.
* @param translations Translations passed in editor config, if not provided use the global `window.CKEDITOR_TRANSLATIONS`.
* @returns Translated sentence.
*/
export function _translate(language, message, quantity = 1, translations) {
if (typeof quantity !== 'number') {
/**
* An incorrect value was passed to the translation function. This was probably caused
* by an 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 a number as the first element.
*
* @error translation-service-quantity-not-a-number
*/
throw new CKEditorError('translation-service-quantity-not-a-number', null, { quantity });
}
const normalizedTranslations = translations || global.window.CKEDITOR_TRANSLATIONS;
const numberOfLanguages = getNumberOfLanguages(normalizedTranslations);
if (numberOfLanguages === 1) {
// Override the language to the only supported one.
// This can't be done in the `Locale` class, because the translations comes after the `Locale` class initialization.
language = Object.keys(normalizedTranslations)[0];
}
const messageId = message.id || message.string;
if (numberOfLanguages === 0 || !hasTranslation(language, messageId, normalizedTranslations)) {
if (quantity !== 1) {
// Return the default plural form that was passed in the `message.plural` parameter.
return message.plural;
}
return message.string;
}
const dictionary = normalizedTranslations[language].dictionary;
const getPluralForm = normalizedTranslations[language].getPluralForm || (n => n === 1 ? 0 : 1);
const translation = dictionary[messageId];
if (typeof translation === 'string') {
return translation;
}
const pluralFormIndex = Number(getPluralForm(quantity));
// Note: The `translate` function is not responsible for replacing `%0, %1, ...` with values.
return translation[pluralFormIndex];
}
/**
* Clears dictionaries for test purposes.
*
* @internal
*/
export function _clear() {
if (global.window.CKEDITOR_TRANSLATIONS) {
global.window.CKEDITOR_TRANSLATIONS = {};
}
}
/**
* If array then merge objects which are inside otherwise return given object.
*
* @internal
* @param translations Translations passed in editor config.
*/
export function _unifyTranslations(translations) {
return Array.isArray(translations) ?
translations.reduce((acc, translation) => merge(acc, translation)) :
translations;
}
/**
* Checks whether the dictionary exists and translation in that dictionary exists.
*/
function hasTranslation(language, messageId, translations) {
return !!translations[language] && !!translations[language].dictionary[messageId];
}
function getNumberOfLanguages(translations) {
return Object.keys(translations).length;
}