Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 66 additions & 0 deletions docs/guide/advanced/composition.md
Original file line number Diff line number Diff line change
Expand Up @@ -468,6 +468,72 @@ If you do not want to inherit the locale from the global scope, the `inheritLoca
Changes to the `locale` at the local scope have **no effect on the global scope locale, but only within the local scope**.
:::

## Isolated scope

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.

### Why isolated scope?

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:

- Is **not registered** with the component's uid (no duplicate detection)
- Does **not propagate** to child components via `provide`
- Does **not merge** SFC i18n custom blocks
- **Inherits locale** from the parent/global scope (by default)
- **Falls back** to the parent/global scope for missing translation keys

### Usage in composables

<!-- eslint-skip -->

```ts
// useProjectStatus.ts
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'

export function useProjectStatus(project) {
const { t } = useI18n({
useScope: 'isolated',
messages: {
en: { active: 'Active', inactive: 'Inactive' },
ja: { active: '稼働中', inactive: '停止中' }
}
})

return computed(() => project.isActive ? t('active') : t('inactive'))
}
```

<!-- eslint-skip -->

```vue
<!-- MyComponent.vue -->
<script setup>
import { useI18n } from 'vue-i18n'
import { useProjectStatus } from './useProjectStatus'

// Local scope for the component
const { t } = useI18n({
messages: {
en: { title: 'Project Dashboard' },
ja: { title: 'プロジェクトダッシュボード' }
}
})

// Composable with isolated scope — no conflict!
const status = useProjectStatus(project)
</script>

<template>
<h1>{{ t('title') }}</h1>
<p>{{ status }}</p>
</template>
```

:::tip NOTE
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`.
:::
Comment on lines +471 to +535
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify doc wording vs runtime behavior for isolated scope inheritance.

echo "=== Checking locale inheritance wording in documentation ==="
rg -n "Inherits locale|inherits locale|parent/global scope|global scope by default" \
  docs/guide/advanced/composition.md \
  docs/jp/guide/advanced/composition.md \
  docs/zh/guide/advanced/composition.md 2>/dev/null || echo "Documentation files not found, checking available files..."

echo -e "\n=== Checking for locale inheritance patterns ==="
rg -n "locale.*inherit|inherit.*locale" docs/guide/advanced/composition.md -A2 -B2 2>/dev/null || echo "Pattern not found"

echo -e "\n=== Checking runtime implementation ==="
rg -n "parentComposer.*gl|__root.*parent" packages/vue-i18n-core/src/i18n.ts -B3 -A3 2>/dev/null || echo "Runtime code not found"

Repository: intlify/vue-i18n

Length of output: 2113


Fix locale inheritance wording inconsistency in the isolated scope section.

Line 482 correctly describes "inherits locale from the parent/global scope," but line 534 states only "global scope." The runtime implementation (composerOptions.__root = parentComposer || gl, confirmed at lines 574 and 643 in packages/vue-i18n-core/src/i18n.ts) uses the parent Composer first, then falls back to global scope—matching the parent/global behavior. Line 534 should be updated to reflect this:

📝 Suggested wording update (EN)
-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`.
+The isolated scope inherits locale from the parent scope (if available), otherwise from the global scope by default. When the inherited locale source changes, the isolated scope's locale is automatically updated. You can disable this behavior by setting `inheritLocale: false`.

Also update the JP/ZH documentation sections if they contain similar locale inheritance descriptions.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/guide/advanced/composition.md` around lines 471 - 535, Update the
wording in the isolated scope tip to say the isolated scope inherits locale from
the parent/global scope (not just “global scope”) to match runtime behavior (see
composerOptions.__root set to parentComposer || gl in the core); change the
sentence in the EN isolated scope section (the NOTE tip) to mention
“parent/global scope” and also audit and update the JP and ZH translations of
the same tip to the equivalent parent/global phrasing so all locales are
consistent with useI18n and Composer behavior.


## Mapping from VueI18n to Composer

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).
66 changes: 66 additions & 0 deletions docs/jp/guide/advanced/composition.md
Original file line number Diff line number Diff line change
Expand Up @@ -468,6 +468,72 @@ locale.value = 'en' // change!
ローカルスコープでの `locale` の変更は、**グローバルスコープのロケールには影響せず、ローカルスコープ内でのみ有効です**。
:::

