@xen-orchestra/web-core 0.0.2 → 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/env.d.ts +1 -0
  2. package/lib/assets/no-result.svg +81 -0
  3. package/lib/assets/under-construction.svg +195 -0
  4. package/lib/components/CardNumbers.vue +94 -0
  5. package/lib/components/DonutChart.vue +92 -0
  6. package/lib/components/LegendTitle.vue +31 -0
  7. package/lib/components/StatusPill.vue +2 -2
  8. package/lib/components/UiCard.vue +29 -0
  9. package/lib/components/UiLegend.vue +68 -0
  10. package/lib/components/UiSeparator.vue +11 -0
  11. package/lib/components/card/CardSubtitle.vue +24 -0
  12. package/lib/components/card/CardTitle.vue +52 -0
  13. package/lib/components/chip/UiChip.vue +2 -2
  14. package/lib/components/console/RemoteConsole.vue +1 -1
  15. package/lib/components/dropdown/DropdownTitle.vue +7 -3
  16. package/lib/components/icon/VmIcon.vue +1 -1
  17. package/lib/components/menu/MenuItem.vue +1 -1
  18. package/lib/components/menu/MenuList.vue +5 -5
  19. package/lib/components/stacked-bar/StackedBar.vue +49 -0
  20. package/lib/components/stacked-bar/StackedBarSegment.vue +73 -0
  21. package/lib/components/state-hero/ComingSoonHero.vue +9 -0
  22. package/lib/components/state-hero/LoadingHero.vue +9 -0
  23. package/lib/components/state-hero/ObjectNotFoundHero.vue +13 -0
  24. package/lib/components/state-hero/StateHero.vue +53 -0
  25. package/lib/components/table/ColumnTitle.vue +150 -0
  26. package/lib/components/table/UiTable.vue +58 -0
  27. package/lib/components/tooltip/TooltipList.vue +0 -2
  28. package/lib/components/tree/TreeItemLabel.vue +4 -0
  29. package/lib/composables/tree/types.ts +1 -1
  30. package/lib/i18n.ts +33 -1
  31. package/lib/layouts/CoreLayout.vue +6 -97
  32. package/lib/locales/de.json +31 -4
  33. package/lib/locales/en.json +45 -13
  34. package/lib/locales/fr.json +45 -13
  35. package/lib/types/utility.type.ts +2 -0
  36. package/lib/utils/if-else.utils.ts +3 -3
  37. package/package.json +15 -7
@@ -0,0 +1,24 @@
1
+ <!-- v1.0 -->
2
+ <template>
3
+ <div class="card-subtitle">
4
+ <span class="typo h7-semi-bold"><slot /></span>
5
+ <span v-if="$slots.info" class="typo p3-medium"><slot name="info" /></span>
6
+ </div>
7
+ </template>
8
+
9
+ <script setup lang="ts">
10
+ defineSlots<{
11
+ default: () => void
12
+ info: () => void
13
+ }>()
14
+ </script>
15
+
16
+ <style lang="postcss" scoped>
17
+ .card-subtitle {
18
+ color: var(--color-purple-base);
19
+ border-bottom: 0.1rem solid var(--color-purple-base);
20
+ display: flex;
21
+ justify-content: space-between;
22
+ gap: 0.8rem;
23
+ }
24
+ </style>
@@ -0,0 +1,52 @@
1
+ <!-- v1.0 -->
2
+ <template>
3
+ <div class="card-title">
4
+ <div class="main-content">
5
+ <div class="title typo h6-medium"><slot /></div>
6
+
7
+ <div v-if="$slots.info" class="info typo h7-semi-bold"><slot name="info" /></div>
8
+ </div>
9
+ <p v-if="$slots.description" class="description typo p3-regular">
10
+ <slot name="description" />
11
+ </p>
12
+ </div>
13
+ </template>
14
+
15
+ <script lang="ts" setup>
16
+ defineSlots<{
17
+ default: () => void
18
+ info: () => void
19
+ description: () => void
20
+ }>()
21
+ </script>
22
+
23
+ <style lang="postcss" scoped>
24
+ .card-title {
25
+ display: flex;
26
+ flex-direction: column;
27
+ }
28
+
29
+ .main-content {
30
+ display: flex;
31
+ gap: 1.6rem;
32
+ align-items: center;
33
+ }
34
+
35
+ .title {
36
+ display: flex;
37
+ align-items: center;
38
+ gap: 1.6rem;
39
+ }
40
+
41
+ .info {
42
+ margin-left: auto;
43
+ display: flex;
44
+ align-items: center;
45
+ gap: 0.8rem;
46
+ color: var(--color-purple-base);
47
+ }
48
+
49
+ .description {
50
+ color: var(--color-grey-300);
51
+ }
52
+ </style>
@@ -25,8 +25,8 @@ withDefaults(
25
25
  )
