@vocab/phrase 1.0.0 → 1.1.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/CHANGELOG.md +29 -0
- package/README.md +154 -4
- package/dist/declarations/src/phrase-api.d.ts +5 -2
- package/dist/declarations/src/pull-translations.d.ts +1 -0
- package/dist/declarations/src/push-translations.d.ts +2 -1
- package/dist/vocab-phrase.cjs.dev.js +67 -29
- package/dist/vocab-phrase.cjs.prod.js +67 -29
- package/dist/vocab-phrase.esm.js +67 -29
- package/package.json +1 -1
- package/src/phrase-api.ts +48 -15
- package/src/pull-translations.test.ts +179 -45
- package/src/pull-translations.ts +47 -32
- package/src/push-translations.test.ts +138 -35
- package/src/push-translations.ts +15 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,34 @@
|
|
|
1
1
|
# @vocab/phrase
|
|
2
2
|
|
|
3
|
+
## 1.1.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [`66ed22c`](https://github.com/seek-oss/vocab/commit/66ed22cac6f89018d5fd69fd6f6408e090e1a382) [#93](https://github.com/seek-oss/vocab/pull/93) Thanks [@askoufis](https://github.com/askoufis)! - Add an optional `deleteUnusedKeys` flag to the `push` function. If set to `true`, unused keys will be deleted from Phrase after translations are pushed.
|
|
8
|
+
|
|
9
|
+
**EXAMPLE USAGE**:
|
|
10
|
+
|
|
11
|
+
```js
|
|
12
|
+
import { push } from '@vocab/phrase';
|
|
13
|
+
|
|
14
|
+
const vocabConfig = {
|
|
15
|
+
devLanguage: 'en',
|
|
16
|
+
language: ['en', 'fr'],
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
await push({ branch: 'myBranch', deleteUnusedKeys: true }, vocabConfig);
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
### Patch Changes
|
|
23
|
+
|
|
24
|
+
- [`159d559`](https://github.com/seek-oss/vocab/commit/159d559c87c66c3e91c707fb45a1f67ebec07b4d) [#91](https://github.com/seek-oss/vocab/pull/91) Thanks [@askoufis](https://github.com/askoufis)! - Improve error message when Phrase doesn't return any translations for the dev language
|
|
25
|
+
|
|
26
|
+
## 1.0.1
|
|
27
|
+
|
|
28
|
+
### Patch Changes
|
|
29
|
+
|
|
30
|
+
- [`20eec77`](https://github.com/seek-oss/vocab/commit/20eec770705d05048ad8b32575cb92720b887f5b) [#76](https://github.com/seek-oss/vocab/pull/76) Thanks [@askoufis](https://github.com/askoufis)! - `vocab pull` no longer errors when phrase returns no translations for a configured language
|
|
31
|
+
|
|
3
32
|
## 1.0.0
|
|
4
33
|
|
|
5
34
|
### Major 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.
|
|
@@ -283,6 +432,7 @@ Vocab can be used to synchronize your translations with translations from a remo
|
|
|
283
432
|
|
|
284
433
|
```bash
|
|
285
434
|
$ vocab push --branch my-branch
|
|
435
|
+
$ vocab push --branch my-branch --delete-unused-keys
|
|
286
436
|
$ vocab pull --branch my-branch
|
|
287
437
|
```
|
|
288
438
|
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { TranslationsByKey } from './../../types/src/index';
|
|
2
2
|
import type { TranslationsByLanguage } from '@vocab/types';
|
|
3
3
|
import fetch from 'node-fetch';
|
|
4
|
-
export declare function callPhrase(relativePath: string, options?: Parameters<typeof fetch>[1]): Promise<
|
|
4
|
+
export declare function callPhrase<T = any>(relativePath: string, options?: Parameters<typeof fetch>[1]): Promise<T>;
|
|
5
5
|
export declare function pullAllTranslations(branch: string): Promise<TranslationsByLanguage>;
|
|
6
|
-
export declare function pushTranslationsByLocale(contents: TranslationsByKey, locale: string, branch: string): Promise<
|
|
6
|
+
export declare function pushTranslationsByLocale(contents: TranslationsByKey, locale: string, branch: string): Promise<{
|
|
7
|
+
uploadId: string;
|
|
8
|
+
}>;
|
|
9
|
+
export declare function deleteUnusedKeys(uploadId: string, locale: string, branch: string): Promise<void>;
|
|
7
10
|
export declare function ensureBranch(branch: string): Promise<void>;
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { UserConfig } from '@vocab/types';
|
|
2
2
|
interface PushOptions {
|
|
3
3
|
branch: string;
|
|
4
|
+
deleteUnusedKeys?: boolean;
|
|
4
5
|
}
|
|
5
6
|
/**
|
|
6
7
|
* Uploading to the Phrase API for each language. Adding a unique namespace to each key using file path they key came from
|
|
7
8
|
*/
|
|
8
|
-
export declare function push({ branch }: PushOptions, config: UserConfig): Promise<void>;
|
|
9
|
+
export declare function push({ branch, deleteUnusedKeys }: PushOptions, config: UserConfig): Promise<void>;
|
|
9
10
|
export {};
|
|
@@ -44,14 +44,14 @@ function _callPhrase(path, options = {}) {
|
|
|
44
44
|
}).then(async response => {
|
|
45
45
|
console.log(`${path}: ${response.status} - ${response.statusText}`);
|
|
46
46
|
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
|
-
|
|
47
|
+
trace('\nLink:', response.headers.get('Link'), '\n'); // Print All Headers:
|
|
48
48
|
// console.log(Array.from(r.headers.entries()));
|
|
49
49
|
|
|
50
50
|
try {
|
|
51
51
|
var _response$headers$get;
|
|
52
52
|
|
|
53
53
|
const result = await response.json();
|
|
54
|
-
|
|
54
|
+
trace(`Internal Result (Length: ${result.length})\n`);
|
|
55
55
|
|
|
56
56
|
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
57
|
var _response$headers$get2, _response$headers$get3;
|
|
@@ -59,10 +59,10 @@ function _callPhrase(path, options = {}) {
|
|
|
59
59
|
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
60
|
|
|
61
61
|
if (!nextPageUrl) {
|
|
62
|
-
throw new Error('
|
|
62
|
+
throw new Error("Can't parse next page URL");
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
-
console.log('Results
|
|
65
|
+
console.log('Results received with next page: ', nextPageUrl);
|
|
66
66
|
const nextPageResult = await _callPhrase(nextPageUrl, options);
|
|
67
67
|
return [...result, ...nextPageResult];
|
|
68
68
|
}
|
|
@@ -83,7 +83,6 @@ async function callPhrase(relativePath, options = {}) {
|
|
|
83
83
|
}
|
|
84
84
|
|
|
85
85
|
return _callPhrase(`https://api.phrase.com/v2/projects/${projectId}/${relativePath}`, options).then(result => {
|
|
86
|
-
// console.log('Result:', result);
|
|
87
86
|
if (Array.isArray(result)) {
|
|
88
87
|
console.log('Result length:', result.length);
|
|
89
88
|
}
|
|
@@ -121,12 +120,35 @@ async function pushTranslationsByLocale(contents, locale, branch) {
|
|
|
121
120
|
formData.append('locale_id', locale);
|
|
122
121
|
formData.append('branch', branch);
|
|
123
122
|
formData.append('update_translations', 'true');
|
|
124
|
-
|
|
125
|
-
|
|
123
|
+
log('Starting to upload:', locale, '\n');
|
|
124
|
+
const {
|
|
125
|
+
id
|
|
126
|
+
} = await callPhrase(`uploads`, {
|
|
126
127
|
method: 'POST',
|
|
127
128
|
body: formData
|
|
128
129
|
});
|
|
130
|
+
log('Upload ID:', id, '\n');
|
|
129
131
|
log('Successfully Uploaded:', locale, '\n');
|
|
132
|
+
return {
|
|
133
|
+
uploadId: id
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
async function deleteUnusedKeys(uploadId, locale, branch) {
|
|
137
|
+
const query = `unmentioned_in_upload:${uploadId}`;
|
|
138
|
+
const {
|
|
139
|
+
records_affected
|
|
140
|
+
} = await callPhrase('keys', {
|
|
141
|
+
method: 'DELETE',
|
|
142
|
+
headers: {
|
|
143
|
+
'Content-Type': 'application/json'
|
|
144
|
+
},
|
|
145
|
+
body: JSON.stringify({
|
|
146
|
+
branch,
|
|
147
|
+
locale_id: locale,
|
|
148
|
+
q: query
|
|
149
|
+
})
|
|
150
|
+
});
|
|
151
|
+
log('Successfully deleted', records_affected, 'unused keys from branch', branch);
|
|
130
152
|
}
|
|
131
153
|
async function ensureBranch(branch) {
|
|
132
154
|
await callPhrase(`branches`, {
|
|
@@ -138,7 +160,7 @@ async function ensureBranch(branch) {
|
|
|
138
160
|
name: branch
|
|
139
161
|
})
|
|
140
162
|
});
|
|
141
|
-
|
|
163
|
+
log('Created branch:', branch);
|
|
142
164
|
}
|
|
143
165
|
|
|
144
166
|
async function pull({
|
|
@@ -149,6 +171,13 @@ async function pull({
|
|
|
149
171
|
const alternativeLanguages = core.getAltLanguages(config);
|
|
150
172
|
const allPhraseTranslations = await pullAllTranslations(branch);
|
|
151
173
|
trace(`Pulling translations from Phrase for languages ${config.devLanguage} and ${alternativeLanguages.join(', ')}`);
|
|
174
|
+
const phraseLanguages = Object.keys(allPhraseTranslations);
|
|
175
|
+
trace(`Found Phrase translations for languages ${phraseLanguages.join(', ')}`);
|
|
176
|
+
|
|
177
|
+
if (!phraseLanguages.includes(config.devLanguage)) {
|
|
178
|
+
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.`);
|
|
179
|
+
}
|
|
180
|
+
|
|
152
181
|
const allVocabTranslations = await core.loadAllTranslations({
|
|
153
182
|
fallbacks: 'none',
|
|
154
183
|
includeNodeModules: false
|
|
@@ -174,31 +203,33 @@ async function pull({
|
|
|
174
203
|
await writeFile(loadedTranslation.filePath, `${JSON.stringify(defaultValues, null, 2)}\n`);
|
|
175
204
|
|
|
176
205
|
for (const alternativeLanguage of alternativeLanguages) {
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
206
|
+
if (alternativeLanguage in allPhraseTranslations) {
|
|
207
|
+
const altTranslations = { ...loadedTranslation.languages[alternativeLanguage]
|
|
208
|
+
};
|
|
209
|
+
const phraseAltTranslations = allPhraseTranslations[alternativeLanguage];
|
|
210
|
+
|
|
211
|
+
for (const key of localKeys) {
|
|
212
|
+
var _phraseAltTranslation;
|
|
180
213
|
|
|
181
|
-
|
|
182
|
-
|
|
214
|
+
const phraseKey = core.getUniqueKey(key, loadedTranslation.namespace);
|
|
215
|
+
const phraseTranslationMessage = (_phraseAltTranslation = phraseAltTranslations[phraseKey]) === null || _phraseAltTranslation === void 0 ? void 0 : _phraseAltTranslation.message;
|
|
183
216
|
|
|
184
|
-
|
|
185
|
-
|
|
217
|
+
if (!phraseTranslationMessage) {
|
|
218
|
+
trace(`Missing translation. No translation for key ${key} in phrase as ${phraseKey} in language ${alternativeLanguage}.`);
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
186
221
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
222
|
+
altTranslations[key] = { ...altTranslations[key],
|
|
223
|
+
message: phraseTranslationMessage
|
|
224
|
+
};
|
|
190
225
|
}
|
|
191
226
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
227
|
+
const altTranslationFilePath = core.getAltLanguageFilePath(loadedTranslation.filePath, alternativeLanguage);
|
|
228
|
+
await mkdir(path__default['default'].dirname(altTranslationFilePath), {
|
|
229
|
+
recursive: true
|
|
230
|
+
});
|
|
231
|
+
await writeFile(altTranslationFilePath, `${JSON.stringify(altTranslations, null, 2)}\n`);
|
|
195
232
|
}
|
|
196
|
-
|
|
197
|
-
const altTranslationFilePath = core.getAltLanguageFilePath(loadedTranslation.filePath, alternativeLanguage);
|
|
198
|
-
await mkdir(path__default['default'].dirname(altTranslationFilePath), {
|
|
199
|
-
recursive: true
|
|
200
|
-
});
|
|
201
|
-
await writeFile(altTranslationFilePath, `${JSON.stringify(altTranslations, null, 2)}\n`);
|
|
202
233
|
}
|
|
203
234
|
}
|
|
204
235
|
}
|
|
@@ -207,7 +238,8 @@ async function pull({
|
|
|
207
238
|
* Uploading to the Phrase API for each language. Adding a unique namespace to each key using file path they key came from
|
|
208
239
|
*/
|
|
209
240
|
async function push({
|
|
210
|
-
branch
|
|
241
|
+
branch,
|
|
242
|
+
deleteUnusedKeys: deleteUnusedKeys$1
|
|
211
243
|
}, config) {
|
|
212
244
|
const allLanguageTranslations = await core.loadAllTranslations({
|
|
213
245
|
fallbacks: 'none',
|
|
@@ -240,7 +272,13 @@ async function push({
|
|
|
240
272
|
|
|
241
273
|
for (const language of allLanguages) {
|
|
242
274
|
if (phraseTranslations[language]) {
|
|
243
|
-
|
|
275
|
+
const {
|
|
276
|
+
uploadId
|
|
277
|
+
} = await pushTranslationsByLocale(phraseTranslations[language], language, branch);
|
|
278
|
+
|
|
279
|
+
if (deleteUnusedKeys$1) {
|
|
280
|
+
await deleteUnusedKeys(uploadId, language, branch);
|
|
281
|
+
}
|
|
244
282
|
}
|
|
245
283
|
}
|
|
246
284
|
}
|
|
@@ -44,14 +44,14 @@ function _callPhrase(path, options = {}) {
|
|
|
44
44
|
}).then(async response => {
|
|
45
45
|
console.log(`${path}: ${response.status} - ${response.statusText}`);
|
|
46
46
|
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
|
-
|
|
47
|
+
trace('\nLink:', response.headers.get('Link'), '\n'); // Print All Headers:
|
|
48
48
|
// console.log(Array.from(r.headers.entries()));
|
|
49
49
|
|
|
50
50
|
try {
|
|
51
51
|
var _response$headers$get;
|
|
52
52
|
|
|
53
53
|
const result = await response.json();
|
|
54
|
-
|
|
54
|
+
trace(`Internal Result (Length: ${result.length})\n`);
|
|
55
55
|
|
|
56
56
|
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
57
|
var _response$headers$get2, _response$headers$get3;
|
|
@@ -59,10 +59,10 @@ function _callPhrase(path, options = {}) {
|
|
|
59
59
|
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
60
|
|
|
61
61
|
if (!nextPageUrl) {
|
|
62
|
-
throw new Error('
|
|
62
|
+
throw new Error("Can't parse next page URL");
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
-
console.log('Results
|
|
65
|
+
console.log('Results received with next page: ', nextPageUrl);
|
|
66
66
|
const nextPageResult = await _callPhrase(nextPageUrl, options);
|
|
67
67
|
return [...result, ...nextPageResult];
|
|
68
68
|
}
|
|
@@ -83,7 +83,6 @@ async function callPhrase(relativePath, options = {}) {
|
|
|
83
83
|
}
|
|
84
84
|
|
|
85
85
|
return _callPhrase(`https://api.phrase.com/v2/projects/${projectId}/${relativePath}`, options).then(result => {
|
|
86
|
-
// console.log('Result:', result);
|
|
87
86
|
if (Array.isArray(result)) {
|
|
88
87
|
console.log('Result length:', result.length);
|
|
89
88
|
}
|
|
@@ -121,12 +120,35 @@ async function pushTranslationsByLocale(contents, locale, branch) {
|
|
|
121
120
|
formData.append('locale_id', locale);
|
|
122
121
|
formData.append('branch', branch);
|
|
123
122
|
formData.append('update_translations', 'true');
|
|
124
|
-
|
|
125
|
-
|
|
123
|
+
log('Starting to upload:', locale, '\n');
|
|
124
|
+
const {
|
|
125
|
+
id
|
|
126
|
+
} = await callPhrase(`uploads`, {
|
|
126
127
|
method: 'POST',
|
|
127
128
|
body: formData
|
|
128
129
|
});
|
|
130
|
+
log('Upload ID:', id, '\n');
|
|
129
131
|
log('Successfully Uploaded:', locale, '\n');
|
|
132
|
+
return {
|
|
133
|
+
uploadId: id
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
async function deleteUnusedKeys(uploadId, locale, branch) {
|
|
137
|
+
const query = `unmentioned_in_upload:${uploadId}`;
|
|
138
|
+
const {
|
|
139
|
+
records_affected
|
|
140
|
+
} = await callPhrase('keys', {
|
|
141
|
+
method: 'DELETE',
|
|
142
|
+
headers: {
|
|
143
|
+
'Content-Type': 'application/json'
|
|
144
|
+
},
|
|
145
|
+
body: JSON.stringify({
|
|
146
|
+
branch,
|
|
147
|
+
locale_id: locale,
|
|
148
|
+
q: query
|
|
149
|
+
})
|
|
150
|
+
});
|
|
151
|
+
log('Successfully deleted', records_affected, 'unused keys from branch', branch);
|
|
130
152
|
}
|
|
131
153
|
async function ensureBranch(branch) {
|
|
132
154
|
await callPhrase(`branches`, {
|
|
@@ -138,7 +160,7 @@ async function ensureBranch(branch) {
|
|
|
138
160
|
name: branch
|
|
139
161
|
})
|
|
140
162
|
});
|
|
141
|
-
|
|
163
|
+
log('Created branch:', branch);
|
|
142
164
|
}
|
|
143
165
|
|
|
144
166
|
async function pull({
|
|
@@ -149,6 +171,13 @@ async function pull({
|
|
|
149
171
|
const alternativeLanguages = core.getAltLanguages(config);
|
|
150
172
|
const allPhraseTranslations = await pullAllTranslations(branch);
|
|
151
173
|
trace(`Pulling translations from Phrase for languages ${config.devLanguage} and ${alternativeLanguages.join(', ')}`);
|
|
174
|
+
const phraseLanguages = Object.keys(allPhraseTranslations);
|
|
175
|
+
trace(`Found Phrase translations for languages ${phraseLanguages.join(', ')}`);
|
|
176
|
+
|
|
177
|
+
if (!phraseLanguages.includes(config.devLanguage)) {
|
|
178
|
+
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.`);
|
|
179
|
+
}
|
|
180
|
+
|
|
152
181
|
const allVocabTranslations = await core.loadAllTranslations({
|
|
153
182
|
fallbacks: 'none',
|
|
154
183
|
includeNodeModules: false
|
|
@@ -174,31 +203,33 @@ async function pull({
|
|
|
174
203
|
await writeFile(loadedTranslation.filePath, `${JSON.stringify(defaultValues, null, 2)}\n`);
|
|
175
204
|
|
|
176
205
|
for (const alternativeLanguage of alternativeLanguages) {
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
206
|
+
if (alternativeLanguage in allPhraseTranslations) {
|
|
207
|
+
const altTranslations = { ...loadedTranslation.languages[alternativeLanguage]
|
|
208
|
+
};
|
|
209
|
+
const phraseAltTranslations = allPhraseTranslations[alternativeLanguage];
|
|
210
|
+
|
|
211
|
+
for (const key of localKeys) {
|
|
212
|
+
var _phraseAltTranslation;
|
|
180
213
|
|
|
181
|
-
|
|
182
|
-
|
|
214
|
+
const phraseKey = core.getUniqueKey(key, loadedTranslation.namespace);
|
|
215
|
+
const phraseTranslationMessage = (_phraseAltTranslation = phraseAltTranslations[phraseKey]) === null || _phraseAltTranslation === void 0 ? void 0 : _phraseAltTranslation.message;
|
|
183
216
|
|
|
184
|
-
|
|
185
|
-
|
|
217
|
+
if (!phraseTranslationMessage) {
|
|
218
|
+
trace(`Missing translation. No translation for key ${key} in phrase as ${phraseKey} in language ${alternativeLanguage}.`);
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
186
221
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
222
|
+
altTranslations[key] = { ...altTranslations[key],
|
|
223
|
+
message: phraseTranslationMessage
|
|
224
|
+
};
|
|
190
225
|
}
|
|
191
226
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
227
|
+
const altTranslationFilePath = core.getAltLanguageFilePath(loadedTranslation.filePath, alternativeLanguage);
|
|
228
|
+
await mkdir(path__default['default'].dirname(altTranslationFilePath), {
|
|
229
|
+
recursive: true
|
|
230
|
+
});
|
|
231
|
+
await writeFile(altTranslationFilePath, `${JSON.stringify(altTranslations, null, 2)}\n`);
|
|
195
232
|
}
|
|
196
|
-
|
|
197
|
-
const altTranslationFilePath = core.getAltLanguageFilePath(loadedTranslation.filePath, alternativeLanguage);
|
|
198
|
-
await mkdir(path__default['default'].dirname(altTranslationFilePath), {
|
|
199
|
-
recursive: true
|
|
200
|
-
});
|
|
201
|
-
await writeFile(altTranslationFilePath, `${JSON.stringify(altTranslations, null, 2)}\n`);
|
|
202
233
|
}
|
|
203
234
|
}
|
|
204
235
|
}
|
|
@@ -207,7 +238,8 @@ async function pull({
|
|
|
207
238
|
* Uploading to the Phrase API for each language. Adding a unique namespace to each key using file path they key came from
|
|
208
239
|
*/
|
|
209
240
|
async function push({
|
|
210
|
-
branch
|
|
241
|
+
branch,
|
|
242
|
+
deleteUnusedKeys: deleteUnusedKeys$1
|
|
211
243
|
}, config) {
|
|
212
244
|
const allLanguageTranslations = await core.loadAllTranslations({
|
|
213
245
|
fallbacks: 'none',
|
|
@@ -240,7 +272,13 @@ async function push({
|
|
|
240
272
|
|
|
241
273
|
for (const language of allLanguages) {
|
|
242
274
|
if (phraseTranslations[language]) {
|
|
243
|
-
|
|
275
|
+
const {
|
|
276
|
+
uploadId
|
|
277
|
+
} = await pushTranslationsByLocale(phraseTranslations[language], language, branch);
|
|
278
|
+
|
|
279
|
+
if (deleteUnusedKeys$1) {
|
|
280
|
+
await deleteUnusedKeys(uploadId, language, branch);
|
|
281
|
+
}
|
|
244
282
|
}
|
|
245
283
|
}
|
|
246
284
|
}
|