@jjlmoya/utils-games-development 1.52.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 (35) hide show
  1. package/package.json +1 -1
  2. package/src/entries.ts +4 -2
  3. package/src/tests/locale_completeness.test.ts +1 -1
  4. package/src/tests/tool_validation.test.ts +1 -1
  5. package/src/tool/localizationSanitizer/app-client.ts +270 -0
  6. package/src/tool/localizationSanitizer/bibliography.astro +6 -0
  7. package/src/tool/localizationSanitizer/bibliography.ts +14 -0
  8. package/src/tool/localizationSanitizer/component.astro +116 -0
  9. package/src/tool/localizationSanitizer/csv-parser.ts +95 -0
  10. package/src/tool/localizationSanitizer/csv-sanitizer.ts +98 -0
  11. package/src/tool/localizationSanitizer/entry.ts +28 -0
  12. package/src/tool/localizationSanitizer/i18n/de.ts +176 -0
  13. package/src/tool/localizationSanitizer/i18n/en.ts +176 -0
  14. package/src/tool/localizationSanitizer/i18n/es.ts +176 -0
  15. package/src/tool/localizationSanitizer/i18n/fr.ts +176 -0
  16. package/src/tool/localizationSanitizer/i18n/id.ts +176 -0
  17. package/src/tool/localizationSanitizer/i18n/it.ts +176 -0
  18. package/src/tool/localizationSanitizer/i18n/ja.ts +176 -0
  19. package/src/tool/localizationSanitizer/i18n/ko.ts +176 -0
  20. package/src/tool/localizationSanitizer/i18n/nl.ts +176 -0
  21. package/src/tool/localizationSanitizer/i18n/pl.ts +176 -0
  22. package/src/tool/localizationSanitizer/i18n/pt.ts +176 -0
  23. package/src/tool/localizationSanitizer/i18n/ru.ts +176 -0
  24. package/src/tool/localizationSanitizer/i18n/sv.ts +176 -0
  25. package/src/tool/localizationSanitizer/i18n/tr.ts +176 -0
  26. package/src/tool/localizationSanitizer/i18n/zh.ts +176 -0
  27. package/src/tool/localizationSanitizer/index.ts +12 -0
  28. package/src/tool/localizationSanitizer/json-sanitizer.ts +118 -0
  29. package/src/tool/localizationSanitizer/localization-sanitizer.css +634 -0
  30. package/src/tool/localizationSanitizer/logic.test.ts +38 -0
  31. package/src/tool/localizationSanitizer/logic.ts +23 -0
  32. package/src/tool/localizationSanitizer/seo.astro +15 -0
  33. package/src/tool/localizationSanitizer/types.ts +55 -0
  34. package/src/tool/localizationSanitizer/ui.ts +39 -0
  35. package/src/tools.ts +2 -1
