@vocab/phrase 0.0.0-compiled-translation-import-order-20230328231631 → 0.0.0-feature-ignore-flag-push-20241014224750

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.
@@ -1,2 +1,2 @@
1
- export { pull } from './pull-translations';
2
- export { push } from './push-translations';
1
+ export { pull } from "./pull-translations.js";
2
+ export { push } from "./push-translations.js";
@@ -1,7 +1,8 @@
1
- import type { UserConfig } from '@vocab/types';
1
+ import { type UserConfig } from '@vocab/core';
2
2
  interface PullOptions {
3
3
  branch?: string;
4
4
  deleteUnusedKeys?: boolean;
5
+ errorOnNoGlobalKeyTranslation?: boolean;
5
6
  }
6
- export declare function pull({ branch }: PullOptions, config: UserConfig): Promise<void>;
7
+ export declare function pull({ branch, errorOnNoGlobalKeyTranslation }: PullOptions, config: UserConfig): Promise<void>;
7
8
  export {};
@@ -1,11 +1,12 @@
1
- import { UserConfig } from '@vocab/types';
1
+ import { type UserConfig } from '@vocab/core';
2
2
  interface PushOptions {
3
3
  branch: string;
4
4
  deleteUnusedKeys?: boolean;
5
+ ignore?: string[];
5
6
  }
6
7
  /**
7
8
  * Uploads translations to the Phrase API for each language.
8
9
  * A unique namespace is appended to each key using the file path the key came from.
9
10
  */
10
- export declare function push({ branch, deleteUnusedKeys }: PushOptions, config: UserConfig): Promise<void>;
11
+ export declare function push({ branch, deleteUnusedKeys, ignore }: PushOptions, config: UserConfig): Promise<void>;
11
12
  export {};
@@ -1 +1,2 @@
1
1
  export * from "./declarations/src/index";
2
+ //# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidm9jYWItcGhyYXNlLmNqcy5kLnRzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi9kZWNsYXJhdGlvbnMvc3JjL2luZGV4LmQudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEifQ==
@@ -5,18 +5,14 @@ Object.defineProperty(exports, '__esModule', { value: true });
5
5
  var fs = require('fs');
6
6
  var path = require('path');
7
7
  var core = require('@vocab/core');
8
- var FormData = require('form-data');
9
- var fetch = require('node-fetch');
10
- var chalk = require('chalk');
8
+ var pc = require('picocolors');
11
9
  var debug = require('debug');
12
10
  var sync = require('csv-stringify/sync');
13
11
 
14
12
  function _interopDefault (e) { return e && e.__esModule ? e : { 'default': e }; }
15
13
 
16
14
  var path__default = /*#__PURE__*/_interopDefault(path);
17
- var FormData__default = /*#__PURE__*/_interopDefault(FormData);
18
- var fetch__default = /*#__PURE__*/_interopDefault(fetch);
19
- var chalk__default = /*#__PURE__*/_interopDefault(chalk);
15
+ var pc__default = /*#__PURE__*/_interopDefault(pc);
20
16
  var debug__default = /*#__PURE__*/_interopDefault(debug);
21
17
 
22
18
  const mkdir = fs.promises.mkdir;
@@ -25,40 +21,49 @@ const writeFile = fs.promises.writeFile;
25
21
  const trace = debug__default["default"](`vocab:phrase`);
26
22
  const log = (...params) => {
27
23
  // eslint-disable-next-line no-console
28
- console.log(chalk__default["default"].yellow('Vocab'), ...params);
24
+ console.log(pc__default["default"].yellow('Vocab'), ...params);
29
25
  };
30
26
 