## 分離スコープ

分離スコープは、**コンポーネントに紐づかない**独立した Composer インスタンスを作成します。これは、コンポーネントのローカルスコープと競合することなく、コンポーザブル内で独自の翻訳メッセージを持つ `useI18n` を使用したい場合に便利です。

### なぜ分離スコープが必要か?

コンポーネントとコンポーザブルの両方がローカルスコープで `useI18n` を呼び出すと、1つのコンポーネントにつき1つのローカルスコープしか許可されていないため、2回目の呼び出しが最初の呼び出しと競合します。分離スコープは、以下の特性を持つ Composer を作成することでこの問題を解決します:

- コンポーネントの uid に**登録されない**(重複検出の対象外)
- `provide` を通じて子コンポーネントに**伝播しない**
- SFC i18n カスタムブロックを**マージしない**
- 親/グローバルスコープからロケールを**継承する**(デフォルト)
- 翻訳キーが見つからない場合、親/グローバルスコープに**フォールバックする**

### コンポーザブルでの使用方法

<!-- eslint-skip -->

```ts
// useProjectStatus.ts
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'

export function useProjectStatus(project) {
const { t } = useI18n({
useScope: 'isolated',
messages: {
en: { active: 'Active', inactive: 'Inactive' },
ja: { active: '稼働中', inactive: '停止中' }
}
})

return computed(() => project.isActive ? t('active') : t('inactive'))
}
```

<!-- eslint-skip -->

```vue
<!-- MyComponent.vue -->
<script setup>
import { useI18n } from 'vue-i18n'
import { useProjectStatus } from './useProjectStatus'

// コンポーネントのローカルスコープ
const { t } = useI18n({
messages: {
en: { title: 'Project Dashboard' },
ja: { title: 'プロジェクトダッシュボード' }
}
})

// 分離スコープを使用するコンポーザブル — 競合なし!
const status = useProjectStatus(project)
</script>

<template>
<h1>{{ t('title') }}</h1>
<p>{{ status }}</p>
</template>
```

:::tip NOTE
分離スコープはデフォルトでグローバルスコープからロケールを継承します。グローバルロケールが変更されると、分離スコープのロケールも自動的に更新されます。この動作を無効にするには、`inheritLocale: false` を設定してください。
:::

## VueI18n から Composer へのマッピング

