pseudo-localization 3.0.4 → 3.1.1

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 CHANGED
@@ -37,7 +37,7 @@ Pseudo-localization helps detect issues such as:
37
37
  npm install pseudo-localization
38
38
  ```
39
39
 
40
- ### Manual Installation
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 ./path/to/file.json
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 file or STDIN
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,2 @@
1
+ #! /usr/bin/env node
2
+ export {};
@@ -0,0 +1,74 @@
1
+ #! /usr/bin/env node
2
+ import { pseudoLocalizeString } from "../localize.js";
3
+ function isStrategy(str) {
4
+ return ['accented', 'bidi'].includes(str);
5
+ }
6
+ function parseArgs(argv) {
7
+ const options = {
8
+ string: '',
9
+ strategy: 'accented', // Default strategy
10
+ };
11
+ const args = argv.slice(2); // Skip node and script name
12
+ let i = 0;
13
+ while (i < args.length) {
14
+ const arg = args[i];
15
+ if (arg === '--strategy' || arg === '-s') {
16
+ // Handle the `--strategy` or `-s` flag
17
+ const value = args[i + 1];
18
+ if (!isStrategy(value)) {
19
+ throw new Error(`Invalid value for --strategy. Choose 'accented' or 'bidi'.`);
20
+ }
21
+ options.strategy = value;
22
+ i += 2; // Skip the flag and its value
23
+ }
24
+ else if (!arg.startsWith('--') && !arg.startsWith('-')) {
25
+ // Assume it's the positional `string` argument
26
+ if (!options.string) {
27
+ options.string = arg;
28
+ }
29
+ else {
30
+ throw new Error('Unexpected additional argument: ' + arg);
31
+ }
32
+ i += 1;
33
+ }
34
+ else {
35
+ throw new Error(`Unknown argument: ${arg}`);
36
+ }
37
+ }
38
+ if (!options.string) {
39
+ throw new Error('The <string> argument is required.');
40
+ }
41
+ return options;
42
+ }
43
+ function displayHelp() {
44
+ console.log(`
45
+ Usage: <script> <string> [options]
46
+
47
+ Pseudo localize a string
48
+
49
+ Arguments:
50
+ <string> String to pseudo-localize
51
+
52
+ Options:
53
+ --strategy, -s Set the strategy for localization (choices: 'accented', 'bidi', default: 'accented')
54
+ --help Show help
55
+ `);
56
+ }
57
+ function main() {
58
+ const argv = process.argv;
59
+ if (argv.includes('--help')) {
60
+ displayHelp();
61
+ process.exit(0);
62
+ }
63
+ try {
64
+ const { string, strategy } = parseArgs(argv);
65
+ process.stdout.write(pseudoLocalizeString(string, { strategy }));
66
+ }
67
+ catch (error) {
68
+ console.error(error);
69
+ displayHelp();
70
+ process.exit(1);
71
+ }
72
+ }
73
+ main();
74
+ //# 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,EAAE,oBAAoB,EAAiB,MAAM,gBAAgB,CAAC;AAOrE,SAAS,UAAU,CAAC,GAAW;IAC7B,OAAO,CAAC,UAAU,EAAE,MAAM,CAAC,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC;AAC5C,CAAC;AAED,SAAS,SAAS,CAAC,IAAc;IAC/B,MAAM,OAAO,GAAS;QACpB,MAAM,EAAE,EAAE;QACV,QAAQ,EAAE,UAAU,EAAE,mBAAmB;KAC1C,CAAC;IAEF,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,4BAA4B;IACxD,IAAI,CAAC,GAAG,CAAC,CAAC;IAEV,OAAO,CAAC,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC;QACvB,MAAM,GAAG,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC;QAEpB,IAAI,GAAG,KAAK,YAAY,IAAI,GAAG,KAAK,IAAI,EAAE,CAAC;YACzC,uCAAuC;YACvC,MAAM,KAAK,GAAG,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;YAC1B,IAAI,CAAC,UAAU,CAAC,KAAK,CAAC,EAAE,CAAC;gBACvB,MAAM,IAAI,KAAK,CACb,4DAA4D,CAC7D,CAAC;YACJ,CAAC;YACD,OAAO,CAAC,QAAQ,GAAG,KAAK,CAAC;YACzB,CAAC,IAAI,CAAC,CAAC,CAAC,8BAA8B;QACxC,CAAC;aAAM,IAAI,CAAC,GAAG,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;YACzD,+CAA+C;YAC/C,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE,CAAC;gBACpB,OAAO,CAAC,MAAM,GAAG,GAAG,CAAC;YACvB,CAAC;iBAAM,CAAC;gBACN,MAAM,IAAI,KAAK,CAAC,kCAAkC,GAAG,GAAG,CAAC,CAAC;YAC5D,CAAC;YACD,CAAC,IAAI,CAAC,CAAC;QACT,CAAC;aAAM,CAAC;YACN,MAAM,IAAI,KAAK,CAAC,qBAAqB,GAAG,EAAE,CAAC,CAAC;QAC9C,CAAC;IACH,CAAC;IAED,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE,CAAC;QACpB,MAAM,IAAI,KAAK,CAAC,oCAAoC,CAAC,CAAC;IACxD,CAAC;IAED,OAAO,OAAO,CAAC;AACjB,CAAC;AAED,SAAS,WAAW;IAClB,OAAO,CAAC,GAAG,CAAC;;;;;;;;;;;CAWb,CAAC,CAAC;AACH,CAAC;AAED,SAAS,IAAI;IACX,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;IAE1B,IAAI,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAC;QAC5B,WAAW,EAAE,CAAC;QACd,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;IAED,IAAI,CAAC;QACH,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,GAAG,SAAS,CAAC,IAAI,CAAC,CAAC;QAC7C,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,oBAAoB,CAAC,MAAM,EAAE,EAAE,QAAQ,EAAE,CAAC,CAAC,CAAC;IACnE,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;QACrB,WAAW,EAAE,CAAC;QACd,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;AACH,CAAC;AAED,IAAI,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
@@ -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;
@@ -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.4",
3
+ "version": "3.1.1",
4
4
  "description": "pseudo-localization for internationalization testing",
5
5
  "files": [
6
- "bin",
7
- "src/localize.mjs",
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
- "import": "./src/localize.mjs"
13
+ "types": "./dist/localize.d.ts",
14
+ "import": "./dist/localize.js"
15
15
  },
16
16
  "./dom": {
17
- "import": "./src/dom.mjs"
17
+ "types": "./dist/dom.d.ts",
18
+ "import": "./dist/dom.js"
18
19
  }
19
20
  },
20
21
  "bin": {
21
- "pseudo-localization": "./bin/pseudo-localize.mjs"
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
- "check-formatting": "prettier --list-different src/**/*.mjs",
29
- "format": "prettier --write src/**/*.mjs",
30
- "lint": "eslint src/**/*.mjs",
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": [
@@ -58,8 +59,5 @@
58
59
  "prettier": "^3.4.2",
59
60
  "typescript": "^5.7.2",
60
61
  "typescript-eslint": "^8.18.0"
61
- },
62
- "dependencies": {
63
- "yargs": "^17.7.2"
64
62
  }
65
63
  }
@@ -0,0 +1,89 @@
1
+ #! /usr/bin/env node
2
+
3
+ import { pseudoLocalizeString, type Strategy } from '../localize.ts';
4
+
5
+ type Args = {
6
+ string: string;
7
+ strategy: 'accented' | 'bidi';
8
+ };
9
+
10
+ function isStrategy(str: string): str is Strategy {
11
+ return ['accented', 'bidi'].includes(str);
12
+ }
13
+
14
+ function parseArgs(argv: string[]): Args {
15
+ const options: Args = {
16
+ string: '',
17
+ strategy: 'accented', // Default strategy
18
+ };
19
+
20
+ const args = argv.slice(2); // Skip node and script name
21
+ let i = 0;
22
+
23
+ while (i < args.length) {
24
+ const arg = args[i];
25
+
26
+ if (arg === '--strategy' || arg === '-s') {
27
+ // Handle the `--strategy` or `-s` flag
28
+ const value = args[i + 1];
29
+ if (!isStrategy(value)) {
30
+ throw new Error(
31
+ `Invalid value for --strategy. Choose 'accented' or 'bidi'.`
32
+ );
33
+ }
34
+ options.strategy = value;
35
+ i += 2; // Skip the flag and its value
36
+ } else if (!arg.startsWith('--') && !arg.startsWith('-')) {
37
+ // Assume it's the positional `string` argument
38
+ if (!options.string) {
39
+ options.string = arg;
40
+ } else {
41
+ throw new Error('Unexpected additional argument: ' + arg);
42
+ }
43
+ i += 1;
44
+ } else {
45
+ throw new Error(`Unknown argument: ${arg}`);
46
+ }
47
+ }
48
+
49
+ if (!options.string) {
50
+ throw new Error('The <string> argument is required.');
51
+ }
52
+
53
+ return options;
54
+ }
55
+
56
+ function displayHelp() {
57
+ console.log(`
58
+ Usage: <script> <string> [options]
59
+
60
+ Pseudo localize a string
61
+
62
+ Arguments:
63
+ <string> String to pseudo-localize
64
+
65
+ Options:
66
+ --strategy, -s Set the strategy for localization (choices: 'accented', 'bidi', default: 'accented')
67
+ --help Show help
68
+ `);
69
+ }
70
+
71
+ function main() {
72
+ const argv = process.argv;
73
+
74
+ if (argv.includes('--help')) {
75
+ displayHelp();
76
+ process.exit(0);
77
+ }
78
+
79
+ try {
80
+ const { string, strategy } = parseArgs(argv);
81
+ process.stdout.write(pseudoLocalizeString(string, { strategy }));
82
+ } catch (error) {
83
+ console.error(error);
84
+ displayHelp();
85
+ process.exit(1);
86
+ }
87
+ }
88
+
89
+ main();
@@ -1,30 +1,28 @@
1
- import { pseudoLocalizeString } from './localize.mjs';
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
- * @typedef {import("./localize.mjs").PseudoLocalizeStringOptions} PseudoLocalizeStringOptions
14
- */
15
-
16
- /**
17
- * @typedef {Object} StartOnlyOptions
18
- * @property {string[]} [blacklistedNodeNames]
19
- * Node tags to ignore in pseudo-localization
20
- * @property {Node} [root]
21
- * The element to pseudo-localize text within.
22
- * Default: `document.body`
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
- * @param {StartOptions} options
45
- * @returns {() => void} stop
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
- let rootEl = root ?? document.body;
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
- * @param {Node} element
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 (let mutation of mutationsList) {
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
+ export type Strategy = 'accented' | 'bidi';
41
39
 
42
- /**
43
- * @typedef {Object} PseudoLocalizeStringOptions
44
- * @property {Strategy} [strategy] The strategy to employ.
45
- * - `accented`: In Accented English all Latin letters are replaced by accented
46
- * Unicode counterparts which don't impair the readability of the content.
47
- * This allows developers to quickly test if any given string is being
48
- * correctly displayed in its 'translated' form. Additionally, simple
49
- * heuristics are used to make certain words longer to better simulate the
50
- * experience of international users.
51
- * - `bidi`: Bidi English is a fake [RTL](https://developer.mozilla.org/en-US/docs/Glossary/rtl) locale. All words are surrounded by
52
- Unicode formatting marks forcing the RTL directionality of characters.
53
- In addition, to make the reversed text easier to read, individual
54
- letters are flipped.
55
- */
40
+ export 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
- * @param {string} string - The string to pseudo-localize.
60
- * @param {PseudoLocalizeStringOptions} options - Options for pseudo-localization.
61
- * @returns {string} The pseudo-localized string.
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' } = {}
66
- ) => {
67
- let opts = strategies[strategy];
68
+ string: string,
69
+ { strategy = 'accented' }: PseudoLocalizeStringOptions = {}
70
+ ): string => {
71
+ const opts = strategies[strategy];
68
72
 
69
73
  let pseudoLocalizedText = '';
70
- for (let character of string) {
74
+ for (const character of string) {
71
75
  if (character in opts.map) {
72
- const char = /** @type {keyof typeof opts.map} */ (character);
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 (
@@ -1,31 +0,0 @@
1
- #! /usr/bin/env node
2
-
3
- import yargs from 'yargs/yargs';
4
- import { hideBin } from 'yargs/helpers';
5
- import { pseudoLocalizeString } from '../src/localize.mjs';
6
-
7
- yargs(hideBin(process.argv))
8
- .usage(
9
- '$0 <string> [options]',
10
- 'Pseudo localize a string',
11
- (yargs) => {
12
- return yargs
13
- .positional('string', {
14
- describe: 'String to pseudo-localize',
15
- type: 'string',
16
- })
17
- .option('strategy', {
18
- alias: 's',
19
- type: 'string',
20
- describe: 'Set the strategy for localization',
21
- choices: ['accented', 'bidi'],
22
- default: 'accented',
23
- });
24
- },
25
- ({ string, strategy }) => {
26
- process.stdout.write(pseudoLocalizeString(string, { strategy }));
27
- }
28
- )
29
- .help()
30
- .version()
31
- .parse();