libp2r2p 0.9.0 → 0.10.1
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 +0 -54
- package/idb/index.js +1 -0
- package/idb-queue/index.js +1 -0
- package/index.js +0 -1
- package/nip04/index.js +0 -2
- package/nip46/services/client.js +1 -2
- package/nip46/services/server-session.js +1 -2
- package/nip98/index.js +1 -0
- package/package.json +5 -4
- package/private-channel/services/received-chunks.js +1 -0
- package/private-messenger/services/channel-state.js +1 -0
- package/relay/helpers/publish.js +1 -0
- package/relay/services/relay-pool.js +3 -3
- package/i18n/index.js +0 -237
package/README.md
CHANGED
|
@@ -326,60 +326,6 @@ Low-level relay sockets, subscriptions, message parsing, and serialization are
|
|
|
326
326
|
internal implementation details; use `RelayPool` or the `relayPool` singleton
|
|
327
327
|
from `libp2r2p/relay`.
|
|
328
328
|
|
|
329
|
-
## Internationalization
|
|
330
|
-
|
|
331
|
-
The dependency-free `libp2r2p/i18n` subpath exposes locale detection and a
|
|
332
|
-
small translator suitable for keeping each component's translations beside
|
|
333
|
-
that component. Translation keys are literal strings, not dotted paths.
|
|
334
|
-
|
|
335
|
-
```js
|
|
336
|
-
import { getT } from 'libp2r2p/i18n'
|
|
337
|
-
|
|
338
|
-
const t = getT({
|
|
339
|
-
'Allow {{size}}': {
|
|
340
|
-
en: 'Allow {{size}}',
|
|
341
|
-
'pt-BR': 'Permitir {{size}}'
|
|
342
|
-
},
|
|
343
|
-
'Delete {{count}} items': {
|
|
344
|
-
en: {
|
|
345
|
-
one: 'Delete {{count}} item',
|
|
346
|
-
other: 'Delete {{count}} items'
|
|
347
|
-
}
|
|
348
|
-
}
|
|
349
|
-
})
|
|
350
|
-
|
|
351
|
-
t('Allow {{size}}', { size: '10 MiB' })
|
|
352
|
-
```
|
|
353
|
-
|
|
354
|
-
`getCurrentDeviceLocale()` prefers the locale resolved by `Intl`, then browser
|
|
355
|
-
language hints, and preserves the full canonical BCP 47 locale. `getT()`
|
|
356
|
-
matches exact and compatible language variants, falls back to English and then
|
|
357
|
-
to the key itself, interpolates `{{name}}` values, and uses `Intl.PluralRules`
|
|
358
|
-
when a locale value supplies plural forms. Missing interpolation values remain
|
|
359
|
-
visible in the returned string.
|
|
360
|
-
|
|
361
|
-
Catalog validation is opt-in. `validateLocales()` can be used independently,
|
|
362
|
-
or `getT()` can validate once when creating the translator:
|
|
363
|
-
|
|
364
|
-
```js
|
|
365
|
-
import { getT, validateLocales } from 'libp2r2p/i18n'
|
|
366
|
-
|
|
367
|
-
const validation = {
|
|
368
|
-
requiredLocales: ['en', 'pt-BR'],
|
|
369
|
-
referenceLocale: 'en',
|
|
370
|
-
requireReferenceKey: true
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
validateLocales(locales, validation)
|
|
374
|
-
const t = getT(locales, { validation })
|
|
375
|
-
```
|
|
376
|
-
|
|
377
|
-
Validation checks catalog structure, required locales, plural `other` forms,
|
|
378
|
-
and placeholder parity across every translation. `requireReferenceKey` also
|
|
379
|
-
requires the reference locale's string, or its plural `other` form, to equal
|
|
380
|
-
the literal translation key. Without `validation`, sparse catalogs continue
|
|
381
|
-
to use the normal locale fallback behavior.
|
|
382
|
-
|
|
383
329
|
## Binary encodings
|
|
384
330
|
|
|
385
331
|
Base16, Base36, Base62, Base64/Base64URL, and Base93 helpers are available
|
package/idb/index.js
CHANGED
package/idb-queue/index.js
CHANGED
|
@@ -34,6 +34,7 @@ The queue state record is absent while the queue is empty.
|
|
|
34
34
|
function deferred () {
|
|
35
35
|
let resolve
|
|
36
36
|
let reject
|
|
37
|
+
// eslint-disable-next-line promise/param-names
|
|
37
38
|
const promise = new Promise((nextResolve, nextReject) => {
|
|
38
39
|
resolve = nextResolve
|
|
39
40
|
reject = nextReject
|
package/index.js
CHANGED
|
@@ -12,7 +12,6 @@ export * as error from './error/index.js'
|
|
|
12
12
|
export * as event from './event/index.js'
|
|
13
13
|
export * as idb from './idb/index.js'
|
|
14
14
|
export * as idbQueue from './idb-queue/index.js'
|
|
15
|
-
export * as i18n from './i18n/index.js'
|
|
16
15
|
export * as key from './key/index.js'
|
|
17
16
|
export * as kind from './kind/index.js'
|
|
18
17
|
export * as network from './network/index.js'
|
package/nip04/index.js
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
import { cbc } from '@noble/ciphers/aes.js'
|
|
2
2
|
import { randomBytes } from '@noble/ciphers/utils.js'
|
|
3
|
-
|
|
4
|
-
import { base16ToBytes } from '../base16/index.js'
|
|
5
3
|
import { base64ToBytes, bytesToBase64 } from '../base64/index.js'
|
|
6
4
|
import { sharedXOnlySecret } from '../ecdh/index.js'
|
|
7
5
|
import { ValidationError } from '../error/index.js'
|
package/nip46/services/client.js
CHANGED
|
@@ -198,8 +198,7 @@ export class Nip46Client {
|
|
|
198
198
|
}
|
|
199
199
|
|
|
200
200
|
#openResponseContext (pointer) {
|
|
201
|
-
|
|
202
|
-
context = this.#transport.openContext({
|
|
201
|
+
const context = this.#transport.openContext({
|
|
203
202
|
filter: {
|
|
204
203
|
kinds: [NIP46_KIND],
|
|
205
204
|
authors: [pointer.remoteSignerPubkey],
|
|
@@ -112,8 +112,7 @@ export class Nip46ServerSession {
|
|
|
112
112
|
}
|
|
113
113
|
|
|
114
114
|
#openContext (relays) {
|
|
115
|
-
|
|
116
|
-
context = this.#transport.openContext({
|
|
115
|
+
const context = this.#transport.openContext({
|
|
117
116
|
filter: {
|
|
118
117
|
kinds: [NIP46_KIND],
|
|
119
118
|
'#p': [this.#transport.pubkey],
|
package/nip98/index.js
CHANGED
|
@@ -27,6 +27,7 @@ export async function getToken ({
|
|
|
27
27
|
payloadHash
|
|
28
28
|
}) {
|
|
29
29
|
if (typeof loginUrl !== 'string' || loginUrl.length === 0) throw new ValidationError('INVALID_LOGIN_URL')
|
|
30
|
+
// eslint-disable-next-line no-new
|
|
30
31
|
try { new URL(loginUrl) } catch (cause) { throw new ValidationError('INVALID_LOGIN_URL', { cause }) }
|
|
31
32
|
if (typeof httpMethod !== 'string' || httpMethod.trim().length === 0) throw new ValidationError('INVALID_HTTP_METHOD')
|
|
32
33
|
if (typeof signEvent !== 'function') throw new ValidationError('SIGN_EVENT_SHOULD_BE_A_FUNCTION')
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "libp2r2p",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.1",
|
|
4
4
|
"description": "Peer-to-relay-to-peer",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"p2r2p",
|
|
@@ -24,7 +24,6 @@
|
|
|
24
24
|
"event",
|
|
25
25
|
"idb",
|
|
26
26
|
"idb-queue",
|
|
27
|
-
"i18n",
|
|
28
27
|
"index.js",
|
|
29
28
|
"key",
|
|
30
29
|
"kind",
|
|
@@ -65,7 +64,6 @@
|
|
|
65
64
|
"./event": "./event/index.js",
|
|
66
65
|
"./idb": "./idb/index.js",
|
|
67
66
|
"./idb-queue": "./idb-queue/index.js",
|
|
68
|
-
"./i18n": "./i18n/index.js",
|
|
69
67
|
"./key": "./key/index.js",
|
|
70
68
|
"./kind": "./kind/index.js",
|
|
71
69
|
"./network": "./network/index.js",
|
|
@@ -94,6 +92,9 @@
|
|
|
94
92
|
"@scure/base": "2.0.0"
|
|
95
93
|
},
|
|
96
94
|
"devDependencies": {
|
|
97
|
-
"
|
|
95
|
+
"eslint": "^9.39.5",
|
|
96
|
+
"fake-indexeddb": "6.2.5",
|
|
97
|
+
"globals": "^17.8.0",
|
|
98
|
+
"neostandard": "^0.13.0"
|
|
98
99
|
}
|
|
99
100
|
}
|
|
@@ -25,6 +25,7 @@ clearing state owned by another instance of the same principal identity.
|
|
|
25
25
|
function deferred () {
|
|
26
26
|
let resolve
|
|
27
27
|
let reject
|
|
28
|
+
// eslint-disable-next-line promise/param-names
|
|
28
29
|
const promise = new Promise((nextResolve, nextReject) => {
|
|
29
30
|
resolve = nextResolve
|
|
30
31
|
reject = nextReject
|
package/relay/helpers/publish.js
CHANGED
|
@@ -40,6 +40,7 @@ export function createPublishSettlements (promises, timeout, { onSettled } = {})
|
|
|
40
40
|
let isFinished = false
|
|
41
41
|
let resolve
|
|
42
42
|
|
|
43
|
+
// eslint-disable-next-line promise/param-names
|
|
43
44
|
const promise = new Promise(nextResolve => { resolve = nextResolve })
|
|
44
45
|
const finish = () => {
|
|
45
46
|
if (isFinished) return
|
|
@@ -175,7 +175,7 @@ export class RelayPool {
|
|
|
175
175
|
|
|
176
176
|
// Disconnect from a relay
|
|
177
177
|
async disconnect (url) {
|
|
178
|
-
const normalizedUrl =
|
|
178
|
+
const normalizedUrl = normalizeRelayUrl(url)
|
|
179
179
|
if (this.#relays.has(normalizedUrl)) {
|
|
180
180
|
const relay = this.#relays.get(normalizedUrl)
|
|
181
181
|
if (relay.ws?.readyState < 2) await relay.close()?.catch(console.log)
|
|
@@ -414,6 +414,7 @@ export class RelayPool {
|
|
|
414
414
|
this.#getRelay(url).then(relay => {
|
|
415
415
|
if (isResolved || !pending.has(url)) return
|
|
416
416
|
let hasEvents = false
|
|
417
|
+
// eslint-disable-next-line prefer-const
|
|
417
418
|
let sub
|
|
418
419
|
|
|
419
420
|
// Actual EOSE and filter satisfaction share the same graceful close path.
|
|
@@ -658,14 +659,13 @@ export class RelayPool {
|
|
|
658
659
|
// the historical gap is yielded before newer socket events.
|
|
659
660
|
let liveBuffer = (gapSince !== null && gapSince > 0) ? [] : null
|
|
660
661
|
let liveEose = false
|
|
661
|
-
let liveSub
|
|
662
662
|
|
|
663
663
|
// Open the live sub first so the relay starts buffering incoming events
|
|
664
664
|
// before we scan its database for the reconnect gap fill.
|
|
665
665
|
// Forward until to the relay so it can enforce the boundary server-side.
|
|
666
666
|
const liveFilter = { ...baseFilter, since: now, limit: 0 }
|
|
667
667
|
if (filterUntil !== null) liveFilter.until = filterUntil
|
|
668
|
-
liveSub = relay.subscribe([liveFilter], {
|
|
668
|
+
const liveSub = relay.subscribe([liveFilter], {
|
|
669
669
|
onevent: (event) => {
|
|
670
670
|
// A limit:0 relay may still send retained events before EOSE. Do not
|
|
671
671
|
// expose them from a strictly-live stream.
|
package/i18n/index.js
DELETED
|
@@ -1,237 +0,0 @@
|
|
|
1
|
-
import { ValidationError } from '../error/index.js'
|
|
2
|
-
|
|
3
|
-
const DEFAULT_LOCALE = 'en'
|
|
4
|
-
const INTERPOLATION_RE = /{{\s*([A-Za-z0-9_.-]+)\s*}}/g
|
|
5
|
-
|
|
6
|
-
function assertLocales (locales) {
|
|
7
|
-
if (!locales || typeof locales !== 'object' || Array.isArray(locales)) {
|
|
8
|
-
throw new ValidationError('INVALID_LOCALES', { message: 'locales should be an object' })
|
|
9
|
-
}
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
function placeholderSignature (value) {
|
|
13
|
-
return [...String(value).matchAll(INTERPOLATION_RE)]
|
|
14
|
-
.map(match => match[1])
|
|
15
|
-
.sort()
|
|
16
|
-
.join(',')
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
function validateTranslationValue (key, locale, value, expectedPlaceholders) {
|
|
20
|
-
if (typeof value === 'string') {
|
|
21
|
-
if (placeholderSignature(value) !== expectedPlaceholders) {
|
|
22
|
-
throw new ValidationError('I18N_PLACEHOLDER_MISMATCH', { message: `placeholder mismatch for "${key}" (${locale})` })
|
|
23
|
-
}
|
|
24
|
-
return
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
if (!value || typeof value !== 'object' || Array.isArray(value) || typeof value.other !== 'string') {
|
|
28
|
-
throw new ValidationError('INVALID_TRANSLATION', { message: `translation for "${key}" (${locale}) should be a string or plural object with an other form` })
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
for (const form of Object.values(value)) {
|
|
32
|
-
if (typeof form !== 'string') {
|
|
33
|
-
throw new ValidationError('INVALID_PLURAL_TRANSLATION', { message: `plural forms for "${key}" (${locale}) should be strings` })
|
|
34
|
-
}
|
|
35
|
-
if (placeholderSignature(form) !== expectedPlaceholders) {
|
|
36
|
-
throw new ValidationError('I18N_PLACEHOLDER_MISMATCH', { message: `placeholder mismatch for "${key}" (${locale})` })
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export function validateLocales (locales, options = {}) {
|
|
42
|
-
assertLocales(locales)
|
|
43
|
-
if (!options || typeof options !== 'object' || Array.isArray(options)) {
|
|
44
|
-
throw new ValidationError('INVALID_I18N_VALIDATION_OPTIONS', { message: 'validation options should be an object' })
|
|
45
|
-
}
|
|
46
|
-
const {
|
|
47
|
-
requiredLocales = [],
|
|
48
|
-
referenceLocale = DEFAULT_LOCALE,
|
|
49
|
-
requireReferenceKey = false
|
|
50
|
-
} = options
|
|
51
|
-
if (!Array.isArray(requiredLocales) || requiredLocales.some(locale => typeof locale !== 'string' || !locale)) {
|
|
52
|
-
throw new ValidationError('INVALID_REQUIRED_LOCALES', { message: 'requiredLocales should be an array of non-empty strings' })
|
|
53
|
-
}
|
|
54
|
-
if (typeof referenceLocale !== 'string' || !referenceLocale) {
|
|
55
|
-
throw new ValidationError('INVALID_REFERENCE_LOCALE', { message: 'referenceLocale should be a non-empty string' })
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
for (const [key, translations] of Object.entries(locales)) {
|
|
59
|
-
if (!translations || typeof translations !== 'object' || Array.isArray(translations)) {
|
|
60
|
-
throw new ValidationError('INVALID_TRANSLATIONS', { message: `translations for "${key}" should be an object` })
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
for (const locale of requiredLocales) {
|
|
64
|
-
if (!Object.prototype.hasOwnProperty.call(translations, locale)) {
|
|
65
|
-
throw new ValidationError('MISSING_TRANSLATION', { message: `missing translation for "${key}" (${locale})` })
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
const expectedPlaceholders = placeholderSignature(key)
|
|
70
|
-
for (const [locale, value] of Object.entries(translations)) {
|
|
71
|
-
validateTranslationValue(key, locale, value, expectedPlaceholders)
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
if (requireReferenceKey) {
|
|
75
|
-
if (!Object.prototype.hasOwnProperty.call(translations, referenceLocale)) {
|
|
76
|
-
throw new ValidationError('MISSING_REFERENCE_TRANSLATION', { message: `missing reference translation for "${key}" (${referenceLocale})` })
|
|
77
|
-
}
|
|
78
|
-
const reference = translations[referenceLocale]
|
|
79
|
-
const referenceValue = typeof reference === 'string' ? reference : reference.other
|
|
80
|
-
if (referenceValue !== key) {
|
|
81
|
-
throw new ValidationError('REFERENCE_TRANSLATION_MISMATCH', { message: `reference translation should match key "${key}" (${referenceLocale})` })
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
return locales
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
function canonicalizeLocale (locale) {
|
|
90
|
-
if (typeof locale !== 'string' || !locale.trim()) return null
|
|
91
|
-
const value = locale.trim().replace(/_/g, '-')
|
|
92
|
-
try {
|
|
93
|
-
return Intl.getCanonicalLocales(value)[0] ?? null
|
|
94
|
-
} catch {
|
|
95
|
-
return null
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
function getIntlLocale () {
|
|
100
|
-
try {
|
|
101
|
-
return Intl.DateTimeFormat().resolvedOptions().locale
|
|
102
|
-
} catch {
|
|
103
|
-
return null
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
function getNavigator () {
|
|
108
|
-
try {
|
|
109
|
-
return globalThis.navigator
|
|
110
|
-
} catch {
|
|
111
|
-
return null
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
export function getCurrentDeviceLocale () {
|
|
116
|
-
const navigator = getNavigator()
|
|
117
|
-
const candidates = [
|
|
118
|
-
getIntlLocale(),
|
|
119
|
-
navigator?.language,
|
|
120
|
-
navigator?.languages?.[0],
|
|
121
|
-
DEFAULT_LOCALE
|
|
122
|
-
]
|
|
123
|
-
|
|
124
|
-
for (const candidate of candidates) {
|
|
125
|
-
const locale = canonicalizeLocale(candidate)
|
|
126
|
-
if (locale) return locale
|
|
127
|
-
}
|
|
128
|
-
return DEFAULT_LOCALE
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
function localeLanguage (locale) {
|
|
132
|
-
try {
|
|
133
|
-
return new Intl.Locale(locale).language
|
|
134
|
-
} catch {
|
|
135
|
-
return locale.split('-')[0].toLowerCase()
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
function preferredChineseLocale (locale) {
|
|
140
|
-
if (localeLanguage(locale) !== 'zh') return null
|
|
141
|
-
try {
|
|
142
|
-
const { script, region } = new Intl.Locale(locale)
|
|
143
|
-
if (script === 'Hant' || ['TW', 'HK', 'MO'].includes(region)) return 'zh-TW'
|
|
144
|
-
return 'zh-CN'
|
|
145
|
-
} catch {
|
|
146
|
-
return /(?:^|-)(?:hant|tw|hk|mo)(?:-|$)/i.test(locale) ? 'zh-TW' : 'zh-CN'
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
function localeCandidates (translations, requestedLocale, fallbackLocale) {
|
|
151
|
-
const keys = Object.keys(translations)
|
|
152
|
-
const canonicalKeys = new Map()
|
|
153
|
-
for (const key of keys) {
|
|
154
|
-
const canonical = canonicalizeLocale(key)
|
|
155
|
-
if (canonical && !canonicalKeys.has(canonical.toLowerCase())) {
|
|
156
|
-
canonicalKeys.set(canonical.toLowerCase(), { key, canonical })
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
const result = []
|
|
161
|
-
const seen = new Set()
|
|
162
|
-
const add = locale => {
|
|
163
|
-
const canonical = canonicalizeLocale(locale)
|
|
164
|
-
const match = canonical && canonicalKeys.get(canonical.toLowerCase())
|
|
165
|
-
if (match && !seen.has(match.key)) {
|
|
166
|
-
seen.add(match.key)
|
|
167
|
-
result.push(match)
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
const addCompatible = locale => {
|
|
171
|
-
const canonical = canonicalizeLocale(locale)
|
|
172
|
-
if (!canonical) return
|
|
173
|
-
add(canonical)
|
|
174
|
-
add(preferredChineseLocale(canonical))
|
|
175
|
-
const language = localeLanguage(canonical)
|
|
176
|
-
const match = [...canonicalKeys.values()].find(candidate => localeLanguage(candidate.canonical) === language)
|
|
177
|
-
if (match) add(match.canonical)
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
addCompatible(requestedLocale)
|
|
181
|
-
addCompatible(fallbackLocale)
|
|
182
|
-
addCompatible(DEFAULT_LOCALE)
|
|
183
|
-
return result
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
function selectPlural (forms, locale, values) {
|
|
187
|
-
if (!forms || typeof forms !== 'object' || Array.isArray(forms)) return null
|
|
188
|
-
const count = Number(values?.count)
|
|
189
|
-
if (!Number.isFinite(count)) return null
|
|
190
|
-
|
|
191
|
-
let category = 'other'
|
|
192
|
-
try {
|
|
193
|
-
category = new Intl.PluralRules(locale).select(count)
|
|
194
|
-
} catch {}
|
|
195
|
-
const value = forms[category] ?? forms.other
|
|
196
|
-
return typeof value === 'string' ? value : null
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
function selectTemplate (translations, requestedLocale, fallbackLocale, values) {
|
|
200
|
-
if (!translations || typeof translations !== 'object' || Array.isArray(translations)) return null
|
|
201
|
-
for (const { key, canonical } of localeCandidates(translations, requestedLocale, fallbackLocale)) {
|
|
202
|
-
const value = translations[key]
|
|
203
|
-
if (typeof value === 'string') return value
|
|
204
|
-
const plural = selectPlural(value, canonical, values)
|
|
205
|
-
if (plural !== null) return plural
|
|
206
|
-
}
|
|
207
|
-
return null
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
function interpolate (template, values) {
|
|
211
|
-
const source = String(template)
|
|
212
|
-
if (!values || typeof values !== 'object') return source
|
|
213
|
-
return source.replace(INTERPOLATION_RE, (token, name) => (
|
|
214
|
-
Object.prototype.hasOwnProperty.call(values, name)
|
|
215
|
-
? String(values[name])
|
|
216
|
-
: token
|
|
217
|
-
))
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
export function getT (locales, {
|
|
221
|
-
locale = getCurrentDeviceLocale(),
|
|
222
|
-
fallbackLocale = DEFAULT_LOCALE,
|
|
223
|
-
validation
|
|
224
|
-
} = {}) {
|
|
225
|
-
assertLocales(locales)
|
|
226
|
-
if (validation !== undefined) validateLocales(locales, validation)
|
|
227
|
-
|
|
228
|
-
const requestedLocale = canonicalizeLocale(locale) ?? DEFAULT_LOCALE
|
|
229
|
-
const canonicalFallback = canonicalizeLocale(fallbackLocale) ?? DEFAULT_LOCALE
|
|
230
|
-
|
|
231
|
-
return function t (key, values) {
|
|
232
|
-
if (typeof key !== 'string') throw new ValidationError('INVALID_TRANSLATION_KEY', { message: 'translation key should be a string' })
|
|
233
|
-
const translations = Object.prototype.hasOwnProperty.call(locales, key) ? locales[key] : null
|
|
234
|
-
const template = selectTemplate(translations, requestedLocale, canonicalFallback, values) ?? key
|
|
235
|
-
return interpolate(template, values)
|
|
236
|
-
}
|
|
237
|
-
}
|