@vocab/phrase 0.0.10 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,35 @@
1
1
  # @vocab/phrase
2
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
+
3
33
  ## 0.0.10
4
34
 
5
35
  ### Patch Changes
package/README.md CHANGED
@@ -1,35 +1,32 @@
1
1
  # Vocab
2
2
 
3
- Vocab is a strongly typed internationalisation framework for React.
3
+ Vocab is a strongly typed internationalization framework for React.
4
4
 
5
- ## Getting started
5
+ Vocab helps you ship multiple languages without compromising the reliability of your site or slowing down delivery.
6
6
 
7
- ### Step 1: Install Dependencies
7
+ - Shareable translations
8
8
 
9
- Vocab is a monorepo with different packages you can install depending on your usage, the below list will get you started using the cli, React and webpack integrations.
9
+ Translations are co-located with the components that use them. Vocab uses the module graph allowing shared components to be installed with package managers like npm, just like any other module.
10
10
 
11
- ```bash
12
- $ npm i --save @vocab/cli @vocab/react @vocab/webpack
13
- ```
11
+ - Loading translations dynamically
14
12
 
15
- ### Step 2: Setup Webpack plugin
13
+ Vocab only loads the current user's language. If the language changes Vocab can load the new language behind the scenes without reloading the page.
16
14
 
17
- Before starting to write code you'll need to setup webpack to understand how to use `translation.json` files.
15
+ - Strongly typed with TypeScript
18
16
 
19
- This is done using the **VocabWebpackPlugin**.
17
+ When using translations TypeScript will ensure code only accesses valid translations and translations are passed all required dynamic values.
20
18
 
21
- **webpack.config.js**
19
+ ## Getting started
22
20
 
23
- ```js
24
- const VocabWebpackPlugin = require('@vocab/webpack').default;
21
+ ### Step 1: Install Dependencies
25
22
 
26
- module.exports = {
27
- ...,
28
- plugins: [new VocabWebpackPlugin({})]
29
- }
23
+ Vocab is a monorepo with different packages you can install depending on your usage, the below list will get you started using the cli, React and webpack integrations.
24
+
25
+ ```bash
26
+ $ npm i --save @vocab/cli @vocab/react @vocab/webpack
30
27
  ```
31
28
 
32
- ### Step 3: Configure Vocab
29
+ ### Step 2: Configure Vocab
33
30
 
34
31
  You can configure Vocab directly when calling the API or via a `vocab.config.js` file.
35
32
 
@@ -48,7 +45,7 @@ module.exports = {
48
45
 
49
46
  Vocab doesn't tell you how to select or change your language. You just need to tell Vocab what language to use.
50
47
 
51
- **Note:** Using methods discussed later we'll make sure the first language is loaded on page load. However, after this changing languages may then lead to a period of no translations as Vocab downloads the new language's translations.
48
+ **Note:** Using methods discussed later we'll make sure the first language is loaded on page load. However, after this, changing languages may then lead to a period of no translations as Vocab downloads the new language's translations.
52
49
 
53
50
  **src/App.tsx**
54
51
 
@@ -100,7 +97,7 @@ function MyComponent({ children }) {
100
97
 
101
98
  ### Step 5: Create translations
102
99
 
103
- So far your app will run, but you're missing any translations other than the initial language. The below file can be created manually; however, you can also integrate with a remote translation platform to push and pull translations automatically. See [External translation tooling](#external-translation-tooling) for more information.
100
+ So far, your app will run, but you're missing any translations other than the initial language. The below file can be created manually; however, you can also integrate with a remote translation platform to push and pull translations automatically. See [External translation tooling](#external-translation-tooling) for more information.
104
101
 
105
102
  **./example.vocab/fr-FR.translations.json**
106
103
 
@@ -108,18 +105,35 @@ So far your app will run, but you're missing any translations other than the ini
108
105
  {
109
106
  "my key": {
110
107
  "message": "Bonjour de Vocab",
111
- "decription": "An optional description to help when translating"
108
+ "description": "An optional description to help when translating"
112
109
  }
113
110
  }
114
111
  ```
