@pyreon/i18n 0.0.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/LICENSE +21 -0
- package/README.md +227 -0
- package/lib/analysis/devtools.js.html +5406 -0
- package/lib/analysis/index.js.html +5406 -0
- package/lib/devtools.js +81 -0
- package/lib/devtools.js.map +1 -0
- package/lib/index.js +330 -0
- package/lib/index.js.map +1 -0
- package/lib/types/devtools.d.ts +74 -0
- package/lib/types/devtools.d.ts.map +1 -0
- package/lib/types/devtools2.d.ts +30 -0
- package/lib/types/devtools2.d.ts.map +1 -0
- package/lib/types/index.d.ts +334 -0
- package/lib/types/index.d.ts.map +1 -0
- package/lib/types/index2.d.ts +244 -0
- package/lib/types/index2.d.ts.map +1 -0
- package/package.json +57 -0
- package/src/context.ts +57 -0
- package/src/create-i18n.ts +309 -0
- package/src/devtools.ts +94 -0
- package/src/index.ts +18 -0
- package/src/interpolation.ts +28 -0
- package/src/pluralization.ts +31 -0
- package/src/tests/devtools.test.ts +166 -0
- package/src/tests/i18n.test.ts +859 -0
- package/src/tests/setup.ts +1 -0
- package/src/trans.ts +111 -0
- package/src/types.ts +112 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import '@happy-dom/global-registrator'
|
package/src/trans.ts
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { h, Fragment } from '@pyreon/core'
|
|
2
|
+
import type { VNode, Props } from '@pyreon/core'
|
|
3
|
+
import type { InterpolationValues } from './types'
|
|
4
|
+
|
|
5
|
+
const TAG_RE = /<(\w+)>(.*?)<\/\1>/gs
|
|
6
|
+
|
|
7
|
+
interface RichPart {
|
|
8
|
+
tag: string
|
|
9
|
+
children: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Parse a translated string into an array of plain text and rich tag segments.
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* parseRichText("Hello <bold>world</bold>, click <link>here</link>")
|
|
17
|
+
* // → ["Hello ", { tag: "bold", children: "world" }, ", click ", { tag: "link", children: "here" }]
|
|
18
|
+
*/
|
|
19
|
+
export function parseRichText(text: string): (string | RichPart)[] {
|
|
20
|
+
const parts: (string | RichPart)[] = []
|
|
21
|
+
let lastIndex = 0
|
|
22
|
+
|
|
23
|
+
for (const match of text.matchAll(TAG_RE)) {
|
|
24
|
+
const before = text.slice(lastIndex, match.index)
|
|
25
|
+
if (before) parts.push(before)
|
|
26
|
+
parts.push({ tag: match[1]!, children: match[2]! })
|
|
27
|
+
lastIndex = match.index! + match[0].length
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const after = text.slice(lastIndex)
|
|
31
|
+
if (after) parts.push(after)
|
|
32
|
+
|
|
33
|
+
return parts
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface TransProps extends Props {
|
|
37
|
+
/** Translation key (supports namespace:key syntax). */
|
|
38
|
+
i18nKey: string
|
|
39
|
+
/** Interpolation values for {{placeholder}} syntax. */
|
|
40
|
+
values?: InterpolationValues
|
|
41
|
+
/**
|
|
42
|
+
* Component map for rich interpolation.
|
|
43
|
+
* Keys match tag names in the translation string.
|
|
44
|
+
* Values are component functions: `(children: any) => VNode`
|
|
45
|
+
*
|
|
46
|
+
* @example
|
|
47
|
+
* // Translation: "Read the <terms>terms</terms> and <privacy>policy</privacy>"
|
|
48
|
+
* components={{
|
|
49
|
+
* terms: (children) => <a href="/terms">{children}</a>,
|
|
50
|
+
* privacy: (children) => <a href="/privacy">{children}</a>,
|
|
51
|
+
* }}
|
|
52
|
+
*/
|
|
53
|
+
components?: Record<string, (children: any) => any>
|
|
54
|
+
/**
|
|
55
|
+
* The i18n instance's `t` function.
|
|
56
|
+
* Can be obtained from `useI18n()` or passed directly.
|
|
57
|
+
*/
|
|
58
|
+
t: (key: string, values?: InterpolationValues) => string
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Rich JSX interpolation component for translations.
|
|
63
|
+
*
|
|
64
|
+
* Allows embedding JSX components within translated strings using XML-like tags.
|
|
65
|
+
* The `t` function resolves the translation and interpolates `{{values}}` first,
|
|
66
|
+
* then `<tag>content</tag>` patterns are mapped to the provided components.
|
|
67
|
+
*
|
|
68
|
+
* @example
|
|
69
|
+
* // Translation: "You have <bold>{{count}}</bold> unread messages"
|
|
70
|
+
* const { t } = useI18n()
|
|
71
|
+
* <Trans
|
|
72
|
+
* t={t}
|
|
73
|
+
* i18nKey="messages.unread"
|
|
74
|
+
* values={{ count: 5 }}
|
|
75
|
+
* components={{
|
|
76
|
+
* bold: (children) => <strong>{children}</strong>,
|
|
77
|
+
* }}
|
|
78
|
+
* />
|
|
79
|
+
* // Renders: You have <strong>5</strong> unread messages
|
|
80
|
+
*
|
|
81
|
+
* @example
|
|
82
|
+
* // Translation: "Read our <terms>terms of service</terms> and <privacy>privacy policy</privacy>"
|
|
83
|
+
* <Trans
|
|
84
|
+
* t={t}
|
|
85
|
+
* i18nKey="legal"
|
|
86
|
+
* components={{
|
|
87
|
+
* terms: (children) => <a href="/terms">{children}</a>,
|
|
88
|
+
* privacy: (children) => <a href="/privacy">{children}</a>,
|
|
89
|
+
* }}
|
|
90
|
+
* />
|
|
91
|
+
*/
|
|
92
|
+
export function Trans(props: TransProps): VNode | string {
|
|
93
|
+
const translated = props.t(props.i18nKey, props.values)
|
|
94
|
+
|
|
95
|
+
if (!props.components) return translated
|
|
96
|
+
|
|
97
|
+
const parts = parseRichText(translated)
|
|
98
|
+
|
|
99
|
+
// If the result is a single plain string, return it directly
|
|
100
|
+
if (parts.length === 1 && typeof parts[0] === 'string') return parts[0]
|
|
101
|
+
|
|
102
|
+
const children = parts.map((part) => {
|
|
103
|
+
if (typeof part === 'string') return part
|
|
104
|
+
const component = props.components![part.tag]
|
|
105
|
+
// Unmatched tags: render children as plain text (no raw HTML markup)
|
|
106
|
+
if (!component) return part.children
|
|
107
|
+
return component(part.children)
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
return h(Fragment, null, ...children)
|
|
111
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import type { Signal, Computed } from '@pyreon/reactivity'
|
|
2
|
+
|
|
3
|
+
/** A nested dictionary of translation strings. */
|
|
4
|
+
export type TranslationDictionary = {
|
|
5
|
+
[key: string]: string | TranslationDictionary
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/** Map of locale → dictionary (or namespace → dictionary). */
|
|
9
|
+
export type TranslationMessages = Record<string, TranslationDictionary>
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Async function that loads translations for a locale and namespace.
|
|
13
|
+
* Return the dictionary for that namespace, or undefined if not found.
|
|
14
|
+
*/
|
|
15
|
+
export type NamespaceLoader = (
|
|
16
|
+
locale: string,
|
|
17
|
+
namespace: string,
|
|
18
|
+
) => Promise<TranslationDictionary | undefined>
|
|
19
|
+
|
|
20
|
+
/** Interpolation values for translation strings. */
|
|
21
|
+
export type InterpolationValues = Record<string, string | number>
|
|
22
|
+
|
|
23
|
+
/** Pluralization rules map — locale → function that picks the plural form. */
|
|
24
|
+
export type PluralRules = Record<string, (count: number) => string>
|
|
25
|
+
|
|
26
|
+
/** Options for creating an i18n instance. */
|
|
27
|
+
export interface I18nOptions {
|
|
28
|
+
/** The initial locale (e.g. "en"). */
|
|
29
|
+
locale: string
|
|
30
|
+
/** Fallback locale used when a key is missing in the active locale. */
|
|
31
|
+
fallbackLocale?: string
|
|
32
|
+
/** Static messages keyed by locale. */
|
|
33
|
+
messages?: Record<string, TranslationDictionary>
|
|
34
|
+
/**
|
|
35
|
+
* Async loader for namespace-based translation loading.
|
|
36
|
+
* Called with (locale, namespace) when `loadNamespace()` is invoked.
|
|
37
|
+
*/
|
|
38
|
+
loader?: NamespaceLoader
|
|
39
|
+
/**
|
|
40
|
+
* Default namespace used when `t()` is called without a namespace prefix.
|
|
41
|
+
* Defaults to "common".
|
|
42
|
+
*/
|
|
43
|
+
defaultNamespace?: string
|
|
44
|
+
/**
|
|
45
|
+
* Custom plural rules per locale.
|
|
46
|
+
* If not provided, uses `Intl.PluralRules` where available.
|
|
47
|
+
*/
|
|
48
|
+
pluralRules?: PluralRules
|
|
49
|
+
/**
|
|
50
|
+
* Missing key handler — called when a translation key is not found.
|
|
51
|
+
* Useful for logging, reporting, or returning a custom fallback.
|
|
52
|
+
*/
|
|
53
|
+
onMissingKey?: (
|
|
54
|
+
locale: string,
|
|
55
|
+
key: string,
|
|
56
|
+
namespace?: string,
|
|
57
|
+
) => string | undefined
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** The public i18n instance returned by `createI18n()`. */
|
|
61
|
+
export interface I18nInstance {
|
|
62
|
+
/**
|
|
63
|
+
* Translate a key with optional interpolation.
|
|
64
|
+
* Reads the current locale reactively — re-evaluates in effects/computeds.
|
|
65
|
+
*
|
|
66
|
+
* Key format: "key" (uses default namespace) or "namespace:key".
|
|
67
|
+
* Nested keys use dots: "user.greeting" or "auth:errors.invalid".
|
|
68
|
+
*
|
|
69
|
+
* Interpolation: "Hello {{name}}" + { name: "Alice" } → "Hello Alice"
|
|
70
|
+
* Pluralization: key with "_one", "_other" etc. suffixes + { count: N }
|
|
71
|
+
*/
|
|
72
|
+
t: (key: string, values?: InterpolationValues) => string
|
|
73
|
+
|
|
74
|
+
/** Current locale (reactive signal). */
|
|
75
|
+
locale: Signal<string>
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Load a namespace's translations for the given locale (or current locale).
|
|
79
|
+
* Returns a promise that resolves when loading is complete.
|
|
80
|
+
*/
|
|
81
|
+
loadNamespace: (namespace: string, locale?: string) => Promise<void>
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Whether any namespace is currently being loaded.
|
|
85
|
+
*/
|
|
86
|
+
isLoading: Computed<boolean>
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Set of namespaces that have been loaded for the current locale.
|
|
90
|
+
*/
|
|
91
|
+
loadedNamespaces: Computed<Set<string>>
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Check if a translation key exists in the current locale.
|
|
95
|
+
*/
|
|
96
|
+
exists: (key: string) => boolean
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Add translations for a locale (merged with existing).
|
|
100
|
+
* Useful for adding translations at runtime without async loading.
|
|
101
|
+
*/
|
|
102
|
+
addMessages: (
|
|
103
|
+
locale: string,
|
|
104
|
+
messages: TranslationDictionary,
|
|
105
|
+
namespace?: string,
|
|
106
|
+
) => void
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Get all available locales (those with any registered messages).
|
|
110
|
+
*/
|
|
111
|
+
availableLocales: Computed<string[]>
|
|
112
|
+
}
|