pseudo-localization 3.0.0 → 3.0.3

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
@@ -1,248 +1,183 @@
1
- <sub>Inspired by pseudo-localization at [Netflix](https://medium.com/netflix-techblog/pseudo-localization-netflix-12fff76fbcbe) and [Firefox](https://reviewboard.mozilla.org/r/248606/diff/2#index_header)</sub>
1
+ <sub>Inspired by pseudo-localization at [Netflix](https://medium.com/netflix-techblog/pseudo-localization-netflix-12fff76fbcbe) and [Firefox](https://reviewboard.mozilla.org/r/248606/diff/2#index_header).</sub>
2
2
 
3
- # pseudo-localization
3
+ # Pseudo-Localization
4
4
 
5
- | English | Pseudo Language |
6
- | ------------- | ------------- |
7
- | <img width="559" alt="screen shot 2018-08-12 at 1 23 18 pm" src="https://user-images.githubusercontent.com/2373958/44001651-21f32b42-9e36-11e8-80eb-5b88e8fd9b13.png"> | <img width="661" alt="after" src="https://user-images.githubusercontent.com/2373958/44311352-2a29fb00-a3e6-11e8-88ed-5485697f7a40.png"> |
5
+ Pseudo-localization helps developers test UI elements for localization issues before actual translations are available. This package transforms text into a pseudo-language to simulate real-world localization challenges.
6
+
7
+ ## Preview
8
+
9
+ | English | Pseudo Language |
10
+ | ----------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
11
+ | ![English example](https://user-images.githubusercontent.com/2373958/44001651-21f32b42-9e36-11e8-80eb-5b88e8fd9b13.png) | ![Pseudo-localized example](https://user-images.githubusercontent.com/2373958/44311352-2a29fb00-a3e6-11e8-88ed-5485697f7a40.png) |
12
+
13
+ ### [Live Demo](https://tryggvigy.github.io/pseudo-localization/hamlet.html)
14
+
15
+ See it in action on [https://tryggvigy.github.io/pseudo-localization/hamlet.html](https://tryggvigy.github.io/pseudo-localization/hamlet.html)
16
+
17
+ Changes to the DOM trigger pseudo-localization in real time. Try modifying text nodes or adding/removing elements via DevTools.
8
18
 
9
19
  ---
10
20
 
11
- [![Edit pseudo-localization-react](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/1q7n08or4j)
21
+ ## Why Use This?
22
+
23
+ Pseudo-localization helps detect issues such as:
12
24
 
13
- `pseudo-localization` is a script that performs [pseudolocalization](https://en.wikipedia.org/wiki/Pseudolocalization) against the DOM or individual strings.
25
+ - **Text Overflow** Translations are often longer than the original text, which may cause UI breaking.
26
+ - **Font Rendering** – Certain languages have larger glyphs or diacritics that may be cut off.
27
+ - **Right-to-Left (RTL) Support** – Ensures proper layout handling for RTL languages.
28
+ - **Hardcoded Strings** – Identifies untranslated or hardcoded text that should be localized.
14
29
 
15
- [Demo here](https://tryggvigy.github.io/pseudo-localization/hamlet.html). Changing text nodes and adding or removing trees of elements will trigger a pseudo-localization run on all the new text added to the DOM. Try it yourself by changing text or adding/removing elements using the devtools.
30
+ ---
16
31
 
17
32
  ## Installation
18
33
 
19
- ### Through npm
20
- ```
34
+ ### Via npm
35
+
36
+ ```sh
21
37
  npm install pseudo-localization
22
38
  ```
23
39
 
24
- ### Raw script (without npm)
25
- Copy paste the files in [`src`](https://github.com/tryggvigy/pseudo-localization/blob/master/src) and use as you wish. It's not a lot of code.
26
-
40
+ ### Manual Installation
27
41
 
28
- # Usage
42
+ Copy the files from [`src`](https://github.com/tryggvigy/pseudo-localization/blob/master/src) and use them directly.
29
43
 
30
- ### import or require the npm module
31
-
32
- `pseudo-localization` can be used like so:
33
-
34
- ```js
35
- import pseudoLocalization from 'pseudo-localization';
36
- // Or using CommonJS
37
- // const pseudoLocalization = require('pseudo-localization').default;
44
+ ---
38
45
 
39
- pseudoLocalization.start();
46
+ ## Usage
40
47
 
41
- pseudoLocalization.isEnabled() // true
48
+ ### `pseudoLocalizeString`
42
49
 
43
- // Later, if needed
44
- pseudoLocalization.stop();
50
+ Transform individual strings:
45
51
 
46
- pseudoLocalization.isEnabled() // false
52
+ ```js
53
+ import { pseudoLocalizeString } from 'pseudo-localization';
47
54
 
55
+ console.log(pseudoLocalizeString('hello')); // ħḗḗŀŀǿǿ
56
+ console.log(pseudoLocalizeString('hello', { strategy: 'bidi' })); // oʅʅǝɥ
48
57
  ```
49
58
 
50
- To use `pseudo-localization` in React, Vue, Ember or anything else, hook the `start` and `stop` methods into your libraries
51
- component lifecycles. In React for example:
59
+ Use-case: Ensure text is passing through a translation function.
52
60
 
53
61
  ```js
54
- import React from 'react';
55
- import pseudoLocalization from 'pseudo-localization';
56
-
57
- class PseudoLocalization extends React.Component {
58
- componentDidMount() {
59
- pseudoLocalization.start();
60
- }
61
- componentWillUnmount() {
62
- pseudoLocalization.stop();
63
- }
64
- }
62
+ import translate from './my-translation-lib';
63
+ const _ = (key) => pseudoLocalizeString(translate(key, navigator.language));
65
64
 
66
- // And use it
67
-
68
- class Page extends React.Component {
69
- render() {
70
- return (
71
- <main>
72
- <PseudoLocalization />
73
- <h1>I will get pseudo localized along with everything else under document.body!</h1>
74
- <main>
75
- );
76
- }
77
- }
65
+ console.log(_('Some Localized Text')); // Şǿǿḿḗḗ Ŀǿǿƈȧȧŀīẑḗḗḓ Ŧḗḗẋŧ
78
66
  ```
79
67
 
80
- Using hooks? Here's an example:
68
+ ---
81
69
 
82
- ```jsx
83
- import React from 'react';
84
- import pseudoLocalization from 'pseudo-localization';
70
+ ### `pseudo-localization/dom`
85
71
 
86
- function PseudoLocalization() {
87
- React.useEffect(() => {
88
- pseudoLocalization.start();
72
+ Automatically localize the entire page or parts of the DOM.
89
73
 
90
- return () => {
91
- pseudoLocalization.stop()
92
- };
93
- }, []);
94
- }
74
+ #### React Example
95
75
 
96
- // And use it
76
+ ```jsx
77
+ import React, { useEffect } from 'react';
78
+ import { PseudoLocalizeDom } from 'pseudo-localization/dom';
97
79
 
98
80
  function Page() {
99
- return (
100
- <main>
101
- <PseudoLocalization />
102
- <h1>I will get pseudo localized along with everything else under document.body!</h1>
103
- <main>
104
- );
81
+ useEffect(() => PseudoLocalizeDom.start(), []);
82
+ return <h1>This text will be pseudo-localized!</h1>;
105
83
  }
106
84
  ```
107
85
 
108
- You can also call the underlying `localize` function to pseudo-localize any string. This is useful for non-browser environments like nodejs.
86
+ ---
109
87
 
88
+ ## Strategies
110
89
 
111
- ```js
112
- import { localize } from 'pseudo-localization';
113
- // Or using CommonJS
114
- // const { localize } = require('pseudo-localization');
90
+ Pseudo-localization supports two strategies:
115
91
 
116
- console.log(localize('hello')); // --> ħḗḗŀŀǿǿ
117
- console.log(localize('hello', { strategy: 'bidi' })); // --> oʅʅǝɥ
118
- ```
92
+ ### 1. Accented (`accented`)
119
93
 
120
- A good use-case for `localize` is testing that strings are _actually_ being localized and not hard coded.
94
+ Expands text and replaces Latin letters with accented Unicode counterparts.
121
95
 
122
96
  ```js
123
- import { localize } from 'pseudo-localization';
124
- import translate from './my-translation-lib';
125
-
126
- // Pseudo localize every string returned from your normal translation function.
127
- const _ = key => localize(translate(key, navigator.language));
128
-
129
- _('Some Localized Text'); // Şǿǿḿḗḗ Ŀǿǿƈȧȧŀīẑḗḗḓ Ŧḗḗẋŧ
130
- // Or, in React for example
131
- const Header = () => <h1>{_('Localized Header Text')}</h1>;
97
+ pseudoLocalization.start({ strategy: 'accented' });
132
98
  ```
133
99
 
134
- Any strings that do not pass through the translation function will now stand out in the UI because the will not be pseudo-localized.
135
-
136
- ## Strategies
137
- `pseudo-localization` supports two strategies:
138
-
139
- 1. accented
140
- 2. bidi
141
-
142
- ### accented - Ȧȧƈƈḗḗƞŧḗḗḓ Ḗḗƞɠŀīīşħ
100
+ Example output: **Ȧȧƈƈḗḗƞŧḗḗḓ Ḗḗƞɠŀīīşħ**
143
101
 
144
- Usage: `pseudoLocalization.start({ strategy: 'accented' });` or simply `pseudoLocalization.start();`.
145
-
146
- In Accented English all Latin letters are replaced by accented
147
- Unicode counterparts which don't impair the readability of the content.
148
- This allows developers to quickly test if any given string is being
149
- correctly displayed in its 'translated' form. Additionally, simple
150
- heuristics are used to make certain words longer to better simulate the
151
- experience of international users.
152
-
153
- <img width="622" alt="screen shot 2018-08-19 at 18 48 29" src="https://user-images.githubusercontent.com/2373958/44311259-62303e80-a3e4-11e8-884a-54c77416b922.png">
102
+ ![Accented example](https://user-images.githubusercontent.com/2373958/44311259-62303e80-a3e4-11e8-884a-54c77416b922.png)
154
103
 
104
+ ---
155
105
 
156
- ### bidi - ɥsıʅƃuƎ ıpıԐ
106
+ ### 2. Bidirectional (`bidi`)
157
107
 
158
- Usage: `pseudoLocalization.start({ strategy: 'bidi' });`.
108
+ Simulates an RTL language by reversing words and using right-to-left Unicode formatting.
159
109
 
160
- Bidi English is a fake [RTL](https://developer.mozilla.org/en-US/docs/Glossary/rtl) locale. All words are surrounded by
161
- Unicode formatting marks forcing the RTL directionality of characters.
162
- In addition, to make the reversed text easier to read, individual
163
- letters are flipped.
110
+ ```js
111
+ pseudoLocalization.start({ strategy: 'bidi' });
112
+ ```
164
113
 
165
- <img width="602" alt="screen shot 2018-08-19 at 18 45 49" src="https://user-images.githubusercontent.com/2373958/44311263-770cd200-a3e4-11e8-97e4-9a1896bd5975.png">
114
+ Example output: **ɥsıʅƃuƎ ıpıԐ**
166
115
 
116
+ ![Bidi example](https://user-images.githubusercontent.com/2373958/44311263-770cd200-a3e4-11e8-97e4-9a1896bd5975.png)
167
117
 
168
- ## Why?
169
- To catch localization problems like:
170
- - Translated text that is significantly longer than the source language, and does not fit within the UI constraints, or which causes text breaks at awkward positions.
171
- - Font glyphs that are significantly larger than, or possess diacritic marks not found in, the source language, and which may be cut off vertically.
172
- - Languages for which the reading order is not left-to-right, which is especially problematic for user input.
173
- - Application code that assumes all characters fit into a limited character set, such as ASCII or ANSI, which can produce actual logic bugs if left uncaught.
118
+ ---
174
119
 
175
- In addition, the pseudo-localization process may uncover places where an element should be localizable, but is hard coded in a source language.
120
+ ## API Reference
176
121
 
177
- ## Docs
178
- `pseudo-localization` exports three functions.
179
- - `pseudoLocalization.start(options)`
180
- - `pseudoLocalization.stop()`
181
- - `pseudoLocalization.localize(string, options)`
122
+ ### `pseudoLocalizeString(str: string, options?: Options): string`
182
123
 
183
- ### `pseudoLocalization.start(options)`
184
- Pseudo localizes the page and watched the DOM for additions/updates to continuously pseudo localize new content.
124
+ - `str`: String to localize.
125
+ - `options.strategy`: `'accented'` (default) or `'bidi'`.
185
126
 
186
- Accepts an `options` object as an argument. Here are the keys in the `options` object.
127
+ ### `PseudoLocalizeDom.start(options?: DomOptions): StopFn`
187
128
 
188
- #### `strategy` - default (`'accented'`)
189
- The pseudo localization strategy to use when transforming strings. Accepted values are `accented` or `bidi`.
129
+ Pseudo-localizes the page and watches for DOM changes.
190
130
 
191
- #### `blacklistedNodeNames` - default (`['STYLE']`)
192
- An array of [Node.nodeName](https://developer.mozilla.org/en-US/docs/Web/API/Node/nodeName) strings that will be ignored when localizing. This is useful for skipping `<style>`, `<text>` svg nodes or other nodes that potentially doesn't make sense to apply pseudo localization to. `<style>` is skipped by default when `blacklistedNodeNames` is not provided.
131
+ ```js
132
+ import { PseudoLocalizeDom } from 'pseudo-localization/dom';
133
+ const stop = new PseudoLocalizeDom().start();
134
+ // Stop pseudo-localization later
135
+ stop();
136
+ ```
193
137
 
194
- ### `pseudoLocalization.stop()`
195
- Stops watching the DOM for additions/updates to continuously pseudo localize new content.
138
+ #### `DomOptions`
196
139
 
197
- ### `pseudoLocalization.localize(string, options)`
198
- Accepts a string to apply pseudo localization to. Returns the pseudo localized version on the string.
199
- This function is used by `pseudoLocalization.start` internally.
140
+ - `strategy`: `'accented'` or `'bidi'`.
141
+ - `blacklistedNodeNames`: Nodes to ignore (default: `['STYLE']`).
142
+ - `root`: Root element for localization (default: `document.body`).
200
143
 
201
- Accepts an `options` object as an argument. Here are the keys in the `options` object.
144
+ ---
202
145
 
203
- #### `strategy` - default (`'accented'`)
204
- The pseudo localization strategy to use when transforming strings. Accepted values are `accented` or `bidi`.
146
+ ## CLI Usage
205
147
 
206
- ## CLI Interface
207
- For easy scripting a CLI interface is exposed. The interface supports raw input, JSON files, and CommonJS modules.
148
+ A command-line interface (CLI) is available for quick testing and automation.
208
149
 
209
- ```bash
150
+ ```sh
210
151
  npx pseudo-localization ./path/to/file.json
211
152
 
212
- # pass in a JS transpiled ES module or an exported CJS module
213
- npx pseudo-localization ./path/to/file
153
+ # Pipe a string
154
+ echo "hello world" | npx pseudo-localization
214
155
 
215
- # pass in JSON files through STDIN
216
- cat ./path/to/file.json | npx pseudo-localization --strategy bidi
217
-
218
- # pass a string via a pipe
219
- echo hello world | npx pseudo-localization
220
-
221
- # direct input pseudo-localization
156
+ # Direct input
222
157
  npx pseudo-localization -i "hello world"
223
158
  ```
224
159
 
225
- CLI Options:
160
+ ### CLI Options
226
161
 
227
162
  ```
228
163
  pseudo-localization [src] [options]
229
164
 
230
- Pseudo localize a string, JSON file, or a JavaScript object
231
-
232
165
  Positionals:
233
- src The source as a path or from STDIN [string]
166
+ src Input file or STDIN
234
167
 
235
168
  Options:
236
- -o, --output Writes output to STDOUT by default. Optionally specify a JSON
237
- file to write the pseudo-localizations to [string]
238
- -i, --input Pass in direct input to pseudo-localization [string]
239
- --debug Print out all stack traces and other debug info [boolean]
240
- --pretty Pretty print JSON output [boolean]
241
- --strategy Set the strategy for localization
242
- [choices: "accented", "bidi"] [default: "accented"]
243
- --help Show help [boolean]
244
- --version Show version number [boolean]
169
+ -o, --output Output file (defaults to STDOUT)
170
+ --strategy Localization strategy (accented or bidi)
171
+ --help Show help
172
+ --version Show version
245
173
  ```
246
174
 
247
- ## Support
248
- Works in all evergreen browsers.
175
+ ---
176
+
177
+ ## Browser Compatibility
178
+
179
+ Works in all modern browsers.
180
+
181
+ ---
182
+
183
+ By using pseudo-localization, you can catch UI issues early, ensuring your app is truly localization-ready!
package/package.json CHANGED
@@ -1,10 +1,11 @@
1
1
  {
2
2
  "name": "pseudo-localization",
3
- "version": "3.0.0",
3
+ "version": "3.0.3",
4
4
  "description": "pseudo-localization for internationalization testing",
5
5
  "files": [
6
- "dist",
7
- "bin"
6
+ "bin",
7
+ "src/localize.mjs",
8
+ "src/dom.mjs"
8
9
  ],
9
10
  "main": "./src/localize.mjs",
10
11
  "type": "module",
@@ -12,22 +13,22 @@
12
13
  ".": {
13
14
  "import": "./src/localize.mjs"
14
15
  },
15
- "dom": {
16
+ "./dom": {
16
17
  "import": "./src/dom.mjs"
17
18
  }
18
19
  },
19
20
  "bin": {
20
- "pseudo-localization": "bin/pseudo-localize.mjs"
21
+ "pseudo-localization": "./bin/pseudo-localize.mjs"
21
22
  },
22
23
  "engines": {
23
24
  "node": "^23"
24
25
  },
25
26
  "scripts": {
26
27
  "start": "node devserver.mjs",
27
- "check-formatting": "prettier --list-different src/**/*.ts",
28
- "format": "prettier --write src/**/*.ts",
29
- "lint": "eslint src/**/*.js",
30
- "test-js": "jest src/*",
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",
31
32
  "test": "npm run test-js jest && npm run check-formatting && npm run check-types && npm run lint",
32
33
  "check-types": "tsc --noEmit",
33
34
  "tsc": "tsc"
package/src/dom.mjs ADDED
@@ -0,0 +1,165 @@
1
+ import { pseudoLocalizeString } from './localize.mjs';
2
+
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) {
9
+ return !!str && typeof str === 'string';
10
+ }
11
+
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
+ */
24
+
25
+ /**
26
+ * @typedef {StartOnlyOptions & PseudoLocalizeStringOptions} StartOptions
27
+ */
28
+
29
+ /**
30
+ * A container for pseudo-localization of the DOM.
31
+ *
32
+ * @example
33
+ * new PseudoLocalizeDom().start();
34
+ */
35
+ export class PseudoLocalizeDom {
36
+ /**
37
+ * Indicates which elements the pseudo-localization is currently running on.
38
+ *
39
+ * @type {WeakSet<Node>}
40
+ */
41
+ #enabledOn = new WeakSet();
42
+
43
+ /**
44
+ * @param {StartOptions} options
45
+ * @returns {() => void} stop
46
+ */
47
+ start({
48
+ strategy = 'accented',
49
+ blacklistedNodeNames = ['STYLE'],
50
+ root,
51
+ } = {}) {
52
+ let rootEl = root ?? document.body;
53
+
54
+ if (this.#enabledOn.has(rootEl)) {
55
+ console.warn(
56
+ `Start aborted. pseudo-localization is already enabled on`,
57
+ rootEl
58
+ );
59
+ return () => {};
60
+ }
61
+
62
+ const observerConfig = {
63
+ characterData: true,
64
+ childList: true,
65
+ subtree: true,
66
+ };
67
+
68
+ /**
69
+ * @param {Node} node
70
+ */
71
+ const textNodesUnder = (node) => {
72
+ const walker = document.createTreeWalker(
73
+ node,
74
+ NodeFilter.SHOW_TEXT,
75
+ (node) => {
76
+ const isAllWhitespace =
77
+ node.nodeValue && !/[^\s]/.test(node.nodeValue);
78
+ if (isAllWhitespace) {
79
+ return NodeFilter.FILTER_REJECT;
80
+ }
81
+
82
+ const isBlacklistedNode =
83
+ node.parentElement &&
84
+ blacklistedNodeNames.includes(node.parentElement.nodeName);
85
+ if (isBlacklistedNode) {
86
+ return NodeFilter.FILTER_REJECT;
87
+ }
88
+
89
+ return NodeFilter.FILTER_ACCEPT;
90
+ }
91
+ );
92
+
93
+ let currNode;
94
+ const textNodes = [];
95
+ while ((currNode = walker.nextNode())) textNodes.push(currNode);
96
+ return textNodes;
97
+ };
98
+
99
+ /**
100
+ * @param {Node} element
101
+ */
102
+ const pseudoLocalize = (element) => {
103
+ const textNodesUnderElement = textNodesUnder(element);
104
+ for (let textNode of textNodesUnderElement) {
105
+ const nodeValue = textNode.nodeValue;
106
+ if (isNonEmptyString(nodeValue)) {
107
+ textNode.nodeValue = pseudoLocalizeString(nodeValue, { strategy });
108
+ }
109
+ }
110
+ };
111
+
112
+ // Pseudo localize the DOM
113
+ pseudoLocalize(document.body);
114
+
115
+ // Start observing the DOM for changes and run
116
+ // pseudo localization on any added text nodes
117
+ const observer = new MutationObserver((mutationsList) => {
118
+ for (let mutation of mutationsList) {
119
+ if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
120
+ // Turn the observer off while performing dom manipulation to prevent
121
+ // infinite dom mutation callback loops
122
+ observer.disconnect();
123
+ // For every node added, recurse down it's subtree and convert
124
+ // all children as well
125
+ mutation.addedNodes.forEach(pseudoLocalize.bind(this));
126
+ observer.observe(document.body, observerConfig);
127
+ } else if (mutation.type === 'characterData') {
128
+ const nodeValue = mutation.target.nodeValue;
129
+ const isBlacklistedNode =
130
+ !!mutation.target.parentElement &&
131
+ blacklistedNodeNames.includes(
132
+ mutation.target.parentElement.nodeName
133
+ );
134
+ if (isNonEmptyString(nodeValue) && !isBlacklistedNode) {
135
+ // Turn the observer off while performing dom manipulation to prevent
136
+ // infinite dom mutation callback loops
137
+ observer.disconnect();
138
+ // The target will always be a text node so it can be converted
139
+ // directly
140
+ mutation.target.nodeValue = pseudoLocalizeString(nodeValue, {
141
+ strategy,
142
+ });
143
+ observer.observe(document.body, observerConfig);
144
+ }
145
+ }
146
+ }
147
+ });
148
+
149
+ observer.observe(document.body, observerConfig);
150
+ this.#enabledOn.add(rootEl);
151
+
152
+ const stop = () => {
153
+ if (!this.#enabledOn.has(rootEl)) {
154
+ console.warn(
155
+ 'Stop aborted. pseudo-localization is not running on',
156
+ rootEl
157
+ );
158
+ return;
159
+ }
160
+ observer.disconnect();
161
+ this.#enabledOn.delete(rootEl);
162
+ };
163
+ return stop;
164
+ }
165
+ }
@@ -0,0 +1,91 @@
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
+
11
+ /* prettier-ignore */
12
+ const BIDI_MAP = {
13
+ a: 'ɐ', A: '∀', b: 'q', B: 'Ԑ', c: 'ɔ', C: 'Ↄ', d: 'p', D: 'ᗡ', e: 'ǝ', E: 'Ǝ',
14
+ f: 'ɟ', F: 'Ⅎ', g: 'ƃ', G: '⅁', h: 'ɥ', H: 'H', i: 'ı', I: 'I', j: 'ɾ', J: 'ſ',
15
+ k: 'ʞ', K: 'Ӽ', l: 'ʅ', L: '⅂', m: 'ɯ', M: 'W', n: 'u', N: 'N', o: 'o', O: 'O',
16
+ p: 'd', P: 'Ԁ', q: 'b', Q: 'Ò', r: 'ɹ', R: 'ᴚ', s: 's', S: 'S', t: 'ʇ', T: '⊥',
17
+ u: 'n', U: '∩', v: 'ʌ', V: 'Ʌ', w: 'ʍ', W: 'M', x: 'x', X: 'X', y: 'ʎ', Y: '⅄',
18
+ z: 'z', Z: 'Z',
19
+ };
20
+
21
+ const strategies = {
22
+ accented: {
23
+ prefix: '',
24
+ postfix: '',
25
+ map: ACCENTED_MAP,
26
+ elongate: true,
27
+ },
28
+ bidi: {
29
+ // Surround words with Unicode formatting marks forcing
30
+ // right-to-left directionality of characters
31
+ prefix: '\u202e',
32
+ postfix: '\u202c',
33
+ map: BIDI_MAP,
34
+ elongate: false,
35
+ },
36
+ };
37
+
38
+ /**
39
+ * @typedef {'accented' | 'bidi'} Strategy
40
+ */
41
+
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
+ */
56
+
57
+ /**
58
+ * 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
+ export const pseudoLocalizeString = (
64
+ string,
65
+ { strategy = 'accented' } = {}
66
+ ) => {
67
+ let opts = strategies[strategy];
68
+
69
+ let pseudoLocalizedText = '';
70
+ for (let character of string) {
71
+ if (character in opts.map) {
72
+ const char = /** @type {keyof typeof opts.map} */ (character);
73
+ const cl = char.toLowerCase();
74
+ // duplicate "a", "e", "o" and "u" to emulate ~30% longer text
75
+ if (
76
+ opts.elongate &&
77
+ (cl === 'a' || cl === 'e' || cl === 'o' || cl === 'u')
78
+ ) {
79
+ pseudoLocalizedText += opts.map[char] + opts.map[char];
80
+ } else pseudoLocalizedText += opts.map[char];
81
+ } else pseudoLocalizedText += character;
82
+ }
83
+
84
+ // Add pre/postfix if the string does not already contain them respectively.
85
+ const prefix = pseudoLocalizedText.startsWith(opts.prefix) ? '' : opts.prefix;
86
+ const postfix = pseudoLocalizedText.endsWith(opts.postfix)
87
+ ? ''
88
+ : opts.postfix;
89
+
90
+ return prefix + pseudoLocalizedText + postfix;
91
+ };