From 7f4e5381d04d21267aa5c6b5129dbfe2915b5e37 Mon Sep 17 00:00:00 2001 From: Johannes Millan Date: Fri, 16 Jan 2026 18:48:41 +0100 Subject: [PATCH] fix(plugins): wire up translation loading to i18n service (CRITICAL) Fixes the critical issue where translations were loaded but never passed to PluginI18nService, making the i18n system non-functional. Changes: - Inject PluginI18nService in PluginService - Load translations into i18n service in 3 locations: - _loadPluginLazy() for lazy-loaded plugins - _loadPlugin() for file-based plugins - _loadUploadedPlugin() for cached plugins - Improve LANGUAGE_CHANGE hook type guard - Use explicit LanguageCode type predicate - Remove non-null assertion (no longer needed) This makes api.translate() functional for all plugin loading paths. --- src/app/plugins/plugin-hooks.effects.ts | 5 ++-- src/app/plugins/plugin.service.ts | 32 ++++++++++++++++++++++--- 2 files changed, 32 insertions(+), 5 deletions(-) diff --git a/src/app/plugins/plugin-hooks.effects.ts b/src/app/plugins/plugin-hooks.effects.ts index 8e6ebb4fd..2a7b7f632 100644 --- a/src/app/plugins/plugin-hooks.effects.ts +++ b/src/app/plugins/plugin-hooks.effects.ts @@ -45,6 +45,7 @@ import { } from '../features/work-context/store/work-context-meta.actions'; import { LOCAL_ACTIONS } from '../util/local-actions.token'; import { PlannerActions } from '../features/planner/store/planner.actions'; +import { LanguageCode } from '../core/locale.constants'; @Injectable() export class PluginHooksEffects { @@ -210,11 +211,11 @@ export class PluginHooksEffects { filter((action) => action.sectionKey === 'localization'), withLatestFrom(this.store.pipe(select(selectLocalizationConfig))), map(([_, localizationConfig]) => localizationConfig.lng), - filter((lng) => !!lng), + filter((lng): lng is LanguageCode => typeof lng === 'string' && lng.length > 0), distinctUntilChanged(), tap((newLanguage) => { // Update plugin i18n service with new language - this.pluginI18nService.setCurrentLanguage(newLanguage!); + this.pluginI18nService.setCurrentLanguage(newLanguage); // Dispatch hook to notify plugins this.pluginService.dispatchHook(PluginHooks.LANGUAGE_CHANGE, { diff --git a/src/app/plugins/plugin.service.ts b/src/app/plugins/plugin.service.ts index 8030dd7d6..78cdc1bfb 100644 --- a/src/app/plugins/plugin.service.ts +++ b/src/app/plugins/plugin.service.ts @@ -31,6 +31,7 @@ import { validatePluginManifest } from './util/validate-manifest.util'; import { TranslateService } from '@ngx-translate/core'; import { T } from '../t.const'; import { PluginLog } from '../core/log'; +import { PluginI18nService } from './plugin-i18n.service'; @Injectable({ providedIn: 'root', @@ -48,6 +49,7 @@ export class PluginService implements OnDestroy { private readonly _cleanupService = inject(PluginCleanupService); private readonly _pluginLoader = inject(PluginLoaderService); private readonly _translateService = inject(TranslateService); + private readonly _pluginI18nService = inject(PluginI18nService); private _isInitialized = false; private _loadedPlugins: PluginInstance[] = []; @@ -373,13 +375,21 @@ export class PluginService implements OnDestroy { private async _loadPluginLazy(state: PluginState): Promise { // Load the plugin code and assets const assets = await this._pluginLoader.loadPluginAssets(state.path); - const { code: pluginCode, indexHtml } = assets; + const { code: pluginCode, indexHtml, translations } = assets; // Store assets if (indexHtml) { this._pluginIndexHtml.set(state.manifest.id, indexHtml); } + // Load translations into i18n service + if (translations && Object.keys(translations).length > 0) { + this._pluginI18nService.loadPluginTranslationsFromContent( + state.manifest.id, + translations, + ); + } + // Create base config const baseCfg = this._getBaseCfg(); @@ -454,7 +464,7 @@ export class PluginService implements OnDestroy { try { // Use the loader service for lazy loading const assets = await this._pluginLoader.loadPluginAssets(pluginPath); - const { manifest, code: pluginCode, indexHtml, icon } = assets; + const { manifest, code: pluginCode, indexHtml, icon, translations } = assets; // Store assets if loaded if (indexHtml) { @@ -466,6 +476,14 @@ export class PluginService implements OnDestroy { this._pluginIconsSignal.set(new Map(this._pluginIcons)); } + // Load translations into i18n service + if (translations && Object.keys(translations).length > 0) { + this._pluginI18nService.loadPluginTranslationsFromContent( + manifest.id, + translations, + ); + } + // Check if plugin should be loaded based on persisted enabled state const isPluginEnabled = await this._pluginMetaPersistenceService.isPluginEnabled( manifest.id, @@ -1210,7 +1228,7 @@ export class PluginService implements OnDestroy { try { // Use the loader service for uploaded plugins const assets = await this._pluginLoader.loadUploadedPluginAssets(pluginId); - const { manifest, code: pluginCode, indexHtml, icon } = assets; + const { manifest, code: pluginCode, indexHtml, icon, translations } = assets; // Store assets if loaded if (indexHtml) { @@ -1222,6 +1240,14 @@ export class PluginService implements OnDestroy { this._pluginIconsSignal.set(new Map(this._pluginIcons)); } + // Load translations into i18n service + if (translations && Object.keys(translations).length > 0) { + this._pluginI18nService.loadPluginTranslationsFromContent( + manifest.id, + translations, + ); + } + // Validate manifest const manifestValidation = validatePluginManifest(manifest); if (!manifestValidation.isValid) {