Skip to content

Commit d055524

Browse files
authored
feat: support isolated scope (#2404)
1 parent 8039e71 commit d055524

6 files changed

Lines changed: 416 additions & 4 deletions

File tree

docs/guide/advanced/composition.md

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -468,6 +468,72 @@ If you do not want to inherit the locale from the global scope, the `inheritLoca
468468
Changes to the `locale` at the local scope have **no effect on the global scope locale, but only within the local scope**.
469469
:::
470470

471+
## Isolated scope
472+
473+
The isolated scope creates an independent Composer instance that is **not tied to the component**. This is useful when you want to use `useI18n` inside a composable with its own translation messages, without conflicting with the component's local scope.
474+
475+
### Why isolated scope?
476+
477+
When a component and a composable both call `useI18n` with local scope, the second call conflicts with the first because only one local scope per component is allowed. The isolated scope solves this by creating a Composer that:
478+
479+
- Is **not registered** with the component's uid (no duplicate detection)
480+
- Does **not propagate** to child components via `provide`
481+
- Does **not merge** SFC i18n custom blocks
482+
- **Inherits locale** from the parent/global scope (by default)
483+
- **Falls back** to the parent/global scope for missing translation keys
484+
485+
### Usage in composables
486+
487+
<!-- eslint-skip -->
488+
489+
```ts
490+
// useProjectStatus.ts
491+
import { computed } from 'vue'
492+
import { useI18n } from 'vue-i18n'
493+
494+
export function useProjectStatus(project) {
495+
const { t } = useI18n({
496+
useScope: 'isolated',
497+
messages: {
498+
en: { active: 'Active', inactive: 'Inactive' },
499+
ja: { active: '稼働中', inactive: '停止中' }
500+
}
501+
})
502+
503+
return computed(() => project.isActive ? t('active') : t('inactive'))
504+
}
505+
```
506+
507+
<!-- eslint-skip -->
508+
509+
```vue
510+
<!-- MyComponent.vue -->
511+
<script setup>
512+
import { useI18n } from 'vue-i18n'
513+
import { useProjectStatus } from './useProjectStatus'
514+
515+
// Local scope for the component
516+
const { t } = useI18n({
517+
messages: {
518+
en: { title: 'Project Dashboard' },
519+
ja: { title: 'プロジェクトダッシュボード' }
520+
}
521+
})
522+
523+
// Composable with isolated scope — no conflict!
524+
const status = useProjectStatus(project)
525+
</script>
526+
527+
<template>
528+
<h1>{{ t('title') }}</h1>
529+
<p>{{ status }}</p>
530+
</template>
531+
```
532+
533+
:::tip NOTE
534+
The isolated scope inherits locale from the global scope by default. When the global locale changes, the isolated scope's locale is automatically updated. You can disable this behavior by setting `inheritLocale: false`.
535+
:::
536+
471537
## Mapping from VueI18n to Composer
472538

473539
If you are migrating from v11 or earlier, see the [v12 Breaking Changes](../migration/breaking12#drop-legacy-api-mode) for a detailed mapping between the VueI18n instance (Legacy API) and the Composer instance (Composition API).

docs/jp/guide/advanced/composition.md

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -468,6 +468,72 @@ locale.value = 'en' // change!
468468
ローカルスコープでの `locale` の変更は、**グローバルスコープのロケールには影響せず、ローカルスコープ内でのみ有効です**
469469
:::
470470

471+
## 分離スコープ
472+
473+
分離スコープは、**コンポーネントに紐づかない**独立した Composer インスタンスを作成します。これは、コンポーネントのローカルスコープと競合することなく、コンポーザブル内で独自の翻訳メッセージを持つ `useI18n` を使用したい場合に便利です。
474+
475+
### なぜ分離スコープが必要か?
476+
477+
コンポーネントとコンポーザブルの両方がローカルスコープで `useI18n` を呼び出すと、1つのコンポーネントにつき1つのローカルスコープしか許可されていないため、2回目の呼び出しが最初の呼び出しと競合します。分離スコープは、以下の特性を持つ Composer を作成することでこの問題を解決します:
478+
479+
- コンポーネントの uid に**登録されない**(重複検出の対象外)
480+
- `provide` を通じて子コンポーネントに**伝播しない**
481+
- SFC i18n カスタムブロックを**マージしない**
482+
- 親/グローバルスコープからロケールを**継承する**(デフォルト)
483+
- 翻訳キーが見つからない場合、親/グローバルスコープに**フォールバックする**
484+
485+
### コンポーザブルでの使用方法
486+
487+
<!-- eslint-skip -->
488+
489+
```ts
490+
// useProjectStatus.ts
491+
import { computed } from 'vue'
492+
import { useI18n } from 'vue-i18n'
493+
494+
export function useProjectStatus(project) {
495+
const { t } = useI18n({
496+
useScope: 'isolated',
497+
messages: {
498+
en: { active: 'Active', inactive: 'Inactive' },
499+
ja: { active: '稼働中', inactive: '停止中' }
500+
}
501+
})
502+
503+
return computed(() => project.isActive ? t('active') : t('inactive'))
504+
}
505+
```
506+
507+
<!-- eslint-skip -->
508+
509+
```vue
510+
<!-- MyComponent.vue -->
511+
<script setup>
512+
import { useI18n } from 'vue-i18n'
513+
import { useProjectStatus } from './useProjectStatus'
514+
515+
// コンポーネントのローカルスコープ
516+
const { t } = useI18n({
517+
messages: {
518+
en: { title: 'Project Dashboard' },
519+
ja: { title: 'プロジェクトダッシュボード' }
520+
}
521+
})
522+
523+
// 分離スコープを使用するコンポーザブル — 競合なし!
524+
const status = useProjectStatus(project)
525+
</script>
526+
527+
<template>
528+
<h1>{{ t('title') }}</h1>
529+
<p>{{ status }}</p>
530+
</template>
531+
```
532+
533+
:::tip NOTE
534+
分離スコープはデフォルトでグローバルスコープからロケールを継承します。グローバルロケールが変更されると、分離スコープのロケールも自動的に更新されます。この動作を無効にするには、`inheritLocale: false` を設定してください。
535+
:::
536+
471537
## VueI18n から Composer へのマッピング
472538

473539
v11 以前から移行する場合は、VueI18n インスタンス(Legacy API)と Composer インスタンス(Composition API)の詳細なマッピングについて、[v12 破壊的変更](../migration/breaking12#drop-legacy-api-mode) を参照してください。

docs/zh/guide/advanced/composition.md

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -468,6 +468,72 @@ locale.value = 'en' // change!
468468
本地作用域中对 `locale` 的更改 **对全局作用域区域设置没有影响,仅在本地作用域内有效**
469469
:::
470470

471+
## 隔离作用域
472+
473+
隔离作用域创建一个**不与组件绑定**的独立 Composer 实例。当你想在组合式函数中使用带有自己翻译消息的 `useI18n`,而不与组件的本地作用域冲突时,这非常有用。
474+
475+
### 为什么需要隔离作用域?
476+
477+
当组件和组合式函数都使用本地作用域调用 `useI18n` 时,第二次调用会与第一次冲突,因为每个组件只允许一个本地作用域。隔离作用域通过创建以下特性的 Composer 来解决这个问题:
478+
479+
- **不注册**到组件的 uid(不进行重复检测)
480+
- **不通过** `provide` 传播到子组件
481+
- **不合并** SFC i18n 自定义块
482+
- 默认从父级/全局作用域**继承区域设置**
483+
- 当翻译键缺失时,**回退**到父级/全局作用域
484+
485+
### 在组合式函数中使用
486+
487+
<!-- eslint-skip -->
488+
489+
```ts
490+
// useProjectStatus.ts
491+
import { computed } from 'vue'
492+
import { useI18n } from 'vue-i18n'
493+
494+
export function useProjectStatus(project) {
495+
const { t } = useI18n({
496+
useScope: 'isolated',
497+
messages: {
498+
en: { active: 'Active', inactive: 'Inactive' },
499+
ja: { active: '稼働中', inactive: '停止中' }
500+
}
501+
})
502+
503+
return computed(() => project.isActive ? t('active') : t('inactive'))
504+
}
505+
```
506+
507+
<!-- eslint-skip -->
508+
509+
```vue
510+
<!-- MyComponent.vue -->
511+
<script setup>
512+
import { useI18n } from 'vue-i18n'
513+
import { useProjectStatus } from './useProjectStatus'
514+
515+
// 组件的本地作用域
516+
const { t } = useI18n({
517+
messages: {
518+
en: { title: 'Project Dashboard' },
519+
ja: { title: 'プロジェクトダッシュボード' }
520+
}
521+
})
522+
523+
// 使用隔离作用域的组合式函数 - 没有冲突!
524+
const status = useProjectStatus(project)
525+
</script>
526+
527+
<template>
528+
<h1>{{ t('title') }}</h1>
529+
<p>{{ status }}</p>
530+
</template>
531+
```
532+
533+
:::tip NOTE
534+
隔离作用域默认从全局作用域继承区域设置。当全局区域设置更改时,隔离作用域的区域设置会自动更新。你可以通过设置 `inheritLocale: false` 来禁用此行为。
535+
:::
536+
471537
## VueI18n 到 Composer 的映射
472538

473539
如果你正在从 v11 或更早版本迁移,请参阅 [v12 破坏性变更](../migration/breaking12#drop-legacy-api-mode) 以获取 VueI18n 实例(传统 API)和 Composer 实例(组合式 API)之间的详细映射。

packages/vue-i18n-core/src/components/base.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import type { Locale } from '@intlify/core-base'
22
import type { Composer } from '../composer'
33
import type { I18nScope } from '../i18n'
44

5-
export type ComponentI18nScope = Exclude<I18nScope, 'local'>
5+
export type ComponentI18nScope = Exclude<I18nScope, 'local' | 'isolated'>
66

77
/**
88
* BaseFormat Props for Components that is offered Vue I18n
@@ -57,9 +57,9 @@ export const BaseFormatPropsValidators: Record<string, any> = {
5757
scope: {
5858
type: String,
5959
// NOTE: avoid https://github.com/microsoft/rushstack/issues/1050
60-
validator: (val: Exclude<I18nScope, 'local'> /* ComponentI18nScope */): boolean =>
60+
validator: (val: Exclude<I18nScope, 'local' | 'isolated'> /* ComponentI18nScope */): boolean =>
6161
val === 'parent' || val === 'global',
62-
default: 'parent' as Exclude<I18nScope, 'local'> /* ComponentI18nScope */
62+
default: 'parent' as Exclude<I18nScope, 'local' | 'isolated'> /* ComponentI18nScope */
6363
},
6464
i18n: {
6565
type: Object

packages/vue-i18n-core/src/i18n.ts

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,7 @@ export interface I18nInternal<
183183
*
184184
* @VueI18nGeneral
185185
*/
186-
export type I18nScope = 'local' | 'parent' | 'global'
186+
export type I18nScope = 'local' | 'parent' | 'global' | 'isolated'
187187

188188
/**
189189
* I18n Options for `useI18n`
@@ -563,6 +563,52 @@ export function useI18n<
563563
>
564564
}
565565

566+
// Isolated scope - independent composer not tied to component uid
567+
if (scope === 'isolated') {
568+
const i18nInternal = i18n as unknown as I18nInternal
569+
570+
const composerOptions = assign({}, options) as ComposerOptions & ComposerInternalOptions
571+
572+
// Set parent Composer as fallback root
573+
const parentComposer = inject(I18nComposerKey, null)
574+
composerOptions.__root = parentComposer || gl
575+
576+
const composer = createComposer(composerOptions) as Composer
577+
578+
// ComposerExtend
579+
if (i18nInternal.__composerExtend) {
580+
;(composer as any)[DisposeSymbol] = i18nInternal.__composerExtend(composer)
581+
}
582+
583+
// DevTools emitter setup
584+
let emitter: VueDevToolsEmitter | null = null
585+
if ((__DEV__ || __FEATURE_PROD_VUE_DEVTOOLS__) && !__NODE_JS__) {
586+
emitter = createEmitter<VueDevToolsEmitterEvents>()
587+
const _composer = composer as any
588+
_composer[EnableEmitter]?.(emitter)
589+
emitter.on('*', addTimelineEvent)
590+
}
591+
592+
// Lifecycle management via onScopeDispose
593+
const currentScope = getCurrentScope()
594+
if (currentScope) {
595+
onScopeDispose(() => {
596+
if ((__DEV__ || __FEATURE_PROD_VUE_DEVTOOLS__) && !__NODE_JS__) {
597+
emitter?.off('*', addTimelineEvent)
598+
const _composer = composer as any
599+
_composer[DisableEmitter]?.()
600+
}
601+
const dispose = (composer as any)[DisposeSymbol]
602+
if (dispose) {
603+
dispose()
604+
delete (composer as any)[DisposeSymbol]
605+
}
606+
})
607+
}
608+
609+
return composer as unknown as Composer<Messages, DateTimeFormats, NumberFormats, OptionLocale>
610+
}
611+
566612
// Local scope
567613
const i18nInternal = i18n as unknown as I18nInternal
568614

0 commit comments

Comments
 (0)