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

PoC for new handling of translations. #15346

Draft
wants to merge 19 commits into
base: ck/5672-poc-of-new-installation-method
Choose a base branch
from
Draft
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
1 change: 1 addition & 0 deletions package.json
Expand Up @@ -141,6 +141,7 @@
"mkdirp": "^1.0.4",
"node-fetch": "^2.6.7",
"nyc": "^15.0.1",
"pofile": "^1.1.4",
"postcss-loader": "^4.3.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
Expand Down
3 changes: 2 additions & 1 deletion packages/ckeditor5-core/src/context.ts
Expand Up @@ -158,7 +158,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.config.get( 'translations' )
} );

this.t = this.locale.t;
Expand Down
3 changes: 2 additions & 1 deletion packages/ckeditor5-core/src/editor/editor.ts
Expand Up @@ -277,8 +277,9 @@ 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 );
const translations = config.translations || undefined;

this._context = config.context || new Context( { language } );
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
Expand Down
16 changes: 16 additions & 0 deletions packages/ckeditor5-core/src/editor/editorconfig.ts
Expand Up @@ -34,6 +34,7 @@ import type Editor from './editor';
* about setting configuration options.
*/
export interface EditorConfig {

context?: Context;

/**
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 editor.
*/
translations?: Translations | Array<Translations>;
}

/**
Expand Down Expand Up @@ -694,3 +700,13 @@ export interface UiConfig {
**/
poweredBy?: PoweredByConfig;
}

/**
* Translations object definition.
*/
export type Translations = {
[ language: string ]: {
dictionary: { [ messageId: string ]: string | ReadonlyArray<string> };
getPluralForm?: ( n: number ) => number;
};
};
3 changes: 2 additions & 1 deletion packages/ckeditor5-core/src/index.ts
Expand Up @@ -23,7 +23,8 @@ export type {
LanguageConfig,
ToolbarConfig,
ToolbarConfigItem,
UiConfig
UiConfig,
Translations
} from './editor/editorconfig';

export { default as attachToForm } from './editor/utils/attachtoform';
Expand Down
1 change: 1 addition & 0 deletions packages/ckeditor5-essentials/package.json
Expand Up @@ -50,6 +50,7 @@
"CHANGELOG.md"
],
"scripts": {
"dll:build": "webpack",
"rollup": "rollup -c ../../scripts/build.mjs",
"build": "tsc -p ./tsconfig.json"
}
Expand Down
32 changes: 28 additions & 4 deletions packages/ckeditor5-utils/src/locale.ts
Expand Up @@ -12,6 +12,8 @@
import toArray from './toarray';
import { _translate, type Message } from './translation-service';
import { getLanguageDirection, type LanguageDirection } from './language';
import type { Translations } from '@ckeditor/ckeditor5-core';
import { merge } from 'lodash-es';

/**
* Represents the localization services.
Expand Down Expand Up @@ -97,6 +99,11 @@ export default class Locale {
*/
public readonly t: LocaleTranslate;

/**
* Object that contains translations.
*/
public readonly 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 +114,27 @@ 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?: Translations | Array<Translations> | undefined; } = {}
) {
this.uiLanguage = uiLanguage;
this.contentLanguage = contentLanguage || this.uiLanguage;
this.uiLanguageDirection = getLanguageDirection( this.uiLanguage );
this.contentLanguageDirection = getLanguageDirection( this.contentLanguage );

let unifiedTranslations: Translations | undefined;

if ( Array.isArray( translations ) ) {
unifiedTranslations = translations.reduce(
( acc, singleTranslationObject ) => merge( acc, singleTranslationObject ) ) as Translations | undefined;
} else {
unifiedTranslations = translations as Translations | undefined;
}

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

Expand Down Expand Up @@ -153,8 +174,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 +185,11 @@ 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>,
translations?: { [key: string]: string } | null
) => string;

/**
* Fills the `%0, %1, ...` string placeholders with values.
Expand Down
74 changes: 53 additions & 21 deletions packages/ckeditor5-utils/src/translation-service.ts
Expand Up @@ -9,16 +9,13 @@
* @module utils/translation-service
*/

