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 () => {