Skip to content

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.

TrangTiếng ViệtTiếng Anh
Trang chủ//en
Deep Books/deepbooks/en/deepbooks
Một khoá/courses/:slug/en/courses/:slug

Đổ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ã ​

  1. app/lib/i18n.ts, langFromPath, stripLang, localizePath, useLang, pick. Mọi <Link to> nội bộ phải đi qua localizePath (hoặc useLocalizedHref); 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ì.
  2. 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 đó qua pick().
  3. app/routes.ts, mảng PAGES khai một lần, sinh ra hai họ route: đường gốc và en/… với id riêng. Thêm một trang công khai là thêm một dòng vào PAGES, 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:

TrangVì sao
/privacy, /termsVă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-guideTài liệu nội bộ, đã noindex.
/onboardingLuồng sau khi đăng ký, đã noindex và chỉ người đã có tài khoản mới thấy.
/launch-dayTrang đí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ảngCột *_en đang có
legacy_ebookstitle_en, description_en, key_concept_en, related_concepts_en, jobs_to_be_done_en, reader_pains_en
communitiesname_en, tagline_en
legacy_coursesname_en, và description_en (thêm 09.09.2026)
legacy_events / eventstitle_en, description_en (thêm 09.09.2026)
legacy_announcements / announcementstitle_en, content_en (thêm 09.09.2026)
legacy_news_articles / news_articlestitle_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áchDùng khi
ui(lang, "key") trong i18n.strings.tsChuỗi dùng chung nhiều trang, hoặc chuỗi lẻ
Một object COPY/trường *En ngay trong file routeMộ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.

Tài liệu nội bộ nền tảng Conan School.