copy_tuner_client 2.2.0 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -9,28 +9,60 @@ module CopyTunerClient
9
9
  # @option options [Logger] :logger where errors should be logged
10
10
  # @option options [Fixnum] :polling_delay how long to wait in between requests
11
11
  def initialize(cache, options)
12
- @cache = cache
13
- @polling_delay = options[:polling_delay]
14
- @logger = options[:logger]
15
- @command_queue = CopyTunerClient::QueueWithTimeout.new
16
- @mutex = Mutex.new
17
- @thread = nil
12
+ @cache = cache
13
+ @polling_delay = options[:polling_delay]
14
+ @logger = options[:logger]
15
+ @command_queue = CopyTunerClient::QueueWithTimeout.new
16
+ @mutex = Mutex.new
17
+ @thread = nil
18
+ @last_synced_at = nil
19
+ # スレッドの生死とは別に、ライフサイクルの意図を持つ。
20
+ # @running: ポーリングを継続する意図があるか(start で true / stop で false)
21
+ # @aborted: 張り直しても同じ理由で死ぬと分かっている終わり方をしたか
22
+ @running = false
23
+ @aborted = false
18
24
  end
19
25
 
20
26
  def start
21
27
  @mutex.synchronize do
22
- if @thread.nil?
23
- @logger.info 'start poller thread'
24
- @thread = Thread.new { poll } or logger.error("Couldn't start poller thread")
25
- end
28
+ @running = true
29
+ # 回復の見込みがない理由で終了しているなら張り直さない。fork のたびに同じ例外で
30
+ # 死ぬスレッドを作り直してログを埋めるだけになる
31
+ next if @aborted
32
+ # fork 後の子は親から dead な Thread オブジェクトを継承するため、nil かどうかだけでは
33
+ # 「動いていない」を判定できない。死んでいるスレッドは張り直す
34
+ next if @thread&.alive?
35
+
36
+ # コマンドキューは世代ごとに作り直し、スレッドに自分のキューを渡す。前の世代宛に
37
+ # 積まれたまま未消費で残った :stop を次の世代が 1 周目で拾って自殺するのを、
38
+ # 「1 つのキューは 1 本のスレッドだけのもの」という不変条件で構造的に防ぐ
39
+ queue = CopyTunerClient::QueueWithTimeout.new
40
+ @command_queue = queue
41
+ @logger.info 'start poller thread'
42
+ @thread = Thread.new { poll(queue) } or logger.error("Couldn't start poller thread")
26
43
  end
27
44
  end
28
45
 
46
+ # 戻り値は ForkHook が「fork 後に張り直すか」を決めるのに使う。スレッドの生死ではなく
47
+ # ポーリングを継続する意図があったかを返す。想定外の例外で死んだだけのスレッドは
48
+ # fork 後に張り直したいが、生死で判定すると張り直せなくなる
49
+ #
50
+ # @return [Boolean] ポーリング継続の意図があった(= fork 後に張り直すべき)なら +true+
29
51
  def stop
30
52
  @mutex.synchronize do
31
- @command_queue.uniq_push(:stop)
32
- @thread&.join
53
+ resumable = @running && !@aborted
54
+ @running = false
55
+
56
+ thread = @thread
33
57
  @thread = nil
58
+ # 例外で終わったスレッドは非 nil のまま dead で残る。それに :stop を積んでも誰も
59
+ # pop しないので、生きているときだけ積んで待つ
60
+ if thread&.alive?
61
+ @command_queue.uniq_push(:stop)
62
+ thread.join
63
+ end
64
+
65
+ resumable
34
66
  end
35
67
  end
36
68
 
@@ -46,20 +78,52 @@ module CopyTunerClient
46
78
 
47
79
  attr_reader :cache, :logger, :polling_delay
48
80
 
49
- def poll
50
- loop do
51
- cache.sync
52
- logger.flush if logger.respond_to?(:flush)
53
- begin
54
- command = @command_queue.pop_with_timeout(polling_delay)
55
- break if command == :stop
56
- rescue ThreadError
57
- # timeout
58
- end
81
+ def poll(queue)
82
+ timeout = remaining_delay
83
+ until wait_for_command(queue, timeout) == :stop
84
+ sync
85
+ timeout = polling_delay
59
86
  end
60
- @logger.info 'stop poller thread'
87
+ logger.info 'stop poller thread'
61
88
  rescue InvalidApiKey => e
