Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,6 @@ __screenshots__/

.vercel
.now

# Superpowers
docs/superpowers/
26 changes: 26 additions & 0 deletions packages/docs/src/pages/en/getting-started/upgrade-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,32 @@ This page contains a detailed list of breaking changes and the steps required to

<PageFeatures />

## Locale

Vuetify's locale system is now powered by `@vuetify/v0` under the hood. The consumer-facing API (`useLocale`, `useRtl`, `VLocaleProvider`) is unchanged for the majority of users. If you only use `t()`, `n()`, `current`, `isRtl`, `rtlClasses`, or `decimalSeparator`, no changes are needed.

### LocaleInstance

Several properties have been removed from the `LocaleInstance` type:

- `provide()` — use `VLocaleProvider` or the `provideLocale()` composable instead
- `name` — adapter identity is no longer exposed
- `messages` — messages are managed internally; register them via `createVuetify({ locale: { messages } })`
- `fallback` — configure at creation time via `createVuetify({ locale: { fallback: 'en' } })`

```diff
const locale = useLocale()

- const scoped = locale.provide({ locale: 'fr' })
- console.log(locale.name) // 'vuetify'
- console.log(locale.messages.value)
- console.log(locale.fallback.value)
```

### vue-i18n adapter

The vue-i18n adapter continues to work with the same import path and configuration. No changes required.

## `useDisplay`

- Returned Refs are now readonly.
Expand Down
199 changes: 148 additions & 51 deletions packages/vuetify/src/composables/locale.ts
Original file line number Diff line number Diff line change
@@ -1,71 +1,31 @@
// Utilities
import { computed, inject, provide, ref, toRef } from 'vue'
import { createVuetifyAdapter } from '@/locale/adapters/vuetify'
import { createLocale as createV0Locale, createRtl as createV0Rtl, isFunction } from '@vuetify/v0'
import { computed, inject, provide, ref, shallowRef, toRef, watch } from 'vue'
import en from '@/locale/en'

// Types
import type { InjectionKey, Ref, ShallowRef } from 'vue'
import type { LocaleContext, RtlContext } from '@vuetify/v0'
import type { InjectionKey, Ref } from 'vue'

export interface LocaleMessages {
[key: string]: LocaleMessages | string
}

export interface LocaleOptions {
decimalSeparator?: string
messages?: LocaleMessages
messages?: Record<string, LocaleMessages>
locale?: string
fallback?: string
adapter?: LocaleInstance
}

export interface LocaleInstance {
name: string
decimalSeparator: ShallowRef<string>
messages: Ref<LocaleMessages>
Copy link
Copy Markdown
Member

@KaelWD KaelWD Apr 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No idea if anyone actually did this but it used to be possible to add to messages at runtime, now they all have to be declared upfront.

current: Ref<string>
fallback: Ref<string>
t: (key: string, ...params: unknown[]) => string
n: (value: number) => string
provide: (props: LocaleOptions) => LocaleInstance
n: (value: number, options?: Intl.NumberFormatOptions) => string
decimalSeparator: Ref<string>
}

export const LocaleSymbol: InjectionKey<LocaleInstance & RtlInstance> = Symbol.for('vuetify:locale')

function isLocaleInstance (obj: any): obj is LocaleInstance {
return obj.name != null
}

export function createLocale (options?: LocaleOptions & RtlOptions) {
const i18n = options?.adapter && isLocaleInstance(options?.adapter) ? options?.adapter : createVuetifyAdapter(options)
const rtl = createRtl(i18n, options)

return { ...i18n, ...rtl }
}

export function useLocale () {
const locale = inject(LocaleSymbol)

if (!locale) throw new Error('[Vuetify] Could not find injected locale instance')

return locale
}

export function provideLocale (props: LocaleOptions & RtlProps) {
const locale = inject(LocaleSymbol)

if (!locale) throw new Error('[Vuetify] Could not find injected locale instance')

const i18n = locale.provide(props)
const rtl = provideRtl(i18n, locale.rtl, props)

const data = { ...i18n, ...rtl }

provide(LocaleSymbol, data)

return data
}

// RTL

export interface RtlOptions {
rtl?: Record<string, boolean>
}
Expand All @@ -80,8 +40,19 @@ export interface RtlInstance {
rtlClasses: Ref<string>
}

/** @internal */
export interface InternalLocaleData {
_messages: Record<string, LocaleMessages>
_fallback: string
}

export interface FullLocaleInstance extends LocaleInstance, RtlInstance, InternalLocaleData {}

export const LocaleSymbol: InjectionKey<FullLocaleInstance> = Symbol.for('vuetify:locale')
export const RtlSymbol: InjectionKey<RtlInstance> = Symbol.for('vuetify:rtl')

const LANG_PREFIX = '$vuetify.'

