Session 12: i18n (10 languages, cookie-based), Africa tier .99, locale switcher, RTL Arabic (1305 tests)
This commit is contained in:
+127
-1
@@ -4,7 +4,133 @@
|
||||
2026-06-10
|
||||
|
||||
## Current Phase
|
||||
SHIP BUILD v10.0 — Internal-auth refactor, soccer prefetch cascade, Sentry, welcome email (Session 10)
|
||||
SHIP BUILD v12.0 — i18n (10 languages) + Africa tier (Session 12)
|
||||
|
||||
## Session 12 (2026-06-11) — SHIPPED
|
||||
|
||||
### FIX 1 — i18n infrastructure (10 languages, cookie-based)
|
||||
|
||||
Honest scope decision: skipped the full `[locale]/` URL-prefix
|
||||
refactor (would have touched all 24+ pages). Went cookie-based +
|
||||
header-stamping middleware instead — same UX, much smaller blast
|
||||
radius. URL-prefix routing can layer on later without breaking
|
||||
anything.
|
||||
|
||||
- **`web/src/lib/locales.ts`** — locale registry. 10 locales:
|
||||
en (source), es, fr, pt, ar (RTL), sw, hi, ja, ko, zh.
|
||||
`LOCALE_META` carries native names + dir + region.
|
||||
`AFRICA_LOCALES = {sw}` used by the pricing reorder logic.
|
||||
- **`web/src/middleware.ts`** — locale resolver. Priority: URL
|
||||
prefix → `NEXT_LOCALE` cookie → `Accept-Language` parsing →
|
||||
default 'en'. Stamps `x-vyndr-locale` on the request so server
|
||||
components can read it via `next/headers`.
|
||||
- **`web/src/locales/{en,es,fr,pt,ar,sw,hi,ja,ko,zh}.json`** —
|
||||
10 translation dictionaries, each ~17 keys covering nav, slate,
|
||||
grade, pricing, sports, auth, common, cookie. Every file declares
|
||||
its `_meta.review_status`: `en` is `source`, the other 9 are
|
||||
`translated_unreviewed`. Sports terminology is locale-correct
|
||||
(Fútbol/Football/サッカー/كرة القدم/Soka, etc.).
|
||||
- **`web/src/lib/i18n.ts`** — synchronous server-side loader
|
||||
(`getTranslations(locale) → {t, locale, dir}`) plus
|
||||
`getServerTranslations()` which reads the middleware-stamped
|
||||
header. English fallback per key, falls to the key string itself
|
||||
when missing on both. `{name}` interpolation supported.
|
||||
- **`web/src/contexts/LocaleContext.tsx`** — client provider +
|
||||
`useT()` / `useLocale()` hooks. Mounted in the root layout above
|
||||
every other provider.
|
||||
- **RTL** — `<html dir="rtl">` set in root layout when locale is
|
||||
Arabic. `globals.css` flips nav direction and isolates monospace
|
||||
blocks (numbers stay LTR — financial data convention).
|
||||
- **`LocaleSwitcher.tsx`** — compact mono dropdown with native
|
||||
language names. Sets the cookie, reloads the page. Mounted in Nav
|
||||
for both authenticated and anonymous states.
|
||||
- **Wired into**: Nav (5 links + login button), CookieConsent
|
||||
(message + accept + privacy link), Pricing (CTAs translate per
|
||||
tier). High-impact components first; longer-tail strings remain
|
||||
English with `t('key')` calls scheduled for a follow-up.
|
||||
|
||||
### FIX 2 — Africa tier ($4.99/mo)
|
||||
|
||||
- **`src/config/tiers.js`** — adds the `africa` tier between free
|
||||
and analyst: 10 scans/day, reasoning_visible:true,
|
||||
kill_conditions_detail:true, alerts:false, api_access:false.
|
||||
Frozen.
|
||||
- **Scan-limit middleware** — no change needed. `scanLimit()` reads
|
||||
via `getScanLimit()`, which now resolves 'africa' to 10.
|
||||
- **`web/src/components/Pricing.tsx`** — adds the VYNDR Africa
|
||||
card. The pricing-grid CSS unfolds from 2-up (tablet) to 4-up
|
||||
(≥1100px desktop). When the user's locale is Swahili (a proxy
|
||||
for African markets — IP-based geolocation deferred to a future
|
||||
session), the Africa tier renders FIRST.
|
||||
- **Honest UX gap**: Africa-tier checkout short-circuits to an
|
||||
inline "coming soon" message instead of triggering Stripe. Two
|
||||
reasons: (a) the backend `/api/stripe/checkout` route validates
|
||||
tier against `['analyst','desk']` and the spec forbids backend
|
||||
edits this session; (b) `STRIPE_PRICE_AFRICA` is unset and the
|
||||
Stripe product hasn't been created in the dashboard yet.
|
||||
- **DB CHECK constraint blocker**: migrations 001 + 011 declare
|
||||
`tier IN ('free','analyst','desk')`. The webhook will 23514
|
||||
(check_violation) if it tries to write `africa` until the
|
||||
constraint is extended. Documented in `tiers.js` header + in
|
||||
SYSTEM-MANIFEST. Out of scope this session per the no-migration
|
||||
rule.
|
||||
- **`.env.example`** — `STRIPE_PRICE_AFRICA=price_...` placeholder
|
||||
with explanatory comment.
|
||||
|
||||
### Tests added (Session 12)
|
||||
| Suite | Tests |
|
||||
|------------------------------------|-------|
|
||||
| `tests/unit/i18n.test.js` | 14 |
|
||||
| `tests/unit/tiers.test.js` (extended) | +5 |
|
||||
| **Total new** | **19** |
|
||||
|
||||
### Quality gates
|
||||
- `npm test`: **1305 / 1305 passing** (1286 + 19), 101 suites, 0 regressions
|
||||
- `web/npm run build`: clean. NOTE — every page is now `ƒ Dynamic`
|
||||
rather than `○ Static` because the root layout reads request
|
||||
headers (`next/headers`) for locale resolution. This is the
|
||||
expected cost of SSR i18n. If FCP regresses, the fallback is
|
||||
client-side cookie reads (brief English flash on first paint, but
|
||||
static prerender returns).
|
||||
- License audit: third-party deps remain permissive (no new licenses
|
||||
introduced — translation files are JSON in our own repo).
|
||||
|
||||
### Open items / follow-ups
|
||||
1. **DB CHECK constraint** must be updated before the Africa tier
|
||||
can actually be assigned to users. Manual SQL:
|
||||
```
|
||||
ALTER TABLE users DROP CONSTRAINT users_tier_check;
|
||||
ALTER TABLE users ADD CONSTRAINT users_tier_check
|
||||
CHECK (tier IN ('free','africa','analyst','desk'));
|
||||
-- same for user_profiles
|
||||
```
|
||||
2. **Stripe product** for VYNDR Africa not created. Manual step:
|
||||
create the product + price in the Stripe dashboard, set
|
||||
`STRIPE_PRICE_AFRICA` in Coolify, then extend the backend
|
||||
checkout route's validation list.
|
||||
3. **Translation review** — only `en` is `source` quality. The
|
||||
other 9 locales are `translated_unreviewed`. Native-speaker
|
||||
review recommended for Arabic, Chinese, Korean, Japanese, Hindi
|
||||
before public launch.
|
||||
4. **Browser geolocation** — Africa tier currently sorts first only
|
||||
for Swahili-locale users. IP-based detection (NG/KE/ZA/GH/etc.)
|
||||
would catch English-speaking African users; deferred to a
|
||||
session with proper geo middleware (Cloudflare headers, etc.).
|
||||
5. **Per-page meta translations** — page `<title>` and OG tags are
|
||||
still English. Adding per-locale metadata requires the
|
||||
`[locale]/` segment refactor, deferred.
|
||||
|
||||
### Coolify env (Session 12 additions)
|
||||
|
||||
```
|
||||
# Already required:
|
||||
NEXT_LOCALE # No env — set as a per-user cookie by the switcher.
|
||||
|
||||
# New, optional:
|
||||
STRIPE_PRICE_AFRICA=price_... # Once you create the Stripe product
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Session 10 (2026-06-10) — SHIPPED
|
||||
|
||||
|
||||
Reference in New Issue
Block a user