89
+ # キーが不正なら張り直しても同じ結果になるので、以後の再開を止める
90
+ @aborted = true
62
91
  logger.error(e.message)
92
+ rescue StandardError => e
93
+ # 例外はスレッドの外へ漏らさない。stop の join は fork の直前にも呼ばれるため、
94
+ # 漏らすと poller の失敗がアプリ側の fork まで巻き添えにする。
95
+ # ここで握ると report_on_exception による stderr 出力も消えるので backtrace を残す
96
+ logger.error("poller thread aborted: #{e.class}: #{e.message}\n#{e.backtrace&.first(5)&.join("\n")}")
97
+ end
98
+
99
+ def sync
100
+ cache.sync
101
+ @last_synced_at = monotonic_now
102
+ logger.flush if logger.respond_to?(:flush)
103
+ end
104
+
105
+ # 最初の待ち時間を「前回 sync からの残り」にすることで、stop / start を繰り返しても
106
+ # sync の間隔を保つ。fork のたびに stop / start されるので、これが無いと worker を
107
+ # fork する数だけ親プロセスで sync(HTTP 往復)が走り直し、その分 join も待たされる。
108
+ # 基点には NTP や手動の時刻変更で巻き戻らない CLOCK_MONOTONIC を使う
109
+ # (Time.now だと時計が後ろに飛んだぶんだけ待ち時間が伸びて同期が止まる)
110
+ def remaining_delay
111
+ return 0 if @last_synced_at.nil?
112
+
113
+ remaining = polling_delay - (monotonic_now - @last_synced_at)
114
+ remaining.negative? ? 0 : remaining
115
+ end
116
+
117
+ # 自分の世代のキューを受け取る。@command_queue は start のたびに差し替わるため、
118
+ # 終了処理中の古いスレッドが新しい世代のキューを覗いてしまわないようにする
119
+ def wait_for_command(queue, timeout)
120
+ queue.pop_with_timeout(timeout)
121
+ rescue ThreadError
122
+ nil # timeout
123
+ end
124
+
125
+ def monotonic_now
126
+ Process.clock_gettime(Process::CLOCK_MONOTONIC)
63
127
  end
64
128
  end
65
129
  end
@@ -1,3 +1,5 @@
1
+ require 'copy_tuner_client/fork_hook'
2
+
1
3
  module CopyTunerClient
2
4
  # Starts the poller from a worker process, or register hooks for a spawner
3
5
  # process (such as in Unicorn or Passenger). Also registers hooks for exiting
@@ -14,6 +16,9 @@ module CopyTunerClient
14
16
 
15
17
  # Starts the poller or registers hooks
16
18
  def start
19
+ # fork の前後で poller を張り直すフックは、どのプロセスでも必ず登録する
20
+ ForkHook.install
21
+
17
22
  if spawner?
18
23
  register_spawn_hooks
19
24
  else
@@ -115,9 +120,12 @@ module CopyTunerClient
115
120
  ::Process.singleton_class.prepend(hook_module)
116
121
  end
117
122
 
123
+ # 起動方法(rails server / puma -C)とモード(single / cluster / preload の有無)ごとに
124
+ # どのプロセスで poller が起動するかは docs/poller-startup.md にまとめてある
118
125
  def register_puma_hook
119
- # If Puma is clustered mode without preload_app, this method is called on worker process.
120
- # Just start poller and return.
126
+ # preload cluster worker はここに来る。この構成ではアプリのロードが
127
+ # Puma::Runner#start_server の中で起きるため、今から prepend しても実行中の呼び出しには
128
+ # 間に合わない。つまりこの分岐が poller を起動する唯一の経路で、最適化ではなく必須
121
129
  if $PROGRAM_NAME.include?('cluster worker')
122
130
  @logger.info('Puma would be clustered mode without preload_app')
123
131
  @poller.start
@@ -125,8 +133,9 @@ module CopyTunerClient
125
133
  end
126
134
 
127
135
  @logger.info('Register Puma fork hook')
128
- # If Puma is clustered mode with preload_app, this method is called before fork.
129
- # Delay poller start until Puma::Runner#start_server which is called on worker process.
136
+ # preload ありの cluster ではこのメソッドが fork 前の master で走る。master
137
+ # リクエストを捌かないのでここでは起動せず、worker 側で呼ばれる
138
+ # Puma::Runner#start_server まで遅らせる
130
139
  poller = @poller
