Skip to content

Coding Conventions — đặt tên trong code ​

Documentation Conventions quy định cách viết tài liệu. File này quy định cách đặt tên trong code, và hiện chỉ có một luật, nhưng là luật áp cho toàn bộ repo.

1. Luật nền: định danh tiếng Anh, nội dung tiếng Việt ​

Chủ dự án chốt 2026-08-26 (SRC-600):

Mọi hằng số đều cần là tiếng Anh, mọi slug đều cần là tiếng Anh.

Ranh giới là định danh ↔ nội dung, không phải code ↔ không-code:

Ngôn ngữVì sao
Định danh — hằng số, biến, hàm, type, key, slug, tên filetiếng AnhĐịnh danh là thứ người khác tìm kiếm và ghép nối. LECH_LOP_TOI_DA không grep được cùng grade, lechTamLop() không đứng cạnh gradeWindow() trong autocomplete, và một người mới (hay một AI) đọc nó phải dịch trước khi hiểu.
Nội dung — nhãn trên màn hình, rationale_vi, name_vi, thông báo lỗi cho learnertiếng Việt (qua lang.tsx)Đây là sản phẩm nói với người Việt. Dịch sang tiếng Anh trong code rồi dịch ngược lúc render là thêm một tầng mất mát.

Luật này không động tới chuỗi hiển thị. Một hằng số tiếng Anh giữ một câu tiếng Việt là đúng:

ts
const GRADE_SPAN_MAX = 2;                       // ✅ tên Anh
const label = "lệch quá 2 lớp";                  // ✅ nội dung Việt

2. Bốn loại slug, cùng một luật ​

"Slug" ở đây gồm mọi chuỗi định danh dạng kebab-case:

LoạiVí dụ saiVí dụ đúng
Đường dẫn URL/{school}/toan-canh/{subject}/{school}/overview/{subject}
Key nội bộ / giá trị enumtab: "ngan-hang-de"tab: "exam-bank"
Class CSS.n12-cua-hong.n12-gate-red
Tên file / thư mục codeHocLieu.tsxMaterials.tsx

3. Ba ngoại lệ, và chỉ ba ​

Ngoại lệ phải nằm ở đây; không có ngoại lệ "ngầm hiểu".

  1. Danh từ riêng tiếng Việt trong dữ liệu: tên người, tên trường (thpt-chuyen-ha-noi-amsterdam), tên địa danh. Đó là dữ liệu, không phải định danh do ta đặt ra.
  2. ID nội dung chương trình học (ch-phan-ung-hoa-hoc, sp-do-duoc-cai-gi-tren-vat-song-1): chúng là tên khái niệm trong chương trình Việt Nam, đang nằm trong D1 và trong hàng trăm file seed. Đổi là một cuộc migration nội dung riêng, không phải việc đặt tên.
  3. Mã môn học (toan, van, su, dia): đã là khoá ngoại ở nhiều bảng và xuất hiện trong URL công khai; giữ nguyên cho tới khi có đợt migration riêng.

Ba ngoại lệ này có một điểm chung: chúng là dữ liệu đã lưu, không phải tên do lập trình viên đặt lúc viết code. Luật §1 áp trọn vẹn cho phần lập trình viên tự đặt.

4. Đổi tên một thứ đã chạy thì phải làm gì ​

Đổi cái gìBắt buộc kèm
Định danh trong codekhông gì thêm — đổi hết một lượt, npm run typecheck xanh
Slug URL công khaialias đường cũ (301/rewrite) giữ ít nhất 6 tháng, sửa link trong docs/, sửa e2e
Giá trị đã lưu trong D1migration đánh số theo skill d1-migrate, idempotent, chạy được cả khi code deploy trước

Đường link đã chia sẻ là thứ ta không thu hồi được. Bỏ alias là im lặng làm hỏng link của người khác.

4b. Một ca đã làm: hai enum trạng thái (2026-08-26) ​

blueprint_weights.required_state và parent_beliefs.perceived_state từng là enum tiếng Việt. Migration 0082_english_state_enums.sql đổi chúng thành:

CũMớiCũMới
chacsolidon_dinhsteady
lung_layshakydang_chac/nhac_lai/nguy_co_quen/nen_on_lai (nhãn Retention)solid/refresh/at_risk/relearn
honggap

Ba chi tiết đáng lấy làm mẫu cho lần sau:

  • CHECK mới nhận CẢ HAI bộ tên. CI không chạy migration khi push thường nên code lên trước, bảng lên sau; chỉ nhận tên mới thì bản deploy cũ bị chặn ghi, chỉ nhận tên cũ thì bản mới bị chặn. Siết còn bốn tên tiếng Anh là việc của một migration sau.
  • Đọc thì quy về tên mới, đừng so tên thô. normalizeState() trong modules/knowledge/mastery.ts là chỗ duy nhất biết bảng tên cũ. Không có nó thì mọi blueprint chưa migrate tụt ngưỡng về mặc định — hỏng lặng lẽ, không ai thấy.
  • Snapshot đã lưu không migrate được. learner_model_versions giữ JSON của những lần chạy trước; chúng còn nguyên tên cũ mãi mãi. Vì thế bảng nhãn ở app giữ luôn cả khoá cũ.

