Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
45 changes: 40 additions & 5 deletions packages/ckeditor5-core/src/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
Collection,
CKEditorError,
Locale,
global,
type LocaleTranslate
} from '@ckeditor/ckeditor5-utils';

Expand All @@ -20,6 +21,8 @@ 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}
* or plugins that work outside the editor. Use it instead of {@link module:core/editor/editor~Editor.create `Editor.create()`}
Expand Down Expand Up @@ -160,11 +163,43 @@ export 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 ) {
/**
* When translations are not provided directly but available via global.window.CKEDITOR_TRANSLATIONS
* (which can be set by CKEditorTranslationsPlugin during manual tests with --language flag,
* or loaded from CDN translation files), we need to ensure the config.language.ui value is properly set.
*
* When translations are loaded via the global variable, the config.language.ui might be missing
* or incorrect, which can cause issues with utilities that depend on the language configuration
* (e.g., date formatting utilities). To fix this, we check if the configured language has matching
* translations, and if not, fall back to the first available language from the global translations object.
*
* Note: The _translate function from translation-service.ts gets translations from
* global.window.CKEDITOR_TRANSLATIONS when translations injected into Locale are empty.
* Since _translate is called often and has no access to the editor config, this is the better place
* to check if translations will be taken from the global variable and update config.language.ui accordingly.
*/
const globalTranslations = cloneDeep( global.window.CKEDITOR_TRANSLATIONS );
const uiLanguageFromConfig = typeof languageConfig === 'string' ? languageConfig : languageConfig.ui;
const hasMatchingTranslations = globalTranslations[ uiLanguageFromConfig! ];
const defaultGlobalTranslationsLanguage = Object.keys( globalTranslations )[ 0 ];

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

if ( !hasMatchingTranslations && this.config.get( 'language.ui' ) !== defaultGlobalTranslationsLanguage ) {
this.config.define( 'language.ui', defaultGlobalTranslationsLanguage );
}
} 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( 'FontColorUIBase', () => {
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 @@ -889,7 +889,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 @@ -2077,7 +2077,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