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

Support manual override of text blocks #14712

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
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
3 changes: 3 additions & 0 deletions examples/api-samples/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@
},
{
"frontendOnly": "lib/browser-only/api-samples-frontend-only-module"
},
{
"frontendPreload": "lib/browser/api-samples-preload-module"
}
],
"keywords": [
Expand Down
23 changes: 23 additions & 0 deletions examples/api-samples/src/browser/api-samples-preload-module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// *****************************************************************************
// Copyright (C) 2024 TypeFox and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************

import { ContainerModule } from '@theia/core/shared/inversify';
import { I18nReplacementContribution } from '@theia/core/lib/browser/preload/i18n-replacement-contribution';
import { I18nSampleReplacementContribution } from './i18n/i18n-replacement-sample';

export default new ContainerModule(bind => {
bind(I18nReplacementContribution).to(I18nSampleReplacementContribution).inSingletonScope();
});
37 changes: 37 additions & 0 deletions examples/api-samples/src/browser/i18n/i18n-replacement-sample.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// *****************************************************************************
// Copyright (C) 2024 TypeFox and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************

import { I18nReplacementContribution } from '@theia/core/lib/browser/preload/i18n-replacement-contribution';

export class I18nSampleReplacementContribution implements I18nReplacementContribution {

getReplacement(locale: string): Record<string, string> {
switch (locale) {
case 'en': {
return {
'About': 'About Theia',
};
}
case 'de': {
return {
'About': 'Über Theia',
};
}
}
return {};
}

}
2 changes: 1 addition & 1 deletion examples/playwright/src/tests/theia-quick-command.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ test.describe('Theia Quick Command', () => {

test('should trigger \'About\' command after typing', async () => {
await quickCommand.type('About');
await quickCommand.trigger('About');
await quickCommand.trigger('About Theia');
expect(await quickCommand.isOpen()).toBe(false);
const aboutDialog = new TheiaAboutDialog(app);
expect(await aboutDialog.isVisible()).toBe(true);
Expand Down
10 changes: 3 additions & 7 deletions packages/core/src/browser-only/i18n/i18n-frontend-only-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,10 @@ import { LanguageQuickPickService } from '../../browser/i18n/language-quick-pick
export default new ContainerModule(bind => {
const i18nMock: AsyncLocalizationProvider = {
getCurrentLanguage: async (): Promise<string> => 'en',
setCurrentLanguage: async (_languageId: string): Promise<void> => {

},
getAvailableLanguages: async (): Promise<LanguageInfo[]> =>
[]
,
setCurrentLanguage: async (_languageId: string): Promise<void> => { },
getAvailableLanguages: async (): Promise<LanguageInfo[]> => [],
loadLocalization: async (_languageId: string): Promise<Localization> => ({
translations: {},
translations: new Map(),
languageId: 'en'
})
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import { Localization } from '../../common/i18n/localization';
// loaded after regular preload module
export default new ContainerModule((bind, unbind, isBound, rebind) => {
const frontendOnlyLocalizationServer: LocalizationServer = {
loadLocalization: async (languageId: string): Promise<Localization> => ({ translations: {}, languageId })
loadLocalization: async (languageId: string): Promise<Localization> => ({ translations: new Map(), languageId })
};
if (isBound(LocalizationServer)) {
rebind(LocalizationServer).toConstantValue(frontendOnlyLocalizationServer);
Expand Down
26 changes: 24 additions & 2 deletions packages/core/src/browser/preload/i18n-preload-contribution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,24 +17,30 @@
import { PreloadContribution } from './preloader';
import { FrontendApplicationConfigProvider } from '../frontend-application-config-provider';
import { nls } from '../../common/nls';
import { inject, injectable } from 'inversify';
import { inject, injectable, named } from 'inversify';
import { LocalizationServer } from '../../common/i18n/localization-server';
import { ContributionProvider } from '../../common';
import { I18nReplacementContribution } from './i18n-replacement-contribution';

@injectable()
export class I18nPreloadContribution implements PreloadContribution {

@inject(LocalizationServer)
protected readonly localizationServer: LocalizationServer;

@inject(ContributionProvider) @named(I18nReplacementContribution)
protected readonly i18nReplacementContributions: ContributionProvider<I18nReplacementContribution>;

async initialize(): Promise<void> {
const defaultLocale = FrontendApplicationConfigProvider.get().defaultLocale;
if (defaultLocale && !nls.locale) {
Object.assign(nls, {
locale: defaultLocale
});
}
let locale = nls.locale ?? nls.defaultLocale;
if (nls.locale && nls.locale !== nls.defaultLocale) {
const localization = await this.localizationServer.loadLocalization(nls.locale);
const localization = await this.localizationServer.loadLocalization(locale);
if (localization.languagePack) {
nls.localization = localization;
} else {
Expand All @@ -43,8 +49,24 @@ export class I18nPreloadContribution implements PreloadContribution {
Object.assign(nls, {
locale: defaultLocale || undefined
});
locale = defaultLocale;
}
}
const replacements = this.getReplacements(locale);
if (replacements.size > 0) {
nls.localization ??= { translations: new Map(), languageId: locale };
nls.localization.replacements = replacements;
}
}

protected getReplacements(locale: string): Map<string, string> {
const replacements = new Map<string, string>();
for (const contribution of this.i18nReplacementContributions.getContributions()) {
for (const [value, replacement] of Object.entries(contribution.getReplacement(locale))) {
replacements.set(value, replacement);
}
}
return replacements;
}

}
21 changes: 21 additions & 0 deletions packages/core/src/browser/preload/i18n-replacement-contribution.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// *****************************************************************************
// Copyright (C) 2023 TypeFox and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************

export const I18nReplacementContribution = Symbol('I18nReplacementContribution');

export interface I18nReplacementContribution {
getReplacement(locale: string): Record<string, string>;
}
8 changes: 5 additions & 3 deletions packages/core/src/browser/preload/preload-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,19 +21,21 @@ import { I18nPreloadContribution } from './i18n-preload-contribution';
import { OSPreloadContribution } from './os-preload-contribution';
import { ThemePreloadContribution } from './theme-preload-contribution';
import { LocalizationServer, LocalizationServerPath } from '../../common/i18n/localization-server';
import { WebSocketConnectionProvider } from '../messaging/ws-connection-provider';
import { ServiceConnectionProvider } from '../messaging/service-connection-provider';
import { OSBackendProvider, OSBackendProviderPath } from '../../common/os';
import { I18nReplacementContribution } from './i18n-replacement-contribution';

export default new ContainerModule(bind => {
bind(Preloader).toSelf().inSingletonScope();
bindContributionProvider(bind, PreloadContribution);
bindContributionProvider(bind, I18nReplacementContribution);

bind(LocalizationServer).toDynamicValue(ctx =>
WebSocketConnectionProvider.createProxy<LocalizationServer>(ctx.container, LocalizationServerPath)
ServiceConnectionProvider.createProxy<LocalizationServer>(ctx.container, LocalizationServerPath)
).inSingletonScope();

bind(OSBackendProvider).toDynamicValue(ctx =>
WebSocketConnectionProvider.createProxy<OSBackendProvider>(ctx.container, OSBackendProviderPath)
ServiceConnectionProvider.createProxy<OSBackendProvider>(ctx.container, OSBackendProviderPath)
).inSingletonScope();

bind(I18nPreloadContribution).toSelf().inSingletonScope();
Expand Down
14 changes: 10 additions & 4 deletions packages/core/src/common/i18n/localization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ export interface AsyncLocalizationProvider {
}

export interface Localization extends LanguageInfo {
translations: { [key: string]: string };
translations: Map<string, string>;
replacements?: Map<string, string>;
}

export interface LanguageInfo {
Expand All @@ -50,9 +51,14 @@ export namespace Localization {
export function localize(localization: Localization | undefined, key: string, defaultValue: string, ...args: FormatType[]): string {
let value = defaultValue;
if (localization) {
const translation = localization.translations[key];
if (translation) {
value = normalize(translation);
const replacement = localization.replacements?.get(defaultValue);
if (replacement) {
value = replacement;
} else {
const translation = localization.translations.get(key);
if (translation) {
value = normalize(translation);
}
}
}
return format(value, args);
Expand Down
12 changes: 6 additions & 6 deletions packages/core/src/node/i18n/localization-contribution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ export class LocalizationRegistry {
}));
}

protected createLocalization(locale: string | LanguageInfo, translations: () => Promise<Record<string, string>>): LazyLocalization {
protected createLocalization(locale: string | LanguageInfo, translations: () => Promise<Map<string, string>>): LazyLocalization {
let localization: LazyLocalization;
if (typeof locale === 'string') {
localization = {
Expand All @@ -82,22 +82,22 @@ export class LocalizationRegistry {
return localization;
}

protected flattenTranslations(localization: unknown): Record<string, string> {
protected flattenTranslations(localization: unknown): Map<string, string> {
if (isObject(localization)) {
const record: Record<string, string> = {};
const record = new Map<string, string>();
for (const [key, value] of Object.entries(localization)) {
if (typeof value === 'string') {
record[key] = value;
record.set(key, value);
} else if (isObject(value)) {
const flattened = this.flattenTranslations(value);
for (const [flatKey, flatValue] of Object.entries(flattened)) {
record[`${key}/${flatKey}`] = flatValue;
record.set(`${key}/${flatKey}`, flatValue);
}
}
}
return record;
} else {
return {};
return new Map();
}
}

Expand Down
8 changes: 5 additions & 3 deletions packages/core/src/node/i18n/localization-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import { isObject } from '../../common/types';
* Allows to load localizations on demand when requested by the user.
*/
export interface LazyLocalization extends LanguageInfo {
getTranslations(): Promise<Record<string, string>>;
getTranslations(): Promise<Map<string, string>>;
}

export namespace LazyLocalization {
Expand Down Expand Up @@ -110,14 +110,16 @@ export class LocalizationProvider {
async loadLocalization(languageId: string): Promise<Localization> {
const merged: Localization = {
languageId,
translations: {}
translations: new Map()
};
const localizations = await Promise.all(this.localizations.filter(e => e.languageId === languageId).map(LazyLocalization.toLocalization));
for (const localization of localizations) {
merged.languageName ||= localization.languageName;
merged.localizedLanguageName ||= localization.localizedLanguageName;
merged.languagePack ||= localization.languagePack;
Object.assign(merged.translations, localization.translations);
for (const [key, value] of localization.translations.entries()) {
merged.translations.set(key, value);
}
}
return merged;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,19 +65,19 @@ export namespace NotebookCellCommands {
export const EXECUTE_SINGLE_CELL_COMMAND = Command.toDefaultLocalizedCommand({
id: 'notebook.cell.execute-cell',
category: 'Notebook',
label: nls.localizeByDefault('Execute Cell'),
label: 'Execute Cell',
iconClass: codicon('play'),
});
/** Parameters: notebookModel: NotebookModel, cell: NotebookCellModel */
export const EXECUTE_SINGLE_CELL_AND_FOCUS_NEXT_COMMAND = Command.toDefaultLocalizedCommand({
id: 'notebook.cell.execute-cell-and-focus-next',
label: nls.localizeByDefault('Execute Notebook Cell and Select Below'),
label: 'Execute Notebook Cell and Select Below',
category: 'Notebook',
});
/** Parameters: notebookModel: NotebookModel, cell: NotebookCellModel */
export const EXECUTE_SINGLE_CELL_AND_INSERT_BELOW_COMMAND = Command.toDefaultLocalizedCommand({
id: 'notebook.cell.execute-cell-and-insert-below',
label: nls.localizeByDefault('Execute Notebook Cell and Insert Below'),
label: 'Execute Notebook Cell and Insert Below',
category: 'Notebook',
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -290,9 +290,9 @@ function buildLocalizations(packageUri: string, localizations: PluginLocalizatio
languageName: localization.languageName,
localizedLanguageName: localization.localizedLanguageName,
languagePack: true,
async getTranslations(): Promise<Record<string, string>> {
async getTranslations(): Promise<Map<string, string>> {
cachedLocalization ??= loadTranslations(packagePath, localization.translations);
return cachedLocalization;
return cachedLocalization.then(translations => new Map(Object.entries(translations)));
},
};
theiaLocalizations.push(theiaLocalization);
Expand Down
Loading