@vocab/phrase 1.0.1 → 1.2.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.
@@ -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); // Ensure languages are ordered for locale mapping
34
+
35
+ const orderedLanguages = [devLanguage, ...altLanguages];
36
+ const devLanguageTranslations = translations[devLanguage];
37
+ const csv = Object.entries(devLanguageTranslations).map(([key, {
38
+ message,
39
+ description,
40
+ tags
41
+ }]) => {
42
+ const altTranslationMessages = altLanguages.map(language => {
43
+ var _translations$languag, _translations$languag2;
44
+
45
+ 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;
46
+ });
47
+ return [message, ...altTranslationMessages, key, description, tags === null || tags === void 0 ? void 0 : tags.join(',')];
48
+ });
49
+ const csvString = sync.stringify(csv, {
50
+ delimiter: ',',
51
+ header: false
52
+ }); // Column indices start at 1
53
+
54
+ const localeMapping = Object.fromEntries(orderedLanguages.map((language, index) => [language, index + 1]));
55
+ const keyIndex = orderedLanguages.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,70 @@ 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 result = await callPhrase(`uploads`, {
125
182
  method: 'POST',
126
183
  body: formData
127
184
  });
128
- log('Successfully Uploaded:', locale, '\n');
185
+ trace('Upload result:\n', result);
186
+
187
+ if (result && 'id' in result) {
188
+ log('Upload ID:', result.id, '\n');
189
+ log('Successfully Uploaded\n');
190
+ } else {
191
+ log(`Error uploading: ${result === null || result === void 0 ? void 0 : result.message}\n`);
192
+ log('Response:', result);
193
+ throw new Error('Error uploading');
194
+ }
195
+
196
+ return {
197
+ uploadId: result.id
198
+ };
199
+ }
200
+ async function deleteUnusedKeys(uploadId, branch) {
201
+ const query = `unmentioned_in_upload:${uploadId}`;
202
+ const {
203
+ records_affected
204
+ } = await callPhrase('keys', {
205
+ method: 'DELETE',
206
+ headers: {
207
+ 'Content-Type': 'application/json'
208
+ },
209
+ body: JSON.stringify({
210
+ branch,
211
+ q: query
212
+ })
213
+ });
214
+ log('Successfully deleted', records_affected, 'unused keys from branch', branch);
129
215
  }
130
216
  async function ensureBranch(branch) {
131
217
  await callPhrase(`branches`, {
@@ -137,7 +223,7 @@ async function ensureBranch(branch) {
137
223
  name: branch
138
224
  })
139
225
  });
140
- trace('Created branch:', branch);
226
+ log('Created branch:', branch);
141
227
  }
142
228
 