import type { Translations, Editor } from '@ckeditor/ckeditor5-core';
import CKEditorError from './ckeditorerror';
import global from './dom/global';
import { merge } from 'lodash-es';

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 @@ -114,18 +111,38 @@ if ( !global.window.CKEDITOR_TRANSLATIONS ) {
export function add(
language: string,
translations: { readonly [ messageId: string ]: string | ReadonlyArray<string> },
getPluralForm?: ( n: number ) => number
getPluralForm?: ( n: number ) => number,
editor?: Editor
): void {
if ( !global.window.CKEDITOR_TRANSLATIONS[ language ] ) {
global.window.CKEDITOR_TRANSLATIONS[ language ] = {} as any;
}

const languageTranslations = global.window.CKEDITOR_TRANSLATIONS[ language ];
let mergedExistingTranslations: Translations | undefined;

if ( editor ) {
const existingTranslations = editor.config.get( 'translations' );

if ( Array.isArray( existingTranslations ) ) {
mergedExistingTranslations = existingTranslations.reduce(
( acc, singleTranslationObject ) => merge( acc, singleTranslationObject ) ) as Translations | undefined;
} else {
mergedExistingTranslations = existingTranslations as Translations | undefined;
}
}

const languageTranslations = mergedExistingTranslations ?
mergedExistingTranslations[ language ] :
global.window.CKEDITOR_TRANSLATIONS[ language ];

languageTranslations.dictionary = languageTranslations.dictionary || {};
languageTranslations.getPluralForm = getPluralForm || languageTranslations.getPluralForm;

Object.assign( languageTranslations.dictionary, translations );

if ( editor && mergedExistingTranslations ) {
editor.config.set( 'translations', { language: languageTranslations } );
}
}

/**
Expand Down Expand Up @@ -163,9 +180,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
): string {
if ( typeof quantity !== 'number' ) {
/**
* An incorrect value was passed to the translation function. This was probably caused
Expand All @@ -177,17 +200,17 @@ export function _translate( language: string, message: Message, quantity: number
throw new CKEditorError( 'translation-service-quantity-not-a-number', null, { quantity } );
}

const numberOfLanguages = getNumberOfLanguages();
const numberOfLanguages = getNumberOfLanguages( translations );

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 = translations ? Object.keys( translations )[ 0 ] : Object.keys( global.window.CKEDITOR_TRANSLATIONS )[ 0 ];
}

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

if ( numberOfLanguages === 0 || !hasTranslation( language, messageId ) ) {
if ( numberOfLanguages === 0 || !hasTranslation( language, messageId, translations ) ) {
if ( quantity !== 1 ) {
// Return the default plural form that was passed in the `message.plural` parameter.
return message.plural!;
Expand All @@ -196,9 +219,11 @@ 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 translation = dictionary[ messageId ];
const dictionary = ( translations ? translations : global.window.CKEDITOR_TRANSLATIONS )[ language ].dictionary;
const getPluralForm = ( translations ? translations : global.window.CKEDITOR_TRANSLATIONS )[ language ].getPluralForm ||
( n => n === 1 ? 0 : 1 );

const translation = dictionary[ messageId ] || '';

if ( typeof translation === 'string' ) {
return translation;
Expand All @@ -216,21 +241,28 @@ 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 = {};
}
}

/**
* Checks whether the dictionary exists and translation in that dictionary exists.
*/
function hasTranslation( language: string, messageId: string ): boolean {
function hasTranslation( language: string, messageId: string, translations?: Translations ): boolean {
return (
!!global.window.CKEDITOR_TRANSLATIONS[ language ] &&
!!global.window.CKEDITOR_TRANSLATIONS[ language ].dictionary[ messageId ]
translations ? (
!!translations[ language ] &&
!!translations[ language ].dictionary[ messageId as any ]
) : (
!!global.window.CKEDITOR_TRANSLATIONS[ language ] &&
!!global.window.CKEDITOR_TRANSLATIONS[ language ].dictionary[ messageId ]
)
);
}

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

/**
Expand Down
9 changes: 8 additions & 1 deletion scripts/build-ckeditor5.mjs
Expand Up @@ -20,11 +20,13 @@ import postcssImport from 'postcss-import';

import path from 'path';

import po2js from './translations/rollup-po2js/po2js.mjs';

// Indicates whether to emit source maps
const sourceMap = process.env.DEVELOPMENT || false;

// Current working directory
const cwd = path.resolve();
const cwd = process.cwd();

// Content of the `package.json`
const pkg = JSON.parse( await readFile( path.join( cwd, 'package.json' ) ) );
Expand Down Expand Up @@ -87,6 +89,11 @@ export default [
declarationMap: false, // TODO
},
sourceMap
} ),
po2js( {
type: 'all',
destDirectory: path.join( cwd, 'dist', 'translations' ),
banner
} )
]
},
Expand Down
18 changes: 13 additions & 5 deletions scripts/build.mjs
Expand Up @@ -20,11 +20,13 @@ import postcssImport from 'postcss-import';

import path from 'path';

import po2js from './translations/rollup-po2js/po2js.mjs';

// Indicates whether to emit source maps
const sourceMap = process.env.DEVELOPMENT || false;

// Current working directory
const cwd = path.resolve();
const cwd = process.cwd();

// Content of the `package.json`
const pkg = JSON.parse( await readFile( path.join( cwd, 'package.json') ) );
Expand All @@ -35,8 +37,8 @@ const external = [
...Object.keys( pkg.peerDependencies || {} )
];

const inputPath = path.join( cwd, 'src', 'index.ts');
const tsConfigPath = path.join( cwd, 'tsconfig.json');
const inputPath = path.join( cwd, 'src', 'index.ts' );
const tsConfigPath = path.join( cwd, 'tsconfig.json' );

// Banner added to the top of the output files
const banner =
Expand All @@ -57,7 +59,7 @@ export default [
input: inputPath,
output: {
format: 'esm',
file: path.join( cwd, 'dist', 'index.js'),
file: path.join( cwd, 'dist', 'index.js' ),
assetFileNames: '[name][extname]',
sourcemap: sourceMap,
banner
Expand Down Expand Up @@ -86,11 +88,17 @@ export default [
tsconfig: tsConfigPath,
typescript,
compilerOptions: {
declarationDir: path.join( cwd, 'dist', 'types'),
declarationDir: path.join( cwd, 'dist', 'types' ),
declaration: true,
declarationMap: false, // TODO
},
sourceMap
} ),
po2js( {
type: 'single',
sourceDirectory: path.join( cwd, 'lang', 'translations' ),
destDirectory: path.join( cwd, 'dist', 'translations' ),
banner
} )
]
},
Expand Down