Song ngữ ở www.conan.school
www.conan.school chạy hai bản ngôn ngữ: tiếng Việt ở đường gốc, tiếng Anh dưới tiền tố /en. Công tắc đổi ngôn ngữ nằm ở góc trên phải của thanh điều hướng.
| Trang | Tiếng Việt | Tiếng Anh |
|---|---|---|
| Trang chủ | / | /en |
| Deep Books | /deepbooks | /en/deepbooks |
| Một khoá | /courses/:slug | /en/courses/:slug |
Vì sao là tiền tố, không phải cookie
Đổi ngôn ngữ mà URL không đổi thì máy tìm kiếm chỉ index được một bản, bản tiếng Anh coi như không tồn tại, và không có gì đỏ ở đâu để ai biết. Đây đúng loại hỏng mà hệ vẫn trông như đang chạy: công tắc bấm được, chữ đổi ngay, mà lưu lượng tiếng Anh mãi bằng không.
Tiền tố cho phép ba thứ mà cookie không cho: mỗi bản tự canonical về chính mình, hai bản khai hreflang trỏ lẫn nhau, và cả hai vào được sitemap.
Tiếng Việt giữ nguyên URL cũ, mọi liên kết ngoài và mọi thứ đã index đều trỏ vào đó.
Ba chỗ trong mã
app/lib/i18n.ts,langFromPath,stripLang,localizePath,useLang,pick. Mọi<Link to>nội bộ phải đi qualocalizePath(hoặcuseLocalizedHref); thiếu một chỗ là một cú bấm ném người đọc tiếng Anh về bản tiếng Việt, không báo lỗi gì.app/lib/i18n.strings.ts, chuỗi giao diện dùng chung (khung trang, điều hướng, nút, trạng thái rỗng). Chuỗi riêng của MỘT trang nằm ngay trong route đó quapick().app/routes.ts, mảngPAGESkhai một lần, sinh ra hai họ route: đường gốc vàen/…vớiidriêng. Thêm một trang công khai là thêm một dòng vàoPAGES, cả hai ngôn ngữ có ngay. Không nhân đôi: sitemap (.xml),auth/callback,logout,onboarding.
Nội dung từ D1
Dữ liệu đã có sẵn cột *_en (title_en, description_en, name_en…). Dùng pick(lang, row.title_vi, row.title_en), luôn có fallback về tiếng Việt, vì phần lớn cột *_en chưa được điền và một trang rỗng tệ hơn một trang sai ngôn ngữ.
Sitemap
sitemap-pages.xml sinh thêm bản /en cho mọi trang tĩnh, ưu tiên bằng 0,8 lần bản tiếng Việt. Các sitemap con còn lại (khoá học, sách, JTBD…) chưa khai bản /en, đó là việc còn lại, làm khi nội dung tiếng Anh của họ URL đó thật sự có ruột chứ không phải chỉ có khung.
Đã dịch
Mọi trang danh sách và trang giới thiệu đã song ngữ: khung trang (điều hướng, chân trang, 404, công tắc), /, /start, /courses, /books, /deepbooks, /playbooks, /communities, /workshops, /events, /news, /announcements, /challenges, /mentors, /members, /membership, /jobs, /reviews, /learner-stories, /photos, /what-we-believe, /conan-vision, và The Conan Journey.
Trang chi tiết đã song ngữ phần giao diện: /communities/:slug, /courses/:slug, /books/:slug (và /deepbooks/:slug dùng chung module), /events/:slug, /news/:slug, /announcements/:slug, /workshops/:slug.
Đã thêm ở đợt 6: /mentors/:slug, /members/:slug, /learner-stories/:slug, /playbooks/:slug. Đợt 7: /books/:slug/chapters/*, /books/:slug/sections/*, /courses/:slug/units/*, /jobs/:.../:..., /workshop-setup-guide.
Mọi trang công khai của www giờ đã có bản tiếng Anh phần giao diện.
Ba trang cố ý không dịch, và lý do khác nhau:
| Trang | Vì sao |
|---|---|
/privacy, /terms | Văn bản pháp lý. Dịch mà không có người rà là tạo ra hai bản có hiệu lực khác nhau. |
/style-guide | Tài liệu nội bộ, đã noindex. |
/onboarding | Luồng sau khi đăng ký, đã noindex và chỉ người đã có tài khoản mới thấy. |
/launch-day | Trang đích của một buổi ra mắt cụ thể, viết cho một khán giả Việt Nam có thật. |
Giao diện dịch được, nội dung thì tuỳ dữ liệu
Đây là ranh giới thật của việc dịch trang chi tiết, và nó nằm ở D1, không ở mã:
| Bảng | Cột *_en đang có |
|---|---|
legacy_ebooks | title_en, description_en, key_concept_en, related_concepts_en, jobs_to_be_done_en, reader_pains_en |
communities | name_en, tagline_en |
legacy_courses | name_en, và description_en (thêm 09.09.2026) |
legacy_events / events | title_en, description_en (thêm 09.09.2026) |
legacy_announcements / announcements | title_en, content_en (thêm 09.09.2026) |
legacy_news_articles / news_articles | title_en, excerpt_en, content_en (thêm 09.09.2026) |
Migration 20260919b_english_content_columns.sql mở chỗ chứa cho tất cả những cột còn thiếu, và mã đã đọc chúng qua pick(), nội dung điền tới đâu, /en hiện tới đó, không cần sửa mã nữa.
Nó không điền gì cả, và đó là cố ý: bản dịch là quyết định của người soạn, còn một bản dịch máy chèn thẳng vào D1 thì sáu tháng nữa không ai biết nó từ đâu ra.
Cột phải thêm cho CẢ HAI họ bảng. legacy_* và bảng không tiền tố cùng tồn tại, và đường đọc khác nhau: getD1Client() tự thêm tiền tố legacy_ (nên .from('news_articles') chạy lên legacy_news_articles), còn SQL thô ở src/domains/events/index.ts đọc thẳng bảng eventskhông tiền tố. Thêm cột cho một họ thôi là cột nằm ở nơi mã không đọc tới, câu lệnh hợp lệ, không lỗi, và không bao giờ hiện ra trang.
Chừng nào các cột đó còn rỗng, các sitemap con (khoá học, sách, chương) vẫn chưa khai bản /en: khai một họ URL mà thân trang vẫn nguyên tiếng Việt là mời máy tìm kiếm index một bản trùng lặp.
/privacy và /terms cố ý để nguyên tiếng Việt: đó là văn bản pháp lý, dịch mà không có người rà là tạo ra hai bản có hiệu lực khác nhau.
Hai cách chứa bản dịch, và khi nào dùng cái nào
| Cách | Dùng khi |
|---|---|
ui(lang, "key") trong i18n.strings.ts | Chuỗi dùng chung nhiều trang, hoặc chuỗi lẻ |
Một object COPY/trường *En ngay trong file route | Một khối nội dung của riêng trang đó |
Cách thứ hai giữ bản dịch nằm cạnh bản gốc, sửa chữ chỉ mở một file, và không ai phải nhớ xoá khoá mồ côi ở từ điển chung khi trang bị gỡ.
Bẫy đã gặp khi thêm chuỗi
ui() trả lại chính tên khoá khi khoá không tồn tại, nên một khoá gõ sai hay một khối chuỗi chèn hụt hiện ra thành coursesTitle chình ình giữa trang thay vì báo lỗi. Sau khi thêm chuỗi, mở thử trang đó, build xanh không chứng minh được gì ở đây.