translation-service.js 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216
  1. /**
  2. * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  4. */
  5. /* globals window */
  6. /**
  7. * @module utils/translation-service
  8. */
  9. import CKEditorError from './ckeditorerror';
  10. /* istanbul ignore else */
  11. if ( !window.CKEDITOR_TRANSLATIONS ) {
  12. window.CKEDITOR_TRANSLATIONS = {};
  13. }
  14. /**
  15. * Adds translations to existing ones or overrides the existing translations. These translations will later
  16. * be available for the {@link module:utils/locale~Locale#t `t()`} function.
  17. *
  18. * The `translations` is an object which consists of `messageId: translation` pairs. Note that the message ID can be
  19. * either constructed from the message string or from the message ID if it was passed
  20. * (this happens rarely and mostly for short messages or messages with placeholders).
  21. * Since the editor displays only the message string, the message ID can be found either in the source code or in the
  22. * built translations for another language.
  23. *
  24. * add( 'pl', {
  25. * 'Cancel': 'Anuluj',
  26. * 'IMAGE': 'obraz', // Note that the `IMAGE` comes from the message ID, while the string can be `image`.
  27. * } );
  28. *
  29. * If the message is supposed to support various plural forms, make sure to provide an array with the singular form and all plural forms:
  30. *
  31. * add( 'pl', {
  32. * 'Add space': [ 'Dodaj spację', 'Dodaj %0 spacje', 'Dodaj %0 spacji' ]
  33. * } );
  34. *
  35. * You should also specify the third argument (the `getPluralForm()` function) that will be used to determine the plural form if no
  36. * language file was loaded for that language. All language files coming from CKEditor 5 sources will have this option set, so
  37. * these plural form rules will be reused by other translations added to the registered languages. The `getPluralForm()` function
  38. * can return either a Boolean or a number.
  39. *
  40. * add( 'en', {
  41. * // ... Translations.
  42. * }, n => n !== 1 );
  43. * add( 'pl', {
  44. * // ... Translations.
  45. * }, n => n == 1 ? 0 : n % 10 >= 2 && n % 10 <= 4 && ( n % 100 < 10 || n % 100 >= 20 ) ? 1 : 2 );
  46. *
  47. * All translations extend the global `window.CKEDITOR_TRANSLATIONS` object. An example of this object can be found below:
  48. *
  49. * {
  50. * pl: {
  51. * dictionary: {
  52. * 'Cancel': 'Anuluj',
  53. * 'Add space': [ 'Dodaj spację', 'Dodaj %0 spacje', 'Dodaj %0 spacji' ]
  54. * },
  55. * // A function that returns the plural form index.
  56. * getPluralForm: n => n !==1
  57. * }
  58. * // Other languages.
  59. * }
  60. *
  61. * If you cannot import this function from this module (e.g. because you use a CKEditor 5 build), you can
  62. * still add translations by extending the global `window.CKEDITOR_TRANSLATIONS` object by using a function like
  63. * the one below:
  64. *
  65. * function addTranslations( language, translations, getPluralForm ) {
  66. * if ( !window.CKEDITOR_TRANSLATIONS ) {
  67. * window.CKEDITOR_TRANSLATIONS = {};
  68. * }
  69. * if ( !window.CKEDITOR_TRANSLATIONS[ language ] ) {
  70. * window.CKEDITOR_TRANSLATIONS[ language ] = {};
  71. * }
  72. *
  73. * const languageTranslations = window.CKEDITOR_TRANSLATIONS[ language ];
  74. *
  75. * languageTranslations.dictionary = languageTranslations.dictionary || {};
  76. * languageTranslations.getPluralForm = getPluralForm || languageTranslations.getPluralForm;
  77. *
  78. * // Extend the dictionary for the given language.
  79. * Object.assign( languageTranslations.dictionary, translations );
  80. * }
  81. *
  82. * @param {String} language Target language.
  83. * @param {Object.<String,*>} translations An object with translations which will be added to the dictionary.
  84. * For each message ID the value should be either a translation or an array of translations if the message
  85. * should support plural forms.
  86. * @param {Function} getPluralForm A function that returns the plural form index (a number).
  87. */
  88. export function add( language, translations, getPluralForm ) {
  89. if ( !window.CKEDITOR_TRANSLATIONS[ language ] ) {
  90. window.CKEDITOR_TRANSLATIONS[ language ] = {};
  91. }
  92. const languageTranslations = window.CKEDITOR_TRANSLATIONS[ language ];
  93. languageTranslations.dictionary = languageTranslations.dictionary || {};
  94. languageTranslations.getPluralForm = getPluralForm || languageTranslations.getPluralForm;
  95. Object.assign( languageTranslations.dictionary, translations );
  96. }
  97. /**
  98. * **Note:** This method is internal, use {@link module:utils/locale~Locale#t the `t()` function} instead to translate
  99. * the editor UI parts.
  100. *
  101. * This function is responsible for translating messages to the specified language. It uses translations added perviously
  102. * by {@link module:utils/translation-service~add} (a translations dictionary and the `getPluralForm()` function
  103. * to provide accurate translations of plural forms).
  104. *
  105. * When no translation is defined in the dictionary or the dictionary does not exist, this function returns
  106. * the original message string or the message plural depending on the number of elements.
  107. *
  108. * translate( 'pl', { string: 'Cancel' } ); // 'Cancel'
  109. *
  110. * The third optional argument is the number of elements, based on which the single form or one of the plural forms
  111. * should be picked when the message is supposed to support various plural forms.
  112. *
  113. * translate( 'en', { string: 'Add a space', plural: 'Add %0 spaces' }, 1 ); // 'Add a space'
  114. * translate( 'en', { string: 'Add a space', plural: 'Add %0 spaces' }, 3 ); // 'Add %0 spaces'
  115. *
  116. * The message should provide an ID using the `id` property when the message strings are not unique and their
  117. * translations should be different.
  118. *
  119. * translate( 'en', { string: 'image', id: 'ADD_IMAGE' } );
  120. * translate( 'en', { string: 'image', id: 'AN_IMAGE' } );
  121. *
  122. * @protected
  123. * @param {String} language Target language.
  124. * @param {module:utils/translation-service~Message|String} message A message that will be translated.
  125. * @param {Number} [quantity] The number of elements for which a plural form should be picked from the target language dictionary.
  126. * @returns {String} Translated sentence.
  127. */
  128. export function _translate( language, message, quantity = 1 ) {
  129. if ( typeof quantity !== 'number' ) {
  130. /**
  131. * An incorrect value was passed to the translation function. This was probably caused
  132. * by an incorrect message interpolation of a plural form. Note that for messages supporting plural forms
  133. * the second argument of the `t()` function should always be a number or an array with a number as the first element.
  134. *
  135. * @error translation-service-quantity-not-a-number
  136. */
  137. throw new CKEditorError( 'translation-service-quantity-not-a-number', null, { quantity } );
  138. }
  139. const numberOfLanguages = getNumberOfLanguages();
  140. if ( numberOfLanguages === 1 ) {
  141. // Override the language to the only supported one.
  142. // This can't be done in the `Locale` class, because the translations comes after the `Locale` class initialization.
  143. language = Object.keys( window.CKEDITOR_TRANSLATIONS )[ 0 ];
  144. }
  145. const messageId = message.id || message.string;
  146. if ( numberOfLanguages === 0 || !hasTranslation( language, messageId ) ) {
  147. if ( quantity !== 1 ) {
  148. // Return the default plural form that was passed in the `message.plural` parameter.
  149. return message.plural;
  150. }
  151. return message.string;
  152. }
  153. const dictionary = window.CKEDITOR_TRANSLATIONS[ language ].dictionary;
  154. const getPluralForm = window.CKEDITOR_TRANSLATIONS[ language ].getPluralForm || ( n => n === 1 ? 0 : 1 );
  155. if ( typeof dictionary[ messageId ] === 'string' ) {
  156. return dictionary[ messageId ];
  157. }
  158. const pluralFormIndex = Number( getPluralForm( quantity ) );
  159. // Note: The `translate` function is not responsible for replacing `%0, %1, ...` with values.
  160. return dictionary[ messageId ][ pluralFormIndex ];
  161. }
  162. /**
  163. * Clears dictionaries for test purposes.
  164. *
  165. * @protected
  166. */
  167. export function _clear() {
  168. window.CKEDITOR_TRANSLATIONS = {};
  169. }
  170. // Checks whether the dictionary exists and translation in that dictionary exists.
  171. function hasTranslation( language, messageId ) {
  172. return (
  173. !!window.CKEDITOR_TRANSLATIONS[ language ] &&
  174. !!window.CKEDITOR_TRANSLATIONS[ language ].dictionary[ messageId ]
  175. );
  176. }
  177. function getNumberOfLanguages() {
  178. return Object.keys( window.CKEDITOR_TRANSLATIONS ).length;
  179. }
  180. /**
  181. * The internationalization message interface. A message that implements this interface can be passed to the `t()` function
  182. * to be translated to the target UI language.
  183. *
  184. * @typedef {Object} module:utils/translation-service~Message
  185. *
  186. * @property {String} string The message string to translate. Acts as a default translation if the translation for a given language
  187. * is not defined. When the message is supposed to support plural forms, the string should be the English singular form of the message.
  188. * @property {String} [id] The message ID. If passed, the message ID is taken from this property instead of the `message.string`.
  189. * This property is useful when various messages share the same message string, for example, the `editor` string in `in the editor`
  190. * and `my editor` sentences.
  191. * @property {String} [plural] The plural form of the message. This property should be skipped when a message is not supposed
  192. * to support plural forms. Otherwise it should always be set to a string with the English plural form of the message.
  193. */