ckeditor5-symfony 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (174) hide show
  1. package/dist/ckeditor5-symfony-error.d.ts +7 -0
  2. package/dist/ckeditor5-symfony-error.d.ts.map +1 -0
  3. package/dist/elements/context/context.d.ts +18 -0
  4. package/dist/elements/context/context.d.ts.map +1 -0
  5. package/dist/elements/context/contexts-registry.d.ts +9 -0
  6. package/dist/elements/context/contexts-registry.d.ts.map +1 -0
  7. package/dist/elements/context/index.d.ts +4 -0
  8. package/dist/elements/context/index.d.ts.map +1 -0
  9. package/dist/elements/context/typings.d.ts +34 -0
  10. package/dist/elements/context/typings.d.ts.map +1 -0
  11. package/dist/elements/editable.d.ts +18 -0
  12. package/dist/elements/editable.d.ts.map +1 -0
  13. package/dist/elements/editor/custom-editor-plugins.d.ts +54 -0
  14. package/dist/elements/editor/custom-editor-plugins.d.ts.map +1 -0
  15. package/dist/elements/editor/editor.d.ts +23 -0
  16. package/dist/elements/editor/editor.d.ts.map +1 -0
  17. package/dist/elements/editor/editors-registry.d.ts +9 -0
  18. package/dist/elements/editor/editors-registry.d.ts.map +1 -0
  19. package/dist/elements/editor/index.d.ts +3 -0
  20. package/dist/elements/editor/index.d.ts.map +1 -0
  21. package/dist/elements/editor/plugins/index.d.ts +2 -0
  22. package/dist/elements/editor/plugins/index.d.ts.map +1 -0
  23. package/dist/elements/editor/plugins/sync-editor-with-input.d.ts +6 -0
  24. package/dist/elements/editor/plugins/sync-editor-with-input.d.ts.map +1 -0
  25. package/dist/elements/editor/typings.d.ts +99 -0
  26. package/dist/elements/editor/typings.d.ts.map +1 -0
  27. package/dist/elements/editor/utils/create-editor-in-context.d.ts +44 -0
  28. package/dist/elements/editor/utils/create-editor-in-context.d.ts.map +1 -0
  29. package/dist/elements/editor/utils/index.d.ts +12 -0
  30. package/dist/elements/editor/utils/index.d.ts.map +1 -0
  31. package/dist/elements/editor/utils/is-single-root-editor.d.ts +9 -0
  32. package/dist/elements/editor/utils/is-single-root-editor.d.ts.map +1 -0
  33. package/dist/elements/editor/utils/load-editor-constructor.d.ts +9 -0
  34. package/dist/elements/editor/utils/load-editor-constructor.d.ts.map +1 -0
  35. package/dist/elements/editor/utils/load-editor-plugins.d.ts +20 -0
  36. package/dist/elements/editor/utils/load-editor-plugins.d.ts.map +1 -0
  37. package/dist/elements/editor/utils/load-editor-translations.d.ts +14 -0
  38. package/dist/elements/editor/utils/load-editor-translations.d.ts.map +1 -0
  39. package/dist/elements/editor/utils/normalize-custom-translations.d.ts +11 -0
  40. package/dist/elements/editor/utils/normalize-custom-translations.d.ts.map +1 -0
  41. package/dist/elements/editor/utils/query-all-editor-ids.d.ts +5 -0
  42. package/dist/elements/editor/utils/query-all-editor-ids.d.ts.map +1 -0
  43. package/dist/elements/editor/utils/query-editor-editables.d.ts +25 -0
  44. package/dist/elements/editor/utils/query-editor-editables.d.ts.map +1 -0
  45. package/dist/elements/editor/utils/resolve-editor-config-elements-references.d.ts +9 -0
  46. package/dist/elements/editor/utils/resolve-editor-config-elements-references.d.ts.map +1 -0
  47. package/dist/elements/editor/utils/set-editor-editable-height.d.ts +9 -0
  48. package/dist/elements/editor/utils/set-editor-editable-height.d.ts.map +1 -0
  49. package/dist/elements/editor/utils/wrap-with-watchdog.d.ts +24 -0
  50. package/dist/elements/editor/utils/wrap-with-watchdog.d.ts.map +1 -0
  51. package/dist/elements/index.d.ts +6 -0
  52. package/dist/elements/index.d.ts.map +1 -0
  53. package/dist/elements/register-custom-elements.d.ts +5 -0
  54. package/dist/elements/register-custom-elements.d.ts.map +1 -0
  55. package/dist/elements/ui-part.d.ts +18 -0
  56. package/dist/elements/ui-part.d.ts.map +1 -0
  57. package/dist/index.cjs +5 -0
  58. package/dist/index.cjs.map +1 -0
  59. package/dist/index.d.ts +3 -0
  60. package/dist/index.d.ts.map +1 -0
  61. package/dist/index.mjs +1089 -0
  62. package/dist/index.mjs.map +1 -0
  63. package/dist/shared/async-registry.d.ts +136 -0
  64. package/dist/shared/async-registry.d.ts.map +1 -0
  65. package/dist/shared/camel-case.d.ts +8 -0
  66. package/dist/shared/camel-case.d.ts.map +1 -0
  67. package/dist/shared/debounce.d.ts +2 -0
  68. package/dist/shared/debounce.d.ts.map +1 -0
  69. package/dist/shared/deep-camel-case-keys.d.ts +8 -0
  70. package/dist/shared/deep-camel-case-keys.d.ts.map +1 -0
  71. package/dist/shared/filter-object-values.d.ts +9 -0
  72. package/dist/shared/filter-object-values.d.ts.map +1 -0
  73. package/dist/shared/index.d.ts +15 -0
  74. package/dist/shared/index.d.ts.map +1 -0
  75. package/dist/shared/is-empty-object.d.ts +2 -0
  76. package/dist/shared/is-empty-object.d.ts.map +1 -0
  77. package/dist/shared/is-plain-object.d.ts +8 -0
  78. package/dist/shared/is-plain-object.d.ts.map +1 -0
  79. package/dist/shared/map-object-values.d.ts +11 -0
  80. package/dist/shared/map-object-values.d.ts.map +1 -0
  81. package/dist/shared/once.d.ts +2 -0
  82. package/dist/shared/once.d.ts.map +1 -0
  83. package/dist/shared/shallow-equal.d.ts +9 -0
  84. package/dist/shared/shallow-equal.d.ts.map +1 -0
  85. package/dist/shared/timeout.d.ts +8 -0
  86. package/dist/shared/timeout.d.ts.map +1 -0
  87. package/dist/shared/uid.d.ts +7 -0
  88. package/dist/shared/uid.d.ts.map +1 -0
  89. package/dist/shared/wait-for-dom-ready.d.ts +5 -0
  90. package/dist/shared/wait-for-dom-ready.d.ts.map +1 -0
  91. package/dist/shared/wait-for.d.ts +20 -0
  92. package/dist/shared/wait-for.d.ts.map +1 -0
  93. package/dist/types/can-be-promise.type.d.ts +2 -0
  94. package/dist/types/can-be-promise.type.d.ts.map +1 -0
  95. package/dist/types/index.d.ts +3 -0
  96. package/dist/types/index.d.ts.map +1 -0
  97. package/dist/types/required-by.type.d.ts +2 -0
  98. package/dist/types/required-by.type.d.ts.map +1 -0
  99. package/package.json +40 -0
  100. package/src/ckeditor5-symfony-error.ts +9 -0
  101. package/src/elements/context/context.test.ts +291 -0
  102. package/src/elements/context/context.ts +99 -0
  103. package/src/elements/context/contexts-registry.test.ts +10 -0
  104. package/src/elements/context/contexts-registry.ts +10 -0
  105. package/src/elements/context/index.ts +3 -0
  106. package/src/elements/context/typings.ts +39 -0
  107. package/src/elements/editable.test.ts +334 -0
  108. package/src/elements/editable.ts +114 -0
  109. package/src/elements/editor/custom-editor-plugins.test.ts +103 -0
  110. package/src/elements/editor/custom-editor-plugins.ts +86 -0
  111. package/src/elements/editor/editor.test.ts +438 -0
  112. package/src/elements/editor/editor.ts +279 -0
  113. package/src/elements/editor/editors-registry.test.ts +10 -0
  114. package/src/elements/editor/editors-registry.ts +10 -0
  115. package/src/elements/editor/index.ts +2 -0
  116. package/src/elements/editor/plugins/index.ts +1 -0
  117. package/src/elements/editor/plugins/sync-editor-with-input.ts +78 -0
  118. package/src/elements/editor/typings.ts +114 -0
  119. package/src/elements/editor/utils/create-editor-in-context.ts +90 -0
  120. package/src/elements/editor/utils/index.ts +11 -0
  121. package/src/elements/editor/utils/is-single-root-editor.test.ts +40 -0
  122. package/src/elements/editor/utils/is-single-root-editor.ts +11 -0
  123. package/src/elements/editor/utils/load-editor-constructor.test.ts +62 -0
  124. package/src/elements/editor/utils/load-editor-constructor.ts +29 -0
  125. package/src/elements/editor/utils/load-editor-plugins.test.ts +100 -0
  126. package/src/elements/editor/utils/load-editor-plugins.ts +73 -0
  127. package/src/elements/editor/utils/load-editor-translations.ts +233 -0
  128. package/src/elements/editor/utils/normalize-custom-translations.test.ts +152 -0
  129. package/src/elements/editor/utils/normalize-custom-translations.ts +18 -0
  130. package/src/elements/editor/utils/query-all-editor-ids.ts +9 -0
  131. package/src/elements/editor/utils/query-editor-editables.ts +101 -0
  132. package/src/elements/editor/utils/resolve-editor-config-elements-references.test.ts +93 -0
  133. package/src/elements/editor/utils/resolve-editor-config-elements-references.ts +36 -0
  134. package/src/elements/editor/utils/set-editor-editable-height.test.ts +131 -0
  135. package/src/elements/editor/utils/set-editor-editable-height.ts +15 -0
  136. package/src/elements/editor/utils/wrap-with-watchdog.test.ts +45 -0
  137. package/src/elements/editor/utils/wrap-with-watchdog.ts +51 -0
  138. package/src/elements/index.ts +14 -0
  139. package/src/elements/register-custom-elements.ts +24 -0
  140. package/src/elements/ui-part.test.ts +142 -0
  141. package/src/elements/ui-part.ts +80 -0
  142. package/src/index.ts +6 -0
  143. package/src/shared/async-registry.test.ts +737 -0
  144. package/src/shared/async-registry.ts +353 -0
  145. package/src/shared/camel-case.test.ts +35 -0
  146. package/src/shared/camel-case.ts +11 -0
  147. package/src/shared/debounce.test.ts +72 -0
  148. package/src/shared/debounce.ts +16 -0
  149. package/src/shared/deep-camel-case-keys.test.ts +34 -0
  150. package/src/shared/deep-camel-case-keys.ts +26 -0
  151. package/src/shared/filter-object-values.test.ts +25 -0
  152. package/src/shared/filter-object-values.ts +17 -0
  153. package/src/shared/index.ts +14 -0
  154. package/src/shared/is-empty-object.test.ts +78 -0
  155. package/src/shared/is-empty-object.ts +3 -0
  156. package/src/shared/is-plain-object.test.ts +38 -0
  157. package/src/shared/is-plain-object.ts +15 -0
  158. package/src/shared/map-object-values.test.ts +29 -0
  159. package/src/shared/map-object-values.ts +19 -0
  160. package/src/shared/once.test.ts +116 -0
  161. package/src/shared/once.ts +12 -0
  162. package/src/shared/shallow-equal.test.ts +51 -0
  163. package/src/shared/shallow-equal.ts +30 -0
  164. package/src/shared/timeout.test.ts +65 -0
  165. package/src/shared/timeout.ts +13 -0
  166. package/src/shared/uid.test.ts +25 -0
  167. package/src/shared/uid.ts +8 -0
  168. package/src/shared/wait-for-dom-ready.test.ts +87 -0
  169. package/src/shared/wait-for-dom-ready.ts +21 -0
  170. package/src/shared/wait-for.test.ts +24 -0
  171. package/src/shared/wait-for.ts +56 -0
  172. package/src/types/can-be-promise.type.ts +1 -0
  173. package/src/types/index.ts +2 -0
  174. package/src/types/required-by.type.ts +1 -0
