mirror of
https://github.com/hanzoai/i18n.git
synced 2026-10-30 14:24:32 +00:00
translate asked for every missing key in one request. Its own documented
workload is 1377 keys — ~97 KB of prompt needing ~21k tokens back — so the reply
truncated, failed to parse, and nothing was written; the next run asked the
identical question and truncated identically, so no progress was possible at that
scale. It batches by BYTES now, because a key costs whatever its string costs,
with a key cap for namespaces of very short strings. A batch that fails costs its
own keys and they stay missing for the next run, which is the same state every
other refusal leaves them in; every batch failing is an outage and is raised.
accept('en-US;q=') dropped the tag. Number('') is 0, which is finite, so an empty
q scored zero and read as "not acceptable" — the exact opposite of the rule
written above it, discarding a language over a missing character.
sitemap refuses what a sitemap may not hold. Fourteen locales turn a 2,500-page
site into 35,000 entries, and a file past 50,000 URLs is rejected WHOLE — no
index at all, reported nowhere but Search Console. It also strips C0 controls,
which have no XML escape, so one byte from a slug cannot make the document
unparseable.
--call is quoted into its pattern. Unescaped, `--call '.*'` matched any call and
defeated the word boundary the pattern exists for.
validate refuses --locales en-US. English is what everything is checked AGAINST,
so that asked the gate to examine nothing and it answered "clean" and exited 0 —
a gate that cannot fail, wearing the output of one that passed.
native drops its NativeModules fallback. It could not run: the package is ESM,
require is not defined there, and the ReferenceError went into the catch, so the
branch meant to rescue an older runtime returned nothing on every runtime. It was
aimed below our own floor anyway — the declared peer is react-native >= 0.71 and
Hermes has shipped full ICU since exactly there. Its test asserted
`Array.isArray(x) || typeof x === 'object'`, which no array can fail; it now pins
what the caller needs and fails if locales() throws.
92 tests, every fix mutation-checked.
Co-authored-by: Hanzo Dev <[email protected]>
94 lines
4.4 KiB
TypeScript
94 lines
4.4 KiB
TypeScript
import assert from 'node:assert/strict'
|
|
import { test } from 'node:test'
|
|
|
|
import { merge, scan } from '../src/scan.ts'
|
|
|
|
const keys = (code: string) => scan(code, 'shared').map((k) => `${k.namespace}:${k.key}`)
|
|
|
|
test('a key is whatever t() was asked for', () => {
|
|
assert.deepEqual(keys(`t('swap.title')`), ['shared:swap.title'])
|
|
assert.deepEqual(keys(`t("swap.title")`), ['shared:swap.title'])
|
|
assert.deepEqual(keys(`const { t } = useT(); return t('a') + t('b')`), ['shared:a', 'shared:b'])
|
|
})
|
|
|
|
test('a key names its own namespace when it wants one', () => {
|
|
assert.deepEqual(keys(`t('exchange:swap.title')`), ['exchange:swap.title'])
|
|
// A key with two colons keeps the rest of itself.
|
|
assert.deepEqual(scan(`t('exchange:a:b')`, 'shared')[0].key, 'a:b')
|
|
})
|
|
|
|
test('English written beside the key is the English we keep', () => {
|
|
assert.equal(scan(`t('swap.title', 'Swap tokens')`, 'shared')[0].english, 'Swap tokens')
|
|
assert.equal(scan(`t('exchange:swap.go', 'Go')`, 'shared')[0].english, 'Go')
|
|
})
|
|
|
|
test('a key with no English stands in for itself', () => {
|
|
// Not blank: an empty value renders an empty screen, and a key at least says
|
|
// which string is missing. `cli` reports these so somebody writes them.
|
|
assert.equal(scan(`t('swap.title')`, 'shared')[0].english, 'swap.title')
|
|
})
|
|
|
|
test('Trans is a call by another name', () => {
|
|
assert.deepEqual(keys(`<Trans i18nKey="legal.terms">read the <a>terms</a></Trans>`), ['shared:legal.terms'])
|
|
assert.deepEqual(keys(`<Trans i18nKey='wallet:send.hint' />`), ['wallet:send.hint'])
|
|
})
|
|
|
|
test('functions that merely end in t are not translation', () => {
|
|
// The narrowness is the point. `split(` and `format(` end in `t(` and appear
|
|
// in every file; matching them files hundreds of keys named `,` and `%s`.
|
|
assert.deepEqual(keys(`s.split('x'); format('%s'); at('0'); print('hi')`), [])
|
|
// The ones that genuinely are, via a boundary: bare, member, method.
|
|
assert.deepEqual(keys(`i18n.t('a'); this.t('b'); t('c')`), ['shared:a', 'shared:b', 'shared:c'])
|
|
})
|
|
|
|
test('a product may have named its lookup something else', () => {
|
|
// useT() returns a function the product binds under whatever name it likes;
|
|
// hanzo/chat wrapped it as localize and has 2471 calls of it. The binding name
|
|
// is local, the keys are not.
|
|
assert.deepEqual(scan(`localize('com_ui_save')`, 'shared', ['localize']).map((k) => k.key), ['com_ui_save'])
|
|
assert.deepEqual(scan(`t('a') localize('b')`, 'shared', ['t', 'localize']).map((k) => k.key), ['a', 'b'])
|
|
// Still a boundary, so a function that merely ends in the name is not it.
|
|
assert.deepEqual(scan(`unlocalize('x')`, 'shared', ['localize']), [])
|
|
assert.deepEqual(scan(`localize('a')`, 'shared'), [])
|
|
})
|
|
|
|
test('a key built at runtime is not extracted', () => {
|
|
// Nothing static can know what `section` holds. Silently emitting the literal
|
|
// part would file a key no screen ever asks for.
|
|
assert.deepEqual(keys('t(`nav.${section}`)'), [])
|
|
assert.deepEqual(keys(`t(key)`), [])
|
|
})
|
|
|
|
test('merge adds what is missing and edits nothing', () => {
|
|
const before = { 'swap.title': 'Trade tokens' }
|
|
const after = merge(before, scan(`t('swap.title', 'Swap') t('swap.go', 'Go')`, 'shared'), 'shared')
|
|
// The reviewed English wins over whatever a developer typed to see it render.
|
|
assert.deepEqual(after, { 'swap.go': 'Go', 'swap.title': 'Trade tokens' })
|
|
})
|
|
|
|
test('merge is per namespace', () => {
|
|
const found = scan(`t('exchange:a', 'A') t('wallet:b', 'B')`, 'shared')
|
|
assert.deepEqual(merge({}, found, 'exchange'), { a: 'A' })
|
|
assert.deepEqual(merge({}, found, 'wallet'), { b: 'B' })
|
|
})
|
|
|
|
test('merge run twice is merge run once', () => {
|
|
const found = scan(`t('a', 'A') t('b', 'B')`, 'shared')
|
|
const once = merge({}, found, 'shared')
|
|
assert.deepEqual(merge(once, found, 'shared'), once)
|
|
assert.equal(JSON.stringify(merge(once, found, 'shared')), JSON.stringify(once))
|
|
})
|
|
|
|
|
|
test('a --call name is matched as a name, not as a pattern', () => {
|
|
// These arrive from a command line. Unescaped, `.*` matches any call at all —
|
|
// defeating the word boundary the pattern exists for — and `a(` is an
|
|
// unterminated group that crashes before scanning anything.
|
|
assert.deepEqual(scan(`notAcall('x.y', 'X')`, 'app', ['.*']), [])
|
|
assert.doesNotThrow(() => scan(`t('a.b', 'A')`, 'app', ['a(']))
|
|
// The ordinary case still works, including a product's own wrapper name.
|
|
assert.deepEqual(scan(`localize('a.b', 'A')`, 'app', ['localize']), [
|
|
{ namespace: 'app', key: 'a.b', english: 'A' },
|
|
])
|
|
})
|