@vocab/phrase 0.0.0-package-files-20231142931 → 0.0.0-push-split-translation-files-20230508031119
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/LICENSE +21 -0
- package/dist/declarations/src/csv.d.ts +9 -0
- package/dist/declarations/src/file.d.ts +4 -4
- package/dist/declarations/src/index.d.ts +2 -2
- package/dist/declarations/src/logger.d.ts +3 -3
- package/dist/declarations/src/phrase-api.d.ts +12 -10
- package/dist/declarations/src/pull-translations.d.ts +7 -7
- package/dist/declarations/src/push-translations.d.ts +11 -10
- package/dist/vocab-phrase.cjs.dev.js +141 -77
- package/dist/vocab-phrase.cjs.prod.js +141 -77
- package/dist/vocab-phrase.esm.js +138 -74
- package/package.json +9 -8
- package/CHANGELOG.md +0 -154
|
@@ -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
|
|
|
@@ -21,20 +22,66 @@ var debug__default = /*#__PURE__*/_interopDefault(debug);
|
|
|
21
22
|
const mkdir = fs.promises.mkdir;
|
|
22
23
|
const writeFile = fs.promises.writeFile;
|
|
23
24
|
|
|
24
|
-
const trace = debug__default[
|
|
25
|
+
const trace = debug__default["default"](`vocab:phrase`);
|
|
25
26
|
const log = (...params) => {
|
|
26
27
|
// eslint-disable-next-line no-console
|
|
27
|
-
console.log(chalk__default[
|
|
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);
|
|
34
|
+
// Ensure languages are ordered for locale mapping
|
|
35
|
+
// Might not need this anymore?
|
|
36
|
+
// const orderedLanguages = [devLanguage, ...altLanguages];
|
|
37
|
+
|
|
38
|
+
const devLanguageTranslations = translations[devLanguage];
|
|
39
|
+
const csvFilesByLanguage = Object.fromEntries(languages.map(language => [language, []]));
|
|
40
|
+
Object.entries(devLanguageTranslations).map(([key, {
|
|
41
|
+
message,
|
|
42
|
+
description,
|
|
43
|
+
tags
|
|
44
|
+
}]) => {
|
|
45
|
+
const sharedData = [key, description, tags === null || tags === void 0 ? void 0 : tags.join(',')];
|
|
46
|
+
const devLanguageRow = [...sharedData, message];
|
|
47
|
+
csvFilesByLanguage[devLanguage].push(devLanguageRow);
|
|
48
|
+
altLanguages.map(language => {
|
|
49
|
+
var _translations$languag, _translations$languag2;
|
|
50
|
+
const altTranslationMessage = (_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;
|
|
51
|
+
if (altTranslationMessage) {
|
|
52
|
+
csvFilesByLanguage[language].push([...sharedData, altTranslationMessage]);
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
const csvFilesWithKeys = Object.fromEntries(Object.entries(csvFilesByLanguage).filter(([_, csvFile]) => csvFile.length > 0));
|
|
57
|
+
const csvFileStrings = Object.fromEntries(Object.entries(csvFilesWithKeys).map(([language, csvFile]) => {
|
|
58
|
+
const csvFileString = sync.stringify(csvFile, {
|
|
59
|
+
delimiter: ',',
|
|
60
|
+
header: false
|
|
61
|
+
});
|
|
62
|
+
return [language, csvFileString];
|
|
63
|
+
}));
|
|
64
|
+
|
|
65
|
+
// Column indices start at 1
|
|
66
|
+
const keyIndex = 1;
|
|
67
|
+
const commentIndex = keyIndex + 1;
|
|
68
|
+
const tagColumn = commentIndex + 1;
|
|
69
|
+
return {
|
|
70
|
+
csvFileStrings,
|
|
71
|
+
keyIndex,
|
|
72
|
+
commentIndex,
|
|
73
|
+
tagColumn
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/* eslint-disable no-console */
|
|
30
78
|
function _callPhrase(path, options = {}) {
|
|
31
79
|
const phraseApiToken = process.env.PHRASE_API_TOKEN;
|
|
32
|
-
|
|
33
80
|
if (!phraseApiToken) {
|
|
34
81
|
throw new Error('Missing PHRASE_API_TOKEN');
|
|
35
82
|
}
|
|
36
|
-
|
|
37
|
-
|
|
83
|
+
return fetch__default["default"](path, {
|
|
84
|
+
...options,
|
|
38
85
|
headers: {
|
|
39
86
|
Authorization: `token ${phraseApiToken}`,
|
|
40
87
|
// Provide identification via User Agent as requested in https://developers.phrase.com/api/#overview--identification-via-user-agent
|
|
@@ -43,30 +90,26 @@ function _callPhrase(path, options = {}) {
|
|
|
43
90
|
}
|
|
44
91
|
}).then(async response => {
|
|
45
92
|
console.log(`${path}: ${response.status} - ${response.statusText}`);
|
|
46
|
-
|
|
47
|
-
|
|
93
|
+
const secondsUntilLimitReset = Math.ceil(Number.parseFloat(response.headers.get('X-Rate-Limit-Reset') || '0') - Date.now() / 1000);
|
|
94
|
+
console.log(`Rate Limit: ${response.headers.get('X-Rate-Limit-Remaining')} of ${response.headers.get('X-Rate-Limit-Limit')} remaining. (${secondsUntilLimitReset} seconds remaining)`);
|
|
95
|
+
trace('\nLink:', response.headers.get('Link'), '\n');
|
|
96
|
+
// Print All Headers:
|
|
48
97
|
// console.log(Array.from(r.headers.entries()));
|
|
49
98
|
|
|
50
99
|
try {
|
|
51
100
|
var _response$headers$get;
|
|
52
|
-
|
|
53
101
|
const result = await response.json();
|
|
54
102
|
trace(`Internal Result (Length: ${result.length})\n`);
|
|
55
|
-
|
|
56
103
|
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
104
|
var _response$headers$get2, _response$headers$get3;
|
|
58
|
-
|
|
59
105
|
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
|
-
|
|
61
106
|
if (!nextPageUrl) {
|
|
62
107
|
throw new Error("Can't parse next page URL");
|
|
63
108
|
}
|
|
64
|
-
|
|
65
109
|
console.log('Results received with next page: ', nextPageUrl);
|
|
66
110
|
const nextPageResult = await _callPhrase(nextPageUrl, options);
|
|
67
111
|
return [...result, ...nextPageResult];
|
|
68
112
|
}
|
|
69
|
-
|
|
70
113
|
return result;
|
|
71
114
|
} catch (e) {
|
|
72
115
|
console.error('Unable to parse response as JSON', e);
|
|
@@ -74,19 +117,15 @@ function _callPhrase(path, options = {}) {
|
|
|
74
117
|
}
|
|
75
118
|
});
|
|
76
119
|
}
|
|
77
|
-
|
|
78
120
|
async function callPhrase(relativePath, options = {}) {
|
|
79
121
|
const projectId = process.env.PHRASE_PROJECT_ID;
|
|
80
|
-
|
|
81
122
|
if (!projectId) {
|
|
82
123
|
throw new Error('Missing PHRASE_PROJECT_ID');
|
|
83
124
|
}
|
|
84
|
-
|
|
85
125
|
return _callPhrase(`https://api.phrase.com/v2/projects/${projectId}/${relativePath}`, options).then(result => {
|
|
86
126
|
if (Array.isArray(result)) {
|
|
87
127
|
console.log('Result length:', result.length);
|
|
88
128
|
}
|
|
89
|
-
|
|
90
129
|
return result;
|
|
91
130
|
}).catch(error => {
|
|
92
131
|
console.error(`Error calling phrase for ${relativePath}:`, error);
|
|
@@ -96,44 +135,64 @@ async function callPhrase(relativePath, options = {}) {
|
|
|
96
135
|
async function pullAllTranslations(branch) {
|
|
97
136
|
const phraseResult = await callPhrase(`translations?branch=${branch}&per_page=100`);
|
|
98
137
|
const translations = {};
|
|
99
|
-
|
|
100
138
|
for (const r of phraseResult) {
|
|
101
139
|
if (!translations[r.locale.code]) {
|
|
102
140
|
translations[r.locale.code] = {};
|
|
103
141
|
}
|
|
104
|
-
|
|
105
142
|
translations[r.locale.code][r.key.name] = {
|
|
106
143
|
message: r.content
|
|
107
144
|
};
|
|
108
145
|
}
|
|
109
|
-
|
|
110
146
|
return translations;
|
|
111
147
|
}
|
|
112
|
-
async function
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
contentType: 'application/json',
|
|
117
|
-
filename: `${locale}.json`
|
|
118
|
-
});
|
|
119
|
-
formData.append('file_format', 'json');
|
|
120
|
-
formData.append('locale_id', locale);
|
|
121
|
-
formData.append('branch', branch);
|
|
122
|
-
formData.append('update_translations', 'true');
|
|
123
|
-
log('Starting to upload:', locale, '\n');
|
|
148
|
+
async function pushTranslations(translationsByLanguage, {
|
|
149
|
+
devLanguage,
|
|
150
|
+
branch
|
|
151
|
+
}) {
|
|
124
152
|
const {
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
|
|
153
|
+
csvFileStrings,
|
|
154
|
+
keyIndex,
|
|
155
|
+
commentIndex,
|
|
156
|
+
tagColumn
|
|
157
|
+
} = translationsToCsv(translationsByLanguage, devLanguage);
|
|
158
|
+
const uploadIds = [];
|
|
159
|
+
for (const [language, csvFileString] of Object.entries(csvFileStrings)) {
|
|
160
|
+
const formData = new FormData__default["default"]();
|
|
161
|
+
const fileContents = Buffer.from(csvFileString);
|
|
162
|
+
formData.append('file', fileContents, {
|
|
163
|
+
contentType: 'text/csv',
|
|
164
|
+
filename: 'translations.csv'
|
|
165
|
+
});
|
|
166
|
+
formData.append('file_format', 'csv');
|
|
167
|
+
formData.append('branch', branch);
|
|
168
|
+
formData.append('update_translations', 'true');
|
|
169
|
+
formData.append('update_descriptions', 'true');
|
|
170
|
+
formData.append('locale_id', language);
|
|
171
|
+
formData.append('format_options[key_index]', keyIndex);
|
|
172
|
+
formData.append('format_options[comment_index]', commentIndex);
|
|
173
|
+
formData.append('format_options[tag_column]', tagColumn);
|
|
174
|
+
formData.append('format_options[enable_pluralization]', 'false');
|
|
175
|
+
log(`Uploading translations for language ${language}`);
|
|
176
|
+
const result = await callPhrase(`uploads`, {
|
|
177
|
+
method: 'POST',
|
|
178
|
+
body: formData
|
|
179
|
+
});
|
|
180
|
+
trace('Upload result:\n', result);
|
|
181
|
+
if (result && 'id' in result) {
|
|
182
|
+
log('Upload ID:', result.id, '\n');
|
|
183
|
+
log('Successfully Uploaded\n');
|
|
184
|
+
} else {
|
|
185
|
+
log(`Error uploading: ${result === null || result === void 0 ? void 0 : result.message}\n`);
|
|
186
|
+
log('Response:', result);
|
|
187
|
+
throw new Error('Error uploading');
|
|
188
|
+
}
|
|
189
|
+
uploadIds.push(result.id);
|
|
190
|
+
}
|
|
132
191
|
return {
|
|
133
|
-
|
|
192
|
+
uploadIds
|
|
134
193
|
};
|
|
135
194
|
}
|
|
136
|
-
async function deleteUnusedKeys(uploadId,
|
|
195
|
+
async function deleteUnusedKeys(uploadId, branch) {
|
|
137
196
|
const query = `unmentioned_in_upload:${uploadId}`;
|
|
138
197
|
const {
|
|
139
198
|
records_affected
|
|
@@ -144,7 +203,6 @@ async function deleteUnusedKeys(uploadId, locale, branch) {
|
|
|
144
203
|
},
|
|
145
204
|
body: JSON.stringify({
|
|
146
205
|
branch,
|
|
147
|
-
locale_id: locale,
|
|
148
206
|
q: query
|
|
149
207
|
})
|
|
150
208
|
});
|
|
@@ -173,59 +231,56 @@ async function pull({
|
|
|
173
231
|
trace(`Pulling translations from Phrase for languages ${config.devLanguage} and ${alternativeLanguages.join(', ')}`);
|
|
174
232
|
const phraseLanguages = Object.keys(allPhraseTranslations);
|
|
175
233
|
trace(`Found Phrase translations for languages ${phraseLanguages.join(', ')}`);
|
|
176
|
-
|
|
177
234
|
if (!phraseLanguages.includes(config.devLanguage)) {
|
|
178
235
|
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
236
|
}
|
|
180
|
-
|
|
181
237
|
const allVocabTranslations = await core.loadAllTranslations({
|
|
182
238
|
fallbacks: 'none',
|
|
183
|
-
includeNodeModules: false
|
|
239
|
+
includeNodeModules: false,
|
|
240
|
+
withTags: true
|
|
184
241
|
}, config);
|
|
185
|
-
|
|
186
242
|
for (const loadedTranslation of allVocabTranslations) {
|
|
187
243
|
const devTranslations = loadedTranslation.languages[config.devLanguage];
|
|
188
|
-
|
|
189
244
|
if (!devTranslations) {
|
|
190
245
|
throw new Error('No dev language translations loaded');
|
|
191
246
|
}
|
|
192
|
-
|
|
193
|
-
|
|
247
|
+
const defaultValues = {
|
|
248
|
+
...devTranslations
|
|
194
249
|
};
|
|
195
250
|
const localKeys = Object.keys(defaultValues);
|
|
196
|
-
|
|
197
251
|
for (const key of localKeys) {
|
|
198
|
-
defaultValues[key] = {
|
|
252
|
+
defaultValues[key] = {
|
|
253
|
+
...defaultValues[key],
|
|
199
254
|
...allPhraseTranslations[config.devLanguage][core.getUniqueKey(key, loadedTranslation.namespace)]
|
|
200
255
|
};
|
|
201
256
|
}
|
|
202
257
|
|
|
258
|
+
// Only write a `_meta` field if necessary
|
|
259
|
+
if (Object.keys(loadedTranslation.metadata).length > 0) {
|
|
260
|
+
defaultValues._meta = loadedTranslation.metadata;
|
|
261
|
+
}
|
|
203
262
|
await writeFile(loadedTranslation.filePath, `${JSON.stringify(defaultValues, null, 2)}\n`);
|
|
204
|
-
|
|
205
263
|
for (const alternativeLanguage of alternativeLanguages) {
|
|
206
264
|
if (alternativeLanguage in allPhraseTranslations) {
|
|
207
|
-
const altTranslations = {
|
|
265
|
+
const altTranslations = {
|
|
266
|
+
...loadedTranslation.languages[alternativeLanguage]
|
|
208
267
|
};
|
|
209
268
|
const phraseAltTranslations = allPhraseTranslations[alternativeLanguage];
|
|
210
|
-
|
|
211
269
|
for (const key of localKeys) {
|
|
212
270
|
var _phraseAltTranslation;
|
|
213
|
-
|
|
214
271
|
const phraseKey = core.getUniqueKey(key, loadedTranslation.namespace);
|
|
215
272
|
const phraseTranslationMessage = (_phraseAltTranslation = phraseAltTranslations[phraseKey]) === null || _phraseAltTranslation === void 0 ? void 0 : _phraseAltTranslation.message;
|
|
216
|
-
|
|
217
273
|
if (!phraseTranslationMessage) {
|
|
218
274
|
trace(`Missing translation. No translation for key ${key} in phrase as ${phraseKey} in language ${alternativeLanguage}.`);
|
|
219
275
|
continue;
|
|
220
276
|
}
|
|
221
|
-
|
|
222
|
-
|
|
277
|
+
altTranslations[key] = {
|
|
278
|
+
...altTranslations[key],
|
|
223
279
|
message: phraseTranslationMessage
|
|
224
280
|
};
|
|
225
281
|
}
|
|
226
|
-
|
|
227
282
|
const altTranslationFilePath = core.getAltLanguageFilePath(loadedTranslation.filePath, alternativeLanguage);
|
|
228
|
-
await mkdir(path__default[
|
|
283
|
+
await mkdir(path__default["default"].dirname(altTranslationFilePath), {
|
|
229
284
|
recursive: true
|
|
230
285
|
});
|
|
231
286
|
await writeFile(altTranslationFilePath, `${JSON.stringify(altTranslations, null, 2)}\n`);
|
|
@@ -235,7 +290,8 @@ async function pull({
|
|
|
235
290
|
}
|
|
236
291
|
|
|
237
292
|
/**
|
|
238
|
-
*
|
|
293
|
+
* Uploads translations to the Phrase API for each language.
|
|
294
|
+
* A unique namespace is appended to each key using the file path the key came from.
|
|
239
295
|
*/
|
|
240
296
|
async function push({
|
|
241
297
|
branch,
|
|
@@ -243,42 +299,50 @@ async function push({
|
|
|
243
299
|
}, config) {
|
|
244
300
|
const allLanguageTranslations = await core.loadAllTranslations({
|
|
245
301
|
fallbacks: 'none',
|
|
246
|
-
includeNodeModules: false
|
|
302
|
+
includeNodeModules: false,
|
|
303
|
+
withTags: true
|
|
247
304
|
}, config);
|
|
248
305
|
trace(`Pushing translations to branch ${branch}`);
|
|
249
306
|
const allLanguages = config.languages.map(v => v.name);
|
|
250
307
|
await ensureBranch(branch);
|
|
251
308
|
trace(`Pushing translations to phrase for languages ${allLanguages.join(', ')}`);
|
|
252
309
|
const phraseTranslations = {};
|
|
253
|
-
|
|
254
310
|
for (const loadedTranslation of allLanguageTranslations) {
|
|
255
311
|
for (const language of allLanguages) {
|
|
256
312
|
const localTranslations = loadedTranslation.languages[language];
|
|
257
|
-
|
|
258
313
|
if (!localTranslations) {
|
|
259
314
|
continue;
|
|
260
315
|
}
|
|
261
|
-
|
|
262
316
|
if (!phraseTranslations[language]) {
|
|
263
317
|
phraseTranslations[language] = {};
|
|
264
318
|
}
|
|
265
|
-
|
|
319
|
+
const {
|
|
320
|
+
metadata: {
|
|
321
|
+
tags: sharedTags = []
|
|
322
|
+
}
|
|
323
|
+
} = loadedTranslation;
|
|
266
324
|
for (const localKey of Object.keys(localTranslations)) {
|
|
267
325
|
const phraseKey = core.getUniqueKey(localKey, loadedTranslation.namespace);
|
|
268
|
-
|
|
326
|
+
const {
|
|
327
|
+
tags = [],
|
|
328
|
+
...localTranslation
|
|
329
|
+
} = localTranslations[localKey];
|
|
330
|
+
if (language === config.devLanguage) {
|
|
331
|
+
localTranslation.tags = [...tags, ...sharedTags];
|
|
332
|
+
}
|
|
333
|
+
phraseTranslations[language][phraseKey] = localTranslation;
|
|
269
334
|
}
|
|
270
335
|
}
|
|
271
336
|
}
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
}
|
|
337
|
+
const {
|
|
338
|
+
uploadIds
|
|
339
|
+
} = await pushTranslations(phraseTranslations, {
|
|
340
|
+
devLanguage: config.devLanguage,
|
|
341
|
+
branch
|
|
342
|
+
});
|
|
343
|
+
if (deleteUnusedKeys$1) {
|
|
344
|
+
for (const uploadId of uploadIds) {
|
|
345
|
+
await deleteUnusedKeys(uploadId, branch);
|
|
282
346
|
}
|
|
283
347
|
}
|
|
284
348
|
}
|