26
26
 
27
27
  const emit = defineEmits<{
28
- (event: 'edit'): void
29
- (event: 'remove'): void
28
+ edit: []
29
+ remove: []
30
30
  }>()
31
31
  </script>
32
32
 
@@ -1,6 +1,6 @@
1
1
  <template>
2
2
  <div :class="uiStore.isMobile ? 'mobile' : undefined" class="remote-console">
3
- <div ref="consoleContainer" class="console"></div>
3
+ <div ref="consoleContainer" class="console" />
4
4
  </div>
5
5
  </template>
6
6
 
@@ -9,8 +9,12 @@
9
9
  <slot />
10
10
  </div>
11
11
  <div v-if="onToggleSelectAll" class="buttons">
12
- <span v-if="selected !== 'all'" @click="emit('toggleSelectAll', true)">Select all</span>
13
- <span v-if="selected !== 'none'" @click="emit('toggleSelectAll', false)">Deselect all</span>
12
+ <span v-if="selected !== 'all'" @click="emit('toggleSelectAll', true)">
13
+ {{ $t('core.select.all') }}
14
+ </span>
15
+ <span v-if="selected !== 'none'" @click="emit('toggleSelectAll', false)">
16
+ {{ $t('core.select.none') }}
17
+ </span>
14
18
  </div>
15
19
  </div>
16
20
  </template>
@@ -29,7 +33,7 @@ withDefaults(
29
33
  )
30
34
 
31
35
  const emit = defineEmits<{
32
- (event: 'toggleSelectAll', value: boolean): void
36
+ toggleSelectAll: [value: boolean]
33
37
  }>()
34
38
  </script>
35
39
 
@@ -7,8 +7,8 @@
7
7
  </template>
8
8
 
9
9
  <script lang="ts" setup>
10
- import PowerStateIcon from '@core/components/PowerStateIcon.vue'
11
10
  import UiIcon from '@core/components/icon/UiIcon.vue'
11
+ import PowerStateIcon from '@core/components/PowerStateIcon.vue'
12
12
  import type { POWER_STATE } from '@core/types/power-state.type'
13
13
  import { faDisplay } from '@fortawesome/free-solid-svg-icons'
14
14
  import { FontAwesomeLayers } from '@fortawesome/vue-fontawesome'
@@ -25,8 +25,8 @@
25
25
 
26
26
  <script lang="ts" setup>
27
27
  import UiIcon from '@core/components/icon/UiIcon.vue'
28
- import MenuTrigger from '@core/components/menu/MenuTrigger.vue'
29
28
  import MenuList from '@core/components/menu/MenuList.vue'
29
+ import MenuTrigger from '@core/components/menu/MenuTrigger.vue'
30
30
  import { useContext } from '@core/composables/context.composable'
31
31
  import { DisabledContext } from '@core/context'
32
32
  import { IK_CLOSE_MENU, IK_MENU_HORIZONTAL } from '@core/utils/injection-keys.util'
@@ -12,9 +12,13 @@
12
12
  import { useContext } from '@core/composables/context.composable'
13
13
  import { DisabledContext } from '@core/context'
14
14
  import { IK_CLOSE_MENU, IK_MENU_HORIZONTAL, IK_MENU_TELEPORTED } from '@core/utils/injection-keys.util'
15
+ import { onClickOutside, unrefElement, whenever } from '@vueuse/core'
15
16
  import placementJs, { type Options } from 'placement.js'
16
17
  import { computed, inject, nextTick, provide, ref, useSlots } from 'vue'
17
- import { onClickOutside, unrefElement, whenever } from '@vueuse/core'
18
+
19
+ defineOptions({
20
+ inheritAttrs: false,
21
+ })
18
22
 
