diff --git a/PROJECT.md b/PROJECT.md index b1ff35d..e4e132b 100644 --- a/PROJECT.md +++ b/PROJECT.md @@ -37,7 +37,7 @@ Japanese small business owners — restaurants, retail shops, manufacturers, and - Design tokens (colour, typography, spacing, radius, shadows, motion) - Component specifications (Button, Input, Card, Badge, Navigation, Loading, Empty States, Toast) - Page layout system (breakpoints, containers, mobile/desktop structure) -- Japanese localisation rules (fonts, UI copy, date/number formatting) +- Multi-locale support: Japanese (`ja`), English (`en`), Simplified Chinese (`zh-Hans`) - Shared infrastructure setup (Supabase, Stripe, Vercel, Sentry, PostHog, Resend) - Two initial products: ShiftMate and FaxBridge - Japan legal compliance pages (プライバシーポリシー, 利用規約, 特定商取引法に基づく表記) @@ -52,7 +52,8 @@ Japanese small business owners — restaurants, retail shops, manufacturers, and - Framer Motion or page transitions in MVP - Warning toasts (use inline form errors instead) - Full-page spinners for data fetching (skeletons only) -- Internationalisation for languages other than Japanese +- RTL script support or Traditional Chinese (`zh-Hant`) +- Machine translation or browser `Accept-Language` auto-detection in MVP --- @@ -77,7 +78,7 @@ Japanese small business owners — restaurants, retail shops, manufacturers, and ## Constraints - Mobile-first — 390px primary target; Japanese SMB owners use smartphones -- All user-facing text in Japanese +- All user-facing text in the active locale (`ja` | `en` | `zh-Hans`) — default is Japanese - CSS variables for all tokens — never hardcode hex or px values - TypeScript strict mode — no `any`, explicit return types on all functions - shadcn/ui customised to Mori spec, never used raw @@ -137,11 +138,22 @@ All values are defined as CSS custom properties on `:root`. **Never hardcode hex ```css :root { - /* === Font Families === */ + /* === Font Families (locale-aware) === */ + /* CJK default — Japanese and Chinese use this base stack */ + --font-body: + 'Hiragino Kaku Gothic ProN', 'Hiragino Sans', 'Noto Sans JP', 'Yu Gothic', sans-serif; + --font-heading: + 'Hiragino Kaku Gothic ProN', 'Hiragino Sans', 'Noto Sans JP', 'Yu Gothic', sans-serif; + /* Legacy alias — kept for backward compatibility */ --font-sans: 'Geist', 'Hiragino Kaku Gothic ProN', 'Hiragino Sans', 'Yu Gothic', 'Meiryo', sans-serif; --font-mono: 'Geist Mono', 'Osaka-Mono', monospace; + /* === Line Height (locale-aware) === */ + /* CJK scripts need looser leading than Latin */ + --leading-body: 1.8; + --leading-heading: 1.4; + /* === Type Scale === */ --text-xs: 0.75rem; /* 12px — labels, captions */ --text-sm: 0.875rem; /* 14px — secondary body */ @@ -157,14 +169,27 @@ All values are defined as CSS custom properties on `:root`. **Never hardcode hex --font-medium: 500; --font-semibold: 600; /* use sparingly, headings only */ } + +/* Locale overrides — applied via data-locale on */ +[data-locale='en'] { + --font-body: 'Inter', 'Helvetica Neue', Arial, sans-serif; + --font-heading: 'Inter', 'Helvetica Neue', Arial, sans-serif; + --leading-body: 1.6; + --leading-heading: 1.25; +} +[data-locale='zh-Hans'] { + --font-body: 'PingFang SC', 'Noto Sans SC', 'Microsoft YaHei', sans-serif; + --font-heading: 'PingFang SC', 'Noto Sans SC', 'Microsoft YaHei', sans-serif; +} ``` **Rules:** -- Body text: `--text-base`, `--font-normal`, line-height `1.7` +- Body text: `--text-base`, `--font-normal`, line-height `var(--leading-body)` - UI labels: `--text-sm`, `--font-medium` -- Headings: `--font-semibold`, never bold (700) in UI -- Japanese text inherits font-family fallback automatically — no special handling needed +- Headings: `--font-semibold`, line-height `var(--leading-heading)`, never bold (700) in UI +- Always reference `var(--font-body)` or `var(--font-heading)` — never hardcode a font family +- The design system does **not** load web fonts — each product loads fonts via `next/font` ### Spacing Scale @@ -430,56 +455,77 @@ Mobile: Desktop: --- -## Japanese Language & Localisation +## Localisation + +Supported locales: **`ja`** (Japanese, default) · **`en`** (English) · **`zh-Hans`** (Simplified Chinese) + +### Locale Setup (Next.js App Router) + +```ts +// next.config.ts +i18n: { + locales: ['ja', 'en', 'zh'], + defaultLocale: 'ja', +} + +// app/[locale]/layout.tsx — set data-locale on for CSS overrides + +``` + +Legacy unprefixed routes must redirect to `/ja/` equivalents via `next.config.ts` `redirects`. ### Font Rendering ```css body { - font-family: var(--font-sans); + font-family: var(--font-body); -webkit-font-smoothing: antialiased; text-rendering: optimizeLegibility; - /* Do NOT set font-feature-settings — let the system handle it */ -} - -.ui-label-ja { - letter-spacing: 0.025em; + line-height: var(--leading-body); } ``` ### UI Copy Guidelines -``` -Actions: ✓ 保存する / 削除する / 追加する ✗ 保存 / 削除 (too abrupt) -Loading: 処理中... -Success: 完了しました -Error: エラーが発生しました -Empty: ✓ まだシフトがありません ✗ データがありません (too cold) -Confirm: 本当に削除しますか?この操作は取り消せません。 - [キャンセル] [削除する] — destructive on the right, always -``` +Use `useCopy()` from `@mori/ui` to get locale-aware strings. Never hardcode copy in components. + +| Pattern | `ja` | `en` | `zh-Hans` | +| ---------------- | ---------------------------------------------- | --------------------------------------- | ------------------ | +| Save action | 保存する | Save | 保存 | +| Delete action | 削除する | Delete | 删除 | +| Cancel | キャンセル | Cancel | 取消 | +| Loading | 処理中... | Loading... | 处理中... | +| Success | ✓ 保存しました | ✓ Saved | ✓ 已保存 | +| Error | エラーが発生しました。もう一度お試しください。 | Something went wrong. Please try again. | 发生错误,请重试。 | +| Empty state | まだデータがありません | No data yet | 暂无数据 | +| Destructive hint | この操作は取り消せません。 | This action cannot be undone. | 此操作无法撤消。 | + +**Rules:** + +- Japanese actions end in `する` — `保存する`, `削除する`, `追加する` (never `保存`, `削除`) +- Destructive action always on the RIGHT in confirm dialogs — all locales +- `zh-Hans` copy is marked `// TODO: zh-Hans review` until human-reviewed ### Date & Number Formatting +Use `formatDate` and `formatCurrency` from `src/lib/locale/format.ts`: + ```tsx -const formatDate = (date: Date) => - new Intl.DateTimeFormat('ja-JP', { - year: 'numeric', - month: 'long', - day: 'numeric', - weekday: 'short', - }).format(date) -// → 2025年1月28日(火) - -const formatYen = (amount: number) => `¥${amount.toLocaleString('ja-JP')}` -// → ¥1,980 - -const formatTime = (date: Date) => - new Intl.DateTimeFormat('ja-JP', { - hour: '2-digit', - minute: '2-digit', - hour12: false, - }).format(date) +import { formatDate, formatCurrency } from '@mori/ui' + +formatDate(new Date('2025-01-28'), 'ja') // → 2025年1月28日(火) +formatDate(new Date('2025-01-28'), 'en') // → Tuesday, January 28, 2025 +formatDate(new Date('2025-01-28'), 'zh-Hans') // → 2025年1月28日 星期二 + +formatCurrency(1980, 'ja') // → ¥1,980 (all locales — products are JPY-priced) +formatCurrency(1980, 'en') // → ¥1,980 +formatCurrency(1980, 'zh-Hans') // → ¥1,980 + +// Time (24-hour, all locales) +const formatTime = (date: Date, locale: string) => + new Intl.DateTimeFormat(locale, { hour: '2-digit', minute: '2-digit', hour12: false }).format( + date + ) // → 09:00 ``` @@ -561,6 +607,29 @@ NEXT_PUBLIC_POSTHOG_KEY= --- +## Locale Persistence (Supabase) + +Add a `locale` column to `user_preferences` so the user's locale follows them across devices: + +```sql +-- Migration: add locale preference to user_preferences +alter table user_preferences + add column if not exists locale text default 'ja'; +``` + +**Pattern:** + +1. On login: read `user_preferences.locale`, redirect to `//dashboard` +2. When user switches locale (e.g. via a language picker): update `user_preferences.locale` and reload + +```ts +// On locale switch +await supabase.from('user_preferences').upsert({ user_id: userId, locale: newLocale }) +router.push(`/${newLocale}${pathname}`) +``` + +--- + ## Code Quality Rules ``` diff --git a/openspec/changes/archive/2026-03-31-localisation-ja-en-zh/.openspec.yaml b/openspec/changes/archive/2026-03-31-localisation-ja-en-zh/.openspec.yaml new file mode 100644 index 0000000..8fb8631 --- /dev/null +++ b/openspec/changes/archive/2026-03-31-localisation-ja-en-zh/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-03-31 diff --git a/openspec/changes/archive/2026-03-31-localisation-ja-en-zh/design.md b/openspec/changes/archive/2026-03-31-localisation-ja-en-zh/design.md new file mode 100644 index 0000000..09e6308 --- /dev/null +++ b/openspec/changes/archive/2026-03-31-localisation-ja-en-zh/design.md @@ -0,0 +1,101 @@ +## Context + +Mori DS currently assumes Japanese as the only locale. UI copy is hardcoded in Japanese across component specs, PROJECT.md conventions, and product repos. Typography tokens assume CJK character metrics. There is no locale context, no switching mechanism, and no structured copy catalogue. + +This design adds first-class multi-locale support for Japanese (`ja`), English (`en`), and Simplified Chinese (`zh-Hans`) without introducing a third-party i18n library, keeping the stack lean and fully type-safe. + +## Goals / Non-Goals + +**Goals:** +- Define a `Locale` type and locale context consumed by all Mori components +- Establish a structured copy catalogue (`MoriCopy`) keyed by locale, covering all standard UI patterns +- Define per-locale typography tokens (font stacks, line-height) as CSS variable overrides on `[data-locale]` +- Specify locale detection and switching for Next.js App Router (URL prefix strategy) +- Provide locale-aware formatting utilities for dates, currency, and numbers using native `Intl` +- Keep design system and product repos fully type-safe — accessing a missing copy key is a compile error + +**Non-Goals:** +- Right-to-left (RTL) script support +- Traditional Chinese (`zh-Hant`) +- Machine translation or auto-detection from browser `Accept-Language` in MVP +- Per-component copy overrides (product repos use the catalogue; they don't fork it) +- iOS localisation (deferred to Phase 2) + +## Decisions + +### Decision 1: No third-party i18n library — use a typed copy catalogue + `Intl` + +**Choice:** A hand-written `MoriCopy` TypeScript object (keyed `{ ja, en, 'zh-Hans' }`) plus native `Intl.DateTimeFormat`, `Intl.NumberFormat`. + +**Alternatives considered:** +- `next-intl`: Mature, file-based, but adds a runtime dependency and its own routing adapter. Overkill for three locales and a controlled copy set. +- `react-i18next`: Popular but adds ~30 kB and requires a separate translation file pipeline. Excessive for a design system with a bounded copy catalogue. + +**Rationale:** The copy catalogue is small and bounded. Type-safety via TypeScript literal types gives us compile-time exhaustiveness checking with zero runtime cost. Native `Intl` handles all formatting needs. + +--- + +### Decision 2: URL-prefix locale routing (`/ja/`, `/en/`, `/zh/`) + +**Choice:** Next.js built-in `i18n` config with `locales: ['ja', 'en', 'zh']` and `defaultLocale: 'ja'`. All routes are prefixed; `ja` prefix is canonical but can be omitted in links via `as` prop. + +**Alternatives considered:** +- Cookie/header-based detection with no URL change: Breaks deep links, makes locale invisible to SEO crawlers, harder to test. +- Subdomain strategy (`en.shiftmate.jp`): Requires DNS config per locale; too operationally heavy for MVP. + +**Rationale:** URL prefix is the standard Next.js approach, works out of the box with App Router, and makes locale explicit for both users and search engines. Vercel edge routing handles redirects automatically. + +--- + +### Decision 3: Per-locale typography via `[data-locale]` CSS attribute on `` + +**Choice:** Set `data-locale="ja|en|zh"` on the `` element in the root layout. CSS rules scoped to `[data-locale="en"]` override font-stack and line-height tokens. + +**Alternatives considered:** +- Separate CSS variable sets per locale injected via inline `style`: Works but pollutes JSX and is harder to audit. +- Tailwind `variant` strategy: Not standard in Tailwind v3; requires plugin. + +**Rationale:** CSS attribute selectors on `` are zero-JS, SSR-safe, and easy to inspect in DevTools. A single `[data-locale="en"] { --font-body: ...; --leading-body: ...; }` block is sufficient. + +--- + +### Decision 4: Locale stored in Supabase user profile, not local storage + +**Choice:** `user_preferences.locale` column (`text`, default `'ja'`). On first login, locale is inferred from the URL prefix and saved. Subsequent logins restore the saved locale. + +**Alternatives considered:** +- `localStorage` only: Lost on new device, not synced across browser/mobile. +- Cookie only: Works for SSR but not queryable server-side in Supabase RLS context. + +**Rationale:** Server-side persistence means the locale follows the user across devices. Supabase RLS already protects the column. The URL prefix takes precedence for unauthenticated pages. + +## Risks / Trade-offs + +**[Risk] Copy catalogue divergence** — As products add feature-specific copy, they may fork the catalogue rather than contributing back. +→ Mitigation: The `MoriCopy` type is the source of truth in this repo. Product repos extend it via a `ProductCopy` type that intersects with `MoriCopy`; they cannot shadow core keys. + +**[Risk] CJK line-height assumptions bleed into English layouts** — Existing components are designed with CJK metrics. English text at the same line-height will feel loose. +→ Mitigation: `localisation-typography` spec defines explicit `--leading-body` overrides per locale. All components must use `--leading-body` rather than a hardcoded value. + +**[Risk] Simplified Chinese copy quality** — Machine-translated zh-Hans copy will feel unnatural to native speakers. +→ Mitigation: Initial zh-Hans copy is human-reviewed before any public release. Copy is marked `// TODO: zh-Hans review` until confirmed. + +**[Risk] URL prefix breaks existing ShiftMate/FaxBridge links** — Existing `/dashboard` paths become `/ja/dashboard`. +→ Mitigation: Next.js `redirects` config maps legacy unprefixed paths to `/ja/` equivalents. Old links continue to work. + +## Migration Plan + +1. Merge this change into Mori DS main; publish updated `PROJECT.md` and spec files +2. Add `i18n` config to Next.js in ShiftMate and FaxBridge repos +3. Add `user_preferences.locale` column via Supabase migration (nullable, default `'ja'`) +4. Wrap root layout with `LocaleProvider`; set `data-locale` on `` +5. Replace all hardcoded Japanese copy strings with `copy[locale].*` references +6. Add `redirects` for legacy unprefixed routes +7. QA: smoke-test all three locales on mobile (390px) and desktop + +Rollback: Remove `i18n` config from Next.js; legacy paths continue to work. User preference column can remain (nullable, unused). + +## Open Questions + +- Should `zh-Hans` use `¥` (yen) or `¥`/`元` for currency display when a Chinese-market product is added? (Deferred — current products are Japan-only priced in JPY) +- Do LINE API notifications (ShiftMate) need locale-aware templates? (Out of scope for this change; tracked separately) diff --git a/openspec/changes/archive/2026-03-31-localisation-ja-en-zh/proposal.md b/openspec/changes/archive/2026-03-31-localisation-ja-en-zh/proposal.md new file mode 100644 index 0000000..a956a49 --- /dev/null +++ b/openspec/changes/archive/2026-03-31-localisation-ja-en-zh/proposal.md @@ -0,0 +1,34 @@ +## Why + +The Mori Design System is currently Japanese-only, with English and Chinese explicitly out of scope. Supporting Japanese, English, and Simplified Chinese expands the addressable market to include non-Japanese-speaking operators (e.g. Chinese-owned restaurants, international franchise staff) and enables future products to launch outside Japan without a design system rewrite. + +## What Changes + +- Add a `locale` context (`ja` | `en` | `zh-Hans`) to the design system, driving UI copy, date/number formatting, and typography +- Define locale-aware typography tokens (font stacks, line-height adjustments) for CJK vs Latin scripts +- Specify a locale-detection and locale-switching pattern (URL prefix or user preference stored in Supabase profile) +- Update all component specs to reference locale-aware copy tokens rather than hardcoded Japanese strings +- Update Japanese UI copy conventions doc to become a multi-locale copy conventions doc +- Define English and Simplified Chinese equivalents for all standard UI copy patterns (actions, errors, empty states, confirmations) +- Remove "i18n for languages other than Japanese" from the Out of Scope list in PROJECT.md + +## Capabilities + +### New Capabilities + +- `localisation-core`: Locale context, detection, and switching mechanism; supported locales (`ja`, `en`, `zh-Hans`); locale-aware formatting utilities for dates, currency, and numbers +- `localisation-copy`: Standard UI copy patterns (actions, errors, loading, empty states, confirmations, success messages) in all three locales +- `localisation-typography`: Per-locale font stack and typographic adjustment tokens (CJK vs Latin line-height, font-size scaling) + +### Modified Capabilities + +- `design-tokens`: Add locale-aware typography token variants (font stacks, line-height) alongside existing fixed tokens + +## Impact + +- **PROJECT.md**: Remove i18n from Out of Scope; add locale tokens to design tokens section; update UI conventions section to be multi-locale +- **All component specs**: Copy strings must reference locale-aware copy keys rather than hardcoded Japanese text +- **Product repos** (ShiftMate, FaxBridge): Must wrap root layout with locale provider; update all hardcoded Japanese copy to use copy keys +- **Supabase schema**: `user_preferences` table needs a `locale` column +- **Next.js routing**: URL prefix strategy (`/ja/`, `/en/`, `/zh/`) or cookie/header-based detection (TBD in design) +- **No new dependencies required** — use native `Intl` APIs and Next.js built-in i18n routing diff --git a/openspec/changes/archive/2026-03-31-localisation-ja-en-zh/specs/design-tokens/spec.md b/openspec/changes/archive/2026-03-31-localisation-ja-en-zh/specs/design-tokens/spec.md new file mode 100644 index 0000000..328242c --- /dev/null +++ b/openspec/changes/archive/2026-03-31-localisation-ja-en-zh/specs/design-tokens/spec.md @@ -0,0 +1,16 @@ +## MODIFIED Requirements + +### Requirement: Token file covers all six groups +`src/tokens/globals.css` SHALL define CSS custom properties on `:root` for all six token groups specified in PROJECT.md: colour palette, typography scale, spacing scale, border radius, shadows, and motion. The file SHALL additionally define locale-specific overrides for typography tokens (`--font-body`, `--font-heading`, `--leading-body`, `--leading-heading`) scoped to `[data-locale]` attribute selectors as specified in the `localisation-typography` spec. + +#### Scenario: All groups present +- **WHEN** the CSS file is parsed +- **THEN** it SHALL contain a custom property for every token listed in PROJECT.md — no group is missing, no token is omitted + +#### Scenario: Mobile viewport — tokens load +- **WHEN** a product imports `globals.css` on a 390px viewport +- **THEN** all `:root` custom properties are available and resolve correctly — no breakpoint or media query restricts token definitions + +#### Scenario: Locale typography overrides present +- **WHEN** `globals.css` is parsed +- **THEN** it SHALL contain `[data-locale="en"]` and `[data-locale="zh-Hans"]` blocks that override `--font-body`, `--font-heading`, `--leading-body`, and `--leading-heading` diff --git a/openspec/changes/archive/2026-03-31-localisation-ja-en-zh/specs/localisation-copy/spec.md b/openspec/changes/archive/2026-03-31-localisation-ja-en-zh/specs/localisation-copy/spec.md new file mode 100644 index 0000000..4d5e82c --- /dev/null +++ b/openspec/changes/archive/2026-03-31-localisation-ja-en-zh/specs/localisation-copy/spec.md @@ -0,0 +1,68 @@ +## ADDED Requirements + +### Requirement: MoriCopy catalogue covers all standard UI patterns +`src/lib/locale/copy.ts` SHALL export a `MoriCopy` object with the shape `Record`. The `CopyKeys` type SHALL be a TypeScript interface covering every standard UI pattern listed below. Accessing a key that does not exist on `CopyKeys` SHALL be a compile error. + +Standard UI patterns that MUST be covered: + +| Key | `ja` | `en` | `zh-Hans` | +|-----|------|------|-----------| +| `action.save` | `保存する` | `Save` | `保存` | +| `action.delete` | `削除する` | `Delete` | `删除` | +| `action.cancel` | `キャンセル` | `Cancel` | `取消` | +| `action.add` | `追加する` | `Add` | `添加` | +| `action.edit` | `編集する` | `Edit` | `编辑` | +| `action.confirm` | `確認する` | `Confirm` | `确认` | +| `action.close` | `閉じる` | `Close` | `关闭` | +| `action.back` | `戻る` | `Back` | `返回` | +| `action.next` | `次へ` | `Next` | `下一步` | +| `action.submit` | `送信する` | `Submit` | `提交` | +| `status.loading` | `処理中...` | `Loading...` | `处理中...` | +| `status.saving` | `保存中...` | `Saving...` | `保存中...` | +| `status.success` | `✓ 保存しました` | `✓ Saved` | `✓ 已保存` | +| `status.error` | `エラーが発生しました。もう一度お試しください。` | `Something went wrong. Please try again.` | `发生错误,请重试。` | +| `empty.default` | `まだデータがありません` | `No data yet` | `暂无数据` | +| `empty.search` | `検索結果がありません` | `No results found` | `未找到结果` | +| `confirm.destructiveHint` | `この操作は取り消せません。` | `This action cannot be undone.` | `此操作无法撤消。` | +| `dialog.close` | `閉じる` | `Close` | `关闭` | +| `pagination.previous` | `前へ` | `Previous` | `上一页` | +| `pagination.next` | `次へ` | `Next` | `下一页` | + +#### Scenario: All keys present for all locales +- **WHEN** the `MoriCopy` object is type-checked +- **THEN** TypeScript SHALL confirm that every `Locale` key maps to a complete `CopyKeys` object with no missing properties + +#### Scenario: Missing key is a compile error +- **WHEN** code accesses `copy[locale].nonExistentKey` +- **THEN** TypeScript SHALL emit a property-not-found error + +--- + +### Requirement: Copy is consumed via the `useCopy()` hook +`src/lib/locale/hooks.ts` SHALL export a `useCopy(): CopyKeys` hook. It MUST call `useLocale()` internally and return `MoriCopy[locale]`. Components MUST use this hook to access copy strings rather than importing `MoriCopy` directly. + +#### Scenario: Hook returns locale-appropriate copy +- **WHEN** `useCopy()` is called inside a `LocaleProvider` with locale `'en'` +- **THEN** `copy.action.save` SHALL equal `'Save'` + +#### Scenario: Hook returns Japanese copy by default +- **WHEN** `useCopy()` is called inside a `LocaleProvider` with locale `'ja'` +- **THEN** `copy.status.loading` SHALL equal `'処理中...'` + +--- + +### Requirement: Simplified Chinese copy is marked for human review +All `zh-Hans` values in `MoriCopy` SHALL be accompanied by a `// TODO: zh-Hans review` comment in source until a native Simplified Chinese speaker has reviewed and approved the copy. The build SHALL NOT fail due to these comments. + +#### Scenario: Review flag present on zh-Hans entries +- **WHEN** `src/lib/locale/copy.ts` is read +- **THEN** each `zh-Hans` copy entry SHALL have a `// TODO: zh-Hans review` comment on the same or preceding line + +--- + +### Requirement: Destructive confirmation layout follows locale convention +In all locales, the destructive action button SHALL appear on the RIGHT side of a confirmation dialog footer. Cancel SHALL appear on the LEFT. This rule is not locale-specific but is codified here as a universal convention. + +#### Scenario: Confirm dialog button order +- **WHEN** a destructive confirm dialog renders in any locale +- **THEN** the button order (left to right) SHALL be: [Cancel / キャンセル / 取消] then [Delete / 削除する / 删除] diff --git a/openspec/changes/archive/2026-03-31-localisation-ja-en-zh/specs/localisation-core/spec.md b/openspec/changes/archive/2026-03-31-localisation-ja-en-zh/specs/localisation-core/spec.md new file mode 100644 index 0000000..10cfc46 --- /dev/null +++ b/openspec/changes/archive/2026-03-31-localisation-ja-en-zh/specs/localisation-core/spec.md @@ -0,0 +1,89 @@ +## ADDED Requirements + +### Requirement: Supported locales are defined as a TypeScript type +The system SHALL define a `Locale` type as `'ja' | 'en' | 'zh-Hans'` in `src/lib/locale/types.ts`. All locale-aware APIs MUST use this type. Using a raw `string` where `Locale` is expected SHALL be a compile error. + +#### Scenario: Type exhaustiveness +- **WHEN** a switch statement covers all `Locale` values +- **THEN** TypeScript SHALL infer the default branch as `never`, confirming exhaustiveness + +#### Scenario: Invalid locale rejected +- **WHEN** a value `'fr'` is passed where `Locale` is expected +- **THEN** the TypeScript compiler SHALL emit an error + +--- + +### Requirement: Locale context is provided at the root layout +The root layout in each product repo SHALL render a `LocaleProvider` component (exported from `@mori/ui`) that makes the current `Locale` available via a `useLocale()` hook to all descendant components. The provider MUST accept a `locale: Locale` prop. + +#### Scenario: Locale available to child components +- **WHEN** a component calls `useLocale()` inside a `LocaleProvider` +- **THEN** it SHALL receive the `Locale` value passed to the provider + +#### Scenario: Missing provider throws +- **WHEN** a component calls `useLocale()` outside any `LocaleProvider` +- **THEN** it SHALL throw a descriptive error: `"useLocale must be used inside LocaleProvider"` + +--- + +### Requirement: Next.js i18n routing is configured with URL prefixes +Each product's `next.config.ts` SHALL include an `i18n` block with `locales: ['ja', 'en', 'zh']` and `defaultLocale: 'ja'`. The URL prefix SHALL be the locale code (e.g. `/en/dashboard`). The `ja` locale SHALL be the default and MAY be served without an explicit prefix via Next.js `prefetch` links. + +#### Scenario: English locale URL +- **WHEN** a user navigates to `/en/dashboard` +- **THEN** the page SHALL render with locale `'en'` active + +#### Scenario: Default locale redirect +- **WHEN** a user navigates to `/dashboard` (no prefix) +- **THEN** Next.js SHALL serve the page as locale `'ja'` + +--- + +### Requirement: Locale is persisted to the user profile +When an authenticated user accesses the app with a locale prefix, the system SHALL update `user_preferences.locale` in Supabase to the current locale. On subsequent logins, the app SHALL redirect to the locale saved in the profile. + +#### Scenario: Locale saved on first access +- **WHEN** an authenticated user loads `/en/dashboard` for the first time +- **THEN** `user_preferences.locale` SHALL be updated to `'en'` in Supabase + +#### Scenario: Locale restored on login +- **WHEN** a user with `user_preferences.locale = 'zh-Hans'` logs in at `/` +- **THEN** the app SHALL redirect them to `/zh/` + +--- + +### Requirement: Locale-aware date formatting utility +`src/lib/locale/format.ts` SHALL export a `formatDate(date: Date, locale: Locale): string` function. It MUST use `Intl.DateTimeFormat` with locale-appropriate options: +- `ja`: `2025年1月28日(火)` — uses `'ja-JP'` with `era`-omitted full date + weekday +- `en`: `Tuesday, January 28, 2025` — uses `'en-US'` with `weekday: 'long', year: 'numeric', month: 'long', day: 'numeric'` +- `zh-Hans`: `2025年1月28日 星期二` — uses `'zh-Hans-CN'` with full date + weekday + +#### Scenario: Japanese date format +- **WHEN** `formatDate(new Date('2025-01-28'), 'ja')` is called +- **THEN** it SHALL return `'2025年1月28日(火)'` + +#### Scenario: English date format +- **WHEN** `formatDate(new Date('2025-01-28'), 'en')` is called +- **THEN** it SHALL return `'Tuesday, January 28, 2025'` + +#### Scenario: Chinese date format +- **WHEN** `formatDate(new Date('2025-01-28'), 'zh-Hans')` is called +- **THEN** it SHALL return `'2025年1月28日 星期二'` + +--- + +### Requirement: Locale-aware currency formatting utility +`format.ts` SHALL export a `formatCurrency(amount: number, locale: Locale): string` function using `Intl.NumberFormat`. All three locales display JPY with no decimal places and a `¥` prefix. The thousand separator SHALL be a comma. + +#### Scenario: Currency formatted for all locales +- **WHEN** `formatCurrency(1980, locale)` is called for any supported locale +- **THEN** it SHALL return `'¥1,980'` + +--- + +### Requirement: `data-locale` attribute is set on `` +The root layout SHALL set `data-locale` on the `` element to the active `Locale` value (`'ja'`, `'en'`, or `'zh-Hans'`). This is the hook used by CSS to apply per-locale typography overrides. + +#### Scenario: Attribute present in rendered HTML +- **WHEN** the page is rendered with locale `'en'` +- **THEN** the `` element SHALL have `data-locale="en"` diff --git a/openspec/changes/archive/2026-03-31-localisation-ja-en-zh/specs/localisation-typography/spec.md b/openspec/changes/archive/2026-03-31-localisation-ja-en-zh/specs/localisation-typography/spec.md new file mode 100644 index 0000000..7a9d261 --- /dev/null +++ b/openspec/changes/archive/2026-03-31-localisation-ja-en-zh/specs/localisation-typography/spec.md @@ -0,0 +1,67 @@ +## ADDED Requirements + +### Requirement: Per-locale font stack tokens are defined +`src/tokens/globals.css` SHALL define locale-specific font stack overrides scoped to `[data-locale]` attributes on ``. These override the default CJK-optimised stack when the locale is `en`. + +Font stack rules: + +| Locale selector | `--font-body` | `--font-heading` | +|-----------------|---------------|-----------------| +| `:root` (default, CJK) | `'Hiragino Kaku Gothic ProN', 'Hiragino Sans', 'Noto Sans JP', 'Yu Gothic', sans-serif` | same as body | +| `[data-locale="en"]` | `'Inter', 'Helvetica Neue', Arial, sans-serif` | `'Inter', 'Helvetica Neue', Arial, sans-serif` | +| `[data-locale="zh-Hans"]` | `'PingFang SC', 'Noto Sans SC', 'Microsoft YaHei', sans-serif` | same as body | + +The `[data-locale="ja"]` selector SHALL NOT override `:root` (Japanese uses the default). + +#### Scenario: English font stack applied +- **WHEN** `` is rendered +- **THEN** `--font-body` SHALL resolve to the Latin stack starting with `'Inter'` + +#### Scenario: Chinese font stack applied +- **WHEN** `` is rendered +- **THEN** `--font-body` SHALL resolve to the CJK stack starting with `'PingFang SC'` + +#### Scenario: Japanese uses default +- **WHEN** `` or `` (no attribute) is rendered +- **THEN** `--font-body` SHALL resolve to the default Hiragino/Noto stack from `:root` + +--- + +### Requirement: Per-locale line-height tokens are defined +`globals.css` SHALL define `--leading-body` and `--leading-heading` overrides per locale. CJK scripts require looser line-height than Latin for readability. + +Line-height rules: + +| Locale selector | `--leading-body` | `--leading-heading` | +|-----------------|-----------------|---------------------| +| `:root` (CJK default) | `1.8` | `1.4` | +| `[data-locale="en"]` | `1.6` | `1.25` | +| `[data-locale="zh-Hans"]` | `1.8` | `1.4` | + +All components using body text MUST reference `var(--leading-body)` rather than a hardcoded `line-height` value. + +#### Scenario: English line-height is tighter +- **WHEN** `` is rendered +- **THEN** `--leading-body` SHALL resolve to `1.6` + +#### Scenario: CJK line-height is looser +- **WHEN** `` or `` is rendered +- **THEN** `--leading-body` SHALL resolve to `1.8` + +--- + +### Requirement: All components reference line-height via token +Every Mori component that sets `line-height` on body text SHALL use `var(--leading-body)`. Components that set `line-height` on heading text SHALL use `var(--leading-heading)`. Hardcoded numeric `line-height` values on text elements SHALL NOT appear in component source. + +#### Scenario: No hardcoded line-height in components +- **WHEN** component source files under `src/components/` are audited +- **THEN** no element with visible text SHALL have a hardcoded `line-height` CSS property — each SHALL reference `--leading-body` or `--leading-heading` + +--- + +### Requirement: No web fonts are loaded by the design system +The Mori DS token file SHALL NOT include `@font-face` rules or external font `@import` statements. Font loading is the responsibility of each product repo (e.g. via Next.js `next/font`). The design system only defines which font family names to reference in the stack. + +#### Scenario: No @font-face in globals.css +- **WHEN** `src/tokens/globals.css` is parsed +- **THEN** it SHALL contain zero `@font-face` blocks and zero `@import` statements loading external font URLs diff --git a/openspec/changes/archive/2026-03-31-localisation-ja-en-zh/tasks.md b/openspec/changes/archive/2026-03-31-localisation-ja-en-zh/tasks.md new file mode 100644 index 0000000..54410eb --- /dev/null +++ b/openspec/changes/archive/2026-03-31-localisation-ja-en-zh/tasks.md @@ -0,0 +1,50 @@ +## 1. Foundation — Types and Tokens + +- [x] 1.1 Create `src/lib/locale/types.ts` — define `Locale` type as `'ja' | 'en' | 'zh-Hans'` +- [x] 1.2 Add `[data-locale="en"]` and `[data-locale="zh-Hans"]` typography override blocks to `src/tokens/globals.css` (font stacks + line-height tokens per `localisation-typography` spec) +- [x] 1.3 Audit all components in `src/components/` and replace any hardcoded `line-height` values with `var(--leading-body)` or `var(--leading-heading)` +- [x] 1.4 Update `openspec/specs/design-tokens/spec.md` in the main specs folder to reflect the new locale typography override requirements (archive-time sync) + +## 2. Locale Context and Hooks + +- [x] 2.1 Create `src/lib/locale/context.tsx` — `LocaleProvider` component and `useLocale()` hook +- [x] 2.2 Write unit tests for `LocaleProvider` (renders children, provides locale, throws outside provider) +- [x] 2.3 Export `LocaleProvider` and `useLocale` from `src/lib/locale/index.ts` + +## 3. Copy Catalogue + +- [x] 3.1 Create `src/lib/locale/copy.ts` — define `CopyKeys` interface and `MoriCopy` object with all 20 standard patterns in `ja`, `en`, and `zh-Hans` (mark each zh-Hans entry with `// TODO: zh-Hans review`) +- [x] 3.2 Create `useCopy()` hook in `src/lib/locale/hooks.ts` — returns `MoriCopy[locale]` via `useLocale()` +- [x] 3.3 Write unit tests for `useCopy()` — verify correct copy returned per locale + +## 4. Formatting Utilities + +- [x] 4.1 Create `src/lib/locale/format.ts` — implement `formatDate(date, locale)` using `Intl.DateTimeFormat` per locale spec +- [x] 4.2 Implement `formatCurrency(amount, locale)` in `format.ts` using `Intl.NumberFormat` (JPY, no decimals) +- [x] 4.3 Write unit tests for `formatDate` — assert exact output strings for all three locales +- [x] 4.4 Write unit tests for `formatCurrency` — assert `'¥1,980'` for all three locales + +## 5. Next.js Routing Configuration (Design System Guidance) + +- [x] 5.1 Add `i18n` routing configuration example to `PROJECT.md` (locales array, defaultLocale, URL prefix strategy) +- [x] 5.2 Document `data-locale` root layout setup in `PROJECT.md` (how to set attribute on `` in App Router) +- [x] 5.3 Document legacy route redirect pattern in `PROJECT.md` (unprefixed `/dashboard` → `/ja/dashboard`) + +## 6. Supabase Schema Guidance + +- [x] 6.1 Add `user_preferences.locale` column migration SQL example to `PROJECT.md` (`text`, nullable, default `'ja'`) +- [x] 6.2 Document locale persistence pattern in `PROJECT.md` (read on login, write on locale change) + +## 7. PROJECT.md Updates + +- [x] 7.1 Remove "Internationalisation for languages other than Japanese" from the Out of Scope section +- [x] 7.2 Update UI copy conventions section to be multi-locale (replace Japanese-only table with ja/en/zh-Hans table) +- [x] 7.3 Update the "All user-facing text in Japanese" constraint to "All user-facing text in the active locale" +- [x] 7.4 Update date/time formatting guidance to reference `formatDate` utility with locale parameter + +## 8. Exports and Package Surface + +- [x] 8.1 Export all locale utilities from the top-level package entry point (`src/index.ts` or equivalent) — `LocaleProvider`, `useLocale`, `useCopy`, `formatDate`, `formatCurrency`, `Locale` type +- [x] 8.2 Run `pnpm typecheck` — confirm zero type errors +- [x] 8.3 Run `pnpm test` — confirm all new unit tests pass +- [x] 8.4 Run `pnpm lint` — confirm zero lint errors diff --git a/openspec/specs/design-tokens/spec.md b/openspec/specs/design-tokens/spec.md index 6a94740..dd0c1e2 100644 --- a/openspec/specs/design-tokens/spec.md +++ b/openspec/specs/design-tokens/spec.md @@ -6,7 +6,7 @@ Define the requirements for Mori design token files — ensuring all six token g ## Requirements ### Requirement: Token file covers all six groups -`src/tokens/globals.css` SHALL define CSS custom properties on `:root` for all six token groups specified in PROJECT.md: colour palette, typography scale, spacing scale, border radius, shadows, and motion. +`src/tokens/globals.css` SHALL define CSS custom properties on `:root` for all six token groups specified in PROJECT.md: colour palette, typography scale, spacing scale, border radius, shadows, and motion. The file SHALL additionally define locale-specific overrides for typography tokens (`--font-body`, `--font-heading`, `--leading-body`, `--leading-heading`) scoped to `[data-locale]` attribute selectors as specified in the `localisation-typography` spec. #### Scenario: All groups present - **WHEN** the CSS file is parsed @@ -16,6 +16,10 @@ Define the requirements for Mori design token files — ensuring all six token g - **WHEN** a product imports `globals.css` on a 390px viewport - **THEN** all `:root` custom properties are available and resolve correctly — no breakpoint or media query restricts token definitions +#### Scenario: Locale typography overrides present +- **WHEN** `globals.css` is parsed +- **THEN** it SHALL contain `[data-locale="en"]` and `[data-locale="zh-Hans"]` blocks that override `--font-body`, `--font-heading`, `--leading-body`, and `--leading-heading` + ### Requirement: Token names match PROJECT.md exactly Every CSS custom property name in `globals.css` SHALL match the name defined in PROJECT.md character-for-character (e.g., `--color-brand`, `--space-4`, `--radius-md`). @@ -25,4 +29,4 @@ Every CSS custom property name in `globals.css` SHALL match the name defined in #### Scenario: Token count matches specification - **WHEN** the total token count per group is checked -- **THEN** it SHALL equal: colour 20, typography 13, spacing 10, radius 5, shadows 3, motion 6 (57 tokens total) +- **THEN** it SHALL equal: colour 19, typography 17, spacing 10, radius 5, shadows 3, motion 6 (60 tokens total) diff --git a/openspec/specs/localisation-copy/spec.md b/openspec/specs/localisation-copy/spec.md new file mode 100644 index 0000000..fea2a1d --- /dev/null +++ b/openspec/specs/localisation-copy/spec.md @@ -0,0 +1,73 @@ +# localisation-copy Specification + +## Purpose +Define the requirements for the Mori copy catalogue — the typed `MoriCopy` object, `useCopy()` hook, Simplified Chinese review process, and universal confirmation dialog button-order convention. + +## Requirements + +### Requirement: MoriCopy catalogue covers all standard UI patterns +`src/lib/locale/copy.ts` SHALL export a `MoriCopy` object with the shape `Record`. The `CopyKeys` type SHALL be a TypeScript interface covering every standard UI pattern listed below. Accessing a key that does not exist on `CopyKeys` SHALL be a compile error. + +Standard UI patterns that MUST be covered: + +| Key | `ja` | `en` | `zh-Hans` | +|-----|------|------|-----------| +| `action.save` | `保存する` | `Save` | `保存` | +| `action.delete` | `削除する` | `Delete` | `删除` | +| `action.cancel` | `キャンセル` | `Cancel` | `取消` | +| `action.add` | `追加する` | `Add` | `添加` | +| `action.edit` | `編集する` | `Edit` | `编辑` | +| `action.confirm` | `確認する` | `Confirm` | `确认` | +| `action.close` | `閉じる` | `Close` | `关闭` | +| `action.back` | `戻る` | `Back` | `返回` | +| `action.next` | `次へ` | `Next` | `下一步` | +| `action.submit` | `送信する` | `Submit` | `提交` | +| `status.loading` | `処理中...` | `Loading...` | `处理中...` | +| `status.saving` | `保存中...` | `Saving...` | `保存中...` | +| `status.success` | `✓ 保存しました` | `✓ Saved` | `✓ 已保存` | +| `status.error` | `エラーが発生しました。もう一度お試しください。` | `Something went wrong. Please try again.` | `发生错误,请重试。` | +| `empty.default` | `まだデータがありません` | `No data yet` | `暂无数据` | +| `empty.search` | `検索結果がありません` | `No results found` | `未找到结果` | +| `confirm.destructiveHint` | `この操作は取り消せません。` | `This action cannot be undone.` | `此操作无法撤消。` | +| `dialog.close` | `閉じる` | `Close` | `关闭` | +| `pagination.previous` | `前へ` | `Previous` | `上一页` | +| `pagination.next` | `次へ` | `Next` | `下一页` | + +#### Scenario: All keys present for all locales +- **WHEN** the `MoriCopy` object is type-checked +- **THEN** TypeScript SHALL confirm that every `Locale` key maps to a complete `CopyKeys` object with no missing properties + +#### Scenario: Missing key is a compile error +- **WHEN** code accesses `copy[locale].nonExistentKey` +- **THEN** TypeScript SHALL emit a property-not-found error + +--- + +### Requirement: Copy is consumed via the `useCopy()` hook +`src/lib/locale/hooks.ts` SHALL export a `useCopy(): CopyKeys` hook. It MUST call `useLocale()` internally and return `MoriCopy[locale]`. Components MUST use this hook to access copy strings rather than importing `MoriCopy` directly. + +#### Scenario: Hook returns locale-appropriate copy +- **WHEN** `useCopy()` is called inside a `LocaleProvider` with locale `'en'` +- **THEN** `copy.action.save` SHALL equal `'Save'` + +#### Scenario: Hook returns Japanese copy by default +- **WHEN** `useCopy()` is called inside a `LocaleProvider` with locale `'ja'` +- **THEN** `copy.status.loading` SHALL equal `'処理中...'` + +--- + +### Requirement: Simplified Chinese copy is marked for human review +All `zh-Hans` values in `MoriCopy` SHALL be accompanied by a `// TODO: zh-Hans review` comment in source until a native Simplified Chinese speaker has reviewed and approved the copy. The build SHALL NOT fail due to these comments. + +#### Scenario: Review flag present on zh-Hans entries +- **WHEN** `src/lib/locale/copy.ts` is read +- **THEN** each `zh-Hans` copy entry SHALL have a `// TODO: zh-Hans review` comment on the same or preceding line + +--- + +### Requirement: Destructive confirmation layout follows locale convention +In all locales, the destructive action button SHALL appear on the RIGHT side of a confirmation dialog footer. Cancel SHALL appear on the LEFT. This rule is not locale-specific but is codified here as a universal convention. + +#### Scenario: Confirm dialog button order +- **WHEN** a destructive confirm dialog renders in any locale +- **THEN** the button order (left to right) SHALL be: [Cancel / キャンセル / 取消] then [Delete / 削除する / 删除] diff --git a/openspec/specs/localisation-core/spec.md b/openspec/specs/localisation-core/spec.md new file mode 100644 index 0000000..f8fc123 --- /dev/null +++ b/openspec/specs/localisation-core/spec.md @@ -0,0 +1,94 @@ +# localisation-core Specification + +## Purpose +Define the requirements for Mori's locale infrastructure — the `Locale` type, `LocaleProvider`, Next.js i18n routing, user-profile persistence, and locale-aware formatting utilities used across all Mori products. + +## Requirements + +### Requirement: Supported locales are defined as a TypeScript type +The system SHALL define a `Locale` type as `'ja' | 'en' | 'zh-Hans'` in `src/lib/locale/types.ts`. All locale-aware APIs MUST use this type. Using a raw `string` where `Locale` is expected SHALL be a compile error. + +#### Scenario: Type exhaustiveness +- **WHEN** a switch statement covers all `Locale` values +- **THEN** TypeScript SHALL infer the default branch as `never`, confirming exhaustiveness + +#### Scenario: Invalid locale rejected +- **WHEN** a value `'fr'` is passed where `Locale` is expected +- **THEN** the TypeScript compiler SHALL emit an error + +--- + +### Requirement: Locale context is provided at the root layout +The root layout in each product repo SHALL render a `LocaleProvider` component (exported from `@mori/ui`) that makes the current `Locale` available via a `useLocale()` hook to all descendant components. The provider MUST accept a `locale: Locale` prop. + +#### Scenario: Locale available to child components +- **WHEN** a component calls `useLocale()` inside a `LocaleProvider` +- **THEN** it SHALL receive the `Locale` value passed to the provider + +#### Scenario: Missing provider throws +- **WHEN** a component calls `useLocale()` outside any `LocaleProvider` +- **THEN** it SHALL throw a descriptive error: `"useLocale must be used inside LocaleProvider"` + +--- + +### Requirement: Next.js i18n routing is configured with URL prefixes +Each product's `next.config.ts` SHALL include an `i18n` block with `locales: ['ja', 'en', 'zh']` and `defaultLocale: 'ja'`. The URL prefix SHALL be the locale code (e.g. `/en/dashboard`). The `ja` locale SHALL be the default and MAY be served without an explicit prefix via Next.js `prefetch` links. + +#### Scenario: English locale URL +- **WHEN** a user navigates to `/en/dashboard` +- **THEN** the page SHALL render with locale `'en'` active + +#### Scenario: Default locale redirect +- **WHEN** a user navigates to `/dashboard` (no prefix) +- **THEN** Next.js SHALL serve the page as locale `'ja'` + +--- + +### Requirement: Locale is persisted to the user profile +When an authenticated user accesses the app with a locale prefix, the system SHALL update `user_preferences.locale` in Supabase to the current locale. On subsequent logins, the app SHALL redirect to the locale saved in the profile. + +#### Scenario: Locale saved on first access +- **WHEN** an authenticated user loads `/en/dashboard` for the first time +- **THEN** `user_preferences.locale` SHALL be updated to `'en'` in Supabase + +#### Scenario: Locale restored on login +- **WHEN** a user with `user_preferences.locale = 'zh-Hans'` logs in at `/` +- **THEN** the app SHALL redirect them to `/zh/` + +--- + +### Requirement: Locale-aware date formatting utility +`src/lib/locale/format.ts` SHALL export a `formatDate(date: Date, locale: Locale): string` function. It MUST use `Intl.DateTimeFormat` with locale-appropriate options: +- `ja`: `2025年1月28日(火)` — uses `'ja-JP'` with `era`-omitted full date + weekday +- `en`: `Tuesday, January 28, 2025` — uses `'en-US'` with `weekday: 'long', year: 'numeric', month: 'long', day: 'numeric'` +- `zh-Hans`: `2025年1月28日 星期二` — uses `'zh-Hans-CN'` with full date + weekday + +#### Scenario: Japanese date format +- **WHEN** `formatDate(new Date('2025-01-28'), 'ja')` is called +- **THEN** it SHALL return `'2025年1月28日(火)'` + +#### Scenario: English date format +- **WHEN** `formatDate(new Date('2025-01-28'), 'en')` is called +- **THEN** it SHALL return `'Tuesday, January 28, 2025'` + +#### Scenario: Chinese date format +- **WHEN** `formatDate(new Date('2025-01-28'), 'zh-Hans')` is called +- **THEN** it SHALL return `'2025年1月28日 星期二'` + +--- + +### Requirement: Locale-aware currency formatting utility +`format.ts` SHALL export a `formatCurrency(amount: number, locale: Locale): string` function using `Intl.NumberFormat`. All three locales display JPY with no decimal places and a `¥` prefix. The thousand separator SHALL be a comma. + +#### Scenario: Currency formatted for all locales +- **WHEN** `formatCurrency(1980, locale)` is called for any supported locale +- **THEN** it SHALL return `'¥1,980'` + +--- + +### Requirement: `data-locale` attribute is set on `` +The root layout SHALL set `data-locale` on the `` element to the active `Locale` value (`'ja'`, `'en'`, or `'zh-Hans'`). This is the hook used by CSS to apply per-locale typography overrides. + +#### Scenario: Attribute present in rendered HTML +- **WHEN** the page is rendered with locale `'en'` +- **THEN** the `` element SHALL have `data-locale="en"` diff --git a/openspec/specs/localisation-typography/spec.md b/openspec/specs/localisation-typography/spec.md new file mode 100644 index 0000000..a4d9875 --- /dev/null +++ b/openspec/specs/localisation-typography/spec.md @@ -0,0 +1,72 @@ +# localisation-typography Specification + +## Purpose +Define the requirements for Mori's per-locale typography tokens — font stack overrides and line-height tokens scoped to `[data-locale]` attributes — ensuring each supported locale renders with an appropriate and readable typeface and rhythm. + +## Requirements + +### Requirement: Per-locale font stack tokens are defined +`src/tokens/globals.css` SHALL define locale-specific font stack overrides scoped to `[data-locale]` attributes on ``. These override the default CJK-optimised stack when the locale is `en`. + +Font stack rules: + +| Locale selector | `--font-body` | `--font-heading` | +|-----------------|---------------|-----------------| +| `:root` (default, CJK) | `'Hiragino Kaku Gothic ProN', 'Hiragino Sans', 'Noto Sans JP', 'Yu Gothic', sans-serif` | same as body | +| `[data-locale="en"]` | `'Inter', 'Helvetica Neue', Arial, sans-serif` | `'Inter', 'Helvetica Neue', Arial, sans-serif` | +| `[data-locale="zh-Hans"]` | `'PingFang SC', 'Noto Sans SC', 'Microsoft YaHei', sans-serif` | same as body | + +The `[data-locale="ja"]` selector SHALL NOT override `:root` (Japanese uses the default). + +#### Scenario: English font stack applied +- **WHEN** `` is rendered +- **THEN** `--font-body` SHALL resolve to the Latin stack starting with `'Inter'` + +#### Scenario: Chinese font stack applied +- **WHEN** `` is rendered +- **THEN** `--font-body` SHALL resolve to the CJK stack starting with `'PingFang SC'` + +#### Scenario: Japanese uses default +- **WHEN** `` or `` (no attribute) is rendered +- **THEN** `--font-body` SHALL resolve to the default Hiragino/Noto stack from `:root` + +--- + +### Requirement: Per-locale line-height tokens are defined +`globals.css` SHALL define `--leading-body` and `--leading-heading` overrides per locale. CJK scripts require looser line-height than Latin for readability. + +Line-height rules: + +| Locale selector | `--leading-body` | `--leading-heading` | +|-----------------|-----------------|---------------------| +| `:root` (CJK default) | `1.8` | `1.4` | +| `[data-locale="en"]` | `1.6` | `1.25` | +| `[data-locale="zh-Hans"]` | `1.8` | `1.4` | + +All components using body text MUST reference `var(--leading-body)` rather than a hardcoded `line-height` value. + +#### Scenario: English line-height is tighter +- **WHEN** `` is rendered +- **THEN** `--leading-body` SHALL resolve to `1.6` + +#### Scenario: CJK line-height is looser +- **WHEN** `` or `` is rendered +- **THEN** `--leading-body` SHALL resolve to `1.8` + +--- + +### Requirement: All components reference line-height via token +Every Mori component that sets `line-height` on body text SHALL use `var(--leading-body)`. Components that set `line-height` on heading text SHALL use `var(--leading-heading)`. Hardcoded numeric `line-height` values on text elements SHALL NOT appear in component source. + +#### Scenario: No hardcoded line-height in components +- **WHEN** component source files under `src/components/` are audited +- **THEN** no element with visible text SHALL have a hardcoded `line-height` CSS property — each SHALL reference `--leading-body` or `--leading-heading` + +--- + +### Requirement: No web fonts are loaded by the design system +The Mori DS token file SHALL NOT include `@font-face` rules or external font `@import` statements. Font loading is the responsibility of each product repo (e.g. via Next.js `next/font`). The design system only defines which font family names to reference in the stack. + +#### Scenario: No @font-face in globals.css +- **WHEN** `src/tokens/globals.css` is parsed +- **THEN** it SHALL contain zero `@font-face` blocks and zero `@import` statements loading external font URLs diff --git a/showcase/src/App.tsx b/showcase/src/App.tsx index 1ef4956..5a830c7 100644 --- a/showcase/src/App.tsx +++ b/showcase/src/App.tsx @@ -13,7 +13,7 @@ export function App(): JSX.Element { return (
-
+
diff --git a/showcase/src/components/SectionWrapper.tsx b/showcase/src/components/SectionWrapper.tsx index c5b3f15..553bd08 100644 --- a/showcase/src/components/SectionWrapper.tsx +++ b/showcase/src/components/SectionWrapper.tsx @@ -16,10 +16,7 @@ export function SectionWrapper({ children, }: SectionWrapperProps): JSX.Element { return ( -
+
-

+

{titleEn}

-

+

{titleJa}

diff --git a/showcase/src/components/ShowcaseHeader.tsx b/showcase/src/components/ShowcaseHeader.tsx index 126c483..d82b441 100644 --- a/showcase/src/components/ShowcaseHeader.tsx +++ b/showcase/src/components/ShowcaseHeader.tsx @@ -10,13 +10,13 @@ export function ShowcaseHeader(): JSX.Element { return (

Design System

森 @@ -36,15 +36,12 @@ export function ShowcaseHeader(): JSX.Element { {STATS.map((stat) => (

{stat.value}

-

+

{stat.label}

diff --git a/showcase/src/components/ShowcaseSidebar.tsx b/showcase/src/components/ShowcaseSidebar.tsx index 082bbe7..408606c 100644 --- a/showcase/src/components/ShowcaseSidebar.tsx +++ b/showcase/src/components/ShowcaseSidebar.tsx @@ -33,7 +33,7 @@ export function ShowcaseSidebar(): JSX.Element { return (