Skip to content

Commit 0e7e35b

Browse files
committed
feat: implement pseudo-locales and transformer functionality
1 parent 813abc7 commit 0e7e35b

File tree

6 files changed

+129
-102
lines changed

6 files changed

+129
-102
lines changed

src/pseudo-locale.ts

Lines changed: 0 additions & 98 deletions
This file was deleted.

src/pseudo-locale/Transformer.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/**
2+
* Based on: https://hg-edge.mozilla.org/mozilla-central/file/a1f74e8c/intl/l10n/L10nRegistry.jsm#l425
3+
*/
4+
export class Transformer {
5+
private readonly caps: readonly number[]
6+
private readonly small: readonly number[]
7+
private readonly elongate: boolean
8+
9+
constructor(caps: string, smalls: string, elongate: boolean) {
10+
this.caps = Array.from(caps, (c) => c.codePointAt(0)!)
11+
this.small = Array.from(smalls, (c) => c.codePointAt(0)!)
12+
this.elongate = elongate
13+
this.stringify = this.stringify.bind(this)
14+
Object.freeze(this)
15+
}
16+
17+
stringify(message: string) {
18+
const points = Array.from(message, (c) => c.codePointAt(0)!)
19+
return String.fromCodePoint.apply(null, Array.from(this.transform(points)))
20+
}
21+
22+
private *transform(points: readonly number[]): Iterable<number> {
23+
for (const point of points) {
24+
if (point >= 0x61 && point <= 0x7a) {
25+
yield this.small[point - 0x61] // a-z
26+
if (!this.isElongate(point)) continue
27+
yield this.small[point - 0x61] // duplicate "a", "e", "o" and "u" to emulate ~30% longer text
28+
} else if (point >= 0x41 && point <= 0x5a) {
29+
yield this.caps[point - 0x41] // A-Z
30+
} else {
31+
yield point // non-alphabetic characters remain unchanged
32+
}
33+
}
34+
}
35+
36+
private isElongate(code: number): boolean {
37+
if (!this.elongate) return false
38+
return code === 0x61 || code === 0x65 || code === 0x6f || code === 0x75
39+
}
40+
}

src/pseudo-locale/ast.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { createLiteralElement, isLiteralElement, MessageFormatElement } from '@formatjs/icu-messageformat-parser'
2+
import { Transformer } from './Transformer'
3+
4+
export const ACCENTED_MAP = new Transformer(
5+
'\u0226\u0181\u0187\u1e12\u1e16\u0191\u0193\u0126\u012a\u0134\u0136\u013f\u1e3e\u0220\u01fe\u01a4\u024a\u0158\u015e\u0166\u016c\u1e7c\u1e86\u1e8a\u1e8e\u1e90',
6+
'\u0227\u0180\u0188\u1e13\u1e17\u0192\u0260\u0127\u012b\u0135\u0137\u0140\u1e3f\u019e\u01ff\u01a5\u024b\u0159\u015f\u0167\u016d\u1e7d\u1e87\u1e8b\u1e8f\u1e91',
7+
true
8+
)
9+
10+
export const FLIPPED_MAP = new Transformer(
11+
'\u2200\u0510\u2183\u15e1\u018e\u2132\u2141\x48\x49\u017f\u04fc\u2142\x57\x4e\x4f\u0500\xd2\u1d1a\x53\u22a5\u2229\u0245\x4d\x58\u2144\x5a',
12+
'\u0250\x71\u0254\x70\u01dd\u025f\u0183\u0265\u0131\u027e\u029e\u0285\u026f\x75\x6f\x64\x62\u0279\x73\u0287\x6e\u028c\u028d\x78\u028e\x7a',
13+
false
14+
)
15+
16+
export function createEnglishTransformer(brackets: string, transformer: Transformer) {
17+
const [leftBracket, rightBracket] = brackets
18+
return function* (elements: Iterable<MessageFormatElement>) {
19+
yield createLiteralElement(leftBracket)
20+
yield* modifyLiteralElement(elements, transformer.stringify)
21+
yield createLiteralElement(rightBracket)
22+
}
23+
}
24+
25+
export function* modifyLiteralElement(
26+
elements: Iterable<MessageFormatElement>,
27+
modifier: (input: string) => string
28+
): Iterable<MessageFormatElement> {
29+
for (const element of elements) {
30+
if (isLiteralElement(element)) {
31+
yield { ...element, value: modifier(element.value) }
32+
} else if ('options' in element) {
33+
const entries = Object.entries(element.options).map(([key, option]) => [
34+
key,
35+
{ value: Array.from(modifyLiteralElement(option.value, modifier)) },
36+
])
37+
yield { ...element, options: Object.fromEntries(entries) }
38+
} else if ('children' in element) {
39+
yield { ...element, children: Array.from(modifyLiteralElement(element.children, modifier)) }
40+
} else {
41+
yield element
42+
}
43+
}
44+
}

