mirror of
https://github.com/ArthurDanjou/ui.git
synced 2026-01-14 12:14:41 +01:00
feat(module): support i18n in components (#2553)
Co-authored-by: Benjamin Canac <canacb1@gmail.com>
This commit is contained in:
@@ -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: []
|
||||
})
|
||||
})) || []
|
||||
})))
|
||||
|
||||
181
docs/content/1.getting-started/7.i18n/1.nuxt.md
Normal file
181
docs/content/1.getting-started/7.i18n/1.nuxt.md
Normal 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).
|
||||
190
docs/content/1.getting-started/7.i18n/2.vue.md
Normal file
190
docs/content/1.getting-started/7.i18n/2.vue.md
Normal 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).
|
||||
@@ -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
|
||||
|
||||
@@ -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 }
|
||||
},
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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)"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)"
|
||||
/>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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 })"
|
||||
/>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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 })"
|
||||
/>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
8
src/runtime/composables/defineLocale.ts
Normal file
8
src/runtime/composables/defineLocale.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import type { Locale, LocalePair } from '../types/locale'
|
||||
|
||||
export function defineLocale(name: string, pair: LocalePair): Locale {
|
||||
return {
|
||||
name,
|
||||
ui: pair
|
||||
}
|
||||
}
|
||||
13
src/runtime/composables/useLocale.ts
Normal file
13
src/runtime/composables/useLocale.ts
Normal 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
37
src/runtime/locale/de.ts
Normal 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
37
src/runtime/locale/en.ts
Normal 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
37
src/runtime/locale/fr.ts
Normal 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'
|
||||
}
|
||||
})
|
||||
4
src/runtime/locale/index.ts
Normal file
4
src/runtime/locale/index.ts
Normal 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
37
src/runtime/locale/ru.ts
Normal 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: 'Нет данных'
|
||||
}
|
||||
})
|
||||
@@ -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'
|
||||
|
||||
40
src/runtime/types/locale.ts
Normal file
40
src/runtime/types/locale.ts
Normal 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
|
||||
}
|
||||
37
src/runtime/utils/locale.ts
Normal file
37
src/runtime/utils/locale.ts
Normal 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)
|
||||
}
|
||||
}
|
||||
@@ -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 */
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user