31
27
  function translationsToCsv(translations, devLanguage) {
32
28
  const languages = Object.keys(translations);
33
29
  const altLanguages = languages.filter(language => language !== devLanguage);
34
- // Ensure languages are ordered for locale mapping
35
- const orderedLanguages = [devLanguage, ...altLanguages];
36
30
  const devLanguageTranslations = translations[devLanguage];
37
- const csv = Object.entries(devLanguageTranslations).map(([key, {
31
+ const csvFilesByLanguage = Object.fromEntries(languages.map(language => [language, []]));
32
+ Object.entries(devLanguageTranslations).map(([key, {
38
33
  message,
39
34
  description,
40
35
  tags
41
36
  }]) => {
42
- const altTranslationMessages = altLanguages.map(language => {
43
- var _translations$languag, _translations$languag2;
44
- 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;
37
+ const sharedData = [key, description, tags === null || tags === void 0 ? void 0 : tags.join(',')];
38
+ const devLanguageRow = [...sharedData, message];
39
+ csvFilesByLanguage[devLanguage].push(devLanguageRow);
40
+ altLanguages.map(language => {
41
+ var _translations$languag;
42
+ const altTranslationMessage = (_translations$languag = translations[language]) === null || _translations$languag === void 0 || (_translations$languag = _translations$languag[key]) === null || _translations$languag === void 0 ? void 0 : _translations$languag.message;
43
+ if (altTranslationMessage) {
44
+ csvFilesByLanguage[language].push([...sharedData, altTranslationMessage]);
45
+ }
45
46
  });
46
- return [message, ...altTranslationMessages, key, description, tags === null || tags === void 0 ? void 0 : tags.join(',')];
47
- });
48
- const csvString = sync.stringify(csv, {
49
- delimiter: ',',
50
- header: false
51
47
  });
48
+ const csvFileStrings = Object.fromEntries(Object.entries(csvFilesByLanguage)
49
+ // Ensure CSV files are only created if the language has at least 1 translation
50
+ .filter(([_, csvFile]) => csvFile.length > 0).map(([language, csvFile]) => {
51
+ const csvFileString = sync.stringify(csvFile, {
52
+ delimiter: ',',
53
+ header: false
54
+ });
55
+ return [language, csvFileString];
56
+ }));
52
57
 
53
58
  // Column indices start at 1
54
- const localeMapping = Object.fromEntries(orderedLanguages.map((language, index) => [language, index + 1]));
55
- const keyIndex = orderedLanguages.length + 1;
59
+ const keyIndex = 1;
56
60
  const commentIndex = keyIndex + 1;
57
61
  const tagColumn = commentIndex + 1;
62
+ const messageIndex = tagColumn + 1;
58
63
  return {
59
- csvString,
60
- localeMapping,
64
+ csvFileStrings,
61
65
  keyIndex,
66
+ messageIndex,
62
67
  commentIndex,
63
68
  tagColumn
64
69
  };
@@ -70,12 +75,12 @@ function _callPhrase(path, options = {}) {
70
75
  if (!phraseApiToken) {
71
76
  throw new Error('Missing PHRASE_API_TOKEN');
72
77
  }
73
- return fetch__default["default"](path, {
78
+ return fetch(path, {
74
79
  ...options,
75
80
  headers: {
76
81
  Authorization: `token ${phraseApiToken}`,
77
82
  // Provide identification via User Agent as requested in https://developers.phrase.com/api/#overview--identification-via-user-agent
78
- 'User-Agent': 'SEEK Demo Candidate App (jhope@seek.com.au)',
83
+ 'User-Agent': 'Vocab Client (https://github.com/seek-oss/vocab)',
79
84
  ...options.headers
80
85
  }
81
86
  }).then(async response => {
@@ -139,45 +144,48 @@ async function pushTranslations(translationsByLanguage, {
139
144
  devLanguage,
140
145
  branch
141
146
  }) {
142
- const formData = new FormData__default["default"]();
143
147
  const {
144
- csvString,
145
- localeMapping,
148
+ csvFileStrings,
146
149
  keyIndex,
147
150
  commentIndex,
148
- tagColumn
151
+ tagColumn,
152
+ messageIndex
149
153
  } = translationsToCsv(translationsByLanguage, devLanguage);
150
- const fileContents = Buffer.from(csvString);
151
- formData.append('file', fileContents, {
152
- contentType: 'text/csv',
153
- filename: 'translations.csv'
154
- });
155
- formData.append('file_format', 'csv');
156
- formData.append('branch', branch);
157
- formData.append('update_translations', 'true');
158
- for (const [locale, index] of Object.entries(localeMapping)) {
159
- formData.append(`locale_mapping[${locale}]`, index);
160
- }
161
- formData.append('format_options[key_index]', keyIndex);
162
- formData.append('format_options[comment_index]', commentIndex);
163
- formData.append('format_options[tag_column]', tagColumn);
164
- formData.append('format_options[enable_pluralization]', 'false');
165
- log('Uploading translations');
166
- const result = await callPhrase(`uploads`, {
167
- method: 'POST',
168
- body: formData
169
- });
170
- trace('Upload result:\n', result);
171
- if (result && 'id' in result) {
172
- log('Upload ID:', result.id, '\n');
173
- log('Successfully Uploaded\n');
174
- } else {
175
- log(`Error uploading: ${result === null || result === void 0 ? void 0 : result.message}\n`);
176
- log('Response:', result);
177
- throw new Error('Error uploading');
154
+ let devLanguageUploadId = '';
155
+ for (const [language, csvFileString] of Object.entries(csvFileStrings)) {
156
+ const formData = new FormData();
157
+ formData.append('file', new Blob([csvFileString], {
158
+ type: 'text/csv'
159
+ }), `${language}.translations.csv`);
160
+ formData.append('file_format', 'csv');
161
+ formData.append('branch', branch);
162
+ formData.append('update_translations', 'true');
163
+ formData.append('update_descriptions', 'true');
164
+ formData.append(`locale_mapping[${language}]`, messageIndex.toString());
165
+ formData.append('format_options[key_index]', keyIndex.toString());
166
+ formData.append('format_options[comment_index]', commentIndex.toString());
167
+ formData.append('format_options[tag_column]', tagColumn.toString());
168
+ formData.append('format_options[enable_pluralization]', 'false');
169
+ log(`Uploading translations for language ${language}`);
170
+ const result = await callPhrase(`uploads`, {
171
+ method: 'POST',
172
+ body: formData
173
+ });
174
+ trace('Upload result:\n', result);
175
+ if (result && 'id' in result) {
176
+ log('Upload ID:', result.id, '\n');
177
+ log('Successfully Uploaded\n');
178
+ } else {
179
+ log(`Error uploading: ${result === null || result === void 0 ? void 0 : result.message}\n`);
180
+ log('Response:', result);
181
+ throw new Error('Error uploading');
182
+ }
183
+ if (language === devLanguage) {
184
+ devLanguageUploadId = result.id;
185
+ }
178
186
  }
179
187
  return {
180
- uploadId: result.id
188
+ devLanguageUploadId
181
189
  };
182
190
  }
183
191
  async function deleteUnusedKeys(uploadId, branch) {
@@ -210,7 +218,8 @@ async function ensureBranch(branch) {
210
218
  }
211
219
 
212
220
  async function pull({
213
- branch = 'local-development'
221
+ branch = 'local-development',
222
+ errorOnNoGlobalKeyTranslation
214
223
  }, config) {
215
224
  trace(`Pulling translations from branch ${branch}`);
216
225
  await ensureBranch(branch);
@@ -237,9 +246,10 @@ async function pull({
237
246
  };
238
247
  const localKeys = Object.keys(defaultValues);
239
248
  for (const key of localKeys) {
249
+ var _defaultValues$key$gl;
240
250
  defaultValues[key] = {
241
251
  ...defaultValues[key],
242
- ...allPhraseTranslations[config.devLanguage][core.getUniqueKey(key, loadedTranslation.namespace)]
252
+ ...allPhraseTranslations[config.devLanguage][(_defaultValues$key$gl = defaultValues[key].globalKey) !== null && _defaultValues$key$gl !== void 0 ? _defaultValues$key$gl : core.getUniqueKey(key, loadedTranslation.namespace)]
243
253
  };
244
254
  }
245
255
 
@@ -255,11 +265,14 @@ async function pull({
255
265
  };
256
266
  const phraseAltTranslations = allPhraseTranslations[alternativeLanguage];
257
267
  for (const key of localKeys) {
258
- var _phraseAltTranslation;
259
- const phraseKey = core.getUniqueKey(key, loadedTranslation.namespace);
268
+ var _defaultValues$key$gl2, _phraseAltTranslation;
269
+ const phraseKey = (_defaultValues$key$gl2 = defaultValues[key].globalKey) !== null && _defaultValues$key$gl2 !== void 0 ? _defaultValues$key$gl2 : core.getUniqueKey(key, loadedTranslation.namespace);
260
270
  const phraseTranslationMessage = (_phraseAltTranslation = phraseAltTranslations[phraseKey]) === null || _phraseAltTranslation === void 0 ? void 0 : _phraseAltTranslation.message;
261
271
  if (!phraseTranslationMessage) {
262
272
  trace(`Missing translation. No translation for key ${key} in phrase as ${phraseKey} in language ${alternativeLanguage}.`);
273
+ if (errorOnNoGlobalKeyTranslation && defaultValues[key].globalKey) {
274
+ throw new Error(`Missing translation for global key ${key} in language ${alternativeLanguage}`);
275
+ }
263
276
  continue;
264
277
  }
265
278
  altTranslations[key] = {
@@ -283,13 +296,17 @@ async function pull({
283
296
  */
284
297
  async function push({
285
298
  branch,
286
- deleteUnusedKeys: deleteUnusedKeys$1
299
+ deleteUnusedKeys: deleteUnusedKeys$1,
300
+ ignore
287
301
  }, config) {
288
302
  const allLanguageTranslations = await core.loadAllTranslations({
289
303
  fallbacks: 'none',
290
304
  includeNodeModules: false,
291
305
  withTags: true
292
- }, config);
306
+ }, {
307
+ ...config,
308
+ ignore: [...(config.ignore || []), ...(ignore || [])]
309
+ });
293
310
  trace(`Pushing translations to branch ${branch}`);
294
311
  const allLanguages = config.languages.map(v => v.name);
295
312
  await ensureBranch(branch);
@@ -310,7 +327,6 @@ async function push({
310
327
  }
311
328
  } = loadedTranslation;
312
329
  for (const localKey of Object.keys(localTranslations)) {
313
- const phraseKey = core.getUniqueKey(localKey, loadedTranslation.namespace);
314
330
  const {
315
331
  tags = [],
316
332
  ...localTranslation
@@ -318,18 +334,20 @@ async function push({
318
334
  if (language === config.devLanguage) {
319
335
  localTranslation.tags = [...tags, ...sharedTags];
320
336
  }
337
+ const globalKey = loadedTranslation.languages[config.devLanguage][localKey].globalKey;
338
+ const phraseKey = globalKey !== null && globalKey !== void 0 ? globalKey : core.getUniqueKey(localKey, loadedTranslation.namespace);
321
339
  phraseTranslations[language][phraseKey] = localTranslation;
322
340
  }
323
341
  }
324
342
  }
325
343
  const {
326
- uploadId
344
+ devLanguageUploadId
327
345
  } = await pushTranslations(phraseTranslations, {
328
346
  devLanguage: config.devLanguage,
329
347
  branch
330
348
  });
331
349
  if (deleteUnusedKeys$1) {
332
- await deleteUnusedKeys(uploadId, branch);
350
+ await deleteUnusedKeys(devLanguageUploadId, branch);
333
351
  }
334
352
  }
335
353
 
@@ -5,18 +5,14 @@ Object.defineProperty(exports, '__esModule', { value: true });
5
5
  var fs = require('fs');
6
6
  var path = require('path');
7
7
  var core = require('@vocab/core');
8
- var FormData = require('form-data');
9
- var fetch = require('node-fetch');
10
- var chalk = require('chalk');
8
+ var pc = require('picocolors');
11
9
  var debug = require('debug');
12
10
  var sync = require('csv-stringify/sync');
13
11
 
14
12
  function _interopDefault (e) { return e && e.__esModule ? e : { 'default': e }; }
15
13
 
16
14
  var path__default = /*#__PURE__*/_interopDefault(path);
17
- var FormData__default = /*#__PURE__*/_interopDefault(FormData);
18
- var fetch__default = /*#__PURE__*/_interopDefault(fetch);
19
- var chalk__default = /*#__PURE__*/_interopDefault(chalk);
15
+ var pc__default = /*#__PURE__*/_interopDefault(pc);
20
16
  var debug__default = /*#__PURE__*/_interopDefault(debug);
21
17
 
22
18
  const mkdir = fs.promises.mkdir;
@@ -25,40 +21,49 @@ const writeFile = fs.promises.writeFile;
25
21
  const trace = debug__default["default"](`vocab:phrase`);
26
22
  const log = (...params) => {
27
23
  // eslint-disable-next-line no-console
28
- console.log(chalk__default["default"].yellow('Vocab'), ...params);
24
+ console.log(pc__default["default"].yellow('Vocab'), ...params);
29
25
  };
30
26
 
31
27
  function translationsToCsv(translations, devLanguage) {
32
28
  const languages = Object.keys(translations);
33
29
  const altLanguages = languages.filter(language => language !== devLanguage);
34
- // Ensure languages are ordered for locale mapping
35
- const orderedLanguages = [devLanguage, ...altLanguages];
36
30
  const devLanguageTranslations = translations[devLanguage];
37
- const csv = Object.entries(devLanguageTranslations).map(([key, {
31
+ const csvFilesByLanguage = Object.fromEntries(languages.map(language => [language, []]));
32
+ Object.entries(devLanguageTranslations).map(([key, {
38
33
  message,
39
34
  description,
40
35
  tags
41
36
  }]) => {
42
- const altTranslationMessages = altLanguages.map(language => {
43
- var _translations$languag, _translations$languag2;
44
- 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;
37
+ const sharedData = [key, description, tags === null || tags === void 0 ? void 0 : tags.join(',')];
38
+ const devLanguageRow = [...sharedData, message];
39
+ csvFilesByLanguage[devLanguage].push(devLanguageRow);
40
+ altLanguages.map(language => {
41
+ var _translations$languag;
42
+ const altTranslationMessage = (_translations$languag = translations[language]) === null || _translations$languag === void 0 || (_translations$languag = _translations$languag[key]) === null || _translations$languag === void 0 ? void 0 : _translations$languag.message;
43
+ if (altTranslationMessage) {
44
+ csvFilesByLanguage[language].push([...sharedData, altTranslationMessage]);
45
+ }
45
46
  });
46
- return [message, ...altTranslationMessages, key, description, tags === null || tags === void 0 ? void 0 : tags.join(',')];
47
- });
48
- const csvString = sync.stringify(csv, {
49
- delimiter: ',',
50
- header: false
51
47
  });
48
+ const csvFileStrings = Object.fromEntries(Object.entries(csvFilesByLanguage)
49
+ // Ensure CSV files are only created if the language has at least 1 translation
50
+ .filter(([_, csvFile]) => csvFile.length > 0).map(([language, csvFile]) => {
51
+ const csvFileString = sync.stringify(csvFile, {
52
+ delimiter: ',',
53
+ header: false
54
+ });
55
+ return [language, csvFileString];
56
+ }));
52
57
 
53
58
  // Column indices start at 1
54
- const localeMapping = Object.fromEntries(orderedLanguages.map((language, index) => [language, index + 1]));
55
- const keyIndex = orderedLanguages.length + 1;
59
+ const keyIndex = 1;
56
60
  const commentIndex = keyIndex + 1;
57
61
  const tagColumn = commentIndex + 1;
62
+ const messageIndex = tagColumn + 1;
58
63
  return {
59
- csvString,
60
- localeMapping,
64
+ csvFileStrings,
61
65
  keyIndex,
66
+ messageIndex,
62
67
  commentIndex,
63
68
  tagColumn
64
69
  };
@@ -70,12 +75,12 @@ function _callPhrase(path, options = {}) {
70
75
  if (!phraseApiToken) {
71
76
  throw new Error('Missing PHRASE_API_TOKEN');
72
77
  }
73
- return fetch__default["default"](path, {
78
+ return fetch(path, {
74
79
  ...options,
75
80
  headers: {
76
81
  Authorization: `token ${phraseApiToken}`,
77
82
  // Provide identification via User Agent as requested in https://developers.phrase.com/api/#overview--identification-via-user-agent
78
- 'User-Agent': 'SEEK Demo Candidate App (jhope@seek.com.au)',
83
+ 'User-Agent': 'Vocab Client (https://github.com/seek-oss/vocab)',
79
84
  ...options.headers
80
85
  }
81
86
  }).then(async response => {
@@ -139,45 +144,48 @@ async function pushTranslations(translationsByLanguage, {
139
144
  devLanguage,
140
145
  branch
141
146
  }) {
142
- const formData = new FormData__default["default"]();
143
147
  const {
144
- csvString,
145
- localeMapping,
148
+ csvFileStrings,
146
149
  keyIndex,
147
150
  commentIndex,
148
- tagColumn
151
+ tagColumn,
152
+ messageIndex
149
153
  } = translationsToCsv(translationsByLanguage, devLanguage);
150
- const fileContents = Buffer.from(csvString);
151
- formData.append('file', fileContents, {
152
- contentType: 'text/csv',
153
- filename: 'translations.csv'
154
- });
155
- formData.append('file_format', 'csv');
156
- formData.append('branch', branch);
157
- formData.append('update_translations', 'true');
158
- for (const [locale, index] of Object.entries(localeMapping)) {
159
- formData.append(`locale_mapping[${locale}]`, index);
160
- }
161
- formData.append('format_options[key_index]', keyIndex);
162
- formData.append('format_options[comment_index]', commentIndex);
163
- formData.append('format_options[tag_column]', tagColumn);
164
- formData.append('format_options[enable_pluralization]', 'false');
165
- log('Uploading translations');
166
- const result = await callPhrase(`uploads`, {
167
- method: 'POST',
168
- body: formData
169
- });
170
- trace('Upload result:\n', result);
171
- if (result && 'id' in result) {
172
- log('Upload ID:', result.id, '\n');
173
- log('Successfully Uploaded\n');
174
- } else {
175
- log(`Error uploading: ${result === null || result === void 0 ? void 0 : result.message}\n`);
176
- log('Response:', result);
177
- throw new Error('Error uploading');
154
+ let devLanguageUploadId = '';
155
+ for (const [language, csvFileString] of Object.entries(csvFileStrings)) {
156
+ const formData = new FormData();
157
+ formData.append('file', new Blob([csvFileString], {
158
+ type: 'text/csv'
159
+ }), `${language}.translations.csv`);
160
+ formData.append('file_format', 'csv');
161
+ formData.append('branch', branch);
162
+ formData.append('update_translations', 'true');
163
+ formData.append('update_descriptions', 'true');
164
+ formData.append(`locale_mapping[${language}]`, messageIndex.toString());
165
+ formData.append('format_options[key_index]', keyIndex.toString());
166
+ formData.append('format_options[comment_index]', commentIndex.toString());
167
+ formData.append('format_options[tag_column]', tagColumn.toString());
168
+ formData.append('format_options[enable_pluralization]', 'false');
169
+ log(`Uploading translations for language ${language}`);
170
+ const result = await callPhrase(`uploads`, {
171
+ method: 'POST',
172
+ body: formData
173
+ });
174
+ trace('Upload result:\n', result);
175
+ if (result && 'id' in result) {
176
+ log('Upload ID:', result.id, '\n');
177
+ log('Successfully Uploaded\n');
178
+ } else {
179
+ log(`Error uploading: ${result === null || result === void 0 ? void 0 : result.message}\n`);
180
+ log('Response:', result);
181
+ throw new Error('Error uploading');
182
+ }
183
+ if (language === devLanguage) {
184
+ devLanguageUploadId = result.id;
185
+ }
178
186
  }
179
187
  return {
180
- uploadId: result.id
188
+ devLanguageUploadId
181
189
  };
182
190
  }
183
191
  async function deleteUnusedKeys(uploadId, branch) {
@@ -210,7 +218,8 @@ async function ensureBranch(branch) {
210
218
  }
211
219
 
212
220
  async function pull({
213
- branch = 'local-development'
221
+ branch = 'local-development',
222
+ errorOnNoGlobalKeyTranslation
214
223
  }, config) {
215
224
  trace(`Pulling translations from branch ${branch}`);
216
225
  await ensureBranch(branch);
@@ -237,9 +246,10 @@ async function pull({
237
246
  };
238
247
  const localKeys = Object.keys(defaultValues);
239
248
  for (const key of localKeys) {
249
+ var _defaultValues$key$gl;
240
250
  defaultValues[key] = {
241
251
  ...defaultValues[key],
242
- ...allPhraseTranslations[config.devLanguage][core.getUniqueKey(key, loadedTranslation.namespace)]
252
+ ...allPhraseTranslations[config.devLanguage][(_defaultValues$key$gl = defaultValues[key].globalKey) !== null && _defaultValues$key$gl !== void 0 ? _defaultValues$key$gl : core.getUniqueKey(key, loadedTranslation.namespace)]
243
253
  };
244
254
  }
245
255
 
@@ -255,11 +265,14 @@ async function pull({
255
265
  };
256
266
  const phraseAltTranslations = allPhraseTranslations[alternativeLanguage];
257
267
  for (const key of localKeys) {
258
- var _phraseAltTranslation;
259
- const phraseKey = core.getUniqueKey(key, loadedTranslation.namespace);
268
+ var _defaultValues$key$gl2, _phraseAltTranslation;
269
+ const phraseKey = (_defaultValues$key$gl2 = defaultValues[key].globalKey) !== null && _defaultValues$key$gl2 !== void 0 ? _defaultValues$key$gl2 : core.getUniqueKey(key, loadedTranslation.namespace);
260
270
  const phraseTranslationMessage = (_phraseAltTranslation = phraseAltTranslations[phraseKey]) === null || _phraseAltTranslation === void 0 ? void 0 : _phraseAltTranslation.message;
261
271
  if (!phraseTranslationMessage) {
262
272
  trace(`Missing translation. No translation for key ${key} in phrase as ${phraseKey} in language ${alternativeLanguage}.`);
273
+ if (errorOnNoGlobalKeyTranslation && defaultValues[key].globalKey) {
274
+ throw new Error(`Missing translation for global key ${key} in language ${alternativeLanguage}`);
275
+ }
263
276
  continue;
264
277
  }
265
278
  altTranslations[key] = {
@@ -283,13 +296,17 @@ async function pull({
283
296
  */
284
297
  async function push({
285
298
  branch,
286
- deleteUnusedKeys: deleteUnusedKeys$1
299
+ deleteUnusedKeys: deleteUnusedKeys$1,
300
+ ignore
287
301
  }, config) {
288
302
  const allLanguageTranslations = await core.loadAllTranslations({
289
303
  fallbacks: 'none',
290
304
  includeNodeModules: false,
291
305
  withTags: true
292
- }, config);
306
+ }, {
307
+ ...config,
308
+ ignore: [...(config.ignore || []), ...(ignore || [])]
309
+ });
293
310
  trace(`Pushing translations to branch ${branch}`);
294
311
  const allLanguages = config.languages.map(v => v.name);
295
312
  await ensureBranch(branch);
@@ -310,7 +327,6 @@ async function push({
310
327
  }
311
328
  } = loadedTranslation;
312
329
  for (const localKey of Object.keys(localTranslations)) {
313
- const phraseKey = core.getUniqueKey(localKey, loadedTranslation.namespace);
314
330
  const {
315
331
  tags = [],
316
332
  ...localTranslation
@@ -318,18 +334,20 @@ async function push({
318
334
  if (language === config.devLanguage) {
319
335
  localTranslation.tags = [...tags, ...sharedTags];
320
336
  }
337
+ const globalKey = loadedTranslation.languages[config.devLanguage][localKey].globalKey;
338
+ const phraseKey = globalKey !== null && globalKey !== void 0 ? globalKey : core.getUniqueKey(localKey, loadedTranslation.namespace);
321
339
  phraseTranslations[language][phraseKey] = localTranslation;
322
340
  }
323
341
  }
324
342
  }
325
343
  const {
326
- uploadId
344
+ devLanguageUploadId
327
345
  } = await pushTranslations(phraseTranslations, {
328
346
  devLanguage: config.devLanguage,
329
347
  branch
330
348
  });
331
349
  if (deleteUnusedKeys$1) {
332
- await deleteUnusedKeys(uploadId, branch);
350
+ await deleteUnusedKeys(devLanguageUploadId, branch);
333
351
  }
334
352
  }
335
353
 
@@ -1,9 +1,7 @@
1
1
  import { promises } from 'fs';
2
2
  import path from 'path';
3
3
  import { getAltLanguages, loadAllTranslations, getUniqueKey, getAltLanguageFilePath } from '@vocab/core';
4
- import FormData from 'form-data';
5
- import fetch from 'node-fetch';
6
- import chalk from 'chalk';
4
+ import pc from 'picocolors';
7
5
  import debug from 'debug';
8
6
  import { stringify } from 'csv-stringify/sync';
9
7
 
@@ -13,40 +11,49 @@ const writeFile = promises.writeFile;
13
11
  const trace = debug(`vocab:phrase`);
14
12
  const log = (...params) => {
15
13
  // eslint-disable-next-line no-console
16
- console.log(chalk.yellow('Vocab'), ...params);
14
+ console.log(pc.yellow('Vocab'), ...params);
17
15
  };
18
16
 
19
17
  function translationsToCsv(translations, devLanguage) {
20
18
  const languages = Object.keys(translations);
21
19
  const altLanguages = languages.filter(language => language !== devLanguage);
22
- // Ensure languages are ordered for locale mapping
23
- const orderedLanguages = [devLanguage, ...altLanguages];
24
20
  const devLanguageTranslations = translations[devLanguage];
25
- const csv = Object.entries(devLanguageTranslations).map(([key, {
21
+ const csvFilesByLanguage = Object.fromEntries(languages.map(language => [language, []]));
22
+ Object.entries(devLanguageTranslations).map(([key, {
26
23
  message,
27
24
  description,
28
25
  tags
29
26
  }]) => {
30
- const altTranslationMessages = altLanguages.map(language => {
31
- var _translations$languag, _translations$languag2;
32
- 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;
27
+ const sharedData = [key, description, tags === null || tags === void 0 ? void 0 : tags.join(',')];
28
+ const devLanguageRow = [...sharedData, message];
29
+ csvFilesByLanguage[devLanguage].push(devLanguageRow);
30
+ altLanguages.map(language => {
31
+ var _translations$languag;
32
+ const altTranslationMessage = (_translations$languag = translations[language]) === null || _translations$languag === void 0 || (_translations$languag = _translations$languag[key]) === null || _translations$languag === void 0 ? void 0 : _translations$languag.message;
33
+ if (altTranslationMessage) {
34
+ csvFilesByLanguage[language].push([...sharedData, altTranslationMessage]);
35
+ }
33
36
  });
34
- return [message, ...altTranslationMessages, key, description, tags === null || tags === void 0 ? void 0 : tags.join(',')];
35
- });
36
- const csvString = stringify(csv, {
37
- delimiter: ',',
38
- header: false
39
37
  });
38
+ const csvFileStrings = Object.fromEntries(Object.entries(csvFilesByLanguage)
39
+ // Ensure CSV files are only created if the language has at least 1 translation
40
+ .filter(([_, csvFile]) => csvFile.length > 0).map(([language, csvFile]) => {
41
+ const csvFileString = stringify(csvFile, {
42
+ delimiter: ',',
43
+ header: false
44
+ });
45
+ return [language, csvFileString];
46
+ }));
40
47
 
41
48
  // Column indices start at 1
42
- const localeMapping = Object.fromEntries(orderedLanguages.map((language, index) => [language, index + 1]));
43
- const keyIndex = orderedLanguages.length + 1;
49
+ const keyIndex = 1;
44
50
  const commentIndex = keyIndex + 1;
45
51
  const tagColumn = commentIndex + 1;
52
+ const messageIndex = tagColumn + 1;
46
53
  return {
47
- csvString,
48
- localeMapping,
54
+ csvFileStrings,
49
55
  keyIndex,
56
+ messageIndex,
50
57
  commentIndex,
51
58
  tagColumn
52
59
  };
@@ -63,7 +70,7 @@ function _callPhrase(path, options = {}) {
63
70
  headers: {
64
71
  Authorization: `token ${phraseApiToken}`,
65
72
  // Provide identification via User Agent as requested in https://developers.phrase.com/api/#overview--identification-via-user-agent
66
- 'User-Agent': 'SEEK Demo Candidate App (jhope@seek.com.au)',
73
+ 'User-Agent': 'Vocab Client (https://github.com/seek-oss/vocab)',
67
74
  ...options.headers
68
75
  }
69
76
  }).then(async response => {
@@ -127,45 +134,48 @@ async function pushTranslations(translationsByLanguage, {
127
134
  devLanguage,
128
135
  branch
129
136
  }) {
130
- const formData = new FormData();
131
137
  const {
132
- csvString,
133
- localeMapping,
138
+ csvFileStrings,
134
139
  keyIndex,
135
140
  commentIndex,
136
- tagColumn
141
+ tagColumn,
142
+ messageIndex
137
143
  } = translationsToCsv(translationsByLanguage, devLanguage);
138
- const fileContents = Buffer.from(csvString);
139
- formData.append('file', fileContents, {
140
- contentType: 'text/csv',
141
- filename: 'translations.csv'
142
- });
143
- formData.append('file_format', 'csv');
144
- formData.append('branch', branch);
145
- formData.append('update_translations', 'true');
146
- for (const [locale, index] of Object.entries(localeMapping)) {
147
- formData.append(`locale_mapping[${locale}]`, index);
148
- }
149
- formData.append('format_options[key_index]', keyIndex);
150
- formData.append('format_options[comment_index]', commentIndex);
151
- formData.append('format_options[tag_column]', tagColumn);
152
- formData.append('format_options[enable_pluralization]', 'false');
153
- log('Uploading translations');
154
- const result = await callPhrase(`uploads`, {
155
- method: 'POST',
156
- body: formData
157
- });
158
- trace('Upload result:\n', result);
159
- if (result && 'id' in result) {
160
- log('Upload ID:', result.id, '\n');
161
- log('Successfully Uploaded\n');
162
- } else {
163
- log(`Error uploading: ${result === null || result === void 0 ? void 0 : result.message}\n`);
164
- log('Response:', result);
165
- throw new Error('Error uploading');
144
+ let devLanguageUploadId = '';
145
+ for (const [language, csvFileString] of Object.entries(csvFileStrings)) {
146
+ const formData = new FormData();
147
+ formData.append('file', new Blob([csvFileString], {
148
+ type: 'text/csv'
149
+ }), `${language}.translations.csv`);
150
+ formData.append('file_format', 'csv');
151
+ formData.append('branch', branch);
152
+ formData.append('update_translations', 'true');
153
+ formData.append('update_descriptions', 'true');
154
+ formData.append(`locale_mapping[${language}]`, messageIndex.toString());
155
+ formData.append('format_options[key_index]', keyIndex.toString());
156
+ formData.append('format_options[comment_index]', commentIndex.toString());
157
+ formData.append('format_options[tag_column]', tagColumn.toString());
158
+ formData.append('format_options[enable_pluralization]', 'false');
159
+ log(`Uploading translations for language ${language}`);
160
+ const result = await callPhrase(`uploads`, {
161
+ method: 'POST',
162
+ body: formData
163
+ });
164
+ trace('Upload result:\n', result);
165
+ if (result && 'id' in result) {
166
+ log('Upload ID:', result.id, '\n');
167
+ log('Successfully Uploaded\n');
168
+ } else {
169
+ log(`Error uploading: ${result === null || result === void 0 ? void 0 : result.message}\n`);
170
+ log('Response:', result);
171
+ throw new Error('Error uploading');
172
+ }
173
+ if (language === devLanguage) {
174
+ devLanguageUploadId = result.id;
175
+ }
166
176
  }
167
177
  return {
168
- uploadId: result.id
178
+ devLanguageUploadId
169
179
  };
170
180
  }
171
181
  async function deleteUnusedKeys(uploadId, branch) {
@@ -198,7 +208,8 @@ async function ensureBranch(branch) {
198
208
  }
199
209
 
200
210
  async function pull({
201
- branch = 'local-development'
211
+ branch = 'local-development',
212
+ errorOnNoGlobalKeyTranslation
202
213
  }, config) {
203
214
  trace(`Pulling translations from branch ${branch}`);
204
215
  await ensureBranch(branch);
@@ -225,9 +236,10 @@ async function pull({
225
236
  };
226
237
  const localKeys = Object.keys(defaultValues);
227
238
  for (const key of localKeys) {
239
+ var _defaultValues$key$gl;
228
240
  defaultValues[key] = {
229
241
  ...defaultValues[key],
230
- ...allPhraseTranslations[config.devLanguage][getUniqueKey(key, loadedTranslation.namespace)]
242
+ ...allPhraseTranslations[config.devLanguage][(_defaultValues$key$gl = defaultValues[key].globalKey) !== null && _defaultValues$key$gl !== void 0 ? _defaultValues$key$gl : getUniqueKey(key, loadedTranslation.namespace)]
231
243
  };
232
244
  }
233
245
 
@@ -243,11 +255,14 @@ async function pull({
243
255
  };
244
256
  const phraseAltTranslations = allPhraseTranslations[alternativeLanguage];
245
257
  for (const key of localKeys) {
246
- var _phraseAltTranslation;
247
- const phraseKey = getUniqueKey(key, loadedTranslation.namespace);
258
+ var _defaultValues$key$gl2, _phraseAltTranslation;
259
+ const phraseKey = (_defaultValues$key$gl2 = defaultValues[key].globalKey) !== null && _defaultValues$key$gl2 !== void 0 ? _defaultValues$key$gl2 : getUniqueKey(key, loadedTranslation.namespace);
248
260
  const phraseTranslationMessage = (_phraseAltTranslation = phraseAltTranslations[phraseKey]) === null || _phraseAltTranslation === void 0 ? void 0 : _phraseAltTranslation.message;
249
261
  if (!phraseTranslationMessage) {
250
262
  trace(`Missing translation. No translation for key ${key} in phrase as ${phraseKey} in language ${alternativeLanguage}.`);
263
+ if (errorOnNoGlobalKeyTranslation && defaultValues[key].globalKey) {
264
+ throw new Error(`Missing translation for global key ${key} in language ${alternativeLanguage}`);
265
+ }
251
266
  continue;
252
267
  }
253
268
  altTranslations[key] = {
@@ -271,13 +286,17 @@ async function pull({
271
286
  */
272
287
  async function push({
273
288
  branch,
274
- deleteUnusedKeys: deleteUnusedKeys$1
289
+ deleteUnusedKeys: deleteUnusedKeys$1,
290
+ ignore
275
291
  }, config) {
276
292
  const allLanguageTranslations = await loadAllTranslations({
277
293
  fallbacks: 'none',
278
294
  includeNodeModules: false,
279
295
  withTags: true
280
- }, config);
296
+ }, {
297
+ ...config,
298
+ ignore: [...(config.ignore || []), ...(ignore || [])]
299
+ });
281
300
  trace(`Pushing translations to branch ${branch}`);
282
301
  const allLanguages = config.languages.map(v => v.name);
283
302
  await ensureBranch(branch);
@@ -298,7 +317,6 @@ async function push({
298
317
  }
299
318
  } = loadedTranslation;
300
319
  for (const localKey of Object.keys(localTranslations)) {
301
- const phraseKey = getUniqueKey(localKey, loadedTranslation.namespace);
302
320
  const {
303
321
  tags = [],
304
322
  ...localTranslation
@@ -306,18 +324,20 @@ async function push({
306
324
  if (language === config.devLanguage) {
307
325
  localTranslation.tags = [...tags, ...sharedTags];
308
326
  }
327
+ const globalKey = loadedTranslation.languages[config.devLanguage][localKey].globalKey;
328
+ const phraseKey = globalKey !== null && globalKey !== void 0 ? globalKey : getUniqueKey(localKey, loadedTranslation.namespace);
309
329
  phraseTranslations[language][phraseKey] = localTranslation;
310
330
  }
311
331
  }
312
332
  }
313
333
  const {
314
- uploadId
334
+ devLanguageUploadId
315
335
  } = await pushTranslations(phraseTranslations, {
316
336
  devLanguage: config.devLanguage,
317
337
  branch
318
338
  });
319
339
  if (deleteUnusedKeys$1) {
320
- await deleteUnusedKeys(uploadId, branch);
340
+ await deleteUnusedKeys(devLanguageUploadId, branch);
321
341
  }
322
342
  }
323
343
 
package/package.json CHANGED
@@ -1,22 +1,27 @@
1
1
  {
2
2
  "name": "@vocab/phrase",
3
- "version": "0.0.0-compiled-translation-import-order-20230328231631",
3
+ "version": "0.0.0-feature-ignore-flag-push-20241014224750",
4
+ "repository": {
5
+ "type": "git",
6
+ "url": "https://github.com/seek-oss/vocab.git",
7
+ "directory": "packages/phrase"
8
+ },
9
+ "engines": {
10
+ "node": ">=18"
11
+ },
4
12
  "main": "dist/vocab-phrase.cjs.js",
5
13
  "module": "dist/vocab-phrase.esm.js",
6
14
  "author": "SEEK",
7
15
  "license": "MIT",
8
16
  "dependencies": {
9
- "@vocab/core": "0.0.0-compiled-translation-import-order-20230328231631",
10
- "@vocab/types": "^1.1.2",
11
- "chalk": "^4.1.0",
12
17
  "csv-stringify": "^6.2.3",
13
18
  "debug": "^4.3.1",
14
- "form-data": "^3.0.0",
15
- "node-fetch": "^2.6.1"
19
+ "picocolors": "^1.0.0",
20
+ "@vocab/core": "^1.6.2"
16
21
  },
17
22
  "devDependencies": {
18
23
  "@types/debug": "^4.1.5",
19
- "@types/node-fetch": "^2.5.7"
24
+ "@types/node": "^18.11.9"
20
25
  },
21
26
  "files": [
22
27
  "dist"
@@ -1,10 +0,0 @@
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,4 +0,0 @@
1
- /// <reference types="node" />
2
- import { promises as fs } from 'fs';
3
- export declare const mkdir: typeof fs.mkdir;
4
- export declare const writeFile: typeof fs.writeFile;
@@ -1,3 +0,0 @@
1
- import debug from 'debug';
2
- export declare const trace: debug.Debugger;
3
- export declare const log: (...params: unknown[]) => void;
@@ -1,12 +0,0 @@
1
- import type { TranslationsByLanguage } from '@vocab/types';
2
- import fetch from 'node-fetch';
3
- export declare function callPhrase<T = any>(relativePath: string, options?: Parameters<typeof fetch>[1]): Promise<T>;
4
- export declare function pullAllTranslations(branch: string): Promise<TranslationsByLanguage>;
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>;
12
- export declare function ensureBranch(branch: string): Promise<void>;