function genDefaults () {
return {
af: false,
Expand Down Expand Up @@ -129,18 +100,144 @@ function genDefaults () {
}
}

export function createRtl (i18n: LocaleInstance, options?: RtlOptions): RtlInstance {
function createLocaleInstance (
v0Locale: LocaleContext,
options?: { decimalSeparator?: string }
): LocaleInstance {
const current = computed<string>({
get: () => String(v0Locale.selectedId.value ?? 'en'),
set: v => v0Locale.select(v),
})

function t (key: string, ...params: unknown[]): string {
const stripped = key.startsWith(LANG_PREFIX) ? key.slice(LANG_PREFIX.length) : key
return v0Locale.t(stripped, ...params)
}

function n (value: number, options?: Intl.NumberFormatOptions): string {
if (options) {
return new Intl.NumberFormat([current.value], options).format(value)
}
return v0Locale.n(value)
}

const decimalSeparator = toRef(() => {
if (options?.decimalSeparator) return options.decimalSeparator
const formatted = n(0.1)
return formatted.includes(',') ? ',' : '.'
})

return {
current,
t,
n,
decimalSeparator,
}
}

function createRtlInstance (
locale: LocaleInstance,
rtlMap: Ref<Record<string, boolean>>,
v0Rtl: RtlContext
): RtlInstance {
const isRtl = shallowRef(v0Rtl.isRtl.value)

watch(() => locale.current.value, current => {
const value = rtlMap.value[current] ?? false
v0Rtl.isRtl.value = value
isRtl.value = value
}, { immediate: true })

return {
isRtl,
rtl: rtlMap,
rtlClasses: toRef(() => `v-locale--is-${isRtl.value ? 'rtl' : 'ltr'}`),
}
}

export function createLocale (options?: LocaleOptions & RtlOptions) {
if (options?.adapter) {
const rtl = createRtlFromAdapter(options.adapter, options)
return { ...options.adapter, ...rtl }
}

const messages = { en, ...options?.messages }
const defaultLocale = options?.locale ?? 'en'
const fallback = options?.fallback ?? 'en'

const v0Locale = createV0Locale({
default: defaultLocale,
fallback,
messages,
})

const v0Rtl = createV0Rtl({
default: false,
target: null,
})

const rtlMap = ref<Record<string, boolean>>(options?.rtl ?? genDefaults())
const locale = createLocaleInstance(v0Locale, options)
const rtl = createRtlInstance(locale, rtlMap, v0Rtl)

return { ...locale, ...rtl, _messages: messages, _fallback: fallback } satisfies FullLocaleInstance
}

function createRtlFromAdapter (adapter: LocaleInstance, options?: RtlOptions): RtlInstance {
const rtl = ref<Record<string, boolean>>(options?.rtl ?? genDefaults())
const isRtl = computed(() => rtl.value[i18n.current.value] ?? false)
const isRtl = computed(() => rtl.value[adapter.current.value] ?? false)

return {
isRtl,
rtl,
rtlClasses: toRef(() => `v-locale--is-${isRtl.value ? 'rtl' : 'ltr'}`),
} satisfies RtlInstance
}

export function useLocale () {
const locale = inject(LocaleSymbol)

if (!locale) throw new Error('[Vuetify] Could not find injected locale instance')

return locale
}

export function provideLocale (props: LocaleOptions & RtlProps) {
const parent = inject(LocaleSymbol)

if (!parent) throw new Error('[Vuetify] Could not find injected locale instance')

if ('provide' in parent && isFunction(parent.provide)) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Especially when you're still using provide.

const i18n = parent.provide(props)
const rtl = provideRtl(i18n, parent.rtl, props)
const data = { ...i18n, ...rtl }
provide(LocaleSymbol, data)
return data
}

const parentData = parent
const parentMessages = parentData._messages ?? {}
const parentFallback = parentData._fallback ?? 'en'
const messages = props.messages
? { ...parentMessages, [props.locale ?? parent.current.value]: props.messages }
: parentMessages
const fallback = props.fallback ?? parentFallback

const v0Locale = createV0Locale({
default: props.locale ?? parent.current.value,
fallback,
messages,
})

const locale = createLocaleInstance(v0Locale, props)
const rtl = provideRtl(locale, parent.rtl, props)

const data = { ...locale, ...rtl, _messages: messages, _fallback: fallback } satisfies FullLocaleInstance
Copy link
Copy Markdown
Member

@KaelWD KaelWD Apr 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any particular reason to "remove" messages and fallback if they're just being renamed and made private?

provide(LocaleSymbol, data)
return data
}

export function provideRtl (locale: LocaleInstance, rtl: RtlInstance['rtl'], props: RtlProps): RtlInstance {
function provideRtl (locale: LocaleInstance, rtl: RtlInstance['rtl'], props: RtlProps): RtlInstance {
const isRtl = computed(() => props.rtl ?? rtl.value[locale.current.value] ?? false)

return {
Expand Down
11 changes: 9 additions & 2 deletions packages/vuetify/src/locale/adapters/vue-i18n.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,13 @@ import type { Ref } from 'vue'
import type { I18n, useI18n } from 'vue-i18n'
import type { LocaleInstance, LocaleMessages, LocaleOptions } from '@/composables/locale'

export interface VueI18nLocaleInstance extends LocaleInstance {
name: string
fallback: Ref<string>
messages: Ref<LocaleMessages>
provide: (props: LocaleOptions) => VueI18nLocaleInstance
Copy link
Copy Markdown
Member

@KaelWD KaelWD Apr 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why were name and provide removed on LocaleInstance but not here?

}

type VueI18nAdapterParams = {
i18n: I18n<any, {}, {}, string, false>
useI18n: typeof useI18n
Expand Down Expand Up @@ -38,7 +45,7 @@ function createProvideFunction (data: {
messages: Ref<LocaleMessages>
useI18n: typeof useI18n
}) {
return (props: LocaleOptions): LocaleInstance => {
return (props: LocaleOptions): VueI18nLocaleInstance => {
const current = useProvided(props, 'locale', data.current)
const fallback = useProvided(props, 'fallback', data.fallback)
const messages = useProvided(props, 'messages', data.messages)
Expand Down Expand Up @@ -69,7 +76,7 @@ function createProvideFunction (data: {
}
}

export function createVueI18nAdapter ({ i18n, useI18n }: VueI18nAdapterParams): LocaleInstance {
export function createVueI18nAdapter ({ i18n, useI18n }: VueI18nAdapterParams): VueI18nLocaleInstance {
const current = i18n.global.locale
const fallback = i18n.global.fallbackLocale as Ref<any>
const messages = i18n.global.messages
Expand Down
Loading
Loading