1616
1717import * as path from 'path' ;
1818import * as fs from '@theia/core/shared/fs-extra' ;
19- import { LocalizationProvider } from '@theia/core/lib/node/i18n/localization-provider' ;
19+ import { LazyLocalization , LocalizationProvider } from '@theia/core/lib/node/i18n/localization-provider' ;
2020import { Localization } from '@theia/core/lib/common/i18n/localization' ;
2121import { inject , injectable } from '@theia/core/shared/inversify' ;
2222import { DeployedPlugin , Localization as PluginLocalization , PluginIdentifiers , Translation } from '../../common' ;
2323import { EnvVariablesServer } from '@theia/core/lib/common/env-variables' ;
2424import { BackendApplicationContribution } from '@theia/core/lib/node' ;
25- import { Disposable , DisposableCollection , isObject , MaybePromise , nls , URI } from '@theia/core' ;
25+ import { Disposable , DisposableCollection , isObject , MaybePromise , nls , Path , URI } from '@theia/core' ;
2626import { Deferred } from '@theia/core/lib/common/promise-util' ;
2727import { LanguagePackBundle , LanguagePackService } from '../../common/language-pack-service' ;
2828
@@ -70,11 +70,8 @@ export class HostedPluginLocalizationService implements BackendApplicationContri
7070 if ( plugin . contributes ?. localizations ) {
7171 // Indicator that this plugin is a vscode language pack
7272 // Language packs translate Theia and some builtin vscode extensions
73- const localizations = buildLocalizations ( plugin . contributes . localizations ) ;
74- disposable . push ( Disposable . create ( ( ) => {
75- this . localizationProvider . removeLocalizations ( ...localizations ) ;
76- } ) ) ;
77- this . localizationProvider . addLocalizations ( ...localizations ) ;
73+ const localizations = buildLocalizations ( plugin . metadata . model . packageUri , plugin . contributes . localizations ) ;
74+ disposable . push ( this . localizationProvider . addLocalizations ( ...localizations ) ) ;
7875 }
7976 if ( plugin . metadata . model . l10n || plugin . contributes ?. localizations ) {
8077 // Indicator that this plugin is a vscode language pack or has its own localization bundles
@@ -100,26 +97,29 @@ export class HostedPluginLocalizationService implements BackendApplicationContri
10097 const pluginId = plugin . metadata . model . id ;
10198 const packageUri = new URI ( plugin . metadata . model . packageUri ) ;
10299 if ( plugin . contributes ?. localizations ) {
100+ const l10nPromises : Promise < void > [ ] = [ ] ;
103101 for ( const localization of plugin . contributes . localizations ) {
104102 for ( const translation of localization . translations ) {
105- const l10n = getL10nTranslation ( translation ) ;
106- if ( l10n ) {
107- const translatedPluginId = translation . id ;
108- const translationUri = packageUri . resolve ( translation . path ) ;
109- const locale = localization . languageId ;
110- // We store a bundle for another extension in here
111- // Hence we use `translatedPluginId` instead of `pluginId`
112- this . languagePackService . storeBundle ( translatedPluginId , locale , {
113- contents : processL10nBundle ( l10n ) ,
114- uri : translationUri . toString ( )
115- } ) ;
116- disposable . push ( Disposable . create ( ( ) => {
117- // Only dispose the deleted locale for the specific plugin
118- this . languagePackService . deleteBundle ( translatedPluginId , locale ) ;
119- } ) ) ;
120- }
103+ l10nPromises . push ( getL10nTranslation ( plugin . metadata . model . packageUri , translation ) . then ( l10n => {
104+ if ( l10n ) {
105+ const translatedPluginId = translation . id ;
106+ const translationUri = packageUri . resolve ( translation . path ) ;
107+ const locale = localization . languageId ;
108+ // We store a bundle for another extension in here
109+ // Hence we use `translatedPluginId` instead of `pluginId`
110+ this . languagePackService . storeBundle ( translatedPluginId , locale , {
111+ contents : processL10nBundle ( l10n ) ,
112+ uri : translationUri . toString ( )
113+ } ) ;
114+ disposable . push ( Disposable . create ( ( ) => {
115+ // Only dispose the deleted locale for the specific plugin
116+ this . languagePackService . deleteBundle ( translatedPluginId , locale ) ;
117+ } ) ) ;
118+ }
119+ } ) ) ;
121120 }
122121 }
122+ await Promise . all ( l10nPromises ) ;
123123 }
124124 // The `l10n` field of the plugin model points to a relative directory path within the plugin
125125 // It is supposed to contain localization bundles that contain translations of the plugin strings into different languages
@@ -150,11 +150,13 @@ export class HostedPluginLocalizationService implements BackendApplicationContri
150150 */
151151 async localizePlugin ( plugin : DeployedPlugin ) : Promise < DeployedPlugin > {
152152 const currentLanguage = this . localizationProvider . getCurrentLanguage ( ) ;
153- const localization = this . localizationProvider . loadLocalization ( currentLanguage ) ;
154153 const pluginPath = new URI ( plugin . metadata . model . packageUri ) . path . fsPath ( ) ;
155154 const pluginId = plugin . metadata . model . id ;
156155 try {
157- const translations = await loadPackageTranslations ( pluginPath , currentLanguage ) ;
156+ const [ localization , translations ] = await Promise . all ( [
157+ this . localizationProvider . loadLocalization ( currentLanguage ) ,
158+ loadPackageTranslations ( pluginPath , currentLanguage ) ,
159+ ] ) ;
158160 plugin = localizePackage ( plugin , translations , ( key , original ) => {
159161 const fullKey = `${ pluginId } /package/${ key } ` ;
160162 return Localization . localize ( localization , fullKey , original ) ;
@@ -218,10 +220,24 @@ export class HostedPluginLocalizationService implements BackendApplicationContri
218220
219221// New plugin localization logic using vscode.l10n
220222
221- function getL10nTranslation ( translation : Translation ) : UnprocessedL10nBundle | undefined {
223+ async function getL10nTranslation ( packageUri : string , translation : Translation ) : Promise < UnprocessedL10nBundle | undefined > {
222224 // 'bundle' is a special key that contains all translations for the l10n vscode API
223225 // If that doesn't exist, we can assume that the language pack is using the old vscode-nls API
224- return translation . contents . bundle ;
226+ if ( translation . cachedContents ) {
227+ return translation . cachedContents . bundle ;
228+ } else {
229+ const translationPath = new URI ( packageUri ) . path . join ( translation . path ) . fsPath ( ) ;
230+ try {
231+ const translationJson = await fs . readJson ( translationPath ) ;
232+ translation . cachedContents = translationJson ?. contents ;
233+ return translationJson ?. contents ?. bundle ;
234+ } catch ( err ) {
235+ console . error ( 'Failed reading translation file from: ' + translationPath , err ) ;
236+ // Store an empty object, so we don't reattempt to load the file
237+ translation . cachedContents = { } ;
238+ return undefined ;
239+ }
240+ }
225241}
226242
227243async function loadPluginBundles ( l10nUri : URI ) : Promise < Record < string , LanguagePackBundle > | undefined > {
@@ -262,28 +278,47 @@ function processL10nBundle(bundle: UnprocessedL10nBundle): Record<string, string
262278
263279// Old plugin localization logic for vscode-nls
264280// vscode-nls was used until version 1.73 of VSCode to translate extensions
281+ // This style of localization is still used by vscode language packs
265282
266- function buildLocalizations ( localizations : PluginLocalization [ ] ) : Localization [ ] {
267- const theiaLocalizations : Localization [ ] = [ ] ;
283+ function buildLocalizations ( packageUri : string , localizations : PluginLocalization [ ] ) : LazyLocalization [ ] {
284+ const theiaLocalizations : LazyLocalization [ ] = [ ] ;
285+ const packagePath = new URI ( packageUri ) . path ;
268286 for ( const localization of localizations ) {
269- const theiaLocalization : Localization = {
287+ let cachedLocalization : Promise < Record < string , string > > | undefined ;
288+ const theiaLocalization : LazyLocalization = {
270289 languageId : localization . languageId ,
271290 languageName : localization . languageName ,
272291 localizedLanguageName : localization . localizedLanguageName ,
273292 languagePack : true ,
274- translations : { }
293+ async getTranslations ( ) : Promise < Record < string , string > > {
294+ cachedLocalization ??= loadTranslations ( packagePath , localization . translations ) ;
295+ return cachedLocalization ;
296+ } ,
275297 } ;
276- for ( const translation of localization . translations ) {
277- for ( const [ scope , value ] of Object . entries ( translation . contents ) ) {
298+ theiaLocalizations . push ( theiaLocalization ) ;
299+ }
300+ return theiaLocalizations ;
301+ }
302+
303+ async function loadTranslations ( packagePath : Path , translations : Translation [ ] ) : Promise < Record < string , string > > {
304+ const allTranslations = await Promise . all ( translations . map ( async translation => {
305+ const values : Record < string , string > = { } ;
306+ const translationPath = packagePath . join ( translation . path ) . fsPath ( ) ;
307+ try {
308+ const translationJson = await fs . readJson ( translationPath ) ;
309+ const translationContents : Record < string , Record < string , string > > = translationJson ?. contents ;
310+ for ( const [ scope , value ] of Object . entries ( translationContents ?? { } ) ) {
278311 for ( const [ key , item ] of Object . entries ( value ) ) {
279312 const translationKey = buildTranslationKey ( translation . id , scope , key ) ;
280- theiaLocalization . translations [ translationKey ] = item ;
313+ values [ translationKey ] = item ;
281314 }
282315 }
316+ } catch ( err ) {
317+ console . error ( 'Failed to load translation from: ' + translationPath , err ) ;
283318 }
284- theiaLocalizations . push ( theiaLocalization ) ;
285- }
286- return theiaLocalizations ;
319+ return values ;
320+ } ) ) ;
321+ return Object . assign ( { } , ... allTranslations ) ;
287322}
288323
289324function buildTranslationKey ( pluginId : string , scope : string , key : string ) : string {
0 commit comments