19
23
  const props = withDefaults(
20
24
  defineProps<{
@@ -26,10 +30,6 @@ const props = withDefaults(
26
30
  { disabled: undefined }
27
31
  )
28
32
 
29
- defineOptions({
30
- inheritAttrs: false,
31
- })
32
-
33
33
  const slots = useSlots()
34
34
  const isOpen = ref(false)
35
35
  const menu = ref()
@@ -0,0 +1,49 @@
1
+ <!-- v1.0 -->
2
+ <template>
3
+ <div class="stacked-bar">
4
+ <StackedBarSegment
5
+ v-for="(segment, index) in computedSegments"
6
+ :key="index"
7
+ :color="segment.color"
8
+ :percentage="segment.percentage"
9
+ />
10
+ </div>
11
+ </template>
12
+
13
+ <script lang="ts" setup>
14
+ import StackedBarSegment from '@core/components/stacked-bar/StackedBarSegment.vue'
15
+ import type { Color } from '@core/types/color.type'
16
+ import { computed } from 'vue'
17
+
18
+ type Segment = {
19
+ value: number
20
+ color: Color
21
+ }
22
+
23
+ const props = defineProps<{
24
+ segments: Segment[]
25
+ maxValue?: number
26
+ }>()
27
+
28
+ const totalValue = computed(() => props.segments.reduce((acc, bar) => acc + bar.value, 0))
29
+
30
+ const max = computed(() => Math.max(props.maxValue ?? 0, totalValue.value))
31
+
32
+ const computedSegments = computed(() => {
33
+ return props.segments.map(segment => ({
34
+ ...segment,
35
+ percentage: (segment.value / max.value) * 100,
36
+ }))
37
+ })
38
+ </script>
39
+
40
+ <style lang="postcss" scoped>
41
+ .stacked-bar {
42
+ width: 100%;
43
+ display: flex;
44
+ height: 4rem;
45
+ border-radius: 0.8rem;
46
+ overflow: hidden;
47
+ background-color: var(--background-color-purple-10);
48
+ }
49
+ </style>
@@ -0,0 +1,73 @@
1
+ <template>
2
+ <div
3
+ v-tooltip="{ selector: '.ellipsis' }"
4
+ :class="color"
5
+ :style="{ width: percentage + '%' }"
6
+ class="stacked-bar-segment typo c4-semi-bold"
7
+ >
8
+ <div ref="ellipsisElement" :class="{ hidden }" class="ellipsis">
9
+ {{ $n(percentage / 100, 'percent') }}
10
+ </div>
11
+ </div>
12
+ </template>
13
+
14
+ <script lang="ts" setup>
15
+ import { vTooltip } from '@core/directives/tooltip.directive'
16
+ import type { Color } from '@core/types/color.type'
17
+ import { hasEllipsis } from '@core/utils/has-ellipsis.util'
18
+ import { useResizeObserver } from '@vueuse/core'
19
+ import { ref } from 'vue'
20
+
21
+ defineProps<{
22
+ color: Color
23
+ percentage: number
24
+ }>()
25
+
26
+ const hidden = ref(false)
27
+ const ellipsisElement = ref<HTMLElement | null>(null)
28
+
29
+ useResizeObserver(ellipsisElement, ([entry]) => {
30
+ hidden.value = hasEllipsis(entry.target)
31
+ })
32
+ </script>
33
+
34
+ <style lang="postcss" scoped>
35
+ /* COLOR VARIANT */
36
+ .stacked-bar-segment {
37
+ &.info {
38
+ --background-color: var(--color-purple-base);
39
+ }
40
+
41
+ &.success {
42
+ --background-color: var(--color-green-base);
43
+ }
44
+
45
+ &.warning {
46
+ --background-color: var(--color-orange-base);
47
+ }
48
+
49
+ &.error {
50
+ --background-color: var(--color-red-base);
51
+ }
52
+ }
53
+
54
+ /* IMPLEMENTATION */
55
+ .stacked-bar-segment {
56
+ display: flex;
57
+ justify-content: center;
58
+ align-items: center;
59
+ white-space: nowrap;
60
+ color: var(--color-grey-600);
61
+ background-color: var(--background-color);
62
+ }
63
+
64
+ .ellipsis {
65
+ overflow: hidden;
66
+ white-space: nowrap;
67
+ min-width: 0;
68
+ }
69
+
70
+ .hidden {
71
+ visibility: hidden;
72
+ }
73
+ </style>
@@ -0,0 +1,9 @@
1
+ <template>
2
+ <StateHero image="under-construction" class="coming-soon-hero">
3
+ {{ $t('coming-soon') }}
4
+ </StateHero>
5
+ </template>
6
+
7
+ <script lang="ts" setup>
8
+ import StateHero from '@core/components/state-hero/StateHero.vue'
9
+ </script>
@@ -0,0 +1,9 @@
1
+ <template>
2
+ <StateHero busy class="loading-hero">
3
+ {{ $t('loading-in-progress') }}
4
+ </StateHero>
5
+ </template>
6
+
7
+ <script lang="ts" setup>
8
+ import StateHero from '@core/components/state-hero/StateHero.vue'
9
+ </script>
@@ -0,0 +1,13 @@
1
+ <template>
2
+ <StateHero image="no-result" class="object-not-found-hero">
3
+ {{ $t('object-not-found', { id }) }}
4
+ </StateHero>
5
+ </template>
6
+
7
+ <script lang="ts" setup>
8
+ import StateHero from '@core/components/state-hero/StateHero.vue'
9
+
10
+ defineProps<{
11
+ id: string
12
+ }>()
13
+ </script>
@@ -0,0 +1,53 @@
1
+ <template>
2
+ <div class="state-hero">
3
+ <UiSpinner v-if="busy" class="spinner" />
4
+ <img v-else-if="imageSrc" :src="imageSrc" alt="" class="image" />
5
+ <p v-if="$slots.default" class="typo h2-black text">
6
+ <slot />
7
+ </p>
8
+ </div>
9
+ </template>
10
+
11
+ <script lang="ts" setup>
12
+ import UiSpinner from '@core/components/UiSpinner.vue'
13
+ import { computed } from 'vue'
14
+
15
+ const props = defineProps<{
16
+ busy?: boolean
17
+ image?: 'no-result' | 'under-construction' // TODO: 'offline' | 'no-data' | 'not-found' | 'all-good' | 'all-done' | 'error'
18
+ }>()
19
+
20
+ const imageSrc = computed(() => {
21
+ try {
22
+ return new URL(`../../assets/${props.image}.svg`, import.meta.url).href
23
+ } catch {
24
+ return undefined
25
+ }
26
+ })
27
+ </script>
28
+
29
+ <style lang="postcss" scoped>
30
+ .state-hero {
31
+ flex: 1;
32
+ display: flex;
33
+ flex-direction: column;
34
+ align-items: center;
35
+ justify-content: center;
36
+ gap: 4.2rem;
37
+ }
38
+
39
+ .image {
40
+ width: 90%;
41
+ max-width: 55rem;
42
+ }
43
+
44
+ .spinner {
45
+ color: var(--color-purple-base);
46
+ font-size: 10rem;
47
+ }
48
+
49
+ .text {
50
+ color: var(--color-purple-base);
51
+ margin-top: 4rem;
52
+ }
53
+ </style>
@@ -0,0 +1,150 @@
1
+ <!-- v1.0 -->
2
+ <template>
3
+ <MenuList :disabled placement="bottom-start" shadow>
4
+ <template #trigger="{ open, isOpen }">
5
+ <th
6
+ :class="{ interactive, disabled, focus: isOpen }"
7
+ class="column-header"
8
+ @click="ev => (interactive ? open(ev) : noop())"
9
+ >
10
+ <div class="content">
11
+ <span class="label">
12
+ <UiIcon :icon />
13
+ <slot />
14
+ </span>
15
+ <UiIcon :icon="currentInteraction?.icon" />
16
+ </div>
17
+ </th>
18
+ </template>
19
+ <MenuItem
20
+ v-for="interaction in interactions"
21
+ :key="interaction.id"
22
+ v-tooltip="$t('coming-soon')"
23
+ :disabled="interaction.disabled"
24
+ :on-click="() => updateInteraction(interaction)"
25
+ >
26
+ <UiIcon :icon="interaction.icon" />{{ interaction.label }}
27
+ <i v-if="currentInteraction?.id === interaction.id" class="current-interaction typo p3-regular-italic">
28
+ {{ $t('core.current').toLowerCase() }}
29
+ </i>
30
+ </MenuItem>
31
+ </MenuList>
32
+ </template>
33
+
34
+ <script lang="ts" setup>
35
+ import UiIcon from '@core/components/icon/UiIcon.vue'
36
+ import MenuItem from '@core/components/menu/MenuItem.vue'
37
+ import MenuList from '@core/components/menu/MenuList.vue'
38
+ import { vTooltip } from '@core/directives/tooltip.directive'
39
+ import type { IconDefinition } from '@fortawesome/fontawesome-common-types'
40
+ import { faArrowDown, faArrowUp, faEyeSlash, faFilter, faLayerGroup } from '@fortawesome/free-solid-svg-icons'
41
+ import { noop } from '@vueuse/core'
42
+ import { computed, inject } from 'vue'
43
+ import { useI18n } from 'vue-i18n'
44
+ import { useRouter } from 'vue-router'
45
+
46
+ type InteractionId = 'sort-asc' | 'sort-desc' | 'group' | 'filter' | 'hide'
47
+ type Interaction = {
48
+ disabled?: boolean
49
+ id: InteractionId
50
+ icon: IconDefinition
51
+ label: string
52
+ }
53
+
54
+ const props = withDefaults(
55
+ defineProps<{
56
+ id?: string
57
+ icon?: IconDefinition
58
+ interactive?: boolean
59
+ disabled?: boolean
60
+ }>(),
61
+ {
62
+ disabled: false,
63
+ interactive: true,
64
+ }
65
+ )
66
+ const { t } = useI18n()
67
+ const router = useRouter()
68
+
69
+ const interactions = computed<Interaction[]>(() => [
70
+ { id: 'sort-asc', icon: faArrowDown, label: t('core.sort.ascending'), disabled: true },
71
+ { id: 'sort-desc', icon: faArrowUp, label: t('core.sort.descending'), disabled: true },
72
+ { id: 'group', icon: faLayerGroup, label: t('core.group'), disabled: true },
73
+ { id: 'filter', icon: faFilter, label: t('core.filter'), disabled: true },
74
+ { id: 'hide', icon: faEyeSlash, label: t('core.hide'), disabled: true },
75
+ ])
76
+
77
+ const tableName = inject<string>('tableName')
78
+
79
+ const currentInteraction = computed(() =>
80
+ interactions.value.find(interaction => router.currentRoute.value.query[columnName] === interaction.id)
81
+ )
82
+
83
+ const columnName = `${tableName}__${props.id}`
84
+
85
+ const updateInteraction = (interaction: Interaction) => {
86
+ router.replace({
87
+ query: {
88
+ [columnName]: interaction.id,
89
+ },
90
+ })
91
+ }
92
+ </script>
93
+
94
+ <style lang="postcss" scoped>
95
+ /* COLOR VARIANTS */
96
+ .column-header.interactive {
97
+ --color: var(--color-purple-base);
98
+ --background-color: var(--background-color-primary);
99
+
100
+ &.focus {
101
+ --color: var(--color-purple-base);
102
+ --background-color: var(--background-color-purple-10);
103
+ }
104
+
105
+ &:hover {
106
+ --color: var(--color-purple-d20);
107
+ --background-color: var(--background-color-purple-20);
108
+ }
109
+
110
+ &:active {
111
+ --color: var(--color-purple-d40);
112
+ --background-color: var(--background-color-purple-30);
113
+ }
114
+
115
+ &.disabled {
116
+ --color: var(--color-grey-400);
117
+ --background-color: var(--background-color-secondary);
118
+ }
119
+ }
120
+ /* IMPLEMENTATION */
121
+ .column-header.interactive {
122
+ cursor: pointer;
123
+ color: var(--color);
124
+ background-color: var(--background-color);
125
+ &.disabled {
126
+ cursor: not-allowed;
127
+ }
128
+ }
129
+
130
+ .current-interaction {
131
+ color: var(--color-grey-300);
132
+ }
133
+
134
+ .content {
135
+ display: flex;
136
+ align-items: center;
137
+ justify-content: space-between;
138
+ gap: 1rem;
139
+ }
140
+
141
+ .label {
142
+ display: flex;
143
+ align-items: center;
144
+ gap: 1rem;
145
+ }
146
+
147
+ .filter-icon {
148
+ cursor: pointer;
149
+ }
150
+ </style>
@@ -0,0 +1,58 @@
1
+ <template>
2
+ <table :class="{ 'vertical-border': verticalBorder }" class="ui-table typo p2-regular">
3
+ <slot />
4
+ </table>
5
+ </template>
6
+
7
+ <script lang="ts" setup>
8
+ import { provide } from 'vue'
9
+
10
+ const props = defineProps<{
11
+ name?: string
12
+ verticalBorder?: boolean
13
+ }>()
14
+
15
+ provide('tableName', props.name)
16
+ </script>
17
+
18
+ <style lang="postcss" scoped>
19
+ .ui-table {
20
+ width: 100%;
21
+ border-spacing: 0;
22
+ background-color: var(--background-color-primary);
23
+ line-height: 2.4rem;
24
+ color: var(--color-grey-200);
25
+
26
+ :deep(th),
27
+ :deep(td) {
28
+ padding: 1rem;
29
+ border-top: 0.1rem solid var(--color-grey-500);
30
+ text-align: left;
31
+ }
32
+
33
+ :deep(th) {
34
+ font-weight: 700;
35
+ }
36
+
37
+ :deep(thead) {
38
+ th,
39
+ td {
40
+ color: var(--color-purple-base);
41
+ font-size: 1.4rem;
42
+ font-weight: 400;
43
+ text-transform: uppercase;
44
+ }
45
+ }
46
+
47
+ &.vertical-border {
48
+ :deep(th),
49
+ :deep(td) {
50
+ border-right: 0.1rem solid var(--color-grey-500);
51
+
52
+ &:last-child {
53
+ border-right: none;
54
+ }
55
+ }
56
+ }
57
+ }
58
+ </style>
@@ -11,5 +11,3 @@ import { storeToRefs } from 'pinia'
11
11
  const tooltipStore = useTooltipStore()
12
12
  const { tooltips } = storeToRefs(tooltipStore)
13
13
  </script>
14
-
15
- <style scoped></style>
@@ -51,6 +51,10 @@ import { faAngleDown, faAngleRight } from '@fortawesome/free-solid-svg-icons'
51
51
  import { inject, ref } from 'vue'
52
52
  import type { RouteLocationRaw } from 'vue-router'
53
53
 
54
+ defineOptions({
55
+ inheritAttrs: false,
56
+ })
57
+
54
58
  defineProps<{
55
59
  icon?: IconDefinition
56
60
  route: RouteLocationRaw
@@ -1,9 +1,9 @@
1
- import { useTree } from '@core/composables/tree.composable'
2
1
  import type { Branch } from '@core/composables/tree/branch'
3
2
  import type { BranchDefinition } from '@core/composables/tree/branch-definition'
4
3
  import type { Leaf } from '@core/composables/tree/leaf'
5
4
  import type { LeafDefinition } from '@core/composables/tree/leaf-definition'
6
5
  import type { TreeNodeBase } from '@core/composables/tree/tree-node-base'
6
+ import { useTree } from '@core/composables/tree.composable'
7
7
 
8
8
  export type TreeNodeId = string | number
9
9
 
package/lib/i18n.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { createI18n } from 'vue-i18n'
2
1
  import messages from '@intlify/unplugin-vue-i18n/messages'
2
+ import { createI18n } from 'vue-i18n'
3
3
 
4
4
  interface Locales {
5
5
  [key: string]: {
@@ -27,6 +27,15 @@ export default createI18n({
27
27
  locale: localStorage.getItem('lang') ?? 'en',
28
28
  fallbackLocale: 'en',
29
29
  messages,
30
+ missing: (locale, key, vm) => {
31
+ if (!import.meta.env.DEV) {
32
+ return key
33
+ }
34
+
35
+ console.warn(`i18n key not found: ${key}`, `Used in ${vm?.type.__name ?? 'unknown component'}`)
36
+
37
+ return `🌎❗⟨${key}⟩`
38
+ },
30
39
  datetimeFormats: {
31
40
  en: {
32
41
  date_short: {
@@ -155,4 +164,27 @@ export default createI18n({
155
164
  },
156
165
  },
157
166
  },
167
+ numberFormats: {
168
+ en: {
169
+ percent: {
170
+ style: 'percent',
171
+ minimumFractionDigits: 0,
172
+ maximumFractionDigits: 2,
173
+ },
174
+ },
175
+ fr: {
176
+ percent: {
177
+ style: 'percent',
178
+ minimumFractionDigits: 0,
179
+ maximumFractionDigits: 2,
180
+ },
181
+ },
182
+ de: {
183
+ percent: {
184
+ style: 'percent',
185
+ minimumFractionDigits: 0,
186
+ maximumFractionDigits: 2,
187
+ },
188
+ },
189
+ },
158
190
  })