From 653c968a3e5407004db9e0d8dbed9b3fe1133ed4 Mon Sep 17 00:00:00 2001
From: Sasha Sorokin <10401817+Brawaru@users.noreply.github.com>
Date: Tue, 20 Jun 2023 16:00:37 +0200
Subject: [PATCH 01/20] Add missing FormattedNumber types
---
src/components/number.ts | 14 ++++++++++++++
1 file changed, 14 insertions(+)
diff --git a/src/components/number.ts b/src/components/number.ts
index 0e4c8b6..f6477c3 100644
--- a/src/components/number.ts
+++ b/src/components/number.ts
@@ -1,6 +1,10 @@
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.
@@ -8,6 +12,16 @@ 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'>
From 86f54ae86afac10046499e50c10edf391e3de7a0 Mon Sep 17 00:00:00 2001
From: Sasha Sorokin <10401817+Brawaru@users.noreply.github.com>
Date: Tue, 20 Jun 2023 19:52:24 +0200
Subject: [PATCH 02/20] Make Fragment component stateful
---
src/components/fragment.ts | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/src/components/fragment.ts b/src/components/fragment.ts
index 18074f6..355bb11 100644
--- a/src/components/fragment.ts
+++ b/src/components/fragment.ts
@@ -1,9 +1,9 @@
-import type { VNode } from 'vue'
+import { defineComponent, type VNode } from 'vue'
interface FragmentProps {
of: VNode[] | VNode
}
-export function Fragment(props: FragmentProps) {
- return Array.isArray(props.of) ? props.of : [props.of]
-}
+export const Fragment = defineComponent(
+ (props: FragmentProps) => () => props.of,
+)
From d338ea485a67f8867ffacf7a7d1240eb9e6edde8 Mon Sep 17 00:00:00 2001
From: Sasha Sorokin <10401817+Brawaru@users.noreply.github.com>
Date: Tue, 20 Jun 2023 20:27:00 +0200
Subject: [PATCH 03/20] Make Fragment component accept VNodeChild
---
src/components/fragment.ts | 8 ++++++--
1 file changed, 6 insertions(+), 2 deletions(-)
diff --git a/src/components/fragment.ts b/src/components/fragment.ts
index 355bb11..9504771 100644
--- a/src/components/fragment.ts
+++ b/src/components/fragment.ts
@@ -1,9 +1,13 @@
-import { defineComponent, type VNode } from 'vue'
+import { defineComponent, type VNodeChild } from 'vue'
interface FragmentProps {
- of: VNode[] | VNode
+ of: VNodeChild
}
export const Fragment = defineComponent(
(props: FragmentProps) => () => props.of,
+ {
+ // eslint-disable-next-line vue/require-prop-types
+ props: ['of'],
+ },
)
From 9a05ff2109db39654d71f7a6a0f12a66b1c04848 Mon Sep 17 00:00:00 2001
From: Sasha Sorokin <10401817+Brawaru@users.noreply.github.com>
Date: Tue, 20 Jun 2023 22:05:01 +0200
Subject: [PATCH 04/20] Fix lists component type
---
src/components/lists.ts | 12 +++++++-----
1 file changed, 7 insertions(+), 5 deletions(-)
diff --git a/src/components/lists.ts b/src/components/lists.ts
index 5af5f89..fa68e1d 100644
--- a/src/components/lists.ts
+++ b/src/components/lists.ts
@@ -14,11 +14,12 @@ interface FormattedListDefinedProps- {
items: readonly Item[]
}
-export interface FormattedListProps
-
- extends FormattedListDefinedProps
- ,
- FormatListOptions {}
+export type FormattedListProps
- =
+ FormattedListDefinedProps
- & FormatListOptions
-export interface FormattedListSlots
- {
+export interface FormattedListSlots<
+ Item extends string | VNode = string | VNode,
+> {
default(props: {
children: Item extends string ? string : string | Item | (string | Item)[]
}): any
@@ -56,4 +57,5 @@ export const FormattedList = defineComponent(
},
},
},
-)
+) as
- (props: FormattedListProps
- ) => any
+// override because typescript is stupid
From c5cbf9c1106d4dfe198734fc17db483750050f12 Mon Sep 17 00:00:00 2001
From: Sasha Sorokin <10401817+Brawaru@users.noreply.github.com>
Date: Wed, 28 Jun 2023 13:36:15 +0200
Subject: [PATCH 05/20] Fix FormattedListParts component type
---
src/components/listsParts.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/components/listsParts.ts b/src/components/listsParts.ts
index 72e4221..c3aaf84 100644
--- a/src/components/listsParts.ts
+++ b/src/components/listsParts.ts
@@ -54,4 +54,4 @@ export const FormattedListParts = defineComponent(
},
},
},
-)
+) as
- (props: FormattedListPartsProps
- ) => any
From c6acb75b565e137dd9fd0f2408653f74c444213a Mon Sep 17 00:00:00 2001
From: Sasha Sorokin <10401817+Brawaru@users.noreply.github.com>
Date: Wed, 28 Jun 2023 13:36:52 +0200
Subject: [PATCH 06/20] Change default slot fallback of FormattedListParts
---
src/components/listsParts.ts | 8 ++++++--
1 file changed, 6 insertions(+), 2 deletions(-)
diff --git a/src/components/listsParts.ts b/src/components/listsParts.ts
index c3aaf84..2643a62 100644
--- a/src/components/listsParts.ts
+++ b/src/components/listsParts.ts
@@ -6,6 +6,7 @@ import {
type SlotsType,
computed,
type PropType,
+ toDisplayString,
} from 'vue'
import { useVIntl } from '../runtime/index.ts'
import { normalizeAttrs } from './utils/index.ts'
@@ -38,9 +39,12 @@ export const FormattedListParts = defineComponent(
return () => {
const { items } = props as FormattedListPartsDefinedProps
-
- const parts = vintl.intl.formatListToParts(items, options.value) as any
+ const parts = vintl.intl.formatListToParts(items, options.value)
- return ctx.slots.default?.({ parts }) ?? parts
+ return (
+ ctx.slots.default?.({ parts }) ??
+ parts.map((part) => toDisplayString(part))
+ )
}
},
{
From a570d9607d625893d969b114e3b5dd2c6f16e4b9 Mon Sep 17 00:00:00 2001
From: Sasha Sorokin <10401817+Brawaru@users.noreply.github.com>
Date: Wed, 28 Jun 2023 15:33:25 +0200
Subject: [PATCH 07/20] Allow non-singular units in FormattedRelativeTime
---
src/components/relativeTime.ts | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/src/components/relativeTime.ts b/src/components/relativeTime.ts
index a48f5a6..7d800d4 100644
--- a/src/components/relativeTime.ts
+++ b/src/components/relativeTime.ts
@@ -12,7 +12,7 @@ import { normalizeAttrs } from './utils/index.ts'
interface RealProps {
value: number
- unit: Intl.RelativeTimeFormatUnitSingular
+ unit: Intl.RelativeTimeFormatUnit
}
export interface FormattedRelativeTimeProps
@@ -53,9 +53,9 @@ export const FormattedRelativeTime = defineComponent(
inheritAttrs: false,
props: {
unit: {
- type: String as PropType,
+ type: String as PropType,
required: false,
- default: 'second',
+ default: 'seconds',
},
value: {
type: Number,
From 451499bd7420b60e8d2fa9f56a32a7a998aa1c5c Mon Sep 17 00:00:00 2001
From: Sasha Sorokin <10401817+Brawaru@users.noreply.github.com>
Date: Wed, 28 Jun 2023 17:22:06 +0200
Subject: [PATCH 08/20] Improve FormattedMessage types
---
src/components/message.ts | 48 +++++++++++++++++++++++++--------------
1 file changed, 31 insertions(+), 17 deletions(-)
diff --git a/src/components/message.ts b/src/components/message.ts
index a0e0313..d0190fc 100644
--- a/src/components/message.ts
+++ b/src/components/message.ts
@@ -1,34 +1,42 @@
import {
computed,
defineComponent,
- type VNode,
type SlotsType,
type SetupContext,
} from 'vue'
+import type { FormatXMLElementFn, PrimitiveType } from 'intl-messageformat'
import { useVIntl } from '../runtime/index.ts'
-import { type MessageContent, type MessageValues } from '../types/index.ts'
+import { type MessageContent, type MessageValueType } from '../types/index.ts'
import { createRecord, normalizeDynamicOutput } from './utils/index.ts'
function isValueSlotName(slotName: string): slotName is `~${string}` {
return slotName.startsWith('~')
}
-interface FormattedMessageProps {
+type ValuesRecord = Record<
+ string,
+ PrimitiveType | T | FormatXMLElementFn
+>
+
+export interface FormattedMessageProps {
id: string
description?: string | object
defaultMessage?: MessageContent
- values?: Values
+ values?: ValuesRecord
}
-interface FormattedMessageSlots {
- [key: string]: (ctx: { children: (VNode | string)[]; values: Values }) => any
- [key: `~${string}`]: (ctx: { values: Values }) => any
+export interface FormattedMessageSlots {
+ [key: string]: (ctx: {
+ children: (T | string)[]
+ values: ValuesRecord
+ }) => string | T | (string | T)[]
+ [key: `~${string}`]: (ctx: { values: ValuesRecord }) => string | T
}
export const FormattedMessage = defineComponent(
- function FormattedMessage(
- props: FormattedMessageProps,
- ctx: SetupContext<{}, SlotsType>>>,
+ function FormattedMessage(
+ props: FormattedMessageProps,
+ ctx: SetupContext<{}, SlotsType>>>,
) {
const descriptor = computed(() => ({
id: props.id,
@@ -36,11 +44,11 @@ export const FormattedMessage = defineComponent(
description: props.description,
}))
- const values = computed(() => {
- const combinedValues = createRecord() as Values
+ const values = computed>(() => {
+ const combinedValues = createRecord() as ValuesRecord
Object.assign(combinedValues, props.values)
- const slotValues = createRecord() as MessageValues
+ const slotValues = createRecord() as ValuesRecord
const { slots } = ctx
@@ -52,8 +60,11 @@ export const FormattedMessage = defineComponent(
values: combinedValues,
})
} else {
- slotValues[slotKey] = (children: (string | VNode)[]) =>
- slots[slotKey]!({ values: combinedValues, children })
+ slotValues[slotKey] = (children: (T | string)[]) =>
+ slots[slotKey]!({
+ values: combinedValues,
+ children,
+ })
}
}
@@ -65,7 +76,10 @@ export const FormattedMessage = defineComponent(
const vintl = useVIntl()
return () => {
- const output = vintl.intl.formatMessage(descriptor.value, values.value)
+ const output = vintl.intl.formatMessage(
+ descriptor.value,
+ values.value as any,
+ )
return normalizeDynamicOutput(output)
}
@@ -97,4 +111,4 @@ export const FormattedMessage = defineComponent(
},
slots: Object as any,
},
-)
+) as (props: FormattedMessageProps) => any
From 98c639f6e2e020b0ea405b63a52f3e89b69ce8ff Mon Sep 17 00:00:00 2001
From: Sasha Sorokin <10401817+Brawaru@users.noreply.github.com>
Date: Thu, 29 Jun 2023 09:37:25 +0200
Subject: [PATCH 09/20] Revamp normalization by FormattedMessage
---
src/components/message.ts | 69 +++++++++++++++++++++++++++++++++------
1 file changed, 59 insertions(+), 10 deletions(-)
diff --git a/src/components/message.ts b/src/components/message.ts
index d0190fc..50ec2f1 100644
--- a/src/components/message.ts
+++ b/src/components/message.ts
@@ -3,11 +3,12 @@ import {
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, normalizeDynamicOutput } from './utils/index.ts'
+import { createRecord } from './utils/index.ts'
function isValueSlotName(slotName: string): slotName is `~${string}` {
return slotName.startsWith('~')
@@ -33,6 +34,48 @@ export interface FormattedMessageSlots {
[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,
@@ -48,7 +91,9 @@ export const FormattedMessage = defineComponent(
const combinedValues = createRecord() as ValuesRecord
Object.assign(combinedValues, props.values)
- const slotValues = createRecord() as ValuesRecord
+ const slotValues = createRecord() as ValuesRecord<
+ T | SlotOutput | ((children: (T | string)[]) => SlotOutput)
+ >
const { slots } = ctx
@@ -56,15 +101,19 @@ export const FormattedMessage = defineComponent(
if (slots[slotKey] == null) continue
if (isValueSlotName(slotKey)) {
- slotValues[slotKey.slice(1)] = slots[slotKey]!({
- values: combinedValues,
- })
- } else {
- slotValues[slotKey] = (children: (T | string)[]) =>
+ slotValues[slotKey.slice(1)] = new SlotOutput(
slots[slotKey]!({
values: combinedValues,
- children,
- })
+ }),
+ )
+ } else {
+ slotValues[slotKey] = (children: (T | string)[]) =>
+ new SlotOutput(
+ slots[slotKey]!({
+ values: combinedValues,
+ children,
+ }),
+ )
}
}
@@ -81,7 +130,7 @@ export const FormattedMessage = defineComponent(
values.value as any,
)
- return normalizeDynamicOutput(output)
+ return normalizeOutput(output)
}
},
{
From 1d4979e7040e32b6d2c779395a4e1d58dc07e0ca Mon Sep 17 00:00:00 2001
From: Sasha Sorokin <10401817+Brawaru@users.noreply.github.com>
Date: Thu, 29 Jun 2023 09:43:33 +0200
Subject: [PATCH 10/20] Simply FormattedList children normalisation
(by removing all normalisation and let Vue handle that)
---
src/components/lists.ts | 6 ++----
src/components/utils/index.ts | 14 --------------
2 files changed, 2 insertions(+), 18 deletions(-)
diff --git a/src/components/lists.ts b/src/components/lists.ts
index fa68e1d..3d64c79 100644
--- a/src/components/lists.ts
+++ b/src/components/lists.ts
@@ -8,7 +8,7 @@ import {
type VNode,
} from 'vue'
import { useVIntl } from '../runtime/index.ts'
-import { normalizeAttrs, normalizeDynamicOutput } from './utils/index.ts'
+import { normalizeAttrs } from './utils/index.ts'
interface FormattedListDefinedProps
- {
items: readonly Item[]
@@ -41,9 +41,7 @@ export const FormattedList = defineComponent(
const children = vintl.intl.formatList(items, options.value) as any
- return (
- ctx.slots.default?.({ children }) ?? normalizeDynamicOutput(children)
- )
+ return ctx.slots.default?.({ children }) ?? children
}
},
{
diff --git a/src/components/utils/index.ts b/src/components/utils/index.ts
index 185575a..5a02f98 100644
--- a/src/components/utils/index.ts
+++ b/src/components/utils/index.ts
@@ -1,22 +1,8 @@
-import { isVNode, createTextVNode, type VNode } from 'vue'
-import type { MessageValueType } from '../../types/index.ts'
import { camelize } from '../../utils/strings.ts'
export * from './simpleDefiner.ts'
export * from './partsDefiner.ts'
-export function vnodeOrText(value: T | string) {
- return isVNode(value) ? value : createTextVNode(value as any)
-}
-
-export function normalizeDynamicOutput(
- value: T | string | (string | T)[],
-) {
- return (
- Array.isArray(value) ? value.map(vnodeOrText) : [vnodeOrText(value)]
- ) satisfies VNode[]
-}
-
export function normalizeAttrs(attrs: Record) {
const normalizedAttrs: Record = Object.create(null)
for (const key in attrs) normalizedAttrs[camelize(key)] = attrs[key]
From b5b8c410447584c82690f57c7270b54190911506 Mon Sep 17 00:00:00 2001
From: Sasha Sorokin <10401817+Brawaru@users.noreply.github.com>
Date: Thu, 29 Jun 2023 09:54:01 +0200
Subject: [PATCH 11/20] Make FormattedPlural fall back to other slot if rule
slot is not defined
---
src/components/plural.ts | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/src/components/plural.ts b/src/components/plural.ts
index efb20c2..ce880b7 100644
--- a/src/components/plural.ts
+++ b/src/components/plural.ts
@@ -43,7 +43,9 @@ export const FormattedPlural = defineComponent(
const rule = vintl.intl.formatPlural(value, $options.value)
- const ruleRender = ctx.slots[rule]?.() ?? []
+ const ruleSlot = ctx.slots[rule] ?? ctx.slots.other
+
+ const ruleRender = ruleSlot != null ? ruleSlot() : []
return ctx.slots.default?.({ children: ruleRender }) ?? ruleRender
}
From 365733a14c6b283b67c0c76da059431514d4bd14 Mon Sep 17 00:00:00 2001
From: Sasha Sorokin <10401817+Brawaru@users.noreply.github.com>
Date: Thu, 29 Jun 2023 09:54:22 +0200
Subject: [PATCH 12/20] Make FormattedPluralSlots generic optional
---
src/components/plural.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/components/plural.ts b/src/components/plural.ts
index ce880b7..b7dc695 100644
--- a/src/components/plural.ts
+++ b/src/components/plural.ts
@@ -23,7 +23,7 @@ export interface FormattedPluralProps
extends FormattedPluralDefinedProps,
FormatPluralOptions {}
-export interface FormattedPluralSlots extends PluralSlots {
+export interface FormattedPluralSlots extends PluralSlots {
default(props: { children: string | T | (string | T)[] }): any
}
From ff909dc29a38026cfe5864e40e1783a521d71371 Mon Sep 17 00:00:00 2001
From: Sasha Sorokin <10401817+Brawaru@users.noreply.github.com>
Date: Thu, 29 Jun 2023 09:55:20 +0200
Subject: [PATCH 13/20] Pass value prop to FormattedPlural slots
---
src/components/plural.ts | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/components/plural.ts b/src/components/plural.ts
index b7dc695..e987e18 100644
--- a/src/components/plural.ts
+++ b/src/components/plural.ts
@@ -12,7 +12,7 @@ import { normalizeAttrs } from './utils/index.ts'
type PluralSelectors = ReturnType
type PluralSlots = {
- [K in PluralSelectors]: () => any
+ [K in PluralSelectors]: (props: { value: number }) => any
}
interface FormattedPluralDefinedProps {
@@ -45,7 +45,7 @@ export const FormattedPlural = defineComponent(
const ruleSlot = ctx.slots[rule] ?? ctx.slots.other
- const ruleRender = ruleSlot != null ? ruleSlot() : []
+ const ruleRender = ruleSlot != null ? ruleSlot({ value }) : []
return ctx.slots.default?.({ children: ruleRender }) ?? ruleRender
}
From 3f7b620f4c20a697c5ca0e2a0d4248accb9682ee Mon Sep 17 00:00:00 2001
From: Sasha Sorokin <10401817+Brawaru@users.noreply.github.com>
Date: Thu, 29 Jun 2023 09:55:37 +0200
Subject: [PATCH 14/20] Make all slots of FormattedPlural optional
---
src/components/plural.ts | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/components/plural.ts b/src/components/plural.ts
index e987e18..6484fbd 100644
--- a/src/components/plural.ts
+++ b/src/components/plural.ts
@@ -12,7 +12,7 @@ import { normalizeAttrs } from './utils/index.ts'
type PluralSelectors = ReturnType
type PluralSlots = {
- [K in PluralSelectors]: (props: { value: number }) => any
+ [K in PluralSelectors]?: (props: { value: number }) => any
}
interface FormattedPluralDefinedProps {
@@ -24,7 +24,7 @@ export interface FormattedPluralProps
FormatPluralOptions {}
export interface FormattedPluralSlots extends PluralSlots {
- default(props: { children: string | T | (string | T)[] }): any
+ default?(props: { children: string | T | (string | T)[] }): any
}
export const FormattedPlural = defineComponent(
From 8b7f7b829d7ab38ab0bbb38445c5889b8b6993b1 Mon Sep 17 00:00:00 2001
From: Sasha Sorokin <10401817+Brawaru@users.noreply.github.com>
Date: Thu, 29 Jun 2023 10:31:52 +0200
Subject: [PATCH 15/20] Fix FormattedPlural prop/attr handling
---
src/components/plural.ts | 10 ++--------
1 file changed, 2 insertions(+), 8 deletions(-)
diff --git a/src/components/plural.ts b/src/components/plural.ts
index 6484fbd..1db5c88 100644
--- a/src/components/plural.ts
+++ b/src/components/plural.ts
@@ -51,19 +51,13 @@ export const FormattedPlural = defineComponent(
}
},
{
+ inheritAttrs: false,
props: {
value: {
type: Number,
required: false,
default: 0,
},
- options: {
- type: Object,
- required: false,
- default() {
- return {}
- },
- },
},
},
-)
+) as (props: FormattedPluralProps) => any
From d9bba71169ba6af98774664a1b0a7245aee6c359 Mon Sep 17 00:00:00 2001
From: Sasha Sorokin <10401817+Brawaru@users.noreply.github.com>
Date: Thu, 29 Jun 2023 10:32:22 +0200
Subject: [PATCH 16/20] Make generic of FormattedPlural optional
---
src/components/plural.ts | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/src/components/plural.ts b/src/components/plural.ts
index 1db5c88..53a0804 100644
--- a/src/components/plural.ts
+++ b/src/components/plural.ts
@@ -5,7 +5,6 @@ import {
type SetupContext,
type SlotsType,
} from 'vue'
-import { type MessageValueType } from '../types/index.ts'
import { useVIntl } from '../runtime/index.ts'
import { normalizeAttrs } from './utils/index.ts'
@@ -28,7 +27,7 @@ export interface FormattedPluralSlots extends PluralSlots {
}
export const FormattedPlural = defineComponent(
- (
+ (
props: FormattedPluralProps,
ctx: SetupContext<{}, SlotsType>>>,
) => {
From 7fabcf32512a60931c3cb31dc89a29df98eac898 Mon Sep 17 00:00:00 2001
From: Sasha Sorokin <10401817+Brawaru@users.noreply.github.com>
Date: Thu, 29 Jun 2023 13:27:23 +0200
Subject: [PATCH 17/20] In FormattedListParts, return part values as is if no
default slot exists
---
src/components/listsParts.ts | 10 +++++-----
1 file changed, 5 insertions(+), 5 deletions(-)
diff --git a/src/components/listsParts.ts b/src/components/listsParts.ts
index 2643a62..de78173 100644
--- a/src/components/listsParts.ts
+++ b/src/components/listsParts.ts
@@ -6,7 +6,6 @@ import {
type SlotsType,
computed,
type PropType,
- toDisplayString,
} from 'vue'
import { useVIntl } from '../runtime/index.ts'
import { normalizeAttrs } from './utils/index.ts'
@@ -41,10 +40,11 @@ export const FormattedListParts = defineComponent(
const parts = vintl.intl.formatListToParts(items, options.value)
- return (
- ctx.slots.default?.({ parts }) ??
- parts.map((part) => toDisplayString(part))
- )
+ const defaultSlot = ctx.slots.default
+
+ return defaultSlot == null
+ ? parts.map((part) => part.value)
+ : defaultSlot({ parts })
}
},
{
From 945ac406bcfb2ccec472eee487ba3038efdadacd Mon Sep 17 00:00:00 2001
From: Sasha Sorokin <10401817+Brawaru@users.noreply.github.com>
Date: Mon, 3 Jul 2023 16:49:23 +0200
Subject: [PATCH 18/20] Add tests for all unique components
---
.eslintrc.json | 2 +-
package.json | 1 +
pnpm-lock.yaml | 315 ++++++++++++++++--
test/components/FormattedList/index.test.ts | 59 ++++
test/components/FormattedList/listDisplay.tsx | 65 ++++
.../FormattedListParts/index.test.ts | 62 ++++
.../FormattedListParts/listPartsDisplay.tsx | 79 +++++
.../components/FormattedMessage/index.test.ts | 65 ++++
.../FormattedMessage/messageDisplay.tsx | 89 +++++
test/components/FormattedNumber/counter.tsx | 54 +++
test/components/FormattedNumber/index.test.ts | 70 ++++
.../FormattedNumberParts/counter.tsx | 43 +++
.../FormattedNumberParts/index.test.ts | 58 ++++
test/components/FormattedPlural/index.test.ts | 97 ++++++
.../FormattedPlural/pluralDisplay.tsx | 66 ++++
.../FormattedRelativeTime/index.test.ts | 67 ++++
.../relativeTimeDisplay.tsx | 55 +++
test/utils/index.ts | 33 ++
tsconfig.tests.json | 15 +-
vitest.config.ts | 4 +
20 files changed, 1275 insertions(+), 24 deletions(-)
create mode 100644 test/components/FormattedList/index.test.ts
create mode 100644 test/components/FormattedList/listDisplay.tsx
create mode 100644 test/components/FormattedListParts/index.test.ts
create mode 100644 test/components/FormattedListParts/listPartsDisplay.tsx
create mode 100644 test/components/FormattedMessage/index.test.ts
create mode 100644 test/components/FormattedMessage/messageDisplay.tsx
create mode 100644 test/components/FormattedNumber/counter.tsx
create mode 100644 test/components/FormattedNumber/index.test.ts
create mode 100644 test/components/FormattedNumberParts/counter.tsx
create mode 100644 test/components/FormattedNumberParts/index.test.ts
create mode 100644 test/components/FormattedPlural/index.test.ts
create mode 100644 test/components/FormattedPlural/pluralDisplay.tsx
create mode 100644 test/components/FormattedRelativeTime/index.test.ts
create mode 100644 test/components/FormattedRelativeTime/relativeTimeDisplay.tsx
create mode 100644 test/utils/index.ts
diff --git a/.eslintrc.json b/.eslintrc.json
index 72f950f..180a629 100644
--- a/.eslintrc.json
+++ b/.eslintrc.json
@@ -41,7 +41,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/package.json b/package.json
index 99ffce8..b8593db 100644
--- a/package.json
+++ b/package.json
@@ -76,6 +76,7 @@
"devDependencies": {
"@changesets/cli": "^2.26.1",
"@nuxtjs/eslint-config-typescript": "^12.0.0",
+ "@testing-library/vue": "^7.0.0",
"@types/node": "^18.16.16",
"del-cli": "^5.0.0",
"eslint": "^8.42.0",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 1fada30..b725380 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -34,6 +34,9 @@ devDependencies:
'@nuxtjs/eslint-config-typescript':
specifier: ^12.0.0
version: 12.0.0(eslint@8.42.0)(typescript@5.1.3)
+ '@testing-library/vue':
+ specifier: ^7.0.0
+ version: 7.0.0(@vue/compiler-sfc@3.3.4)(vue@3.3.4)
'@types/node':
specifier: ^18.16.16
version: 18.16.16
@@ -1193,6 +1196,38 @@ packages:
rollup: 3.20.6
dev: true
+ /@testing-library/dom@9.3.0:
+ resolution: {integrity: sha512-Dffe68pGwI6WlLRYR2I0piIkyole9cSBH5jGQKCGMRpHW5RHCqAUaqc2Kv0tUyd4dU4DLPKhJIjyKOnjv4tuUw==}
+ engines: {node: '>=14'}
+ dependencies:
+ '@babel/code-frame': 7.21.4
+ '@babel/runtime': 7.22.3
+ '@types/aria-query': 5.0.1
+ 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.4)(vue@3.3.4):
+ 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.0
+ '@vue/compiler-sfc': 3.3.4
+ '@vue/test-utils': 2.3.2(vue@3.3.4)
+ vue: 3.3.4
+ dev: true
+
+ /@types/aria-query@5.0.1:
+ resolution: {integrity: sha512-XTIieEY+gvJ39ChLcB4If5zHtPxt3Syj5rgZR+e1ctpmK8NjPf0zFqsz4JpLJT0xla9GFDKjy8Cpu331nrmE1Q==}
+ dev: true
+
/@types/chai-subset@1.3.3:
resolution: {integrity: sha512-frBecisrNGz+F4T6bcc+NLeolfiojh5FxW2klu669+8BARtyQv2C/GkNW6FUodVe4BroGMP/wER/YDGc7rEllw==}
dependencies:
@@ -1529,6 +1564,18 @@ packages:
/@vue/shared@3.3.4:
resolution: {integrity: sha512-7OjdcV8vQ74eiz1TZLzZP4JwqM5fA94K6yntPS5Z25r9HDuGNzaGdgvwKYq6S+MxwF0TFRwe50fIR/MYnakdkQ==}
+ /@vue/test-utils@2.3.2(vue@3.3.4):
+ resolution: {integrity: sha512-hJnVaYhbrIm0yBS0+e1Y0Sj85cMyAi+PAbK4JHqMRUZ6S622Goa+G7QzkRSyvCteG8wop7tipuEbHoZo26wsSA==}
+ peerDependencies:
+ vue: ^3.0.1
+ dependencies:
+ js-beautify: 1.14.6
+ vue: 3.3.4
+ optionalDependencies:
+ '@vue/compiler-dom': 3.3.4
+ '@vue/server-renderer': 3.3.4(vue@3.3.4)
+ dev: true
+
/@vueuse/core@10.1.2(vue@3.3.4):
resolution: {integrity: sha512-roNn8WuerI56A5uiTyF/TEYX0Y+VKlhZAF94unUfdhbDUI+NfwQMn4FUnUscIRUhv3344qvAghopU4bzLPNFlA==}
dependencies:
@@ -1604,6 +1651,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.8.2):
resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==}
peerDependencies:
@@ -1702,14 +1753,27 @@ 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.1
+ dev: true
+
+ /array-buffer-byte-length@1.0.0:
+ resolution: {integrity: sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A==}
+ dependencies:
+ call-bind: 1.0.2
+ is-array-buffer: 3.0.2
+ dev: true
+
/array-includes@3.1.6:
resolution: {integrity: sha512-sgTbLvL6cNnw24FnbaDyjmvddQ2ML8arZsgaJhoABMoplz/4QRhtrYS+alr1BUM1Bwp6dhx8vVCBSLG+StwOFw==}
engines: {node: '>= 0.4'}
dependencies:
call-bind: 1.0.2
- define-properties: 1.1.4
+ define-properties: 1.2.0
es-abstract: 1.20.5
- get-intrinsic: 1.1.3
+ get-intrinsic: 1.2.1
is-string: 1.0.7
dev: true
@@ -1723,7 +1787,7 @@ packages:
engines: {node: '>= 0.4'}
dependencies:
call-bind: 1.0.2
- define-properties: 1.1.4
+ define-properties: 1.2.0
es-abstract: 1.20.5
es-shim-unscopables: 1.0.0
dev: true
@@ -1737,6 +1801,11 @@ packages:
resolution: {integrity: sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==}
dev: true
+ /available-typed-arrays@1.0.5:
+ resolution: {integrity: sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==}
+ engines: {node: '>= 0.4'}
+ dev: true
+
/balanced-match@1.0.2:
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
dev: true
@@ -1821,7 +1890,7 @@ packages:
resolution: {integrity: sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==}
dependencies:
function-bind: 1.1.1
- get-intrinsic: 1.1.3
+ get-intrinsic: 1.2.1
dev: true
/callsites@3.1.0:
@@ -1971,6 +2040,10 @@ packages:
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
dev: true
+ /commander@2.20.3:
+ resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==}
+ dev: true
+
/comment-parser@1.3.1:
resolution: {integrity: sha512-B52sN2VNghyq5ofvUsqZjmk6YkihBX5vMSChmSK9v4ShjKf3Vk5Xcmgpw4o+iIgtrnM/u5FiMpz9VKb8lpBveA==}
engines: {node: '>= 12.0.0'}
@@ -1998,6 +2071,13 @@ packages:
well-known-symbols: 2.0.0
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.1.0:
resolution: {integrity: sha512-rrrJE6rP0qzl/Srg+C9x/AE5Kxfux7reVm1Wh0wCjuXvih6DqZgqDZe8auTD28fzJ9TF0mHlSDrPpWlujQRo1Q==}
dev: true
@@ -2130,6 +2210,29 @@ packages:
type-detect: 4.0.8
dev: true
+ /deep-equal@2.2.1:
+ resolution: {integrity: sha512-lKdkdV6EOGoVn65XaOsPdH4rMxTZOnmFyuIkMjM1i5HHCbfjC97dawgTAy0deYNfuqUqW+Q5VrVaQYtUpSd6yQ==}
+ 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.0
+ side-channel: 1.0.4
+ which-boxed-primitive: 1.0.2
+ which-collection: 1.0.1
+ which-typed-array: 1.1.9
+ dev: true
+
/deep-is@0.1.4:
resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==}
dev: true
@@ -2158,6 +2261,14 @@ packages:
object-keys: 1.1.1
dev: true
+ /define-properties@1.2.0:
+ resolution: {integrity: sha512-xvqAVKGfT1+UAvPwKTVw/njhdQ8ZhXK4lI0bCIuCMrp2up9nPnaDftrLtmpTazqd1o+UY4zgzU+avtMbDP+ldA==}
+ engines: {node: '>= 0.4'}
+ dependencies:
+ has-property-descriptors: 1.0.0
+ object-keys: 1.1.1
+ dev: true
+
/defu@6.1.2:
resolution: {integrity: sha512-+uO4+qr7msjNNWKYPHqN/3+Dx3NFkmIzayk2L1MyZQlvgZb/J1A0fo410dpKrN2SnqFjt8n4JL8fDJE0wIgjFQ==}
dev: true
@@ -2221,6 +2332,20 @@ packages:
esutils: 2.0.3
dev: true
+ /dom-accessibility-api@0.5.16:
+ resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==}
+ dev: true
+
+ /editorconfig@0.15.3:
+ resolution: {integrity: sha512-M9wIMFx96vq0R4F+gRpY3o2exzb8hEj/n9S8unZtHSvYjibBp/iMufSzvmOcV/laG0ZtuTVGtiJggPOSW2r93g==}
+ hasBin: true
+ dependencies:
+ commander: 2.20.3
+ lru-cache: 4.1.5
+ semver: 5.7.1
+ sigmund: 1.0.1
+ dev: true
+
/electron-to-chromium@1.4.368:
resolution: {integrity: sha512-e2aeCAixCj9M7nJxdB/wDjO6mbYX+lJJxSJCXDzlr5YPGYVofuJwGN9nKg2o6wWInjX6XmxRinn3AeJMK81ltw==}
dev: true
@@ -2263,7 +2388,7 @@ packages:
es-to-primitive: 1.2.1
function-bind: 1.1.1
function.prototype.name: 1.1.5
- get-intrinsic: 1.1.3
+ get-intrinsic: 1.2.1
get-symbol-description: 1.0.0
gopd: 1.0.1
has: 1.0.3
@@ -2279,13 +2404,27 @@ packages:
object-inspect: 1.12.2
object-keys: 1.1.1
object.assign: 4.1.4
- regexp.prototype.flags: 1.4.3
+ regexp.prototype.flags: 1.5.0
safe-regex-test: 1.0.0
string.prototype.trimend: 1.0.6
string.prototype.trimstart: 1.0.6
unbox-primitive: 1.0.2
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-shim-unscopables@1.0.0:
resolution: {integrity: sha512-Jm6GPcCdC30eMLbZ2x8z2WuRwAws3zTBBKuusffYVUrNj/GVSUAZ+xKMaUpfNDR5IbyNA5LJbaecoUVbmUcB1w==}
dependencies:
@@ -2817,6 +2956,12 @@ packages:
tabbable: 6.1.2
dev: true
+ /for-each@0.3.3:
+ resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==}
+ dependencies:
+ is-callable: 1.2.7
+ dev: true
+
/fs-extra@11.1.1:
resolution: {integrity: sha512-MGIE4HOvQCeUCzmlHs0vXpih4ysz4wg9qiSAu6cd42lVwPbTM1TjV7RusoyQqMmk/95gdQZX72u+YW+c3eEpFQ==}
engines: {node: '>=14.14'}
@@ -2865,7 +3010,7 @@ packages:
engines: {node: '>= 0.4'}
dependencies:
call-bind: 1.0.2
- define-properties: 1.1.4
+ define-properties: 1.2.0
es-abstract: 1.20.5
functions-have-names: 1.2.3
dev: true
@@ -2888,11 +3033,12 @@ packages:
resolution: {integrity: sha512-Hm0ixYtaSZ/V7C8FJrtZIuBBI+iSgL+1Aq82zSu8VQNB4S3Gk8e7Qs3VwBDJAhmRZcFqkl3tQu36g/Foh5I5ig==}
dev: true
- /get-intrinsic@1.1.3:
- resolution: {integrity: sha512-QJVz1Tj7MS099PevUG5jvnt9tSkXN8K14dxQlikJuPt4uD9hHAHjLyLBiLR5zELelBdD9QNRAXZzsJx0WaDL9A==}
+ /get-intrinsic@1.2.1:
+ resolution: {integrity: sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==}
dependencies:
function-bind: 1.1.1
has: 1.0.3
+ has-proto: 1.0.1
has-symbols: 1.0.3
dev: true
@@ -2901,7 +3047,7 @@ packages:
engines: {node: '>= 0.4'}
dependencies:
call-bind: 1.0.2
- get-intrinsic: 1.1.3
+ get-intrinsic: 1.2.1
dev: true
/get-tsconfig@4.2.0:
@@ -2990,7 +3136,7 @@ packages:
/gopd@1.0.1:
resolution: {integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==}
dependencies:
- get-intrinsic: 1.1.3
+ get-intrinsic: 1.2.1
dev: true
/graceful-fs@4.2.11:
@@ -3038,7 +3184,12 @@ packages:
/has-property-descriptors@1.0.0:
resolution: {integrity: sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==}
dependencies:
- get-intrinsic: 1.1.3
+ get-intrinsic: 1.2.1
+ dev: true
+
+ /has-proto@1.0.1:
+ resolution: {integrity: sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==}
+ engines: {node: '>= 0.4'}
dev: true
/has-symbols@1.0.3:
@@ -3132,11 +3283,15 @@ packages:
resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
dev: true
+ /ini@1.3.8:
+ resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==}
+ dev: true
+
/internal-slot@1.0.4:
resolution: {integrity: sha512-tA8URYccNzMo94s5MQZgH8NB/XTa6HsOo0MLfXTKKEnHVVdegzaQoFZ7Jp44bdvLvY2waT5dc+j5ICEswhi7UQ==}
engines: {node: '>= 0.4'}
dependencies:
- get-intrinsic: 1.1.3
+ get-intrinsic: 1.2.1
has: 1.0.3
side-channel: 1.0.4
dev: true
@@ -3150,6 +3305,22 @@ 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:
+ call-bind: 1.0.2
+ get-intrinsic: 1.2.1
+ is-typed-array: 1.1.10
+ dev: true
+
/is-arrayish@0.2.1:
resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==}
dev: true
@@ -3230,6 +3401,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
@@ -3285,6 +3460,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:
@@ -3312,12 +3491,34 @@ packages:
has-symbols: 1.0.3
dev: true
+ /is-typed-array@1.1.10:
+ resolution: {integrity: sha512-PJqgEHiWZvMpaFZ3uTc8kHPM4+4ADTlDniuQL7cU/UDA0Ql7F70yGfHph3cLNe+c9toaigv+DFzTJKhc2CtO6A==}
+ engines: {node: '>= 0.4'}
+ dependencies:
+ available-typed-arrays: 1.0.5
+ call-bind: 1.0.2
+ for-each: 0.3.3
+ gopd: 1.0.1
+ has-tostringtag: 1.0.0
+ 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'}
@@ -3330,6 +3531,10 @@ packages:
is-docker: 2.2.1
dev: true
+ /isarray@2.0.5:
+ resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==}
+ dev: true
+
/isexe@2.0.0:
resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
dev: true
@@ -3339,6 +3544,17 @@ packages:
hasBin: true
dev: true
+ /js-beautify@1.14.6:
+ resolution: {integrity: sha512-GfofQY5zDp+cuHc+gsEXKPpNw2KbPddreEo35O6jT6i0RVK6LhsoYBhq5TvK4/n74wnA0QbK8gGd+jUZwTMKJw==}
+ engines: {node: '>=10'}
+ hasBin: true
+ dependencies:
+ config-chain: 1.1.13
+ editorconfig: 0.15.3
+ glob: 8.1.0
+ nopt: 6.0.0
+ dev: true
+
/js-string-escape@1.0.1:
resolution: {integrity: sha512-Smw4xcfIQ5LVjAOuJCvN/zIodzA/BBSsluuoSykP+lUvScIi4U6RJLfwHet5cxFnCswUjISV8oAXaqaJDY3chg==}
engines: {node: '>= 0.8'}
@@ -3506,6 +3722,11 @@ packages:
yallist: 4.0.0
dev: true
+ /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==}
engines: {node: '>=12'}
@@ -3889,6 +4110,14 @@ packages:
resolution: {integrity: sha512-5GFldHPXVG/YZmFzJvKK2zDSzPKhEp0+ZR5SVaoSag9fsL5YgHbUHDfnG5494ISANDcK4KwPXAx2xqVEydmd7w==}
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:
@@ -3918,6 +4147,14 @@ packages:
resolution: {integrity: sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==}
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.1.4
+ dev: true
+
/object-keys@1.1.1:
resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==}
engines: {node: '>= 0.4'}
@@ -3938,7 +4175,7 @@ packages:
engines: {node: '>= 0.4'}
dependencies:
call-bind: 1.0.2
- define-properties: 1.1.4
+ define-properties: 1.2.0
es-abstract: 1.20.5
dev: true
@@ -4188,6 +4425,10 @@ packages:
react-is: 17.0.2
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
@@ -4288,12 +4529,12 @@ packages:
hasBin: true
dev: true
- /regexp.prototype.flags@1.4.3:
- resolution: {integrity: sha512-fjggEOO3slI6Wvgjwflkc4NFRCTZAu5CnNfBd5qOMYhWdn67nJBBu34/TkD++eeFmd8C9r9jfXJ27+nSiRkSUA==}
+ /regexp.prototype.flags@1.5.0:
+ resolution: {integrity: sha512-0SutC3pNudRKgquxGoRGIz946MZVHqbNfPjBdxeOhBrdgDKlRoXmYLQN9xRbrR09ZXWeGAdPuif7egofn6v5LA==}
engines: {node: '>= 0.4'}
dependencies:
call-bind: 1.0.2
- define-properties: 1.1.4
+ define-properties: 1.2.0
functions-have-names: 1.2.3
dev: true
@@ -4389,7 +4630,7 @@ packages:
resolution: {integrity: sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA==}
dependencies:
call-bind: 1.0.2
- get-intrinsic: 1.1.3
+ get-intrinsic: 1.2.1
is-regex: 1.1.4
dev: true
@@ -4466,7 +4707,7 @@ packages:
resolution: {integrity: sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==}
dependencies:
call-bind: 1.0.2
- get-intrinsic: 1.1.3
+ get-intrinsic: 1.2.1
object-inspect: 1.12.2
dev: true
@@ -4474,6 +4715,10 @@ packages:
resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==}
dev: true
+ /sigmund@1.0.1:
+ resolution: {integrity: sha512-fCvEXfh6NWpm+YSuY2bpXb/VIihqWA6hLsgboC+0nl71Q7N7o2eaCW8mJa/NLvQhs6jpd3VZV4UiUQlV6+lc8g==}
+ dev: true
+
/signal-exit@3.0.7:
resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==}
dev: true
@@ -4546,6 +4791,13 @@ packages:
resolution: {integrity: sha512-uUZI65yrV2Qva5gqE0+A7uVAvO40iPo6jGhs7s8keRfHCmtg+uB2X6EiLGCI9IgL1J17xGhvoOqSz79lzICPTA==}
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.4
+ dev: true
+
/stream-transform@2.1.3:
resolution: {integrity: sha512-9GHUiM5hMiCi6Y03jD2ARC1ettBXkQBoQAe7nJsPknnI0ow10aXjTnew8QtYQmLjzn974BnmWEAJgCY6ZP1DeQ==}
dependencies:
@@ -4565,7 +4817,7 @@ packages:
resolution: {integrity: sha512-JySq+4mrPf9EsDBEDYMOb/lM7XQLulwg5R/m1r0PXEFqrV0qHvl58sdTilSXtKOflCsK2E8jxf+GKC0T07RWwQ==}
dependencies:
call-bind: 1.0.2
- define-properties: 1.1.4
+ define-properties: 1.2.0
es-abstract: 1.20.5
dev: true
@@ -4573,7 +4825,7 @@ packages:
resolution: {integrity: sha512-omqjMDaY92pbn5HOX7f9IccLA+U1tA9GvtU4JrodiXFfYB7jPzzHpRzpglLAjtUV6bB557zwClJezTqnAiYnQA==}
dependencies:
call-bind: 1.0.2
- define-properties: 1.1.4
+ define-properties: 1.2.0
es-abstract: 1.20.5
dev: true
@@ -5162,6 +5414,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
@@ -5174,6 +5435,18 @@ packages:
path-exists: 4.0.0
dev: true
+ /which-typed-array@1.1.9:
+ resolution: {integrity: sha512-w9c4xkx6mPidwp7180ckYWfMmvxpjlZuIudNtDf4N/tTAUB8VJbX25qZoAsrtGuYNnGw3pa0AXgbGKRB8/EceA==}
+ engines: {node: '>= 0.4'}
+ dependencies:
+ available-typed-arrays: 1.0.5
+ call-bind: 1.0.2
+ for-each: 0.3.3
+ gopd: 1.0.1
+ has-tostringtag: 1.0.0
+ is-typed-array: 1.1.10
+ dev: true
+
/which@1.3.1:
resolution: {integrity: sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==}
hasBin: true
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/utils/index.ts b/test/utils/index.ts
new file mode 100644
index 0000000..c12699f
--- /dev/null
+++ b/test/utils/index.ts
@@ -0,0 +1,33 @@
+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,
+) {
+ const plugin = createPlugin({
+ controllerOpts: {
+ locales: locales.map((tag) => ({ tag })),
+ listen: { localeload: loadLocale },
+ },
+ })
+
+ 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.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,
+ },
})
From 3eaf0613f566421397f1a2bf612aeaf669502b1c Mon Sep 17 00:00:00 2001
From: Sasha Sorokin <10401817+Brawaru@users.noreply.github.com>
Date: Mon, 10 Jul 2023 20:30:57 +0200
Subject: [PATCH 19/20] Allow to pass message payloads as is in
createVIntlPlugin test util
---
test/utils/index.ts | 13 +++++++++++--
1 file changed, 11 insertions(+), 2 deletions(-)
diff --git a/test/utils/index.ts b/test/utils/index.ts
index c12699f..6a9d3d8 100644
--- a/test/utils/index.ts
+++ b/test/utils/index.ts
@@ -7,12 +7,21 @@ export function withAbnormalSpacesReplaced(value: string): string {
export function createVIntlPlugin(
locales: string[],
- loadLocale?: (e: LocaleLoadEvent) => void | Promise,
+ loadLocale?:
+ | ((e: LocaleLoadEvent) => void | Promise)
+ | Record | undefined>,
) {
const plugin = createPlugin({
controllerOpts: {
locales: locales.map((tag) => ({ tag })),
- listen: { localeload: loadLocale },
+ listen: {
+ localeload:
+ typeof loadLocale === 'function' || loadLocale == null
+ ? loadLocale
+ : (e) => {
+ e.addMessages(loadLocale[e.locale.tag] ?? {})
+ },
+ },
},
})
From b194662643b5cc4057d54a8a6ab5b42ec210aac8 Mon Sep 17 00:00:00 2001
From: Sasha Sorokin <10401817+Brawaru@users.noreply.github.com>
Date: Fri, 22 Sep 2023 22:06:00 +0200
Subject: [PATCH 20/20] Add useMessage composable
---
.changeset/tender-zoos-shop.md | 7 +
src/runtime/index.ts | 1 +
src/runtime/useMessages.ts | 161 ++++++++++++++++++
test/composables/useMessages/index.test.ts | 75 ++++++++
.../useMessages/messageDisplay.tsx | 92 ++++++++++
5 files changed, 336 insertions(+)
create mode 100644 .changeset/tender-zoos-shop.md
create mode 100644 src/runtime/useMessages.ts
create mode 100644 test/composables/useMessages/index.test.ts
create mode 100644 test/composables/useMessages/messageDisplay.tsx
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/src/runtime/index.ts b/src/runtime/index.ts
index 7273a40..242fd05 100644
--- a/src/runtime/index.ts
+++ b/src/runtime/index.ts
@@ -1 +1,2 @@
export { useVIntl } from './useVIntl.js'
+export { useMessages, useMessage } from './useMessages.js'
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/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}
+
+
+
+
+
+
+
+
+
+ >
+ )
+ }
+})