-
Notifications
You must be signed in to change notification settings - Fork 3.7k
Angular - Hybrid localization support #24731
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
f62fa79
7f9c976
f585f7d
cdad015
60322bd
5bc485c
b7d61f2
ef25844
cbe5e57
6a364d9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,58 @@ | ||
| import { Component, inject, OnInit } from '@angular/core'; | ||
| import { LocalizationPipe, UILocalizationService, SessionStateService } from '@abp/ng.core'; | ||
| import { CommonModule } from '@angular/common'; | ||
| import { CardComponent, CardBodyComponent } from '@abp/ng.theme.shared'; | ||
| import { AsyncPipe } from '@angular/common'; | ||
|
|
||
| @Component({ | ||
| selector: 'app-localization-test', | ||
| standalone: true, | ||
| imports: [CommonModule, LocalizationPipe, CardComponent, CardBodyComponent, AsyncPipe], | ||
| template: ` | ||
| <div class="container mt-5"> | ||
| <h2>Hybrid Localization Test</h2> | ||
| <abp-card cardClass="mt-4"> | ||
| <abp-card-body> | ||
| <h5>Backend Localization (if available)</h5> | ||
| <p><strong>MyProjectName::Welcome:</strong> {{ 'MyProjectName::Welcome' | abpLocalization }}</p> | ||
| <p><strong>AbpAccount::Login:</strong> {{ 'AbpAccount::Login' | abpLocalization }}</p> | ||
| </abp-card-body> | ||
| </abp-card> | ||
| <abp-card cardClass="mt-4"> | ||
| <abp-card-body> | ||
| <h5>UI Localization (from /assets/localization/{{ currentLanguage$ | async }}.json)</h5> | ||
| <p><strong>MyProjectName::CustomKey:</strong> {{ 'MyProjectName::CustomKey' | abpLocalization }}</p> | ||
| <p><strong>MyProjectName::TestMessage:</strong> {{ 'MyProjectName::TestMessage' | abpLocalization }}</p> | ||
| </abp-card-body> | ||
| </abp-card> | ||
| <abp-card cardClass="mt-4"> | ||
| <abp-card-body> | ||
| <h5>UI Override (UI > Backend Priority)</h5> | ||
| <p><strong>AbpAccount::Login:</strong> {{ 'AbpAccount::Login' | abpLocalization }}</p> | ||
| <p class="text-muted">If backend has "Login", UI version should override it</p> | ||
| </abp-card-body> | ||
| </abp-card> | ||
| <abp-card cardClass="mt-4"> | ||
| <abp-card-body> | ||
| <h5>Loaded UI Localizations</h5> | ||
| <pre>{{ loadedLocalizations | json }}</pre> | ||
| </abp-card-body> | ||
| </abp-card> | ||
| </div> | ||
| `, | ||
| }) | ||
| export class LocalizationTestComponent implements OnInit { | ||
| private uiLocalizationService = inject(UILocalizationService); | ||
| private sessionState = inject(SessionStateService); | ||
|
|
||
| loadedLocalizations: any = {}; | ||
| currentLanguage$ = this.sessionState.getLanguage$(); | ||
|
|
||
| ngOnInit() { | ||
| this.loadedLocalizations = this.uiLocalizationService.getLoadedLocalizations(); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| { | ||
| "MyProjectName": { | ||
| "Welcome": "Welcome from UI (en.json)", | ||
| "CustomKey": "This is a UI-only localization", | ||
| "TestMessage": "UI localization is working!" | ||
| }, | ||
| "AbpAccount": { | ||
| "Login": "Sign In (UI Override)" | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| { | ||
| "MyProjectName": { | ||
| "Welcome": "UI'dan Hoş Geldiniz (tr.json)", | ||
| "CustomKey": "Bu sadece UI'da olan bir localization", | ||
| "TestMessage": "UI localization çalışıyor!" | ||
| }, | ||
| "AbpAccount": { | ||
| "Login": "Giriş Yap (UI Override)" | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,119 @@ | ||
| import { Injectable, inject } from '@angular/core'; | ||
| import { HttpClient } from '@angular/common/http'; | ||
| import { BehaviorSubject, distinctUntilChanged, switchMap, of } from 'rxjs'; | ||
| import { catchError, shareReplay, tap } from 'rxjs/operators'; | ||
| 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 UILocalizationResource { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The interface name
Consider renaming to something more descriptive like
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. it is not necessary change |
||
| [resourceName: string]: Record<string, string>; | ||
| } | ||
|
|
||
| /** | ||
| * Service for managing UI localizations in ABP Angular applications. | ||
| * 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); | ||
|
|
||
| private loadedLocalizations$ = new BehaviorSubject<Record<string, UILocalizationResource>>({}); | ||
|
|
||
| private currentLanguage$ = this.sessionState.getLanguage$(); | ||
|
|
||
| constructor() { | ||
| const uiLocalization = this.options.uiLocalization; | ||
| if (uiLocalization?.enabled) { | ||
| this.subscribeToLanguageChanges(); | ||
| } | ||
| } | ||
|
|
||
| private subscribeToLanguageChanges() { | ||
| this.currentLanguage$ | ||
| .pipe( | ||
| distinctUntilChanged(), | ||
| switchMap(culture => this.loadLocalizationFile(culture)) | ||
| ) | ||
| .subscribe(); | ||
|
Comment on lines
+40
to
+43
|
||
| } | ||
|
|
||
| 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<UILocalizationResource>(url).pipe( | ||
| catchError(() => { | ||
| // If file not found or error occurs, return null | ||
| return of(null); | ||
| }), | ||
| tap(data => { | ||
| if (data) { | ||
| this.processLocalizationData(culture, data); | ||
| } | ||
| }), | ||
| ); | ||
| } | ||
|
|
||
| private processLocalizationData(culture: string, data: UILocalizationResource) { | ||
| const abpFormat: ABP.Localization[] = [ | ||
| { | ||
| culture, | ||
| resources: Object.entries(data).map(([resourceName, texts]) => ({ | ||
| resourceName, | ||
| texts, | ||
| })), | ||
| }, | ||
| ]; | ||
| this.localizationService.addLocalization(abpFormat); | ||
|
|
||
| const current = this.loadedLocalizations$.value; | ||
| current[culture] = data; | ||
| this.loadedLocalizations$.next(current); | ||
| } | ||
|
|
||
| addAngularLocalizeLocalization( | ||
| culture: string, | ||
| resourceName: string, | ||
| translations: Record<string, string>, | ||
| ): void { | ||
| const abpFormat: ABP.Localization[] = [ | ||
| { | ||
| culture, | ||
| resources: [ | ||
| { | ||
| resourceName, | ||
| texts: translations, | ||
| }, | ||
| ], | ||
| }, | ||
| ]; | ||
| this.localizationService.addLocalization(abpFormat); | ||
|
|
||
| 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); | ||
| } | ||
|
|
||
| getLoadedLocalizations(culture?: string): UILocalizationResource { | ||
| const lang = culture || this.sessionState.getLanguage(); | ||
| return this.loadedLocalizations$.value[lang] || {}; | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The comment says "UI-only mode" but this feature appears to be hybrid UI+backend localization merging (based on
uiLocalizationoptions and the merge logic inLocalizationService). Please adjust the comment to accurately describe what is being enabled/initialized here.