src/pseudo-locale/index.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import type { MessageFormatElement } from '@formatjs/icu-messageformat-parser'
2+
import { createLiteralElement } from '@formatjs/icu-messageformat-parser'
3+
import { ACCENTED_MAP, createEnglishTransformer, FLIPPED_MAP, modifyLiteralElement } from './ast'
4+
5+
const locales: Record<string, (elements: Iterable<MessageFormatElement>) => Iterable<MessageFormatElement>> = {
6+
*'xx-LS'(elements) {
7+
yield* elements
8+
yield createLiteralElement('S'.repeat(25))
9+
},
10+
'xx-AC': (elements) => modifyLiteralElement(elements, (value) => value.toUpperCase()),
11+
*'xx-HA'(elements) {
12+
yield createLiteralElement('[javascript]')
13+
yield* elements
14+
},
15+
'en-XA': createEnglishTransformer('\u005b\u005d', ACCENTED_MAP),
16+
'en-XB': createEnglishTransformer('\u202e\u202c', FLIPPED_MAP),
17+
}
18+
19+
export function getPseudoLocaleNames(): string[] {
20+
return Object.keys(locales)
21+
}
22+
23+
export function getPseudoLocale(name: string): (elements: MessageFormatElement[]) => MessageFormatElement[] {
24+
const handle = locales[name]
25+
return (elements) => Array.from(handle(elements))
26+
}

tests/helpers.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { MessageFormatElement } from '@formatjs/icu-messageformat-parser'
2+
import { printAST } from '@formatjs/icu-messageformat-parser/printer'
13
import { createFsFromVolume, IFs, Volume } from 'memfs'
24
import Module from 'node:module'
35
import * as path from 'node:path'
@@ -38,8 +40,13 @@ export class WebpackCompiler {
3840
return this.fs.promises.readFile(path.join(this.basePath, name), 'utf-8') as Promise<string>
3941
}
4042

41-
async execute<T>() {
42-
return execute<T>(await this.getEntryFile())
43+
async execute(): Promise<Record<string, MessageFormatElement[]>> {
44+
return execute(await this.getEntryFile())
45+
}
46+
47+
async getMessages(): Promise<Record<string, string>> {
48+
const messages = await this.execute()
49+
return Object.fromEntries(Object.entries(messages).map(([key, value]) => [key, printAST(value)]))
4350
}
4451
}
4552

tests/webpack.spec.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,14 +51,22 @@ describe('Webpack', () => {
5151
})
5252

5353
describe('Pseudo-Locales', async () => {
54-
const variants: readonly string[] = ['xx-LS', 'xx-AC', 'xx-HA', 'en-XA', 'en-XB']
54+
const variants = new Map<string, Record<string, string>>([
55+
['xx-LS', { hello: 'worldSSSSSSSSSSSSSSSSSSSSSSSSS', tag: '<i>hello world</i>SSSSSSSSSSSSSSSSSSSSSSSSS' }],
56+
['xx-AC', { hello: 'WORLD', tag: '<i>HELLO WORLD</i>' }],
57+
['xx-HA', { hello: '[javascript]world', tag: '[javascript]<i>hello world</i>' }],
58+
['en-XA', { hello: '[ẇǿǿřŀḓ]', tag: '[<i>ħḗḗŀŀǿǿ ẇǿǿřŀḓ</i>]' }],
59+
['en-XB', { hello: '\u202eʍoɹʅp\u202c', tag: '\u202e<i>ɥǝʅʅo ʍoɹʅp</i>\u202c' }],
60+
])
5561

56-
for (const pseudoLocale of variants) {
62+
for (const [pseudoLocale, expexted] of variants.entries()) {
5763
it(pseudoLocale, async () => {
5864
const instance = new WebpackCompiler('messages.default.json', { pseudoLocale })
5965
const { compilation } = await instance.run()
6066
expect(compilation.errors).lengthOf(0)
6167
expect(compilation.warnings).lengthOf(0)
68+
const messages = await instance.getMessages()
69+
expect(messages).deep.equals(expexted)
6270
})
6371
}
6472

0 commit comments

Comments
 (0)