i18nexus-cli 3.5.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,15 +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` | |
141
- | `--ai-instructions` or `-ai` | |
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` | |
142
142
 
143
143
  ### Notes
144
144
 
@@ -149,7 +149,7 @@ Your project API key (Can also be set using environment variable `I18NEXUS_API_K
149
149
  A personal access token that you have generated in your i18nexus account (Can also be set using environment variable `I18NEXUS_PERSONAL_ACCESS_TOKEN`)
150
150
 
151
151
  `--namespace`
152
- 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.
153
153
 
154
154
  `--key`
155
155
  The key of the string to create
@@ -157,23 +157,23 @@ The key of the string to create
157
157
  `--value`
158
158
  The value of the string to create
159
159
 
160
- `--details`
161
- The details of the string to create (optional)
160
+ `--notes`
161
+ Team notes about the string to create (optional)
162
162
 
163
163
  `--ai-instructions`
164
164
  Instructions or context for AI machine translator (optional)
165
165
 
166
166
  ## Updating existing strings
167
167
 
168
- `i18nexus update-string <namespace> <key>` or `i18nexus u <namespace> <key>`
168
+ `i18nexus update-string <namespace:key>` or `i18nexus u <namespace:key>`
169
169
 
170
170
  ```sh
171
- 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>
172
172
  ```
173
173
 
174
174
  The above snippet will update a the value of the string with key `welcome_msg` in your `common` namespace to `Welcome`.
175
175
 
176
- 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.
177
177
 
178
178
  You can then update the key, value, details, and/or namespace by using the command options:
179
179
 
@@ -208,8 +208,8 @@ The new key of the string
208
208
  `--value`
209
209
  The new value of the string
210
210
 
211
- `--details`
212
- The new details of the string
211
+ `--notes`
212
+ The new team notes of the string
213
213
 
214
214
  `--ai-instructions`
215
215
  Instructions or context for AI machine translator
@@ -224,13 +224,13 @@ Confirmed translations of this string will be retained
224
224
 
225
225
  ## Deleting strings
226
226
 
227
- `i18nexus delete-string <namespace> <key>` or `i18nexus d <namespace> <key>`
227
+ `i18nexus delete-string <namespace:key>` or `i18nexus d <namespace:key>`
228
228
 
229
229
  ```sh
230
- 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>
231
231
  ```
232
232
 
233
- 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.
234
234
 
235
235
  ### Options
236
236
 
@@ -261,11 +261,12 @@ This is the equivalent of using the Import tool in the i18nexus web application.
261
261
 
262
262
  ### Options
263
263
 
264
- | Option | Required? |
265
- | ------------------- | --------- |
266
- | `--api-key` or `-k` | &#10004; |
267
- | `--pat` or `-t` | &#10004; |
268
- | `--overwrite` | |
264
+ | Option | Required? |
265
+ | ---------------------- | ------------- |
266
+ | `--api-key` or `-k` | &#10004; |
267
+ | `--pat` or `-t` | &#10004; |
268
+ | `--namespace` or `-ns` | Conditionally |
269
+ | `--overwrite` | |
269
270
 
270
271
  ### Notes
271
272
 
@@ -275,6 +276,9 @@ Your project API key (Can also be set using environment variable `I18NEXUS_API_K
275
276
  `--pat`
276
277
  A personal access token that you have generated in your i18nexus account (Can also be set using environment variable `I18NEXUS_PERSONAL_ACCESS_TOKEN`)
277
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
+
278
282
  `--overwrite`
279
283
  If any keys already exist in the target namespace, overwrite the values with the imported values.
280
284
 
@@ -302,3 +306,33 @@ Your project API key (Can also be set using environment variable `I18NEXUS_API_K
302
306
 
303
307
  `--pat`
304
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,13 +76,13 @@ 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)'
81
82
  )
82
83
  .option(
83
- '-d, --details <stringDetails>',
84
- 'The details of the string to create (optional)'
84
+ '-n, --notes <teamNotes>',
85
+ 'Team notes about the string to create (optional)'
85
86
  )
86
87
  .option(
87
88
  '-ai, --ai-instructions <stringAiInstructions>',
@@ -91,7 +92,7 @@ program
91
92
  addString({
92
93
  key: options.key,
93
94
  value: options.value,
94
- details: options.details,
95
+ notes: options.notes,
95
96
  namespace: options.namespace,
96
97
  apiKey: options.apiKey,
97
98
  pat: options.pat,
@@ -100,7 +101,7 @@ program
100
101
  });
101
102
 
102
103
  program
103
- .command('update-string <namespaceOfString> <keyOfString>')
104
+ .command('update-string <namespace:key>')
104
105
  .alias('u')
105
106
  .description('Update a base string through PATCH request')
106
107
  .requiredOption(
@@ -119,7 +120,7 @@ program
119
120
  '-ns, --namespace <stringNamespace>',
120
121
  'The new namespace of the string'
121
122
  )
122
- .option('-d, --details <stringDetails>', 'The new details of the string')
123
+ .option('-n, --notes <teamNotes>', 'The new team notes of the string')
123
124
  .option(
124
125
  '-ai, --ai-instructions <stringAiInstructions>',
125
126
  'Instructions/Context for AI machine translator (optional)'
@@ -132,15 +133,26 @@ program
132
133
  '--retain-confirmed',
133
134
  'Do not reset confirmed translations of this string with machine translations.'
134
135
  )
135
- .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
+
136
148
  updateString({
137
149
  id: {
138
- namespace: namespaceOfString,
139
- key: keyOfString
150
+ namespace: ns,
151
+ key: key
140
152
  },
141
153
  key: options.key,
142
154
  value: options.value,
143
- details: options.details,
155
+ notes: options.notes,
144
156
  aiInstructions: options.aiInstructions,
145
157
  namespace: options.namespace,
146
158
  apiKey: options.apiKey,
@@ -151,7 +163,7 @@ program
151
163
  });
152
164
 
153
165
  program
154
- .command('delete-string <namespaceOfString> <keyOfString>')
166
+ .command('delete-string <namespace:key>')
155
167
  .alias('d')
156
168
  .description('Delete a base string and its translations')
157
169
  .requiredOption(
@@ -164,11 +176,22 @@ program
164
176
  'A personal access token generated for your account in i18nexus.',
165
177
  process.env.I18NEXUS_PERSONAL_ACCESS_TOKEN
166
178
  )
167
- .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
+
168
191
  deleteString({
169
192
  id: {
170
- namespace: namespaceOfString,
171
- key: keyOfString
193
+ namespace: ns,
194
+ key: key
172
195
  },
173
196
  apiKey: options.apiKey,
174
197
  pat: options.pat
@@ -190,9 +213,9 @@ program
190
213
  'A personal access token generated for your account in i18nexus.',
191
214
  process.env.I18NEXUS_PERSONAL_ACCESS_TOKEN
192
215
  )
193
- .requiredOption(
216
+ .option(
194
217
  '-ns, --namespace <namespace>',
195
- '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)'
196
219
  )
197
220
  .option(
198
221
  '--overwrite',
@@ -231,4 +254,27 @@ program
231
254
  });
232
255
  });
233
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
+
234
280
  program.parse(process.argv);
@@ -14,7 +14,7 @@ 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
18
  ai_instructions: opt.aiInstructions
19
19
  }),
20
20
  headers: {
@@ -29,7 +29,13 @@ const addString = async opt => {
29
29
 
30
30
  await response.json();
31
31
 
32
- 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
+ }
33
39
  console.log(colors.green(`"${opt.key}": "${opt.value}"`));
34
40
  };
35
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,7 @@ 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
32
  ai_instructions: opt.aiInstructions,
33
33
  reset_confirmed: resetConfirmed
34
34
  }),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "i18nexus-cli",
3
- "version": "3.5.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": {
@@ -20,6 +20,7 @@
20
20
  "colors": "^1.4.0",
21
21
  "commander": "^7.2.0",
22
22
  "https-proxy-agent": "^5.0.0",
23
+ "localtunnel": "^2.0.2",
23
24
  "node-fetch": "^2.6.7"
24
25
  }
25
26
  }