@unisense.io/ngx-translator
v1.4.1
Published
Angular SDK for Unisense Translation Management — fetch, cache, ICU format, TranslatePipe, TranslateDirective.
Maintainers
Readme
@unisense.io/ngx-translator
Angular SDK for Unisense — the open-source Translation Management platform.
Manage all your i18n translations in one place, then consume them in your Angular app with a single pipe or directive.
Create your free workspace at unisense.io.
Supports Angular 12 through 21+ via three entry points, each optimised for the features available in that Angular era.
Quick-start: which entry point do I need?
| My Angular version | Entry point to import from | Integration style |
|--------------------|---------------------------|-------------------|
| 12 – 13 | …/legacy | NgModule + constructor DI + BehaviorSubject |
| 14 – 16 | …/compat | Standalone or NgModule + inject() + Observable |
| 17 | …/compat or (main) | Compat works; main entry (Signals) is preferred |
| 18 – 21+ | (main package) | Standalone or NgModule + Signals ✨ |
npm install @unisense.io/ngx-translatorVariant A — Angular 12-13 (/legacy)
Entry point
import { ... } from '@unisense.io/ngx-translator/legacy';Setup — app.module.ts
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import {
NgxTranslatorLegacyModule,
cookieLanguageStorage,
navigatorLanguageDetector,
} from '@unisense.io/ngx-translator/legacy';
@NgModule({
declarations: [AppComponent],
imports: [
BrowserModule,
NgxTranslatorLegacyModule.forRoot({
apiUrl: 'https://your-unisense-instance.com/api',
projectId: 'clxxxxxxxxxxxxxxxxx',
apiKey: 'unisense_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx', // Required
defaultLocale: 'en',
preloadLocales: ['fr'], // optional
languageStorage: cookieLanguageStorage('lang'), // optional
languageDetectors: [navigatorLanguageDetector()],// optional
}),
],
bootstrap: [AppComponent],
})
export class AppModule {}Setup — feature module
Import without forRoot() to get pipe + directive in a feature module:
import { NgModule } from '@angular/core';
import { NgxTranslatorLegacyModule } from '@unisense.io/ngx-translator/legacy';
@NgModule({
imports: [NgxTranslatorLegacyModule],
declarations: [ProductListComponent],
})
export class ProductsModule {}Component usage
No extra imports needed — the pipe and directive are exported by the module.
import { Component } from '@angular/core';
@Component({
selector: 'app-home',
template: `
<h1>{{ 'homepage.title' | translate }}</h1>
<p>{{ 'greeting' | translate: { name: 'Alice' } }}</p>
<span [translate]="'items.count'" [translateParams]="{ count: total }"></span>
`,
})
export class HomeComponent {
total = 42;
}Service injection
import { Component, OnInit } from '@angular/core';
import { TranslatorService } from '@unisense.io/ngx-translator/legacy';
@Component({ ... })
export class MyComponent implements OnInit {
constructor(private readonly translator: TranslatorService) {}
ngOnInit() {
// Synchronous
const title = this.translator.instant('homepage.title');
// Observable — re-emits on locale change
this.translator.translate('homepage.title').subscribe(t => (this.title = t));
// Switch language
this.translator.changeLanguage('fr');
}
}Note: This variant works with both RxJS 6 and RxJS 7 (the
use()method avoidslastValueFrom).
Variant B — Angular 14-17 (/compat)
Entry point
import { ... } from '@unisense.io/ngx-translator/compat';Two setup paths exist: standalone (recommended for Angular 14+) and NgModule.
Path B1 — Standalone app (app.config.ts)
import { ApplicationConfig } from '@angular/core';
import { provideHttpClient } from '@angular/common/http';
import { provideRouter } from '@angular/router';
import {
provideTranslator,
cookieLanguageStorage,
navigatorLanguageDetector,
} from '@unisense.io/ngx-translator/compat';
export const appConfig: ApplicationConfig = {
providers: [
provideHttpClient(),
provideRouter(routes),
provideTranslator({
apiUrl: 'https://your-unisense-instance.com/api',
projectId: 'clxxxxxxxxxxxxxxxxx',
apiKey: 'unisense_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
defaultLocale: 'en',
preloadLocales: ['fr'],
languageStorage: cookieLanguageStorage('lang'),
languageDetectors: [navigatorLanguageDetector()],
}),
],
};main.ts
import { bootstrapApplication } from '@angular/platform-browser';
import { AppComponent } from './app/app.component';
import { appConfig } from './app/app.config';
bootstrapApplication(AppComponent, appConfig).catch(console.error);Standalone component
import { Component, inject } from '@angular/core';
import {
TranslatePipe,
TranslateDirective,
TranslatorService,
} from '@unisense.io/ngx-translator/compat';
@Component({
standalone: true,
imports: [TranslatePipe, TranslateDirective],
template: `
<h1>{{ 'homepage.title' | translate }}</h1>
<p>{{ 'greeting' | translate: { name: user.name } }}</p>
<span [translate]="'items.count'" [translateParams]="{ count: total }"></span>
<button (click)="switchLang('fr')">Français</button>
`,
})
export class HomeComponent {
private readonly translator = inject(TranslatorService);
user = { name: 'Alice' };
total = 42;
switchLang(lang: string) {
this.translator.changeLanguage(lang);
}
}Path B2 — NgModule app (app.module.ts)
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { HttpClientModule } from '@angular/common/http';
import { NgxTranslatorCompatModule } from '@unisense.io/ngx-translator/compat';
@NgModule({
declarations: [AppComponent],
imports: [
BrowserModule,
HttpClientModule,
NgxTranslatorCompatModule.forRoot({
apiUrl: 'https://your-unisense-instance.com/api',
projectId: 'clxxxxxxxxxxxxxxxxx',
apiKey: 'unisense_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
defaultLocale: 'en',
}),
],
bootstrap: [AppComponent],
})
export class AppModule {}Variant C — Angular 17-21+ (main entry, recommended)
Entry point
import { ... } from '@unisense.io/ngx-translator';Uses Angular Signals (signal(), effect()) for zero-overhead reactivity.
Path C1 — Standalone app (Angular 17+, recommended)
app.config.ts
import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
import { provideHttpClient } from '@angular/common/http';
import { provideRouter } from '@angular/router';
import {
provideTranslator,
cookieLanguageStorage,
navigatorLanguageDetector,
} from '@unisense.io/ngx-translator';
export const appConfig: ApplicationConfig = {
providers: [
provideZoneChangeDetection({ eventCoalescing: true }),
provideRouter(routes),
provideHttpClient(),
provideTranslator({
apiUrl: 'https://your-unisense-instance.com/api',
projectId: 'clxxxxxxxxxxxxxxxxx',
apiKey: 'unisense_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
defaultLocale: 'en',
preloadLocales: ['fr', 'de'],
languageStorage: cookieLanguageStorage('preferred_lang'),
languageDetectors: [navigatorLanguageDetector()],
}),
],
};Standalone component
import { Component, inject, computed } from '@angular/core';
import {
TranslatePipe,
TranslateDirective,
TranslatorService,
} from '@unisense.io/ngx-translator';
@Component({
standalone: true,
imports: [TranslatePipe, TranslateDirective],
template: `
<h1>{{ 'homepage.title' | translate }}</h1>
<p>{{ 'greeting' | translate: { name: 'Alice' } }}</p>
<!-- Signal-driven computed property -->
<title>{{ pageTitle() }}</title>
<!-- Show loading state -->
@if (translator.loading()) {
<p>Loading translations…</p>
}
`,
})
export class HomeComponent {
readonly translator = inject(TranslatorService);
// Recomputes automatically when locale or translations change
readonly pageTitle = computed(() => this.translator.instant('page.title'));
}Path C2 — NgModule app (Angular 17+)
app.module.ts
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { HttpClientModule } from '@angular/common/http';
import {
NgxTranslatorModule,
provideTranslator,
} from '@unisense.io/ngx-translator';
@NgModule({
declarations: [AppComponent],
imports: [BrowserModule, HttpClientModule, NgxTranslatorModule],
providers: [
provideTranslator({
apiUrl: 'https://your-unisense-instance.com/api',
projectId: 'clxxxxxxxxxxxxxxxxx',
apiKey: 'unisense_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
defaultLocale: 'en',
}),
],
bootstrap: [AppComponent],
})
export class AppModule {}Path C3 — Service-only (libraries, SSR, programmatic)
For use inside a library, a resolver, or server-side rendering where you don't need the pipe/directive.
import { Component, inject } from '@angular/core';
import { TranslatorService } from '@unisense.io/ngx-translator';
@Component({ ... })
export class MyComponent {
private readonly translator = inject(TranslatorService);
// Synchronous
getTitle() {
return this.translator.instant('page.title');
}
// Observable stream
getTitle$() {
return this.translator.translate('page.title');
}
// Signal (Angular 17+)
getLocale() {
return this.translator.locale(); // Signal<string>
}
}Configuration reference
All three variants share the same TranslatorConfig interface:
interface TranslatorConfig {
/** Base URL of the Unisense API — no trailing slash */
apiUrl: string;
/** Project ID from the Unisense dashboard */
projectId: string;
/** API Key for the project — required for public translation fetching */
apiKey: string;
/** Fallback locale when all detectors return undefined */
defaultLocale: string;
/** Extra locales to fetch in background after init */
preloadLocales?: string[];
/**
* Ordered chain of locale detectors.
* Defaults to [navigatorLanguageDetector()].
*/
languageDetectors?: LanguageDetectorMiddleware[];
/**
* Locale persistence layer.
* Defaults to cookieLanguageStorage('preferred_language').
* Set to null to disable persistence.
*/
languageStorage?: LanguageStorageMiddleware | null;
/**
* Where to cache downloaded translation bundles between page reloads.
* Defaults to 'session'.
* See the "Bundle Cache" section for details.
*/
bundleCache?: 'session' | 'local' | 'none';
}Locale resolution order
languageStorage.getLanguage()— stored cookie / localStoragelanguageDetectors[0..n]— first detector returning a value winsconfig.defaultLocale— hard-coded fallback
TranslatorService API
All three variants expose the same public API.
Properties
| Property | Modern (17+) | Compat (14-17) | Legacy (12-13) | Description |
|----------|-------------|---------------|---------------|-------------|
| language | string | string | string | Active locale getter |
| languageAsync | Observable<string> | Observable<string> | Observable<string> | Stream of active locale |
| locale | Signal<string> | (N/A) | (N/A) | Signal — modern only |
| loading | Signal<boolean> | (N/A) | (N/A) | Signal — modern only |
| ready | Signal<boolean> | (N/A) | (N/A) | Signal — modern only |
| snapshot | ReadonlySignal<…> | (N/A) | (N/A) | Translations signal — modern only |
Methods (all variants)
// Synchronous translation
instant(key: string): string
instant(key: string, defaultValue: string): string
instant(key: string, params: TranslateParams): string
instant(key: string, defaultValue: string, params: TranslateParams): string
t(key, params?) // shorthand alias for instant()
// Observable translation — re-emits on locale change
translate(key: string): Observable<string>
translate(key: string, defaultValue: string): Observable<string>
translate(key: string, params: TranslateParams): Observable<string>
// Language switch
use(locale: string): Promise<void>
changeLanguage(locale: string): Promise<void> // alias
// Available languages
getLanguages(): Observable<Language[]>
getLanguagesAsync(): Promise<Language[]>
// Events
on(event: UnisenseEvent): Observable<EventType[event]>
subscribe(event, handler): { unsubscribe(): void }Events
| Event | Payload | When |
|-------|---------|------|
| language | { type: 'language', value: string } | Locale changed |
| loading | boolean | Initial locale fetch start/end |
| fetching | boolean | Any locale fetch start/end |
| initialLoad | void | First locale loaded |
| error | Error | Fetch failed |
| update | void | Translations table updated |
Templates
| translate pipe
{{ 'homepage.title' | translate }}
{{ 'greeting' | translate: { name: 'Alice' } }}
{{ 'items.count' | translate: { count: total } }}[translate] directive
<span translate="homepage.title"></span>
<button [translate]="btnKey" [translateParams]="{ count: total }"></button>ICU Message Format
Built-in parser — no external dependency needed.
Simple variable
"Hello, {name}!"Plural
"{count, plural, =0 {No items} one {# item} other {# items}}"Supported cases: =0, =1, =N, zero, one, two, few, many, other.
Select
"{gender, select, male {his profile} female {her profile} other {their profile}}"Nested ICU
"{count, plural, =0 {No {type} found} one {One {type}} other {# {type}s}}"Language Detection & Storage
Built-in
import {
cookieLanguageStorage,
navigatorLanguageDetector,
} from '@unisense.io/ngx-translator'; // or /legacy or /compatCustom detector (URL param example)
const urlParamDetector: LanguageDetectorMiddleware = {
detect() {
return new URLSearchParams(window.location.search).get('lang') ?? undefined;
},
};Custom storage (localStorage example)
const localStorageMiddleware: LanguageStorageMiddleware = {
getLanguage: () => localStorage.getItem('locale') ?? undefined,
setLanguage: (lang) => localStorage.setItem('locale', lang),
};Disable persistence
languageStorage: nullBundle Cache
By default (v1.4.0+) the SDK caches every downloaded translation bundle in sessionStorage so that language switches never trigger a network request within the same browser session — not even after a page refresh (F5).
How it works
translator.changeLanguage('fr')
│
├─ 1. In-memory Map → hit → instant (< 1 µs), no HTTP
├─ 2. sessionStorage → hit → JSON.parse (< 1 ms), no HTTP
└─ 3. HTTP fetch → stores result in both layersThe first time a locale is requested it goes to the network. Every subsequent request — including after F5 — is served from storage without touching the server.
Configuration
provideTranslator({
apiUrl: 'https://your-unisense-instance.com/api',
projectId: 'clxxxxxxxxxxxxxxxxx',
defaultLocale: 'en',
// 'session' (default) — sessionStorage: cleared when the tab is closed.
// Language switches and F5 reloads are instant, no HTTP.
bundleCache: 'session',
// 'local' — localStorage: persists across browser sessions.
// The bundle is invalidated automatically when the server's
// translation version changes (translationsVersion field).
// bundleCache: 'local',
// 'none' — in-memory only, reverts to pre-v1.4 behaviour.
// bundleCache: 'none',
})Strategy comparison
| Strategy | Survives F5 | Survives tab close | Shared across tabs | Evicted when |
|---|---|---|---|---|
| 'session' (default) | ✅ | ❌ | ❌ | Tab closed |
| 'local' | ✅ | ✅ | ✅ | Browser cache cleared |
| 'none' | ❌ | ❌ | ❌ | Never stored |
SSR / Angular Universal
On the server there is no window — the SDK detects this automatically and falls back to 'none' regardless of the configured strategy. No extra configuration needed.
Storage keys
Bundles are stored under the key unisense:{projectId}:{locale}.
Example: unisense:clxxx:fr, unisense:clxxx:en.
You can inspect or clear them manually in DevTools → Application → Storage.
Language Switcher
import { Component, inject } from '@angular/core';
import { TranslatorService, Language } from '@unisense.io/ngx-translator'; // or /compat
@Component({
standalone: true,
template: `
<select (change)="switch($event)">
@for (lang of languages; track lang.code) {
<option [value]="lang.code" [selected]="lang.code === translator.language">
{{ lang.flagEmoji }} {{ lang.name }}
</option>
}
</select>
`,
})
export class LanguageSwitcherComponent {
readonly translator = inject(TranslatorService);
languages: Language[] = [];
ngOnInit() {
this.translator.getLanguages().subscribe(l => (this.languages = l));
}
switch(event: Event) {
this.translator.changeLanguage((event.target as HTMLSelectElement).value);
}
}SSR / Angular Universal
Disable browser-specific middleware on the server:
// server.config.ts
export const serverConfig = mergeApplicationConfig(appConfig, {
providers: [
provideTranslator({
apiUrl: 'https://api.example.com/api',
projectId: 'clxxxxxxxxxxxxxxxxx',
apiKey: 'unisense_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
defaultLocale: 'en',
languageStorage: null, // no document.cookie on server
languageDetectors: [], // no navigator on server
}),
],
});Migration from Tolgee (@tolgee/ngx)
| Tolgee | Unisense | Notes |
|--------|---------|-------|
| NgxTolgeeModule | NgxTranslatorLegacyModule / NgxTranslatorCompatModule / NgxTranslatorModule | Same role |
| TranslateService | TranslatorService | Also re-exported as TranslateService alias |
| t(), instant(), changeLanguage() | Identical | No changes needed |
| lang.tag | lang.code | tag kept as deprecated alias |
| lang.base | lang.isBase | base kept as deprecated alias |
| provideTolgee({ defaultLanguage }) | provideTranslator({ defaultLocale }) | Renamed key |
| provideTolgee({ staticData }) | provideTranslator({ projectId }) | Replaced by server-side fetch |
Troubleshooting
"NullInjectorError: No provider for TRANSLATOR_CONFIG"
You forgot to call forRoot(config) on the module import, or forgot provideTranslator(config) in standalone setup.
Translations not loading / 404 or 401 on API call
- Check
apiUrlhas no trailing slash. - The SDK calls
GET {apiUrl}/public/{projectId}/translations/{locale}. - Ensure you have provided a valid
apiKeyin the config. - Make sure
provideHttpClient()(standalone) orHttpClientModule(NgModule) is in your providers.
"Cannot find module '@unisense.io/ngx-translator/compat'" (TS2307)
Your TypeScript config uses "moduleResolution": "node" which does not understand the exports field in package.json.
Two options:
Option A — upgrade tsconfig.json (recommended for Angular 16+):
{ "compilerOptions": { "moduleResolution": "bundler" } }Option B — the package ships typesVersions as a fallback (since v1.3.1), which resolves the subpaths automatically without any tsconfig change.
"No matching export for 'provideAppInitializer'" (Angular 17)
provideAppInitializer was introduced in Angular 18. If you are on Angular 17, use:
- Main entry v1.3.1+ —
provideTranslator()now usesAPP_INITIALIZERinternally, compatible with Angular 17–21+. /compatentry — always usesAPP_INITIALIZER, compatible with Angular 14–17.
Pipe not re-rendering after changeLanguage()
- Modern variant (17+):
TranslatePipereads thesnapshot()signal — re-renders automatically. Check thatTranslatePipeis inimports. - Compat/Legacy variants: The pipe subscribes to the
'update'event and callsmarkForCheck(). Make sureChangeDetectionStrategy.OnPushis applied correctly — the pipe handles it internally.
Angular 12 with RxJS 6
The /legacy entry point uses an internal lastValue() helper that works with both RxJS 6 and RxJS 7. No changes needed in your project.
Changelog
1.4.0
- Feature: Two-level bundle cache —
sessionStorage(default),localStorage, or'none'.
Language switches and F5 reloads no longer trigger a network request. Configure viabundleCacheinTranslatorConfig. - Feature:
bundleCache: 'local'useslocalStoragefor cross-session persistence.
Bundles include av(version) field that matches the server'stranslationsVersion— ready for future version-based eviction. - Fix:
preload()now checks the bundle cache before making an HTTP request, sopreloadLocalesis also zero-cost on repeated page loads. - API:
ApiResponsenow includes an optionalversionfield (returned by Unisense API ≥ 1.4). - SSR:
resolveStorage()guards againstwindowbeing undefined on the server — no change needed inserver.config.ts.
1.3.1
- Fix: Main entry now uses
APP_INITIALIZERinstead ofprovideAppInitializer, restoring compatibility with Angular 17 (provideAppInitializeris Angular 18+ only). - Fix: Added
typesVersionstopackage.json— TypeScript projects using"moduleResolution": "node"can now resolve/compatand/legacysubpaths without any tsconfig change. - Docs: Variant B compat range extended to Angular 14–17; quick-start table updated.
1.3.0
- Initial public release with Signals API, Observable compat layer, and NgModule legacy support.
Related packages
| Package | Framework | Link |
|---|---|---|
| @unisense.io/ngx-translator | Angular 12 – 21+ | this package |
| @unisense/react-translator | React 16.8+ | npm |
License
MIT © Unisense