@@ -0,0 +1,176 @@
1
+ import type { ToolLocaleContent } from '../../../types';
2
+ import type { LocalizationSanitizerUI } from '../ui';
3
+
4
+ export const content: ToolLocaleContent<LocalizationSanitizerUI> = {
5
+ slug: 'sanering-lokaliseringsfiler-godot-unity',
6
+ title: 'Sanering av CSV och JSON lokaliseringsfiler för Godot och Unity',
7
+ description: 'Inspektera CSV och JSON översättningsfiler för tomma celler, duplicerade nycklar och felaktiga rader samt exportera en ren kopia för spelmotorn.',
8
+ ui: {
9
+ csvTab: 'CSV fil',
10
+ jsonTab: 'JSON fil',
11
+ dropTitle: 'Släpp en översättningsfil här',
12
+ dropSubtitle: 'Inspektera strukturen i webbläsaren och behåll källfilen på din enhet.',
13
+ browseButton: 'Bläddra filer',
14
+ sampleButton: 'Ladda exempel',
15
+ clearButton: 'Rensa',
16
+ inputLabel: 'Inmatningsfil för översättning',
17
+ formatLabel: 'Format',
18
+ healthLabel: 'Filstatus',
19
+ readyStatus: 'Redo för import',
20
+ reviewStatus: 'Kräver granskning',
21
+ emptyCellsLabel: 'Tomma celler',
22
+ duplicateKeysLabel: 'Duplicerade nycklar',
23
+ malformedRowsLabel: 'Felaktiga rader',
24
+ cleanRowsLabel: 'Rena rader',
25
+ issueListTitle: 'Resultat',
26
+ noIssues: 'Inga fel hittades i denna analys.',
27
+ previewTitle: 'Översättningsvisning',
28
+ previewSubtitle: 'De första raderna visar den normaliserade strukturen som exporteras.',
29
+ exportTitle: 'Exportera ren fil',
30
+ exportSubtitle: 'Duplikater tas bort saknade kolumner fylls i och CSV värden escapar korrekt.',
31
+ downloadButton: 'Ladda ner ren fil',
32
+ copyButton: 'Kopiera resultat',
33
+ copiedMessage: 'Rent resultat kopierat till urklipp.',
34
+ emptyIssue: 'Tom översättningscell',
35
+ duplicateIssue: 'Duplicerad nyckels rad borttagen',
36
+ malformedIssue: 'Kolumn eller citatteckenfel',
37
+ parseIssue: 'Kunde inte läsa filen.',
38
+ rowLabel: 'Rad',
39
+ columnLabel: 'Kolumn',
40
+ keyLabel: 'Nyckel',
41
+ sampleFileName: 'lokalisering-exempel.csv',
42
+ privacyNote: 'Lokal behandling i webbläsaren',
43
+ waitingTitle: 'Väntar på fil',
44
+ waitingSubtitle: 'Släpp en CSV eller JSON fil för att starta granskningen.',
45
+ fileTypeNote: 'UTF8 CSV eller strukturerad JSON',
46
+ },
47
+ seo: [
48
+ { type: 'title', level: 2, text: 'Varför lokaliseringsfiler misslyckas vid import' },
49
+ {
50
+ type: 'paragraph',
51
+ html: 'Översättningstabeller är enkla att redigera men lätta att förstöra. Ett kommatecken inuti en mening eller saknade citattecken kan förskjuta språkkolumnerna.',
52
+ },
53
+ {
54
+ type: 'paragraph',
55
+ html: 'Godots CSV importör och Unity Localization kräver konsekvent struktur. Detta verktyg kontrollerar filen innan import.',
56
+ },
57
+ {
58
+ type: 'stats',
59
+ columns: 4,
60
+ items: [
61
+ { value: 'CSV', label: 'Kommakontroll' },
62
+ { value: 'JSON', label: 'Array & map support' },
63
+ { value: '0 uppladdningar', label: 'Helt lokalt' },
64
+ { value: '1 klick', label: 'Ren export' },
65
+ ],
66
+ },
67
+ { type: 'title', level: 2, text: 'Vad verktyget kontrollerar' },
68
+ {
69
+ type: 'comparative',
70
+ columns: 2,
71
+ items: [
72
+ {
73
+ title: 'Upptäckts före import',
74
+ description: 'Svårupptäckta fel i stora filer',
75
+ points: ['Tomma celler', 'Duplicerade nycklar', 'Rader med fel antal kolumner', 'Ogiltiga citattecken'],
76
+ },
77
+ {
78
+ title: 'Normaliserat vid export',
79
+ description: 'Säkra automatiska korrigeringar',
80
+ points: ['Saknade kolumner fylls i', 'Extra fält slås ihop i sista kolumnen', 'Första nyckeln behålls', 'Originalfilen påverkas inte'],
81
+ },
82
+ ],
83
+ },
84
+ { type: 'title', level: 2, text: 'Att granska den sanerade filen' },
85
+ {
86
+ type: 'paragraph',
87
+ html: 'Strukturell sanering ersätter inte språkgranskning. Använd resultatlistan för att komplettera saknade översättningar.',
88
+ },
89
+ {
90
+ type: 'table',
91
+ headers: ['Fynd', 'Betydelse', 'Rekommenderad åtgärd'],
92
+ rows: [
93
+ ['Tom cell', 'En språkkolumn saknar text', 'Översätt eller bekräfta avsiktlig tomhet'],
94
+ ['Duplicerad nyckel', 'Flera rader använder samma nyckel', 'Jämför rader och behåll den första'],
95
+ ['Felaktig rad', 'Kolumnantal avviker från rubriken', 'Kontrollera den sammanslagna kolumnen'],
96
+ ['Läsfel', 'JSON ogiltig', 'Korrigera syntaxen före import'],
97
+ ],
98
+ },
99
+ { type: 'title', level: 2, text: 'CSV konventioner för spel' },
100
+ {
101
+ type: 'paragraph',
102
+ html: 'Text som innehåller kommatecken eller radbrytningar måste omges av dubbla citattecken.',
103
+ },
104
+ {
105
+ type: 'tip',
106
+ title: 'Spara originalet',
107
+ html: 'Behåll alltid översättarens originalfil som reservkopia.',
108
+ },
109
+ {
110
+ type: 'glossary',
111
+ items: [
112
+ { term: 'Översättningsnyckel', definition: 'Unik identifierare som används i spelkoden.' },
113
+ { term: 'CSV fält', definition: 'Enskilt värde mellan avskiljare.' },
114
+ { term: 'Escaping', definition: 'Användning av citattecken för att behålla skiljetecken som text.' },
115
+ { term: 'Locale', definition: 'Språk och regionkod som sv, en eller ja.' },
116
+ ],
117
+ },
118
+ ],
119
+ faq: [
120
+ {
121
+ question: 'Laddas filer upp till en server?',
122
+ answer: 'Nej, all bearbetning sker lokalt i webbläsaren.',
123
+ },
124
+ {
125
+ question: 'Vad händer om en CSV rad har extra kommatecken?',
126
+ answer: 'Raden markeras som felaktig och extra fält slås ihop i sista kolumnen.',
127
+ },
128
+ {
129
+ question: 'Hur rensas duplicerade nycklar?',
130
+ answer: 'Den första förekomsten behålls och följande ignoreras.',
131
+ },
132
+ {
133
+ question: 'Kontrollerar verktyget översättningskvalitet?',
134
+ answer: 'Nej, det kontrollerar endast filstruktur och tomma celler.',
135
+ },
136
+ ],
137
+ howTo: [
138
+ { name: 'Välj format', text: 'Välj CSV eller JSON.' },
139
+ { name: 'Granska resultat', text: 'Släpp filen och kontrollera fel listan.' },
140
+ { name: 'Exportera och testa', text: 'Ladda ner den rena filen och importera i motorn.' },
141
+ ],
142
+ schemas: [
143
+ {
144
+ '@context': 'https://schema.org',
145
+ '@type': 'SoftwareApplication',
146
+ name: 'Sanering av CSV och JSON lokaliseringsfiler för Godot och Unity',
147
+ applicationCategory: 'DeveloperApplication',
148
+ operatingSystem: 'Any',
149
+ offers: { '@type': 'Offer', price: '0', priceCurrency: 'SEK' },
150
+ },
151
+ {
152
+ '@context': 'https://schema.org',
153
+ '@type': 'FAQPage',
154
+ mainEntity: [{
155
+ '@type': 'Question',
156
+ name: 'Laddas filer upp till en server?',
157
+ acceptedAnswer: { '@type': 'Answer', text: 'Nej, all bearbetning sker lokalt i webbläsaren.' },
158
+ }],
159
+ },
160
+ {
161
+ '@context': 'https://schema.org',
162
+ '@type': 'HowTo',
163
+ name: 'Hur man sanerar en lokaliseringsfil',
164
+ step: [
165
+ { '@type': 'HowToStep', name: 'Välj format', text: 'Välj CSV eller JSON.' },
166
+ { '@type': 'HowToStep', name: 'Granska resultat', text: 'Släpp filen och kontrollera fel listan.' },
167
+ { '@type': 'HowToStep', name: 'Exporteera och testa', text: 'Ladda ner den rena filen och importera i motorn.' },
168
+ ],
169
+ },
170
+ ],
171
+ bibliography: [
172
+ { name: 'Godot ResourceImporterCSVTranslation Dokumentation', url: 'https://docs.godotengine.org/en/stable/classes/class_resourceimportercsvtranslation.html' },
173
+ { name: 'Unity Localization CSV Import Dokumentation', url: 'https://docs.unity3d.com/Packages/com.unity.localization@1.5/manual/CSV.html' },
174
+ { name: 'RFC 4180 CSV Specifikation', url: 'https://datatracker.ietf.org/doc/html/rfc4180' },
175
+ ],
176
+ };
@@ -0,0 +1,176 @@
1
+ import type { ToolLocaleContent } from '../../../types';
2
+ import type { LocalizationSanitizerUI } from '../ui';
3
+
4
+ export const content: ToolLocaleContent<LocalizationSanitizerUI> = {
5
+ slug: 'yerellestirme-csv-temizleyici-godot-unity',
6
+ title: 'Godot ve Unity Yerellestirme CSV ve JSON Temizleyici',
7
+ description: 'Ceviri CSV veya JSON dosyalarinizdaki bos hucreleri, yinelenen anahtarlari ve bozuk satirlari inceleyin ve motor aktarimina hazir temiz bir kopya disari aktarin.',
8
+ ui: {
9
+ csvTab: 'CSV Dosyasi',
10
+ jsonTab: 'JSON Dosyasi',
11
+ dropTitle: 'Ceviri dosyasini buraya birakin',
12
+ dropSubtitle: 'Yapiyi tarayicinizda inceleyin, kaynak dosyayi cihazinizda tutun.',
13
+ browseButton: 'Dosya sec',
14
+ sampleButton: 'Ornek yukle',
15
+ clearButton: 'Temizle',
16
+ inputLabel: 'Ceviri dosyasi girdisi',
17
+ formatLabel: 'Format',
18
+ healthLabel: 'Dosya sagligi',
19
+ readyStatus: 'Aktarima hazir',
20
+ reviewStatus: 'Inceleme gerekli',
21
+ emptyCellsLabel: 'Bos hucreler',
22
+ duplicateKeysLabel: 'Yinelenen anahtarlar',
23
+ malformedRowsLabel: 'Bozuk satirlar',
24
+ cleanRowsLabel: 'Temiz satirlar',
25
+ issueListTitle: 'Bulgular',
26
+ noIssues: 'Bu taramada sorun bulunamadi.',
27
+ previewTitle: 'Ceviri onizlemesi',
28
+ previewSubtitle: 'Ilk satirlar disari aktarilacak normalize edilmis yapiyi gosterir.',
29
+ exportTitle: 'Temiz dosyayi disari aktar',
30
+ exportSubtitle: 'Yinelenenler kaldirilir, eksik sutunlar doldurulur ve CSV degerleri duzenlenir.',
31
+ downloadButton: 'Temiz dosyayi indir',
32
+ copyButton: 'Sonucu kopyala',
33
+ copiedMessage: 'Temiz sonuc panoya kopyalandi.',
34
+ emptyIssue: 'Bos ceviri bucresi',
35
+ duplicateIssue: 'Yinelenen anahtar kaldirildi',
36
+ malformedIssue: 'Sutun veya tirnak uyumsuzlugu',
37
+ parseIssue: 'Dosya okunamadi.',
38
+ rowLabel: 'Satir',
39
+ columnLabel: 'Sutun',
40
+ keyLabel: 'Anahtar',
41
+ sampleFileName: 'yerellestirme-ornek.csv',
42
+ privacyNote: 'Yerel tarayici islemi',
43
+ waitingTitle: 'Dosya bekleniyor',
44
+ waitingSubtitle: 'Incelemeyi baslatmak icin bir CSV veya JSON dosyasi birakin.',
45
+ fileTypeNote: 'UTF-8 CSV veya yapilandirilmis JSON',
46
+ },
47
+ seo: [
48
+ { type: 'title', level: 2, text: 'Yerellestirme dosyalari motor aktariminda neden bozulur' },
49
+ {
50
+ type: 'paragraph',
51
+ html: 'Ceviri tablolari kolayca duzenlenebilir ancak bicimlendirme hatalarina karsi hassastir. Cumle icindeki bir virgül veya eksik tirnak isareti sutunlarin kaymasina neden olabilir.',
52
+ },
53
+ {
54
+ type: 'paragraph',
55
+ html: 'Godot CSV aktarici ve Unity Localization paketi tutarli bir yapi gerektirir. Bu arac, dosyalarinizi aktarim öncesinde denetler.',
56
+ },
57
+ {
58
+ type: 'stats',
59
+ columns: 4,
60
+ items: [
61
+ { value: 'CSV', label: 'Virgül denetimi' },
62
+ { value: 'JSON', label: 'Dizi ve nesne destegi' },
63
+ { value: '0 yukleme', label: 'Tamamen yerel' },
64
+ { value: '1 tik', label: 'Temiz aktarim' },
65
+ ],
66
+ },
67
+ { type: 'title', level: 2, text: 'Aracin denetledigi hususlar' },
68
+ {
69
+ type: 'comparative',
70
+ columns: 2,
71
+ items: [
72
+ {
73
+ title: 'Aktarim öncesi tespit edilenler',
74
+ description: 'Büyük dosyalarda fark edilmesi zor hatalar',
75
+ points: ['Bos ceviri hucreleri', 'Yinelenen ceviri anahtarlari', 'Eksik veya fazla sutunlu satirlar', 'Gecersiz tirnak sözdizimi'],
76
+ },
77
+ {
78
+ title: 'Aktarimda normalize edilenler',
79
+ description: 'Guvenli otomatik düzeltmeler',
80
+ points: ['Eksik sutunlarin doldurulmasi', 'Fazla alanlarin son sutunda birlestirilmesi', 'Ilk anahtar kaydinin korunmasi', 'Orijinal dosyanin korunmasi'],
81
+ },
82
+ ],
83
+ },
84
+ { type: 'title', level: 2, text: 'Temizlenen dosyanin incelenmesi' },
85
+ {
86
+ type: 'paragraph',
87
+ html: 'Yapisal temizleme dil kontrolünün yerini almaz. Eksik cevirileri tamamlamak icin bulgu listesini kullanin.',
88
+ },
89
+ {
90
+ type: 'table',
91
+ headers: ['Bulgu', 'Anlami', 'Onerilen eylem'],
92
+ rows: [
93
+ ['Bos hucre', 'Dil sutununda metin yok', 'Cevirin veya bilerek bos birakildigini onaylayin'],
94
+ ['Yinelenen anahtar', 'Birden fazla satir ayni anahtari kullaniyor', 'Satirlari karsilastirip ilkini koruyun'],
95
+ ['Bozuk satir', 'Sutun sayisi baslikla uyuşmuyor', 'Birlesik son sutunu kontrol edin'],
96
+ ['Okuma hatasi', 'JSON gecersiz', 'Aktarmadan önce sözdizimini düzeltin'],
97
+ ],
98
+ },
99
+ { type: 'title', level: 2, text: 'Oyunlar icin CSV kurallari' },
100
+ {
101
+ type: 'paragraph',
102
+ html: 'Virgül veya satir sonu iceren metinler cift tirnak icine alinmalidir.',
103
+ },
104
+ {
105
+ type: 'tip',
106
+ title: 'Orijinali saklayin',
107
+ html: 'Cevirmenden gelen orijinal dosyayi daima yedek olarak saklayin.',
108
+ },
109
+ {
110
+ type: 'glossary',
111
+ items: [
112
+ { term: 'Ceviri anahtari', definition: 'Oyun kodunda kullanilan benzersiz tanimlayici.' },
113
+ { term: 'CSV alani', definition: 'Ayiricilar arasindaki tekil deger.' },
114
+ { term: 'Escaping', definition: 'Noktalama isaretlerini korumak icin tirnak kullanimi.' },
115
+ { term: 'Locale', definition: 'tr, en veya ja gibi dil ve bölge kodu.' },
116
+ ],
117
+ },
118
+ ],
119
+ faq: [
120
+ {
121
+ question: 'Dosyalar sunucuya yukleniyor mu?',
122
+ answer: 'Hayir, tum islemler tamamen tarayici icinde yerel olarak yapilir.',
123
+ },
124
+ {
125
+ question: 'CSV satirinda fazla virgül varsa ne olur?',
126
+ answer: 'Satir bozuk olarak isaretlenir ve fazla alanlar son sutunda birlestirilir.',
127
+ },
128
+ {
129
+ question: 'Yinelenen anahtarlar nasil temizlenir?',
130
+ answer: 'Ilk kayit korunur, sonraki yinelenenler disari aktarilan dosyadan cikarilir.',
131
+ },
132
+ {
133
+ question: 'Arac ceviri kalitesini kontrol eder mi?',
134
+ answer: 'Hayir, yalnızca dosya yapisini ve bos hucreleri denetler.',
135
+ },
136
+ ],
137
+ howTo: [
138
+ { name: 'Format secin', text: 'CSV veya JSON secin.' },
139
+ { name: 'Bulgulari inceleyin', text: 'Dosyayi birakin ve hata listesini kontrol edin.' },
140
+ { name: 'Disari aktarin ve test edin', text: 'Temiz dosyayi indirin ve motorunuzda test edin.' },
141
+ ],
142
+ schemas: [
143
+ {
144
+ '@context': 'https://schema.org',
145
+ '@type': 'SoftwareApplication',
146
+ name: 'Godot ve Unity Yerellestirme CSV ve JSON Temizleyici',
147
+ applicationCategory: 'DeveloperApplication',
148
+ operatingSystem: 'Any',
149
+ offers: { '@type': 'Offer', price: '0', priceCurrency: 'TRY' },
150
+ },
151
+ {
152
+ '@context': 'https://schema.org',
153
+ '@type': 'FAQPage',
154
+ mainEntity: [{
155
+ '@type': 'Question',
156
+ name: 'Dosyalar sunucuya yukleniyor mu?',
157
+ acceptedAnswer: { '@type': 'Answer', text: 'Hayir, tum islemler tamamen tarayici icinde yerel olarak yapilir.' },
158
+ }],
159
+ },
160
+ {
161
+ '@context': 'https://schema.org',
162
+ '@type': 'HowTo',
163
+ name: 'Yerellestirme dosyasi nasil temizlenir',
164
+ step: [
165
+ { '@type': 'HowToStep', name: 'Format secin', text: 'CSV veya JSON secin.' },
166
+ { '@type': 'HowToStep', name: 'Bulgulari inceleyin', text: 'Dosyayi birakin ve hata listesini kontrol edin.' },
167
+ { '@type': 'HowToStep', name: 'Disari aktarin ve test edin', text: 'Temiz dosyayi indirin ve motorunuzda test edin.' },
168
+ ],
169
+ },
170
+ ],
171
+ bibliography: [
172
+ { name: 'Godot ResourceImporterCSVTranslation Dokumantasyonu', url: 'https://docs.godotengine.org/en/stable/classes/class_resourceimportercsvtranslation.html' },
173
+ { name: 'Unity Localization CSV Import Dokumantasyonu', url: 'https://docs.unity3d.com/Packages/com.unity.localization@1.5/manual/CSV.html' },
174
+ { name: 'RFC 4180 CSV Spesifikasyonu', url: 'https://datatracker.ietf.org/doc/html/rfc4180' },
175
+ ],
176
+ };
@@ -0,0 +1,176 @@
1
+ import type { ToolLocaleContent } from '../../../types';
2
+ import type { LocalizationSanitizerUI } from '../ui';
3
+
4
+ export const content: ToolLocaleContent<LocalizationSanitizerUI> = {
5
+ slug: 'localization-sanitizer',
6
+ title: 'Godot 与 Unity 本地化 CSV 及 JSON 修复工具',
7
+ description: '检查翻译 CSV 和 JSON 文件中的空单元格、重复键及损坏行,并导出可直接导入游戏引擎的规范化文件。',
8
+ ui: {
9
+ csvTab: 'CSV 文件',
10
+ jsonTab: 'JSON 文件',
11
+ dropTitle: '拖放翻译文件至此处',
12
+ dropSubtitle: '在浏览器中检查结构,源文件保留在本地设备上。',
13
+ browseButton: '浏览文件',
14
+ sampleButton: '加载示例',
15
+ clearButton: '清空',
16
+ inputLabel: '翻译文件输入',
17
+ formatLabel: '格式',
18
+ healthLabel: '文件状态',
19
+ readyStatus: '可直接导入',
20
+ reviewStatus: '需要检查',
21
+ emptyCellsLabel: '空单元格',
22
+ duplicateKeysLabel: '重复键',
23
+ malformedRowsLabel: '损坏行',
24
+ cleanRowsLabel: '正常行',
25
+ issueListTitle: '检查结果',
26
+ noIssues: '未发现结构问题。',
27
+ previewTitle: '翻译预览',
28
+ previewSubtitle: '前几行展示了即将导出的规范化结构。',
29
+ exportTitle: '导出规范文件',
30
+ exportSubtitle: '自动移除重复键、补齐缺失列并规范 CSV 转义字符。',
31
+ downloadButton: '下载规范文件',
32
+ copyButton: '复制结果',
33
+ copiedMessage: '已成功复制规范化结果到剪贴板。',
34
+ emptyIssue: '空翻译单元格',
35
+ duplicateIssue: '已移除重复键',
36
+ malformedIssue: '列数或引号不匹配',
37
+ parseIssue: '无法解析文件。',
38
+ rowLabel: '行',
39
+ columnLabel: '列',
40
+ keyLabel: '键',
41
+ sampleFileName: 'localization-sample.csv',
42
+ privacyNote: '本地浏览器处理',
43
+ waitingTitle: '等待文件',
44
+ waitingSubtitle: '拖放 CSV 或 JSON 文件开始检查。',
45
+ fileTypeNote: 'UTF8 CSV 或结构化 JSON',
46
+ },
47
+ seo: [
48
+ { type: 'title', level: 2, text: '本地化文件导入引擎时为何容易出错' },
49
+ {
50
+ type: 'paragraph',
51
+ html: '翻译表格虽然便于编辑,但也容易产生格式错误。包含逗号的文本若缺少转义引号,会导致整行文本错列。',
52
+ },
53
+ {
54
+ type: 'paragraph',
55
+ html: 'Godot CSV 导入器和 Unity Localization 均需要严格的列格式。本工具在导入前帮助您排查并规范文件。',
56
+ },
57
+ {
58
+ type: 'stats',
59
+ columns: 4,
60
+ items: [
61
+ { value: 'CSV', label: '逗号及转义检查' },
62
+ { value: 'JSON', label: '数组与对象映射' },
63
+ { value: '0 上传', label: '纯本地处理' },
64
+ { value: '1 键', label: '一键导出' },
65
+ ],
66
+ },
67
+ { type: 'title', level: 2, text: '工具检查项目' },
68
+ {
69
+ type: 'comparative',
70
+ columns: 2,
71
+ items: [
72
+ {
73
+ title: '导入前排查',
74
+ description: '大文件中难以肉眼发现的问题',
75
+ points: ['缺失翻译的空单元格', '重复的翻译 Key', '列数不一致的损坏行', '非法的引号语法'],
76
+ },
77
+ {
78
+ title: '导出时规范化',
79
+ description: '安全的自动修复',
80
+ points: ['自动补齐缺失列', '合并多余列至末尾列', '保留首次出现的 Key', '源文件安全无损'],
81
+ },
82
+ ],
83
+ },
84
+ { type: 'title', level: 2, text: '如何审核处理后的文件' },
85
+ {
86
+ type: 'paragraph',
87
+ html: '结构修复无法替代文本校对。请根据检查结果列表补充未完成的翻译。',
88
+ },
89
+ {
90
+ type: 'table',
91
+ headers: ['检查项', '含义', '建议操作'],
92
+ rows: [
93
+ ['空单元格', '语言列缺失文本', '补充翻译或确认是否故意留空'],
94
+ ['重复键', '多行使用了相同的 Key', '对比文本后采用保留的首次记录'],
95
+ ['损坏行', '列数与表头不符', '检查合并后的末尾列内容'],
96
+ ['解析错误', 'JSON 格式非法', '导入前修复 JSON 语法'],
97
+ ],
98
+ },
99
+ { type: 'title', level: 2, text: '游戏本地化 CSV 规范' },
100
+ {
101
+ type: 'paragraph',
102
+ html: '包含逗号或换行符的文本必须使用双引号包裹。',
103
+ },
104
+ {
105
+ type: 'tip',
106
+ title: '保留原始文件',
107
+ html: '请务必保留译员交付的原始文件作为备份与对比。',
108
+ },
109
+ {
110
+ type: 'glossary',
111
+ items: [
112
+ { term: '翻译键 (Key)', definition: '游戏代码中引用的唯一文本标识符。' },
113
+ { term: 'CSV 字段', definition: '分隔符之间的单个文本值。' },
114
+ { term: '转义 (Escaping)', definition: '使用引号包裹以保留标点符号。' },
115
+ { term: 'Locale', definition: '语言与地区代码,如 zh, en, ja。' },
116
+ ],
117
+ },
118
+ ],
119
+ faq: [
120
+ {
121
+ question: '文件会被上传到服务器吗?',
122
+ answer: '不会,所有解析与处理均在本地浏览器内存中完成。',
123
+ },
124
+ {
125
+ question: '如果 CSV 行中有多余的逗号会怎样?',
126
+ answer: '该行会被标记为损坏行,多余字段会在导出时合并至末尾列。',
127
+ },
128
+ {
129
+ question: '如何处理重复键?',
130
+ answer: '保留第一次出现的记录,后续重复 Key 将被过滤。',
131
+ },
132
+ {
133
+ question: '本工具会检查翻译质量吗?',
134
+ answer: '不会,本工具仅检查文件结构与单元格完整性。',
135
+ },
136
+ ],
137
+ howTo: [
138
+ { name: '选择格式', text: '选择 CSV 或 JSON 格式。' },
139
+ { name: '查看检查结果', text: '拖放文件并排查问题列表。' },
140
+ { name: '导出并测试', text: '下载规范文件并导入游戏引擎测试。' },
141
+ ],
142
+ schemas: [
143
+ {
144
+ '@context': 'https://schema.org',
145
+ '@type': 'SoftwareApplication',
146
+ name: 'Godot 与 Unity 本地化 CSV 及 JSON 修复工具',
147
+ applicationCategory: 'DeveloperApplication',
148
+ operatingSystem: 'Any',
149
+ offers: { '@type': 'Offer', price: '0', priceCurrency: 'CNY' },
150
+ },
151
+ {
152
+ '@context': 'https://schema.org',
153
+ '@type': 'FAQPage',
154
+ mainEntity: [{
155
+ '@type': 'Question',
156
+ name: '文件会被上传到服务器吗?',
157
+ acceptedAnswer: { '@type': 'Answer', text: '不会,所有解析与处理均在本地浏览器内存中完成。' },
158
+ }],
159
+ },
160
+ {
161
+ '@context': 'https://schema.org',
162
+ '@type': 'HowTo',
163
+ name: '如何修复游戏本地化文件',
164
+ step: [
165
+ { '@type': 'HowToStep', name: '选择格式', text: '选择 CSV 或 JSON 格式。' },
166
+ { '@type': 'HowToStep', name: '查看检查结果', text: '拖放文件并排查问题列表。' },
167
+ { '@type': 'HowToStep', name: '导出并测试', text: '下载规范文件并导入游戏引擎测试。' },
168
+ ],
169
+ },
170
+ ],
171
+ bibliography: [
172
+ { name: 'Godot ResourceImporterCSVTranslation 文档', url: 'https://docs.godotengine.org/en/stable/classes/class_resourceimportercsvtranslation.html' },
173
+ { name: 'Unity Localization CSV Import 文档', url: 'https://docs.unity3d.com/Packages/com.unity.localization@1.5/manual/CSV.html' },
174
+ { name: 'RFC 4180 CSV 规范', url: 'https://datatracker.ietf.org/doc/html/rfc4180' },
175
+ ],
176
+ };
@@ -0,0 +1,12 @@
1
+ import type { ToolDefinition } from '../../types';
2
+ import { localizationSanitizer } from './entry';
3
+
4
+ export * from './entry';
5
+ export * from './logic';
6
+
7
+ export const LOCALIZATION_SANITIZER_TOOL: ToolDefinition = {
8
+ entry: localizationSanitizer,
9
+ Component: () => import('./component.astro'),
10
+ SEOComponent: () => import('./seo.astro'),
11
+ BibliographyComponent: () => import('./bibliography.astro'),
12
+ };
@@ -0,0 +1,118 @@
1
+ import { emptyResult, issue } from './csv-sanitizer';
2
+ import type { JsonRowRecord, SanitizerIssue, SanitizerResult } from './types';
3
+
4
+ function textValue(value: unknown): string {
5
+ if (value === null || value === undefined) return '';
6
+ if (typeof value === 'string') return value;
7
+ if (typeof value === 'object') return JSON.stringify(value);
8
+ return String(value);
9
+ }
10
+
11
+ function objectRecords(value: unknown): JsonRowRecord[] | null {
12
+ if (!Array.isArray(value)) return null;
13
+ return value.map((item, index) => ({
14
+ source: item !== null && typeof item === 'object' && !Array.isArray(item) ? (item as Record<string, unknown>) : {},
15
+ row: index + 1,
16
+ }));
17
+ }
18
+
19
+ function parseJsonRecords(source: string): { records: JsonRowRecord[]; isMap: boolean; mapValuesAreObjects: boolean } | null {
20
+ let parsed: unknown;
21
+ try {
22
+ parsed = JSON.parse(source.replace(/^\uFEFF/, ''));
23
+ } catch {
24
+ return null;
25
+ }
26
+ const arrayRecords = objectRecords(parsed);
27
+ const isMap = !Array.isArray(parsed) && parsed !== null && typeof parsed === 'object';
28
+ const mapEntries = isMap ? Object.entries(parsed as Record<string, unknown>) : [];
29
+ const mapValuesAreObjects = mapEntries.length > 0 && mapEntries.every(([, value]) => value !== null && typeof value === 'object' && !Array.isArray(value));
30
+ const records: JsonRowRecord[] = arrayRecords ?? (mapEntries.length > 0
31
+ ? mapEntries.map(([key, value], index) => ({ source: mapValuesAreObjects ? { ...(value as Record<string, unknown>), key } : { key, value }, row: index + 1 }))
32
+ : []);
33
+ return { records, isMap, mapValuesAreObjects };
34
+ }
35
+
36
+ function buildCleanMapValue(cleanRecords: Record<string, unknown>[], keyHeader: string, mapValuesAreObjects: boolean): Record<string, unknown> {
37
+ return Object.fromEntries(cleanRecords.map((record) => {
38
+ const key = String(record[keyHeader]);
39
+ if (mapValuesAreObjects) {
40
+ const nested = { ...record };
41
+ delete nested[keyHeader];
42
+ return [key, nested];
43
+ }
44
+ return [key, record.value ?? ''];
45
+ }));
46
+ }
47
+
48
+ interface ProcessRecordParams {
49
+ record: JsonRowRecord;
50
+ headers: string[];
51
+ keyHeader: string;
52
+ seenKeys: Set<string>;
53
+ issues: SanitizerIssue[];
54
+ }
55
+
56
+ function processJsonRecord(params: ProcessRecordParams): { values: string[]; key: string; cleanRecord: Record<string, unknown> } | null {
57
+ const { record, headers, keyHeader, seenKeys, issues } = params;
58
+ const values = headers.map((header) => textValue(record.source[header]));
59
+ const key = (values[0] ?? '').trim();
60
+
61
+ if (!key) {
62
+ issues.push(issue({ kind: 'empty', row: record.row, column: keyHeader, key: '', detail: 'empty_key' }));
63
+ }
64
+ values.forEach((value, columnIndex) => {
65
+ if (columnIndex !== 0 && value.trim() === '') {
66
+ issues.push(issue({ kind: 'empty', row: record.row, column: headers[columnIndex] ?? '', key, detail: 'empty_translation' }));
67
+ }
68
+ });
69
+
70
+ if (key && seenKeys.has(key)) {
71
+ issues.push(issue({ kind: 'duplicate', row: record.row, column: keyHeader, key, detail: 'duplicate_key' }));
72
+ return null;
73
+ }
74
+ if (!key) return null;
75
+
76
+ seenKeys.add(key);
77
+ const cleanRecord: Record<string, unknown> = {};
78
+ headers.forEach((header, columnIndex) => {
79
+ cleanRecord[header] = columnIndex === 0 ? key : record.source[header] ?? '';
80
+ });
81
+ return { values, key, cleanRecord };
82
+ }
83
+
84
+ function resolveJsonHeaders(records: JsonRowRecord[]): { headers: string[]; keyHeader: string } {
85
+ const sourceHeaders = Array.from(new Set(records.flatMap((record) => Object.keys(record.source))));
86
+ const keyHeader = sourceHeaders.find((header) => /^(key|id|identifier|loc[_-]?key)$/i.test(header)) || sourceHeaders[0] || 'key';
87
+ return { headers: [keyHeader, ...sourceHeaders.filter((header) => header !== keyHeader)], keyHeader };
88
+ }
89
+
90
+ export function sanitizeJson(source: string): SanitizerResult {
91
+ const parsedData = parseJsonRecords(source);
92
+ if (!parsedData) return emptyResult('json', 'invalid_json');
93
+ const { records, isMap, mapValuesAreObjects } = parsedData;
94
+ if (records.length === 0) return emptyResult('json', 'expected_array_or_object');
95
+
96
+ const { headers, keyHeader } = resolveJsonHeaders(records);
97
+ const issues: SanitizerIssue[] = [];
98
+ const rows: string[][] = [];
99
+ const seenKeys = new Set<string>();
100
+ const cleanRecords: Record<string, unknown>[] = [];
101
+
102
+ records.forEach((record) => {
103
+ const res = processJsonRecord({ record, headers, keyHeader, seenKeys, issues });
104
+ if (res) { rows.push(res.values); cleanRecords.push(res.cleanRecord); }
105
+ });
106
+
107
+ return {
108
+ format: 'json', valid: true, headers, rows,
109
+ cleanText: JSON.stringify(isMap ? buildCleanMapValue(cleanRecords, keyHeader, mapValuesAreObjects) : cleanRecords, null, 2),
110
+ issues,
111
+ stats: {
112
+ totalRows: records.length, cleanRows: rows.length,
113
+ emptyCells: issues.filter((item) => item.kind === 'empty').length,
114
+ duplicateKeys: issues.filter((item) => item.kind === 'duplicate').length,
115
+ malformedRows: 0,
116
+ },
117
+ };
118
+ }