pseudo-localization 3.0.3 → 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -11
- package/dist/bin/pseudo-localize.d.ts +2 -0
- package/dist/bin/pseudo-localize.js +25 -0
- package/dist/bin/pseudo-localize.js.map +1 -0
- package/dist/dom.d.ts +32 -0
- package/dist/dom.js +106 -0
- package/dist/dom.js.map +1 -0
- package/dist/localize.d.ts +28 -0
- package/dist/localize.js +67 -0
- package/dist/localize.js.map +1 -0
- package/package.json +17 -15
- package/{bin/pseudo-localize.mjs → src/bin/pseudo-localize.ts} +4 -4
- package/src/{dom.mjs → dom.ts} +31 -40
- package/src/localize.test.ts +40 -0
- package/src/{localize.mjs → localize.ts} +29 -25
package/README.md
CHANGED
|
@@ -37,7 +37,7 @@ Pseudo-localization helps detect issues such as:
|
|
|
37
37
|
npm install pseudo-localization
|
|
38
38
|
```
|
|
39
39
|
|
|
40
|
-
###
|
|
40
|
+
### Copy The Source Code
|
|
41
41
|
|
|
42
42
|
Copy the files from [`src`](https://github.com/tryggvigy/pseudo-localization/blob/master/src) and use them directly.
|
|
43
43
|
|
|
@@ -148,13 +148,7 @@ stop();
|
|
|
148
148
|
A command-line interface (CLI) is available for quick testing and automation.
|
|
149
149
|
|
|
150
150
|
```sh
|
|
151
|
-
npx pseudo-localization
|
|
152
|
-
|
|
153
|
-
# Pipe a string
|
|
154
|
-
echo "hello world" | npx pseudo-localization
|
|
155
|
-
|
|
156
|
-
# Direct input
|
|
157
|
-
npx pseudo-localization -i "hello world"
|
|
151
|
+
npx pseudo-localization "hello world"
|
|
158
152
|
```
|
|
159
153
|
|
|
160
154
|
### CLI Options
|
|
@@ -163,13 +157,11 @@ npx pseudo-localization -i "hello world"
|
|
|
163
157
|
pseudo-localization [src] [options]
|
|
164
158
|
|
|
165
159
|
Positionals:
|
|
166
|
-
src Input
|
|
160
|
+
src Input string
|
|
167
161
|
|
|
168
162
|
Options:
|
|
169
|
-
-o, --output Output file (defaults to STDOUT)
|
|
170
163
|
--strategy Localization strategy (accented or bidi)
|
|
171
164
|
--help Show help
|
|
172
|
-
--version Show version
|
|
173
165
|
```
|
|
174
166
|
|
|
175
167
|
---
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
#! /usr/bin/env node
|
|
2
|
+
import yargs from 'yargs/yargs';
|
|
3
|
+
import { hideBin } from 'yargs/helpers';
|
|
4
|
+
import { pseudoLocalizeString } from "../localize.js";
|
|
5
|
+
yargs(hideBin(process.argv))
|
|
6
|
+
.usage('$0 <string> [options]', 'Pseudo localize a string', (yargs) => {
|
|
7
|
+
return yargs
|
|
8
|
+
.positional('string', {
|
|
9
|
+
describe: 'String to pseudo-localize',
|
|
10
|
+
type: 'string',
|
|
11
|
+
demandOption: true,
|
|
12
|
+
})
|
|
13
|
+
.option('strategy', {
|
|
14
|
+
alias: 's',
|
|
15
|
+
type: 'string',
|
|
16
|
+
describe: 'Set the strategy for localization',
|
|
17
|
+
choices: ['accented', 'bidi'],
|
|
18
|
+
default: 'accented',
|
|
19
|
+
});
|
|
20
|
+
}, ({ string, strategy }) => {
|
|
21
|
+
process.stdout.write(pseudoLocalizeString(string, { strategy }));
|
|
22
|
+
})
|
|
23
|
+
.help()
|
|
24
|
+
.parse();
|
|
25
|
+
//# sourceMappingURL=pseudo-localize.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pseudo-localize.js","sourceRoot":"","sources":["../../src/bin/pseudo-localize.ts"],"names":[],"mappings":";AAEA,OAAO,KAAK,MAAM,aAAa,CAAC;AAChC,OAAO,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC;AACxC,OAAO,EAAE,oBAAoB,EAAE,MAAM,gBAAgB,CAAC;AAEtD,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;KACzB,KAAK,CACJ,uBAAuB,EACvB,0BAA0B,EAC1B,CAAC,KAAK,EAAE,EAAE;IACR,OAAO,KAAK;SACT,UAAU,CAAC,QAAQ,EAAE;QACpB,QAAQ,EAAE,2BAA2B;QACrC,IAAI,EAAE,QAAQ;QACd,YAAY,EAAE,IAAI;KACnB,CAAC;SACD,MAAM,CAAC,UAAU,EAAE;QAClB,KAAK,EAAE,GAAG;QACV,IAAI,EAAE,QAAQ;QACd,QAAQ,EAAE,mCAAmC;QAC7C,OAAO,EAAE,CAAC,UAAU,EAAE,MAAM,CAAU;QACtC,OAAO,EAAE,UAAmB;KAC7B,CAAC,CAAC;AACP,CAAC,EACD,CAAC,EAAE,MAAM,EAAE,QAAQ,EAAE,EAAE,EAAE;IACvB,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,oBAAoB,CAAC,MAAM,EAAE,EAAE,QAAQ,EAAE,CAAC,CAAC,CAAC;AACnE,CAAC,CACF;KACA,IAAI,EAAE;KACN,KAAK,EAAE,CAAC"}
|
package/dist/dom.d.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { type PseudoLocalizeStringOptions } from './localize.ts';
|
|
2
|
+
type StartOptions = PseudoLocalizeStringOptions & {
|
|
3
|
+
/**
|
|
4
|
+
* Node tags to ignore in pseudo-localization.
|
|
5
|
+
*
|
|
6
|
+
* @default ['STYLE']
|
|
7
|
+
*/
|
|
8
|
+
blacklistedNodeNames?: string[];
|
|
9
|
+
/**
|
|
10
|
+
* The element to pseudo-localize text within.
|
|
11
|
+
*
|
|
12
|
+
* @default document.body
|
|
13
|
+
*/
|
|
14
|
+
root?: Node;
|
|
15
|
+
};
|
|
16
|
+
type StopFn = () => void;
|
|
17
|
+
/**
|
|
18
|
+
* A container for pseudo-localization of the DOM.
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* new PseudoLocalizeDom().start();
|
|
22
|
+
*/
|
|
23
|
+
export declare class PseudoLocalizeDom {
|
|
24
|
+
#private;
|
|
25
|
+
/**
|
|
26
|
+
* Start pseudo-localizing the DOM.
|
|
27
|
+
*
|
|
28
|
+
* Returns a stop function to disable pseudo-localization.
|
|
29
|
+
*/
|
|
30
|
+
start({ strategy, blacklistedNodeNames, root, }?: StartOptions): StopFn;
|
|
31
|
+
}
|
|
32
|
+
export {};
|
package/dist/dom.js
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { pseudoLocalizeString, } from "./localize.js";
|
|
2
|
+
function isNonEmptyString(str) {
|
|
3
|
+
return !!str && typeof str === 'string';
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* A container for pseudo-localization of the DOM.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* new PseudoLocalizeDom().start();
|
|
10
|
+
*/
|
|
11
|
+
export class PseudoLocalizeDom {
|
|
12
|
+
/**
|
|
13
|
+
* Indicates which elements the pseudo-localization is currently running on.
|
|
14
|
+
*/
|
|
15
|
+
#enabledOn = new WeakSet();
|
|
16
|
+
/**
|
|
17
|
+
* Start pseudo-localizing the DOM.
|
|
18
|
+
*
|
|
19
|
+
* Returns a stop function to disable pseudo-localization.
|
|
20
|
+
*/
|
|
21
|
+
start({ strategy = 'accented', blacklistedNodeNames = ['STYLE'], root, } = {}) {
|
|
22
|
+
const rootEl = root ?? document.body;
|
|
23
|
+
if (this.#enabledOn.has(rootEl)) {
|
|
24
|
+
console.warn(`Start aborted. pseudo-localization is already enabled on`, rootEl);
|
|
25
|
+
return () => { };
|
|
26
|
+
}
|
|
27
|
+
const observerConfig = {
|
|
28
|
+
characterData: true,
|
|
29
|
+
childList: true,
|
|
30
|
+
subtree: true,
|
|
31
|
+
};
|
|
32
|
+
const textNodesUnder = (node) => {
|
|
33
|
+
const walker = document.createTreeWalker(node, NodeFilter.SHOW_TEXT, (node) => {
|
|
34
|
+
const isAllWhitespace = node.nodeValue && !/[^\s]/.test(node.nodeValue);
|
|
35
|
+
if (isAllWhitespace) {
|
|
36
|
+
return NodeFilter.FILTER_REJECT;
|
|
37
|
+
}
|
|
38
|
+
const isBlacklistedNode = node.parentElement &&
|
|
39
|
+
blacklistedNodeNames.includes(node.parentElement.nodeName);
|
|
40
|
+
if (isBlacklistedNode) {
|
|
41
|
+
return NodeFilter.FILTER_REJECT;
|
|
42
|
+
}
|
|
43
|
+
return NodeFilter.FILTER_ACCEPT;
|
|
44
|
+
});
|
|
45
|
+
let currNode;
|
|
46
|
+
const textNodes = [];
|
|
47
|
+
while ((currNode = walker.nextNode()))
|
|
48
|
+
textNodes.push(currNode);
|
|
49
|
+
return textNodes;
|
|
50
|
+
};
|
|
51
|
+
const pseudoLocalize = (node) => {
|
|
52
|
+
const textNodesUnderElement = textNodesUnder(node);
|
|
53
|
+
for (const textNode of textNodesUnderElement) {
|
|
54
|
+
const nodeValue = textNode.nodeValue;
|
|
55
|
+
if (isNonEmptyString(nodeValue)) {
|
|
56
|
+
textNode.nodeValue = pseudoLocalizeString(nodeValue, { strategy });
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
// Pseudo localize the DOM
|
|
61
|
+
pseudoLocalize(document.body);
|
|
62
|
+
// Start observing the DOM for changes and run
|
|
63
|
+
// pseudo localization on any added text nodes
|
|
64
|
+
const observer = new MutationObserver((mutationsList) => {
|
|
65
|
+
for (const mutation of mutationsList) {
|
|
66
|
+
if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
|
|
67
|
+
// Turn the observer off while performing dom manipulation to prevent
|
|
68
|
+
// infinite dom mutation callback loops
|
|
69
|
+
observer.disconnect();
|
|
70
|
+
// For every node added, recurse down it's subtree and convert
|
|
71
|
+
// all children as well
|
|
72
|
+
mutation.addedNodes.forEach(pseudoLocalize.bind(this));
|
|
73
|
+
observer.observe(document.body, observerConfig);
|
|
74
|
+
}
|
|
75
|
+
else if (mutation.type === 'characterData') {
|
|
76
|
+
const nodeValue = mutation.target.nodeValue;
|
|
77
|
+
const isBlacklistedNode = !!mutation.target.parentElement &&
|
|
78
|
+
blacklistedNodeNames.includes(mutation.target.parentElement.nodeName);
|
|
79
|
+
if (isNonEmptyString(nodeValue) && !isBlacklistedNode) {
|
|
80
|
+
// Turn the observer off while performing dom manipulation to prevent
|
|
81
|
+
// infinite dom mutation callback loops
|
|
82
|
+
observer.disconnect();
|
|
83
|
+
// The target will always be a text node so it can be converted
|
|
84
|
+
// directly
|
|
85
|
+
mutation.target.nodeValue = pseudoLocalizeString(nodeValue, {
|
|
86
|
+
strategy,
|
|
87
|
+
});
|
|
88
|
+
observer.observe(document.body, observerConfig);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
observer.observe(document.body, observerConfig);
|
|
94
|
+
this.#enabledOn.add(rootEl);
|
|
95
|
+
const stop = () => {
|
|
96
|
+
if (!this.#enabledOn.has(rootEl)) {
|
|
97
|
+
console.warn('Stop aborted. pseudo-localization is not running on', rootEl);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
observer.disconnect();
|
|
101
|
+
this.#enabledOn.delete(rootEl);
|
|
102
|
+
};
|
|
103
|
+
return stop;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
//# sourceMappingURL=dom.js.map
|
package/dist/dom.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dom.js","sourceRoot":"","sources":["../src/dom.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export type Strategy = 'accented' | 'bidi';
|
|
2
|
+
export type PseudoLocalizeStringOptions = {
|
|
3
|
+
/**
|
|
4
|
+
* The strategy to employ.
|
|
5
|
+
* - `accented`:
|
|
6
|
+
* In Accented English all Latin letters are replaced by accented
|
|
7
|
+
* Unicode counterparts which don't impair the readability of the content.
|
|
8
|
+
* This allows developers to quickly test if any given string is being
|
|
9
|
+
* correctly displayed in its 'translated' form. Additionally, simple
|
|
10
|
+
* heuristics are used to make certain words longer to better simulate the
|
|
11
|
+
* experience of international users.
|
|
12
|
+
* - `bidi`:
|
|
13
|
+
* Bidi English is a fake [RTL](https://developer.mozilla.org/en-US/docs/Glossary/rtl) locale.
|
|
14
|
+
* All words are surrounded by
|
|
15
|
+
* Unicode formatting marks forcing the RTL directionality of characters.
|
|
16
|
+
* In addition, to make the reversed text easier to read, individual
|
|
17
|
+
* letters are flipped.
|
|
18
|
+
*/
|
|
19
|
+
strategy?: Strategy;
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* Pseudo-localizes a string using a specified strategy.
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* pseudoLocalizeString('orange'); // 'ǿǿřȧȧƞɠḗḗ'
|
|
26
|
+
* pseudoLocalizeString('orange', {strategy: 'bidi'}); // 'ǝƃuɐɹo'
|
|
27
|
+
*/
|
|
28
|
+
export declare const pseudoLocalizeString: (string: string, { strategy }?: PseudoLocalizeStringOptions) => string;
|
package/dist/localize.js
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/* prettier-ignore */
|
|
2
|
+
const ACCENTED_MAP = {
|
|
3
|
+
a: 'ȧ', A: 'Ȧ', b: 'ƀ', B: 'Ɓ', c: 'ƈ', C: 'Ƈ', d: 'ḓ', D: 'Ḓ', e: 'ḗ', E: 'Ḗ',
|
|
4
|
+
f: 'ƒ', F: 'Ƒ', g: 'ɠ', G: 'Ɠ', h: 'ħ', H: 'Ħ', i: 'ī', I: 'Ī', j: 'ĵ', J: 'Ĵ',
|
|
5
|
+
k: 'ķ', K: 'Ķ', l: 'ŀ', L: 'Ŀ', m: 'ḿ', M: 'Ḿ', n: 'ƞ', N: 'Ƞ', o: 'ǿ', O: 'Ǿ',
|
|
6
|
+
p: 'ƥ', P: 'Ƥ', q: 'ɋ', Q: 'Ɋ', r: 'ř', R: 'Ř', s: 'ş', S: 'Ş', t: 'ŧ', T: 'Ŧ',
|
|
7
|
+
v: 'ṽ', V: 'Ṽ', u: 'ŭ', U: 'Ŭ', w: 'ẇ', W: 'Ẇ', x: 'ẋ', X: 'Ẋ', y: 'ẏ', Y: 'Ẏ',
|
|
8
|
+
z: 'ẑ', Z: 'Ẑ',
|
|
9
|
+
};
|
|
10
|
+
/* prettier-ignore */
|
|
11
|
+
const BIDI_MAP = {
|
|
12
|
+
a: 'ɐ', A: '∀', b: 'q', B: 'Ԑ', c: 'ɔ', C: 'Ↄ', d: 'p', D: 'ᗡ', e: 'ǝ', E: 'Ǝ',
|
|
13
|
+
f: 'ɟ', F: 'Ⅎ', g: 'ƃ', G: '⅁', h: 'ɥ', H: 'H', i: 'ı', I: 'I', j: 'ɾ', J: 'ſ',
|
|
14
|
+
k: 'ʞ', K: 'Ӽ', l: 'ʅ', L: '⅂', m: 'ɯ', M: 'W', n: 'u', N: 'N', o: 'o', O: 'O',
|
|
15
|
+
p: 'd', P: 'Ԁ', q: 'b', Q: 'Ò', r: 'ɹ', R: 'ᴚ', s: 's', S: 'S', t: 'ʇ', T: '⊥',
|
|
16
|
+
u: 'n', U: '∩', v: 'ʌ', V: 'Ʌ', w: 'ʍ', W: 'M', x: 'x', X: 'X', y: 'ʎ', Y: '⅄',
|
|
17
|
+
z: 'z', Z: 'Z',
|
|
18
|
+
};
|
|
19
|
+
const strategies = {
|
|
20
|
+
accented: {
|
|
21
|
+
prefix: '',
|
|
22
|
+
postfix: '',
|
|
23
|
+
map: ACCENTED_MAP,
|
|
24
|
+
elongate: true,
|
|
25
|
+
},
|
|
26
|
+
bidi: {
|
|
27
|
+
// Surround words with Unicode formatting marks forcing
|
|
28
|
+
// right-to-left directionality of characters
|
|
29
|
+
prefix: '\u202e',
|
|
30
|
+
postfix: '\u202c',
|
|
31
|
+
map: BIDI_MAP,
|
|
32
|
+
elongate: false,
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* Pseudo-localizes a string using a specified strategy.
|
|
37
|
+
*
|
|
38
|
+
* @example
|
|
39
|
+
* pseudoLocalizeString('orange'); // 'ǿǿřȧȧƞɠḗḗ'
|
|
40
|
+
* pseudoLocalizeString('orange', {strategy: 'bidi'}); // 'ǝƃuɐɹo'
|
|
41
|
+
*/
|
|
42
|
+
export const pseudoLocalizeString = (string, { strategy = 'accented' } = {}) => {
|
|
43
|
+
const opts = strategies[strategy];
|
|
44
|
+
let pseudoLocalizedText = '';
|
|
45
|
+
for (const character of string) {
|
|
46
|
+
if (character in opts.map) {
|
|
47
|
+
const char = character;
|
|
48
|
+
const cl = char.toLowerCase();
|
|
49
|
+
// duplicate "a", "e", "o" and "u" to emulate ~30% longer text
|
|
50
|
+
if (opts.elongate &&
|
|
51
|
+
(cl === 'a' || cl === 'e' || cl === 'o' || cl === 'u')) {
|
|
52
|
+
pseudoLocalizedText += opts.map[char] + opts.map[char];
|
|
53
|
+
}
|
|
54
|
+
else
|
|
55
|
+
pseudoLocalizedText += opts.map[char];
|
|
56
|
+
}
|
|
57
|
+
else
|
|
58
|
+
pseudoLocalizedText += character;
|
|
59
|
+
}
|
|
60
|
+
// Add pre/postfix if the string does not already contain them respectively.
|
|
61
|
+
const prefix = pseudoLocalizedText.startsWith(opts.prefix) ? '' : opts.prefix;
|
|
62
|
+
const postfix = pseudoLocalizedText.endsWith(opts.postfix)
|
|
63
|
+
? ''
|
|
64
|
+
: opts.postfix;
|
|
65
|
+
return prefix + pseudoLocalizedText + postfix;
|
|
66
|
+
};
|
|
67
|
+
//# sourceMappingURL=localize.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"localize.js","sourceRoot":"","sources":["../src/localize.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,37 +1,38 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pseudo-localization",
|
|
3
|
-
"version": "3.0
|
|
3
|
+
"version": "3.1.0",
|
|
4
4
|
"description": "pseudo-localization for internationalization testing",
|
|
5
5
|
"files": [
|
|
6
|
-
"
|
|
7
|
-
"src
|
|
8
|
-
"src/dom.mjs"
|
|
6
|
+
"dist",
|
|
7
|
+
"src"
|
|
9
8
|
],
|
|
10
|
-
"main": "./src/localize.mjs",
|
|
11
9
|
"type": "module",
|
|
10
|
+
"sideEffects": false,
|
|
12
11
|
"exports": {
|
|
13
12
|
".": {
|
|
14
|
-
"
|
|
13
|
+
"types": "./dist/localize.d.ts",
|
|
14
|
+
"import": "./dist/localize.js"
|
|
15
15
|
},
|
|
16
16
|
"./dom": {
|
|
17
|
-
"
|
|
17
|
+
"types": "./dist/dom.d.ts",
|
|
18
|
+
"import": "./dist/dom.js"
|
|
18
19
|
}
|
|
19
20
|
},
|
|
20
21
|
"bin": {
|
|
21
|
-
"pseudo-localization": "./bin/pseudo-localize.
|
|
22
|
+
"pseudo-localization": "./dist/bin/pseudo-localize.js"
|
|
22
23
|
},
|
|
23
24
|
"engines": {
|
|
24
25
|
"node": "^23"
|
|
25
26
|
},
|
|
26
27
|
"scripts": {
|
|
28
|
+
"build": "tsc -p tsconfig.lib.json",
|
|
29
|
+
"check-formatting": "prettier --list-different src/**/*.ts",
|
|
30
|
+
"format": "prettier --write src/**/*.ts",
|
|
31
|
+
"lint": "eslint src/**/*.ts",
|
|
27
32
|
"start": "node devserver.mjs",
|
|
28
|
-
"
|
|
29
|
-
"
|
|
30
|
-
"
|
|
31
|
-
"test-js": "node ./src/localize.test.mjs",
|
|
32
|
-
"test": "npm run test-js jest && npm run check-formatting && npm run check-types && npm run lint",
|
|
33
|
-
"check-types": "tsc --noEmit",
|
|
34
|
-
"tsc": "tsc"
|
|
33
|
+
"test-js": "node --experimental-strip-types --disable-warning=ExperimentalWarning --test './src/**/*.test.ts'",
|
|
34
|
+
"test": "npm run test-js && npm run check-formatting && npm run typecheck && npm run lint",
|
|
35
|
+
"typecheck": "tsc -p tsconfig.lib.json --noEmit"
|
|
35
36
|
},
|
|
36
37
|
"author": "Tryggvi Gylfason (http://twitter.com/tryggvigy)",
|
|
37
38
|
"keywords": [
|
|
@@ -53,6 +54,7 @@
|
|
|
53
54
|
"devDependencies": {
|
|
54
55
|
"@eslint/js": "^9.17.0",
|
|
55
56
|
"@types/node": "^22.10.2",
|
|
57
|
+
"@types/yargs": "^17.0.33",
|
|
56
58
|
"eslint": "^9.17.0",
|
|
57
59
|
"globals": "^15.13.0",
|
|
58
60
|
"prettier": "^3.4.2",
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import yargs from 'yargs/yargs';
|
|
4
4
|
import { hideBin } from 'yargs/helpers';
|
|
5
|
-
import { pseudoLocalizeString } from '../
|
|
5
|
+
import { pseudoLocalizeString } from '../localize.ts';
|
|
6
6
|
|
|
7
7
|
yargs(hideBin(process.argv))
|
|
8
8
|
.usage(
|
|
@@ -13,13 +13,14 @@ yargs(hideBin(process.argv))
|
|
|
13
13
|
.positional('string', {
|
|
14
14
|
describe: 'String to pseudo-localize',
|
|
15
15
|
type: 'string',
|
|
16
|
+
demandOption: true,
|
|
16
17
|
})
|
|
17
18
|
.option('strategy', {
|
|
18
19
|
alias: 's',
|
|
19
20
|
type: 'string',
|
|
20
21
|
describe: 'Set the strategy for localization',
|
|
21
|
-
choices: ['accented', 'bidi'],
|
|
22
|
-
default: 'accented',
|
|
22
|
+
choices: ['accented', 'bidi'] as const,
|
|
23
|
+
default: 'accented' as const,
|
|
23
24
|
});
|
|
24
25
|
},
|
|
25
26
|
({ string, strategy }) => {
|
|
@@ -27,5 +28,4 @@ yargs(hideBin(process.argv))
|
|
|
27
28
|
}
|
|
28
29
|
)
|
|
29
30
|
.help()
|
|
30
|
-
.version()
|
|
31
31
|
.parse();
|
package/src/{dom.mjs → dom.ts}
RENAMED
|
@@ -1,30 +1,28 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
pseudoLocalizeString,
|
|
3
|
+
type PseudoLocalizeStringOptions,
|
|
4
|
+
} from './localize.ts';
|
|
2
5
|
|
|
3
|
-
|
|
4
|
-
* Checks if the given value is a non-empty string.
|
|
5
|
-
* @param {unknown} str - The value to check.
|
|
6
|
-
* @returns {str is string} `true` if the value is a non-empty string, otherwise `false`.
|
|
7
|
-
*/
|
|
8
|
-
function isNonEmptyString(str) {
|
|
6
|
+
function isNonEmptyString(str: string | null | undefined): str is string {
|
|
9
7
|
return !!str && typeof str === 'string';
|
|
10
8
|
}
|
|
11
9
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
10
|
+
type StartOptions = PseudoLocalizeStringOptions & {
|
|
11
|
+
/**
|
|
12
|
+
* Node tags to ignore in pseudo-localization.
|
|
13
|
+
*
|
|
14
|
+
* @default ['STYLE']
|
|
15
|
+
*/
|
|
16
|
+
blacklistedNodeNames?: string[];
|
|
17
|
+
/**
|
|
18
|
+
* The element to pseudo-localize text within.
|
|
19
|
+
*
|
|
20
|
+
* @default document.body
|
|
21
|
+
*/
|
|
22
|
+
root?: Node;
|
|
23
|
+
};
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
* @typedef {StartOnlyOptions & PseudoLocalizeStringOptions} StartOptions
|
|
27
|
-
*/
|
|
25
|
+
type StopFn = () => void;
|
|
28
26
|
|
|
29
27
|
/**
|
|
30
28
|
* A container for pseudo-localization of the DOM.
|
|
@@ -35,21 +33,20 @@ function isNonEmptyString(str) {
|
|
|
35
33
|
export class PseudoLocalizeDom {
|
|
36
34
|
/**
|
|
37
35
|
* Indicates which elements the pseudo-localization is currently running on.
|
|
38
|
-
*
|
|
39
|
-
* @type {WeakSet<Node>}
|
|
40
36
|
*/
|
|
41
|
-
#enabledOn = new WeakSet();
|
|
37
|
+
#enabledOn = new WeakSet<Node>();
|
|
42
38
|
|
|
43
39
|
/**
|
|
44
|
-
*
|
|
45
|
-
*
|
|
40
|
+
* Start pseudo-localizing the DOM.
|
|
41
|
+
*
|
|
42
|
+
* Returns a stop function to disable pseudo-localization.
|
|
46
43
|
*/
|
|
47
44
|
start({
|
|
48
45
|
strategy = 'accented',
|
|
49
46
|
blacklistedNodeNames = ['STYLE'],
|
|
50
47
|
root,
|
|
51
|
-
} = {}) {
|
|
52
|
-
|
|
48
|
+
}: StartOptions = {}): StopFn {
|
|
49
|
+
const rootEl = root ?? document.body;
|
|
53
50
|
|
|
54
51
|
if (this.#enabledOn.has(rootEl)) {
|
|
55
52
|
console.warn(
|
|
@@ -65,10 +62,7 @@ export class PseudoLocalizeDom {
|
|
|
65
62
|
subtree: true,
|
|
66
63
|
};
|
|
67
64
|
|
|
68
|
-
|
|
69
|
-
* @param {Node} node
|
|
70
|
-
*/
|
|
71
|
-
const textNodesUnder = (node) => {
|
|
65
|
+
const textNodesUnder = (node: Node) => {
|
|
72
66
|
const walker = document.createTreeWalker(
|
|
73
67
|
node,
|
|
74
68
|
NodeFilter.SHOW_TEXT,
|
|
@@ -96,12 +90,9 @@ export class PseudoLocalizeDom {
|
|
|
96
90
|
return textNodes;
|
|
97
91
|
};
|
|
98
92
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
const pseudoLocalize = (element) => {
|
|
103
|
-
const textNodesUnderElement = textNodesUnder(element);
|
|
104
|
-
for (let textNode of textNodesUnderElement) {
|
|
93
|
+
const pseudoLocalize = (node: Node) => {
|
|
94
|
+
const textNodesUnderElement = textNodesUnder(node);
|
|
95
|
+
for (const textNode of textNodesUnderElement) {
|
|
105
96
|
const nodeValue = textNode.nodeValue;
|
|
106
97
|
if (isNonEmptyString(nodeValue)) {
|
|
107
98
|
textNode.nodeValue = pseudoLocalizeString(nodeValue, { strategy });
|
|
@@ -115,7 +106,7 @@ export class PseudoLocalizeDom {
|
|
|
115
106
|
// Start observing the DOM for changes and run
|
|
116
107
|
// pseudo localization on any added text nodes
|
|
117
108
|
const observer = new MutationObserver((mutationsList) => {
|
|
118
|
-
for (
|
|
109
|
+
for (const mutation of mutationsList) {
|
|
119
110
|
if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
|
|
120
111
|
// Turn the observer off while performing dom manipulation to prevent
|
|
121
112
|
// infinite dom mutation callback loops
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import { strict as assert } from 'node:assert';
|
|
3
|
+
import { pseudoLocalizeString as localize } from './localize.ts';
|
|
4
|
+
|
|
5
|
+
test('accented', () => {
|
|
6
|
+
assert.equal(localize('abcd'), 'ȧȧƀƈḓ');
|
|
7
|
+
// aeou are duplicated
|
|
8
|
+
assert.equal(localize('aeou'), 'ȧȧḗḗǿǿŭŭ');
|
|
9
|
+
assert.equal(localize('foo bar'), 'ƒǿǿǿǿ ƀȧȧř');
|
|
10
|
+
assert.equal(localize('CAPITAL_LETTERS'), 'ƇȦȦƤĪŦȦȦĿ_ĿḖḖŦŦḖḖŘŞ');
|
|
11
|
+
// Everything except ASCII alphabet is passed through
|
|
12
|
+
assert.equal(localize('123,. n -~ðÞ'), '123,. ƞ -~ðÞ');
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
test('bidi', () => {
|
|
16
|
+
/**
|
|
17
|
+
* @type {{ strategy: 'bidi' }}
|
|
18
|
+
*/
|
|
19
|
+
const bidi = {
|
|
20
|
+
strategy: 'bidi',
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* There are invisivle unicode formatting marks
|
|
25
|
+
* surrounding each output. For example
|
|
26
|
+
*
|
|
27
|
+
* "ɐqɔp" is actually "\u202e" + "ɐqɔp" + "\u202c"
|
|
28
|
+
*
|
|
29
|
+
* The presense of the formatting marks cause most UIs
|
|
30
|
+
* (likely including your text editor) to render the
|
|
31
|
+
* string backwards, if you will, or from right-to-left.
|
|
32
|
+
*/
|
|
33
|
+
assert.equal(localize('abcd', bidi), 'ɐqɔp');
|
|
34
|
+
assert.equal(localize('aeou', bidi), 'ɐǝon');
|
|
35
|
+
assert.equal(localize('foo bar', bidi), 'ɟoo qɐɹ');
|
|
36
|
+
assert.equal(localize('CAPITAL_LETTERS', bidi), 'Ↄ∀ԀI⊥∀⅂_⅂Ǝ⊥⊥ƎᴚS');
|
|
37
|
+
assert.equal(localize('123,. n -~ðÞ', bidi), '123,. u -~ðÞ');
|
|
38
|
+
// formatting marks are not duplicated if already present
|
|
39
|
+
assert.equal(localize('ɟoo', bidi), 'ɟoo');
|
|
40
|
+
});
|
|
@@ -35,41 +35,45 @@ const strategies = {
|
|
|
35
35
|
},
|
|
36
36
|
};
|
|
37
37
|
|
|
38
|
-
|
|
39
|
-
* @typedef {'accented' | 'bidi'} Strategy
|
|
40
|
-
*/
|
|
38
|
+
type Strategy = 'accented' | 'bidi';
|
|
41
39
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
40
|
+
type PseudoLocalizeStringOptions = {
|
|
41
|
+
/**
|
|
42
|
+
* The strategy to employ.
|
|
43
|
+
* - `accented`:
|
|
44
|
+
* In Accented English all Latin letters are replaced by accented
|
|
45
|
+
* Unicode counterparts which don't impair the readability of the content.
|
|
46
|
+
* This allows developers to quickly test if any given string is being
|
|
47
|
+
* correctly displayed in its 'translated' form. Additionally, simple
|
|
48
|
+
* heuristics are used to make certain words longer to better simulate the
|
|
49
|
+
* experience of international users.
|
|
50
|
+
* - `bidi`:
|
|
51
|
+
* Bidi English is a fake [RTL](https://developer.mozilla.org/en-US/docs/Glossary/rtl) locale.
|
|
52
|
+
* All words are surrounded by
|
|
53
|
+
* Unicode formatting marks forcing the RTL directionality of characters.
|
|
54
|
+
* In addition, to make the reversed text easier to read, individual
|
|
55
|
+
* letters are flipped.
|
|
56
|
+
*/
|
|
57
|
+
strategy?: Strategy;
|
|
58
|
+
};
|
|
56
59
|
|
|
57
60
|
/**
|
|
58
61
|
* Pseudo-localizes a string using a specified strategy.
|
|
59
|
-
*
|
|
60
|
-
* @
|
|
61
|
-
*
|
|
62
|
+
*
|
|
63
|
+
* @example
|
|
64
|
+
* pseudoLocalizeString('orange'); // 'ǿǿřȧȧƞɠḗḗ'
|
|
65
|
+
* pseudoLocalizeString('orange', {strategy: 'bidi'}); // 'ǝƃuɐɹo'
|
|
62
66
|
*/
|
|
63
67
|
export const pseudoLocalizeString = (
|
|
64
|
-
string,
|
|
65
|
-
{ strategy = 'accented' } = {}
|
|
68
|
+
string: string,
|
|
69
|
+
{ strategy = 'accented' }: PseudoLocalizeStringOptions = {}
|
|
66
70
|
) => {
|
|
67
|
-
|
|
71
|
+
const opts = strategies[strategy];
|
|
68
72
|
|
|
69
73
|
let pseudoLocalizedText = '';
|
|
70
|
-
for (
|
|
74
|
+
for (const character of string) {
|
|
71
75
|
if (character in opts.map) {
|
|
72
|
-
const char =
|
|
76
|
+
const char = character as keyof typeof opts.map;
|
|
73
77
|
const cl = char.toLowerCase();
|
|
74
78
|
// duplicate "a", "e", "o" and "u" to emulate ~30% longer text
|
|
75
79
|
if (
|