diff --git a/README.md b/README.md
index b0711d2cd..a01bf9a07 100644
--- a/README.md
+++ b/README.md
@@ -91,6 +91,8 @@ The examples are offered that use the following two API styles:
- Legacy API: `warnHtmlInMessage` property.
- For development mode, warning is default.
- For production mode, HTML message detect is not check due to performance.
+- Legacy API `sync` option:
+ - default: change to `false` from `true`
- `VueI18n.version` -> `import { VERSION } from 'vue-i18n'`
- `VueI18n.availabilities` -> `import { availabilities } from 'vue-i18n'`
- See the details [here](https://github.com/intlify/vue-i18n-next/blob/master/docs/vue-i18n.md)
@@ -158,6 +160,7 @@ yarn add vue-i18n@next
- properties
- [x] locale
- [x] fallbackLocale
+ - [x] inheritLocale
- [x] availableLocales
- [x] messages
- [x] modifiers
@@ -190,6 +193,7 @@ yarn add vue-i18n@next
- VueI18n
- [x] locale
- [x] fallbackLocale
+ - [x] sync
- [x] availableLocales
- [x] messages
- [x] pluralizationRules
diff --git a/e2e/fallback/component.test.js b/e2e/fallback/component.test.js
index 6f9dd2157..62fd5d8d0 100644
--- a/e2e/fallback/component.test.js
+++ b/e2e/fallback/component.test.js
@@ -8,9 +8,7 @@
test('initial rendering', async () => {
await expect(page).toMatch('こんにちは、世界')
- await expect(page).toMatch(
- 'Component1 locale messages: こんにちは、component1'
- )
+ await expect(page).toMatch('Component1 locale messages: hello component1')
await expect(page).toMatch(
'Fallback global locale messages: おはよう、世界!'
)
diff --git a/e2e/scope/inherit-locale.test.js b/e2e/scope/inherit-locale.test.js
new file mode 100644
index 000000000..b24427196
--- /dev/null
+++ b/e2e/scope/inherit-locale.test.js
@@ -0,0 +1,63 @@
+;['composable', 'legacy'].forEach(pattern => {
+ describe(`${pattern}`, () => {
+ beforeAll(async () => {
+ await page.goto(
+ `http://localhost:8080/examples/${pattern}/scope/inherit-locale.html`
+ )
+ })
+
+ test('initial rendering', async () => {
+ await expect(page).toMatchElement('#app p', {
+ text: 'こんにちは、世界!'
+ })
+ await expect(page).toMatchElement('#app div.child p', {
+ text: 'こんにちは!'
+ })
+ await expect(page).toMatchElement('#app label[for=checkbox]', {
+ text: 'root から locale を継承する'
+ })
+ })
+
+ 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 !' })
+ await expect(page).toMatchElement('#app label[for=checkbox]', {
+ text: 'Inherit locale from root'
+ })
+
+ // 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: 'こんにちは!'
+ })
+ await expect(page).toMatchElement('#app label[for=checkbox]', {
+ text: 'root から locale を継承する'
+ })
+
+ // checkbox off
+ await expect(page).toClick('#checkbox')
+ await expect(page).toSelect('#app select', 'ja')
+ 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: 'こんにちは!'
+ })
+ await expect(page).toMatchElement('#app label[for=checkbox]', {
+ text: 'root から locale を継承する'
+ })
+
+ // checkbox on
+ await expect(page).toClick('#checkbox')
+ await expect(page).toSelect('#app select', 'ja')
+ 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 !' })
+ await expect(page).toMatchElement('#app label[for=checkbox]', {
+ text: 'Inherit locale from root'
+ })
+ })
+ })
+})
diff --git a/e2e/scope/local.test.js b/e2e/scope/local.test.js
index 35de1f5c8..8adbeafb6 100644
--- a/e2e/scope/local.test.js
+++ b/e2e/scope/local.test.js
@@ -10,23 +10,21 @@
await expect(page).toMatchElement('#app p', {
text: 'こんにちは、世界!'
})
- await expect(page).toMatchElement('#app div.child p', {
- text: 'こんにちは!'
- })
+ await expect(page).toMatchElement('#app div.child p', { text: 'Hi !' })
})
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: 'こんにちは!'
- })
+ await expect(page).toMatchElement('#app div.child p', { text: 'Hi !' })
// Child
- await expect(page).toSelect('#app div.child select', 'en')
+ 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: 'Hi !' })
+ await expect(page).toMatchElement('#app div.child p', {
+ text: 'こんにちは!'
+ })
})
})
})
diff --git a/examples/composable/scope/inherit-locale.html b/examples/composable/scope/inherit-locale.html
new file mode 100644
index 000000000..d799ed865
--- /dev/null
+++ b/examples/composable/scope/inherit-locale.html
@@ -0,0 +1,98 @@
+
+
+
+
+ Inherit locale example
+
+
+
+
+
+
Root
+
+
{{ t("message.hello") }}
+
+
+
+
+
diff --git a/examples/legacy/scope/inherit-locale.html b/examples/legacy/scope/inherit-locale.html
new file mode 100644
index 000000000..fc17dd331
--- /dev/null
+++ b/examples/legacy/scope/inherit-locale.html
@@ -0,0 +1,94 @@
+
+
+
+
+ Inherit locale example
+
+
+
+
+
+
Root
+
+
{{ $t("message.hello") }}
+
+
+
+
+
diff --git a/src/components/DatetimeFormat.ts b/src/components/DatetimeFormat.ts
index 096fa11c5..bc73cfc47 100644
--- a/src/components/DatetimeFormat.ts
+++ b/src/components/DatetimeFormat.ts
@@ -1,10 +1,5 @@
-import {
- getCurrentInstance,
- defineComponent,
- SetupContext,
- PropType
-} from 'vue'
-import { useI18n, getComposer } from '../i18n'
+import { defineComponent, SetupContext, PropType } from 'vue'
+import { useI18n } from '../i18n'
import { DateTimeOptions } from '../core'
import { renderFormatter, FormattableProps } from './formatRenderer'
@@ -51,14 +46,7 @@ export const DatetimeFormat = defineComponent({
},
/* eslint-enable */
setup(props, context: SetupContext) {
- const instance = getCurrentInstance()
- // TODO: should be raise unexpected error, if `instance` is null
- const i18n =
- instance !== null
- ? instance.parent !== null
- ? getComposer(instance.parent)
- : useI18n()
- : useI18n()
+ const i18n = useI18n({ useScope: 'parent' })
return renderFormatter<
FormattableProps,
diff --git a/src/components/NumberFormat.ts b/src/components/NumberFormat.ts
index 684989e8e..b02c30576 100644
--- a/src/components/NumberFormat.ts
+++ b/src/components/NumberFormat.ts
@@ -1,5 +1,5 @@
-import { getCurrentInstance, defineComponent, SetupContext } from 'vue'
-import { useI18n, getComposer } from '../i18n'
+import { defineComponent, SetupContext } from 'vue'
+import { useI18n } from '../i18n'
import { NumberOptions } from '../core'
import { renderFormatter, FormattableProps } from './formatRenderer'
@@ -41,14 +41,7 @@ export const NumberFormat = defineComponent({
},
/* eslint-enable */
setup(props, context: SetupContext) {
- const instance = getCurrentInstance()
- // TODO: should be raise unexpected error, if `instance` is null
- const i18n =
- instance !== null
- ? instance.parent !== null
- ? getComposer(instance.parent)
- : useI18n()
- : useI18n()
+ const i18n = useI18n({ useScope: 'parent' })
return renderFormatter<
FormattableProps,
diff --git a/src/components/Translation.ts b/src/components/Translation.ts
index 25fc6e993..7d1612004 100644
--- a/src/components/Translation.ts
+++ b/src/components/Translation.ts
@@ -3,10 +3,9 @@ import {
Fragment,
defineComponent,
SetupContext,
- VNodeArrayChildren,
- getCurrentInstance
+ VNodeArrayChildren
} from 'vue'
-import { useI18n, getComposer } from '../i18n'
+import { useI18n } from '../i18n'
import { TranslateOptions, Locale } from '../core'
import { NamedValue } from '../message/runtime'
import { isNumber, isString } from '../utils'
@@ -41,14 +40,7 @@ export const Translation = defineComponent({
/* eslint-enable */
setup(props: TranslationProps, context: SetupContext) {
const { slots, attrs } = context
- const instance = getCurrentInstance()
- // TODO: should be raise unexpected error, if `instance` is null
- const i18n =
- instance !== null
- ? instance.parent !== null
- ? getComposer(instance.parent)
- : useI18n()
- : useI18n()
+ const i18n = useI18n({ useScope: 'parent' })
const keys = Object.keys(slots).filter(key => key !== '_')
return () => {
diff --git a/src/composer.ts b/src/composer.ts
index b1482387f..f1cb37629 100644
--- a/src/composer.ts
+++ b/src/composer.ts
@@ -9,12 +9,11 @@ import {
ref,
computed,
getCurrentInstance,
- App,
ComponentInternalInstance,
- createTextVNode
+ createTextVNode,
+ watch
} from 'vue'
import { WritableComputedRef, ComputedRef } from '@vue/reactivity'
-import { apply } from './plugin'
import { Path, parse as parsePath } from './path'
import {
DateTimeFormats,
@@ -83,12 +82,15 @@ export type PreCompileHandler = () => {
}
export type CustomBlocks = string[] | PreCompileHandler
-/*!
- * Composer Options
+/**
+ * Composer Options
+ *
+ * This is options to create composer.
*/
export type ComposerOptions = {
locale?: Locale
fallbackLocale?: FallbackLocale
+ inheritLocale?: boolean
messages?: LocaleMessages
datetimeFormats?: DateTimeFormats
numberFormats?: NumberFormats
@@ -101,12 +103,14 @@ export type ComposerOptions = {
fallbackFormat?: boolean
postTranslation?: PostTranslationHandler
warnHtmlMessage?: boolean
- __i18n?: CustomBlocks // for custom blocks, and internal
- __root?: Composer // for internal
+ __i18n?: CustomBlocks
+ __root?: Composer
}
-/*!
- * Composer Interfaces
+/**
+ * Composer Interfaces
+ *
+ * This is the interface for being used for Vue 3 Composition API.
*/
export type Composer = {
/*!
@@ -114,12 +118,14 @@ export type Composer = {
*/
locale: WritableComputedRef
fallbackLocale: WritableComputedRef
+ inheritLocale: boolean
readonly availableLocales: Locale[]
readonly messages: ComputedRef
readonly datetimeFormats: ComputedRef
readonly numberFormats: ComputedRef
readonly modifiers: LinkedModifiers
readonly pluralRules?: PluralizationRules
+ readonly isGlobal: boolean
missingWarn: boolean | RegExp
fallbackWarn: boolean | RegExp
fallbackRoot: boolean
@@ -166,7 +172,6 @@ export type Composer = {
setPostTranslationHandler(handler: PostTranslationHandler | null): void
getMissingHandler(): MissingHandler | null
setMissingHandler(handler: MissingHandler | null): void
- install(app: App, ...options: unknown[]): void
__transrateVNode(...args: unknown[]): unknown // for internal
__numberParts(...args: unknown[]): string | Intl.NumberFormatPart[] // for internal
__datetimeParts(...args: unknown[]): string | Intl.DateTimeFormatPart[] // for internal
@@ -251,12 +256,19 @@ export function addPreCompileMessages(
})
}
+/**
+ * Create composer interface factory
+ * @internal
+ */
export function createComposer(options: ComposerOptions = {}): Composer {
const { __root } = options
+ const _isGlobal = __root === undefined
+
+ let _inheritLocale = !!options.inheritLocale
const _locale = ref(
// prettier-ignore
- __root
+ __root && _inheritLocale
? __root.locale.value
: isString(options.locale)
? options.locale
@@ -265,7 +277,7 @@ export function createComposer(options: ComposerOptions = {}): Composer {
const _fallbackLocale = ref(
// prettier-ignore
- __root
+ __root && _inheritLocale
? __root.fallbackLocale.value
: isString(options.fallbackLocale) ||
isArray(options.fallbackLocale) ||
@@ -439,7 +451,7 @@ export function createComposer(options: ComposerOptions = {}): Composer {
// NOTE:
// if this composer is global (__root is `undefined`), add dependency trakcing!
// by containing this, we can reactively notify components that reference the global composer.
- if (!__root) {
+ if (!_isGlobal) {
_locale.value
}
@@ -626,6 +638,24 @@ export function createComposer(options: ComposerOptions = {}): Composer {
// for debug
composerID++
+ // watch root locale & fallbackLocale
+ if (__root) {
+ watch(__root.locale, (val: Locale) => {
+ if (_inheritLocale) {
+ _locale.value = val
+ _context.locale = val
+ updateFallbackLocale(_context, _locale.value, _fallbackLocale.value)
+ }
+ })
+ watch(__root.fallbackLocale, (val: FallbackLocale) => {
+ if (_inheritLocale) {
+ _fallbackLocale.value = val
+ _context.fallbackLocale = val
+ updateFallbackLocale(_context, _locale.value, _fallbackLocale.value)
+ }
+ })
+ }
+
// export composable API!
const composer = {
/*!
@@ -633,6 +663,17 @@ export function createComposer(options: ComposerOptions = {}): Composer {
*/
locale,
fallbackLocale,
+ get inheritLocale(): boolean {
+ return _inheritLocale
+ },
+ set inheritLocale(val: boolean) {
+ _inheritLocale = val
+ if (val && __root) {
+ _locale.value = __root.locale.value
+ _fallbackLocale.value = __root.fallbackLocale.value
+ updateFallbackLocale(_context, _locale.value, _fallbackLocale.value)
+ }
+ },
get availableLocales(): Locale[] {
return Object.keys(_messages.value).sort()
},
@@ -645,6 +686,9 @@ export function createComposer(options: ComposerOptions = {}): Composer {
get pluralRules(): PluralizationRules | undefined {
return _pluralRules
},
+ get isGlobal(): boolean {
+ return _isGlobal
+ },
get missingWarn(): boolean | RegExp {
return _missingWarn
},
@@ -699,9 +743,6 @@ export function createComposer(options: ComposerOptions = {}): Composer {
setPostTranslationHandler,
getMissingHandler,
setMissingHandler,
- install(app: App, ...options: unknown[]): void {
- apply(app, composer, ...options)
- },
__transrateVNode,
__numberParts,
__datetimeParts
diff --git a/src/i18n.ts b/src/i18n.ts
index 150fc8ac5..1127c8579 100644
--- a/src/i18n.ts
+++ b/src/i18n.ts
@@ -1,76 +1,89 @@
import {
- provide,
inject,
onMounted,
onUnmounted,
InjectionKey,
getCurrentInstance,
ComponentInternalInstance,
- ComponentOptions
+ ComponentOptions,
+ App
} from 'vue'
import { Composer, ComposerOptions, createComposer } from './composer'
import { createVueI18n, VueI18n, VueI18nOptions } from './legacy'
+import { apply } from './plugin'
+import { defineMixin } from './mixin'
import { isEmptyObject } from './utils'
-const generateSymbolID = (): string =>
- `vue-i18n-${new Date().getUTCMilliseconds().toString()}`
-
-export const GlobalI18nSymbol: InjectionKey = Symbol.for('vue-i18n')
-let globalInstance: VueI18n | Composer | null = null
-
-const providers: Map<
- ComponentInternalInstance,
- InjectionKey
-> = new Map()
-
-const getGlobalComposer = (): Composer => {
- if (globalInstance === null) throw new Error('TODO') // TODO:
- return '__composer' in globalInstance
- ? globalInstance.__composer
- : globalInstance
-}
-
-// TODO: if we don't need the below, should be removed!
-// This code should be removed with using rollup (`/*#__PURE__*/`)
-export function enumProviders(): void {
- if (__DEV__) {
- providers.forEach((sym, instance) => {
- console.log('provider:', instance, sym)
- })
- }
-}
-
-export function getComposer(
- instance: ComponentInternalInstance | null
-): Composer {
- if (!instance) {
- return getGlobalComposer()
- }
- const symbol = providers.get(instance)
- return symbol ? inject(symbol, getGlobalComposer()) : getGlobalComposer()
-}
-
-/*!
+/**
* I18n Options
*
- * {@link createI18n} factory option.
- *
* @remarks
- * `I18nOptions` is union type of {@link ComposerOptions} and {@link VueI18nOptions}, so you can specify these options.
+ * `I18nOptions` is inherited {@link ComposerOptions} and {@link VueI18nOptions}, so you can specify these options.
*
*/
export type I18nOptions = {
/**
+ * Whether vue-i18n legacy API use on your Vue App.
* @defaultValue `false`
*/
legacy?: boolean
} & (ComposerOptions | VueI18nOptions)
-/*!
- * I18n factory
+/**
+ * I18n API mode
+ */
+export type I18nMode = 'legacy' | 'composable'
+
+/**
+ * I18n interface
+ */
+export type I18n = {
+ readonly mode: I18nMode
+ install(app: App, ...options: unknown[]): void
+}
+
+/**
+ * I18n interface for internal usage
+ * @internal
+ */
+export type I18nInternal = {
+ readonly _global: Composer
+ _getComposer(instance: ComponentInternalInstance): Composer | null
+ _setComposer(instance: ComponentInternalInstance, composer: Composer): void
+ _deleteComposer(instance: ComponentInternalInstance): void
+ _getLegacy(instance: ComponentInternalInstance): VueI18n | null
+ _setLegacy(instance: ComponentInternalInstance, legacy: VueI18n): void
+ _deleteLegacy(instance: ComponentInternalInstance): void
+}
+
+/**
+ * I18n Scope
+ */
+export type I18nScope = 'local' | 'parent' | 'global'
+
+/**
+ * `useI18n` options
+ *
+ * @remarks
+ * `UseI18nOptions` is inherited {@link ComposerOptions}, so you can specify these options.
+ */
+export type UseI18nOptions = {
+ useScope?: I18nScope // default 'global'
+} & ComposerOptions
+
+/**
+ * I18n instance injectin key
+ * @internal
+ */
+export const I18nSymbol: InjectionKey = Symbol.for(
+ 'vue-i18n'
+)
+
+/**
+ * I18n factory function
*
* @param options - see the {@link I18nOptions}
- * @returns {@link Composer} object, or {@link VueI18n} object
+ * @returns {@link I18n} object
*
* @remarks
* When you use Composable API, you need to specify options of {@link ComposerOptions}.
@@ -133,26 +146,71 @@ export type I18nOptions = {
* app.mount('#app')
* ```
*/
-export function createI18n(options: I18nOptions = {}): Composer | VueI18n {
- if (globalInstance !== null) {
- return globalInstance
+export function createI18n(options: I18nOptions = {}): I18n {
+ const __legacyMode = !!options.legacy
+ const __composers = new Map()
+ const __legaceis = new Map()
+ const __global = __legacyMode
+ ? createVueI18n(options)
+ : createComposer(options)
+
+ const i18n = {
+ // mode
+ get mode(): I18nMode {
+ return __legacyMode ? 'legacy' : 'composable'
+ },
+ install(app: App, ...options: unknown[]): void {
+ apply(app, i18n, ...options)
+ if (__legacyMode) {
+ app.mixin(
+ defineMixin(
+ __global as VueI18n,
+ (__global as VueI18n).__composer,
+ i18n
+ )
+ )
+ }
+ },
+ get _global(): Composer {
+ return __legacyMode
+ ? (__global as VueI18n).__composer
+ : (__global as Composer)
+ },
+ _getComposer(instance: ComponentInternalInstance): Composer | null {
+ return __composers.get(instance) || null
+ },
+ _setComposer(
+ instance: ComponentInternalInstance,
+ composer: Composer
+ ): void {
+ __composers.set(instance, composer)
+ },
+ _deleteComposer(instance: ComponentInternalInstance): void {
+ __composers.delete(instance)
+ },
+ _getLegacy(instance: ComponentInternalInstance): VueI18n | null {
+ return __legaceis.get(instance) || null
+ },
+ _setLegacy(instance: ComponentInternalInstance, legacy: VueI18n): void {
+ __legaceis.set(instance, legacy)
+ },
+ _deleteLegacy(instance: ComponentInternalInstance): void {
+ __legaceis.delete(instance)
+ }
}
- const legacyMode = !!options.legacy
- return (globalInstance = legacyMode
- ? createVueI18n(options)
- : createComposer(options))
+ return i18n
}
-/*!
- * Use Composable API
+/**
+ * Use Composable API starting function
*
- * @param options - See the {@link ComponentOptions}
+ * @param options - See {@link UseI18nOptions}
* @returns {@link Composer} object
*
* @remarks
* This function is mainly used by `setup`.
- * If options are specified Composer object is created for each component, and you can be localized on the component.
+ * If options are specified, Composer object is created for each component and you can be localized on the component.
* If options are not specified, you can be localized using the global Composer.
*
* @example
@@ -189,61 +247,114 @@ export function createI18n(options: I18nOptions = {}): Composer | VueI18n {
*
* ```
*/
-export function useI18n(options: ComposerOptions = {}): Composer {
- const globalComposer = getGlobalComposer()
+export function useI18n(options: UseI18nOptions = {}): Composer {
+ const i18n = inject(I18nSymbol)
+ // TODO: should be error
+ if (!i18n) {
+ throw new Error('TODO')
+ }
+
+ const global = i18n._global
+ let emptyOption = false
+ // prettier-ignore
+ const scope: I18nScope = (emptyOption = isEmptyObject(options)) // eslint-disable-line no-cond-assign
+ ? 'global'
+ : !options.useScope
+ ? 'local'
+ : options.useScope
+
+ if (emptyOption) {
+ return global
+ }
+
+ // TODO: should be unexpected error (vue runtime error!)
const instance = getCurrentInstance()
- if (instance === null || isEmptyObject(options)) {
- return globalComposer
+ if (instance == null) {
+ throw new Error('TODO')
+ }
+
+ if (scope === 'parent') {
+ let composer = getComposer(i18n, instance)
+ if (composer == null) {
+ // TODO: warning!
+ composer = global
+ }
+ return composer
+ } else if (scope === 'global') {
+ return global
}
- const symbol = providers.get(instance)
- if (!symbol) {
+ // scope 'local' case
+ if (i18n.mode === 'legacy') {
+ // TODO:
+ throw new Error('TODO')
+ }
+
+ let composer = i18n._getComposer(instance)
+ if (composer == null) {
const type = instance.type as ComponentOptions
if (type.__i18n) {
options.__i18n = type.__i18n
}
- if (globalComposer) {
- options.__root = globalComposer
+ if (global) {
+ options.__root = global
}
- const composer = createComposer(options)
- setupLifeCycle(instance, composer)
+ composer = createComposer(options)
+ setupLifeCycle(i18n, instance, composer)
- const sym: InjectionKey = Symbol.for(generateSymbolID())
- providers.set(instance, sym)
- provide(sym, composer)
+ i18n._setComposer(instance, composer)
+ }
- return composer
- } else {
- const composer = inject(symbol) || globalComposer
- if (!composer) throw new Error('TODO') // TODO:
- return composer
+ return composer
+}
+
+function getComposer(
+ i18n: I18n & I18nInternal,
+ target: ComponentInternalInstance
+): Composer | null {
+ let composer: Composer | null = null
+ const root = target.root
+ let current: ComponentInternalInstance | null = target.parent
+ while (current != null) {
+ if (i18n.mode === 'composable') {
+ composer = i18n._getComposer(current)
+ } else {
+ const vueI18n = i18n._getLegacy(current)
+ if (vueI18n != null) {
+ composer = vueI18n.__composer
+ }
+ }
+ if (composer != null) {
+ break
+ }
+ if (root === current) {
+ break
+ }
+ current = current.parent
}
+ return composer
}
function setupLifeCycle(
- instance: ComponentInternalInstance | null,
+ i18n: I18nInternal,
+ target: ComponentInternalInstance,
composer: Composer
): void {
onMounted(() => {
// inject composer instance to DOM for intlify-devtools
- if (instance) {
- if (instance.proxy) {
- instance.proxy.$el.__intlify__ = composer
- }
+ if (target.proxy) {
+ target.proxy.$el.__intlify__ = composer
}
- })
+ }, target)
onUnmounted(() => {
// remove composer instance from DOM for intlify-devtools
- const instance = getCurrentInstance()
- if (instance) {
- if (instance.proxy && instance.proxy.$el.__intlify__) {
- instance.proxy.$el.__intlify__ = undefined
- delete instance.proxy.$el.__intlify__
- }
+ if (target.proxy && target.proxy.$el.__intlify__) {
+ delete target.proxy.$el.__intlify__
}
- })
+ i18n._deleteComposer(target)
+ }, target)
}
diff --git a/src/index.ts b/src/index.ts
index 7979f1ccc..2fc14841b 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -36,6 +36,14 @@ export {
VueI18nOptions,
VueI18n
} from './legacy'
-export { createI18n, useI18n, I18nOptions } from './i18n'
+export {
+ createI18n,
+ useI18n,
+ I18nOptions,
+ I18n,
+ I18nMode,
+ I18nScope,
+ UseI18nOptions
+} from './i18n'
export { I18nPluginOptions } from './plugin'
export const VERSION = __VERSION__
diff --git a/src/legacy.ts b/src/legacy.ts
index 4f1c50e35..a3a871414 100644
--- a/src/legacy.ts
+++ b/src/legacy.ts
@@ -4,8 +4,6 @@
* This module is offered legacy vue-i18n API compatibility
*/
-import { App } from 'vue'
-import { defineMixin } from './mixin'
import { Path, resolveValue } from './path'
import {
PluralizationRule,
@@ -58,10 +56,10 @@ export interface Formatter {
interpolate(message: string, values: any, path: string): Array | null
}
-/*!
+/**
* VueI18n Options
*
- * This option type is compatible with the constructor options of `VueI18n` class (offered with vue-i18n@8.x).
+ * This option is compatible with the constructor options of `VueI18n` class (offered with vue-i18n@8.x).
*/
export type VueI18nOptions = {
locale?: Locale
@@ -83,14 +81,14 @@ export type VueI18nOptions = {
pluralizationRules?: PluralizationRules
postTranslation?: PostTranslationHandler
sync?: boolean
- __i18n?: CustomBlocks // for custom blocks, and internal
- __root?: Composer // for internal
+ __i18n?: CustomBlocks
+ __root?: Composer
}
-/*!
+/**
* VueI18n Interfaces
*
- * This type is compatible with interface of `VueI18n` class (offered with vue-i18n@8.x).
+ * This interface is compatible with interface of `VueI18n` class (offered with vue-i18n@8.x).
*/
export type VueI18n = {
/*!
@@ -156,11 +154,11 @@ export type VueI18n = {
setNumberFormat(locale: Locale, format: NumberFormat): void
mergeNumberFormat(locale: Locale, format: NumberFormat): void
getChoiceIndex: (choice: Choice, choicesLength: number) => number
- install(app: App, ...options: unknown[]): void
}
/**
- * Convert to I18n Composer Options from VueI18n Options
+ * Convert to I18n Composer Options from VueI18n Options
+ * @internal
*/
function convertComposerOptions(options: VueI18nOptions): ComposerOptions {
const locale = isString(options.locale) ? options.locale : 'en-US'
@@ -193,6 +191,7 @@ function convertComposerOptions(options: VueI18nOptions): ComposerOptions {
const warnHtmlMessage = isString(options.warnHtmlInMessage)
? options.warnHtmlInMessage !== 'off'
: true
+ const inheritLocale = !!options.sync
if (__DEV__ && options.formatter) {
warn(`not supportted 'formatter' option`)
@@ -227,15 +226,15 @@ function convertComposerOptions(options: VueI18nOptions): ComposerOptions {
pluralRules: pluralizationRules,
postTranslation,
warnHtmlMessage,
+ inheritLocale,
__i18n,
__root
}
}
-/*!
- * createVueI18n factory
- *
- * This function is compatible with constructor of `VueI18n` class (offered with vue-i18n@8.x) like `new VueI18n(...)`.
+/**
+ * create VueI18n interface factory
+ * @internal
*/
export function createVueI18n(options: VueI18nOptions = {}): VueI18n {
const composer = createComposer(convertComposerOptions(options))
@@ -340,11 +339,12 @@ export function createVueI18n(options: VueI18nOptions = {}): VueI18n {
composer.setPostTranslationHandler(handler)
},
+ // sync
get sync(): boolean {
- return isBoolean(options.sync) ? options.sync : false
+ return composer.inheritLocale
},
set sync(val: boolean) {
- options.sync = val
+ composer.inheritLocale = val
},
// warnInHtmlMessage
@@ -492,12 +492,6 @@ export function createVueI18n(options: VueI18nOptions = {}): VueI18n {
getChoiceIndex(choice: Choice, choicesLength: number): number {
__DEV__ && warn(`not supportted 'getChoiceIndex' method.`)
return -1
- },
-
- // install
- install(app: App, ...options: unknown[]): void {
- composer.install(app, ...options)
- app.mixin(defineMixin(vueI18n, composer))
}
}
diff --git a/src/mixin.ts b/src/mixin.ts
index 615036216..1f30f9318 100644
--- a/src/mixin.ts
+++ b/src/mixin.ts
@@ -1,4 +1,4 @@
-import { ComponentOptions } from 'vue'
+import { ComponentOptions, getCurrentInstance } from 'vue'
import { Path } from './path'
import { Locale } from './core/context'
import { Composer } from './composer'
@@ -10,16 +10,23 @@ import {
DateTimeFormatResult,
NumberFormatResult
} from './legacy'
+import { I18nInternal } from './i18n'
// supports compatibility for legacy vue-i18n APIs
export function defineMixin(
legacy: VueI18n,
- composer: Composer
+ composer: Composer,
+ i18n: I18nInternal
): ComponentOptions {
return {
beforeCreate() {
- const options = this.$options
+ const instance = getCurrentInstance()
+ if (!instance) {
+ // TODO:
+ throw new Error('TODO')
+ }
+ const options = this.$options
if (options.i18n) {
const optionsI18n = options.i18n as VueI18nOptions
if (options.__i18n) {
@@ -27,12 +34,17 @@ export function defineMixin(
}
optionsI18n.__root = composer
this.$i18n = createVueI18n(optionsI18n)
+
+ i18n._setLegacy(instance, this.$i18n)
} else if (options.__i18n) {
this.$i18n = createVueI18n({
__i18n: options.__i18n,
__root: composer
})
+
+ i18n._setLegacy(instance, this.$i18n)
} else {
+ // set global
this.$i18n = legacy
}
@@ -52,8 +64,22 @@ export function defineMixin(
},
beforeDestroy() {
- this.$el.__intlify__ = undefined
+ const instance = getCurrentInstance()
+ if (!instance) {
+ // TODO:
+ throw new Error('TODO')
+ }
+
delete this.$el.__intlify__
+
+ delete this.$t
+ delete this.$tc
+ delete this.$te
+ delete this.$d
+ delete this.$n
+
+ i18n._deleteLegacy(instance)
+ delete this.$i18n
}
}
}
diff --git a/src/plugin.ts b/src/plugin.ts
index 0c2e25e40..2747e7b24 100644
--- a/src/plugin.ts
+++ b/src/plugin.ts
@@ -1,6 +1,5 @@
import { App, FunctionDirective } from 'vue'
-import { Composer } from './composer'
-import { GlobalI18nSymbol } from './i18n'
+import { I18nSymbol, I18n } from './i18n'
import { Translation, NumberFormat, DatetimeFormat } from './components'
import { hook as vT } from './directive'
import { isPlainObject, isString, warn } from './utils'
@@ -9,11 +8,7 @@ export type I18nPluginOptions = {
'i18n-t'?: string
}
-export function apply(
- app: App,
- composer: Composer,
- ...options: unknown[]
-): void {
+export function apply(app: App, i18n: I18n, ...options: unknown[]): void {
const pluginOptions = parseOptions(...options)
if (__DEV__ && isString(pluginOptions['i18n-t'])) {
@@ -31,7 +26,7 @@ export function apply(
app.directive('t', vT as FunctionDirective) // TODO:
// setup global provider
- app.provide(GlobalI18nSymbol, composer)
+ app.provide(I18nSymbol, i18n)
}
function parseOptions(...options: unknown[]): I18nPluginOptions {
diff --git a/test/composer.test.ts b/test/composer.test.ts
index 88ea31214..468a020fb 100644
--- a/test/composer.test.ts
+++ b/test/composer.test.ts
@@ -13,7 +13,7 @@ import {
addPreCompileMessages
} from '../src/composer'
import { generateFormatCacheKey } from '../src/utils'
-import { watch } from 'vue'
+import { watch, nextTick } from 'vue'
describe('locale', () => {
test('default value', () => {
@@ -48,6 +48,69 @@ describe('fallbackLocale', () => {
})
})
+describe('inheritLocale', () => {
+ test('default value', () => {
+ const root = createComposer({ locale: 'en' })
+ const { inheritLocale, locale } = createComposer({
+ locale: 'ja',
+ __root: root
+ })
+ expect(inheritLocale).toEqual(false)
+ expect(locale.value).toEqual('ja')
+ })
+
+ test('initialize with composer option', () => {
+ const root = createComposer({ locale: 'en' })
+ const { inheritLocale, locale } = createComposer({
+ locale: 'ja',
+ inheritLocale: true,
+ __root: root
+ })
+ expect(inheritLocale).toEqual(true)
+ expect(locale.value).toEqual('en')
+ })
+
+ test('sync root locale, fallbackLocale', async () => {
+ const root = createComposer({
+ locale: 'en',
+ fallbackLocale: ['ja', 'fr']
+ })
+ const composer = createComposer({
+ locale: 'ja',
+ fallbackLocale: ['zh', 'de'],
+ inheritLocale: true,
+ __root: root
+ })
+ await nextTick()
+
+ expect(composer.locale.value).toEqual('en')
+ expect(composer.fallbackLocale.value).toEqual(['ja', 'fr'])
+
+ root.locale.value = 'ja'
+ root.fallbackLocale.value = ['zh', 'de']
+ await nextTick()
+
+ expect(composer.locale.value).toEqual('ja')
+ expect(composer.fallbackLocale.value).toEqual(['zh', 'de'])
+
+ composer.inheritLocale = false
+ await nextTick()
+
+ root.locale.value = 'en'
+ root.fallbackLocale.value = ['ja', 'fr']
+ await nextTick()
+
+ expect(composer.locale.value).toEqual('ja')
+ expect(composer.fallbackLocale.value).toEqual(['zh', 'de'])
+
+ composer.inheritLocale = true
+ await nextTick()
+
+ expect(composer.locale.value).toEqual('en')
+ expect(composer.fallbackLocale.value).toEqual(['ja', 'fr'])
+ })
+})
+
describe('availableLocales', () => {
test('not initialize messages at composer creating', () => {
const { availableLocales } = createComposer({})
diff --git a/test/i18n.test.ts b/test/i18n.test.ts
new file mode 100644
index 000000000..0ff1e3f75
--- /dev/null
+++ b/test/i18n.test.ts
@@ -0,0 +1,25 @@
+import { createI18n } from '../src/i18n'
+
+describe('createI18n', () => {
+ test('legay mode', () => {
+ const i18n = createI18n({
+ legacy: true
+ })
+
+ expect(i18n.mode).toEqual('legacy')
+ })
+
+ test('composable mode', () => {
+ const i18n = createI18n({})
+
+ expect(i18n.mode).toEqual('composable')
+ })
+})
+
+describe('useI18n', () => {
+ test.todo('basic')
+ test.todo('global scope')
+ test.todo('parent scope')
+ test.todo('not plugin installed')
+ test.todo('not used in setup function')
+})