Skip to content

Commit

Permalink
Support manual override of text blocks
Browse files Browse the repository at this point in the history
  • Loading branch information
msujew committed Jan 13, 2025
1 parent 6ea0f22 commit b95e4ff
Show file tree
Hide file tree
Showing 14 changed files with 144 additions and 32 deletions.
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

0 comments on commit b95e4ff

Please sign in to comment.