decantr 0.9.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/AGENTS.md +868 -0
- package/CHANGELOG.md +255 -0
- package/CLAUDE.md +178 -0
- package/LICENSE +21 -0
- package/README.md +229 -0
- package/cli/art.js +127 -0
- package/cli/commands/a11y.js +61 -0
- package/cli/commands/audit.js +225 -0
- package/cli/commands/build.js +38 -0
- package/cli/commands/dev.js +18 -0
- package/cli/commands/doctor.js +197 -0
- package/cli/commands/figma-sync.js +48 -0
- package/cli/commands/figma-tokens.js +55 -0
- package/cli/commands/generate.js +26 -0
- package/cli/commands/init.js +116 -0
- package/cli/commands/lint.js +209 -0
- package/cli/commands/mcp.js +530 -0
- package/cli/commands/migrate.js +175 -0
- package/cli/commands/test.js +38 -0
- package/cli/commands/validate.js +354 -0
- package/cli/index.js +113 -0
- package/package.json +95 -0
- package/reference/atoms.md +517 -0
- package/reference/behaviors.md +384 -0
- package/reference/build-tooling.md +275 -0
- package/reference/color-guidelines.md +965 -0
- package/reference/component-lifecycle.md +137 -0
- package/reference/compound-spacing.md +95 -0
- package/reference/decantation-process.md +499 -0
- package/reference/dev-server-routes.md +93 -0
- package/reference/form-system.md +253 -0
- package/reference/i18n.md +336 -0
- package/reference/icons.md +576 -0
- package/reference/llm-primer.md +953 -0
- package/reference/plugins.md +252 -0
- package/reference/registry-consumption.md +76 -0
- package/reference/router.md +217 -0
- package/reference/shells.md +116 -0
- package/reference/spatial-guidelines.md +541 -0
- package/reference/ssr.md +234 -0
- package/reference/state-data.md +215 -0
- package/reference/state-patterns.md +166 -0
- package/reference/state.md +194 -0
- package/reference/style-system.md +110 -0
- package/reference/tokens.md +460 -0
- package/src/app.js +19 -0
- package/src/chart/_animate.js +266 -0
- package/src/chart/_base.js +109 -0
- package/src/chart/_data.js +209 -0
- package/src/chart/_format.js +106 -0
- package/src/chart/_interact.js +364 -0
- package/src/chart/_palette.js +105 -0
- package/src/chart/_renderer.js +52 -0
- package/src/chart/_scene.js +262 -0
- package/src/chart/_shared.js +371 -0
- package/src/chart/index.js +637 -0
- package/src/chart/layouts/_layout-base.js +328 -0
- package/src/chart/layouts/cartesian.js +148 -0
- package/src/chart/layouts/hierarchy.js +562 -0
- package/src/chart/layouts/polar.js +101 -0
- package/src/chart/renderers/canvas.js +179 -0
- package/src/chart/renderers/svg.js +256 -0
- package/src/chart/renderers/webgpu.js +715 -0
- package/src/chart/types/_type-base.js +26 -0
- package/src/chart/types/area.js +134 -0
- package/src/chart/types/bar.js +173 -0
- package/src/chart/types/box-plot.js +125 -0
- package/src/chart/types/bubble.js +63 -0
- package/src/chart/types/candlestick.js +115 -0
- package/src/chart/types/chord.js +85 -0
- package/src/chart/types/combination.js +108 -0
- package/src/chart/types/funnel.js +68 -0
- package/src/chart/types/gauge.js +163 -0
- package/src/chart/types/heatmap.js +98 -0
- package/src/chart/types/histogram.js +71 -0
- package/src/chart/types/line.js +111 -0
- package/src/chart/types/org-chart.js +93 -0
- package/src/chart/types/pie.js +81 -0
- package/src/chart/types/radar.js +96 -0
- package/src/chart/types/radial.js +68 -0
- package/src/chart/types/range-area.js +55 -0
- package/src/chart/types/range-bar.js +61 -0
- package/src/chart/types/sankey.js +73 -0
- package/src/chart/types/scatter.js +66 -0
- package/src/chart/types/sparkline.js +81 -0
- package/src/chart/types/sunburst.js +69 -0
- package/src/chart/types/swimlane.js +88 -0
- package/src/chart/types/treemap.js +62 -0
- package/src/chart/types/waterfall.js +100 -0
- package/src/components/_base.js +1658 -0
- package/src/components/_behaviors.js +1140 -0
- package/src/components/_primitives.js +534 -0
- package/src/components/_qr-encoder.js +539 -0
- package/src/components/accordion.js +207 -0
- package/src/components/affix.js +62 -0
- package/src/components/alert-dialog.js +75 -0
- package/src/components/alert.js +47 -0
- package/src/components/aspect-ratio.js +24 -0
- package/src/components/avatar-group.js +55 -0
- package/src/components/avatar.js +38 -0
- package/src/components/back-top.js +75 -0
- package/src/components/badge.js +74 -0
- package/src/components/banner.js +68 -0
- package/src/components/breadcrumb.js +162 -0
- package/src/components/button.js +115 -0
- package/src/components/calendar.js +131 -0
- package/src/components/card.js +192 -0
- package/src/components/carousel.js +98 -0
- package/src/components/cascader.js +261 -0
- package/src/components/checkbox.js +80 -0
- package/src/components/chip.js +81 -0
- package/src/components/code-block.js +82 -0
- package/src/components/collapsible.js +50 -0
- package/src/components/color-palette.js +438 -0
- package/src/components/color-picker.js +314 -0
- package/src/components/combobox.js +181 -0
- package/src/components/command.js +174 -0
- package/src/components/comment.js +206 -0
- package/src/components/context-menu.js +76 -0
- package/src/components/data-table.js +724 -0
- package/src/components/date-picker.js +217 -0
- package/src/components/date-range-picker.js +244 -0
- package/src/components/datetime-picker.js +271 -0
- package/src/components/descriptions.js +68 -0
- package/src/components/drawer.js +179 -0
- package/src/components/dropdown.js +88 -0
- package/src/components/empty.js +41 -0
- package/src/components/float-button.js +90 -0
- package/src/components/form.js +106 -0
- package/src/components/hover-card.js +49 -0
- package/src/components/icon.js +87 -0
- package/src/components/image.js +97 -0
- package/src/components/index.js +117 -0
- package/src/components/input-group.js +75 -0
- package/src/components/input-number.js +155 -0
- package/src/components/input-otp.js +178 -0
- package/src/components/input.js +91 -0
- package/src/components/kbd.js +36 -0
- package/src/components/label.js +25 -0
- package/src/components/list.js +118 -0
- package/src/components/masked-input.js +236 -0
- package/src/components/mentions.js +165 -0
- package/src/components/menu.js +259 -0
- package/src/components/message.js +80 -0
- package/src/components/modal.js +147 -0
- package/src/components/navigation-menu.js +166 -0
- package/src/components/notification.js +84 -0
- package/src/components/pagination.js +104 -0
- package/src/components/placeholder.js +132 -0
- package/src/components/popconfirm.js +70 -0
- package/src/components/popover.js +58 -0
- package/src/components/progress.js +61 -0
- package/src/components/qrcode.js +251 -0
- package/src/components/radiogroup.js +120 -0
- package/src/components/range-slider.js +176 -0
- package/src/components/rate.js +186 -0
- package/src/components/resizable.js +83 -0
- package/src/components/result.js +57 -0
- package/src/components/scroll-area.js +43 -0
- package/src/components/segmented.js +97 -0
- package/src/components/select.js +165 -0
- package/src/components/separator.js +31 -0
- package/src/components/shell.js +407 -0
- package/src/components/skeleton.js +39 -0
- package/src/components/slider.js +141 -0
- package/src/components/sortable-list.js +176 -0
- package/src/components/space.js +42 -0
- package/src/components/spinner.js +112 -0
- package/src/components/splitter.js +147 -0
- package/src/components/statistic.js +136 -0
- package/src/components/steps.js +99 -0
- package/src/components/switch.js +95 -0
- package/src/components/table.js +44 -0
- package/src/components/tabs.js +216 -0
- package/src/components/tag.js +115 -0
- package/src/components/textarea.js +82 -0
- package/src/components/time-picker.js +153 -0
- package/src/components/time-range-picker.js +170 -0
- package/src/components/timeline.js +226 -0
- package/src/components/toast.js +71 -0
- package/src/components/toggle.js +213 -0
- package/src/components/tooltip.js +57 -0
- package/src/components/tour.js +159 -0
- package/src/components/transfer.js +163 -0
- package/src/components/tree-select.js +274 -0
- package/src/components/tree.js +141 -0
- package/src/components/typography.js +136 -0
- package/src/components/upload.js +118 -0
- package/src/components/visually-hidden.js +20 -0
- package/src/components/watermark.js +124 -0
- package/src/core/index.js +539 -0
- package/src/core/lifecycle.js +69 -0
- package/src/css/atoms.js +651 -0
- package/src/css/components.js +940 -0
- package/src/css/derive.js +1296 -0
- package/src/css/index.js +265 -0
- package/src/css/runtime.js +268 -0
- package/src/css/styles/addons/bioluminescent.js +93 -0
- package/src/css/styles/addons/clay.js +70 -0
- package/src/css/styles/addons/clean.js +57 -0
- package/src/css/styles/addons/command-center.js +143 -0
- package/src/css/styles/addons/dopamine.js +83 -0
- package/src/css/styles/addons/editorial.js +80 -0
- package/src/css/styles/addons/glassmorphism.js +99 -0
- package/src/css/styles/addons/liquid-glass.js +105 -0
- package/src/css/styles/addons/prismatic.js +100 -0
- package/src/css/styles/addons/retro.js +63 -0
- package/src/css/styles/auradecantism.js +96 -0
- package/src/css/theme-registry.js +444 -0
- package/src/data/entity.js +281 -0
- package/src/data/index.js +13 -0
- package/src/data/persist.js +225 -0
- package/src/data/query.js +839 -0
- package/src/data/realtime.js +299 -0
- package/src/data/url.js +177 -0
- package/src/data/worker.js +134 -0
- package/src/explorer/archetypes.js +243 -0
- package/src/explorer/atoms.js +228 -0
- package/src/explorer/charts.js +497 -0
- package/src/explorer/components.js +129 -0
- package/src/explorer/foundations.js +949 -0
- package/src/explorer/icons.js +178 -0
- package/src/explorer/patterns.js +247 -0
- package/src/explorer/recipes.js +194 -0
- package/src/explorer/shared/pattern-examples.js +1337 -0
- package/src/explorer/shared/showcase-renderer.js +958 -0
- package/src/explorer/shared/spec-table.js +41 -0
- package/src/explorer/shared/usage-links.js +87 -0
- package/src/explorer/shell-config.js +10 -0
- package/src/explorer/shells.js +551 -0
- package/src/explorer/styles.js +161 -0
- package/src/explorer/tokens.js +262 -0
- package/src/explorer/tools.js +525 -0
- package/src/form/index.js +804 -0
- package/src/i18n/index.js +251 -0
- package/src/icons/essential.js +479 -0
- package/src/icons/index.js +53 -0
- package/src/plugins/index.js +282 -0
- package/src/registry/archetypes/content-site.json +71 -0
- package/src/registry/archetypes/docs-explorer.json +23 -0
- package/src/registry/archetypes/ecommerce.json +104 -0
- package/src/registry/archetypes/financial-dashboard.json +77 -0
- package/src/registry/archetypes/index.json +41 -0
- package/src/registry/archetypes/portfolio.json +82 -0
- package/src/registry/archetypes/recipe-community.json +159 -0
- package/src/registry/archetypes/saas-dashboard.json +86 -0
- package/src/registry/architect/cross-cutting.json +45 -0
- package/src/registry/architect/domains/ecommerce.json +294 -0
- package/src/registry/architect/domains/financial-services.json +302 -0
- package/src/registry/architect/index.json +26 -0
- package/src/registry/architect/traits.json +379 -0
- package/src/registry/atoms.json +16 -0
- package/src/registry/chart-showcase.json +160 -0
- package/src/registry/chart.json +136 -0
- package/src/registry/components.json +8616 -0
- package/src/registry/core.json +216 -0
- package/src/registry/css.json +319 -0
- package/src/registry/data.json +135 -0
- package/src/registry/foundations.json +11 -0
- package/src/registry/icons.json +463 -0
- package/src/registry/index.json +101 -0
- package/src/registry/patterns/activity-feed.json +37 -0
- package/src/registry/patterns/article-content.json +27 -0
- package/src/registry/patterns/auth-form.json +37 -0
- package/src/registry/patterns/author-card.json +20 -0
- package/src/registry/patterns/card-grid.json +127 -0
- package/src/registry/patterns/category-nav.json +26 -0
- package/src/registry/patterns/chart-grid.json +36 -0
- package/src/registry/patterns/chat-interface.json +37 -0
- package/src/registry/patterns/checklist-card.json +55 -0
- package/src/registry/patterns/comparison-panel.json +27 -0
- package/src/registry/patterns/component-showcase.json +24 -0
- package/src/registry/patterns/contact-form.json +31 -0
- package/src/registry/patterns/cta-section.json +20 -0
- package/src/registry/patterns/data-table.json +37 -0
- package/src/registry/patterns/detail-header.json +83 -0
- package/src/registry/patterns/detail-panel.json +27 -0
- package/src/registry/patterns/explorer-shell.json +22 -0
- package/src/registry/patterns/filter-bar.json +33 -0
- package/src/registry/patterns/filter-sidebar.json +27 -0
- package/src/registry/patterns/form-sections.json +110 -0
- package/src/registry/patterns/goal-tracker.json +27 -0
- package/src/registry/patterns/hero.json +107 -0
- package/src/registry/patterns/index.json +47 -0
- package/src/registry/patterns/kpi-grid.json +36 -0
- package/src/registry/patterns/media-gallery.json +20 -0
- package/src/registry/patterns/order-history.json +20 -0
- package/src/registry/patterns/pagination.json +19 -0
- package/src/registry/patterns/photo-to-recipe.json +36 -0
- package/src/registry/patterns/pipeline-tracker.json +28 -0
- package/src/registry/patterns/post-list.json +27 -0
- package/src/registry/patterns/pricing-table.json +32 -0
- package/src/registry/patterns/scorecard.json +28 -0
- package/src/registry/patterns/search-bar.json +20 -0
- package/src/registry/patterns/specimen-grid.json +19 -0
- package/src/registry/patterns/stat-card.json +55 -0
- package/src/registry/patterns/stats-bar.json +55 -0
- package/src/registry/patterns/steps-card.json +55 -0
- package/src/registry/patterns/table-of-contents.json +19 -0
- package/src/registry/patterns/testimonials.json +21 -0
- package/src/registry/patterns/timeline.json +27 -0
- package/src/registry/patterns/token-inspector.json +21 -0
- package/src/registry/patterns/wizard.json +27 -0
- package/src/registry/recipe-auradecantism.json +69 -0
- package/src/registry/recipe-clean.json +65 -0
- package/src/registry/recipe-command-center.json +78 -0
- package/src/registry/router.json +73 -0
- package/src/registry/schema/README.md +197 -0
- package/src/registry/skeletons.json +259 -0
- package/src/registry/state.json +137 -0
- package/src/registry/tokens.json +40 -0
- package/src/router/hash.js +17 -0
- package/src/router/history.js +18 -0
- package/src/router/index.js +598 -0
- package/src/ssr/index.js +922 -0
- package/src/state/arrays.js +181 -0
- package/src/state/devtools.js +647 -0
- package/src/state/index.js +498 -0
- package/src/state/middleware.js +288 -0
- package/src/state/scheduler.js +206 -0
- package/src/state/store.js +300 -0
- package/src/tags/index.js +19 -0
- package/src/tannins/auth.js +396 -0
- package/src/test/dom.js +352 -0
- package/src/test/index.js +62 -0
- package/src/test/state.js +306 -0
- package/tools/a11y-audit.js +487 -0
- package/tools/analyzer.js +315 -0
- package/tools/audit.js +706 -0
- package/tools/builder.js +1422 -0
- package/tools/css-extract.js +188 -0
- package/tools/dev-server.js +316 -0
- package/tools/dts-gen.js +1260 -0
- package/tools/figma-components.js +329 -0
- package/tools/figma-patterns.js +516 -0
- package/tools/figma-plugin/code.js +453 -0
- package/tools/figma-plugin/manifest.json +14 -0
- package/tools/figma-plugin/ui.html +268 -0
- package/tools/figma-render.js +293 -0
- package/tools/figma-tokens.js +712 -0
- package/tools/figma-upload.js +318 -0
- package/tools/generate.js +738 -0
- package/tools/icons.js +133 -0
- package/tools/init-templates.js +265 -0
- package/tools/install-hooks.sh +5 -0
- package/tools/migrations/0.5.0.js +53 -0
- package/tools/migrations/0.6.0.js +95 -0
- package/tools/minify.js +170 -0
- package/tools/pre-commit +4 -0
- package/tools/registry.js +662 -0
- package/tools/reset-playground.js +61 -0
- package/tools/starter-templates/content-site/app.js +49 -0
- package/tools/starter-templates/content-site/essence.js +19 -0
- package/tools/starter-templates/content-site/pages.js +31 -0
- package/tools/starter-templates/ecommerce/app.js +50 -0
- package/tools/starter-templates/ecommerce/essence.js +19 -0
- package/tools/starter-templates/ecommerce/pages.js +31 -0
- package/tools/starter-templates/landing-page/app.js +38 -0
- package/tools/starter-templates/landing-page/essence.js +18 -0
- package/tools/starter-templates/landing-page/pages.js +21 -0
- package/tools/starter-templates/portfolio/app.js +45 -0
- package/tools/starter-templates/portfolio/essence.js +19 -0
- package/tools/starter-templates/portfolio/pages.js +33 -0
- package/tools/starter-templates/saas-dashboard/app.js +70 -0
- package/tools/starter-templates/saas-dashboard/essence.js +19 -0
- package/tools/starter-templates/saas-dashboard/pages.js +31 -0
- package/tools/verify-pack.js +203 -0
- package/types/chart.d.ts +77 -0
- package/types/components.d.ts +587 -0
- package/types/core.d.ts +89 -0
- package/types/css.d.ts +149 -0
- package/types/data.d.ts +238 -0
- package/types/form.d.ts +164 -0
- package/types/i18n.d.ts +51 -0
- package/types/icons.d.ts +27 -0
- package/types/index.d.ts +13 -0
- package/types/router.d.ts +116 -0
- package/types/ssr.d.ts +102 -0
- package/types/state.d.ts +83 -0
- package/types/tags.d.ts +62 -0
- package/types/tannins.d.ts +63 -0
- package/types/test.d.ts +48 -0
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
# Form System Reference
|
|
2
|
+
|
|
3
|
+
`import { createForm, validators, useFormField } from 'decantr/form';`
|
|
4
|
+
|
|
5
|
+
## createForm(config)
|
|
6
|
+
|
|
7
|
+
### Config Shape
|
|
8
|
+
|
|
9
|
+
| Key | Type | Default | Description |
|
|
10
|
+
|-----|------|---------|-------------|
|
|
11
|
+
| `fields` | `{ [name]: FieldConfig }` | `{}` | Field definitions |
|
|
12
|
+
| `onSubmit` | `async (values, form) => void` | — | Submit handler |
|
|
13
|
+
| `validate` | `(values) => { [name]: string[] }` | — | Cross-field validation |
|
|
14
|
+
| `validateOn` | `'onChange' \| 'onBlur' \| 'onSubmit'` | `'onBlur'` | Validation trigger mode |
|
|
15
|
+
|
|
16
|
+
### FieldConfig
|
|
17
|
+
|
|
18
|
+
| Key | Type | Default | Description |
|
|
19
|
+
|-----|------|---------|-------------|
|
|
20
|
+
| `value` | `any` | `''` | Initial value |
|
|
21
|
+
| `validators` | `Function[]` | `[]` | Array of validator functions |
|
|
22
|
+
| `transform` | `(raw) => transformed` | — | Transform applied on every `setValue` |
|
|
23
|
+
|
|
24
|
+
### FormInstance API
|
|
25
|
+
|
|
26
|
+
| Member | Type | Description |
|
|
27
|
+
|--------|------|-------------|
|
|
28
|
+
| `field(name)` | `(string) => FieldInstance` | Get/create field by name |
|
|
29
|
+
| `fields` | `Proxy` | Dot-access fields: `form.fields.email.value()` |
|
|
30
|
+
| `values()` | `() => Object` | Memo — all current field + fieldArray values |
|
|
31
|
+
| `errors()` | `() => { [name]: string[] }` | Memo — all fields with errors |
|
|
32
|
+
| `isValid()` | `() => boolean` | Memo — true when all fields have zero errors |
|
|
33
|
+
| `isDirty()` | `() => boolean` | Memo — true when any field differs from initial |
|
|
34
|
+
| `isSubmitting()` | `() => boolean` | Signal — true during `onSubmit` execution |
|
|
35
|
+
| `isSubmitted()` | `() => boolean` | Signal — true after successful submit |
|
|
36
|
+
| `submitCount()` | `() => number` | Signal — number of submit attempts |
|
|
37
|
+
| `submit()` | `() => Promise<void>` | Touch all fields, validate, call `onSubmit` if valid |
|
|
38
|
+
| `reset(values?)` | `(Object?) => void` | Reset all fields; optional new initial values |
|
|
39
|
+
| `setValues(partial)` | `(Object) => void` | Batch-set multiple field values |
|
|
40
|
+
| `setErrors(errs)` | `(Object) => void` | Batch-set errors: `{ name: string \| string[] }` |
|
|
41
|
+
| `validate()` | `() => Promise<boolean>` | Run all field + cross-field validators |
|
|
42
|
+
| `fieldArray(name)` | `(string) => FieldArrayInstance` | Get/create field array |
|
|
43
|
+
| `watch(name, cb)` | `(string, (val, prev) => void) => unsub` | Watch single field changes |
|
|
44
|
+
| `watchAll(cb)` | `((values) => void) => unsub` | Watch any field change |
|
|
45
|
+
|
|
46
|
+
### Submit Flow
|
|
47
|
+
|
|
48
|
+
1. Increment `submitCount`
|
|
49
|
+
2. Touch all fields (batch)
|
|
50
|
+
3. Run all field validators (sync + async)
|
|
51
|
+
4. Run `config.validate` cross-field validator (if provided)
|
|
52
|
+
5. If all valid, set `isSubmitting(true)`, call `onSubmit(values, form)`
|
|
53
|
+
6. On completion, set `isSubmitted(true)`, `isSubmitting(false)`
|
|
54
|
+
|
|
55
|
+
## FieldInstance API
|
|
56
|
+
|
|
57
|
+
Returned by `form.field(name)` or `form.fields.name`.
|
|
58
|
+
|
|
59
|
+
| Member | Type | Description |
|
|
60
|
+
|--------|------|-------------|
|
|
61
|
+
| `name` | `string` | Field name |
|
|
62
|
+
| `value()` | `() => any` | Signal — current value |
|
|
63
|
+
| `error()` | `() => string \| null` | Memo — first error or null |
|
|
64
|
+
| `errors()` | `() => string[]` | Signal — all error messages |
|
|
65
|
+
| `touched()` | `() => boolean` | Signal — true after blur |
|
|
66
|
+
| `dirty()` | `() => boolean` | Memo — true when value differs from initial |
|
|
67
|
+
| `valid()` | `() => boolean` | Memo — true when no errors |
|
|
68
|
+
| `setValue(v)` | `(any \| (prev) => any) => void` | Set value (accepts updater fn); triggers validation per `validateOn` |
|
|
69
|
+
| `setTouched()` | `() => void` | Mark as touched; triggers validation if `validateOn: 'onBlur'` |
|
|
70
|
+
| `setError(msg)` | `(string) => void` | Set a single error manually |
|
|
71
|
+
| `reset(val?)` | `(any?) => void` | Reset to initial (or provided) value, clear errors/touched |
|
|
72
|
+
| `validate()` | `() => Promise<boolean>` | Run all validators imperatively |
|
|
73
|
+
| `bind()` | `() => BindProps` | Returns props object for component binding |
|
|
74
|
+
|
|
75
|
+
### bind() Return Shape
|
|
76
|
+
|
|
77
|
+
```
|
|
78
|
+
{ value, onchange, onblur, error }
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
- `value` — signal getter
|
|
82
|
+
- `onchange` — auto-extracts `e.target.value` from DOM events or accepts raw values
|
|
83
|
+
- `onblur` — calls `setTouched()`
|
|
84
|
+
- `error` — signal getter (first error or null)
|
|
85
|
+
|
|
86
|
+
Spread into any form component: `Input({ ...form.field('email').bind() })`
|
|
87
|
+
|
|
88
|
+
## validators
|
|
89
|
+
|
|
90
|
+
All return `(value, allValues) => string|null`. Custom message via last `msg` param.
|
|
91
|
+
|
|
92
|
+
| Validator | Signature | Default Message |
|
|
93
|
+
|-----------|-----------|-----------------|
|
|
94
|
+
| `required` | `(msg?)` | `'Required'` |
|
|
95
|
+
| `minLength` | `(n, msg?)` | `'Must be at least {n} characters'` |
|
|
96
|
+
| `maxLength` | `(n, msg?)` | `'Must be at most {n} characters'` |
|
|
97
|
+
| `min` | `(n, msg?)` | `'Must be at least {n}'` |
|
|
98
|
+
| `max` | `(n, msg?)` | `'Must be at most {n}'` |
|
|
99
|
+
| `pattern` | `(regex, msg?)` | `'Invalid format'` |
|
|
100
|
+
| `email` | `(msg?)` | `'Invalid email address'` |
|
|
101
|
+
| `match` | `(fieldName, msg?)` | `'Must match {fieldName}'` |
|
|
102
|
+
| `custom` | `(fn, msg?)` | `'Invalid'` |
|
|
103
|
+
| `async` | `(fn, msg?)` | `'Invalid'` |
|
|
104
|
+
|
|
105
|
+
### Validator behavior
|
|
106
|
+
|
|
107
|
+
- **Sync validators** run immediately on trigger (`onChange`/`onBlur`/`onSubmit`)
|
|
108
|
+
- **Async validators** (`validators.async()`) auto-debounce at 300ms; only run if all sync validators pass
|
|
109
|
+
- `custom(fn)` — `fn(value, allValues)` returns `true` (valid) or error string
|
|
110
|
+
- `async(fn)` — `fn(value, allValues)` returns `Promise<true|string>`
|
|
111
|
+
- `match(fieldName)` — cross-field equality check via `allValues[fieldName]`
|
|
112
|
+
|
|
113
|
+
## useFormField(form, name)
|
|
114
|
+
|
|
115
|
+
Convenience hook — delegates to `form.field(name)`.
|
|
116
|
+
|
|
117
|
+
| Key | Type | Source |
|
|
118
|
+
|-----|------|--------|
|
|
119
|
+
| `value` | `() => any` | `field.value` |
|
|
120
|
+
| `setValue` | `(any) => void` | `field.setValue` |
|
|
121
|
+
| `error` | `() => string \| null` | `field.error` |
|
|
122
|
+
| `errors` | `() => string[]` | `field.errors` |
|
|
123
|
+
| `touched` | `() => boolean` | `field.touched` |
|
|
124
|
+
| `dirty` | `() => boolean` | `field.dirty` |
|
|
125
|
+
| `valid` | `() => boolean` | `field.valid` |
|
|
126
|
+
| `onBlur` | `() => void` | `field.setTouched` |
|
|
127
|
+
| `bind` | `() => BindProps` | `field.bind` |
|
|
128
|
+
|
|
129
|
+
## fieldArray(name)
|
|
130
|
+
|
|
131
|
+
Returned by `form.fieldArray(name)`. Initial value from `config.fields[name].value` (must be array).
|
|
132
|
+
|
|
133
|
+
| Member | Type | Description |
|
|
134
|
+
|--------|------|-------------|
|
|
135
|
+
| `items()` | `() => any[]` | Signal — current array |
|
|
136
|
+
| `length()` | `() => number` | Memo — item count |
|
|
137
|
+
| `append(value)` | `(any) => void` | Add to end |
|
|
138
|
+
| `prepend(value)` | `(any) => void` | Add to beginning |
|
|
139
|
+
| `remove(index)` | `(number) => void` | Remove at index |
|
|
140
|
+
| `move(from, to)` | `(number, number) => void` | Move item between indices |
|
|
141
|
+
| `swap(a, b)` | `(number, number) => void` | Swap two items |
|
|
142
|
+
| `replace(index, value)` | `(number, any) => void` | Replace item at index |
|
|
143
|
+
|
|
144
|
+
## Integration Example
|
|
145
|
+
|
|
146
|
+
```javascript
|
|
147
|
+
import { tags } from 'decantr/tags';
|
|
148
|
+
import { css } from 'decantr/css';
|
|
149
|
+
import { createForm, validators } from 'decantr/form';
|
|
150
|
+
import { Input, Button, Text } from 'decantr/components';
|
|
151
|
+
import { cond, text } from 'decantr/core';
|
|
152
|
+
|
|
153
|
+
const { form: formEl, div } = tags;
|
|
154
|
+
|
|
155
|
+
const form = createForm({
|
|
156
|
+
fields: {
|
|
157
|
+
email: { value: '', validators: [validators.required(), validators.email()] },
|
|
158
|
+
password: { value: '', validators: [validators.required(), validators.minLength(8)] },
|
|
159
|
+
},
|
|
160
|
+
validateOn: 'onBlur',
|
|
161
|
+
async onSubmit(values) {
|
|
162
|
+
await fetch('/api/login', { method: 'POST', body: JSON.stringify(values) });
|
|
163
|
+
},
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
formEl({ class: css('_flex _col _gap4 _mw[24rem]'), onsubmit: (e) => { e.preventDefault(); form.submit(); } },
|
|
167
|
+
div({ class: css('_flex _col _gap1') },
|
|
168
|
+
Input({ type: 'email', placeholder: 'Email', ...form.field('email').bind() }),
|
|
169
|
+
cond(() => form.fields.email.touched() && form.fields.email.error(),
|
|
170
|
+
() => Text({ class: css('_fgdestructive _textSm') }, text(() => form.fields.email.error()))
|
|
171
|
+
),
|
|
172
|
+
),
|
|
173
|
+
div({ class: css('_flex _col _gap1') },
|
|
174
|
+
Input({ type: 'password', placeholder: 'Password', ...form.field('password').bind() }),
|
|
175
|
+
cond(() => form.fields.password.touched() && form.fields.password.error(),
|
|
176
|
+
() => Text({ class: css('_fgdestructive _textSm') }, text(() => form.fields.password.error()))
|
|
177
|
+
),
|
|
178
|
+
),
|
|
179
|
+
Button({ type: 'submit', disabled: () => form.isSubmitting() }, 'Login'),
|
|
180
|
+
);
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
## Async Validation
|
|
184
|
+
|
|
185
|
+
```javascript
|
|
186
|
+
const form = createForm({
|
|
187
|
+
fields: {
|
|
188
|
+
username: {
|
|
189
|
+
value: '',
|
|
190
|
+
validators: [
|
|
191
|
+
validators.required(),
|
|
192
|
+
validators.minLength(3),
|
|
193
|
+
validators.async(async (value) => {
|
|
194
|
+
const res = await fetch(`/api/check-username?u=${value}`);
|
|
195
|
+
const { available } = await res.json();
|
|
196
|
+
return available ? true : 'Username already taken';
|
|
197
|
+
}),
|
|
198
|
+
],
|
|
199
|
+
},
|
|
200
|
+
},
|
|
201
|
+
});
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
Async validators auto-debounce (300ms). They only execute when all sync validators pass. Pending async validation does not block — errors update reactively when the promise resolves.
|
|
205
|
+
|
|
206
|
+
## Error Handling
|
|
207
|
+
|
|
208
|
+
### Server-side errors
|
|
209
|
+
|
|
210
|
+
```javascript
|
|
211
|
+
const form = createForm({
|
|
212
|
+
fields: { email: { value: '' }, password: { value: '' } },
|
|
213
|
+
async onSubmit(values, form) {
|
|
214
|
+
const res = await fetch('/api/login', { method: 'POST', body: JSON.stringify(values) });
|
|
215
|
+
if (!res.ok) {
|
|
216
|
+
const { errors } = await res.json();
|
|
217
|
+
form.setErrors(errors); // { email: 'Not found', password: 'Incorrect' }
|
|
218
|
+
}
|
|
219
|
+
},
|
|
220
|
+
});
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
### Cross-field validation
|
|
224
|
+
|
|
225
|
+
```javascript
|
|
226
|
+
const form = createForm({
|
|
227
|
+
fields: {
|
|
228
|
+
password: { value: '' },
|
|
229
|
+
confirm: { value: '' },
|
|
230
|
+
},
|
|
231
|
+
validate(values) {
|
|
232
|
+
const errs = {};
|
|
233
|
+
if (values.password !== values.confirm) {
|
|
234
|
+
errs.confirm = ['Passwords do not match'];
|
|
235
|
+
}
|
|
236
|
+
return errs;
|
|
237
|
+
},
|
|
238
|
+
});
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
Cross-field `validate` runs during `form.validate()` and `form.submit()`, after all per-field validators pass.
|
|
242
|
+
|
|
243
|
+
### Per-field imperative errors
|
|
244
|
+
|
|
245
|
+
```javascript
|
|
246
|
+
form.field('email').setError('This email is banned');
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
Cleared on next `setValue` or `reset`.
|
|
250
|
+
|
|
251
|
+
---
|
|
252
|
+
|
|
253
|
+
**See also:** `reference/component-lifecycle.md`, `reference/atoms.md`
|
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
# Internationalization (i18n)
|
|
2
|
+
|
|
3
|
+
Decantr's i18n module provides reactive locale management, translation with interpolation, pluralization via `Intl.PluralRules`, RTL/LTR direction control, and runtime message merging. Zero external dependencies.
|
|
4
|
+
|
|
5
|
+
## Quick Start
|
|
6
|
+
|
|
7
|
+
```js
|
|
8
|
+
import { createI18n } from 'decantr/i18n';
|
|
9
|
+
|
|
10
|
+
const { t, setLocale, setDirection } = createI18n({
|
|
11
|
+
locale: 'en',
|
|
12
|
+
fallbackLocale: 'en',
|
|
13
|
+
messages: {
|
|
14
|
+
en: {
|
|
15
|
+
nav: { home: 'Home', about: 'About' },
|
|
16
|
+
greeting: 'Hello, {name}!',
|
|
17
|
+
items_one: '{count} item',
|
|
18
|
+
items_other: '{count} items'
|
|
19
|
+
},
|
|
20
|
+
fr: {
|
|
21
|
+
nav: { home: 'Accueil', about: 'A propos' },
|
|
22
|
+
greeting: 'Bonjour, {name} !',
|
|
23
|
+
items_one: '{count} article',
|
|
24
|
+
items_other: '{count} articles'
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
t('nav.home'); // "Home"
|
|
30
|
+
t('greeting', { name: 'Alice' }); // "Hello, Alice!"
|
|
31
|
+
t('items', { count: 3 }); // "3 items"
|
|
32
|
+
|
|
33
|
+
setLocale('fr');
|
|
34
|
+
t('nav.home'); // "Accueil"
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## API
|
|
38
|
+
|
|
39
|
+
### `createI18n(config): I18nInstance`
|
|
40
|
+
|
|
41
|
+
Creates a reactive i18n instance.
|
|
42
|
+
|
|
43
|
+
**Config:**
|
|
44
|
+
|
|
45
|
+
| Field | Type | Required | Description |
|
|
46
|
+
|-------|------|----------|-------------|
|
|
47
|
+
| `locale` | `string` | Yes | Initial locale (e.g. `'en'`, `'fr'`, `'ar'`) |
|
|
48
|
+
| `messages` | `Record<string, Record<string, any>>` | Yes | Messages keyed by locale |
|
|
49
|
+
| `fallbackLocale` | `string` | No | Locale to use when key is missing in the active locale |
|
|
50
|
+
|
|
51
|
+
**Returns:**
|
|
52
|
+
|
|
53
|
+
| Method | Signature | Description |
|
|
54
|
+
|--------|-----------|-------------|
|
|
55
|
+
| `t` | `(key: string, params?: Record<string, any>) => string` | Translate a key |
|
|
56
|
+
| `locale` | `() => string` | Signal getter for current locale |
|
|
57
|
+
| `setLocale` | `(locale: string) => void` | Set the active locale |
|
|
58
|
+
| `setDirection` | `(dir: 'ltr' \| 'rtl') => void` | Set `dir` attribute on `<html>` |
|
|
59
|
+
| `addMessages` | `(locale: string, messages: Record<string, any>) => void` | Merge messages into a locale |
|
|
60
|
+
|
|
61
|
+
## Translation Keys
|
|
62
|
+
|
|
63
|
+
### Dot Notation
|
|
64
|
+
|
|
65
|
+
Keys use dot-delimited paths to navigate nested message objects:
|
|
66
|
+
|
|
67
|
+
```js
|
|
68
|
+
const messages = {
|
|
69
|
+
en: {
|
|
70
|
+
nav: {
|
|
71
|
+
home: 'Home',
|
|
72
|
+
settings: {
|
|
73
|
+
profile: 'Profile',
|
|
74
|
+
security: 'Security'
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
t('nav.home'); // "Home"
|
|
81
|
+
t('nav.settings.profile'); // "Profile"
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### Interpolation
|
|
85
|
+
|
|
86
|
+
Use `{param}` placeholders. Values are converted to strings automatically:
|
|
87
|
+
|
|
88
|
+
```js
|
|
89
|
+
const messages = {
|
|
90
|
+
en: {
|
|
91
|
+
welcome: 'Welcome, {name}!',
|
|
92
|
+
stats: '{count} views in {days} days'
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
t('welcome', { name: 'Alice' }); // "Welcome, Alice!"
|
|
97
|
+
t('stats', { count: 1200, days: 7 }); // "1200 views in 7 days"
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Unmatched placeholders are left as-is:
|
|
101
|
+
|
|
102
|
+
```js
|
|
103
|
+
t('welcome'); // "Welcome, {name}!"
|
|
104
|
+
t('welcome', {}); // "Welcome, {name}!"
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### Pluralization
|
|
108
|
+
|
|
109
|
+
Pass a `count` parameter to trigger pluralization. The system appends a suffix to the key using `Intl.PluralRules`:
|
|
110
|
+
|
|
111
|
+
| Suffix | English Example |
|
|
112
|
+
|--------|----------------|
|
|
113
|
+
| `_zero` | count = 0 (some languages) |
|
|
114
|
+
| `_one` | count = 1 |
|
|
115
|
+
| `_two` | count = 2 (Arabic, Welsh, etc.) |
|
|
116
|
+
| `_few` | count = 2-4 (Slavic languages) |
|
|
117
|
+
| `_many` | count = 5-20 (Slavic languages) |
|
|
118
|
+
| `_other` | Everything else |
|
|
119
|
+
|
|
120
|
+
```js
|
|
121
|
+
const messages = {
|
|
122
|
+
en: {
|
|
123
|
+
items_one: '{count} item',
|
|
124
|
+
items_other: '{count} items'
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
t('items', { count: 0 }); // "0 items" (_other in English)
|
|
129
|
+
t('items', { count: 1 }); // "1 item" (_one)
|
|
130
|
+
t('items', { count: 42 }); // "42 items" (_other)
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
If the plural-suffixed key is not found, the base key is used:
|
|
134
|
+
|
|
135
|
+
```js
|
|
136
|
+
const messages = { en: { items: 'Items' } };
|
|
137
|
+
|
|
138
|
+
t('items', { count: 5 }); // "Items" (base key fallback)
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Pluralization works with interpolation:
|
|
142
|
+
|
|
143
|
+
```js
|
|
144
|
+
const messages = {
|
|
145
|
+
en: {
|
|
146
|
+
cart_one: 'Your cart has {count} item ({total})',
|
|
147
|
+
cart_other: 'Your cart has {count} items ({total})'
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
t('cart', { count: 3, total: '$45.00' });
|
|
152
|
+
// "Your cart has 3 items ($45.00)"
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
## Fallback Chain
|
|
156
|
+
|
|
157
|
+
When a key is not found in the current locale, the system tries:
|
|
158
|
+
|
|
159
|
+
1. Current locale messages
|
|
160
|
+
2. Fallback locale messages (if configured)
|
|
161
|
+
3. The raw key string
|
|
162
|
+
|
|
163
|
+
```js
|
|
164
|
+
const { t, setLocale } = createI18n({
|
|
165
|
+
locale: 'de',
|
|
166
|
+
fallbackLocale: 'en',
|
|
167
|
+
messages: {
|
|
168
|
+
en: { save: 'Save', cancel: 'Cancel' },
|
|
169
|
+
de: { save: 'Speichern' }
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
t('save'); // "Speichern" (found in de)
|
|
174
|
+
t('cancel'); // "Cancel" (fallback to en)
|
|
175
|
+
t('missing'); // "missing" (key returned)
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
## Reactivity
|
|
179
|
+
|
|
180
|
+
The `t()` function reads the internal locale signal, so it integrates with Decantr's reactive system:
|
|
181
|
+
|
|
182
|
+
```js
|
|
183
|
+
import { createEffect, createMemo } from 'decantr/state';
|
|
184
|
+
|
|
185
|
+
const { t, setLocale } = createI18n({ ... });
|
|
186
|
+
|
|
187
|
+
// Effect re-runs when locale changes
|
|
188
|
+
createEffect(() => {
|
|
189
|
+
document.title = t('page.title');
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
// Memo derives from t()
|
|
193
|
+
const greeting = createMemo(() => t('welcome', { name: userName() }));
|
|
194
|
+
|
|
195
|
+
// Locale change triggers all reactive consumers
|
|
196
|
+
setLocale('fr');
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
The `locale()` getter is also a signal:
|
|
200
|
+
|
|
201
|
+
```js
|
|
202
|
+
createEffect(() => {
|
|
203
|
+
console.log('Locale changed to:', locale());
|
|
204
|
+
});
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
## RTL Support
|
|
208
|
+
|
|
209
|
+
Use `setDirection()` to set the `dir` attribute on the `<html>` element:
|
|
210
|
+
|
|
211
|
+
```js
|
|
212
|
+
const { setLocale, setDirection } = createI18n({ ... });
|
|
213
|
+
|
|
214
|
+
function switchToArabic() {
|
|
215
|
+
setLocale('ar');
|
|
216
|
+
setDirection('rtl');
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function switchToEnglish() {
|
|
220
|
+
setLocale('en');
|
|
221
|
+
setDirection('ltr');
|
|
222
|
+
}
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
Combine with Decantr's RTL logical property atoms (`_mis`, `_mie`, `_pis`, `_pie`) for layout adaptation:
|
|
226
|
+
|
|
227
|
+
```js
|
|
228
|
+
import { css } from 'decantr/css';
|
|
229
|
+
|
|
230
|
+
// These atoms use CSS logical properties, so they automatically
|
|
231
|
+
// flip in RTL mode when dir="rtl" is set on <html>
|
|
232
|
+
div({ class: css('_flex _gap4 _pis4 _pie4') }, ...children);
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
## Runtime Message Loading
|
|
236
|
+
|
|
237
|
+
Use `addMessages()` to load translations lazily:
|
|
238
|
+
|
|
239
|
+
```js
|
|
240
|
+
const { t, setLocale, addMessages } = createI18n({
|
|
241
|
+
locale: 'en',
|
|
242
|
+
fallbackLocale: 'en',
|
|
243
|
+
messages: { en: { loading: 'Loading...' } }
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
// Load locale on demand
|
|
247
|
+
async function loadLocale(locale) {
|
|
248
|
+
const res = await fetch(`/locales/${locale}.json`);
|
|
249
|
+
const messages = await res.json();
|
|
250
|
+
addMessages(locale, messages);
|
|
251
|
+
setLocale(locale);
|
|
252
|
+
}
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
`addMessages()` deep-merges into existing messages, so you can load partial translations:
|
|
256
|
+
|
|
257
|
+
```js
|
|
258
|
+
// Initial load
|
|
259
|
+
addMessages('en', { common: { save: 'Save' } });
|
|
260
|
+
|
|
261
|
+
// Later: add page-specific translations
|
|
262
|
+
addMessages('en', { dashboard: { title: 'Dashboard' } });
|
|
263
|
+
|
|
264
|
+
// Both work:
|
|
265
|
+
t('common.save'); // "Save"
|
|
266
|
+
t('dashboard.title'); // "Dashboard"
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
## Usage with Components
|
|
270
|
+
|
|
271
|
+
```js
|
|
272
|
+
import { createI18n } from 'decantr/i18n';
|
|
273
|
+
import { Button, Input } from 'decantr/components';
|
|
274
|
+
import { tags } from 'decantr/tags';
|
|
275
|
+
import { text } from 'decantr/core';
|
|
276
|
+
|
|
277
|
+
const { t } = createI18n({
|
|
278
|
+
locale: 'en',
|
|
279
|
+
messages: {
|
|
280
|
+
en: {
|
|
281
|
+
form: {
|
|
282
|
+
email: 'Email address',
|
|
283
|
+
password: 'Password',
|
|
284
|
+
submit: 'Sign In',
|
|
285
|
+
forgot: 'Forgot password?'
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
const { div, h2, a } = tags;
|
|
292
|
+
|
|
293
|
+
function LoginForm() {
|
|
294
|
+
return div(
|
|
295
|
+
h2({}, text(() => t('form.submit'))),
|
|
296
|
+
Input({ placeholder: t('form.email'), type: 'email' }),
|
|
297
|
+
Input({ placeholder: t('form.password'), type: 'password' }),
|
|
298
|
+
Button({}, text(() => t('form.submit'))),
|
|
299
|
+
a({ href: '#' }, text(() => t('form.forgot')))
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
## Message File Organization
|
|
305
|
+
|
|
306
|
+
Recommended structure for larger apps:
|
|
307
|
+
|
|
308
|
+
```
|
|
309
|
+
src/
|
|
310
|
+
locales/
|
|
311
|
+
en/
|
|
312
|
+
common.json # Shared keys (nav, buttons, errors)
|
|
313
|
+
dashboard.json # Dashboard page
|
|
314
|
+
settings.json # Settings page
|
|
315
|
+
fr/
|
|
316
|
+
common.json
|
|
317
|
+
dashboard.json
|
|
318
|
+
settings.json
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
Load and merge at startup:
|
|
322
|
+
|
|
323
|
+
```js
|
|
324
|
+
import common from './locales/en/common.json' with { type: 'json' };
|
|
325
|
+
import dashboard from './locales/en/dashboard.json' with { type: 'json' };
|
|
326
|
+
|
|
327
|
+
const { t } = createI18n({
|
|
328
|
+
locale: 'en',
|
|
329
|
+
messages: {
|
|
330
|
+
en: { common, dashboard }
|
|
331
|
+
}
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
t('common.save'); // "Save"
|
|
335
|
+
t('dashboard.title'); // "Dashboard"
|
|
336
|
+
```
|