Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add fluent bundle option when setting translation source map #7

Merged
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
2 changes: 1 addition & 1 deletion projects/ngx-fluent-example/src/app/app.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export class AppComponent implements OnInit {
ngOnInit() {
this.fluentService.setTranslationSourceMap({
en: 'assets/i18n/en.ftl',
sv: 'assets/i18n/sv.ftl',
sv: { path: 'assets/i18n/sv.ftl' },
});

this.fluentService.setLocale('en');
Expand Down
154 changes: 111 additions & 43 deletions projects/ngx-fluent/src/lib/ngx-fluent.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,67 +8,135 @@ describe('NgxFluentService', () => {
let fluentService: NgxFluentService;
let httpSpy: jasmine.SpyObj<HttpClient>;

beforeEach(() => {
const _httpSpy = jasmine.createSpyObj('HttpClient', ['get', 'pipe']);
describe('SetTranslationSourceMap option uses Record<string, string>', () => {
beforeEach(() => {
const _httpSpy = jasmine.createSpyObj('HttpClient', ['get', 'pipe']);

TestBed.configureTestingModule({
providers: [NgxFluentService, { provide: HttpClient, useValue: _httpSpy }],
TestBed.configureTestingModule({
providers: [NgxFluentService, { provide: HttpClient, useValue: _httpSpy }],
});

httpSpy = TestBed.inject(HttpClient) as jasmine.SpyObj<HttpClient>;
httpSpy.get.and.returnValue(of(''));

fluentService = TestBed.inject(NgxFluentService);
fluentService.setTranslationSourceMap({
en: 'assets/locales/en.ftl',
});
});

httpSpy = TestBed.inject(HttpClient) as jasmine.SpyObj<HttpClient>;
httpSpy.get.and.returnValue(of(''));
it('localeChanges should emit new locale', () => {
fluentService.setLocale('test-locale');

fluentService = TestBed.inject(NgxFluentService);
fluentService.setTranslationSourceMap({
en: 'assets/locales/en.ftl',
fluentService.localeChanges.subscribe((locale) => {
expect(locale).toBe('test-locale');
});
});
});

it('localeChanges should emit new locale', () => {
fluentService.setLocale('test-locale');
it('currentLocale should return null initially', () => {
expect(fluentService.currentLocale).toBeNull();
});

fluentService.localeChanges.subscribe((locale) => {
expect(locale).toBe('test-locale');
it('currentLocale should return new locale after setting setLocale', () => {
fluentService.setLocale('en');
expect(fluentService.currentLocale).toBe('en');
});
});

it('currentLocale should return null initially', () => {
expect(fluentService.currentLocale).toBeNull();
});
it('unresolved locale returns null', async () => {
const key = 'test-key';
fluentService.setLocale('non-existent');

it('currentLocale should return new locale after setting setLocale', () => {
fluentService.setLocale('en');
expect(fluentService.currentLocale).toBe('en');
});
const result = await fluentService.translate(key);
expect(result).toBeNull();
});

it('unresolved locale returns null', async () => {
const key = 'test-key';
fluentService.setLocale('non-existent');
it('resolved locale and message returns translation', async () => {
const key = 'test-key';
const value = 'test-translation';
const translation = `${key} = ${value}`;

const result = await fluentService.translate(key);
expect(result).toBeNull();
});
httpSpy.get.and.returnValue(of(translation));
fluentService.setLocale('en');

const result = await fluentService.translate(key);
expect(result).toBe(value);
});

it('resolved locale and message returns translation', async () => {
const key = 'test-key';
const value = 'test-translation';
const translation = `${key} = ${value}`;
it('resolved locale and unresolved message returns null', async () => {
const key = 'unknown-key';
const translation = 'test-key = test-translation';

httpSpy.get.and.returnValue(of(translation));
fluentService.setLocale('en');
httpSpy.get.and.returnValue(of(translation));
fluentService.setLocale('en');

const result = await fluentService.translate(key);
expect(result).toBe(value);
const result = await fluentService.translate(key);
expect(result).toBeNull();
});
});

it('resolved locale and unresolved message returns null', async () => {
const key = 'unknown-key';
const translation = 'test-key = test-translation';
describe('SetTranslationSourceMap option uses Record<string, FluentBundleOptions>', () => {
beforeEach(() => {
const _httpSpy = jasmine.createSpyObj('HttpClient', ['get', 'pipe']);

TestBed.configureTestingModule({
providers: [NgxFluentService, { provide: HttpClient, useValue: _httpSpy }],
});

httpSpy = TestBed.inject(HttpClient) as jasmine.SpyObj<HttpClient>;
httpSpy.get.and.returnValue(of(''));

fluentService = TestBed.inject(NgxFluentService);
fluentService.setTranslationSourceMap({
en: { path: 'assets/locales/en.ftl', bundleConfig: { useIsolating: false } },
});
});

it('localeChanges should emit new locale', () => {
fluentService.setLocale('test-locale');

fluentService.localeChanges.subscribe((locale) => {
expect(locale).toBe('test-locale');
});
});

it('currentLocale should return null initially', () => {
expect(fluentService.currentLocale).toBeNull();
});

it('currentLocale should return new locale after setting setLocale', () => {
fluentService.setLocale('en');
expect(fluentService.currentLocale).toBe('en');
});

it('unresolved locale returns null', async () => {
const key = 'test-key';
fluentService.setLocale('non-existent');

httpSpy.get.and.returnValue(of(translation));
fluentService.setLocale('en');
const result = await fluentService.translate(key);
expect(result).toBeNull();
});

it('resolved locale and message returns translation', async () => {
const key = 'test-key';
const value = 'test-translation';
const translation = `${key} = ${value}`;

httpSpy.get.and.returnValue(of(translation));
fluentService.setLocale('en');

const result = await fluentService.translate(key);
expect(result).toBe(value);
});

it('resolved locale and unresolved message returns null', async () => {
const key = 'unknown-key';
const translation = 'test-key = test-translation';

const result = await fluentService.translate(key);
expect(result).toBeNull();
httpSpy.get.and.returnValue(of(translation));
fluentService.setLocale('en');

const result = await fluentService.translate(key);
expect(result).toBeNull();
});
});
});
45 changes: 39 additions & 6 deletions projects/ngx-fluent/src/lib/ngx-fluent.service.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,29 @@
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { BehaviorSubject, map, catchError, of, lastValueFrom } from 'rxjs';
import { FluentBundle, FluentResource } from '@fluent/bundle';
import { FluentBundle, FluentFunction, FluentResource, TextTransform } from '@fluent/bundle';

type FluentBundleOptions = {
functions?: Record<string, FluentFunction>;
useIsolating?: boolean;
transform?: TextTransform;
};
type TranslationSourceMapParams = Record<
string,
| string
| {
path: string;
bundleConfig?: FluentBundleOptions;
}
>;

@Injectable({
providedIn: 'root',
})
export class NgxFluentService {
private locale = new BehaviorSubject<string | null>(null);

private translationSourceMap: Record<string, string> = {};
private translationSourceMap: TranslationSourceMapParams = {};
private translationsMap = new Map<string, FluentBundle | null>();

localeChanges = this.locale.asObservable();
Expand All @@ -23,11 +37,30 @@ export class NgxFluentService {
return of(translationBundle);
}

// If we don't have the translation, fetch it.
const translationSourceUrl = this.translationSourceMap[locale] ?? '';
const localizedTranslationSourceMap = this.translationSourceMap[locale];

let translationSourceUrl: string;
let fluentBundleOptions: FluentBundleOptions;

switch (typeof localizedTranslationSourceMap) {
case 'string':
translationSourceUrl = localizedTranslationSourceMap ?? '';
break;

case 'object':
translationSourceUrl = localizedTranslationSourceMap.path ?? '';
fluentBundleOptions = localizedTranslationSourceMap.bundleConfig ?? {};
break;

default:
// If we don't have the translation, fetch it.
translationSourceUrl = '';
break;
}

return this.http.get(translationSourceUrl, { responseType: 'text' }).pipe(
map((content) => {
const bundle = new FluentBundle(locale);
const bundle = new FluentBundle(locale, fluentBundleOptions);
const resource = new FluentResource(content);
const errors = bundle.addResource(resource);

Expand Down Expand Up @@ -57,7 +90,7 @@ export class NgxFluentService {
});
}

setTranslationSourceMap(translationSourceMap: Record<string, string>) {
setTranslationSourceMap(translationSourceMap: TranslationSourceMapParams) {
this.translationSourceMap = translationSourceMap;
}

Expand Down