
ส่งมอบสามสิบภาษาโดยไม่มีตาข่ายรองรับ
GeekBye v1.8.3 ต่อ react-i18next เข้าไปและแปลทั้งแอปเป็น 30 ภาษา ส่วนที่เป็น library นั้นเป็นงานปกติ ส่วนที่น่าสนใจคือสิ่งที่ขาดหายไป: ไม่มีการเช็ก key parity และไม่มีตัวดึงสตริง — ภาษาอังกฤษที่ hardcode ไว้จึงหลุดออกมา, typo ภาษาเดนมาร์กหลุดขึ้น production, และ changelog เองก็ตกลงกันไม่ได้ว่าแอปพูดได้ 28, 29 หรือ 30 ภาษากันแน่
บางรีลีสคือฟีเจอร์ GeekBye v1.8.3 หน้าตาเหมือนเป็นแบบนั้น — "ตอนนี้แอปพูดได้ 30 ภาษา" — แต่ฟีเจอร์คือส่วนที่ง่าย และมันไม่ใช่สิ่งที่โพสต์นี้จะพูดถึง ระหว่างวันที่ 17 ถึง 19 กุมภาพันธ์ 2026 pull request สองตัวต่อ react-i18next เข้าไปในแอป, รีแฟกเตอร์สตริงภาษาอังกฤษที่ hardcode ไว้ทุกตัวให้กลายเป็น translation call, และรวมไฟล์ locale สามสิบไฟล์เข้ามา นั่นคืองานจริง แต่มันเป็นงานชนิดที่ library ถูกออกแบบมาให้กลายเป็นเรื่องปกติ ส่วนวิศวกรรมที่น่าสนใจคือทุกอย่างที่รีลีสนี้_ไม่ได้_สร้าง — ตาข่ายรองรับที่น่าจะดักสิ่งที่มัน ship ออกมาแบบพังไว้ได้
การต่อสายคือ 80% ที่ง่าย
คอมมิต infrastructure เพิ่ม react-i18next ทับบน i18next และทำสิ่งที่เป็นแบบแผน: i18n/index.ts ที่เรียก initReactI18next, import ไฟล์ locale JSON ทั้งสามสิบตัวแบบ static, และลงทะเบียนพวกมันไว้ใต้ออบเจกต์ resources ตัวเดียว ไม่มี lazy loading — ทุกภาษาถูก ship มาใน bundle สตริงคำแปลอยู่ใน src/i18n/locales/*.json ในรูป map แบบ flat ที่ namespace ด้วยจุด: sidebar.meetings, startMeeting.start, หนึ่ง key ต่อหนึ่งสตริง
การย้ายจากภาษาอังกฤษที่ hardcode ไว้ไปเป็น t('key') ทำแบบตรงไปตรงมา ทีละกลุ่มคอมโพเนนต์ และคุณอ่านการกวาดทั้งหมดได้จาก commit log: sidebar, navigation และ chat ก่อน; ตามด้วย settings, login และ onboarding; แล้วจึง meetings, dashboard, toast และ tooltip ของ navbar แต่ละคอมมิตแทนสตริงตัวอักษรตรง ๆ ยกชุดด้วย translation call ไฟล์ที่ไม่ใช่ภาษาอังกฤษ 29 ไฟล์ลงจอดในคอมมิตเดียว — ก้อนเก้าพันบรรทัดที่แปลต้นฉบับภาษาอังกฤษเป็นทุกอย่างตั้งแต่บัลแกเรียไปจนถึงเวียดนาม
ไม่มีอะไรในนั้นที่ทำให้ถูกได้ยาก เพราะการทำมัน_ผิด_นั้นเสียงดัง: provider ที่ขาดไปทำให้ crash, JSON ที่ผิดรูปแบบ parse ไม่ผ่าน, import ที่ผิดทำให้ build พัง library ให้ราวกันตกสำหรับความผิดพลาดเชิงกลไกแก่คุณ สิ่งที่มันไม่ได้ให้คือราวกันตกสำหรับความผิดพลาดที่เกิดขึ้นจริง
สามการตั้งค่าภาษา และทำไมนั่นถึงถูกต้อง
ก่อนจะไปถึงสิ่งที่พัง มีอย่างหนึ่งที่ v1.8.3 ทำถูกเป๊ะ เพราะมันคือความแตกต่างที่แอปจำนวนมากยุบรวมเข้าด้วยกันทั้งที่ไม่ควร หลังรีลีสนี้ GeekBye มีการตั้งค่าภาษาที่เป็นอิสระต่อกัน_สาม_ตัว:
- ภาษาที่ AI ตอบ (
output_language) — ภาษาที่ผู้ช่วยตอบกลับ เก่าแก่ที่สุดในสามตัว มาตั้งแต่พฤศจิกายน 2025 - ภาษาของ transcript (
transcriptLanguage) — ภาษาที่ speech engine ถอดเสียงจากไมโครโฟนของคุณออกมา ต่อเข้ามาก่อนหน้านั้นไม่กี่รีลีส ใน v1.7.6 - ภาษาของแอป (
app_language) — ภาษาของปุ่ม, เมนู และ onboarding ใหม่ใน v1.8.3
พวกนี้เป็นแกนที่แตกต่างกันจริง ๆ นักพัฒนาในเบอร์ลินอาจอยากได้อินเทอร์เฟซภาษาเยอรมัน, คำตอบจาก AI เป็นภาษาอังกฤษ (เพราะนั่นคือภาษาของเอกสารที่เขากำลัง paste อยู่), และ transcription เป็นภาษาอังกฤษ (เพราะมีตติงเป็นภาษาอังกฤษ) การยุบสิ่งเหล่านั้นเข้าเป็นการตั้งค่า "ภาษา" ตัวเดียวจะผิดสำหรับคน ๆ นั้น และขั้นตอน onboarding ที่เพิ่มในรีลีสนี้ก็นำเสนอทั้งสามตัวเป็น dropdown แยกกันด้วยเหตุผลนั้นพอดี
จุดที่น่าประทับใจคือเส้นทางการอัปเกรด migration ครั้งเดียวคัดลอกค่า output_language ที่มีอยู่ของผู้ใช้แต่ละคนไปยัง key app_language ตัวใหม่ในการเปิดครั้งแรกหลังอัปเดต และ provider ก็คลี่คลายภาษาผ่าน fallback chain — app_language, แล้วจึง output_language, แล้วจึง 'en' ดังนั้นผู้ใช้ที่เคยบอกแอปไปแล้วว่าอยากได้คำตอบ AI เป็นภาษาสเปน ก็ตื่นมาพบอินเทอร์เฟซภาษาสเปน แทนที่จะถูกรีเซ็ตกลับเป็นภาษาอังกฤษแล้วต้องไปนั่งงมหาการตั้งค่า การ seed ค่า preference ตัวใหม่จากตัวเก่าที่เกี่ยวข้องกันคือความเมตตาเล็ก ๆ ที่ราคาแค่ migration เดียว แต่ช่วยให้ทุกคนที่อัปเกรดไม่ต้องเจ็บใจนิด ๆ นั้น
การ re-render ตอน onboarding ที่ไม่ยอมเกิด
มีบั๊กจริงหนึ่งตัวจากงาน i18n ที่ควรหยิบออกมา เพราะมันคือกับดักของ React ที่ใครก็ตามที่ทำการสลับภาษาแบบ runtime จะต้องเจอ onboarding wizard สร้างรายการตัวเลือกบางส่วนของมันไว้ข้างใน useMemo เมื่อคุณเปลี่ยนภาษา_บนขั้นตอนเลือกภาษาเอง_ UI ควรจะวาดใหม่เป็นภาษาใหม่ทันที — แต่มันไม่ เพราะ t ไม่ได้อยู่ใน dependency array ของ memo ฟังก์ชัน translation จะเปลี่ยน identity เมื่อภาษาเปลี่ยน; ถ้าค่า memoized ของคุณสร้างมาจาก t แต่ไม่ได้ขึ้นอยู่กับมัน React ก็ยินดียื่นเวอร์ชันเก่าของภาษาก่อนหน้าให้คุณ ตัวแก้ปรากฏสองครั้งใน log — ครั้งหนึ่งสำหรับ wizard, อีกครั้งสำหรับรายการ dropdown ใน settings — ซึ่งบอกคุณว่ามันคือความผิดพลาดชนิดที่คุณจะทำในทุกที่ที่คุณ memoize เนื้อหาที่แปลแล้ว จนกว่าคุณจะเรียนรู้ที่จะใส่ t เข้าไปเสมอ
onboarding ยังพก wrinkle ที่ changelog ไม่ได้เอ่ยถึง: เพราะนี่คือ codebase แบบ white-label จึงมีต้นไม้ onboarding _สอง_ต้นให้แปล — wizard ของ GeekBye และของ Pavleur — อยู่ใต้ namespace ของ key คู่ขนานกันคือ onboarding.* และ onboarding.pavleur.* ทุกสตริงของ onboarding ถูกแปลสองรอบ เป็นเสียงสองแบรนด์ ครบทั้งยี่สิบเก้าภาษา
สิ่งที่ build ไม่รู้ตัวว่าตัวเองขาด
นี่คือส่วนที่ทำให้เรื่องนี้เป็นบทเรียนแทนที่จะเป็นแค่ checklist v1.8.3 ไม่มีวิธีอัตโนมัติในการดึงสตริงที่ hardcode ไว้ และไม่มีการเช็ก key parity ใน CI ข้ามไฟล์ locale ดังนั้นนิยามของคำว่า "เสร็จ" คือ "มีคนไปเปิดดูคอมโพเนนต์ที่ตัวเองจำได้ แล้วห่อสตริงที่ตัวเองเห็น" นิยามแบบนั้นมันรั่ว
สองสัปดาห์หลังรีลีส คอมมิต cleanup ก็ลงจอดพร้อมข้อความ "translate all hardcoded English strings across 10 components into 30 languages." สิบคอมโพเนนต์ — ฟอร์มรายงานปัญหา, แท็บ summary และ metrics, แท็บ report, เนื้อหา transcript และ chat, prompt แจ้งเตือนถึงขีดจำกัดการใช้งาน, ข้อความ chat, ปุ่ม copy-code, ปุ่ม floating action — ถูก ship มาใน v1.8.3 โดยยังพูดภาษาอังกฤษกับทุกคน ในทุกภาษา build ของใครก็ไม่พัง ไม่มีเทสต์ตัวไหนเป็นสีแดง ผู้ใช้ชาวเยอรมันแค่เห็นภาษาอังกฤษในแท็บ report และตัวตรวจจับเดียวที่มีคือมนุษย์ที่บังเอิญไปเห็นมันเข้าในที่สุด
การขาดหายตัวเดียวกันนี้ก็ผลิตความน่าอายชิ้นเล็กกว่าออกมา คำแปลภาษาเดนมาร์กถูก ship มาพร้อมคำที่ผิด — Luk application helt แทนที่จะเป็น Luk applikationen helt — และถูกแก้ในรีลีสถัดมา เพราะไม่มีด่านให้เจ้าของภาษาตรวจ และไม่มีทางที่ pipeline จะรู้ได้ key สำหรับ "quit app" หายไปจากชุดแรก และถูกเติมกลับหลายวันต่อมา และทุก ๆ ฟีเจอร์ที่ ship ตามหลังมา — preset ปุ่ม modifier, mic mute, recap chip, ความทึบของ overlay — ล้วนลากคอมมิต "แปล key ใหม่เข้า 29 locale" ของตัวเองตามหลังมาด้วย ไม่มีวันจบ เพราะไม่มีอะไรบังคับใช้ invariant นั้นโดยอัตโนมัติ
คุณวัดความยุ่งเหยิงได้จากข้อความคอมมิตด้วยซ้ำ ไฟล์ locale สามสิบไฟล์ชุดเดียวกันถูกอธิบาย รีลีสแล้วรีลีสเล่า ว่าเป็น "30 languages," "29 languages," และ — ในคอมมิตหนึ่ง — "28 locales" ไม่มีอะไรถูกเพิ่มหรือลบ; มีไฟล์สามสิบไฟล์ที่ทุก tag ตั้งแต่ v1.8.3 ไปจนถึง v2 ตัวเลขมันไถลไปเพราะไม่มีแหล่งความจริงเดียวสำหรับ "เรา ship กี่ภาษา" ดังนั้นผู้เขียนคอมมิตแต่ละคนจึงนับด้วยมือ บางทีก็นับภาษาอังกฤษเป็นหนึ่งภาษา บางทีก็นับเป็นต้นฉบับ changelog ที่รักษาตัวเลขของตัวเองให้นิ่งไม่ได้คือสัญญาณเล็ก ๆ ที่ซื่อสัตย์ว่าสิ่งที่มันอธิบายอยู่นั้นไม่ได้ถูกบังคับใช้ด้วยเครื่องจักร
กับระเบิดของ pluralization
อีกหนึ่งอย่างที่ควรชี้ให้เห็น ไม่ใช่เพราะมันพังเสียงดัง แต่เพราะมันคือบั๊กชนิดที่การจัดวางแบบนี้เชื้อเชิญเข้ามาและจะไม่เตือนคุณ การทำ pluralization ในไฟล์ locale เหล่านี้ทำด้วยมือ ด้วยการจับคู่ key: meetings.meetingCount กับ meetings.meetingCountPlural, profiles.filesAttached กับ filesAttachedPlural นั่นคือการแยกเอกพจน์/พหูพจน์แบบอังกฤษที่สะอาดหมดจด แต่มันก็ผิดสำหรับภาษาส่วนหนึ่งที่แอปตอนนี้อ้างว่ารองรับ ภาษารัสเซีย โปแลนด์ และเช็ก มีรูปพหูพจน์สามหรือสี่แบบขึ้นอยู่กับจำนวน — "1 file," "2 files," "5 files," และ "21 files" อาจใช้ท้ายคำที่ต่างกัน scheme เอกพจน์/พหูพจน์แบบสอง key แสดงสิ่งนั้นไม่ได้ ตัวเลขพวกนั้นจึงถูก render ออกมาผิดหลักไวยากรณ์ i18next มีกลไก plural จริงจังไว้สำหรับกรณีนี้พอดี; การจับคู่ด้วยมือหลบเลี่ยงมันไป และไม่มีคอมมิตไหนยอมรับช่องว่างนี้ มันคือ pluralization เวอร์ชันของการรั่วของสตริงที่ hardcode ไว้: มองไม่เห็นจาก build เห็นได้เฉพาะเจ้าของภาษาเท่านั้น
สามสิ่งที่รีลีสนี้สอนเรา
- library คือ 80% ที่ง่าย; เครื่องมือคือ 20% ที่ยาก การต่อ
react-i18nextแล้วแปลสตริงยกชุดคือปัญหาที่แก้แล้ว การรักษาไฟล์สามสิบตัวให้สอดคล้องกันตลอดไปไม่ใช่ — และมันต้องการตัวดึงสตริงที่ hardcode ไว้กับการเช็ก key ที่ขาดใน CI ซึ่งไม่มี library ตัวไหน ship มาให้คุณ สร้างพวกนั้น_ก่อน_ที่คุณจะอ้างว่า "i18n เต็มรูปแบบ" ไม่งั้นคุณก็อ้าง invariant ที่คุณไม่มีทางรักษาไว้ได้ - แยกการตั้งค่าภาษาที่ต่างกันจริง ๆ ออกจากกัน ภาษาของ UI, ภาษาของ transcript และภาษาที่ AI ตอบ คือสามแกน และ v1.8.3 ก็ถูกแล้วที่ให้แต่ละตัวมี key ของตัวเอง — และถูกยิ่งกว่านั้นที่ seed ตัวใหม่จากตัวเก่าที่เกี่ยวข้องผ่าน migration การอัปเกรดจึงไม่มีค่าใช้จ่ายอะไรกับผู้ใช้
- ตัวเลขใน changelog ของคุณคือ metric ของวินัย เมื่อไฟล์ชุดเดียวกันเป็น 28, 29 และ 30 ภาษาข้ามสามคอมมิต ตัวเลขไม่ใช่ปัญหา — แหล่งความจริงที่ขาดหายต่างหาก ถ้าไม่มีเครื่องจักรนับภาษาให้คุณ ก็ไม่มีใครนับมันเหมือนกันได้สองครั้ง
สำหรับบทก่อนหน้าของเรื่องยุค v1 ดูออกแบบ Dashboard ใหม่โดยยึด wave chart หนึ่งอันเป็นหัวใจ (v1.8.2); และสำหรับภาพรวมทั้งเส้นเรื่อง ดูกายวิภาคของการส่งมอบซอฟต์แวร์ให้สมบูรณ์แบบ