@vocab/react 1.0.1 → 1.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/CHANGELOG.md +23 -0
- package/README.md +153 -4
- package/dist/declarations/src/index.d.ts +5 -2
- package/dist/vocab-react.cjs.dev.js +15 -1
- package/dist/vocab-react.cjs.prod.js +15 -1
- package/dist/vocab-react.esm.js +16 -2
- package/package.json +5 -5
- package/src/index.tsx +27 -5
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,28 @@
|
|
|
1
1
|
# @vocab/react
|
|
2
2
|
|
|
3
|
+
## 1.1.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [`e9c7067`](https://github.com/seek-oss/vocab/commit/e9c7067b31215a176e70ac1e73f2c878107f328f) [#83](https://github.com/seek-oss/vocab/pull/83) Thanks [@michaeltaranto](https://github.com/michaeltaranto)! - Add React 18 support
|
|
8
|
+
|
|
9
|
+
## 1.1.0
|
|
10
|
+
|
|
11
|
+
### Minor Changes
|
|
12
|
+
|
|
13
|
+
- [`6de02b3`](https://github.com/seek-oss/vocab/commit/6de02b35839e8ecdd9016fec49a95e17d3696f87) [#69](https://github.com/seek-oss/vocab/pull/69) Thanks [@mattcompiles](https://github.com/mattcompiles)! - Automatically assign keys to React elements
|
|
14
|
+
|
|
15
|
+
Previously, when using React elements within translation templates, React would warn about missing keys as the return type is an array. This meant you needed to supply a key manually. Vocab can now automatically assign a key to React elements. Keys that are passed explicitly will remain untouched.
|
|
16
|
+
|
|
17
|
+
## 1.0.2
|
|
18
|
+
|
|
19
|
+
### Patch Changes
|
|
20
|
+
|
|
21
|
+
- [`3ec6dba`](https://github.com/seek-oss/vocab/commit/3ec6dbaad590299cc33e2d9d4a877576eb05853a) [#63](https://github.com/seek-oss/vocab/pull/63) Thanks [@jahredhope](https://github.com/jahredhope)! - Migrate to new @formatjs/icu-messageformat-parser as intl-messageformat-parser has been deprecated
|
|
22
|
+
|
|
23
|
+
- Updated dependencies [[`3ec6dba`](https://github.com/seek-oss/vocab/commit/3ec6dbaad590299cc33e2d9d4a877576eb05853a)]:
|
|
24
|
+
- @vocab/types@1.0.1
|
|
25
|
+
|
|
3
26
|
## 1.0.1
|
|
4
27
|
|
|
5
28
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -110,7 +110,7 @@ So far, your app will run, but you're missing any translations other than the in
|
|
|
110
110
|
}
|
|
111
111
|
```
|
|
112
112
|
|
|
113
|
-
### Step 6: [Optional]
|
|
113
|
+
### Step 6: [Optional] Set up Webpack plugin
|
|
114
114
|
|
|
115
115
|
Right now every language is loaded into your web application all the time, which could lead to a large bundle size. Ideally you will want to switch out the Node/default runtime for web runtime that will load only the active language.
|
|
116
116
|
|
|
@@ -171,8 +171,10 @@ In the below example we use two messages, one that passes in a single parameter
|
|
|
171
171
|
Vocab will automatically parse these strings as ICU messages, identify the required parameters and ensure TypeScript knows the values must be passed in.
|
|
172
172
|
|
|
173
173
|
```tsx
|
|
174
|
-
t('my key with param', {name: 'Vocab'});
|
|
175
|
-
t('my key with component', {
|
|
174
|
+
t('my key with param', { name: 'Vocab' });
|
|
175
|
+
t('my key with component', {
|
|
176
|
+
Link: (children) => <a href="/foo">{children}</a>
|
|
177
|
+
});
|
|
176
178
|
```
|
|
177
179
|
|
|
178
180
|
## Configuration
|
|
@@ -182,6 +184,14 @@ Configuration can either be passed into the Node API directly or be gathered fro
|
|
|
182
184
|
**vocab.config.js**
|
|
183
185
|
|
|
184
186
|
```js
|
|
187
|
+
function capitalize(element) {
|
|
188
|
+
return element.toUpperCase();
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function pad(message) {
|
|
192
|
+
return '[' + message + ']';
|
|
193
|
+
}
|
|
194
|
+
|
|
185
195
|
module.exports = {
|
|
186
196
|
devLanguage: 'en',
|
|
187
197
|
languages: [
|
|
@@ -190,11 +200,25 @@ module.exports = {
|
|
|
190
200
|
{ name: 'en-US', extends: 'en' },
|
|
191
201
|
{ name: 'fr-FR' }
|
|
192
202
|
],
|
|
203
|
+
/**
|
|
204
|
+
* An array of languages to generate based off translations for existing languages
|
|
205
|
+
* Default: []
|
|
206
|
+
*/
|
|
207
|
+
generatedLanguages: [
|
|
208
|
+
{
|
|
209
|
+
name: 'generatedLangauge',
|
|
210
|
+
extends: 'en',
|
|
211
|
+
generator: {
|
|
212
|
+
transformElement: capitalize,
|
|
213
|
+
transformMessage: pad
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
],
|
|
193
217
|
/**
|
|
194
218
|
* The root directory to compile and validate translations
|
|
195
219
|
* Default: Current working directory
|
|
196
220
|
*/
|
|
197
|
-
projectRoot:
|
|
221
|
+
projectRoot: './example/',
|
|
198
222
|
/**
|
|
199
223
|
* A custom suffix to name vocab translation directories
|
|
200
224
|
* Default: '.vocab'
|
|
@@ -207,6 +231,131 @@ module.exports = {
|
|
|
207
231
|
};
|
|
208
232
|
```
|
|
209
233
|
|
|
234
|
+
## Generated languages
|
|
235
|
+
|
|
236
|
+
Vocab supports the creation of generated languages via the `generatedLanguages` config.
|
|
237
|
+
|
|
238
|
+
Generated languages are created by running a message `generator` over every translation message in an existing translation.
|
|
239
|
+
A `generator` may contain a `transformElement` function, a `transformMessage` function, or both.
|
|
240
|
+
Both of these functions accept a single string parameter and return a string.
|
|
241
|
+
|
|
242
|
+
`transformElement` is applied to string literal values contained within `MessageFormatElement`s.
|
|
243
|
+
A `MessageFormatElement` is an object representing a node in the AST of a compiled translation message.
|
|
244
|
+
Simply put, any text that would end up being translated by a translator, i.e. anything that is not part of the [ICU Message syntax], will be passed to `transformElement`.
|
|
245
|
+
An example of a use case for this function would be adding [diacritics] to every letter in order to stress your UI from a vertical line-height perspective.
|
|
246
|
+
|
|
247
|
+
`transformMessage` receives the entire translation message _after_ `transformElement` has been applied to its individual elements.
|
|
248
|
+
An example of a use case for this function would be adding padding text to the start/end of your messages in order to easily identify which text in your app has not been extracted into a `translations.json` file.
|
|
249
|
+
|
|
250
|
+
By default, a generated language's messages will be based off the `devLanguage`'s messages, but this can be overridden by providing an `extends` value that references another language.
|
|
251
|
+
|
|
252
|
+
**vocab.config.js**
|
|
253
|
+
|
|
254
|
+
```js
|
|
255
|
+
function capitalize(message) {
|
|
256
|
+
return message.toUpperCase();
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function pad(message) {
|
|
260
|
+
return '[' + message + ']';
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
module.exports = {
|
|
264
|
+
devLanguage: 'en',
|
|
265
|
+
languages: [{ name: 'en' }, { name: 'fr' }],
|
|
266
|
+
generatedLanguages: [
|
|
267
|
+
{
|
|
268
|
+
name: 'generatedLanguage',
|
|
269
|
+
extends: 'en',
|
|
270
|
+
generator: {
|
|
271
|
+
transformElement: capitalize,
|
|
272
|
+
transformMessage: pad
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
]
|
|
276
|
+
};
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
Generated languages are consumed the same way as regular languages.
|
|
280
|
+
Any Vocab API that accepts a `language` parameter will work with a generated language as well as a regular language.
|
|
281
|
+
|
|
282
|
+
**App.tsx**
|
|
283
|
+
|
|
284
|
+
```tsx
|
|
285
|
+
const App = () => (
|
|
286
|
+
<VocabProvider language="generatedLanguage">
|
|
287
|
+
...
|
|
288
|
+
</VocabProvider>
|
|
289
|
+
);
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
[icu message syntax]: https://formatjs.io/docs/intl-messageformat/#message-syntax
|
|
293
|
+
[diacritics]: https://en.wikipedia.org/wiki/Diacritic
|
|
294
|
+
|
|
295
|
+
## Pseudo-localization
|
|
296
|
+
|
|
297
|
+
The `@vocab/pseudo-localize` package exports low-level functions that can be used for pseudo-localization of translation messages.
|
|
298
|
+
|
|
299
|
+
```ts
|
|
300
|
+
import {
|
|
301
|
+
extendVowels,
|
|
302
|
+
padString,
|
|
303
|
+
pseudoLocalize,
|
|
304
|
+
substituteCharacters
|
|
305
|
+
} from '@vocab/pseudo-localize';
|
|
306
|
+
|
|
307
|
+
const message = 'Hello';
|
|
308
|
+
|
|
309
|
+
// [Hello]
|
|
310
|
+
const paddedMessage = padString(message);
|
|
311
|
+
|
|
312
|
+
// Ḩẽƚƚö
|
|
313
|
+
const substitutedMessage = substituteCharacters(message);
|
|
314
|
+
|
|
315
|
+
// Heelloo
|
|
316
|
+
const extendedMessage = extendVowels(message);
|
|
317
|
+
|
|
318
|
+
// Extend the message and then substitute characters
|
|
319
|
+
// Ḩẽẽƚƚöö
|
|
320
|
+
const pseudoLocalizedMessage = pseudoLocalize(message);
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
Pseudo-localization is a transformation that can be applied to a translation message.
|
|
324
|
+
Vocab's implementation of this transformation contains the following elements:
|
|
325
|
+
|
|
326
|
+
- _Start and end markers (`padString`):_ All strings are encapsulated in `[` and `]`. If a developer doesn’t see these characters they know the string has been clipped by an inflexible UI element.
|
|
327
|
+
- _Transformation of ASCII characters to extended character equivalents (`substituteCharacters`):_ Stresses the UI from a vertical line-height perspective, tests font and encoding support, and weeds out strings that haven’t been externalized correctly (they will not have the pseudo-localization applied to them).
|
|
328
|
+
- _Padding text (`extendVowels`):_ Simulates translation-induced expansion. Vocab's implementation of this involves repeating vowels (and `y`) to simulate a 40% expansion in the message's length.
|
|
329
|
+
|
|
330
|
+
This Netflix technology [blog post][blog post] inspired Vocab's implementation of this
|
|
331
|
+
functionality.
|
|
332
|
+
|
|
333
|
+
### Generating a pseudo-localized language using Vocab
|
|
334
|
+
|
|
335
|
+
Vocab can generate a pseudo-localized language via the [`generatedLanguages` config][generated languages config], either via the webpack plugin or your `vocab.config.js` file.
|
|
336
|
+
`@vocab/pseudo-localize` exports a `generator` that can be used directly in your config.
|
|
337
|
+
|
|
338
|
+
**vocab.config.js**
|
|
339
|
+
|
|
340
|
+
```js
|
|
341
|
+
const { generator } = require('@vocab/pseudo-localize');
|
|
342
|
+
|
|
343
|
+
module.exports = {
|
|
344
|
+
devLanguage: 'en',
|
|
345
|
+
languages: [{ name: 'en' }, { name: 'fr' }],
|
|
346
|
+
generatedLanguages: [
|
|
347
|
+
{
|
|
348
|
+
name: 'pseudo',
|
|
349
|
+
extends: 'en',
|
|
350
|
+
generator
|
|
351
|
+
}
|
|
352
|
+
]
|
|
353
|
+
};
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
[blog post]: https://netflixtechblog.com/pseudo-localization-netflix-12fff76fbcbe
|
|
357
|
+
[generated languages config]: #generated-languages
|
|
358
|
+
|
|
210
359
|
## Use without React
|
|
211
360
|
|
|
212
361
|
If you need to use Vocab outside of React, you can access the returned Vocab file directly. You'll then be responsible for when to load translations and how to update on translation load.
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import { TranslationFile, LanguageName, ParsedFormatFnByKey, ParsedFormatFn } from '@vocab/types';
|
|
2
|
-
import {
|
|
2
|
+
import { ReactNode } from 'react';
|
|
3
3
|
declare type Locale = string;
|
|
4
4
|
interface TranslationsValue {
|
|
5
5
|
language: LanguageName;
|
|
6
6
|
locale?: Locale;
|
|
7
7
|
}
|
|
8
|
-
|
|
8
|
+
interface VocabProviderProps extends TranslationsValue {
|
|
9
|
+
children: ReactNode;
|
|
10
|
+
}
|
|
11
|
+
export declare const VocabProvider: ({ children, language, locale, }: VocabProviderProps) => JSX.Element;
|
|
9
12
|
export declare const useLanguage: () => TranslationsValue;
|
|
10
13
|
declare type FormatXMLElementReactNodeFn = (parts: ReactNode[]) => ReactNode;
|
|
11
14
|
declare type MapToReactNodeFunction<Params extends Record<string, any>> = {
|
|
@@ -65,7 +65,21 @@ function useTranslations(translations) {
|
|
|
65
65
|
return '';
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
-
|
|
68
|
+
const result = translationsObject[key].format(params);
|
|
69
|
+
|
|
70
|
+
if (Array.isArray(result)) {
|
|
71
|
+
for (let i = 0; i < result.length; i++) {
|
|
72
|
+
const item = result[i];
|
|
73
|
+
|
|
74
|
+
if (typeof item === 'object' && item && !item.key && /*#__PURE__*/React.isValidElement(item)) {
|
|
75
|
+
result[i] = /*#__PURE__*/React.cloneElement(item, {
|
|
76
|
+
key: `_vocab-${i}`
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return result;
|
|
69
83
|
};
|
|
70
84
|
|
|
71
85
|
return {
|
|
@@ -65,7 +65,21 @@ function useTranslations(translations) {
|
|
|
65
65
|
return '';
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
-
|
|
68
|
+
const result = translationsObject[key].format(params);
|
|
69
|
+
|
|
70
|
+
if (Array.isArray(result)) {
|
|
71
|
+
for (let i = 0; i < result.length; i++) {
|
|
72
|
+
const item = result[i];
|
|
73
|
+
|
|
74
|
+
if (typeof item === 'object' && item && !item.key && /*#__PURE__*/React.isValidElement(item)) {
|
|
75
|
+
result[i] = /*#__PURE__*/React.cloneElement(item, {
|
|
76
|
+
key: `_vocab-${i}`
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return result;
|
|
69
83
|
};
|
|
70
84
|
|
|
71
85
|
return {
|
package/dist/vocab-react.esm.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { useMemo, useContext, useReducer } from 'react';
|
|
1
|
+
import React, { useMemo, useContext, useReducer, isValidElement, cloneElement } from 'react';
|
|
2
2
|
|
|
3
3
|
const TranslationsContext = /*#__PURE__*/React.createContext(undefined);
|
|
4
4
|
const VocabProvider = ({
|
|
@@ -57,7 +57,21 @@ function useTranslations(translations) {
|
|
|
57
57
|
return '';
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
-
|
|
60
|
+
const result = translationsObject[key].format(params);
|
|
61
|
+
|
|
62
|
+
if (Array.isArray(result)) {
|
|
63
|
+
for (let i = 0; i < result.length; i++) {
|
|
64
|
+
const item = result[i];
|
|
65
|
+
|
|
66
|
+
if (typeof item === 'object' && item && !item.key && /*#__PURE__*/isValidElement(item)) {
|
|
67
|
+
result[i] = /*#__PURE__*/cloneElement(item, {
|
|
68
|
+
key: `_vocab-${i}`
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return result;
|
|
61
75
|
};
|
|
62
76
|
|
|
63
77
|
return {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vocab/react",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.1",
|
|
4
4
|
"main": "dist/vocab-react.cjs.js",
|
|
5
5
|
"module": "dist/vocab-react.esm.js",
|
|
6
6
|
"author": "SEEK",
|
|
@@ -9,11 +9,11 @@
|
|
|
9
9
|
"react": ">=16.3.0"
|
|
10
10
|
},
|
|
11
11
|
"dependencies": {
|
|
12
|
-
"@vocab/types": "^1.0.
|
|
13
|
-
"intl-messageformat": "^9.
|
|
12
|
+
"@vocab/types": "^1.0.1",
|
|
13
|
+
"intl-messageformat": "^9.9.0"
|
|
14
14
|
},
|
|
15
15
|
"devDependencies": {
|
|
16
|
-
"@types/react": "^
|
|
17
|
-
"react": "^
|
|
16
|
+
"@types/react": "^18.0.9",
|
|
17
|
+
"react": "^18.1.0"
|
|
18
18
|
}
|
|
19
19
|
}
|
package/src/index.tsx
CHANGED
|
@@ -5,11 +5,12 @@ import {
|
|
|
5
5
|
ParsedFormatFn,
|
|
6
6
|
} from '@vocab/types';
|
|
7
7
|
import React, {
|
|
8
|
-
FunctionComponent,
|
|
9
8
|
ReactNode,
|
|
10
9
|
useContext,
|
|
11
10
|
useMemo,
|
|
12
11
|
useReducer,
|
|
12
|
+
isValidElement,
|
|
13
|
+
cloneElement,
|
|
13
14
|
} from 'react';
|
|
14
15
|
|
|
15
16
|
type Locale = string;
|
|
@@ -23,11 +24,14 @@ const TranslationsContext = React.createContext<TranslationsValue | undefined>(
|
|
|
23
24
|
undefined,
|
|
24
25
|
);
|
|
25
26
|
|
|
26
|
-
|
|
27
|
+
interface VocabProviderProps extends TranslationsValue {
|
|
28
|
+
children: ReactNode;
|
|
29
|
+
}
|
|
30
|
+
export const VocabProvider = ({
|
|
27
31
|
children,
|
|
28
32
|
language,
|
|
29
33
|
locale,
|
|
30
|
-
}) => {
|
|
34
|
+
}: VocabProviderProps) => {
|
|
31
35
|
const value = useMemo(() => ({ language, locale }), [language, locale]);
|
|
32
36
|
|
|
33
37
|
return (
|
|
@@ -49,6 +53,7 @@ export const useLanguage = (): TranslationsValue => {
|
|
|
49
53
|
'Attempted to access translation without language set. Did you forget to pass language to VocabProvider?',
|
|
50
54
|
);
|
|
51
55
|
}
|
|
56
|
+
|
|
52
57
|
return context;
|
|
53
58
|
};
|
|
54
59
|
|
|
@@ -83,7 +88,7 @@ type TranslateFn<FormatFnByKey extends ParsedFormatFnByKey> = {
|
|
|
83
88
|
|
|
84
89
|
export function useTranslations<
|
|
85
90
|
Language extends string,
|
|
86
|
-
FormatFnByKey extends ParsedFormatFnByKey
|
|
91
|
+
FormatFnByKey extends ParsedFormatFnByKey,
|
|
87
92
|
>(
|
|
88
93
|
translations: TranslationFile<Language, FormatFnByKey>,
|
|
89
94
|
): {
|
|
@@ -92,6 +97,7 @@ export function useTranslations<
|
|
|
92
97
|
} {
|
|
93
98
|
const { language, locale } = useLanguage();
|
|
94
99
|
const [, forceRender] = useReducer((s: number) => s + 1, 0);
|
|
100
|
+
|
|
95
101
|
const translationsObject = translations.getLoadedMessages(
|
|
96
102
|
language as any,
|
|
97
103
|
locale || language,
|
|
@@ -125,7 +131,23 @@ export function useTranslations<
|
|
|
125
131
|
return '';
|
|
126
132
|
}
|
|
127
133
|
|
|
128
|
-
|
|
134
|
+
const result = translationsObject[key].format(params);
|
|
135
|
+
|
|
136
|
+
if (Array.isArray(result)) {
|
|
137
|
+
for (let i = 0; i < result.length; i++) {
|
|
138
|
+
const item = result[i];
|
|
139
|
+
if (
|
|
140
|
+
typeof item === 'object' &&
|
|
141
|
+
item &&
|
|
142
|
+
!item.key &&
|
|
143
|
+
isValidElement(item)
|
|
144
|
+
) {
|
|
145
|
+
result[i] = cloneElement(item, { key: `_vocab-${i}` });
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return result;
|
|
129
151
|
};
|
|
130
152
|
|
|
131
153
|
return {
|