mirror of
https://github.com/hanzoai/i18n.git
synced 2026-10-30 14:24:32 +00:00
A whole catalog is every key of every reader, now some 12,900 of them,
and a page draws a few hundred. I18nProvider takes `scope`, the part of
the site shown: `loadCatalog(code, scope)` is asked for it with the
language and for each new part once on a move, its answer joined to
what is shown.
`loadCatalog` is required and the bundled catalogs are their own entry,
`@hanzo/i18n/bundled`: referenced from the provider's default, they put
78 catalog chunks into every host's build whether it loaded them or not
(7.8 MB of hanzo.ai's export today, some 130 MB at the new size).
review.json's `{ "partial" }` with no `why` is an offered locale a run
has not finished: still offered, a key it lacks reads English.
tools/translate writes each batch as it answers, the locale file and one
provenance run per locale per run, so a stopped run keeps what came back:
on a local model a locale is hours of work.
Claude-Session: https://claude.ai/code/session_015XYKLjZtDTJEnMo4eK3AFH
88 lines
2.9 KiB
TypeScript
88 lines
2.9 KiB
TypeScript
import { useState } from 'react'
|
||
import { createRoot } from 'react-dom/client'
|
||
|
||
import { I18nProvider, LanguagePicker, useI18n } from '../../src/react.ts'
|
||
|
||
function App({ review }: { review: boolean }) {
|
||
const { t, dir, format } = useI18n()
|
||
return (
|
||
<main>
|
||
<LanguagePicker review={review} />
|
||
<h1>{t('Welcome')}</h1>
|
||
<p id="range">{t('Prefill at {range} tokens a second', { range: '1,150–1,400' })}</p>
|
||
<p id="terms">{t('Read the {terms}.', { terms: <a href="/terms">{t('Terms')}</a> })}</p>
|
||
<p id="count">{format.number(1234567)}</p>
|
||
<p id="total">{t('Total: {amount}', { amount: format.money(1234.5, 'USD') })}</p>
|
||
<p id="names">{t('Built with {names}', { names: format.list(['Chat', 'Dev', 'Kai']) })}</p>
|
||
<p id="when">{format.relative(Date.now() - 3 * 60000)}</p>
|
||
<aside data-side={dir === 'rtl' ? 'right' : 'left'}>{t('Settings')}</aside>
|
||
<code>hanzo status</code>
|
||
<span id="plain" data-i18n-mirror>→</span>
|
||
<section dir="rtl">
|
||
<code id="inner">hanzo status</code>
|
||
<span id="mirrored" data-i18n-mirror>→</span>
|
||
</section>
|
||
</main>
|
||
)
|
||
}
|
||
|
||
const params = new URL(location.href).searchParams
|
||
const user = params.get('user')
|
||
|
||
/** Arabic in two parts, as a host that serves a catalog by section hands it over. */
|
||
const PARTS: Record<string, Record<string, string>> = {
|
||
home: { Welcome: 'مرحبًا' },
|
||
settings: { Settings: 'الإعدادات' },
|
||
}
|
||
const asked: string[] = []
|
||
;(window as unknown as { asked: string[] }).asked = asked
|
||
|
||
function Scoped() {
|
||
const [scope, setScope] = useState('home')
|
||
return (
|
||
<I18nProvider
|
||
userId="scoped"
|
||
scope={scope}
|
||
loadCatalog={async (code, part) => {
|
||
asked.push(`${code}:${part}`)
|
||
return code === 'ar' ? (PARTS[part ?? ''] ?? {}) : {}
|
||
}}
|
||
>
|
||
<button id="move" onClick={() => setScope(scope === 'home' ? 'settings' : 'home')}>
|
||
move
|
||
</button>
|
||
<App review={false} />
|
||
</I18nProvider>
|
||
)
|
||
}
|
||
|
||
createRoot(document.getElementById('root')!).render(
|
||
params.has('scoped') ? (
|
||
<Scoped />
|
||
) : (
|
||
<I18nProvider
|
||
userId={user === 'guest' ? null : user ?? 'alice'}
|
||
loadCatalog={async (code) => {
|
||
if (code === 'fr-FR') {
|
||
throw new Error('test network error')
|
||
}
|
||
if (code === 'ar' || code.startsWith('ar-')) {
|
||
return {
|
||
Welcome: 'مرحبًا',
|
||
Settings: 'الإعدادات',
|
||
Language: 'اللغة',
|
||
'Prefill at {range} tokens a second': 'ملء مسبق بسرعة {range} رمز في الثانية',
|
||
'Read the {terms}.': 'اقرأ {terms}.',
|
||
Terms: 'الشروط',
|
||
'Total: {amount}': 'المجموع: {amount}',
|
||
'Built with {names}': 'مبني باستخدام {names}',
|
||
}
|
||
}
|
||
return { Welcome: `Welcome:${code}` }
|
||
}}
|
||
>
|
||
<App review={params.has('review')} />
|
||
</I18nProvider>
|
||
),
|
||
)
|