Skip to content

Commit 5bc8773

Browse files
committed
feat: add explicit lang parameter to translation API
Adds an optional `lang` parameter to the core translation methods so callers can resolve a key in a specific language without switching the active language: - instant() and the internal translation pipeline - get(), stream(), getStreamOnTranslationChange() - translate() signal method - standalone translate() function - ITranslateService interface Covered by tests for array keys, parent-service traversal, and undefined-safe signal typing. Closes #1616 Closes #1101 Also addresses long-standing requests for this capability: #91, #313, #570, #1132, #1334 Partially addresses (service methods only — pipe-level lang parameter is still pending): #233, #719
1 parent ec81eb8 commit 5bc8773

6 files changed

Lines changed: 200 additions & 21 deletions

File tree

projects/ngx-translate/src/lib/translate.function.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,15 @@ import { inject, Signal } from "@angular/core";
22
import { TranslateService } from "./translate.service";
33
import {
44
InterpolationParameters,
5+
Language,
56
Translation,
67
TranslationObject,
78
} from "./translate.service.interface";
89

910
export function translate(
1011
key: string | Signal<string>,
1112
params?: InterpolationParameters | Signal<InterpolationParameters | undefined>,
13+
lang?: Language | Signal<Language>,
1214
): Signal<Translation | TranslationObject> {
13-
return inject(TranslateService).translate(key, params);
15+
return inject(TranslateService).translate(key, params, lang);
1416
}

