Skip to content

Commit 1fb50ce

Browse files
committed
refactor
1 parent ad5a713 commit 1fb50ce

File tree

12 files changed

+299
-367
lines changed

12 files changed

+299
-367
lines changed

formatter.test.ts

Lines changed: 87 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -1,106 +1,95 @@
1-
import { assertEquals } from "@std/assert";
1+
import { assertEquals } from "@std/assert/assert-equals";
22
import { Formatter } from "./formatter.ts";
3+
import { defaultHooks } from "./plugins/default.ts";
34

4-
Deno.test("formatter, very simple text", () => {
5-
const formatter = new Formatter();
6-
7-
assertEquals(
8-
formatter.format("Hello World"),
9-
"Hello World",
10-
);
11-
});
12-
13-
Deno.test("formatter, basic template value", () => {
14-
const formatter = new Formatter();
15-
16-
assertEquals(
17-
formatter.format("Hello {target}", {
18-
target: "World!",
19-
}),
20-
"Hello World!",
21-
);
22-
});
23-
24-
Deno.test("formatter, template value with method", () => {
25-
const formatter = new Formatter({
26-
plugin: {
27-
({ current: self }) {
28-
if (self === "사과") {
29-
return "사과가";
30-
}
31-
return `${self}이`;
32-
},
5+
Deno.test("formatter, plural", () => {
6+
const messages = {
7+
ko: {
8+
"You have {count} file{count.other:}s{/}.":
9+
"{count}개의 파일이 있습니다.",
10+
},
11+
ar: {
12+
"You have {count} file{count.other:}s{/}.":
13+
"{count.zero:}لا يوجد لديك ملفات.{.one:}لديك ملف واحد.{.two:}لديك ملفان.{.few:}لديك {count} ملفات قليلة.{.many:}لديك {count} ملفات كثيرة.{.other:}لديك {count} ملفات.{/}",
3314
},
34-
});
15+
};
3516

36-
assertEquals(
37-
formatter.format("{name.이} 없습니다.", {
38-
name: "파일",
39-
}),
40-
"파일이 없습니다.",
41-
);
42-
assertEquals(
43-
formatter.format("{name.이} 없습니다.", {
44-
name: "사과",
45-
}),
46-
"사과가 없습니다.",
47-
);
48-
});
17+
{
18+
const formatter = new Formatter({
19+
locale: "en",
20+
hooks: defaultHooks,
21+
});
4922

50-
Deno.test("formatter, template with method that returns template", () => {
51-
const formatter = new Formatter({
52-
plugin: {
53-
one({ self, args, metadata }) {
54-
if (self == 1) {
55-
metadata.matched = true;
56-
return typeof args[0] === "function" ? args[0]() : args[0];
57-
}
58-
return "";
59-
},
60-
other({ current, args, metadata }) {
61-
if (!metadata.matched) {
62-
return typeof args[0] === "function" ? args[0]() : args[0];
63-
}
64-
return current;
65-
},
66-
male({ self, args, metadata }) {
67-
if (self === "male") {
68-
metadata.matched = true;
69-
return typeof args[0] === "function" ? args[0]() : args[0];
70-
}
71-
return "";
72-
},
73-
female({ self, args, metadata }) {
74-
if (self === "female") {
75-
metadata.matched = true;
76-
return typeof args[0] === "function" ? args[0]() : args[0];
77-
}
78-
return "";
79-
},
80-
},
81-
});
23+
assertEquals(
24+
formatter.format("You have {count} file{count.other:}s{/}.", {
25+
count: 1,
26+
}),
27+
"You have 1 file.",
28+
);
29+
assertEquals(
30+
formatter.format("You have {count} file{count.other:}s{/}.", {
31+
count: 2,
32+
}),
33+
"You have 2 files.",
34+
);
35+
}
36+
37+
{
38+
const formatter = new Formatter({
39+
locale: "ko",
40+
hooks: defaultHooks,
41+
messages: messages.ko,
42+
});
8243

83-
assertEquals(
84-
formatter.format(
85-
"{user} added {photoCount.one:}a new photo{.other:}{_} new photos{/} to {userGender.male:}his{.female:}her{.other:}their{/} steam.",
86-
{
87-
user: "Alex",
88-
photoCount: 3,
89-
userGender: "female",
90-
},
91-
),
92-
"Alex added 3 new photos to her steam.",
93-
);
44+
assertEquals(
45+
formatter.format("You have {count} file{count.other:}s{/}.", {
46+
count: 1,
47+
}),
48+
"1개의 파일이 있습니다.",
49+
);
50+
}
51+
{
52+
const formatter = new Formatter({
53+
locale: "ar",
54+
hooks: defaultHooks,
55+
messages: messages.ar,
56+
});
9457

95-
assertEquals(
96-
formatter.format(
97-
"{user} added {photoCount.one:}a new photo{.other:}{_} new photos{/} to {userGender.male:}his{.female:}her{.other:}their{/} steam.",
98-
{
99-
user: "Alex",
100-
photoCount: 1,
101-
userGender: "unknown",
102-
},
103-
),
104-
"Alex added a new photo to their steam.",
105-
);
58+
assertEquals(
59+
formatter.format("You have {count} file{count.other:}s{/}.", {
60+
count: 0,
61+
}),
62+
"لا يوجد لديك ملفات.",
63+
);
64+
assertEquals(
65+
formatter.format("You have {count} file{count.other:}s{/}.", {
66+
count: 1,
67+
}),
68+
"لديك ملف واحد.",
69+
);
70+
assertEquals(
71+
formatter.format("You have {count} file{count.other:}s{/}.", {
72+
count: 2,
73+
}),
74+
"لديك ملفان.",
75+
);
76+
assertEquals(
77+
formatter.format("You have {count} file{count.other:}s{/}.", {
78+
count: 3,
79+
}),
80+
"لديك 3 ملفات قليلة.",
81+
);
82+
assertEquals(
83+
formatter.format("You have {count} file{count.other:}s{/}.", {
84+
count: 11,
85+
}),
86+
"لديك 11 ملفات كثيرة.",
87+
);
88+
assertEquals(
89+
formatter.format("You have {count} file{count.other:}s{/}.", {
90+
count: 100,
91+
}),
92+
"لديك 100 ملفات.",
93+
);
94+
}
10695
});

formatter.ts

Lines changed: 18 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,62 +1,28 @@
11
import { Interpreter } from "./interpreter.ts";
2-
import type {
3-
FormatParameters,
4-
Plugin,
5-
PrimitiveType,
6-
Runtime,
7-
} from "./types.ts";
2+
import { defaultHooks } from "./plugins/default.ts";
3+
import type { FormatParameters, Hook, Runtime } from "./types.ts";
84

9-
export interface FormatterOptions {
5+
export interface FormatterOptions<Messages extends Record<string, string>> {
106
locale?: string;
11-
runtime?: Runtime;
12-
plugin?: Plugin;
7+
messages?: Messages;
8+
hooks?: Record<string, Hook>;
139
}
1410

15-
export class Formatter {
16-
readonly locale: string;
11+
export class Formatter<Messages extends Record<string, string>> {
1712
readonly runtime: Runtime;
18-
readonly plugin: Plugin;
19-
20-
constructor(options: FormatterOptions = {}) {
21-
this.locale = options.locale ?? "en";
22-
this.runtime = options.runtime ?? new Interpreter();
23-
this.plugin = options.plugin ?? {};
24-
}
25-
26-
format(text: string, parameters: FormatParameters = {}): string {
27-
return this.runtime.execute(
28-
text,
29-
parameters,
30-
createDecorator(this.locale, this.plugin),
13+
readonly messages: Messages;
14+
constructor({ messages, ...options }: FormatterOptions<Messages>) {
15+
this.runtime = new Interpreter(
16+
options.hooks ?? defaultHooks,
17+
options.locale,
3118
);
19+
this.messages = messages ?? {} as Messages;
3220
}
33-
}
3421

35-
function createDecorator(locale: string, plugin: Plugin) {
36-
return (self: PrimitiveType) => {
37-
let current = self;
38-
const metadata = {} as Record<string, unknown>;
39-
const decorated = new Proxy({}, {
40-
get(_, prop) {
41-
if (prop === "toString") {
42-
return () => current?.toString();
43-
}
44-
if (prop in plugin) {
45-
const hook = plugin[prop as string];
46-
return (...args: (string | number | (() => string))[]) => {
47-
current = hook({
48-
self,
49-
current,
50-
args,
51-
metadata,
52-
locale,
53-
});
54-
return decorated;
55-
};
56-
}
57-
return undefined;
58-
},
59-
});
60-
return decorated;
61-
};
22+
format<MessageId extends keyof Messages>(
23+
text: MessageId,
24+
parameters: FormatParameters = {},
25+
): string {
26+
return this.runtime.execute(this.messages[text] ?? text, parameters);
27+
}
6228
}

0 commit comments

Comments
 (0)