@jjlmoya/utils-games-development 1.51.0 → 1.53.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.
Files changed (61) hide show
  1. package/package.json +1 -1
  2. package/src/category/index.ts +2 -1
  3. package/src/entries.ts +7 -2
  4. package/src/tests/locale_completeness.test.ts +2 -2
  5. package/src/tests/tool_validation.test.ts +2 -2
  6. package/src/tool/localizationSanitizer/app-client.ts +270 -0
  7. package/src/tool/localizationSanitizer/bibliography.astro +6 -0
  8. package/src/tool/localizationSanitizer/bibliography.ts +14 -0
  9. package/src/tool/localizationSanitizer/component.astro +116 -0
  10. package/src/tool/localizationSanitizer/csv-parser.ts +95 -0
  11. package/src/tool/localizationSanitizer/csv-sanitizer.ts +98 -0
  12. package/src/tool/localizationSanitizer/entry.ts +28 -0
  13. package/src/tool/localizationSanitizer/i18n/de.ts +176 -0
  14. package/src/tool/localizationSanitizer/i18n/en.ts +176 -0
  15. package/src/tool/localizationSanitizer/i18n/es.ts +176 -0
  16. package/src/tool/localizationSanitizer/i18n/fr.ts +176 -0
  17. package/src/tool/localizationSanitizer/i18n/id.ts +176 -0
  18. package/src/tool/localizationSanitizer/i18n/it.ts +176 -0
  19. package/src/tool/localizationSanitizer/i18n/ja.ts +176 -0
  20. package/src/tool/localizationSanitizer/i18n/ko.ts +176 -0
  21. package/src/tool/localizationSanitizer/i18n/nl.ts +176 -0
  22. package/src/tool/localizationSanitizer/i18n/pl.ts +176 -0
  23. package/src/tool/localizationSanitizer/i18n/pt.ts +176 -0
  24. package/src/tool/localizationSanitizer/i18n/ru.ts +176 -0
  25. package/src/tool/localizationSanitizer/i18n/sv.ts +176 -0
  26. package/src/tool/localizationSanitizer/i18n/tr.ts +176 -0
  27. package/src/tool/localizationSanitizer/i18n/zh.ts +176 -0
  28. package/src/tool/localizationSanitizer/index.ts +12 -0
  29. package/src/tool/localizationSanitizer/json-sanitizer.ts +118 -0
  30. package/src/tool/localizationSanitizer/localization-sanitizer.css +634 -0
  31. package/src/tool/localizationSanitizer/logic.test.ts +38 -0
  32. package/src/tool/localizationSanitizer/logic.ts +23 -0
  33. package/src/tool/localizationSanitizer/seo.astro +15 -0
  34. package/src/tool/localizationSanitizer/types.ts +55 -0
  35. package/src/tool/localizationSanitizer/ui.ts +39 -0
  36. package/src/tool/saveFileEditor/bibliography.astro +6 -0
  37. package/src/tool/saveFileEditor/bibliography.ts +12 -0
  38. package/src/tool/saveFileEditor/component.astro +351 -0
  39. package/src/tool/saveFileEditor/entry.ts +28 -0
  40. package/src/tool/saveFileEditor/game-save-file-editor.css +250 -0
  41. package/src/tool/saveFileEditor/i18n/de.ts +182 -0
  42. package/src/tool/saveFileEditor/i18n/en.ts +182 -0
  43. package/src/tool/saveFileEditor/i18n/es.ts +182 -0
  44. package/src/tool/saveFileEditor/i18n/fr.ts +182 -0
  45. package/src/tool/saveFileEditor/i18n/id.ts +182 -0
  46. package/src/tool/saveFileEditor/i18n/it.ts +182 -0
  47. package/src/tool/saveFileEditor/i18n/ja.ts +182 -0
  48. package/src/tool/saveFileEditor/i18n/ko.ts +182 -0
  49. package/src/tool/saveFileEditor/i18n/nl.ts +182 -0
  50. package/src/tool/saveFileEditor/i18n/pl.ts +182 -0
  51. package/src/tool/saveFileEditor/i18n/pt.ts +182 -0
  52. package/src/tool/saveFileEditor/i18n/ru.ts +182 -0
  53. package/src/tool/saveFileEditor/i18n/sv.ts +182 -0
  54. package/src/tool/saveFileEditor/i18n/tr.ts +182 -0
  55. package/src/tool/saveFileEditor/i18n/zh.ts +182 -0
  56. package/src/tool/saveFileEditor/index.ts +11 -0
  57. package/src/tool/saveFileEditor/logic.test.ts +69 -0
  58. package/src/tool/saveFileEditor/logic.ts +139 -0
  59. package/src/tool/saveFileEditor/seo.astro +15 -0
  60. package/src/tool/saveFileEditor/ui.ts +24 -0
  61. package/src/tools.ts +4 -1
