feat(module): support i18n in components (#2553)

Co-authored-by: Benjamin Canac <canacb1@gmail.com>
This commit is contained in:
Alex
2024-11-08 21:22:57 +05:00
committed by GitHub
parent 1e7638bd03
commit 26362408b1
30 changed files with 673 additions and 18 deletions

View File

@@ -79,6 +79,11 @@ const updatedNavigation = computed(() => navigation.value?.map(item => ({
title: 'Installation',
active: route.path.startsWith('/getting-started/installation'),
children: []
}),
...(child.path === '/getting-started/i18n' && {
title: 'I18n',
active: route.path.startsWith('/getting-started/i18n'),
children: []
})
})) || []
})))

View File

@@ -0,0 +1,181 @@
---
navigation.title: Nuxt
title: Internationalization (i18n) in a Nuxt app
description: 'Learn how to internationalize your Nuxt app and support multi-directional support (LTR/RTL).'
select:
items:
- label: Nuxt
icon: i-logos-nuxt-icon
to: /getting-started/i18n/nuxt
- label: Vue
icon: i-logos-vue
to: /getting-started/i18n/vue
---
## Usage
Nuxt UI provides an [App](/components/app) component that wraps your app to provide global configurations.
### Locale
Use the `locale` prop with the locale you want to use from `@nuxt/ui/locale`:
```vue [app.vue]
<script setup lang="ts">
import { fr } from '@nuxt/ui/locale'
</script>
<template>
<UApp :locale="fr">
<NuxtPage />
</UApp>
</template>
```
#### Custom locale
You also have the option to add your own locale using `defineLocale`:
```vue [app.vue]
<script setup lang="ts">
const locale = defineLocale('My custom locale', {
// implement pairs
})
</script>
<template>
<UApp :locale="locale">
<NuxtPage />
</UApp>
</template>
```
#### Dynamic locale
To dynamically switch between languages, you can use the [NuxtI18n](https://i18n.nuxtjs.org/docs/getting-started) module.
1. Install the `@nuxtjs/i18n` package:
::code-group{sync="pm"}
```bash [pnpm]
pnpm add @nuxtjs/i18n@next
```
```bash [yarn]
yarn add @nuxtjs/i18n@next
```
```bash [npm]
npm install @nuxtjs/i18n@next
```
```bash [bun]
bun add @nuxtjs/i18n@next
```
::
2. Add the `@nuxtjs/i18n` module to your `nuxt.config.ts`{lang="ts-type"} and define the locales you want to support:
```ts [nuxt.config.ts]
export default defineNuxtConfig({
modules: [
'@nuxt/ui',
'@nuxtjs/i18n'
],
i18n: {
locales: [{
code: 'de',
name: 'Deutsch'
}, {
code: 'en',
name: 'English'
}, {
code: 'fr',
name: 'Français'
}]
}
})
```
3. Use the `locale` prop with the `useI18n` locale you want to use from `@nuxt/ui/locale`:
```vue [app.vue]
<script setup lang="ts">
import * as locales from '@nuxt/ui/locale'
const { locale } = useI18n()
</script>
<template>
<UApp :locale="locales[locale]">
<NuxtPage />
</UApp>
</template>
```
### Direction
Use the `dir` prop with `ltr` or `rtl` to set the global reading direction of your app:
```vue [app.vue]
<template>
<UApp dir="rtl">
<NuxtPage />
</UApp>
</template>
```
#### Dynamic direction
To dynamically change the global reading direction of your app, you can use the [useTextDirection](https://vueuse.org/core/useTextDirection/) composable.
1. Install the `@vueuse/core` package:
::code-group{sync="pm"}
```bash [pnpm]
pnpm add @vueuse/core
```
```bash [yarn]
yarn add @vueuse/core
```
```bash [npm]
npm install @vueuse/core
```
```bash [bun]
bun add @vueuse/core
```
::
2. Then in your `app.vue`:
```vue [app.vue]
<script setup lang="ts">
import { useTextDirection } from '@vueuse/core'
const textDirection = useTextDirection({ initialValue: 'ltr' })
const dir = computed(() => textDirection.value === 'rtl' ? 'rtl' : 'ltr')
</script>
<template>
<UApp :dir="dir">
<NuxtPage />
</UApp>
</template>
```
## Supported languages
<!-- TODO: add auto generating language list https://github.com/nuxt/ui/issues/2565 -->
* `de` - Deutsch
* `en` - English (default)
* `fr` - Français
* `ru` - Русский
If you need any other languages, a [PR](https://github.com/nuxt/ui/pulls) is always welcome, you only need to add a language file [here](https://github.com/nuxt/ui/tree/v3/src/runtime/locale).

View File

@@ -0,0 +1,190 @@
---
navigation.title: Vue
title: Internationalization (i18n) in a Vue app
description: 'Learn how to internationalize your Vue app and support multi-directional support (LTR/RTL).'
select:
items:
- label: Nuxt
icon: i-logos-nuxt-icon
to: /getting-started/i18n/nuxt
- label: Vue
icon: i-logos-vue
to: /getting-started/i18n/vue
---
## Usage
Nuxt UI provides an [App](/components/app) component that wraps your app to provide global configurations.
### Locale
Use the `locale` prop with the locale you want to use from `@nuxt/ui/locale`:
```vue [App.vue]
<script setup lang="ts">
import { fr } from '@nuxt/ui/locale'
</script>
<template>
<UApp :locale="fr">
<RouterView />
</UApp>
</template>
```
#### Custom locale
You also have the option to add your locale using `defineLocale`:
```vue [App.vue]
<script setup lang="ts">
import { defineLocale } from '@nuxt/ui/runtime/composables/defineLocale'
const locale = defineLocale('My custom locale', {
// implement pairs
})
</script>
<template>
<UApp :locale="locale">
<RouterView />
</UApp>
</template>
```
#### Dynamic locale
To dynamically switch between languages, you can use the [VueI18n](https://vue-i18n.intlify.dev/) plugin.
1. Install the `vue-i18n` package:
::code-group{sync="pm"}
```bash [pnpm]
pnpm add vue-i18n@10
```
```bash [yarn]
yarn add vue-i18n@10
```
```bash [npm]
npm install vue-i18n@10
```
```bash [bun]
bun add vue-i18n@10
```
::
2. Define the `createI18n` instance and register the `i18n` plugin in your `main.ts` file:
```ts{3-19,22} [main.ts]
import { createApp } from 'vue'
import App from './App.vue'
import { createI18n } from 'vue-i18n'
const messages = {
en: {
// ...
},
de: {
// ...
},
}
const i18n = createI18n({
legacy: false,
locale: 'en',
availableLocales: ['en', 'de'],
messages,
})
createApp(App)
.use(i18n)
.mount('#app')
```
3. Use the `locale` prop with the `useI18n` locale you want to use from `@nuxt/ui/locale`:
```vue [App.vue]
<script setup lang="ts">
import { useI18n } from 'vue-i18n'
import * as locales from '@nuxt/ui/locale'
const { locale } = useI18n()
</script>
<template>
<UApp :locale="locales[locale]">
<RouterView />
</UApp>
</template>
```
### Direction
Use the `dir` prop with `ltr` or `rtl` to set the global reading direction of your app:
```vue [App.vue]
<template>
<UApp dir="rtl">
<NuxtPage />
</UApp>
</template>
```
#### Dynamic direction
To dynamically change the global reading direction of your app, you can use the [useTextDirection](https://vueuse.org/core/useTextDirection/) composable.
1. Install the `@vueuse/core` package:
::code-group{sync="pm"}
```bash [pnpm]
pnpm add @vueuse/core
```
```bash [yarn]
yarn add @vueuse/core
```
```bash [npm]
npm install @vueuse/core
```
```bash [bun]
bun add @vueuse/core
```
::
2. Then in your `App.vue`:
```vue [App.vue]
<script setup lang="ts">
import { computed } from 'vue'
import { useTextDirection } from '@vueuse/core'
const textDirection = useTextDirection()
const dir = computed(() => textDirection.value === 'rtl' ? 'rtl' : 'ltr')
</script>
<template>
<UApp :dir="dir">
<RouterView />
</UApp>
</template>
```
## Supported languages
<!-- TODO: add auto generating language list https://github.com/nuxt/ui/issues/2565 -->
* `de` - Deutsch
* `en` - English (default)
* `fr` - Français
* `ru` - Русский
If you need any other languages, a [PR](https://github.com/nuxt/ui/pulls) is always welcome, you only need to add a language file [here](https://github.com/nuxt/ui/tree/v3/src/runtime/locale).

View File

@@ -27,6 +27,14 @@ Use it as at the root of your app:
</template>
```
::tip{to="/getting-started/i18n/nuxt#locale"}
Learn how to use the `locale` prop to change the locale of your app.
::
::tip{to="/getting-started/i18n/nuxt#direction"}
Learn how to use the `dir` prop to change the global reading direction of your app.
::
## API
### Props

View File

@@ -57,6 +57,7 @@ export default defineNuxtConfig({
routeRules: {
'/': { redirect: '/getting-started', prerender: false },
'/getting-started/installation': { redirect: '/getting-started/installation/nuxt', prerender: false },
'/getting-started/i18n': { redirect: '/getting-started/i18n/nuxt', prerender: false },
'/composables': { redirect: '/composables/define-shortcuts', prerender: false },
'/components': { redirect: '/components/app', prerender: false }
},

View File

@@ -30,7 +30,11 @@
"./vue-plugin": {
"types": "./vue-plugin.d.ts"
},
"./runtime/*": "./dist/runtime/*"
"./runtime/*": "./dist/runtime/*",
"./locale": {
"types": "./dist/runtime/locale/index.d.ts",
"import": "./dist/runtime/locale/index.js"
}
},
"imports": {
"#build/ui/*": "./.nuxt/ui/*.ts"

View File

@@ -74,6 +74,7 @@ const emits = defineEmits<AlertEmits>()
const slots = defineSlots<AlertSlots>()
const appConfig = useAppConfig()
const { t } = useLocale()
const multiline = computed(() => !!props.title && !!props.description)
@@ -123,7 +124,7 @@ const ui = computed(() => alert({
size="md"
color="neutral"
variant="link"
aria-label="Close"
:aria-label="t('ui.alert.close')"
v-bind="typeof close === 'object' ? close : undefined"
:class="ui.close({ class: props.ui?.close })"
@click="emits('update:open', false)"

View File

@@ -1,11 +1,12 @@
<script lang="ts">
import type { ConfigProviderProps, TooltipProviderProps } from 'radix-vue'
import { extendDevtoolsMeta } from '../composables/extendDevtoolsMeta'
import type { ToasterProps } from '../types'
import type { ToasterProps, Locale } from '../types'
export interface AppProps extends Omit<ConfigProviderProps, 'useId'> {
tooltip?: TooltipProviderProps
toaster?: ToasterProps | null
locale?: Locale
}
export interface AppSlots {
@@ -26,6 +27,7 @@ import { reactivePick } from '@vueuse/core'
import UToaster from './Toaster.vue'
import UModalProvider from './ModalProvider.vue'
import USlideoverProvider from './SlideoverProvider.vue'
import { localeContextInjectionKey } from '../composables/useLocale'
const props = defineProps<AppProps>()
defineSlots<AppSlots>()
@@ -33,6 +35,8 @@ defineSlots<AppSlots>()
const configProviderProps = useForwardProps(reactivePick(props, 'dir', 'scrollBody'))
const tooltipProps = toRef(() => props.tooltip)
const toasterProps = toRef(() => props.toaster)
provide(localeContextInjectionKey, computed(() => props.locale))
</script>
<template>

View File

@@ -134,6 +134,7 @@ const props = withDefaults(defineProps<CarouselProps<T>>(), {
defineSlots<CarouselSlots<T>>()
const appConfig = useAppConfig()
const { t } = useLocale()
const rootProps = useForwardProps(reactivePick(props, 'active', 'align', 'breakpoints', 'containScroll', 'dragFree', 'dragThreshold', 'duration', 'inViewThreshold', 'loop', 'skipSnaps', 'slidesToScroll', 'startIndex', 'watchDrag', 'watchResize', 'watchSlides', 'watchFocus'))
const ui = computed(() => carousel({
@@ -279,7 +280,7 @@ defineExpose({
size="md"
color="neutral"
variant="outline"
aria-label="Prev"
:aria-label="t('ui.carousel.prev')"
v-bind="typeof prev === 'object' ? prev : undefined"
:class="ui.prev({ class: props.ui?.prev })"
@click="scrollPrev"
@@ -290,7 +291,7 @@ defineExpose({
size="md"
color="neutral"
variant="outline"
aria-label="Next"
:aria-label="t('ui.carousel.next')"
v-bind="typeof next === 'object' ? next : undefined"
:class="ui.next({ class: props.ui?.next })"
@click="scrollNext"
@@ -300,7 +301,7 @@ defineExpose({
<div v-if="dots" :class="ui.dots({ class: props.ui?.dots })">
<template v-for="(_, index) in scrollSnaps" :key="index">
<button
:aria-label="`Go to slide ${index + 1}`"
:aria-label="t('ui.carousel.goto', { slide: index + 1 })"
:class="ui.dot({ class: props.ui?.dot, active: selectedIndex === index })"
@click="scrollTo(index)"
/>

View File

@@ -144,6 +144,7 @@ const slots = defineSlots<CommandPaletteSlots<G, T>>()
const searchTerm = defineModel<string>('searchTerm', { default: '' })
const appConfig = useAppConfig()
const { t } = useLocale()
const rootProps = useForwardPropsEmits(reactivePick(props, 'as', 'disabled', 'multiple', 'modelValue', 'defaultValue', 'selectedValue', 'resetSearchTermOnBlur'), emits)
const inputProps = useForwardProps(reactivePick(props, 'loading', 'loadingIcon', 'placeholder'))
@@ -245,7 +246,7 @@ const groups = computed(() => {
size="md"
color="neutral"
variant="ghost"
aria-label="Close"
:aria-label="t('ui.commandPalette.close')"
v-bind="typeof close === 'object' ? close : undefined"
:class="ui.close({ class: props.ui?.close })"
@click="emits('update:open', false)"
@@ -259,7 +260,7 @@ const groups = computed(() => {
<ComboboxContent :class="ui.content({ class: props.ui?.content })" :dismissable="false">
<ComboboxEmpty :class="ui.empty({ class: props.ui?.empty })">
<slot name="empty" :search-term="searchTerm">
{{ searchTerm ? `No results for ${searchTerm}` : 'No results' }}
{{ searchTerm ? t('ui.commandPalette.noMatch', { searchTerm }) : t('ui.commandPalette.noData') }}
</slot>
</ComboboxEmpty>

View File

@@ -141,6 +141,7 @@ import { get, escapeRegExp } from '../utils'
import UIcon from './Icon.vue'
import UAvatar from './Avatar.vue'
import UChip from './Chip.vue'
import { useLocale } from '../composables/useLocale'
defineOptions({ inheritAttrs: false })
@@ -157,6 +158,7 @@ const slots = defineSlots<InputMenuSlots<T>>()
const searchTerm = defineModel<string>('searchTerm', { default: '' })
const appConfig = useAppConfig()
const { t } = useLocale()
const rootProps = useForwardPropsEmits(reactivePick(props, 'as', 'modelValue', 'defaultValue', 'selectedValue', 'open', 'defaultOpen', 'resetSearchTermOnBlur'), emits)
const contentProps = toRef(() => defu(props.content, { side: 'bottom', sideOffset: 8, position: 'popper' }) as ComboboxContentProps)
const arrowProps = toRef(() => props.arrow as ComboboxArrowProps)
@@ -347,7 +349,7 @@ defineExpose({
<ComboboxContent :class="ui.content({ class: props.ui?.content })" v-bind="contentProps">
<ComboboxEmpty :class="ui.empty({ class: props.ui?.empty })">
<slot name="empty" :search-term="searchTerm">
{{ searchTerm ? `No results for ${searchTerm}` : 'No results' }}
{{ searchTerm ? t('ui.inputMenu.noMatch', { searchTerm }) : t('ui.inputMenu.noData') }}
</slot>
</ComboboxEmpty>

View File

@@ -103,6 +103,7 @@ const contentEvents = computed(() => {
})
const appConfig = useAppConfig()
const { t } = useLocale()
const ui = computed(() => modal({
transition: props.transition,
@@ -143,7 +144,7 @@ const ui = computed(() => modal({
size="md"
color="neutral"
variant="ghost"
aria-label="Close"
:aria-label="t('ui.modal.close')"
v-bind="typeof close === 'object' ? close : undefined"
:class="ui.close({ class: props.ui?.close })"
/>

View File

@@ -147,6 +147,8 @@ const slots = defineSlots<SelectMenuSlots<T>>()
const searchTerm = defineModel<string>('searchTerm', { default: '' })
const appConfig = useAppConfig()
const { t } = useLocale()
const rootProps = useForwardPropsEmits(reactivePick(props, 'modelValue', 'defaultValue', 'selectedValue', 'open', 'defaultOpen', 'resetSearchTermOnBlur'), emits)
const contentProps = toRef(() => defu(props.content, { side: 'bottom', sideOffset: 8, position: 'popper' }) as ComboboxContentProps)
const arrowProps = toRef(() => props.arrow as ComboboxArrowProps)
@@ -284,7 +286,7 @@ function onUpdateOpen(value: boolean) {
<ComboboxEmpty :class="ui.empty({ class: props.ui?.empty })">
<slot name="empty" :search-term="searchTerm">
{{ searchTerm ? `No results for ${searchTerm}` : 'No results' }}
{{ searchTerm ? t('ui.selectMenu.noMatch', { searchTerm }) : t('ui.selectMenu.noData') }}
</slot>
</ComboboxEmpty>

View File

@@ -102,6 +102,7 @@ const contentEvents = computed(() => {
})
const appConfig = useAppConfig()
const { t } = useLocale()
const ui = computed(() => slideover({
transition: props.transition,
@@ -142,7 +143,7 @@ const ui = computed(() => slideover({
size="md"
color="neutral"
variant="ghost"
aria-label="Close"
:aria-label="t('ui.slideover.close')"
v-bind="typeof close === 'object' ? close : undefined"
:class="ui.close({ class: props.ui?.close })"
/>

View File

@@ -114,6 +114,7 @@ import { upperFirst } from 'scule'
const props = defineProps<TableProps<T>>()
defineSlots<TableSlots<T>>()
const { t } = useLocale()
const data = computed(() => props.data ?? [])
const columns = computed<TableColumn<T>[]>(() => props.columns ?? Object.keys(data.value[0] ?? {}).map((accessorKey: string) => ({ accessorKey, header: upperFirst(accessorKey) })))
@@ -231,7 +232,7 @@ defineExpose({
<tr v-else :class="ui.tr({ class: [props.ui?.tr] })">
<td :colspan="columns?.length" :class="ui.empty({ class: props.ui?.empty })">
<slot name="empty">
No results
{{ t('ui.table.noData') }}
</slot>
</td>
</tr>

View File

@@ -74,6 +74,7 @@ const emits = defineEmits<ToastEmits>()
const slots = defineSlots<ToastSlots>()
const appConfig = useAppConfig()
const { t } = useLocale()
const rootProps = useForwardPropsEmits(reactivePick(props, 'as', 'defaultOpen', 'open', 'duration', 'type'), emits)
const multiline = computed(() => !!props.title && !!props.description)
@@ -151,7 +152,7 @@ defineExpose({
size="md"
color="neutral"
variant="link"
aria-label="Close"
:aria-label="t('ui.toast.close')"
v-bind="typeof close === 'object' ? close : undefined"
:class="ui.close({ class: props.ui?.close })"
@click.stop

View File

@@ -0,0 +1,8 @@
import type { Locale, LocalePair } from '../types/locale'
export function defineLocale(name: string, pair: LocalePair): Locale {
return {
name,
ui: pair
}
}

View File

@@ -0,0 +1,13 @@
import { computed, inject, ref } from 'vue'
import type { InjectionKey, Ref } from 'vue'
import type { Locale } from '../types/locale'
import { buildLocaleContext } from '../utils/locale'
import { en } from '../locale'
export const localeContextInjectionKey: InjectionKey<Ref<Locale | undefined>> = Symbol('nuxt-ui.locale-context')
export const useLocale = (localeOverrides?: Ref<Locale | undefined>) => {
const locale = localeOverrides || inject(localeContextInjectionKey, ref())!
return buildLocaleContext(computed(() => locale.value || en))
}

37
src/runtime/locale/de.ts Normal file
View File

@@ -0,0 +1,37 @@
import { defineLocale } from '../composables/defineLocale'
export default defineLocale('Deutsch', {
inputMenu: {
noMatch: 'Nichts gefunden',
noData: 'Keine Daten'
},
commandPalette: {
noMatch: 'Nichts gefunden',
noData: 'Keine Daten',
close: 'Schließen'
},
selectMenu: {
noMatch: 'Nichts gefunden',
noData: 'Keine Daten'
},
toast: {
close: 'Schließen'
},
carousel: {
prev: 'Weiter',
next: 'Zurück',
goto: 'Gehe zu {slide}'
},
modal: {
close: 'Schließen'
},
slideover: {
close: 'Schließen'
},
alert: {
close: 'Schließen'
},
table: {
noData: 'Keine Daten'
}
})

37
src/runtime/locale/en.ts Normal file
View File

@@ -0,0 +1,37 @@
import { defineLocale } from '../composables/defineLocale'
export default defineLocale('English', {
inputMenu: {
noMatch: 'No matching data',
noData: 'No data'
},
commandPalette: {
noMatch: 'No matching data',
noData: 'No data',
close: 'Close'
},
selectMenu: {
noMatch: 'No matching data',
noData: 'No data'
},
toast: {
close: 'Close'
},
carousel: {
prev: 'Prev',
next: 'Next',
goto: 'Go to slide {slide}'
},
modal: {
close: 'Close'
},
slideover: {
close: 'Close'
},
alert: {
close: 'Close'
},
table: {
noData: 'No data'
}
})

37
src/runtime/locale/fr.ts Normal file
View File

@@ -0,0 +1,37 @@
import { defineLocale } from '../composables/defineLocale'
export default defineLocale('Français', {
inputMenu: {
noMatch: 'Aucune donnée correspondante',
noData: 'Aucune donnée'
},
commandPalette: {
noMatch: 'Aucune donnée correspondante',
noData: 'Aucune donnée',
close: 'Fermer'
},
selectMenu: {
noMatch: 'Aucune donnée correspondante',
noData: 'Aucune donnée'
},
toast: {
close: 'Fermer'
},
carousel: {
prev: 'Précédent',
next: 'Suivant',
goto: 'Aller à {slide}'
},
modal: {
close: 'Fermer'
},
slideover: {
close: 'Fermer'
},
alert: {
close: 'Fermer'
},
table: {
noData: 'Aucune donnée'
}
})

View File

@@ -0,0 +1,4 @@
export { default as de } from './de'
export { default as en } from './en'
export { default as fr } from './fr'
export { default as ru } from './ru'

37
src/runtime/locale/ru.ts Normal file
View File

@@ -0,0 +1,37 @@
import { defineLocale } from '../composables/defineLocale'
export default defineLocale('Русский', {
inputMenu: {
noMatch: 'Совпадений не найдено',
noData: 'Нет данных'
},
commandPalette: {
noMatch: 'Совпадений не найдено',
noData: 'Нет данных',
close: 'Закрыть'
},
selectMenu: {
noMatch: 'Совпадений не найдено',
noData: 'Нет данных'
},
toast: {
close: 'Закрыть'
},
carousel: {
prev: 'Назад',
next: 'Далее',
goto: 'Перейти к {slide}'
},
modal: {
close: 'Закрыть'
},
slideover: {
close: 'Закрыть'
},
alert: {
close: 'Закрыть'
},
table: {
noData: 'Нет данных'
}
})

View File

@@ -42,3 +42,4 @@ export * from '../components/Toast.vue'
export * from '../components/Toaster.vue'
export * from '../components/Tooltip.vue'
export * from './form'
export * from './locale'

View File

@@ -0,0 +1,40 @@
export type LocalePair = {
inputMenu: {
noMatch: string
noData: string
}
commandPalette: {
noMatch: string
noData: string
close: string
}
selectMenu: {
noMatch: string
noData: string
}
toast: {
close: string
}
carousel: {
prev: string
next: string
goto: string
}
modal: {
close: string
}
slideover: {
close: string
}
alert: {
close: string
}
table: {
noData: string
}
}
export type Locale = {
name: string
ui: LocalePair
}

View File

@@ -0,0 +1,37 @@
import type { Ref } from 'vue'
import type { Locale } from '../types/locale'
import type { MaybeRef } from '@vueuse/core'
import { computed, isRef, ref, unref } from 'vue'
import { get } from './index'
export type TranslatorOption = Record<string, string | number>
export type Translator = (path: string, option?: TranslatorOption) => string
export type LocaleContext = {
locale: Ref<Locale>
lang: Ref<string>
t: Translator
}
export function buildTranslator(locale: MaybeRef<Locale>): Translator {
return (path, option) => translate(path, option, unref(locale))
}
export function translate(path: string, option: undefined | TranslatorOption, locale: Locale): string {
const prop: string = get(locale, path, path)
return prop.replace(
/\{(\w+)\}/g,
(_, key) => `${option?.[key] ?? `{${key}}`}`
)
}
export function buildLocaleContext(locale: MaybeRef<Locale>): LocaleContext {
const lang = computed(() => unref(locale).name)
const localeRef = isRef(locale) ? locale : ref(locale)
return {
lang,
locale: localeRef,
t: buildTranslator(locale)
}
}

View File

@@ -1278,7 +1278,7 @@ exports[`CommandPalette > renders without results correctly 1`] = `
</div>
<!--teleport start-->
<div id="radix-vue-combobox-content-v-0" role="listbox" data-state="open" style="display: flex; flex-direction: column; outline-color: none; outline-style: none; outline-width: initial;" class="relative overflow-hidden">
<div class="py-6 text-center text-sm text-[var(--ui-text-muted)]">No results</div>
<div class="py-6 text-center text-sm text-[var(--ui-text-muted)]">No data</div>
<div class="divide-y divide-[var(--ui-border)] scroll-py-1" data-radix-combobox-viewport="" role="presentation" style="position: relative; flex-grow: 1; flex-shrink: 1; flex-basis: 0%; overflow: auto;"></div>
<style>
/* Hide scrollbars cross-browser and enable momentum scroll for touch devices */

View File

@@ -1284,7 +1284,7 @@ exports[`CommandPalette > renders without results correctly 1`] = `
</div>
<!--teleport start-->
<div id="radix-vue-combobox-content-v-0-0-0" role="listbox" data-state="open" style="display: flex; flex-direction: column; outline-color: none; outline-style: none; outline-width: initial;" class="relative overflow-hidden">
<div class="py-6 text-center text-sm text-[var(--ui-text-muted)]">No results</div>
<div class="py-6 text-center text-sm text-[var(--ui-text-muted)]">No data</div>
<div class="divide-y divide-[var(--ui-border)] scroll-py-1" data-radix-combobox-viewport="" role="presentation" style="position: relative; flex-grow: 1; flex-shrink: 1; flex-basis: 0%; overflow: auto;"></div>
<style>
/* Hide scrollbars cross-browser and enable momentum scroll for touch devices */

View File

@@ -1243,7 +1243,7 @@ exports[`Table > renders without results correctly 1`] = `
</thead>
<tbody class="divide-y divide-[var(--ui-border)]">
<tr class="data-[selected=true]:bg-[var(--ui-bg-elevated)]/50">
<td colspan="0" class="py-6 text-center text-sm text-[var(--ui-text-muted)]"> No results </td>
<td colspan="0" class="py-6 text-center text-sm text-[var(--ui-text-muted)]">No data</td>
</tr>
</tbody>
</table>

View File

@@ -1243,7 +1243,7 @@ exports[`Table > renders without results correctly 1`] = `
</thead>
<tbody class="divide-y divide-[var(--ui-border)]">
<tr class="data-[selected=true]:bg-[var(--ui-bg-elevated)]/50">
<td colspan="0" class="py-6 text-center text-sm text-[var(--ui-text-muted)]"> No results </td>
<td colspan="0" class="py-6 text-center text-sm text-[var(--ui-text-muted)]">No data</td>
</tr>
</tbody>
</table>