@vocab/phrase 0.0.0-delete-unused-keys-20228144520 → 0.0.0-package-files-20231142931

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,10 +1,36 @@
1
1
  # @vocab/phrase
2
2
 
3
- ## 0.0.0-delete-unused-keys-20228144520
3
+ ## 0.0.0-package-files-20231142931
4
+
5
+ ### Patch Changes
6
+
7
+ - [`29c81d3`](https://github.com/seek-oss/vocab/commit/29c81d370799f631d97c45e727b8cf81453bd398) Thanks [@askoufis](https://github.com/askoufis)! - Exclude source files from package build
8
+
9
+ - Updated dependencies [[`29c81d3`](https://github.com/seek-oss/vocab/commit/29c81d370799f631d97c45e727b8cf81453bd398)]:
10
+ - @vocab/types@0.0.0-package-files-20231142931
11
+
12
+ ## 1.1.0
4
13
 
5
14
  ### Minor Changes
6
15
 
7
- - [`fcd1482`](https://github.com/seek-oss/vocab/commit/fcd1482753e74274935f00ac0fc3afe1bf7f1989) Thanks [@askoufis](https://github.com/askoufis)! - Add an optional `deleteUnusedKeys` flag to the `push` function. If set to `true`, unused keys will be deleted from Phrase after translations are pushed.
16
+ - [`66ed22c`](https://github.com/seek-oss/vocab/commit/66ed22cac6f89018d5fd69fd6f6408e090e1a382) [#93](https://github.com/seek-oss/vocab/pull/93) Thanks [@askoufis](https://github.com/askoufis)! - Add an optional `deleteUnusedKeys` flag to the `push` function. If set to `true`, unused keys will be deleted from Phrase after translations are pushed.
17
+
18
+ **EXAMPLE USAGE**:
19
+
20
+ ```js
21
+ import { push } from '@vocab/phrase';
22
+
23
+ const vocabConfig = {
24
+ devLanguage: 'en',
25
+ language: ['en', 'fr'],
26
+ };
27
+
28
+ await push({ branch: 'myBranch', deleteUnusedKeys: true }, vocabConfig);
29
+ ```
30
+
31
+ ### Patch Changes
32
+
33
+ - [`159d559`](https://github.com/seek-oss/vocab/commit/159d559c87c66c3e91c707fb45a1f67ebec07b4d) [#91](https://github.com/seek-oss/vocab/pull/91) Thanks [@askoufis](https://github.com/askoufis)! - Improve error message when Phrase doesn't return any translations for the dev language
8
34
 
9
35
  ## 1.0.1
10
36
 
@@ -44,14 +44,14 @@ function _callPhrase(path, options = {}) {
44
44
  }).then(async response => {
45
45
  console.log(`${path}: ${response.status} - ${response.statusText}`);
46
46
  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:
47
+ trace('\nLink:', response.headers.get('Link'), '\n'); // Print All Headers:
48
48
  // console.log(Array.from(r.headers.entries()));
49
49
 
50
50
  try {
51
51
  var _response$headers$get;
52
52
 
53
53
  const result = await response.json();
54
- console.log(`Internal Result (Length: ${result.length})\n`);
54
+ trace(`Internal Result (Length: ${result.length})\n`);
55
55
 
56
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;
@@ -59,10 +59,10 @@ function _callPhrase(path, options = {}) {
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 : [];
60
60
 
61
61
  if (!nextPageUrl) {
62
- throw new Error('Cant parse next page URL');
62
+ throw new Error("Can't parse next page URL");
63
63
  }
64
64
 
65
- console.log('Results recieved with next page: ', nextPageUrl);
65
+ console.log('Results received with next page: ', nextPageUrl);
66
66
  const nextPageResult = await _callPhrase(nextPageUrl, options);
67
67
  return [...result, ...nextPageResult];
68
68
  }
@@ -120,7 +120,7 @@ async function pushTranslationsByLocale(contents, locale, branch) {
120
120
  formData.append('locale_id', locale);
121
121
  formData.append('branch', branch);
122
122
  formData.append('update_translations', 'true');
123
- trace('Starting to upload:', locale);
123
+ log('Starting to upload:', locale, '\n');
124
124
  const {
125
125
  id
126
126
  } = await callPhrase(`uploads`, {
@@ -135,7 +135,9 @@ async function pushTranslationsByLocale(contents, locale, branch) {
135
135
  }
136
136
  async function deleteUnusedKeys(uploadId, locale, branch) {
137
137
  const query = `unmentioned_in_upload:${uploadId}`;
138
- const result = await callPhrase('keys', {
138
+ const {
139
+ records_affected
140
+ } = await callPhrase('keys', {
139
141
  method: 'DELETE',
140
142
  headers: {
141
143
  'Content-Type': 'application/json'
@@ -146,7 +148,7 @@ async function deleteUnusedKeys(uploadId, locale, branch) {
146
148
  q: query
147
149
  })
148
150
  });
149
- log('Successfully deleted', result.records_affected, 'unused keys from branch', branch);
151
+ log('Successfully deleted', records_affected, 'unused keys from branch', branch);
150
152
  }
151
153
  async function ensureBranch(branch) {
152
154
  await callPhrase(`branches`, {
@@ -158,7 +160,7 @@ async function ensureBranch(branch) {
158
160
  name: branch
159
161
  })
160
162
  });
161
- trace('Created branch:', branch);
163
+ log('Created branch:', branch);
162
164
  }
163
165
 
164
166
  async function pull({
@@ -169,6 +171,13 @@ async function pull({
169
171
  const alternativeLanguages = core.getAltLanguages(config);
170
172
  const allPhraseTranslations = await pullAllTranslations(branch);
171
173
  trace(`Pulling translations from Phrase for languages ${config.devLanguage} and ${alternativeLanguages.join(', ')}`);
174
+ const phraseLanguages = Object.keys(allPhraseTranslations);
175
+ trace(`Found Phrase translations for languages ${phraseLanguages.join(', ')}`);
176
+
177
+ if (!phraseLanguages.includes(config.devLanguage)) {
178
+ 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.`);
179
+ }
180
+
172
181
  const allVocabTranslations = await core.loadAllTranslations({
173
182
  fallbacks: 'none',
174
183
  includeNodeModules: false
@@ -44,14 +44,14 @@ function _callPhrase(path, options = {}) {
44
44
  }).then(async response => {
45
45
  console.log(`${path}: ${response.status} - ${response.statusText}`);
46
46
  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:
47
+ trace('\nLink:', response.headers.get('Link'), '\n'); // Print All Headers:
48
48
  // console.log(Array.from(r.headers.entries()));
49
49
 
50
50
  try {
51
51
  var _response$headers$get;
52
52
 
53
53
  const result = await response.json();
54
- console.log(`Internal Result (Length: ${result.length})\n`);
54
+ trace(`Internal Result (Length: ${result.length})\n`);
55
55
 
56
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;
@@ -59,10 +59,10 @@ function _callPhrase(path, options = {}) {
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 : [];
60
60
 
61
61
  if (!nextPageUrl) {
62
- throw new Error('Cant parse next page URL');
62
+ throw new Error("Can't parse next page URL");
63
63
  }
64
64
 
65
- console.log('Results recieved with next page: ', nextPageUrl);
65
+ console.log('Results received with next page: ', nextPageUrl);
66
66
  const nextPageResult = await _callPhrase(nextPageUrl, options);
67
67
  return [...result, ...nextPageResult];
68
68
  }
@@ -120,7 +120,7 @@ async function pushTranslationsByLocale(contents, locale, branch) {
120
120
  formData.append('locale_id', locale);
121
121
  formData.append('branch', branch);
122
122
  formData.append('update_translations', 'true');
123
- trace('Starting to upload:', locale);
123
+ log('Starting to upload:', locale, '\n');
124
124
  const {
125
125
  id
126
126
  } = await callPhrase(`uploads`, {
@@ -135,7 +135,9 @@ async function pushTranslationsByLocale(contents, locale, branch) {
135
135
  }
136
136
  async function deleteUnusedKeys(uploadId, locale, branch) {
137
137
  const query = `unmentioned_in_upload:${uploadId}`;
138
- const result = await callPhrase('keys', {
138
+ const {
139
+ records_affected
140
+ } = await callPhrase('keys', {
139
141
  method: 'DELETE',
140
142
  headers: {
141
143
  'Content-Type': 'application/json'
@@ -146,7 +148,7 @@ async function deleteUnusedKeys(uploadId, locale, branch) {
146
148
  q: query
147
149
  })
148
150
  });
149
- log('Successfully deleted', result.records_affected, 'unused keys from branch', branch);
151
+ log('Successfully deleted', records_affected, 'unused keys from branch', branch);
150
152
  }
151
153
  async function ensureBranch(branch) {
152
154
  await callPhrase(`branches`, {
@@ -158,7 +160,7 @@ async function ensureBranch(branch) {
158
160
  name: branch
159
161
  })
160
162
  });
161
- trace('Created branch:', branch);
163
+ log('Created branch:', branch);
162
164
  }
163
165
 
164
166
  async function pull({
@@ -169,6 +171,13 @@ async function pull({
169
171
  const alternativeLanguages = core.getAltLanguages(config);
170
172
  const allPhraseTranslations = await pullAllTranslations(branch);
171
173
  trace(`Pulling translations from Phrase for languages ${config.devLanguage} and ${alternativeLanguages.join(', ')}`);
174
+ const phraseLanguages = Object.keys(allPhraseTranslations);
175
+ trace(`Found Phrase translations for languages ${phraseLanguages.join(', ')}`);
176
+
177
+ if (!phraseLanguages.includes(config.devLanguage)) {
178
+ 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.`);
179
+ }
180
+
172
181
  const allVocabTranslations = await core.loadAllTranslations({
173
182
  fallbacks: 'none',
174
183
  includeNodeModules: false
@@ -32,14 +32,14 @@ function _callPhrase(path, options = {}) {
32
32
  }).then(async response => {
33
33
  console.log(`${path}: ${response.status} - ${response.statusText}`);
34
34
  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:
35
+ trace('\nLink:', response.headers.get('Link'), '\n'); // Print All Headers:
36
36
  // console.log(Array.from(r.headers.entries()));
37
37
 
38
38
  try {
39
39
  var _response$headers$get;
40
40
 
41
41
  const result = await response.json();
42
- console.log(`Internal Result (Length: ${result.length})\n`);
42
+ trace(`Internal Result (Length: ${result.length})\n`);
43
43
 
44
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;
@@ -47,10 +47,10 @@ function _callPhrase(path, options = {}) {
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 : [];
48
48
 
49
49
  if (!nextPageUrl) {
50
- throw new Error('Cant parse next page URL');
50
+ throw new Error("Can't parse next page URL");
51
51
  }
52
52
 
53
- console.log('Results recieved with next page: ', nextPageUrl);
53
+ console.log('Results received with next page: ', nextPageUrl);
54
54
  const nextPageResult = await _callPhrase(nextPageUrl, options);
55
55
  return [...result, ...nextPageResult];
56
56
  }
@@ -108,7 +108,7 @@ async function pushTranslationsByLocale(contents, locale, branch) {
108
108
  formData.append('locale_id', locale);
109
109
  formData.append('branch', branch);
110
110
  formData.append('update_translations', 'true');
111
- trace('Starting to upload:', locale);
111
+ log('Starting to upload:', locale, '\n');
112
112
  const {
113
113
  id
114
114
  } = await callPhrase(`uploads`, {
@@ -123,7 +123,9 @@ async function pushTranslationsByLocale(contents, locale, branch) {
123
123
  }
124
124
  async function deleteUnusedKeys(uploadId, locale, branch) {
125
125
  const query = `unmentioned_in_upload:${uploadId}`;
126
- const result = await callPhrase('keys', {
126
+ const {
127
+ records_affected
128
+ } = await callPhrase('keys', {
127
129
  method: 'DELETE',
128
130
  headers: {
129
131
  'Content-Type': 'application/json'
@@ -134,7 +136,7 @@ async function deleteUnusedKeys(uploadId, locale, branch) {
134
136
  q: query
135
137
  })
136
138
  });
137
- log('Successfully deleted', result.records_affected, 'unused keys from branch', branch);
139
+ log('Successfully deleted', records_affected, 'unused keys from branch', branch);
138
140
  }
139
141
  async function ensureBranch(branch) {
140
142
  await callPhrase(`branches`, {
@@ -146,7 +148,7 @@ async function ensureBranch(branch) {
146
148
  name: branch
147
149
  })
148
150
  });
149
- trace('Created branch:', branch);
151
+ log('Created branch:', branch);
150
152
  }
151
153
 
152
154
  async function pull({
@@ -157,6 +159,13 @@ async function pull({
157
159
  const alternativeLanguages = getAltLanguages(config);
158
160
  const allPhraseTranslations = await pullAllTranslations(branch);
159
161
  trace(`Pulling translations from Phrase for languages ${config.devLanguage} and ${alternativeLanguages.join(', ')}`);
162
+ const phraseLanguages = Object.keys(allPhraseTranslations);
163
+ trace(`Found Phrase translations for languages ${phraseLanguages.join(', ')}`);
164
+
165
+ if (!phraseLanguages.includes(config.devLanguage)) {
166
+ 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.`);
167
+ }
168
+
160
169
  const allVocabTranslations = await loadAllTranslations({
161
170
  fallbacks: 'none',
162
171
  includeNodeModules: false
package/package.json CHANGED
@@ -1,18 +1,21 @@
1
1
  {
2
2
  "name": "@vocab/phrase",
3
- "version": "0.0.0-delete-unused-keys-20228144520",
3
+ "version": "0.0.0-package-files-20231142931",
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
9
  "@vocab/core": "^1.0.0",
10
- "@vocab/types": "^1.0.0",
10
+ "@vocab/types": "^0.0.0-package-files-20231142931",
11
11
  "chalk": "^4.1.0",
12
12
  "debug": "^4.3.1",
13
13
  "form-data": "^3.0.0",
14
14
  "node-fetch": "^2.6.1"
15
15
  },
16
+ "files": [
17
+ "dist"
18
+ ],
16
19
  "devDependencies": {
17
20
  "@types/node-fetch": "^2.5.7"
18
21
  }
package/src/file.ts DELETED
@@ -1,4 +0,0 @@
1
- import { promises as fs } from 'fs';
2
-
3
- export const mkdir = fs.mkdir;
4
- export const writeFile = fs.writeFile;
package/src/index.ts DELETED
@@ -1,2 +0,0 @@
1
- export { pull } from './pull-translations';
2
- export { push } from './push-translations';
package/src/logger.ts DELETED
@@ -1,9 +0,0 @@
1
- import chalk from 'chalk';
2
- import debug from 'debug';
3
-
4
- export const trace = debug(`vocab:phrase`);
5
-
6
- export const log = (...params: unknown[]) => {
7
- // eslint-disable-next-line no-console
8
- console.log(chalk.yellow('Vocab'), ...params);
9
- };
package/src/phrase-api.ts DELETED
@@ -1,173 +0,0 @@
1
- import FormData from 'form-data';
2
- import { TranslationsByKey } from './../../types/src/index';
3
- /* eslint-disable no-console */
4
- import type { TranslationsByLanguage } from '@vocab/types';
5
- import fetch from 'node-fetch';
6
- import { log, trace } from './logger';
7
-
8
- function _callPhrase(path: string, options: Parameters<typeof fetch>[1] = {}) {
9
- const phraseApiToken = process.env.PHRASE_API_TOKEN;
10
-
11
- if (!phraseApiToken) {
12
- throw new Error('Missing PHRASE_API_TOKEN');
13
- }
14
-
15
- return fetch(path, {
16
- ...options,
17
- headers: {
18
- Authorization: `token ${phraseApiToken}`,
19
- // Provide identification via User Agent as requested in https://developers.phrase.com/api/#overview--identification-via-user-agent
20
- 'User-Agent': 'SEEK Demo Candidate App (jhope@seek.com.au)',
21
- ...options.headers,
22
- },
23
- }).then(async (response) => {
24
- console.log(`${path}: ${response.status} - ${response.statusText}`);
25
- console.log(
26
- `Rate Limit: ${response.headers.get(
27
- 'X-Rate-Limit-Remaining',
28
- )} of ${response.headers.get(
29
- 'X-Rate-Limit-Limit',
30
- )} remaining. (${response.headers.get(
31
- 'X-Rate-Limit-Reset',
32
- )} seconds remaining})`,
33
- );
34
- console.log('\nLink:', response.headers.get('Link'), '\n');
35
- // Print All Headers:
36
- // console.log(Array.from(r.headers.entries()));
37
-
38
- try {
39
- const result = await response.json();
40
-
41
- console.log(`Internal Result (Length: ${result.length})\n`);
42
-
43
- if (
44
- (!options.method || options.method === 'GET') &&
45
- response.headers.get('Link')?.includes('rel=next')
46
- ) {
47
- const [, nextPageUrl] =
48
- response.headers.get('Link')?.match(/<([^>]*)>; rel=next/) ?? [];
49
-
50
- if (!nextPageUrl) {
51
- throw new Error('Cant parse next page URL');
52
- }
53
-
54
- console.log('Results recieved with next page: ', nextPageUrl);
55
-
56
- const nextPageResult = (await _callPhrase(nextPageUrl, options)) as any;
57
-
58
- return [...result, ...nextPageResult];
59
- }
60
-
61
- return result;
62
- } catch (e) {
63
- console.error('Unable to parse response as JSON', e);
64
- return response.text();
65
- }
66
- });
67
- }
68
-
69
- export async function callPhrase<T = any>(
70
- relativePath: string,
71
- options: Parameters<typeof fetch>[1] = {},
72
- ): Promise<T> {
73
- const projectId = process.env.PHRASE_PROJECT_ID;
74
-
75
- if (!projectId) {
76
- throw new Error('Missing PHRASE_PROJECT_ID');
77
- }
78
- return _callPhrase(
79
- `https://api.phrase.com/v2/projects/${projectId}/${relativePath}`,
80
- options,
81
- )
82
- .then((result) => {
83
- if (Array.isArray(result)) {
84
- console.log('Result length:', result.length);
85
- }
86
- return result;
87
- })
88
- .catch((error) => {
89
- console.error(`Error calling phrase for ${relativePath}:`, error);
90
- throw Error;
91
- });
92
- }
93
-
94
- export async function pullAllTranslations(
95
- branch: string,
96
- ): Promise<TranslationsByLanguage> {
97
- const phraseResult = await callPhrase<
98
- Array<{
99
- key: { name: string };
100
- locale: { code: string };
101
- content: string;
102
- }>
103
- >(`translations?branch=${branch}&per_page=100`);
104
- const translations: TranslationsByLanguage = {};
105
- for (const r of phraseResult) {
106
- if (!translations[r.locale.code]) {
107
- translations[r.locale.code] = {};
108
- }
109
- translations[r.locale.code][r.key.name] = { message: r.content };
110
- }
111
- return translations;
112
- }
113
-
114
- export async function pushTranslationsByLocale(
115
- contents: TranslationsByKey,
116
- locale: string,
117
- branch: string,
118
- ) {
119
- const formData = new FormData();
120
- const fileContents = Buffer.from(JSON.stringify(contents));
121
- formData.append('file', fileContents, {
122
- contentType: 'application/json',
123
- filename: `${locale}.json`,
124
- });
125
-
126
- formData.append('file_format', 'json');
127
- formData.append('locale_id', locale);
128
- formData.append('branch', branch);
129
- formData.append('update_translations', 'true');
130
-
131
- trace('Starting to upload:', locale);
132
-
133
- const { id } = await callPhrase<{ id: string }>(`uploads`, {
134
- method: 'POST',
135
- body: formData,
136
- });
137
- log('Upload ID:', id, '\n');
138
- log('Successfully Uploaded:', locale, '\n');
139
-
140
- return { uploadId: id };
141
- }
142
-
143
- export async function deleteUnusedKeys(
144
- uploadId: string,
145
- locale: string,
146
- branch: string,
147
- ) {
148
- const query = `unmentioned_in_upload:${uploadId}`;
149
- const result = await callPhrase<{ records_affected: number }>('keys', {
150
- method: 'DELETE',
151
- headers: {
152
- 'Content-Type': 'application/json',
153
- },
154
- body: JSON.stringify({ branch, locale_id: locale, q: query }),
155
- });
156
- log(
157
- 'Successfully deleted',
158
- result.records_affected,
159
- 'unused keys from branch',
160
- branch,
161
- );
162
- }
163
-
164
- export async function ensureBranch(branch: string) {
165
- await callPhrase(`branches`, {
166
- method: 'POST',
167
- headers: {
168
- 'Content-Type': 'application/json',
169
- },
170
- body: JSON.stringify({ name: branch }),
171
- });
172
- trace('Created branch:', branch);
173
- }
@@ -1,169 +0,0 @@
1
- import path from 'path';
2
- import { pull } from './pull-translations';
3
- import { pullAllTranslations } from './phrase-api';
4
- import { writeFile } from './file';
5
- import { GeneratedLanguageTarget, LanguageTarget } from '@vocab/types';
6
-
7
- jest.mock('./file', () => ({
8
- writeFile: jest.fn(() => Promise.resolve),
9
- mkdir: jest.fn(() => Promise.resolve),
10
- }));
11
-
12
- jest.mock('./phrase-api', () => ({
13
- ensureBranch: jest.fn(() => Promise.resolve()),
14
- pullAllTranslations: jest.fn(() => Promise.resolve({ en: {}, fr: {} })),
15
- }));
16
-
17
- function runPhrase(options: {
18
- languages: LanguageTarget[];
19
- generatedLanguages: GeneratedLanguageTarget[];
20
- }) {
21
- return pull(
22
- { branch: 'tester' },
23
- {
24
- ...options,
25
- devLanguage: 'en',
26
- projectRoot: path.resolve(__dirname, '..', '..', '..', 'fixtures/phrase'),
27
- },
28
- );
29
- }
30
-
31
- describe('pull translations', () => {
32
- describe('when pulling translations for languages that already have translations', () => {
33
- beforeEach(() => {
34
- (pullAllTranslations as jest.Mock).mockClear();
35
- (writeFile as jest.Mock).mockClear();
36
- });
37
- (pullAllTranslations as jest.Mock).mockImplementation(() =>
38
- Promise.resolve({
39
- en: {
40
- 'hello.mytranslations': {
41
- message: 'Hi there',
42
- },
43
- },
44
- fr: {
45
- 'hello.mytranslations': {
46
- message: 'merci',
47
- },
48
- },
49
- }),
50
- );
51
-
52
- const options = {
53
- languages: [{ name: 'en' }, { name: 'fr' }],
54
- generatedLanguages: [
55
- {
56
- name: 'generatedLanguage',
57
- extends: 'en',
58
- generator: {
59
- transformMessage: (message: string) => `[${message}]`,
60
- },
61
- },
62
- ],
63
- };
64
-
65
- it('should resolve', async () => {
66
- await expect(runPhrase(options)).resolves.toBeUndefined();
67
-
68
- expect(writeFile as jest.Mock).toHaveBeenCalledTimes(2);
69
- });
70
-
71
- it('should update keys', async () => {
72
- await expect(runPhrase(options)).resolves.toBeUndefined();
73
-
74
- expect(
75
- (writeFile as jest.Mock).mock.calls.map(
76
- ([_filePath, contents]: [string, string]) => JSON.parse(contents),
77
- ),
78
- ).toMatchInlineSnapshot(`
79
- Array [
80
- Object {
81
- "hello": Object {
82
- "message": "Hi there",
83
- },
84
- "world": Object {
85
- "message": "world",
86
- },
87
- },
88
- Object {
89
- "hello": Object {
90
- "message": "merci",
91
- },
92
- "world": Object {
93
- "message": "monde",
94
- },
95
- },
96
- ]
97
- `);
98
- });
99
- });
100
-
101
- describe('when pulling translations and some languages do not have any translations', () => {
102
- beforeEach(() => {
103
- (pullAllTranslations as jest.Mock).mockClear();
104
- (writeFile as jest.Mock).mockClear();
105
- });
106
- (pullAllTranslations as jest.Mock).mockImplementation(() =>
107
- Promise.resolve({
108
- en: {
109
- 'hello.mytranslations': {
110
- message: 'Hi there',
111
- },
112
- },
113
- fr: {
114
- 'hello.mytranslations': {
115
- message: 'merci',
116
- },
117
- },
118
- }),
119
- );
120
-
121
- const options = {
122
- languages: [{ name: 'en' }, { name: 'fr' }, { name: 'ja' }],
123
- generatedLanguages: [
124
- {
125
- name: 'generatedLanguage',
126
- extends: 'en',
127
- generator: {
128
- transformMessage: (message: string) => `[${message}]`,
129
- },
130
- },
131
- ],
132
- };
133
-
134
- it('should resolve', async () => {
135
- await expect(runPhrase(options)).resolves.toBeUndefined();
136
-
137
- expect(writeFile as jest.Mock).toHaveBeenCalledTimes(2);
138
- });
139
-
140
- it('should update keys', async () => {
141
- await expect(runPhrase(options)).resolves.toBeUndefined();
142
-
143
- expect(
144
- (writeFile as jest.Mock).mock.calls.map(
145
- ([_filePath, contents]: [string, string]) => JSON.parse(contents),
146
- ),
147
- ).toMatchInlineSnapshot(`
148
- Array [
149
- Object {
150
- "hello": Object {
151
- "message": "Hi there",
152
- },
153
- "world": Object {
154
- "message": "world",
155
- },
156
- },
157
- Object {
158
- "hello": Object {
159
- "message": "merci",
160
- },
161
- "world": Object {
162
- "message": "monde",
163
- },
164
- },
165
- ]
166
- `);
167
- });
168
- });
169
- });
@@ -1,103 +0,0 @@
1
- import { writeFile, mkdir } from './file';
2
- import path from 'path';
3
-
4
- import {
5
- loadAllTranslations,
6
- getAltLanguageFilePath,
7
- getAltLanguages,
8
- getUniqueKey,
9
- } from '@vocab/core';
10
- import type { UserConfig } from '@vocab/types';
11
-
12
- import { pullAllTranslations, ensureBranch } from './phrase-api';
13
- import { trace } from './logger';
14
-
15
- interface PullOptions {
16
- branch?: string;
17
- deleteUnusedKeys?: boolean;
18
- }
19
-
20
- export async function pull(
21
- { branch = 'local-development' }: PullOptions,
22
- config: UserConfig,
23
- ) {
24
- trace(`Pulling translations from branch ${branch}`);
25
- await ensureBranch(branch);
26
- const alternativeLanguages = getAltLanguages(config);
27
- const allPhraseTranslations = await pullAllTranslations(branch);
28
- trace(
29
- `Pulling translations from Phrase for languages ${
30
- config.devLanguage
31
- } and ${alternativeLanguages.join(', ')}`,
32
- );
33
-
34
- const allVocabTranslations = await loadAllTranslations(
35
- { fallbacks: 'none', includeNodeModules: false },
36
- config,
37
- );
38
-
39
- for (const loadedTranslation of allVocabTranslations) {
40
- const devTranslations = loadedTranslation.languages[config.devLanguage];
41
-
42
- if (!devTranslations) {
43
- throw new Error('No dev language translations loaded');
44
- }
45
-
46
- const defaultValues = { ...devTranslations };
47
- const localKeys = Object.keys(defaultValues);
48
-
49
- for (const key of localKeys) {
50
- defaultValues[key] = {
51
- ...defaultValues[key],
52
- ...allPhraseTranslations[config.devLanguage][
53
- getUniqueKey(key, loadedTranslation.namespace)
54
- ],
55
- };
56
- }
57
- await writeFile(
58
- loadedTranslation.filePath,
59
- `${JSON.stringify(defaultValues, null, 2)}\n`,
60
- );
61
-
62
- for (const alternativeLanguage of alternativeLanguages) {
63
- if (alternativeLanguage in allPhraseTranslations) {
64
- const altTranslations = {
65
- ...loadedTranslation.languages[alternativeLanguage],
66
- };
67
- const phraseAltTranslations =
68
- allPhraseTranslations[alternativeLanguage];
69
-
70
- for (const key of localKeys) {
71
- const phraseKey = getUniqueKey(key, loadedTranslation.namespace);
72
- const phraseTranslationMessage =
73
- phraseAltTranslations[phraseKey]?.message;
74
-
75
- if (!phraseTranslationMessage) {
76
- trace(
77
- `Missing translation. No translation for key ${key} in phrase as ${phraseKey} in language ${alternativeLanguage}.`,
78
- );
79
- continue;
80
- }
81
-
82
- altTranslations[key] = {
83
- ...altTranslations[key],
84
- message: phraseTranslationMessage,
85
- };
86
- }
87
-
88
- const altTranslationFilePath = getAltLanguageFilePath(
89
- loadedTranslation.filePath,
90
- alternativeLanguage,
91
- );
92
-
93
- await mkdir(path.dirname(altTranslationFilePath), {
94
- recursive: true,
95
- });
96
- await writeFile(
97
- altTranslationFilePath,
98
- `${JSON.stringify(altTranslations, null, 2)}\n`,
99
- );
100
- }
101
- }
102
- }
103
- }
@@ -1,150 +0,0 @@
1
- import path from 'path';
2
- import { push } from './push-translations';
3
- import { pushTranslationsByLocale, deleteUnusedKeys } from './phrase-api';
4
- import { writeFile } from './file';
5
-
6
- jest.mock('./file', () => ({
7
- writeFile: jest.fn(() => Promise.resolve),
8
- mkdir: jest.fn(() => Promise.resolve),
9
- }));
10
-
11
- jest.mock('./phrase-api', () => ({
12
- ensureBranch: jest.fn(() => Promise.resolve()),
13
- pushTranslationsByLocale: jest.fn(() => Promise.resolve({ en: {}, fr: {} })),
14
- deleteUnusedKeys: jest.fn(() => Promise.resolve()),
15
- }));
16
-
17
- const uploadId = '1234';
18
-
19
- function runPhrase(config: { deleteUnusedKeys: boolean }) {
20
- return push(
21
- { branch: 'tester', deleteUnusedKeys: config.deleteUnusedKeys },
22
- {
23
- devLanguage: 'en',
24
- languages: [{ name: 'en' }, { name: 'fr' }],
25
- generatedLanguages: [
26
- {
27
- name: 'generatedLanguage',
28
- extends: 'en',
29
- generator: {
30
- transformMessage: (message: string) => `[${message}]`,
31
- },
32
- },
33
- ],
34
- projectRoot: path.resolve(__dirname, '..', '..', '..', 'fixtures/phrase'),
35
- },
36
- );
37
- }
38
-
39
- describe('push', () => {
40
- describe('when deleteUnusedKeys is false', () => {
41
- const config = { deleteUnusedKeys: false };
42
-
43
- beforeEach(() => {
44
- (pushTranslationsByLocale as jest.Mock).mockClear();
45
- (writeFile as jest.Mock).mockClear();
46
- (deleteUnusedKeys as jest.Mock).mockClear();
47
-
48
- (pushTranslationsByLocale as jest.Mock).mockImplementation(() =>
49
- Promise.resolve({ uploadId }),
50
- );
51
- });
52
-
53
- it('should resolve', async () => {
54
- await expect(runPhrase(config)).resolves.toBeUndefined();
55
-
56
- expect(pushTranslationsByLocale as jest.Mock).toHaveBeenCalledTimes(2);
57
- });
58
-
59
- it('should update keys', async () => {
60
- await expect(runPhrase(config)).resolves.toBeUndefined();
61
-
62
- expect(pushTranslationsByLocale as jest.Mock).toHaveBeenCalledWith(
63
- {
64
- 'hello.mytranslations': {
65
- message: 'Hello',
66
- },
67
- 'world.mytranslations': {
68
- message: 'world',
69
- },
70
- },
71
- 'en',
72
- 'tester',
73
- );
74
-
75
- expect(pushTranslationsByLocale as jest.Mock).toHaveBeenCalledWith(
76
- {
77
- 'hello.mytranslations': {
78
- message: 'Bonjour',
79
- },
80
- 'world.mytranslations': {
81
- message: 'monde',
82
- },
83
- },
84
- 'fr',
85
- 'tester',
86
- );
87
- });
88
-
89
- it('should not delete unused keys', () => {
90
- expect(deleteUnusedKeys).not.toHaveBeenCalled();
91
- });
92
- });
93
-
94
- describe('when deleteUnusedKeys is true', () => {
95
- const config = { deleteUnusedKeys: true };
96
-
97
- beforeEach(() => {
98
- (pushTranslationsByLocale as jest.Mock).mockClear();
99
- (writeFile as jest.Mock).mockClear();
100
- (deleteUnusedKeys as jest.Mock).mockClear();
101
-
102
- (pushTranslationsByLocale as jest.Mock).mockImplementation(() =>
103
- Promise.resolve({ uploadId }),
104
- );
105
- });
106
-
107
- it('should resolve', async () => {
108
- await expect(runPhrase(config)).resolves.toBeUndefined();
109
-
110
- expect(pushTranslationsByLocale as jest.Mock).toHaveBeenCalledTimes(2);
111
- });
112
-
113
- it('should update keys', async () => {
114
- await expect(runPhrase(config)).resolves.toBeUndefined();
115
-
116
- expect(pushTranslationsByLocale as jest.Mock).toHaveBeenCalledWith(
117
- {
118
- 'hello.mytranslations': {
119
- message: 'Hello',
120
- },
121
- 'world.mytranslations': {
122
- message: 'world',
123
- },
124
- },
125
- 'en',
126
- 'tester',
127
- );
128
-
129
- expect(pushTranslationsByLocale as jest.Mock).toHaveBeenCalledWith(
130
- {
131
- 'hello.mytranslations': {
132
- message: 'Bonjour',
133
- },
134
- 'world.mytranslations': {
135
- message: 'monde',
136
- },
137
- },
138
- 'fr',
139
- 'tester',
140
- );
141
- });
142
-
143
- it('should delete unused keys', async () => {
144
- await expect(runPhrase(config)).resolves.toBeUndefined();
145
-
146
- expect(deleteUnusedKeys).toHaveBeenCalledWith(uploadId, 'en', 'tester');
147
- expect(deleteUnusedKeys).toHaveBeenCalledWith(uploadId, 'fr', 'tester');
148
- });
149
- });
150
- });
@@ -1,67 +0,0 @@
1
- import { TranslationsByLanguage } from './../../types/src/index';
2
-
3
- import {
4
- ensureBranch,
5
- pushTranslationsByLocale,
6
- deleteUnusedKeys as phraseDeleteUnusedKeys,
7
- } from './phrase-api';
8
- import { trace } from './logger';
9
- import { loadAllTranslations, getUniqueKey } from '@vocab/core';
10
- import { UserConfig } from '@vocab/types';
11
-
12
- interface PushOptions {
13
- branch: string;
14
- deleteUnusedKeys?: boolean;
15
- }
16
-
17
- /**
18
- * Uploading to the Phrase API for each language. Adding a unique namespace to each key using file path they key came from
19
- */
20
- export async function push(
21
- { branch, deleteUnusedKeys }: PushOptions,
22
- config: UserConfig,
23
- ) {
24
- const allLanguageTranslations = await loadAllTranslations(
25
- { fallbacks: 'none', includeNodeModules: false },
26
- config,
27
- );
28
- trace(`Pushing translations to branch ${branch}`);
29
- const allLanguages = config.languages.map((v) => v.name);
30
- await ensureBranch(branch);
31
-
32
- trace(
33
- `Pushing translations to phrase for languages ${allLanguages.join(', ')}`,
34
- );
35
-
36
- const phraseTranslations: TranslationsByLanguage = {};
37
-
38
- for (const loadedTranslation of allLanguageTranslations) {
39
- for (const language of allLanguages) {
40
- const localTranslations = loadedTranslation.languages[language];
41
- if (!localTranslations) {
42
- continue;
43
- }
44
- if (!phraseTranslations[language]) {
45
- phraseTranslations[language] = {};
46
- }
47
- for (const localKey of Object.keys(localTranslations)) {
48
- const phraseKey = getUniqueKey(localKey, loadedTranslation.namespace);
49
- phraseTranslations[language][phraseKey] = localTranslations[localKey];
50
- }
51
- }
52
- }
53
-
54
- for (const language of allLanguages) {
55
- if (phraseTranslations[language]) {
56
- const { uploadId } = await pushTranslationsByLocale(
57
- phraseTranslations[language],
58
- language,
59
- branch,
60
- );
61
-
62
- if (deleteUnusedKeys) {
63
- await phraseDeleteUnusedKeys(uploadId, language, branch);
64
- }
65
- }
66
- }
67
- }