ネストされたラムダスロットのi18nスコープ解決バグを修正

viewcomponent/view_component

ViewComponentのラムダバックドスロットで相対翻訳キー t(".key") を使用した際に、ブロックが定義されたパーシャルではなく中間コンポーネントのスコープに解決されていたバグが修正されました。

背景

#2513 で報告されたこのバグは、3階層以上のネストを持つスロット構造において t(".key") の翻訳スコープが意図しないコンポーネントに解決されるというものでした。たとえば、パーシャル内で以下のような3階層のスロット呼び出しを行うと、shared.lambda_slot_translation.lambda_action ではなく lambda_slot_inner_component.lambda_action のような中間コンポーネントのスコープが使われてしまいます。

<%= render LambdaSlotOuterComponent.new do |outer| %>
  <% outer.with_inner do |inner| %>
    <% inner.with_aside do %>
      <%= t(".lambda_action") %>
    <% end %>
  <% end %>
<% end %>

#2520 でこの問題を修正するために __vc_content_block_virtual_path が導入され、ブロック定義時点の @virtual_path をキャプチャしてブロック評価時に復元する仕組みが実装されました。しかし、ラムダバックドスロットでは修正が不完全でした。

技術的な変更

__vc_set_slot 内のラムダブランチが誤った仮想パスを参照していたことが根本原因です。#2520 ではラムダブランチにも with_captured_virtual_path によるラッピングが追加されましたが、渡していた値が slot.__vc_content_block_virtual_path(ブロック定義時のパーシャルのパス)ではなく @old_virtual_path(中間の親コンポーネントのパス)でした。

修正は lib/view_component/slotable.rb__vc_set_slot メソッドに集中しています。ブロックがある場合にキャプチャした仮想パスをローカル変数 captured_block_virtual_path に保持し、それをラムダブランチに渡すように変更しています。

変更前:

def __vc_set_slot(slot_name, slot_definition = nil, *args, **kwargs, &block)
  slot_definition ||= self.class.registered_slots[slot_name]
  slot = Slot.new(self)

  # ...
  if block
    slot.__vc_content_block = block
    slot.__vc_content_block_virtual_path = view_context.instance_variable_get(:@virtual_path)
  end

  # ...
  renderable_value =
    if block
      renderable_function.call(*args, **kwargs) do |*rargs|
        with_captured_virtual_path(@old_virtual_path) do  # ← 誤り:中間コンポーネントのパスを使用
          view_context.capture(*rargs, &block)
        end
      end
    end
end

変更後:

def __vc_set_slot(slot_name, slot_definition = nil, *args, **kwargs, &block)
  slot_definition ||= self.class.registered_slots[slot_name]
  slot = Slot.new(self)
  captured_block_virtual_path = nil  # ← ローカル変数を追加

  # ...
  if block
    slot.__vc_content_block = block
    captured_block_virtual_path = view_context.instance_variable_get(:@virtual_path)  # ← ローカル変数にキャプチャ
    slot.__vc_content_block_virtual_path = captured_block_virtual_path
  end

  # ...
  renderable_value =
    if block
      renderable_function.call(*args, **kwargs) do |*rargs|
        with_captured_virtual_path(captured_block_virtual_path) do  # ← 正しいパスを使用
          view_context.capture(*rargs, &block)
        end
      end
    end
end

なお、slot.__vc_content_block_virtual_pathattr_writer で定義されており書き込み専用です。PR内では attr_reader を追加してスロットオブジェクト経由で読み出す案も検討されましたが、ローカル変数を介することで既存のインターフェースを変えずに問題を解決しています。

設計判断

attr_reader を公開せずローカル変数で解決する方式 が採用されました。slot.__vc_content_block_virtual_path に対して attr_reader を追加すれば直接読み出せますが、それはスロットの内部状態を外部に露出することになります。ローカル変数 captured_block_virtual_path__vc_set_slot スコープ内に留めることで、既存インターフェースへの影響を最小限にしています。

