Detailed specifications for the (planned) admin translation console and
the live catalog-delivery flow. Largest file: every screen's anatomy,
states, behaviors, edge cases, a11y, motion. Components per 00-shared/03 ;
motion tokens per 00-shared/08 ; a11y baseline per 00-shared/09 .
The only production surface today. The app:
Resolves locale via Accept-Language (middleware, i18n.middleware.ts:13-18)
— or passes ?locale= explicitly.
Calls GET /api/v1/i18n/messages?locale=hi → full Record<string,string>
(i18n.controller.ts:13-17, getMessages fallback to en,
i18n.service.ts:47-49).
Caches locally; refreshes on locale change or app update
(cache version = catalog hash (planned) ).
Contract edge cases:
locale=xx (unknown) → English catalog (i18n.service.spec.ts:48-51).
Missing header → English.
Response is a flat key→string map; no nested structure — the app must
flatten ARB/JSON to the same flat map for lookup parity.
AppBar "Translations" [Sync status chip] [FAB +]
SearchBar (debounced, min 2 chars)
Filter chips: All | missing:hi | missing:ta | has-params | namespace:errors
────────────────────────────────────────────────────────
DataTable / ListTile rows:
key (mono, truncated) · params chips · coverage badge · edited
[Pagination: page size 25, "load more"]
State Behavior
loading AppSkeleton rows (10); search disabled until first paint
success rows with per-locale coverage badge (ok = all shipped locales present, missing = ≥1 shipped locale lacks key)
empty AppEmptyState "No keys" + CTA "Create first key"; distinguishes "no filters match" vs "zero keys"
error 401 → redirect login; 403 (no i18n.* perm (planned) ) → permission state with copy; 5xx → retry CTA
offline AppOfflineBanner + cached last data (stale-while-revalidate)
Row tap → Key Editor (§2). Long-press → multi-select for bulk delete/export.
Locale filter chips are multi-select; missing:<locale> preselects keys the
given locale lacks (drill-down from Locale Management).
Search matches key + source value (en) + params.
Keyboard: / focuses search. Esc clears filters.
Motion: rows stagger m-fast; filter change animates list m-base fade.
Search labeled "Search translation keys"; results announced count
(live region).
Badges have text labels ("missing in hi"), not color-only.
Focus lands on first row after search completes.
Header: key (mono) · params chips {days} · [Delete] [Save]
Tabs (desktop: side rail, phone: horizontal chips): en · hi · ta · te · kn · ml · gu · mr · bn
Per-locale field card:
[locale tag + name] [status chip: shipped|declared|missing]
AppTextField (multiline ≥3 rows, auto-grow)
└─ placeholder checker: "params match source" / "missing {days}" (inline, amber)
Dirty indicator per field (dot); unsaved-changes guard on exit.
State Behavior
loading skeleton; key header + params only
loaded per-locale values from GET /api/v1/i18n/keys/:key (planned)
missing locale field empty + amber outline + chip missing; block "promote" of a locale with missing keys
saving field-level spinner; Save disabled while pending
saved snackbar "Saved" (module-level); per-field check animates
conflict stale version → dialog "Reload or overwrite"
error field error inline; network → AppOfflineBanner, no data loss (drafts kept in memory)
Placeholder parity check (critical) — runs on every edit:
value.match(/\{(\w+)\}/g) compared against the en source set
(i18n.service.ts:42-44 regex). Mismatch = amber inline warning, Save
allowed but flagged; CI drift check (planned)
(IMPLEMENTATION_PLAN.md:845) blocks promotion.
Empty value in shipped locale → Save blocked with red inline error
(empty string would degrade the never-empty guarantee,
i18n.service.ts:40).
Delete → AppDialog confirm (destructive, danger button); deletes key in
all locales. Undo snackbar 5 s (planned) .
Unicode note: Devanagari/Tamil/Telugu etc. fields keep
TextCapitalization.none; no autocorrect; maxLines ≥ 3, soft wrap.
Character counter: warn > 200 chars (push/payload limits (forward-looking) ).
Per-field Semantics labels "value in hi".
Status changes (missing → saved) announced via live region.
Tab navigation order: source (en) first, then locales, actions last.
Form (bottom sheet on phone, card on desktop, ≤ 720 dp):
key (dotted path; mono; prefix helper "errors., leave., …")
params (AppChips input — free text chips, e.g. {days}, {name})
en value (required)
hi value (optional; shipped → recommended)
[Create key]
Rule Behavior
key format /^[a-z][a-z0-9]*(\.[a-z][a-z0-9]*)*$/; invalid → inline error
duplicate 409 → inline "key exists" + "Open existing" link
placeholder self-check en value's {...} tokens must ⊆ declared params; mismatch → block Create
create POST /api/v1/i18n/keys (planned) → navigate to Editor with snackbar
error 5xx → retry; offline → banner
Key field hint: "lowercase, dots separate namespaces".
Errors linked to fields; first invalid focused.
Grid of AppCards (2 cols phone, 4 cols desktop):
[flag/script glyph] en — English [status chip shipped]
Coverage ring: 9/9 · "Add locale" dashed card
[Bottom sheet: add locale — pick from SUPPORTED_LOCALES minus shipped]
State Behavior
loading card skeletons
empty no declared locales → full-width add CTA
partial coverage ring amber + "x missing" link → Key List filtered missing:<locale>
promote ready coverage 100% + no param drift → "Promote to shipped" enabled (makes SUPPORTED_LOCALES include it; today code-level i18n.service.ts:9-21 (planned : data-driven)**)
demote confirm dialog; blocking if it is the only locale with a key
Adding a locale creates an empty catalog — every key shows missing
(drift flow, 03_User_Journey.md §J5).
Coverage = present keys / shipped-locale keys per locale.
RTL note: if an RTL locale (ar/ur) is ever added (forward-looking) ,
this screen shows a direction field; all LTR today.
Header: last CI run · [Run now] [Export CSV]
Table: locale × missing keys (count) → expand to key list → jump to editor
Placeholder drift panel: key, source params, locale params, [Fix]
success / stale ("last run 3 d ago") / failed (CI unavailable).
Empty: "All locales in sync" with celebratory success state.
Case Handling
hi.json missing a keyfalls back to en at runtime (i18n.service.ts:40); editor shows missing; never blank string
Param reorder/rename between locales flagged in editor + sync; runtime keeps literal {name} if param absent (i18n.service.ts:43)
Huge catalog (>10k keys) list paginates; fetch returns flat map (memory: ~1 MB per locale — acceptable; app caches one locale)
Locale promoted mid-session app refetches catalog on next launch; version header (planned) enables delta push (forward-looking)
Concurrent editors last-write-wins today; optimistic lock (version field) (planned) — conflict dialog §2
Offline admin console read-only cached view; saves queue (planned) ; catalog delivery to mobile unaffected (server-side)
List/filter transitions: m-base (200 ms) fade+slide.
Field save: m-fast check morph. Error shake reserved for form submit.
Coverage ring: animates on data load m-base ease-out.