---
url: https://docs.nemo12.com/engineering/coding-conventions.md
description: >-
  Quy ước đặt tên trong code: định danh tiếng Anh, nội dung người dùng tiếng
  Việt, áp cho toàn bộ repo.
---

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

[Documentation Conventions](../conventions.md) 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 file | **tiế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 learner | **tiế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ại | Ví dụ sai | Ví dụ đúng |
| --- | --- | --- |
| **Đường dẫn URL** | `/{school}/toan-canh/{subject}` | `/{school}/overview/{subject}` |
| **Key nội bộ / giá trị enum** | `tab: "ngan-hang-de"` | `tab: "exam-bank"` |
| **Class CSS** | `.n12-cua-hong` | `.n12-gate-red` |
| **Tên file / thư mục code** | `HocLieu.tsx` | `Materials.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 code | không gì thêm — đổi hết một lượt, `npm run typecheck` xanh |
| Slug URL công khai | **alias đường cũ** (301/rewrite) giữ ít nhất 6 tháng, sửa link trong `docs/`, sửa e2e |
| Giá trị đã lưu trong D1 | migration đánh số theo [skill `d1-migrate`](skills.md), 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ới | | Cũ | Mới |
| --- | --- | --- | --- | --- |
| `chac` | `solid` | | `on_dinh` | `steady` |
| `lung_lay` | `shaky` | | `dang_chac`/`nhac_lai`/`nguy_co_quen`/`nen_on_lai` (nhãn Retention) | `solid`/`refresh`/`at_risk`/`relearn` |
| `hong` | `gap` | | | |

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ách | Viết thế nào | Dùng khi |
| --- | --- | --- |
| Khối `LEGACY_*` | đặt slug cũ trong một object tên có `LEGACY` | bảng tra slug cũ → slug mới (§4) |
| Pragma cả file | `// slug-exempt: <lý do>` trong ~2000 ký tự đầu | file chứa ID nội dung hay mã môn (§3.2, §3.3) |
| Đánh dấu một dòng | `legacy-slug` trong chú thích cuối dòng | mộ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ố](incident-log/incidents-09-plus.md) §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`](../../packages/catalog/index.ts). 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

* Nguồn: SRC-600 (chủ dự án 2026-08-26) · SRC-935 (tự phát hiện khi rà lại Audit #015, 21.09.2026).
* Liên quan: [Documentation Conventions](../conventions.md) · [Dependency Hygiene](dependency-hygiene.md).