非ラムダスロットは Slot#to_s 呼び出し時に評価を遅延するため attr_writer 経由で保持したパスを正しく使えていました。一方、ラムダバックドスロットは __vc_set_slot 内で即座に評価されるため、with_captured_virtual_path に渡す値の正確さが直接バグの有無を左右します。今回の変更はこの評価タイミングの違いに明確に対応したものです。

テストとして LambdaSlotOuterComponentLambdaSlotInnerComponent → ラムダバックドスロットの3階層構造を持つフィクスチャが追加され、deeply_nested_translation_test.rbtest_translation_in_nested_lambda_backed_slot が追加されています。

まとめ

本PRは2行の変更で、ラムダバックドスロットにおける t(".key") の翻訳スコープ解決を正しいパーシャルのスコープに修正しています。非ラムダスロットと評価タイミングが異なるラムダスロットへの対応漏れが原因であり、ローカル変数を介したパスの受け渡しで既存インターフェースを保ちつつ修正を完結させた設計は、変更の局所性という観点で参考になります。

記事メタデータ

Generated by:
Claude Sonnet 4.6 for DiffDaily
LLM Trace:
9250850d

この記事はAIによって自動生成されています。内容の正確性については、必ずソースコードやPRを確認してください。

品質レビュー結果

Review Status:
承認済み
Review Count:
1回
Reviewed by:
Gemini 2.5 Pro for DiffDaily

Review Criteria:

記事構成 ✓ PASS

Title, Context, Technical Detailの存在と明確さ

リード文(総論)→背景・技術詳細・設計判断(各論)→まとめ(結論)の3部構成が明確に適用されており、理想的な記事構成です。

カスタムMarkdown構文 ✓ PASS

シンタックスハイライト・GitHubリンク記法の正確性

ファイル名付きシンタックスハイライト、PR・Issue番号のリンク記法ともに正しく使用されています。

対象読者への適合性 ✓ PASS

エンジニア向けの適切な技術レベルと表現

ViewComponentの内部実装に関するトピックであり、専門知識を持つエンジニアという対象読者に完全に適合しています。

パラグラフ・ライティング ✓ PASS

トピックセンテンス・1段落1トピック・段落長

各セクションが総論→各論の構成で書かれ、各段落もトピックセンテンスで始まるなど、パラグラフ・ライティングの原則が守られており、非常に読みやすいです。

Diff内容との照合 ✓ PASS

コードブロックとDiff内容の一致

記事内で引用されているコードブロックは、提供されたDiffの内容と正確に一致しています。変更前後の対比も適切です。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

「ラムダバックドスロット」「@virtual_path」などの技術用語が、PRの文脈に沿って正確に使用されています。

説明の技術的正確性 ✓ PASS

技術的主張の正確性と論理性

ラムダスロットと非ラムダスロットの評価タイミングの違いなど、バグの根本原因に関する説明がPR DescriptionとDiffに基づいており、技術的に正確です。

事実の突合 ⚠ WARNING

PR情報による主張の裏付け(ハルシネーション検出)

ほぼすべての主張がPR情報で裏付けられていますが、「まとめ」セクションの「2行の変更で」という表現は、Diffの行数(4行追加、2行削除)と厳密には一致しません。

数値・固有名詞の確認 ✓ PASS

PR番号・コミットID・バージョン等の正確性

PR番号(#2633, #2513, #2520)やその他の固有名詞はすべて正確に記載されています。

タイトル・説明との一致 ✓ PASS

記事タイトル・説明とPR内容の一致

記事のタイトル「ネストされたラムダスロットのi18nスコープ解決バグを修正」は、PRのタイトルと内容を的確に反映しています。

外部知識の正確性 ✓ PASS

PRに記載のない外部知識(LTS、サポート状況など)の不使用

記事の内容はすべて提供されたPR情報に基づいており、PRに記載のない外部知識(バージョン情報、リリース予定など)の追加はありません。

時間表現の正確性 ✓ PASS

時間表現がPR情報と一致しているか

過去の修正(#2520)と今回の修正の関係性など、時間的な前後関係が正しく表現されています。