diff --git a/e2e/composition.test.js b/e2e/composition.test.js
new file mode 100644
index 000000000..252058dc2
--- /dev/null
+++ b/e2e/composition.test.js
@@ -0,0 +1,34 @@
+;['legacy'].forEach(pattern => {
+ describe(`${pattern}`, () => {
+ beforeAll(async () => {
+ await page.goto(
+ `http://localhost:8080/examples/${pattern}/composition.html`
+ )
+ })
+
+ test('initial rendering', async () => {
+ await expect(page).toMatchElement('#app p', {
+ text: 'こんにちは、世界!'
+ })
+ await expect(page).toMatchElement('#app div.child p', {
+ text: 'やあ!'
+ })
+ })
+
+ test('change locale', async () => {
+ // root
+ await expect(page).toSelect('#app select', 'en')
+ await expect(page).toMatchElement('#app p', { text: 'hello world!' })
+ await expect(page).toMatchElement('#app div.child p', {
+ text: 'Hi there!'
+ })
+
+ // Child
+ await expect(page).toSelect('#app div.child select', 'ja')
+ await expect(page).toMatchElement('#app p', { text: 'hello world!' })
+ await expect(page).toMatchElement('#app div.child p', {
+ text: 'やあ!'
+ })
+ })
+ })
+})
diff --git a/examples/legacy/composition.html b/examples/legacy/composition.html
new file mode 100644
index 000000000..ae15004ec
--- /dev/null
+++ b/examples/legacy/composition.html
@@ -0,0 +1,96 @@
+
+
+
+
+ Allow Composition API on legacy example
+
+
+
+
+
+
Root
+
+
{{ $t("message.hello") }}
+
+
+
+
+
diff --git a/packages/vue-i18n-core/src/composer.ts b/packages/vue-i18n-core/src/composer.ts
index b6ed6e306..3d767bf88 100644
--- a/packages/vue-i18n-core/src/composer.ts
+++ b/packages/vue-i18n-core/src/composer.ts
@@ -106,6 +106,8 @@ import type {
import type { VueDevToolsEmitter } from '@intlify/vue-devtools'
import { isLegacyVueI18n } from './utils'
+export { DEFAULT_LOCALE } from '@intlify/core-base'
+
// extend VNode interface
export const DEVTOOLS_META = '__INTLIFY_META__'
diff --git a/packages/vue-i18n-core/src/errors.ts b/packages/vue-i18n-core/src/errors.ts
index 97ae13043..4da8b5a62 100644
--- a/packages/vue-i18n-core/src/errors.ts
+++ b/packages/vue-i18n-core/src/errors.ts
@@ -32,6 +32,8 @@ export const I18nErrorCodes = {
NOT_COMPATIBLE_LEGACY_VUE_I18N: inc(), // 25
// bridge support vue 2.x only
BRIDGE_SUPPORT_VUE_2_ONLY: inc(), // 26
+ // need to define `i18n` option in `allowComposition: true` and `useScope: 'local' at `useI18n``
+ MUST_DEFINE_I18N_OPTION_IN_ALLOW_COMPOSITION: inc(), // 27
// for enhancement
__EXTEND_POINT__: inc() // 27
} as const
@@ -65,5 +67,7 @@ export const errorMessages: { [code: number]: string } = {
[I18nErrorCodes.NOT_COMPATIBLE_LEGACY_VUE_I18N]:
'Not compatible legacy VueI18n.',
[I18nErrorCodes.BRIDGE_SUPPORT_VUE_2_ONLY]:
- 'vue-i18n-bridge support Vue 2.x only'
+ 'vue-i18n-bridge support Vue 2.x only',
+ [I18nErrorCodes.MUST_DEFINE_I18N_OPTION_IN_ALLOW_COMPOSITION]:
+ 'Must define ‘i18n’ option in Composition API with using local scope in Legacy API mode'
}
diff --git a/packages/vue-i18n-core/src/i18n.ts b/packages/vue-i18n-core/src/i18n.ts
index 940a1b4af..029036be2 100644
--- a/packages/vue-i18n-core/src/i18n.ts
+++ b/packages/vue-i18n-core/src/i18n.ts
@@ -5,18 +5,26 @@ import {
onUnmounted,
InjectionKey,
getCurrentInstance,
- isRef
+ shallowRef,
+ isRef,
+ ref,
+ computed
} from 'vue'
import {
inBrowser,
isEmptyObject,
isBoolean,
+ isString,
+ isArray,
+ isPlainObject,
+ isRegExp,
+ isFunction,
warn,
makeSymbol,
createEmitter,
assign
} from '@intlify/shared'
-import { createComposer } from './composer'
+import { createComposer, DEFAULT_LOCALE } from './composer'
import { createVueI18n } from './legacy'
import { I18nWarnCodes, getWarnMessage } from './warnings'
import { I18nErrorCodes, createI18nError } from './errors'
@@ -35,15 +43,28 @@ import { enableDevTools, addTimelineEvent } from './devtools'
import {
isLegacyVueI18n,
getComponentOptions,
+ getLocaleMessages,
adjustI18nResources
} from './utils'
import type { ComponentInternalInstance, App } from 'vue'
import type {
Locale,
+ Path,
FallbackLocale,
SchemaParams,
- LocaleParams
+ LocaleMessages,
+ LocaleMessage,
+ LocaleMessageValue,
+ LocaleMessageDictionary,
+ PostTranslationHandler,
+ DateTimeFormats as DateTimeFormatsType,
+ NumberFormats as NumberFormatsType,
+ DateTimeFormat,
+ NumberFormat,
+ LocaleParams,
+ LinkedModifiers,
+ PluralizationRules
} from '@intlify/core-base'
import type {
VueDevToolsEmitter,
@@ -51,6 +72,7 @@ import type {
} from '@intlify/vue-devtools'
import type {
VueMessageType,
+ MissingHandler,
DefaultLocaleMessageSchema,
DefaultDateTimeFormatSchema,
DefaultNumberFormatSchema,
@@ -137,6 +159,18 @@ export interface I18nAdditionalOptions {
* @defaultValue `false`
*/
globalInjection?: boolean
+ /**
+ * Whether to allow the Composition API to be used in Legacy API mode.
+ *
+ * @remarks
+ * If this option is enabled, you can use {@link useI18n} in Legacy API mode. This option is supported to support the migration from Legacy API mode to Composition API mode.
+ *
+ * @VueI18nWarning Note that the Composition API made available with this option doesn't work on SSR.
+ * @VueI18nSee [Composition API](../guide/advanced/composition)
+ *
+ * @defaultValue `false`
+ */
+ allowComposition?: boolean
}
/**
@@ -186,6 +220,13 @@ export interface I18n<
: Legacy extends false
? Composer
: unknown
+ /**
+ * The property whether or not the Composition API is available
+ *
+ * @remarks
+ * If you specified `allowComposition: true` option in Legacy API mode, return `true`, else `false`. else you use the Composition API mode, this property will always return `true`.
+ */
+ readonly allowComposition: boolean
/**
* Install entry point
*
@@ -446,6 +487,12 @@ export function createI18n(options: any = {}, VueI18nLegacy?: any): any {
: isBoolean(options.globalInjection)
? options.globalInjection
: true
+ // prettier-ignore
+ const __allowComposition = __LITE__
+ ? true
+ : __FEATURE_LEGACY_API__ && __legacyMode
+ ? !!options.allowComposition
+ : true
const __instances = new Map()
const __global = createGlobal(options, __legacyMode, VueI18nLegacy)
const symbol: InjectionKey | string = /* #__PURE__*/ makeSymbol(
@@ -475,6 +522,10 @@ export function createI18n(options: any = {}, VueI18nLegacy?: any): any {
? 'legacy'
: 'composition'
},
+ // allowComposition
+ get allowComposition(): boolean {
+ return __allowComposition
+ },
// install plugin
async install(app: App, ...options: unknown[]): Promise {
if (
@@ -559,6 +610,11 @@ export function createI18n(options: any = {}, VueI18nLegacy?: any): any {
return __legacyMode ? 'legacy' : 'composition'
}
})
+ Object.defineProperty(i18n, 'allowComposition', {
+ get() {
+ return __allowComposition
+ }
+ })
Object.defineProperty(i18n, '__instances', {
get() {
return __instances
@@ -708,6 +764,16 @@ export function useI18n<
const componentOptions = getComponentOptions(instance)
const scope = getScope(options, componentOptions)
+ if (!__LITE__ && __FEATURE_LEGACY_API__) {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ if (i18n.mode === 'legacy' && !(options as any).__useComponent) {
+ if (!i18n.allowComposition) {
+ throw createI18nError(I18nErrorCodes.NOT_AVAILABLE_IN_LEGACY_MODE)
+ }
+ return useI18nForLegacy(instance, scope, global, options)
+ }
+ }
+
if (scope === 'global') {
adjustI18nResources(global, options, componentOptions)
return global as Composer<
@@ -735,11 +801,6 @@ export function useI18n<
>
}
- // scope 'local' case
- if (i18n.mode === 'legacy') {
- throw createI18nError(I18nErrorCodes.NOT_AVAILABLE_IN_LEGACY_MODE)
- }
-
const i18nInternal = i18n as unknown as I18nInternal
let composer = i18nInternal.__getInstance(instance)
if (composer == null) {
@@ -998,6 +1059,445 @@ function setupLifeCycle(
}
}
+function useI18nForLegacy(
+ instance: ComponentInternalInstance,
+ scope: I18nScope,
+ root: Composer,
+ options: any = {} // eslint-disable-line @typescript-eslint/no-explicit-any
+): Composer {
+ type Message = VueMessageType
+
+ const isLocale = scope === 'local'
+ const _composer = shallowRef(null)
+
+ if (isLocale && instance.proxy && !instance.proxy.$options.i18n) {
+ throw createI18nError(
+ I18nErrorCodes.MUST_DEFINE_I18N_OPTION_IN_ALLOW_COMPOSITION
+ )
+ }
+
+ const _inheritLocale = isBoolean(options.inheritLocale)
+ ? options.inheritLocale
+ : true
+
+ const _locale = ref(
+ // prettier-ignore
+ isLocale && _inheritLocale
+ ? root.locale.value
+ : isString(options.locale)
+ ? options.locale
+ : DEFAULT_LOCALE
+ )
+
+ const _fallbackLocale = ref(
+ // prettier-ignore
+ isLocale && _inheritLocale
+ ? root.fallbackLocale.value
+ : isString(options.fallbackLocale) ||
+ isArray(options.fallbackLocale) ||
+ isPlainObject(options.fallbackLocale) ||
+ options.fallbackLocale === false
+ ? options.fallbackLocale
+ : _locale.value
+ )
+
+ const _messages = ref>>(
+ getLocaleMessages>>(
+ _locale.value as Locale,
+ options
+ )
+ )
+
+ // prettier-ignore
+ const _datetimeFormats = ref(
+ isPlainObject(options.datetimeFormats)
+ ? options.datetimeFormats
+ : { [_locale.value]: {} }
+ )
+
+ // prettier-ignore
+ const _numberFormats = ref(
+ isPlainObject(options.numberFormats)
+ ? options.numberFormats
+ : { [_locale.value]: {} }
+ )
+
+ // prettier-ignore
+ const _missingWarn = isLocale
+ ? root.missingWarn
+ : isBoolean(options.missingWarn) || isRegExp(options.missingWarn)
+ ? options.missingWarn
+ : true
+
+ // prettier-ignore
+ const _fallbackWarn = isLocale
+ ? root.fallbackWarn
+ : isBoolean(options.fallbackWarn) || isRegExp(options.fallbackWarn)
+ ? options.fallbackWarn
+ : true
+
+ // prettier-ignore
+ const _fallbackRoot = isLocale
+ ? root.fallbackRoot
+ : isBoolean(options.fallbackRoot)
+ ? options.fallbackRoot
+ : true
+
+ // configure fall back to root
+ const _fallbackFormat = !!options.fallbackFormat
+
+ // runtime missing
+ const _missing = isFunction(options.missing) ? options.missing : null
+
+ // postTranslation handler
+ const _postTranslation = isFunction(options.postTranslation)
+ ? options.postTranslation
+ : null
+
+ // prettier-ignore
+ const _warnHtmlMessage = isLocale
+ ? root.warnHtmlMessage
+ : isBoolean(options.warnHtmlMessage)
+ ? options.warnHtmlMessage
+ : true
+
+ const _escapeParameter = !!options.escapeParameter
+
+ // prettier-ignore
+ const _modifiers = isLocale
+ ? root.modifiers
+ : isPlainObject(options.modifiers)
+ ? options.modifiers
+ : {}
+
+ // pluralRules
+ const _pluralRules = options.pluralRules || (isLocale && root.pluralRules)
+
+ // track reactivity
+ function trackReactivityValues() {
+ return [
+ _locale.value,
+ _fallbackLocale.value,
+ _messages.value,
+ _datetimeFormats.value,
+ _numberFormats.value
+ ]
+ }
+
+ // locale
+ const locale = computed({
+ get: () => {
+ return _composer.value ? _composer.value.locale.value : _locale.value
+ },
+ set: val => {
+ if (_composer.value) {
+ _composer.value.locale.value = val
+ }
+ _locale.value = val
+ }
+ })
+
+ // fallbackLocale
+ const fallbackLocale = computed({
+ get: () => {
+ return _composer.value
+ ? _composer.value.fallbackLocale.value
+ : _fallbackLocale.value
+ },
+ set: val => {
+ if (_composer.value) {
+ _composer.value.fallbackLocale.value = val
+ }
+ _fallbackLocale.value = val
+ }
+ })
+
+ // messages
+ const messages = computed, Message>>(
+ () => {
+ if (_composer.value) {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ return _composer.value.messages.value as any
+ } else {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ return _messages.value as any
+ }
+ }
+ )
+
+ const datetimeFormats = computed(
+ () => _datetimeFormats.value
+ )
+
+ const numberFormats = computed(() => _numberFormats.value)
+
+ function getPostTranslationHandler(): PostTranslationHandler | null {
+ return _composer.value
+ ? _composer.value.getPostTranslationHandler()
+ : _postTranslation
+ }
+
+ function setPostTranslationHandler(
+ handler: PostTranslationHandler | null
+ ): void {
+ if (_composer.value) {
+ _composer.value.setPostTranslationHandler(handler)
+ }
+ }
+
+ function getMissingHandler(): MissingHandler | null {
+ return _composer.value ? _composer.value.getMissingHandler() : _missing
+ }
+
+ function setMissingHandler(handler: MissingHandler | null): void {
+ if (_composer.value) {
+ _composer.value.setMissingHandler(handler)
+ }
+ }
+
+ function warpWithDeps(fn: () => unknown) {
+ trackReactivityValues()
+ return fn() as R
+ }
+
+ function t(...args: unknown[]): string {
+ return _composer.value
+ ? warpWithDeps(
+ () => Reflect.apply(_composer.value!.t, null, [...args]) as string
+ )
+ : warpWithDeps(() => '')
+ }
+
+ function rt(...args: unknown[]): string {
+ return _composer.value
+ ? Reflect.apply(_composer.value.rt, null, [...args])
+ : ''
+ }
+
+ function d(...args: unknown[]): string {
+ return _composer.value
+ ? warpWithDeps(
+ () => Reflect.apply(_composer.value!.d, null, [...args]) as string
+ )
+ : warpWithDeps(() => '')
+ }
+
+ function n(...args: unknown[]): string {
+ return _composer.value
+ ? warpWithDeps(
+ () => Reflect.apply(_composer.value!.n, null, [...args]) as string
+ )
+ : warpWithDeps(() => '')
+ }
+
+ function tm(key: Path): LocaleMessageValue | {} {
+ return _composer.value ? _composer.value.tm(key) : {}
+ }
+
+ function te(key: Path, locale?: Locale): boolean {
+ return _composer.value ? _composer.value.te(key, locale) : false
+ }
+
+ function getLocaleMessage(locale: Locale): LocaleMessage {
+ return _composer.value ? _composer.value.getLocaleMessage(locale) : {}
+ }
+
+ function setLocaleMessage(locale: Locale, message: LocaleMessage) {
+ if (_composer.value) {
+ _composer.value.setLocaleMessage(locale, message)
+ _messages.value[locale] = message
+ }
+ }
+
+ function mergeLocaleMessage(
+ locale: Locale,
+ message: LocaleMessageDictionary
+ ): void {
+ if (_composer.value) {
+ _composer.value.mergeLocaleMessage(locale, message)
+ }
+ }
+
+ function getDateTimeFormat(locale: Locale): DateTimeFormat {
+ return _composer.value ? _composer.value.getDateTimeFormat(locale) : {}
+ }
+
+ function setDateTimeFormat(locale: Locale, format: DateTimeFormat): void {
+ if (_composer.value) {
+ _composer.value.setDateTimeFormat(locale, format)
+ _datetimeFormats.value[locale] = format
+ }
+ }
+
+ function mergeDateTimeFormat(locale: Locale, format: DateTimeFormat): void {
+ if (_composer.value) {
+ _composer.value.mergeDateTimeFormat(locale, format)
+ }
+ }
+
+ function getNumberFormat(locale: Locale): NumberFormat {
+ return _composer.value ? _composer.value.getNumberFormat(locale) : {}
+ }
+
+ function setNumberFormat(locale: Locale, format: NumberFormat): void {
+ if (_composer.value) {
+ _composer.value.setNumberFormat(locale, format)
+ _numberFormats.value[locale] = format
+ }
+ }
+
+ function mergeNumberFormat(locale: Locale, format: NumberFormat): void {
+ if (_composer.value) {
+ _composer.value.mergeNumberFormat(locale, format)
+ }
+ }
+
+ const wrapper = {
+ get id(): number {
+ return _composer.value ? _composer.value.id : -1
+ },
+ locale,
+ fallbackLocale,
+ messages,
+ datetimeFormats,
+ numberFormats,
+ get inheritLocale(): boolean {
+ return _composer.value ? _composer.value.inheritLocale : _inheritLocale
+ },
+ set inheritLocale(val: boolean) {
+ if (_composer.value) {
+ _composer.value.inheritLocale = val
+ }
+ },
+ get availableLocales(): Locale[] {
+ return _composer.value
+ ? _composer.value.availableLocales
+ : Object.keys(_messages.value)
+ },
+ get modifiers(): LinkedModifiers {
+ return (
+ _composer.value ? _composer.value.modifiers : _modifiers
+ ) as LinkedModifiers
+ },
+ get pluralRules(): PluralizationRules {
+ return (
+ _composer.value ? _composer.value.pluralRules : _pluralRules
+ ) as PluralizationRules
+ },
+ get isGlobal(): boolean {
+ return _composer.value ? _composer.value.isGlobal : false
+ },
+ get missingWarn(): boolean | RegExp {
+ return _composer.value ? _composer.value.missingWarn : _missingWarn
+ },
+ set missingWarn(val: boolean | RegExp) {
+ if (_composer.value) {
+ _composer.value.missingWarn = val
+ }
+ },
+ get fallbackWarn(): boolean | RegExp {
+ return _composer.value ? _composer.value.fallbackWarn : _fallbackWarn
+ },
+ set fallbackWarn(val: boolean | RegExp) {
+ if (_composer.value) {
+ _composer.value.missingWarn = val
+ }
+ },
+ get fallbackRoot(): boolean {
+ return _composer.value ? _composer.value.fallbackRoot : _fallbackRoot
+ },
+ set fallbackRoot(val: boolean) {
+ if (_composer.value) {
+ _composer.value.fallbackRoot = val
+ }
+ },
+ get fallbackFormat(): boolean {
+ return _composer.value ? _composer.value.fallbackFormat : _fallbackFormat
+ },
+ set fallbackFormat(val: boolean) {
+ if (_composer.value) {
+ _composer.value.fallbackFormat = val
+ }
+ },
+ get warnHtmlMessage(): boolean {
+ return _composer.value
+ ? _composer.value.warnHtmlMessage
+ : _warnHtmlMessage
+ },
+ set warnHtmlMessage(val: boolean) {
+ if (_composer.value) {
+ _composer.value.warnHtmlMessage = val
+ }
+ },
+ get escapeParameter(): boolean {
+ return _composer.value
+ ? _composer.value.escapeParameter
+ : _escapeParameter
+ },
+ set escapeParameter(val: boolean) {
+ if (_composer.value) {
+ _composer.value.escapeParameter = val
+ }
+ },
+ t,
+ getPostTranslationHandler,
+ setPostTranslationHandler,
+ getMissingHandler,
+ setMissingHandler,
+ rt,
+ d,
+ n,
+ tm,
+ te,
+ getLocaleMessage,
+ setLocaleMessage,
+ mergeLocaleMessage,
+ getDateTimeFormat,
+ setDateTimeFormat,
+ mergeDateTimeFormat,
+ getNumberFormat,
+ setNumberFormat,
+ mergeNumberFormat
+ }
+
+ function sync(composer: Composer): void {
+ composer.locale.value = _locale.value
+ composer.fallbackLocale.value = _fallbackLocale.value
+ Object.keys(_messages.value).forEach(locale => {
+ composer.mergeLocaleMessage(locale, _messages.value[locale])
+ })
+ Object.keys(_datetimeFormats.value).forEach(locale => {
+ composer.mergeDateTimeFormat(locale, _datetimeFormats.value[locale])
+ })
+ Object.keys(_numberFormats.value).forEach(locale => {
+ composer.mergeNumberFormat(locale, _numberFormats.value[locale])
+ })
+ composer.escapeParameter = _escapeParameter
+ composer.fallbackFormat = _fallbackFormat
+ composer.fallbackRoot = _fallbackRoot
+ composer.fallbackWarn = _fallbackWarn
+ composer.missingWarn = _missingWarn
+ composer.warnHtmlMessage = _warnHtmlMessage
+ }
+
+ onBeforeMount(() => {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ const composer = (_composer.value = (instance.proxy?.$i18n as any)
+ .__composer as Composer)
+ if (scope === 'global') {
+ _locale.value = composer.locale.value
+ _fallbackLocale.value = composer.fallbackLocale.value
+ _messages.value = composer.messages.value
+ _datetimeFormats.value = composer.datetimeFormats.value
+ _numberFormats.value = composer.numberFormats.value
+ } else if (isLocale) {
+ sync(composer)
+ }
+ })
+
+ return wrapper as unknown as Composer
+}
+
/**
* Exported global composer instance
*
diff --git a/packages/vue-i18n-core/test/i18n.test.ts b/packages/vue-i18n-core/test/i18n.test.ts
index 207ce93b6..36c71567c 100644
--- a/packages/vue-i18n-core/test/i18n.test.ts
+++ b/packages/vue-i18n-core/test/i18n.test.ts
@@ -93,6 +93,41 @@ describe('createI18n with flat json messages', () => {
})
})
+describe('allowComposition option', () => {
+ describe('legacy mode', () => {
+ test('default', () => {
+ const i18n = createI18n({})
+ expect(i18n.allowComposition).toEqual(false)
+ })
+
+ test('specify `true`', () => {
+ const i18n = createI18n({
+ allowComposition: true
+ })
+
+ expect(i18n.allowComposition).toEqual(true)
+ })
+ })
+
+ describe('composition mode', () => {
+ test('default', () => {
+ const i18n = createI18n({
+ legacy: false
+ })
+ expect(i18n.allowComposition).toEqual(true)
+ })
+
+ test('specify `false`', () => {
+ const i18n = createI18n({
+ legacy: false,
+ allowComposition: false
+ })
+
+ expect(i18n.allowComposition).toEqual(true)
+ })
+ })
+})
+
describe('useI18n', () => {
let org: any // eslint-disable-line @typescript-eslint/no-explicit-any
let spy: any // eslint-disable-line @typescript-eslint/no-explicit-any
@@ -343,22 +378,155 @@ describe('useI18n', () => {
expect(error).toEqual(errorMessages[I18nErrorCodes.NOT_INSLALLED])
})
- test(errorMessages[I18nErrorCodes.NOT_AVAILABLE_IN_LEGACY_MODE], async () => {
- const i18n = createI18n({
- legacy: true,
- locale: 'ja',
- messages: {
- en: {
- hello: 'hello!'
+ describe('On legacy', () => {
+ describe('default', () => {
+ test(
+ errorMessages[I18nErrorCodes.NOT_AVAILABLE_IN_LEGACY_MODE],
+ async () => {
+ const i18n = createI18n({
+ legacy: true,
+ locale: 'ja',
+ messages: {
+ en: {
+ hello: 'hello!'
+ }
+ }
+ })
+
+ let error = ''
+ const App = defineComponent({
+ setup() {
+ try {
+ useI18n({
+ locale: 'en',
+ messages: {
+ en: {
+ hello: 'hello!'
+ }
+ }
+ })
+ } catch (e) {
+ error = e.message
+ }
+ return {}
+ },
+ template: `foo
`
+ })
+ await mount(App, i18n)
+ expect(error).toEqual(
+ errorMessages[I18nErrorCodes.NOT_AVAILABLE_IN_LEGACY_MODE]
+ )
}
- }
+ )
})
- let error = ''
- const App = defineComponent({
- setup() {
- try {
- useI18n({
+ describe('enable', () => {
+ describe('t', () => {
+ test('translation & locale changing', async () => {
+ const i18n = createI18n({
+ allowComposition: true,
+ locale: 'ja',
+ messages: {
+ en: {
+ hello: 'hello!'
+ },
+ ja: {
+ hello: 'こんにちは!'
+ }
+ }
+ })
+
+ const App = defineComponent({
+ setup() {
+ const { locale, t } = useI18n()
+ return { locale, t }
+ },
+ template: `{{ t('hello') }}
`
+ })
+ const { html } = await mount(App, i18n)
+ expect(html()).toEqual('こんにちは!
')
+
+ i18n.global.locale = 'en'
+ await nextTick()
+ expect(html()).toEqual('hello!
')
+ })
+
+ test('local scope', async () => {
+ const i18n = createI18n({
+ allowComposition: true,
+ locale: 'en',
+ messages: {
+ en: {
+ hello: 'hello!'
+ },
+ ja: {}
+ }
+ })
+
+ const App = defineComponent({
+ setup() {
+ const { locale, t } = useI18n({
+ useScope: 'local',
+ messages: {
+ en: {
+ world: 'world!'
+ },
+ ja: {
+ world: '世界!'
+ }
+ }
+ })
+ return { locale, t }
+ },
+ i18n: {},
+ template: `{{ locale }}:{{ t('world') }}
`
+ })
+ const { html } = await mount(App, i18n)
+ expect(html()).toEqual('en:world!
')
+
+ i18n.global.locale = 'ja'
+ await nextTick()
+ expect(html()).toEqual('ja:世界!
')
+ })
+
+ test('use i18n option', async () => {
+ const i18n = createI18n({
+ allowComposition: true,
+ locale: 'en',
+ messages: {
+ en: {
+ hello: 'hello!'
+ },
+ ja: {}
+ }
+ })
+
+ const App = defineComponent({
+ setup() {
+ const { locale, t } = useI18n({
+ useScope: 'local'
+ })
+ return { locale, t }
+ },
+ i18n: {
+ messages: {
+ en: {
+ world: 'world!'
+ },
+ ja: {
+ world: '世界!'
+ }
+ }
+ },
+ template: `{{ locale }}:{{ t('world') }}
`
+ })
+ const { html } = await mount(App, i18n)
+ expect(html()).toEqual('en:world!
')
+ })
+
+ test('not defined i18n option in local scope', async () => {
+ const i18n = createI18n({
+ allowComposition: true,
locale: 'en',
messages: {
en: {
@@ -366,17 +534,127 @@ describe('useI18n', () => {
}
}
})
- } catch (e) {
- error = e.message
- }
- return {}
- },
- template: `foo
`
+
+ let error = ''
+ const App = defineComponent({
+ setup() {
+ try {
+ useI18n({ useScope: 'local' })
+ } catch (e) {
+ error = e.message
+ }
+ return {}
+ }
+ })
+ await mount(App, i18n)
+ expect(error).toEqual(
+ errorMessages[
+ I18nErrorCodes.MUST_DEFINE_I18N_OPTION_IN_ALLOW_COMPOSITION
+ ]
+ )
+ })
+ })
+ })
+
+ describe('d', () => {
+ test('datetime formatting', async () => {
+ const i18n = createI18n({
+ allowComposition: true,
+ locale: 'en-US',
+ fallbackLocale: ['ja-JP'],
+ datetimeFormats: {
+ 'en-US': {
+ short: {
+ year: 'numeric',
+ month: '2-digit',
+ day: '2-digit',
+ hour: '2-digit',
+ minute: '2-digit',
+ timeZone: 'America/New_York'
+ }
+ },
+ 'ja-JP': {
+ long: {
+ year: 'numeric',
+ month: '2-digit',
+ day: '2-digit',
+ hour: '2-digit',
+ minute: '2-digit',
+ second: '2-digit',
+ timeZone: 'Asia/Tokyo'
+ },
+ short: {
+ year: 'numeric',
+ month: '2-digit',
+ day: '2-digit',
+ hour: '2-digit',
+ minute: '2-digit',
+ timeZone: 'Asia/Tokyo'
+ }
+ }
+ }
+ })
+
+ const App = defineComponent({
+ setup() {
+ const { d } = useI18n()
+ const dt = new Date(Date.UTC(2012, 11, 20, 3, 0, 0))
+ return { d, dt }
+ },
+ template: `{{ d(dt, 'long') }}
`
+ })
+ const { html } = await mount(App, i18n)
+ expect(html()).toEqual('2012/12/20 12:00:00
')
+ })
+ })
+
+ describe('n', () => {
+ test('number formatting', async () => {
+ const i18n = createI18n({
+ allowComposition: true,
+ locale: 'en-US',
+ fallbackLocale: ['ja-JP'],
+ numberFormats: {
+ 'en-US': {
+ currency: {
+ style: 'currency',
+ currency: 'USD',
+ currencyDisplay: 'symbol'
+ },
+ decimal: {
+ style: 'decimal',
+ useGrouping: false
+ }
+ },
+ 'ja-JP': {
+ currency: {
+ style: 'currency',
+ currency: 'JPY' /*, currencyDisplay: 'symbol'*/
+ },
+ numeric: {
+ style: 'decimal',
+ useGrouping: false
+ },
+ percent: {
+ style: 'percent',
+ useGrouping: false
+ }
+ }
+ }
+ })
+
+ const App = defineComponent({
+ setup() {
+ const { n } = useI18n()
+ const value = 0.99
+ return { n, value }
+ },
+ template: `{{ n(value, { key: 'percent' }) }}
`
+ })
+ const { html } = await mount(App, i18n)
+ expect(html()).toEqual('99%
')
+ })
})
- await mount(App, i18n)
- expect(error).toEqual(
- errorMessages[I18nErrorCodes.NOT_AVAILABLE_IN_LEGACY_MODE]
- )
})
test(errorMessages[I18nErrorCodes.NOT_INSLALLED_WITH_PROVIDE], async () => {