Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
6 changes: 3 additions & 3 deletions projects/ngx-translate/src/lib/translate.pipe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,15 @@ import { InterpolationParameters, Translation } from "./translate.service.interf
standalone: true,
pure: false, // required to update the value when the signal changes
})
export class TranslatePipe implements PipeTransform {
private translateService = inject(TranslateService);
export class TranslatePipe<Key extends string = string> implements PipeTransform {
private translateService = inject<TranslateService<Key>>(TranslateService<Key>);

private cachedSignal: Signal<Translation> | null = null;
private lastKey: string | null = null;
private lastParams: InterpolationParameters | undefined;

/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
transform(query: string | undefined | null, ...args: any[]): any {
transform(query: Key | undefined | null, ...args: any[]): any {
if (!query || !query.length) {
return query;
}
Expand Down
20 changes: 10 additions & 10 deletions projects/ngx-translate/src/lib/translate.service.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ export interface FallbackLangChangeEvent {
translations: InterpolatableTranslationObject;
}

export abstract class ITranslateService {
export abstract class ITranslateService<Key extends string = string> {
public abstract readonly onTranslationChange: Observable<TranslationChangeEvent>;
public abstract readonly onLangChange: Observable<LangChangeEvent>;
public abstract readonly onFallbackLangChange: Observable<FallbackLangChangeEvent>;
Expand All @@ -64,7 +64,7 @@ export abstract class ITranslateService {
public abstract resetLang(lang: Language): void;

public abstract instant(
key: string | string[],
key: Key | Key[],
interpolateParams?: InterpolationParameters,
lang?: Language,
): Translation;
Expand All @@ -83,31 +83,31 @@ export abstract class ITranslateService {
* @returns A Signal that emits the translated value
*/
public abstract translate(
key: string | string[] | (() => string | string[]),
key: Key | Key[] | (() => Key | Key[]),
params?: InterpolationParameters | (() => InterpolationParameters | undefined),
lang?: Language | (() => Language | undefined),
): Signal<Translation | TranslationObject>;

public abstract stream(
key: string | string[],
key: Key | Key[],
interpolateParams?: InterpolationParameters,
lang?: Language,
): Observable<Translation>;

public abstract getStreamOnTranslationChange(
key: string | string[],
key: Key | Key[],
interpolateParams?: InterpolationParameters,
lang?: Language,
): Observable<Translation>;

public abstract set(
key: string,
key: Key,
translation: string | TranslationObject,
lang?: Language,
): void;

public abstract get(
key: string | string[],
key: Key | Key[],
interpolateParams?: InterpolationParameters,
lang?: Language,
): Observable<Translation>;
Expand All @@ -125,7 +125,7 @@ export abstract class ITranslateService {
): void;

public abstract getParsedResult(
key: string | string[],
key: Key | Key[],
interpolateParams?: InterpolationParameters,
lang?: Language,
): StrictTranslation | Observable<StrictTranslation>;
Expand Down Expand Up @@ -180,7 +180,7 @@ export abstract class ITranslateService {
* A `null` return means the service is the terminus of its translation
* fallback chain — equivalent to "is this a root?".
*/
public abstract getParent(): ITranslateService | null;
public abstract getParent(): ITranslateService<Key> | null;

/**
* Returns the root of this service's hierarchy — the topmost service in
Expand All @@ -189,5 +189,5 @@ export abstract class ITranslateService {
*
* A root service returns itself.
*/
public abstract getRoot(): ITranslateService;
public abstract getRoot(): ITranslateService<Key>;
}
32 changes: 16 additions & 16 deletions projects/ngx-translate/src/lib/translate.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ const makeObservable = <T>(value: T | Observable<T>): Observable<T> => {
};

@Injectable()
export class TranslateService implements ITranslateService {
export class TranslateService<Key extends string = string> implements ITranslateService<Key> {
protected readonly loadingTranslations = new LoadingTranslationsRegistry();
protected lastUseLanguage: Language | null = null;

Expand All @@ -86,7 +86,7 @@ export class TranslateService implements ITranslateService {
protected missingTranslationHandler = inject(MissingTranslationHandler);
protected store: TranslateStore = inject(TranslateStore);

protected readonly parent: TranslateService | null;
protected readonly parent: TranslateService<Key> | null;

protected get isRoot(): boolean {
return this.parent === null;
Expand Down Expand Up @@ -122,9 +122,9 @@ export class TranslateService implements ITranslateService {
* A root service returns itself. Equivalent to walking `getParent()` until
* it returns `null`, but provided as a convenience.
*/
public getRoot(): TranslateService {
public getRoot(): TranslateService<Key> {
// eslint-disable-next-line @typescript-eslint/no-this-alias
let svc: TranslateService = this;
let svc: TranslateService<Key> = this;
while (svc.parent) svc = svc.parent;
return svc;
}
Expand All @@ -136,13 +136,13 @@ export class TranslateService implements ITranslateService {
* A `null` return means the service is the terminus of its translation
* fallback chain — equivalent to "is this a root?".
*/
public getParent(): TranslateService | null {
public getParent(): TranslateService<Key> | null {
return this.parent;
}

protected hasTranslationInChain(lang: Language): boolean {
// eslint-disable-next-line @typescript-eslint/no-this-alias
for (let svc: TranslateService | null = this; svc; svc = svc.parent) {
for (let svc: TranslateService<Key> | null = this; svc; svc = svc.parent) {
if (svc.store.hasTranslationFor(lang)) return true;
}
return false;
Expand All @@ -151,7 +151,7 @@ export class TranslateService implements ITranslateService {
protected chainTranslationChange$(): Observable<TranslationChangeEvent> {
const streams: Observable<TranslationChangeEvent>[] = [];
// eslint-disable-next-line @typescript-eslint/no-this-alias
for (let svc: TranslateService | null = this; svc; svc = svc.parent) {
for (let svc: TranslateService<Key> | null = this; svc; svc = svc.parent) {
streams.push(svc.store.translationChange$);
}
return streams.length === 1 ? streams[0] : merge(...streams);
Expand Down Expand Up @@ -688,7 +688,7 @@ export class TranslateService implements ITranslateService {
* Returns the parsed result of the translations
*/
public getParsedResult(
key: string | string[],
key: Key | Key[],
interpolateParams?: InterpolationParameters,
lang?: Language,
): StrictTranslation | Observable<StrictTranslation> {
Expand All @@ -698,7 +698,7 @@ export class TranslateService implements ITranslateService {
}

protected getParsedResultForArray(
key: string[],
key: Key[],
interpolateParams: InterpolationParameters | undefined,
lang?: Language,
) {
Expand Down Expand Up @@ -731,7 +731,7 @@ export class TranslateService implements ITranslateService {
* @returns the translated key, or an object of translated keys
*/
public get(
key: string | string[],
key: Key | Key[],
interpolateParams?: InterpolationParameters,
lang?: Language,
): Observable<Translation> {
Expand Down Expand Up @@ -759,7 +759,7 @@ export class TranslateService implements ITranslateService {
* @returns A stream of the translated key, or an object of translated keys
*/
public getStreamOnTranslationChange(
key: string | string[],
key: Key | Key[],
interpolateParams?: InterpolationParameters,
lang?: Language,
): Observable<Translation> {
Expand Down Expand Up @@ -792,7 +792,7 @@ export class TranslateService implements ITranslateService {
* @returns A stream of the translated key, or an object of translated keys
*/
public stream(
key: string | string[],
key: Key | Key[],
interpolateParams?: InterpolationParameters,
lang?: Language,
): Observable<Translation> {
Expand Down Expand Up @@ -827,7 +827,7 @@ export class TranslateService implements ITranslateService {
* bypassing the current language and fallback chain.
*/
public instant(
key: string | string[],
key: Key | Key[],
interpolateParams?: InterpolationParameters,
lang?: Language,
): Translation {
Expand Down Expand Up @@ -890,7 +890,7 @@ export class TranslateService implements ITranslateService {
* labels = this.translate.translate(['SAVE', 'CANCEL']);
*/
public translate(
key: string | string[] | (() => string | string[]),
key: Key | Key[] | (() => Key | Key[]),
params?: InterpolationParameters | (() => InterpolationParameters | undefined),
lang?: Language | (() => Language | undefined),
): Signal<Translation | TranslationObject> {
Expand All @@ -903,7 +903,7 @@ export class TranslateService implements ITranslateService {
});
}

protected keyToObject(key: string | string[]) {
protected keyToObject(key: Key | Key[]) {
if (Array.isArray(key)) {
return key.reduce((acc: Record<string, string>, currKey: string) => {
acc[currKey] = currKey;
Expand All @@ -917,7 +917,7 @@ export class TranslateService implements ITranslateService {
* Sets the translated value of a key, after compiling it
*/
public set(
key: string,
key: Key,
translation: string | TranslationObject,
lang: Language = this.getCurrentLang()!,
): void {
Expand Down
61 changes: 61 additions & 0 deletions projects/ngx-translate/src/tests/translate.type-safety.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { TestBed } from "@angular/core/testing"
import { provideTranslateLoader, TranslatePipe, TranslateService, TranslationObject } from "../public-api"
import { provideTestableTranslateService, FakeLoader } from "./test-helpers"

const translations: TranslationObject = {a: "A", b: {"a": "BA", "b": "BB"}, c: "C"}
type MyKeys = "a" | "b.a" | "b.b" | "c";

describe('Key type safety tests', () => {
it('should not have typescript errors when using TranslateService', () => {
TestBed.configureTestingModule({
providers: [
provideTestableTranslateService({
loader: provideTranslateLoader(FakeLoader),
}),
],
});
const translate = TestBed.inject<TranslateService<MyKeys>>(TranslateService<MyKeys>);
translate.setTranslation("en", translations);
translate.use("en");

translate.instant('a');
translate.stream('b.a');
translate.get('b.b');

// @ts-expect-error
translate.get('c.c');
// @ts-expect-error
translate.get('b');

expect(translate.instant('a')).toBe("A");
})

it('should not have typescript errors when using TranslatePipe', () => {
TestBed.configureTestingModule({
providers: [
provideTestableTranslateService({
loader: provideTranslateLoader(FakeLoader),
}),
{
provide: TranslatePipe,
useClass: TranslatePipe,
}
],
});
const translate = TestBed.inject<TranslateService<MyKeys>>(TranslateService<MyKeys>);
const translatePipe = TestBed.inject<TranslatePipe<MyKeys>>(TranslatePipe<MyKeys>);
translate.setTranslation("en", translations);
translate.use("en");

translatePipe.transform('a');
translatePipe.transform('b.a');
translatePipe.transform('b.b');

// @ts-expect-error
translatePipe.transform('c.c');
// @ts-expect-error
translatePipe.transform('b');

expect(translatePipe.transform('b.a')).toBe("BA");
})
})