https://github.com/yagcioe/ngx-signal-i18n
https://github.com/yagcioe/ngx-signal-i18n
Last synced: 5 days ago
JSON representation
- Host: GitHub
- URL: https://github.com/yagcioe/ngx-signal-i18n
- Owner: yagcioe
- Created: 2024-08-19T18:43:09.000Z (10 months ago)
- Default Branch: main
- Last Pushed: 2025-06-10T20:51:45.000Z (9 days ago)
- Last Synced: 2025-06-10T21:38:34.163Z (9 days ago)
- Language: TypeScript
- Size: 4.46 MB
- Stars: 0
- Watchers: 1
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
Awesome Lists containing this project
- awesome-angular - ngx-signal-i18n - This package provides a typesafe and lazy-loaded internationalization (i18n) solution for Angular applications, built on top of signals for improved reactivity. It is compatible with zoneless Angular. (Table of contents / Angular)
README
# ngx-signal-i18n
This package provides a typesafe and lazy-loaded internationalization (i18n) solution for Angular applications, built on top of signals for improved reactivity. It is compatible with zoneless Angular.

This project is inspired by [typesafe-i18n](https://github.com/ivanhofer/typesafe-i18n)
## Features
⛑️ Typesafety: Enforces type safety for translations depending on a default language.\
🦥 Lazy Loading: Translations are loaded on demand, improving initial bundle size.\
🚦 Signal Based: Leverages signals for efficient reactivity and change detection.\
🏃♂️ Zoneless Angular Compatibility: Works seamlessly with zoneless Angular applications.\
📄 Parameterized Translations: Supports translations with parameters for dynamic content.\
🛠️ Template Interpolation Pipe: Provides a pipe for easy interpolation of translations in your templates.\
🎭 Masking Utility: Provides utility types for omitting and picking specific translations which comes in handy when dealing with parameterized translations.\
🦺 Testing Proxy: Includes a proxy utility to simplify mocking translations during testing.\
🐤 Lightweight Build: ~ 1.5kb\
📦 Minimal Dependencies\
⛔ No Magic: Just Typescript## Table of Content
- [Installation](#installation)
- [Usage](#usage)
- [Deep Pick and Omit Utility](#deep-pick-and-omit-utility)
- [Configuration](#configuration)
- [Writing Tests](#writing-tests)
- [Examples](#examples)## Installation
```Bash
npm i ngx-signal-i18n
```## Usage
```html
en
de
simple access for non interpolated values
{{ translationService.translation().title}}
{{ translationService.translation().simpleNest.str}}
{{ translationService.translation().nest.title}}
interpolated values from the ts file
{{ interpolatedTranslations().title }}
{{ interpolatedTranslations().simpleNest.str }}
{{ interpolatedTranslations().nest.title }}
{{ interpolatedTranslations().nest.anotherInterpolatedValue()}}
{{ interpolatedTranslations().interpolatable()}}
inline interpolation with the interpolation pipe
{{ translationService.translation().title | interpolate: undefined }}
{{ translationService.translation().simpleNest.str | interpolate: undefined}}
{{ translationService.translation().nest.title | interpolate: undefined}}
{{ (translationService.translation().nest
| interpolate: { anotherInterpolatedValue: [numSignal] }).anotherInterpolatedValue() }}
{{ (translationService.translation().nest | interpolate: { anotherInterpolatedValue: [numSignal]}).title }}
{{ (translationService.translation().nest.anotherInterpolatedValue | interpolate: [numSignal])() }}
{{ (translationService.translation().interpolatable | interpolate: [textSignal])() }}
```
```ts
// app.component.ts
import { Component, computed, inject, signal } from '@angular/core';
import { interpolate, InterpolatePipe } from 'ngx-signal-i18n';
import { SupportedLanguage } from '../i18n/i18n-config';
import { TranslationService } from '../i18n/translation.service';@Component({
selector: 'app-root',
standalone: true,
imports: [InterpolatePipe],
templateUrl: './app.component.html',
})
export class AppComponent {
protected translationService = inject(TranslationService);protected textSignal = signal('text');
protected numSignal = signal(0);protected interpolatedTranslations = computed(() => {
return interpolate(this.translationService.translation(), {
interpolatable: [this.textSignal],
nest: { anotherInterpolatedValue: [this.numSignal] }
})
})protected onLanguageChange($event: Event): void {
const lang = ($event.target as any).value as SupportedLanguage;
this.translationService.setLanguage(lang);
}
}
```
## Deep Pick and Omit Utility
Sometimes translation structures require more parameters than you have available. Providing unnecessary parameters can be cumbersome. To simplify this process, this library offers `pick` and `omit` functions that extract specific values from complex objects using a boolean mask.```ts
import { Mask, omit, pick } from 'ngx-signal-i18n';const originalObject = {
a: 12,
b: "13",
c: {
d: 14,
e: {
f: 15
},
f: 16
},
g: () => 17
}const mask = {
a: true,
b: false,
c: {
d: true,
e: {},
},
g: undefined
} as const satisfies Maskconst picked = pick(originalObject, mask)
const omitted = omit(originalObject, mask)
console.log(picked) // {a: 12, c: {d: 14, e: {}}}
console.log(omitted) // {b: "13", c: {e: {f: 15}, h: 16}}```

## Configuration
>In order to prevent a lot of syntax boilerplate to deal with undefined, having a language and translation loaded is required!
### 1. Define Main Translation Files
Define the main translation which defines the structure every other Translation must follow.
>Every function is run in a reactive context. Therefore calling any signal within the function will result in reevaluation when the called signal updates!```ts
// src/i18n/en/index.ts
import { Signal } from '@angular/core';
import { TranslationShape} from 'ngx-signal-i18n';const en = {
title: 'title',
interpolatable: (text: Signal) => `this is a interpolated value: ${text()}`,
nest: {
title: 'nested title',
anotherInterpolatedValue: (num: Signal) => `this is a nested value ${num()}`,
},
simpleNest: {
str: 'F',
}
} satisfies TranslationShape;export default en;
```
### 2. Define Translation config
```ts
// src/i18n/i18n.config.ts
import { InjectionToken } from '@angular/core';
import en from './en';export const Locales = ['de', 'en'] as const
export type Locale = typeof Locales[number]export type Translation = typeof en;
export const DEFAULT_TRANSLATION = new InjectionToken("DEFAULT_TRANSLATION")
```
### 3. Add another Translation to the project
Add another translation that has the type of the main translation```ts
// src/i18n/de/index.ts
import { Signal } from '@angular/core';
import { Translation } from '../i18n-config';const de: Translation = {
title: 'Titel',
interpolatable: (text: Signal) => `Das ist ein intepolierter Wert: ${text()}`,
nest: {
title: 'geschachtelter Titel',
anotherInterpolatedValue: (num: Signal) => `Das ist ein geschachtelter interpolierter Wert ${num()}`,
},
simpleNest: {
str: 'F',
}
};export default de;
```
### 4. Create Translation Service```ts
// src/i18n/translation.service.ts
import { Inject, Injectable } from '@angular/core';
import { NgxSignalI18nBaseService } from 'ngx-signal-i18n';
import { DEFAULT_TRANSLATION, Locale, Translation } from './i18n-config';@Injectable({
providedIn: 'root',
})
export class TranslationService extends NgxSignalI18nBaseService {constructor(@Inject(DEFAULT_TRANSLATION) defaultTranslation: Translation) {
super(defaultTranslation, true);
}protected override async resolutionStrategy(lang: Locale): Promise {
// lazy load translation file
return (await import(`./${lang}/index.ts`)).default
}
}
```
### 5. Add Providers to Angular DI
```ts
import { ApplicationConfig, provideExperimentalZonelessChangeDetection } from '@angular/core';
import { provideLocale } from "ngx-signal-i18n";
import en from '../i18n/en';
import { DEFAULT_TRANSLATION, Locale } from '../i18n/i18n-config';export const appConfig: ApplicationConfig = {
providers: [
// this app is zoneless
provideExperimentalZonelessChangeDetection(),
// hard code inital locale and Translation
provideLocale("en"),
{ provide: DEFAULT_TRANSLATION, useValue: en }
]
};
```
### 6. Include Translation files in tsconfig
Add the following line to `tsconfig.app.json` and `tsconfig.spec.json`
```json
{
"compilerOptions": {
"include": [
"./src/**/i18n/**/index.ts", // <-- add this line
]
}
}
```## Writing Tests
When writing tests, the specific language often doesn't matter. This package provides a utility function that creates a proxy of a Translation object. Instead of returning actual translation values, the proxy returns the path to the desired translationDeclare `TranslationTestingService`
```ts
// src/i18n/translation-testing.service.ts
import { Injectable } from '@angular/core';
import { createProxy } from 'ngx-signal-i18n';
import en from './en';
import { Locale, Translation } from './i18n-config';
import { TranslationService } from './translation.service';@Injectable()
export class TranslationTestingService extends TranslationService {private translationMock:Translation
constructor() {
const translationMock = createProxy(en)
// override the default translation with a proxy that return the access path instead of the value
super(translationMock)
this.translationMock = translationMock;
}protected override async resolutionStrategy(_: Locale): Promise {
// don't actually resolve translation because the proxy will return the same value anyway
return this.translationMock
}
}
```Replace `TranslationService` with `TranslationTestingService` with the Angular DI in tests
```ts
// app.component.spec.ts
import { provideExperimentalZonelessChangeDetection } from '@angular/core';
import { TestBed } from '@angular/core/testing';
import { TranslationTestingService } from '../i18n/translation-testing.service';
import { TranslationService } from '../i18n/translation.service';
import { AppComponent } from './app.component';
import { provideLocale } from 'ngx-signal-i18n';
import { DEFAULT_TRANSLATION } from '../i18n/i18n-config';
import en from '../i18n/en';describe('AppComponent', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
providers: [
// this app is zoneless
provideExperimentalZonelessChangeDetection(),
// replace TranslationService with TranslationTestingService for tests
provideLocale("en"),
{ provide: DEFAULT_TRANSLATION, useValue: en },
{ provide: TranslationService, useClass: TranslationTestingService },
],
imports: [AppComponent],
}).compileComponents();});
it('should create the app', () => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.componentInstance;
expect(app).toBeTruthy();
});
});
```
## Examples
See sample projects on [GitHub](https://github.com/yagcioe/ngx-signal-i18n)
- [Simple Example](https://github.com/yagcioe/ngx-signal-i18n/tree/main/samples/simple)
- [Lazy Loaded Module and Translations](https://github.com/yagcioe/ngx-signal-i18n/tree/main/samples/lazyModules)
- [StackBlitz](https://stackblitz.com/edit/stackblitz-starters-w7jtm3?file=src%2Fmain.ts)