Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions npm/ng-packs/packages/core/src/lib/models/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,26 @@ export namespace ABP {
othersGroup?: string;
dynamicLayouts?: Map<string, string>;
disableProjectNameInTitle?: boolean;
uiLocalization?: UILocalizationOptions;
}

export interface UILocalizationOptions {
/**
* 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
* 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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -113,6 +118,11 @@ export function provideAbpCore(...features: CoreFeature<CoreFeatureKind>[]) {
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);
}
Comment on lines +121 to +125
Copy link

Copilot AI Jan 27, 2026

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 uiLocalization options and the merge logic in LocalizationService). Please adjust the comment to accurately describe what is being enabled/initialized here.

Copilot uses AI. Check for mistakes.
await getInitialData();
}),
LocaleProvider,
Expand Down
1 change: 1 addition & 0 deletions npm/ng-packs/packages/core/src/lib/services/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, Record<string, string>>
>;
Expand Down Expand Up @@ -90,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
Copy link

Copilot AI Jan 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The inline comment is partly in Turkish ("UI localization'lar backend'i override eder"), which is inconsistent with the rest of the codebase and makes the intent harder to understand for non-Turkish readers. Please rewrite it in English (and keep it consistent with the comment above about UI > Backend priority).

Suggested change
// UI > Backend priority: UI localization'lar backend'i override eder
// UI > Backend priority: UI localizations override backend localizations

Copilot uses AI. Check for mistakes.
resource = { ...remoteTexts, ...resource };

local?.set(resourceName, resource);
});
Expand Down
120 changes: 120 additions & 0 deletions npm/ng-packs/packages/core/src/lib/services/ui-localization.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
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 LocalizationResource {
[resourceName: string]: Record<string, string>;
}
Copy link

Copilot AI Jan 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The exported interface name LocalizationResource is easy to confuse with ABP.LocalizationResource (already defined in models/common.ts) but represents a different shape (JSON file content keyed by resourceName). Consider renaming it to something more specific (e.g., UiLocalizationFile, UiLocalizationDictionary, etc.) to avoid ambiguity for consumers importing from the services barrel.

Copilot uses AI. Check for mistakes.

/**
* 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, LocalizationResource>>({});

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)),
shareReplay(1),
Copy link
Contributor

@sumeyyeKurtulus sumeyyeKurtulus Jan 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hello @erdemcaygor thank you for developing a side feature to enhance the localization. I agree with the co-pilot command here.
The shareReplay(1) usage here is not needed since there is exactly one subscription for this observable.

)
.subscribe();
Comment on lines +40 to +43
Copy link

Copilot AI Jan 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shareReplay(1) is applied to a stream that is immediately subscribed to and not exposed to other consumers, so it provides no benefit here and can be misleading. Consider removing it, or alternatively expose a shared observable if caching/reuse is intended.

Copilot uses AI. Check for mistakes.
}

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<LocalizationResource>(url).pipe(
Copy link

Copilot AI Jan 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR introduces new behavior (loading /assets/localization/{culture}.json and merging it with backend resources, including error handling when the file is missing), but there are no tests covering UILocalizationService. Since the package already has service-level tests under src/lib/tests (e.g. localization.service.spec.ts), please add tests that verify loading on language changes and UI > backend priority merging.

Copilot uses AI. Check for mistakes.
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: LocalizationResource) {
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): LocalizationResource {
const lang = culture || this.sessionState.getLanguage();
return this.loadedLocalizations$.value[lang] || {};
}
}
Loading