v11 以前から移行する場合は、VueI18n インスタンス(Legacy API)と Composer インスタンス(Composition API)の詳細なマッピングについて、[v12 破壊的変更](../migration/breaking12#drop-legacy-api-mode) を参照してください。
66 changes: 66 additions & 0 deletions docs/zh/guide/advanced/composition.md
Original file line number Diff line number Diff line change
Expand Up @@ -468,6 +468,72 @@ locale.value = 'en' // change!
本地作用域中对 `locale` 的更改 **对全局作用域区域设置没有影响,仅在本地作用域内有效**。
:::

## 隔离作用域

隔离作用域创建一个**不与组件绑定**的独立 Composer 实例。当你想在组合式函数中使用带有自己翻译消息的 `useI18n`,而不与组件的本地作用域冲突时,这非常有用。

### 为什么需要隔离作用域?

当组件和组合式函数都使用本地作用域调用 `useI18n` 时,第二次调用会与第一次冲突,因为每个组件只允许一个本地作用域。隔离作用域通过创建以下特性的 Composer 来解决这个问题:

- **不注册**到组件的 uid(不进行重复检测)
- **不通过** `provide` 传播到子组件
- **不合并** SFC i18n 自定义块
- 默认从父级/全局作用域**继承区域设置**
- 当翻译键缺失时,**回退**到父级/全局作用域

### 在组合式函数中使用

<!-- eslint-skip -->

```ts
// useProjectStatus.ts
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'

export function useProjectStatus(project) {
const { t } = useI18n({
useScope: 'isolated',
messages: {
en: { active: 'Active', inactive: 'Inactive' },
ja: { active: '稼働中', inactive: '停止中' }
}
})

return computed(() => project.isActive ? t('active') : t('inactive'))
}
```

<!-- eslint-skip -->

```vue
<!-- MyComponent.vue -->
<script setup>
import { useI18n } from 'vue-i18n'
import { useProjectStatus } from './useProjectStatus'

// 组件的本地作用域
const { t } = useI18n({
messages: {
en: { title: 'Project Dashboard' },
ja: { title: 'プロジェクトダッシュボード' }
}
})

// 使用隔离作用域的组合式函数 - 没有冲突!
const status = useProjectStatus(project)
</script>

<template>
<h1>{{ t('title') }}</h1>
<p>{{ status }}</p>
</template>
```

:::tip NOTE
隔离作用域默认从全局作用域继承区域设置。当全局区域设置更改时,隔离作用域的区域设置会自动更新。你可以通过设置 `inheritLocale: false` 来禁用此行为。
:::

## VueI18n 到 Composer 的映射

如果你正在从 v11 或更早版本迁移,请参阅 [v12 破坏性变更](../migration/breaking12#drop-legacy-api-mode) 以获取 VueI18n 实例(传统 API)和 Composer 实例(组合式 API)之间的详细映射。
6 changes: 3 additions & 3 deletions packages/vue-i18n-core/src/components/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { Locale } from '@intlify/core-base'
import type { Composer } from '../composer'
import type { I18nScope } from '../i18n'

export type ComponentI18nScope = Exclude<I18nScope, 'local'>
export type ComponentI18nScope = Exclude<I18nScope, 'local' | 'isolated'>

/**
* BaseFormat Props for Components that is offered Vue I18n
Expand Down Expand Up @@ -57,9 +57,9 @@ export const BaseFormatPropsValidators: Record<string, any> = {
scope: {
type: String,
// NOTE: avoid https://github.com/microsoft/rushstack/issues/1050
validator: (val: Exclude<I18nScope, 'local'> /* ComponentI18nScope */): boolean =>
validator: (val: Exclude<I18nScope, 'local' | 'isolated'> /* ComponentI18nScope */): boolean =>
val === 'parent' || val === 'global',
default: 'parent' as Exclude<I18nScope, 'local'> /* ComponentI18nScope */
default: 'parent' as Exclude<I18nScope, 'local' | 'isolated'> /* ComponentI18nScope */
},
i18n: {
type: Object
Expand Down
48 changes: 47 additions & 1 deletion packages/vue-i18n-core/src/i18n.ts
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ export interface I18nInternal<
*
* @VueI18nGeneral
*/
export type I18nScope = 'local' | 'parent' | 'global'
export type I18nScope = 'local' | 'parent' | 'global' | 'isolated'

/**
* I18n Options for `useI18n`
Expand Down Expand Up @@ -563,6 +563,52 @@ export function useI18n<
>
}

// Isolated scope - independent composer not tied to component uid
if (scope === 'isolated') {
const i18nInternal = i18n as unknown as I18nInternal

const composerOptions = assign({}, options) as ComposerOptions & ComposerInternalOptions

// Set parent Composer as fallback root
const parentComposer = inject(I18nComposerKey, null)
composerOptions.__root = parentComposer || gl

const composer = createComposer(composerOptions) as Composer

// ComposerExtend
if (i18nInternal.__composerExtend) {
;(composer as any)[DisposeSymbol] = i18nInternal.__composerExtend(composer)
}

// DevTools emitter setup
let emitter: VueDevToolsEmitter | null = null
if ((__DEV__ || __FEATURE_PROD_VUE_DEVTOOLS__) && !__NODE_JS__) {
emitter = createEmitter<VueDevToolsEmitterEvents>()
const _composer = composer as any
_composer[EnableEmitter]?.(emitter)
emitter.on('*', addTimelineEvent)
}

// Lifecycle management via onScopeDispose
const currentScope = getCurrentScope()
if (currentScope) {
onScopeDispose(() => {
if ((__DEV__ || __FEATURE_PROD_VUE_DEVTOOLS__) && !__NODE_JS__) {
emitter?.off('*', addTimelineEvent)
const _composer = composer as any
_composer[DisableEmitter]?.()
}
const dispose = (composer as any)[DisposeSymbol]
if (dispose) {
dispose()
delete (composer as any)[DisposeSymbol]
}
})
}

return composer as unknown as Composer<Messages, DateTimeFormats, NumberFormats, OptionLocale>
}

// Local scope
const i18nInternal = i18n as unknown as I18nInternal

Expand Down
Loading
Loading