diff --git a/apps/transloco-playground/src/app/home/home.component.html b/apps/transloco-playground/src/app/home/home.component.html
index 8ca6dba1..ef89c7fe 100644
--- a/apps/transloco-playground/src/app/home/home.component.html
+++ b/apps/transloco-playground/src/app/home/home.component.html
@@ -1,10 +1,13 @@
-
Translation in {{ '@for' }}
- @for (key of translateList; track key) {
+ @for (key of translateList; track key; let index = $index) {
-
- {{ key | transloco }}
+ Index {{ index + 1 }}: {{ key | transloco }}
}
diff --git a/apps/transloco-playground/src/app/home/home.component.ts b/apps/transloco-playground/src/app/home/home.component.ts
index 528c16c0..58a6b108 100644
--- a/apps/transloco-playground/src/app/home/home.component.ts
+++ b/apps/transloco-playground/src/app/home/home.component.ts
@@ -1,8 +1,10 @@
-import { ChangeDetectionStrategy, Component } from '@angular/core';
+import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
-import { TranslocoModule } from '@jsverse/transloco';
-
-import { environment } from '../../environments/environment';
+import {
+ TranslocoModule,
+ translateObjectSignal,
+ translateSignal,
+} from '@jsverse/transloco';
@Component({
selector: 'app-home',
@@ -13,16 +15,21 @@ import { environment } from '../../environments/environment';
imports: [TranslocoModule],
})
export class HomeComponent {
- dynamic = '🦄';
- key = 'home';
+ dynamic = signal('🦄');
+ key = signal('home');
+
+ transloco = translateSignal('home');
+ translocoObject = translateObjectSignal('nested');
+ translocoParams = translateSignal('alert', { value: this.dynamic });
+ translocoKeys = translateSignal(this.key);
- translateList = ['b', 'c'];
+ translateList = ['home', 'a.b.c', 'b', 'c'];
changeKey() {
- this.key = this.key === 'home' ? 'fromList' : 'home';
+ this.key.update((key) => (key === 'home' ? 'fromList' : 'home'));
}
changeParam() {
- this.dynamic = this.dynamic === '🦄' ? '🦄🦄🦄' : '🦄';
+ this.dynamic.update((dynamic) => (dynamic === '🦄' ? '🦄🦄🦄' : '🦄'));
}
}
diff --git a/apps/transloco-playground/src/app/inline-loaders/inline-loaders.component.html b/apps/transloco-playground/src/app/inline-loaders/inline-loaders.component.html
index f45d1c80..5a47f715 100644
--- a/apps/transloco-playground/src/app/inline-loaders/inline-loaders.component.html
+++ b/apps/transloco-playground/src/app/inline-loaders/inline-loaders.component.html
@@ -1,8 +1,12 @@
- {{ t('inline.title') }}
+ Directive: {{ t('inline.title') }}
+
- {{ t('alert') }}
+ Directive Global Scope:
+ {{ t('alert', { value: 'global' }) }}
-
{{ 'inline.title' | transloco }}
+
Pipe: {{ 'inline.title' | transloco }}
+
Async: {{ title$ | async }}
+
Signal: {{ title() }}
diff --git a/apps/transloco-playground/src/app/inline-loaders/inline-loaders.component.ts b/apps/transloco-playground/src/app/inline-loaders/inline-loaders.component.ts
index 6279714e..1d692747 100644
--- a/apps/transloco-playground/src/app/inline-loaders/inline-loaders.component.ts
+++ b/apps/transloco-playground/src/app/inline-loaders/inline-loaders.component.ts
@@ -1,3 +1,4 @@
+import { AsyncPipe } from '@angular/common';
import { Component, OnInit, inject } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
@@ -5,6 +6,7 @@ import {
TranslocoModule,
TranslocoService,
TRANSLOCO_SCOPE,
+ translateSignal,
} from '@jsverse/transloco';
@Component({
@@ -12,15 +14,17 @@ import {
templateUrl: './inline-loaders.component.html',
styleUrls: ['./inline-loaders.component.scss'],
standalone: true,
- imports: [TranslocoModule],
+ imports: [TranslocoModule, AsyncPipe],
})
export default class InlineLoadersComponent implements OnInit {
private translocoService = inject(TranslocoService);
private scope = inject(TRANSLOCO_SCOPE);
- private title$ = this.translocoService
+ public title$ = this.translocoService
.selectTranslate('title', {}, this.scope)
.pipe(takeUntilDestroyed());
+ public title = translateSignal('title');
+
ngOnInit() {
console.log(this.scope);
this.title$.subscribe(console.log);
diff --git a/libs/transloco/src/index.ts b/libs/transloco/src/index.ts
index aa556d5c..3b8512f8 100644
--- a/libs/transloco/src/index.ts
+++ b/libs/transloco/src/index.ts
@@ -63,3 +63,4 @@ export {
provideTranslocoLang,
TranslocoOptions,
} from './lib/transloco.providers';
+export { translateSignal, translateObjectSignal } from './lib/transloco.signal';
diff --git a/libs/transloco/src/lib/tests/signal.spec.ts b/libs/transloco/src/lib/tests/signal.spec.ts
new file mode 100644
index 00000000..ff028d72
--- /dev/null
+++ b/libs/transloco/src/lib/tests/signal.spec.ts
@@ -0,0 +1,120 @@
+import { Component, signal } from '@angular/core';
+import { fakeAsync } from '@angular/core/testing';
+import { createComponentFactory, Spectator } from '@ngneat/spectator';
+
+import { TranslocoModule } from '../transloco.module';
+import { translateSignal, translateObjectSignal } from '../transloco.signal';
+
+import { providersMock, runLoader } from './mocks';
+
+@Component({
+ imports: [TranslocoModule],
+ template: `
+
{{ translatedText() }}
+
{{ translatedObject().title }}
+
{{ translatedDynamicKey() }}
+
{{ translatedDynamicParam() }}
+ `,
+})
+class TestComponent {
+ translatedText = translateSignal('home');
+ translatedObject = translateObjectSignal('nested');
+
+ dynamicKey = signal('home');
+ dynamicParam = signal('Signal');
+
+ translatedDynamicKey = translateSignal(this.dynamicKey);
+ translatedDynamicParam = translateSignal('alert', {
+ value: this.dynamicParam,
+ });
+
+ translatedObjectDynamicKey = translateObjectSignal(this.dynamicKey);
+ translatedObjectDynamicParam = translateObjectSignal(
+ this.dynamicKey,
+ this.dynamicParam,
+ );
+
+ changeKey(key: string) {
+ this.dynamicKey.set(key);
+ }
+
+ changeParam(param: any) {
+ this.dynamicParam.set(param);
+ }
+}
+
+describe('translateSignal in component', () => {
+ let spectator: Spectator
;
+ const createComponent = createComponentFactory({
+ component: TestComponent,
+ imports: [TranslocoModule],
+ providers: providersMock,
+ });
+
+ it('should translate a static key', fakeAsync(() => {
+ spectator = createComponent();
+ runLoader();
+ spectator.detectChanges();
+ expect(spectator.query('#text')).toHaveText('home english');
+ }));
+
+ it('should translate a dynamic key', fakeAsync(() => {
+ spectator = createComponent();
+ runLoader();
+ spectator.detectChanges();
+ spectator.component.changeKey('fromList');
+ spectator.detectChanges();
+ expect(spectator.query('#dynamicKey')).toHaveText('from list');
+ }));
+
+ it('should translate with params', fakeAsync(() => {
+ spectator = createComponent();
+ runLoader();
+ spectator.detectChanges();
+ spectator.component.changeParam('Signal Changed');
+ spectator.detectChanges();
+ expect(spectator.query('#dynamicParam')).toHaveText(
+ 'alert Signal Changed english',
+ );
+ }));
+});
+
+describe('translateObjectSignal in component', () => {
+ let spectator: Spectator;
+ const createComponent = createComponentFactory({
+ component: TestComponent,
+ imports: [TranslocoModule],
+ providers: providersMock,
+ });
+
+ it('should translate a static key to an object', fakeAsync(() => {
+ spectator = createComponent();
+ runLoader();
+ spectator.detectChanges();
+ expect(spectator.query('#textObject')).toHaveText('Title english');
+ }));
+
+ it('should translate a dynamic key to an object', fakeAsync(() => {
+ spectator = createComponent();
+ runLoader();
+ spectator.detectChanges();
+ spectator.component.changeKey('key.is.like');
+ spectator.detectChanges();
+ expect(spectator.component.translatedObjectDynamicKey()).toEqual({
+ path: 'key is like path',
+ });
+ }));
+
+ it('should translate with params to an object', fakeAsync(() => {
+ spectator = createComponent();
+ runLoader();
+ spectator.detectChanges();
+ spectator.component.changeKey('a.b');
+ spectator.component.changeParam({ c: { fromList: 'Signal Changed' } });
+ spectator.detectChanges();
+ console.log(spectator.component.translatedObjectDynamicParam());
+ expect(spectator.component.translatedObjectDynamicParam()).toEqual({
+ c: 'a.b.c Signal Changed english',
+ });
+ }));
+});
diff --git a/libs/transloco/src/lib/transloco.signal.ts b/libs/transloco/src/lib/transloco.signal.ts
new file mode 100644
index 00000000..c285723b
--- /dev/null
+++ b/libs/transloco/src/lib/transloco.signal.ts
@@ -0,0 +1,159 @@
+import {
+ assertInInjectionContext,
+ computed,
+ inject,
+ Injector,
+ isSignal,
+ runInInjectionContext,
+ Signal,
+} from '@angular/core';
+import { toObservable, toSignal } from '@angular/core/rxjs-interop';
+import { switchMap } from 'rxjs';
+
+import { TRANSLOCO_SCOPE } from './transloco-scope';
+import { TranslocoService } from './transloco.service';
+import { HashMap, Translation, TranslocoScope } from './types';
+
+type ScopeType = string | TranslocoScope | TranslocoScope[];
+type SignalKey = Signal | Signal | Signal[];
+type TranslateSignalKey = string | string[] | SignalKey;
+type TranslateSignalParams =
+ | HashMap
+ | HashMap>
+ | Signal;
+type TranslateSignalRef = T extends unknown[] | Signal
+ ? Signal
+ : Signal;
+type TranslateObjectSignalRef = T extends unknown[] | Signal
+ ? Signal
+ : Signal;
+
+/**
+ * Gets the translated value of a key as Signal
+ *
+ * @example
+ * text = translateSignal('hello');
+ * textList = translateSignal(['green', 'blue']);
+ * textVar = translateSignal('hello', { variable: 'world' });
+ * textSpanish = translateSignal('hello', { variable: 'world' }, 'es');
+ * textTodosScope = translateSignal('hello', { variable: 'world' }, { scope: 'todos' });
+ *
+ * @example
+ * dynamicKey = signal('hello');
+ * dynamicParam = signal({ variable: 'world' });
+ * text = translateSignal(this.dynamicKey, this.dynamicParam);
+ *
+ */
+export function translateSignal(
+ key: T,
+ params?: TranslateSignalParams,
+ lang?: ScopeType,
+ injector?: Injector,
+): TranslateSignalRef {
+ if (!injector) {
+ assertInInjectionContext(translateSignal);
+ }
+ injector ??= inject(Injector);
+ const result = runInInjectionContext(injector, () => {
+ const service = inject(TranslocoService);
+ const scope = resolveScope(lang);
+ return toObservable(computerKeysAndParams(key, params)).pipe(
+ switchMap((dynamic) =>
+ service.selectTranslate(dynamic.key, dynamic.params, scope),
+ ),
+ );
+ });
+ return toSignal(result, { initialValue: Array.isArray(key) ? [''] : '' });
+}
+
+/**
+ * Gets the translated object of a key as Signal
+ *
+ * @example
+ * object = translateObjectSignal('nested.object');
+ * title = object().title;
+ *
+ * @example
+ * dynamicKey = signal('nested.object');
+ * dynamicParam = signal({ variable: 'world' });
+ * object = translateObjectSignal(this.dynamicKey, this.dynamicParam);
+ */
+export function translateObjectSignal(
+ key: T,
+ params?: TranslateSignalParams,
+ lang?: ScopeType,
+ injector?: Injector,
+): TranslateObjectSignalRef {
+ if (!injector) {
+ assertInInjectionContext(translateObjectSignal);
+ }
+ injector ??= inject(Injector);
+ const result = runInInjectionContext(injector, () => {
+ const service = inject(TranslocoService);
+ const scope = resolveScope(lang);
+ return toObservable(computerKeysAndParams(key, params)).pipe(
+ switchMap((dynamic) =>
+ service.selectTranslateObject(
+ dynamic.key,
+ dynamic.params,
+ scope as string,
+ ),
+ ),
+ );
+ });
+ return toSignal(result, { initialValue: Array.isArray(key) ? [] : {} });
+}
+
+function computerParams(params: HashMap> | Signal) {
+ if (isSignal(params)) {
+ return computed(() => params());
+ }
+ return computed(() => {
+ return Object.entries(params).reduce((acc, [key, value]) => {
+ acc[key] = isSignal(value) ? value() : value;
+ return acc;
+ }, {} as HashMap);
+ });
+}
+
+function computerKeys(
+ keys: Signal | Signal | Signal[],
+) {
+ if (Array.isArray(keys)) {
+ return computed(() => keys.map((key) => (isSignal(key) ? key() : key)));
+ }
+ return computed(() => keys());
+}
+
+function isSignalKey(key: TranslateSignalKey): key is SignalKey {
+ return Array.isArray(key) ? key.some(isSignal) : isSignal(key);
+}
+
+function isSignalParams(
+ params?: HashMap,
+): params is HashMap> | Signal {
+ return params
+ ? isSignal(params) || Object.values(params).some(isSignal)
+ : false;
+}
+
+function computerKeysAndParams(
+ key: TranslateSignalKey,
+ params?: TranslateSignalParams,
+) {
+ const computedKeys = isSignalKey(key)
+ ? computerKeys(key)
+ : computed(() => key);
+ const computedParams = isSignalParams(params)
+ ? computerParams(params)
+ : computed(() => params);
+ return computed(() => ({ key: computedKeys(), params: computedParams() }));
+}
+
+function resolveScope(scope?: ScopeType) {
+ if (typeof scope === 'undefined' || scope === '') {
+ const translocoScope = inject(TRANSLOCO_SCOPE, { optional: true });
+ return translocoScope ?? undefined;
+ }
+ return scope;
+}
diff --git a/tsconfig.json b/tsconfig.json
index 540f38eb..b4686478 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -1,4 +1,5 @@
{
+ "extends": "./tsconfig.base.json",
"compileOnSave": false,
"compilerOptions": {
"resolveJsonModule": true,