@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/dist/vocab-phrase.esm.js
CHANGED
|
@@ -32,14 +32,14 @@ function _callPhrase(path, options = {}) {
|
|
|
32
32
|
}).then(async response => {
|
|
33
33
|
console.log(`${path}: ${response.status} - ${response.statusText}`);
|
|
34
34
|
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})`);
|
|
35
|
-
|
|
35
|
+
trace('\nLink:', response.headers.get('Link'), '\n'); // Print All Headers:
|
|
36
36
|
// console.log(Array.from(r.headers.entries()));
|
|
37
37
|
|
|
38
38
|
try {
|
|
39
39
|
var _response$headers$get;
|
|
40
40
|
|
|
41
41
|
const result = await response.json();
|
|
42
|
-
|
|
42
|
+
trace(`Internal Result (Length: ${result.length})\n`);
|
|
43
43
|
|
|
44
44
|
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')) {
|
|
45
45
|
var _response$headers$get2, _response$headers$get3;
|
|
@@ -47,10 +47,10 @@ function _callPhrase(path, options = {}) {
|
|
|
47
47
|
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 : [];
|
|
48
48
|
|
|
49
49
|
if (!nextPageUrl) {
|
|
50
|
-
throw new Error('
|
|
50
|
+
throw new Error("Can't parse next page URL");
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
-
console.log('Results
|
|
53
|
+
console.log('Results received with next page: ', nextPageUrl);
|
|
54
54
|
const nextPageResult = await _callPhrase(nextPageUrl, options);
|
|
55
55
|
return [...result, ...nextPageResult];
|
|
56
56
|
}
|
|
@@ -71,7 +71,6 @@ async function callPhrase(relativePath, options = {}) {
|
|
|
71
71
|
}
|
|
72
72
|
|
|
73
73
|
return _callPhrase(`https://api.phrase.com/v2/projects/${projectId}/${relativePath}`, options).then(result => {
|
|
74
|
-
// console.log('Result:', result);
|
|
75
74
|
if (Array.isArray(result)) {
|
|
76
75
|
console.log('Result length:', result.length);
|
|
77
76
|
}
|
|
@@ -109,12 +108,35 @@ async function pushTranslationsByLocale(contents, locale, branch) {
|
|
|
109
108
|
formData.append('locale_id', locale);
|
|
110
109
|
formData.append('branch', branch);
|
|
111
110
|
formData.append('update_translations', 'true');
|
|
112
|
-
|
|
113
|
-
|
|
111
|
+
log('Starting to upload:', locale, '\n');
|
|
112
|
+
const {
|
|
113
|
+
id
|
|
114
|
+
} = await callPhrase(`uploads`, {
|
|
114
115
|
method: 'POST',
|
|
115
116
|
body: formData
|
|
116
117
|
});
|
|
118
|
+
log('Upload ID:', id, '\n');
|
|
117
119
|
log('Successfully Uploaded:', locale, '\n');
|
|
120
|
+
return {
|
|
121
|
+
uploadId: id
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
async function deleteUnusedKeys(uploadId, locale, branch) {
|
|
125
|
+
const query = `unmentioned_in_upload:${uploadId}`;
|
|
126
|
+
const {
|
|
127
|
+
records_affected
|
|
128
|
+
} = await callPhrase('keys', {
|
|
129
|
+
method: 'DELETE',
|
|
130
|
+
headers: {
|
|
131
|
+
'Content-Type': 'application/json'
|
|
132
|
+
},
|
|
133
|
+
body: JSON.stringify({
|
|
134
|
+
branch,
|
|
135
|
+
locale_id: locale,
|
|
136
|
+
q: query
|
|
137
|
+
})
|
|
138
|
+
});
|
|
139
|
+
log('Successfully deleted', records_affected, 'unused keys from branch', branch);
|
|
118
140
|
}
|
|
119
141
|
async function ensureBranch(branch) {
|
|
120
142
|
await callPhrase(`branches`, {
|
|
@@ -126,7 +148,7 @@ async function ensureBranch(branch) {
|
|
|
126
148
|
name: branch
|
|
127
149
|
})
|
|
128
150
|
});
|
|
129
|
-
|
|
151
|
+
log('Created branch:', branch);
|
|
130
152
|
}
|
|
131
153
|
|
|
132
154
|
async function pull({
|
|
@@ -137,6 +159,13 @@ async function pull({
|
|
|
137
159
|
const alternativeLanguages = getAltLanguages(config);
|
|
138
160
|
const allPhraseTranslations = await pullAllTranslations(branch);
|
|
139
161
|
trace(`Pulling translations from Phrase for languages ${config.devLanguage} and ${alternativeLanguages.join(', ')}`);
|
|
162
|
+
const phraseLanguages = Object.keys(allPhraseTranslations);
|
|
163
|
+
trace(`Found Phrase translations for languages ${phraseLanguages.join(', ')}`);
|
|
164
|
+
|
|
165
|
+
if (!phraseLanguages.includes(config.devLanguage)) {
|
|
166
|
+
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.`);
|
|
167
|
+
}
|
|
168
|
+
|
|
140
169
|
const allVocabTranslations = await loadAllTranslations({
|
|
141
170
|
fallbacks: 'none',
|
|
142
171
|
includeNodeModules: false
|
|
@@ -162,31 +191,33 @@ async function pull({
|
|
|
162
191
|
await writeFile(loadedTranslation.filePath, `${JSON.stringify(defaultValues, null, 2)}\n`);
|
|
163
192
|
|
|
164
193
|
for (const alternativeLanguage of alternativeLanguages) {
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
194
|
+
if (alternativeLanguage in allPhraseTranslations) {
|
|
195
|
+
const altTranslations = { ...loadedTranslation.languages[alternativeLanguage]
|
|
196
|
+
};
|
|
197
|
+
const phraseAltTranslations = allPhraseTranslations[alternativeLanguage];
|
|
198
|
+
|
|
199
|
+
for (const key of localKeys) {
|
|
200
|
+
var _phraseAltTranslation;
|
|
168
201
|
|
|
169
|
-
|
|
170
|
-
|
|
202
|
+
const phraseKey = getUniqueKey(key, loadedTranslation.namespace);
|
|
203
|
+
const phraseTranslationMessage = (_phraseAltTranslation = phraseAltTranslations[phraseKey]) === null || _phraseAltTranslation === void 0 ? void 0 : _phraseAltTranslation.message;
|
|
171
204
|
|
|
172
|
-
|
|
173
|
-
|
|
205
|
+
if (!phraseTranslationMessage) {
|
|
206
|
+
trace(`Missing translation. No translation for key ${key} in phrase as ${phraseKey} in language ${alternativeLanguage}.`);
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
174
209
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
210
|
+
altTranslations[key] = { ...altTranslations[key],
|
|
211
|
+
message: phraseTranslationMessage
|
|
212
|
+
};
|
|
178
213
|
}
|
|
179
214
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
215
|
+
const altTranslationFilePath = getAltLanguageFilePath(loadedTranslation.filePath, alternativeLanguage);
|
|
216
|
+
await mkdir(path.dirname(altTranslationFilePath), {
|
|
217
|
+
recursive: true
|
|
218
|
+
});
|
|
219
|
+
await writeFile(altTranslationFilePath, `${JSON.stringify(altTranslations, null, 2)}\n`);
|
|
183
220
|
}
|
|
184
|
-
|
|
185
|
-
const altTranslationFilePath = getAltLanguageFilePath(loadedTranslation.filePath, alternativeLanguage);
|
|
186
|
-
await mkdir(path.dirname(altTranslationFilePath), {
|
|
187
|
-
recursive: true
|
|
188
|
-
});
|
|
189
|
-
await writeFile(altTranslationFilePath, `${JSON.stringify(altTranslations, null, 2)}\n`);
|
|
190
221
|
}
|
|
191
222
|
}
|
|
192
223
|
}
|
|
@@ -195,7 +226,8 @@ async function pull({
|
|
|
195
226
|
* Uploading to the Phrase API for each language. Adding a unique namespace to each key using file path they key came from
|
|
196
227
|
*/
|
|
197
228
|
async function push({
|
|
198
|
-
branch
|
|
229
|
+
branch,
|
|
230
|
+
deleteUnusedKeys: deleteUnusedKeys$1
|
|
199
231
|
}, config) {
|
|
200
232
|
const allLanguageTranslations = await loadAllTranslations({
|
|
201
233
|
fallbacks: 'none',
|
|
@@ -228,7 +260,13 @@ async function push({
|
|
|
228
260
|
|
|
229
261
|
for (const language of allLanguages) {
|
|
230
262
|
if (phraseTranslations[language]) {
|
|
231
|
-
|
|
263
|
+
const {
|
|
264
|
+
uploadId
|
|
265
|
+
} = await pushTranslationsByLocale(phraseTranslations[language], language, branch);
|
|
266
|
+
|
|
267
|
+
if (deleteUnusedKeys$1) {
|
|
268
|
+
await deleteUnusedKeys(uploadId, language, branch);
|
|
269
|
+
}
|
|
232
270
|
}
|
|
233
271
|
}
|
|
234
272
|
}
|
package/package.json
CHANGED
package/src/phrase-api.ts
CHANGED
|
@@ -31,14 +31,14 @@ function _callPhrase(path: string, options: Parameters<typeof fetch>[1] = {}) {
|
|
|
31
31
|
'X-Rate-Limit-Reset',
|
|
32
32
|
)} seconds remaining})`,
|
|
33
33
|
);
|
|
34
|
-
|
|
34
|
+
trace('\nLink:', response.headers.get('Link'), '\n');
|
|
35
35
|
// Print All Headers:
|
|
36
36
|
// console.log(Array.from(r.headers.entries()));
|
|
37
37
|
|
|
38
38
|
try {
|
|
39
39
|
const result = await response.json();
|
|
40
40
|
|
|
41
|
-
|
|
41
|
+
trace(`Internal Result (Length: ${result.length})\n`);
|
|
42
42
|
|
|
43
43
|
if (
|
|
44
44
|
(!options.method || options.method === 'GET') &&
|
|
@@ -48,10 +48,10 @@ function _callPhrase(path: string, options: Parameters<typeof fetch>[1] = {}) {
|
|
|
48
48
|
response.headers.get('Link')?.match(/<([^>]*)>; rel=next/) ?? [];
|
|
49
49
|
|
|
50
50
|
if (!nextPageUrl) {
|
|
51
|
-
throw new Error('
|
|
51
|
+
throw new Error("Can't parse next page URL");
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
-
console.log('Results
|
|
54
|
+
console.log('Results received with next page: ', nextPageUrl);
|
|
55
55
|
|
|
56
56
|
const nextPageResult = (await _callPhrase(nextPageUrl, options)) as any;
|
|
57
57
|
|
|
@@ -66,10 +66,10 @@ function _callPhrase(path: string, options: Parameters<typeof fetch>[1] = {}) {
|
|
|
66
66
|
});
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
-
export async function callPhrase(
|
|
69
|
+
export async function callPhrase<T = any>(
|
|
70
70
|
relativePath: string,
|
|
71
71
|
options: Parameters<typeof fetch>[1] = {},
|
|
72
|
-
) {
|
|
72
|
+
): Promise<T> {
|
|
73
73
|
const projectId = process.env.PHRASE_PROJECT_ID;
|
|
74
74
|
|
|
75
75
|
if (!projectId) {
|
|
@@ -80,7 +80,6 @@ export async function callPhrase(
|
|
|
80
80
|
options,
|
|
81
81
|
)
|
|
82
82
|
.then((result) => {
|
|
83
|
-
// console.log('Result:', result);
|
|
84
83
|
if (Array.isArray(result)) {
|
|
85
84
|
console.log('Result length:', result.length);
|
|
86
85
|
}
|
|
@@ -95,18 +94,23 @@ export async function callPhrase(
|
|
|
95
94
|
export async function pullAllTranslations(
|
|
96
95
|
branch: string,
|
|
97
96
|
): Promise<TranslationsByLanguage> {
|
|
98
|
-
const phraseResult
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
97
|
+
const phraseResult = await callPhrase<
|
|
98
|
+
Array<{
|
|
99
|
+
key: { name: string };
|
|
100
|
+
locale: { code: string };
|
|
101
|
+
content: string;
|
|
102
|
+
}>
|
|
103
|
+
>(`translations?branch=${branch}&per_page=100`);
|
|
104
|
+
|
|
103
105
|
const translations: TranslationsByLanguage = {};
|
|
106
|
+
|
|
104
107
|
for (const r of phraseResult) {
|
|
105
108
|
if (!translations[r.locale.code]) {
|
|
106
109
|
translations[r.locale.code] = {};
|
|
107
110
|
}
|
|
108
111
|
translations[r.locale.code][r.key.name] = { message: r.content };
|
|
109
112
|
}
|
|
113
|
+
|
|
110
114
|
return translations;
|
|
111
115
|
}
|
|
112
116
|
|
|
@@ -127,13 +131,41 @@ export async function pushTranslationsByLocale(
|
|
|
127
131
|
formData.append('branch', branch);
|
|
128
132
|
formData.append('update_translations', 'true');
|
|
129
133
|
|
|
130
|
-
|
|
134
|
+
log('Starting to upload:', locale, '\n');
|
|
131
135
|
|
|
132
|
-
await callPhrase(`uploads`, {
|
|
136
|
+
const { id } = await callPhrase<{ id: string }>(`uploads`, {
|
|
133
137
|
method: 'POST',
|
|
134
138
|
body: formData,
|
|
135
139
|
});
|
|
140
|
+
log('Upload ID:', id, '\n');
|
|
136
141
|
log('Successfully Uploaded:', locale, '\n');
|
|
142
|
+
|
|
143
|
+
return { uploadId: id };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export async function deleteUnusedKeys(
|
|
147
|
+
uploadId: string,
|
|
148
|
+
locale: string,
|
|
149
|
+
branch: string,
|
|
150
|
+
) {
|
|
151
|
+
const query = `unmentioned_in_upload:${uploadId}`;
|
|
152
|
+
const { records_affected } = await callPhrase<{ records_affected: number }>(
|
|
153
|
+
'keys',
|
|
154
|
+
{
|
|
155
|
+
method: 'DELETE',
|
|
156
|
+
headers: {
|
|
157
|
+
'Content-Type': 'application/json',
|
|
158
|
+
},
|
|
159
|
+
body: JSON.stringify({ branch, locale_id: locale, q: query }),
|
|
160
|
+
},
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
log(
|
|
164
|
+
'Successfully deleted',
|
|
165
|
+
records_affected,
|
|
166
|
+
'unused keys from branch',
|
|
167
|
+
branch,
|
|
168
|
+
);
|
|
137
169
|
}
|
|
138
170
|
|
|
139
171
|
export async function ensureBranch(branch: string) {
|
|
@@ -144,5 +176,6 @@ export async function ensureBranch(branch: string) {
|
|
|
144
176
|
},
|
|
145
177
|
body: JSON.stringify({ name: branch }),
|
|
146
178
|
});
|
|
147
|
-
|
|
179
|
+
|
|
180
|
+
log('Created branch:', branch);
|
|
148
181
|
}
|
|
@@ -2,6 +2,7 @@ import path from 'path';
|
|
|
2
2
|
import { pull } from './pull-translations';
|
|
3
3
|
import { pullAllTranslations } from './phrase-api';
|
|
4
4
|
import { writeFile } from './file';
|
|
5
|
+
import { GeneratedLanguageTarget, LanguageTarget } from '@vocab/types';
|
|
5
6
|
|
|
6
7
|
jest.mock('./file', () => ({
|
|
7
8
|
writeFile: jest.fn(() => Promise.resolve),
|
|
@@ -13,68 +14,201 @@ jest.mock('./phrase-api', () => ({
|
|
|
13
14
|
pullAllTranslations: jest.fn(() => Promise.resolve({ en: {}, fr: {} })),
|
|
14
15
|
}));
|
|
15
16
|
|
|
16
|
-
|
|
17
|
+
const devLanguage = 'en';
|
|
18
|
+
|
|
19
|
+
function runPhrase(options: {
|
|
20
|
+
languages: LanguageTarget[];
|
|
21
|
+
generatedLanguages: GeneratedLanguageTarget[];
|
|
22
|
+
}) {
|
|
17
23
|
return pull(
|
|
18
24
|
{ branch: 'tester' },
|
|
19
25
|
{
|
|
20
|
-
|
|
21
|
-
|
|
26
|
+
...options,
|
|
27
|
+
devLanguage,
|
|
22
28
|
projectRoot: path.resolve(__dirname, '..', '..', '..', 'fixtures/phrase'),
|
|
23
29
|
},
|
|
24
30
|
);
|
|
25
31
|
}
|
|
26
32
|
|
|
27
|
-
describe('pull', () => {
|
|
28
|
-
|
|
29
|
-
(
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
33
|
+
describe('pull translations', () => {
|
|
34
|
+
describe('when pulling translations for languages that already have translations', () => {
|
|
35
|
+
beforeEach(() => {
|
|
36
|
+
jest.mocked(pullAllTranslations).mockClear();
|
|
37
|
+
jest.mocked(writeFile).mockClear();
|
|
38
|
+
jest.mocked(pullAllTranslations).mockImplementation(() =>
|
|
39
|
+
Promise.resolve({
|
|
40
|
+
en: {
|
|
41
|
+
'hello.mytranslations': {
|
|
42
|
+
message: 'Hi there',
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
fr: {
|
|
46
|
+
'hello.mytranslations': {
|
|
47
|
+
message: 'merci',
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
}),
|
|
51
|
+
);
|
|
52
|
+
});
|
|
34
53
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
message:
|
|
54
|
+
const options = {
|
|
55
|
+
languages: [{ name: 'en' }, { name: 'fr' }],
|
|
56
|
+
generatedLanguages: [
|
|
57
|
+
{
|
|
58
|
+
name: 'generatedLanguage',
|
|
59
|
+
extends: 'en',
|
|
60
|
+
generator: {
|
|
61
|
+
transformMessage: (message: string) => `[${message}]`,
|
|
43
62
|
},
|
|
44
63
|
},
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
64
|
+
],
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
it('should resolve', async () => {
|
|
68
|
+
await expect(runPhrase(options)).resolves.toBeUndefined();
|
|
69
|
+
|
|
70
|
+
expect(jest.mocked(writeFile)).toHaveBeenCalledTimes(2);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('should update keys', async () => {
|
|
74
|
+
await expect(runPhrase(options)).resolves.toBeUndefined();
|
|
75
|
+
|
|
76
|
+
expect(
|
|
77
|
+
jest
|
|
78
|
+
.mocked(writeFile)
|
|
79
|
+
.mock.calls.map(([_filePath, contents]) =>
|
|
80
|
+
JSON.parse(contents as string),
|
|
81
|
+
),
|
|
82
|
+
).toMatchInlineSnapshot(`
|
|
83
|
+
[
|
|
84
|
+
{
|
|
85
|
+
"hello": {
|
|
86
|
+
"message": "Hi there",
|
|
87
|
+
},
|
|
88
|
+
"world": {
|
|
89
|
+
"message": "world",
|
|
90
|
+
},
|
|
48
91
|
},
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
)
|
|
59
|
-
)
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
92
|
+
{
|
|
93
|
+
"hello": {
|
|
94
|
+
"message": "merci",
|
|
95
|
+
},
|
|
96
|
+
"world": {
|
|
97
|
+
"message": "monde",
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
]
|
|
101
|
+
`);
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
describe('when pulling translations and some languages do not have any translations', () => {
|
|
106
|
+
beforeEach(() => {
|
|
107
|
+
jest.mocked(pullAllTranslations).mockClear();
|
|
108
|
+
jest.mocked(writeFile).mockClear();
|
|
109
|
+
jest.mocked(pullAllTranslations).mockImplementation(() =>
|
|
110
|
+
Promise.resolve({
|
|
111
|
+
en: {
|
|
112
|
+
'hello.mytranslations': {
|
|
113
|
+
message: 'Hi there',
|
|
114
|
+
},
|
|
64
115
|
},
|
|
65
|
-
|
|
66
|
-
|
|
116
|
+
fr: {
|
|
117
|
+
'hello.mytranslations': {
|
|
118
|
+
message: 'merci',
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
}),
|
|
122
|
+
);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
const options = {
|
|
126
|
+
languages: [{ name: 'en' }, { name: 'fr' }, { name: 'ja' }],
|
|
127
|
+
generatedLanguages: [
|
|
128
|
+
{
|
|
129
|
+
name: 'generatedLanguage',
|
|
130
|
+
extends: 'en',
|
|
131
|
+
generator: {
|
|
132
|
+
transformMessage: (message: string) => `[${message}]`,
|
|
67
133
|
},
|
|
68
134
|
},
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
135
|
+
],
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
it('should resolve', async () => {
|
|
139
|
+
await expect(runPhrase(options)).resolves.toBeUndefined();
|
|
140
|
+
|
|
141
|
+
expect(jest.mocked(writeFile)).toHaveBeenCalledTimes(2);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('should update keys', async () => {
|
|
145
|
+
await expect(runPhrase(options)).resolves.toBeUndefined();
|
|
146
|
+
|
|
147
|
+
expect(
|
|
148
|
+
jest
|
|
149
|
+
.mocked(writeFile)
|
|
150
|
+
.mock.calls.map(([_filePath, contents]) =>
|
|
151
|
+
JSON.parse(contents as string),
|
|
152
|
+
),
|
|
153
|
+
).toMatchInlineSnapshot(`
|
|
154
|
+
[
|
|
155
|
+
{
|
|
156
|
+
"hello": {
|
|
157
|
+
"message": "Hi there",
|
|
158
|
+
},
|
|
159
|
+
"world": {
|
|
160
|
+
"message": "world",
|
|
161
|
+
},
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
"hello": {
|
|
165
|
+
"message": "merci",
|
|
166
|
+
},
|
|
167
|
+
"world": {
|
|
168
|
+
"message": "monde",
|
|
169
|
+
},
|
|
72
170
|
},
|
|
73
|
-
|
|
74
|
-
|
|
171
|
+
]
|
|
172
|
+
`);
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
describe('when pulling translations and the project has not configured translations for the dev language', () => {
|
|
177
|
+
beforeEach(() => {
|
|
178
|
+
jest.mocked(pullAllTranslations).mockClear();
|
|
179
|
+
jest.mocked(writeFile).mockClear();
|
|
180
|
+
jest.mocked(pullAllTranslations).mockImplementation(() =>
|
|
181
|
+
Promise.resolve({
|
|
182
|
+
fr: {
|
|
183
|
+
'hello.mytranslations': {
|
|
184
|
+
message: 'merci',
|
|
185
|
+
},
|
|
186
|
+
},
|
|
187
|
+
}),
|
|
188
|
+
);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
const options = {
|
|
192
|
+
languages: [{ name: 'en' }, { name: 'fr' }],
|
|
193
|
+
generatedLanguages: [
|
|
194
|
+
{
|
|
195
|
+
name: 'generatedLanguage',
|
|
196
|
+
extends: 'en',
|
|
197
|
+
generator: {
|
|
198
|
+
transformMessage: (message: string) => `[${message}]`,
|
|
75
199
|
},
|
|
76
200
|
},
|
|
77
|
-
]
|
|
78
|
-
|
|
201
|
+
],
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
it('should throw an error', async () => {
|
|
205
|
+
await expect(runPhrase(options)).rejects.toThrow(
|
|
206
|
+
new Error(
|
|
207
|
+
`Phrase did not return any translations for the configured development language "en".\nPlease ensure this language is present in your Phrase project's configuration.`,
|
|
208
|
+
),
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
expect(jest.mocked(writeFile)).toHaveBeenCalledTimes(0);
|
|
212
|
+
});
|
|
79
213
|
});
|
|
80
214
|
});
|
package/src/pull-translations.ts
CHANGED
|
@@ -14,6 +14,7 @@ import { trace } from './logger';
|
|
|
14
14
|
|
|
15
15
|
interface PullOptions {
|
|
16
16
|
branch?: string;
|
|
17
|
+
deleteUnusedKeys?: boolean;
|
|
17
18
|
}
|
|
18
19
|
|
|
19
20
|
export async function pull(
|
|
@@ -30,6 +31,17 @@ export async function pull(
|
|
|
30
31
|
} and ${alternativeLanguages.join(', ')}`,
|
|
31
32
|
);
|
|
32
33
|
|
|
34
|
+
const phraseLanguages = Object.keys(allPhraseTranslations);
|
|
35
|
+
trace(
|
|
36
|
+
`Found Phrase translations for languages ${phraseLanguages.join(', ')}`,
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
if (!phraseLanguages.includes(config.devLanguage)) {
|
|
40
|
+
throw new Error(
|
|
41
|
+
`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.`,
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
33
45
|
const allVocabTranslations = await loadAllTranslations(
|
|
34
46
|
{ fallbacks: 'none', includeNodeModules: false },
|
|
35
47
|
config,
|
|
@@ -59,41 +71,44 @@ export async function pull(
|
|
|
59
71
|
);
|
|
60
72
|
|
|
61
73
|
for (const alternativeLanguage of alternativeLanguages) {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
74
|
+
if (alternativeLanguage in allPhraseTranslations) {
|
|
75
|
+
const altTranslations = {
|
|
76
|
+
...loadedTranslation.languages[alternativeLanguage],
|
|
77
|
+
};
|
|
78
|
+
const phraseAltTranslations =
|
|
79
|
+
allPhraseTranslations[alternativeLanguage];
|
|
80
|
+
|
|
81
|
+
for (const key of localKeys) {
|
|
82
|
+
const phraseKey = getUniqueKey(key, loadedTranslation.namespace);
|
|
83
|
+
const phraseTranslationMessage =
|
|
84
|
+
phraseAltTranslations[phraseKey]?.message;
|
|
85
|
+
|
|
86
|
+
if (!phraseTranslationMessage) {
|
|
87
|
+
trace(
|
|
88
|
+
`Missing translation. No translation for key ${key} in phrase as ${phraseKey} in language ${alternativeLanguage}.`,
|
|
89
|
+
);
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
altTranslations[key] = {
|
|
94
|
+
...altTranslations[key],
|
|
95
|
+
message: phraseTranslationMessage,
|
|
96
|
+
};
|
|
77
97
|
}
|
|
78
98
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
}
|
|
99
|
+
const altTranslationFilePath = getAltLanguageFilePath(
|
|
100
|
+
loadedTranslation.filePath,
|
|
101
|
+
alternativeLanguage,
|
|
102
|
+
);
|
|
84
103
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
}
|
|
93
|
-
await writeFile(
|
|
94
|
-
altTranslationFilePath,
|
|
95
|
-
`${JSON.stringify(altTranslations, null, 2)}\n`,
|
|
96
|
-
);
|
|
104
|
+
await mkdir(path.dirname(altTranslationFilePath), {
|
|
105
|
+
recursive: true,
|
|
106
|
+
});
|
|
107
|
+
await writeFile(
|
|
108
|
+
altTranslationFilePath,
|
|
109
|
+
`${JSON.stringify(altTranslations, null, 2)}\n`,
|
|
110
|
+
);
|
|
111
|
+
}
|
|
97
112
|
}
|
|
98
113
|
}
|
|
99
114
|
}
|