131
140
  hook_module =
132
141
  Module.new do
@@ -51,10 +51,16 @@ module CopyTunerClient
51
51
 
52
52
  def wait_with_timeout(timeout)
53
53
  # wait for element or timeout
54
- timeout_time = timeout + Time.now.to_f
55
- while @queue.empty? && (remaining_time = timeout_time - Time.now.to_f).positive?
54
+ # NTP step 補正や手動の時刻変更で巻き戻らない CLOCK_MONOTONIC で締め切りを測る。
55
+ # ウォールクロックだと時計が後ろへ飛んだぶんだけ待ち時間が伸びる
56
+ deadline = monotonic_now + timeout
57
+ while @queue.empty? && (remaining_time = deadline - monotonic_now).positive?
56
58
  @received.wait(@mutex, remaining_time)
57
59
  end
58
60
  end
61
+
62
+ def monotonic_now
63
+ Process.clock_gettime(Process::CLOCK_MONOTONIC)
64
+ end
59
65
  end
60
66
  end
@@ -1,6 +1,6 @@
1
1
  module CopyTunerClient
2
2
  # Client version
3
- VERSION = '2.2.0'.freeze
3
+ VERSION = '2.3.0'.freeze
4
4
 
5
5
  # API version being used to communicate with the server
6
6
  API_VERSION = '2.0'.freeze
@@ -64,9 +64,10 @@ bin/rails runner '
64
64
  > NOTE: ここで `export` を実行できるのは gem がまだ入っているから。完了判定は gem 撤去より**前**に行う。
65
65
 
66
66
  > 補助目印: migrate は prefix を移すたびにオリジナル(`0000_original_*.yml`)から該当サブツリーを削除するので、
67
- > 全移行完了時点で `0000_original_*.yml` はほぼ空(残るのは非表現値の隔離 `0005_rails_non_blurb.yml` 等のみ)に
68
- > なっているはず。`local_first_key?` のマッチ判定が主の関門で、ファイルが空かどうかは副次的な目視確認。
69
- > 食い違うとき(regexp は全マッチなのにオリジナルに blurb 化できるキーが残っている等)は削除漏れを疑う。
67
+ > **全移行を経た場合**は `0000_original_*.yml` がほぼ空になっているはず(非表現値も `migrate_prefix.rb`
68
+ > 移行分側の `--out` へ再適用済みで、オリジナル側に隔離ファイルとして残ることはない)。`local_first_key?`
69
+ > のマッチ判定が主の関門で、ファイルが空かどうかは副次的な目視確認。食い違うとき(regexp は全マッチなのに
70
+ > オリジナルに blurb 化できるキーが残っている等)は削除漏れを疑う。
70
71
 
71
72
  ### 2. 最終不正キーチェック
72
73
 
@@ -129,8 +130,7 @@ copyray コメント注入も消える。あわせて `config/environments/*.rb`
129
130
 
130
131
  - copy_tuner 専用の deploy ワークフローファイル(main push で翻訳をデプロイする専用ファイル)… 丸ごと削除。
131
132
  - AI エージェント用ワークフローの `mcp__copy-tuner__*` allowedTools 許可 … 削除。
132
- - CI の「翻訳を export するステップ」… migrate-prefix の初回で削除済みのはず。**まだ残っていれば**ここで削除する
133
- (`git grep copy_tuner .github/` で確認)。
133
+ - CI の「翻訳を export するステップ」… ここで削除する(`git grep copy_tuner .github/` で確認)。
134
134
 
135
135
  ### 6. deploy / 起動スクリプトを撤去
136
136
 
@@ -2,24 +2,33 @@
2
2
  name: copy-tuner-to-locales-migrate-prefix
3
3
  description: >-
4
4
  copy_tuner(CopyTuner / copy_tuner_client)で集中管理している i18n データを、prefix(正規表現)単位で
5
- Rails 標準の config/locales(YAML)管理へ段階移行するスキル。gem の local_first_key_regexp を使い、
6
- 1 回につき 1 prefix をローカルへ寄せて regexp に積み上げる。全 prefix 完了後の gem 撤去は
7
- copy-tuner-to-locales-cleanup スキルで行う。
5
+ Rails 標準の config/locales(YAML)管理へ移すスキル。gem の local_first_key_regexp を使うので、
6
+ gem を残したまま特定 prefix だけをローカル管理にできる(部分ローカル化)。1 回の実行で 1 prefix
7
+ 全 prefix を移して gem ごと撤去したい場合は繰り返し、完了後 copy-tuner-to-locales-cleanup スキルへ進む。
8
+ 対象 prefix はスキル引数で指定でき、未指定なら export を俯瞰して選定する。
8
9
  disable-model-invocation: true