143
229
  async function pull({
@@ -148,9 +234,17 @@ async function pull({
148
234
  const alternativeLanguages = core.getAltLanguages(config);
149
235
  const allPhraseTranslations = await pullAllTranslations(branch);
150
236
  trace(`Pulling translations from Phrase for languages ${config.devLanguage} and ${alternativeLanguages.join(', ')}`);
237
+ const phraseLanguages = Object.keys(allPhraseTranslations);
238
+ trace(`Found Phrase translations for languages ${phraseLanguages.join(', ')}`);
239
+
240
+ if (!phraseLanguages.includes(config.devLanguage)) {
241
+ 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.`);
242
+ }
243
+
151
244
  const allVocabTranslations = await core.loadAllTranslations({
152
245
  fallbacks: 'none',
153
- includeNodeModules: false
246
+ includeNodeModules: false,
247
+ withTags: true
154
248
  }, config);
155
249
 
156
250
  for (const loadedTranslation of allVocabTranslations) {
@@ -168,6 +262,11 @@ async function pull({
168
262
  defaultValues[key] = { ...defaultValues[key],
169
263
  ...allPhraseTranslations[config.devLanguage][core.getUniqueKey(key, loadedTranslation.namespace)]
170
264
  };
265
+ } // Only write a `_meta` field if necessary
266
+
267
+
268
+ if (Object.keys(loadedTranslation.metadata).length > 0) {
269
+ defaultValues._meta = loadedTranslation.metadata;
171
270
  }
172
271
 
173
272
  await writeFile(loadedTranslation.filePath, `${JSON.stringify(defaultValues, null, 2)}\n`);
@@ -205,14 +304,17 @@ async function pull({
205
304
  }
206
305
 
207
306
  /**
208
- * Uploading to the Phrase API for each language. Adding a unique namespace to each key using file path they key came from
307
+ * Uploads translations to the Phrase API for each language.
308
+ * A unique namespace is appended to each key using the file path the key came from.
209
309
  */
210
310
  async function push({
211
- branch
311
+ branch,
312
+ deleteUnusedKeys: deleteUnusedKeys$1
212
313
  }, config) {
213
314
  const allLanguageTranslations = await core.loadAllTranslations({
214
315
  fallbacks: 'none',
215
- includeNodeModules: false
316
+ includeNodeModules: false,
317
+ withTags: true
216
318
  }, config);
217
319
  trace(`Pushing translations to branch ${branch}`);
218
320
  const allLanguages = config.languages.map(v => v.name);
@@ -232,17 +334,37 @@ async function push({
232
334
  phraseTranslations[language] = {};
233
335
  }
234
336
 
337
+ const {
338
+ metadata: {
339
+ tags: sharedTags = []
340
+ }
341
+ } = loadedTranslation;
342
+
235
343
  for (const localKey of Object.keys(localTranslations)) {
236
344
  const phraseKey = core.getUniqueKey(localKey, loadedTranslation.namespace);
237
- phraseTranslations[language][phraseKey] = localTranslations[localKey];
345
+ const {
346
+ tags = [],
347
+ ...localTranslation
348
+ } = localTranslations[localKey];
349
+
350
+ if (language === config.devLanguage) {
351
+ localTranslation.tags = [...tags, ...sharedTags];
352
+ }
353
+
354
+ phraseTranslations[language][phraseKey] = localTranslation;
238
355
  }
239
356
  }
240
357
  }
241
358
 
242
- for (const language of allLanguages) {
243
- if (phraseTranslations[language]) {
244
- await pushTranslationsByLocale(phraseTranslations[language], language, branch);
245
- }
359
+ const {
360
+ uploadId
361
+ } = await pushTranslations(phraseTranslations, {
362
+ devLanguage: config.devLanguage,
363
+ branch
364
+ });
365
+
366
+ if (deleteUnusedKeys$1) {
367
+ await deleteUnusedKeys(uploadId, branch);
246
368
  }
247
369
  }
248
370
 
@@ -5,6 +5,7 @@ import FormData from 'form-data';
5
5
  import fetch from 'node-fetch';
6
6
  import chalk from 'chalk';
7
7
  import debug from 'debug';
8
+ import { stringify } from 'csv-stringify/sync';
8
9
 
9
10
  const mkdir = promises.mkdir;
10
11
  const writeFile = promises.writeFile;
@@ -15,6 +16,44 @@ const log = (...params) => {
15
16
  console.log(chalk.yellow('Vocab'), ...params);
16
17
  };
17
18
 
19
+ function translationsToCsv(translations, devLanguage) {
20
+ const languages = Object.keys(translations);
21
+ const altLanguages = languages.filter(language => language !== devLanguage); // Ensure languages are ordered for locale mapping
22
+
23
+ const orderedLanguages = [devLanguage, ...altLanguages];
24
+ const devLanguageTranslations = translations[devLanguage];
25
+ const csv = Object.entries(devLanguageTranslations).map(([key, {
26
+ message,
27
+ description,
28
+ tags
29
+ }]) => {
30
+ const altTranslationMessages = altLanguages.map(language => {
31
+ var _translations$languag, _translations$languag2;
32
+
33
+ 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;
34
+ });
35
+ return [message, ...altTranslationMessages, key, description, tags === null || tags === void 0 ? void 0 : tags.join(',')];
36
+ });
37
+ const csvString = stringify(csv, {
38
+ delimiter: ',',
39
+ header: false
40
+ }); // Column indices start at 1
41
+
42
+ const localeMapping = Object.fromEntries(orderedLanguages.map((language, index) => [language, index + 1]));
43
+ const keyIndex = orderedLanguages.length + 1;
44
+ const commentIndex = keyIndex + 1;
45
+ const tagColumn = commentIndex + 1;
46
+ return {
47
+ csvString,
48
+ localeMapping,
49
+ keyIndex,
50
+ commentIndex,
51
+ tagColumn
52
+ };
53
+ }
54
+
55
+ /* eslint-disable no-console */
56
+
18
57
  function _callPhrase(path, options = {}) {
19
58
  const phraseApiToken = process.env.PHRASE_API_TOKEN;
20
59
 
@@ -32,14 +71,14 @@ function _callPhrase(path, options = {}) {
32
71
  }).then(async response => {
33
72
  console.log(`${path}: ${response.status} - ${response.statusText}`);
34
73
  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
- console.log('\nLink:', response.headers.get('Link'), '\n'); // Print All Headers:
74
+ trace('\nLink:', response.headers.get('Link'), '\n'); // Print All Headers:
36
75
  // console.log(Array.from(r.headers.entries()));
37
76
 
38
77
  try {
39
78
  var _response$headers$get;
40
79
 
41
80
  const result = await response.json();
42
- console.log(`Internal Result (Length: ${result.length})\n`);
81
+ trace(`Internal Result (Length: ${result.length})\n`);
43
82
 
44
83
  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
84
  var _response$headers$get2, _response$headers$get3;
@@ -47,10 +86,10 @@ function _callPhrase(path, options = {}) {
47
86
  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
87
 
49
88
  if (!nextPageUrl) {
50
- throw new Error('Cant parse next page URL');
89
+ throw new Error("Can't parse next page URL");
51
90
  }
52
91
 
53
- console.log('Results recieved with next page: ', nextPageUrl);
92
+ console.log('Results received with next page: ', nextPageUrl);
54
93
  const nextPageResult = await _callPhrase(nextPageUrl, options);
55
94
  return [...result, ...nextPageResult];
56
95
  }
@@ -97,23 +136,70 @@ async function pullAllTranslations(branch) {
97
136
 
98
137
  return translations;
99
138
  }
100
- async function pushTranslationsByLocale(contents, locale, branch) {
139
+ async function pushTranslations(translationsByLanguage, {
140
+ devLanguage,
141
+ branch
142
+ }) {
101
143
  const formData = new FormData();
102
- const fileContents = Buffer.from(JSON.stringify(contents));
144
+ const {
145
+ csvString,
146
+ localeMapping,
147
+ keyIndex,
148
+ commentIndex,
149
+ tagColumn
150
+ } = translationsToCsv(translationsByLanguage, devLanguage);
151
+ const fileContents = Buffer.from(csvString);
103
152
  formData.append('file', fileContents, {
104
- contentType: 'application/json',
105
- filename: `${locale}.json`
153
+ contentType: 'text/csv',
154
+ filename: 'translations.csv'
106
155
  });
107
- formData.append('file_format', 'json');
108
- formData.append('locale_id', locale);
156
+ formData.append('file_format', 'csv');
109
157
  formData.append('branch', branch);
110
158
  formData.append('update_translations', 'true');
111
- trace('Starting to upload:', locale);
112
- await callPhrase(`uploads`, {
159
+
160
+ for (const [locale, index] of Object.entries(localeMapping)) {
161
+ formData.append(`locale_mapping[${locale}]`, index);
162
+ }
163
+
164
+ formData.append('format_options[key_index]', keyIndex);
165
+ formData.append('format_options[comment_index]', commentIndex);
166
+ formData.append('format_options[tag_column]', tagColumn);
167
+ formData.append('format_options[enable_pluralization]', 'false');
168
+ log('Uploading translations');
169
+ const result = await callPhrase(`uploads`, {
113
170
  method: 'POST',
114
171
  body: formData
115
172
  });
116
- log('Successfully Uploaded:', locale, '\n');
173
+ trace('Upload result:\n', result);
174
+
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
+
184
+ return {
185
+ uploadId: result.id
186
+ };
187
+ }
188
+ async function deleteUnusedKeys(uploadId, branch) {
189
+ const query = `unmentioned_in_upload:${uploadId}`;
190
+ const {
191
+ records_affected
192
+ } = await callPhrase('keys', {
193
+ method: 'DELETE',
194
+ headers: {
195
+ 'Content-Type': 'application/json'
196
+ },
197
+ body: JSON.stringify({
198
+ branch,
199
+ q: query
200
+ })
201
+ });
202
+ log('Successfully deleted', records_affected, 'unused keys from branch', branch);
117
203
  }
118
204
  async function ensureBranch(branch) {
119
205
  await callPhrase(`branches`, {
@@ -125,7 +211,7 @@ async function ensureBranch(branch) {
125
211
  name: branch
126
212
  })
127
213
  });
128
- trace('Created branch:', branch);
214
+ log('Created branch:', branch);
129
215
  }
130
216
 
131
217
  async function pull({
@@ -136,9 +222,17 @@ async function pull({
136
222
  const alternativeLanguages = getAltLanguages(config);
137
223
  const allPhraseTranslations = await pullAllTranslations(branch);
138
224
  trace(`Pulling translations from Phrase for languages ${config.devLanguage} and ${alternativeLanguages.join(', ')}`);
225
+ const phraseLanguages = Object.keys(allPhraseTranslations);
226
+ trace(`Found Phrase translations for languages ${phraseLanguages.join(', ')}`);
227
+
228
+ if (!phraseLanguages.includes(config.devLanguage)) {
229
+ 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.`);
230
+ }
231
+
139
232
  const allVocabTranslations = await loadAllTranslations({
140
233
  fallbacks: 'none',
141
- includeNodeModules: false
234
+ includeNodeModules: false,
235
+ withTags: true
142
236
  }, config);
143
237
 
144
238
  for (const loadedTranslation of allVocabTranslations) {
@@ -156,6 +250,11 @@ async function pull({
156
250
  defaultValues[key] = { ...defaultValues[key],
157
251
  ...allPhraseTranslations[config.devLanguage][getUniqueKey(key, loadedTranslation.namespace)]
158
252
  };
253
+ } // Only write a `_meta` field if necessary
254
+
255
+
256
+ if (Object.keys(loadedTranslation.metadata).length > 0) {
257
+ defaultValues._meta = loadedTranslation.metadata;
159
258
  }
160
259
 
161
260
  await writeFile(loadedTranslation.filePath, `${JSON.stringify(defaultValues, null, 2)}\n`);
@@ -193,14 +292,17 @@ async function pull({
193
292
  }
194
293
 
195
294
  /**
196
- * Uploading to the Phrase API for each language. Adding a unique namespace to each key using file path they key came from
295
+ * Uploads translations to the Phrase API for each language.
296
+ * A unique namespace is appended to each key using the file path the key came from.
197
297
  */
198
298
  async function push({
199
- branch
299
+ branch,
300
+ deleteUnusedKeys: deleteUnusedKeys$1
200
301
  }, config) {
201
302
  const allLanguageTranslations = await loadAllTranslations({
202
303
  fallbacks: 'none',
203
- includeNodeModules: false
304
+ includeNodeModules: false,
305
+ withTags: true
204
306
  }, config);
205
307
  trace(`Pushing translations to branch ${branch}`);
206
308
  const allLanguages = config.languages.map(v => v.name);
@@ -220,17 +322,37 @@ async function push({
220
322
  phraseTranslations[language] = {};
221
323
  }
222
324
 
325
+ const {
326
+ metadata: {
327
+ tags: sharedTags = []
328
+ }
329
+ } = loadedTranslation;
330
+
223
331
  for (const localKey of Object.keys(localTranslations)) {
224
332
  const phraseKey = getUniqueKey(localKey, loadedTranslation.namespace);
225
- phraseTranslations[language][phraseKey] = localTranslations[localKey];
333
+ const {
334
+ tags = [],
335
+ ...localTranslation
336
+ } = localTranslations[localKey];
337
+
338
+ if (language === config.devLanguage) {
339
+ localTranslation.tags = [...tags, ...sharedTags];
340
+ }
341
+
342
+ phraseTranslations[language][phraseKey] = localTranslation;
226
343
  }
227
344
  }
228
345
  }
229
346
 
230
- for (const language of allLanguages) {
231
- if (phraseTranslations[language]) {
232
- await pushTranslationsByLocale(phraseTranslations[language], language, branch);
233
- }
347
+ const {
348
+ uploadId
349
+ } = await pushTranslations(phraseTranslations, {
350
+ devLanguage: config.devLanguage,
351
+ branch
352
+ });
353
+
354
+ if (deleteUnusedKeys$1) {
355
+ await deleteUnusedKeys(uploadId, branch);
234
356
  }
235
357
  }
236
358
 
package/package.json CHANGED
@@ -1,19 +1,23 @@
1
1
  {
2
2
  "name": "@vocab/phrase",
3
- "version": "1.0.1",
3
+ "version": "1.2.0",
4
4
  "main": "dist/vocab-phrase.cjs.js",
5
5
  "module": "dist/vocab-phrase.esm.js",
6
6
  "author": "SEEK",
7
7
  "license": "MIT",
8
8
  "dependencies": {
9
- "@vocab/core": "^1.0.0",
10
- "@vocab/types": "^1.0.0",
9
+ "@vocab/core": "^1.2.0",
10
+ "@vocab/types": "^1.1.2",
11
11
  "chalk": "^4.1.0",
12
+ "csv-stringify": "^6.2.3",
12
13
  "debug": "^4.3.1",
13
14
  "form-data": "^3.0.0",
14
15
  "node-fetch": "^2.6.1"
15
16
  },
16
17
  "devDependencies": {
17
18
  "@types/node-fetch": "^2.5.7"
18
- }
19
+ },
20
+ "files": [
21
+ "dist"
22
+ ]
19
23
  }
package/CHANGELOG.md DELETED
@@ -1,122 +0,0 @@
1
- # @vocab/phrase
2
-
3
- ## 1.0.1
4
-
5
- ### Patch Changes
6
-
7
- - [`20eec77`](https://github.com/seek-oss/vocab/commit/20eec770705d05048ad8b32575cb92720b887f5b) [#76](https://github.com/seek-oss/vocab/pull/76) Thanks [@askoufis](https://github.com/askoufis)! - `vocab pull` no longer errors when phrase returns no translations for a configured language
8
-
9
- ## 1.0.0
10
-
11
- ### Major Changes
12
-
13
- - [`3031054`](https://github.com/seek-oss/vocab/commit/303105440851db6126f0606e1607745b27dd981c) [#51](https://github.com/seek-oss/vocab/pull/51) Thanks [@jahredhope](https://github.com/jahredhope)! - Release v1.0.0
14
-
15
- Release Vocab as v1.0.0 to signify a stable API and support future [semver versioning](https://semver.org/) releases.
16
-
17
- Vocab has seen a lot of iteration and changes since it was first published on 20 November 2020. We are now confident with the API and believe Vocab is ready for common use.
18
-
19
- ### Patch Changes
20
-
21
- - Updated dependencies [[`0074382`](https://github.com/seek-oss/vocab/commit/007438273ef70f5d5ded45777933651ad8df36f6), [`3031054`](https://github.com/seek-oss/vocab/commit/303105440851db6126f0606e1607745b27dd981c)]:
22
- - @vocab/core@1.0.0
23
- - @vocab/types@1.0.0
24
-
25
- ## 0.0.11
26
-
27
- ### Patch Changes
28
-
29
- - Updated dependencies [[`5b1fdc0`](https://github.com/seek-oss/vocab/commit/5b1fdc019522b12e7ef94b2fec57b54a9310d41c)]:
30
- - @vocab/core@0.0.11
31
- - @vocab/types@0.0.9
32
-
33
- ## 0.0.10
34
-
35
- ### Patch Changes
36
-
37
- - Updated dependencies [[`7c96a14`](https://github.com/seek-oss/vocab/commit/7c96a142f602132d38c1df1a47a1f4657dc5c94c)]:
38
- - @vocab/core@0.0.10
39
-
40
- ## 0.0.9
41
-
42
- ### Patch Changes
43
-
44
- - Updated dependencies [[`3034bd3`](https://github.com/seek-oss/vocab/commit/3034bd3de610a9d1f3bfbd8caefa27064dee2710), [`c110745`](https://github.com/seek-oss/vocab/commit/c110745b79df1a8ade6b1d8a49e798b04a7b95e1)]:
45
- - @vocab/core@0.0.9
46
-
47
- ## 0.0.8
48
-
49
- ### Patch Changes
50
-
51
- - Updated dependencies [[`f2fca67`](https://github.com/seek-oss/vocab/commit/f2fca679c66ae65405a0aa24f0a0e472026aad0d)]:
52
- - @vocab/core@0.0.8
53
- - @vocab/types@0.0.8
54
-
55
- ## 0.0.7
56
-
57
- ### Patch Changes
58
-
59
- - [`283bcad`](https://github.com/seek-oss/vocab/commit/283bcada06e622ab14ed891743ed3f55cf09e245) [#33](https://github.com/seek-oss/vocab/pull/33) Thanks [@mattcompiles](https://github.com/mattcompiles)! - Move all vocab files to single directory with configurable suffix
60
-
61
- - Updated dependencies [[`283bcad`](https://github.com/seek-oss/vocab/commit/283bcada06e622ab14ed891743ed3f55cf09e245), [`ad0d240`](https://github.com/seek-oss/vocab/commit/ad0d2404545ded8e11621eae8f29467ff3352366), [`f3992ef`](https://github.com/seek-oss/vocab/commit/f3992efbf08939ebf853fac650a49cc46dc51dfb), [`f3992ef`](https://github.com/seek-oss/vocab/commit/f3992efbf08939ebf853fac650a49cc46dc51dfb)]:
62
- - @vocab/core@0.0.7
63
- - @vocab/types@0.0.7
64
-
65
- ## 0.0.6
66
-
67
- ### Patch Changes
68
-
69
- - [`80a46c0`](https://github.com/seek-oss/vocab/commit/80a46c01a55408675f5822c3618519f80136c3ab) [#27](https://github.com/seek-oss/vocab/pull/27) Thanks [@mattcompiles](https://github.com/mattcompiles)! - Add `ignore` config for ignoring files/folders from cli scripts
70
-
71
- * [`80a46c0`](https://github.com/seek-oss/vocab/commit/80a46c01a55408675f5822c3618519f80136c3ab) [#27](https://github.com/seek-oss/vocab/pull/27) Thanks [@mattcompiles](https://github.com/mattcompiles)! - Ignore node_modules from push, pull and compile scripts
72
-
73
- * Updated dependencies [[`80a46c0`](https://github.com/seek-oss/vocab/commit/80a46c01a55408675f5822c3618519f80136c3ab), [`80a46c0`](https://github.com/seek-oss/vocab/commit/80a46c01a55408675f5822c3618519f80136c3ab)]:
74
- - @vocab/core@0.0.6
75
- - @vocab/types@0.0.6
76
-
77
- ## 0.0.5
78
-
79
- ### Patch Changes
80
-
81
- - Updated dependencies [[`371ed16`](https://github.com/seek-oss/vocab/commit/371ed16a232a04dab13afa7e2b352dfb6724eea4), [`c222d68`](https://github.com/seek-oss/vocab/commit/c222d68a3c0c24723a338eccb959798881f6a118)]:
82
- - @vocab/core@0.0.5
83
-
84
- ## 0.0.4
85
-
86
- ### Patch Changes
87
-
88
- - [`5f5c581`](https://github.com/seek-oss/vocab/commit/5f5c581a65bff28729ee19e1ec0bdea488a9d6c2) [#19](https://github.com/seek-oss/vocab/pull/19) Thanks [@jahredhope](https://github.com/jahredhope)! - Compile useable TypeScript importable files with `vocab compile`.
89
-
90
- The new `vocab compile` step replaces `vocab generate-types` in creating a fully functional **translations.ts** file.
91
-
92
- This allows vocab to be used **without the Webpack Plugin**, however use of the plugin is still heavily advised to ensure optimal loading of translation content on the web.
93
-
94
- Support for unit testing is now better than ever! The newly created **translations.ts** means your unit test code will see the same code as available while rendering.
95
-
96
- See the [documentation](https://github.com/seek-oss/vocab) for further usage details.
97
-
98
- * [`02f943c`](https://github.com/seek-oss/vocab/commit/02f943ca892913b41f9e4720a72400777cf14b3d) [#17](https://github.com/seek-oss/vocab/pull/17) Thanks [@jahredhope](https://github.com/jahredhope)! - Add additional debug traces
99
-
100
- * Updated dependencies [[`5f5c581`](https://github.com/seek-oss/vocab/commit/5f5c581a65bff28729ee19e1ec0bdea488a9d6c2), [`02f943c`](https://github.com/seek-oss/vocab/commit/02f943ca892913b41f9e4720a72400777cf14b3d)]:
101
- - @vocab/core@0.0.4
102
- - @vocab/types@0.0.5
103
-
104
- ## 0.0.3
105
-
106
- ### Patch Changes
107
-
108
- - [`08de30d`](https://github.com/seek-oss/vocab/commit/08de30d338c2a5ebdcf14da7c736dddf22e7ca9e) [#14](https://github.com/seek-oss/vocab/pull/14) Thanks [@mattcompiles](https://github.com/mattcompiles)! - Add ability to override files namespace with \$namespace
109
-
110
- * [`26b52f4`](https://github.com/seek-oss/vocab/commit/26b52f4878ded440841e08c858bdc9e685500c2a) [#16](https://github.com/seek-oss/vocab/pull/16) Thanks [@jahredhope](https://github.com/jahredhope)! - Enable debugging with DEBUG environment variable
111
-
112
- * Updated dependencies [[`08de30d`](https://github.com/seek-oss/vocab/commit/08de30d338c2a5ebdcf14da7c736dddf22e7ca9e), [`ed6cf40`](https://github.com/seek-oss/vocab/commit/ed6cf408973f2e9c4d07a71fcb52f40294ebaf65), [`26b52f4`](https://github.com/seek-oss/vocab/commit/26b52f4878ded440841e08c858bdc9e685500c2a), [`b5a5a05`](https://github.com/seek-oss/vocab/commit/b5a5a05a5bb87b48e6e9160af75f555728143ea2)]:
113
- - @vocab/core@0.0.3
114
- - @vocab/types@0.0.4
115
-
116
- ## 0.0.2
117
-
118
- ### Patch Changes
119
-
120
- - Updated dependencies [[`4710f34`](https://github.com/seek-oss/vocab/commit/4710f341f2827643e3eff69ef7e26d44ec6e8a2b), [`4710f34`](https://github.com/seek-oss/vocab/commit/4710f341f2827643e3eff69ef7e26d44ec6e8a2b)]:
121
- - @vocab/types@0.0.3
122
- - @vocab/core@0.0.2