Skip to content

Commit f879ddb

Browse files
author
Timofei Iatsenko
authored
feat(message-utils): make generateMessageId to be working in browser (#1776)
* feat(message-utils): make generateMessageId to be working in browser / lambda / etc * refactor(message-utils): extract tests for compileMessage fn
1 parent efcd405 commit f879ddb

5 files changed

Lines changed: 240 additions & 34 deletions

File tree

packages/core/src/interpolate.test.ts

Lines changed: 7 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import { compileMessage as compile } from "@lingui/message-utils/compileMessage"
2-
import { mockEnv, mockConsole } from "@lingui/jest-mocks"
32
import { interpolate } from "./interpolate"
43
import { Locale, Locales } from "./i18n"
54

@@ -9,15 +8,6 @@ describe("interpolate", () => {
98
return interpolate(tokens, locale || "en", locales)
109
}
1110

12-
it("should handle an error if message has syntax errors", () => {
13-
mockConsole((console) => {
14-
expect(compile("Invalid {message")).toEqual("Invalid {message")
15-
expect(console.error).toBeCalledWith(
16-
expect.stringMatching("Unexpected message end at line")
17-
)
18-
})
19-
})
20-
2111
it("should process string chunks with provided map fn", () => {
2212
const tokens = compile(
2313
"Message {value, plural, one {{value} Book} other {# Books}}",
@@ -36,28 +26,18 @@ describe("interpolate", () => {
3626
])
3727
})
3828

39-
it("should compile static message", () => {
40-
const cache = compile("Static message")
41-
expect(cache).toEqual("Static message")
42-
43-
mockEnv("production", () => {
44-
const cache = compile("Static message")
45-
expect(cache).toEqual("Static message")
46-
})
47-
})
48-
49-
it("should compile message with variable", () => {
29+
it("should interpolate message with variable", () => {
5030
const cache = compile("Hey {name}!")
5131
expect(interpolate(cache, "en", [])({ name: "Joe" })).toEqual("Hey Joe!")
5232
})
5333

5434
it("should not interpolate escaped placeholder", () => {
5535
const msg = prepare("Hey '{name}'!")
5636

57-
expect(msg({})).toEqual("Hey {name}!")
37+
expect(msg({ name: "Joe" })).toEqual("Hey {name}!")
5838
})
5939

60-
it("should compile plurals", () => {
40+
it("should interpolate plurals", () => {
6141
const plural = prepare(
6242
"{value, plural, one {{value} Book} other {# Books}}"
6343
)
@@ -72,7 +52,7 @@ describe("interpolate", () => {
7252
expect(offset({ value: 3 })).toEqual("2 Books")
7353
})
7454

75-
it("should compile plurals with falsy value choice", () => {
55+
it("should interpolate plurals with falsy value choice", () => {
7656
const plural = prepare("{value, plural, one {} other {# Books}}")
7757
expect(plural({ value: 1 })).toEqual("")
7858
expect(plural({ value: 2 })).toEqual("2 Books")
@@ -85,7 +65,7 @@ describe("interpolate", () => {
8565
expect(plural({ value: 30 })).toEqual("30% discount")
8666
})
8767

88-
it("should compile selectordinal", () => {
68+
it("should interpolate selectordinal", () => {
8969
const cache = prepare(
9070
"{value, selectordinal, one {#st Book} two {#nd Book}}"
9171
)
@@ -119,7 +99,7 @@ describe("interpolate", () => {
11999
)
120100
})
121101

122-
it("should compile select", () => {
102+
it("should interpolate select", () => {
123103
const cache = prepare("{value, select, female {She} other {They}}")
124104
expect(cache({ value: "female" })).toEqual("She")
125105
expect(cache({ value: "n/a" })).toEqual("They")
@@ -161,7 +141,7 @@ describe("interpolate", () => {
161141
expectedCurrency2,
162142
] = tc
163143

164-
it(`should compile custom format for locale=${locale} and locales=${locales}`, () => {
144+
it(`should interpolate custom format for locale=${locale} and locales=${locales}`, () => {
165145
const number = prepare("{value, number}", locale, locales)
166146
expect(number({ value: 0.1 })).toEqual(expectedNumber)
167147

packages/message-utils/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,8 @@
4747
"generateMessageId.js"
4848
],
4949
"dependencies": {
50-
"@messageformat/parser": "^5.0.0"
50+
"@messageformat/parser": "^5.0.0",
51+
"js-sha256": "^0.10.1"
5152
},
5253
"devDependencies": {
5354
"@lingui/jest-mocks": "workspace:^",
Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
import { mockConsole } from "@lingui/jest-mocks"
2+
import { compileMessage } from "./compileMessage"
3+
4+
describe("compileMessage", () => {
5+
it("should handle an error if message has syntax errors", () => {
6+
mockConsole((console) => {
7+
expect(compileMessage("Invalid {message")).toEqual("Invalid {message")
8+
expect(console.error).toBeCalledWith(
9+
expect.stringMatching("Unexpected message end at line")
10+
)
11+
})
12+
})
13+
14+
it("should process string chunks with provided map fn", () => {
15+
const tokens = compileMessage(
16+
"Message {value, plural, one {{value} Book} other {# Books}}",
17+
(text) => `<${text}>`
18+
)
19+
expect(tokens).toEqual([
20+
"<Message >",
21+
[
22+
"value",
23+
"plural",
24+
{
25+
one: [["value"], "< Book>"],
26+
other: ["#", "< Books>"],
27+
},
28+
],
29+
])
30+
})
31+
32+
it("should compileMessage static message", () => {
33+
const tokens = compileMessage("Static message")
34+
expect(tokens).toEqual("Static message")
35+
})
36+
37+
it("should compileMessage message with variable", () => {
38+
const tokens = compileMessage("Hey {name}!")
39+
expect(tokens).toMatchInlineSnapshot(`
40+
[
41+
Hey ,
42+
[
43+
name,
44+
],
45+
!,
46+
]
47+
`)
48+
})
49+
50+
it("should not interpolate escaped placeholder", () => {
51+
const tokens = compileMessage("Hey '{name}'!")
52+
expect(tokens).toMatchInlineSnapshot(`Hey {name}!`)
53+
})
54+
55+
it("should compile plurals", () => {
56+
const tokens = compileMessage(
57+
"{value, plural, offset:1 =0 {No Books} one {# Book} other {# Books}}"
58+
)
59+
expect(tokens).toMatchInlineSnapshot(`
60+
[
61+
[
62+
value,
63+
plural,
64+
{
65+
0: No Books,
66+
offset: 1,
67+
one: [
68+
#,
69+
Book,
70+
],
71+
other: [
72+
#,
73+
Books,
74+
],
75+
},
76+
],
77+
]
78+
`)
79+
})
80+
81+
it("should compile selectordinal", () => {
82+
const tokens = compileMessage(
83+
"{value, selectordinal, one {#st Book} two {#nd Book}}"
84+
)
85+
expect(tokens).toMatchInlineSnapshot(`
86+
[
87+
[
88+
value,
89+
selectordinal,
90+
{
91+
offset: undefined,
92+
one: [
93+
#,
94+
st Book,
95+
],
96+
two: [
97+
#,
98+
nd Book,
99+
],
100+
},
101+
],
102+
]
103+
`)
104+
})
105+
106+
it("should compile nested choice components", () => {
107+
const tokens = compileMessage(
108+
`{
109+
gender, select,
110+
male {{numOfGuests, plural, one {He invites one guest} other {He invites # guests}}}
111+
female {{numOfGuests, plural, one {She invites one guest} other {She invites # guests}}}
112+
other {They is {gender}}}`
113+
)
114+
expect(tokens).toMatchInlineSnapshot(`
115+
[
116+
[
117+
gender,
118+
select,
119+
{
120+
female: [
121+
[
122+
numOfGuests,
123+
plural,
124+
{
125+
offset: undefined,
126+
one: She invites one guest,
127+
other: [
128+
She invites ,
129+
#,
130+
guests,
131+
],
132+
},
133+
],
134+
],
135+
male: [
136+
[
137+
numOfGuests,
138+
plural,
139+
{
140+
offset: undefined,
141+
one: He invites one guest,
142+
other: [
143+
He invites ,
144+
#,
145+
guests,
146+
],
147+
},
148+
],
149+
],
150+
offset: undefined,
151+
other: [
152+
They is ,
153+
[
154+
gender,
155+
],
156+
],
157+
},
158+
],
159+
]
160+
`)
161+
})
162+
163+
it("should compile select", () => {
164+
const tokens = compileMessage("{value, select, female {She} other {They}}")
165+
expect(tokens).toMatchInlineSnapshot(`
166+
[
167+
[
168+
value,
169+
select,
170+
{
171+
female: She,
172+
offset: undefined,
173+
other: They,
174+
},
175+
],
176+
]
177+
`)
178+
})
179+
180+
it("should compile date", () => {
181+
const tokens = compileMessage("{value, date}")
182+
expect(tokens).toMatchInlineSnapshot(`
183+
[
184+
[
185+
value,
186+
date,
187+
],
188+
]
189+
`)
190+
})
191+
192+
it("should compile number", () => {
193+
expect(compileMessage("{value, number, percent}")).toMatchInlineSnapshot(`
194+
[
195+
[
196+
value,
197+
number,
198+
percent,
199+
],
200+
]
201+
`)
202+
expect(compileMessage("{value, number}")).toMatchInlineSnapshot(`
203+
[
204+
[
205+
value,
206+
number,
207+
],
208+
]
209+
`)
210+
})
211+
})
Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,17 @@
1-
import crypto from "crypto"
1+
import { sha256 } from "js-sha256"
22

33
const UNIT_SEPARATOR = "\u001F"
44

55
export function generateMessageId(msg: string, context = "") {
6-
return crypto
7-
.createHash("sha256")
8-
.update(msg + UNIT_SEPARATOR + (context || ""))
9-
.digest("base64")
10-
.slice(0, 6)
6+
return hexToBase64(sha256(msg + UNIT_SEPARATOR + (context || ""))).slice(0, 6)
7+
}
8+
9+
function hexToBase64(hexStr: string) {
10+
let base64 = ""
11+
for (let i = 0; i < hexStr.length; i++) {
12+
base64 += !((i - 1) & 1)
13+
? String.fromCharCode(parseInt(hexStr.substring(i - 1, i + 1), 16))
14+
: ""
15+
}
16+
return btoa(base64)
1117
}

yarn.lock

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3411,6 +3411,7 @@ __metadata:
34113411
dependencies:
34123412
"@lingui/jest-mocks": "workspace:^"
34133413
"@messageformat/parser": ^5.0.0
3414+
js-sha256: ^0.10.1
34143415
unbuild: 2.0.0
34153416
languageName: unknown
34163417
linkType: soft
@@ -10653,6 +10654,13 @@ __metadata:
1065310654
languageName: unknown
1065410655
linkType: soft
1065510656

10657+
"js-sha256@npm:^0.10.1":
10658+
version: 0.10.1
10659+
resolution: "js-sha256@npm:0.10.1"
10660+
checksum: 6eb5c9f95aa902cec1930f036deb3bc664024b75fede456c0ac74b855797776c18620f47efec36787077a56ba2f3b8d6aacc7733ff8a2b5bb9ce6b655a35c5e6
10661+
languageName: node
10662+
linkType: hard
10663+
1065610664
"js-tokens@npm:^3.0.0 || ^4.0.0, js-tokens@npm:^4.0.0":
1065710665
version: 4.0.0
1065810666
resolution: "js-tokens@npm:4.0.0"

0 commit comments

Comments
 (0)