114114
115115 .card-actions { display : flex; gap : 10px ; justify-content : center; margin-top : 18px ; flex-wrap : wrap; }
116116
117+ .lang-toggle {
118+ position : fixed; top : 16px ; right : 18px ; z-index : 60 ;
119+ font-family : 'JetBrains Mono' , monospace; font-size : 12px ; font-weight : 700 ; letter-spacing : 1px ;
120+ color : # 4ade80 ; background : # 0f1d18 ; border : 1px solid # 1f4435 ; border-radius : 8px ;
121+ padding : 6px 12px ; cursor : pointer;
122+ }
123+ .lang-toggle : hover { border-color : # 34d399 ; }
124+
117125 /* ---------- toast ---------- */
118126 .toast {
119127 position : fixed; left : 50% ; bottom : 28px ; transform : translateX (-50% ) translateY (8px );
140148</ head >
141149< body >
142150
151+ < button class ="lang-toggle " id ="langBtn " type ="button "> EN</ button >
152+
143153< div id ="landing " class ="landing hidden ">
144154 < div class ="hero ">
145155 < div class ="kicker "> MMORPG Character Sheet · For Claude Users</ div >
146156 < h1 > Claude Status Check</ h1 >
147- < p > คุณใช้ Claude สายไหน? เช็คสเตตัส 6 ด้าน รับ class + level ของตัวเอง — ข้อมูลดิบไม่ออกจากเครื่องคุณ</ p >
157+ < p id =" heroDesc " > คุณใช้ Claude สายไหน? เช็คสเตตัส 6 ด้าน รับ class + level ของตัวเอง — ข้อมูลดิบไม่ออกจากเครื่องคุณ</ p >
148158 </ div >
149159 < div class ="entry-grid ">
150160 < div class ="entry ">
151- < h2 > สาย Claude Code</ h2 >
152- < div class ="sub "> รันคำสั่งเดียวใน terminal — สแกน transcript ในเครื่อง แล้วได้ลิงก์การ์ดทันที</ div >
161+ < h2 id =" entry1Title " > สาย Claude Code</ h2 >
162+ < div class ="sub " id =" entry1Sub " > รันคำสั่งเดียวใน terminal — สแกน transcript ในเครื่อง แล้วได้ลิงก์การ์ดทันที</ div >
153163 < div class ="cmd-row ">
154164 < div class ="cmd " id ="npxCmd "> npx github:MankhongGarden/claude-stats</ div >
155165 < button class ="btn " id ="copyCmdBtn " type ="button "> คัดลอก</ button >
156166 </ div >
157- < div class ="privacy "> < b > Privacy:</ b > สแกนในเครื่องเท่านั้น · ลิงก์แชร์มีแค่ตัวเลขสรุป 6 ค่า</ div >
167+ < div class ="privacy "> < b > Privacy:</ b > < span id =" privacy1Text " > สแกนในเครื่องเท่านั้น · ลิงก์แชร์มีแค่ตัวเลขสรุป 6 ค่า</ span > </ div >
158168 </ div >
159169 < div class ="entry ">
160- < h2 > สาย claude.ai < span class ="badge "> BETA</ span > </ h2 >
161- < div class ="sub "> ใช้ไฟล์ export จาก claude.ai — วิเคราะห์ในเบราว์เซอร์นี้ทั้งหมด</ div >
170+ < h2 > < span id =" entry2Title " > สาย claude.ai</ span > < span class ="badge "> BETA</ span > </ h2 >
171+ < div class ="sub " id =" entry2Sub " > ใช้ไฟล์ export จาก claude.ai — วิเคราะห์ในเบราว์เซอร์นี้ทั้งหมด</ div >
162172 < div class ="drop " id ="dropZone ">
163- < div class ="big "> วางไฟล์ conversations.json ที่นี่</ div >
164- < div class ="hint "> หรือคลิกเพื่อเลือกไฟล์</ div >
173+ < div class ="big " id =" dropBig " > วางไฟล์ conversations.json ที่นี่</ div >
174+ < div class ="hint " id =" dropHint " > หรือคลิกเพื่อเลือกไฟล์</ div >
165175 </ div >
166176 < input type ="file " id ="fileInput " accept =".json,application/json " class ="hidden ">
167- < div class ="steps "> ขอไฟล์: Settings → Privacy → Export data → ไฟล์จะส่งมาทางอีเมล แตก zip แล้วเลือก < code > conversations.json</ code > </ div >
168- < div class ="privacy "> < b > Privacy:</ b > ไฟล์ถูกอ่านในเครื่องเท่านั้น ไม่มีการอัปโหลด</ div >
177+ < div class ="steps " id =" steps " > ขอไฟล์: Settings → Privacy → Export data → ไฟล์จะส่งมาทางอีเมล แตก zip แล้วเลือก < code > conversations.json</ code > </ div >
178+ < div class ="privacy "> < b > Privacy:</ b > < span id =" privacy2Text " > ไฟล์ถูกอ่านในเครื่องเท่านั้น ไม่มีการอัปโหลด</ span > </ div >
169179 </ div >
170180 </ div >
171181 < div class ="demo-row ">
@@ -213,6 +223,97 @@ <h2>สาย claude.ai <span class="badge">BETA</span></h2>
213223 var current = null ;
214224 var toastTimer = null ;
215225
226+ /* ---------- i18n ---------- */
227+
228+ var STR = {
229+ th : {
230+ heroDesc : 'คุณใช้ Claude สายไหน? เช็คสเตตัส 6 ด้าน รับ class + level ของตัวเอง — ข้อมูลดิบไม่ออกจากเครื่องคุณ' ,
231+ entry1Title : 'สาย Claude Code' ,
232+ entry1Sub : 'รันคำสั่งเดียวใน terminal — สแกน transcript ในเครื่อง แล้วได้ลิงก์การ์ดทันที' ,
233+ privacy1Text : 'สแกนในเครื่องเท่านั้น · ลิงก์แชร์มีแค่ตัวเลขสรุป 6 ค่า' ,
234+ entry2Title : 'สาย claude.ai' ,
235+ entry2Sub : 'ใช้ไฟล์ export จาก claude.ai — วิเคราะห์ในเบราว์เซอร์นี้ทั้งหมด' ,
236+ dropBig : 'วางไฟล์ conversations.json ที่นี่' ,
237+ dropHint : 'หรือคลิกเพื่อเลือกไฟล์' ,
238+ steps : 'ขอไฟล์: Settings → Privacy → Export data → ไฟล์จะส่งมาทางอีเมล แตก zip แล้วเลือก ' ,
239+ privacy2Text : 'ไฟล์ถูกอ่านในเครื่องเท่านั้น ไม่มีการอัปโหลด' ,
240+ demoLink : 'ดูการ์ดตัวอย่างก่อน →' ,
241+ copyCmdBtn : 'คัดลอก' ,
242+ pngBtn : 'ดาวน์โหลดการ์ด PNG' ,
243+ copyLinkBtn : 'คัดลอกลิงก์แชร์' ,
244+ resetBtn : 'เริ่มใหม่' ,
245+ badLink : 'ลิงก์ไม่ถูกต้องหรือเสียหาย — กลับสู่หน้าแรก' ,
246+ copiedCmd : 'คัดลอกคำสั่งแล้ว — ไปวางใน terminal ได้เลย' ,
247+ copiedLink : 'คัดลอกลิงก์แล้ว — ในลิงก์มีแค่ตัวเลขสรุป 6 ค่า' ,
248+ copyFail : 'คัดลอกไม่สำเร็จ — copy เองได้เลย: ' ,
249+ readFail : 'อ่านไฟล์ไม่สำเร็จ ลองใหม่อีกครั้ง' ,
250+ badFormat : 'รูปแบบไฟล์ไม่ตรงที่คาด (parser ยังเป็น beta)' ,
251+ issueLink : 'แจ้งปัญหาบน GitHub' ,
252+ cdnFail : 'โหลดตัวสร้างรูปไม่สำเร็จ (CDN) ลอง refresh' ,
253+ pngDone : 'ดาวน์โหลดการ์ดแล้ว' ,
254+ pngFail : 'สร้างรูปไม่สำเร็จ ลองใหม่อีกครั้ง' ,
255+ scoringFail : 'โหลด lib/scoring.js ไม่สำเร็จ'
256+ } ,
257+ en : {
258+ heroDesc : 'Which Claude build are you? Check your 6 stats and get your class + level — raw data never leaves your machine.' ,
259+ entry1Title : 'Claude Code users' ,
260+ entry1Sub : 'One command in your terminal — scans local transcripts, prints your card and a share link.' ,
261+ privacy1Text : 'Scans locally only · the share link carries just 6 aggregate numbers' ,
262+ entry2Title : 'claude.ai users' ,
263+ entry2Sub : 'Use your claude.ai data export — analyzed entirely in this browser.' ,
264+ dropBig : 'Drop conversations.json here' ,
265+ dropHint : 'or click to choose a file' ,
266+ steps : 'Get the file: Settings → Privacy → Export data → it arrives by email; unzip and pick ' ,
267+ privacy2Text : 'The file is read locally — nothing is uploaded' ,
268+ demoLink : 'See a sample card first →' ,
269+ copyCmdBtn : 'Copy' ,
270+ pngBtn : 'Download card PNG' ,
271+ copyLinkBtn : 'Copy share link' ,
272+ resetBtn : 'Start over' ,
273+ badLink : 'Invalid or corrupted link — back to home' ,
274+ copiedCmd : 'Command copied — paste it in your terminal' ,
275+ copiedLink : 'Link copied — it carries only the 6 aggregate numbers' ,
276+ copyFail : 'Copy failed — grab it manually: ' ,
277+ readFail : 'Could not read the file, please try again' ,
278+ badFormat : 'Unexpected file format (the parser is still beta)' ,
279+ issueLink : 'Report on GitHub' ,
280+ cdnFail : 'Image library failed to load (CDN) — try refreshing' ,
281+ pngDone : 'Card downloaded' ,
282+ pngFail : 'Could not generate the image, please try again' ,
283+ scoringFail : 'Failed to load lib/scoring.js'
284+ }
285+ } ;
286+
287+ var lang = ( function ( ) {
288+ try {
289+ var saved = localStorage . getItem ( 'cs_lang' ) ;
290+ if ( saved === 'th' || saved === 'en' ) return saved ;
291+ } catch ( e ) { }
292+ return ( navigator . language || '' ) . toLowerCase ( ) . indexOf ( 'th' ) === 0 ? 'th' : 'en' ;
293+ } ) ( ) ;
294+
295+ function t ( key ) {
296+ return ( STR [ lang ] && STR [ lang ] [ key ] ) || STR . th [ key ] || key ;
297+ }
298+
299+ function applyLang ( ) {
300+ document . documentElement . lang = lang ;
301+ [ 'heroDesc' , 'entry1Title' , 'entry1Sub' , 'privacy1Text' , 'entry2Title' , 'entry2Sub' ,
302+ 'dropBig' , 'dropHint' , 'privacy2Text' , 'demoLink' ,
303+ 'copyCmdBtn' , 'pngBtn' , 'copyLinkBtn' , 'resetBtn' ] . forEach ( function ( id ) {
304+ var node = document . getElementById ( id ) ;
305+ if ( node ) node . textContent = t ( id ) ;
306+ } ) ;
307+ var steps = document . getElementById ( 'steps' ) ;
308+ while ( steps . firstChild ) steps . removeChild ( steps . firstChild ) ;
309+ steps . appendChild ( document . createTextNode ( t ( 'steps' ) ) ) ;
310+ var code = document . createElement ( 'code' ) ;
311+ code . textContent = 'conversations.json' ;
312+ steps . appendChild ( code ) ;
313+ document . getElementById ( 'langBtn' ) . textContent = lang === 'th' ? 'EN' : 'ไทย' ;
314+ if ( current ) renderCard ( current ) ;
315+ }
316+
216317 function el ( tag , cls , text ) {
217318 var e = document . createElement ( tag ) ;
218319 if ( cls ) e . className = cls ;
@@ -225,7 +326,7 @@ <h2>สาย claude.ai <span class="badge">BETA</span></h2>
225326 toastEl . appendChild ( document . createTextNode ( msg ) ) ;
226327 if ( issueLink ) {
227328 toastEl . appendChild ( document . createTextNode ( ' ' ) ) ;
228- var a = el ( 'a' , null , 'แจ้งปัญหาบน GitHub' ) ;
329+ var a = el ( 'a' , null , t ( 'issueLink' ) ) ;
229330 a . href = 'https://github.com/MankhongGarden/claude-stats/issues' ;
230331 a . target = '_blank' ;
231332 a . rel = 'noopener' ;
@@ -247,7 +348,7 @@ <h2>สาย claude.ai <span class="badge">BETA</span></h2>
247348 var ok = false ;
248349 try { ok = document . execCommand ( 'copy' ) ; } catch ( e ) { ok = false ; }
249350 document . body . removeChild ( ta ) ;
250- toast ( ok ? okMsg : 'คัดลอกไม่สำเร็จ — copy เองได้เลย: ' + text ) ;
351+ toast ( ok ? okMsg : t ( 'copyFail' ) + text ) ;
251352 }
252353 if ( navigator . clipboard && navigator . clipboard . writeText ) {
253354 navigator . clipboard . writeText ( text ) . then ( function ( ) { toast ( okMsg ) ; } , fallback ) ;
@@ -278,7 +379,7 @@ <h2>สาย claude.ai <span class="badge">BETA</span></h2>
278379 var lv = S . level ( effort ) ;
279380
280381 document . getElementById ( 'className' ) . textContent = cls [ 0 ] ;
281- document . getElementById ( 'classSub' ) . textContent = cls [ 1 ] ;
382+ document . getElementById ( 'classSub' ) . textContent = ( lang === 'th' ? cls [ 1 ] : cls [ 2 ] ) || cls [ 1 ] ;
282383 document . getElementById ( 'lv' ) . textContent = String ( lv ) ;
283384 document . getElementById ( 'srcBadge' ) . textContent = isWeb ? 'WEB · BETA' : 'CLI' ;
284385
@@ -287,7 +388,9 @@ <h2>สาย claude.ai <span class="badge">BETA</span></h2>
287388 S . AXES . forEach ( function ( k ) {
288389 var li = document . createElement ( 'li' ) ;
289390 var label = el ( 'span' , 'stat-label' , S . META [ k ] . en ) ;
290- label . appendChild ( el ( 'small' , null , S . META [ k ] . th + ' · ' + S . META [ k ] . desc ) ) ;
391+ label . appendChild ( el ( 'small' , null , lang === 'th'
392+ ? S . META [ k ] . th + ' · ' + S . META [ k ] . desc
393+ : S . META [ k ] . descEn || S . META [ k ] . desc ) ) ;
291394 var bar = el ( 'span' , 'bar' ) ;
292395 var fill = el ( 'i' ) ;
293396 fill . style . width = stats [ k ] + '%' ;
@@ -349,7 +452,7 @@ <h2>สาย claude.ai <span class="badge">BETA</span></h2>
349452 if ( h . indexOf ( '#s=' ) === 0 ) {
350453 var payload = S . decodeShare ( h . slice ( 3 ) ) ;
351454 if ( payload ) { renderCard ( payload ) ; return ; }
352- toast ( 'ลิงก์ไม่ถูกต้องหรือเสียหาย — กลับสู่หน้าแรก' ) ;
455+ toast ( t ( 'badLink' ) ) ;
353456 }
354457 showLanding ( ) ;
355458 }
@@ -410,7 +513,7 @@ <h2>สาย claude.ai <span class="badge">BETA</span></h2>
410513 function handleFile ( file ) {
411514 if ( ! file ) return ;
412515 var reader = new FileReader ( ) ;
413- reader . onerror = function ( ) { toast ( 'อ่านไฟล์ไม่สำเร็จ ลองใหม่อีกครั้ง' ) ; } ;
516+ reader . onerror = function ( ) { toast ( t ( 'readFail' ) ) ; } ;
414517 reader . onload = function ( ) {
415518 var parsed = null ;
416519 try {
@@ -420,7 +523,7 @@ <h2>สาย claude.ai <span class="badge">BETA</span></h2>
420523 parsed = null ;
421524 }
422525 if ( ! parsed ) {
423- toast ( 'รูปแบบไฟล์ไม่ตรงที่คาด (parser ยังเป็น beta)' , true ) ;
526+ toast ( t ( 'badFormat' ) , true ) ;
424527 return ;
425528 }
426529 var stats = S . scoreAxes ( parsed . raw , 'web' ) ;
@@ -433,7 +536,7 @@ <h2>สาย claude.ai <span class="badge">BETA</span></h2>
433536 /* ---------- wire up ---------- */
434537
435538 document . getElementById ( 'copyCmdBtn' ) . addEventListener ( 'click' , function ( ) {
436- copyText ( 'npx github:MankhongGarden/claude-stats' , 'คัดลอกคำสั่งแล้ว — ไปวางใน terminal ได้เลย' ) ;
539+ copyText ( 'npx github:MankhongGarden/claude-stats' , t ( 'copiedCmd' ) ) ;
437540 } ) ;
438541
439542 var dropZone = document . getElementById ( 'dropZone' ) ;
@@ -455,7 +558,7 @@ <h2>สาย claude.ai <span class="badge">BETA</span></h2>
455558 } ) ;
456559
457560 document . getElementById ( 'copyLinkBtn' ) . addEventListener ( 'click' , function ( ) {
458- copyText ( window . location . href , 'คัดลอกลิงก์แล้ว — ในลิงก์มีแค่ตัวเลขสรุป 6 ค่า' ) ;
561+ copyText ( window . location . href , t ( 'copiedLink' ) ) ;
459562 } ) ;
460563
461564 document . getElementById ( 'resetBtn' ) . addEventListener ( 'click' , function ( ) {
@@ -466,7 +569,7 @@ <h2>สาย claude.ai <span class="badge">BETA</span></h2>
466569 } ) ;
467570
468571 document . getElementById ( 'pngBtn' ) . addEventListener ( 'click' , function ( ) {
469- if ( ! window . htmlToImage ) { toast ( 'โหลดตัวสร้างรูปไม่สำเร็จ (CDN) ลอง refresh' ) ; return ; }
572+ if ( ! window . htmlToImage ) { toast ( t ( 'cdnFail' ) ) ; return ; }
470573 var node = document . getElementById ( 'cardNode' ) ;
471574 var topKey = current ? S . topAxis ( current . stats ) : S . AXES [ 0 ] ;
472575 var effort = current
@@ -480,15 +583,22 @@ <h2>สาย claude.ai <span class="badge">BETA</span></h2>
480583 a . download = filename ;
481584 a . href = dataUrl ;
482585 a . click ( ) ;
483- toast ( 'ดาวน์โหลดการ์ดแล้ว' ) ;
586+ toast ( t ( 'pngDone' ) ) ;
484587 } )
485- . catch ( function ( ) { toast ( 'สร้างรูปไม่สำเร็จ ลองใหม่อีกครั้ง' ) ; } ) ;
588+ . catch ( function ( ) { toast ( t ( 'pngFail' ) ) ; } ) ;
589+ } ) ;
590+
591+ document . getElementById ( 'langBtn' ) . addEventListener ( 'click' , function ( ) {
592+ lang = lang === 'th' ? 'en' : 'th' ;
593+ try { localStorage . setItem ( 'cs_lang' , lang ) ; } catch ( e ) { }
594+ applyLang ( ) ;
486595 } ) ;
487596
488597 window . addEventListener ( 'hashchange' , route ) ;
489598
599+ applyLang ( ) ;
490600 if ( ! S ) {
491- toast ( 'โหลด lib/scoring.js ไม่สำเร็จ' ) ;
601+ toast ( t ( 'scoringFail' ) ) ;
492602 showLanding ( ) ;
493603 } else {
494604 route ( ) ;
0 commit comments