@djangocfg/i18n 2.1.541 → 2.1.543

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@djangocfg/i18n",
3
- "version": "2.1.541",
3
+ "version": "2.1.543",
4
4
  "description": "Lightweight i18n library for @djangocfg packages with built-in translations for English, Russian, and Korean",
5
5
  "keywords": [
6
6
  "i18n",
@@ -67,7 +67,7 @@
67
67
  "i18n:add": "tsx src/cli/index.ts add"
68
68
  },
69
69
  "peerDependencies": {
70
- "react": "^19.2.4"
70
+ "react": "^19.0.0"
71
71
  },
72
72
  "dependencies": {
73
73
  "citty": "^0.1.6",
@@ -75,7 +75,7 @@
75
75
  "jiti": "^2.7.0"
76
76
  },
77
77
  "devDependencies": {
78
- "@djangocfg/typescript-config": "^2.1.541",
78
+ "@djangocfg/typescript-config": "^2.1.543",
79
79
  "@types/node": "^25.9.5",
80
80
  "@types/react": "19.2.15",
81
81
  "eslint": "^9.39.5",
@@ -11,6 +11,8 @@ import {
11
11
  readLocaleFile,
12
12
  writeLocaleFile,
13
13
  getValueAtPath,
14
+ setValueAtPath,
15
+ renderTsLocale,
14
16
  loadLocale,
15
17
  } from '../utils';
16
18
 
@@ -65,7 +67,6 @@ export const addCommand = defineCommand({
65
67
  consola.info(`Directory: ${localesDir}`);
66
68
  consola.log('');
67
69
 
68
- const keyParts = keyPath.split('.');
69
70
  let updated = 0;
70
71
  let skipped = 0;
71
72
 
@@ -92,14 +93,16 @@ export const addCommand = defineCommand({
92
93
  const { content, exportName } = readLocaleFile(config, locale);
93
94
 
94
95
  if (config.fileExtension === '.json') {
95
- // JSON file
96
96
  const json = JSON.parse(content);
97
- setNestedValue(json, keyParts, translation);
97
+ setValueAtPath(json, keyPath, translation);
98
98
  writeLocaleFile(config, locale, JSON.stringify(json, null, 2));
99
99
  } else {
100
- // TypeScript file - use regex to insert
101
- const newContent = insertKeyIntoTS(content, exportName, keyParts, translation);
102
- writeLocaleFile(config, locale, newContent);
100
+ // `existing` is the same file, already parsed by `loadLocale` above
101
+ // for the key-exists check. Editing that object and re-serialising is
102
+ // what makes nested keys work: the old text-splicing path inserted a
103
+ // duplicate parent block and lost its siblings.
104
+ setValueAtPath(existing, keyPath, translation);
105
+ writeLocaleFile(config, locale, renderTsLocale(content, exportName, existing));
103
106
  }
104
107
 
105
108
  consola.success(` ${locale}: Added "${translation}"`);
@@ -116,98 +119,3 @@ export const addCommand = defineCommand({
116
119
  }
117
120
  },
118
121
  });
119
-
120
- /**
121
- * Set nested value in object
122
- */
123
- function setNestedValue(obj: Record<string, unknown>, keys: string[], value: string): void {
124
- let current = obj;
125
- for (let i = 0; i < keys.length - 1; i++) {
126
- const key = keys[i];
127
- if (!(key in current) || typeof current[key] !== 'object') {
128
- current[key] = {};
129
- }
130
- current = current[key] as Record<string, unknown>;
131
- }
132
- current[keys[keys.length - 1]] = value;
133
- }
134
-
135
- /**
136
- * Insert key into TypeScript locale file
137
- */
138
- function insertKeyIntoTS(
139
- content: string,
140
- exportName: string,
141
- keyParts: string[],
142
- value: string
143
- ): string {
144
- // Find the object for the first key part
145
- const firstKey = keyParts[0];
146
- const restKeys = keyParts.slice(1);
147
-
148
- // Check if top-level key exists
149
- const topLevelRegex = new RegExp(`(${firstKey}:\\s*\\{)`, 'g');
150
- const hasTopLevel = topLevelRegex.test(content);
151
-
152
- if (!hasTopLevel) {
153
- // Add new top-level key before closing brace
154
- const closingBrace = content.lastIndexOf('}');
155
- const indent = ' ';
156
- const newEntry = buildNestedEntry(keyParts, value, indent);
157
- return (
158
- content.slice(0, closingBrace) + '\n' + newEntry + '\n' + content.slice(closingBrace)
159
- );
160
- }
161
-
162
- // Navigate to the right nesting level and insert
163
- // This is a simplified approach - for complex cases, consider AST parsing
164
- let insertPoint = content.indexOf(`${firstKey}:`);
165
- if (insertPoint === -1) return content;
166
-
167
- // Find the opening brace for this section
168
- let braceCount = 0;
169
- let foundStart = false;
170
- let insertIndex = insertPoint;
171
-
172
- for (let i = insertPoint; i < content.length; i++) {
173
- if (content[i] === '{') {
174
- braceCount++;
175
- foundStart = true;
176
- } else if (content[i] === '}') {
177
- braceCount--;
178
- if (foundStart && braceCount === 0) {
179
- // Insert before this closing brace
180
- insertIndex = i;
181
- break;
182
- }
183
- }
184
- }
185
-
186
- // Build the entry
187
- const indent = ' '; // 4 spaces for nested
188
- const entry = restKeys.length === 0
189
- ? `${indent}${keyParts[keyParts.length - 1]}: '${escapeQuotes(value)}',`
190
- : buildNestedEntry(restKeys, value, indent);
191
-
192
- return content.slice(0, insertIndex) + '\n' + entry + '\n ' + content.slice(insertIndex);
193
- }
194
-
195
- /**
196
- * Build nested entry string
197
- */
198
- function buildNestedEntry(keys: string[], value: string, baseIndent: string): string {
199
- if (keys.length === 1) {
200
- return `${baseIndent}${keys[0]}: '${escapeQuotes(value)}',`;
201
- }
202
-
203
- const [first, ...rest] = keys;
204
- const nestedContent = buildNestedEntry(rest, value, baseIndent + ' ');
205
- return `${baseIndent}${first}: {\n${nestedContent}\n${baseIndent}},`;
206
- }
207
-
208
- /**
209
- * Escape single quotes in value
210
- */
211
- function escapeQuotes(value: string): string {
212
- return value.replace(/'/g, "\\'");
213
- }
@@ -123,12 +123,15 @@ export function readLocaleFile(
123
123
  return { content, exportName: '' };
124
124
  }
125
125
 
126
- const exportMatch = content.match(/export const (\w+)/);
127
- if (!exportMatch) {
126
+ // Destructured rather than indexed: `\w+` cannot match empty, so a match
127
+ // always carries group 1 — but only the binding proves that to the compiler,
128
+ // and the same `throw` covers both halves instead of asserting one of them.
129
+ const [, exportName] = content.match(/export const (\w+)/) ?? [];
130
+ if (!exportName) {
128
131
  throw new Error(`Could not find export in ${locale}${config.fileExtension}`);
129
132
  }
130
133
 
131
- return { content, exportName: exportMatch[1] };
134
+ return { content, exportName };
132
135
  }
133
136
 
134
137
  /**
@@ -183,23 +186,124 @@ export function getValueAtPath(obj: Record<string, unknown>, keyPath: string): u
183
186
  }
184
187
 
185
188
  /**
186
- * Set value at nested path
189
+ * Set value at nested path.
190
+ *
191
+ * `isPlainRecord` rather than `typeof x === 'object'`: that test is true for
192
+ * `null` and for arrays. A `null` on the path crashed with "Cannot set
193
+ * properties of null", and an array swallowed the write silently — the value
194
+ * went nowhere and the CLI reported success.
195
+ *
196
+ * An empty `keyPath` writes nothing. `''.split('.')` yields `['']`, and the
197
+ * previous code indexed `keys[-1]`, producing a literal `"undefined"` key in
198
+ * the locale file.
187
199
  */
188
200
  export function setValueAtPath(
189
201
  obj: Record<string, unknown>,
190
202
  keyPath: string,
191
203
  value: unknown
192
204
  ): void {
193
- const keys = keyPath.split('.');
205
+ const keys = keyPath.split('.').filter(Boolean);
206
+ const leaf = keys.pop();
207
+ if (leaf === undefined) return;
208
+
194
209
  let current = obj;
210
+ for (const key of keys) {
211
+ const next = current[key];
212
+ current = isPlainRecord(next) ? next : (current[key] = {});
213
+ }
214
+
215
+ current[leaf] = value;
216
+ }
217
+
218
+ /** A value that can carry nested keys: an object, but not `null` and not an array. */
219
+ function isPlainRecord(value: unknown): value is Record<string, unknown> {
220
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
221
+ }
222
+
223
+ /** Object keys that need no quoting in a TS/JS object literal. */
224
+ const BARE_KEY = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
225
+
226
+ /**
227
+ * Serialise a translations object back to a TypeScript object literal.
228
+ *
229
+ * Locale leaves are always strings and branches always plain objects, so this
230
+ * needs no general-purpose value support — anything else is a bug upstream and
231
+ * `JSON.stringify` is the honest fallback rather than a silent `[object]`.
232
+ */
233
+ function toObjectLiteral(value: unknown, indent: string): string {
234
+ if (!isPlainRecord(value)) return JSON.stringify(value ?? null);
235
+
236
+ const inner = indent + ' ';
237
+ const body = Object.entries(value)
238
+ .map(([key, child]) => {
239
+ const name = BARE_KEY.test(key) ? key : JSON.stringify(key);
240
+ return `${inner}${name}: ${toObjectLiteral(child, inner)},`;
241
+ })
242
+ .join('\n');
243
+
244
+ return body ? `{\n${body}\n${indent}}` : '{}';
245
+ }
195
246
 
196
- for (let i = 0; i < keys.length - 1; i++) {
197
- const key = keys[i];
198
- if (!(key in current) || typeof current[key] !== 'object') {
199
- current[key] = {};
247
+ /**
248
+ * Replace the exported object literal in a TS locale file, keeping everything
249
+ * around it — the type import, the doc comment, and the `: I18nTranslations`
250
+ * annotation — untouched.
251
+ *
252
+ * The previous approach edited the text with `indexOf` and brace counting, and
253
+ * inserted a whole nested chain at the outermost level: adding `ui.form.cancel`
254
+ * to a file that already had `ui.form.save` produced a SECOND `form:` block
255
+ * inside `ui`. In a TS object literal the later key wins, so the existing
256
+ * translation was silently dropped — a data-loss bug, not a formatting one.
257
+ * The file's own comment said "for complex cases, consider AST parsing".
258
+ * Serialising the object `loadLocale` already parsed removes the need for both.
259
+ */
260
+ export function renderTsLocale(
261
+ originalContent: string,
262
+ exportName: string,
263
+ translations: Record<string, unknown>
264
+ ): string {
265
+ const declaration = new RegExp(`(export\\s+const\\s+${exportName}\\b[^=]*=\\s*)`);
266
+ const match = originalContent.match(declaration);
267
+ if (!match || match.index === undefined) {
268
+ throw new Error(`Could not find "export const ${exportName}" to update`);
269
+ }
270
+
271
+ const bodyStart = match.index + match[0].length;
272
+ if (originalContent[bodyStart] !== '{') {
273
+ throw new Error(`Expected an object literal after "export const ${exportName}"`);
274
+ }
275
+
276
+ const bodyEnd = matchBrace(originalContent, bodyStart);
277
+ return (
278
+ originalContent.slice(0, bodyStart) +
279
+ toObjectLiteral(translations, '') +
280
+ originalContent.slice(bodyEnd + 1)
281
+ );
282
+ }
283
+
284
+ /**
285
+ * Index of the `}` closing the `{` at `start`.
286
+ *
287
+ * String-aware: a brace inside a translation (`"{count} items"`) would
288
+ * otherwise unbalance the count and truncate the file.
289
+ */
290
+ function matchBrace(source: string, start: number): number {
291
+ let depth = 0;
292
+ let quote: string | null = null;
293
+
294
+ for (let i = start; i < source.length; i++) {
295
+ const char = source[i];
296
+
297
+ if (quote) {
298
+ if (char === '\\') i++;
299
+ else if (char === quote) quote = null;
300
+ continue;
200
301
  }
201
- current = current[key] as Record<string, unknown>;
302
+
303
+ if (char === '"' || char === "'" || char === '`') quote = char;
304
+ else if (char === '{') depth++;
305
+ else if (char === '}' && --depth === 0) return i;
202
306
  }
203
307
 
204
- current[keys[keys.length - 1]] = value;
308
+ throw new Error('Unbalanced braces in locale file');
205
309
  }
package/src/context.tsx CHANGED
@@ -117,11 +117,14 @@ export function I18nProvider({
117
117
  [onLocaleChange]
118
118
  )
119
119
 
120
- // Sync with external locale changes
120
+ // Sync with external locale changes.
121
+ //
122
+ // The comparison moved inside the updater rather than `locale` joining the
123
+ // deps: this effect must run when the PROP changes, not when local state
124
+ // does. Depending on `locale` would re-run it after every `setLocale` call
125
+ // and reset the user's choice back to `initialLocale`.
121
126
  React.useEffect(() => {
122
- if (initialLocale !== locale) {
123
- setLocaleState(initialLocale)
124
- }
127
+ setLocaleState((current) => (current === initialLocale ? current : initialLocale))
125
128
  }, [initialLocale])
126
129
 
127
130
  const value = React.useMemo<I18nContextValue>(
package/src/locales/ar.ts CHANGED
@@ -470,61 +470,6 @@ export const ar: I18nTranslations = {
470
470
  },
471
471
  },
472
472
 
473
- centrifugo: {
474
- monitor: {
475
- title: 'مراقب Centrifugo',
476
- description: 'مراقبة وتصحيح WebSocket في الوقت الفعلي',
477
- openMonitor: 'فتح مراقب Centrifugo',
478
- openDebugPanel: 'فتح لوحة تصحيح Centrifugo',
479
- widgetTitle: 'مراقب WebSocket',
480
- },
481
- tabs: {
482
- connection: 'الاتصال',
483
- messages: 'الرسائل',
484
- subscriptions: 'الاشتراكات',
485
- },
486
- subscriptionsList: {
487
- title: 'الاشتراكات النشطة',
488
- notConnected: 'غير متصل بـ Centrifugo',
489
- noActiveSubscriptions: 'لا توجد اشتراكات نشطة',
490
- },
491
- status: {
492
- connected: 'متصل',
493
- disconnected: 'غير متصل',
494
- connecting: 'جارٍ الاتصال...',
495
- connect: 'اتصال',
496
- uptime: 'وقت التشغيل:',
497
- subscriptions: 'الاشتراكات:',
498
- realtimeUnavailable: 'ميزات الوقت الفعلي غير متاحة',
499
- },
500
- feed: {
501
- title: 'تغذية الرسائل',
502
- noMessages: 'لا توجد رسائل بعد',
503
- pausedClickToResume: 'متوقف - انقر على تشغيل للاستئناف',
504
- autoScrollOn: 'التمرير التلقائي: مفعّل',
505
- autoScrollOff: 'التمرير التلقائي: معطّل',
506
- viewData: 'عرض البيانات',
507
- },
508
- filters: {
509
- title: 'الفلاتر',
510
- active: 'نشط',
511
- clear: 'مسح',
512
- searchMessages: 'البحث في الرسائل...',
513
- level: 'المستوى:',
514
- type: 'النوع:',
515
- source: 'المصدر',
516
- autoScrollToLatest: 'التمرير التلقائي للأحدث',
517
- },
518
- debug: {
519
- title: 'تصحيح Centrifugo',
520
- description: 'حالة اتصال WebSocket والسجلات والاشتراكات',
521
- openDebugPanel: 'فتح لوحة تصحيح Centrifugo',
522
- tabConnection: 'الاتصال',
523
- tabLogs: 'السجلات',
524
- tabSubscriptions: 'الاشتراكات',
525
- },
526
- },
527
-
528
473
  tools: {
529
474
  code: {
530
475
  copyCode: 'نسخ الكود',
package/src/locales/da.ts CHANGED
@@ -470,61 +470,6 @@ export const da: I18nTranslations = {
470
470
  },
471
471
  },
472
472
 
473
- centrifugo: {
474
- monitor: {
475
- title: 'Centrifugo-monitor',
476
- description: 'Realtids WebSocket-overvågning og fejlfinding',
477
- openMonitor: 'Åbn Centrifugo-monitor',
478
- openDebugPanel: 'Åbn Centrifugo-fejlfindingspanel',
479
- widgetTitle: 'WebSocket-monitor',
480
- },
481
- tabs: {
482
- connection: 'Forbindelse',
483
- messages: 'Beskeder',
484
- subscriptions: 'Abonnementer',
485
- },
486
- subscriptionsList: {
487
- title: 'Aktive abonnementer',
488
- notConnected: 'Ikke forbundet til Centrifugo',
489
- noActiveSubscriptions: 'Ingen aktive abonnementer',
490
- },
491
- status: {
492
- connected: 'Forbundet',
493
- disconnected: 'Afbrudt',
494
- connecting: 'Forbinder...',
495
- connect: 'Forbind',
496
- uptime: 'Oppetid:',
497
- subscriptions: 'Abonnementer:',
498
- realtimeUnavailable: 'Realtidsfunktioner utilgængelige',
499
- },
500
- feed: {
501
- title: 'Beskedstrøm',
502
- noMessages: 'Ingen beskeder endnu',
503
- pausedClickToResume: 'Sat på pause - Klik på afspil for at fortsætte',
504
- autoScrollOn: 'Automatisk scroll: TIL',
505
- autoScrollOff: 'Automatisk scroll: FRA',
506
- viewData: 'Vis data',
507
- },
508
- filters: {
509
- title: 'Filtre',
510
- active: 'Aktiv',
511
- clear: 'Ryd',
512
- searchMessages: 'Søg i beskeder...',
513
- level: 'Niveau:',
514
- type: 'Type:',
515
- source: 'Kilde',
516
- autoScrollToLatest: 'Automatisk scroll til nyeste',
517
- },
518
- debug: {
519
- title: 'Centrifugo-fejlfinding',
520
- description: 'WebSocket-forbindelsesstatus, logs og abonnementer',
521
- openDebugPanel: 'Åbn Centrifugo-fejlfindingspanel',
522
- tabConnection: 'Forbindelse',
523
- tabLogs: 'Logs',
524
- tabSubscriptions: 'Abonnementer',
525
- },
526
- },
527
-
528
473
  tools: {
529
474
  code: {
530
475
  copyCode: 'Kopier kode',
package/src/locales/de.ts CHANGED
@@ -470,61 +470,6 @@ export const de: I18nTranslations = {
470
470
  },
471
471
  },
472
472
 
473
- centrifugo: {
474
- monitor: {
475
- title: 'Centrifugo Monitor',
476
- description: 'Echtzeit-WebSocket-Überwachung und Debugging',
477
- openMonitor: 'Centrifugo Monitor öffnen',
478
- openDebugPanel: 'Centrifugo Debug-Panel öffnen',
479
- widgetTitle: 'WebSocket Monitor',
480
- },
481
- tabs: {
482
- connection: 'Verbindung',
483
- messages: 'Nachrichten',
484
- subscriptions: 'Abonnements',
485
- },
486
- subscriptionsList: {
487
- title: 'Aktive Abonnements',
488
- notConnected: 'Nicht mit Centrifugo verbunden',
489
- noActiveSubscriptions: 'Keine aktiven Abonnements',
490
- },
491
- status: {
492
- connected: 'Verbunden',
493
- disconnected: 'Getrennt',
494
- connecting: 'Verbinden...',
495
- connect: 'Verbinden',
496
- uptime: 'Betriebszeit:',
497
- subscriptions: 'Abonnements:',
498
- realtimeUnavailable: 'Echtzeit-Funktionen nicht verfügbar',
499
- },
500
- feed: {
501
- title: 'Nachrichten-Feed',
502
- noMessages: 'Noch keine Nachrichten',
503
- pausedClickToResume: 'Pausiert - Klicken Sie auf Wiedergabe zum Fortsetzen',
504
- autoScrollOn: 'Auto-Scroll: AN',
505
- autoScrollOff: 'Auto-Scroll: AUS',
506
- viewData: 'Daten anzeigen',
507
- },
508
- filters: {
509
- title: 'Filter',
510
- active: 'Aktiv',
511
- clear: 'Löschen',
512
- searchMessages: 'Nachrichten suchen...',
513
- level: 'Stufe:',
514
- type: 'Typ:',
515
- source: 'Quelle',
516
- autoScrollToLatest: 'Auto-Scroll zum Neuesten',
517
- },
518
- debug: {
519
- title: 'Centrifugo Debug',
520
- description: 'WebSocket-Verbindungsstatus, Protokolle und Abonnements',
521
- openDebugPanel: 'Centrifugo Debug-Panel öffnen',
522
- tabConnection: 'Verbindung',
523
- tabLogs: 'Protokolle',
524
- tabSubscriptions: 'Abonnements',
525
- },
526
- },
527
-
528
473
  tools: {
529
474
  code: {
530
475
  copyCode: 'Code kopieren',
package/src/locales/en.ts CHANGED
@@ -479,61 +479,6 @@ export const en: I18nTranslations = {
479
479
  },
480
480
  },
481
481
 
482
- centrifugo: {
483
- monitor: {
484
- title: 'Centrifugo Monitor',
485
- description: 'Real-time WebSocket monitoring and debugging',
486
- openMonitor: 'Open Centrifugo Monitor',
487
- openDebugPanel: 'Open Centrifugo Debug Panel',
488
- widgetTitle: 'WebSocket Monitor',
489
- },
490
- tabs: {
491
- connection: 'Connection',
492
- messages: 'Messages',
493
- subscriptions: 'Subscriptions',
494
- },
495
- subscriptionsList: {
496
- title: 'Active Subscriptions',
497
- notConnected: 'Not connected to Centrifugo',
498
- noActiveSubscriptions: 'No active subscriptions',
499
- },
500
- status: {
501
- connected: 'Connected',
502
- disconnected: 'Disconnected',
503
- connecting: 'Connecting...',
504
- connect: 'Connect',
505
- uptime: 'Uptime:',
506
- subscriptions: 'Subscriptions:',
507
- realtimeUnavailable: 'Real-time features unavailable',
508
- },
509
- feed: {
510
- title: 'Messages Feed',
511
- noMessages: 'No messages yet',
512
- pausedClickToResume: 'Paused - Click play to resume',
513
- autoScrollOn: 'Auto-scroll: ON',
514
- autoScrollOff: 'Auto-scroll: OFF',
515
- viewData: 'View data',
516
- },
517
- filters: {
518
- title: 'Filters',
519
- active: 'Active',
520
- clear: 'Clear',
521
- searchMessages: 'Search messages...',
522
- level: 'Level:',
523
- type: 'Type:',
524
- source: 'Source',
525
- autoScrollToLatest: 'Auto-scroll to latest',
526
- },
527
- debug: {
528
- title: 'Centrifugo Debug',
529
- description: 'WebSocket connection status, logs, and subscriptions',
530
- openDebugPanel: 'Open Centrifugo Debug Panel',
531
- tabConnection: 'Connection',
532
- tabLogs: 'Logs',
533
- tabSubscriptions: 'Subscriptions',
534
- },
535
- },
536
-
537
482
  tools: {
538
483
  code: {
539
484
  copyCode: 'Copy code',
package/src/locales/es.ts CHANGED
@@ -470,61 +470,6 @@ export const es: I18nTranslations = {
470
470
  },
471
471
  },
472
472
 
473
- centrifugo: {
474
- monitor: {
475
- title: 'Monitor Centrifugo',
476
- description: 'Monitoreo y depuración de WebSocket en tiempo real',
477
- openMonitor: 'Abrir Monitor Centrifugo',
478
- openDebugPanel: 'Abrir Panel de Depuración Centrifugo',
479
- widgetTitle: 'Monitor WebSocket',
480
- },
481
- tabs: {
482
- connection: 'Conexión',
483
- messages: 'Mensajes',
484
- subscriptions: 'Suscripciones',
485
- },
486
- subscriptionsList: {
487
- title: 'Suscripciones activas',
488
- notConnected: 'No conectado a Centrifugo',
489
- noActiveSubscriptions: 'No hay suscripciones activas',
490
- },
491
- status: {
492
- connected: 'Conectado',
493
- disconnected: 'Desconectado',
494
- connecting: 'Conectando...',
495
- connect: 'Conectar',
496
- uptime: 'Tiempo activo:',
497
- subscriptions: 'Suscripciones:',
498
- realtimeUnavailable: 'Funciones en tiempo real no disponibles',
499
- },
500
- feed: {
501
- title: 'Feed de mensajes',
502
- noMessages: 'No hay mensajes aún',
503
- pausedClickToResume: 'Pausado - Haz clic en reproducir para continuar',
504
- autoScrollOn: 'Auto-scroll: ACTIVADO',
505
- autoScrollOff: 'Auto-scroll: DESACTIVADO',
506
- viewData: 'Ver datos',
507
- },
508
- filters: {
509
- title: 'Filtros',
510
- active: 'Activo',
511
- clear: 'Limpiar',
512
- searchMessages: 'Buscar mensajes...',
513
- level: 'Nivel:',
514
- type: 'Tipo:',
515
- source: 'Fuente',
516
- autoScrollToLatest: 'Auto-scroll a lo más reciente',
517
- },
518
- debug: {
519
- title: 'Depuración Centrifugo',
520
- description: 'Estado de conexión WebSocket, logs y suscripciones',
521
- openDebugPanel: 'Abrir Panel de Depuración Centrifugo',
522
- tabConnection: 'Conexión',
523
- tabLogs: 'Logs',
524
- tabSubscriptions: 'Suscripciones',
525
- },
526
- },
527
-
528
473
  tools: {
529
474
  code: {
530
475
  copyCode: 'Copiar código',