@vocab/phrase 1.0.1 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +214 -5
- package/dist/declarations/src/csv.d.ts +10 -0
- package/dist/declarations/src/phrase-api.d.ts +8 -3
- package/dist/declarations/src/pull-translations.d.ts +1 -0
- package/dist/declarations/src/push-translations.d.ts +4 -2
- package/dist/vocab-phrase.cjs.dev.js +145 -23
- package/dist/vocab-phrase.cjs.prod.js +145 -23
- package/dist/vocab-phrase.esm.js +145 -23
- package/package.json +8 -4
- package/CHANGELOG.md +0 -122
- package/src/file.ts +0 -4
- package/src/index.ts +0 -2
- package/src/logger.ts +0 -9
- package/src/phrase-api.ts +0 -147
- package/src/pull-translations.test.ts +0 -148
- package/src/pull-translations.ts +0 -102
- package/src/push-translations.test.ts +0 -65
- package/src/push-translations.ts +0 -55
package/README.md
CHANGED
|
@@ -184,6 +184,14 @@ Configuration can either be passed into the Node API directly or be gathered fro
|
|
|
184
184
|
**vocab.config.js**
|
|
185
185
|
|
|
186
186
|
```js
|
|
187
|
+
function capitalize(element) {
|
|
188
|
+
return element.toUpperCase();
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function pad(message) {
|
|
192
|
+
return '[' + message + ']';
|
|
193
|
+
}
|
|
194
|
+
|
|
187
195
|
module.exports = {
|
|
188
196
|
devLanguage: 'en',
|
|
189
197
|
languages: [
|
|
@@ -192,11 +200,25 @@ module.exports = {
|
|
|
192
200
|
{ name: 'en-US', extends: 'en' },
|
|
193
201
|
{ name: 'fr-FR' }
|
|
194
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
|
+
],
|
|
195
217
|
/**
|
|
196
218
|
* The root directory to compile and validate translations
|
|
197
219
|
* Default: Current working directory
|
|
198
220
|
*/
|
|
199
|
-
projectRoot: './example/'
|
|
221
|
+
projectRoot: './example/',
|
|
200
222
|
/**
|
|
201
223
|
* A custom suffix to name vocab translation directories
|
|
202
224
|
* Default: '.vocab'
|
|
@@ -209,6 +231,131 @@ module.exports = {
|
|
|
209
231
|
};
|
|
210
232
|
```
|
|
211
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
|
+
|
|
212
359
|
## Use without React
|
|
213
360
|
|
|
214
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.
|
|
@@ -275,19 +422,81 @@ Or to re-run the compiler when files change use:
|
|
|
275
422
|
$ vocab compile --watch
|
|
276
423
|
```
|
|
277
424
|
|
|
278
|
-
## External
|
|
425
|
+
## External Translation Tooling
|
|
279
426
|
|
|
280
427
|
Vocab can be used to synchronize your translations with translations from a remote translation platform.
|
|
281
428
|
|
|
282
|
-
| Platform
|
|
283
|
-
|
|
|
284
|
-
| [Phrase]
|
|
429
|
+
| Platform | Environment Variables |
|
|
430
|
+
| -------- | ----------------------------------- |
|
|
431
|
+
| [Phrase] | PHRASE_PROJECT_ID, PHRASE_API_TOKEN |
|
|
285
432
|
|
|
286
433
|
```bash
|
|
287
434
|
$ vocab push --branch my-branch
|
|
288
435
|
$ vocab pull --branch my-branch
|
|
289
436
|
```
|
|
290
437
|
|
|
438
|
+
### [Phrase] Platform Features
|
|
439
|
+
|
|
440
|
+
#### Delete Unused keys
|
|
441
|
+
|
|
442
|
+
When uploading translations, Phrase identifies keys that exist in the Phrase project, but were not
|
|
443
|
+
referenced in the upload. These keys can be deleted from Phrase by providing the
|
|
444
|
+
`---delete-unused-keys` flag to `vocab push`. E.g.
|
|
445
|
+
|
|
446
|
+
```sh
|
|
447
|
+
$ vocab push --branch my-branch --delete-unused-keys
|
|
448
|
+
```
|
|
449
|
+
|
|
450
|
+
[phrase]: https://developers.phrase.com/api/
|
|
451
|
+
|
|
452
|
+
#### [Tags]
|
|
453
|
+
|
|
454
|
+
`vocab push` supports uploading [tags] to Phrase.
|
|
455
|
+
|
|
456
|
+
Tags can be added to an individual key via the `tags` property:
|
|
457
|
+
|
|
458
|
+
```jsonc
|
|
459
|
+
// translations.json
|
|
460
|
+
{
|
|
461
|
+
"Hello": {
|
|
462
|
+
"message": "Hello",
|
|
463
|
+
"tags": ["greeting", "home_page"]
|
|
464
|
+
},
|
|
465
|
+
"Goodbye": {
|
|
466
|
+
"message": "Goodbye",
|
|
467
|
+
"tags": ["home_page"]
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
```
|
|
471
|
+
|
|
472
|
+
Tags can also be added under a top-level `_meta` field. This will result in the tags applying to all
|
|
473
|
+
keys specified in the file:
|
|
474
|
+
|
|
475
|
+
```jsonc
|
|
476
|
+
// translations.json
|
|
477
|
+
{
|
|
478
|
+
"_meta": {
|
|
479
|
+
"tags": ["home_page"]
|
|
480
|
+
},
|
|
481
|
+
"Hello": {
|
|
482
|
+
"message": "Hello",
|
|
483
|
+
"tags": ["greeting"]
|
|
484
|
+
},
|
|
485
|
+
"Goodbye": {
|
|
486
|
+
"message": "Goodbye"
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
```
|
|
490
|
+
|
|
491
|
+
In the above example, both the `Hello` and `Goodbye` keys would have the `home_page` tag attached to
|
|
492
|
+
them, but only the `Hello` key would have the `usage_greeting` tag attached to it.
|
|
493
|
+
|
|
494
|
+
**NOTE**: Only tags specified on keys in your [`devLanguage`][configuration] will be uploaded.
|
|
495
|
+
Tags on keys in other languages will be ignored.
|
|
496
|
+
|
|
497
|
+
[tags]: https://support.phrase.com/hc/en-us/articles/5822598372252-Tags-Strings-
|
|
498
|
+
[configuration]: #Configuration
|
|
499
|
+
|
|
291
500
|
## Troubleshooting
|
|
292
501
|
|
|
293
502
|
### Problem: Passed locale is being ignored or using en-US instead
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { TranslationsByLanguage } from '@vocab/types';
|
|
2
|
+
export declare function translationsToCsv(translations: TranslationsByLanguage, devLanguage: string): {
|
|
3
|
+
csvString: string;
|
|
4
|
+
localeMapping: {
|
|
5
|
+
[k: string]: number;
|
|
6
|
+
};
|
|
7
|
+
keyIndex: number;
|
|
8
|
+
commentIndex: number;
|
|
9
|
+
tagColumn: number;
|
|
10
|
+
};
|
|
@@ -1,7 +1,12 @@
|
|
|
1
|
-
import { TranslationsByKey } from './../../types/src/index';
|
|
2
1
|
import type { TranslationsByLanguage } from '@vocab/types';
|
|
3
2
|
import fetch from 'node-fetch';
|
|
4
|
-
export declare function callPhrase(relativePath: string, options?: Parameters<typeof fetch>[1]): Promise<
|
|
3
|
+
export declare function callPhrase<T = any>(relativePath: string, options?: Parameters<typeof fetch>[1]): Promise<T>;
|
|
5
4
|
export declare function pullAllTranslations(branch: string): Promise<TranslationsByLanguage>;
|
|
6
|
-
export declare function
|
|
5
|
+
export declare function pushTranslations(translationsByLanguage: TranslationsByLanguage, { devLanguage, branch }: {
|
|
6
|
+
devLanguage: string;
|
|
7
|
+
branch: string;
|
|
8
|
+
}): Promise<{
|
|
9
|
+
uploadId: string;
|
|
10
|
+
}>;
|
|
11
|
+
export declare function deleteUnusedKeys(uploadId: string, branch: string): Promise<void>;
|
|
7
12
|
export declare function ensureBranch(branch: string): Promise<void>;
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { UserConfig } from '@vocab/types';
|
|
2
2
|
interface PushOptions {
|
|
3
3
|
branch: string;
|
|
4
|
+
deleteUnusedKeys?: boolean;
|
|
4
5
|
}
|
|
5
6
|
/**
|
|
6
|
-
*
|
|
7
|
+
* Uploads translations to the Phrase API for each language.
|
|
8
|
+
* A unique namespace is appended to each key using the file path the key came from.
|
|
7
9
|
*/
|
|
8
|
-
export declare function push({ branch }: PushOptions, config: UserConfig): Promise<void>;
|
|
10
|
+
export declare function push({ branch, deleteUnusedKeys }: PushOptions, config: UserConfig): Promise<void>;
|
|
9
11
|
export {};
|
|
@@ -9,6 +9,7 @@ var FormData = require('form-data');
|
|
|
9
9
|
var fetch = require('node-fetch');
|
|
10
10
|
var chalk = require('chalk');
|
|
11
11
|
var debug = require('debug');
|
|
12
|
+
var sync = require('csv-stringify/sync');
|
|
12
13
|
|
|
13
14
|
function _interopDefault (e) { return e && e.__esModule ? e : { 'default': e }; }
|
|
14
15
|
|
|
@@ -27,6 +28,44 @@ const log = (...params) => {
|
|
|
27
28
|
console.log(chalk__default['default'].yellow('Vocab'), ...params);
|
|
28
29
|
};
|
|
29
30
|
|
|
31
|
+
function translationsToCsv(translations, devLanguage) {
|
|
32
|
+
const languages = Object.keys(translations);
|
|
33
|
+
const altLanguages = languages.filter(language => language !== devLanguage); // Ensure languages are ordered for locale mapping
|
|
34
|
+
|
|
35
|
+
const orderedLanguages = [devLanguage, ...altLanguages];
|
|
36
|
+
const devLanguageTranslations = translations[devLanguage];
|
|
37
|
+
const csv = Object.entries(devLanguageTranslations).map(([key, {
|
|
38
|
+
message,
|
|
39
|
+
description,
|
|
40
|
+
tags
|
|
41
|
+
}]) => {
|
|
42
|
+
const altTranslationMessages = altLanguages.map(language => {
|
|
43
|
+
var _translations$languag, _translations$languag2;
|
|
44
|
+
|
|
45
|
+
return (_translations$languag = translations[language]) === null || _translations$languag === void 0 ? void 0 : (_translations$languag2 = _translations$languag[key]) === null || _translations$languag2 === void 0 ? void 0 : _translations$languag2.message;
|
|
46
|
+
});
|
|
47
|
+
return [message, ...altTranslationMessages, key, description, tags === null || tags === void 0 ? void 0 : tags.join(',')];
|
|
48
|
+
});
|
|
49
|
+
const csvString = sync.stringify(csv, {
|
|
50
|
+
delimiter: ',',
|
|
51
|
+
header: false
|
|
52
|
+
}); // Column indices start at 1
|
|
53
|
+
|
|
54
|
+
const localeMapping = Object.fromEntries(orderedLanguages.map((language, index) => [language, index + 1]));
|
|
55
|
+
const keyIndex = orderedLanguages.length + 1;
|
|
56
|
+
const commentIndex = keyIndex + 1;
|
|
57
|
+
const tagColumn = commentIndex + 1;
|
|
58
|
+
return {
|
|
59
|
+
csvString,
|
|
60
|
+
localeMapping,
|
|
61
|
+
keyIndex,
|
|
62
|
+
commentIndex,
|
|
63
|
+
tagColumn
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/* eslint-disable no-console */
|
|
68
|
+
|
|
30
69
|
function _callPhrase(path, options = {}) {
|
|
31
70
|
const phraseApiToken = process.env.PHRASE_API_TOKEN;
|
|
32
71
|
|
|
@@ -44,14 +83,14 @@ function _callPhrase(path, options = {}) {
|
|
|
44
83
|
}).then(async response => {
|
|
45
84
|
console.log(`${path}: ${response.status} - ${response.statusText}`);
|
|
46
85
|
console.log(`Rate Limit: ${response.headers.get('X-Rate-Limit-Remaining')} of ${response.headers.get('X-Rate-Limit-Limit')} remaining. (${response.headers.get('X-Rate-Limit-Reset')} seconds remaining})`);
|
|
47
|
-
|
|
86
|
+
trace('\nLink:', response.headers.get('Link'), '\n'); // Print All Headers:
|
|
48
87
|
// console.log(Array.from(r.headers.entries()));
|
|
49
88
|
|
|
50
89
|
try {
|
|
51
90
|
var _response$headers$get;
|
|
52
91
|
|
|
53
92
|
const result = await response.json();
|
|
54
|
-
|
|
93
|
+
trace(`Internal Result (Length: ${result.length})\n`);
|
|
55
94
|
|
|
56
95
|
if ((!options.method || options.method === 'GET') && (_response$headers$get = response.headers.get('Link')) !== null && _response$headers$get !== void 0 && _response$headers$get.includes('rel=next')) {
|
|
57
96
|
var _response$headers$get2, _response$headers$get3;
|
|
@@ -59,10 +98,10 @@ function _callPhrase(path, options = {}) {
|
|
|
59
98
|
const [, nextPageUrl] = (_response$headers$get2 = (_response$headers$get3 = response.headers.get('Link')) === null || _response$headers$get3 === void 0 ? void 0 : _response$headers$get3.match(/<([^>]*)>; rel=next/)) !== null && _response$headers$get2 !== void 0 ? _response$headers$get2 : [];
|
|
60
99
|
|
|
61
100
|
if (!nextPageUrl) {
|
|
62
|
-
throw new Error('
|
|
101
|
+
throw new Error("Can't parse next page URL");
|
|
63
102
|
}
|
|
64
103
|
|
|
65
|
-
console.log('Results
|
|
104
|
+
console.log('Results received with next page: ', nextPageUrl);
|
|
66
105
|
const nextPageResult = await _callPhrase(nextPageUrl, options);
|
|
67
106
|
return [...result, ...nextPageResult];
|
|
68
107
|
}
|
|
@@ -109,23 +148,70 @@ async function pullAllTranslations(branch) {
|
|
|
109
148
|
|
|
110
149
|
return translations;
|
|
111
150
|
}
|
|
112
|
-
async function
|
|
151
|
+
async function pushTranslations(translationsByLanguage, {
|
|
152
|
+
devLanguage,
|
|
153
|
+
branch
|
|
154
|
+
}) {
|
|
113
155
|
const formData = new FormData__default['default']();
|
|
114
|
-
const
|
|
156
|
+
const {
|
|
157
|
+
csvString,
|
|
158
|
+
localeMapping,
|
|
159
|
+
keyIndex,
|
|
160
|
+
commentIndex,
|
|
161
|
+
tagColumn
|
|
162
|
+
} = translationsToCsv(translationsByLanguage, devLanguage);
|
|
163
|
+
const fileContents = Buffer.from(csvString);
|
|
115
164
|
formData.append('file', fileContents, {
|
|
116
|
-
contentType: '
|
|
117
|
-
filename:
|
|
165
|
+
contentType: 'text/csv',
|
|
166
|
+
filename: 'translations.csv'
|
|
118
167
|
});
|
|
119
|
-
formData.append('file_format', '
|
|
120
|
-
formData.append('locale_id', locale);
|
|
168
|
+
formData.append('file_format', 'csv');
|
|
121
169
|
formData.append('branch', branch);
|
|
122
170
|
formData.append('update_translations', 'true');
|
|
123
|
-
|
|
124
|
-
|
|
171
|
+
|
|
172
|
+
for (const [locale, index] of Object.entries(localeMapping)) {
|
|
173
|
+
formData.append(`locale_mapping[${locale}]`, index);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
formData.append('format_options[key_index]', keyIndex);
|
|
177
|
+
formData.append('format_options[comment_index]', commentIndex);
|
|
178
|
+
formData.append('format_options[tag_column]', tagColumn);
|
|
179
|
+
formData.append('format_options[enable_pluralization]', 'false');
|
|
180
|
+
log('Uploading translations');
|
|
181
|
+
const result = await callPhrase(`uploads`, {
|
|
125
182
|
method: 'POST',
|
|
126
183
|
body: formData
|
|
127
184
|
});
|
|
128
|
-
|
|
185
|
+
trace('Upload result:\n', result);
|
|
186
|
+
|
|
187
|
+
if (result && 'id' in result) {
|
|
188
|
+
log('Upload ID:', result.id, '\n');
|
|
189
|
+
log('Successfully Uploaded\n');
|
|
190
|
+
} else {
|
|
191
|
+
log(`Error uploading: ${result === null || result === void 0 ? void 0 : result.message}\n`);
|
|
192
|
+
log('Response:', result);
|
|
193
|
+
throw new Error('Error uploading');
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return {
|
|
197
|
+
uploadId: result.id
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
async function deleteUnusedKeys(uploadId, branch) {
|
|
201
|
+
const query = `unmentioned_in_upload:${uploadId}`;
|
|
202
|
+
const {
|
|
203
|
+
records_affected
|
|
204
|
+
} = await callPhrase('keys', {
|
|
205
|
+
method: 'DELETE',
|
|
206
|
+
headers: {
|
|
207
|
+
'Content-Type': 'application/json'
|
|
208
|
+
},
|
|
209
|
+
body: JSON.stringify({
|
|
210
|
+
branch,
|
|
211
|
+
q: query
|
|
212
|
+
})
|
|
213
|
+
});
|
|
214
|
+
log('Successfully deleted', records_affected, 'unused keys from branch', branch);
|
|
129
215
|
}
|
|
130
216
|
async function ensureBranch(branch) {
|
|
131
217
|
await callPhrase(`branches`, {
|
|
@@ -137,7 +223,7 @@ async function ensureBranch(branch) {
|
|
|
137
223
|
name: branch
|
|
138
224
|
})
|
|
139
225
|
});
|
|
140
|
-
|
|
226
|
+
log('Created branch:', branch);
|
|
141
227
|
}
|
|
142
228
|
|
|
143
229
|
async function pull({
|
|
@@ -148,9 +234,17 @@ async function pull({
|
|
|
148
234
|
const alternativeLanguages = core.getAltLanguages(config);
|
|
149
235
|
const allPhraseTranslations = await pullAllTranslations(branch);
|
|
150
236
|
trace(`Pulling translations from Phrase for languages ${config.devLanguage} and ${alternativeLanguages.join(', ')}`);
|
|
237
|
+
const phraseLanguages = Object.keys(allPhraseTranslations);
|
|
238
|
+
trace(`Found Phrase translations for languages ${phraseLanguages.join(', ')}`);
|
|
239
|
+
|
|
240
|
+
if (!phraseLanguages.includes(config.devLanguage)) {
|
|
241
|
+
throw new Error(`Phrase did not return any translations for the configured development language "${config.devLanguage}".\nPlease ensure this language is present in your Phrase project's configuration.`);
|
|
242
|
+
}
|
|
243
|
+
|
|
151
244
|
const allVocabTranslations = await core.loadAllTranslations({
|
|
152
245
|
fallbacks: 'none',
|
|
153
|
-
includeNodeModules: false
|
|
246
|
+
includeNodeModules: false,
|
|
247
|
+
withTags: true
|
|
154
248
|
}, config);
|
|
155
249
|
|
|
156
250
|
for (const loadedTranslation of allVocabTranslations) {
|
|
@@ -168,6 +262,11 @@ async function pull({
|
|
|
168
262
|
defaultValues[key] = { ...defaultValues[key],
|
|
169
263
|
...allPhraseTranslations[config.devLanguage][core.getUniqueKey(key, loadedTranslation.namespace)]
|
|
170
264
|
};
|
|
265
|
+
} // Only write a `_meta` field if necessary
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
if (Object.keys(loadedTranslation.metadata).length > 0) {
|
|
269
|
+
defaultValues._meta = loadedTranslation.metadata;
|
|
171
270
|
}
|
|
172
271
|
|
|
173
272
|
await writeFile(loadedTranslation.filePath, `${JSON.stringify(defaultValues, null, 2)}\n`);
|
|
@@ -205,14 +304,17 @@ async function pull({
|
|
|
205
304
|
}
|
|
206
305
|
|
|
207
306
|
/**
|
|
208
|
-
*
|
|
307
|
+
* Uploads translations to the Phrase API for each language.
|
|
308
|
+
* A unique namespace is appended to each key using the file path the key came from.
|
|
209
309
|
*/
|
|
210
310
|
async function push({
|
|
211
|
-
branch
|
|
311
|
+
branch,
|
|
312
|
+
deleteUnusedKeys: deleteUnusedKeys$1
|
|
212
313
|
}, config) {
|
|
213
314
|
const allLanguageTranslations = await core.loadAllTranslations({
|
|
214
315
|
fallbacks: 'none',
|
|
215
|
-
includeNodeModules: false
|
|
316
|
+
includeNodeModules: false,
|
|
317
|
+
withTags: true
|
|
216
318
|
}, config);
|
|
217
319
|
trace(`Pushing translations to branch ${branch}`);
|
|
218
320
|
const allLanguages = config.languages.map(v => v.name);
|
|
@@ -232,17 +334,37 @@ async function push({
|
|
|
232
334
|
phraseTranslations[language] = {};
|
|
233
335
|
}
|
|
234
336
|
|
|
337
|
+
const {
|
|
338
|
+
metadata: {
|
|
339
|
+
tags: sharedTags = []
|
|
340
|
+
}
|
|
341
|
+
} = loadedTranslation;
|
|
342
|
+
|
|
235
343
|
for (const localKey of Object.keys(localTranslations)) {
|
|
236
344
|
const phraseKey = core.getUniqueKey(localKey, loadedTranslation.namespace);
|
|
237
|
-
|
|
345
|
+
const {
|
|
346
|
+
tags = [],
|
|
347
|
+
...localTranslation
|
|
348
|
+
} = localTranslations[localKey];
|
|
349
|
+
|
|
350
|
+
if (language === config.devLanguage) {
|
|
351
|
+
localTranslation.tags = [...tags, ...sharedTags];
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
phraseTranslations[language][phraseKey] = localTranslation;
|
|
238
355
|
}
|
|
239
356
|
}
|
|
240
357
|
}
|
|
241
358
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
359
|
+
const {
|
|
360
|
+
uploadId
|
|
361
|
+
} = await pushTranslations(phraseTranslations, {
|
|
362
|
+
devLanguage: config.devLanguage,
|
|
363
|
+
branch
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
if (deleteUnusedKeys$1) {
|
|
367
|
+
await deleteUnusedKeys(uploadId, branch);
|
|
246
368
|
}
|
|
247
369
|
}
|
|
248
370
|
|