projects/ngx-translate/src/lib/translate.service.interface.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ export abstract class ITranslateService {
6666
public abstract instant(
6767
key: string | string[],
6868
interpolateParams?: InterpolationParameters,
69+
lang?: Language,
6970
): Translation;
7071

7172
/**
@@ -79,16 +80,19 @@ export abstract class ITranslateService {
7980
public abstract translate(
8081
key: string | Signal<string>,
8182
params?: InterpolationParameters | Signal<InterpolationParameters | undefined>,
83+
lang?: Language | Signal<Language>,
8284
): Signal<Translation | TranslationObject>;
8385

8486
public abstract stream(
8587
key: string | string[],
8688
interpolateParams?: InterpolationParameters,
89+
lang?: Language,
8790
): Observable<Translation>;
8891

8992
public abstract getStreamOnTranslationChange(
9093
key: string | string[],
9194
interpolateParams?: InterpolationParameters,
95+
lang?: Language,
9296
): Observable<Translation>;
9397

9498
public abstract set(
@@ -100,6 +104,7 @@ export abstract class ITranslateService {
100104
public abstract get(
101105
key: string | string[],
102106
interpolateParams?: InterpolationParameters,
107+
lang?: Language,
103108
): Observable<Translation>;
104109

105110
public abstract setTranslation(
@@ -111,6 +116,7 @@ export abstract class ITranslateService {
111116
public abstract getParsedResult(
112117
key: string | string[],
113118
interpolateParams?: InterpolationParameters,
119+
lang?: Language,
114120
): StrictTranslation | Observable<StrictTranslation>;
115121

116122
public abstract getBrowserLang(): Language | undefined;

projects/ngx-translate/src/lib/translate.service.ts

Lines changed: 35 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -374,8 +374,9 @@ export class TranslateService implements ITranslateService {
374374
protected getParsedResultForKey(
375375
key: string,
376376
interpolateParams?: InterpolationParameters,
377+
lang?: Language,
377378
): StrictTranslation | Observable<StrictTranslation> {
378-
const textToInterpolate = this.getTextToInterpolate(key);
379+
const textToInterpolate = this.getTextToInterpolate(key, lang);
379380

380381
if (isDefinedAndNotNull(textToInterpolate)) {
381382
return this.runInterpolation(textToInterpolate, interpolateParams);
@@ -402,7 +403,15 @@ export class TranslateService implements ITranslateService {
402403
return this.isRoot ? this._fallbackLang() : (this.parent?.getFallbackLang() ?? null);
403404
}
404405

405-
protected getTextToInterpolate(key: string): InterpolatableTranslation | undefined {
406+
protected getTextToInterpolate(key: string, lang?: Language): InterpolatableTranslation | undefined {
407+
if (lang) {
408+
const res = this.store.getTranslationValue(lang, key);
409+
if (res !== undefined) {
410+
return res;
411+
}
412+
return this.parent?.getTextToInterpolate(key, lang);
413+
}
414+
406415
const currentLang = this.getCurrentLang();
407416
const fallbackLang = this.getFallbackLang();
408417

@@ -473,21 +482,23 @@ export class TranslateService implements ITranslateService {
473482
public getParsedResult(
474483
key: string | string[],
475484
interpolateParams?: InterpolationParameters,
485+
lang?: Language,
476486
): StrictTranslation | Observable<StrictTranslation> {
477487
return key instanceof Array
478-
? this.getParsedResultForArray(key, interpolateParams)
479-
: this.getParsedResultForKey(key, interpolateParams);
488+
? this.getParsedResultForArray(key, interpolateParams, lang)
489+
: this.getParsedResultForKey(key, interpolateParams, lang);
480490
}
481491

482492
protected getParsedResultForArray(
483493
key: string[],
484494
interpolateParams: InterpolationParameters | undefined,
495+
lang?: Language,
485496
) {
486497
const result: Record<string, StrictTranslation | Observable<StrictTranslation>> = {};
487498

488499
let observables = false;
489500
for (const k of key) {
490-
result[k] = this.getParsedResultForKey(k, interpolateParams);
501+
result[k] = this.getParsedResultForKey(k, interpolateParams, lang);
491502
observables = observables || isObservable(result[k]);
492503
}
493504

@@ -514,6 +525,7 @@ export class TranslateService implements ITranslateService {
514525
public get(
515526
key: string | string[],
516527
interpolateParams?: InterpolationParameters,
528+
lang?: Language,
517529
): Observable<Translation> {
518530
if (!isDefinedAndNotNull(key) || !key.length) {
519531
return of("");
@@ -523,12 +535,12 @@ export class TranslateService implements ITranslateService {
523535
if (this.lastUseLanguage && this.loadingTranslations[this.lastUseLanguage]) {
524536
return this.loadingTranslations[this.lastUseLanguage].pipe(
525537
concatMap(() => {
526-
return makeObservable(this.getParsedResult(key, interpolateParams));
538+
return makeObservable(this.getParsedResult(key, interpolateParams, lang));
527539
}),
528540
);
529541
}
530542

531-
return makeObservable(this.getParsedResult(key, interpolateParams));
543+
return makeObservable(this.getParsedResult(key, interpolateParams, lang));
532544
}
533545

534546
/**
@@ -539,16 +551,17 @@ export class TranslateService implements ITranslateService {
539551
public getStreamOnTranslationChange(
540552
key: string | string[],
541553
interpolateParams?: InterpolationParameters,
554+
lang?: Language,
542555
): Observable<Translation> {
543556
if (!isDefinedAndNotNull(key) || !key.length) {
544557
throw new Error(`Parameter "key" is required and cannot be empty`);
545558
}
546559

547560
return concat(
548-
defer(() => this.get(key, interpolateParams)),
561+
defer(() => this.get(key, interpolateParams, lang)),
549562
this.onTranslationChange.pipe(
550563
switchMap(() => {
551-
const res = this.getParsedResult(key, interpolateParams);
564+
const res = this.getParsedResult(key, interpolateParams, lang);
552565
return makeObservable(res);
553566
}),
554567
),
@@ -563,16 +576,17 @@ export class TranslateService implements ITranslateService {
563576
public stream(
564577
key: string | string[],
565578
interpolateParams?: InterpolationParameters,
579+
lang?: Language,
566580
): Observable<Translation> {
567581
if (!isDefinedAndNotNull(key) || !key.length) {
568582
throw new Error(`Parameter "key" required`);
569583
}
570584

571585
return concat(
572-
defer(() => this.get(key, interpolateParams)),
586+
defer(() => this.get(key, interpolateParams, lang)),
573587
this.onLangChange.pipe(
574588
switchMap(() => {
575-
const res = this.getParsedResult(key, interpolateParams);
589+
const res = this.getParsedResult(key, interpolateParams, lang);
576590
return makeObservable(res);
577591
}),
578592
),
@@ -583,16 +597,20 @@ export class TranslateService implements ITranslateService {
583597
* Returns a translation instantly from the internal state of loaded translation.
584598
* All rules regarding the current language, the preferred language of even fallback languages
585599
* will be used except any promise handling.
600+
*
601+
* When `lang` is provided, the lookup goes directly to the specified language,
602+
* bypassing the current language and fallback chain.
586603
*/
587604
public instant(
588605
key: string | string[],
589606
interpolateParams?: InterpolationParameters,
607+
lang?: Language,
590608
): Translation {
591609
if (!isDefinedAndNotNull(key) || key.length === 0) {
592610
return "";
593611
}
594612

595-
const result = this.getParsedResult(key, interpolateParams);
613+
const result = this.getParsedResult(key, interpolateParams, lang);
596614

597615
return isObservable(result) ? this.keyToObject(key) : result;
598616
}
@@ -623,17 +641,21 @@ export class TranslateService implements ITranslateService {
623641
public translate(
624642
key: string | Signal<string>,
625643
params?: InterpolationParameters | Signal<InterpolationParameters | undefined>,
644+
lang?: Language | Signal<Language | undefined>,
626645
): Signal<Translation | TranslationObject> {
627646
return computed(() => {
628647
// Unwrap signals if needed
629648
const currentKey = isSignal(key) ? key() : key;
630649
const currentParams = params !== undefined && isSignal(params)
631650
? (params as Signal<InterpolationParameters | undefined>)()
632651
: params;
652+
const currentLang = lang !== undefined && isSignal(lang)
653+
? lang()
654+
: lang;
633655

634656
// instant() internally reads the store's translations() signal,
635657
// which provides reactivity for lang/translation/fallback changes.
636-
return this.instant(currentKey, currentParams);
658+
return this.instant(currentKey, currentParams, currentLang);
637659
});
638660
}
639661

projects/ngx-translate/src/tests/translate.function.spec.ts

Lines changed: 38 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,17 +10,23 @@ import {
1010
TranslationObject,
1111
} from "../public-api";
1212

13-
const translations: TranslationObject = {
14-
HELLO: "Hello",
15-
GREETING: "Hello {{name}}",
16-
NESTED: {
17-
KEY: "Nested value",
13+
const translations: Record<string, TranslationObject> = {
14+
en: {
15+
HELLO: "Hello",
16+
GREETING: "Hello {{name}}",
17+
NESTED: {
18+
KEY: "Nested value",
19+
},
20+
},
21+
de: {
22+
HELLO: "Hallo",
23+
GREETING: "Hallo {{name}}",
1824
},
1925
};
2026

2127
class FakeLoader implements TranslateLoader {
22-
getTranslation(): Observable<TranslationObject> {
23-
return of(translations);
28+
getTranslation(lang: string): Observable<TranslationObject> {
29+
return of(translations[lang] ?? {});
2430
}
2531
}
2632

@@ -98,4 +104,29 @@ describe("translate() standalone function", () => {
98104
expect(result()).toBe("Hello Bob");
99105
});
100106
});
107+
108+
it("should return translation from specified language", () => {
109+
const service = TestBed.inject(TranslateService);
110+
service.setFallbackLang("de");
111+
TestBed.runInInjectionContext(() => {
112+
const result = translate("HELLO", undefined, "de");
113+
TestBed.flushEffects();
114+
expect(result()).toBe("Hallo");
115+
});
116+
});
117+
118+
it("should react to lang signal changes", () => {
119+
const service = TestBed.inject(TranslateService);
120+
service.setFallbackLang("de");
121+
TestBed.runInInjectionContext(() => {
122+
const lang = signal("de");
123+
const result = translate("HELLO", undefined, lang);
124+
TestBed.flushEffects();
125+
expect(result()).toBe("Hallo");
126+
127+
lang.set("en");
128+
TestBed.flushEffects();
129+
expect(result()).toBe("Hello");
130+
});
131+
});
101132
});

projects/ngx-translate/src/tests/translate.hierarchy.spec.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,37 @@ describe("TranslateService Hierarchy", () => {
169169
expect(childService.fallbackLang).toBe(rootService.fallbackLang);
170170
});
171171

172+
it("should traverse parent with explicit lang parameter", () => {
173+
const rootInjector = Injector.create({
174+
providers: [
175+
provideTranslateService({
176+
loader: {
177+
provide: TranslateLoader,
178+
useValue: new FakeLoader({ ROOT_KEY: "root-val" }),
179+
},
180+
}),
181+
],
182+
});
183+
const rootService = rootInjector.get(TranslateService);
184+
rootService.use("en");
185+
186+
const childInjector = Injector.create({
187+
providers: [
188+
provideChildTranslateService({
189+
loader: {
190+
provide: TranslateLoader,
191+
useValue: new FakeLoader({ CHILD_KEY: "child-val" }),
192+
},
193+
}),
194+
],
195+
parent: rootInjector,
196+
});
197+
const childService = childInjector.get(TranslateService);
198+
199+
// Child doesn't have ROOT_KEY, parent does — should traverse with explicit lang
200+
expect(childService.instant("ROOT_KEY", undefined, "en")).toEqual("root-val");
201+
});
202+
172203
it("should propagate parent translation changes to child translate() signal", () => {
173204
const rootInjector = Injector.create({
174205
providers: [

0 commit comments

Comments
 (0)