i18nexus-cli 3.6.1 → 3.8.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
@@ -92,6 +92,7 @@ If you wish to download your files to a different directory, you can use the `--
92
92
  | `--ver` or `-v` | `latest` |
93
93
  | `--confirmed` | `false` |
94
94
  | `--clean` | `false` |
95
+ | `--compact` | `false` |
95
96
 
96
97
  ### Notes
97
98
 
@@ -110,6 +111,9 @@ Downloads only translations that have been confirmed in i18nexus
110
111
  `--clean`
111
112
  Before download, clears your destination folder specified in --path. As a safety precaution, this only deletes folders with names that match a simple language code regex. You should still ensure you are not storing any files in your destination folder that you do not want deleted.
112
113
 
114
+ `--compact`
115
+ Output JSON without extra whitespace (default is pretty-printed)
116
+
113
117
  ## Personal Access Tokens
114
118
 
115
119
  The following commands require a Personal Access Token (PAT) because they write data to your i18nexus project. PATs are created in your i18nexus account dashboard.
@@ -315,9 +319,20 @@ A personal access token that you have generated in your i18nexus account (Can al
315
319
  i18nexus listen -k <PROJECT_API_KEY>
316
320
  ```
317
321
 
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.
322
+ This command opens a WebSocket connection from your local environment to the i18nexus servers to receive real-time updates whenever strings are added, updated, or deleted in your project.
323
+
324
+ When changes occur, your local JSON files are automatically refreshed to stay in sync.
325
+
326
+ Many developers prefer to run this automatically alongside their development server. For example, in a Next.js project, you can do the following in your `package.json` using [concurrently](https://www.npmjs.com/package/concurrently):
319
327
 
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.
328
+ ```json
329
+ "scripts": {
330
+ "dev": "concurrently --raw \"next dev --turbo\" \"i18nexus listen\"",
331
+ "build": "i18nexus pull && next build",
332
+ "start": "i18nexus pull && next start",
333
+ "lint": "next lint"
334
+ }
335
+ ```
321
336
 
322
337
  ### Options
323
338
 
@@ -325,14 +340,15 @@ When a string is added, updated, or deleted in your project, a webhook is sent t
325
340
  | ------------------- | --------- | ----------------------- |
326
341
  | `--api-key` or `-k` | &#10004; | |
327
342
  | `--path` or `-p` | | See `pull` default path |
328
- | `--port` | | '3002' |
343
+ | `--compact` | | `false` |
329
344
 
330
345
  ### Notes
331
346
 
332
347
  `--api-key`
333
348
  Your project API key (Can also be set using environment variable `I18NEXUS_API_KEY`)
334
349
 
335
- `--port`
336
- The local port your server will listen on to receive incoming webhook requests from i18nexus. Defaults to `3002`.
350
+ `--path`
351
+ The path to the destination folder in which translation files will be downloaded
337
352
 
338
- The tunnel will remain active as long as the CLI is running. Press `Ctrl+C` to stop listening.
353
+ `--compact`
354
+ Output JSON without extra whitespace (default is pretty-printed)
package/bin/index.js CHANGED
@@ -47,12 +47,17 @@ program
47
47
  'Removes and rebuilds destination folder before download',
48
48
  false
49
49
  )
50
+ .option(
51
+ '--compact',
52
+ 'Output JSON without extra whitespace (default is pretty-printed)'
53
+ )
50
54
  .action(options => {
51
55
  pull({
52
56
  apiKey: options.apiKey,
53
57
  version: options.ver,
54
58
  path: options.path,
55
59
  clean: options.clean,
60
+ compact: options.compact,
56
61
  confirmed: options.confirmed
57
62
  });
58
63
  });
@@ -268,12 +273,15 @@ program
268
273
  '-p, --path <path>',
269
274
  'The path to the destination folder in which translation files will be downloaded'
270
275
  )
271
- .option('--port <port>', 'Port your local dev server runs on', '3002')
276
+ .option(
277
+ '--compact',
278
+ 'Output JSON without extra whitespace (default is pretty-printed)'
279
+ )
272
280
  .action(options => {
273
281
  listen({
274
282
  apiKey: options.apiKey,
275
- port: options.port,
276
- path: options.path
283
+ path: options.path,
284
+ compact: options.compact
277
285
  });
278
286
  });
279
287
 
@@ -1,30 +1,19 @@
1
1
  #!/usr/bin/env node
2
- /* commands/listen.js */
3
-
4
- const http = require('http');
5
- const localtunnel = require('localtunnel');
2
+ const WebSocket = require('ws');
6
3
  const colors = require('colors');