115
112
 
116
- ### Step 6: Optimize for fast page loading
113
+ ### Step 6: [Optional] Set up Webpack plugin
114
+
115
+ Right now every language is loaded into your web application all the time, which could lead to a large bundle size. Ideally you will want to switch out the Node/default runtime for web runtime that will load only the active language.
116
+
117
+ This is done using the **VocabWebpackPlugin**. Applying this plugin to your client webpack configuration will replace all vocab files with a dynamic asynchronous chunks designed for the web.
118
+
119
+ **webpack.config.js**
120
+
121
+ ```js
122
+ const { VocabWebpackPlugin } = require('@vocab/webpack');
123
+
124
+ module.exports = {
125
+ ...,
126
+ plugins: [new VocabWebpackPlugin()]
127
+ }
128
+ ```
129
+
130
+ ### Step 7: [Optional] Optimize for fast page loading
117
131
 
118
132
  Using the above method without optimizing what chunks webpack uses you may find the page needing to do an extra round trip to load languages on a page.
119
133
 
120
134
  This is where `getChunkName` can be used to retrieve the Webpack chunk used for a specific language.
121
135
 
122
- For example here is a Server Render function that would add the current language chunk to [Loadable component's ChunkExtractor](https://loadable-components.com/docs/api-loadable-server/#chunkextractor).
136
+ For example, here is a Server Render function that would add the current language chunk to [Loadable component's ChunkExtractor](https://loadable-components.com/docs/api-loadable-server/#chunkextractor).
123
137
 
124
138
  **src/render.tsx**
125
139
 
@@ -135,6 +149,34 @@ const extractor = new ChunkExtractor();
135
149
  extractor.addChunk(chunkName);
136
150
  ```
137
151
 
152
+ ## ICU Message format
153
+
154
+ Translation messages can sometimes contain dynamic values, such as dates/times, links or usernames. These values can often exist somewhere in the middle of a message and change location based on translation.
155
+
156
+ To support this Vocab uses [Format.js's intl-messageformat] allowing you to use [ICU Message syntax](https://formatjs.io/docs/core-concepts/icu-syntax/) in your messages.
157
+
158
+ In the below example we use two messages, one that passes in a single parameter and one uses a component.
159
+
160
+ ```json
161
+ {
162
+ "my key with param": {
163
+ "message": "Bonjour de {name}"
164
+ },
165
+ "my key with component": {
166
+ "message": "Bonjour de <Link>Vocab</Link>"
167
+ }
168
+ }
169
+ ```
170
+
171
+ Vocab will automatically parse these strings as ICU messages, identify the required parameters and ensure TypeScript knows the values must be passed in.
172
+
173
+ ```tsx
174
+ t('my key with param', { name: 'Vocab' });
175
+ t('my key with component', {
176
+ Link: (children) => <a href="/foo">{children}</a>
177
+ });
178
+ ```
179
+
138
180
  ## Configuration
139
181
 
140
182
  Configuration can either be passed into the Node API directly or be gathered from the nearest _vocab.config.js_ file.
@@ -154,7 +196,7 @@ module.exports = {
154
196
  * The root directory to compile and validate translations
155
197
  * Default: Current working directory
156
198
  */
157
- projectRoot: ['./example/'];
199
+ projectRoot: './example/';
158
200
  /**
159
201
  * A custom suffix to name vocab translation directories
160
202
  * Default: '.vocab'
@@ -167,6 +209,56 @@ module.exports = {
167
209
  };
168
210
  ```
169
211
 
212
+ ## Use without React
213
+
214
+ If you need to use Vocab outside of React, you can access the returned Vocab file directly. You'll then be responsible for when to load translations and how to update on translation load.
215
+
216
+ #### Async access
217
+
218
+ - `getMessages(language: string) => Promise<Messages>` returns messages for the given language formatted according to the correct locale. If the language has not been loaded it will load the language before resolving.
219
+
220
+ **Note:** To optimize loading time you may want to call `load` (see below) ahead of use.
221
+
222
+ #### Sync access
223
+
224
+ - `load(language: string) => Promise<void>` attempts to pre-load messages for the given language. Resolving once complete. Note this only ensures the language is available and does not return any translations.
225
+ - `getLoadedMessages(language: string) => Messages | null` returns messages for the given language formatted according to the correct locale. If the language has not been loaded it will return `null`. Note that this will not load the language if it's not available. Useful when a synchronous (non-promise) return is required.
226
+
227
+ **Example: Promise based formatting of messages**
228
+
229
+ ```typescript
230
+ import translations from './.vocab';
231
+
232
+ async function getFooMessage(language) {
233
+ let messages = await translations.getMessages(language);
234
+ return messages['my key'].format();
235
+ }
236
+
237
+ getFooMessage().then((m) => console.log(m));
238
+ ```
239
+
240
+ **Example: Synchronously returning a message**
241
+
242
+ ```typescript
243
+ import translations from './.vocab';
244
+
245
+ function getFooMessageSync(language) {
246
+ let messages = translations.getLoadedMessages(language);
247
+ if (!messages) {
248
+ // Translations not loaded, start loading and return null for now
249
+ translations.load();
250
+ return null;
251
+ }
252
+ return messages.foo.format();
253
+ }
254
+
255
+ translations.load();
256
+
257
+ const onClick = () => {
258
+ console.log(getFooMessageSync());
259
+ };
260
+ ```
261
+
170
262
  ## Generate Types
171
263
 
172
264
  Vocab generates custom `index.ts` files that give your React components strongly typed translations to work with.
@@ -185,7 +277,7 @@ $ vocab compile --watch
185
277
 
186
278
  ## External translation tooling
187
279
 
188
- Vocab can be used to syncronize your translations with translations from a remote translation platform.
280
+ Vocab can be used to synchronize your translations with translations from a remote translation platform.
189
281
 
190
282
  | Platform | Environment Variables |
191
283
  | -------------------------------------------- | ----------------------------------- |
@@ -196,6 +288,13 @@ $ vocab push --branch my-branch
196
288
  $ vocab pull --branch my-branch
197
289
  ```
198
290
 
291
+ ## Troubleshooting
292
+
293
+ ### Problem: Passed locale is being ignored or using en-US instead
294
+
295
+ When running in Node.js the locale formatting is supported by [Node.js's Internationalization support](https://nodejs.org/api/intl.html#intl_internationalization_support). Node.js will silently switch to the closest locale it can find if the passed locale is not available.
296
+ See Node's documentation on [Options for building Node.js](https://nodejs.org/api/intl.html#intl_options_for_building_node_js) for information on ensuring Node has the locales you need.
297
+
199
298
  ### License
200
299
 
201
300
  MIT.
@@ -53,7 +53,7 @@ function _callPhrase(path, options = {}) {
53
53
  const result = await response.json();
54
54
  console.log(`Internal Result (Length: ${result.length})\n`);
55
55
 
56
- if ((!options.method || options.method === 'GET') && ((_response$headers$get = response.headers.get('Link')) === null || _response$headers$get === void 0 ? void 0 : _response$headers$get.includes('rel=next'))) {
56
+ 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
57
  var _response$headers$get2, _response$headers$get3;
58
58
 
59
59
  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 : [];
@@ -83,7 +83,6 @@ async function callPhrase(relativePath, options = {}) {
83
83
  }
84
84
 
85
85
  return _callPhrase(`https://api.phrase.com/v2/projects/${projectId}/${relativePath}`, options).then(result => {
86
- // console.log('Result:', result);
87
86
  if (Array.isArray(result)) {
88
87
  console.log('Result length:', result.length);
89
88
  }
@@ -174,31 +173,33 @@ async function pull({
174
173
  await writeFile(loadedTranslation.filePath, `${JSON.stringify(defaultValues, null, 2)}\n`);
175
174
 
176
175
  for (const alternativeLanguage of alternativeLanguages) {
177
- const altTranslations = { ...loadedTranslation.languages[alternativeLanguage]
178
- };
179
- const phraseAltTranslations = allPhraseTranslations[alternativeLanguage];
176
+ if (alternativeLanguage in allPhraseTranslations) {
177
+ const altTranslations = { ...loadedTranslation.languages[alternativeLanguage]
178
+ };
179
+ const phraseAltTranslations = allPhraseTranslations[alternativeLanguage];
180
+
181
+ for (const key of localKeys) {
182
+ var _phraseAltTranslation;
180
183
 
181
- for (const key of localKeys) {
182
- var _phraseAltTranslation;
184
+ const phraseKey = core.getUniqueKey(key, loadedTranslation.namespace);
185
+ const phraseTranslationMessage = (_phraseAltTranslation = phraseAltTranslations[phraseKey]) === null || _phraseAltTranslation === void 0 ? void 0 : _phraseAltTranslation.message;
183
186
 
184
- const phraseKey = core.getUniqueKey(key, loadedTranslation.namespace);
185
- const phraseTranslationMessage = (_phraseAltTranslation = phraseAltTranslations[phraseKey]) === null || _phraseAltTranslation === void 0 ? void 0 : _phraseAltTranslation.message;
187
+ if (!phraseTranslationMessage) {
188
+ trace(`Missing translation. No translation for key ${key} in phrase as ${phraseKey} in language ${alternativeLanguage}.`);
189
+ continue;
190
+ }
186
191
 
187
- if (!phraseTranslationMessage) {
188
- trace(`Missing translation. No translation for key ${key} in phrase as ${phraseKey} in language ${alternativeLanguage}.`);
189
- continue;
192
+ altTranslations[key] = { ...altTranslations[key],
193
+ message: phraseTranslationMessage
194
+ };
190
195
  }
191
196
 
192
- altTranslations[key] = { ...altTranslations[key],
193
- message: phraseTranslationMessage
194
- };
197
+ const altTranslationFilePath = core.getAltLanguageFilePath(loadedTranslation.filePath, alternativeLanguage);
198
+ await mkdir(path__default['default'].dirname(altTranslationFilePath), {
199
+ recursive: true
200
+ });
201
+ await writeFile(altTranslationFilePath, `${JSON.stringify(altTranslations, null, 2)}\n`);
195
202
  }
196
-
197
- const altTranslationFilePath = core.getAltLanguageFilePath(loadedTranslation.filePath, alternativeLanguage);
198
- await mkdir(path__default['default'].dirname(altTranslationFilePath), {
199
- recursive: true
200
- });
201
- await writeFile(altTranslationFilePath, `${JSON.stringify(altTranslations, null, 2)}\n`);
202
203
  }
203
204
  }
204
205
  }
@@ -53,7 +53,7 @@ function _callPhrase(path, options = {}) {
53
53
  const result = await response.json();
54
54
  console.log(`Internal Result (Length: ${result.length})\n`);
55
55
 
56
- if ((!options.method || options.method === 'GET') && ((_response$headers$get = response.headers.get('Link')) === null || _response$headers$get === void 0 ? void 0 : _response$headers$get.includes('rel=next'))) {
56
+ 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
57
  var _response$headers$get2, _response$headers$get3;
58
58
 
59
59
  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 : [];
@@ -83,7 +83,6 @@ async function callPhrase(relativePath, options = {}) {
83
83
  }
84
84
 
85
85
  return _callPhrase(`https://api.phrase.com/v2/projects/${projectId}/${relativePath}`, options).then(result => {
86
- // console.log('Result:', result);
87
86
  if (Array.isArray(result)) {
88
87
  console.log('Result length:', result.length);
89
88
  }
@@ -174,31 +173,33 @@ async function pull({
174
173
  await writeFile(loadedTranslation.filePath, `${JSON.stringify(defaultValues, null, 2)}\n`);
175
174
 
176
175
  for (const alternativeLanguage of alternativeLanguages) {
177
- const altTranslations = { ...loadedTranslation.languages[alternativeLanguage]
178
- };
179
- const phraseAltTranslations = allPhraseTranslations[alternativeLanguage];
176
+ if (alternativeLanguage in allPhraseTranslations) {
177
+ const altTranslations = { ...loadedTranslation.languages[alternativeLanguage]
178
+ };
179
+ const phraseAltTranslations = allPhraseTranslations[alternativeLanguage];
180
+
181
+ for (const key of localKeys) {
182
+ var _phraseAltTranslation;
180
183
 
181
- for (const key of localKeys) {
182
- var _phraseAltTranslation;
184
+ const phraseKey = core.getUniqueKey(key, loadedTranslation.namespace);
185
+ const phraseTranslationMessage = (_phraseAltTranslation = phraseAltTranslations[phraseKey]) === null || _phraseAltTranslation === void 0 ? void 0 : _phraseAltTranslation.message;
183
186
 
184
- const phraseKey = core.getUniqueKey(key, loadedTranslation.namespace);
185
- const phraseTranslationMessage = (_phraseAltTranslation = phraseAltTranslations[phraseKey]) === null || _phraseAltTranslation === void 0 ? void 0 : _phraseAltTranslation.message;
187
+ if (!phraseTranslationMessage) {
188
+ trace(`Missing translation. No translation for key ${key} in phrase as ${phraseKey} in language ${alternativeLanguage}.`);
189
+ continue;
190
+ }
186
191
 
187
- if (!phraseTranslationMessage) {
188
- trace(`Missing translation. No translation for key ${key} in phrase as ${phraseKey} in language ${alternativeLanguage}.`);
189
- continue;
192
+ altTranslations[key] = { ...altTranslations[key],
193
+ message: phraseTranslationMessage
194
+ };
190
195
  }
191
196
 
192
- altTranslations[key] = { ...altTranslations[key],
193
- message: phraseTranslationMessage
194
- };
197
+ const altTranslationFilePath = core.getAltLanguageFilePath(loadedTranslation.filePath, alternativeLanguage);
198
+ await mkdir(path__default['default'].dirname(altTranslationFilePath), {
199
+ recursive: true
200
+ });
201
+ await writeFile(altTranslationFilePath, `${JSON.stringify(altTranslations, null, 2)}\n`);
195
202
  }
196
-
197
- const altTranslationFilePath = core.getAltLanguageFilePath(loadedTranslation.filePath, alternativeLanguage);
198
- await mkdir(path__default['default'].dirname(altTranslationFilePath), {
199
- recursive: true
200
- });
201
- await writeFile(altTranslationFilePath, `${JSON.stringify(altTranslations, null, 2)}\n`);
202
203
  }
203
204
  }
204
205
  }
@@ -41,7 +41,7 @@ function _callPhrase(path, options = {}) {
41
41
  const result = await response.json();
42
42
  console.log(`Internal Result (Length: ${result.length})\n`);
43
43
 
44
- if ((!options.method || options.method === 'GET') && ((_response$headers$get = response.headers.get('Link')) === null || _response$headers$get === void 0 ? void 0 : _response$headers$get.includes('rel=next'))) {
44
+ if ((!options.method || options.method === 'GET') && (_response$headers$get = response.headers.get('Link')) !== null && _response$headers$get !== void 0 && _response$headers$get.includes('rel=next')) {
45
45
  var _response$headers$get2, _response$headers$get3;
46
46
 
47
47
  const [, nextPageUrl] = (_response$headers$get2 = (_response$headers$get3 = response.headers.get('Link')) === null || _response$headers$get3 === void 0 ? void 0 : _response$headers$get3.match(/<([^>]*)>; rel=next/)) !== null && _response$headers$get2 !== void 0 ? _response$headers$get2 : [];
@@ -71,7 +71,6 @@ async function callPhrase(relativePath, options = {}) {
71
71
  }
72
72
 
73
73
  return _callPhrase(`https://api.phrase.com/v2/projects/${projectId}/${relativePath}`, options).then(result => {
74
- // console.log('Result:', result);
75
74
  if (Array.isArray(result)) {
76
75
  console.log('Result length:', result.length);
77
76
  }
@@ -162,31 +161,33 @@ async function pull({
162
161
  await writeFile(loadedTranslation.filePath, `${JSON.stringify(defaultValues, null, 2)}\n`);
163
162
 
164
163
  for (const alternativeLanguage of alternativeLanguages) {
165
- const altTranslations = { ...loadedTranslation.languages[alternativeLanguage]
166
- };
167
- const phraseAltTranslations = allPhraseTranslations[alternativeLanguage];
164
+ if (alternativeLanguage in allPhraseTranslations) {
165
+ const altTranslations = { ...loadedTranslation.languages[alternativeLanguage]
166
+ };
167
+ const phraseAltTranslations = allPhraseTranslations[alternativeLanguage];
168
+
169
+ for (const key of localKeys) {
170
+ var _phraseAltTranslation;
168
171
 
169
- for (const key of localKeys) {
170
- var _phraseAltTranslation;
172
+ const phraseKey = getUniqueKey(key, loadedTranslation.namespace);
173
+ const phraseTranslationMessage = (_phraseAltTranslation = phraseAltTranslations[phraseKey]) === null || _phraseAltTranslation === void 0 ? void 0 : _phraseAltTranslation.message;
171
174
 
172
- const phraseKey = getUniqueKey(key, loadedTranslation.namespace);
173
- const phraseTranslationMessage = (_phraseAltTranslation = phraseAltTranslations[phraseKey]) === null || _phraseAltTranslation === void 0 ? void 0 : _phraseAltTranslation.message;
175
+ if (!phraseTranslationMessage) {
176
+ trace(`Missing translation. No translation for key ${key} in phrase as ${phraseKey} in language ${alternativeLanguage}.`);
177
+ continue;
178
+ }
174
179
 
175
- if (!phraseTranslationMessage) {
176
- trace(`Missing translation. No translation for key ${key} in phrase as ${phraseKey} in language ${alternativeLanguage}.`);
177
- continue;
180
+ altTranslations[key] = { ...altTranslations[key],
181
+ message: phraseTranslationMessage
182
+ };
178
183
  }
179
184
 
180
- altTranslations[key] = { ...altTranslations[key],
181
- message: phraseTranslationMessage
182
- };
185
+ const altTranslationFilePath = getAltLanguageFilePath(loadedTranslation.filePath, alternativeLanguage);
186
+ await mkdir(path.dirname(altTranslationFilePath), {
187
+ recursive: true
188
+ });
189
+ await writeFile(altTranslationFilePath, `${JSON.stringify(altTranslations, null, 2)}\n`);
183
190
  }
184
-
185
- const altTranslationFilePath = getAltLanguageFilePath(loadedTranslation.filePath, alternativeLanguage);
186
- await mkdir(path.dirname(altTranslationFilePath), {
187
- recursive: true
188
- });
189
- await writeFile(altTranslationFilePath, `${JSON.stringify(altTranslations, null, 2)}\n`);
190
191
  }
191
192
  }
192
193
  }
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "@vocab/phrase",
3
- "version": "0.0.10",
3
+ "version": "1.0.1",
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": "^0.0.10",
10
- "@vocab/types": "^0.0.8",
9
+ "@vocab/core": "^1.0.0",
10
+ "@vocab/types": "^1.0.0",
11
11
  "chalk": "^4.1.0",
12
12
  "debug": "^4.3.1",
13
13
  "form-data": "^3.0.0",
package/src/phrase-api.ts CHANGED
@@ -80,7 +80,6 @@ export async function callPhrase(
80
80
  options,
81
81
  )
82
82
  .then((result) => {
83
- // console.log('Result:', result);
84
83
  if (Array.isArray(result)) {
85
84
  console.log('Result length:', result.length);
86
85
  }
@@ -2,6 +2,7 @@ import path from 'path';
2
2
  import { pull } from './pull-translations';
3
3
  import { pullAllTranslations } from './phrase-api';
4
4
  import { writeFile } from './file';
5
+ import { LanguageTarget } from '@vocab/types';
5
6
 
6
7
  jest.mock('./file', () => ({
7
8
  writeFile: jest.fn(() => Promise.resolve),
@@ -13,28 +14,83 @@ jest.mock('./phrase-api', () => ({
13
14
  pullAllTranslations: jest.fn(() => Promise.resolve({ en: {}, fr: {} })),
14
15
  }));
15
16
 
16
- function runPhrase() {
17
+ function runPhrase(options: { languages: LanguageTarget[] }) {
17
18
  return pull(
18
19
  { branch: 'tester' },
19
20
  {
21
+ ...options,
20
22
  devLanguage: 'en',
21
- languages: [{ name: 'en' }, { name: 'fr' }],
22
23
  projectRoot: path.resolve(__dirname, '..', '..', '..', 'fixtures/phrase'),
23
24
  },
24
25
  );
25
26
  }
26
27
 
27
- describe('pull', () => {
28
- beforeEach(() => {
29
- (pullAllTranslations as jest.Mock).mockClear();
30
- (writeFile as jest.Mock).mockClear();
31
- });
32
- it('should resolve', async () => {
33
- await expect(runPhrase()).resolves.toBeUndefined();
28
+ describe('pull translations', () => {
29
+ describe('when pulling translations for languages that already have translations', () => {
30
+ beforeEach(() => {
31
+ (pullAllTranslations as jest.Mock).mockClear();
32
+ (writeFile as jest.Mock).mockClear();
33
+ });
34
+ (pullAllTranslations as jest.Mock).mockImplementation(() =>
35
+ Promise.resolve({
36
+ en: {
37
+ 'hello.mytranslations': {
38
+ message: 'Hi there',
39
+ },
40
+ },
41
+ fr: {
42
+ 'hello.mytranslations': {
43
+ message: 'merci',
44
+ },
45
+ },
46
+ }),
47
+ );
34
48
 
35
- expect(writeFile as jest.Mock).toHaveBeenCalledTimes(2);
49
+ const options = {
50
+ languages: [{ name: 'en' }, { name: 'fr' }],
51
+ };
52
+
53
+ it('should resolve', async () => {
54
+ await expect(runPhrase(options)).resolves.toBeUndefined();
55
+
56
+ expect(writeFile as jest.Mock).toHaveBeenCalledTimes(2);
57
+ });
58
+
59
+ it('should update keys', async () => {
60
+ await expect(runPhrase(options)).resolves.toBeUndefined();
61
+
62
+ expect(
63
+ (writeFile as jest.Mock).mock.calls.map(
64
+ ([_filePath, contents]: [string, string]) => JSON.parse(contents),
65
+ ),
66
+ ).toMatchInlineSnapshot(`
67
+ Array [
68
+ Object {
69
+ "hello": Object {
70
+ "message": "Hi there",
71
+ },
72
+ "world": Object {
73
+ "message": "world",
74
+ },
75
+ },
76
+ Object {
77
+ "hello": Object {
78
+ "message": "merci",
79
+ },
80
+ "world": Object {
81
+ "message": "monde",
82
+ },
83
+ },
84
+ ]
85
+ `);
86
+ });
36
87
  });
37
- it('should update keys', async () => {
88
+
89
+ describe('when pulling translations and some languages do not have any translations', () => {
90
+ beforeEach(() => {
91
+ (pullAllTranslations as jest.Mock).mockClear();
92
+ (writeFile as jest.Mock).mockClear();
93
+ });
38
94
  (pullAllTranslations as jest.Mock).mockImplementation(() =>
39
95
  Promise.resolve({
40
96
  en: {
@@ -50,13 +106,24 @@ describe('pull', () => {
50
106
  }),
51
107
  );
52
108
 
53
- await expect(runPhrase()).resolves.toBeUndefined();
109
+ const options = {
110
+ languages: [{ name: 'en' }, { name: 'fr' }, { name: 'ja' }],
111
+ };
112
+
113
+ it('should resolve', async () => {
114
+ await expect(runPhrase(options)).resolves.toBeUndefined();
115
+
116
+ expect(writeFile as jest.Mock).toHaveBeenCalledTimes(2);
117
+ });
118
+
119
+ it('should update keys', async () => {
120
+ await expect(runPhrase(options)).resolves.toBeUndefined();
54
121
 
55
- expect(
56
- (writeFile as jest.Mock).mock.calls.map(
57
- ([_filePath, contents]: [string, string]) => JSON.parse(contents),
58
- ),
59
- ).toMatchInlineSnapshot(`
122
+ expect(
123
+ (writeFile as jest.Mock).mock.calls.map(
124
+ ([_filePath, contents]: [string, string]) => JSON.parse(contents),
125
+ ),
126
+ ).toMatchInlineSnapshot(`
60
127
  Array [
61
128
  Object {
62
129
  "hello": Object {
@@ -76,5 +143,6 @@ describe('pull', () => {
76
143
  },
77
144
  ]
78
145
  `);
146
+ });
79
147
  });
80
148
  });
@@ -59,41 +59,44 @@ export async function pull(
59
59
  );
60
60
 
61
61
  for (const alternativeLanguage of alternativeLanguages) {
62
- const altTranslations = {
63
- ...loadedTranslation.languages[alternativeLanguage],
64
- };
65
- const phraseAltTranslations = allPhraseTranslations[alternativeLanguage];
62
+ if (alternativeLanguage in allPhraseTranslations) {
63
+ const altTranslations = {
64
+ ...loadedTranslation.languages[alternativeLanguage],
65
+ };
66
+ const phraseAltTranslations =
67
+ allPhraseTranslations[alternativeLanguage];
66
68
 
67
- for (const key of localKeys) {
68
- const phraseKey = getUniqueKey(key, loadedTranslation.namespace);
69
- const phraseTranslationMessage =
70
- phraseAltTranslations[phraseKey]?.message;
69
+ for (const key of localKeys) {
70
+ const phraseKey = getUniqueKey(key, loadedTranslation.namespace);
71
+ const phraseTranslationMessage =
72
+ phraseAltTranslations[phraseKey]?.message;
71
73
 
72
- if (!phraseTranslationMessage) {
73
- trace(
74
- `Missing translation. No translation for key ${key} in phrase as ${phraseKey} in language ${alternativeLanguage}.`,
75
- );
76
- continue;
77
- }
74
+ if (!phraseTranslationMessage) {
75
+ trace(
76
+ `Missing translation. No translation for key ${key} in phrase as ${phraseKey} in language ${alternativeLanguage}.`,
77
+ );
78
+ continue;
79
+ }
78
80
 
79
- altTranslations[key] = {
80
- ...altTranslations[key],
81
- message: phraseTranslationMessage,
82
- };
83
- }
81
+ altTranslations[key] = {
82
+ ...altTranslations[key],
83
+ message: phraseTranslationMessage,
84
+ };
85
+ }
84
86
 
85
- const altTranslationFilePath = getAltLanguageFilePath(
86
- loadedTranslation.filePath,
87
- alternativeLanguage,
88
- );
87
+ const altTranslationFilePath = getAltLanguageFilePath(
88
+ loadedTranslation.filePath,
89
+ alternativeLanguage,
90
+ );
89
91
 
90
- await mkdir(path.dirname(altTranslationFilePath), {
91
- recursive: true,
92
- });
93
- await writeFile(
94
- altTranslationFilePath,
95
- `${JSON.stringify(altTranslations, null, 2)}\n`,
96
- );
92
+ await mkdir(path.dirname(altTranslationFilePath), {
93
+ recursive: true,
94
+ });
95
+ await writeFile(
96
+ altTranslationFilePath,
97
+ `${JSON.stringify(altTranslations, null, 2)}\n`,
98
+ );
99
+ }
97
100
  }
98
101
  }
99
102
  }