i18nexus-cli 3.4.0 → 3.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -6,11 +6,11 @@
6
6
 
7
7
  [i18nexus](https://i18nexus.com) is a translation management web application designed for use with i18next, next-intl, and react-intl. Learn more with these quick tutorials:
8
8
 
9
- - [react-i18next Walkthrough](https://i18nexus.com/tutorials/react/react-i18next)
10
- - [react-intl Walkthrough](https://i18nexus.com/tutorials/react/react-intl)
11
9
  - [next-intl for Next.js App Router Walkthrough](https://i18nexus.com/tutorials/nextjs/next-intl)
12
10
  - [react-i18next for Next.js App Router Walkthrough](https://i18nexus.com/tutorials/nextjs/react-i18next)
13
11
  - [next-i18next for Next.js Pages Router Walkthrough](https://i18nexus.com/tutorials/nextjs/next-i18next)
12
+ - [react-i18next Walkthrough](https://i18nexus.com/tutorials/react/react-i18next)
13
+ - [react-intl Walkthrough](https://i18nexus.com/tutorials/react/react-intl)
14
14
 
15
15
  ## Who is this CLI meant for?
16
16
 
@@ -130,14 +130,15 @@ Translations for the string will be automatically generated and machine translat
130
130
 
131
131
  ### Options
132
132
 
133
- | Option | Required? |
134
- | ---------------------- | --------- |
135
- | `--api-key` or `-k` | ✔ |
136
- | `--pat` or `-t` | ✔ |
137
- | `--namespace` or `-ns` | ✔ |
138
- | `--key` or `-K` | ✔ |
139
- | `--value` or `-v` | ✔ |
140
- | `--details` or `-d` | |
133
+ | Option | Required? |
134
+ | ---------------------------- | ------------- |
135
+ | `--api-key` or `-k` | ✔ |
136
+ | `--pat` or `-t` | ✔ |
137
+ | `--key` or `-K` | ✔ |
138
+ | `--value` or `-v` | ✔ |
139
+ | `--namespace` or `-ns` | Conditionally |
140
+ | `--notes` or `-n` | |
141
+ | `--ai-instructions` or `-ai` | |
141
142
 
142
143
  ### Notes
143
144
 
@@ -148,7 +149,7 @@ Your project API key (Can also be set using environment variable `I18NEXUS_API_K
148
149
  A personal access token that you have generated in your i18nexus account (Can also be set using environment variable `I18NEXUS_PERSONAL_ACCESS_TOKEN`)
149
150
 
150
151
  `--namespace`
151
- The namespace in which to create the string
152
+ The namespace in which to create the string. Only required if your project uses namespaces and has more than one namespace.
152
153
 
153
154
  `--key`
154
155
  The key of the string to create
@@ -156,35 +157,39 @@ The key of the string to create
156
157
  `--value`
157
158
  The value of the string to create
158
159
 
159
- `--details`
160
- The details of the string to create (optional)
160
+ `--notes`
161
+ Team notes about the string to create (optional)
162
+
163
+ `--ai-instructions`
164
+ Instructions or context for AI machine translator (optional)
161
165
 
162
166
  ## Updating existing strings
163
167
 
164
- `i18nexus update-string <namespace> <key>` or `i18nexus u <namespace> <key>`
168
+ `i18nexus update-string <namespace:key>` or `i18nexus u <namespace:key>`
165
169
 
166
170
  ```sh
167
- i18nexus u common welcome_msg -v 'Welcome' -k <PROJECT_API_KEY> -t <YOUR_PERSONAL_ACCESS_TOKEN>
171
+ i18nexus u common:welcome_msg -v 'Welcome' -k <PROJECT_API_KEY> -t <YOUR_PERSONAL_ACCESS_TOKEN>
168
172
  ```
169
173
 
170
174
  The above snippet will update a the value of the string with key `welcome_msg` in your `common` namespace to `Welcome`.
171
175
 
172
- The first 2 arguments are the namespace and the key of the string you wish to update.
176
+ The required argument is the namespace and the key of the string you wish to update, joined with a colon (":"). If your project does not use namespaces or only has one namespace, only the key is necessary.
173
177
 
174
178
  You can then update the key, value, details, and/or namespace by using the command options:
175
179
 
176
180
  ### Options
177
181
 
178
- | Option | Required? |
179
- | ---------------------- | --------- |
180
- | `--api-key` or `-k` | &#10004; |
181
- | `--pat` or `-t` | &#10004; |
182
- | `--namespace` or `-ns` | |
183
- | `--key` or `-K` | |
184
- | `--value` or `-v` | |
185
- | `--details` or `-d` | |
186
- | `--reset-confirmed` | |
187
- | `--retain-confirmed` | |
182
+ | Option | Required? |
183
+ | ---------------------------- | --------- |
184
+ | `--api-key` or `-k` | &#10004; |
185
+ | `--pat` or `-t` | &#10004; |
186
+ | `--namespace` or `-ns` | |
187
+ | `--key` or `-K` | |
188
+ | `--value` or `-v` | |
189
+ | `--details` or `-d` | |
190
+ | `--ai-instructions` or `-ai` | |
191
+ | `--reset-confirmed` | |
192
+ | `--retain-confirmed` | |
188
193
 
189
194
  ### Notes
190
195
 
@@ -203,8 +208,11 @@ The new key of the string
203
208
  `--value`
204
209
  The new value of the string
205
210
 
206
- `--details`
207
- The new details of the string
211
+ `--notes`
212
+ The new team notes of the string
213
+
214
+ `--ai-instructions`
215
+ Instructions or context for AI machine translator
208
216
 
209
217
  #### If you are updating the value of a string that contains translations that have been marked confirmed in i18nexus, you will be required to include one of the following options to your command:
210
218
 
@@ -216,13 +224,13 @@ Confirmed translations of this string will be retained
216
224
 
217
225
  ## Deleting strings
218
226
 
219
- `i18nexus delete-string <namespace> <key>` or `i18nexus d <namespace> <key>`
227
+ `i18nexus delete-string <namespace:key>` or `i18nexus d <namespace:key>`
220
228
 
221
229
  ```sh
222
- i18nexus d common welcome_msg -k <PROJECT_API_KEY> -t <YOUR_PERSONAL_ACCESS_TOKEN>
230
+ i18nexus d common:welcome_msg -k <PROJECT_API_KEY> -t <YOUR_PERSONAL_ACCESS_TOKEN>
223
231
  ```
224
232
 
225
- The above snippet will delete the string `welcome_msg` from your `common` namespace, along with its associated translations.
233
+ The required argument is the namespace and the key of the string you wish to delete, joined with a colon (":"). If your project does not use namespaces or only has one namespace, only the key is necessary.
226
234
 
227
235
  ### Options
228
236
 
@@ -253,11 +261,12 @@ This is the equivalent of using the Import tool in the i18nexus web application.
253
261
 
254
262
  ### Options
255
263
 
256
- | Option | Required? |
257
- | ------------------- | --------- |
258
- | `--api-key` or `-k` | &#10004; |
259
- | `--pat` or `-t` | &#10004; |
260
- | `--overwrite` | |
264
+ | Option | Required? |
265
+ | ---------------------- | ------------- |
266
+ | `--api-key` or `-k` | &#10004; |
267
+ | `--pat` or `-t` | &#10004; |
268
+ | `--namespace` or `-ns` | Conditionally |
269
+ | `--overwrite` | |
261
270
 
262
271
  ### Notes
263
272
 
@@ -267,6 +276,9 @@ Your project API key (Can also be set using environment variable `I18NEXUS_API_K
267
276
  `--pat`
268
277
  A personal access token that you have generated in your i18nexus account (Can also be set using environment variable `I18NEXUS_PERSONAL_ACCESS_TOKEN`)
269
278
 
279
+ `--namespace`
280
+ The namespace in which your strings will be imported. (Only required if your project uses namespaces and has more than one namespace)
281
+
270
282
  `--overwrite`
271
283
  If any keys already exist in the target namespace, overwrite the values with the imported values.
272
284
 
@@ -294,3 +306,33 @@ Your project API key (Can also be set using environment variable `I18NEXUS_API_K
294
306
 
295
307
  `--pat`
296
308
  A personal access token that you have generated in your i18nexus account (Can also be set using environment variable `I18NEXUS_PERSONAL_ACCESS_TOKEN`)
309
+
310
+ ## Listening for live translation updates
311
+
312
+ `i18nexus listen`
313
+
314
+ ```sh
315
+ i18nexus listen -k <PROJECT_API_KEY>
316
+ ```
317
+
318
+ This command starts a tunnel to your local development server using [localtunnel](https://www.npmjs.com/package/localtunnel), allowing i18nexus to send real-time webhooks for translation updates.
319
+
320
+ When a string is added, updated, or deleted in your project, a webhook is sent to your local server, automatically updating your local JSON files.
321
+
322
+ ### Options
323
+
324
+ | Option | Required? | Default |
325
+ | ------------------- | --------- | ----------------------- |
326
+ | `--api-key` or `-k` | &#10004; | |
327
+ | `--path` or `-p` | | See `pull` default path |
328
+ | `--port` | | '3002' |
329
+
330
+ ### Notes
331
+
332
+ `--api-key`
333
+ Your project API key (Can also be set using environment variable `I18NEXUS_API_KEY`)
334
+
335
+ `--port`
336
+ The local port your server will listen on to receive incoming webhook requests from i18nexus. Defaults to `3002`.
337
+
338
+ The tunnel will remain active as long as the CLI is running. Press `Ctrl+C` to stop listening.
package/bin/index.js CHANGED
@@ -8,6 +8,7 @@ const updateString = require('../commands/updateString');
8
8
  const deleteString = require('../commands/deleteString');
9
9
  const importJson = require('../commands/importJson');
10
10
  const addNamespace = require('../commands/addNamespace');
11
+ const listen = require('../commands/listen');
11
12
 
12
13
  // Using Next's env variable loader because
13
14
  // Next supports more than just one .env file
@@ -75,27 +76,32 @@ program
75
76
  '-v, --value <stringValue>',
76
77
  'The value of the string to create'
77
78
  )
78
- .requiredOption(
79
+ .option(
79
80
  '-ns, --namespace <stringNamespace>',
80
- 'The namespace in which to create the string'
81
+ 'The namespace in which to create the string (Only required if your project uses namespaces and has more than one namespace)'
82
+ )
83
+ .option(
84
+ '-n, --notes <teamNotes>',
85
+ 'Team notes about the string to create (optional)'
81
86
  )
82
87
  .option(
83
- '-d, --details <stringDetails>',
84
- 'The details of the string to create (optional)'
88
+ '-ai, --ai-instructions <stringAiInstructions>',
89
+ 'Instructions/Context for AI machine translator (optional)'
85
90
  )
86
91
  .action(options => {
87
92
  addString({
88
93
  key: options.key,
89
94
  value: options.value,
90
- details: options.details,
95
+ notes: options.notes,
91
96
  namespace: options.namespace,
92
97
  apiKey: options.apiKey,
93
- pat: options.pat
98
+ pat: options.pat,
99
+ aiInstructions: options.aiInstructions
94
100
  });
95
101
  });
96
102
 
97
103
  program
98
- .command('update-string <namespaceOfString> <keyOfString>')
104
+ .command('update-string <namespace:key>')
99
105
  .alias('u')
100
106
  .description('Update a base string through PATCH request')
101
107
  .requiredOption(
@@ -114,7 +120,11 @@ program
114
120
  '-ns, --namespace <stringNamespace>',
115
121
  'The new namespace of the string'
116
122
  )
117
- .option('-d, --details <stringDetails>', 'The new details of the string')
123
+ .option('-n, --notes <teamNotes>', 'The new team notes of the string')
124
+ .option(
125
+ '-ai, --ai-instructions <stringAiInstructions>',
126
+ 'Instructions/Context for AI machine translator (optional)'
127
+ )
118
128
  .option(
119
129
  '--reset-confirmed',
120
130
  'Reset confirmed translations of this string with machine translations.'
@@ -123,15 +133,27 @@ program
123
133
  '--retain-confirmed',
124
134
  'Do not reset confirmed translations of this string with machine translations.'
125
135
  )
126
- .action((namespaceOfString, keyOfString, options) => {
136
+ .action((nsKey, options) => {
137
+ let ns, key;
138
+ const split = nsKey.split(':');
139
+
140
+ if (split.length === 1) {
141
+ key = nsKey;
142
+ } else {
143
+ ns = split[0];
144
+ // in case key contains :
145
+ key = split.slice(1).join(':');
146
+ }
147
+
127
148
  updateString({
128
149
  id: {
129
- namespace: namespaceOfString,
130
- key: keyOfString
150
+ namespace: ns,
151
+ key: key
131
152
  },
132
153
  key: options.key,
133
154
  value: options.value,
134
- details: options.details,
155
+ notes: options.notes,
156
+ aiInstructions: options.aiInstructions,
135
157
  namespace: options.namespace,
136
158
  apiKey: options.apiKey,
137
159
  pat: options.pat,
@@ -141,7 +163,7 @@ program
141
163
  });
142
164
 
143
165
  program
144
- .command('delete-string <namespaceOfString> <keyOfString>')
166
+ .command('delete-string <namespace:key>')
145
167
  .alias('d')
146
168
  .description('Delete a base string and its translations')
147
169
  .requiredOption(
@@ -154,11 +176,22 @@ program
154
176
  'A personal access token generated for your account in i18nexus.',
155
177
  process.env.I18NEXUS_PERSONAL_ACCESS_TOKEN
156
178
  )
157
- .action((namespaceOfString, keyOfString, options) => {
179
+ .action((nsKey, options) => {
180
+ let ns, key;
181
+ const split = nsKey.split(':');
182
+
183
+ if (split.length === 1) {
184
+ key = nsKey;
185
+ } else {
186
+ ns = split[0];
187
+ // in case key contains :
188
+ key = split.slice(1).join(':');
189
+ }
190
+
158
191
  deleteString({
159
192
  id: {
160
- namespace: namespaceOfString,
161
- key: keyOfString
193
+ namespace: ns,
194
+ key: key
162
195
  },
163
196
  apiKey: options.apiKey,
164
197
  pat: options.pat
@@ -180,9 +213,9 @@ program
180
213
  'A personal access token generated for your account in i18nexus.',
181
214
  process.env.I18NEXUS_PERSONAL_ACCESS_TOKEN
182
215
  )
183
- .requiredOption(
216
+ .option(
184
217
  '-ns, --namespace <namespace>',
185
- 'The namespace in which your strings will be imported.'
218
+ 'The namespace in which your strings will be imported. (Only required if your project uses namespaces and has more than one namespace)'
186
219
  )
187
220
  .option(
188
221
  '--overwrite',
@@ -221,4 +254,27 @@ program
221
254
  });
222
255
  });
223
256
 
257
+ program
258
+ .command('listen')
259
+ .description(
260
+ 'Register a live webhook for local development and setup tunnel to listen for updates to translations.'
261
+ )
262
+ .requiredOption(
263
+ '-k, --api-key <apiKey>',
264
+ 'The API key for your project',
265
+ process.env.I18NEXUS_API_KEY
266
+ )
267
+ .option(
268
+ '-p, --path <path>',
269
+ 'The path to the destination folder in which translation files will be downloaded'
270
+ )
271
+ .option('--port <port>', 'Port your local dev server runs on', '3002')
272
+ .action(options => {
273
+ listen({
274
+ apiKey: options.apiKey,
275
+ port: options.port,
276
+ path: options.path
277
+ });
278
+ });
279
+
224
280
  program.parse(process.argv);
@@ -14,7 +14,8 @@ const addString = async opt => {
14
14
  key: opt.key,
15
15
  value: opt.value,
16
16
  namespace: opt.namespace,
17
- description: opt.details
17
+ description: opt.notes,
18
+ ai_instructions: opt.aiInstructions
18
19
  }),
19
20
  headers: {
20
21
  Authorization: `Bearer ${opt.pat}`,
@@ -28,7 +29,13 @@ const addString = async opt => {
28
29
 
29
30
  await response.json();
30
31
 
31
- console.log(colors.green(`New string added to namespace "${opt.namespace}"`));
32
+ if (opt.namespace) {
33
+ console.log(
34
+ colors.green(`New string added to namespace "${opt.namespace}":`)
35
+ );
36
+ } else {
37
+ console.log(colors.green(`New string added:`));
38
+ }
32
39
  console.log(colors.green(`"${opt.key}": "${opt.value}"`));
33
40
  };
34
41
 
@@ -0,0 +1,136 @@
1
+ #!/usr/bin/env node
2
+ /* commands/listen.js */
3
+
4
+ const http = require('http');
5
+ const localtunnel = require('localtunnel');
6
+ const colors = require('colors');
7
+ const handleFetch = require('../handleFetch');
8
+ const baseUrl = require('../baseUrl');
9
+ const pull = require('../commands/pull');
10
+
11
+ const LISTEN_PATH = '/webhook'; // fixed internal path
12
+
13
+ const listen = async ({ apiKey, port, path }) => {
14
+ port = Number(port);
15
+
16
+ if (!Number.isInteger(port) || port <= 0) {
17
+ console.log(colors.red('Invalid port'));
18
+ return process.exit(1);
19
+ }
20
+
21
+ await pull(
22
+ {
23
+ apiKey,
24
+ version: 'latest',
25
+ path: path,
26
+ clean: false,
27
+ confirmed: false
28
+ },
29
+ {
30
+ logging: false,
31
+ successLog: 'Latest strings downloaded'
32
+ }
33
+ );
34
+
35
+ /* ────────────── 1. start local HTTP listener ────────────── */
36
+ const server = http.createServer(async (req, res) => {
37
+ if (req.method !== 'POST' || req.url !== LISTEN_PATH) {
38
+ res.writeHead(404).end();
39
+ return;
40
+ }
41
+
42
+ let body = '';
43
+ req.on('data', chunk => (body += chunk));
44
+ req.on('end', async () => {
45
+ try {
46
+ await pull(
47
+ {
48
+ apiKey,
49
+ version: 'latest',
50
+ path: path,
51
+ clean: false,
52
+ confirmed: false
53
+ },
54
+ {
55
+ logging: false,
56
+ successLog: ' ✔ Translations updated'
57
+ }
58
+ );
59
+
60
+ res.writeHead(200).end('ok');
61
+ } catch (e) {
62
+ console.error(colors.red('Failed to download translations:'), e);
63
+ res.writeHead(500).end('error');
64
+ }
65
+ });
66
+ });
67
+
68
+ /* ────────────── 2. start tunnel ────────────── */
69
+ const tunnel = await new Promise(res => {
70
+ server.listen(port, async () => {
71
+ res(await localtunnel({ port }));
72
+ });
73
+ });
74
+
75
+ const publicUrl = `${tunnel.url}${LISTEN_PATH}`;
76
+
77
+ /* ────────────── 3. register dev webhook with i18nexus ────────────── */
78
+ const createRes = await handleFetch(
79
+ `${baseUrl}/project_resources/dev_webhooks`,
80
+ {
81
+ method: 'POST',
82
+ headers: { 'Content-Type': 'application/json' },
83
+ body: JSON.stringify({ api_key: apiKey, url: publicUrl })
84
+ }
85
+ );
86
+
87
+ let closing = false;
88
+
89
+ /* ────────────── graceful shutdown ────────────── */
90
+ const cleanUp = async (exitCode = 0) => {
91
+ if (closing) {
92
+ return;
93
+ }
94
+
95
+ closing = true;
96
+
97
+ try {
98
+ const { collection } = await createRes.json();
99
+ const webhookId = collection[0].id;
100
+
101
+ await handleFetch(`${baseUrl}/project_resources/dev_webhooks`, {
102
+ method: 'DELETE',
103
+ headers: { 'Content-Type': 'application/json' },
104
+ body: JSON.stringify({ api_key: apiKey, id: webhookId })
105
+ });
106
+ } catch (e) {}
107
+ tunnel.close();
108
+ server.close();
109
+ process.exit(exitCode);
110
+ };
111
+
112
+ if (createRes.status !== 200) {
113
+ console.log(colors.red('Failed to register webhook with i18nexus'));
114
+ cleanUp(1);
115
+ return;
116
+ }
117
+
118
+ console.log(colors.green('Listening for i18nexus updates…'));
119
+
120
+ process.on('SIGINT', () => cleanUp());
121
+ process.on('SIGTERM', () => cleanUp());
122
+
123
+ let connectionLost = false;
124
+ tunnel.on('error', () => {
125
+ if (connectionLost) {
126
+ return;
127
+ }
128
+ connectionLost = true;
129
+
130
+ console.error(colors.red('i18nexus listener connection lost'));
131
+
132
+ cleanUp(1);
133
+ });
134
+ };
135
+
136
+ module.exports = listen;
package/commands/pull.js CHANGED
@@ -22,7 +22,12 @@ const cleanDirectory = path => {
22
22
  });
23
23
  };
24
24
 
25
- const pull = async opt => {
25
+ const pull = async (opt, internalOptions = {}) => {
26
+ const {
27
+ logging = true,
28
+ successLog = 'Translations downloaded successfully'
29
+ } = internalOptions;
30
+
26
31
  let path = opt.path;
27
32
 
28
33
  const project = await getProject(opt.apiKey);
@@ -66,7 +71,9 @@ const pull = async opt => {
66
71
  return process.exit(1);
67
72
  }
68
73
 
69
- console.log(`Downloading translations to ${path}...`);
74
+ if (logging) {
75
+ console.log(`Downloading translations to ${path}...`);
76
+ }
70
77
 
71
78
  const lngResponse = await handleFetch(
72
79
  `${baseUrl}/project_resources/languages.json?api_key=${opt.apiKey}`
@@ -143,7 +150,7 @@ const pull = async opt => {
143
150
  }
144
151
  }
145
152
 
146
- console.log(colors.green('Translations downloaded successfully.'));
153
+ console.log(colors.green(successLog));
147
154
  };
148
155
 
149
156
  module.exports = pull;
@@ -28,7 +28,8 @@ const addString = async opt => {
28
28
  key: opt.key,
29
29
  value: opt.value,
30
30
  namespace: opt.namespace,
31
- description: opt.details,
31
+ description: opt.notes,
32
+ ai_instructions: opt.aiInstructions,
32
33
  reset_confirmed: resetConfirmed
33
34
  }),
34
35
  headers: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "i18nexus-cli",
3
- "version": "3.4.0",
3
+ "version": "3.6.0",
4
4
  "description": "Command line interface (CLI) for accessing the i18nexus API",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -9,7 +9,9 @@
9
9
  "keywords": [
10
10
  "i18nexus",
11
11
  "cli",
12
- "i18next"
12
+ "react-intl",
13
+ "i18next",
14
+ "next-intl"
13
15
  ],
14
16
  "author": "i18nexus",
15
17
  "license": "MIT",
@@ -18,6 +20,7 @@
18
20
  "colors": "^1.4.0",
19
21
  "commander": "^7.2.0",
20
22
  "https-proxy-agent": "^5.0.0",
23
+ "localtunnel": "^2.0.2",
21
24
  "node-fetch": "^2.6.7"
22
25
  }
23
26
  }