hanzio 2.2.0 → 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +133 -7
- package/dist/api-wrapper/connection-auth.d.ts +61 -0
- package/dist/cli/fields.d.ts +30 -0
- package/dist/cli/index.d.ts +122 -0
- package/dist/format/index.d.ts +91 -0
- package/dist/format/phone.d.ts +19 -0
- package/dist/http/index.d.ts +14 -0
- package/dist/i18n/catalog.d.ts +47 -0
- package/dist/i18n/check.d.ts +45 -0
- package/dist/i18n/cli.d.ts +2 -0
- package/dist/i18n/content-rules.d.ts +55 -0
- package/dist/i18n/i18next.d.ts +39 -0
- package/dist/i18n/index.d.ts +16 -1
- package/dist/i18n/locales.d.ts +62 -0
- package/dist/i18n/vite.d.ts +16 -0
- package/dist/i18n/vue.d.ts +93 -0
- package/dist/{index-kj1phgrx.js → index-29mmh343.js} +24 -1
- package/dist/{index-zbj9s3s3.js → index-2kz392vw.js} +1 -1
- package/dist/index-5n2e326q.js +409 -0
- package/dist/{index-hy7kbb99.js → index-5tzqfmnj.js} +1 -1
- package/dist/{index-0zvtcbzy.js → index-6f2w90pp.js} +69 -1
- package/dist/index-6ragggbs.js +473 -0
- package/dist/index-mq2qgc20.js +116 -0
- package/dist/index-najbyzpr.js +99 -0
- package/dist/index-ww09bm49.js +250 -0
- package/dist/iso/countries.d.ts +262 -0
- package/dist/iso/currencies.d.ts +165 -0
- package/dist/iso/index.d.ts +4 -0
- package/dist/iso/languages.d.ts +195 -0
- package/dist/iso/phone-language.d.ts +5 -0
- package/dist/promise/index.d.ts +31 -0
- package/dist/redis/client.d.ts +49 -0
- package/dist/redis/concurrency-limit.d.ts +63 -0
- package/dist/redis/index.d.ts +3 -0
- package/dist/redis/lua-script.d.ts +13 -0
- package/dist/secrets/infisical-identity-file.d.ts +12 -0
- package/dist/src/api-wrapper/connection-auth.js +82 -0
- package/dist/src/api-wrapper/index.js +1 -1
- package/dist/src/cli/index.js +10 -0
- package/dist/src/format/index.js +79 -2
- package/dist/src/format/phone.js +21 -0
- package/dist/src/http/index.js +69 -0
- package/dist/src/i18n/check.js +15 -0
- package/dist/src/i18n/cli.js +71 -0
- package/dist/src/i18n/i18next.js +48 -0
- package/dist/src/i18n/index.js +16 -34
- package/dist/src/i18n/vite.js +25 -0
- package/dist/src/i18n/vue.js +51 -0
- package/dist/src/index.js +22 -6
- package/dist/src/iso/index.js +655 -0
- package/dist/src/redis/index.js +577 -0
- package/dist/src/secrets/index.js +78 -35
- package/dist/src/sitemap/index.js +1 -1
- package/dist/src/storage/index.js +8 -117
- package/dist/src/testing/index.js +348 -0
- package/dist/src/zod/index.js +20 -253
- package/dist/string/index.d.ts +21 -0
- package/dist/testing/connection-auth-contract.d.ts +43 -0
- package/dist/testing/helpers.d.ts +26 -0
- package/dist/testing/index.d.ts +5 -0
- package/dist/testing/module-mocker.d.ts +30 -0
- package/dist/testing/prisma.d.ts +30 -0
- package/dist/testing/window.d.ts +18 -0
- package/dist/typedswitch/index.d.ts +55 -34
- package/dist/zod/index.d.ts +10 -0
- package/dist/zod/introspect.d.ts +2 -1
- package/package.json +97 -2
- package/src/api-wrapper/README.md +57 -2
- package/src/api-wrapper/SKILL.md +256 -0
- package/src/api-wrapper/connection-auth.ts +142 -0
- package/src/cli/README.md +220 -0
- package/src/cli/fields.ts +213 -0
- package/src/cli/index.ts +532 -0
- package/src/format/README.md +82 -2
- package/src/format/index.ts +148 -0
- package/src/format/phone.ts +43 -0
- package/src/http/README.md +25 -0
- package/src/http/index.ts +94 -0
- package/src/i18n/README.md +206 -3
- package/src/i18n/catalog.ts +139 -0
- package/src/i18n/check.ts +376 -0
- package/src/i18n/cli.ts +90 -0
- package/src/i18n/content-rules.ts +179 -0
- package/src/i18n/i18next.ts +89 -0
- package/src/i18n/index.ts +34 -0
- package/src/i18n/locales.ts +135 -0
- package/src/i18n/vite.ts +40 -0
- package/src/i18n/vue.ts +82 -0
- package/src/index.ts +1 -1
- package/src/iso/README.md +44 -0
- package/src/iso/countries.ts +272 -0
- package/src/iso/currencies.ts +172 -0
- package/src/iso/index.ts +8 -0
- package/src/iso/languages.ts +208 -0
- package/src/iso/phone-language.ts +46 -0
- package/src/promise/index.ts +100 -0
- package/src/redis/README.md +112 -0
- package/src/redis/client.ts +239 -0
- package/src/redis/concurrency-limit.ts +591 -0
- package/src/redis/index.ts +15 -0
- package/src/redis/lua-script.ts +43 -0
- package/src/secrets/README.md +5 -1
- package/src/secrets/infisical-identity-file.ts +69 -0
- package/src/secrets/infisical.ts +18 -3
- package/src/string/index.ts +59 -0
- package/src/testing/README.md +119 -0
- package/src/testing/connection-auth-contract.ts +303 -0
- package/src/testing/helpers.ts +41 -0
- package/src/testing/index.ts +12 -0
- package/src/testing/module-mocker.ts +50 -0
- package/src/testing/prisma.ts +78 -0
- package/src/testing/window.ts +119 -0
- package/src/typedswitch/README.md +55 -39
- package/src/typedswitch/index.ts +97 -83
- package/src/zod/index.ts +20 -0
- package/src/zod/introspect.ts +1 -1
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// i18next bindings for apps (React Native, React) that keep `{name}`
|
|
2
|
+
// placeholders. Types only from `i18next`: you pass your own instance, already
|
|
3
|
+
// `.use(initReactI18next)` where you need React.
|
|
4
|
+
|
|
5
|
+
import type { i18n as I18next, InitOptions, ResourceLanguage } from 'i18next'
|
|
6
|
+
import { type AsyncStorageLike, createAsyncStorageItem } from '../storage'
|
|
7
|
+
import type { LocaleRegistry } from '.'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Starts `i18n` in the persisted locale, then the first supported device
|
|
11
|
+
* locale, then `locales.defaultLocale` (also the fallback). Interpolation uses
|
|
12
|
+
* `{name}` (not `{{name}}`) and does not escape, matching the catalog check
|
|
13
|
+
* and `hanzio/i18n`.
|
|
14
|
+
*
|
|
15
|
+
* ```ts
|
|
16
|
+
* export const appI18n = initI18next(i18n.use(initReactI18next), {
|
|
17
|
+
* locales: uiLocales,
|
|
18
|
+
* resources: { en: { translation: en }, da: { translation: da } },
|
|
19
|
+
* deviceLocales: () => getLocales().flatMap((l) => [l.languageTag, l.languageCode]),
|
|
20
|
+
* storageKey: 'app.ui-locale',
|
|
21
|
+
* storage: AsyncStorage
|
|
22
|
+
* })
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
export function initI18next<L extends string>(
|
|
26
|
+
i18n: I18next,
|
|
27
|
+
options: Omit<InitOptions, 'lng' | 'resources'> & {
|
|
28
|
+
locales: LocaleRegistry<L>
|
|
29
|
+
resources: Record<L, ResourceLanguage>
|
|
30
|
+
/** Device languages in preference order, e.g. from `expo-localization`. */
|
|
31
|
+
deviceLocales?: () => readonly (string | null | undefined)[]
|
|
32
|
+
/** Remembers the chosen locale (a raw string). Needs `storage`. */
|
|
33
|
+
storageKey?: string
|
|
34
|
+
storage?: AsyncStorageLike
|
|
35
|
+
}
|
|
36
|
+
) {
|
|
37
|
+
const { locales, deviceLocales, storageKey, storage, ...initOptions } =
|
|
38
|
+
options
|
|
39
|
+
const persisted =
|
|
40
|
+
storageKey && storage
|
|
41
|
+
? createAsyncStorageItem<string | null>({
|
|
42
|
+
key: storageKey,
|
|
43
|
+
fallback: null,
|
|
44
|
+
storage,
|
|
45
|
+
serialize: (value) => value ?? '',
|
|
46
|
+
deserialize: (raw) => raw
|
|
47
|
+
})
|
|
48
|
+
: undefined
|
|
49
|
+
|
|
50
|
+
const ready = (async () => {
|
|
51
|
+
const locale = locales.resolve(
|
|
52
|
+
await persisted?.get(),
|
|
53
|
+
...(deviceLocales?.() ?? [])
|
|
54
|
+
)
|
|
55
|
+
await i18n.init({
|
|
56
|
+
fallbackLng: locales.defaultLocale,
|
|
57
|
+
returnNull: false,
|
|
58
|
+
...initOptions,
|
|
59
|
+
interpolation: {
|
|
60
|
+
escapeValue: false,
|
|
61
|
+
prefix: '{',
|
|
62
|
+
suffix: '}',
|
|
63
|
+
...initOptions.interpolation
|
|
64
|
+
},
|
|
65
|
+
lng: locale
|
|
66
|
+
})
|
|
67
|
+
})()
|
|
68
|
+
|
|
69
|
+
async function setLocale(locale: L) {
|
|
70
|
+
if (i18n.language !== locale) await i18n.changeLanguage(locale)
|
|
71
|
+
await persisted?.set(locale)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
/** Resolves once `i18n.init` finishes. Await it before the first render. */
|
|
76
|
+
ready,
|
|
77
|
+
setLocale,
|
|
78
|
+
/** The active locale, or the default before `ready`. */
|
|
79
|
+
locale: () => locales.match(i18n.language) ?? locales.defaultLocale,
|
|
80
|
+
/**
|
|
81
|
+
* Follows a server-side preference (the signed-in user's locale). An
|
|
82
|
+
* unsupported or missing tag changes nothing.
|
|
83
|
+
*/
|
|
84
|
+
async syncLocale(tag: string | null | undefined) {
|
|
85
|
+
const locale = locales.match(tag)
|
|
86
|
+
if (locale) await setLocale(locale)
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
package/src/i18n/index.ts
CHANGED
|
@@ -123,3 +123,37 @@ export function createTranslator<const T extends Messages>(messages: T) {
|
|
|
123
123
|
...(args as [Record<string, PlaceholderValue>?])
|
|
124
124
|
)
|
|
125
125
|
}
|
|
126
|
+
|
|
127
|
+
/** Generated key → placeholder-name map: `{ 'greeting': 'name', 'common.save': never }`. */
|
|
128
|
+
export type TranslationParamsMap = Record<string, string>
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Strict `t` over a generated key → placeholder-name map (see `hanzio-i18n
|
|
132
|
+
* generate`). Keys without placeholders take no params; keys with placeholders
|
|
133
|
+
* require every named value.
|
|
134
|
+
*/
|
|
135
|
+
export type TypedTranslate<Params extends TranslationParamsMap> = <
|
|
136
|
+
Key extends keyof Params & string
|
|
137
|
+
>(
|
|
138
|
+
...args: [Params[Key]] extends [never]
|
|
139
|
+
? [key: Key]
|
|
140
|
+
: [key: Key, params: Record<Params[Key], PlaceholderValue>]
|
|
141
|
+
) => string
|
|
142
|
+
|
|
143
|
+
/** Explicit escape hatch for data-driven keys validated outside TypeScript. */
|
|
144
|
+
export type DynamicTranslate = (
|
|
145
|
+
key: string,
|
|
146
|
+
params?: Record<string, PlaceholderValue>
|
|
147
|
+
) => string
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Narrows a framework's string-wide `t` (vue-i18n, i18next) to the generated
|
|
151
|
+
* map. Types only; returns `t` unchanged.
|
|
152
|
+
*/
|
|
153
|
+
export function typedTranslate<Params extends TranslationParamsMap>(
|
|
154
|
+
t: (...args: never[]) => unknown
|
|
155
|
+
): TypedTranslate<Params> {
|
|
156
|
+
return t as unknown as TypedTranslate<Params>
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export * from './locales'
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
export type LocaleRegistry<L extends string> = {
|
|
2
|
+
/** Locale ids in declaration order. They name the catalog files. */
|
|
3
|
+
readonly locales: readonly L[]
|
|
4
|
+
readonly defaultLocale: L
|
|
5
|
+
tag(locale: L): string
|
|
6
|
+
name(locale: L): string
|
|
7
|
+
/**
|
|
8
|
+
* A supported locale for a BCP-47-ish tag (`'da'`, `'da-DK'`, `'en_US'`), or
|
|
9
|
+
* `null`. Matches the id, then the tag, then the language subtag, then the
|
|
10
|
+
* aliases.
|
|
11
|
+
*/
|
|
12
|
+
match(tag: string | null | undefined): L | null
|
|
13
|
+
/** The first candidate that matches, else `defaultLocale`. */
|
|
14
|
+
resolve(...candidates: readonly (string | null | undefined)[]): L
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** The locale union of a registry: `LocaleOf<typeof locales>`. */
|
|
18
|
+
export type LocaleOf<R> = R extends LocaleRegistry<infer L> ? L : never
|
|
19
|
+
|
|
20
|
+
const normalizeTag = (tag: string) =>
|
|
21
|
+
tag.trim().replaceAll('_', '-').toLowerCase()
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* The app's shipped UI locales, shared by every runtime and the catalog check.
|
|
25
|
+
*
|
|
26
|
+
* ```ts
|
|
27
|
+
* export const uiLocales = defineLocales({
|
|
28
|
+
* locales: {
|
|
29
|
+
* en: { tag: 'en-US', name: 'English' },
|
|
30
|
+
* nb: { tag: 'nb-NO', name: 'Norsk bokmål' }
|
|
31
|
+
* },
|
|
32
|
+
* defaultLocale: 'en',
|
|
33
|
+
* aliases: { no: 'nb' }
|
|
34
|
+
* })
|
|
35
|
+
* uiLocales.match('no-NO') // 'nb'
|
|
36
|
+
* ```
|
|
37
|
+
*/
|
|
38
|
+
export function defineLocales<
|
|
39
|
+
const T extends Record<
|
|
40
|
+
string,
|
|
41
|
+
{
|
|
42
|
+
/** BCP-47 tag for `Intl` and `<html lang>`, e.g. `'da-DK'`. */
|
|
43
|
+
tag: string
|
|
44
|
+
/** The language naming itself, for locale pickers: `'Dansk'`. */
|
|
45
|
+
name: string
|
|
46
|
+
}
|
|
47
|
+
>
|
|
48
|
+
>(options: {
|
|
49
|
+
locales: T
|
|
50
|
+
defaultLocale: keyof T & string
|
|
51
|
+
/** Extra tags or language subtags that mean a locale: `{ no: 'nb' }`. */
|
|
52
|
+
aliases?: Record<string, keyof T & string>
|
|
53
|
+
}): LocaleRegistry<keyof T & string> {
|
|
54
|
+
type L = keyof T & string
|
|
55
|
+
const { locales: info, defaultLocale, aliases = {} } = options
|
|
56
|
+
const locales = Object.keys(info) as L[]
|
|
57
|
+
const lookup = new Map<string, L>()
|
|
58
|
+
for (const locale of locales) lookup.set(normalizeTag(locale), locale)
|
|
59
|
+
for (const locale of locales) {
|
|
60
|
+
const tag = normalizeTag(info[locale]!.tag)
|
|
61
|
+
if (!lookup.has(tag)) lookup.set(tag, locale)
|
|
62
|
+
}
|
|
63
|
+
const aliasLookup = new Map<string, L>()
|
|
64
|
+
for (const [alias, locale] of Object.entries(aliases)) {
|
|
65
|
+
if (!Object.hasOwn(info, locale)) {
|
|
66
|
+
throw new Error(`Alias "${alias}" points at unknown locale "${locale}"`)
|
|
67
|
+
}
|
|
68
|
+
aliasLookup.set(normalizeTag(alias), locale)
|
|
69
|
+
}
|
|
70
|
+
if (!Object.hasOwn(info, defaultLocale)) {
|
|
71
|
+
throw new Error(`Default locale "${defaultLocale}" is not in locales`)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const match = (tag: string | null | undefined): L | null => {
|
|
75
|
+
if (!tag) return null
|
|
76
|
+
const full = normalizeTag(tag)
|
|
77
|
+
const language = full.split('-')[0] ?? ''
|
|
78
|
+
return (
|
|
79
|
+
lookup.get(full) ??
|
|
80
|
+
aliasLookup.get(full) ??
|
|
81
|
+
lookup.get(language) ??
|
|
82
|
+
aliasLookup.get(language) ??
|
|
83
|
+
null
|
|
84
|
+
)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
locales,
|
|
89
|
+
defaultLocale,
|
|
90
|
+
tag: (locale) => info[locale]!.tag,
|
|
91
|
+
name: (locale) => info[locale]!.name,
|
|
92
|
+
match,
|
|
93
|
+
resolve: (...candidates) => {
|
|
94
|
+
for (const candidate of candidates) {
|
|
95
|
+
const locale = match(candidate)
|
|
96
|
+
if (locale) return locale
|
|
97
|
+
}
|
|
98
|
+
return defaultLocale
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* A locale picker that switches immediately and rolls back when `persist`
|
|
105
|
+
* (for example saving the user's language on the server) rejects.
|
|
106
|
+
*
|
|
107
|
+
* ```ts
|
|
108
|
+
* const selectLocale = createLocaleSelector({
|
|
109
|
+
* getLocale: () => locale.value,
|
|
110
|
+
* setLocale,
|
|
111
|
+
* persist: (locale) => api.user.updateLocale({ locale }),
|
|
112
|
+
* onError: () => toast.error(t('settings.language.error'))
|
|
113
|
+
* })
|
|
114
|
+
* ```
|
|
115
|
+
*/
|
|
116
|
+
export function createLocaleSelector<L extends string>(options: {
|
|
117
|
+
getLocale: () => L
|
|
118
|
+
setLocale: (locale: L) => unknown
|
|
119
|
+
persist?: (locale: L) => Promise<unknown>
|
|
120
|
+
onError?: (error: unknown) => void
|
|
121
|
+
}) {
|
|
122
|
+
const { getLocale, setLocale, persist, onError } = options
|
|
123
|
+
return async (next: L) => {
|
|
124
|
+
const previous = getLocale()
|
|
125
|
+
if (previous === next) return
|
|
126
|
+
await setLocale(next)
|
|
127
|
+
if (!persist) return
|
|
128
|
+
try {
|
|
129
|
+
await persist(next)
|
|
130
|
+
} catch (error) {
|
|
131
|
+
await setLocale(previous)
|
|
132
|
+
onError?.(error)
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
package/src/i18n/vite.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { Plugin } from 'vite'
|
|
2
|
+
|
|
3
|
+
const INTLIFY_VIRTUAL_PREFIX = 'virtual:intlify-i18n-'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Dev-server plugin: reload the page with fresh catalogs when a
|
|
7
|
+
* `locales/*.json` file changes.
|
|
8
|
+
*
|
|
9
|
+
* `@intlify/unplugin-vue-i18n` serves each catalog as a `virtual:intlify-i18n-N`
|
|
10
|
+
* module that the module graph cannot tie back to its JSON file, and its own
|
|
11
|
+
* hot update only invalidates its bundle module, so an edited key kept serving
|
|
12
|
+
* the old catalog until the dev server restarted. This invalidates every
|
|
13
|
+
* catalog module and reloads: `createI18n` reads messages once at startup, so a
|
|
14
|
+
* hot swap would not reach the running instance anyway.
|
|
15
|
+
*/
|
|
16
|
+
export function localeCatalogReloadPlugin(
|
|
17
|
+
options: {
|
|
18
|
+
/** Which changed files are catalogs. Default: `.json` in a `locales` folder. */
|
|
19
|
+
match?: RegExp
|
|
20
|
+
} = {}
|
|
21
|
+
): Plugin {
|
|
22
|
+
const match = options.match ?? /[/\\]locales[/\\][^/\\]+\.json$/
|
|
23
|
+
return {
|
|
24
|
+
name: 'hanzio-locale-catalog-reload',
|
|
25
|
+
apply: 'serve',
|
|
26
|
+
hotUpdate({ file }) {
|
|
27
|
+
if (this.environment.name !== 'client') return
|
|
28
|
+
if (!match.test(file)) return
|
|
29
|
+
|
|
30
|
+
const graph = this.environment.moduleGraph
|
|
31
|
+
for (const [id, module] of graph.idToModuleMap) {
|
|
32
|
+
if (id.startsWith(INTLIFY_VIRTUAL_PREFIX))
|
|
33
|
+
graph.invalidateModule(module)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
this.environment.hot.send({ type: 'full-reload' })
|
|
37
|
+
return []
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
package/src/i18n/vue.ts
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
// vue-i18n bindings: a locale-registry-aware `createI18n` with a typed
|
|
2
|
+
// composer. Needs `vue-i18n` (and so `vue`).
|
|
3
|
+
|
|
4
|
+
import type { WritableComputedRef } from 'vue'
|
|
5
|
+
import { createI18n, type I18nOptions, useI18n } from 'vue-i18n'
|
|
6
|
+
import { createStorageItem, type SyncStorageLike } from '../storage'
|
|
7
|
+
import {
|
|
8
|
+
type LocaleRegistry,
|
|
9
|
+
type Messages,
|
|
10
|
+
type TranslationParamsMap,
|
|
11
|
+
typedTranslate
|
|
12
|
+
} from '.'
|
|
13
|
+
|
|
14
|
+
type BrowserGlobals = {
|
|
15
|
+
navigator?: { language?: string }
|
|
16
|
+
document?: { documentElement: { lang: string } }
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const browser = globalThis as BrowserGlobals
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* `createI18n` in composition mode, starting in the persisted locale, then the
|
|
23
|
+
* browser language, then `locales.defaultLocale` (also the fallback locale).
|
|
24
|
+
* `setLocale` is the one write path: it switches vue-i18n, persists the choice
|
|
25
|
+
* and keeps `<html lang>` accurate. `useI18n<Params>()` is vue-i18n's composer
|
|
26
|
+
* with the locale and `t` narrowed to the registry and a generated key map;
|
|
27
|
+
* make it the one place the app calls vue-i18n's `useI18n`.
|
|
28
|
+
*/
|
|
29
|
+
export function createVueI18n<L extends string>(
|
|
30
|
+
options: Omit<I18nOptions, 'legacy' | 'locale' | 'messages'> & {
|
|
31
|
+
locales: LocaleRegistry<L>
|
|
32
|
+
/** One catalog per locale; a missing locale is a type error. */
|
|
33
|
+
messages: Record<L, Messages>
|
|
34
|
+
/**
|
|
35
|
+
* Remembers the chosen locale (a raw string) so screens before sign-in
|
|
36
|
+
* come up in the right language. Omit to persist nothing.
|
|
37
|
+
*/
|
|
38
|
+
storageKey?: string
|
|
39
|
+
/** Defaults to `localStorage`, falling back to memory. */
|
|
40
|
+
storage?: SyncStorageLike
|
|
41
|
+
}
|
|
42
|
+
) {
|
|
43
|
+
const { locales, storageKey, storage, ...i18nOptions } = options
|
|
44
|
+
const persisted = storageKey
|
|
45
|
+
? createStorageItem<string | null>({
|
|
46
|
+
key: storageKey,
|
|
47
|
+
fallback: null,
|
|
48
|
+
storage,
|
|
49
|
+
serialize: (value) => value ?? '',
|
|
50
|
+
deserialize: (raw) => raw
|
|
51
|
+
})
|
|
52
|
+
: undefined
|
|
53
|
+
const initial = locales.resolve(persisted?.get(), browser.navigator?.language)
|
|
54
|
+
const i18n = createI18n({
|
|
55
|
+
fallbackLocale: locales.defaultLocale,
|
|
56
|
+
...i18nOptions,
|
|
57
|
+
legacy: false,
|
|
58
|
+
locale: initial
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
const apply = (locale: L) => {
|
|
62
|
+
;(i18n.global.locale as WritableComputedRef<string>).value = locale
|
|
63
|
+
if (browser.document) browser.document.documentElement.lang = locale
|
|
64
|
+
}
|
|
65
|
+
apply(initial)
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
i18n,
|
|
69
|
+
setLocale(locale: L) {
|
|
70
|
+
apply(locale)
|
|
71
|
+
persisted?.set(locale)
|
|
72
|
+
},
|
|
73
|
+
useI18n<Params extends TranslationParamsMap>() {
|
|
74
|
+
const composer = useI18n()
|
|
75
|
+
return {
|
|
76
|
+
...composer,
|
|
77
|
+
locale: composer.locale as WritableComputedRef<L>,
|
|
78
|
+
t: typedTranslate<Params>(composer.t)
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Dependency-free helpers only. Modules that need zod, network access or a
|
|
2
2
|
// specific runtime live on subpath exports (hanzio/zod, hanzio/api-wrapper,
|
|
3
3
|
// hanzio/jwt, hanzio/sitemap, hanzio/p-queue, hanzio/cool-console-log,
|
|
4
|
-
// hanzio/secrets).
|
|
4
|
+
// hanzio/secrets, hanzio/redis, hanzio/http, hanzio/testing).
|
|
5
5
|
export * from './typedswitch'
|
|
6
6
|
export * from './array'
|
|
7
7
|
export * from './string'
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# hanzio/iso
|
|
2
|
+
|
|
3
|
+
ISO codes as literal unions, with English names. Static data with no
|
|
4
|
+
dependencies, so it works where `Intl.DisplayNames` does not (Hermes). For
|
|
5
|
+
names in other languages, use `Intl.DisplayNames`.
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
import {
|
|
9
|
+
countryCodes,
|
|
10
|
+
countryFlag,
|
|
11
|
+
countryNames,
|
|
12
|
+
currencyNames,
|
|
13
|
+
languageNames,
|
|
14
|
+
matchLanguageCode,
|
|
15
|
+
type CountryCode
|
|
16
|
+
} from 'hanzio/iso'
|
|
17
|
+
|
|
18
|
+
countryNames.DK // 'Denmark'
|
|
19
|
+
countryFlag('DK') // '🇩🇰'
|
|
20
|
+
currencyNames.EUR // 'Euro'
|
|
21
|
+
languageNames.nb // 'Norwegian Bokmål'
|
|
22
|
+
matchLanguageCode('da_DK') // 'da'
|
|
23
|
+
|
|
24
|
+
const options = countryCodes.map((code) => ({
|
|
25
|
+
value: code,
|
|
26
|
+
label: `${countryFlag(code)} ${countryNames[code]}`
|
|
27
|
+
}))
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
| Data | Codes | Type and guard |
|
|
31
|
+
| --- | --- | --- |
|
|
32
|
+
| `countryNames` | All 249 ISO 3166-1 alpha-2 codes | `CountryCode`, `isCountryCode` |
|
|
33
|
+
| `currencyNames` | ISO 4217 codes in circulation (no historic codes, funds or metals) | `CurrencyCode`, `isCurrencyCode` |
|
|
34
|
+
| `languageNames` | All 183 ISO 639-1 codes, lowercase | `LanguageCode`, `isLanguageCode` |
|
|
35
|
+
|
|
36
|
+
`countryCodes`, `currencyCodes` and `languageCodes` list the keys.
|
|
37
|
+
`matchLanguageCode(tag)` returns the language subtag of a BCP-47-ish tag when
|
|
38
|
+
it is an ISO 639-1 code; map languages your app treats as one (`'nn'` to
|
|
39
|
+
`'nb'`, say) yourself.
|
|
40
|
+
|
|
41
|
+
`languageFromE164PhoneNumber(number)` guesses a conversation language from a
|
|
42
|
+
phone number's calling code (`'+4520304050'` → `'da'`). It covers the Nordics,
|
|
43
|
+
the UK, Ireland, North America and the larger Western European countries, and
|
|
44
|
+
returns `null` for anything else.
|