Internationalization & RTL
Multi-Direction Support
Introduction
This documentation provides guidance on how to utilize multi-directional support in Radix Vue with SSR support. Radix Primitives rely on Floating UI
to position floating elements, which requires to be fed the current direction of the web app.
Radix components are LTR by default, but you are in control of what direction (only LTR, RTL, or both) you want to support. This section provides best practices to easily support RTL direction.
RTL
ConfigProvider
is a wrapper component to provide global configurations, including the directionality of the web app.
When creating localized apps that require right-to-left (RTL) reading direction, you need to wrap your application with the ConfigProvider
component to ensure all of the primitives adjust their behavior based on the dir
prop.
To make all Radix primitives RTL, wrap your entire App in ConfigProvider
and pass the dir
prop with the value rtl
.
Add the following code to your app.vue
or main layout component:
<script setup lang="ts">
import { ConfigProvider } from 'radix-vue'
</script>
<template>
<ConfigProvider dir="rtl">
<slot />
</ConfigProvider>
</template>
All Radix components that are wrapped in the provider inherit the dir
attribute.
Dynamic Direction
To dynamically change the direction of Radix primitives, we could leverage the useTextDirection
composable and combine it with our ConfigProvider
.
But first, we need to install the @vueuse/core
package.
$ npm add @vueuse/core
Then in your root Vue file:
<script setup lang="ts">
import { computed } from 'vue'
import { ConfigProvider } from 'radix-vue'
import { useTextDirection } from '@vueuse/core'
const textDirection = useTextDirection()
const dir = computed(() => textDirection.value === 'rtl' ? 'rtl' : 'ltr')
</script>
<template>
<ConfigProvider :dir="dir">
<slot />
</ConfigProvider>
</template>
To support SSR - when the server has no access to the html
and its direction, set initialValue
in useTextDirection
.
<script setup lang="ts">
import { ConfigProvider } from 'radix-vue'
import { useTextDirection } from '@vueuse/core'
const textDirection = useTextDirection({ initialValue: 'rtl' })
const dir = computed(() => textDirection.value === 'rtl' ? 'rtl' : 'ltr')
</script>
<template>
<ConfigProvider :dir="dir">
<slot />
</ConfigProvider>
</template>
INFO
The dir
prop doesn't support auto
as a value, so we need an intermediate Ref to explicitly define the direction.
textDirection
is a Ref
, and by changing the value of it to either "ltr" or "rtl", the dir
attribute on the html
tag changes as well.
Internationalization
Some languages are written from LTR and others are written in RTL. In a multi-language web app, you need to configure directionality alongside the translations. This is a simplified guide on how to achieve that using radix-vue
primitives.
But first, let's install some required packages.
Dependencies
We rely on VueI18n
to manage different translations we want to support.
$ npm add vue-i18n@9
Go ahead and add some translations for the word "hello" in different languages at main.ts
.
import { createApp } from 'vue'
import './style.css'
import App from './App.vue'
import { createI18n } from 'vue-i18n'
const messages = {
en: {
hello: 'Hello',
},
fa: {
hello: 'درود',
},
ar: {
hello: 'مرحبا',
},
ja: {
hello: 'こんにちは',
}
}
const i18n = createI18n({
legacy: false, // you must set `false` to use the Composition API
locale: 'en', // set default locale
availableLocales: ['en', 'fa', 'ar', 'ja'],
messages,
})
createApp(App)
.use(i18n)
.mount('#app')
Language Selector
After setting the translations and adding the vue-i18n
plugin, we need a language selector in your app.vue
. By changing the language using this radix
select primitive:
- The translations are reactive to the new language
- The direction of the web app is reactive to the new language
<script setup lang="ts">
import { ConfigProvider, SelectContent, SelectGroup, SelectItem, SelectItemIndicator, SelectItemText, SelectLabel, SelectPortal, SelectRoot, SelectScrollDownButton, SelectScrollUpButton, SelectTrigger, SelectValue, SelectViewport, } from 'radix-vue'
import { useTextDirection } from '@vueuse/core'
import { useI18n } from 'vue-i18n'
import { ref } from 'vue'
type LanguageInfo = {
label: string
value: string
dir: 'ltr' | 'rtl'
}
const dir = useTextDirection({ initialValue: 'ltr' })
const { locale } = useI18n()
const selectedLanguage = ref<string>()
const languages: LanguageInfo[] = [
{ label: 'English', value: 'en', dir: 'ltr' },
{ label: 'Persian', value: 'fa', dir: 'rtl' },
{ label: 'Arabic', value: 'ar', dir: 'rtl' },
{ label: 'Japanese', value: 'ja', dir: 'ltr' },
]
function selectLanguage(newLanguage: string) {
const langInfo = languages.find(item => item.value === newLanguage)
if (!langInfo)
return
dir.value = langInfo.dir
locale.value = langInfo.value
}
</script>
<template>
<ConfigProvider :dir="dir">
<div class="flex flex-col max-w-[1400px] mx-auto gap-y-[8rem] justify-center items-center p-10">
<div class="text-2xl">
👋 {{ $t("hello") }}
</div>
<div class="text-2xl">
HTML is in <span class="text-bold text-purple-500">{{ dir }}</span> mode
</div>
<SelectRoot
v-model="selectedLanguage"
@update:model-value="selectLanguage"
>
<SelectTrigger
class="inline-flex min-w-[160px] items-center justify-between rounded px-[15px] text-[13px] leading-none h-[35px] gap-[5px] bg-white text-grass11 shadow-[0_2px_10px] shadow-black/10 hover:bg-mauve3 focus:shadow-[0_0_0_2px] focus:shadow-black data-[placeholder]:text-green9 outline-none"
aria-label="Customize options"
>
<SelectValue placeholder="Select a language..." />
<Icon
icon="radix-icons:chevron-down"
class="h-3.5 w-3.5"
/>
</SelectTrigger>
<SelectPortal>
<SelectContent
class="min-w-[160px] bg-white rounded shadow-[0px_10px_38px_-10px_rgba(22,_23,_24,_0.35),_0px_10px_20px_-15px_rgba(22,_23,_24,_0.2)] will-change-[opacity,transform] data-[side=top]:animate-slideDownAndFade data-[side=right]:animate-slideLeftAndFade data-[side=bottom]:animate-slideUpAndFade data-[side=left]:animate-slideRightAndFade z-[100]"
:side-offset="5"
>
<SelectScrollUpButton
class="flex items-center justify-center h-[25px] bg-white text-violet11 cursor-default"
>
<Icon icon="radix-icons:chevron-up" />
</SelectScrollUpButton>
<SelectViewport class="p-[5px]">
<SelectLabel class="px-[25px] text-xs leading-[25px] text-mauve11">
Languages
</SelectLabel>
<SelectGroup>
<SelectItem
v-for="(option, index) in languages"
:key="index"
class="text-[13px] leading-none text-grass11 rounded-[3px] flex items-center h-[25px] pr-[35px] pl-[25px] relative select-none data-[disabled]:text-mauve8 data-[disabled]:pointer-events-none data-[highlighted]:outline-none data-[highlighted]:bg-green9 data-[highlighted]:text-green1"
:value="option.value"
>
<SelectItemIndicator class="absolute left-0 w-[25px] inline-flex items-center justify-center">
<Icon icon="radix-icons:check" />
</SelectItemIndicator>
<SelectItemText>
{{ option.label }}
</SelectItemText>
</SelectItem>
</SelectGroup>
</SelectViewport>
<SelectScrollDownButton
class="flex items-center justify-center h-[25px] bg-white text-violet11 cursor-default"
>
<Icon icon="radix-icons:chevron-down" />
</SelectScrollDownButton>
</SelectContent>
</SelectPortal>
</SelectRoot>
</div>
</ConfigProvider>
</template>