@vocab/phrase 0.0.0-phrase-pull-dev-language-202281412540 → 0.0.0-tags-support-2023174816

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.
@@ -0,0 +1,10 @@
1
+ import type { TranslationsByLanguage } from '@vocab/types';
2
+ export declare function translationsToCsv(translations: TranslationsByLanguage, devLanguage: string): {
3
+ csvString: string;
4
+ localeMapping: {
5
+ [k: string]: number;
6
+ };
7
+ keyIndex: number;
8
+ commentIndex: number;
9
+ tagColumn: number;
10
+ };
@@ -1,7 +1,12 @@
1
- import { TranslationsByKey } from './../../types/src/index';
2
1
  import type { TranslationsByLanguage } from '@vocab/types';
3
2
  import fetch from 'node-fetch';
4
- export declare function callPhrase(relativePath: string, options?: Parameters<typeof fetch>[1]): Promise<any>;
3
+ export declare function callPhrase<T = any>(relativePath: string, options?: Parameters<typeof fetch>[1]): Promise<T>;
5
4
  export declare function pullAllTranslations(branch: string): Promise<TranslationsByLanguage>;
6
- export declare function pushTranslationsByLocale(contents: TranslationsByKey, locale: string, branch: string): Promise<void>;
5
+ export declare function pushTranslations(translationsByLanguage: TranslationsByLanguage, { devLanguage, branch }: {
6
+ devLanguage: string;
7
+ branch: string;
8
+ }): Promise<{
9
+ uploadId: string;
10
+ }>;
11
+ export declare function deleteUnusedKeys(uploadId: string, branch: string): Promise<void>;
7
12
  export declare function ensureBranch(branch: string): Promise<void>;
@@ -1,6 +1,7 @@
1
1
  import type { UserConfig } from '@vocab/types';
2
2
  interface PullOptions {
3
3
  branch?: string;
4
+ deleteUnusedKeys?: boolean;
4
5
  }
5
6
  export declare function pull({ branch }: PullOptions, config: UserConfig): Promise<void>;
6
7
  export {};
@@ -1,9 +1,11 @@
1
1
  import { UserConfig } from '@vocab/types';
2
2
  interface PushOptions {
3
3
  branch: string;
4
+ deleteUnusedKeys?: boolean;
4
5
  }
5
6
  /**
6
- * Uploading to the Phrase API for each language. Adding a unique namespace to each key using file path they key came from
7
+ * Uploads translations to the Phrase API for each language.
8
+ * A unique namespace is appended to each key using the file path the key came from.
7
9
  */
8
- export declare function push({ branch }: PushOptions, config: UserConfig): Promise<void>;
10
+ export declare function push({ branch, deleteUnusedKeys }: PushOptions, config: UserConfig): Promise<void>;
9
11
  export {};
@@ -9,6 +9,7 @@ var FormData = require('form-data');
9
9
  var fetch = require('node-fetch');
10
10
  var chalk = require('chalk');
11
11
  var debug = require('debug');
12
+ var sync = require('csv-stringify/sync');
12
13
 
13
14
  function _interopDefault (e) { return e && e.__esModule ? e : { 'default': e }; }
14
15
 
@@ -27,6 +28,44 @@ const log = (...params) => {
27
28
  console.log(chalk__default['default'].yellow('Vocab'), ...params);
28
29
  };
29
30
 
