i18nexus-cli 3.8.1 → 3.8.3

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
@@ -73,14 +73,25 @@ If your i18nexus project is set to **`react-intl`**:
73
73
 
74
74
  ```
75
75
  .
76
- └── messages
77
- └── locales
76
+ └── src
77
+ └── messages
78
78
  ├── en
79
79
  | └── common.json
80
80
  └── de
81
81
  └── common.json
82
82
  ```
83
83
 
84
+ If your i18nexus project is set to **`react-intl`** and a `src` directory does not exist:
85
+
86
+ ```
87
+ .
88
+ └── messages
89
+ ├── en
90
+ | └── common.json
91
+ └── de
92
+ └── common.json
93
+ ```
94
+
84
95
  If you wish to download your files to a different directory, you can use the `--path` option to specify your download destination. See all options below:
85
96
 
86
97
  ### Options
@@ -6,6 +6,7 @@ const baseUrl = require('../baseUrl');
6
6
  const { URL } = require('url');
7
7
 
8
8
  const listen = async ({ apiKey, path, compact }) => {
9
+ // Initial sync
9
10
  await pull(
10
11
  {
11
12
  apiKey,
@@ -15,69 +16,149 @@ const listen = async ({ apiKey, path, compact }) => {
15
16
  confirmed: false,
16
17
  compact
17
18
  },
18
- {
19
- logging: false,
20
- successLog: 'Latest strings downloaded'
21
- }
19
+ { logging: false, successLog: 'Latest strings downloaded' }
22
20
  );
23
21
 
24
22
  const wsUrl = new URL(`${baseUrl.replace(/^http/, 'ws')}/cable`);
25
23
  wsUrl.searchParams.set('api_key', apiKey);
26
24
 
27
- const ws = new WebSocket(wsUrl.toString(), {
28
- headers: {
29
- Origin: 'http://cli.i18nexus.com'
30
- }
31
- });
25
+ let ws;
26
+ let pingInterval; // timer sending pings
27
+ let watchdogInterval; // timer detecting sleep/drift
28
+ let lastPong = Date.now();
29
+ let reconnectAttempts = 0;
30
+ const PING_EVERY_MS = 30_000; // send ping every 30s
31
+ const STALE_AFTER_MS = 90_000; // if >90s since last pong => stale
32
+ const WATCHDOG_EVERY_MS = 10_000; // check for sleep every 10s
33
+
34
+ const backoff = () => {
35
+ const base = Math.min(60_000, 1000 * Math.pow(2, reconnectAttempts)); // 1s -> 60s
36
+ const jitter = Math.floor(Math.random() * 1000);
37
+ return base + jitter;
38
+ };
39
+
40
+ const clearTimers = () => {
41
+ if (pingInterval) clearInterval(pingInterval);
42
+ if (watchdogInterval) clearInterval(watchdogInterval);
43
+ pingInterval = undefined;
44
+ watchdogInterval = undefined;
45
+ };
46
+
47
+ const startHeartbeat = () => {
48
+ // respond to server pings automatically (ws does this), we send pings to keep NAT/CF alive
49
+ ws.on('pong', () => {
50
+ lastPong = Date.now();
51
+ });
52
+
53
+ pingInterval = setInterval(() => {
54
+ if (ws && ws.readyState === WebSocket.OPEN) {
55
+ try {
56
+ ws.ping();
57
+ } catch (_) {}
58
+ }
59
+ // consider stale?
60
+ if (Date.now() - lastPong > STALE_AFTER_MS) {
61
+ try {
62
+ ws.terminate();
63
+ } catch (_) {}
64
+ }
65
+ }, PING_EVERY_MS);
32
66
 
33
- ws.on('open', () => {
34
- console.log(colors.green('Listening for i18nexus string updates...'));
67
+ // detect sleep/wake (large timer drift)
68
+ let lastTick = Date.now();
69
+ watchdogInterval = setInterval(() => {
70
+ const now = Date.now();
71
+ if (now - lastTick > PING_EVERY_MS * 3) {
72
+ // slept for a while
73
+ try {
74
+ ws.terminate();
75
+ } catch (_) {}
76
+ }
77
+ lastTick = now;
78
+ }, WATCHDOG_EVERY_MS);
79
+ };
35
80
 
81
+ const subscribe = () => {
36
82
  ws.send(
37
83
  JSON.stringify({
38
84
  command: 'subscribe',
39
85
  identifier: JSON.stringify({ channel: 'CliListenChannel' })
40
86
  })
41
87
  );
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') {
50
- await pull(
51
- {
52
- apiKey,
53
- version: 'latest',
54
- path,
55
- clean: false,
56
- confirmed: false,
57
- compact
58
- },
59
- {
60
- logging: false,
61
- successLog: ' ✔ Translations updated'
62
- }
63
- );
88
+ };
89
+
90
+ const connect = () => {
91
+ ws = new WebSocket(wsUrl.toString(), {
92
+ headers: { Origin: 'http://cli.i18nexus.com' }
93
+ });
94
+
95
+ ws.on('open', () => {
96
+ reconnectAttempts = 0;
97
+ lastPong = Date.now();
98
+ console.log(colors.green('Listening for i18nexus string updates...'));
99
+ subscribe();
100
+ startHeartbeat();
101
+
102
+ // Optional: TCP keepalive at the socket layer
103
+ if (ws._socket && ws._socket.setKeepAlive) {
104
+ ws._socket.setKeepAlive(true, 60_000);
64
105
  }
65
- } catch (e) {
66
- console.error(colors.red('i18nexus Sync Failed'));
67
- }
68
- });
106
+ });
69
107
 
70
- ws.on('close', () => {
71
- process.exit(1);
72
- });
108
+ ws.on('message', async message => {
109
+ try {
110
+ const data = JSON.parse(message);
111
+ const payload = data.message;
112
+ if (payload?.event === 'strings.changed') {
113
+ await pull(
114
+ {
115
+ apiKey,
116
+ version: 'latest',
117
+ path,
118
+ clean: false,
119
+ confirmed: false,
120
+ compact
121
+ },
122
+ { logging: false, successLog: ' ✔ Translations updated' }
123
+ );
124
+ }
125
+ } catch (e) {
126
+ console.error(colors.red('i18nexus Sync Failed'));
127
+ }
128
+ });
129
+
130
+ const scheduleReconnect = why => {
131
+ clearTimers();
132
+ if (ws) {
133
+ ws.removeAllListeners();
134
+ }
135
+ reconnectAttempts += 1;
136
+ const delay = backoff();
137
+ console.log(
138
+ colors.yellow(
139
+ `Attempting i18nexus reconnect in ${Math.round(delay / 1000)}s...`
140
+ )
141
+ );
142
+ setTimeout(connect, delay);
143
+ };
73
144
 
74
- ws.on('error', err => {
75
- console.error(colors.red('i18nexus Connection Error'));
76
- process.exit(1);
77
- });
145
+ ws.on('close', () => scheduleReconnect('close'));
146
+ ws.on('error', () => scheduleReconnect('error'));
147
+ };
148
+
149
+ connect();
78
150
 
79
151
  const cleanUp = () => {
80
- ws.close();
152
+ clearTimers();
153
+ if (
154
+ ws &&
155
+ (ws.readyState === WebSocket.OPEN ||
156
+ ws.readyState === WebSocket.CONNECTING)
157
+ ) {
158
+ try {
159
+ ws.close();
160
+ } catch (_) {}
161
+ }
81
162
  process.exit(0);
82
163
  };
83
164
 
package/commands/pull.js CHANGED
@@ -59,6 +59,12 @@ const pull = async (opt, internalOptions = {}) => {
59
59
  } else {
60
60
  path = `${process.cwd()}/public/locales`;
61
61
  }
62
+ } else if (projectLibrary === 'react-intl') {
63
+ const hasSrcDir = fs.existsSync(`${process.cwd()}/src`);
64
+
65
+ path = hasSrcDir
66
+ ? `${process.cwd()}/src/messages`
67
+ : `${process.cwd()}/messages`;
62
68
  } else {
63
69
  path = `${process.cwd()}/messages`;
64
70
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "i18nexus-cli",
3
- "version": "3.8.1",
3
+ "version": "3.8.3",
4
4
  "description": "Command line interface (CLI) for accessing the i18nexus API",
5
5
  "main": "index.js",
6
6
  "bin": {