This commit is contained in:
Nông Đức Huy
2026-08-13 23:20:22 +07:00
parent 3e5d38ec18
commit 3d6b0e0d4e
145 changed files with 7817 additions and 801 deletions
+73 -12
View File
@@ -240,7 +240,51 @@ consumer transient too, which for `PrismaService` would mean a second connection
---
## 9. Naming conventions
## 9. Internationalisation
Two languages, **two mechanisms**, split by who owns the string. Conflating them is the usual
way an i18n project ends up half-finished. See
[ADR-0013](./adr/0013-content-translations-in-typed-tables-ui-strings-in-message-catalogs.md).
| String | Owner | Lives in | Changes via |
| -------------------------------------- | ----------------- | ----------------------------------- | ---------------- |
| "Add to bag", "Filters" | Designers/devs | `src/messages/{vi,en}.json` | Deploy |
| Sport names, sort labels, availability | Devs (code enums) | Message catalogs | Deploy |
| Product name, description, spec rows | Merchandisers | `*_translations` tables | Admin, no deploy |
| Category / collection / brand names | Merchandisers | `*_translations` tables | Admin, no deploy |
| Colour labels ("Đen" / "Black") | Merchandisers | `product_option_value_translations` | Admin, no deploy |
### Resolution rules
1. **Locale in, resolved out.** The API takes `?locale=` (falling back to `Accept-Language`,
then `vi`) and returns already-resolved strings. The frontend never sees a translation table
and never writes fallback logic.
2. **Field-level fallback.** A translation row wins per column; any column that is null or blank
falls back to the base row. A product with a translated name but no translated description
renders the translated name and the original description — never a blank.
3. **Missing translations are legitimate.** Sizes (`S`, `M`, `L`) carry no translation rows at
all, because they are identical in both languages. The fallback covers it.
4. **Every cache key is namespaced by locale.** Forgetting this serves the first visitor's
language to everyone — the classic i18n caching bug. `CACHE_KEYS` builds every key in one file
precisely so this cannot be forgotten locally.
### URLs
Storefront uses `localePrefix: 'as-needed'`: Vietnamese from clean paths, English under `/en`.
Product slugs are themselves translated, so `/products/ao-chay-bo-aero` and
`/en/products/aero-run-tee` are the same product. Consequently:
- Every product carries `alternateSlugs`, which feeds `hreflang` alternates and the language
switcher.
- Product lookup matches a slug in _any_ locale, then redirects to the canonical URL for the
requested locale. One redirect keeps every cross-locale link alive without duplicate content.
The admin resolves locale from a cookie with no URL segment — it is `noindex` everywhere, so
locale-in-path would add a proxy hop and double the route tree for nothing.
---
## 10. Naming conventions
| Thing | Convention | Example |
| --------------------- | ------------------------- | ------------------------------ |
@@ -265,7 +309,7 @@ Booleans read as assertions: `isActive`, `hasVariants`, `canRefund`. Money field
---
## 10. Configuration and environment
## 11. Configuration and environment
Four `.env` files, each with a committed `.env.example`:
@@ -292,7 +336,7 @@ Rules:
---
## 11. Where premature abstraction must be avoided
## 12. Where premature abstraction must be avoided
Places where the instinct to generalise should be resisted until a second real case appears:
@@ -314,7 +358,7 @@ Places where the instinct to generalise should be resisted until a second real c
---
## 12. Architectural risks to prevent from day one
## 13. Architectural risks to prevent from day one
| Risk | Why it is fatal later | Prevention in place |
| ---------------------------------- | --------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ |
@@ -333,14 +377,31 @@ Places where the instinct to generalise should be resisted until a second real c
---
## 13. Deliberate limitations of milestone 0
## 14. Deliberate limitations
Stated plainly so they are choices rather than oversights:
Stated plainly so they are choices rather than oversights.
- **No token issuance.** Guards verify and enforce; login, refresh and registration are M2.
- **No cart/order/payment tables.** The first migration stays reviewable; they arrive in M5.
**Shipped (M0–M1)**
- Catalog reads: products, variants, options, categories, collections, brands, navigation —
localised, cached, filtered and faceted.
- RBAC enforcement, response envelope, structured logging, media pipeline.
- Storefront browsing and PDP in Vietnamese and English.
**Not built yet, and why**
- **No token issuance.** Guards verify and enforce; login, refresh rotation and registration are
M2. Every endpoint written from here is protected by default, before a credential exists.
- **No cart, order or payment tables.** They arrive in M5, so the migration history stays
reviewable and the variant model gets proven against real reads first.
- **`best_selling` and `relevance` sorts fall back to newest.** There is no order data (M5) and
no ranking (M6). Falling back is honest; a fake ranking would not be.
- **No sport/gender facet counts.** Those dimensions are navigated by route, not refined within a
page, so a count would render nowhere.
- **Facet counts are computed per request.** Fine at this catalog size; the fix when it stops
being fine is a search index (ADR-0012), not a bigger query.
- **The event bus is in-process and lossy.** Anything that must not be lost stays in the same
database transaction as its cause. A durable outbox comes when a use case demands it.
- **No observability beyond logs.** OpenTelemetry traces and metrics are worth adding once there
is production traffic to explain.
- **No CDN, TLS or WAF config.** That belongs to the deployment repository, not this one.
database transaction as its cause.
- **No observability beyond logs.** Traces and metrics are worth adding once there is production
traffic to explain.
- **No CDN, TLS or WAF config.** That belongs to the deployment repository.