@@ -0,0 +1,634 @@
1
+ .ls-root {
2
+ --ls-ink: #17202f;
3
+ --ls-muted: #687487;
4
+ --ls-line: #dce2ea;
5
+ --ls-panel: #fff;
6
+ --ls-panel-soft: #f4f7fa;
7
+ --ls-accent: #e56a47;
8
+ --ls-accent-dark: #bc4d30;
9
+ --ls-cyan: #19a6a0;
10
+ --ls-yellow: #e7a633;
11
+ --ls-red: #ce4f61;
12
+
13
+ width: 100%;
14
+ max-width: 1220px;
15
+ margin: 0 auto;
16
+ color: var(--ls-ink);
17
+ }
18
+
19
+ .theme-dark .ls-root {
20
+ --ls-ink: #f1f5f9;
21
+ --ls-muted: #a3afc0;
22
+ --ls-line: #273244;
23
+ --ls-panel: #111926;
24
+ --ls-panel-soft: #172232;
25
+ --ls-accent: #f1845d;
26
+ --ls-accent-dark: #da6744;
27
+ --ls-cyan: #42c3b8;
28
+ --ls-yellow: #efbd57;
29
+ --ls-red: #f07889;
30
+ }
31
+
32
+ .ls-root * {
33
+ box-sizing: border-box;
34
+ }
35
+
36
+ .ls-root button,
37
+ .ls-root input {
38
+ font: inherit;
39
+ }
40
+
41
+ .ls-topline,
42
+ .ls-format-bar,
43
+ .ls-drop-panel,
44
+ .ls-workbench,
45
+ .ls-export-card {
46
+ border: 1px solid var(--ls-line);
47
+ background: var(--ls-panel);
48
+ }
49
+
50
+ .ls-topline {
51
+ border-bottom: 0;
52
+ border-radius: 18px 18px 0 0;
53
+ display: flex;
54
+ align-items: center;
55
+ justify-content: space-between;
56
+ gap: 1rem;
57
+ padding: 1rem 1.25rem;
58
+ }
59
+
60
+ .ls-topline > div {
61
+ display: flex;
62
+ align-items: center;
63
+ gap: 0.85rem;
64
+ min-width: 0;
65
+ }
66
+
67
+ .ls-topline strong {
68
+ overflow: hidden;
69
+ text-overflow: ellipsis;
70
+ white-space: nowrap;
71
+ }
72
+
73
+ .ls-kicker,
74
+ .ls-section-eyebrow {
75
+ color: var(--ls-accent);
76
+ font-size: 0.68rem;
77
+ font-weight: 800;
78
+ letter-spacing: 0.14em;
79
+ }
80
+
81
+ .ls-privacy-note,
82
+ .ls-file-type-note,
83
+ .ls-section-heading p,
84
+ .ls-export-copy p,
85
+ .ls-drop-copy span {
86
+ color: var(--ls-muted);
87
+ font-size: 0.78rem;
88
+ }
89
+
90
+ .ls-privacy-note {
91
+ text-align: right;
92
+ }
93
+
94
+ .ls-format-bar {
95
+ display: flex;
96
+ align-items: center;
97
+ gap: 0.35rem;
98
+ padding: 0.55rem 1.25rem;
99
+ background: var(--ls-panel-soft);
100
+ }
101
+
102
+ .ls-format-label {
103
+ color: var(--ls-muted);
104
+ font-size: 0.72rem;
105
+ font-weight: 700;
106
+ margin-right: auto;
107
+ text-transform: uppercase;
108
+ letter-spacing: 0.08em;
109
+ }
110
+
111
+ .ls-format-button,
112
+ .ls-button {
113
+ border: 0;
114
+ cursor: pointer;
115
+ transition: transform 160ms ease, background-color 160ms ease, border-color 160ms ease, color 160ms ease;
116
+ }
117
+
118
+ .ls-format-button {
119
+ border: 1px solid transparent;
120
+ border-radius: 7px;
121
+ background: transparent;
122
+ color: var(--ls-muted);
123
+ font-size: 0.75rem;
124
+ font-weight: 800;
125
+ padding: 0.38rem 0.8rem;
126
+ }
127
+
128
+ .ls-format-button:hover,
129
+ .ls-format-button.is-active {
130
+ background: var(--ls-panel);
131
+ border-color: var(--ls-line);
132
+ color: var(--ls-accent-dark);
133
+ }
134
+
135
+ .ls-drop-panel {
136
+ align-items: center;
137
+ border-bottom: 0;
138
+ display: grid;
139
+ gap: 1rem;
140
+ grid-template-columns: auto 1fr auto;
141
+ padding: 1.2rem 1.25rem;
142
+ }
143
+
144
+ .ls-drop-panel:focus-visible,
145
+ .ls-button:focus-visible,
146
+ .ls-format-button:focus-visible {
147
+ outline: 3px solid color-mix(in srgb, var(--ls-cyan) 45%, transparent);
148
+ outline-offset: 2px;
149
+ }
150
+
151
+ .ls-drop-panel.is-dragging {
152
+ background: color-mix(in srgb, var(--ls-cyan) 12%, var(--ls-panel));
153
+ border-color: var(--ls-cyan);
154
+ }
155
+
156
+ .ls-drop-mark {
157
+ display: grid;
158
+ gap: 0.22rem;
159
+ transform: rotate(-6deg);
160
+ }
161
+
162
+ .ls-drop-mark span {
163
+ border: 1px solid var(--ls-accent);
164
+ color: var(--ls-accent-dark);
165
+ font-size: 0.58rem;
166
+ font-weight: 900;
167
+ letter-spacing: 0.08em;
168
+ padding: 0.22rem 0.36rem;
169
+ }
170
+
171
+ .ls-drop-mark span:last-child {
172
+ background: var(--ls-accent);
173
+ color: var(--ls-panel);
174
+ transform: translateX(0.4rem);
175
+ }
176
+
177
+ .ls-drop-copy {
178
+ display: grid;
179
+ gap: 0.22rem;
180
+ }
181
+
182
+ .ls-drop-copy strong {
183
+ font-size: 1rem;
184
+ }
185
+
186
+ .ls-drop-copy span {
187
+ display: block;
188
+ }
189
+
190
+ .ls-file-type-note {
191
+ color: var(--ls-cyan);
192
+ font-size: 0.69rem;
193
+ font-weight: 700;
194
+ }
195
+
196
+ .ls-drop-actions,
197
+ .ls-export-actions {
198
+ display: flex;
199
+ flex-wrap: wrap;
200
+ gap: 0.5rem;
201
+ }
202
+
203
+ .ls-button {
204
+ border-radius: 8px;
205
+ font-size: 0.76rem;
206
+ font-weight: 800;
207
+ padding: 0.68rem 0.9rem;
208
+ white-space: nowrap;
209
+ }
210
+
211
+ .ls-button:hover:not(:disabled) {
212
+ transform: translateY(-1px);
213
+ }
214
+
215
+ .ls-button:disabled {
216
+ cursor: not-allowed;
217
+ opacity: 0.42;
218
+ }
219
+
220
+ .ls-button-primary {
221
+ background: var(--ls-accent);
222
+ color: var(--ls-panel);
223
+ }
224
+
225
+ .ls-button-primary:hover:not(:disabled) {
226
+ background: var(--ls-accent-dark);
227
+ }
228
+
229
+ .ls-button-quiet,
230
+ .ls-button-outline {
231
+ background: transparent;
232
+ border: 1px solid var(--ls-line);
233
+ color: var(--ls-ink);
234
+ }
235
+
236
+ .ls-button-quiet:hover,
237
+ .ls-button-outline:hover:not(:disabled) {
238
+ border-color: var(--ls-accent);
239
+ color: var(--ls-accent-dark);
240
+ }
241
+
242
+ .ls-workbench {
243
+ display: grid;
244
+ grid-template-columns: 0.8fr 1.6fr;
245
+ }
246
+
247
+ .ls-inspector,
248
+ .ls-preview-panel {
249
+ min-width: 0;
250
+ padding: 1.35rem;
251
+ }
252
+
253
+ .ls-inspector {
254
+ background: var(--ls-panel-soft);
255
+ border-right: 1px solid var(--ls-line);
256
+ }
257
+
258
+ .ls-section-heading {
259
+ align-items: flex-start;
260
+ display: flex;
261
+ gap: 0.7rem;
262
+ margin-bottom: 1rem;
263
+ }
264
+
265
+ .ls-section-heading h2,
266
+ .ls-export-copy h2 {
267
+ font-size: 1rem;
268
+ line-height: 1.2;
269
+ margin: 0.18rem 0 0;
270
+ }
271
+
272
+ .ls-section-heading p,
273
+ .ls-export-copy p {
274
+ line-height: 1.5;
275
+ margin: 0.4rem 0 0;
276
+ }
277
+
278
+ .ls-section-index {
279
+ color: var(--ls-accent);
280
+ font-size: 0.72rem;
281
+ font-weight: 900;
282
+ padding-top: 0.16rem;
283
+ }
284
+
285
+ .ls-health-card {
286
+ align-items: center;
287
+ background: var(--ls-panel);
288
+ border: 1px solid var(--ls-line);
289
+ border-radius: 12px;
290
+ display: flex;
291
+ gap: 0.85rem;
292
+ padding: 0.85rem;
293
+ }
294
+
295
+ .ls-health-ring {
296
+ align-items: center;
297
+ background: conic-gradient(var(--ls-cyan) var(--health-angle, 0deg), var(--ls-line) 0deg);
298
+ border-radius: 50%;
299
+ display: flex;
300
+ height: 62px;
301
+ justify-content: center;
302
+ position: relative;
303
+ width: 62px;
304
+ }
305
+
306
+ .ls-health-ring::after {
307
+ background: var(--ls-panel);
308
+ border-radius: 50%;
309
+ content: '';
310
+ inset: 6px;
311
+ position: absolute;
312
+ }
313
+
314
+ .ls-health-ring span {
315
+ font-size: 0.95rem;
316
+ font-weight: 900;
317
+ position: relative;
318
+ z-index: 1;
319
+ }
320
+
321
+ .ls-health-card strong,
322
+ .ls-health-card span:last-child {
323
+ display: block;
324
+ }
325
+
326
+ .ls-health-card strong {
327
+ font-size: 0.84rem;
328
+ }
329
+
330
+ .ls-health-card span:last-child {
331
+ color: var(--ls-muted);
332
+ font-size: 0.7rem;
333
+ margin-top: 0.22rem;
334
+ }
335
+
336
+ .ls-stat-list {
337
+ border-bottom: 1px solid var(--ls-line);
338
+ padding: 0.7rem 0 0.85rem;
339
+ }
340
+
341
+ .ls-stat-row {
342
+ align-items: center;
343
+ color: var(--ls-muted);
344
+ display: flex;
345
+ font-size: 0.75rem;
346
+ gap: 0.45rem;
347
+ padding: 0.3rem 0;
348
+ }
349
+
350
+ .ls-stat-row strong {
351
+ color: var(--ls-ink);
352
+ font-size: 0.82rem;
353
+ margin-left: auto;
354
+ }
355
+
356
+ .ls-stat-dot,
357
+ .ls-issue-marker {
358
+ background: var(--ls-accent);
359
+ border-radius: 50%;
360
+ display: inline-block;
361
+ height: 7px;
362
+ width: 7px;
363
+ }
364
+
365
+ .ls-dot-duplicate {
366
+ background: var(--ls-red);
367
+ }
368
+
369
+ .ls-dot-malformed {
370
+ background: var(--ls-yellow);
371
+ }
372
+
373
+ .ls-dot-clean {
374
+ background: var(--ls-cyan);
375
+ }
376
+
377
+ .ls-issue-section {
378
+ margin-top: 1.1rem;
379
+ }
380
+
381
+ .ls-section-heading-small {
382
+ margin-bottom: 0.55rem;
383
+ }
384
+
385
+ .ls-section-heading-small h2 {
386
+ font-size: 0.84rem;
387
+ }
388
+
389
+ .ls-issue-list {
390
+ display: grid;
391
+ gap: 0.45rem;
392
+ max-height: 220px;
393
+ overflow: auto;
394
+ }
395
+
396
+ .ls-issue-card {
397
+ align-items: flex-start;
398
+ background: var(--ls-panel);
399
+ border: 1px solid var(--ls-line);
400
+ border-left: 3px solid var(--ls-accent);
401
+ border-radius: 7px;
402
+ display: flex;
403
+ gap: 0.55rem;
404
+ padding: 0.55rem 0.6rem;
405
+ }
406
+
407
+ .ls-issue-duplicate {
408
+ border-left-color: var(--ls-red);
409
+ }
410
+
411
+ .ls-issue-malformed {
412
+ border-left-color: var(--ls-yellow);
413
+ }
414
+
415
+ .ls-issue-parse {
416
+ border-left-color: var(--ls-red);
417
+ }
418
+
419
+ .ls-issue-marker {
420
+ flex: 0 0 auto;
421
+ margin-top: 0.26rem;
422
+ }
423
+
424
+ .ls-issue-duplicate .ls-issue-marker {
425
+ background: var(--ls-red);
426
+ }
427
+
428
+ .ls-issue-malformed .ls-issue-marker {
429
+ background: var(--ls-yellow);
430
+ }
431
+
432
+ .ls-issue-copy strong,
433
+ .ls-issue-copy span {
434
+ display: block;
435
+ }
436
+
437
+ .ls-issue-copy strong {
438
+ font-size: 0.7rem;
439
+ }
440
+
441
+ .ls-issue-copy span,
442
+ .ls-issue-more,
443
+ .ls-no-issues {
444
+ color: var(--ls-muted);
445
+ font-size: 0.66rem;
446
+ }
447
+
448
+ .ls-issue-more {
449
+ display: block;
450
+ padding: 0.2rem 0;
451
+ text-align: center;
452
+ }
453
+
454
+ .ls-no-issues {
455
+ border: 1px dashed var(--ls-cyan);
456
+ border-radius: 7px;
457
+ color: var(--ls-cyan);
458
+ padding: 0.75rem;
459
+ }
460
+
461
+ .ls-preview-panel {
462
+ display: flex;
463
+ flex-direction: column;
464
+ gap: 1rem;
465
+ }
466
+
467
+ .ls-preview-frame {
468
+ background: var(--ls-panel-soft);
469
+ border: 1px solid var(--ls-line);
470
+ border-radius: 12px;
471
+ min-height: 265px;
472
+ overflow: auto;
473
+ position: relative;
474
+ }
475
+
476
+ .ls-empty-state {
477
+ align-items: center;
478
+ color: var(--ls-muted);
479
+ display: flex;
480
+ flex-direction: column;
481
+ gap: 0.5rem;
482
+ justify-content: center;
483
+ min-height: 265px;
484
+ padding: 2rem;
485
+ text-align: center;
486
+ }
487
+
488
+ .ls-empty-state strong {
489
+ color: var(--ls-ink);
490
+ font-size: 0.86rem;
491
+ }
492
+
493
+ .ls-empty-state span:last-child {
494
+ font-size: 0.73rem;
495
+ }
496
+
497
+ .ls-empty-grid {
498
+ background: repeating-linear-gradient(90deg, var(--ls-line) 0 1px, transparent 1px 12px), repeating-linear-gradient(0deg, var(--ls-line) 0 1px, transparent 1px 12px);
499
+ border: 1px solid var(--ls-line);
500
+ height: 62px;
501
+ opacity: 0.8;
502
+ transform: rotate(3deg);
503
+ width: 82px;
504
+ }
505
+
506
+ .ls-table {
507
+ border-collapse: collapse;
508
+ font-size: 0.72rem;
509
+ min-width: 100%;
510
+ table-layout: fixed;
511
+ }
512
+
513
+ .ls-table th,
514
+ .ls-table td {
515
+ border-bottom: 1px solid var(--ls-line);
516
+ max-width: 220px;
517
+ overflow: hidden;
518
+ padding: 0.65rem 0.7rem;
519
+ text-align: left;
520
+ text-overflow: ellipsis;
521
+ white-space: nowrap;
522
+ }
523
+
524
+ .ls-table th {
525
+ background: color-mix(in srgb, var(--ls-accent) 8%, var(--ls-panel));
526
+ color: var(--ls-accent-dark);
527
+ font-size: 0.65rem;
528
+ letter-spacing: 0.05em;
529
+ position: sticky;
530
+ text-transform: uppercase;
531
+ top: 0;
532
+ }
533
+
534
+ .ls-table td:first-child {
535
+ color: var(--ls-ink);
536
+ font-weight: 800;
537
+ }
538
+
539
+ .ls-table td.is-empty {
540
+ background: color-mix(in srgb, var(--ls-yellow) 14%, transparent);
541
+ color: var(--ls-yellow);
542
+ }
543
+
544
+ .ls-preview-note {
545
+ color: var(--ls-muted);
546
+ display: block;
547
+ font-size: 0.68rem;
548
+ padding: 0.6rem;
549
+ text-align: center;
550
+ }
551
+
552
+ .ls-export-card {
553
+ align-items: center;
554
+ border-radius: 12px;
555
+ display: flex;
556
+ gap: 1rem;
557
+ justify-content: space-between;
558
+ padding: 1rem;
559
+ }
560
+
561
+ .ls-export-copy {
562
+ min-width: 0;
563
+ }
564
+
565
+ .ls-export-actions {
566
+ flex: 0 0 auto;
567
+ }
568
+
569
+ .ls-live-status {
570
+ color: var(--ls-cyan);
571
+ font-size: 0.72rem;
572
+ min-height: 1rem;
573
+ padding: 0.35rem 0.2rem;
574
+ }
575
+
576
+ @media (max-width: 860px) {
577
+ .ls-workbench {
578
+ grid-template-columns: 1fr;
579
+ }
580
+
581
+ .ls-inspector {
582
+ border-bottom: 1px solid var(--ls-line);
583
+ border-right: 0;
584
+ }
585
+
586
+ .ls-issue-list {
587
+ max-height: none;
588
+ }
589
+ }
590
+
591
+ @media (max-width: 620px) {
592
+ .ls-topline,
593
+ .ls-drop-panel,
594
+ .ls-inspector,
595
+ .ls-preview-panel {
596
+ padding: 1rem;
597
+ }
598
+
599
+ .ls-topline,
600
+ .ls-drop-panel,
601
+ .ls-export-card {
602
+ align-items: flex-start;
603
+ flex-direction: column;
604
+ }
605
+
606
+ .ls-drop-panel {
607
+ display: flex;
608
+ }
609
+
610
+ .ls-drop-mark {
611
+ display: none;
612
+ }
613
+
614
+ .ls-drop-actions,
615
+ .ls-export-actions,
616
+ .ls-button {
617
+ width: 100%;
618
+ }
619
+
620
+ .ls-button {
621
+ text-align: center;
622
+ }
623
+
624
+ .ls-privacy-note {
625
+ text-align: left;
626
+ }
627
+ }
628
+
629
+ @media (prefers-reduced-motion: reduce) {
630
+ .ls-root * {
631
+ scroll-behavior: auto;
632
+ transition-duration: 0.01ms;
633
+ }
634
+ }
@@ -0,0 +1,38 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { detectTranslationFormat, sanitizeTranslationFile } from './logic';
3
+
4
+ describe('localization sanitizer', () => {
5
+ it('repairs quoted commas, reports blanks, and removes duplicate CSV keys', () => {
6
+ const result = sanitizeTranslationFile('key,en,es\nmenu.play,Play,"Jugar, ahora"\nmenu.play,Play again,\nmenu.quit,,Salir\n', 'csv');
7
+
8
+ expect(result.valid).toBe(true);
9
+ expect(result.stats.duplicateKeys).toBe(1);
10
+ expect(result.stats.emptyCells).toBe(2);
11
+ expect(result.stats.cleanRows).toBe(2);
12
+ expect(result.cleanText).toContain('"Jugar, ahora"');
13
+ });
14
+
15
+ it('pads missing CSV cells and joins an extra comma into the final translation', () => {
16
+ const result = sanitizeTranslationFile('key,en,es\nhello,Hello\nbye,Bye,Adios,unexpected\n', 'csv');
17
+
18
+ expect(result.stats.malformedRows).toBe(2);
19
+ expect(result.rows[0]).toEqual(['hello', 'Hello', '']);
20
+ expect(result.rows[1]).toEqual(['bye', 'Bye', 'Adios,unexpected']);
21
+ });
22
+
23
+ it('normalizes JSON arrays while keeping the first duplicate record', () => {
24
+ const result = sanitizeTranslationFile('[{"key":"start","en":"Start","es":""},{"key":"start","en":"Begin","es":"Comenzar"}]', 'json');
25
+
26
+ expect(result.valid).toBe(true);
27
+ expect(result.stats.duplicateKeys).toBe(1);
28
+ expect(result.stats.emptyCells).toBe(1);
29
+ expect(JSON.parse(result.cleanText)).toHaveLength(1);
30
+ });
31
+
32
+ it('rejects invalid JSON and detects format from file names or content', () => {
33
+ expect(sanitizeTranslationFile('{broken', 'json').valid).toBe(false);
34
+ expect(detectTranslationFormat('strings.json')).toBe('json');
35
+ expect(detectTranslationFormat('strings.txt', '[{"key":"x"}]')).toBe('json');
36
+ expect(detectTranslationFormat('strings.txt', 'key,en\nx,Hello')).toBe('csv');
37
+ });
38
+ });
@@ -0,0 +1,23 @@
1
+ import { sanitizeCsv } from './csv-sanitizer';
2
+ import { emptyResult } from './csv-sanitizer';
3
+ import { sanitizeJson } from './json-sanitizer';
4
+ import type { SanitizerResult, TranslationFormat } from './types';
5
+
6
+ export type { TranslationFormat, SanitizerIssueKind, SanitizerIssue, SanitizerStats, SanitizerResult } from './types';
7
+
8
+ export function detectTranslationFormat(fileName: string, source = ''): TranslationFormat {
9
+ if (/\.json$/i.test(fileName)) {
10
+ return 'json';
11
+ }
12
+ if (/\.csv$/i.test(fileName)) {
13
+ return 'csv';
14
+ }
15
+ return source.trim().startsWith('{') || source.trim().startsWith('[') ? 'json' : 'csv';
16
+ }
17
+
18
+ export function sanitizeTranslationFile(source: string, format: TranslationFormat): SanitizerResult {
19
+ if (!source.trim()) {
20
+ return emptyResult(format, 'empty_file');
21
+ }
22
+ return format === 'csv' ? sanitizeCsv(source) : sanitizeJson(source);
23
+ }
@@ -0,0 +1,15 @@
1
+ ---
2
+ import { SEORenderer } from '@jjlmoya/utils-shared';
3
+ import { localizationSanitizer } from './entry';
4
+ import type { KnownLocale } from '../../types';
5
+
6
+ interface Props {
7
+ locale?: KnownLocale;
8
+ }
9
+
10
+ const { locale = 'en' } = Astro.props;
11
+ const loader = localizationSanitizer.i18n[locale] ?? localizationSanitizer.i18n.en;
12
+ const content = loader ? await loader() : null;
13
+ ---
14
+
15
+ {content && content.seo.length > 0 && <SEORenderer content={{ locale, sections: content.seo }} />}