
Ship Ba Mươi Ngôn Ngữ Mà Không Có Lưới An Toàn
GeekBye v1.8.3 đấu nối react-i18next và dịch toàn bộ ứng dụng sang 30 ngôn ngữ. Phần thư viện chỉ là việc thường ngày. Phần thú vị nằm ở những gì còn thiếu: không có kiểm tra tính đồng nhất của key và không có công cụ trích xuất chuỗi — nên tiếng Anh hardcode lọt lưới, một lỗi chính tả tiếng Đan Mạch bị ship, và cái changelog thì không thống nhất nổi rằng ứng dụng nói 28, 29, hay 30 ngôn ngữ.
Có những bản phát hành là một tính năng. GeekBye v1.8.3 nhìn thì như vậy — "ứng dụng giờ nói được 30 ngôn ngữ" — nhưng cái tính năng ấy mới là phần dễ, và nó không phải điều bài viết này bàn tới. Giữa ngày 17 và 19 tháng Hai 2026, hai pull request đấu nối react-i18next vào ứng dụng, refactor mọi chuỗi tiếng Anh hardcode thành một lời gọi dịch, và đóng gói ba mươi file locale. Đó là việc thật, nhưng là loại việc mà một thư viện sinh ra để biến thành thường ngày. Phần kỹ thuật thú vị lại là mọi thứ mà bản phát hành không dựng — cái lưới an toàn lẽ ra đã bắt được những gì nó ship kèm lỗi.
Phần đấu nối là 80% dễ
Commit hạ tầng thêm react-i18next lên trên i18next và làm đúng bài quy ước: một i18n/index.ts gọi initReactI18next, import tĩnh toàn bộ ba mươi file JSON locale, và đăng ký chúng dưới một object resources duy nhất. Không lazy loading — mọi ngôn ngữ đều nằm trong bundle. Các chuỗi dịch sống trong src/i18n/locales/*.json dưới dạng map phẳng, đặt tên theo namespace bằng dấu chấm: sidebar.meetings, startMeeting.start, một key ứng một chuỗi.
Việc di dời từ tiếng Anh hardcode sang t('key') được làm một cách ngay thẳng, theo từng nhóm component, và bạn có thể đọc cả cuộc quét ấy trong commit log: sidebar, navigation, và chat trước; rồi settings, login, và onboarding; rồi meetings, dashboard, toast, và tooltip navbar. Mỗi commit thay một lô chuỗi văn bản thô bằng lời gọi dịch. Hai mươi chín file khác tiếng Anh đáp xuống trong một commit duy nhất — một cú thả chín nghìn dòng dịch nguồn tiếng Anh sang đủ thứ, từ tiếng Bulgaria tới tiếng Việt.
Chẳng có gì trong đó khó làm cho đúng, vì làm sai thì ồn ào lắm: thiếu một provider là crash, một JSON hỏng là parse lỗi, một import xấu là gãy bản build. Thư viện cho bạn thanh chắn cho những lỗi cơ học. Cái nó không cho bạn là một thanh chắn cho đúng loại lỗi đã thực sự xảy ra.
Ba thiết lập ngôn ngữ, và vì sao thế là đúng
Trước khi tới phần hỏng hóc, có một thứ v1.8.3 làm hoàn toàn đúng, vì đó là một phân biệt mà nhiều ứng dụng gộp bừa lại và lẽ ra không nên. Sau bản phát hành này, GeekBye có ba thiết lập ngôn ngữ độc lập:
- Ngôn ngữ phản hồi AI (
output_language) — thứ tiếng trợ lý dùng để trả lời. Cổ nhất trong ba, từ tháng Mười Một 2025. - Ngôn ngữ transcript (
transcriptLanguage) — thứ tiếng mà engine giọng nói dùng để phiên âm microphone của bạn. Được đấu nối vài bản trước, ở v1.7.6. - Ngôn ngữ ứng dụng (
app_language) — thứ tiếng của các nút, menu, và onboarding. Mới có ở v1.8.3.
Đây thật sự là những trục khác nhau. Một lập trình viên ở Berlin có thể muốn một giao diện tiếng Đức, câu trả lời AI bằng tiếng Anh (vì tài liệu họ đang dán vào là tiếng Anh), và phiên âm tiếng Anh (vì cuộc họp diễn ra bằng tiếng Anh). Gộp tất cả vào một thiết lập "ngôn ngữ" duy nhất sẽ sai với người đó, và bước onboarding thêm vào ở bản này trình bày cả ba như những dropdown riêng biệt chính vì lý do ấy.
Nét tinh tế nằm ở đường nâng cấp. Một migration một-lần chép output_language hiện có của từng người dùng vào key app_language mới ở lần khởi chạy đầu tiên sau khi cập nhật, và provider phân giải ngôn ngữ qua một chuỗi dự phòng — app_language, rồi output_language, rồi 'en'. Nhờ vậy những người dùng vốn đã báo với ứng dụng rằng họ muốn câu trả lời AI tiếng Tây Ban Nha thức dậy với một giao diện tiếng Tây Ban Nha, thay vì bị reset về tiếng Anh và phải đi lùng cái thiết lập. Gieo một tùy chọn mới từ một tùy chọn cũ có liên quan là một sự ân cần nhỏ, tốn một migration mà tiết kiệm cho mỗi người nâng cấp một nỗi bực bội vặt.
Cú re-render onboarding lẽ ra phải xảy ra mà không
Có một con bug thực thụ đáng lôi ra khỏi công việc i18n, vì nó là một cái bẫy React mà bất kỳ ai làm chuyển ngôn ngữ lúc runtime đều sẽ vấp. Onboarding wizard dựng vài danh sách tùy chọn của nó bên trong một useMemo. Khi bạn đổi ngôn ngữ ngay tại chính bước chọn ngôn ngữ, giao diện lẽ ra phải vẽ lại tức thì bằng ngôn ngữ mới — nhưng không, vì t không nằm trong mảng phụ thuộc của memo. Hàm dịch đổi danh tính khi ngôn ngữ đổi; nếu giá trị được memo hóa của bạn dựng từ t nhưng không phụ thuộc vào nó, React vui vẻ trao cho bạn bản cũ, của ngôn ngữ trước. Bản sửa xuất hiện hai lần trong log — một cho wizard, một cho danh sách dropdown trong settings — điều này nói cho bạn biết đây là loại lỗi bạn mắc ở mọi chỗ bạn memo hóa nội dung đã dịch, cho tới khi bạn học được thói quen luôn liệt kê t.
Onboarding còn mang theo một nếp gấp mà changelog không nhắc: vì đây là một codebase white-label, có hai cây onboarding phải dịch — một wizard GeekBye và một wizard Pavleur — sống dưới các namespace key song song onboarding.* và onboarding.pavleur.*. Mỗi chuỗi onboarding được dịch hai lần, thành hai giọng thương hiệu, trên khắp cả hai mươi chín ngôn ngữ.
Điều bản build không biết là nó đang thiếu
Đây là phần biến chuyện này thành một bài học thay vì một checklist. v1.8.3 không có cách tự động nào để trích xuất chuỗi hardcode và không có kiểm tra CI cho tính đồng nhất key giữa các locale. Nên định nghĩa của "xong" là "ai đó đã ghé qua những component họ nhớ và bọc những chuỗi họ thấy." Cái định nghĩa đó rò lỗ chỗ.
Hai tuần sau bản phát hành, một commit dọn dẹp đáp xuống với thông điệp "translate all hardcoded English strings across 10 components into 30 languages." Mười component — form báo lỗi, tab tóm tắt và tab số liệu, tab báo cáo, nội dung transcript và chat, lời nhắc giới-hạn-sử-dụng, tin nhắn chat, nút copy-code, các nút hành động nổi — đã được ship trong v1.8.3 mà vẫn nói tiếng Anh với mọi người, ở mọi ngôn ngữ. Bản build của chẳng ai gãy. Không có test nào đỏ. Một người dùng Đức chỉ đơn giản thấy tiếng Anh trong tab báo cáo, và bộ dò duy nhất là một con người rốt cuộc ngó vào nó.
Cũng chính sự vắng mặt ấy sinh ra những nỗi ngượng nhỏ hơn. Một bản dịch tiếng Đan Mạch được ship với từ sai — Luk application helt thay vì Luk applikationen helt — và được vá ở một bản sau, vì không có cổng kiểm duyệt bởi người bản ngữ và không có cách nào để pipeline biết. Các key "thoát ứng dụng" thiếu trong lô đầu và được bổ sung ngược lại vài ngày sau. Và mọi tính năng ship về sau — một preset phím bổ trợ, tắt mic, chip tóm tắt nhanh, độ mờ overlay — đều lôi theo sau nó một commit "dịch các key mới sang 29 locale", mãi mãi, vì chẳng có gì thực thi cái bất biến ấy một cách tự động.
Bạn thậm chí có thể đo được sự lộn xộn ngay trong các thông điệp commit. Cùng một bộ ba mươi file locale được mô tả, từ bản này sang bản khác, là "30 ngôn ngữ", "29 ngôn ngữ", và — trong một commit — "28 locale". Chẳng có gì được thêm hay bớt; có ba mươi file ở mọi tag từ v1.8.3 tới v2. Con số trôi dạt vì không có nguồn chân lý duy nhất cho câu hỏi "chúng ta ship bao nhiêu ngôn ngữ", nên mỗi tác giả commit tự đếm bằng tay, khi thì tính tiếng Anh là một ngôn ngữ, khi thì tính nó là nguồn. Một changelog không giữ nổi con số của chính mình cho ổn định là một tín hiệu nhỏ, ngay thẳng rằng cái thứ nó mô tả không đang được một cỗ máy thực thi.
Cái bẫy chực nổ của pluralization
Còn một điều đáng nêu cờ, không phải vì nó hỏng ầm ĩ mà vì nó là loại bug mà cách dựng này mời gọi và sẽ không cảnh báo bạn. Pluralization trong các file locale này được làm thủ công, bằng các cặp key: meetings.meetingCount và meetings.meetingCountPlural, profiles.filesAttached và filesAttachedPlural. Đó là một cách tách số ít/số nhiều gọn gàng kiểu tiếng Anh. Nó cũng sai với một mảng lớn các ngôn ngữ mà ứng dụng giờ tuyên bố hỗ trợ. Tiếng Nga, Ba Lan, và Séc có ba hoặc bốn dạng số nhiều tùy theo con số — "1 file", "2 file", "5 file", và "21 file" có thể mang những đuôi từ khác nhau. Một sơ đồ hai-key số ít/số nhiều không diễn đạt nổi điều đó, nên những con số ấy hiển thị sai ngữ pháp. i18next có bộ máy số nhiều thực thụ dành đúng cho việc này; các cặp thủ công né tránh nó, và không commit nào thừa nhận khoảng hở. Đây là phiên bản pluralization của chỗ rò rỉ chuỗi hardcode: vô hình với bản build, chỉ hiện ra với một người nói bản ngữ.
Ba điều bản phát hành này dạy chúng tôi
- Thư viện là 80% dễ; công cụ là 20% khó. Đấu nối
react-i18nextvà dịch hàng loạt chuỗi là một bài toán đã giải xong. Giữ ba mươi file luôn đồng nhất mãi mãi thì không — và nó cần một trình trích xuất cho các chuỗi hardcode và một kiểm tra CI cho các key thiếu, mà chẳng thư viện nào ship sẵn cho bạn. Hãy dựng chúng trước khi bạn tuyên bố "i18n đầy đủ", nếu không bạn đã tuyên bố một bất biến mà bạn chẳng có cách nào giữ. - Hãy tách những thiết lập ngôn ngữ thật sự khác nhau. Ngôn ngữ giao diện, ngôn ngữ transcript, và ngôn ngữ phản hồi AI là ba trục, và v1.8.3 đã đúng khi cho mỗi thứ một key riêng — và còn đúng hơn khi gieo cái mới từ một cái cũ có liên quan qua migration, để việc nâng cấp không tốn gì của người dùng.
- Con số trong changelog của bạn là một thước đo kỷ luật. Khi cùng một bộ file là 28, 29, và 30 ngôn ngữ qua ba commit, con số không phải vấn đề — thiếu nguồn chân lý mới là vấn đề. Nếu một cỗ máy không đang đếm các ngôn ngữ của bạn, thì chẳng ai đếm chúng theo cùng một cách hai lần.
Về chương trước trong câu chuyện v1, xem Thiết kế lại Dashboard xoay quanh một biểu đồ sóng duy nhất (v1.8.2); và cho toàn bộ vòng cung, xem giải phẫu việc xuất xưởng phần mềm đến độ hoàn hảo.