5. Cổng kiểm ​

node scripts/check-english-identifiers.mjs — nằm trong npm run check:docs, nên CI đã chạy sẵn. Nó quét định danh khai báo (const/function/type/…) và slug trong string literal, tách theo camelCase/snake/kebab, rồi báo đỏ khi gặp âm tiết tiếng Việt.

Ba cách khai ngoại lệ, theo thứ tự nên dùng:

CáchViết thế nàoDùng khi
Khối LEGACY_*đặt slug cũ trong một object tên có LEGACYbảng tra slug cũ → slug mới (§4)
Pragma cả file// slug-exempt: <lý do> trong ~2000 ký tự đầufile chứa ID nội dung hay mã môn (§3.2, §3.3)
Đánh dấu một dònglegacy-slug trong chú thích cuối dòngmột chỗ lẻ

Phần quét định danh khai báo không bao giờ tắt được — mọi ngoại lệ chỉ tắt phần slug, vì ngoại lệ §3 đều nói về dữ liệu, không về tên do lập trình viên đặt.

Cổng chạy trên cây làm việc nên vẫn dính bẫy "máy local nói dối" ghi ở CLAUDE.md gốc repo (cố ý không đặt link vì file đó nằm ngoài srcDir của VitePress, link sẽ làm đỏ cả build) — git status --short sạch rồi mới tin kết quả xanh. Xem thêm sổ sự cố §9.

6. Dữ liệu dùng chung cho nhiều app thì nằm ở packages/, không chép sang từng app (SRC-867) ​

Ngày 2026-09-19 hai file packages.ts — một ở apps/web/src, một ở apps/marlins/src — được gộp làm một, tức chuyển từ hai bản sang một: packages/catalog. Chúng là cùng một danh mục gói học và giá, và cả hai đều mở đầu bằng đúng câu "nguồn duy nhất cho trang công khai" — trong khi có hai bản.

Cái giá đã trả, không phải giả định. Một đợt sửa chữ ngày 2026-09-19 chỉ chạm bản web, nên gần một ngày bố mẹ trên marlins đọc "Nemo Dive dành cho learner", "xây dựng Learner Model" trong khi nemo12.com đã nói khác. Không cổng nào bắt được: mỗi bản tự nó đều hợp lệ, typecheck xanh, test xanh. Lần rà sau mới lộ ra, và lộ ra vì có người đi tìm chứ không vì máy báo.

Luật: một dữ liệu mà hai app trở lên cùng hiển thị thì đặt ở packages/<tên> và khai làm workspace dependency ("@nemo12/<tên>": "*"), không cp sang app thứ hai. Chép sang là tạo hai nguồn, và hai nguồn chỉ khác nhau vào ngày ai đó sửa một bên.

Dấu hiệu nhận ra sớm: hai file cùng tên ở hai app. find apps -name '<tên>.ts' | wc -l ra số lớn hơn 1 là đáng mở ra xem — đôi khi trùng tên mà khác việc, nhưng trùng cả nội dung thì không.

Không áp cho dữ liệu SINH RA từ nguồn khác. apps/marlins/src/parentCoursesData.ts được sinh từ D1 và có dòng "ĐỪNG SỬA TAY" ngay đầu file; ở đó nguồn thật là D1, bản trong repo chỉ là ảnh chụp. Luật này nói về dữ liệu mà repo là nguồn.

7. Khối JSX phải nằm TRONG thân component (SRC-935) ​

Một phần tử JSX đứng một mình ở cấp module là cú pháp hợp lệ trong .tsx, và đó chính là chỗ nguy hiểm: <a>...</a> trên một dòng riêng là một expression statement, còn {/* ... */} là một block statement chứa chú thích. tsc không phàn nàn, eslint không đỏ, vite build chạy trót lọt. React dựng phần tử ấy đúng một lần lúc nạp module rồi vứt đi, nên nó không bao giờ được vẽ.

Đã xảy ra thật ngày 20.09.2026 (Audit #015, D-4): một lượt gộp nhánh đặt khối lối vào "Join a live class" ra ngoài thân IeltsTaskPage. /ielts/live vẫn chạy nếu gõ thẳng địa chỉ, nhưng không còn đường nào đi tới nó từ giao diện — một tính năng biến mất mà không một cổng nào đỏ. Nó chỉ lộ ra khi có người đọc lại cấu trúc file bằng mắt.

Cổng scripts/check-orphan-jsx.mjs (trong npm run check:code) quét bằng parser của TypeScript, không bằng biểu thức chính quy: JSX nhiều dòng không dùng {} nên mọi cách đếm ngoặc đều đọc nhầm các dòng nối tiếp của một phần tử bình thường thành "cấp module" — bản thử đầu tiên bằng regex cho ra 55 chỗ nghi ngờ mà cả 55 đều sai.

Cổng này không bắt được lớp lỗi anh em: một component khai báo đúng nhưng không ai gọi. Chỗ ấy phải gác bằng e2e hỏi TRANG ĐÃ DỰNG chứ không hỏi mã nguồn — xem apps/learn/e2e/ieltsDashboard.spec.ts.

Trace ​