mirror of
https://github.com/hanzoai/i18n.git
synced 2026-10-30 14:24:32 +00:00
The extractor reads the children of <T> as keys. JSX text is normalized by the compiler before T sees it, so the key is SWC's rule applied to the source (tools/extract/src/jsx.ts): entities decoded, lines cut at every terminator, spaces and tabs trimmed where they touch a break unless an entity wrote them, the rest joined by one space. Each case is compiled with esbuild and held equal to the children T receives; the same cases and the entity edges were measured against Next 16.3's SWC. useI18n().format (and format(locale) from the root) is Intl at the locale: number, percent, money, date, relative and list, with Intl's options passed through, so en-US output is toLocaleString's byte for byte. Right to left every result is one isolate and a list isolates each item, so Latin names read right to left; isolate leaves a whole isolate alone, so a figure handed to t carries one pair of marks. The browser test reads Arabic and ar-EG digits, one isolate per figure, and the list's order, which fails without the item isolates. Claude-Session: https://claude.ai/code/session_01BLLMyBFSvAfCt7ZGqYuiEz
56 lines
2.0 KiB
TypeScript
56 lines
2.0 KiB
TypeScript
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')
|
||
|
||
createRoot(document.getElementById('root')!).render(
|
||
<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>,
|
||
)
|