7
- const handleFetch = require('../handleFetch');
8
- const baseUrl = require('../baseUrl');
9
4
  const pull = require('../commands/pull');
5
+ const baseUrl = require('../baseUrl');
6
+ const { URL } = require('url');
10
7
 
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
-
8
+ const listen = async ({ apiKey, path, compact = false }) => {
21
9
  await pull(
22
10
  {
23
11
  apiKey,
24
12
  version: 'latest',
25
- path: path,
13
+ path,
26
14
  clean: false,
27
- confirmed: false
15
+ confirmed: false,
16
+ compact
28
17
  },
29
18
  {
30
19
  logging: false,
@@ -32,105 +21,68 @@ const listen = async ({ apiKey, port, path }) => {
32
21
  }
33
22
  );
34
23
 
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;
24
+ const wsUrl = new URL(`${baseUrl.replace(/^http/, 'ws')}/cable`);
25
+ wsUrl.searchParams.set('api_key', apiKey);
26
+
27
+ const ws = new WebSocket(wsUrl.toString(), {
28
+ headers: {
29
+ Origin: 'http://cli.i18nexus.com'
40
30
  }
31
+ });
32
+
33
+ ws.on('open', () => {
34
+ console.log(colors.green('Listening for i18nexus string updates...'));
41
35
 
42
- let body = '';
43
- req.on('data', chunk => (body += chunk));
44
- req.on('end', async () => {
45
- try {
36
+ ws.send(
37
+ JSON.stringify({
38
+ command: 'subscribe',
39
+ identifier: JSON.stringify({ channel: 'CliListenChannel' })
40
+ })
41
+ );
42
+ });
43
+
44
+ ws.on('message', async message => {
45
+ try {
46
+ const data = JSON.parse(message);
47
+ const payload = data.message;
48
+
49
+ if (payload?.event === 'strings.changed') {
46
50
  await pull(
47
51
  {
48
52
  apiKey,
49
53
  version: 'latest',
50
- path: path,
54
+ path,
51
55
  clean: false,
52
- confirmed: false
56
+ confirmed: false,
57
+ compact
53
58
  },
54
59
  {
55
60
  logging: false,
56
61
  successLog: ' ✔ Translations updated'
57
62
  }
58
63
  );
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
64
  }
65
- });
65
+ } catch (e) {
66
+ console.error(colors.red('i18nexus Sync Failed'));
67
+ }
66
68
  });
67
69
 
68
- /* ────────────── 2. start tunnel ────────────── */
69
- const tunnel = await new Promise(res => {
70
- server.listen(port, async () => {
71
- res(await localtunnel({ port }));
72
- });
70
+ ws.on('close', () => {
71
+ process.exit(1);
73
72
  });
74
73
 
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;
74
+ ws.on('error', err => {
75
+ console.error(colors.red('i18nexus Connection Error'));
76
+ process.exit(1);
77
+ });
100
78
 
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);
79
+ const cleanUp = () => {
80
+ ws.close();
81
+ process.exit(0);
110
82
  };
111
83
 
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
- });
84
+ process.on('SIGINT', cleanUp);
85
+ process.on('SIGTERM', cleanUp);
134
86
  };
135
87
 
136
88
  module.exports = listen;
package/commands/pull.js CHANGED
@@ -127,13 +127,15 @@ const pull = async (opt, internalOptions = {}) => {
127
127
  cleanDirectory(path);
128
128
  }
129
129
 
130
+ const spacing = opt.compact ? 0 : 2;
131
+
130
132
  if (projectLibrary === 'next-intl') {
131
133
  for (let lng in translations) {
132
134
  fs.mkdirSync(path, { recursive: true });
133
135
 
134
136
  fs.writeFileSync(
135
137
  `${path}/${lng}.json`,
136
- JSON.stringify(translations[lng])
138
+ JSON.stringify(translations[lng], null, spacing)
137
139
  );
138
140
  }
139
141
  } else {
@@ -144,7 +146,7 @@ const pull = async (opt, internalOptions = {}) => {
144
146
  for (let namespace in translations[lng]) {
145
147
  fs.writeFileSync(
146
148
  `${lngFilePath}/${namespace}.json`,
147
- JSON.stringify(translations[lng][namespace])
149
+ JSON.stringify(translations[lng][namespace], null, spacing)
148
150
  );
149
151
  }
150
152
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "i18nexus-cli",
3
- "version": "3.6.1",
3
+ "version": "3.8.0",
4
4
  "description": "Command line interface (CLI) for accessing the i18nexus API",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -20,7 +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",
24
- "node-fetch": "^2.6.7"
23
+ "node-fetch": "^2.6.7",
24
+ "ws": "^8.18.3"
25
25
  }
26
26
  }