@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 +30 -0
- package/README.md +124 -25
- package/dist/vocab-phrase.cjs.dev.js +22 -21
- package/dist/vocab-phrase.cjs.prod.js +22 -21
- package/dist/vocab-phrase.esm.js +22 -21
- package/package.json +3 -3
- package/src/phrase-api.ts +0 -1
- package/src/pull-translations.test.ts +85 -17
- package/src/pull-translations.ts +33 -30
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
|
|
3
|
+
Vocab is a strongly typed internationalization framework for React.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Vocab helps you ship multiple languages without compromising the reliability of your site or slowing down delivery.
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
- Shareable translations
|
|
8
8
|
|
|
9
|
-
|
|
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
|
-
|
|
12
|
-
$ npm i --save @vocab/cli @vocab/react @vocab/webpack
|
|
13
|
-
```
|
|
11
|
+
- Loading translations dynamically
|
|
14
12
|
|
|
15
|
-
|
|
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
|
-
|
|
15
|
+
- Strongly typed with TypeScript
|
|
18
16
|
|
|
19
|
-
|
|
17
|
+
When using translations TypeScript will ensure code only accesses valid translations and translations are passed all required dynamic values.
|
|
20
18
|
|
|
21
|
-
|
|
19
|
+
## Getting started
|
|
22
20
|
|
|
23
|
-
|
|
24
|
-
const VocabWebpackPlugin = require('@vocab/webpack').default;
|
|
21
|
+
### Step 1: Install Dependencies
|
|
25
22
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
|
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
|
-
"
|
|
108
|
+
"description": "An optional description to help when translating"
|
|
112
109
|
}
|
|
113
110
|
}
|
|
114
111
|
```
|
|
115
112
|
|
|
116
|
-
### Step 6:
|
|
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:
|
|
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
|
|
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') && (
|
|
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
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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
|
-
|
|
182
|
-
|
|
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
|
-
|
|
185
|
-
|
|
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
|
-
|
|
188
|
-
|
|
189
|
-
|
|
192
|
+
altTranslations[key] = { ...altTranslations[key],
|
|
193
|
+
message: phraseTranslationMessage
|
|
194
|
+
};
|
|
190
195
|
}
|
|
191
196
|
|
|
192
|
-
|
|
193
|
-
|
|
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') && (
|
|
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
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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
|
-
|
|
182
|
-
|
|
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
|
-
|
|
185
|
-
|
|
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
|
-
|
|
188
|
-
|
|
189
|
-
|
|
192
|
+
altTranslations[key] = { ...altTranslations[key],
|
|
193
|
+
message: phraseTranslationMessage
|
|
194
|
+
};
|
|
190
195
|
}
|
|
191
196
|
|
|
192
|
-
|
|
193
|
-
|
|
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
|
}
|
package/dist/vocab-phrase.esm.js
CHANGED
|
@@ -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') && (
|
|
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
|
-
|
|
166
|
-
|
|
167
|
-
|
|
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
|
-
|
|
170
|
-
|
|
172
|
+
const phraseKey = getUniqueKey(key, loadedTranslation.namespace);
|
|
173
|
+
const phraseTranslationMessage = (_phraseAltTranslation = phraseAltTranslations[phraseKey]) === null || _phraseAltTranslation === void 0 ? void 0 : _phraseAltTranslation.message;
|
|
171
174
|
|
|
172
|
-
|
|
173
|
-
|
|
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
|
-
|
|
176
|
-
|
|
177
|
-
|
|
180
|
+
altTranslations[key] = { ...altTranslations[key],
|
|
181
|
+
message: phraseTranslationMessage
|
|
182
|
+
};
|
|
178
183
|
}
|
|
179
184
|
|
|
180
|
-
|
|
181
|
-
|
|
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": "
|
|
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
|
-
"@vocab/types": "^0.0
|
|
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
|
@@ -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
|
-
|
|
29
|
-
(
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
});
|
package/src/pull-translations.ts
CHANGED
|
@@ -59,41 +59,44 @@ export async function pull(
|
|
|
59
59
|
);
|
|
60
60
|
|
|
61
61
|
for (const alternativeLanguage of alternativeLanguages) {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
62
|
+
if (alternativeLanguage in allPhraseTranslations) {
|
|
63
|
+
const altTranslations = {
|
|
64
|
+
...loadedTranslation.languages[alternativeLanguage],
|
|
65
|
+
};
|
|
66
|
+
const phraseAltTranslations =
|
|
67
|
+
allPhraseTranslations[alternativeLanguage];
|
|
66
68
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
69
|
+
for (const key of localKeys) {
|
|
70
|
+
const phraseKey = getUniqueKey(key, loadedTranslation.namespace);
|
|
71
|
+
const phraseTranslationMessage =
|
|
72
|
+
phraseAltTranslations[phraseKey]?.message;
|
|
71
73
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
81
|
+
altTranslations[key] = {
|
|
82
|
+
...altTranslations[key],
|
|
83
|
+
message: phraseTranslationMessage,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
84
86
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
87
|
+
const altTranslationFilePath = getAltLanguageFilePath(
|
|
88
|
+
loadedTranslation.filePath,
|
|
89
|
+
alternativeLanguage,
|
|
90
|
+
);
|
|
89
91
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
}
|