libp2r2p 0.2.0 → 0.4.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 CHANGED
@@ -174,3 +174,70 @@ clearing is asynchronous too: `await messenger.clearChannel(channelPubkey)`.
174
174
  Use explicit subpath imports for bundle size. The package root re-exports the
175
175
  main messenger API for convenience, but applications that only need one piece
176
176
  should import that subpath directly.
177
+
178
+ ## Internationalization
179
+
180
+ The dependency-free `libp2r2p/i18n` subpath exposes locale detection and a
181
+ small translator suitable for keeping each component's translations beside
182
+ that component. Translation keys are literal strings, not dotted paths.
183
+
184
+ ```js
185
+ import { getT } from 'libp2r2p/i18n'
186
+
187
+ const t = getT({
188
+ 'Allow {{size}}': {
189
+ en: 'Allow {{size}}',
190
+ 'pt-BR': 'Permitir {{size}}'
191
+ },
192
+ 'Delete {{count}} items': {
193
+ en: {
194
+ one: 'Delete {{count}} item',
195
+ other: 'Delete {{count}} items'
196
+ }
197
+ }
198
+ })
199
+
200
+ t('Allow {{size}}', { size: '10 MiB' })
201
+ ```
202
+
203
+ `getCurrentDeviceLocale()` prefers the locale resolved by `Intl`, then browser
204
+ language hints, and preserves the full canonical BCP 47 locale. `getT()`
205
+ matches exact and compatible language variants, falls back to English and then
206
+ to the key itself, interpolates `{{name}}` values, and uses `Intl.PluralRules`
207
+ when a locale value supplies plural forms. Missing interpolation values remain
208
+ visible in the returned string.
209
+
210
+ Catalog validation is opt-in. `validateLocales()` can be used independently,
211
+ or `getT()` can validate once when creating the translator:
212
+
213
+ ```js
214
+ import { getT, validateLocales } from 'libp2r2p/i18n'
215
+
216
+ const validation = {
217
+ requiredLocales: ['en', 'pt-BR'],
218
+ referenceLocale: 'en',
219
+ requireReferenceKey: true
220
+ }
221
+
222
+ validateLocales(locales, validation)
223
+ const t = getT(locales, { validation })
224
+ ```
225
+
226
+ Validation checks catalog structure, required locales, plural `other` forms,
227
+ and placeholder parity across every translation. `requireReferenceKey` also
228
+ requires the reference locale's string, or its plural `other` form, to equal
229
+ the literal translation key. Without `validation`, sparse catalogs continue
230
+ to use the normal locale fallback behavior.
231
+
232
+ ## Binary encodings
233
+
234
+ Base16, Base36, Base62, Base64/Base64URL, and Base93 helpers are available
235
+ through their matching `libp2r2p/<encoding>` subpaths. Base36 exposes both a
236
+ binary-safe variable-width codec and the canonical 32-byte/50-character
237
+ NsiteBase36 representation from NIP-5A. Base62 uses the same case-sensitive
238
+ alphabet as app NIP-19 entities; its default byte mode preserves leading zero
239
+ bytes, while integer mode supports fixed-width identifiers.
240
+
241
+ In NIP-5A, "no padding" means that no separate padding character such as `=`
242
+ is used. Leading `0` digits are nevertheless required to make every Nsite
243
+ Base36 value exactly 50 characters long.
package/base16/index.js CHANGED
@@ -5,8 +5,13 @@ export function bytesToBase16 (bytes) {
5
5
  }
6
6
 
7
7
  export function base16ToBytes (base16) {
8
+ if (typeof base16 !== 'string') throw new TypeError('Base16 value should be a string')
9
+ if (base16.length % 2 !== 0) throw new Error('Invalid Base16 length')
10
+ if (!/^[0-9a-f]*$/i.test(base16)) throw new Error('Invalid Base16 character')
8
11
  const out = new Uint8Array(base16.length / 2)
9
- for (let i = 0; i < out.length; i++) out[i] = parseInt(base16.slice(i * 2, i * 2 + 2), 16)
12
+ for (let i = 0; i < out.length; i++) {
13
+ out[i] = Number.parseInt(base16.slice(i * 2, i * 2 + 2), 16)
14
+ }
10
15
  return out
11
16
  }