9
10
  ---
10
11
 
11
- # copy_tuner → config/locales 段階移行スキル(prefix 単位)
12
+ # copy_tuner → config/locales prefix 単位ローカル化スキル
12
13
 
13
- copy_tuner(`copy_tuner_client` gem)で集中管理している i18n データを、**prefix(正規表現)単位で**少しずつ
14
- Rails 標準の `config/locales` 配下の YAML 管理へ移していくためのワークフロー。**1 回の実行で 1 prefix だけ**
15
- 移行し、これを繰り返す。全 prefix の移行が完了したら `copy-tuner-to-locales-cleanup` スキルで gem・CI・
16
- deploy・docs・MCP をまとめて撤去する。
14
+ copy_tuner(`copy_tuner_client` gem)で集中管理している i18n データを、**prefix(正規表現)単位で**
15
+ Rails 標準の `config/locales` 配下の YAML 管理へ移すためのワークフロー。**1 回の実行で 1 prefix だけ**扱う。
16
+
17
+ 使い方は 2 つある:
18
+
19
+ - **部分ローカル化** — 特定 prefix だけを恒久的に `config/locales` 管理にする。**gem は残したまま**で、
20
+ CopyTuner 管理と locales 管理の二層が**定常状態**になる。
21
+ - **全移行** — 上記を全 prefix ぶん繰り返す。全 prefix の移行が完了したら
22
+ `copy-tuner-to-locales-cleanup` スキルで gem・CI・deploy・docs・MCP をまとめて撤去する。
23
+
24
+ **どちらの用途でもこのスキルがやること(手順 0〜10)は同一**で、差は「何回回すか」と「最後に cleanup へ
25
+ 進むか」だけ。
17
26
 
18
27
  このスキルは**特定のリポジトリに依存しない**。project_id・ファイルパス・CI 構成はプロジェクトごとに異なるので、
19
28
  固有値を覚えるのではなく**毎サイクル、自分が編集する箇所(initializer の regexp・config/locales)を探索して
20
29
  見つけ直す**(手順 2)。種別ごとの典型例は `references/example-touchpoints.md` を参照。
21
30
 
22
- ## なぜ prefix 単位で段階移行するのか
31
+ ## なぜ prefix 単位で切るのか
23
32
 
24
33
  一発で全 i18n をローカル化すると、移行漏れ(ローカル YAML に書き忘れたキー)が**一斉に未訳化**して事故になる。
25
34
  prefix 単位なら、移した範囲だけが影響を受け、移行漏れはその範囲の未訳として小さく顕在化する。安全な prefix から
@@ -38,11 +47,21 @@ prefix 単位なら、移した範囲だけが影響を受け、移行漏れは
38
47
  - マッチしないキーは従来どおり CopyTuner キャッシュ優先 → 無ければローカル、という動作のまま。
39
48
  - regexp は**単一**(配列非対応)。複数 prefix は `Regexp.union` で 1 本に積み上げる。
40
49
 
41
- gem を残したまま regexp に prefix を足していくだけなので、移行途中でも CopyTuner と config/locales が安全に
42
- 共存する。
50
+ gem を残したまま regexp に prefix を足していくだけなので、CopyTuner と config/locales は安全に共存する。
51
+ 部分ローカル化ならこの共存が定常状態、全移行なら移行途中の状態としてそのまま成り立つ。
43
52
 
44
53
  ## ワークフロー(1 サイクル = 1 prefix)
45
54
 
55
+ ### 0. 対象 prefix の受け取り(引数)
56
+
57
+ スキル引数で対象 prefix を渡せる(例: `devise` / `activerecord.attributes` / `views.users`)。
58
+
59
+ - **引数あり** … それを今回の対象とし、**手順 4 の選定はスキップ**する。ただし**手順 3 の全件 export は
60
+ 実行する**(手順 6 の `--export` 入力として必須なので省けない)。
61
+ - 引数の prefix が export に**存在しなければ**、その旨をユーザーに報告して中断する(手順 6 のスクリプトも
62
+ 同じ条件で異常終了するが、手順 3 の時点で気づけるほうが早い)。
63
+ - **引数なし** … 従来どおり手順 4 の基準で 1 つ選び、**選定結果をユーザーに提示してから**手順 5 へ進む。
64
+
46
65
  ### 1. gem 前提確認
