Files
zeekay de18ed418c @hanzo/i18n: a catalog in parts, loadCatalog required, offered locales may ship partial
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
2026-10-30 01:18:49 -07:00

88 lines
2.9 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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>
),
)