Skip to content

Commit faea479

Browse files
committed
feat(i18n): optionally return observables and support variable substitution
1 parent 3aa2781 commit faea479

File tree

7 files changed

+123
-6
lines changed

7 files changed

+123
-6
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ dist
77
demo/bundle
88
.idea
99
.vscode
10+
.cache
1011
doc/
1112
documentation/
1213
dist/

src/i18n/i18n.module.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { NgModule, SkipSelf, Optional } from "@angular/core";
22

33
import { I18n } from "./i18n.service";
44

5-
export { I18n } from "./i18n.service";
5+
export { I18n, replace } from "./i18n.service";
66

77
// either provides a new instance of ModalPlaceholderService, or returns the parent
88
export function I18N_SERVICE_PROVIDER_FACTORY(parentService: I18n) {

src/i18n/i18n.service.ts

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,63 @@
11
import { Injectable } from "@angular/core";
2+
import { Subject, BehaviorSubject } from "rxjs";
3+
import { map } from "rxjs/operators";
4+
import { stringify } from "querystring";
25

36
const EN = require("./en.json");
47

8+
export const replace = (subject, variables) => subject.pipe(
9+
map<string, void>(str => {
10+
const keys = Object.keys(variables);
11+
for (const key of keys) {
12+
const value = variables[key];
13+
while (str.includes(`{{${key}}}`)) {
14+
str = str.replace(`{{${key}}}`, value);
15+
}
16+
}
17+
return str;
18+
})
19+
);
20+
521
@Injectable()
622
export class I18n {
723
protected translationStrings = EN;
824

25+
protected translations = new Map();
26+
927
public set(strings) {
1028
this.translationStrings = Object.assign({}, EN, strings);
29+
const translations = Array.from(this.translations);
30+
for (const [path, subject] of translations) {
31+
subject.next(this.getValueFromPath(path));
32+
}
33+
}
34+
35+
public get(path?) {
36+
if (!path) {
37+
return this.translationStrings;
38+
}
39+
const value = this.getValueFromPath(path);
40+
if (this.translations.has(path)) {
41+
return this.translations.get(path);
42+
}
43+
const translation = new BehaviorSubject(value);
44+
this.translations.set(path, translation);
45+
return translation;
46+
}
47+
48+
public replace(subject, variables) {
49+
return replace(subject, variables);
1150
}
1251

13-
public get() {
14-
return this.translationStrings;
52+
protected getValueFromPath(path) {
53+
let value = this.translationStrings;
54+
for (const segment of path.split(".")) {
55+
if (value[segment]) {
56+
value = value[segment];
57+
} else {
58+
throw new Error(`no key ${segment} at ${path}`);
59+
}
60+
}
61+
return value;
1562
}
1663
}

src/i18n/i18n.spec.ts

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import { I18n } from "./i18n.service";
2+
3+
const EN = require("./en.json");
4+
5+
let service;
6+
7+
describe("i18n service", () => {
8+
beforeEach(() => {
9+
service = new I18n();
10+
});
11+
12+
it("should translate a string", done => {
13+
service.get("BANNER.CLOSE_BUTTON").subscribe(value => {
14+
expect(value).toBe(EN.BANNER.CLOSE_BUTTON);
15+
done();
16+
});
17+
});
18+
19+
it("should update strings", done => {
20+
service.set({ "BANNER": { "CLOSE_BUTTON": "test" }});
21+
22+
service.get("BANNER.CLOSE_BUTTON").subscribe(value => {
23+
expect(value).toBe("test");
24+
done();
25+
});
26+
});
27+
28+
it("should emit updated string", () => {
29+
const subject = service.get("BANNER.CLOSE_BUTTON");
30+
31+
const spy = spyOn(subject, "next");
32+
33+
service.set({ "BANNER": { "CLOSE_BUTTON": "test" } });
34+
35+
expect(spy).toHaveBeenCalled();
36+
});
37+
38+
it("should replace variables", done => {
39+
service.set({ "TEST": "{{foo}} bar"});
40+
41+
service.replace(service.get("TEST"), {foo: "test"}).subscribe(value => {
42+
expect(value).toBe("test bar");
43+
done();
44+
});
45+
});
46+
47+
it("should replace multiple of the same variable", done => {
48+
service.set({ "TEST": "{{foo}} {{foo}}" });
49+
50+
service.replace(service.get("TEST"), { foo: "test" }).subscribe(value => {
51+
expect(value).toBe("test test");
52+
done();
53+
});
54+
});
55+
56+
it("should replace multiple variables", done => {
57+
service.set({ "TEST": "{{foo}} {{bar}}" });
58+
59+
service.replace(service.get("TEST"), { foo: "test", bar: "asdf" }).subscribe(value => {
60+
expect(value).toBe("test asdf");
61+
done();
62+
});
63+
});
64+
});

src/notification/notification-content.interface.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ export interface NotificationContent {
44
target?: string;
55
duration?: number;
66
smart?: boolean;
7-
closeLabel?: string;
7+
closeLabel?: any;
88
message: string;
99
}
1010

src/notification/notification.component.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
import { NotificationContent } from "./notification-content.interface";
1212
import { I18n } from "./../i18n/i18n.module";
1313
import { NotificationDisplayService } from "./notification-display.service";
14+
import { of } from "rxjs";
1415

1516
/**
1617
* Notification messages are displayed toward the top of the UI and do not interrupt user’s work.
@@ -33,7 +34,7 @@ import { NotificationDisplayService } from "./notification-display.service";
3334
<button
3435
(click)="onClose()"
3536
class="bx--inline-notification__close-button"
36-
[attr.aria-label]="notificationObj.closeLabel"
37+
[attr.aria-label]="notificationObj.closeLabel | async"
3738
type="button">
3839
<svg
3940
class="bx--inline-notification__close-icon"
@@ -59,6 +60,9 @@ export class Notification {
5960
return this._notificationObj;
6061
}
6162
set notificationObj(obj: NotificationContent) {
63+
if (obj.closeLabel) {
64+
obj.closeLabel = of(obj.closeLabel);
65+
}
6266
this._notificationObj = Object.assign({}, this.defaultNotificationObj, obj);
6367
}
6468

@@ -87,7 +91,7 @@ export class Notification {
8791
title: "",
8892
message: "",
8993
type: "info",
90-
closeLabel: this.i18n.get().NOTIFICATION.CLOSE_BUTTON
94+
closeLabel: this.i18n.get("NOTIFICATION.CLOSE_BUTTON")
9195
};
9296
protected _notificationObj: NotificationContent = Object.assign({}, this.defaultNotificationObj);
9397

src/notification/notification.stories.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { withKnobs, boolean, object } from "@storybook/addon-knobs/angular";
66
import { Component } from "@angular/core";
77

88
import { NotificationModule, NotificationService } from "./notification.module";
9+
import { I18n } from "../i18n/i18n.module";
910

1011
@Component({
1112
selector: "app-notification-story",

0 commit comments

Comments
 (0)