diff --git a/Applications/Pgan.PoracleWebNet.App/ClientApp/src/app/app.config.spec.ts b/Applications/Pgan.PoracleWebNet.App/ClientApp/src/app/app.config.spec.ts new file mode 100644 index 00000000..e8842650 --- /dev/null +++ b/Applications/Pgan.PoracleWebNet.App/ClientApp/src/app/app.config.spec.ts @@ -0,0 +1,46 @@ +import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing'; +import { TestBed } from '@angular/core/testing'; +import { TranslateService } from '@ngx-translate/core'; + +import { appConfig } from './app.config'; + +/** + * Guards the NG0200 regression introduced by the ngx-translate v18 upgrade. + * + * v18 loads `fallbackLang` eagerly from inside the TranslateService constructor. Setting it in + * `provideTranslateService()` therefore resolves TranslateLoader while the injector is still + * building TranslateService, which fails with NG0200 (circular dependency) and leaves the + * fallback language — English — rendering raw keys, while every other locale loaded fine. + * + * These tests exercise the real `appConfig` providers, so re-adding `fallbackLang` there fails + * here rather than silently in production. + */ +describe('appConfig translation wiring', () => { + beforeEach(() => { + TestBed.resetTestingModule(); + TestBed.configureTestingModule({ + providers: [...appConfig.providers, provideHttpClientTesting()], + }); + }); + + it('constructs TranslateService without a circular dependency', () => { + expect(() => TestBed.inject(TranslateService)).not.toThrow(); + }); + + it('does not fetch any translation before a language is requested', () => { + TestBed.inject(TranslateService); + // An eager fallback load would already have issued a request here. + TestBed.inject(HttpTestingController).verify(); + }); + + it('loads English through the configured HTTP loader once requested', () => { + const translate = TestBed.inject(TranslateService); + const http = TestBed.inject(HttpTestingController); + + translate.use('en'); + http.expectOne('./assets/i18n/en.json').flush({ NAV: { DASHBOARD: 'Dashboard' } }); + + expect(translate.instant('NAV.DASHBOARD')).toBe('Dashboard'); + http.verify(); + }); +}); diff --git a/Applications/Pgan.PoracleWebNet.App/ClientApp/src/app/app.config.ts b/Applications/Pgan.PoracleWebNet.App/ClientApp/src/app/app.config.ts index 0144378f..f8888de9 100644 --- a/Applications/Pgan.PoracleWebNet.App/ClientApp/src/app/app.config.ts +++ b/Applications/Pgan.PoracleWebNet.App/ClientApp/src/app/app.config.ts @@ -18,8 +18,17 @@ export const appConfig: ApplicationConfig = { // refresh + retry) before errorInterceptor redirects to the login page. provideHttpClient(withInterceptors([authInterceptor, errorInterceptor, oidcRefreshInterceptor])), provideAnimationsAsync(), + // No `fallbackLang` here on purpose. ngx-translate v18 loads the fallback language eagerly + // from inside the TranslateService constructor, which resolves TranslateLoader while the + // injector is still building TranslateService -- a circular dependency that fails with + // NG0200 ("error loading translation for en"). Only the fallback language is affected, so + // English silently rendered raw keys while every other locale, loaded later via use(), + // worked fine. v17's `defaultLanguage` did not load eagerly, which is why this only + // appeared after the v18 upgrade. + // + // I18nService.init() calls setFallbackLang('en') instead, after bootstrap, where the + // injector is complete and the loader resolves normally. provideTranslateService({ - fallbackLang: 'en', loader: { provide: TranslateLoader, useClass: TranslateHttpLoader, diff --git a/CHANGELOG.md b/CHANGELOG.md index 451a9665..4e68384a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Fixed +- **English UI showed raw translation keys (`NAV.DASHBOARD`, `AUTH.SIGN_IN`) after upgrading to v2.12.0.** The ngx-translate v18 upgrade moved `defaultLanguage` to `fallbackLang`, but v18 loads the fallback language *eagerly from inside the `TranslateService` constructor*. Setting it in `provideTranslateService()` therefore resolved `TranslateLoader` while the injector was still building `TranslateService`, failing with `NG0200: Circular dependency detected`. Only the fallback language was affected, so English rendered raw keys while every other locale — loaded later via `use()` — worked normally. The fallback is now set in `I18nService.init()` after bootstrap, where the injector is complete. Regression tests exercise the real `appConfig` providers, so re-adding `fallbackLang` to the provider config fails CI. + ## [2.12.0] - 2026-08-05 ### Added