@@ -0,0 +1,62 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+
3
+ import type { EditorType } from '../typings';
4
+
5
+ import { loadEditorConstructor } from './load-editor-constructor';
6
+
7
+ // Mock the ckeditor5 package
8
+ vi.mock('ckeditor5', async () => {
9
+ const mockEditorConstructor = vi.fn();
10
+
11
+ return {
12
+ InlineEditor: mockEditorConstructor,
13
+ BalloonEditor: mockEditorConstructor,
14
+ ClassicEditor: mockEditorConstructor,
15
+ DecoupledEditor: mockEditorConstructor,
16
+ MultiRootEditor: mockEditorConstructor,
17
+ };
18
+ });
19
+
20
+ describe('loadEditorConstructor', () => {
21
+ it('should load InlineEditor for inline type', async () => {
22
+ const constructor = await loadEditorConstructor('inline');
23
+ expect(constructor).toBeDefined();
24
+ });
25
+
26
+ it('should load BalloonEditor for balloon type', async () => {
27
+ const constructor = await loadEditorConstructor('balloon');
28
+ expect(constructor).toBeDefined();
29
+ });
30
+
31
+ it('should load ClassicEditor for classic type', async () => {
32
+ const constructor = await loadEditorConstructor('classic');
33
+ expect(constructor).toBeDefined();
34
+ });
35
+
36
+ it('should load DecoupledEditor for decoupled type', async () => {
37
+ const constructor = await loadEditorConstructor('decoupled');
38
+ expect(constructor).toBeDefined();
39
+ });
40
+
41
+ it('should load MultiRootEditor for multiroot type', async () => {
42
+ const constructor = await loadEditorConstructor('multiroot');
43
+ expect(constructor).toBeDefined();
44
+ });
45
+
46
+ it('should throw error for unsupported editor type', async () => {
47
+ const invalidType = 'invalid' as EditorType;
48
+
49
+ await expect(loadEditorConstructor(invalidType)).rejects.toThrow(
50
+ 'Unsupported editor type: invalid',
51
+ );
52
+ });
53
+
54
+ it('should return the correct constructor for each editor type', async () => {
55
+ const editorTypes: EditorType[] = ['inline', 'balloon', 'classic', 'decoupled', 'multiroot'];
56
+
57
+ for (const type of editorTypes) {
58
+ const constructor = await loadEditorConstructor(type);
59
+ expect(constructor).toBeDefined();
60
+ }
61
+ });
62
+ });
@@ -0,0 +1,29 @@
1
+ import type { EditorType } from '../typings';
2
+
3
+ import { CKEditor5SymfonyError } from '../../../ckeditor5-symfony-error';
4
+
5
+ /**
6
+ * Returns the constructor for the specified CKEditor5 editor type.
7
+ *
8
+ * @param type - The type of the editor to load.
9
+ * @returns A promise that resolves to the editor constructor.
10
+ */
11
+ export async function loadEditorConstructor(type: EditorType) {
12
+ const PKG = await import('ckeditor5');
13
+
14
+ const editorMap = {
15
+ inline: PKG.InlineEditor,
16
+ balloon: PKG.BalloonEditor,
17
+ classic: PKG.ClassicEditor,
18
+ decoupled: PKG.DecoupledEditor,
19
+ multiroot: PKG.MultiRootEditor,
20
+ } as const;
21
+
22
+ const EditorConstructor = editorMap[type];
23
+
24
+ if (!EditorConstructor) {
25
+ throw new CKEditor5SymfonyError(`Unsupported editor type: ${type}`);
26
+ }
27
+
28
+ return EditorConstructor;
29
+ }
@@ -0,0 +1,100 @@
1
+ import { afterEach, describe, expect, it } from 'vitest';
2
+
3
+ import type { EditorPlugin } from '../typings';
4
+
5
+ import { CustomEditorPluginsRegistry } from '../custom-editor-plugins';
6
+ import { loadEditorPlugins } from './load-editor-plugins';
7
+
8
+ class CustomPlugin {
9
+ static get pluginName() {
10
+ return 'CustomPlugin';
11
+ }
12
+ }
13
+
14
+ describe('loadEditorPlugins', () => {
15
+ it('should load plugins from base package', async () => {
16
+ const plugins: EditorPlugin[] = ['Bold', 'Italic'];
17
+ const { loadedPlugins } = await loadEditorPlugins(plugins);
18
+
19
+ expect(loadedPlugins).toHaveLength(2);
20
+ expect(loadedPlugins[0]).toBeDefined();
21
+ expect(loadedPlugins[1]).toBeDefined();
22
+ });
23
+
24
+ it('should load plugins from premium package', async () => {
25
+ const plugins: EditorPlugin[] = ['ExportInlineStyles', 'Comments'];
26
+ const { loadedPlugins } = await loadEditorPlugins(plugins);
27
+
28
+ expect(loadedPlugins).toHaveLength(2);
29
+ expect(loadedPlugins[0]).toBeDefined();
30
+ expect(loadedPlugins[1]).toBeDefined();
31
+ });
32
+
33
+ it('should handle empty plugin array', async () => {
34
+ const plugins: EditorPlugin[] = [];
35
+ const { loadedPlugins } = await loadEditorPlugins(plugins);
36
+
37
+ expect(loadedPlugins).toHaveLength(0);
38
+ });
39
+
40
+ it('should prioritize base package over premium package', async () => {
41
+ const plugins: EditorPlugin[] = ['Bold'];
42
+ const { loadedPlugins } = await loadEditorPlugins(plugins);
43
+
44
+ expect(loadedPlugins).toHaveLength(1);
45
+ expect(loadedPlugins[0]).toBeDefined();
46
+ });
47
+
48
+ it('should return plugin constructors', async () => {
49
+ const plugins: EditorPlugin[] = ['Bold'];
50
+ const { loadedPlugins } = await loadEditorPlugins(plugins);
51
+
52
+ expect(loadedPlugins).toHaveLength(1);
53
+ expect(typeof loadedPlugins[0]).toBe('function');
54
+ });
55
+
56
+ it('should throw an error for unknown plugins', async () => {
57
+ const plugins: EditorPlugin[] = ['UnknownPlugin'];
58
+ await expect(loadEditorPlugins(plugins)).rejects.toThrowError(/not found/);
59
+ });
60
+
61
+ describe('custom plugins', () => {
62
+ afterEach(() => {
63
+ CustomEditorPluginsRegistry.the.unregisterAll();
64
+ });
65
+
66
+ it('should load custom plugins', async () => {
67
+ CustomEditorPluginsRegistry.the.register('CustomPlugin', () => CustomPlugin);
68
+
69
+ const plugins: EditorPlugin[] = ['CustomPlugin'];
70
+ const { loadedPlugins } = await loadEditorPlugins(plugins);
71
+
72
+ expect(loadedPlugins).toHaveLength(1);
73
+ expect(loadedPlugins[0]).toEqual(CustomPlugin);
74
+ });
75
+
76
+ it('should prefer loading custom plugins over base package', async () => {
77
+ CustomEditorPluginsRegistry.the.register('Bold', () => CustomPlugin);
78
+
79
+ const plugins: EditorPlugin[] = ['Bold'];
80
+ const { loadedPlugins } = await loadEditorPlugins(plugins);
81
+
82
+ expect(loadedPlugins).toHaveLength(1);
83
+ expect(loadedPlugins[0]).toEqual(CustomPlugin);
84
+ });
85
+
86
+ it('should be possible to load async custom plugins', async () => {
87
+ CustomEditorPluginsRegistry.the.register('AsyncPlugin', async () => {
88
+ return new Promise((resolve) => {
89
+ setTimeout(() => resolve(CustomPlugin), 20);
90
+ });
91
+ });
92
+
93
+ const plugins: EditorPlugin[] = ['AsyncPlugin'];
94
+ const { loadedPlugins } = await loadEditorPlugins(plugins);
95
+
96
+ expect(loadedPlugins).toHaveLength(1);
97
+ expect(loadedPlugins[0]).toEqual(CustomPlugin);
98
+ });
99
+ });
100
+ });
@@ -0,0 +1,73 @@
1
+ import type { PluginConstructor } from 'ckeditor5';
2
+
3
+ import type { EditorPlugin } from '../typings';
4
+
5
+ import { CKEditor5SymfonyError } from '../../../ckeditor5-symfony-error';
6
+ import { CustomEditorPluginsRegistry } from '../custom-editor-plugins';
7
+
8
+ /**
9
+ * Loads CKEditor plugins from base and premium packages.
10
+ * First tries to load from the base 'ckeditor5' package, then falls back to 'ckeditor5-premium-features'.
11
+ *
12
+ * @param plugins - Array of plugin names to load
13
+ * @returns Promise that resolves to an array of loaded Plugin instances
14
+ * @throws Error if a plugin is not found in either package
15
+ */
16
+ export async function loadEditorPlugins(plugins: EditorPlugin[]): Promise<LoadedPlugins> {
17
+ const basePackage = await import('ckeditor5');
18
+ let premiumPackage: Record<string, any> | null = null;
19
+
20
+ const loaders = plugins.map(async (plugin) => {
21
+ // Let's first try to load the plugin from the base package.
22
+ // Coverage is disabled due to Vitest issues with mocking dynamic imports.
23
+
24
+ // If the plugin is not found in the base package, try custom plugins.
25
+ const customPlugin = await CustomEditorPluginsRegistry.the.get(plugin);
26
+
27
+ if (customPlugin) {
28
+ return customPlugin;
29
+ }
30
+
31
+ // If not found, try to load from the base package.
32
+ const { [plugin]: basePkgImport } = basePackage as Record<string, unknown>;
33
+
34
+ if (basePkgImport) {
35
+ return basePkgImport as PluginConstructor;
36
+ }
37
+
38
+ // Plugin not found in base package, try premium package.
39
+ if (!premiumPackage) {
40
+ try {
41
+ premiumPackage = await import('ckeditor5-premium-features');
42
+ /* v8 ignore next 6 */
43
+ }
44
+ catch (error) {
45
+ console.error(`Failed to load premium package: ${error}`);
46
+ throw new CKEditor5SymfonyError(`Plugin "${plugin}" not found in base package and failed to load premium package.`);
47
+ }
48
+ }
49
+
50
+ /* v8 ignore next */
51
+ const { [plugin]: premiumPkgImport } = premiumPackage || {};
52
+
53
+ if (premiumPkgImport) {
54
+ return premiumPkgImport as PluginConstructor;
55
+ }
56
+
57
+ // Plugin not found in either package, throw an error.
58
+ throw new CKEditor5SymfonyError(`Plugin "${plugin}" not found in base or premium packages.`);
59
+ });
60
+
61
+ return {
62
+ loadedPlugins: await Promise.all(loaders),
63
+ hasPremium: !!premiumPackage,
64
+ };
65
+ }
66
+
67
+ /**
68
+ * Type representing the loaded plugins and whether premium features are available.
69
+ */
70
+ type LoadedPlugins = {
71
+ loadedPlugins: PluginConstructor<any>[];
72
+ hasPremium: boolean;
73
+ };
@@ -0,0 +1,233 @@
1
+ /**
2
+ * Loads all required translations for the editor based on the language configuration.
3
+ *
4
+ * @param language - The language configuration object containing UI and content language codes.
5
+ * @param language.ui - The UI language code.
6
+ * @param language.content - The content language code.
7
+ * @param hasPremium - Whether premium features are enabled and premium translations should be loaded.
8
+ * @returns A promise that resolves to an array of loaded translation objects.
9
+ */
10
+ export async function loadAllEditorTranslations(
11
+ language: { ui: string; content: string; },
12
+ hasPremium: boolean,
13
+ ) {
14
+ const translations = [language.ui, language.content];
15
+ const loadedTranslations = await Promise.all(
16
+ [
17
+ loadEditorPkgTranslations('ckeditor5', translations),
18
+ /* v8 ignore next */
19
+ hasPremium && loadEditorPkgTranslations('ckeditor5-premium-features', translations),
20
+ ].filter(pkg => !!pkg),
21
+ )
22
+ .then(translations => translations.flat());
23
+
24
+ return loadedTranslations;
25
+ }
26
+
27
+ /**
28
+ * Loads the editor translations for the given languages.
29
+ *
30
+ * Make sure this function is properly compiled and bundled in self hosted environments!
31
+ *
32
+ * @param pkg - The package to load translations from ('ckeditor5' or 'ckeditor5-premium-features').
33
+ * @param translations - The list of language codes to load translations for.
34
+ * @returns A promise that resolves to an array of loaded translation packs.
35
+ */
36
+ async function loadEditorPkgTranslations(
37
+ pkg: EditorPkgName,
38
+ translations: string[],
39
+ ) {
40
+ /* v8 ignore next */
41
+ return await Promise.all(
42
+ translations
43
+ .filter(lang => lang !== 'en') // 'en' is the default language, no need to load it.
44
+ .map(async (lang) => {
45
+ const pack = await loadEditorTranslation(pkg, lang);
46
+
47
+ /* v8 ignore next */
48
+ return pack?.default ?? pack;
49
+ })
50
+ .filter(Boolean),
51
+ );
52
+ }
53
+
54
+ /**
55
+ * Type representing the package name for CKEditor 5.
56
+ */
57
+ type EditorPkgName = 'ckeditor5' | 'ckeditor5-premium-features';
58
+
59
+ /**
60
+ * Load translation for CKEditor 5
61
+ * @param pkg - Package type: 'ckeditor5' or 'premium'
62
+ * @param lang - Language code (e.g., 'pl', 'en', 'de')
63
+ * @returns Translation object or null if failed
64
+ */
65
+ async function loadEditorTranslation(pkg: EditorPkgName, lang: string): Promise<any> {
66
+ try {
67
+ /* v8 ignore next 2 */
68
+ if (pkg === 'ckeditor5') {
69
+ /* v8 ignore next 79 */
70
+ switch (lang) {
71
+ case 'af': return await import('ckeditor5/translations/af.js');
72
+ case 'ar': return await import('ckeditor5/translations/ar.js');
73
+ case 'ast': return await import('ckeditor5/translations/ast.js');
74
+ case 'az': return await import('ckeditor5/translations/az.js');
75
+ case 'bg': return await import('ckeditor5/translations/bg.js');
76
+ case 'bn': return await import('ckeditor5/translations/bn.js');
77
+ case 'bs': return await import('ckeditor5/translations/bs.js');
78
+ case 'ca': return await import('ckeditor5/translations/ca.js');
79
+ case 'cs': return await import('ckeditor5/translations/cs.js');
80
+ case 'da': return await import('ckeditor5/translations/da.js');
81
+ case 'de': return await import('ckeditor5/translations/de.js');
82
+ case 'de-ch': return await import('ckeditor5/translations/de-ch.js');
83
+ case 'el': return await import('ckeditor5/translations/el.js');
84
+ case 'en': return await import('ckeditor5/translations/en.js');
85
+ case 'en-au': return await import('ckeditor5/translations/en-au.js');
86
+ case 'en-gb': return await import('ckeditor5/translations/en-gb.js');
87
+ case 'eo': return await import('ckeditor5/translations/eo.js');
88
+ case 'es': return await import('ckeditor5/translations/es.js');
89
+ case 'es-co': return await import('ckeditor5/translations/es-co.js');
90
+ case 'et': return await import('ckeditor5/translations/et.js');
91
+ case 'eu': return await import('ckeditor5/translations/eu.js');
92
+ case 'fa': return await import('ckeditor5/translations/fa.js');
93
+ case 'fi': return await import('ckeditor5/translations/fi.js');
94
+ case 'fr': return await import('ckeditor5/translations/fr.js');
95
+ case 'gl': return await import('ckeditor5/translations/gl.js');
96
+ case 'gu': return await import('ckeditor5/translations/gu.js');
97
+ case 'he': return await import('ckeditor5/translations/he.js');
98
+ case 'hi': return await import('ckeditor5/translations/hi.js');
99
+ case 'hr': return await import('ckeditor5/translations/hr.js');
100
+ case 'hu': return await import('ckeditor5/translations/hu.js');
101
+ case 'hy': return await import('ckeditor5/translations/hy.js');
102
+ case 'id': return await import('ckeditor5/translations/id.js');
103
+ case 'it': return await import('ckeditor5/translations/it.js');
104
+ case 'ja': return await import('ckeditor5/translations/ja.js');
105
+ case 'jv': return await import('ckeditor5/translations/jv.js');
106
+ case 'kk': return await import('ckeditor5/translations/kk.js');
107
+ case 'km': return await import('ckeditor5/translations/km.js');
108
+ case 'kn': return await import('ckeditor5/translations/kn.js');
109
+ case 'ko': return await import('ckeditor5/translations/ko.js');
110
+ case 'ku': return await import('ckeditor5/translations/ku.js');
111
+ case 'lt': return await import('ckeditor5/translations/lt.js');
112
+ case 'lv': return await import('ckeditor5/translations/lv.js');
113
+ case 'ms': return await import('ckeditor5/translations/ms.js');
114
+ case 'nb': return await import('ckeditor5/translations/nb.js');
115
+ case 'ne': return await import('ckeditor5/translations/ne.js');
116
+ case 'nl': return await import('ckeditor5/translations/nl.js');
117
+ case 'no': return await import('ckeditor5/translations/no.js');
118
+ case 'oc': return await import('ckeditor5/translations/oc.js');
119
+ case 'pl': return await import('ckeditor5/translations/pl.js');
120
+ case 'pt': return await import('ckeditor5/translations/pt.js');
121
+ case 'pt-br': return await import('ckeditor5/translations/pt-br.js');
122
+ case 'ro': return await import('ckeditor5/translations/ro.js');
123
+ case 'ru': return await import('ckeditor5/translations/ru.js');
124
+ case 'si': return await import('ckeditor5/translations/si.js');
125
+ case 'sk': return await import('ckeditor5/translations/sk.js');
126
+ case 'sl': return await import('ckeditor5/translations/sl.js');
127
+ case 'sq': return await import('ckeditor5/translations/sq.js');
128
+ case 'sr': return await import('ckeditor5/translations/sr.js');
129
+ case 'sr-latn': return await import('ckeditor5/translations/sr-latn.js');
130
+ case 'sv': return await import('ckeditor5/translations/sv.js');
131
+ case 'th': return await import('ckeditor5/translations/th.js');
132
+ case 'tk': return await import('ckeditor5/translations/tk.js');
133
+ case 'tr': return await import('ckeditor5/translations/tr.js');
134
+ case 'tt': return await import('ckeditor5/translations/tt.js');
135
+ case 'ug': return await import('ckeditor5/translations/ug.js');
136
+ case 'uk': return await import('ckeditor5/translations/uk.js');
137
+ case 'ur': return await import('ckeditor5/translations/ur.js');
138
+ case 'uz': return await import('ckeditor5/translations/uz.js');
139
+ case 'vi': return await import('ckeditor5/translations/vi.js');
140
+ case 'zh': return await import('ckeditor5/translations/zh.js');
141
+ case 'zh-cn': return await import('ckeditor5/translations/zh-cn.js');
142
+ default:
143
+ console.warn(`Language ${lang} not found in ckeditor5 translations`);
144
+ return null;
145
+ }
146
+ }
147
+ /* v8 ignore next 79 */
148
+ else {
149
+ // Premium features translations
150
+ switch (lang) {
151
+ case 'af': return await import('ckeditor5-premium-features/translations/af.js');
152
+ case 'ar': return await import('ckeditor5-premium-features/translations/ar.js');
153
+ case 'ast': return await import('ckeditor5-premium-features/translations/ast.js');
154
+ case 'az': return await import('ckeditor5-premium-features/translations/az.js');
155
+ case 'bg': return await import('ckeditor5-premium-features/translations/bg.js');
156
+ case 'bn': return await import('ckeditor5-premium-features/translations/bn.js');
157
+ case 'bs': return await import('ckeditor5-premium-features/translations/bs.js');
158
+ case 'ca': return await import('ckeditor5-premium-features/translations/ca.js');
159
+ case 'cs': return await import('ckeditor5-premium-features/translations/cs.js');
160
+ case 'da': return await import('ckeditor5-premium-features/translations/da.js');
161
+ case 'de': return await import('ckeditor5-premium-features/translations/de.js');
162
+ case 'de-ch': return await import('ckeditor5-premium-features/translations/de-ch.js');
163
+ case 'el': return await import('ckeditor5-premium-features/translations/el.js');
164
+ case 'en': return await import('ckeditor5-premium-features/translations/en.js');
165
+ case 'en-au': return await import('ckeditor5-premium-features/translations/en-au.js');
166
+ case 'en-gb': return await import('ckeditor5-premium-features/translations/en-gb.js');
167
+ case 'eo': return await import('ckeditor5-premium-features/translations/eo.js');
168
+ case 'es': return await import('ckeditor5-premium-features/translations/es.js');
169
+ case 'es-co': return await import('ckeditor5-premium-features/translations/es-co.js');
170
+ case 'et': return await import('ckeditor5-premium-features/translations/et.js');
171
+ case 'eu': return await import('ckeditor5-premium-features/translations/eu.js');
172
+ case 'fa': return await import('ckeditor5-premium-features/translations/fa.js');
173
+ case 'fi': return await import('ckeditor5-premium-features/translations/fi.js');
174
+ case 'fr': return await import('ckeditor5-premium-features/translations/fr.js');
175
+ case 'gl': return await import('ckeditor5-premium-features/translations/gl.js');
176
+ case 'gu': return await import('ckeditor5-premium-features/translations/gu.js');
177
+ case 'he': return await import('ckeditor5-premium-features/translations/he.js');
178
+ case 'hi': return await import('ckeditor5-premium-features/translations/hi.js');
179
+ case 'hr': return await import('ckeditor5-premium-features/translations/hr.js');
180
+ case 'hu': return await import('ckeditor5-premium-features/translations/hu.js');
181
+ case 'hy': return await import('ckeditor5-premium-features/translations/hy.js');
182
+ case 'id': return await import('ckeditor5-premium-features/translations/id.js');
183
+ case 'it': return await import('ckeditor5-premium-features/translations/it.js');
184
+ case 'ja': return await import('ckeditor5-premium-features/translations/ja.js');
185
+ case 'jv': return await import('ckeditor5-premium-features/translations/jv.js');
186
+ case 'kk': return await import('ckeditor5-premium-features/translations/kk.js');
187
+ case 'km': return await import('ckeditor5-premium-features/translations/km.js');
188
+ case 'kn': return await import('ckeditor5-premium-features/translations/kn.js');
189
+ case 'ko': return await import('ckeditor5-premium-features/translations/ko.js');
190
+ case 'ku': return await import('ckeditor5-premium-features/translations/ku.js');
191
+ case 'lt': return await import('ckeditor5-premium-features/translations/lt.js');
192
+ case 'lv': return await import('ckeditor5-premium-features/translations/lv.js');
193
+ case 'ms': return await import('ckeditor5-premium-features/translations/ms.js');
194
+ case 'nb': return await import('ckeditor5-premium-features/translations/nb.js');
195
+ case 'ne': return await import('ckeditor5-premium-features/translations/ne.js');
196
+ case 'nl': return await import('ckeditor5-premium-features/translations/nl.js');
197
+ case 'no': return await import('ckeditor5-premium-features/translations/no.js');
198
+ case 'oc': return await import('ckeditor5-premium-features/translations/oc.js');
199
+ case 'pl': return await import('ckeditor5-premium-features/translations/pl.js');
200
+ case 'pt': return await import('ckeditor5-premium-features/translations/pt.js');
201
+ case 'pt-br': return await import('ckeditor5-premium-features/translations/pt-br.js');
202
+ case 'ro': return await import('ckeditor5-premium-features/translations/ro.js');
203
+ case 'ru': return await import('ckeditor5-premium-features/translations/ru.js');
204
+ case 'si': return await import('ckeditor5-premium-features/translations/si.js');
205
+ case 'sk': return await import('ckeditor5-premium-features/translations/sk.js');
206
+ case 'sl': return await import('ckeditor5-premium-features/translations/sl.js');
207
+ case 'sq': return await import('ckeditor5-premium-features/translations/sq.js');
208
+ case 'sr': return await import('ckeditor5-premium-features/translations/sr.js');
209
+ case 'sr-latn': return await import('ckeditor5-premium-features/translations/sr-latn.js');
210
+ case 'sv': return await import('ckeditor5-premium-features/translations/sv.js');
211
+ case 'th': return await import('ckeditor5-premium-features/translations/th.js');
212
+ case 'tk': return await import('ckeditor5-premium-features/translations/tk.js');
213
+ case 'tr': return await import('ckeditor5-premium-features/translations/tr.js');
214
+ case 'tt': return await import('ckeditor5-premium-features/translations/tt.js');
215
+ case 'ug': return await import('ckeditor5-premium-features/translations/ug.js');
216
+ case 'uk': return await import('ckeditor5-premium-features/translations/uk.js');
217
+ case 'ur': return await import('ckeditor5-premium-features/translations/ur.js');
218
+ case 'uz': return await import('ckeditor5-premium-features/translations/uz.js');
219
+ case 'vi': return await import('ckeditor5-premium-features/translations/vi.js');
220
+ case 'zh': return await import('ckeditor5-premium-features/translations/zh.js');
221
+ case 'zh-cn': return await import('ckeditor5-premium-features/translations/zh-cn.js');
222
+ default:
223
+ console.warn(`Language ${lang} not found in premium translations`);
224
+ return await import('ckeditor5-premium-features/translations/en.js'); // fallback to English
225
+ }
226
+ }
227
+ /* v8 ignore next 7 */
228
+ }
229
+ catch (error) {
230
+ console.error(`Failed to load translation for ${pkg}/${lang}:`, error);
231
+ return null;
232
+ }
233
+ }
@@ -0,0 +1,152 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import type { EditorCustomTranslationsDictionary } from '../typings';
4
+
5
+ import { normalizeCustomTranslations } from './normalize-custom-translations';
6
+
7
+ describe('normalizeCustomTranslations', () => {
8
+ it('should normalize empty translations object', () => {
9
+ const input: EditorCustomTranslationsDictionary = {};
10
+ const result = normalizeCustomTranslations(input);
11
+
12
+ expect(result).toEqual({});
13
+ });
14
+
15
+ it('should normalize single language translation', () => {
16
+ const input: EditorCustomTranslationsDictionary = {
17
+ en: {
18
+ Bold: 'Bold Text',
19
+ Italic: 'Italic Text',
20
+ },
21
+ };
22
+
23
+ const result = normalizeCustomTranslations(input);
24
+
25
+ expect(result).toEqual({
26
+ en: {
27
+ dictionary: {
28
+ Bold: 'Bold Text',
29
+ Italic: 'Italic Text',
30
+ },
31
+ },
32
+ });
33
+ });
34
+
35
+ it('should normalize multiple language translations', () => {
36
+ const input: EditorCustomTranslationsDictionary = {
37
+ en: {
38
+ Bold: 'Bold Text',
39
+ Italic: 'Italic Text',
40
+ },
41
+ pl: {
42
+ Bold: 'Pogrubienie',
43
+ Italic: 'Kursywa',
44
+ },
45
+ de: {
46
+ Bold: 'Fett',
47
+ Italic: 'Kursiv',
48
+ },
49
+ };
50
+
51
+ const result = normalizeCustomTranslations(input);
52
+
53
+ expect(result).toEqual({
54
+ en: {
55
+ dictionary: {
56
+ Bold: 'Bold Text',
57
+ Italic: 'Italic Text',
58
+ },
59
+ },
60
+ pl: {
61
+ dictionary: {
62
+ Bold: 'Pogrubienie',
63
+ Italic: 'Kursywa',
64
+ },
65
+ },
66
+ de: {
67
+ dictionary: {
68
+ Bold: 'Fett',
69
+ Italic: 'Kursiv',
70
+ },
71
+ },
72
+ });
73
+ });
74
+
75
+ it('should handle empty dictionary for a language', () => {
76
+ const input: EditorCustomTranslationsDictionary = {
77
+ en: {},
78
+ pl: {
79
+ Bold: 'Pogrubienie',
80
+ },
81
+ };
82
+
83
+ const result = normalizeCustomTranslations(input);
84
+
85
+ expect(result).toEqual({
86
+ en: {
87
+ dictionary: {},
88
+ },
89
+ pl: {
90
+ dictionary: {
91
+ Bold: 'Pogrubienie',
92
+ },
93
+ },
94
+ });
95
+ });
96
+
97
+ it('should preserve special characters and unicode in translations', () => {
98
+ const input: EditorCustomTranslationsDictionary = {
99
+ zh: {
100
+ 粗体: '加粗文本',
101
+ 斜体: '斜体文本',
102
+ },
103
+ ar: {
104
+ Bold: 'عريض',
105
+ Italic: 'مائل',
106
+ },
107
+ };
108
+
109
+ const result = normalizeCustomTranslations(input);
110
+
111
+ expect(result).toEqual({
112
+ zh: {
113
+ dictionary: {
114
+ 粗体: '加粗文本',
115
+ 斜体: '斜体文本',
116
+ },
117
+ },
118
+ ar: {
119
+ dictionary: {
120
+ Bold: 'عريض',
121
+ Italic: 'مائل',
122
+ },
123
+ },
124
+ });
125
+ });
126
+
127
+ it('should handle language codes with regions', () => {
128
+ const input: EditorCustomTranslationsDictionary = {
129
+ 'en-US': {
130
+ Color: 'Color',
131
+ },
132
+ 'en-GB': {
133
+ Color: 'Colour',
134
+ },
135
+ };
136
+
137
+ const result = normalizeCustomTranslations(input);
138
+
139
+ expect(result).toEqual({
140
+ 'en-US': {
141
+ dictionary: {
142
+ Color: 'Color',
143
+ },
144
+ },
145
+ 'en-GB': {
146
+ dictionary: {
147
+ Color: 'Colour',
148
+ },
149
+ },
150
+ });
151
+ });
152
+ });
@@ -0,0 +1,18 @@
1
+ import type { Translations } from 'ckeditor5';
2
+
3
+ import type { EditorCustomTranslationsDictionary } from '../typings';
4
+
5
+ import { mapObjectValues } from '../../../shared';
6
+
7
+ /**
8
+ * This function takes a custom translations object and maps it to the format expected by CKEditor5.
9
+ * Each translation dictionary is wrapped in an object with a `dictionary` key.
10
+ *
11
+ * @param translations - The custom translations to normalize.
12
+ * @returns A normalized translations object suitable for CKEditor5.
13
+ */
14
+ export function normalizeCustomTranslations(translations: EditorCustomTranslationsDictionary): Translations {
15
+ return mapObjectValues(translations, dictionary => ({
16
+ dictionary,
17
+ }));
18
+ }