@abdess76/i18nkit 1.0.3 → 1.0.5

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/CHANGELOG.md CHANGED
@@ -6,6 +6,24 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to
7
7
  [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
8
8
 
9
+ ## [1.0.4] - 05-01-2026
10
+
11
+ ### Added
12
+
13
+ - Backup system with session-linked reports
14
+ - Automatic report archival in backup sessions
15
+ - Restore/rollback capabilities from backup sessions
16
+ - Auto-cleanup of old backup sessions
17
+ - Gitignore auto-management for `.i18nkit` directory
18
+ - Manifest schema with `reportFile` field for audit trail
19
+
20
+ ### Changed
21
+
22
+ - Backup directory renamed from `.i18n` to `.i18nkit`
23
+ - Report files now stored alongside backup sessions
24
+ - Improved session status tracking (pending, ready, in_progress, completed,
25
+ failed)
26
+
9
27
  ## [1.0.3] - 05-01-2026
10
28
 
11
29
  ### Changed
package/LICENSE CHANGED
@@ -1,21 +1,21 @@
1
- MIT License
2
-
3
- Copyright (c) 2025 Abdessamad DERRAZ
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Abdessamad DERRAZ
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -50,7 +50,7 @@ npm install --save-dev @abdess76/i18nkit
50
50
  "i18n:apply": "i18nkit --auto-apply --init-langs en,fr",
51
51
  "i18n:check": "i18nkit --check-sync --strict",
52
52
  "i18n:orphans": "i18nkit --find-orphans --strict",
53
- "i18n:translate": "i18nkit --translate fr:en",
53
+ "i18n:translate": "i18nkit --translate en:fr",
54
54
  "i18n:watch": "i18nkit --watch",
55
55
  "i18n:ci": "npm run i18n:check && npm run i18n:orphans"
56
56
  }
@@ -88,7 +88,7 @@ npm run i18n:ci
88
88
  <input [placeholder]="'home.forms.enter_your_name' | transloco" />
89
89
  ```
90
90
 
91
- **Generated `fr.json`:**
91
+ **Generated `en.json`:**
92
92
 
93
93
  ```json