12
17
 
@@ -0,0 +1,114 @@
1
+ import { base16ToBytes, bytesToBase16 } from '../base16/index.js'
2
+
3
+ export const BASE36_ALPHABET = '0123456789abcdefghijklmnopqrstuvwxyz'
4
+
5
+ const BASE = BigInt(BASE36_ALPHABET.length)
6
+ const LEADER = BASE36_ALPHABET[0]
7
+ const CHAR_MAP = new Map(
8
+ [...BASE36_ALPHABET].map((character, index) => [character, BigInt(index)])
9
+ )
10
+ const NSITE_BYTE_LENGTH = 32
11
+ const NSITE_TEXT_LENGTH = 50
12
+ const MAX_NSITE_VALUE = (1n << 256n) - 1n
13
+
14
+ function bytesToInteger (bytes) {
15
+ let number = 0n
16
+ for (const byte of bytes) number = (number << 8n) + BigInt(byte)
17
+ return number
18
+ }
19
+
20
+ function integerToBase36 (number) {
21
+ let result = ''
22
+ while (number > 0n) {
23
+ result = BASE36_ALPHABET[Number(number % BASE)] + result
24
+ number /= BASE
25
+ }
26
+ return result || LEADER
27
+ }
28
+
29
+ function parseBase36Integer (value) {
30
+ let number = 0n
31
+ for (const character of value) {
32
+ const digit = CHAR_MAP.get(character)
33
+ if (digit === undefined) throw new Error('Invalid Base36 character: ' + character)
34
+ number = number * BASE + digit
35
+ }
36
+ return number
37
+ }
38
+
39
+ function integerToMinimalBytes (number) {
40
+ if (number === 0n) return new Uint8Array([0])
41
+ const bytes = []
42
+ while (number > 0n) {
43
+ bytes.unshift(Number(number & 0xffn))
44
+ number >>= 8n
45
+ }
46
+ return Uint8Array.from(bytes)
47
+ }
48
+
49
+ // A binary-safe Base36 codec. Every leading zero byte is represented by a
50
+ // leading zero character, making arbitrary byte arrays round-trip exactly.
51
+ export function bytesToBase36 (bytes) {
52
+ if (bytes.length === 0) return ''
53
+
54
+ const number = bytesToInteger(bytes)
55
+ let result = number === 0n ? '' : integerToBase36(number)
56
+ let leadingZeros = 0
57
+ while (leadingZeros < bytes.length && bytes[leadingZeros] === 0) leadingZeros++
58
+ result = LEADER.repeat(leadingZeros) + result
59
+ return result
60
+ }
61
+
62
+ export function base36ToBytes (value) {
63
+ if (typeof value !== 'string') throw new TypeError('Base36 value should be a string')
64
+ if (value.length === 0) return new Uint8Array()
65
+
66
+ let leadingZeros = 0
67
+ while (value[leadingZeros] === LEADER) leadingZeros++
68
+ const number = parseBase36Integer(value)
69
+ const suffix = number === 0n ? new Uint8Array() : integerToMinimalBytes(number)
70
+ const result = new Uint8Array(leadingZeros + suffix.length)
71
+ result.set(suffix, leadingZeros)
72
+ return result
73
+ }
74
+
75
+ export function base16ToBase36 (value) {
76
+ return bytesToBase36(base16ToBytes(value))
77
+ }
78
+
79
+ export function base36ToBase16 (value) {
80
+ return bytesToBase16(base36ToBytes(value))
81
+ }
82
+
83
+ // NIP-5A treats a raw 32-byte value as one unsigned integer and represents it
84
+ // as exactly 50 lowercase Base36 digits. Its leading zeros are numeric width,
85
+ // not zero-byte markers as they are in the binary-safe codec above.
86
+ export function bytesToNsiteBase36 (bytes) {
87
+ if (bytes.length !== NSITE_BYTE_LENGTH) {
88
+ throw new Error('Nsite Base36 input should be ' + NSITE_BYTE_LENGTH + ' bytes')
89
+ }
90
+ return integerToBase36(bytesToInteger(bytes)).padStart(NSITE_TEXT_LENGTH, LEADER)
91
+ }
92
+
93
+ export function nsiteBase36ToBytes (value) {
94
+ if (typeof value !== 'string') throw new TypeError('Nsite Base36 value should be a string')
95
+ if (value.length !== NSITE_TEXT_LENGTH) {
96
+ throw new Error('Nsite Base36 value should be ' + NSITE_TEXT_LENGTH + ' characters')
97
+ }
98
+ if (!/^[0-9a-z]+$/.test(value)) throw new Error('Invalid Nsite Base36 character')
99
+
100
+ const number = parseBase36Integer(value)
101
+ if (number > MAX_NSITE_VALUE) throw new Error('Nsite Base36 value exceeds 32 bytes')
102
+ const bytes = integerToMinimalBytes(number)
103
+ const result = new Uint8Array(NSITE_BYTE_LENGTH)
104
+ if (number !== 0n) result.set(bytes, NSITE_BYTE_LENGTH - bytes.length)
105
+ return result
106
+ }
107
+
108
+ export function base16ToNsiteBase36 (value) {
109
+ return bytesToNsiteBase36(base16ToBytes(value))
110
+ }
111
+
112
+ export function nsiteBase36ToBase16 (value) {
113
+ return bytesToBase16(nsiteBase36ToBytes(value))
114
+ }
@@ -0,0 +1,127 @@
1
+ import { base16ToBytes, bytesToBase16 } from '../base16/index.js'
2
+
3
+ export const BASE62_ALPHABET = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
4
+
5
+ const BASE = BigInt(BASE62_ALPHABET.length)
6
+ const LEADER = BASE62_ALPHABET[0]
7
+ const CHAR_MAP = new Map(
8
+ [...BASE62_ALPHABET].map((character, index) => [character, BigInt(index)])
9
+ )
10
+
11
+ function readOptions (options, allowedKeys) {
12
+ if (options === undefined) return {}
13
+ if (!options || typeof options !== 'object' || Array.isArray(options)) {
14
+ throw new TypeError('Base62 options should be an object')
15
+ }
16
+ for (const key of Object.keys(options)) {
17
+ if (!allowedKeys.includes(key)) throw new Error('Unknown Base62 option: ' + key)
18
+ }
19
+ return options
20
+ }
21
+
22
+ function readMode (mode = 'bytes') {
23
+ if (mode !== 'bytes' && mode !== 'integer') throw new Error('Invalid Base62 mode: ' + mode)
24
+ return mode
25
+ }
26
+
27
+ function readLength (value, name, defaultValue, minimum = 0) {
28
+ if (value === undefined) return defaultValue
29
+ if (!Number.isSafeInteger(value) || value < minimum) {
30
+ const qualifier = minimum === 0 ? 'non-negative' : 'positive'
31
+ throw new RangeError('Base62 ' + name + ' should be a ' + qualifier + ' safe integer')
32
+ }
33
+ return value
34
+ }
35
+
36
+ function bytesToInteger (bytes) {
37
+ let number = 0n
38
+ for (const byte of bytes) number = (number << 8n) + BigInt(byte)
39
+ return number
40
+ }
41
+
42
+ function integerToBase62 (number) {
43
+ let result = ''
44
+ while (number > 0n) {
45
+ result = BASE62_ALPHABET[Number(number % BASE)] + result
46
+ number /= BASE
47
+ }
48
+ return result || LEADER
49
+ }
50
+
51
+ function parseBase62Integer (value) {
52
+ let number = 0n
53
+ for (const character of value) {
54
+ const digit = CHAR_MAP.get(character)
55
+ if (digit === undefined) throw new Error('Invalid Base62 character: ' + character)
56
+ number = number * BASE + digit
57
+ }
58
+ return number
59
+ }
60
+
61
+ function integerToMinimalBytes (number) {
62
+ if (number === 0n) return new Uint8Array([0])
63
+ const bytes = []
64
+ while (number > 0n) {
65
+ bytes.unshift(Number(number & 0xffn))
66
+ number >>= 8n
67
+ }
68
+ return Uint8Array.from(bytes)
69
+ }
70
+
71
+ // Byte mode preserves every leading zero byte. Integer mode treats the input
72
+ // as an unsigned big-endian value and supports fixed-width textual output.
73
+ export function bytesToBase62 (bytes, options) {
74
+ const { mode: rawMode, minLength: rawMinLength } = readOptions(options, ['mode', 'minLength'])
75
+ const mode = readMode(rawMode)
76
+ if (mode === 'bytes' && rawMinLength !== undefined) {
77
+ throw new Error('Base62 minLength requires integer mode')
78
+ }
79
+ if (mode === 'integer') {
80
+ if (bytes.length === 0) throw new Error('Base62 integer input should not be empty')
81
+ const minLength = readLength(rawMinLength, 'minLength', 0)
82
+ return integerToBase62(bytesToInteger(bytes)).padStart(minLength, LEADER)
83
+ }
84
+ if (bytes.length === 0) return ''
85
+
86
+ const number = bytesToInteger(bytes)
87
+ let result = number === 0n ? '' : integerToBase62(number)
88
+ let leadingZeros = 0
89
+ while (leadingZeros < bytes.length && bytes[leadingZeros] === 0) leadingZeros++
90
+ result = LEADER.repeat(leadingZeros) + result
91
+ return result
92
+ }
93
+
94
+ export function base62ToBytes (value, options) {
95
+ if (typeof value !== 'string') throw new TypeError('Base62 value should be a string')
96
+ const { mode: rawMode, byteLength: rawByteLength } = readOptions(options, ['mode', 'byteLength'])
97
+ const mode = readMode(rawMode)
98
+ if (mode === 'bytes' && rawByteLength !== undefined) {
99
+ throw new Error('Base62 byteLength requires integer mode')
100
+ }
101
+ if (mode === 'integer') {
102
+ if (value.length === 0) throw new Error('Base62 integer value should not be empty')
103
+ const byteLength = readLength(rawByteLength, 'byteLength', undefined, 1)
104
+ const bytes = integerToMinimalBytes(parseBase62Integer(value))
105
+ if (byteLength === undefined) return bytes
106
+ if (bytes.length > byteLength) throw new Error('Base62 integer exceeds ' + byteLength + ' bytes')
107
+ const result = new Uint8Array(byteLength)
108
+ result.set(bytes, byteLength - bytes.length)
109
+ return result
110
+ }
111
+
112
+ let leadingZeros = 0
113
+ while (value[leadingZeros] === LEADER) leadingZeros++
114
+ const number = parseBase62Integer(value)
115
+ const suffix = number === 0n ? new Uint8Array() : integerToMinimalBytes(number)
116
+ const result = new Uint8Array(leadingZeros + suffix.length)
117
+ result.set(suffix, leadingZeros)
118
+ return result
119
+ }
120
+
121
+ export function base16ToBase62 (value, options) {
122
+ return bytesToBase62(base16ToBytes(value), options)
123
+ }
124
+
125
+ export function base62ToBase16 (value, options) {
126
+ return bytesToBase16(base62ToBytes(value, options))
127
+ }
package/base64/index.js CHANGED
@@ -3,6 +3,9 @@
3
3
  // base64 is useful for WebAuthn credential IDs and URL/query-string material.
