Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow passing a 'translations' object in the configuration #15734

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
9 changes: 7 additions & 2 deletions packages/ckeditor5-core/src/context.ts
Expand Up @@ -146,7 +146,11 @@ export default class Context {
* @param config The context configuration.
*/
constructor( config?: ContextConfig ) {
this.config = new Config<ContextConfig>( config, ( this.constructor as typeof Context ).defaultConfig );
// We don't pass translations to the config, because its behavior of splitting keys
// with dots (e.g. `resize.width` => `resize: { width }`) breaks the translations.
const { translations, ...rest } = config || {};
filipsobol marked this conversation as resolved.
Show resolved Hide resolved

this.config = new Config<ContextConfig>( rest, ( this.constructor as typeof Context ).defaultConfig );

const availablePlugins = ( this.constructor as typeof Context ).builtinPlugins;

Expand All @@ -158,7 +162,8 @@ export default class Context {

this.locale = new Locale( {
uiLanguage: typeof languageConfig === 'string' ? languageConfig : languageConfig.ui,
contentLanguage: this.config.get( 'language.content' )
contentLanguage: this.config.get( 'language.content' ),
translations
} );

this.t = this.locale.t;
Expand Down
10 changes: 8 additions & 2 deletions packages/ckeditor5-core/src/editor/editor.ts
Expand Up @@ -278,14 +278,20 @@ export default abstract class Editor extends ObservableMixin() {
// Prefer the language passed as the argument to the constructor instead of the constructor's `defaultConfig`, if both are set.
const language = config.language || ( constructor.defaultConfig && constructor.defaultConfig.language );

this._context = config.context || new Context( { language } );
/**
* We don't pass translations to the config, because its behavior of splitting keys
* with dots (e.g. `resize.width` => `resize: { width }`) breaks the translations.
*/
Comment on lines +281 to +284
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/**
* We don't pass translations to the config, because its behavior of splitting keys
* with dots (e.g. `resize.width` => `resize: { width }`) breaks the translations.
*/
// We don't pass translations to the config, because its behavior of splitting keys
// with dots (e.g. `resize.width` => `resize: { width }`) breaks the translations.

const { translations, ...rest } = config;

this._context = config.context || new Context( { language, translations } );
this._context._addEditor( this, !config.context );

// Clone the plugins to make sure that the plugin array will not be shared
// between editors and make the watchdog feature work correctly.
const availablePlugins = Array.from( constructor.builtinPlugins || [] );

this.config = new Config<EditorConfig>( config, constructor.defaultConfig );
this.config = new Config<EditorConfig>( rest, constructor.defaultConfig );
illia-stv marked this conversation as resolved.
Show resolved Hide resolved
this.config.define( 'plugins', availablePlugins );
this.config.define( this._context._getEditorConfig() );

Expand Down
6 changes: 6 additions & 0 deletions packages/ckeditor5-core/src/editor/editorconfig.ts
Expand Up @@ -7,6 +7,7 @@
* @module core/editor/editorconfig
*/

import type { ArrayOrItem, Translations } from '@ckeditor/ckeditor5-utils';
import type Context from '../context.js';
import type { PluginConstructor } from '../plugin.js';
import type Editor from './editor.js';
Expand Down Expand Up @@ -533,6 +534,11 @@ export interface EditorConfig {
* [order a trial](https://orders.ckeditor.com/trial/premium-features).
*/
licenseKey?: string;

/**
* Translations to be used in the editor.
*/
translations?: ArrayOrItem<Translations>;
}

/**
Expand Down
72 changes: 72 additions & 0 deletions packages/ckeditor5-core/tests/context.js
Expand Up @@ -6,11 +6,14 @@
import Context from '../src/context.js';
import ContextPlugin from '../src/contextplugin.js';
import Plugin from '../src/plugin.js';
import ClassicTestEditor from './_utils/classictesteditor.js';
import Config from '@ckeditor/ckeditor5-utils/src/config.js';
import Locale from '@ckeditor/ckeditor5-utils/src/locale.js';
import VirtualTestEditor from './_utils/virtualtesteditor.js';
import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror.js';

/* globals document */

describe( 'Context', () => {
describe( 'config', () => {
it( 'should be created', () => {
Expand All @@ -24,6 +27,18 @@ describe( 'Context', () => {

expect( context.config.get( 'foo' ) ).to.equal( 'bar' );
} );

it( 'should not set translations in the config', () => {
const context = new Context( { translations: {
pl: {
dictionary: {
bold: 'Pogrubienie'
}
}
} } );

expect( context.config.get( 'translations' ) ).to.equal( undefined );
} );
} );

describe( '_getEditorConfig()', () => {
Expand Down Expand Up @@ -97,6 +112,21 @@ describe( 'Context', () => {
expect( context.locale.uiLanguage ).to.equal( 'en' );
expect( context.locale.contentLanguage ).to.equal( 'ar' );
} );

it( 'is configured with the config.translations', () => {
const context = new Context( {
translations: {
pl: {
dictionary: {
key: ''
},
getPluralForm: () => ''
} }
} );

expect( context.locale.translations.pl.dictionary.key ).to.equal( '' );
expect( context.locale.translations.pl.getPluralForm() ).to.equal( '' );
} );
} );

describe( 'plugins', () => {
Expand Down Expand Up @@ -474,6 +504,48 @@ describe( 'Context', () => {
expect( context.config.get( 'bar' ) ).to.equal( 2 );
} );
} );

describe( 'translations', () => {
let editor, element;

beforeEach( () => {
element = document.createElement( 'div' );
document.body.appendChild( element );

return ClassicTestEditor
.create( element, {
translations: {
pl: {
dictionary: {
bold: 'Pogrubienie',
'a.b': 'value'
}
}
}
} )
.then( _editor => {
editor = _editor;
} );
} );

afterEach( () => {
document.body.removeChild( element );

return editor.destroy();
} );

it( 'should not set translations in the config', () => {
expect( editor.config.get( 'translations' ) ).to.equal( undefined );
} );

it( 'should properly get translations with the key', () => {
expect( editor.locale.translations.pl.dictionary.bold ).to.equal( 'Pogrubienie' );
} );

it( 'should properly get translations with dot in the key', () => {
expect( editor.locale.translations.pl.dictionary[ 'a.b' ] ).to.equal( 'value' );
} );
} );
} );

function getPlugins( editor ) {
Expand Down
14 changes: 14 additions & 0 deletions packages/ckeditor5-core/tests/editor/editor.js
Expand Up @@ -161,6 +161,20 @@ describe( 'Editor', () => {
expect( editor.config.get( 'bar' ) ).to.equal( 'foo' );
} );

it( 'should not have access to translations', () => {
const editor = new TestEditor( {
translations: {
pl: {
dictionary: {
Bold: 'Pogrubienie'
}
}
}
} );

expect( editor.config.get( 'translations' ) ).to.equal( undefined );
} );

it( 'should bind editing.view.document#isReadOnly to the editor#isReadOnly', () => {
const editor = new TestEditor();

Expand Down
2 changes: 1 addition & 1 deletion packages/ckeditor5-utils/src/index.ts
Expand Up @@ -72,7 +72,7 @@ export * from './dom/scroll.js';

export * from './keyboard.js';
export * from './language.js';
export { default as Locale, type LocaleTranslate } from './locale.js';
export { default as Locale, type LocaleTranslate, type Translations } from './locale.js';
export {
default as Collection,
type CollectionAddEvent,
Expand Down
41 changes: 35 additions & 6 deletions packages/ckeditor5-utils/src/locale.ts
Expand Up @@ -9,8 +9,8 @@

/* globals console */

import toArray from './toarray.js';
import { _translate, type Message } from './translation-service.js';
import toArray, { type ArrayOrItem } from './toarray.js';
import { _translate, _unifyTranslations, type Message } from './translation-service.js';
import { getLanguageDirection, type LanguageDirection } from './language.js';

/**
Expand Down Expand Up @@ -97,6 +97,11 @@ export default class Locale {
*/
public readonly t: LocaleTranslate;

/**
* Object that contains translations.
*/
public translations: Translations | undefined;

/**
* Creates a new instance of the locale class. Learn more about
* {@glink features/ui-language configuring the language of the editor}.
Expand All @@ -107,13 +112,24 @@ export default class Locale {
* @param options.contentLanguage The editor content language code in the
* [ISO 639-1](https://en.wikipedia.org/wiki/ISO_639-1) format. If not specified, the same as `options.language`.
* See {@link #contentLanguage}.
* @param translations Translations passed as a editor config parameter.
*/
constructor( { uiLanguage = 'en', contentLanguage }: { readonly uiLanguage?: string; readonly contentLanguage?: string } = {} ) {
constructor(
{
uiLanguage = 'en',
contentLanguage,
translations
}: {
readonly uiLanguage?: string;
readonly contentLanguage?: string;
readonly translations?: ArrayOrItem<Translations>;
} = {}
) {
this.uiLanguage = uiLanguage;
this.contentLanguage = contentLanguage || this.uiLanguage;
this.uiLanguageDirection = getLanguageDirection( this.uiLanguage );
this.contentLanguageDirection = getLanguageDirection( this.contentLanguage );

this.translations = _unifyTranslations( translations );
this.t = ( message, values ) => this._t( message, values );
}

Expand Down Expand Up @@ -154,7 +170,7 @@ export default class Locale {
const hasPluralForm = !!message.plural;
const quantity = hasPluralForm ? values[ 0 ] as number : 1;

const translatedString = _translate( this.uiLanguage, message, quantity );
const translatedString = _translate( this.uiLanguage, message, quantity, this.translations );

return interpolateString( translatedString, values );
}
Expand All @@ -165,7 +181,10 @@ export default class Locale {
* @param 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.
*/
export type LocaleTranslate = ( message: string | Message, values?: number | string | ReadonlyArray<number | string> ) => string;
export type LocaleTranslate = (
message: string | Message,
values?: number | string | ReadonlyArray<number | string>
) => string;

/**
* Fills the `%0, %1, ...` string placeholders with values.
Expand All @@ -175,3 +194,13 @@ function interpolateString( string: string, values: ReadonlyArray<any> ): string
return ( index < values.length ) ? values[ index ] : match;
} );
}

/**
* Translations object definition.
*/
export type Translations = {
[ language: string ]: {
dictionary: { [ messageId: string ]: string | ReadonlyArray<string> };
getPluralForm?: ( n: number ) => number;
};
};
58 changes: 38 additions & 20 deletions packages/ckeditor5-utils/src/translation-service.ts
Expand Up @@ -9,16 +9,14 @@
* @module utils/translation-service
*/

import type { Translations } from './locale.js';
import CKEditorError from './ckeditorerror.js';
import global from './dom/global.js';
import { merge } from 'lodash-es';
import { type ArrayOrItem } from './toarray.js';

declare global {
var CKEDITOR_TRANSLATIONS: {
[ language: string ]: {
dictionary: { [ messageId: string ]: string | ReadonlyArray<string> };
getPluralForm?: ( n: number ) => number;
};
};
var CKEDITOR_TRANSLATIONS: Translations;
}

/* istanbul ignore else -- @preserve */
Expand Down Expand Up @@ -163,9 +161,15 @@ export function add(
* @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: string, message: Message, quantity: number = 1 ): string {
export function _translate(
language: string,
message: Message,
quantity: number = 1,
translations?: Translations | undefined
illia-stv marked this conversation as resolved.
Show resolved Hide resolved
): string {
if ( typeof quantity !== 'number' ) {
/**
* An incorrect value was passed to the translation function. This was probably caused
Expand All @@ -177,17 +181,18 @@ export function _translate( language: string, message: Message, quantity: number
throw new CKEditorError( 'translation-service-quantity-not-a-number', null, { quantity } );
}

const numberOfLanguages = getNumberOfLanguages();
const normalizedTranslations: Translations = 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( global.window.CKEDITOR_TRANSLATIONS )[ 0 ];
language = Object.keys( normalizedTranslations )[ 0 ];
}

const messageId = message.id || message.string;

if ( numberOfLanguages === 0 || !hasTranslation( language, messageId ) ) {
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!;
Expand All @@ -196,8 +201,8 @@ export function _translate( language: string, message: Message, quantity: number
return message.string;
}

const dictionary = global.window.CKEDITOR_TRANSLATIONS[ language ].dictionary;
const getPluralForm = global.window.CKEDITOR_TRANSLATIONS[ language ].getPluralForm || ( n => n === 1 ? 0 : 1 );
const dictionary = normalizedTranslations[ language ].dictionary;
const getPluralForm = normalizedTranslations[ language ].getPluralForm || ( n => n === 1 ? 0 : 1 );
const translation = dictionary[ messageId ];

if ( typeof translation === 'string' ) {
Expand All @@ -216,21 +221,34 @@ export function _translate( language: string, message: Message, quantity: number
* @internal
*/
export function _clear(): void {
global.window.CKEDITOR_TRANSLATIONS = {};
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?: ArrayOrItem<Translations>
): Translations | undefined {
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: string, messageId: string ): boolean {
return (
!!global.window.CKEDITOR_TRANSLATIONS[ language ] &&
!!global.window.CKEDITOR_TRANSLATIONS[ language ].dictionary[ messageId ]
);
function hasTranslation( language: string, messageId: string, translations: Translations ): boolean {
return !!translations[ language ] && !!translations[ language ].dictionary[ messageId ];
}

function getNumberOfLanguages(): number {
return Object.keys( global.window.CKEDITOR_TRANSLATIONS ).length;
function getNumberOfLanguages( translations: Translations ): number {
return Object.keys( translations ).length;
}

/**
Expand Down