feat: i18n — EN/MS/ZH translations with cookie-based locale switching, report form translated

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FFDuBhMKvoWjrWT3ZnGmmr
This commit is contained in:
2026-07-11 19:01:44 +08:00
co-authored by Claude Sonnet 4.6
parent c67c489ab3
commit 87264a5497
12 changed files with 372 additions and 52 deletions
+24
View File
@@ -0,0 +1,24 @@
'use client'
import { createContext, useContext } from 'react'
import type en from '../../messages/en.json'
export type Messages = typeof en
const I18nContext = createContext<Messages | null>(null)
export function I18nProvider({
messages,
children,
}: {
messages: Messages
children: React.ReactNode
}) {
return <I18nContext.Provider value={messages}>{children}</I18nContext.Provider>
}
export function useTranslations<K extends keyof Messages>(namespace: K): Messages[K] {
const ctx = useContext(I18nContext)
if (!ctx) throw new Error('useTranslations must be used inside I18nProvider')
return ctx[namespace]
}
+2
View File
@@ -0,0 +1,2 @@
export const SUPPORTED_LOCALES = ['en', 'ms', 'zh'] as const
export type Locale = typeof SUPPORTED_LOCALES[number]
+20
View File
@@ -0,0 +1,20 @@
import { cookies } from 'next/headers'
import { SUPPORTED_LOCALES, type Locale } from './locales'
import type { Messages } from './context'
export async function getLocale(): Promise<Locale> {
const cookieStore = await cookies()
const lang = cookieStore.get('locale')?.value
if (lang && (SUPPORTED_LOCALES as readonly string[]).includes(lang)) {
return lang as Locale
}
return 'en'
}
export async function loadMessages(locale: Locale): Promise<Messages> {
switch (locale) {
case 'ms': return (await import('../../messages/ms.json')).default as Messages
case 'zh': return (await import('../../messages/zh.json')).default as Messages
default: return (await import('../../messages/en.json')).default as Messages
}
}
+16
View File
@@ -0,0 +1,16 @@
// Stub — full implementation in Task 5 (IndexedDB offline queue)
// This file exists so TypeScript can resolve the dynamic import in report-form.tsx.
export interface PendingReport {
zone_token: string
incident_type: string
description: string
injury_involved: boolean
asset_involved: boolean
medical_status?: string
created_at: string
}
export async function addPendingReport(_report: PendingReport): Promise<void> {
throw new Error('IndexedDB offline store not yet implemented — Task 5 will replace this stub')
}