Skip to content
Open
8 changes: 8 additions & 0 deletions packages/ckeditor5-alignment/tests/alignmentui.js
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,10 @@ describe( 'Alignment UI', () => {
content: 'ar',
ui: 'ar'
},
translations: [ { ar: {
dictionary: [],
getPluralForm: sinon.spy()
} } ],
plugins: [ AlignmentEditing, AlignmentUI ]
} );

Expand Down Expand Up @@ -437,6 +441,10 @@ describe( 'Alignment UI', () => {
language: {
content: 'ar'
},
translations: [ { ar: {
dictionary: [],
getPluralForm: sinon.spy()
} } ],
plugins: [ AlignmentEditing, AlignmentUI ],
alignment: { options: [ 'center', 'justify' ] }
} )
Expand Down
4 changes: 4 additions & 0 deletions packages/ckeditor5-ckbox/tests/ckboxutils.js
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,10 @@ describe( 'CKBoxUtils', () => {
it( 'should set default values', async () => {
const editor = await createTestEditor( {
language: 'pl',
translations: [ { pl: {
dictionary: [],
getPluralForm: sinon.spy()
} } ],
cloudServices: {
tokenUrl: 'http://cs.example.com'
}
Expand Down
6 changes: 5 additions & 1 deletion packages/ckeditor5-ckfinder/tests/ckfindercommand.js
Original file line number Diff line number Diff line change
Expand Up @@ -260,7 +260,11 @@ describe( 'CKFinderCommand', () => {
return VirtualTestEditor
.create( {
plugins: [ Paragraph, ImageBlockEditing, ImageUploadEditing, LinkEditing, Notification, ClipboardPipeline ],
language: 'pl'
language: 'pl',
translations: [ { pl: {
dictionary: [],
getPluralForm: sinon.spy()
} } ]
} )
.then( newEditor => {
editor = newEditor;
Expand Down
37 changes: 32 additions & 5 deletions packages/ckeditor5-core/src/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,15 @@ import {
Collection,
CKEditorError,
Locale,
global,
type LocaleTranslate
} from '@ckeditor/ckeditor5-utils';

import PluginCollection from './plugincollection.js';
import type Editor from './editor/editor.js';
import type { LoadedPlugins, PluginConstructor } from './plugin.js';
import type { EditorConfig } from './editor/editorconfig.js';
import { cloneDeep } from 'es-toolkit/compat';

/**
* Provides a common, higher-level environment for solutions that use multiple {@link module:core/editor/editor~Editor editors}
Expand Down Expand Up @@ -160,11 +162,36 @@ export default class Context {

const languageConfig = this.config.get( 'language' ) || {};

this.locale = new Locale( {
uiLanguage: typeof languageConfig === 'string' ? languageConfig : languageConfig.ui,
contentLanguage: this.config.get( 'language.content' ),
translations
} );
if ( !translations && global.window.CKEDITOR_TRANSLATIONS ) {
/**
* Function _translate from translation-service.ts gets translations from
* global.window.CKEDITOR_TRANSLATIONS when translations injected into Locale are empty.
* Value of global.window.CKEDITOR_TRANSLATIONS is provided by CKEditorTranslationsPlugin from
* ckeditor5-dev-translations package (for example in manual tests) when --language flag is used.
* Function _translate is called often and has no access to the editing editor config, so here is a better place to
* check if translations will be taken from dev-translations, and if yes – update config.language.ui value.
*/
Comment on lines +166 to +181
Copy link
Copy Markdown
Member

@Mati365 Mati365 May 28, 2025

Choose a reason for hiding this comment

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

imho, this comment could be clearer. Two things:

  1. It sounds like global.window.CKEDITOR_TRANSLATIONS is always set by CKEditorTranslationsPlugin, but that’s not true. It can also come from translation files loaded from a CDN, so we shouldn’t assume it always comes from the plugin.
  2. Without reading the issue (#7297), it’s hard to tell what the problem is and why this if is needed. The comment should explain it in a more straightforward way.

const devTranslations = cloneDeep( global.window.CKEDITOR_TRANSLATIONS );
const uiLanguageFromConfig = typeof languageConfig === 'string' ? languageConfig : languageConfig.ui;
const hasMatchingTranslations = devTranslations[ uiLanguageFromConfig! ];
const defaultDevTranslationsLanguage = Object.keys( devTranslations )[ 0 ];

this.locale = new Locale( {
uiLanguage: hasMatchingTranslations ? uiLanguageFromConfig : defaultDevTranslationsLanguage,
contentLanguage: this.config.get( 'language.content' ),
translations: devTranslations
} );

if ( !hasMatchingTranslations && this.config.get( 'language.ui' ) !== defaultDevTranslationsLanguage ) {
this.config.define( 'language.ui', defaultDevTranslationsLanguage );
}
} else {
this.locale = new Locale( {
uiLanguage: typeof languageConfig === 'string' ? languageConfig : languageConfig.ui,
contentLanguage: this.config.get( 'language.content' ),
translations
} );
}

this.t = this.locale.t;

Expand Down
26 changes: 23 additions & 3 deletions packages/ckeditor5-core/tests/context.js
Original file line number Diff line number Diff line change
Expand Up @@ -91,26 +91,46 @@ describe( 'Context', () => {
} );

it( 'is configured with the config.language (UI and the content)', () => {
const context = new Context( { language: 'pl' } );
const context = new Context( { language: 'pl', translations: { pl: {} } } );

expect( context.locale.uiLanguage ).to.equal( 'pl' );
expect( context.locale.contentLanguage ).to.equal( 'pl' );
} );

it( 'is configured with the config.language (different for UI and the content)', () => {
const context = new Context( { language: { ui: 'pl', content: 'ar' } } );
const context = new Context( { language: { ui: 'pl', content: 'ar' }, translations: { pl: {} } } );

expect( context.locale.uiLanguage ).to.equal( 'pl' );
expect( context.locale.contentLanguage ).to.equal( 'ar' );
} );

it( 'is configured with the config.language (just the content)', () => {
const context = new Context( { language: { content: 'ar' } } );
const context = new Context( { language: { content: 'ar' }, translations: { pl: {} } } );

expect( context.locale.uiLanguage ).to.equal( 'en' );
expect( context.locale.contentLanguage ).to.equal( 'ar' );
} );

it( 'is configured with the default config.language (when no translations provided)', () => {
const context = new Context( { language: 'pl' } );

expect( context.locale.uiLanguage ).to.equal( 'en' );
expect( context.locale.contentLanguage ).to.equal( 'en' );
} );

it( 'is configured with config.language (when no translations provided and dev-translations are matching)', () => {
window.CKEDITOR_TRANSLATIONS = {
en: { dictionary: {
key: ''
},
getPluralForm: () => '' }
};
const context = new Context( { language: { ui: 'en', content: 'en' } } );

expect( context.locale.uiLanguage ).to.equal( 'en' );
expect( context.locale.contentLanguage ).to.equal( 'en' );
} );

it( 'is configured with the config.translations', () => {
const context = new Context( {
translations: {
Expand Down
40 changes: 35 additions & 5 deletions packages/ckeditor5-core/tests/editor/editor.js
Original file line number Diff line number Diff line change
Expand Up @@ -351,7 +351,13 @@ describe( 'Editor', () => {

it( 'should use locale instance with a proper configuration passed as the argument to the constructor', () => {
const editor = new TestEditor( {
language: 'pl'
language: 'pl',
translations: [ {
pl: {
dictionary: [],
getPluralForm: sinon.spy()
}
} ]
} );

expect( editor.locale ).to.have.property( 'uiLanguage', 'pl' );
Expand All @@ -360,7 +366,13 @@ describe( 'Editor', () => {

it( 'should use locale instance with a proper configuration set as the defaultConfig option on the constructor', () => {
TestEditor.defaultConfig = {
language: 'pl'
language: 'pl',
translations: [ {
pl: {
dictionary: [],
getPluralForm: sinon.spy()
}
} ]
};

const editor = new TestEditor();
Expand All @@ -371,7 +383,13 @@ describe( 'Editor', () => {

it( 'should prefer the language passed as the argument to the constructor instead of the defaultConfig if both are set', () => {
TestEditor.defaultConfig = {
language: 'de'
language: 'de',
translations: [ {
pl: {
dictionary: [],
getPluralForm: sinon.spy()
}
} ]
};

const editor = new TestEditor( {
Expand All @@ -384,10 +402,22 @@ describe( 'Editor', () => {

it( 'should prefer the language from the context instead of the constructor config or defaultConfig if all are set', async () => {
TestEditor.defaultConfig = {
language: 'de'
language: 'de',
translations: [ {
pl: {
dictionary: [],
getPluralForm: sinon.spy()
}
} ]
};

const context = await Context.create( { language: 'pl' } );
const context = await Context.create( {
language: 'pl',
translations: [ { pl: {
dictionary: [],
getPluralForm: sinon.spy()
} } ]
} );
const editor = new TestEditor( { context, language: 'ru' } );

expect( editor.locale ).to.have.property( 'uiLanguage', 'pl' );
Expand Down
7 changes: 6 additions & 1 deletion packages/ckeditor5-font/tests/ui/colorui.js
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,12 @@ describe( 'ColorUI', () => {
return ClassicTestEditor
.create( element, {
plugins: [ Paragraph, TestColorPlugin, Undo ],
testColor: testColorConfig
testColor: testColorConfig,
language: 'en',
translations: [ { en: {
dictionary: [],
getPluralForm: sinon.spy()
} } ]
} )
.then( newEditor => {
editor = newEditor;
Expand Down
6 changes: 5 additions & 1 deletion packages/ckeditor5-indent/tests/indentui.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,11 @@ describe( 'IndentUI', () => {
rtlEditor = await ClassicTestEditor
.create( element, {
plugins: [ IndentUI, IndentEditing ],
language: 'ar'
language: 'ar',
translations: [ { ar: {
dictionary: [],
getPluralForm: sinon.spy()
} } ]
} );
} );

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1164,6 +1164,10 @@ describe( 'LegacyTodoListEditing', () => {
return VirtualTestEditor
.create( {
language: 'ar',
translations: [ { ar: {
dictionary: [],
getPluralForm: sinon.spy()
} } ],
plugins: [ Paragraph, LegacyTodoListEditing, Typing, BoldEditing, BlockQuoteEditing ]
} )
.then( newEditor => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -756,7 +756,11 @@ describe( 'table cell properties', () => {
beforeEach( async () => {
editor = await VirtualTestEditor.create( {
plugins: [ TableCellPropertiesEditing, Paragraph, TableEditing ],
language: 'ar'
language: 'ar',
translations: [ { ar: {
dictionary: [],
getPluralForm: sinon.spy()
} } ]
} );

model = editor.model;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2062,7 +2062,11 @@ describe( 'TableColumnResizeEditing', () => {
}

editor = await createEditor( {
language: 'ar'
language: 'ar',
translations: [ { ar: {
dictionary: [],
getPluralForm: sinon.spy()
} } ]
} );

model = editor.model;
Expand Down
6 changes: 5 additions & 1 deletion packages/ckeditor5-table/tests/tablekeyboard.js
Original file line number Diff line number Diff line change
Expand Up @@ -3602,7 +3602,11 @@ describe( 'TableKeyboard', () => {
return VirtualTestEditor
.create( {
plugins: [ TableEditing, TableKeyboard, TableSelection, Paragraph, ImageBlockEditing, MediaEmbedEditing ],
language: 'ar'
language: 'ar',
translations: [ { ar: {
dictionary: [],
getPluralForm: sinon.spy()
} } ]
} )
.then( newEditor => {
editor = newEditor;
Expand Down
6 changes: 5 additions & 1 deletion packages/ckeditor5-ui/tests/badge/badge.js
Original file line number Diff line number Diff line change
Expand Up @@ -378,7 +378,11 @@ describe( 'Badge', () => {

it( 'should position to the left side if the UI language is RTL and no side was configured', async () => {
const editor = await createEditor( element, {
language: 'ar'
language: 'ar',
translations: [ { ar: {
dictionary: [],
getPluralForm: sinon.spy()
} } ]
} );

badge = new BadgeExtended( editor );
Expand Down
6 changes: 5 additions & 1 deletion packages/ckeditor5-ui/tests/editorui/evaluationbadge.js
Original file line number Diff line number Diff line change
Expand Up @@ -520,7 +520,11 @@ describe( 'EvaluationBadge', () => {
it( 'should position the badge to the left right if the UI language is RTL (and powered-by is on the left)', async () => {
const editor = await createEditor( element, {
language: 'ar',
licenseKey: developmentLicenseKey
licenseKey: developmentLicenseKey,
translations: [ { ar: {
dictionary: [],
getPluralForm: sinon.spy()
} } ]
} );

testUtils.sinon.stub( editor.ui.getEditableElement( 'main' ), 'getBoundingClientRect' ).returns( {
Expand Down
6 changes: 5 additions & 1 deletion packages/ckeditor5-ui/tests/editorui/poweredby.js
Original file line number Diff line number Diff line change
Expand Up @@ -565,7 +565,11 @@ describe( 'PoweredBy', () => {

it( 'should position the to the left side if the UI language is RTL and no side was configured', async () => {
const editor = await createEditor( element, {
language: 'ar'
language: 'ar',
translations: [ { ar: {
dictionary: [],
getPluralForm: sinon.spy()
} } ]
} );

testUtils.sinon.stub( editor.ui.getEditableElement( 'main' ), 'getBoundingClientRect' ).returns( {
Expand Down
16 changes: 13 additions & 3 deletions packages/ckeditor5-undo/tests/undoui.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@ describe( 'UndoUI', () => {
editorElement = document.createElement( 'div' );
document.body.appendChild( editorElement );

return ClassicTestEditor.create( editorElement, { plugins: [ UndoEditing, UndoUI ] } )
return ClassicTestEditor.create( editorElement, { plugins: [ UndoEditing, UndoUI ],
language: { ui: 'en' }
} )
.then( newEditor => {
editor = newEditor;
} );
Expand Down Expand Up @@ -108,7 +110,11 @@ describe( 'UndoUI', () => {
return ClassicTestEditor
.create( element, {
plugins: [ UndoEditing, UndoUI ],
language: 'ar'
language: 'ar',
translations: [ { ar: {
dictionary: [],
getPluralForm: sinon.spy()
} } ]
} )
.then( newEditor => {
const undoButton = newEditor.ui.componentFactory.create( 'undo' );
Expand All @@ -129,7 +135,11 @@ describe( 'UndoUI', () => {
return ClassicTestEditor
.create( element, {
plugins: [ UndoEditing, UndoUI ],
language: 'ar'
language: 'ar',
translations: [ { ar: {
dictionary: [],
getPluralForm: sinon.spy()
} } ]
} )
.then( newEditor => {
const redoButton = newEditor.ui.componentFactory.create( 'redo' );
Expand Down
Loading