94
94
  {
@@ -102,13 +102,26 @@ npm run i18n:ci
102
102
 
103
103
  ## Commands
104
104
 
105
+ ### Core Commands
106
+
107
+ | Command | Description |
108
+ | -------------- | ---------------------------------------- |
109
+ | `extract` | Extract i18n strings from source files |
110
+ | `check-sync` | Compare language files for missing keys |
111
+ | `find-orphans` | Find translation keys not used in source |
112
+ | `translate` | Translate a language file via API |
113
+ | `watch` | Watch for file changes and re-run |
114
+ | `backup` | Manage backup sessions and restore files |
115
+
116
+ ### NPM Scripts Examples
117
+
105
118
  | Script | Command | Description |
106
119
  | ------------------------ | --------------------------- | ---------------------------- |
107
120
  | `npm run i18n` | `--lang fr --merge` | Extract, merge with existing |
108
121
  | `npm run i18n:apply` | `--auto-apply --init-langs` | Extract + replace + create |
109
122
  | `npm run i18n:check` | `--check-sync --strict` | Validate files are in sync |
110
123
  | `npm run i18n:orphans` | `--find-orphans --strict` | Find unused keys |
111
- | `npm run i18n:translate` | `--translate fr:en` | Translate via API |
124
+ | `npm run i18n:translate` | `--translate en:fr` | Translate via API |
112
125
  | `npm run i18n:watch` | `--watch` | Re-run on file changes |
113
126
 
114
127
  ## Configuration
@@ -188,6 +201,105 @@ jobs:
188
201
  - run: npm run i18n:ci
189
202
  ```
190
203
 
204
+ ## Backup & Restore
205
+
206
+ Source files are backed up before modification. Full session management with
207
+ restore, cleanup, and audit capabilities.
208
+
209
+ ### Automatic Backup
210
+
211
+ ```bash
212
+ # Backup enabled by default with --auto-apply
213
+ npm run i18n:apply
214
+
215
+ # Disable backup if needed
216
+ npx i18nkit --auto-apply --no-backup
217
+
218
+ # Initialize backup structure explicitly
219
+ npx i18nkit --init-backups --auto-gitignore
220
+ ```
221
+
222
+ ### Session Structure
223
+
224
+ ```text
225
+ .i18nkit/
226
+ ├── .gitignore # Auto-managed (excludes backups/, report.json)
227
+ ├── report.json # Current extraction report
228
+ └── backups/
229
+ └── 2026-01-05_15-39-28_apply_ae9c/
230
+ ├── manifest.json # Session metadata with status tracking
231
+ ├── report.json # Archived extraction report
232
+ └── src/ # Original source files
233
+ ```
234
+
235
+ ### Session Management
236
+
237
+ ```bash
238
+ # List all backup sessions
239
+ npx i18nkit --list-backups
240
+
241
+ # Show detailed info for a session
242
+ npx i18nkit --backup-info 2026-01-05_15-39-28_apply_ae9c
243
+ ```
244
+
245
+ ### Restore from Backup
246
+
247
+ ```bash
248
+ # Restore specific session
249
+ npx i18nkit --restore 2026-01-05_15-39-28_apply_ae9c
250
+
251
+ # Restore latest session
252
+ npx i18nkit --restore-latest
253
+
254
+ # Preview restore without applying
255
+ npx i18nkit --restore-latest --dry-run
256
+ ```
257
+
258
+ ### Manual Cleanup
259
+
260
+ ```bash
261
+ # Clean old sessions (interactive)
262
+ npx i18nkit --cleanup-backups
263
+
264
+ # Keep only last 5 sessions
265
+ npx i18nkit --cleanup-backups --keep 5
266
+
267
+ # Remove sessions older than 7 days
268
+ npx i18nkit --cleanup-backups --max-age 7
269
+
270
+ # Combine options
271
+ npx i18nkit --cleanup-backups --keep 3 --max-age 14
272
+ ```
273
+
274
+ ### Auto-Cleanup Configuration
275
+
276
+ Old sessions are automatically cleaned up based on configuration.
277
+
278
+ ```javascript
279
+ // i18nkit.config.js
280
+ module.exports = {
281
+ backup: {
282
+ enabled: true,
283
+ maxSessions: 10, // Keep max 10 sessions
284
+ retentionDays: 30, // Delete sessions older than 30 days
285
+ autoCleanup: true, // Auto-cleanup on each run
286
+ },
287
+ };
288
+ ```
289
+
290
+ ### Session Status
291
+
292
+ Sessions track their lifecycle status:
293
+
294
+ | Status | Description |
295
+ | ------------- | -------------------------------- |
296
+ | `pending` | Session created, not started |
297
+ | `backing_up` | Backup in progress |
298
+ | `in_progress` | Apply operation running |
299
+ | `completed` | Successfully finished |
300
+ | `failed` | Error occurred (check manifest) |
301
+ | `restored` | Files restored from this session |
302
+
191
303
  ## Key Mapping
192
304
 
193
305
  Override auto-generated keys with `.i18n-keys.json`:
@@ -251,23 +363,70 @@ implementations.
251
363
 
252
364
  ## Options
253
365
 
366
+ ### Global Options
367
+
254
368
  ```text
369
+ --config <path> Path to config file (default: i18nkit.config.js)
255
370
  --src <path> Source directory (default: src/app)
256
- --i18n-dir <path> i18n directory (default: src/assets/i18n)
257
- --lang <code> Language code
258
- --format <type> nested | flat (default: nested)
371
+ --locales <path> Locales directory (default: src/assets/i18n)
372
+ --default-lang <code> Default language (default: fr)
373
+ --dry-run Preview changes without writing
374
+ --verbose Detailed output
375
+ --help, -h Show help
376
+ --version, -v Show version
377
+ ```
378
+
379
+ ### Extraction Options
380
+
381
+ ```text
382
+ --lang <code> Language code for output file
383
+ --format <type> Output format: nested | flat (default: nested)
259
384
  --merge Merge with existing translations
260
- --auto-apply Extract and apply pipes
385
+ --auto-apply Extract AND apply pipes in one command
261
386
  --init-langs <codes> Create language files (e.g., en,fr,es)
262
- --check-sync Compare language files
263
- --find-orphans Find unused keys
264
- --translate <src:tgt> Translate via API (e.g., fr:en)
265
- --deepl Use DeepL API
266
- --watch Watch mode
267
- --dry-run Preview only
268
- --strict Exit 1 on issues
387
+ --include-translated Include already translated strings
388
+ --extract-ts-objects Extract from TypeScript object literals
389
+ --skip-translated Skip already translated strings (default: true)
390
+ ```
391
+
392
+ ### Validation Options
393
+
394
+ ```text
395
+ --check-sync Compare language files for missing keys
396
+ --find-orphans Find unused translation keys
397
+ --strict Exit with error code 1 on issues
269
398
  --ci CI mode (strict + json output)
270
- --verbose Detailed output
399
+ ```
400
+
401
+ ### Translation Options
402
+
403
+ ```text
404
+ --translate <src:tgt> Translate via API (e.g., fr:en)
405
+ --deepl Use DeepL API (requires DEEPL_API_KEY)
406
+ --mymemory Use MyMemory API (default)
407
+ --email <email> Email for MyMemory rate limits
408
+ ```
409
+
410
+ ### Backup Options
411
+
412
+ ```text
413
+ --backup Enable backup before modifications (default)
414
+ --no-backup Disable backup
415
+ --list-backups List all backup sessions
416
+ --backup-info <id> Show backup session details
417
+ --restore [id] Restore from backup (latest if no id)
418
+ --restore-latest Restore from most recent backup
419
+ --cleanup-backups Remove old backup sessions
420
+ --keep <n> Keep last n sessions (default: 10)
421
+ --max-age <days> Max session age in days (default: 30)
422
+ --init-backups Initialize backup directory structure
423
+ --auto-gitignore Auto-add .i18nkit to .gitignore
424
+ ```
425
+
426
+ ### Watch Options
427
+
428
+ ```text
429
+ --watch Watch mode for file changes
271
430
  ```
272
431
 
273
432
  ## Exit Codes
@@ -0,0 +1,226 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * @fileoverview CLI commands for backup management.
5
+ * @module commands/backup
6
+ */
7
+
8
+ const backup = require('../core/backup');
9
+
10
+ function formatDate(isoString) {
11
+ return new Date(isoString).toLocaleString();
12
+ }
13
+
14
+ function getSessionAgeString(session) {
15
+ const preview = backup.getCleanupPreview(process.cwd());
16
+ const found = preview.toKeep.find(s => s.id === session.id);
17
+ const hasAge = found?.age !== undefined;
18
+ return hasAge ? `${found.age}d` : '';
19
+ }
20
+
21
+ function formatSessionRow(session) {
22
+ const status = session.status.padEnd(12);
23
+ const files = String(session.fileCount).padStart(3);
24
+ const age = getSessionAgeString(session).padStart(4);
25
+ return `${session.id} | ${status} | ${files} | ${age} | ${session.command || ''}`;
26
+ }
27
+
28
+ function logListHeader(log) {
29
+ log('\nBackup Sessions:');
30
+ log('-'.repeat(100));
31
+ log('ID | Status | Files | Age | Command');
32
+ log('-'.repeat(100));
33
+ }
34
+
35
+ function logListFooter(sessions, log) {
36
+ log('-'.repeat(100));
37
+ log(`Total: ${sessions.length} sessions\n`);
38
+ const latestId = backup.Session.getLatestId(process.cwd());
39
+ if (latestId) {
40
+ log(`Latest: ${latestId}`);
41
+ }
42
+ }
43
+
44
+ function listBackups(ctx) {
45
+ const { cwd, log } = ctx;
46
+ const sessions = backup.listBackupSessions(cwd);
47
+ if (sessions.length === 0) {
48
+ log('No backup sessions found');
49
+ return;
50
+ }
51
+ logListHeader(log);
52
+ sessions.forEach(session => log(formatSessionRow(session)));
53
+ logListFooter(sessions, log);
54
+ }
55
+
56
+ function logInfoHeader(manifest, log) {
57
+ log('\nSession Details:');
58
+ log('-'.repeat(60));
59
+ log(`ID: ${manifest.id}`);
60
+ log(`Status: ${manifest.status}`);
61
+ log(`Command: ${manifest.command}`);
62
+ log(`Timestamp: ${formatDate(manifest.timestamp)}`);
63
+ log(`Files: ${manifest.files.length}`);
64
+ log('-'.repeat(60));
65
+ }
66
+
67
+ function logInfoFiles(files, log) {
68
+ if (files.length > 0) {
69
+ log('\nBacked Up Files:');
70
+ files.forEach(file => log(` ${file.original} (${file.size} bytes)`));
71
+ }
72
+ }
73
+
74
+ function showBackupInfo(ctx) {
75
+ const { cwd, sessionId, log } = ctx;
76
+ const session = backup.getBackupSession(cwd, sessionId);
77
+ if (!session) {
78
+ log(`Session not found: ${sessionId}`);
79
+ return;
80
+ }
81
+ const { manifest } = session;
82
+ logInfoHeader(manifest, log);
83
+ logInfoFiles(manifest.files, log);
84
+ if (manifest.error) {
85
+ log(`\nError: ${manifest.error.message}`);
86
+ }
87
+ }
88
+
89
+ function logRestoreResult(result, log) {
90
+ log(`\nRestored: ${result.restored} files`);
91
+ if (result.skipped > 0) {
92
+ log(`Skipped: ${result.skipped} files`);
93
+ }
94
+ }
95
+
96
+ function restoreBackup(ctx) {
97
+ const { cwd, sessionId, dryRun, verbose, log } = ctx;
98
+ const targetId = sessionId || backup.Session.getLatestId(cwd);
99
+ if (!targetId) {
100
+ log('No backup sessions found');
101
+ return;
102
+ }
103
+ const prefix = dryRun ? '[DRY RUN] Would restore' : 'Restoring';
104
+ log(`\n${prefix} from: ${targetId}`);
105
+ const result = backup.restoreSession(cwd, targetId, { dryRun, verbose, log });
106
+ logRestoreResult(result, log);
107
+ }
108
+
109
+ function logCleanupPreview(preview, log) {
110
+ log('\nCleanup Preview:');
111
+ log(`Would keep: ${preview.toKeep.length} sessions`);
112
+ log(`Would delete: ${preview.toDelete.length} sessions`);
113
+ if (preview.toDelete.length > 0) {
114
+ log('\nSessions to delete:');
115
+ preview.toDelete.forEach(s => log(` ${s.id} (${s.age} days old, ${s.fileCount} files)`));
116
+ }
117
+ }
118
+
119
+ function logCleanupResult(result, log) {
120
+ log(`\nDeleted: ${result.deleted} sessions`);
121
+ log(`Kept: ${result.kept} sessions`);
122
+ }
123
+
124
+ function cleanupBackups(ctx) {
125
+ const { cwd, maxSessions = 10, maxAgeDays = 30, dryRun, verbose, log } = ctx;
126
+ if (dryRun) {
127
+ const preview = backup.getCleanupPreview(cwd, { maxSessions, maxAgeDays });
128
+ logCleanupPreview(preview, log);
129
+ return;
130
+ }
131
+ const result = backup.cleanupOldSessions(cwd, { maxSessions, maxAgeDays, verbose, log });
132
+ logCleanupResult(result, log);
133
+ }
134
+
135
+ function initBackupStructure(ctx) {
136
+ const { cwd, autoAddGitignore = false, verbose, log } = ctx;
137
+ const result = backup.initializeBackupStructure(cwd, { autoAddGitignore, verbose, log });
138
+ log('\nBackup structure initialized');
139
+ if (result.suggestion) {
140
+ log(`\nTip: ${result.suggestion.message}`);
141
+ }
142
+ }
143
+
144
+ function parseSessionIdArg(args, flagIndex) {
145
+ const nextArg = args[flagIndex + 1];
146
+ const isValidArg = nextArg && !nextArg.startsWith('--');
147
+ return isValidArg ? nextArg : null;
148
+ }
149
+
150
+ function parseIntArg(args, flag, defaultValue) {
151
+ const idx = args.indexOf(flag);
152
+ const hasFlag = idx !== -1;
153
+ return hasFlag ? parseInt(args[idx + 1], 10) : defaultValue;
154
+ }
155
+
156
+ const HANDLERS = {
157
+ '--list-backups': (_args, ctx) => listBackups(ctx),
158
+ '--backup-info': (args, ctx) => {
159
+ const sessionId = parseSessionIdArg(args, args.indexOf('--backup-info'));
160
+ if (!sessionId) {
161
+ ctx.log('Usage: i18nkit --backup-info <session-id>');
162
+ return undefined;
163
+ }
164
+ return showBackupInfo({ ...ctx, sessionId });
165
+ },
166
+ '--restore': (args, ctx) => {
167
+ const sessionId = parseSessionIdArg(args, args.indexOf('--restore'));
168
+ return restoreBackup({ ...ctx, sessionId });
169
+ },
170
+ '--cleanup-backups': (args, ctx) => {
171
+ const maxSessions = parseIntArg(args, '--keep', 10);
172
+ const maxAgeDays = parseIntArg(args, '--max-age', 30);
173
+ return cleanupBackups({ ...ctx, maxSessions, maxAgeDays });
174
+ },
175
+ '--init-backups': (args, ctx) => {
176
+ const autoAddGitignore = args.includes('--auto-gitignore');
177
+ return initBackupStructure({ ...ctx, autoAddGitignore });
178
+ },
179
+ };
180
+
181
+ function handleBackupCommand(args, ctx) {
182
+ const cwd = ctx.cwd || process.cwd();
183
+ const log = ctx.log || console.log;
184
+ const baseCtx = {
185
+ cwd,
186
+ log,
187
+ dryRun: args.includes('--dry-run'),
188
+ verbose: args.includes('--verbose'),
189
+ args,
190
+ };
191
+ for (const [flag, handler] of Object.entries(HANDLERS)) {
192
+ if (args.includes(flag)) {
193
+ return handler(args, baseCtx);
194
+ }
195
+ }
196
+ return undefined;
197
+ }
198
+
199
+ function isBackupCommand(args) {
200
+ return Object.keys(HANDLERS).some(cmd => args.includes(cmd));
201
+ }
202
+
203
+ module.exports = {
204
+ name: 'backup',
205
+ aliases: ['--list-backups', '--restore', '--cleanup-backups', '--backup-info', '--init-backups'],
206
+ category: 'maintenance',
207
+ description: 'Manage backup sessions and restore files',
208
+ options: [
209
+ { flag: '--list-backups', description: 'List all backup sessions' },
210
+ { flag: '--backup-info <id>', description: 'Show backup session details' },
211
+ { flag: '--restore [id]', description: 'Restore from backup (latest if no id)' },
212
+ { flag: '--cleanup-backups', description: 'Remove old backup sessions' },
213
+ { flag: '--keep <n>', description: 'Keep last n sessions (default: 10)' },
214
+ { flag: '--max-age <days>', description: 'Max age in days (default: 30)' },
215
+ { flag: '--init-backups', description: 'Initialize backup structure' },
216
+ { flag: '--auto-gitignore', description: 'Auto-add to .gitignore' },
217
+ ],
218
+ examples: [
219
+ 'i18nkit --list-backups',
220
+ 'i18nkit --restore',
221
+ 'i18nkit --cleanup-backups --keep 5 --dry-run',
222
+ ],
223
+ run: ctx => handleBackupCommand(ctx.args, ctx),
224
+ isBackupCommand,
225
+ handleBackupCommand,
226
+ };
@@ -109,8 +109,8 @@ async function loadMergeResult(ctx) {
109
109
  }
110
110
 
111
111
  function getAutoApplyOpts(ctx) {
112
- const { srcDir, backupDir, adapter, backup, dryRun, verbose, interactive, log } = ctx;
113
- return { srcDir, backupDir, adapter, backup, dryRun, verbose, interactive, log };
112
+ const { srcDir, backupDir, reportDir, adapter, backup, dryRun, verbose, interactive, log } = ctx;
113
+ return { srcDir, backupDir, reportDir, adapter, backup, dryRun, verbose, interactive, log };
114
114
  }
115
115
 
116
116
  async function runAutoApply(findings, ctx) {
@@ -2,7 +2,7 @@
2
2
 
3
3
  const fs = require('./fs-adapter');
4
4
  const readline = require('readline');
5
- const { createBackup, getBackupFiles } = require('./backup');
5
+ const { getBackupFiles } = require('./backup');
6
6
 
7
7
  const isValidFinding = f => f.file && f.text && f.key && !f.context?.startsWith('ts_');
8
8
 
@@ -68,12 +68,12 @@ function applyReplacementsToContent(content, fileFindings, adapter) {
68
68
  let result = content;
69
69
  let count = 0;
70
70
  for (const finding of fileFindings) {
71
- const transformed = adapter.transform(
72
- result,
73
- finding.rawText || finding.text,
74
- finding.key,
75
- finding.context,
76
- );
71
+ const transformed = adapter.transform({
72
+ content: result,
73
+ rawText: finding.rawText || finding.text,
74
+ key: finding.key,
75
+ context: finding.context,
76
+ });
77
77
  result = transformed.content;
78
78
  count += transformed.replacements;
79
79
  }
@@ -81,8 +81,7 @@ function applyReplacementsToContent(content, fileFindings, adapter) {
81
81
  }
82
82
 
83
83
  function tryUpdateTsImports(tsFile, opts) {
84
- const { backupDir, adapter, backup = true, dryRun = false } = opts;
85
- createBackup(tsFile, backupDir, { enabled: backup, dryRun });
84
+ const { adapter, dryRun = false } = opts;
86
85
  const tsContent = fs.readFileSync(tsFile, 'utf-8');
87
86
  const updatedTs = adapter.updateImports(tsContent);
88
87
  if (updatedTs === tsContent) {