47
66
 
48
67
  `local_first_key_regexp` が使えるバージョンの `copy_tuner_client` が入っているか確認する。
@@ -65,14 +84,13 @@ grep 結果はセッションをまたいで残らない(複数セッション
65
84
  git grep -nI 'local_first_key_regexp' -- ':!vendor' ':!tmp' ':!node_modules'
66
85
  ls config/locales
67
86
 
68
- # 初回だけ触る(手順 9・10 用): CI の export ステップと i18n 方針ドキュメント
69
- git grep -nI -e 'copy_tuner:export' -e 'CopyTuner' -- '.github/' 'doc/' 'CLAUDE.md'
87
+ # 手順 9 用: i18n 方針ドキュメント
88
+ git grep -nI 'CopyTuner' -- 'doc/' 'CLAUDE.md'
70
89
  ```
71
90
 
72
91
  - 手順 6・7 で毎回触る **initializer の `local_first_key_regexp`** の位置と、**`config/locales/`** の採番慣習
73
92
  (例: `00_`・`10_`)を確認する。
74
- - 手順 9(CI の export ステップ削除)・手順 10(方針ドキュメントの中間状態更新)で**初回だけ**触る箇所も
75
- ここで場所だけ押さえる。
93
+ - 手順 9(i18n 方針ドキュメントの更新)で触る箇所も、ここで場所だけ押さえる。
76
94
 
77
95
  #### 2-1. (初回のみ)既存 locales を `0000_original_` プレフィックスへリネーム
78
96
 
@@ -101,8 +119,12 @@ cleanup は自前で touchpoint を grep し直す。**このスキルでそれ
101
119
 
102
120
  ### 3. 残 prefix の把握
103
121
 
104
- 全件を export して俯瞰し、移行済み(現在の `local_first_key_regexp` がマッチする)prefix と未移行 prefix を
105
- 一覧化する。export は一時ファイルへ書く(`tmp/` 等の捨て場)。
122
+ 全件を export して俯瞰し、ローカル化済み(現在の `local_first_key_regexp` がマッチする)prefix
123
+ copy_tuner 管理のまま残っている prefix を一覧化する。export は一時ファイルへ書く(`tmp/` 等の捨て場)。
124
+
125
+ 対象 prefix が引数で指定されている場合、この一覧化は**対象 prefix が export に存在することの確認と規模把握**の
126
+ ためになる(選定は不要)。いずれの場合も `rake copy_tuner:export` の**実行自体は必須**で、出力は手順 6 の
127
+ `--export` 入力になる。
106
128
 
107
129
  ```bash
108
130
  bundle exec rake copy_tuner:export[tmp/copy_tuner_all.yml]
@@ -114,7 +136,10 @@ bundle exec rake copy_tuner:export[tmp/copy_tuner_all.yml]
114
136
 
115
137
  ### 4. 対象 prefix の選定
116
138
 
117
- prefix から **1 つ**選ぶ。影響が小さく構造が安定したものから始め、最後に大物(`views`)を回す:
139
+ **引数で prefix が指定されている場合はこの手順をスキップ**し、手順 5 へ進む。
140
+
141
+ 残 prefix から **1 つ**選ぶ。以下の順序は**安全度の目安**で、影響が小さく構造が安定したものから始め、最後に
142
+ 大物(`views`)を回す:
118
143
 
119
144
  1. **gem 由来(最安全・先行)**: `devise` / `good_job` / `ice_cube` / `restrict_dependent_destroy` 等。
120
145
  値が安定しアプリ実装に依存しにくい。
@@ -123,8 +148,8 @@ bundle exec rake copy_tuner:export[tmp/copy_tuner_all.yml]
123
148
  3. **バリデーションメッセージ**: `activerecord.errors` / `activemodel.errors`。テストで検知しやすい。
124
149
  4. **モデル名・カラム名**: `activerecord.models` / `activerecord.attributes` / `activemodel.attributes` /
125
150
  `activerecord.enums`。プロジェクトの i18n 方針で「新規キー登録の例外」とされていることが多い
126
- (プロジェクトの i18n 方針ドキュメントでそう規定されていることが多い)。**全撤去がゴールなのでこの prefix も最終的に移行対象に含める**。
127
- 例外規定の撤廃は cleanup で行う。
151
+ (プロジェクトの i18n 方針ドキュメントでそう規定されていることが多い)。**全 prefix を移行する場合は
152
+ この prefix も対象に含める**(例外規定の撤廃は cleanup で行う)。
128
153
  5. **画面テキスト(最大・最後)**: `views` / `text`。量が多く画面影響が大きいので最後に回し、画面確認の比重を
129
154
  上げる。1 回が大きすぎるなら `views.<controller>.` の 2 階層目で更に刻んでよい(regexp を `\Aviews\.users\.`
130
155
  のように書ける)。
@@ -197,6 +222,15 @@ prefix 内で `date.formats`(文字列・export 勝ち)と `date.order`(
197
222
  > 束ねている。スクリプトが中断した場合は `--out` のファイルだけが残る(オリジナルは無傷)ので、原因を直して
198
223
  > 再実行するか `--out` を消してやり直す。
199
224
 
225
+ > NOTE: スクリプトは「対象 prefix の削除で実際に内容が変わったファイルだけ」を書き戻す(変更が無ければ
226
+ > `File.write` をスキップする)。とはいえ `--originals-glob` の指定ミス等で意図しないファイルが対象に
227
+ > 入っていないとも限らないため、実行後は必ず `git diff --stat config/locales/` で「対象 prefix を含む
228
+ > はずのファイルだけに差分が出ているか」を確認する。対象 prefix と無関係なはずのファイルに差分が出て
229
+ > いたら `git checkout -- <file>` で復元し、原因(`--originals-glob` や `--prefix` の指定)を見直す。
230
+
231
+ > NOTE: 書き戻されたファイルでは**コメント・空行が失われる**(YAML 標準ライブラリはこれらを保持しない。
232
+ > 値・エイリアス参照は保たれる)。`git diff` を見て惜しいコメントがあれば手で戻すこと。
233
+
200
234
  ### 7. local_first_key_regexp に prefix を追加
201
235
 
202
236
  initializer(`config/initializers/copy_tuner.rb` 等)の `CopyTunerClient.configure` ブロックで、
@@ -233,33 +267,31 @@ config.local_first_key_regexp = Regexp.union(
233
267
  > `bin/rails runner 'p CopyTunerClient.configuration.local_first_key?("<prefix>.foo")'` が `true`、隣接キー
234
268
  > (`reviews.*` 等)が `false` になることを確認しておくとよい。
235
269
 
236
- ### 9. (初回のみ)CI の Export ステップを削除
270
+ ### 9. i18n 方針ドキュメントを更新
237
271
 
238
- CI copy_tuner を export しているステップ(テストワークフロー内で `bin/rake copy_tuner:export` を走らせる類)
239
- は、**テスト起動前にローカルキャッシュを温める保険**にすぎない。
240
- このステップを削除すると、test 環境は initializer 起動時の `cache.download`(CopyTuner サーバから都度取得)
241
- だけになり、**本番と同じ挙動**になる。未移行 prefix も引き続きサーバから解決できるので、移行途中に消しても安全。
272
+ i18n 方針ドキュメント(`doc/` 等)が「copy_tuner で管理/config/locales は使わない」のまま残ると、
273
+ 他の作業者や AI が「新規キーを copy_tuner と locales のどちらに足すか」を誤判断する。用途に応じて書き分ける
274
+ (テンプレ文は `references/example-touchpoints.md` にある):
242
275
 
243
- > WARNING: `config.disable_test_translation = true` は**入れない**こと。入れると test で CopyTuner DL が
244
- > 止まり、未移行 prefix が一斉に未訳化する。Export ステップ(保険)だけを消すのが正しい。
276
+ - **部分ローカル化** 「移行中」ではなく**恒久的な二層管理**として書く。「以下の prefix config/locales
277
+ 管理」「それ以外は copy_tuner 管理」「新規キーの追加先はどちら」の 3 点を明記する。
278
+ - **全移行** … 「copy_tuner から config/locales へ段階移行中」+現在ローカル化済みの prefix を列挙する
279
+ (最終形への書き換えは cleanup で行う)。
245
280
 
246
- ### 10. ドキュメントの中間状態を更新
281
+ いずれも prefix を増やすたびに列挙を更新する。
247
282
 
248
- i18n 方針ドキュメント(`doc/` 等)が「copy_tuner で管理/config/locales は使わない」のまま残ると、移行途中で
249
- 他の作業者や AI が「新規キーを copy_tuner と locales のどちらに足すか」を誤判断する。**移行中であることと、
250
- 現在ローカル化済みの prefix を明記する**。中間状態テンプレ文は `references/example-touchpoints.md` にある。
251
- prefix を増やすたびに、列挙も更新する。
283
+ ### 10. 結果を報告
252
284
 
253
- ### 11. prefix を報告
285
+ 今回ローカル化した prefix と、現在の `local_first_key_regexp` を報告して 1 サイクル終了。加えて:
254
286
 
255
- 移行済み prefix・残 prefix の一覧と、現在の `local_first_key_regexp` を報告して 1 サイクル終了。
256
- prefix があれば次サイクルでこのスキルを再実行する。全 prefix が移行済みになったら
257
- `copy-tuner-to-locales-cleanup` スキルへ進む。
287
+ - **部分ローカル化** これで完了。残りの prefix copy_tuner 管理のままが定常状態なので、次サイクルは不要。
288
+ - **全移行** copy_tuner 管理のまま残っている prefix の一覧も報告する。残りがあれば次サイクルでこのスキルを
289
+ 再実行する。全 prefix がローカル化済みになったら `copy-tuner-to-locales-cleanup` スキルへ進む。
258
290
 
259
291
  ## 1 サイクル完了の目安
260
292
 
261
293
  - 手順 6 のスクリプトが**移行漏れゼロを確認して正常終了**し、`--out`(`0010_` 以降)に対象 prefix が配置され、
262
294
  `0000_original_*.yml` から該当サブツリーが削除されている(非表現値は `--out` 側に保持済み)。
263
295
  - `local_first_key_regexp` に対象 prefix が `\A` アンカー付きで追加されている(手順 7)。
264
- - 中間状態ドキュメントの「ローカル化済み prefix」が更新されている(手順 10)。
296
+ - i18n 方針ドキュメントの「config/locales 管理の prefix」が更新されている(手順 9)。
265
297
  - (任意)rspec/画面で `translation missing` が出ないことをユーザー判断で確認(手順 8)。
@@ -61,7 +61,8 @@ end
61
61
 
62
62
  → オリジナルを `0000_original_` で先頭固定し、移行分は `0010_` 以降に置く。重複キーはロード順の**後勝ちで
63
63
  export 側が勝つ**(手作業マージ不要)。prefix を移行するたびにオリジナルから該当サブツリーを削除し、残存=
64
- 未移行 prefix の進捗マーカーにする(最終的にオリジナルが空=全移行完了)。
64
+ 未移行 prefix の進捗マーカーにする(**全 prefix を移行する場合は最終的にオリジナルが空になる**)。
65
+ 部分ローカル化では、対象外 prefix がオリジナルに残り続けるのが**定常状態**であり、空にならなくてよい。
65
66
 
66
67
  → Rails 標準フォーマットの **配列**(`date.abbr_day_names` 等)・`date.order` の `:year` 等の**シンボル配列**・
67
68
  `number.*.precision` 等の**非表現値**は copy_tuner で表現できず export に出てこないため、`date`/`number` を移行
@@ -69,10 +70,10 @@ export 側が勝つ**(手作業マージ不要)。prefix を移行するた
69
70
  移行分(`0010_` 以降)の中へ**非表現値ごと取り込む**。別ファイルへの隔離は不要(詳細は
70
71
  `references/export-and-split.md`)。
71
72
 
72
- ### CI — [migrate](Export ステップのみ初回で削除) / [cleanup](残り)
73
+ ### CI — [cleanup]
73
74
 
74
75
  - **CI の翻訳 export ステップ**(テストワークフロー内で `bin/rake copy_tuner:export` を走らせる類)…
75
- 「翻訳 DL 失敗でテストがコケないように」の保険。**[migrate] の初回で削除**(test が本番同等の
76
+ 「翻訳 DL 失敗でテストがコケないように」の保険。**[cleanup] で削除**(test が本番同等の
76
77
  `cache.download` 挙動になる)。`disable_test_translation` は入れない。
77
78
  - **copy_tuner 専用の deploy ワークフロー**(main push で翻訳をデプロイする専用ファイル)…
78
79
  **[cleanup] で丸ごと削除**。
@@ -88,10 +89,11 @@ export 側が勝つ**(手作業マージ不要)。prefix を移行するた
88
89
 
89
90
  `config/environments/production.rb` の `config.i18n.fallbacks = true` 等。標準バックエンドでも有効なので確認のみ。
90
91
 
91
- ### ドキュメント / スキル / MCP — [migrate](中間状態更新) / [cleanup](最終化・撤去)
92
+ ### ドキュメント / スキル / MCP — [migrate](用途別に更新) / [cleanup](最終化・撤去)
92
93
 
93
94
  - **i18n 方針ドキュメント**(`CLAUDE.md`・`doc/` 配下等)… 「copy_tuner サーバで i18n データを管理 /
94
- config/locales 配下は利用しない / 新規キー登録は基本禁止」等の記述。**[migrate] で中間状態に更新**、
95
+ config/locales 配下は利用しない / 新規キー登録は基本禁止」等の記述。**[migrate] で更新**(部分ローカル化なら
96
+ 恒久的な二層管理として、全移行なら段階移行中の中間状態として。テンプレは後掲)。全移行の場合のみ
95
97
  **[cleanup] で最終化**。上記を参照している他のドキュメント(`CLAUDE.md` 等)も連動。
96
98
  - **copy_tuner MCP 操作スキル**(`.claude/skills/` 配下)… **[cleanup] で無効化/削除**。
97
99
  - **補助ドキュメント** … 「多言語対応: copy_tuner サーバで i18n データを管理」のような記述を持つコマンド定義等。
@@ -107,10 +109,28 @@ copy_tuner 側のキー数・export YAML の行数はプロジェクト次第だ
107
109
  `restrict_dependent_destroy` 等。これらのトップセクションが prefix 移行の基本粒度。`views` が最大になりやすいので
108
110
  最後に回す。
109
111
 
110
- ## i18n 方針ドキュメント中間状態テンプレ([migrate] 手順 10 で使う)
112
+ ## i18n 方針ドキュメントテンプレ([migrate] 手順 9 で使う)
111
113
 
112
- 移行中はこのような記述に置き換える。`<列挙>` は現在 `local_first_key_regexp` にマッチしている prefix
113
- 更新する(prefix を増やすたびに更新)。
114
+ 用途に応じて 2 版ある。`<列挙>` は現在 `local_first_key_regexp` にマッチしている prefix に更新する
115
+ prefix を増やすたびに更新)。
116
+
117
+ ### 部分ローカル化版
118
+
119
+ 「移行中」ではなく、この状態が**恒久的に続く二層管理**であることを明記する。
120
+
121
+ ```markdown
122
+ ### 国際化(i18n)
123
+
124
+ - **一部の prefix は config/locales(YAML)管理、それ以外は copy_tuner サーバ管理**
125
+ - config/locales 管理の prefix(`local_first_key_regexp` にマッチ): `<列挙>`
126
+ - 上記以外の prefix は copy_tuner サーバで管理
127
+ - **新規キーの追加先**: 上記 prefix のキーは config/locales へ。それ以外は copy_tuner へ
128
+ - 複数形化対応は不要(日本語環境)
129
+ ```
130
+
131
+ ### 全移行版
132
+
133
+ 「段階移行中」であることと、いずれ cleanup で最終化される中間状態であることを明記する。
114
134
 
115
135
  ```markdown
116
136
  ### 国際化(i18n)
@@ -122,5 +142,6 @@ copy_tuner 側のキー数・export YAML の行数はプロジェクト次第だ
122
142
  - 複数形化対応は不要(日本語環境)
123
143
  ```
124
144
 
125
- > [cleanup] で全 prefix 完了後、この中間記述は「config/locales 管理。copy_tuner 廃止。新規キーは
126
- > config/locales へ。複数形化不要」に最終化する(モデル名・カラム名の例外規定も撤廃)。
145
+ > 上記**全移行版にのみ係る注記**: [cleanup] で全 prefix 完了後、この中間記述は「config/locales 管理。
146
+ > copy_tuner 廃止。新規キーは config/locales へ。複数形化不要」に最終化する(モデル名・カラム名の例外規定も
147
+ > 撤廃)。部分ローカル化版は cleanup を経由しないため、この最終化は適用されない。