@fluenti/react 0.2.1 → 0.3.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/dist/components/DateTime.d.ts +2 -2
- package/dist/components/DateTime.d.ts.map +1 -1
- package/dist/components/Number.d.ts +2 -2
- package/dist/components/Number.d.ts.map +1 -1
- package/dist/components/Plural.d.ts +2 -2
- package/dist/components/Plural.d.ts.map +1 -1
- package/dist/components/Select.d.ts +5 -3
- package/dist/components/Select.d.ts.map +1 -1
- package/dist/components/Trans.d.ts +4 -2
- package/dist/components/Trans.d.ts.map +1 -1
- package/dist/components/icu-rich.d.ts +2 -9
- package/dist/components/icu-rich.d.ts.map +1 -1
- package/dist/components/plural-core.d.ts +2 -2
- package/dist/components/plural-core.d.ts.map +1 -1
- package/dist/components/trans-core.d.ts +1 -2
- package/dist/components/trans-core.d.ts.map +1 -1
- package/dist/context.d.ts +2 -2
- package/dist/context.d.ts.map +1 -1
- package/dist/create-fluenti.d.ts +87 -0
- package/dist/create-fluenti.d.ts.map +1 -0
- package/dist/global-registry.d.ts +4 -4
- package/dist/global-registry.d.ts.map +1 -1
- package/dist/hooks/__useI18n.d.ts +2 -2
- package/dist/hooks/__useI18n.d.ts.map +1 -1
- package/dist/hooks/useI18n.d.ts +2 -2
- package/dist/hooks/useI18n.d.ts.map +1 -1
- package/dist/icu-rich-BOtj4Oxu.js +71 -0
- package/dist/icu-rich-BOtj4Oxu.js.map +1 -0
- package/dist/icu-rich-vPU-0wGQ.cjs +2 -0
- package/dist/icu-rich-vPU-0wGQ.cjs.map +1 -0
- package/dist/index.cjs +1 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +8 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +252 -107
- package/dist/index.js.map +1 -1
- package/dist/provider.d.ts +2 -2
- package/dist/provider.d.ts.map +1 -1
- package/dist/react-runtime.d.ts.map +1 -1
- package/dist/server.cjs +1 -1
- package/dist/server.cjs.map +1 -1
- package/dist/server.d.ts +13 -3
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +24 -13
- package/dist/server.js.map +1 -1
- package/dist/types.d.ts +17 -10
- package/dist/types.d.ts.map +1 -1
- package/dist/vite-plugin.cjs +1 -109
- package/dist/vite-plugin.cjs.map +1 -1
- package/dist/vite-plugin.js +13 -119
- package/dist/vite-plugin.js.map +1 -1
- package/llms-full.txt +185 -0
- package/llms-migration.txt +272 -0
- package/llms.txt +62 -0
- package/package.json +7 -5
- package/dist/icu-rich-DBeWY1k6.js +0 -108
- package/dist/icu-rich-DBeWY1k6.js.map +0 -1
- package/dist/icu-rich-XY1SdM5K.cjs +0 -2
- package/dist/icu-rich-XY1SdM5K.cjs.map +0 -1
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
# Migrating to @fluenti/react from react-i18next / react-intl
|
|
2
|
+
|
|
3
|
+
> Step-by-step guide to migrate a React app from react-i18next or react-intl (FormatJS) to Fluenti.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
Fluenti is a **compile-time** i18n library — translations are compiled to optimized JS at build time. Unlike react-i18next and react-intl which parse messages at runtime, Fluenti produces zero-overhead string functions.
|
|
8
|
+
|
|
9
|
+
Key differences:
|
|
10
|
+
- No runtime message parser — messages become plain strings or template literal functions
|
|
11
|
+
- ICU MessageFormat for all syntax (plurals, selects, numbers, dates)
|
|
12
|
+
- PO or JSON catalog format, managed by CLI extraction and compilation
|
|
13
|
+
- Vite plugin or Next.js plugin for build-time transforms
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Migrating from react-i18next
|
|
18
|
+
|
|
19
|
+
### 1. Install Fluenti
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
pnpm add @fluenti/core @fluenti/react
|
|
23
|
+
pnpm add -D @fluenti/cli
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
### 2. Replace provider setup
|
|
27
|
+
|
|
28
|
+
Before (react-i18next):
|
|
29
|
+
```tsx
|
|
30
|
+
import i18n from 'i18next'
|
|
31
|
+
import { initReactI18next, I18nextProvider } from 'react-i18next'
|
|
32
|
+
|
|
33
|
+
i18n.use(initReactI18next).init({
|
|
34
|
+
resources: { en: { translation: messages } },
|
|
35
|
+
lng: 'en',
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
<I18nextProvider i18n={i18n}><App /></I18nextProvider>
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
After (Fluenti):
|
|
42
|
+
```tsx
|
|
43
|
+
import { I18nProvider } from '@fluenti/react'
|
|
44
|
+
import en from './locales/compiled/en'
|
|
45
|
+
|
|
46
|
+
<I18nProvider locale="en" fallbackLocale="en" messages={{ en }}>
|
|
47
|
+
<App />
|
|
48
|
+
</I18nProvider>
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### 3. Update hook usage
|
|
52
|
+
|
|
53
|
+
Before (react-i18next):
|
|
54
|
+
```tsx
|
|
55
|
+
import { useTranslation } from 'react-i18next'
|
|
56
|
+
const { t, i18n } = useTranslation()
|
|
57
|
+
t('hello', { name: 'World' })
|
|
58
|
+
i18n.changeLanguage('ja')
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
After (Fluenti):
|
|
62
|
+
```tsx
|
|
63
|
+
import { useI18n } from '@fluenti/react'
|
|
64
|
+
const { i18n, setLocale } = useI18n()
|
|
65
|
+
i18n.t('hello', { name: 'World' })
|
|
66
|
+
setLocale('ja')
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### 4. Replace components
|
|
70
|
+
|
|
71
|
+
Before (react-i18next):
|
|
72
|
+
```tsx
|
|
73
|
+
import { Trans } from 'react-i18next'
|
|
74
|
+
<Trans i18nKey="welcome">
|
|
75
|
+
Welcome to <strong>our app</strong>
|
|
76
|
+
</Trans>
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
After (Fluenti):
|
|
80
|
+
```tsx
|
|
81
|
+
import { Trans } from '@fluenti/react'
|
|
82
|
+
<Trans message="Welcome to {0}our app{1}">
|
|
83
|
+
{(text) => <strong>{text}</strong>}
|
|
84
|
+
</Trans>
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### 5. Convert message syntax
|
|
88
|
+
|
|
89
|
+
react-i18next interpolation → ICU:
|
|
90
|
+
```
|
|
91
|
+
# react-i18next
|
|
92
|
+
"hello": "Hello, {{name}}!"
|
|
93
|
+
|
|
94
|
+
# Fluenti (ICU MessageFormat)
|
|
95
|
+
"hello": "Hello, {name}!"
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
react-i18next plurals → ICU plural:
|
|
99
|
+
```
|
|
100
|
+
# react-i18next
|
|
101
|
+
"items": "{{count}} item"
|
|
102
|
+
"items_plural": "{{count}} items"
|
|
103
|
+
|
|
104
|
+
# Fluenti (ICU)
|
|
105
|
+
"items": "{count, plural, one {{count} item} other {{count} items}}"
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### API Mapping: react-i18next → Fluenti
|
|
109
|
+
|
|
110
|
+
| react-i18next | Fluenti |
|
|
111
|
+
|---------------|---------|
|
|
112
|
+
| `useTranslation()` | `useI18n()` |
|
|
113
|
+
| `t(key, options)` | `i18n.t(key, values)` |
|
|
114
|
+
| `i18n.changeLanguage(lng)` | `setLocale(locale)` |
|
|
115
|
+
| `i18n.language` | `locale` |
|
|
116
|
+
| `<Trans>` | `<Trans>` |
|
|
117
|
+
| `<I18nextProvider>` | `<I18nProvider>` |
|
|
118
|
+
| `{{var}}` interpolation | `{var}` interpolation |
|
|
119
|
+
| `_plural` suffix | ICU `{n, plural, ...}` |
|
|
120
|
+
| Namespaces | Not needed (use message IDs) |
|
|
121
|
+
| Backend plugins | `loadMessages` prop on provider |
|
|
122
|
+
|
|
123
|
+
---
|
|
124
|
+
|
|
125
|
+
## Migrating from react-intl (FormatJS)
|
|
126
|
+
|
|
127
|
+
### 1. Install Fluenti
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
pnpm add @fluenti/core @fluenti/react
|
|
131
|
+
pnpm add -D @fluenti/cli
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### 2. Replace provider
|
|
135
|
+
|
|
136
|
+
Before (react-intl):
|
|
137
|
+
```tsx
|
|
138
|
+
import { IntlProvider } from 'react-intl'
|
|
139
|
+
<IntlProvider locale="en" messages={messages}>
|
|
140
|
+
<App />
|
|
141
|
+
</IntlProvider>
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
After (Fluenti):
|
|
145
|
+
```tsx
|
|
146
|
+
import { I18nProvider } from '@fluenti/react'
|
|
147
|
+
import en from './locales/compiled/en'
|
|
148
|
+
<I18nProvider locale="en" fallbackLocale="en" messages={{ en }}>
|
|
149
|
+
<App />
|
|
150
|
+
</I18nProvider>
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
### 3. Update hook usage
|
|
154
|
+
|
|
155
|
+
Before (react-intl):
|
|
156
|
+
```tsx
|
|
157
|
+
import { useIntl } from 'react-intl'
|
|
158
|
+
const intl = useIntl()
|
|
159
|
+
intl.formatMessage({ id: 'hello' }, { name: 'World' })
|
|
160
|
+
intl.formatDate(date, { dateStyle: 'long' })
|
|
161
|
+
intl.formatNumber(1234, { style: 'decimal' })
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
After (Fluenti):
|
|
165
|
+
```tsx
|
|
166
|
+
import { useI18n } from '@fluenti/react'
|
|
167
|
+
const { i18n } = useI18n()
|
|
168
|
+
i18n.t('hello', { name: 'World' })
|
|
169
|
+
i18n.d(date, 'long')
|
|
170
|
+
i18n.n(1234, 'decimal')
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
### 4. Replace components
|
|
174
|
+
|
|
175
|
+
Before (react-intl):
|
|
176
|
+
```tsx
|
|
177
|
+
import { FormattedMessage, FormattedDate, FormattedNumber } from 'react-intl'
|
|
178
|
+
<FormattedMessage id="hello" values={{ name: 'World' }} />
|
|
179
|
+
<FormattedDate value={date} dateStyle="long" />
|
|
180
|
+
<FormattedNumber value={1234} style="decimal" />
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
After (Fluenti):
|
|
184
|
+
```tsx
|
|
185
|
+
import { Trans, DateTime, NumberFormat } from '@fluenti/react'
|
|
186
|
+
<Trans message="Hello, {name}!" values={{ name: 'World' }} />
|
|
187
|
+
<DateTime value={date} style="long" />
|
|
188
|
+
<NumberFormat value={1234} style="decimal" />
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
### 5. Message format
|
|
192
|
+
|
|
193
|
+
react-intl already uses ICU MessageFormat, so most messages work as-is. Key differences:
|
|
194
|
+
|
|
195
|
+
```
|
|
196
|
+
# react-intl — uses defaultMessage
|
|
197
|
+
defineMessage({ id: 'hello', defaultMessage: 'Hello, {name}!' })
|
|
198
|
+
|
|
199
|
+
# Fluenti — messages live in catalog files, referenced by ID
|
|
200
|
+
i18n.t('hello', { name: 'World' })
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
### API Mapping: react-intl → Fluenti
|
|
204
|
+
|
|
205
|
+
| react-intl | Fluenti |
|
|
206
|
+
|------------|---------|
|
|
207
|
+
| `<IntlProvider>` | `<I18nProvider>` |
|
|
208
|
+
| `useIntl()` | `useI18n()` |
|
|
209
|
+
| `intl.formatMessage()` | `i18n.t()` |
|
|
210
|
+
| `intl.formatDate()` | `i18n.d()` |
|
|
211
|
+
| `intl.formatNumber()` | `i18n.n()` |
|
|
212
|
+
| `<FormattedMessage>` | `<Trans>` |
|
|
213
|
+
| `<FormattedDate>` | `<DateTime>` |
|
|
214
|
+
| `<FormattedNumber>` | `<NumberFormat>` |
|
|
215
|
+
| `<FormattedPlural>` | `<Plural>` |
|
|
216
|
+
| `defineMessage()` | `msg()` |
|
|
217
|
+
| ICU MessageFormat | ICU MessageFormat (compatible) |
|
|
218
|
+
|
|
219
|
+
---
|
|
220
|
+
|
|
221
|
+
## Common Steps for Both
|
|
222
|
+
|
|
223
|
+
### Set up Fluenti CLI config
|
|
224
|
+
|
|
225
|
+
```ts
|
|
226
|
+
// fluenti.config.ts
|
|
227
|
+
export default {
|
|
228
|
+
sourceLocale: 'en',
|
|
229
|
+
locales: ['en', 'ja', 'zh-CN'],
|
|
230
|
+
catalogDir: './locales',
|
|
231
|
+
format: 'po',
|
|
232
|
+
include: ['./src/**/*.{tsx,jsx,ts,js}'],
|
|
233
|
+
compileOutDir: './locales/compiled',
|
|
234
|
+
}
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
### Extract and compile
|
|
238
|
+
|
|
239
|
+
```bash
|
|
240
|
+
npx fluenti extract # Extract messages from source
|
|
241
|
+
npx fluenti compile # Compile to JS modules
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
### Vite plugin setup
|
|
245
|
+
|
|
246
|
+
```ts
|
|
247
|
+
// vite.config.ts
|
|
248
|
+
import react from '@vitejs/plugin-react'
|
|
249
|
+
import fluentiReact from '@fluenti/react/vite-plugin'
|
|
250
|
+
|
|
251
|
+
export default {
|
|
252
|
+
plugins: [react(), fluentiReact()],
|
|
253
|
+
}
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
### For Next.js projects
|
|
257
|
+
|
|
258
|
+
Use @fluenti/next instead of the Vite plugin:
|
|
259
|
+
|
|
260
|
+
```ts
|
|
261
|
+
// next.config.ts
|
|
262
|
+
import { withFluenti } from '@fluenti/next'
|
|
263
|
+
export default withFluenti({})
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
## Key Behavioral Differences
|
|
267
|
+
|
|
268
|
+
1. **Compile-time** — syntax errors caught at build time, not runtime
|
|
269
|
+
2. **No runtime parser** — smaller bundle, faster rendering
|
|
270
|
+
3. **PO file support** — compatible with Poedit, Crowdin, Weblate
|
|
271
|
+
4. **CLI-managed catalogs** — extract → translate → compile workflow
|
|
272
|
+
5. **AI translation** — `npx fluenti translate --provider claude` for automated translation
|
package/llms.txt
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# @fluenti/react
|
|
2
|
+
|
|
3
|
+
> React bindings for Fluenti — compile-time `t`, runtime-capable components, and client/server i18n helpers.
|
|
4
|
+
|
|
5
|
+
@fluenti/react provides the React runtime for Fluenti. The recommended public path is:
|
|
6
|
+
|
|
7
|
+
- compile-time authoring: `import { t } from '@fluenti/react'`
|
|
8
|
+
- runtime / imperative access: `useI18n()`
|
|
9
|
+
- server runtime access: `createServerI18n()` from `@fluenti/react/server`
|
|
10
|
+
|
|
11
|
+
## Install
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
pnpm add @fluenti/core @fluenti/react
|
|
15
|
+
pnpm add -D @fluenti/cli @fluenti/vite-plugin
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Main Exports
|
|
19
|
+
|
|
20
|
+
Main entry (`@fluenti/react`):
|
|
21
|
+
|
|
22
|
+
- `I18nProvider`
|
|
23
|
+
- `useI18n()`
|
|
24
|
+
- `t`
|
|
25
|
+
- `Trans`, `Plural`, `Select`, `DateTime`, `NumberFormat`
|
|
26
|
+
- `msg`
|
|
27
|
+
- `I18nContext`
|
|
28
|
+
|
|
29
|
+
Type re-exports from `@fluenti/react`:
|
|
30
|
+
|
|
31
|
+
- `FluentiContext`, `I18nProviderProps`, `Messages`, `AllMessages`, `MessageDescriptor`, `CompileTimeMessageDescriptor`, `CompileTimeT`, `Locale`, `DateFormatOptions`, `NumberFormatOptions`
|
|
32
|
+
- `FluentiTransProps`, `FluentiPluralProps`, `FluentiSelectProps`, `FluentiDateTimeProps`, `NumberFormatProps` — component prop types
|
|
33
|
+
|
|
34
|
+
Server entry (`@fluenti/react/server`):
|
|
35
|
+
|
|
36
|
+
- `createServerI18n()`
|
|
37
|
+
- SSR helpers re-exported from `@fluenti/core`
|
|
38
|
+
|
|
39
|
+
## Important Boundaries
|
|
40
|
+
|
|
41
|
+
- imported `t` is compile-time only and requires the Fluenti plugin / loader
|
|
42
|
+
- `useI18n().t()` is the full runtime API on the client
|
|
43
|
+
- `createServerI18n()` is the full runtime API on the server
|
|
44
|
+
- `Trans / Plural / Select / DateTime / NumberFormat` remain runtime-capable without the build plugin
|
|
45
|
+
|
|
46
|
+
## Preferred Usage (compile-time first)
|
|
47
|
+
|
|
48
|
+
1. t\`\` tagged template — t\`Hello {name}\` (primary compile-time API)
|
|
49
|
+
2. `<Trans>` / `<Plural>` / `<Select>` — rich text with inline markup
|
|
50
|
+
3. `msg` tagged template — outside components (route meta, stores)
|
|
51
|
+
4. `useI18n().t()` / `createServerI18n().t()` — runtime fallback for dynamic keys only
|
|
52
|
+
|
|
53
|
+
❌ AVOID: `t('some.key')` as the default — this is a legacy i18n pattern.
|
|
54
|
+
|
|
55
|
+
## Lazy locale loading
|
|
56
|
+
|
|
57
|
+
React uses `loadMessages(locale)` on `I18nProvider` for async locale chunks.
|
|
58
|
+
|
|
59
|
+
## Docs
|
|
60
|
+
|
|
61
|
+
- Full docs: https://fluenti.dev
|
|
62
|
+
- Source: https://github.com/usefluenti/fluenti
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fluenti/react",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "React bindings for Fluenti — I18nProvider, useI18n, Trans/Plural/Select components",
|
|
6
6
|
"homepage": "https://fluenti.dev",
|
|
@@ -62,7 +62,8 @@
|
|
|
62
62
|
}
|
|
63
63
|
},
|
|
64
64
|
"files": [
|
|
65
|
-
"dist"
|
|
65
|
+
"dist",
|
|
66
|
+
"llms*.txt"
|
|
66
67
|
],
|
|
67
68
|
"peerDependencies": {
|
|
68
69
|
"react": ">=18.0.0",
|
|
@@ -74,8 +75,8 @@
|
|
|
74
75
|
}
|
|
75
76
|
},
|
|
76
77
|
"dependencies": {
|
|
77
|
-
"@fluenti/core": "0.
|
|
78
|
-
"@fluenti/vite-plugin": "0.
|
|
78
|
+
"@fluenti/core": "0.3.1",
|
|
79
|
+
"@fluenti/vite-plugin": "0.3.1"
|
|
79
80
|
},
|
|
80
81
|
"devDependencies": {
|
|
81
82
|
"typescript": "^5.9",
|
|
@@ -94,6 +95,7 @@
|
|
|
94
95
|
"build": "vite build",
|
|
95
96
|
"dev": "vite build --watch",
|
|
96
97
|
"test": "vitest run",
|
|
97
|
-
"typecheck": "tsc --noEmit"
|
|
98
|
+
"typecheck": "tsc --noEmit",
|
|
99
|
+
"bench": "vitest bench"
|
|
98
100
|
}
|
|
99
101
|
}
|
|
@@ -1,108 +0,0 @@
|
|
|
1
|
-
import { Children as e, Fragment as t, cloneElement as n, createElement as r, isValidElement as i } from "react";
|
|
2
|
-
import { hashMessage as a } from "@fluenti/core";
|
|
3
|
-
//#region src/components/trans-core.ts
|
|
4
|
-
function o(n) {
|
|
5
|
-
let r = [], a = "";
|
|
6
|
-
return e.forEach(n, (e) => {
|
|
7
|
-
if (typeof e == "string" || typeof e == "number") a += String(e);
|
|
8
|
-
else if (i(e)) {
|
|
9
|
-
if (e.type === t) {
|
|
10
|
-
let t = o(e.props.children);
|
|
11
|
-
a += s(t.message, r.length), r.push(...t.components);
|
|
12
|
-
return;
|
|
13
|
-
}
|
|
14
|
-
let n = r.length, i = o(e.props.children);
|
|
15
|
-
r.push(e), r.push(...i.components), a += `<${n}>${s(i.message, n + 1)}</${n}>`;
|
|
16
|
-
}
|
|
17
|
-
}), {
|
|
18
|
-
message: a,
|
|
19
|
-
components: r
|
|
20
|
-
};
|
|
21
|
-
}
|
|
22
|
-
function s(e, t) {
|
|
23
|
-
return t === 0 ? e : e.replace(/<(\d+)(\/?>)/g, (e, n, r) => `<${Number(n) + t}${r}`).replace(/<\/(\d+)>/g, (e, n) => `</${Number(n) + t}>`);
|
|
24
|
-
}
|
|
25
|
-
function c(e, i) {
|
|
26
|
-
let a = /<(\d+)>([\s\S]*?)<\/\1>/g, o = [], s = 0, l;
|
|
27
|
-
for (a.lastIndex = 0, l = a.exec(e); l !== null;) {
|
|
28
|
-
l.index > s && o.push(e.slice(s, l.index));
|
|
29
|
-
let t = Number(l[1]), r = l[2], u = i[t];
|
|
30
|
-
if (u) {
|
|
31
|
-
let e = c(r, i);
|
|
32
|
-
o.push(n(u, { key: `trans-${t}` }, e));
|
|
33
|
-
} else o.push(r);
|
|
34
|
-
s = a.lastIndex, l = a.exec(e);
|
|
35
|
-
}
|
|
36
|
-
return s < e.length && o.push(e.slice(s)), o.length === 1 ? o[0] : r(t, null, ...o);
|
|
37
|
-
}
|
|
38
|
-
//#endregion
|
|
39
|
-
//#region src/components/plural-core.ts
|
|
40
|
-
var l = [
|
|
41
|
-
"zero",
|
|
42
|
-
"one",
|
|
43
|
-
"two",
|
|
44
|
-
"few",
|
|
45
|
-
"many",
|
|
46
|
-
"other"
|
|
47
|
-
];
|
|
48
|
-
//#endregion
|
|
49
|
-
//#region src/components/icu-rich.tsx
|
|
50
|
-
function u(e, t) {
|
|
51
|
-
let n = [];
|
|
52
|
-
for (let t of l) {
|
|
53
|
-
let r = e[t];
|
|
54
|
-
r !== void 0 && n.push(`${t === "zero" ? "=0" : t} {${r}}`);
|
|
55
|
-
}
|
|
56
|
-
return `{count, plural, ${t ? `offset:${t} ` : ""}${n.join(" ")}}`;
|
|
57
|
-
}
|
|
58
|
-
function d(e) {
|
|
59
|
-
return `{value, select, ${Object.entries(e).filter(([, e]) => e !== void 0).map(([e, t]) => `${e} {${t}}`).join(" ")}}`;
|
|
60
|
-
}
|
|
61
|
-
function f(e) {
|
|
62
|
-
let t = {}, n = {}, r = 0;
|
|
63
|
-
for (let [i, a] of Object.entries(e)) {
|
|
64
|
-
if (i === "other") {
|
|
65
|
-
t.other = a;
|
|
66
|
-
continue;
|
|
67
|
-
}
|
|
68
|
-
let e = /^[A-Za-z0-9_]+$/.test(i) ? i : `case_${r++}`;
|
|
69
|
-
t[e] = a, n[i] = e;
|
|
70
|
-
}
|
|
71
|
-
return t.other === void 0 && (t.other = ""), {
|
|
72
|
-
forms: t,
|
|
73
|
-
valueMap: n
|
|
74
|
-
};
|
|
75
|
-
}
|
|
76
|
-
function p(e) {
|
|
77
|
-
let t = o(e);
|
|
78
|
-
return {
|
|
79
|
-
message: t.message,
|
|
80
|
-
components: t.components
|
|
81
|
-
};
|
|
82
|
-
}
|
|
83
|
-
function m(e, t) {
|
|
84
|
-
let n = [], r = {};
|
|
85
|
-
for (let i of e) {
|
|
86
|
-
let e = t[i];
|
|
87
|
-
if (e === void 0) continue;
|
|
88
|
-
let a = p(e);
|
|
89
|
-
r[i] = s(a.message, n.length), n.push(...a.components);
|
|
90
|
-
}
|
|
91
|
-
for (let [i, a] of Object.entries(t)) {
|
|
92
|
-
if (e.includes(i) || a === void 0) continue;
|
|
93
|
-
let t = p(a);
|
|
94
|
-
r[i] = s(t.message, n.length), n.push(...t.components);
|
|
95
|
-
}
|
|
96
|
-
return {
|
|
97
|
-
messages: r,
|
|
98
|
-
components: n
|
|
99
|
-
};
|
|
100
|
-
}
|
|
101
|
-
function h(e, t, n, r) {
|
|
102
|
-
let i = n(e, t);
|
|
103
|
-
return r.length > 0 ? c(i, r) : i;
|
|
104
|
-
}
|
|
105
|
-
//#endregion
|
|
106
|
-
export { m as a, a as c, h as i, c as l, d as n, l as o, f as r, o as s, u as t };
|
|
107
|
-
|
|
108
|
-
//# sourceMappingURL=icu-rich-DBeWY1k6.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"icu-rich-DBeWY1k6.js","names":[],"sources":["../src/components/trans-core.ts","../src/components/plural-core.ts","../src/components/icu-rich.tsx"],"sourcesContent":["import {\n Children,\n isValidElement,\n cloneElement,\n createElement,\n Fragment,\n type ReactNode,\n type ReactElement,\n} from 'react'\nimport { hashMessage } from '@fluenti/core'\n\nexport { hashMessage }\n\n/**\n * Extract a message string and component list from React children.\n *\n * Converts:\n * <Trans>Hello <b>{name}</b>, welcome!</Trans>\n * Into:\n * message: \"Hello <0>{name}</0>, welcome!\"\n * components: [<b>{name}</b>]\n *\n * @internal\n */\nexport function extractMessage(children: ReactNode): {\n message: string\n components: ReactElement[]\n} {\n const components: ReactElement[] = []\n let message = ''\n\n Children.forEach(children, (child) => {\n if (typeof child === 'string' || typeof child === 'number') {\n message += String(child)\n } else if (isValidElement(child)) {\n if (child.type === Fragment) {\n const inner = extractMessage((child.props as { children?: ReactNode }).children)\n message += offsetIndices(inner.message, components.length)\n components.push(...inner.components)\n return\n }\n\n const idx = components.length\n const inner = extractMessage((child.props as { children?: ReactNode }).children)\n components.push(child)\n components.push(...inner.components)\n message += `<${idx}>${offsetIndices(inner.message, idx + 1)}</${idx}>`\n }\n })\n\n return { message, components }\n}\n\nexport function offsetIndices(message: string, offset: number): string {\n if (offset === 0) return message\n return message\n .replace(/<(\\d+)(\\/?>)/g, (_match, index: string, suffix: string) => `<${Number(index) + offset}${suffix}`)\n .replace(/<\\/(\\d+)>/g, (_match, index: string) => `</${Number(index) + offset}>`)\n}\n\n/**\n * Reconstruct a translated message string back into React elements.\n *\n * Parses \"<0>content</0>\" tags and replaces them with cloned components.\n *\n * @internal\n */\nexport function reconstruct(\n translated: string,\n components: ReactElement[],\n): ReactNode {\n const TAG_RE = /<(\\d+)>([\\s\\S]*?)<\\/\\1>/g\n const result: ReactNode[] = []\n let lastIndex = 0\n let match: RegExpExecArray | null\n\n TAG_RE.lastIndex = 0\n match = TAG_RE.exec(translated)\n\n while (match !== null) {\n if (match.index > lastIndex) {\n result.push(translated.slice(lastIndex, match.index))\n }\n\n const idx = Number(match[1])\n const innerText = match[2]!\n const component = components[idx]\n\n if (component) {\n // Recursively reconstruct inner content\n const innerContent = reconstruct(innerText, components)\n result.push(\n cloneElement(component, { key: `trans-${idx}` }, innerContent),\n )\n } else {\n result.push(innerText)\n }\n\n lastIndex = TAG_RE.lastIndex\n match = TAG_RE.exec(translated)\n }\n\n if (lastIndex < translated.length) {\n result.push(translated.slice(lastIndex))\n }\n\n return result.length === 1 ? result[0]! : createElement(Fragment, null, ...result)\n}\n","import type { ReactNode } from 'react'\n\nexport const PLURAL_CATEGORIES = ['zero', 'one', 'two', 'few', 'many', 'other'] as const\nexport type PluralCategory = (typeof PLURAL_CATEGORIES)[number]\n\n/**\n * Resolve which plural category to use.\n * Checks for exact =0 match first, then falls back to CLDR rules.\n * @internal\n */\nexport function resolveCategory(\n value: number,\n locale: string,\n available: Record<string, boolean>,\n): PluralCategory {\n if (value === 0 && available['zero']) return 'zero'\n const cldr = new Intl.PluralRules(locale).select(value) as PluralCategory\n if (available[cldr]) return cldr\n return 'other'\n}\n\n/**\n * Replace `#` with the formatted value in a ReactNode.\n * @internal\n */\nexport function replaceHash(node: ReactNode, formatted: string): ReactNode {\n if (typeof node === 'string') {\n return node.replace(/#/g, formatted)\n }\n return node\n}\n","import type { MessageDescriptor } from '@fluenti/core'\nimport type { ReactElement, ReactNode } from 'react'\nimport { extractMessage, offsetIndices, reconstruct } from './trans-core'\nimport { PLURAL_CATEGORIES, type PluralCategory } from './plural-core'\n\nexport interface RichMessagePart {\n message: string\n components: ReactElement[]\n}\n\nexport function buildICUPluralMessage(\n forms: Partial<Record<PluralCategory, string>> & { other: string },\n offset?: number,\n): string {\n const parts: string[] = []\n for (const cat of PLURAL_CATEGORIES) {\n const text = forms[cat]\n if (text === undefined) continue\n parts.push(`${cat === 'zero' ? '=0' : cat} {${text}}`)\n }\n const offsetPrefix = offset ? `offset:${offset} ` : ''\n return `{count, plural, ${offsetPrefix}${parts.join(' ')}}`\n}\n\nexport function buildICUSelectMessage(\n forms: Record<string, string>,\n): string {\n const entries = Object.entries(forms).filter(([, text]) => text !== undefined)\n return `{value, select, ${entries.map(([key, text]) => `${key} {${text}}`).join(' ')}}`\n}\n\nexport function normalizeSelectForms(\n forms: Record<string, string>,\n): {\n forms: Record<string, string>\n valueMap: Record<string, string>\n} {\n const normalized: Record<string, string> = {}\n const valueMap: Record<string, string> = {}\n let index = 0\n\n for (const [key, text] of Object.entries(forms)) {\n if (key === 'other') {\n normalized['other'] = text\n continue\n }\n\n const safeKey = /^[A-Za-z0-9_]+$/.test(key) ? key : `case_${index++}`\n normalized[safeKey] = text\n valueMap[key] = safeKey\n }\n\n if (normalized['other'] === undefined) {\n normalized['other'] = ''\n }\n\n return { forms: normalized, valueMap }\n}\n\nexport function serializeRichNode(node: ReactNode): RichMessagePart {\n const extracted = extractMessage(node)\n return {\n message: extracted.message,\n components: extracted.components,\n }\n}\n\nexport function serializeRichForms<T extends string>(\n keys: readonly T[],\n forms: Partial<Record<T, ReactNode>> & Record<string, ReactNode | undefined>,\n): {\n messages: Partial<Record<T, string>> & Record<string, string>\n components: ReactElement[]\n} {\n const components: ReactElement[] = []\n const messages: Record<string, string> = {}\n\n for (const key of keys) {\n const value = forms[key]\n if (value === undefined) continue\n const extracted = serializeRichNode(value)\n messages[key] = offsetIndices(extracted.message, components.length)\n components.push(...extracted.components)\n }\n\n for (const [key, value] of Object.entries(forms)) {\n if (keys.includes(key as T) || value === undefined) continue\n const extracted = serializeRichNode(value)\n messages[key] = offsetIndices(extracted.message, components.length)\n components.push(...extracted.components)\n }\n\n return { messages: messages as Partial<Record<T, string>> & Record<string, string>, components }\n}\n\nexport function renderRichTranslation(\n descriptor: MessageDescriptor,\n values: Record<string, unknown> | undefined,\n translate: (descriptor: MessageDescriptor, values?: Record<string, unknown>) => string,\n components: ReactElement[],\n): ReactNode {\n const translated = translate(descriptor, values)\n return components.length > 0 ? reconstruct(translated, components) : translated\n}\n"],"mappings":"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"}
|
|
@@ -1,2 +0,0 @@
|
|
|
1
|
-
let e=require(`react`);require(`@fluenti/core`);function t(r){let i=[],a=``;return e.Children.forEach(r,r=>{if(typeof r==`string`||typeof r==`number`)a+=String(r);else if((0,e.isValidElement)(r)){if(r.type===e.Fragment){let e=t(r.props.children);a+=n(e.message,i.length),i.push(...e.components);return}let o=i.length,s=t(r.props.children);i.push(r),i.push(...s.components),a+=`<${o}>${n(s.message,o+1)}</${o}>`}}),{message:a,components:i}}function n(e,t){return t===0?e:e.replace(/<(\d+)(\/?>)/g,(e,n,r)=>`<${Number(n)+t}${r}`).replace(/<\/(\d+)>/g,(e,n)=>`</${Number(n)+t}>`)}function r(t,n){let i=/<(\d+)>([\s\S]*?)<\/\1>/g,a=[],o=0,s;for(i.lastIndex=0,s=i.exec(t);s!==null;){s.index>o&&a.push(t.slice(o,s.index));let c=Number(s[1]),l=s[2],u=n[c];if(u){let t=r(l,n);a.push((0,e.cloneElement)(u,{key:`trans-${c}`},t))}else a.push(l);o=i.lastIndex,s=i.exec(t)}return o<t.length&&a.push(t.slice(o)),a.length===1?a[0]:(0,e.createElement)(e.Fragment,null,...a)}var i=[`zero`,`one`,`two`,`few`,`many`,`other`];function a(e,t){let n=[];for(let t of i){let r=e[t];r!==void 0&&n.push(`${t===`zero`?`=0`:t} {${r}}`)}return`{count, plural, ${t?`offset:${t} `:``}${n.join(` `)}}`}function o(e){return`{value, select, ${Object.entries(e).filter(([,e])=>e!==void 0).map(([e,t])=>`${e} {${t}}`).join(` `)}}`}function s(e){let t={},n={},r=0;for(let[i,a]of Object.entries(e)){if(i===`other`){t.other=a;continue}let e=/^[A-Za-z0-9_]+$/.test(i)?i:`case_${r++}`;t[e]=a,n[i]=e}return t.other===void 0&&(t.other=``),{forms:t,valueMap:n}}function c(e){let n=t(e);return{message:n.message,components:n.components}}function l(e,t){let r=[],i={};for(let a of e){let e=t[a];if(e===void 0)continue;let o=c(e);i[a]=n(o.message,r.length),r.push(...o.components)}for(let[a,o]of Object.entries(t)){if(e.includes(a)||o===void 0)continue;let t=c(o);i[a]=n(t.message,r.length),r.push(...t.components)}return{messages:i,components:r}}function u(e,t,n,i){let a=n(e,t);return i.length>0?r(a,i):a}Object.defineProperty(exports,`a`,{enumerable:!0,get:function(){return l}}),Object.defineProperty(exports,`c`,{enumerable:!0,get:function(){return r}}),Object.defineProperty(exports,`i`,{enumerable:!0,get:function(){return u}}),Object.defineProperty(exports,`n`,{enumerable:!0,get:function(){return o}}),Object.defineProperty(exports,`o`,{enumerable:!0,get:function(){return i}}),Object.defineProperty(exports,`r`,{enumerable:!0,get:function(){return s}}),Object.defineProperty(exports,`s`,{enumerable:!0,get:function(){return t}}),Object.defineProperty(exports,`t`,{enumerable:!0,get:function(){return a}});
|
|
2
|
-
//# sourceMappingURL=icu-rich-XY1SdM5K.cjs.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"icu-rich-XY1SdM5K.cjs","names":[],"sources":["../src/components/trans-core.ts","../src/components/plural-core.ts","../src/components/icu-rich.tsx"],"sourcesContent":["import {\n Children,\n isValidElement,\n cloneElement,\n createElement,\n Fragment,\n type ReactNode,\n type ReactElement,\n} from 'react'\nimport { hashMessage } from '@fluenti/core'\n\nexport { hashMessage }\n\n/**\n * Extract a message string and component list from React children.\n *\n * Converts:\n * <Trans>Hello <b>{name}</b>, welcome!</Trans>\n * Into:\n * message: \"Hello <0>{name}</0>, welcome!\"\n * components: [<b>{name}</b>]\n *\n * @internal\n */\nexport function extractMessage(children: ReactNode): {\n message: string\n components: ReactElement[]\n} {\n const components: ReactElement[] = []\n let message = ''\n\n Children.forEach(children, (child) => {\n if (typeof child === 'string' || typeof child === 'number') {\n message += String(child)\n } else if (isValidElement(child)) {\n if (child.type === Fragment) {\n const inner = extractMessage((child.props as { children?: ReactNode }).children)\n message += offsetIndices(inner.message, components.length)\n components.push(...inner.components)\n return\n }\n\n const idx = components.length\n const inner = extractMessage((child.props as { children?: ReactNode }).children)\n components.push(child)\n components.push(...inner.components)\n message += `<${idx}>${offsetIndices(inner.message, idx + 1)}</${idx}>`\n }\n })\n\n return { message, components }\n}\n\nexport function offsetIndices(message: string, offset: number): string {\n if (offset === 0) return message\n return message\n .replace(/<(\\d+)(\\/?>)/g, (_match, index: string, suffix: string) => `<${Number(index) + offset}${suffix}`)\n .replace(/<\\/(\\d+)>/g, (_match, index: string) => `</${Number(index) + offset}>`)\n}\n\n/**\n * Reconstruct a translated message string back into React elements.\n *\n * Parses \"<0>content</0>\" tags and replaces them with cloned components.\n *\n * @internal\n */\nexport function reconstruct(\n translated: string,\n components: ReactElement[],\n): ReactNode {\n const TAG_RE = /<(\\d+)>([\\s\\S]*?)<\\/\\1>/g\n const result: ReactNode[] = []\n let lastIndex = 0\n let match: RegExpExecArray | null\n\n TAG_RE.lastIndex = 0\n match = TAG_RE.exec(translated)\n\n while (match !== null) {\n if (match.index > lastIndex) {\n result.push(translated.slice(lastIndex, match.index))\n }\n\n const idx = Number(match[1])\n const innerText = match[2]!\n const component = components[idx]\n\n if (component) {\n // Recursively reconstruct inner content\n const innerContent = reconstruct(innerText, components)\n result.push(\n cloneElement(component, { key: `trans-${idx}` }, innerContent),\n )\n } else {\n result.push(innerText)\n }\n\n lastIndex = TAG_RE.lastIndex\n match = TAG_RE.exec(translated)\n }\n\n if (lastIndex < translated.length) {\n result.push(translated.slice(lastIndex))\n }\n\n return result.length === 1 ? result[0]! : createElement(Fragment, null, ...result)\n}\n","import type { ReactNode } from 'react'\n\nexport const PLURAL_CATEGORIES = ['zero', 'one', 'two', 'few', 'many', 'other'] as const\nexport type PluralCategory = (typeof PLURAL_CATEGORIES)[number]\n\n/**\n * Resolve which plural category to use.\n * Checks for exact =0 match first, then falls back to CLDR rules.\n * @internal\n */\nexport function resolveCategory(\n value: number,\n locale: string,\n available: Record<string, boolean>,\n): PluralCategory {\n if (value === 0 && available['zero']) return 'zero'\n const cldr = new Intl.PluralRules(locale).select(value) as PluralCategory\n if (available[cldr]) return cldr\n return 'other'\n}\n\n/**\n * Replace `#` with the formatted value in a ReactNode.\n * @internal\n */\nexport function replaceHash(node: ReactNode, formatted: string): ReactNode {\n if (typeof node === 'string') {\n return node.replace(/#/g, formatted)\n }\n return node\n}\n","import type { MessageDescriptor } from '@fluenti/core'\nimport type { ReactElement, ReactNode } from 'react'\nimport { extractMessage, offsetIndices, reconstruct } from './trans-core'\nimport { PLURAL_CATEGORIES, type PluralCategory } from './plural-core'\n\nexport interface RichMessagePart {\n message: string\n components: ReactElement[]\n}\n\nexport function buildICUPluralMessage(\n forms: Partial<Record<PluralCategory, string>> & { other: string },\n offset?: number,\n): string {\n const parts: string[] = []\n for (const cat of PLURAL_CATEGORIES) {\n const text = forms[cat]\n if (text === undefined) continue\n parts.push(`${cat === 'zero' ? '=0' : cat} {${text}}`)\n }\n const offsetPrefix = offset ? `offset:${offset} ` : ''\n return `{count, plural, ${offsetPrefix}${parts.join(' ')}}`\n}\n\nexport function buildICUSelectMessage(\n forms: Record<string, string>,\n): string {\n const entries = Object.entries(forms).filter(([, text]) => text !== undefined)\n return `{value, select, ${entries.map(([key, text]) => `${key} {${text}}`).join(' ')}}`\n}\n\nexport function normalizeSelectForms(\n forms: Record<string, string>,\n): {\n forms: Record<string, string>\n valueMap: Record<string, string>\n} {\n const normalized: Record<string, string> = {}\n const valueMap: Record<string, string> = {}\n let index = 0\n\n for (const [key, text] of Object.entries(forms)) {\n if (key === 'other') {\n normalized['other'] = text\n continue\n }\n\n const safeKey = /^[A-Za-z0-9_]+$/.test(key) ? key : `case_${index++}`\n normalized[safeKey] = text\n valueMap[key] = safeKey\n }\n\n if (normalized['other'] === undefined) {\n normalized['other'] = ''\n }\n\n return { forms: normalized, valueMap }\n}\n\nexport function serializeRichNode(node: ReactNode): RichMessagePart {\n const extracted = extractMessage(node)\n return {\n message: extracted.message,\n components: extracted.components,\n }\n}\n\nexport function serializeRichForms<T extends string>(\n keys: readonly T[],\n forms: Partial<Record<T, ReactNode>> & Record<string, ReactNode | undefined>,\n): {\n messages: Partial<Record<T, string>> & Record<string, string>\n components: ReactElement[]\n} {\n const components: ReactElement[] = []\n const messages: Record<string, string> = {}\n\n for (const key of keys) {\n const value = forms[key]\n if (value === undefined) continue\n const extracted = serializeRichNode(value)\n messages[key] = offsetIndices(extracted.message, components.length)\n components.push(...extracted.components)\n }\n\n for (const [key, value] of Object.entries(forms)) {\n if (keys.includes(key as T) || value === undefined) continue\n const extracted = serializeRichNode(value)\n messages[key] = offsetIndices(extracted.message, components.length)\n components.push(...extracted.components)\n }\n\n return { messages: messages as Partial<Record<T, string>> & Record<string, string>, components }\n}\n\nexport function renderRichTranslation(\n descriptor: MessageDescriptor,\n values: Record<string, unknown> | undefined,\n translate: (descriptor: MessageDescriptor, values?: Record<string, unknown>) => string,\n components: ReactElement[],\n): ReactNode {\n const translated = translate(descriptor, values)\n return components.length > 0 ? reconstruct(translated, components) : translated\n}\n"],"mappings":"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"}
|