31
+ function translationsToCsv(translations, devLanguage) {
32
+ const languages = Object.keys(translations);
33
+ const altLanguages = languages.filter(language => language !== devLanguage);
34
+ const devLanguageTranslations = translations[devLanguage];
35
+ const csv = Object.entries(devLanguageTranslations).map(([key, {
36
+ message,
37
+ description,
38
+ tags
39
+ }]) => {
40
+ const altTranslationMessages = altLanguages.map(language => {
41
+ var _translations$languag, _translations$languag2;
42
+
43
+ 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;
44
+ });
45
+ return [message, ...altTranslationMessages, key, description, tags === null || tags === void 0 ? void 0 : tags.join(',')];
46
+ }); // Not spreading `languages` to ensure correct ordering of dev language first
47
+ // then alt languages
48
+
49
+ const csvString = sync.stringify(csv, {
50
+ delimiter: ',',
51
+ header: false
52
+ }); // Column indices start at 1
53
+
54
+ const localeMapping = Object.fromEntries(languages.map((language, index) => [language, index + 1]));
55
+ const keyIndex = languages.length + 1;
56
+ const commentIndex = keyIndex + 1;
57
+ const tagColumn = commentIndex + 1;
58
+ return {
59
+ csvString,
60
+ localeMapping,
61
+ keyIndex,
62
+ commentIndex,
63
+ tagColumn
64
+ };
65
+ }
66
+
67
+ /* eslint-disable no-console */
68
+
30
69
  function _callPhrase(path, options = {}) {
31
70
  const phraseApiToken = process.env.PHRASE_API_TOKEN;
32
71
 
@@ -44,14 +83,14 @@ function _callPhrase(path, options = {}) {
44
83
  }).then(async response => {
45
84
  console.log(`${path}: ${response.status} - ${response.statusText}`);
46
85
  console.log(`Rate Limit: ${response.headers.get('X-Rate-Limit-Remaining')} of ${response.headers.get('X-Rate-Limit-Limit')} remaining. (${response.headers.get('X-Rate-Limit-Reset')} seconds remaining})`);
47
- console.log('\nLink:', response.headers.get('Link'), '\n'); // Print All Headers:
86
+ trace('\nLink:', response.headers.get('Link'), '\n'); // Print All Headers:
48
87
  // console.log(Array.from(r.headers.entries()));
49
88
 
50
89
  try {
51
90
  var _response$headers$get;
52
91
 
53
92
  const result = await response.json();
54
- console.log(`Internal Result (Length: ${result.length})\n`);
93
+ trace(`Internal Result (Length: ${result.length})\n`);
55
94
 
56
95
  if ((!options.method || options.method === 'GET') && (_response$headers$get = response.headers.get('Link')) !== null && _response$headers$get !== void 0 && _response$headers$get.includes('rel=next')) {
57
96
  var _response$headers$get2, _response$headers$get3;
@@ -59,10 +98,10 @@ function _callPhrase(path, options = {}) {
59
98
  const [, nextPageUrl] = (_response$headers$get2 = (_response$headers$get3 = response.headers.get('Link')) === null || _response$headers$get3 === void 0 ? void 0 : _response$headers$get3.match(/<([^>]*)>; rel=next/)) !== null && _response$headers$get2 !== void 0 ? _response$headers$get2 : [];
60
99
 
61
100
  if (!nextPageUrl) {
62
- throw new Error('Cant parse next page URL');
101
+ throw new Error("Can't parse next page URL");
63
102
  }
64
103
 
65
- console.log('Results recieved with next page: ', nextPageUrl);
104
+ console.log('Results received with next page: ', nextPageUrl);
66
105
  const nextPageResult = await _callPhrase(nextPageUrl, options);
67
106
  return [...result, ...nextPageResult];
68
107
  }
@@ -109,23 +148,73 @@ async function pullAllTranslations(branch) {
109
148
 
110
149
  return translations;
111
150
  }
112
- async function pushTranslationsByLocale(contents, locale, branch) {
151
+ async function pushTranslations(translationsByLanguage, {
152
+ devLanguage,
153
+ branch
154
+ }) {
113
155
  const formData = new FormData__default['default']();
114
- const fileContents = Buffer.from(JSON.stringify(contents));
156
+ const {
157
+ csvString,
158
+ localeMapping,
159
+ keyIndex,
160
+ commentIndex,
161
+ tagColumn
162
+ } = translationsToCsv(translationsByLanguage, devLanguage);
163
+ const fileContents = Buffer.from(csvString);
115
164
  formData.append('file', fileContents, {
116
- contentType: 'application/json',
117
- filename: `${locale}.json`
165
+ contentType: 'text/csv',
166
+ filename: `translations.csv`
118
167
  });
119
- formData.append('file_format', 'json');
120
- formData.append('locale_id', locale);
168
+ formData.append('file_format', 'csv');
121
169
  formData.append('branch', branch);
122
170
  formData.append('update_translations', 'true');
123
- trace('Starting to upload:', locale);
124
- await callPhrase(`uploads`, {
171
+
172
+ for (const [locale, index] of Object.entries(localeMapping)) {
173
+ formData.append(`locale_mapping[${locale}]`, index);
174
+ }
175
+
176
+ formData.append('format_options[key_index]', keyIndex);
177
+ formData.append('format_options[comment_index]', commentIndex);
178
+ formData.append('format_options[tag_column]', tagColumn);
179
+ formData.append('format_options[enable_pluralization]', 'false');
180
+ log('Uploading translations');
181
+ const res = await callPhrase(`uploads`, {
125
182
  method: 'POST',
126
183
  body: formData
127
184
  });
128
- log('Successfully Uploaded:', locale, '\n');
185
+ trace('Upload result:\n', res); // TODO: Figure out error handling
186
+
187
+ const {
188
+ id
189
+ } = res;
190
+
191
+ if (id) {
192
+ log('Upload ID:', id, '\n');
193
+ log('Successfully Uploaded\n');
194
+ } else {
195
+ log('Error uploading');
196
+ throw new Error('Error uploading');
197
+ }
198
+
199
+ return {
200
+ uploadId: id
201
+ };
202
+ }
203
+ async function deleteUnusedKeys(uploadId, branch) {
204
+ const query = `unmentioned_in_upload:${uploadId}`;
205
+ const {
206
+ records_affected
207
+ } = await callPhrase('keys', {
208
+ method: 'DELETE',
209
+ headers: {
210
+ 'Content-Type': 'application/json'
211
+ },
212
+ body: JSON.stringify({
213
+ branch,
214
+ q: query
215
+ })
216
+ });
217
+ log('Successfully deleted', records_affected, 'unused keys from branch', branch);
129
218
  }
130
219
  async function ensureBranch(branch) {
131
220
  await callPhrase(`branches`, {
@@ -137,7 +226,7 @@ async function ensureBranch(branch) {
137
226
  name: branch
138
227
  })
139
228
  });
140
- trace('Created branch:', branch);
229
+ log('Created branch:', branch);
141
230
  }
142
231
 
143
232
  async function pull({
@@ -149,19 +238,16 @@ async function pull({
149
238
  const allPhraseTranslations = await pullAllTranslations(branch);
150
239
  trace(`Pulling translations from Phrase for languages ${config.devLanguage} and ${alternativeLanguages.join(', ')}`);
151
240
  const phraseLanguages = Object.keys(allPhraseTranslations);
152
- const phraseLanguagesWithTranslations = phraseLanguages.filter(language => {
153
- const phraseTranslationsForLanguage = allPhraseTranslations[language];
154
- return Object.keys(phraseTranslationsForLanguage).length > 0;
155
- });
156
- trace(`Found Phrase translations for languages ${phraseLanguagesWithTranslations.join(', ')}`);
241
+ trace(`Found Phrase translations for languages ${phraseLanguages.join(', ')}`);
157
242
 
158
- if (!phraseLanguagesWithTranslations.includes(config.devLanguage)) {
159
- throw new Error(`Phrase did not return any translations for dev language "${config.devLanguage}".\nEnsure you have configured your Phrase project for your dev language, and have pushed your translations.`);
243
+ if (!phraseLanguages.includes(config.devLanguage)) {
244
+ 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.`);
160
245
  }
161
246
 
162
247
  const allVocabTranslations = await core.loadAllTranslations({
163
248
  fallbacks: 'none',
164
- includeNodeModules: false
249
+ includeNodeModules: false,
250
+ withTags: true
165
251
  }, config);
166
252
 
167
253
  for (const loadedTranslation of allVocabTranslations) {
@@ -216,14 +302,17 @@ async function pull({
216
302
  }
217
303
 
218
304
  /**
219
- * Uploading to the Phrase API for each language. Adding a unique namespace to each key using file path they key came from
305
+ * Uploads translations to the Phrase API for each language.
306
+ * A unique namespace is appended to each key using the file path the key came from.
220
307
  */
221
308
  async function push({
222
- branch
309
+ branch,
310
+ deleteUnusedKeys: deleteUnusedKeys$1
223
311
  }, config) {
224
312
  const allLanguageTranslations = await core.loadAllTranslations({
225
313
  fallbacks: 'none',
226
- includeNodeModules: false
314
+ includeNodeModules: false,
315
+ withTags: true
227
316
  }, config);
228
317
  trace(`Pushing translations to branch ${branch}`);
229
318
  const allLanguages = config.languages.map(v => v.name);
@@ -250,10 +339,15 @@ async function push({
250
339
  }
251
340
  }
252
341
 
253
- for (const language of allLanguages) {
254
- if (phraseTranslations[language]) {
255
- await pushTranslationsByLocale(phraseTranslations[language], language, branch);
256
- }
342
+ const {
343
+ uploadId
344
+ } = await pushTranslations(phraseTranslations, {
345
+ devLanguage: config.devLanguage,
346
+ branch
347
+ });
348
+
349
+ if (deleteUnusedKeys$1) {
350
+ await deleteUnusedKeys(uploadId, branch);
257
351
  }
258
352
  }
259
353
 
@@ -9,6 +9,7 @@ var FormData = require('form-data');
9
9
  var fetch = require('node-fetch');
10
10
  var chalk = require('chalk');
11
11
  var debug = require('debug');
12
+ var sync = require('csv-stringify/sync');
12
13
 
13
14
  function _interopDefault (e) { return e && e.__esModule ? e : { 'default': e }; }
14
15
 
@@ -27,6 +28,44 @@ const log = (...params) => {
27
28
  console.log(chalk__default['default'].yellow('Vocab'), ...params);
28
29
  };
29
30
 
31
+ function translationsToCsv(translations, devLanguage) {
32
+ const languages = Object.keys(translations);
33
+ const altLanguages = languages.filter(language => language !== devLanguage);
34
+ const devLanguageTranslations = translations[devLanguage];
35
+ const csv = Object.entries(devLanguageTranslations).map(([key, {
36
+ message,
37
+ description,
38
+ tags
39
+ }]) => {
40
+ const altTranslationMessages = altLanguages.map(language => {
41
+ var _translations$languag, _translations$languag2;
42
+
43
+ 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;
44
+ });
45
+ return [message, ...altTranslationMessages, key, description, tags === null || tags === void 0 ? void 0 : tags.join(',')];
46
+ }); // Not spreading `languages` to ensure correct ordering of dev language first
47
+ // then alt languages
48
+
49
+ const csvString = sync.stringify(csv, {
50
+ delimiter: ',',
51
+ header: false
52
+ }); // Column indices start at 1
53
+
54
+ const localeMapping = Object.fromEntries(languages.map((language, index) => [language, index + 1]));
55
+ const keyIndex = languages.length + 1;
56
+ const commentIndex = keyIndex + 1;
57
+ const tagColumn = commentIndex + 1;
58
+ return {
59
+ csvString,
60
+ localeMapping,
61
+ keyIndex,
62
+ commentIndex,
63
+ tagColumn
64
+ };
65
+ }
66
+
67
+ /* eslint-disable no-console */
68
+
30
69
  function _callPhrase(path, options = {}) {
31
70
  const phraseApiToken = process.env.PHRASE_API_TOKEN;
32
71
 
@@ -44,14 +83,14 @@ function _callPhrase(path, options = {}) {
44
83
  }).then(async response => {
45
84
  console.log(`${path}: ${response.status} - ${response.statusText}`);
46
85
  console.log(`Rate Limit: ${response.headers.get('X-Rate-Limit-Remaining')} of ${response.headers.get('X-Rate-Limit-Limit')} remaining. (${response.headers.get('X-Rate-Limit-Reset')} seconds remaining})`);
47
- console.log('\nLink:', response.headers.get('Link'), '\n'); // Print All Headers:
86
+ trace('\nLink:', response.headers.get('Link'), '\n'); // Print All Headers:
48
87
  // console.log(Array.from(r.headers.entries()));
49
88
 
50
89
  try {
51
90
  var _response$headers$get;
52
91
 
53
92
  const result = await response.json();
54
- console.log(`Internal Result (Length: ${result.length})\n`);
93
+ trace(`Internal Result (Length: ${result.length})\n`);
55
94
 
56
95
  if ((!options.method || options.method === 'GET') && (_response$headers$get = response.headers.get('Link')) !== null && _response$headers$get !== void 0 && _response$headers$get.includes('rel=next')) {
57
96
  var _response$headers$get2, _response$headers$get3;
@@ -59,10 +98,10 @@ function _callPhrase(path, options = {}) {
59
98
  const [, nextPageUrl] = (_response$headers$get2 = (_response$headers$get3 = response.headers.get('Link')) === null || _response$headers$get3 === void 0 ? void 0 : _response$headers$get3.match(/<([^>]*)>; rel=next/)) !== null && _response$headers$get2 !== void 0 ? _response$headers$get2 : [];
60
99
 
61
100
  if (!nextPageUrl) {
62
- throw new Error('Cant parse next page URL');
101
+ throw new Error("Can't parse next page URL");
63
102
  }
64
103
 
65
- console.log('Results recieved with next page: ', nextPageUrl);
104
+ console.log('Results received with next page: ', nextPageUrl);
66
105
  const nextPageResult = await _callPhrase(nextPageUrl, options);
67
106
  return [...result, ...nextPageResult];
68
107
  }
@@ -109,23 +148,73 @@ async function pullAllTranslations(branch) {
109
148
 
110
149
  return translations;
111
150
  }
112
- async function pushTranslationsByLocale(contents, locale, branch) {
151
+ async function pushTranslations(translationsByLanguage, {
152
+ devLanguage,
153
+ branch
154
+ }) {
113
155
  const formData = new FormData__default['default']();
114
- const fileContents = Buffer.from(JSON.stringify(contents));
156
+ const {
157
+ csvString,
158
+ localeMapping,
159
+ keyIndex,
160
+ commentIndex,
161
+ tagColumn
162
+ } = translationsToCsv(translationsByLanguage, devLanguage);
163
+ const fileContents = Buffer.from(csvString);
115
164
  formData.append('file', fileContents, {
116
- contentType: 'application/json',
117
- filename: `${locale}.json`
165
+ contentType: 'text/csv',
166
+ filename: `translations.csv`
118
167
  });
119
- formData.append('file_format', 'json');
120
- formData.append('locale_id', locale);
168
+ formData.append('file_format', 'csv');
121
169
  formData.append('branch', branch);
122
170
  formData.append('update_translations', 'true');
123
- trace('Starting to upload:', locale);
124
- await callPhrase(`uploads`, {
171
+
172
+ for (const [locale, index] of Object.entries(localeMapping)) {
173
+ formData.append(`locale_mapping[${locale}]`, index);
174
+ }
175
+
176
+ formData.append('format_options[key_index]', keyIndex);
177
+ formData.append('format_options[comment_index]', commentIndex);
178
+ formData.append('format_options[tag_column]', tagColumn);
179
+ formData.append('format_options[enable_pluralization]', 'false');
180
+ log('Uploading translations');
181
+ const res = await callPhrase(`uploads`, {
125
182
  method: 'POST',
126
183
  body: formData
127
184
  });
128
- log('Successfully Uploaded:', locale, '\n');
185
+ trace('Upload result:\n', res); // TODO: Figure out error handling
186
+
187
+ const {
188
+ id
189
+ } = res;
190
+
191
+ if (id) {
192
+ log('Upload ID:', id, '\n');
193
+ log('Successfully Uploaded\n');
194
+ } else {
195
+ log('Error uploading');
196
+ throw new Error('Error uploading');
197
+ }
198
+
199
+ return {
200
+ uploadId: id
201
+ };
202
+ }
203
+ async function deleteUnusedKeys(uploadId, branch) {
204
+ const query = `unmentioned_in_upload:${uploadId}`;
205
+ const {
206
+ records_affected
207
+ } = await callPhrase('keys', {
208
+ method: 'DELETE',
209
+ headers: {
210
+ 'Content-Type': 'application/json'
211
+ },
212
+ body: JSON.stringify({
213
+ branch,
214
+ q: query
215
+ })
216
+ });
217
+ log('Successfully deleted', records_affected, 'unused keys from branch', branch);
129
218
  }
130
219
  async function ensureBranch(branch) {
131
220
  await callPhrase(`branches`, {
@@ -137,7 +226,7 @@ async function ensureBranch(branch) {
137
226
  name: branch
138
227
  })
139
228
  });
140
- trace('Created branch:', branch);
229
+ log('Created branch:', branch);
141
230
  }
142
231
 
143
232
  async function pull({
@@ -149,19 +238,16 @@ async function pull({
149
238
  const allPhraseTranslations = await pullAllTranslations(branch);
150
239
  trace(`Pulling translations from Phrase for languages ${config.devLanguage} and ${alternativeLanguages.join(', ')}`);
151
240
  const phraseLanguages = Object.keys(allPhraseTranslations);
152
- const phraseLanguagesWithTranslations = phraseLanguages.filter(language => {
153
- const phraseTranslationsForLanguage = allPhraseTranslations[language];
154
- return Object.keys(phraseTranslationsForLanguage).length > 0;
155
- });
156
- trace(`Found Phrase translations for languages ${phraseLanguagesWithTranslations.join(', ')}`);
241
+ trace(`Found Phrase translations for languages ${phraseLanguages.join(', ')}`);
157
242
 
158
- if (!phraseLanguagesWithTranslations.includes(config.devLanguage)) {
159
- throw new Error(`Phrase did not return any translations for dev language "${config.devLanguage}".\nEnsure you have configured your Phrase project for your dev language, and have pushed your translations.`);
243
+ if (!phraseLanguages.includes(config.devLanguage)) {
244
+ 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.`);
160
245
  }
161
246
 
162
247
  const allVocabTranslations = await core.loadAllTranslations({
163
248
  fallbacks: 'none',
164
- includeNodeModules: false
249
+ includeNodeModules: false,
250
+ withTags: true
165
251
  }, config);
166
252
 
167
253
  for (const loadedTranslation of allVocabTranslations) {
@@ -216,14 +302,17 @@ async function pull({
216
302
  }
217
303
 
218
304
  /**
219
- * Uploading to the Phrase API for each language. Adding a unique namespace to each key using file path they key came from
305
+ * Uploads translations to the Phrase API for each language.
306
+ * A unique namespace is appended to each key using the file path the key came from.
220
307
  */
221
308
  async function push({
222
- branch
309
+ branch,
310
+ deleteUnusedKeys: deleteUnusedKeys$1
223
311
  }, config) {
224
312
  const allLanguageTranslations = await core.loadAllTranslations({
225
313
  fallbacks: 'none',
226
- includeNodeModules: false
314
+ includeNodeModules: false,
315
+ withTags: true
227
316
  }, config);
228
317
  trace(`Pushing translations to branch ${branch}`);
229
318
  const allLanguages = config.languages.map(v => v.name);
@@ -250,10 +339,15 @@ async function push({
250
339
  }
251
340
  }
252
341
 
253
- for (const language of allLanguages) {
254
- if (phraseTranslations[language]) {
255
- await pushTranslationsByLocale(phraseTranslations[language], language, branch);
256
- }
342
+ const {
343
+ uploadId
344
+ } = await pushTranslations(phraseTranslations, {
345
+ devLanguage: config.devLanguage,
346
+ branch
347
+ });
348
+
349
+ if (deleteUnusedKeys$1) {
350
+ await deleteUnusedKeys(uploadId, branch);
257
351
  }
258
352
  }
259
353