libp2r2p 0.9.0 → 0.10.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
@@ -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/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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "libp2r2p",
3
- "version": "0.9.0",
3
+ "version": "0.10.0",
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",
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
- }