4
4
 
5
5
  export function bytesToBase64 (bytes) {
6
+ if (typeof Buffer === 'function' && typeof Buffer.from === 'function') {
7
+ return Buffer.from(bytes).toString('base64')
8
+ }
6
9
  let s = ''
7
10
  for (let i = 0; i < bytes.length; i++) s += String.fromCharCode(bytes[i])
8
11
  return btoa(s)
package/i18n/index.js ADDED
@@ -0,0 +1,235 @@
1
+ const DEFAULT_LOCALE = 'en'
2
+ const INTERPOLATION_RE = /{{\s*([A-Za-z0-9_.-]+)\s*}}/g
3
+
4
+ function assertLocales (locales) {
5
+ if (!locales || typeof locales !== 'object' || Array.isArray(locales)) {
6
+ throw new TypeError('locales should be an object')
7
+ }
8
+ }
9
+
10
+ function placeholderSignature (value) {
11
+ return [...String(value).matchAll(INTERPOLATION_RE)]
12
+ .map(match => match[1])
13
+ .sort()
14
+ .join(',')
15
+ }
16
+
17
+ function validateTranslationValue (key, locale, value, expectedPlaceholders) {
18
+ if (typeof value === 'string') {
19
+ if (placeholderSignature(value) !== expectedPlaceholders) {
20
+ throw new Error(`placeholder mismatch for "${key}" (${locale})`)
21
+ }
22
+ return
23
+ }
24
+
25
+ if (!value || typeof value !== 'object' || Array.isArray(value) || typeof value.other !== 'string') {
26
+ throw new TypeError(`translation for "${key}" (${locale}) should be a string or plural object with an other form`)
27
+ }
28
+
29
+ for (const form of Object.values(value)) {
30
+ if (typeof form !== 'string') {
31
+ throw new TypeError(`plural forms for "${key}" (${locale}) should be strings`)
32
+ }
33
+ if (placeholderSignature(form) !== expectedPlaceholders) {
34
+ throw new Error(`placeholder mismatch for "${key}" (${locale})`)
35
+ }
36
+ }
37
+ }
38
+
39
+ export function validateLocales (locales, options = {}) {
40
+ assertLocales(locales)
41
+ if (!options || typeof options !== 'object' || Array.isArray(options)) {
42
+ throw new TypeError('validation options should be an object')
43
+ }
44
+ const {
45
+ requiredLocales = [],
46
+ referenceLocale = DEFAULT_LOCALE,
47
+ requireReferenceKey = false
48
+ } = options
49
+ if (!Array.isArray(requiredLocales) || requiredLocales.some(locale => typeof locale !== 'string' || !locale)) {
50
+ throw new TypeError('requiredLocales should be an array of non-empty strings')
51
+ }
52
+ if (typeof referenceLocale !== 'string' || !referenceLocale) {
53
+ throw new TypeError('referenceLocale should be a non-empty string')
54
+ }
55
+
56
+ for (const [key, translations] of Object.entries(locales)) {
57
+ if (!translations || typeof translations !== 'object' || Array.isArray(translations)) {
58
+ throw new TypeError(`translations for "${key}" should be an object`)
59
+ }
60
+
61
+ for (const locale of requiredLocales) {
62
+ if (!Object.prototype.hasOwnProperty.call(translations, locale)) {
63
+ throw new Error(`missing translation for "${key}" (${locale})`)
64
+ }
65
+ }
66
+
67
+ const expectedPlaceholders = placeholderSignature(key)
68
+ for (const [locale, value] of Object.entries(translations)) {
69
+ validateTranslationValue(key, locale, value, expectedPlaceholders)
70
+ }
71
+
72
+ if (requireReferenceKey) {
73
+ if (!Object.prototype.hasOwnProperty.call(translations, referenceLocale)) {
74
+ throw new Error(`missing reference translation for "${key}" (${referenceLocale})`)
75
+ }
76
+ const reference = translations[referenceLocale]
77
+ const referenceValue = typeof reference === 'string' ? reference : reference.other
78
+ if (referenceValue !== key) {
79
+ throw new Error(`reference translation should match key "${key}" (${referenceLocale})`)
80
+ }
81
+ }
82
+ }
83
+
84
+ return locales
85
+ }
86
+
87
+ function canonicalizeLocale (locale) {
88
+ if (typeof locale !== 'string' || !locale.trim()) return null
89
+ const value = locale.trim().replace(/_/g, '-')
90
+ try {
91
+ return Intl.getCanonicalLocales(value)[0] ?? null
92
+ } catch {
93
+ return null
94
+ }
95
+ }
96
+
97
+ function getIntlLocale () {
98
+ try {
99
+ return Intl.DateTimeFormat().resolvedOptions().locale
100
+ } catch {
101
+ return null
102
+ }
103
+ }
104
+
105
+ function getNavigator () {
106
+ try {
107
+ return globalThis.navigator
108
+ } catch {
109
+ return null
110
+ }
111
+ }
112
+
113
+ export function getCurrentDeviceLocale () {
114
+ const navigator = getNavigator()
115
+ const candidates = [
116
+ getIntlLocale(),
117
+ navigator?.language,
118
+ navigator?.languages?.[0],
119
+ DEFAULT_LOCALE
120
+ ]
121
+
122
+ for (const candidate of candidates) {
123
+ const locale = canonicalizeLocale(candidate)
124
+ if (locale) return locale
125
+ }
126
+ return DEFAULT_LOCALE
127
+ }
128
+
129
+ function localeLanguage (locale) {
130
+ try {
131
+ return new Intl.Locale(locale).language
132
+ } catch {
133
+ return locale.split('-')[0].toLowerCase()
134
+ }
135
+ }
136
+
137
+ function preferredChineseLocale (locale) {
138
+ if (localeLanguage(locale) !== 'zh') return null
139
+ try {
140
+ const { script, region } = new Intl.Locale(locale)
141
+ if (script === 'Hant' || ['TW', 'HK', 'MO'].includes(region)) return 'zh-TW'
142
+ return 'zh-CN'
143
+ } catch {
144
+ return /(?:^|-)(?:hant|tw|hk|mo)(?:-|$)/i.test(locale) ? 'zh-TW' : 'zh-CN'
145
+ }
146
+ }
147
+
148
+ function localeCandidates (translations, requestedLocale, fallbackLocale) {
149
+ const keys = Object.keys(translations)
150
+ const canonicalKeys = new Map()
151
+ for (const key of keys) {
152
+ const canonical = canonicalizeLocale(key)
153
+ if (canonical && !canonicalKeys.has(canonical.toLowerCase())) {
154
+ canonicalKeys.set(canonical.toLowerCase(), { key, canonical })
155
+ }
156
+ }
157
+
158
+ const result = []
159
+ const seen = new Set()
160
+ const add = locale => {
161
+ const canonical = canonicalizeLocale(locale)
162
+ const match = canonical && canonicalKeys.get(canonical.toLowerCase())
163
+ if (match && !seen.has(match.key)) {
164
+ seen.add(match.key)
165
+ result.push(match)
166
+ }
167
+ }
168
+ const addCompatible = locale => {
169
+ const canonical = canonicalizeLocale(locale)
170
+ if (!canonical) return
171
+ add(canonical)
172
+ add(preferredChineseLocale(canonical))
173
+ const language = localeLanguage(canonical)
174
+ const match = [...canonicalKeys.values()].find(candidate => localeLanguage(candidate.canonical) === language)
175
+ if (match) add(match.canonical)
176
+ }
177
+
178
+ addCompatible(requestedLocale)
179
+ addCompatible(fallbackLocale)
180
+ addCompatible(DEFAULT_LOCALE)
181
+ return result
182
+ }
183
+
184
+ function selectPlural (forms, locale, values) {
185
+ if (!forms || typeof forms !== 'object' || Array.isArray(forms)) return null
186
+ const count = Number(values?.count)
187
+ if (!Number.isFinite(count)) return null
188
+
189
+ let category = 'other'
190
+ try {
191
+ category = new Intl.PluralRules(locale).select(count)
192
+ } catch {}
193
+ const value = forms[category] ?? forms.other
194
+ return typeof value === 'string' ? value : null
195
+ }
196
+
197
+ function selectTemplate (translations, requestedLocale, fallbackLocale, values) {
198
+ if (!translations || typeof translations !== 'object' || Array.isArray(translations)) return null
199
+ for (const { key, canonical } of localeCandidates(translations, requestedLocale, fallbackLocale)) {
200
+ const value = translations[key]
201
+ if (typeof value === 'string') return value
202
+ const plural = selectPlural(value, canonical, values)
203
+ if (plural !== null) return plural
204
+ }
205
+ return null
206
+ }
207
+
208
+ function interpolate (template, values) {
209
+ const source = String(template)
210
+ if (!values || typeof values !== 'object') return source
211
+ return source.replace(INTERPOLATION_RE, (token, name) => (
212
+ Object.prototype.hasOwnProperty.call(values, name)
213
+ ? String(values[name])
214
+ : token
215
+ ))
216
+ }
217
+
218
+ export function getT (locales, {
219
+ locale = getCurrentDeviceLocale(),
220
+ fallbackLocale = DEFAULT_LOCALE,
221
+ validation
222
+ } = {}) {
223
+ assertLocales(locales)
224
+ if (validation !== undefined) validateLocales(locales, validation)
225
+
226
+ const requestedLocale = canonicalizeLocale(locale) ?? DEFAULT_LOCALE
227
+ const canonicalFallback = canonicalizeLocale(fallbackLocale) ?? DEFAULT_LOCALE
228
+
229
+ return function t (key, values) {
230
+ if (typeof key !== 'string') throw new TypeError('translation key should be a string')
231
+ const translations = Object.prototype.hasOwnProperty.call(locales, key) ? locales[key] : null
232
+ const template = selectTemplate(translations, requestedLocale, canonicalFallback, values) ?? key
233
+ return interpolate(template, values)
234
+ }
235
+ }
package/index.js CHANGED
@@ -1,6 +1,8 @@
1
1
  export * from './private-messenger/index.js'
2
2
 
3
3
  export * as base16 from './base16/index.js'
4
+ export * as base36 from './base36/index.js'
5
+ export * as base62 from './base62/index.js'
4
6
  export * as base64 from './base64/index.js'
5
7
  export * as contentKey from './content-key/index.js'
6
8
  export * as contentKeyEvent from './content-key/event/index.js'
@@ -8,6 +10,7 @@ export * as doubleDh from './double-dh/index.js'
8
10
  export * as ecdh from './ecdh/index.js'
9
11
  export * as idb from './idb/index.js'
10
12
  export * as idbQueue from './idb-queue/index.js'
13
+ export * as i18n from './i18n/index.js'
11
14
  export * as key from './key/index.js'
12
15
  export * as network from './network/index.js'
13
16
  export * as nip19 from './nip19/index.js'
package/nip19/index.js CHANGED
@@ -1,15 +1,9 @@
1
1
  import { bech32 } from '@scure/base'
2
+ import { BASE62_ALPHABET, base62ToBytes, bytesToBase62 } from '../base62/index.js'
2
3
 
3
4
  const MAX_ENTITY_SIZE = 5000
4
5
  const MAX_TLV_VALUE_SIZE = 255
5
6
  const NOSTR_APP_D_TAG_MAX_LENGTH = 260
6
- const BASE62_ALPHABET = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
7
- const BASE62_BASE = BigInt(BASE62_ALPHABET.length)
8
- const BASE62_LEADER = BASE62_ALPHABET[0]
9
- const BASE62_CHAR_MAP = new Map(
10
- [...BASE62_ALPHABET].map((character, index) => [character, BigInt(index)])
11
- )
12
-
13
7
  export const NAPP_ENTITY_REGEX = new RegExp(
14
8
  `^\\+{1,3}[${BASE62_ALPHABET}]{48,${MAX_ENTITY_SIZE}}$`
15
9
  )
@@ -178,45 +172,6 @@ export function nfileDecode (entity) {
178
172
  return result
179
173
  }
180
174
 
181
- function bytesToBase62 (bytes) {
182
- if (bytes.length === 0) return ''
183
- let number = 0n
184
- for (const byte of bytes) number = (number << 8n) + BigInt(byte)
185
-
186
- let result = ''
187
- if (number === 0n) return BASE62_LEADER
188
- while (number > 0n) {
189
- result = BASE62_ALPHABET[Number(number % BASE62_BASE)] + result
190
- number /= BASE62_BASE
191
- }
192
- for (const byte of bytes) {
193
- if (byte !== 0) break
194
- result = BASE62_LEADER + result
195
- }
196
- return result
197
- }
198
-
199
- function base62ToBytes (value) {
200
- if (typeof value !== 'string') throw new TypeError('Base62 value should be a string')
201
- let leadingZeros = 0
202
- while (value[leadingZeros] === BASE62_LEADER) leadingZeros++
203
-
204
- let number = 0n
205
- for (const character of value) {
206
- const digit = BASE62_CHAR_MAP.get(character)
207
- if (digit === undefined) throw new Error(`Invalid Base62 character: ${character}`)
208
- number = number * BASE62_BASE + digit
209
- }
210
- const suffix = []
211
- while (number > 0n) {
212
- suffix.unshift(Number(number & 0xffn))
213
- number >>= 8n
214
- }
215
- const result = new Uint8Array(leadingZeros + suffix.length)
216
- result.set(suffix, leadingZeros)
217
- return result
218
- }
219
-
220
175
  function isNostrAppDTagSafe (value) {
221
176
  return typeof value === 'string' && value.length <= NOSTR_APP_D_TAG_MAX_LENGTH
222
177
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "libp2r2p",
3
- "version": "0.2.0",
3
+ "version": "0.4.0",
4
4
  "description": "Peer-to-relay-to-peer",
5
5
  "keywords": [
6
6
  "p2r2p",
@@ -13,6 +13,8 @@
13
13
  "sideEffects": false,
14
14
  "files": [
15
15
  "base16",
16
+ "base36",
17
+ "base62",
16
18
  "base64",
17
19
  "base93",
18
20
  "content-key",
@@ -20,6 +22,7 @@
20
22
  "ecdh",
21
23
  "idb",
22
24
  "idb-queue",
25
+ "i18n",
23
26
  "index.js",
24
27
  "key",
25
28
  "network",
@@ -40,6 +43,8 @@
40
43
  "exports": {
41
44
  ".": "./index.js",
42
45
  "./base16": "./base16/index.js",
46
+ "./base36": "./base36/index.js",
47
+ "./base62": "./base62/index.js",
43
48
  "./base64": "./base64/index.js",
44
49
  "./base93": "./base93/index.js",
45
50
  "./content-key": "./content-key/index.js",
@@ -48,6 +53,7 @@
48
53
  "./ecdh": "./ecdh/index.js",
49
54
  "./idb": "./idb/index.js",
50
55
  "./idb-queue": "./idb-queue/index.js",
56
+ "./i18n": "./i18n/index.js",
51
57
  "./key": "./key/index.js",
52
58
  "./network": "./network/index.js",
53
59
  "./nip19": "./nip19/index.js",