Files
zeekay be27ff20a7 one @hanzo/i18n: 36 locales, React provider, Arabic RTL
The registry is the 36 locales hanzo.ai ships (zh-Hans/zh-Hant,
es-ES/es-419, ar right-to-left). resolve keeps its rules and adds the
region step, underscores and no -> nb. server and native become
subpaths; the react-i18next re-export is replaced by the provider,
picker and IAM account sync that @hanzo/rooms already uses. Locales in
catalogs/review.json load by code but are not offered.

slots also reads {name}, and faults holds the content checks the
translator and the gate share.
2026-10-22 00:29:05 -07:00

66 lines
2.7 KiB
TypeScript

import assert from 'node:assert/strict'
import { test } from 'node:test'
import { parity, slots } from '../src/slots.ts'
test('a slot is its name, not its punctuation', () => {
assert.deepEqual(slots('Swap {{amount}} {{token}}'), ['amount', 'token'])
assert.deepEqual(slots('no slots here'), [])
assert.deepEqual(slots(''), [])
})
test('spacing inside a slot is not meaning', () => {
// i18next trims, so both of these render the same value. Rejecting the spaced
// one would fail a translation that works.
assert.deepEqual(slots('{{ count }}'), slots('{{count}}'))
assert.ok(parity('{{count}} left', 'noch {{ count }}'))
})
test('a format spec is part of the slot', () => {
// `{{count, number}}` and `{{count}}` format differently, so a translation
// that drops the spec is not the same message.
assert.deepEqual(slots('{{count, number}}'), ['count, number'])
assert.ok(!parity('{{count, number}}', '{{count}}'))
})
test('word order may change, the slots may not', () => {
// German moves the verb; the translation is correct and must pass.
assert.ok(parity('{{amount}} of {{token}}', '{{token}} im Wert von {{amount}}'))
})
test('a repeated slot is counted, not deduplicated', () => {
// The multiset case: same names, wrong number of them. A set comparison calls
// this fine and ships a number where a ticker belongs.
assert.ok(!parity('{{amount}} of {{token}}', '{{amount}} of {{amount}}'))
assert.ok(!parity('{{a}} {{a}}', '{{a}}'))
assert.ok(parity('{{a}} {{a}} {{b}}', '{{b}} {{a}} {{a}}'))
})
test('a missing or invented slot fails', () => {
assert.ok(!parity('Swap {{amount}}', 'Tauschen'))
assert.ok(!parity('Swap', 'Tauschen {{amount}}'))
assert.ok(!parity('{{amount}}', '{{montant}}'))
})
test('a stray brace does not swallow the rest of the string', () => {
// Left greedy, `{{a}} }} {{b}}` reads as one enormous slot and two real ones
// silently become none.
assert.deepEqual(slots('{{a}} }} {{b}}'), ['a', 'b'])
assert.deepEqual(slots('{{}}'), [''])
})
test('an argument this package renders is a slot too, and never an i18next one', () => {
// `translate` fills `{name}`. The braces stay on so that `{{amount}}` → `{amount}`
// fails: same name, and under i18next a literal `{amount}` on the screen.
assert.deepEqual(slots('Hello {name}, {count} new'), ['{count}', '{name}'])
assert.ok(parity('Hello {name}', 'مرحبا {name}'))
assert.ok(!parity('Hello {name}', 'Bonjour'))
assert.ok(!parity('{{amount}}', '{amount}'))
assert.ok(!parity('{amount}', '{{amount}}'))
})
test('braces that are not an argument are text', () => {
assert.deepEqual(slots('{ "a": 1 }'), [])
assert.deepEqual(slots('{count, plural, one {# item}}'), [])
})