diff --git a/packages/ckeditor5-alignment/tests/alignmentui.js b/packages/ckeditor5-alignment/tests/alignmentui.js index ec4011e023f..b470a0af523 100644 --- a/packages/ckeditor5-alignment/tests/alignmentui.js +++ b/packages/ckeditor5-alignment/tests/alignmentui.js @@ -303,6 +303,10 @@ describe( 'Alignment UI', () => { content: 'ar', ui: 'ar' }, + translations: [ { ar: { + dictionary: [], + getPluralForm: sinon.spy() + } } ], plugins: [ AlignmentEditing, AlignmentUI ] } ); @@ -437,6 +441,10 @@ describe( 'Alignment UI', () => { language: { content: 'ar' }, + translations: [ { ar: { + dictionary: [], + getPluralForm: sinon.spy() + } } ], plugins: [ AlignmentEditing, AlignmentUI ], alignment: { options: [ 'center', 'justify' ] } } ) diff --git a/packages/ckeditor5-ckbox/tests/ckboxutils.js b/packages/ckeditor5-ckbox/tests/ckboxutils.js index 4eda62a6a62..9ce87e22f66 100644 --- a/packages/ckeditor5-ckbox/tests/ckboxutils.js +++ b/packages/ckeditor5-ckbox/tests/ckboxutils.js @@ -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' } diff --git a/packages/ckeditor5-ckfinder/tests/ckfindercommand.js b/packages/ckeditor5-ckfinder/tests/ckfindercommand.js index c4f4771b4cd..dac2e1958ab 100644 --- a/packages/ckeditor5-ckfinder/tests/ckfindercommand.js +++ b/packages/ckeditor5-ckfinder/tests/ckfindercommand.js @@ -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; diff --git a/packages/ckeditor5-core/src/context.ts b/packages/ckeditor5-core/src/context.ts index 1c39c2d264e..8ba6f37cc09 100644 --- a/packages/ckeditor5-core/src/context.ts +++ b/packages/ckeditor5-core/src/context.ts @@ -12,6 +12,7 @@ import { Collection, CKEditorError, Locale, + global, type LocaleTranslate } from '@ckeditor/ckeditor5-utils'; @@ -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()`} @@ -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; diff --git a/packages/ckeditor5-core/tests/context.js b/packages/ckeditor5-core/tests/context.js index dd82a04d13d..fe5ec267583 100644 --- a/packages/ckeditor5-core/tests/context.js +++ b/packages/ckeditor5-core/tests/context.js @@ -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: { diff --git a/packages/ckeditor5-core/tests/editor/editor.js b/packages/ckeditor5-core/tests/editor/editor.js index e6ab49e8e35..7204394690e 100644 --- a/packages/ckeditor5-core/tests/editor/editor.js +++ b/packages/ckeditor5-core/tests/editor/editor.js @@ -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' ); @@ -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(); @@ -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( { @@ -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' ); diff --git a/packages/ckeditor5-font/tests/ui/colorui.js b/packages/ckeditor5-font/tests/ui/colorui.js index 987d7930b61..ecc9c3c6e76 100644 --- a/packages/ckeditor5-font/tests/ui/colorui.js +++ b/packages/ckeditor5-font/tests/ui/colorui.js @@ -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; diff --git a/packages/ckeditor5-indent/tests/indentui.js b/packages/ckeditor5-indent/tests/indentui.js index b73e4fa37a5..4dc8909e609 100644 --- a/packages/ckeditor5-indent/tests/indentui.js +++ b/packages/ckeditor5-indent/tests/indentui.js @@ -26,7 +26,11 @@ describe( 'IndentUI', () => { rtlEditor = await ClassicTestEditor .create( element, { plugins: [ IndentUI, IndentEditing ], - language: 'ar' + language: 'ar', + translations: [ { ar: { + dictionary: [], + getPluralForm: sinon.spy() + } } ] } ); } ); diff --git a/packages/ckeditor5-list/tests/legacytodolist/legacytodolistediting.js b/packages/ckeditor5-list/tests/legacytodolist/legacytodolistediting.js index 27f2d7e8d6d..20f6fe54b3e 100644 --- a/packages/ckeditor5-list/tests/legacytodolist/legacytodolistediting.js +++ b/packages/ckeditor5-list/tests/legacytodolist/legacytodolistediting.js @@ -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 => { diff --git a/packages/ckeditor5-table/tests/tablecellproperties/tablecellpropertiesediting.js b/packages/ckeditor5-table/tests/tablecellproperties/tablecellpropertiesediting.js index f979638d54c..2a53dc74178 100644 --- a/packages/ckeditor5-table/tests/tablecellproperties/tablecellpropertiesediting.js +++ b/packages/ckeditor5-table/tests/tablecellproperties/tablecellpropertiesediting.js @@ -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; diff --git a/packages/ckeditor5-table/tests/tablecolumnresize/tablecolumnresizeediting.js b/packages/ckeditor5-table/tests/tablecolumnresize/tablecolumnresizeediting.js index 8740a447a94..16a53fcaa17 100644 --- a/packages/ckeditor5-table/tests/tablecolumnresize/tablecolumnresizeediting.js +++ b/packages/ckeditor5-table/tests/tablecolumnresize/tablecolumnresizeediting.js @@ -2077,7 +2077,11 @@ describe( 'TableColumnResizeEditing', () => { } editor = await createEditor( { - language: 'ar' + language: 'ar', + translations: [ { ar: { + dictionary: [], + getPluralForm: sinon.spy() + } } ] } ); model = editor.model; diff --git a/packages/ckeditor5-table/tests/tablekeyboard.js b/packages/ckeditor5-table/tests/tablekeyboard.js index 5cc04d47731..c2dbaebe331 100644 --- a/packages/ckeditor5-table/tests/tablekeyboard.js +++ b/packages/ckeditor5-table/tests/tablekeyboard.js @@ -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; diff --git a/packages/ckeditor5-ui/tests/badge/badge.js b/packages/ckeditor5-ui/tests/badge/badge.js index a6c69bfcc16..224958300bc 100644 --- a/packages/ckeditor5-ui/tests/badge/badge.js +++ b/packages/ckeditor5-ui/tests/badge/badge.js @@ -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 ); diff --git a/packages/ckeditor5-ui/tests/editorui/evaluationbadge.js b/packages/ckeditor5-ui/tests/editorui/evaluationbadge.js index e199831facb..ef6fbb32bc0 100644 --- a/packages/ckeditor5-ui/tests/editorui/evaluationbadge.js +++ b/packages/ckeditor5-ui/tests/editorui/evaluationbadge.js @@ -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( { diff --git a/packages/ckeditor5-ui/tests/editorui/poweredby.js b/packages/ckeditor5-ui/tests/editorui/poweredby.js index 6865d17f1a1..9ba0fa7a80d 100644 --- a/packages/ckeditor5-ui/tests/editorui/poweredby.js +++ b/packages/ckeditor5-ui/tests/editorui/poweredby.js @@ -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( { diff --git a/packages/ckeditor5-undo/tests/undoui.js b/packages/ckeditor5-undo/tests/undoui.js index 6f4e412afc1..06293b7875d 100644 --- a/packages/ckeditor5-undo/tests/undoui.js +++ b/packages/ckeditor5-undo/tests/undoui.js @@ -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; } ); @@ -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' ); @@ -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' );