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 +104 -169
- package/package.json +10 -9
- package/src/dom.mjs +165 -0
- package/src/localize.mjs +91 -0
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)
|
|
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
|
-
#
|
|
3
|
+
# Pseudo-Localization
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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
|
+
|  |  |
|
|
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
|
-
|
|
21
|
+
## Why Use This?
|
|
22
|
+
|
|
23
|
+
Pseudo-localization helps detect issues such as:
|
|
12
24
|
|
|
13
|
-
|
|
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
|
-
|
|
30
|
+
---
|
|
16
31
|
|
|
17
32
|
## Installation
|
|
18
33
|
|
|
19
|
-
###
|
|
20
|
-
|
|
34
|
+
### Via npm
|
|
35
|
+
|
|
36
|
+
```sh
|
|
21
37
|
npm install pseudo-localization
|
|
22
38
|
```
|
|
23
39
|
|
|
24
|
-
###
|
|
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
|
-
|
|
42
|
+
Copy the files from [`src`](https://github.com/tryggvigy/pseudo-localization/blob/master/src) and use them directly.
|
|
29
43
|
|
|
30
|
-
|
|
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
|
-
|
|
46
|
+
## Usage
|
|
40
47
|
|
|
41
|
-
|
|
48
|
+
### `pseudoLocalizeString`
|
|
42
49
|
|
|
43
|
-
|
|
44
|
-
pseudoLocalization.stop();
|
|
50
|
+
Transform individual strings:
|
|
45
51
|
|
|
46
|
-
|
|
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
|
-
|
|
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
|
|
55
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
68
|
+
---
|
|
81
69
|
|
|
82
|
-
|
|
83
|
-
import React from 'react';
|
|
84
|
-
import pseudoLocalization from 'pseudo-localization';
|
|
70
|
+
### `pseudo-localization/dom`
|
|
85
71
|
|
|
86
|
-
|
|
87
|
-
React.useEffect(() => {
|
|
88
|
-
pseudoLocalization.start();
|
|
72
|
+
Automatically localize the entire page or parts of the DOM.
|
|
89
73
|
|
|
90
|
-
|
|
91
|
-
pseudoLocalization.stop()
|
|
92
|
-
};
|
|
93
|
-
}, []);
|
|
94
|
-
}
|
|
74
|
+
#### React Example
|
|
95
75
|
|
|
96
|
-
|
|
76
|
+
```jsx
|
|
77
|
+
import React, { useEffect } from 'react';
|
|
78
|
+
import { PseudoLocalizeDom } from 'pseudo-localization/dom';
|
|
97
79
|
|
|
98
80
|
function Page() {
|
|
99
|
-
|
|
100
|
-
|
|
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
|
-
|
|
86
|
+
---
|
|
109
87
|
|
|
88
|
+
## Strategies
|
|
110
89
|
|
|
111
|
-
|
|
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
|
-
|
|
117
|
-
console.log(localize('hello', { strategy: 'bidi' })); // --> oʅʅǝɥ
|
|
118
|
-
```
|
|
92
|
+
### 1. Accented (`accented`)
|
|
119
93
|
|
|
120
|
-
|
|
94
|
+
Expands text and replaces Latin letters with accented Unicode counterparts.
|
|
121
95
|
|
|
122
96
|
```js
|
|
123
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+

|
|
154
103
|
|
|
104
|
+
---
|
|
155
105
|
|
|
156
|
-
###
|
|
106
|
+
### 2. Bidirectional (`bidi`)
|
|
157
107
|
|
|
158
|
-
|
|
108
|
+
Simulates an RTL language by reversing words and using right-to-left Unicode formatting.
|
|
159
109
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
letters are flipped.
|
|
110
|
+
```js
|
|
111
|
+
pseudoLocalization.start({ strategy: 'bidi' });
|
|
112
|
+
```
|
|
164
113
|
|
|
165
|
-
|
|
114
|
+
Example output: **ɥsıʅƃuƎ ıpıԐ**
|
|
166
115
|
|
|
116
|
+

|
|
167
117
|
|
|
168
|
-
|
|
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
|
-
|
|
120
|
+
## API Reference
|
|
176
121
|
|
|
177
|
-
|
|
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
|
-
|
|
184
|
-
|
|
124
|
+
- `str`: String to localize.
|
|
125
|
+
- `options.strategy`: `'accented'` (default) or `'bidi'`.
|
|
185
126
|
|
|
186
|
-
|
|
127
|
+
### `PseudoLocalizeDom.start(options?: DomOptions): StopFn`
|
|
187
128
|
|
|
188
|
-
|
|
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
|
-
|
|
192
|
-
|
|
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
|
-
|
|
195
|
-
Stops watching the DOM for additions/updates to continuously pseudo localize new content.
|
|
138
|
+
#### `DomOptions`
|
|
196
139
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
140
|
+
- `strategy`: `'accented'` or `'bidi'`.
|
|
141
|
+
- `blacklistedNodeNames`: Nodes to ignore (default: `['STYLE']`).
|
|
142
|
+
- `root`: Root element for localization (default: `document.body`).
|
|
200
143
|
|
|
201
|
-
|
|
144
|
+
---
|
|
202
145
|
|
|
203
|
-
|
|
204
|
-
The pseudo localization strategy to use when transforming strings. Accepted values are `accented` or `bidi`.
|
|
146
|
+
## CLI Usage
|
|
205
147
|
|
|
206
|
-
|
|
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
|
-
```
|
|
150
|
+
```sh
|
|
210
151
|
npx pseudo-localization ./path/to/file.json
|
|
211
152
|
|
|
212
|
-
#
|
|
213
|
-
npx pseudo-localization
|
|
153
|
+
# Pipe a string
|
|
154
|
+
echo "hello world" | npx pseudo-localization
|
|
214
155
|
|
|
215
|
-
#
|
|
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
|
|
166
|
+
src Input file or STDIN
|
|
234
167
|
|
|
235
168
|
Options:
|
|
236
|
-
-o, --output
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
--
|
|
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
|
-
|
|
248
|
-
|
|
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.
|
|
3
|
+
"version": "3.0.3",
|
|
4
4
|
"description": "pseudo-localization for internationalization testing",
|
|
5
5
|
"files": [
|
|
6
|
-
"
|
|
7
|
-
"
|
|
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/**/*.
|
|
28
|
-
"format": "prettier --write src/**/*.
|
|
29
|
-
"lint": "eslint src/**/*.
|
|
30
|
-
"test-js": "
|
|
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
|
+
}
|
package/src/localize.mjs
ADDED
|
@@ -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
|
+
};
|