diff --git a/.changeset/happy-clocks-hug.md b/.changeset/happy-clocks-hug.md
new file mode 100644
index 0000000..3f6cf52
--- /dev/null
+++ b/.changeset/happy-clocks-hug.md
@@ -0,0 +1,19 @@
+---
+'@vintl/vintl': minor
+---
+
+Add more formatting components similar to `react-intl`
+
+- `FormattedDate`, `FormattedDateParts`
+- `FormattedTime`, `FormattedTimeParts`
+- `FormattedDateTimeRange`
+- `FormattedRelativeTime` (static, unlike `react-intl`)
+- `FormattedNumber`, `FormattedNumberParts`
+- `FormattedPlural`
+- `FormattedList`, `FormattedListParts`
+- `FormattedDisplayName`
+- `FormattedMessage`
+
+Since this is a Vue library, they use slots to pass formatted values (otherwise rendering them as is).
+
+`FormattedMessage` is very similar to `IntlFormatted`, but accepts descriptor properties and does not allow to format raw messages.
diff --git a/.changeset/new-turkeys-pump.md b/.changeset/new-turkeys-pump.md
new file mode 100644
index 0000000..852b494
--- /dev/null
+++ b/.changeset/new-turkeys-pump.md
@@ -0,0 +1,27 @@
+---
+'@vintl/vintl': major
+---
+
+Remove deprecated composables
+
+Composables, such as `useI18n`, `useTranslate` and `useFormatters` were previously deprecated with the warning that they will be removed in the next major version. They now get removed as scheduled.
+
+Migration steps:
+
+- Use `useVIntl` everywhere you used `useI18n`, the latter was just an alias for `useVIntl` in previous versions.
+
+- To retrieve translate function previously returned by `useTranslate`, destructure `formatMessage` function from the controller:
+
+ ```js
+ const { formatMessage } = useVIntl
+ ```
+
+ It is bound to the controller and as such is safe to use on its own.
+
+- To retrieve formatters previously returned by `useFormatters`, destructure `formats` property from the controller:
+
+ ```js
+ const { formats } = useVIntl
+ ```
+
+ It is reactively updated object and also safe to use on its own.
diff --git a/.changeset/olive-bugs-visit.md b/.changeset/olive-bugs-visit.md
new file mode 100644
index 0000000..b6f2ff7
--- /dev/null
+++ b/.changeset/olive-bugs-visit.md
@@ -0,0 +1,7 @@
+---
+'@vintl/vintl': major
+---
+
+Bump Vue version to 3.3.4
+
+We're now requiring a newer Vue version because we are relying on functionality added in Vue 3.3, such as generic components. Since it's not compatible with previous versions of Vue, this is marked as a breaking change.
diff --git a/.changeset/pre.json b/.changeset/pre.json
new file mode 100644
index 0000000..1b116ae
--- /dev/null
+++ b/.changeset/pre.json
@@ -0,0 +1,8 @@
+{
+ "mode": "pre",
+ "tag": "next",
+ "initialVersions": {
+ "@vintl/vintl": "4.2.0"
+ },
+ "changesets": []
+}
diff --git a/.changeset/tender-zoos-shop.md b/.changeset/tender-zoos-shop.md
new file mode 100644
index 0000000..9ddd35b
--- /dev/null
+++ b/.changeset/tender-zoos-shop.md
@@ -0,0 +1,7 @@
+---
+'@vintl/vintl': minor
+---
+
+Add `useMessages` composable
+
+v5 introduces a new API that allows you to create messages more effectively.
diff --git a/.eslintrc.json b/.eslintrc.json
index d852a24..a19df88 100644
--- a/.eslintrc.json
+++ b/.eslintrc.json
@@ -30,6 +30,7 @@
"indent": "off",
"no-useless-constructor": "off",
"@typescript-eslint/no-useless-constructor": "warn",
+ "vue/one-component-per-file": "off",
"@typescript-eslint/no-unsafe-declaration-merging": "off"
},
"parserOptions": {
@@ -41,7 +42,7 @@
"parserOptions": { "project": "./tsconfig.build.json" }
},
{
- "files": ["./vitest.config.ts", "./test/*.test.ts"],
+ "files": ["./vitest.config.ts", "./test/**/*.ts", "./test/**/*.tsx"],
"parserOptions": { "project": "./tsconfig.tests.json" }
}
]
diff --git a/build.config.ts b/build.config.ts
index bf9671f..63b213b 100644
--- a/build.config.ts
+++ b/build.config.ts
@@ -57,7 +57,7 @@ export default defineBuildConfig({
outDir: './dist',
},
{
- input: './src/components',
+ input: './src/components/index',
name: 'components',
builder: 'rollup',
declaration: true,
@@ -79,4 +79,10 @@ export default defineBuildConfig({
},
],
declaration: true,
+ rollup: {
+ esbuild: {
+ jsx: 'automatic',
+ jsxImportSource: 'vue',
+ },
+ },
})
diff --git a/package.json b/package.json
index 99a2160..7ac2169 100644
--- a/package.json
+++ b/package.json
@@ -76,6 +76,7 @@
"devDependencies": {
"@changesets/cli": "^2.26.2",
"@nuxtjs/eslint-config-typescript": "^12.1.0",
+ "@testing-library/vue": "^7.0.0",
"@types/node": "^18.18.7",
"@vue/runtime-core": "^3.3.7",
"del-cli": "^5.1.0",
@@ -86,17 +87,19 @@
"typescript": "^5.2.2",
"unbuild": "^2.0.0",
"vitepress": "1.0.0-rc.24",
- "vitest": "^0.34.6"
+ "vitest": "^0.34.6",
+ "vue": "^3.3.7"
},
"dependencies": {
"@braw/async-computed": "^5.0.2",
+ "@formatjs/ecma402-abstract": "^1.17.2",
"@formatjs/icu-messageformat-parser": "^2.7.0",
"@formatjs/intl": "^2.9.5",
"@formatjs/intl-localematcher": "^0.4.2",
"intl-messageformat": "^10.5.4"
},
"peerDependencies": {
- "vue": "^3.2.47"
+ "vue": "^3.3.7"
},
"publishConfig": {
"access": "public"
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 91cd29e..eeabcec 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -7,7 +7,10 @@ settings:
dependencies:
'@braw/async-computed':
specifier: ^5.0.2
- version: 5.0.2(vue@3.2.47)
+ version: 5.0.2(vue@3.3.7)
+ '@formatjs/ecma402-abstract':
+ specifier: ^1.17.2
+ version: 1.17.2
'@formatjs/icu-messageformat-parser':
specifier: ^2.7.0
version: 2.7.0
@@ -20,9 +23,6 @@ dependencies:
intl-messageformat:
specifier: ^10.5.4
version: 10.5.4
- vue:
- specifier: ^3.2.47
- version: 3.2.47
devDependencies:
'@changesets/cli':
@@ -31,6 +31,9 @@ devDependencies:
'@nuxtjs/eslint-config-typescript':
specifier: ^12.1.0
version: 12.1.0(eslint@8.52.0)(typescript@5.2.2)
+ '@testing-library/vue':
+ specifier: ^7.0.0
+ version: 7.0.0(@vue/compiler-sfc@3.3.7)(vue@3.3.7)
'@types/node':
specifier: ^18.18.7
version: 18.18.7
@@ -64,6 +67,9 @@ devDependencies:
vitest:
specifier: ^0.34.6
version: 0.34.6(happy-dom@12.10.3)
+ vue:
+ specifier: ^3.3.7
+ version: 3.3.7(typescript@5.2.2)
packages:
@@ -360,19 +366,13 @@ packages:
js-tokens: 4.0.0
dev: true
- /@babel/parser@7.20.7:
- resolution: {integrity: sha512-T3Z9oHybU+0vZlY9CiDSJQTD5ZapcW18ZctFMi0MOAl/4BjFF4ul7NVSARLdbGO5vDqy9eQiGTV0LtKfvCYvcg==}
- engines: {node: '>=6.0.0'}
- dependencies:
- '@babel/types': 7.22.11
- dev: false
-
/@babel/parser@7.22.11:
resolution: {integrity: sha512-R5zb8eJIBPJriQtbH/htEQy4k7E2dHWlD2Y2VT07JCzwYZHBxV5ZYtM0UhXSNMT74LyxuM+b1jdL7pSesXbC/g==}
engines: {node: '>=6.0.0'}
hasBin: true
dependencies:
'@babel/types': 7.22.11
+ dev: true
/@babel/parser@7.23.0:
resolution: {integrity: sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw==}
@@ -380,7 +380,6 @@ packages:
hasBin: true
dependencies:
'@babel/types': 7.22.11
- dev: true
/@babel/runtime@7.22.3:
resolution: {integrity: sha512-XsDuspWKLUsxwCp6r7EhsExHtYfbe5oAGQ19kqngTdCPUoPQzOPdUbD/pB9PJiwb2ptYKQDjSJT3R6dC+EPqfQ==}
@@ -429,12 +428,12 @@ packages:
'@babel/helper-validator-identifier': 7.22.5
to-fast-properties: 2.0.0
- /@braw/async-computed@5.0.2(vue@3.2.47):
+ /@braw/async-computed@5.0.2(vue@3.3.7):
resolution: {integrity: sha512-fThqjZBTPvWtbD90Nkd4IldN7dpCkxfvthuk12ZBjkPPjh+wuRGi3HYiUqUSAOOVS0NHSxpsQFfg+qO275FtYA==}
peerDependencies:
vue: ^2.7 || ^3.2.45
dependencies:
- vue: 3.2.47
+ vue: 3.3.7(typescript@5.2.2)
dev: false
/@changesets/apply-release-plan@6.1.4:
@@ -1216,7 +1215,6 @@ packages:
/@jridgewell/sourcemap-codec@1.4.15:
resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==}
- dev: true
/@jridgewell/trace-mapping@0.3.18:
resolution: {integrity: sha512-w+niJYzMHdd7USdiH2U6869nqhD2nbfZXND5Yp93qIbEmnDNk7PD48o+YchRVpzMU7M6jVCbenTR7PA1FLQ9pA==}
@@ -1306,6 +1304,10 @@ packages:
- supports-color
dev: true
+ /@one-ini/wasm@0.1.1:
+ resolution: {integrity: sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==}
+ dev: true
+
/@rollup/plugin-alias@5.0.0(rollup@3.28.1):
resolution: {integrity: sha512-l9hY5chSCjuFRPsnRm16twWBiSApl2uYFLsepQYwtBuAxNMQ/1dJqADld40P0Jkqm65GRTLy/AC6hnpVebtLsA==}
engines: {node: '>=14.0.0'}
@@ -1401,6 +1403,40 @@ packages:
resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==}
dev: true
+ /@testing-library/dom@9.3.3:
+ resolution: {integrity: sha512-fB0R+fa3AUqbLHWyxXa2kGVtf1Fe1ZZFr0Zp6AIbIAzXb2mKbEXl+PCQNUOaq5lbTab5tfctfXRNsWXxa2f7Aw==}
+ engines: {node: '>=14'}
+ dependencies:
+ '@babel/code-frame': 7.22.10
+ '@babel/runtime': 7.22.3
+ '@types/aria-query': 5.0.3
+ aria-query: 5.1.3
+ chalk: 4.1.2
+ dom-accessibility-api: 0.5.16
+ lz-string: 1.5.0
+ pretty-format: 27.5.1
+ dev: true
+
+ /@testing-library/vue@7.0.0(@vue/compiler-sfc@3.3.7)(vue@3.3.7):
+ resolution: {integrity: sha512-JU/q93HGo2qdm1dCgWymkeQlfpC0/0/DBZ2nAHgEAsVZxX11xVIxT7gbXdI7HACQpUbsUWt1zABGU075Fzt9XQ==}
+ engines: {node: '>=14'}
+ peerDependencies:
+ '@vue/compiler-sfc': '>= 3'
+ vue: '>= 3'
+ dependencies:
+ '@babel/runtime': 7.22.3
+ '@testing-library/dom': 9.3.3
+ '@vue/compiler-sfc': 3.3.7
+ '@vue/test-utils': 2.4.1(vue@3.3.7)
+ vue: 3.3.7(typescript@5.2.2)
+ transitivePeerDependencies:
+ - '@vue/server-renderer'
+ dev: true
+
+ /@types/aria-query@5.0.3:
+ resolution: {integrity: sha512-0Z6Tr7wjKJIk4OUEjVUQMtyunLDy339vcMaj38Kpj6jM2OE1p3S4kXExKZ7a3uXQAPCoy3sbrP1wibDKaf39oA==}
+ dev: true
+
/@types/chai-subset@1.3.3:
resolution: {integrity: sha512-frBecisrNGz+F4T6bcc+NLeolfiojh5FxW2klu669+8BARtyQv2C/GkNW6FUodVe4BroGMP/wER/YDGc7rEllw==}
dependencies:
@@ -1678,15 +1714,6 @@ packages:
pretty-format: 29.7.0
dev: true
- /@vue/compiler-core@3.2.47:
- resolution: {integrity: sha512-p4D7FDnQb7+YJmO2iPEv0SQNeNzcbHdGByJDsT4lynf63AFkOTFN07HsiRSvjGo0QrxR/o3d0hUyNCUnBU2Tig==}
- dependencies:
- '@babel/parser': 7.22.11
- '@vue/shared': 3.2.47
- estree-walker: 2.0.2
- source-map: 0.6.1
- dev: false
-
/@vue/compiler-core@3.3.7:
resolution: {integrity: sha512-pACdY6YnTNVLXsB86YD8OF9ihwpolzhhtdLVHhBL6do/ykr6kKXNYABRtNMGrsQXpEXXyAdwvWWkuTbs4MFtPQ==}
dependencies:
@@ -1694,36 +1721,12 @@ packages:
'@vue/shared': 3.3.7
estree-walker: 2.0.2
source-map-js: 1.0.2
- dev: true
-
- /@vue/compiler-dom@3.2.47:
- resolution: {integrity: sha512-dBBnEHEPoftUiS03a4ggEig74J2YBZ2UIeyfpcRM2tavgMWo4bsEfgCGsu+uJIL/vax9S+JztH8NmQerUo7shQ==}
- dependencies:
- '@vue/compiler-core': 3.2.47
- '@vue/shared': 3.2.47
- dev: false
/@vue/compiler-dom@3.3.7:
resolution: {integrity: sha512-0LwkyJjnUPssXv/d1vNJ0PKfBlDoQs7n81CbO6Q0zdL7H1EzqYRrTVXDqdBVqro0aJjo/FOa1qBAPVI4PGSHBw==}
dependencies:
'@vue/compiler-core': 3.3.7
'@vue/shared': 3.3.7
- dev: true
-
- /@vue/compiler-sfc@3.2.47:
- resolution: {integrity: sha512-rog05W+2IFfxjMcFw10tM9+f7i/+FFpZJJ5XHX72NP9eC2uRD+42M3pYcQqDXVYoj74kHMSEdQ/WmCjt8JFksQ==}
- dependencies:
- '@babel/parser': 7.20.7
- '@vue/compiler-core': 3.2.47
- '@vue/compiler-dom': 3.2.47
- '@vue/compiler-ssr': 3.2.47
- '@vue/reactivity-transform': 3.2.47
- '@vue/shared': 3.2.47
- estree-walker: 2.0.2
- magic-string: 0.25.9
- postcss: 8.4.23
- source-map: 0.6.1
- dev: false
/@vue/compiler-sfc@3.3.7:
resolution: {integrity: sha512-7pfldWy/J75U/ZyYIXRVqvLRw3vmfxDo2YLMwVtWVNew8Sm8d6wodM+OYFq4ll/UxfqVr0XKiVwti32PCrruAw==}
@@ -1738,36 +1741,17 @@ packages:
magic-string: 0.30.5
postcss: 8.4.31
source-map-js: 1.0.2
- dev: true
-
- /@vue/compiler-ssr@3.2.47:
- resolution: {integrity: sha512-wVXC+gszhulcMD8wpxMsqSOpvDZ6xKXSVWkf50Guf/S+28hTAXPDYRTbLQ3EDkOP5Xz/+SY37YiwDquKbJOgZw==}
- dependencies:
- '@vue/compiler-dom': 3.2.47
- '@vue/shared': 3.2.47
- dev: false
/@vue/compiler-ssr@3.3.7:
resolution: {integrity: sha512-TxOfNVVeH3zgBc82kcUv+emNHo+vKnlRrkv8YvQU5+Y5LJGJwSNzcmLUoxD/dNzv0bhQ/F0s+InlgV0NrApJZg==}
dependencies:
'@vue/compiler-dom': 3.3.7
'@vue/shared': 3.3.7
- dev: true
/@vue/devtools-api@6.5.1:
resolution: {integrity: sha512-+KpckaAQyfbvshdDW5xQylLni1asvNSGme1JFs8I1+/H5pHEhqUKMEQD/qn3Nx5+/nycBq11qAEi8lk+LXI2dA==}
dev: true
- /@vue/reactivity-transform@3.2.47:
- resolution: {integrity: sha512-m8lGXw8rdnPVVIdIFhf0LeQ/ixyHkH5plYuS83yop5n7ggVJU+z5v0zecwEnX7fa7HNLBhh2qngJJkxpwEEmYA==}
- dependencies:
- '@babel/parser': 7.22.11
- '@vue/compiler-core': 3.2.47
- '@vue/shared': 3.2.47
- estree-walker: 2.0.2
- magic-string: 0.25.9
- dev: false
-
/@vue/reactivity-transform@3.3.7:
resolution: {integrity: sha512-APhRmLVbgE1VPGtoLQoWBJEaQk4V8JUsqrQihImVqKT+8U6Qi3t5ATcg4Y9wGAPb3kIhetpufyZ1RhwbZCIdDA==}
dependencies:
@@ -1776,41 +1760,17 @@ packages:
'@vue/shared': 3.3.7
estree-walker: 2.0.2
magic-string: 0.30.5
- dev: true
-
- /@vue/reactivity@3.2.47:
- resolution: {integrity: sha512-7khqQ/75oyyg+N/e+iwV6lpy1f5wq759NdlS1fpAhFXa8VeAIKGgk2E/C4VF59lx5b+Ezs5fpp/5WsRYXQiKxQ==}
- dependencies:
- '@vue/shared': 3.2.47
- dev: false
/@vue/reactivity@3.3.7:
resolution: {integrity: sha512-cZNVjWiw00708WqT0zRpyAgduG79dScKEPYJXq2xj/aMtk3SKvL3FBt2QKUlh6EHBJ1m8RhBY+ikBUzwc7/khg==}
dependencies:
'@vue/shared': 3.3.7
- dev: true
-
- /@vue/runtime-core@3.2.47:
- resolution: {integrity: sha512-RZxbLQIRB/K0ev0K9FXhNbBzT32H9iRtYbaXb0ZIz2usLms/D55dJR2t6cIEUn6vyhS3ALNvNthI+Q95C+NOpA==}
- dependencies:
- '@vue/reactivity': 3.2.47
- '@vue/shared': 3.2.47
- dev: false
/@vue/runtime-core@3.3.7:
resolution: {integrity: sha512-LHq9du3ubLZFdK/BP0Ysy3zhHqRfBn80Uc+T5Hz3maFJBGhci1MafccnL3rpd5/3wVfRHAe6c+PnlO2PAavPTQ==}
dependencies:
'@vue/reactivity': 3.3.7
'@vue/shared': 3.3.7
- dev: true
-
- /@vue/runtime-dom@3.2.47:
- resolution: {integrity: sha512-ArXrFTjS6TsDei4qwNvgrdmHtD930KgSKGhS5M+j8QxXrDJYLqYw4RRcDy1bz1m1wMmb6j+zGLifdVHtkXA7gA==}
- dependencies:
- '@vue/runtime-core': 3.2.47
- '@vue/shared': 3.2.47
- csstype: 2.6.21
- dev: false
/@vue/runtime-dom@3.3.7:
resolution: {integrity: sha512-PFQU1oeJxikdDmrfoNQay5nD4tcPNYixUBruZzVX/l0eyZvFKElZUjW4KctCcs52nnpMGO6UDK+jF5oV4GT5Lw==}
@@ -1818,17 +1778,6 @@ packages:
'@vue/runtime-core': 3.3.7
'@vue/shared': 3.3.7
csstype: 3.1.2
- dev: true
-
- /@vue/server-renderer@3.2.47(vue@3.2.47):
- resolution: {integrity: sha512-dN9gc1i8EvmP9RCzvneONXsKfBRgqFeFZLurmHOveL7oH6HiFXJw5OGu294n1nHc/HMgTy6LulU/tv5/A7f/LA==}
- peerDependencies:
- vue: 3.2.47
- dependencies:
- '@vue/compiler-ssr': 3.2.47
- '@vue/shared': 3.2.47
- vue: 3.2.47
- dev: false
/@vue/server-renderer@3.3.7(vue@3.3.7):
resolution: {integrity: sha512-UlpKDInd1hIZiNuVVVvLgxpfnSouxKQOSE2bOfQpBuGwxRV/JqqTCyyjXUWiwtVMyeRaZhOYYqntxElk8FhBhw==}
@@ -1838,14 +1787,22 @@ packages:
'@vue/compiler-ssr': 3.3.7
'@vue/shared': 3.3.7
vue: 3.3.7(typescript@5.2.2)
- dev: true
-
- /@vue/shared@3.2.47:
- resolution: {integrity: sha512-BHGyyGN3Q97EZx0taMQ+OLNuZcW3d37ZEVmEAyeoA9ERdGvm9Irc/0Fua8SNyOtV1w6BS4q25wbMzJujO9HIfQ==}
- dev: false
/@vue/shared@3.3.7:
resolution: {integrity: sha512-N/tbkINRUDExgcPTBvxNkvHGu504k8lzlNQRITVnm6YjOjwa4r0nnbd4Jb01sNpur5hAllyRJzSK5PvB9PPwRg==}
+
+ /@vue/test-utils@2.4.1(vue@3.3.7):
+ resolution: {integrity: sha512-VO8nragneNzUZUah6kOjiFmD/gwRjUauG9DROh6oaOeFwX1cZRUNHhdeogE8635cISigXFTtGLUQWx5KCb0xeg==}
+ peerDependencies:
+ '@vue/server-renderer': ^3.0.1
+ vue: ^3.0.1
+ peerDependenciesMeta:
+ '@vue/server-renderer':
+ optional: true
+ dependencies:
+ js-beautify: 1.14.9
+ vue: 3.3.7(typescript@5.2.2)
+ vue-component-type-helpers: 1.8.4
dev: true
/@vueuse/core@10.5.0(vue@3.3.7):
@@ -1923,6 +1880,10 @@ packages:
- vue
dev: true
+ /abbrev@1.1.1:
+ resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==}
+ dev: true
+
/acorn-jsx@5.3.2(acorn@8.10.0):
resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==}
peerDependencies:
@@ -2021,6 +1982,12 @@ packages:
resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
dev: true
+ /aria-query@5.1.3:
+ resolution: {integrity: sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ==}
+ dependencies:
+ deep-equal: 2.2.2
+ dev: true
+
/array-buffer-byte-length@1.0.0:
resolution: {integrity: sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A==}
dependencies:
@@ -2033,9 +2000,9 @@ packages:
engines: {node: '>= 0.4'}
dependencies:
call-bind: 1.0.2
- define-properties: 1.1.4
+ define-properties: 1.2.1
es-abstract: 1.20.5
- get-intrinsic: 1.1.3
+ get-intrinsic: 1.2.1
is-string: 1.0.7
dev: true
@@ -2060,7 +2027,7 @@ packages:
engines: {node: '>= 0.4'}
dependencies:
call-bind: 1.0.2
- define-properties: 1.1.4
+ define-properties: 1.2.1
es-abstract: 1.20.5
es-shim-unscopables: 1.0.0
dev: true
@@ -2335,6 +2302,11 @@ packages:
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
dev: true
+ /commander@10.0.1:
+ resolution: {integrity: sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==}
+ engines: {node: '>=14'}
+ dev: true
+
/comment-parser@1.4.0:
resolution: {integrity: sha512-QLyTNiZ2KDOibvFPlZ6ZngVsZ/0gYnE6uTXi5aoDg8ed3AkJAz4sEje3Y8a29hQ1s6A99MZXe47fLAXQ1rTqaw==}
engines: {node: '>= 12.0.0'}
@@ -2348,6 +2320,13 @@ packages:
resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
dev: true
+ /config-chain@1.1.13:
+ resolution: {integrity: sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==}
+ dependencies:
+ ini: 1.3.8
+ proto-list: 1.2.4
+ dev: true
+
/consola@3.2.3:
resolution: {integrity: sha512-I5qxpzLv+sJhTVEoLYNcTW+bThDCPsit0vLNKShZx6rLtpilNpmmeTPaeqJb9ZE9dV3DGaeby6Vuhrw38WjeyQ==}
engines: {node: ^14.18.0 || >=16.10.0}
@@ -2383,13 +2362,8 @@ packages:
engines: {node: '>=4'}
dev: true
- /csstype@2.6.21:
- resolution: {integrity: sha512-Z1PhmomIfypOpoMjRQB70jfvy/wxT50qW08YXO5lMIJkrdq4yOTR+AW7FqutScmB9NkLwxo+jU+kZLbofZZq/w==}
- dev: false
-
/csstype@3.1.2:
resolution: {integrity: sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==}
- dev: true
/csv-generate@3.4.3:
resolution: {integrity: sha512-w/T+rqR0vwvHqWs/1ZyMDWtHHSJaN06klRqJXBEpDJaM/+dZkso0OKh1VcuuYvK3XM53KysVNq8Ko/epCK8wOw==}
@@ -2467,6 +2441,29 @@ packages:
type-detect: 4.0.8
dev: true
+ /deep-equal@2.2.2:
+ resolution: {integrity: sha512-xjVyBf0w5vH0I42jdAZzOKVldmPgSulmiyPRywoyq7HXC9qdgo17kxJE+rdnif5Tz6+pIrpJI8dCpMNLIGkUiA==}
+ dependencies:
+ array-buffer-byte-length: 1.0.0
+ call-bind: 1.0.2
+ es-get-iterator: 1.1.3
+ get-intrinsic: 1.2.1
+ is-arguments: 1.1.1
+ is-array-buffer: 3.0.2
+ is-date-object: 1.0.5
+ is-regex: 1.1.4
+ is-shared-array-buffer: 1.0.2
+ isarray: 2.0.5
+ object-is: 1.1.5
+ object-keys: 1.1.1
+ object.assign: 4.1.4
+ regexp.prototype.flags: 1.5.1
+ side-channel: 1.0.4
+ which-boxed-primitive: 1.0.2
+ which-collection: 1.0.1
+ which-typed-array: 1.1.11
+ dev: true
+
/deep-is@0.1.4:
resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==}
dev: true
@@ -2491,14 +2488,6 @@ packages:
has-property-descriptors: 1.0.0
dev: true
- /define-properties@1.1.4:
- resolution: {integrity: sha512-uckOqKcfaVvtBdsVkdPv3XjveQJsNQqmhXgRi8uhvWWuPYZCNlzT8qAyblUgNoXdHdjMTzAqeGjAoli8f+bzPA==}
- engines: {node: '>= 0.4'}
- dependencies:
- has-property-descriptors: 1.0.0
- object-keys: 1.1.1
- dev: true
-
/define-properties@1.2.1:
resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==}
engines: {node: '>= 0.4'}
@@ -2576,6 +2565,21 @@ packages:
esutils: 2.0.3
dev: true
+ /dom-accessibility-api@0.5.16:
+ resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==}
+ dev: true
+
+ /editorconfig@1.0.4:
+ resolution: {integrity: sha512-L9Qe08KWTlqYMVvMcTIvMAdl1cDUubzRNYL+WfA4bLDMHe4nemKkpmYzkznE1FwLKu0EEmy6obgQKzMJrg4x9Q==}
+ engines: {node: '>=14'}
+ hasBin: true
+ dependencies:
+ '@one-ini/wasm': 0.1.1
+ commander: 10.0.1
+ minimatch: 9.0.1
+ semver: 7.5.4
+ dev: true
+
/electron-to-chromium@1.4.503:
resolution: {integrity: sha512-LF2IQit4B0VrUHFeQkWhZm97KuJSGF2WJqq1InpY+ECpFRkXd8yTIaTtJxsO0OKDmiBYwWqcrNaXOurn2T2wiA==}
dev: true
@@ -2624,17 +2628,17 @@ packages:
has: 1.0.3
has-property-descriptors: 1.0.0
has-symbols: 1.0.3
- internal-slot: 1.0.4
+ internal-slot: 1.0.5
is-callable: 1.2.7
is-negative-zero: 2.0.2
is-regex: 1.1.4
is-shared-array-buffer: 1.0.2
is-string: 1.0.7
is-weakref: 1.0.2
- object-inspect: 1.12.2
+ object-inspect: 1.12.3
object-keys: 1.1.1
object.assign: 4.1.4
- regexp.prototype.flags: 1.4.3
+ regexp.prototype.flags: 1.5.1
safe-regex-test: 1.0.0
string.prototype.trimend: 1.0.6
string.prototype.trimstart: 1.0.6
@@ -2686,6 +2690,20 @@ packages:
which-typed-array: 1.1.11
dev: true
+ /es-get-iterator@1.1.3:
+ resolution: {integrity: sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw==}
+ dependencies:
+ call-bind: 1.0.2
+ get-intrinsic: 1.2.1
+ has-symbols: 1.0.3
+ is-arguments: 1.1.1
+ is-map: 2.0.2
+ is-set: 2.0.2
+ is-string: 1.0.7
+ isarray: 2.0.5
+ stop-iteration-iterator: 1.0.0
+ dev: true
+
/es-set-tostringtag@2.0.1:
resolution: {integrity: sha512-g3OMbtlwY3QewlqAiMLI47KywjWZoEytKr8pf6iTC8uJq5bIAH52Z9pnQ8pVL6whrCto53JZDuUIsifGeLorTg==}
engines: {node: '>= 0.4'}
@@ -3357,14 +3375,6 @@ packages:
resolution: {integrity: sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==}
dev: true
- /get-intrinsic@1.1.3:
- resolution: {integrity: sha512-QJVz1Tj7MS099PevUG5jvnt9tSkXN8K14dxQlikJuPt4uD9hHAHjLyLBiLR5zELelBdD9QNRAXZzsJx0WaDL9A==}
- dependencies:
- function-bind: 1.1.1
- has: 1.0.3
- has-symbols: 1.0.3
- dev: true
-
/get-intrinsic@1.2.1:
resolution: {integrity: sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==}
dependencies:
@@ -3616,13 +3626,8 @@ packages:
resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
dev: true
- /internal-slot@1.0.4:
- resolution: {integrity: sha512-tA8URYccNzMo94s5MQZgH8NB/XTa6HsOo0MLfXTKKEnHVVdegzaQoFZ7Jp44bdvLvY2waT5dc+j5ICEswhi7UQ==}
- engines: {node: '>= 0.4'}
- dependencies:
- get-intrinsic: 1.2.1
- has: 1.0.3
- side-channel: 1.0.4
+ /ini@1.3.8:
+ resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==}
dev: true
/internal-slot@1.0.5:
@@ -3643,6 +3648,14 @@ packages:
tslib: 2.4.1
dev: false
+ /is-arguments@1.1.1:
+ resolution: {integrity: sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==}
+ engines: {node: '>= 0.4'}
+ dependencies:
+ call-bind: 1.0.2
+ has-tostringtag: 1.0.0
+ dev: true
+
/is-array-buffer@3.0.2:
resolution: {integrity: sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==}
dependencies:
@@ -3723,6 +3736,10 @@ packages:
is-extglob: 2.1.1
dev: true
+ /is-map@2.0.2:
+ resolution: {integrity: sha512-cOZFQQozTha1f4MxLFzlgKYPTyj26picdZTx82hbc/Xf4K/tZOOXSCkMvU4pKioRXGDLJRn0GM7Upe7kR721yg==}
+ dev: true
+
/is-module@1.0.0:
resolution: {integrity: sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==}
dev: true
@@ -3778,6 +3795,10 @@ packages:
has-tostringtag: 1.0.0
dev: true
+ /is-set@2.0.2:
+ resolution: {integrity: sha512-+2cnTEZeY5z/iXGbLhPrOAaK/Mau5k5eXq9j14CpRTftq0pAJu2MwVRSZhyZWBzx3o6X795Lz6Bpb6R0GKf37g==}
+ dev: true
+
/is-shared-array-buffer@1.0.2:
resolution: {integrity: sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==}
dependencies:
@@ -3812,12 +3833,23 @@ packages:
which-typed-array: 1.1.11
dev: true
+ /is-weakmap@2.0.1:
+ resolution: {integrity: sha512-NSBR4kH5oVj1Uwvv970ruUkCV7O1mzgVFO4/rev2cLRda9Tm9HrL70ZPut4rOHgY0FNrUu9BCbXA2sdQ+x0chA==}
+ dev: true
+
/is-weakref@1.0.2:
resolution: {integrity: sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==}
dependencies:
call-bind: 1.0.2
dev: true
+ /is-weakset@2.0.2:
+ resolution: {integrity: sha512-t2yVvttHkQktwnNNmBQ98AhENLdPUTDTE21uPqAQ0ARwQfGeQKRVS0NNurH7bTf7RrvcVn1OOge45CnBeHCSmg==}
+ dependencies:
+ call-bind: 1.0.2
+ get-intrinsic: 1.2.1
+ dev: true
+
/is-windows@1.0.2:
resolution: {integrity: sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==}
engines: {node: '>=0.10.0'}
@@ -3835,6 +3867,17 @@ packages:
resolution: {integrity: sha512-5eEbBDQT/jF1xg6l36P+mWGGoH9Spuy0PCdSr2dtWRDGC6ph/w9ZCL4lmESW8f8F7MwT3XKescfP0wnZWAKL9w==}
dev: true
+ /js-beautify@1.14.9:
+ resolution: {integrity: sha512-coM7xq1syLcMyuVGyToxcj2AlzhkDjmfklL8r0JgJ7A76wyGMpJ1oA35mr4APdYNO/o/4YY8H54NQIJzhMbhBg==}
+ engines: {node: '>=12'}
+ hasBin: true
+ dependencies:
+ config-chain: 1.1.13
+ editorconfig: 1.0.4
+ glob: 8.1.0
+ nopt: 6.0.0
+ dev: true
+
/js-tokens@4.0.0:
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
requiresBuild: true
@@ -3990,11 +4033,10 @@ packages:
yallist: 4.0.0
dev: true
- /magic-string@0.25.9:
- resolution: {integrity: sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==}
- dependencies:
- sourcemap-codec: 1.4.8
- dev: false
+ /lz-string@1.5.0:
+ resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==}
+ hasBin: true
+ dev: true
/magic-string@0.27.0:
resolution: {integrity: sha512-8UnnX2PeRAPZuN12svgR9j7M1uWMovg/CEnIwIG0LFkXSJJe4PdfUGiTGl8V9bsBHFUtfVINcSyYxd7q+kx9fA==}
@@ -4015,7 +4057,6 @@ packages:
engines: {node: '>=12'}
dependencies:
'@jridgewell/sourcemap-codec': 1.4.15
- dev: true
/map-obj@1.0.1:
resolution: {integrity: sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg==}
@@ -4297,6 +4338,13 @@ packages:
brace-expansion: 2.0.1
dev: true
+ /minimatch@9.0.1:
+ resolution: {integrity: sha512-0jWhJpD/MdhPXwPuiRkCbfYfSKp2qnn2eOc279qI7f+osl/l+prKSrvhg157zSYvx/1nmgn2NqdT6k2Z7zSH9w==}
+ engines: {node: '>=16 || 14 >=14.17'}
+ dependencies:
+ brace-expansion: 2.0.1
+ dev: true
+
/minimist-options@4.1.0:
resolution: {integrity: sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A==}
engines: {node: '>= 6'}
@@ -4374,6 +4422,14 @@ packages:
resolution: {integrity: sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==}
dev: true
+ /nopt@6.0.0:
+ resolution: {integrity: sha512-ZwLpbTgdhuZUnZzjd7nb1ZV+4DoiC6/sfiVKok72ym/4Tlf+DFdlHYmT2JPmcNNWV6Pi3SDf1kT+A4r9RTuT9g==}
+ engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0}
+ hasBin: true
+ dependencies:
+ abbrev: 1.1.1
+ dev: true
+
/normalize-package-data@2.5.0:
resolution: {integrity: sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==}
dependencies:
@@ -4399,14 +4455,18 @@ packages:
boolbase: 1.0.0
dev: true
- /object-inspect@1.12.2:
- resolution: {integrity: sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==}
- dev: true
-
/object-inspect@1.12.3:
resolution: {integrity: sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==}
dev: true
+ /object-is@1.1.5:
+ resolution: {integrity: sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw==}
+ engines: {node: '>= 0.4'}
+ dependencies:
+ call-bind: 1.0.2
+ define-properties: 1.2.1
+ dev: true
+
/object-keys@1.1.1:
resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==}
engines: {node: '>= 0.4'}
@@ -4445,7 +4505,7 @@ packages:
engines: {node: '>= 0.4'}
dependencies:
call-bind: 1.0.2
- define-properties: 1.1.4
+ define-properties: 1.2.1
es-abstract: 1.20.5
dev: true
@@ -4625,15 +4685,6 @@ packages:
util-deprecate: 1.0.2
dev: true
- /postcss@8.4.23:
- resolution: {integrity: sha512-bQ3qMcpF6A/YjR55xtoTr0jGOlnPOKAIMdOWiv0EIT6HVPEaJiJB4NLljSbiHoC2RX7DN5Uvjtpbg1NPdwv1oA==}
- engines: {node: ^10 || ^12 || >=14}
- dependencies:
- nanoid: 3.3.6
- picocolors: 1.0.0
- source-map-js: 1.0.2
- dev: false
-
/postcss@8.4.30:
resolution: {integrity: sha512-7ZEao1g4kd68l97aWG/etQKPKq07us0ieSZ2TnFDk11i0ZfDW2AwKHYU8qv4MZKqN2fdBfg+7q0ES06UA73C1g==}
engines: {node: ^10 || ^12 || >=14}
@@ -4650,7 +4701,6 @@ packages:
nanoid: 3.3.6
picocolors: 1.0.0
source-map-js: 1.0.2
- dev: true
/preact@10.13.0:
resolution: {integrity: sha512-ERdIdUpR6doqdaSIh80hvzebHB7O6JxycOhyzAeLEchqOq/4yueslQbfnPwXaNhAYacFTyCclhwkEbOumT0tHw==}
@@ -4700,6 +4750,15 @@ packages:
engines: {node: ^14.13.1 || >=16.0.0}
dev: true
+ /pretty-format@27.5.1:
+ resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==}
+ engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0}
+ dependencies:
+ ansi-regex: 5.0.1
+ ansi-styles: 5.2.0
+ react-is: 17.0.2
+ dev: true
+
/pretty-format@29.7.0:
resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==}
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
@@ -4709,6 +4768,10 @@ packages:
react-is: 18.2.0
dev: true
+ /proto-list@1.2.4:
+ resolution: {integrity: sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==}
+ dev: true
+
/pseudomap@1.0.2:
resolution: {integrity: sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ==}
dev: true
@@ -4732,6 +4795,10 @@ packages:
engines: {node: '>=10'}
dev: true
+ /react-is@17.0.2:
+ resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==}
+ dev: true
+
/react-is@18.2.0:
resolution: {integrity: sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==}
dev: true
@@ -4808,15 +4875,6 @@ packages:
resolution: {integrity: sha512-s2aEVuLhvnVJW6s/iPgEGK6R+/xngd2jNQ+xy4bXNDKxZKJH6jpPHY6kVeVv1IeLCHgswRj+Kl3ELaDjG6V1iw==}
dev: true
- /regexp.prototype.flags@1.4.3:
- resolution: {integrity: sha512-fjggEOO3slI6Wvgjwflkc4NFRCTZAu5CnNfBd5qOMYhWdn67nJBBu34/TkD++eeFmd8C9r9jfXJ27+nSiRkSUA==}
- engines: {node: '>= 0.4'}
- dependencies:
- call-bind: 1.0.2
- define-properties: 1.2.1
- functions-have-names: 1.2.3
- dev: true
-
/regexp.prototype.flags@1.5.1:
resolution: {integrity: sha512-sy6TXMN+hnP/wMy+ISxg3krXx7BAtWVO4UouuCN/ziM9UEne0euamVNafDfvC83bRNr95y0V5iijeDQFUNpvrg==}
engines: {node: '>= 0.4'}
@@ -4939,15 +4997,18 @@ packages:
/semver@5.7.1:
resolution: {integrity: sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==}
+ hasBin: true
dev: true
/semver@6.3.1:
resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==}
+ hasBin: true
dev: true
/semver@7.5.4:
resolution: {integrity: sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==}
engines: {node: '>=10'}
+ hasBin: true
dependencies:
lru-cache: 6.0.0
dev: true
@@ -5040,15 +5101,6 @@ packages:
resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==}
engines: {node: '>=0.10.0'}
- /source-map@0.6.1:
- resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==}
- engines: {node: '>=0.10.0'}
- dev: false
-
- /sourcemap-codec@1.4.8:
- resolution: {integrity: sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==}
- dev: false
-
/spawndamnit@2.0.0:
resolution: {integrity: sha512-j4JKEcncSjFlqIwU5L/rp2N5SIPsdxaRsIv678+TZxZ0SRDJTm8JrxJMjE/XuiEZNEir3S8l0Fa3Ke339WI4qA==}
dependencies:
@@ -5090,6 +5142,13 @@ packages:
resolution: {integrity: sha512-f9aPhy8fYBuMN+sNfakZV18U39PbalgjXG3lLB9WkaYTxijru61wb57V9wxxNthXM5Sd88ETBWi29qLAsHO52Q==}
dev: true
+ /stop-iteration-iterator@1.0.0:
+ resolution: {integrity: sha512-iCGQj+0l0HOdZ2AEeBADlsRC+vsnDsZsbdSiH1yNSjcfKM7fdpCMfqAL/dwF5BLiw/XhRft/Wax6zQbhq2BcjQ==}
+ engines: {node: '>= 0.4'}
+ dependencies:
+ internal-slot: 1.0.5
+ dev: true
+
/stream-transform@2.1.3:
resolution: {integrity: sha512-9GHUiM5hMiCi6Y03jD2ARC1ettBXkQBoQAe7nJsPknnI0ow10aXjTnew8QtYQmLjzn974BnmWEAJgCY6ZP1DeQ==}
dependencies:
@@ -5711,6 +5770,10 @@ packages:
resolution: {integrity: sha512-AFbieoL7a5LMqcnOF04ji+rpXadgOXnZsxQr//r83kLPr7biP7am3g9zbaZIaBGwBRWeSvoMD4mgPdX3e4NWBg==}
dev: true
+ /vue-component-type-helpers@1.8.4:
+ resolution: {integrity: sha512-6bnLkn8O0JJyiFSIF0EfCogzeqNXpnjJ0vW/SZzNHfe6sPx30lTtTXlE5TFs2qhJlAtDFybStVNpL73cPe3OMQ==}
+ dev: true
+
/vue-demi@0.14.6(vue@3.3.7):
resolution: {integrity: sha512-8QA7wrYSHKaYgUxDA5ZC24w+eHm3sYCbp0EzcDwKqN3p6HqtTCGR/GVsPyZW92unff4UlcSh++lmqDWN3ZIq4w==}
engines: {node: '>=12'}
@@ -5744,16 +5807,6 @@ packages:
- supports-color
dev: true
- /vue@3.2.47:
- resolution: {integrity: sha512-60188y/9Dc9WVrAZeUVSDxRQOZ+z+y5nO2ts9jWXSTkMvayiWxCWOWtBQoYjLeccfXkiiPZWAHcV+WTPhkqJHQ==}
- dependencies:
- '@vue/compiler-dom': 3.2.47
- '@vue/compiler-sfc': 3.2.47
- '@vue/runtime-dom': 3.2.47
- '@vue/server-renderer': 3.2.47(vue@3.2.47)
- '@vue/shared': 3.2.47
- dev: false
-
/vue@3.3.7(typescript@5.2.2):
resolution: {integrity: sha512-YEMDia1ZTv1TeBbnu6VybatmSteGOS3A3YgfINOfraCbf85wdKHzscD6HSS/vB4GAtI7sa1XPX7HcQaJ1l24zA==}
peerDependencies:
@@ -5768,7 +5821,6 @@ packages:
'@vue/server-renderer': 3.3.7(vue@3.3.7)
'@vue/shared': 3.3.7
typescript: 5.2.2
- dev: true
/wcwidth@1.0.1:
resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==}
@@ -5803,6 +5855,15 @@ packages:
is-symbol: 1.0.4
dev: true
+ /which-collection@1.0.1:
+ resolution: {integrity: sha512-W8xeTUwaln8i3K/cY1nGXzdnVZlidBcagyNFtBdD5kxnb4TvGKR7FfSIS3mYpwWS1QUCutfKz8IY8RjftB0+1A==}
+ dependencies:
+ is-map: 2.0.2
+ is-set: 2.0.2
+ is-weakmap: 2.0.1
+ is-weakset: 2.0.2
+ dev: true
+
/which-module@2.0.1:
resolution: {integrity: sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==}
dev: true
diff --git a/src/components.ts b/src/components.ts
deleted file mode 100644
index 52adbbf..0000000
--- a/src/components.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-export { IntlFormatted } from './IntlFormatted.js'
-
-export { Fragment } from './utils/Fragment.js'
diff --git a/src/components/dateTime.ts b/src/components/dateTime.ts
new file mode 100644
index 0000000..5c65af6
--- /dev/null
+++ b/src/components/dateTime.ts
@@ -0,0 +1,38 @@
+import {
+ definePartsFormatterComponent,
+ defineSimpleFormatterComponent,
+ type PartsFormatterComponentProps,
+ type PartsFormatterComponentSlots,
+ type SimpleFormatterComponentProps,
+ type SimpleFormatterComponentSlots,
+} from './utils/index.ts'
+
+export const FormattedDate = defineSimpleFormatterComponent('formatDate')
+
+export type FormattedDateProps = SimpleFormatterComponentProps<'formatDate'>
+
+export type FormattedDateSlots = SimpleFormatterComponentSlots<'formatDate'>
+
+export const FormattedDateParts =
+ definePartsFormatterComponent('formatDateToParts')
+
+export type FormattedDatePartsProps =
+ PartsFormatterComponentProps<'formatDateToParts'>
+
+export type FormattedDatePartsSlots =
+ PartsFormatterComponentSlots<'formatDateToParts'>
+
+export const FormattedTime = defineSimpleFormatterComponent('formatTime')
+
+export type FormattedTimeProps = SimpleFormatterComponentProps<'formatTime'>
+
+export type FormattedTimeSlots = SimpleFormatterComponentSlots<'formatTime'>
+
+export const FormattedTimeParts =
+ definePartsFormatterComponent('formatTimeToParts')
+
+export type FormattedTimePartsProps =
+ PartsFormatterComponentProps<'formatTimeToParts'>
+
+export type FormattedTimePartsSlots =
+ PartsFormatterComponentSlots<'formatTimeToParts'>
diff --git a/src/components/dateTimeRange.ts b/src/components/dateTimeRange.ts
new file mode 100644
index 0000000..740c9af
--- /dev/null
+++ b/src/components/dateTimeRange.ts
@@ -0,0 +1,67 @@
+import type { FormatDateOptions } from '@formatjs/intl'
+import {
+ computed,
+ createTextVNode,
+ defineComponent,
+ type PropType,
+ type SetupContext,
+ type SlotsType,
+} from 'vue'
+import { useVIntl } from '../runtime/index.ts'
+import { normalizeAttrs } from './utils/index.ts'
+
+interface FormattedDateTimeRangeDefinedProps {
+ from: Date | number
+ to: Date | number
+}
+
+export interface FormattedDateTimeRangeProps
+ extends FormattedDateTimeRangeDefinedProps,
+ FormatDateOptions {}
+
+export interface FormattedDateTimeRangeSlots {
+ default(props: { formattedValue: string }): any
+}
+
+export const FormattedDateTimeRange = defineComponent(
+ (
+ props: FormattedDateTimeRangeProps,
+ ctx: SetupContext<{}, SlotsType>>,
+ ) => {
+ const vintl = useVIntl()
+
+ const $options = computed(
+ () => normalizeAttrs(ctx.attrs) as FormatDateOptions,
+ )
+
+ return () => {
+ const { from, to } = props as FormattedDateTimeRangeDefinedProps
+
+ const formattedValue = vintl.intl.formatDateTimeRange(
+ from,
+ to,
+ $options.value,
+ )
+
+ return (
+ ctx.slots.default?.({ formattedValue }) ??
+ createTextVNode(formattedValue)
+ )
+ }
+ },
+ {
+ inheritAttrs: false,
+ props: {
+ from: {
+ required: true,
+ type: [Number, Date] as PropType,
+ default: undefined,
+ },
+ to: {
+ required: true,
+ type: [Number, Date] as PropType,
+ default: undefined,
+ },
+ },
+ },
+)
diff --git a/src/components/displayName.ts b/src/components/displayName.ts
new file mode 100644
index 0000000..7da4c55
--- /dev/null
+++ b/src/components/displayName.ts
@@ -0,0 +1,14 @@
+import {
+ defineSimpleFormatterComponent,
+ type SimpleFormatterComponentProps,
+ type SimpleFormatterComponentSlots,
+} from './utils/index.ts'
+
+export const FormattedDisplayName =
+ defineSimpleFormatterComponent('formatDisplayName')
+
+export type FormattedDisplayNameProps =
+ SimpleFormatterComponentProps<'formatDisplayName'>
+
+export type FormattedDisplayNameSlots =
+ SimpleFormatterComponentSlots<'formatDisplayName'>
diff --git a/src/components/fragment.ts b/src/components/fragment.ts
new file mode 100644
index 0000000..9504771
--- /dev/null
+++ b/src/components/fragment.ts
@@ -0,0 +1,13 @@
+import { defineComponent, type VNodeChild } from 'vue'
+
+interface FragmentProps {
+ of: VNodeChild
+}
+
+export const Fragment = defineComponent(
+ (props: FragmentProps) => () => props.of,
+ {
+ // eslint-disable-next-line vue/require-prop-types
+ props: ['of'],
+ },
+)
diff --git a/src/components/index.ts b/src/components/index.ts
new file mode 100644
index 0000000..1fdd151
--- /dev/null
+++ b/src/components/index.ts
@@ -0,0 +1,11 @@
+export * from './dateTime.ts'
+export * from './dateTimeRange.ts'
+export * from './displayName.ts'
+export * from './fragment.ts'
+export * from './intlFormatted.ts'
+export * from './lists.ts'
+export * from './listsParts.ts'
+export * from './message.ts'
+export * from './number.ts'
+export * from './plural.ts'
+export * from './relativeTime.ts'
diff --git a/src/IntlFormatted.ts b/src/components/intlFormatted.ts
similarity index 95%
rename from src/IntlFormatted.ts
rename to src/components/intlFormatted.ts
index 9f74d01..083b45f 100644
--- a/src/IntlFormatted.ts
+++ b/src/components/intlFormatted.ts
@@ -12,16 +12,13 @@ import type {
MessageID,
MessageValues,
MessageValueType,
-} from './types/messages.js'
-import { useI18n } from './runtime/useI18n.js'
+} from '../types/messages.ts'
+import { useVIntl } from '../runtime/index.ts'
+import { createRecord } from './utils/index.ts'
/** Represents a value that can be either `T` or an array of `T`. */
type MaybeArray = T | T[]
-function createObject() {
- return Object.create(null)
-}
-
interface CommonProps {
tags?: string[]
}
@@ -79,10 +76,10 @@ export function IntlFormatted(
)
}
- const { intl, normalizeMessageDescriptor: getDescriptor } = useI18n()
+ const { intl, normalizeMessageDescriptor: getDescriptor } = useVIntl()
/** Initial values are passed to the slots. */
- const initialValues: MessageValues = createObject()
+ const initialValues: MessageValues = createRecord()
/**
* Provided values are values that were automatically provided by the
@@ -90,7 +87,7 @@ export function IntlFormatted(
*
* Initial values are to be merged before assigning provided values.
*/
- const values: MessageValues = createObject()
+ const values: MessageValues = createRecord()
if (props.values != null) {
Object.assign(initialValues, props.values)
diff --git a/src/components/lists.ts b/src/components/lists.ts
new file mode 100644
index 0000000..3d64c79
--- /dev/null
+++ b/src/components/lists.ts
@@ -0,0 +1,59 @@
+import type { FormatListOptions } from '@formatjs/intl'
+import {
+ computed,
+ defineComponent,
+ type PropType,
+ type SetupContext,
+ type SlotsType,
+ type VNode,
+} from 'vue'
+import { useVIntl } from '../runtime/index.ts'
+import { normalizeAttrs } from './utils/index.ts'
+
+interface FormattedListDefinedProps- {
+ items: readonly Item[]
+}
+
+export type FormattedListProps
- =
+ FormattedListDefinedProps
- & FormatListOptions
+
+export interface FormattedListSlots<
+ Item extends string | VNode = string | VNode,
+> {
+ default(props: {
+ children: Item extends string ? string : string | Item | (string | Item)[]
+ }): any
+}
+
+export const FormattedList = defineComponent(
+
- (
+ props: FormattedListProps
- ,
+ ctx: SetupContext<{}, SlotsType>>>,
+ ) => {
+ const vintl = useVIntl()
+
+ const options = computed(() => {
+ return normalizeAttrs(ctx.attrs) as FormatListOptions
+ })
+
+ return () => {
+ const { items } = props as FormattedListDefinedProps
-
+
+ const children = vintl.intl.formatList(items, options.value) as any
+
+ return ctx.slots.default?.({ children }) ?? children
+ }
+ },
+ {
+ inheritAttrs: false,
+ props: {
+ items: {
+ type: Array as PropType,
+ default() {
+ return []
+ },
+ },
+ },
+ },
+) as
- (props: FormattedListProps
- ) => any
+// override because typescript is stupid
diff --git a/src/components/listsParts.ts b/src/components/listsParts.ts
new file mode 100644
index 0000000..de78173
--- /dev/null
+++ b/src/components/listsParts.ts
@@ -0,0 +1,61 @@
+import type { FormatListOptions, IntlFormatters } from '@formatjs/intl'
+import {
+ type VNode,
+ defineComponent,
+ type SetupContext,
+ type SlotsType,
+ computed,
+ type PropType,
+} from 'vue'
+import { useVIntl } from '../runtime/index.ts'
+import { normalizeAttrs } from './utils/index.ts'
+
+interface FormattedListPartsDefinedProps
- {
+ items: readonly Item[]
+}
+
+export interface FormattedListPartsProps
-
+ extends FormattedListPartsDefinedProps
- ,
+ FormatListOptions {}
+
+export interface FormattedListPartsSlots
- {
+ default(props: {
+ parts: ReturnType['formatListToParts']>
+ }): any
+}
+
+export const FormattedListParts = defineComponent(
+
- (
+ props: FormattedListPartsProps
- ,
+ ctx: SetupContext<{}, SlotsType>>>,
+ ) => {
+ const vintl = useVIntl()
+
+ const options = computed(() => {
+ return normalizeAttrs(ctx.attrs) as FormatListOptions
+ })
+
+ return () => {
+ const { items } = props as FormattedListPartsDefinedProps
-
+
+ const parts = vintl.intl.formatListToParts(items, options.value)
+
+ const defaultSlot = ctx.slots.default
+
+ return defaultSlot == null
+ ? parts.map((part) => part.value)
+ : defaultSlot({ parts })
+ }
+ },
+ {
+ inheritAttrs: false,
+ props: {
+ items: {
+ type: Array as PropType,
+ default() {
+ return []
+ },
+ },
+ },
+ },
+) as
- (props: FormattedListPartsProps
- ) => any
diff --git a/src/components/message.ts b/src/components/message.ts
new file mode 100644
index 0000000..50ec2f1
--- /dev/null
+++ b/src/components/message.ts
@@ -0,0 +1,163 @@
+import {
+ computed,
+ defineComponent,
+ type SlotsType,
+ type SetupContext,
+ type VNodeArrayChildren,
+} from 'vue'
+import type { FormatXMLElementFn, PrimitiveType } from 'intl-messageformat'
+import { useVIntl } from '../runtime/index.ts'
+import { type MessageContent, type MessageValueType } from '../types/index.ts'
+import { createRecord } from './utils/index.ts'
+
+function isValueSlotName(slotName: string): slotName is `~${string}` {
+ return slotName.startsWith('~')
+}
+
+type ValuesRecord = Record<
+ string,
+ PrimitiveType | T | FormatXMLElementFn
+>
+
+export interface FormattedMessageProps {
+ id: string
+ description?: string | object
+ defaultMessage?: MessageContent
+ values?: ValuesRecord
+}
+
+export interface FormattedMessageSlots {
+ [key: string]: (ctx: {
+ children: (T | string)[]
+ values: ValuesRecord
+ }) => string | T | (string | T)[]
+ [key: `~${string}`]: (ctx: { values: ValuesRecord }) => string | T
+}
+
+class SlotOutput {
+ constructor(public readonly value: T | T[]) {}
+}
+
+type NonArray = T extends any[] ? never : T
+type VNodeChildAtom = NonArray
+type VNodeArrayChildrenWith = (
+ | T
+ | VNodeChildAtom
+ | VNodeArrayChildrenWith
+)[]
+
+function normalizeOutput(
+ rawOutput:
+ | string
+ | SlotOutput
+ | T
+ | (string | SlotOutput | T)[],
+): VNodeArrayChildrenWith {
+ if (Array.isArray(rawOutput)) {
+ const output: VNodeArrayChildrenWith = []
+ for (const child of rawOutput) {
+ if (child instanceof SlotOutput) {
+ if (Array.isArray(child.value)) {
+ output.push(...child.value)
+ } else {
+ output.push(child.value)
+ }
+ } else {
+ output.push(child)
+ }
+ }
+ return output
+ } else if (rawOutput instanceof SlotOutput) {
+ return Array.isArray(rawOutput.value) ? rawOutput.value : [rawOutput.value]
+ } else if (typeof rawOutput === 'string') {
+ return [rawOutput]
+ }
+
+ return [rawOutput]
+}
+
+export const FormattedMessage = defineComponent(
+ function FormattedMessage(
+ props: FormattedMessageProps,
+ ctx: SetupContext<{}, SlotsType>>>,
+ ) {
+ const descriptor = computed(() => ({
+ id: props.id,
+ defaultMessage: props.defaultMessage,
+ description: props.description,
+ }))
+
+ const values = computed>(() => {
+ const combinedValues = createRecord() as ValuesRecord
+ Object.assign(combinedValues, props.values)
+
+ const slotValues = createRecord() as ValuesRecord<
+ T | SlotOutput | ((children: (T | string)[]) => SlotOutput)
+ >
+
+ const { slots } = ctx
+
+ for (const slotKey in slots) {
+ if (slots[slotKey] == null) continue
+
+ if (isValueSlotName(slotKey)) {
+ slotValues[slotKey.slice(1)] = new SlotOutput(
+ slots[slotKey]!({
+ values: combinedValues,
+ }),
+ )
+ } else {
+ slotValues[slotKey] = (children: (T | string)[]) =>
+ new SlotOutput(
+ slots[slotKey]!({
+ values: combinedValues,
+ children,
+ }),
+ )
+ }
+ }
+
+ Object.assign(combinedValues, slotValues)
+
+ return combinedValues
+ })
+
+ const vintl = useVIntl()
+
+ return () => {
+ const output = vintl.intl.formatMessage(
+ descriptor.value,
+ values.value as any,
+ )
+
+ return normalizeOutput(output)
+ }
+ },
+ {
+ props: {
+ id: {
+ type: String,
+ required: true,
+ default: undefined,
+ },
+ defaultMessage: {
+ type: String,
+ required: false,
+ default: undefined,
+ },
+ description: {
+ type: String,
+ required: false,
+ default: undefined,
+ },
+ values: {
+ type: Object,
+ required: false,
+ default() {
+ return {}
+ },
+ },
+ },
+ slots: Object as any,
+ },
+) as (props: FormattedMessageProps) => any
diff --git a/src/components/number.ts b/src/components/number.ts
new file mode 100644
index 0000000..f6477c3
--- /dev/null
+++ b/src/components/number.ts
@@ -0,0 +1,27 @@
+import {
+ defineSimpleFormatterComponent,
+ definePartsFormatterComponent,
+ type SimpleFormatterComponentSlots,
+ type SimpleFormatterComponentProps,
+ type PartsFormatterComponentProps,
+ type PartsFormatterComponentSlots,
+} from './utils/index.ts'
+
+// This is required so that TypeScript cannot infer the types.
+import type {} from '@formatjs/ecma402-abstract'
+
+export const FormattedNumber = defineSimpleFormatterComponent('formatNumber')
+
+export type FormattedNumberProps = SimpleFormatterComponentProps<'formatNumber'>
+
+export type FormattedNumberSlots = SimpleFormatterComponentSlots<'formatNumber'>
+
+export const FormattedNumberParts = definePartsFormatterComponent(
+ 'formatNumberToParts',
+)
+
+export type FormattedNumberPartsProps =
+ PartsFormatterComponentProps<'formatNumberToParts'>
+
+export type FormattedNumberPartsSlots =
+ PartsFormatterComponentSlots<'formatNumberToParts'>
diff --git a/src/components/plural.ts b/src/components/plural.ts
new file mode 100644
index 0000000..53a0804
--- /dev/null
+++ b/src/components/plural.ts
@@ -0,0 +1,62 @@
+import type { FormatPluralOptions, IntlFormatters } from '@formatjs/intl'
+import {
+ computed,
+ defineComponent,
+ type SetupContext,
+ type SlotsType,
+} from 'vue'
+import { useVIntl } from '../runtime/index.ts'
+import { normalizeAttrs } from './utils/index.ts'
+
+type PluralSelectors = ReturnType
+
+type PluralSlots = {
+ [K in PluralSelectors]?: (props: { value: number }) => any
+}
+
+interface FormattedPluralDefinedProps {
+ value: number
+}
+
+export interface FormattedPluralProps
+ extends FormattedPluralDefinedProps,
+ FormatPluralOptions {}
+
+export interface FormattedPluralSlots extends PluralSlots {
+ default?(props: { children: string | T | (string | T)[] }): any
+}
+
+export const FormattedPlural = defineComponent(
+ (
+ props: FormattedPluralProps,
+ ctx: SetupContext<{}, SlotsType>>>,
+ ) => {
+ const vintl = useVIntl()
+
+ const $options = computed(() => {
+ return normalizeAttrs(ctx.attrs) as FormatPluralOptions
+ })
+
+ return () => {
+ const { value } = props
+
+ const rule = vintl.intl.formatPlural(value, $options.value)
+
+ const ruleSlot = ctx.slots[rule] ?? ctx.slots.other
+
+ const ruleRender = ruleSlot != null ? ruleSlot({ value }) : []
+
+ return ctx.slots.default?.({ children: ruleRender }) ?? ruleRender
+ }
+ },
+ {
+ inheritAttrs: false,
+ props: {
+ value: {
+ type: Number,
+ required: false,
+ default: 0,
+ },
+ },
+ },
+) as (props: FormattedPluralProps) => any
diff --git a/src/components/relativeTime.ts b/src/components/relativeTime.ts
new file mode 100644
index 0000000..7d800d4
--- /dev/null
+++ b/src/components/relativeTime.ts
@@ -0,0 +1,67 @@
+import type { FormatRelativeTimeOptions } from '@formatjs/intl'
+import {
+ createTextVNode,
+ defineComponent,
+ type SetupContext,
+ type SlotsType,
+ type PropType,
+ computed,
+} from 'vue'
+import { useVIntl } from '../runtime/index.ts'
+import { normalizeAttrs } from './utils/index.ts'
+
+interface RealProps {
+ value: number
+ unit: Intl.RelativeTimeFormatUnit
+}
+
+export interface FormattedRelativeTimeProps
+ extends RealProps,
+ FormatRelativeTimeOptions {}
+
+export interface FormattedRelativeTimeSlots {
+ default(props: { formattedValue: string }): any
+}
+
+export const FormattedRelativeTime = defineComponent(
+ (
+ props: FormattedRelativeTimeProps,
+ ctx: SetupContext<{}, SlotsType>>,
+ ) => {
+ const vintl = useVIntl()
+
+ const $options = computed(
+ () => normalizeAttrs(ctx.attrs) as FormatRelativeTimeOptions,
+ )
+
+ return () => {
+ const { value, unit } = props as RealProps
+
+ const formattedValue = vintl.intl.formatRelativeTime(
+ value ?? 0,
+ unit,
+ $options.value,
+ )
+
+ return (
+ ctx.slots.default?.({ formattedValue }) ??
+ createTextVNode(formattedValue)
+ )
+ }
+ },
+ {
+ inheritAttrs: false,
+ props: {
+ unit: {
+ type: String as PropType,
+ required: false,
+ default: 'seconds',
+ },
+ value: {
+ type: Number,
+ required: false,
+ default: 0,
+ },
+ },
+ },
+)
diff --git a/src/components/utils/definersCommon.ts b/src/components/utils/definersCommon.ts
new file mode 100644
index 0000000..c99b1d0
--- /dev/null
+++ b/src/components/utils/definersCommon.ts
@@ -0,0 +1,4 @@
+import type { IntlFormatters } from '@formatjs/intl'
+
+export type PartsFormattersKeys =
+ K extends `${string}Parts` ? K : never
diff --git a/src/components/utils/index.ts b/src/components/utils/index.ts
new file mode 100644
index 0000000..5a02f98
--- /dev/null
+++ b/src/components/utils/index.ts
@@ -0,0 +1,14 @@
+import { camelize } from '../../utils/strings.ts'
+
+export * from './simpleDefiner.ts'
+export * from './partsDefiner.ts'
+
+export function normalizeAttrs(attrs: Record) {
+ const normalizedAttrs: Record = Object.create(null)
+ for (const key in attrs) normalizedAttrs[camelize(key)] = attrs[key]
+ return normalizedAttrs
+}
+
+export function createRecord(): Record {
+ return Object.create(null)
+}
diff --git a/src/components/utils/partsDefiner.ts b/src/components/utils/partsDefiner.ts
new file mode 100644
index 0000000..d998087
--- /dev/null
+++ b/src/components/utils/partsDefiner.ts
@@ -0,0 +1,75 @@
+import type { IntlFormatters } from '@formatjs/intl'
+import {
+ computed,
+ createTextVNode,
+ defineComponent,
+ type SetupContext,
+ type SlotsType,
+} from 'vue'
+import { useVIntl } from '../../runtime/index.ts'
+import type { MessageValueType } from '../../types/index.ts'
+import type { PartsFormattersKeys } from './definersCommon.ts'
+import { normalizeAttrs } from './index.ts'
+import { formatterComponentName } from './simpleDefiner.ts'
+
+export interface PartsFormatterDefinedProps<
+ FormatterName extends PartsFormattersKeys,
+> {
+ value: Parameters[FormatterName]>[0]
+}
+
+export type PartsFormatterOptions =
+ NonNullable[FormatterName]>[1]>
+
+export type PartsFormatterComponentProps<
+ FormatterName extends PartsFormattersKeys,
+> = PartsFormatterDefinedProps &
+ PartsFormatterOptions
+
+export type PartsFormatterComponentSlots<
+ FormatterName extends PartsFormattersKeys,
+> = {
+ default?: (props: { parts: ReturnType }) => any
+}
+
+export function definePartsFormatterComponent<
+ FormatterName extends PartsFormattersKeys,
+>(name: FormatterName) {
+ return defineComponent(
+ (
+ props: PartsFormatterComponentProps,
+ ctx: SetupContext<
+ {},
+ SlotsType>>
+ >,
+ ) => {
+ const vintl = useVIntl()
+
+ const $options = computed(
+ () => normalizeAttrs(ctx.attrs) as PartsFormatterOptions,
+ )
+
+ const $formatter = computed(() => vintl.intl[name])
+
+ return () => {
+ const parts = $formatter.value(
+ props.value as any,
+ $options.value as any,
+ ) as any
+
+ return [
+ ctx.slots.default?.({ parts }) ??
+ createTextVNode(
+ (parts as { value: string }[]).map((part) => part.value).join(''),
+ ),
+ ]
+ }
+ },
+ {
+ name: formatterComponentName(name),
+ inheritAttrs: false,
+ // eslint-disable-next-line vue/require-prop-types
+ props: ['value'],
+ },
+ )
+}
diff --git a/src/components/utils/simpleDefiner.ts b/src/components/utils/simpleDefiner.ts
new file mode 100644
index 0000000..3127ef7
--- /dev/null
+++ b/src/components/utils/simpleDefiner.ts
@@ -0,0 +1,99 @@
+import type { IntlFormatters } from '@formatjs/intl'
+import {
+ computed,
+ createTextVNode,
+ defineComponent,
+ type SetupContext,
+ type SlotsType,
+} from 'vue'
+import { useVIntl } from '../../runtime/index.ts'
+import type { MessageValueType } from '../../types/index.ts'
+import { normalizeAttrs } from './index.ts'
+import type { PartsFormattersKeys } from './definersCommon.ts'
+
+export function formatterComponentName(name: string) {
+ return name.startsWith('format')
+ ? `Formatted${name.split('To').join('')}`
+ : `IntlFormatted$${name}`
+}
+
+type NonUtilityFormattersKeys = K extends `$${string}`
+ ? never
+ : K
+
+type ComplexFormattersKeys =
+ | 'formatMessage'
+ | 'formatDateTimeRange'
+ | 'formatRelativeTime'
+ | 'formatPlural'
+ | 'formatList'
+
+type SimpleFormattersKeys =
+ K extends PartsFormattersKeys
+ ? never
+ : K extends ComplexFormattersKeys
+ ? never
+ : K
+
+export interface SimpleFormatterDefinedProps<
+ FormatterName extends SimpleFormattersKeys,
+> {
+ value: Parameters[FormatterName]>[0]
+}
+
+export type SimpleFormatterOptions =
+ NonNullable[FormatterName]>[1]>
+
+export type SimpleFormatterComponentProps<
+ FormatterName extends SimpleFormattersKeys,
+> = SimpleFormatterDefinedProps &
+ SimpleFormatterOptions
+
+export type SimpleFormatterComponentSlots<
+ FormatterName extends SimpleFormattersKeys,
+> = {
+ default?: (props: {
+ formattedValue: ReturnType
+ }) => any
+}
+
+export function defineSimpleFormatterComponent<
+ FormatterName extends SimpleFormattersKeys,
+>(name: FormatterName) {
+ return defineComponent(
+ (
+ props: SimpleFormatterComponentProps,
+ ctx: SetupContext<
+ {},
+ SlotsType>>
+ >,
+ ) => {
+ const vintl = useVIntl()
+
+ const $options = computed(
+ () =>
+ normalizeAttrs(ctx.attrs) as SimpleFormatterOptions,
+ )
+
+ const $formatter = computed(() => vintl.intl[name])
+
+ return () => {
+ const formattedValue = $formatter.value(
+ props.value as any,
+ $options.value as any,
+ ) as ReturnType
+
+ return [
+ ctx.slots.default?.({ formattedValue }) ??
+ createTextVNode(formattedValue),
+ ]
+ }
+ },
+ {
+ name: formatterComponentName(name),
+ inheritAttrs: false,
+ // eslint-disable-next-line vue/require-prop-types
+ props: ['value'],
+ },
+ )
+}
diff --git a/src/runtime/index.ts b/src/runtime/index.ts
index 060aa2a..c78c93f 100644
--- a/src/runtime/index.ts
+++ b/src/runtime/index.ts
@@ -1,4 +1,3 @@
-export { useI18n, useVIntl } from './useI18n.js'
-export { useFormatters } from './useFormatters.js'
-export { useTranslate } from './useTranslate.js'
+export { useVIntl } from './useVIntl.js'
+export { useMessages, useMessage } from './useMessages.js'
export * from './defineMessages.js'
diff --git a/src/runtime/useFormatters.ts b/src/runtime/useFormatters.ts
deleted file mode 100644
index e2ad4d3..0000000
--- a/src/runtime/useFormatters.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-import type { FormatAliases } from '../partial/intl.js'
-import type { MessageValueType } from '../types/messages.js'
-import { useI18n } from './useI18n.js'
-
-/**
- * A composable to retrieve the format function aliases from the installed
- * controller in the current app.
- *
- * @deprecated Please use `const { formats } = useI18n()` instead.
- * @throws If controller cannot be found in the current application or current
- * application cannot be determined (called outside of `setup()` call).
- */
-export function useFormatters(): FormatAliases {
- return useI18n().formats
-}
diff --git a/src/runtime/useMessages.ts b/src/runtime/useMessages.ts
new file mode 100644
index 0000000..dd46ad9
--- /dev/null
+++ b/src/runtime/useMessages.ts
@@ -0,0 +1,161 @@
+import type { MessageDescriptor as MessageDescriptorBase } from '@formatjs/intl'
+import type { FormatXMLElementFn, PrimitiveType } from 'intl-messageformat'
+import { computed, isRef, reactive, type ComputedRef, type Ref } from 'vue'
+import type { IntlController } from '../controller.ts'
+import type { MessageValueType } from '../index.ts'
+import { useVIntl } from './useVIntl.ts'
+
+type MaybeRef = T | Ref
+
+type PrimitiveValuesRecord = MaybeRef<{
+ [key: string]: MaybeRef>
+}>
+
+type ValuesRecord = MaybeRef<{
+ [key: string]: MaybeRef<
+ PrimitiveType | RichTypes | FormatXMLElementFn
+ >
+}>
+
+interface MessageDescriptor extends MessageDescriptorBase {
+ /**
+ * A record of the values for arguments used in the message. Can contain Vue
+ * references, which will be unwrapped, or be a reference itself.
+ */
+ values?: ValuesRecord
+}
+
+type MessageDescriptorOutput<
+ Descriptor extends MessageDescriptor,
+ RichTypes,
+> = [Descriptor['values']] extends [undefined]
+ ? string
+ : Descriptor['values'] extends PrimitiveValuesRecord
+ ? string
+ : Array | RichTypes | string
+
+type MessageDescriptorsRecord = Record<
+ string,
+ MessageDescriptor
+>
+
+type MessageDescriptorsRecordOutput<
+ Descriptor extends MessageDescriptorsRecord,
+ RichTypes,
+> = {
+ [K in keyof Descriptor]: MessageDescriptorOutput
+}
+
+function formatMessage<
+ Descriptor extends MessageDescriptor,
+ RichTypes,
+>(
+ message: Descriptor,
+ vintl: IntlController,
+): MessageDescriptorOutput {
+ const values = Object.create(null)
+ const rawInputs = message.values
+
+ if (isRef(rawInputs)) {
+ Object.assign(values, rawInputs.value)
+ } else if (rawInputs != null) {
+ for (const k in rawInputs) {
+ const input = rawInputs[k]
+ values[k] = isRef(input) ? input.value : input
+ }
+ }
+
+ return vintl.intl.formatMessage(message, values)
+}
+
+/**
+ * Accepts a plain object of extended message descriptors, which may contain
+ * `values` alongside the message declaration itself. It then creates an object
+ * where each descriptor is mapped to a formatted. The object is reactive and
+ * message properties will be updating when the language, messages or values in
+ * the messages change.
+ *
+ * You can use `toRef` or `useMessages` to create read-only references for
+ * individual.
+ *
+ * @example
+ * const messages = useMessages({
+ * farewell: {
+ * id: 'farewell',
+ * defaultMessage: 'Goodbye, {user}!',
+ * values: {
+ * user: computed(() => user.value.displayName),
+ * },
+ * },
+ * richText: {
+ * id: 'rich-text',
+ * defaultMessage: 'This text is red.',
+ * values: {
+ * red(children) {
+ * return h('span', { style: { color: 'red' } }, [children])
+ * },
+ * },
+ * },
+ * })
+ *
+ * console.log(messages.farewell) // 'Goodbye, Andrea Rees!'
+ *
+ * @param messages A record of message descriptors.
+ * @returns A reactive map of messages.
+ */
+export function useMessages<
+ Descriptor extends MessageDescriptorsRecord,
+ RichTypes = MessageValueType,
+>(messages: Descriptor) {
+ const vintl = useVIntl()
+
+ type PreOutput = {
+ [K in keyof MessageDescriptorsRecordOutput<
+ Descriptor,
+ RichTypes
+ >]: ComputedRef[K]>
+ }
+
+ const target: PreOutput = Object.create(null)
+
+ for (const key of Object.keys(messages) as (keyof Descriptor)[]) {
+ const message = messages[key]
+
+ type Message = Descriptor[typeof key]
+
+ target[key] = computed(() =>
+ formatMessage(message, vintl),
+ )
+ }
+
+ return reactive(target)
+}
+
+/**
+ * Accepts an extended message descriptor, which may contain `values` alongside
+ * the message declaration itself. It then returns a read-only reference that
+ * gets updated when the language, messages or the values for the message
+ * change.
+ *
+ * @example
+ * const helloMessage = useMessage({
+ * id: 'hello',
+ * defaultMessage: 'Hello, {user}!',
+ * values: {
+ * user: computed(() => user.value.displayName),
+ * },
+ * })
+ *
+ * console.log(helloMessage.value) // 'Hello, Andrea Rees!'
+ *
+ * @param message A message descriptor.
+ * @returns A read-only reference to the actual formatted message.
+ */
+export function useMessage<
+ Descriptor extends MessageDescriptor,
+ RichTypes = MessageValueType,
+>(message: Descriptor) {
+ const vintl = useVIntl()
+
+ return computed(() => formatMessage(message, vintl))
+}
diff --git a/src/runtime/useTranslate.ts b/src/runtime/useTranslate.ts
deleted file mode 100644
index 657fd6f..0000000
--- a/src/runtime/useTranslate.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-import type { TranslateFunction } from '../types/translateFunction.js'
-import { useI18n } from './useI18n.js'
-
-/**
- * A composable to retrieve function to format a message from the installed
- * controller in the current app.
- *
- * @deprecated Please use `const { formatMessage } = useI18n()` instead.
- * @throws If controller cannot be found in the current application or current
- * application cannot be determined (called outside of `setup()` call).
- */
-export function useTranslate(): TranslateFunction {
- return useI18n().formatMessage
-}
diff --git a/src/runtime/useI18n.ts b/src/runtime/useVIntl.ts
similarity index 72%
rename from src/runtime/useI18n.ts
rename to src/runtime/useVIntl.ts
index d3a313d..79918d4 100644
--- a/src/runtime/useI18n.ts
+++ b/src/runtime/useVIntl.ts
@@ -20,13 +20,3 @@ export function useVIntl() {
return controller as IntlController
}
-
-/**
- * Alias for {@link useVIntl}.
- *
- * @deprecated This composable name is deprecated and will be removed in next
- * major version. Please use {@link useVIntl} instead.
- */
-export function useI18n() {
- return useVIntl()
-}
diff --git a/src/utils/Fragment.ts b/src/utils/Fragment.ts
deleted file mode 100644
index 055ad89..0000000
--- a/src/utils/Fragment.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-import type { VNode } from 'vue'
-
-interface Props {
- of: VNode[] | VNode
-}
-
-export function Fragment(props: Props) {
- return Array.isArray(props.of) ? props.of : [props.of]
-}
diff --git a/src/utils/index.ts b/src/utils/index.ts
index 4ff8df2..5e95e60 100644
--- a/src/utils/index.ts
+++ b/src/utils/index.ts
@@ -1 +1 @@
-export { Fragment } from './Fragment.js'
+export { Fragment } from '../components/fragment.js'
diff --git a/src/utils/strings.ts b/src/utils/strings.ts
new file mode 100644
index 0000000..c126138
--- /dev/null
+++ b/src/utils/strings.ts
@@ -0,0 +1,5 @@
+const camelRegExp = /-(\w)/g
+
+export function camelize(value: string) {
+ return value.replace(camelRegExp, (_, c) => c.toUpperCase())
+}
diff --git a/test/components/FormattedList/index.test.ts b/test/components/FormattedList/index.test.ts
new file mode 100644
index 0000000..55f4986
--- /dev/null
+++ b/test/components/FormattedList/index.test.ts
@@ -0,0 +1,59 @@
+import { afterAll, beforeEach, afterEach, describe, expect, it } from 'vitest'
+import { cleanup, fireEvent, render } from '@testing-library/vue'
+import {
+ createVIntlPlugin,
+ withAbnormalSpacesReplaced,
+} from '../../utils/index.ts'
+import { ListDisplay } from './listDisplay.tsx'
+
+describe('FormattedList', () => {
+ afterAll(() => cleanup())
+
+ const vintl = createVIntlPlugin(['en-US', 'uk'])
+ const { plugin, controller, resetController } = vintl
+
+ const { getByText, getByTestId } = render(ListDisplay, {
+ global: { plugins: [plugin] },
+ })
+
+ let list: HTMLElement
+
+ const refreshList = () => (list = getByTestId('list-display'))
+
+ beforeEach(async () => {
+ await fireEvent.click(getByText('Reset'))
+ refreshList()
+ })
+
+ afterEach(resetController)
+
+ it('renders', async () => {
+ expect(list.textContent).toMatchInlineSnapshot('"1, 2, or 3"')
+
+ await fireEvent.click(getByText('Add item'))
+ expect(list.textContent).toMatchInlineSnapshot('"1, 2, 3, or 4"')
+ })
+
+ it('changes locale', async () => {
+ await controller.changeLocale('uk')
+ const content = withAbnormalSpacesReplaced(list.textContent!)
+ expect(content).toMatchInlineSnapshot('"1, 2 або 3"')
+ })
+
+ it('renders as a slot', async () => {
+ await fireEvent.click(getByText('Slots on'))
+ const content = refreshList().textContent
+ expect(content).toMatchInlineSnapshot('"List is: 1, 2, or 3"')
+
+ const slot = getByTestId('list-slot')
+ expect(slot.textContent!).toMatchInlineSnapshot('"1, 2, or 3"')
+ })
+
+ it('renders JSX items', async () => {
+ await fireEvent.click(getByText('JSX on'))
+
+ expect(list.querySelector('b')).toBeDefined()
+
+ await fireEvent.click(getByText('Press me'))
+ })
+})
diff --git a/test/components/FormattedList/listDisplay.tsx b/test/components/FormattedList/listDisplay.tsx
new file mode 100644
index 0000000..f16d41d
--- /dev/null
+++ b/test/components/FormattedList/listDisplay.tsx
@@ -0,0 +1,65 @@
+import { computed, defineComponent, ref } from 'vue'
+import {
+ FormattedList,
+ type FormattedListSlots,
+} from '../../../dist/components'
+
+export const ListDisplay = defineComponent(() => {
+ const list = ref(['1', '2', '3'])
+
+ let increment = 3
+
+ const addListItem = () => list.value.push(String(++increment))
+
+ const useSlots = ref(false)
+ const enableSlots = () => (useSlots.value = true)
+
+ const useJSXNodes = ref(false)
+ const enableJSXNodes = () => (useJSXNodes.value = true)
+
+ const listToRender = computed(() => {
+ return useJSXNodes.value
+ ? [...list.value, Bold, ]
+ : list.value
+ })
+
+ const reset = () => {
+ list.value = ['1', '2', '3']
+ increment = 3
+ useSlots.value = false
+ useJSXNodes.value = false
+ }
+
+ return () => {
+ let display: JSX.Element
+
+ if (useSlots.value) {
+ const slots: FormattedListSlots = {
+ default: ({ children }) => (
+ <>
+ {'List is: '}
+ {children}
+ >
+ ),
+ }
+
+ display = (
+
+ {slots}
+
+ )
+ } else {
+ display =
+ }
+
+ return (
+ <>
+
{display}
+
+
+
+
+ >
+ )
+ }
+})
diff --git a/test/components/FormattedListParts/index.test.ts b/test/components/FormattedListParts/index.test.ts
new file mode 100644
index 0000000..310c58e
--- /dev/null
+++ b/test/components/FormattedListParts/index.test.ts
@@ -0,0 +1,62 @@
+import { afterAll, afterEach, beforeEach, describe, expect, it } from 'vitest'
+import { cleanup, fireEvent, render } from '@testing-library/vue'
+import {
+ createVIntlPlugin,
+ withAbnormalSpacesReplaced,
+} from '../../utils/index.ts'
+import { ListPartsDisplay } from './listPartsDisplay.tsx'
+
+describe('FormattedListParts', () => {
+ afterAll(() => cleanup())
+
+ const vintl = createVIntlPlugin(['en-US', 'uk'])
+ const { plugin, controller, resetController } = vintl
+
+ const { getByText, getByTestId } = render(ListPartsDisplay, {
+ global: { plugins: [plugin] },
+ })
+
+ let list: HTMLElement
+
+ const refreshList = () => (list = getByTestId('list-display'))
+
+ beforeEach(async () => {
+ await fireEvent.click(getByText('Reset'))
+ refreshList()
+ })
+
+ afterEach(resetController)
+
+ it('renders', async () => {
+ expect(list.textContent).toMatchInlineSnapshot('"1, 2, or 3"')
+
+ await fireEvent.click(getByText('Add item'))
+ expect(list.textContent).toMatchInlineSnapshot('"1, 2, 3, or 4"')
+ })
+
+ it('changes locale', async () => {
+ await controller.changeLocale('uk')
+
+ const content = withAbnormalSpacesReplaced(list.textContent!)
+ expect(content).toMatchInlineSnapshot('"1, 2 або 3"')
+ })
+
+ it('renders as a slot', async () => {
+ await fireEvent.click(getByText('Slots on'))
+
+ const content = refreshList().textContent
+ expect(content).toMatchInlineSnapshot('"List is: 1, 2, or 3"')
+
+ const slot = getByTestId('list-slot')
+ expect(slot.textContent!).toMatchInlineSnapshot('"1, 2, or 3"')
+ })
+
+ it('renders JSX items', async () => {
+ await fireEvent.click(getByText('JSX on'))
+
+ const bold = refreshList().querySelector('b')
+ expect(bold?.textContent).toMatchInlineSnapshot('"Bold"')
+
+ expect(getByText('Press me')).toBeDefined()
+ })
+})
diff --git a/test/components/FormattedListParts/listPartsDisplay.tsx b/test/components/FormattedListParts/listPartsDisplay.tsx
new file mode 100644
index 0000000..9ba3cd6
--- /dev/null
+++ b/test/components/FormattedListParts/listPartsDisplay.tsx
@@ -0,0 +1,79 @@
+import { computed, defineComponent, ref } from 'vue'
+import {
+ FormattedListParts,
+ type FormattedListPartsSlots,
+} from '../../../dist/components'
+
+export const ListPartsDisplay = defineComponent(() => {
+ const list = ref(['1', '2', '3'])
+
+ let increment = 3
+
+ const addListItem = () => list.value.push(String(++increment))
+
+ const useSlots = ref(false)
+ const enableSlots = () => (useSlots.value = true)
+
+ const useJSXNodes = ref(false)
+ const enableJSXNodes = () => (useJSXNodes.value = true)
+
+ const listToRender = computed(() => {
+ return useJSXNodes.value
+ ? [...list.value, Bold, ]
+ : list.value
+ })
+
+ const reset = () => {
+ list.value = ['1', '2', '3']
+ increment = 3
+ useSlots.value = false
+ useJSXNodes.value = false
+ }
+
+ return () => {
+ let display: JSX.Element
+
+ if (useSlots.value) {
+ const slots: FormattedListPartsSlots = {
+ default: ({ parts }) => (
+ <>
+ {'List is: '}
+
+ {parts.map((part) => (
+ - {part.value}
+ ))}
+
+ >
+ ),
+ }
+
+ display = (
+
+ {slots}
+
+ )
+ } else {
+ display = (
+
+ )
+ }
+
+ return (
+ <>
+ {display}
+
+
+
+
+ >
+ )
+ }
+})
diff --git a/test/components/FormattedMessage/index.test.ts b/test/components/FormattedMessage/index.test.ts
new file mode 100644
index 0000000..a27fdb8
--- /dev/null
+++ b/test/components/FormattedMessage/index.test.ts
@@ -0,0 +1,65 @@
+import { afterAll, afterEach, beforeEach, describe, expect, it } from 'vitest'
+import { cleanup, fireEvent, render } from '@testing-library/vue'
+import { createVIntlPlugin } from '../../utils/index.ts'
+import { messagesPayload, MessageDisplay } from './messageDisplay.tsx'
+
+describe('FormattedMessage', () => {
+ afterAll(() => cleanup())
+
+ const vintl = createVIntlPlugin(['en-US', 'uk'], (e) => {
+ e.addMessages(messagesPayload?.[e.locale.tag] ?? {})
+ })
+
+ const { plugin, controller, resetController } = vintl
+
+ const { getByText, getByTestId } = render(MessageDisplay, {
+ global: { plugins: [plugin] },
+ })
+
+ let display: HTMLElement
+
+ const refreshDisplay = () => (display = getByTestId('message-display'))
+
+ beforeEach(async () => {
+ await fireEvent.click(getByText('Reset'))
+ refreshDisplay()
+ })
+
+ afterEach(async () => {
+ await resetController()
+ })
+
+ const content = () => display.textContent
+
+ it('renders', async () => {
+ expect(content()).toMatchInlineSnapshot(
+ '"Hello, Oleksandr. You have 1 new message"',
+ )
+
+ await fireEvent.click(getByText('+1'))
+ expect(content()).toMatchInlineSnapshot(
+ '"Hello, Oleksandr. You have 2 new messages"',
+ )
+ })
+
+ it('changes locale', async () => {
+ await controller.changeLocale('uk')
+ expect(content()).toMatchInlineSnapshot(
+ '"Привіт, Oleksandr. У вас 1 нове повідомлення"',
+ )
+
+ await fireEvent.click(getByText('+1'))
+ expect(content()).toMatchInlineSnapshot(
+ '"Привіт, Oleksandr. У вас 2 нових повідомлень"',
+ )
+ })
+
+ it('renders with slots', async () => {
+ await fireEvent.click(getByText('Slots on'))
+ refreshDisplay()
+
+ expect(display.innerHTML).toMatchInlineSnapshot(
+ '"Hello, Oleksandr 👋 You have 1 new message"',
+ )
+ })
+})
diff --git a/test/components/FormattedMessage/messageDisplay.tsx b/test/components/FormattedMessage/messageDisplay.tsx
new file mode 100644
index 0000000..28d5649
--- /dev/null
+++ b/test/components/FormattedMessage/messageDisplay.tsx
@@ -0,0 +1,89 @@
+import { defineMessages } from '@formatjs/intl'
+import { defineComponent, ref } from 'vue'
+import {
+ FormattedMessage,
+ type FormattedMessageSlots,
+} from '../../../dist/components'
+
+const messages = defineMessages({
+ greeting: {
+ id: 'greeting',
+ defaultMessage:
+ 'Hello, {name}. You have {count, plural, one {# new message} other {# new messages}}',
+ },
+ greetingBold: {
+ id: 'greeting.bold',
+ defaultMessage:
+ 'Hello, {name} {wave_emoji} You have {count, plural, one {# new message} other {# new messages}}',
+ },
+} as const)
+
+export const messagesPayload: Record<
+ string,
+ {
+ [K in (typeof messages)[keyof typeof messages]['id']]: string
+ }
+> = {
+ 'en-US': {
+ greeting: messages.greeting.defaultMessage,
+ 'greeting.bold': messages.greetingBold.defaultMessage,
+ },
+ uk: {
+ greeting:
+ 'Привіт, {name}. У вас {count, plural, one {# нове повідомлення} other {# нових повідомлень}}',
+ 'greeting.bold':
+ 'Привіт, {name} {wave_emoji} У вас {count, plural, one {# нове повідомлення} other {# нових повідомлень}}',
+ },
+} as const
+
+export const MessageDisplay = defineComponent(() => {
+ const name = ref('Oleksandr')
+
+ const unreadMessages = ref(1)
+ const incrementByOne = () => (unreadMessages.value += 1)
+
+ const useSlots = ref(false)
+ const enableSlots = () => (useSlots.value = true)
+
+ const reset = () => {
+ name.value = 'Oleksandr'
+ unreadMessages.value = 1
+ useSlots.value = false
+ }
+
+ return () => {
+ let display: JSX.Element
+
+ if (useSlots.value) {
+ const slots: FormattedMessageSlots = {
+ '~wave_emoji': () => 👋,
+ bold: ({ children }) => {children},
+ }
+
+ display = (
+
+ {slots}
+
+ )
+ } else {
+ display = (
+
+ )
+ }
+
+ return (
+ <>
+ {display}
+
+
+
+ >
+ )
+ }
+})
diff --git a/test/components/FormattedNumber/counter.tsx b/test/components/FormattedNumber/counter.tsx
new file mode 100644
index 0000000..21d8733
--- /dev/null
+++ b/test/components/FormattedNumber/counter.tsx
@@ -0,0 +1,54 @@
+import { defineComponent, ref } from 'vue'
+import {
+ FormattedNumber,
+ type FormattedNumberSlots,
+} from '../../../dist/components'
+
+export const Counter = defineComponent(() => {
+ const count = ref(0)
+ const incrementByOne = () => (count.value += 1)
+ const incrementByThousand = () => (count.value += 1000)
+
+ const useSlots = ref(false)
+ const enableSlots = () => {
+ useSlots.value = true
+ }
+
+ const reset = () => {
+ count.value = 0
+ useSlots.value = false
+ }
+
+ return () => {
+ let display: JSX.Element
+
+ if (useSlots.value) {
+ const slots: FormattedNumberSlots = {
+ default: ({ formattedValue }) => (
+ <>
+ {'Count is: '}
+ {formattedValue}
+ >
+ ),
+ }
+
+ display = (
+
+ {slots}
+
+ )
+ } else {
+ display =
+ }
+
+ return (
+ <>
+ {display}
+
+
+
+
+ >
+ )
+ }
+})
diff --git a/test/components/FormattedNumber/index.test.ts b/test/components/FormattedNumber/index.test.ts
new file mode 100644
index 0000000..e1b3cf4
--- /dev/null
+++ b/test/components/FormattedNumber/index.test.ts
@@ -0,0 +1,70 @@
+import { afterAll, afterEach, beforeEach, describe, expect, it } from 'vitest'
+import { cleanup, fireEvent, render } from '@testing-library/vue'
+import {
+ createVIntlPlugin,
+ withAbnormalSpacesReplaced,
+} from '../../utils/index.ts'
+import { Counter } from './counter.tsx'
+
+describe('FormattedNumber', () => {
+ afterAll(() => cleanup())
+
+ const vintl = createVIntlPlugin(['en-US', 'uk'])
+ const { plugin, controller, resetController } = vintl
+
+ const { getByText, getByTestId } = render(Counter, {
+ global: { plugins: [plugin] },
+ })
+
+ let counter: HTMLElement
+ const refreshCounter = () => (counter = getByTestId('counter'))
+
+ beforeEach(async () => {
+ await fireEvent.click(getByText('Reset'))
+ refreshCounter()
+ })
+
+ afterEach(resetController)
+
+ const content = () => withAbnormalSpacesReplaced(counter.textContent!)
+
+ it('renders', async () => {
+ expect(content()).toMatchInlineSnapshot('"0"')
+
+ await fireEvent.click(getByText('+1'))
+ expect(content()).toMatchInlineSnapshot('"1"')
+
+ await fireEvent.click(getByText('+1000'))
+ expect(content()).toMatchInlineSnapshot('"1K"')
+ })
+
+ it('changes locale', async () => {
+ await controller.changeLocale('uk')
+
+ expect(content()).toMatchInlineSnapshot('"0"')
+
+ await fireEvent.click(getByText('+1'))
+ expect(content()).toMatchInlineSnapshot('"1"')
+
+ await fireEvent.click(getByText('+1000'))
+ expect(content()).toMatchInlineSnapshot('"1 тис."')
+ })
+
+ it('renders as a slot', async () => {
+ await fireEvent.click(getByText('Slots on'))
+
+ refreshCounter()
+
+ expect(content()).toMatchInlineSnapshot('"Count is: 0"')
+
+ await fireEvent.click(getByText('+1'))
+ expect(content()).toMatchInlineSnapshot('"Count is: 1"')
+
+ await fireEvent.click(getByText('+1000'))
+ expect(content()).toMatchInlineSnapshot('"Count is: 1K"')
+
+ const slot = getByTestId('counter-slot')
+ const slotContent = withAbnormalSpacesReplaced(slot.textContent!)
+ expect(slotContent).toMatchInlineSnapshot('"1K"')
+ })
+})
diff --git a/test/components/FormattedNumberParts/counter.tsx b/test/components/FormattedNumberParts/counter.tsx
new file mode 100644
index 0000000..f5f390a
--- /dev/null
+++ b/test/components/FormattedNumberParts/counter.tsx
@@ -0,0 +1,43 @@
+import { defineComponent, ref } from 'vue'
+import {
+ FormattedNumberParts,
+ type FormattedNumberPartsSlots,
+} from '../../../dist/components'
+
+export const Counter = defineComponent(() => {
+ const count = ref(0)
+
+ const incrementByOne = () => {
+ count.value++
+ }
+
+ const incrementByThousand = () => {
+ count.value += 1000
+ }
+
+ const reset = () => {
+ count.value = 0
+ }
+
+ return () => {
+ const slots: FormattedNumberPartsSlots = {
+ default: ({ parts }) =>
+ parts.map((part) =>
+ part.type === 'integer' ? {part.value} : part.value,
+ ),
+ }
+
+ return (
+
+
+
+ {slots}
+
+
+
+
+
+
+ )
+ }
+})
diff --git a/test/components/FormattedNumberParts/index.test.ts b/test/components/FormattedNumberParts/index.test.ts
new file mode 100644
index 0000000..e334329
--- /dev/null
+++ b/test/components/FormattedNumberParts/index.test.ts
@@ -0,0 +1,58 @@
+import { afterAll, afterEach, beforeEach, describe, expect, it } from 'vitest'
+import { cleanup, fireEvent, render } from '@testing-library/vue'
+import {
+ createVIntlPlugin,
+ withAbnormalSpacesReplaced,
+} from '../../utils/index.ts'
+import { Counter } from './counter.tsx'
+
+describe('FormattedNumberParts', () => {
+ afterAll(() => cleanup())
+
+ const vintl = createVIntlPlugin(['en-US', 'uk'])
+ const { plugin, controller, resetController } = vintl
+
+ const { getByText, getByTestId } = render(Counter, {
+ global: { plugins: [plugin] },
+ })
+
+ let counter: HTMLElement
+ const refreshCounter = () => (counter = getByTestId('counter'))
+
+ const content = () => withAbnormalSpacesReplaced(counter.textContent!)
+
+ beforeEach(async () => {
+ await fireEvent.click(getByText('Reset'))
+ refreshCounter()
+ })
+
+ afterEach(resetController)
+
+ it('renders', async () => {
+ expect(content()).toBe('0')
+
+ await fireEvent.click(getByText('+1'))
+ expect(content()).toBe('1')
+
+ await fireEvent.click(getByText('+1000'))
+ expect(content()).toBe('1K')
+
+ const integerParts = counter.querySelectorAll('b')
+ expect(integerParts).toHaveLength(1)
+ expect(integerParts[0].textContent).toBe('1')
+ })
+
+ it('changes locale', async () => {
+ await controller.changeLocale('uk')
+
+ await fireEvent.click(getByText('+1'))
+ expect(content()).toBe('1')
+
+ await fireEvent.click(getByText('+1000'))
+ expect(content()).toBe('1 тис.')
+
+ const integerParts = counter.querySelectorAll('b')
+ expect(integerParts).toHaveLength(1)
+ expect(integerParts[0].textContent).toBe('1')
+ })
+})
diff --git a/test/components/FormattedPlural/index.test.ts b/test/components/FormattedPlural/index.test.ts
new file mode 100644
index 0000000..8bab8f6
--- /dev/null
+++ b/test/components/FormattedPlural/index.test.ts
@@ -0,0 +1,97 @@
+import { afterAll, afterEach, beforeEach, describe, expect, it } from 'vitest'
+import { cleanup, fireEvent, render } from '@testing-library/vue'
+import { createVIntlPlugin } from '../../utils/index.ts'
+import { PluralDisplay } from './pluralDisplay.tsx'
+
+describe('FormattedPlural', () => {
+ afterAll(() => cleanup())
+
+ const vintl = createVIntlPlugin(['en-US', 'uk'])
+ const { plugin, controller, resetController } = vintl
+
+ const { getByText, getByTestId } = render(PluralDisplay, {
+ global: { plugins: [plugin] },
+ })
+
+ let display: HTMLElement
+ const refreshDisplay = () => (display = getByTestId('plural-display'))
+
+ afterEach(resetController)
+
+ beforeEach(async () => {
+ await fireEvent.click(getByText('Reset'))
+ refreshDisplay()
+ })
+
+ const content = () => display.textContent!
+
+ it('renders', async () => {
+ // 0 => other
+ // 1 => one
+ // 2 => other
+
+ expect(content()).toMatchInlineSnapshot('"other with value 0"')
+
+ await fireEvent.click(getByText('+1'))
+ expect(content()).toMatchInlineSnapshot('"one with value 1"')
+
+ await fireEvent.click(getByText('+1'))
+ expect(content()).toMatchInlineSnapshot('"other with value 2"')
+ })
+
+ it('respects type', async () => {
+ // 0 => other
+ // 1 => one
+ // 2 => two
+ // 3 => few
+
+ await fireEvent.click(getByText('Switch to ordinal'))
+ expect(content()).toMatchInlineSnapshot('"other with value 0"')
+
+ await fireEvent.click(getByText('+1'))
+ expect(content()).toMatchInlineSnapshot('"one with value 1"')
+
+ await fireEvent.click(getByText('+1'))
+ expect(content()).toMatchInlineSnapshot('"two with value 2"')
+
+ await fireEvent.click(getByText('+1'))
+ expect(content()).toMatchInlineSnapshot('"few with value 3"')
+ })
+
+ it('changes locale', async () => {
+ // 0 => many
+ // 1 => one
+ // 2 => few
+
+ await controller.changeLocale('uk')
+ expect(content()).toMatchInlineSnapshot('"many with value 0"')
+
+ await fireEvent.click(getByText('+1'))
+ expect(content()).toMatchInlineSnapshot('"one with value 1"')
+
+ await fireEvent.click(getByText('+1'))
+ expect(content()).toMatchInlineSnapshot('"few with value 2"')
+ })
+
+ it('fallbacks correctly', async () => {
+ // 0 => many => other
+ // 1 => one => one
+ // 2 => few => other
+
+ await controller.changeLocale('uk')
+ await fireEvent.click(getByText('Handle selection of slots'))
+ expect(content()).toMatchInlineSnapshot('"other with value 0"')
+
+ await fireEvent.click(getByText('+1'))
+ expect(content()).toMatchInlineSnapshot('"one with value 1"')
+
+ await fireEvent.click(getByText('+1'))
+ expect(content()).toMatchInlineSnapshot('"other with value 2"')
+ })
+
+ it('renders nothing if no slots handled', async () => {
+ // * => [nothing]
+ await fireEvent.click(getByText('Handle no slots'))
+ expect(content()).toMatchInlineSnapshot('""')
+ })
+})
diff --git a/test/components/FormattedPlural/pluralDisplay.tsx b/test/components/FormattedPlural/pluralDisplay.tsx
new file mode 100644
index 0000000..0358b9d
--- /dev/null
+++ b/test/components/FormattedPlural/pluralDisplay.tsx
@@ -0,0 +1,66 @@
+import { defineComponent, ref } from 'vue'
+import {
+ FormattedPlural,
+ type FormattedPluralSlots,
+} from '../../../dist/components'
+
+export const PluralDisplay = defineComponent(() => {
+ const count = ref(0)
+ const incrementByOne = () => (count.value += 1)
+
+ const slotsHandling = ref<'full' | 'partial' | 'none'>('full')
+ const handleSelectionOfSlots = () => (slotsHandling.value = 'partial')
+ const handleNoSlots = () => (slotsHandling.value = 'none')
+
+ const pluralType = ref('cardinal')
+ const switchToOrdinal = () => (pluralType.value = 'ordinal')
+
+ const reset = () => {
+ count.value = 0
+ slotsHandling.value = 'full'
+ pluralType.value = 'cardinal'
+ }
+
+ return () => {
+ let slots: FormattedPluralSlots
+
+ switch (slotsHandling.value) {
+ case 'full':
+ slots = {
+ zero: ({ value }) => `zero with value ${value}`,
+ one: ({ value }) => `one with value ${value}`,
+ two: ({ value }) => `two with value ${value}`,
+ few: ({ value }) => `few with value ${value}`,
+ many: ({ value }) => `many with value ${value}`,
+ other: ({ value }) => `other with value ${value}`,
+ }
+ break
+ case 'partial':
+ slots = {
+ one: ({ value }) => `one with value ${value}`,
+ other: ({ value }) => `other with value ${value}`,
+ }
+ break
+ case 'none':
+ slots = {}
+ break
+ }
+
+ return (
+ <>
+
+
+ {slots}
+
+
+
+
+
+
+
+ >
+ )
+ }
+})
diff --git a/test/components/FormattedRelativeTime/index.test.ts b/test/components/FormattedRelativeTime/index.test.ts
new file mode 100644
index 0000000..0f432a7
--- /dev/null
+++ b/test/components/FormattedRelativeTime/index.test.ts
@@ -0,0 +1,67 @@
+import { afterAll, afterEach, beforeEach, describe, expect, it } from 'vitest'
+import { cleanup, fireEvent, render } from '@testing-library/vue'
+import {
+ createVIntlPlugin,
+ withAbnormalSpacesReplaced,
+} from '../../utils/index.ts'
+import { RelativeTimeDisplay } from './relativeTimeDisplay.tsx'
+
+describe('FormattedRelativeTime', () => {
+ afterAll(() => cleanup())
+
+ const vintl = createVIntlPlugin(['en-US', 'uk'])
+ const { plugin, controller, resetController } = vintl
+
+ const { getByText, getByTestId } = render(RelativeTimeDisplay, {
+ global: { plugins: [plugin] },
+ })
+
+ let display: HTMLElement
+ const refreshDisplay = () => (display = getByTestId('time-display'))
+
+ beforeEach(async () => {
+ await fireEvent.click(getByText('Reset'))
+ refreshDisplay()
+ })
+
+ afterEach(resetController)
+
+ const content = () => withAbnormalSpacesReplaced(display.textContent!)
+
+ it('renders', async () => {
+ expect(content()).toMatchInlineSnapshot('"in 0 seconds"')
+
+ await fireEvent.click(getByText('+1'))
+ expect(content()).toMatchInlineSnapshot('"in 1 second"')
+
+ await fireEvent.click(getByText('Use minutes'))
+ expect(content()).toMatchInlineSnapshot('"in 1 minute"')
+ })
+
+ it('changes locale', async () => {
+ await controller.changeLocale('uk')
+ expect(content()).toMatchInlineSnapshot('"через 0 секунд"')
+
+ await fireEvent.click(getByText('+1'))
+ expect(content()).toMatchInlineSnapshot('"через 1 секунду"')
+
+ await fireEvent.click(getByText('Use minutes'))
+ expect(content()).toMatchInlineSnapshot('"через 1 хвилину"')
+ })
+
+ it('renders as a slot', async () => {
+ await fireEvent.click(getByText('Slots on'))
+ refreshDisplay()
+
+ expect(content()).toMatchInlineSnapshot('"Relative time is: in 0 seconds"')
+
+ await fireEvent.click(getByText('+1'))
+ expect(content()).toMatchInlineSnapshot('"Relative time is: in 1 second"')
+
+ await fireEvent.click(getByText('Use minutes'))
+ expect(content()).toMatchInlineSnapshot('"Relative time is: in 1 minute"')
+
+ const slot = getByTestId('time-slot')
+ expect(slot.textContent!).toMatchInlineSnapshot('"in 1 minute"')
+ })
+})
diff --git a/test/components/FormattedRelativeTime/relativeTimeDisplay.tsx b/test/components/FormattedRelativeTime/relativeTimeDisplay.tsx
new file mode 100644
index 0000000..8fb2203
--- /dev/null
+++ b/test/components/FormattedRelativeTime/relativeTimeDisplay.tsx
@@ -0,0 +1,55 @@
+import { defineComponent, ref } from 'vue'
+import {
+ FormattedRelativeTime,
+ type FormattedRelativeTimeSlots,
+} from '../../../dist/components'
+
+export const RelativeTimeDisplay = defineComponent(() => {
+ const amount = ref(0)
+ const incrementByOne = () => (amount.value += 1)
+
+ const unit = ref('seconds')
+ const switchToMinutes = () => (unit.value = 'minutes')
+
+ const useSlots = ref(false)
+ const enableSlots = () => (useSlots.value = true)
+
+ const reset = () => {
+ amount.value = 0
+ unit.value = 'seconds'
+ useSlots.value = false
+ }
+
+ return () => {
+ let display: JSX.Element
+
+ if (useSlots.value) {
+ display = (
+
+ {
+ {
+ default: ({ formattedValue }) => (
+ <>
+ {'Relative time is: '}
+ {formattedValue}
+ >
+ ),
+ } satisfies FormattedRelativeTimeSlots
+ }
+
+ )
+ } else {
+ display =
+ }
+
+ return (
+ <>
+ {display}
+
+
+
+
+ >
+ )
+ }
+})
diff --git a/test/composables/useMessages/index.test.ts b/test/composables/useMessages/index.test.ts
new file mode 100644
index 0000000..3ab910d
--- /dev/null
+++ b/test/composables/useMessages/index.test.ts
@@ -0,0 +1,75 @@
+import { cleanup, fireEvent, render } from '@testing-library/vue'
+import { afterAll, afterEach, beforeEach, describe, expect, it } from 'vitest'
+import { createVIntlPlugin } from '../../utils/index.ts'
+import { messagesPayload, MessageDisplay } from './messageDisplay.tsx'
+
+describe('useMessages', () => {
+ afterAll(() => cleanup())
+
+ const vintl = createVIntlPlugin(['en-US', 'uk'], messagesPayload)
+
+ const { plugin, controller, resetController } = vintl
+
+ afterEach(resetController)
+
+ const { getByText, getByTestId } = render(MessageDisplay, {
+ global: { plugins: [plugin] },
+ })
+
+ // const display = getByTestId('message-display')
+ const messageContainer = getByTestId('message-container')
+ const warningContainer = getByTestId('warning-container')
+
+ beforeEach(() => fireEvent.click(getByText('Reset')))
+
+ const messageHTML = () => messageContainer.innerHTML
+ const warningHTML = () => warningContainer.innerHTML
+
+ it('renders', async () => {
+ expect(messageHTML()).toMatchInlineSnapshot(
+ '"Hello, Andrei!You don\'t have unread messages."',
+ )
+ expect(warningHTML()).toMatchInlineSnapshot(
+ '"Warning! This is a warning."',
+ )
+
+ await fireEvent.click(getByText('Add unread'))
+ expect(messageHTML()).toMatchInlineSnapshot(
+ '"Hello, Andrei!You have 1 unread message."',
+ )
+
+ await fireEvent.click(getByText('Set intent to goodbye'))
+ expect(messageHTML()).toMatchInlineSnapshot(
+ '"Goodbye, Andrei!You have 1 unread message."',
+ )
+ })
+
+ it('changes locale', async () => {
+ await controller.changeLocale('uk')
+ expect(messageHTML()).toMatchInlineSnapshot(
+ '"Привіт, Andrei!У вас немає непрочитаних повідомлень."',
+ )
+ expect(warningHTML()).toMatchInlineSnapshot(
+ '"Попередження! Це попередження."',
+ )
+
+ // 1 = one
+ await fireEvent.click(getByText('Add unread'))
+ expect(messageHTML()).toMatchInlineSnapshot(
+ '"Привіт, Andrei!У вас є 1 непрочитане повідомлення."',
+ )
+
+ // 2 = few
+ await fireEvent.click(getByText('Add unread'))
+ expect(messageHTML()).toMatchInlineSnapshot(
+ '"Привіт, Andrei!У вас є 2 непрочитаних повідомлення."',
+ )
+
+ // safe to assume it will continue to handle number updates
+
+ await fireEvent.click(getByText('Set intent to goodbye'))
+ expect(messageHTML()).toMatchInlineSnapshot(
+ '"До побачення, Andrei!У вас є 2 непрочитаних повідомлення."',
+ )
+ })
+})
diff --git a/test/composables/useMessages/messageDisplay.tsx b/test/composables/useMessages/messageDisplay.tsx
new file mode 100644
index 0000000..73d10d6
--- /dev/null
+++ b/test/composables/useMessages/messageDisplay.tsx
@@ -0,0 +1,92 @@
+import { computed, defineComponent, ref } from 'vue'
+import { useMessage, useMessages } from '../../../dist/index'
+
+export const messagesPayload: Record> = {
+ 'en-US': {
+ greeting: 'Hello, {name}!',
+ farewell: 'Goodbye, {name}!',
+ inboxMessages:
+ "You {count, plural, =0 {don't have unread messages} one {have # unread message} other {have # unread messages}}.",
+ warnText: 'Warning! This is a warning.',
+ },
+ uk: {
+ greeting: 'Привіт, {name}!',
+ farewell: 'До побачення, {name}!',
+ inboxMessages:
+ 'У вас {count, plural, =0 {немає непрочитаних повідомлень} one {є # непрочитане повідомлення} few {є # непрочитаних повідомлення} many {є # непрочитаних повідомлень} other {є непрочитаних повідомлень}}.',
+ warnText: 'Попередження! Це попередження.',
+ },
+}
+
+export const MessageDisplay = defineComponent(() => {
+ const incrementByOne = () => (unreadMessages.value += 1)
+
+ const intent = ref<'hello' | 'goodbye'>('hello')
+ const setIntentToHello = () => (intent.value = 'hello')
+ const setIntentToGoodbye = () => (intent.value = 'goodbye')
+
+ const reset = () => {
+ name.value = 'Andrei'
+ unreadMessages.value = 0
+ intent.value = 'hello'
+ }
+
+ const name = ref('Andrei')
+
+ const unreadMessages = ref(0)
+
+ const messages = useMessages({
+ inboxMessages: {
+ id: 'inboxMessages',
+ defaultMessage: messagesPayload['en-US'].inboxMessages,
+ values: { count: unreadMessages },
+ },
+ warnText: {
+ id: 'warnText',
+ defaultMessage: messagesPayload['en-US'].warnText,
+ values: {
+ b(chunks) {
+ return {chunks}
+ },
+ },
+ },
+ })
+
+ const helloMessage = useMessage({
+ id: 'greeting',
+ defaultMessage: messagesPayload['en-US'].greeting,
+ values: { name },
+ })
+
+ const goodbyeMessage = useMessage({
+ id: 'farewell',
+ defaultMessage: messagesPayload['en-US'].farewell,
+ values: { name },
+ })
+
+ const intentMessage = computed(() =>
+ intent.value === 'hello' ? helloMessage.value : goodbyeMessage.value,
+ )
+
+ const Warning = defineComponent(() => () => messages.warnText)
+
+ return () => {
+ return (
+ <>
+
+
+ {intentMessage.value}
+ {messages.inboxMessages}
+
+
+
+
+
+
+
+
+
+ >
+ )
+ }
+})
diff --git a/test/index.test.ts b/test/index.test.ts
index 7710114..08bd865 100644
--- a/test/index.test.ts
+++ b/test/index.test.ts
@@ -3,7 +3,7 @@ import { describe, expect, test, vi } from 'vitest'
import { type App, createApp, defineComponent } from 'vue'
import {
type PreferredLocalesSource,
- useI18n,
+ useVIntl,
type LocaleDescriptor,
} from '../dist'
import { type IntlController, createController } from '../dist/controller'
@@ -35,7 +35,7 @@ let controller: IntlController
const appComponent = defineComponent({
name: 'App',
setup() {
- const i18nResult = useI18n()
+ const i18nResult = useVIntl()
return { i18nResult }
},
})
diff --git a/test/utils/index.ts b/test/utils/index.ts
new file mode 100644
index 0000000..6a9d3d8
--- /dev/null
+++ b/test/utils/index.ts
@@ -0,0 +1,42 @@
+import { type LocaleLoadEvent } from '../../dist/events'
+import { createPlugin } from '../../dist/plugin'
+
+export function withAbnormalSpacesReplaced(value: string): string {
+ return value.replace(/[\u202F\u00A0]/g, ' ')
+}
+
+export function createVIntlPlugin(
+ locales: string[],
+ loadLocale?:
+ | ((e: LocaleLoadEvent) => void | Promise)
+ | Record | undefined>,
+) {
+ const plugin = createPlugin({
+ controllerOpts: {
+ locales: locales.map((tag) => ({ tag })),
+ listen: {
+ localeload:
+ typeof loadLocale === 'function' || loadLocale == null
+ ? loadLocale
+ : (e) => {
+ e.addMessages(loadLocale[e.locale.tag] ?? {})
+ },
+ },
+ },
+ })
+
+ const controller = plugin.getOrCreateController()
+
+ const initialState = { ...controller.$config }
+
+ return {
+ plugin,
+ get controller() {
+ return controller
+ },
+ async resetController() {
+ Object.assign(controller.$config, initialState)
+ await controller.waitUntilReady()
+ },
+ }
+}
diff --git a/tsconfig.json b/tsconfig.json
index bc40df9..34cfcea 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -2,15 +2,19 @@
"extends": "./tsconfig.base.json",
"compilerOptions": {
"module": "ESNext",
- "moduleResolution": "Bundler",
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
"types": [],
"outDir": "./dist",
- "noEmit": true
+ "noEmit": true,
+
+ "jsx": "react-jsx",
+ "jsxImportSource": "vue"
},
- "include": ["./src/**/*.ts"],
+ "include": ["./src/**/*.ts", "./src/**/*.tsx"],
"references": [
{
"path": "./tsconfig.build.json"
diff --git a/tsconfig.tests.json b/tsconfig.tests.json
index 0b65670..dd53719 100644
--- a/tsconfig.tests.json
+++ b/tsconfig.tests.json
@@ -5,13 +5,24 @@
"module": "ESNext",
+ "emitDeclarationOnly": true,
+ "allowImportingTsExtensions": true,
+
"moduleResolution": "bundler",
"esModuleInterop": true,
"types": [],
- "lib": ["ES2022", "DOM"]
+ "lib": ["ES2022", "DOM"],
+
+ "jsx": "react-jsx",
+ "jsxImportSource": "vue"
},
- "include": ["./vitest.config.ts", "./test/*.test.ts", "./tsconfig.tests.json"]
+ "include": [
+ "./vitest.config.ts",
+ "./test/**/*.ts",
+ "./test/**/*.tsx",
+ "./tsconfig.tests.json"
+ ]
}
diff --git a/vitest.config.ts b/vitest.config.ts
index 9e3222f..f5daaed 100644
--- a/vitest.config.ts
+++ b/vitest.config.ts
@@ -1,5 +1,6 @@
///
import { defineConfig } from 'vitest/config'
+import tsconfig from './tsconfig.tests.json'
export default defineConfig({
test: {
@@ -8,4 +9,7 @@ export default defineConfig({
},
environment: 'happy-dom',
},
+ esbuild: {
+ tsconfigRaw: tsconfig as any,
+ },
})