Skip to content

Commit 17899c8

Browse files
committed
refactor: use Maps as internal representation
1 parent 2f81de6 commit 17899c8

File tree

6 files changed

+164
-38
lines changed

6 files changed

+164
-38
lines changed

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ set, the message resolver throws an error reporting the missing key.
7676

7777
A message's content may contain placeholders to be replaced with
7878
actual values passed to `m`. Placeholders are referred to by index
79-
and use the typical mustache notation, i.e. `{{0}}`to refer to the
79+
and use the typical mustache notation, i.e. `{{0}}` to refer to the
8080
first argument.
8181

8282
If you have a messages definition like
@@ -145,7 +145,7 @@ console.log(messageResolver.mpl("inbox.folder.summary", numberOfMessages, "trash
145145
The following section contains a description of the provided `MessageLoader` implementions.
146146
Make sure you also check out the source code and doc comments for the classes.
147147

148-
### `ObjectMessageLoader`
148+
### ObjectMessageLoader
149149

150150
A `MessageLoader` that "loads" messages from a given object. This message loader is typically be
151151
used in an environment where messages are `import`ed from static files during Javascript assembly.
@@ -164,7 +164,7 @@ const messageResolver = MessageResolver.create(new ObjectMessageLoader(en, {
164164
This pattern is very easy to get running and works very for few translations with a small number
165165
of messages.
166166

167-
### `CascadingMessageLoader`
167+
### CascadingMessageLoader
168168

169169
**TODO**
170170

index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,5 +18,5 @@
1818

1919
export { Language, Messages } from "./src/Messages"
2020
export { MessageLoader } from "./src/MessageLoader"
21-
export { ObjectMessageLoader, CascadingMessageLoader } from "./src/loaders"
21+
export { ObjectMessageLoader, CascadingMessageLoader, JsonMessageLoader, JsonSource } from "./src/loaders"
2222
export { MessageResolver, MessageResolvingError } from "./src/MessageResolver"

src/MessageResolver.ts

Lines changed: 18 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -70,25 +70,16 @@ export class MessageResolver {
7070
if (typeof msg === "string") {
7171
return this.reportError(`Expected message for key '${key}' to be plural object but got string "${msg}"`)
7272
}
73+
74+
if (msg.has(amount)) {
75+
return this.formatMessage(msg.get(amount), args, amount)
76+
}
7377

74-
if (typeof msg[amount] !== "undefined") {
75-
return this.formatMessage(msg[amount], args, amount)
78+
if (msg.has("n")) {
79+
return this.formatMessage(msg.get("n"), args, amount)
7680
}
7781

78-
const pm = msg as PluralMessage
79-
80-
if (amount >= 0 && amount <= 12) {
81-
const a = `${amount}` as PluralKey
82-
if (typeof pm[a] !== "undefined") {
83-
return this.formatMessage(pm[a], args, amount)
84-
}
85-
}
86-
87-
if (typeof pm.n === "undefined") {
88-
return this.reportError(`Missing catch-all in plural message for key '${key}'`)
89-
}
90-
91-
return this.formatMessage(pm.n, args, amount)
82+
return this.reportError(`Missing catch-all in plural message for key '${key}'`)
9283
}
9384

9485
/**
@@ -114,7 +105,17 @@ export class MessageResolver {
114105
* @returns the resolved key or an error message (depending on `errorReporting`)
115106
*/
116107
private resolveMessage(key: MessageKey): Message {
117-
return (this.localizedMessages ? this.localizedMessages[key] : undefined) ?? this.defaultMessages[key] ?? this.reportError(`Undefined message key: '${key}'`)
108+
if (this.localizedMessages) {
109+
if (this.localizedMessages.has(key)) {
110+
return this.localizedMessages.get(key)
111+
}
112+
}
113+
114+
if (this.defaultMessages.has(key)) {
115+
return this.defaultMessages.get(key)
116+
}
117+
118+
return this.reportError(`Undefined message key: '${key}'`)
118119
}
119120

120121
/**

src/Messages.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,12 @@ export type MessageKey = string
3030
/**
3131
* Defines the possible plural key values.
3232
*/
33-
export type PluralKey = number | "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" | "10" | "11" | "12" | "n"
33+
export type PluralKey = number | "n"
3434

3535
/**
3636
* Defines the type for message being used to format plurals.
3737
*/
38-
export type PluralMessage = { [Property in PluralKey]?: string }
38+
export type PluralMessage = Map<PluralKey, string>
3939

4040
/**
4141
* Defines the type of a single message which is either a `string` for plain messages
@@ -47,4 +47,4 @@ export type Message = string | PluralMessage
4747
* `Messages` defines a mapping of message keys (strings) to
4848
* message patterns.
4949
*/
50-
export type Messages = { [key: string]: Message }
50+
export type Messages = Map<MessageKey, Message>

src/loaders.ts

Lines changed: 95 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,22 +17,46 @@
1717
*/
1818

1919
import { MessageLoader } from "./MessageLoader"
20-
import { Language, Messages } from "./Messages"
20+
import { Language, Message, MessageKey, Messages, PluralKey } from "./Messages"
21+
22+
export type MessagesObject = {[messageKey in MessageKey]: string | {[pluralKey: string]: string}}
2123

2224
/**
2325
* An implementation of `MessageLoader` that "loads" `Messages` from given Javascript objects.
2426
* This implemenation is especially usefull when loading messages from JSON files that are included
2527
* during Javascript assembly.
2628
*/
2729
export class ObjectMessageLoader implements MessageLoader {
28-
constructor(private readonly defaultMessages: Messages, private readonly messagesByLanguage: { [key in Language]: Messages }) { }
30+
constructor(private readonly defaultMessages: MessagesObject, private readonly messagesByLanguage: { [key in Language]: MessagesObject }) { }
2931

3032
loadDefaultMessages(): Promise<Messages> {
31-
return Promise.resolve(this.defaultMessages)
33+
return Promise.resolve(this.transformMessagesObject(this.defaultMessages))
3234
}
3335

3436
loadMessages(language: Language): Promise<Messages | undefined> {
35-
return Promise.resolve(this.messagesByLanguage[language])
37+
if (typeof this.messagesByLanguage[language] === "undefined") {
38+
return Promise.resolve(undefined)
39+
}
40+
41+
return Promise.resolve(this.transformMessagesObject(this.messagesByLanguage[language]))
42+
}
43+
44+
private transformMessagesObject(o: MessagesObject): Messages {
45+
const m = new Map<MessageKey, Message>()
46+
Object.keys(o).forEach(key => m.set(key, this.transformMessage(o[key])))
47+
return m
48+
}
49+
50+
private transformMessage(m: string | {[pluralKey: string]: string}): Message {
51+
if (typeof m === "string") {
52+
return m
53+
}
54+
55+
const msg = new Map<PluralKey, string>()
56+
57+
Object.keys(m).forEach(k => msg.set(k === "n" ? "n" : parseInt(k), m[k]))
58+
59+
return msg
3660
}
3761
}
3862

@@ -63,16 +87,79 @@ export class CascadingMessageLoader implements MessageLoader {
6387

6488
private async mergeMessages(input: Array<Promise<Messages | undefined>>): Promise<Messages> {
6589
const loaded = await Promise.all(input)
66-
const msg = loaded[0] ?? {}
90+
const msg = loaded[0] ?? new Map<MessageKey, Message>()
91+
6792
loaded.slice(1).forEach(m => {
6893
if (!m) {
6994
return
7095
}
71-
Object.keys(m).forEach(k => {
72-
msg[k] = m[k]
73-
})
96+
m.forEach((val, key) => msg.set(key, val))
7497
})
7598

7699
return msg
77100
}
101+
}
102+
103+
export type JsonMessages = {[key: string]: string | {[pluralKey: string]: string}}
104+
105+
/**
106+
* A strategy interface used for different loaders loading JSON representation
107+
* of a message.
108+
*/
109+
export interface JsonSource {
110+
(language?: Language): Promise<string | undefined>
111+
}
112+
113+
export class JsonMessageLoaderError extends Error {
114+
constructor (msg: string) {
115+
super(msg)
116+
}
117+
}
118+
119+
/**
120+
* A `MessageLoader` that loads messages from `JsonSource`s. The loader parses
121+
* the JSON and normalizes the plural messages by replacing string notated numbers,
122+
* i.e. `"1"` with their numeric keys.
123+
*/
124+
export class JsonMessageLoader implements MessageLoader {
125+
constructor(private readonly defaultsLoader: JsonSource, private readonly localizedLoader: JsonSource) { }
126+
127+
loadDefaultMessages(): Promise<Messages> {
128+
return this.parseJson(this.defaultsLoader())
129+
}
130+
131+
loadMessages(language: Language): Promise<Messages | undefined> {
132+
return this.parseJson(this.localizedLoader(language))
133+
}
134+
135+
private async parseJson(input: Promise<string | undefined>): Promise<Messages | undefined> {
136+
const jsonString = await input
137+
if (typeof jsonString === "undefined") {
138+
return jsonString
139+
}
140+
141+
const messages = new Map<MessageKey, Message>()
142+
143+
const parsed = JSON.parse(jsonString) as JsonMessages
144+
Object.keys(parsed).forEach(messageKey => {
145+
const msg = parsed[messageKey]
146+
if (typeof msg === "string") {
147+
messages.set(messageKey, msg)
148+
} else {
149+
const pluralMessage = new Map<PluralKey, string>()
150+
Object.keys(parsed[messageKey]).forEach((pluralKey: string) => {
151+
if (pluralKey.match(/^[0-9]+$/)) {
152+
pluralMessage.set(parseInt(pluralKey), msg[pluralKey])
153+
} else if (pluralKey === "n") {
154+
pluralMessage.set("n", msg[pluralKey])
155+
} else {
156+
throw new JsonMessageLoaderError(`invalid plural key '${pluralKey}' for message key '${messageKey}'`)
157+
}
158+
})
159+
messages.set(messageKey, pluralMessage)
160+
}
161+
})
162+
163+
return messages
164+
}
78165
}

test/loaders.test.ts

Lines changed: 44 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { expect } from "iko"
22
import { ObjectMessageLoader, CascadingMessageLoader, Messages } from ".."
3+
import { JsonMessageLoader } from "../src/loaders"
34

45
describe("ObjectMessageLoader", () => {
56
const defaultMessages = {
@@ -16,13 +17,17 @@ describe("ObjectMessageLoader", () => {
1617

1718
describe("loadDefaultMessage", () => {
1819
it("should return default messages", async () => {
19-
expect(await loader.loadDefaultMessages()).toBe(defaultMessages)
20+
expect(await loader.loadDefaultMessages())
21+
.toBeMap()
22+
.toHave("foo")
2023
})
2124
})
2225

2326
describe("loadMessages", () => {
2427
it("should return messages when language is found", async () => {
25-
expect(await loader.loadMessages("de")).toBe(messagesByLanguage.de)
28+
expect(await loader.loadMessages("de"))
29+
.toBeMap()
30+
.toHave("foo")
2631
})
2732

2833
it("should return undefined when language is not found", async () => {
@@ -64,11 +69,11 @@ describe("CascadingMessageLoader", () => {
6469
})
6570

6671
it("should merge messages from first", () => {
67-
expect(messages.foo).toBe("bar")
72+
expect(messages.get("foo")).toBe("bar")
6873
})
6974

7075
it("should overwrite messages from second", () => {
71-
expect(messages.spam).toBe("eggs")
76+
expect(messages.get("spam")).toBe("eggs")
7277
})
7378
})
7479

@@ -79,11 +84,44 @@ describe("CascadingMessageLoader", () => {
7984
})
8085

8186
it("should merge messages from first", () => {
82-
expect(messages.foo).toBe("BAR")
87+
expect(messages.get("foo")).toBe("BAR")
8388
})
8489

8590
it("should overwrite messages from second", () => {
86-
expect(messages.spam).toBe("Eier")
91+
expect(messages.get("spam")).toBe("Eier")
92+
})
93+
})
94+
})
95+
96+
describe("JsonMessageLoader", () => {
97+
const messages = {
98+
foo: "bar",
99+
plural: {
100+
"0": "0",
101+
"1": "1",
102+
"n": "n",
103+
},
104+
}
105+
const source = () => Promise.resolve(JSON.stringify(messages))
106+
const loader = new JsonMessageLoader(source, source)
107+
108+
describe("loadDefaultMessages", () => {
109+
let loaded: Messages
110+
111+
before(async () => {
112+
loaded = await loader.loadDefaultMessages()
113+
})
114+
115+
it("should load default messages", () => {
116+
expect(loaded.get("foo")).toBe("bar")
117+
})
118+
119+
it("should normalized plural object", () => {
120+
expect(loaded.get("plural"))
121+
.toBeMap()
122+
.toHave(0)
123+
.toHave(1)
124+
.toHave("n")
87125
})
88126
})
89127
})

0 commit comments

Comments
 (0)