From f62fa79f649e619cfc29468fc6e9728485d89e0f Mon Sep 17 00:00:00 2001 From: erdemcaygor Date: Thu, 22 Jan 2026 18:52:12 +0300 Subject: [PATCH 01/10] hybrid localization --- .../packages/core/src/lib/models/common.ts | 20 +++++++++++++++++++ .../providers/core-module-config.provider.ts | 12 ++++++++++- .../packages/core/src/lib/services/index.ts | 1 + .../src/lib/services/localization.service.ts | 2 ++ 4 files changed, 34 insertions(+), 1 deletion(-) diff --git a/npm/ng-packs/packages/core/src/lib/models/common.ts b/npm/ng-packs/packages/core/src/lib/models/common.ts index 704fde958a..2e52e113aa 100644 --- a/npm/ng-packs/packages/core/src/lib/models/common.ts +++ b/npm/ng-packs/packages/core/src/lib/models/common.ts @@ -16,6 +16,26 @@ export namespace ABP { othersGroup?: string; dynamicLayouts?: Map; disableProjectNameInTitle?: boolean; + uiLocalization?: UILocalizationOptions; + } + + export interface UILocalizationOptions { + /** + * Enable UI localization support via @angular/localize + * When enabled, localization files are automatically loaded based on selected language + * Files should be located at: {basePath}/{culture}.json + * Example: /assets/localization/en.json + * JSON format: { "ResourceName": { "Key": "Value" } } + * Merges with backend localizations (UI > Backend priority) + */ + enabled?: boolean; + /** + * Base path for localization JSON files + * Default: '/assets/localization' + * Files should be located at: {basePath}/{culture}.json + * Example: /assets/localization/en.json + */ + basePath?: string; } export interface Child { diff --git a/npm/ng-packs/packages/core/src/lib/providers/core-module-config.provider.ts b/npm/ng-packs/packages/core/src/lib/providers/core-module-config.provider.ts index 6d46caedeb..08f1beb706 100644 --- a/npm/ng-packs/packages/core/src/lib/providers/core-module-config.provider.ts +++ b/npm/ng-packs/packages/core/src/lib/providers/core-module-config.provider.ts @@ -23,7 +23,12 @@ import { RoutesHandler } from '../handlers'; import { ABP, SortableItem } from '../models'; import { AuthErrorFilterService } from '../abstracts'; import { DEFAULT_DYNAMIC_LAYOUTS } from '../constants'; -import { LocalizationService, LocalStorageListenerService, AbpTitleStrategy } from '../services'; +import { + LocalizationService, + LocalStorageListenerService, + AbpTitleStrategy, + UILocalizationService, +} from '../services'; import { DefaultQueueManager, getInitialData } from '../utils'; import { CookieLanguageProvider, IncludeLocalizationResourcesProvider, LocaleProvider } from './'; import { timezoneInterceptor, transferStateInterceptor } from '../interceptors'; @@ -113,6 +118,11 @@ export function provideAbpCore(...features: CoreFeature[]) { inject(LocalizationService); inject(LocalStorageListenerService); inject(RoutesHandler); + // Initialize UILocalizationService if UI-only mode is enabled + const options = inject(CORE_OPTIONS); + if (options?.uiLocalization?.enabled) { + inject(UILocalizationService); + } await getInitialData(); }), LocaleProvider, diff --git a/npm/ng-packs/packages/core/src/lib/services/index.ts b/npm/ng-packs/packages/core/src/lib/services/index.ts index 61fd1ef480..205fd2f0dc 100644 --- a/npm/ng-packs/packages/core/src/lib/services/index.ts +++ b/npm/ng-packs/packages/core/src/lib/services/index.ts @@ -8,6 +8,7 @@ export * from './http-wait.service'; export * from './lazy-load.service'; export * from './list.service'; export * from './localization.service'; +export * from './ui-localization.service'; export * from './multi-tenancy.service'; export * from './permission.service'; export * from './replaceable-components.service'; diff --git a/npm/ng-packs/packages/core/src/lib/services/localization.service.ts b/npm/ng-packs/packages/core/src/lib/services/localization.service.ts index d91679effb..6ed442db5b 100644 --- a/npm/ng-packs/packages/core/src/lib/services/localization.service.ts +++ b/npm/ng-packs/packages/core/src/lib/services/localization.service.ts @@ -60,6 +60,8 @@ export class LocalizationService { private initLocalizationValues() { localizations$.subscribe(val => this.addLocalization(val)); + // Backend-based localization loading (always enabled) + // UI localizations are merged via addLocalization() (UI > Backend priority) const legacyResources$ = this.configState.getDeep$('localization.values') as Observable< Record> >; From 7f9c97696dc40e384b7a78000232e8791c0e95de Mon Sep 17 00:00:00 2001 From: erdemcaygor Date: Fri, 23 Jan 2026 12:09:05 +0300 Subject: [PATCH 02/10] ui localization service added --- .../lib/services/ui-localization.service.ts | 163 ++++++++++++++++++ 1 file changed, 163 insertions(+) create mode 100644 npm/ng-packs/packages/core/src/lib/services/ui-localization.service.ts diff --git a/npm/ng-packs/packages/core/src/lib/services/ui-localization.service.ts b/npm/ng-packs/packages/core/src/lib/services/ui-localization.service.ts new file mode 100644 index 0000000000..9d25f119b2 --- /dev/null +++ b/npm/ng-packs/packages/core/src/lib/services/ui-localization.service.ts @@ -0,0 +1,163 @@ +import { Injectable, inject } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { BehaviorSubject, distinctUntilChanged, switchMap, of } from 'rxjs'; +import { map, catchError, shareReplay, tap } from 'rxjs/operators'; +import { loadTranslations } from '@angular/localize'; +import { ABP } from '../models/common'; +import { LocalizationService } from './localization.service'; +import { SessionStateService } from './session-state.service'; +import { CORE_OPTIONS } from '../tokens/options.token'; + +export interface LocalizationResource { + [resourceName: string]: Record; +} + +/** + * Service for managing UI localizations via @angular/localize + * Automatically loads localization files based on selected language + * Merges with backend localizations (UI > Backend priority) + */ +@Injectable({ providedIn: 'root' }) +export class UILocalizationService { + private http = inject(HttpClient); + private localizationService = inject(LocalizationService); + private sessionState = inject(SessionStateService); + private options = inject(CORE_OPTIONS); + + // Yüklenen localization'lar (culture -> resourceName -> texts) + private loadedLocalizations$ = new BehaviorSubject>({}); + + // Current language + private currentLanguage$ = this.sessionState.getLanguage$(); + + constructor() { + const uiLocalization = this.options.uiLocalization; + if (uiLocalization?.enabled) { + // Dil değiştiğinde otomatik yükle + this.subscribeToLanguageChanges(); + } + } + + /** + * Dil değişikliğini dinle ve localization dosyasını yükle + */ + private subscribeToLanguageChanges() { + this.currentLanguage$ + .pipe( + distinctUntilChanged(), + switchMap(culture => this.loadLocalizationFile(culture)), + shareReplay(1), + ) + .subscribe(); + } + + /** + * Seçilen dil için localization dosyasını yükle + * Format: /assets/localization/{culture}.json + * JSON format: { "ResourceName": { "Key": "Value" } } + */ + private loadLocalizationFile(culture: string) { + const config = this.options.uiLocalization; + if (!config?.enabled) return of(null); + + const basePath = config.basePath || '/assets/localization'; + const url = `${basePath}/${culture}.json`; + + return this.http.get(url).pipe( + catchError(() => { + // Dosya yoksa sessizce devam et (backend'den gelecek) + return of(null); + }), + tap(data => { + if (data) { + this.processLocalizationData(culture, data); + } + }), + ); + } + + /** + * Localization verisini işle: + * 1. @angular/localize'a ekle (loadTranslations) + * 2. ABP LocalizationService'e ekle (addLocalization) + */ + private processLocalizationData(culture: string, data: LocalizationResource) { + // 1. @angular/localize'a ekle + const loadTranslationsMap: Record = {}; + Object.entries(data).forEach(([resourceName, texts]) => { + Object.entries(texts).forEach(([key, value]) => { + loadTranslationsMap[`${resourceName}::${key}`] = value; + }); + }); + loadTranslations(loadTranslationsMap); + + // 2. ABP LocalizationService'e ekle + const abpFormat: ABP.Localization[] = [ + { + culture, + resources: Object.entries(data).map(([resourceName, texts]) => ({ + resourceName, + texts, + })), + }, + ]; + this.localizationService.addLocalization(abpFormat); + + // 3. Cache'e ekle + const current = this.loadedLocalizations$.value; + current[culture] = data; + this.loadedLocalizations$.next(current); + } + + /** + * Manuel olarak localization ekle (runtime'da) + */ + addAngularLocalizeLocalization( + culture: string, + resourceName: string, + translations: Record, + ): void { + // @angular/localize'a ekle + const loadTranslationsMap: Record = {}; + Object.entries(translations).forEach(([key, value]) => { + loadTranslationsMap[`${resourceName}::${key}`] = value; + }); + loadTranslations(loadTranslationsMap); + + // ABP LocalizationService'e ekle + const abpFormat: ABP.Localization[] = [ + { + culture, + resources: [ + { + resourceName, + texts: translations, + }, + ], + }, + ]; + this.localizationService.addLocalization(abpFormat); + + // Cache'e ekle + const current = this.loadedLocalizations$.value; + if (!current[culture]) { + current[culture] = {}; + } + if (!current[culture][resourceName]) { + current[culture][resourceName] = {}; + } + current[culture][resourceName] = { + ...current[culture][resourceName], + ...translations, + }; + this.loadedLocalizations$.next(current); + } + + /** + * Yüklenen localization'ları al + */ + getLoadedLocalizations(culture?: string): LocalizationResource { + const lang = culture || this.sessionState.getLanguage(); + return this.loadedLocalizations$.value[lang] || {}; + } +} From f585f7dc60db072bb5be3a6ecab6e385fd194d11 Mon Sep 17 00:00:00 2001 From: erdemcaygor Date: Tue, 27 Jan 2026 16:23:25 +0300 Subject: [PATCH 03/10] localization service updated --- .../src/lib/services/localization.service.ts | 3 ++- .../lib/services/ui-localization.service.ts | 24 ------------------- 2 files changed, 2 insertions(+), 25 deletions(-) diff --git a/npm/ng-packs/packages/core/src/lib/services/localization.service.ts b/npm/ng-packs/packages/core/src/lib/services/localization.service.ts index 6ed442db5b..c2fd8759a4 100644 --- a/npm/ng-packs/packages/core/src/lib/services/localization.service.ts +++ b/npm/ng-packs/packages/core/src/lib/services/localization.service.ts @@ -92,7 +92,8 @@ export class LocalizationService { const resourceName = entry[0]; const remoteTexts = entry[1]; let resource = local?.get(resourceName) || {}; - resource = { ...resource, ...remoteTexts }; + // UI > Backend priority: UI localization'lar backend'i override eder + resource = { ...remoteTexts, ...resource }; local?.set(resourceName, resource); }); diff --git a/npm/ng-packs/packages/core/src/lib/services/ui-localization.service.ts b/npm/ng-packs/packages/core/src/lib/services/ui-localization.service.ts index 9d25f119b2..53227262a0 100644 --- a/npm/ng-packs/packages/core/src/lib/services/ui-localization.service.ts +++ b/npm/ng-packs/packages/core/src/lib/services/ui-localization.service.ts @@ -24,23 +24,17 @@ export class UILocalizationService { private sessionState = inject(SessionStateService); private options = inject(CORE_OPTIONS); - // Yüklenen localization'lar (culture -> resourceName -> texts) private loadedLocalizations$ = new BehaviorSubject>({}); - // Current language private currentLanguage$ = this.sessionState.getLanguage$(); constructor() { const uiLocalization = this.options.uiLocalization; if (uiLocalization?.enabled) { - // Dil değiştiğinde otomatik yükle this.subscribeToLanguageChanges(); } } - /** - * Dil değişikliğini dinle ve localization dosyasını yükle - */ private subscribeToLanguageChanges() { this.currentLanguage$ .pipe( @@ -51,11 +45,6 @@ export class UILocalizationService { .subscribe(); } - /** - * Seçilen dil için localization dosyasını yükle - * Format: /assets/localization/{culture}.json - * JSON format: { "ResourceName": { "Key": "Value" } } - */ private loadLocalizationFile(culture: string) { const config = this.options.uiLocalization; if (!config?.enabled) return of(null); @@ -76,11 +65,6 @@ export class UILocalizationService { ); } - /** - * Localization verisini işle: - * 1. @angular/localize'a ekle (loadTranslations) - * 2. ABP LocalizationService'e ekle (addLocalization) - */ private processLocalizationData(culture: string, data: LocalizationResource) { // 1. @angular/localize'a ekle const loadTranslationsMap: Record = {}; @@ -91,7 +75,6 @@ export class UILocalizationService { }); loadTranslations(loadTranslationsMap); - // 2. ABP LocalizationService'e ekle const abpFormat: ABP.Localization[] = [ { culture, @@ -103,15 +86,11 @@ export class UILocalizationService { ]; this.localizationService.addLocalization(abpFormat); - // 3. Cache'e ekle const current = this.loadedLocalizations$.value; current[culture] = data; this.loadedLocalizations$.next(current); } - /** - * Manuel olarak localization ekle (runtime'da) - */ addAngularLocalizeLocalization( culture: string, resourceName: string, @@ -153,9 +132,6 @@ export class UILocalizationService { this.loadedLocalizations$.next(current); } - /** - * Yüklenen localization'ları al - */ getLoadedLocalizations(culture?: string): LocalizationResource { const lang = culture || this.sessionState.getLanguage(); return this.loadedLocalizations$.value[lang] || {}; From cdad015f89a6e45faef85d11cf8d66c715aa0a0e Mon Sep 17 00:00:00 2001 From: erdemcaygor Date: Tue, 27 Jan 2026 16:36:29 +0300 Subject: [PATCH 04/10] localization service updated --- .../lib/services/ui-localization.service.ts | 25 +++---------------- 1 file changed, 3 insertions(+), 22 deletions(-) diff --git a/npm/ng-packs/packages/core/src/lib/services/ui-localization.service.ts b/npm/ng-packs/packages/core/src/lib/services/ui-localization.service.ts index 53227262a0..d6af88b587 100644 --- a/npm/ng-packs/packages/core/src/lib/services/ui-localization.service.ts +++ b/npm/ng-packs/packages/core/src/lib/services/ui-localization.service.ts @@ -1,8 +1,7 @@ import { Injectable, inject } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { BehaviorSubject, distinctUntilChanged, switchMap, of } from 'rxjs'; -import { map, catchError, shareReplay, tap } from 'rxjs/operators'; -import { loadTranslations } from '@angular/localize'; +import { catchError, shareReplay, tap } from 'rxjs/operators'; import { ABP } from '../models/common'; import { LocalizationService } from './localization.service'; import { SessionStateService } from './session-state.service'; @@ -13,7 +12,7 @@ export interface LocalizationResource { } /** - * Service for managing UI localizations via @angular/localize + * Service for managing UI localizations in ABP Angular applications. * Automatically loads localization files based on selected language * Merges with backend localizations (UI > Backend priority) */ @@ -54,7 +53,7 @@ export class UILocalizationService { return this.http.get(url).pipe( catchError(() => { - // Dosya yoksa sessizce devam et (backend'den gelecek) + // If file not found or error occurs, return null return of(null); }), tap(data => { @@ -66,15 +65,6 @@ export class UILocalizationService { } private processLocalizationData(culture: string, data: LocalizationResource) { - // 1. @angular/localize'a ekle - const loadTranslationsMap: Record = {}; - Object.entries(data).forEach(([resourceName, texts]) => { - Object.entries(texts).forEach(([key, value]) => { - loadTranslationsMap[`${resourceName}::${key}`] = value; - }); - }); - loadTranslations(loadTranslationsMap); - const abpFormat: ABP.Localization[] = [ { culture, @@ -96,14 +86,6 @@ export class UILocalizationService { resourceName: string, translations: Record, ): void { - // @angular/localize'a ekle - const loadTranslationsMap: Record = {}; - Object.entries(translations).forEach(([key, value]) => { - loadTranslationsMap[`${resourceName}::${key}`] = value; - }); - loadTranslations(loadTranslationsMap); - - // ABP LocalizationService'e ekle const abpFormat: ABP.Localization[] = [ { culture, @@ -117,7 +99,6 @@ export class UILocalizationService { ]; this.localizationService.addLocalization(abpFormat); - // Cache'e ekle const current = this.loadedLocalizations$.value; if (!current[culture]) { current[culture] = {}; From 60322bdeb59b0876cb63a7546d99ca572371da8d Mon Sep 17 00:00:00 2001 From: erdemcaygor Date: Tue, 27 Jan 2026 16:42:51 +0300 Subject: [PATCH 05/10] refactoring --- npm/ng-packs/packages/core/src/lib/models/common.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/npm/ng-packs/packages/core/src/lib/models/common.ts b/npm/ng-packs/packages/core/src/lib/models/common.ts index 2e52e113aa..1506dc3a8b 100644 --- a/npm/ng-packs/packages/core/src/lib/models/common.ts +++ b/npm/ng-packs/packages/core/src/lib/models/common.ts @@ -21,7 +21,7 @@ export namespace ABP { export interface UILocalizationOptions { /** - * Enable UI localization support via @angular/localize + * Enable UI localization feature * When enabled, localization files are automatically loaded based on selected language * Files should be located at: {basePath}/{culture}.json * Example: /assets/localization/en.json From 5bc485c5fb548cf0b6833d5a3044c6ba7a3d06b0 Mon Sep 17 00:00:00 2001 From: erdemcaygor Date: Tue, 27 Jan 2026 16:46:37 +0300 Subject: [PATCH 06/10] example page added --- .../apps/dev-app/src/app/app.config.ts | 4 ++ .../apps/dev-app/src/app/app.routes.ts | 4 ++ .../dev-app/src/app/home/home.component.html | 1 + .../localization-test.component.ts | 59 +++++++++++++++++++ .../dev-app/src/assets/localization/en.json | 10 ++++ .../dev-app/src/assets/localization/tr.json | 10 ++++ 6 files changed, 88 insertions(+) create mode 100644 npm/ng-packs/apps/dev-app/src/app/localization-test/localization-test.component.ts create mode 100644 npm/ng-packs/apps/dev-app/src/assets/localization/en.json create mode 100644 npm/ng-packs/apps/dev-app/src/assets/localization/tr.json diff --git a/npm/ng-packs/apps/dev-app/src/app/app.config.ts b/npm/ng-packs/apps/dev-app/src/app/app.config.ts index daa8a1f937..6b8bcf2c05 100644 --- a/npm/ng-packs/apps/dev-app/src/app/app.config.ts +++ b/npm/ng-packs/apps/dev-app/src/app/app.config.ts @@ -31,6 +31,10 @@ export const appConfig: ApplicationConfig = { registerLocaleFn: registerLocaleForEsBuild(), sendNullsAsQueryParam: false, skipGetAppConfiguration: false, + uiLocalization: { + enabled: true, + basePath: '/assets/localization', + }, }), ), provideAbpOAuth(), diff --git a/npm/ng-packs/apps/dev-app/src/app/app.routes.ts b/npm/ng-packs/apps/dev-app/src/app/app.routes.ts index c520328975..6c59a18bb1 100644 --- a/npm/ng-packs/apps/dev-app/src/app/app.routes.ts +++ b/npm/ng-packs/apps/dev-app/src/app/app.routes.ts @@ -10,6 +10,10 @@ export const appRoutes: Routes = [ path: 'dynamic-form', loadComponent: () => import('./dynamic-form-page/dynamic-form-page.component').then(m => m.DynamicFormPageComponent), }, + { + path: 'localization-test', + loadComponent: () => import('./localization-test/localization-test.component').then(m => m.LocalizationTestComponent), + }, { path: 'account', loadChildren: () => import('@abp/ng.account').then(m => m.createRoutes()), diff --git a/npm/ng-packs/apps/dev-app/src/app/home/home.component.html b/npm/ng-packs/apps/dev-app/src/app/home/home.component.html index d83f2be7f1..d8b08c06ae 100644 --- a/npm/ng-packs/apps/dev-app/src/app/home/home.component.html +++ b/npm/ng-packs/apps/dev-app/src/app/home/home.component.html @@ -1,6 +1,7 @@