diff --git a/.gitignore b/.gitignore
index f433d067efa..d2a67de2be5 100644
--- a/.gitignore
+++ b/.gitignore
@@ -26,3 +26,6 @@ __screenshots__/
.vercel
.now
+
+# Superpowers
+docs/superpowers/
diff --git a/packages/docs/src/pages/en/getting-started/upgrade-guide.md b/packages/docs/src/pages/en/getting-started/upgrade-guide.md
index b9f5a13974a..06944530202 100644
--- a/packages/docs/src/pages/en/getting-started/upgrade-guide.md
+++ b/packages/docs/src/pages/en/getting-started/upgrade-guide.md
@@ -17,6 +17,32 @@ This page contains a detailed list of breaking changes and the steps required to
+## 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.
diff --git a/packages/vuetify/src/composables/locale.ts b/packages/vuetify/src/composables/locale.ts
index 975747f08c8..6d001750ad2 100644
--- a/packages/vuetify/src/composables/locale.ts
+++ b/packages/vuetify/src/composables/locale.ts
@@ -1,9 +1,11 @@
// 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
@@ -11,61 +13,19 @@ export interface LocaleMessages {
export interface LocaleOptions {
decimalSeparator?: string
- messages?: LocaleMessages
+ messages?: Record
locale?: string
fallback?: string
adapter?: LocaleInstance
}
export interface LocaleInstance {
- name: string
- decimalSeparator: ShallowRef
- messages: Ref
current: Ref
- fallback: Ref
t: (key: string, ...params: unknown[]) => string
- n: (value: number) => string
- provide: (props: LocaleOptions) => LocaleInstance
+ n: (value: number, options?: Intl.NumberFormatOptions) => string
+ decimalSeparator: Ref
}
-export const LocaleSymbol: InjectionKey = 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
}
@@ -80,8 +40,19 @@ export interface RtlInstance {
rtlClasses: Ref
}
+/** @internal */
+export interface InternalLocaleData {
+ _messages: Record
+ _fallback: string
+}
+
+export interface FullLocaleInstance extends LocaleInstance, RtlInstance, InternalLocaleData {}
+
+export const LocaleSymbol: InjectionKey = Symbol.for('vuetify:locale')
export const RtlSymbol: InjectionKey = Symbol.for('vuetify:rtl')
+const LANG_PREFIX = '$vuetify.'
+
function genDefaults () {
return {
af: false,
@@ -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({
+ 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>,
+ 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>(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>(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)) {
+ 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
+ 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 {
diff --git a/packages/vuetify/src/locale/adapters/vue-i18n.ts b/packages/vuetify/src/locale/adapters/vue-i18n.ts
index 762515d04fc..f9d22fdc810 100644
--- a/packages/vuetify/src/locale/adapters/vue-i18n.ts
+++ b/packages/vuetify/src/locale/adapters/vue-i18n.ts
@@ -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
+ messages: Ref
+ provide: (props: LocaleOptions) => VueI18nLocaleInstance
+}
+
type VueI18nAdapterParams = {
i18n: I18n
useI18n: typeof useI18n
@@ -38,7 +45,7 @@ function createProvideFunction (data: {
messages: Ref
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)
@@ -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
const messages = i18n.global.messages
diff --git a/packages/vuetify/src/locale/adapters/vuetify.ts b/packages/vuetify/src/locale/adapters/vuetify.ts
deleted file mode 100644
index ab860444064..00000000000
--- a/packages/vuetify/src/locale/adapters/vuetify.ts
+++ /dev/null
@@ -1,120 +0,0 @@
-// Composables
-import { useProxiedModel } from '@/composables/proxiedModel'
-
-// Utilities
-import { ref, shallowRef, toRef, watch } from 'vue'
-import { consoleError, consoleWarn, getObjectValueByPath } from '@/util'
-
-// Locales
-import en from '@/locale/en'
-
-// Types
-import type { Ref } from 'vue'
-import type { LocaleInstance, LocaleMessages, LocaleOptions } from '@/composables/locale'
-
-const LANG_PREFIX = '$vuetify.'
-
-const replace = (str: string, params: unknown[]) => {
- return str.replace(/\{(\d+)\}/g, (match: string, index: string) => {
- return String(params[Number(index)])
- })
-}
-
-const createTranslateFunction = (
- current: Ref,
- fallback: Ref,
- messages: Ref,
-) => {
- return (key: string, ...params: unknown[]) => {
- if (!key.startsWith(LANG_PREFIX)) {
- return replace(key, params)
- }
-
- const shortKey = key.replace(LANG_PREFIX, '')
- const currentLocale = current.value && messages.value[current.value]
- const fallbackLocale = fallback.value && messages.value[fallback.value]
-
- let str: string = getObjectValueByPath(currentLocale, shortKey, null)
-
- if (!str) {
- consoleWarn(`Translation key "${key}" not found in "${current.value}", trying fallback locale`)
- str = getObjectValueByPath(fallbackLocale, shortKey, null)
- }
-
- if (!str) {
- consoleError(`Translation key "${key}" not found in fallback`)
- str = key
- }
-
- if (typeof str !== 'string') {
- consoleError(`Translation key "${key}" has a non-string value`)
- str = key
- }
-
- return replace(str, params)
- }
-}
-
-function createNumberFunction (current: Ref, fallback: Ref) {
- return (value: number, options?: Intl.NumberFormatOptions) => {
- const numberFormat = new Intl.NumberFormat([current.value, fallback.value], options)
-
- return numberFormat.format(value)
- }
-}
-
-function inferDecimalSeparator (current: Ref, fallback: Ref) {
- const format = createNumberFunction(current, fallback)
- return format(0.1).includes(',') ? ',' : '.'
-}
-
-function useProvided (props: any, prop: string, provided: Ref) {
- const internal = useProxiedModel(props, prop, props[prop] ?? provided.value)
-
- // TODO: Remove when defaultValue works
- internal.value = props[prop] ?? provided.value
-
- watch(provided, v => {
- if (props[prop] == null) {
- internal.value = provided.value
- }
- })
-
- return internal as Ref
-}
-
-function createProvideFunction (state: { current: Ref, fallback: Ref, messages: Ref }) {
- return (props: LocaleOptions): LocaleInstance => {
- const current = useProvided(props, 'locale', state.current)
- const fallback = useProvided(props, 'fallback', state.fallback)
- const messages = useProvided(props, 'messages', state.messages)
-
- return {
- name: 'vuetify',
- current,
- fallback,
- messages,
- decimalSeparator: toRef(() => inferDecimalSeparator(current, fallback)),
- t: createTranslateFunction(current, fallback, messages),
- n: createNumberFunction(current, fallback),
- provide: createProvideFunction({ current, fallback, messages }),
- }
- }
-}
-
-export function createVuetifyAdapter (options?: LocaleOptions): LocaleInstance {
- const current = shallowRef(options?.locale ?? 'en')
- const fallback = shallowRef(options?.fallback ?? 'en')
- const messages = ref({ en, ...options?.messages })
-
- return {
- name: 'vuetify',
- current,
- fallback,
- messages,
- decimalSeparator: toRef(() => options?.decimalSeparator ?? inferDecimalSeparator(current, fallback)),
- t: createTranslateFunction(current, fallback, messages),
- n: createNumberFunction(current, fallback),
- provide: createProvideFunction({ current, fallback, messages }),
- }
-}