copy_tuner_client 2.2.0 → 2.2.1

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3fb9ea1caf782db8dac7ded7d77803a61586b5369b221b62d34e9e0290c58b06
4
- data.tar.gz: ad4723a7fe5af378f68af6f786644cc549a1ebbe2523c3424af8fdaba5381433
3
+ metadata.gz: 6a9a2fe07a403679fa3bf6d10c72c13fadcd31536af653a79b3e6d84e80c10ab
4
+ data.tar.gz: b3300594e38799ec7004f4c4fb0e35b051a447c6dd8b66ffb512bb59d7b851b2
5
5
  SHA512:
6
- metadata.gz: 7a894e4afd8428064c4fef7dd34db7bdfaab23879b5fa3aaabc5921437a3a80b7cf036fac920f8bf76e7274141f5a522f8364a493797084e7fee879f435d7eb4
7
- data.tar.gz: 34211866166e6491e7488a8f87aadc96810afcea2f3b5069ae2b29b4b9e5f0fe9a620556e3fd55d403ed419c9ab917c34f4fee0eafea90e889a7b6d5653b0540
6
+ metadata.gz: 5af9d06699d0361e360ea31dd4bb67ba04d2e711e0324ee9d85ee59914404ef905e173e7b6fd6ef81ede67773784fd4788ccb45fac7399ce57ad1b67d6d094ce
7
+ data.tar.gz: 2143f94b230219cae83f8d317f52ba338b282894178edce8129eac5382be76836209fbb29b8dd9e25ac96d3a91a6b93fa326fed4c426f9b2304b9b12d9233482
data/CHANGELOG.md CHANGED
@@ -1,235 +1,2 @@
1
- ## Unreleased
2
-
3
- - **【後方互換性に影響】** `config.exclude_key_regexp` を削除しました。後継の `config.local_first_key_regexp`
4
- を使ってください。両者は対象キーの形式が異なります(`exclude_key_regexp` は locale 付き `ja.views.foo`、
5
- `local_first_key_regexp` は locale を除いた `views.foo`)。正規表現から locale プレフィックスを外して
6
- 移行してください。挙動も異なり、`local_first_key_regexp` は lookup 時に CopyTuner キャッシュをスキップして
7
- ローカル YAML を優先します(完全分離)。
8
- - **【後方互換性に影響】** `config.project_id` を必須にしました。未設定のまま `configure`(`apply`)すると
9
- `ArgumentError: project_id is required` で失敗します。これまで `project_id` 未設定時は `api_key` へ
10
- フォールバックして deprecation 警告を出していましたが、このフォールバックは削除しました。initializer に
11
- `config.project_id = <プロジェクト ID>` を設定してください。
12
- - Copyray オーバーレイのマーカー方式を刷新。訳文への HTML コメント `<!--COPYRAY key-->` 注入をやめ、
13
- 可視トークン `⟦CT:key⟧` を埋め込んだうえで `CopyrayMiddleware` が `data-copyray-key` 属性に変換し、
14
- トークンを HTML から完全に除去するようになりました。最終配信 HTML にコメント・トークンは残りません。
15
- - **【後方互換性に影響】** `config.html_escape` 設定を削除しました。HTML の安全性判定は i18n 標準
16
- (`.html` / `_html` で終わるキーのみ html_safe)に統一され、この設定は参照されなくなっていました(no-op)。
17
- no-op だったため動作には影響しませんが、`config.html_escape = ...` を設定している initializer は
18
- `NoMethodError` になるため、その行を削除してください。`html_escape = false`(全訳文を html_safe 扱いに
19
- する旧互換挙動)に依存していたアプリは、`.html` / `_html` キー命名へ移行してください。
20
- - Copyray オーバーレイは平文・html_safe(`.html` / `_html` キー)どちらの訳文もハイライト対象です。マーカートークンの
21
- 区切り記号は HTML 特殊文字ではないため、平文訳文が ActionView でエスケープされてもトークンは無傷で残り、
22
- `data-copyray-key` 属性へ正しく変換されます。`<head>` 内(title/meta)はトークンを除去するのみでオーバーレイ
23
- 非対象ですが、従来どおりリスト導線(CopyTuner バー)から編集できます。
24
- - **【後方互換性に影響】** `tt` ヘルパーを削除しました。マーカー方式の刷新で `t`(`translate`)が安全にマーカー
25
- 注入できるようになり、`tt` の存在理由は失われました。`tt` の呼び出しは `t` へ置き換えてください。`tt` を使い続けたい
26
- アプリは、ビューヘルパーで `t` に委譲するだけの `tt` を自前で定義してください(gem 撤去後は `t` と実質同義)。
27
-
28
- ## 0.16.1
29
-
30
- - Support for i18n@1.13.0
31
- - キーの相対パス指定とdefaultオプションを組み合わせた場合の不具合修正
32
-
33
- ## 0.16.0
34
-
35
- - Railsエンジン内のviewではオリジナルのtヘルパが呼ばれるように修正
36
-
37
- ## 0.15.1
38
-
39
- - tヘルパーにdefault引数が渡された場合に初期値として登録されない問題を修正
40
-
41
- ## 0.15.0
42
-
43
- - Drop support for ruby 2.7
44
-
45
- ## 0.14.1
46
-
47
- - Fix super call in define_method
48
-
49
- ## 0.14.0
50
-
51
- - Add Support for good_job
52
- - Drop Support for Resque
53
-
54
- ## 0.13.5
55
-
56
- - Rename assets
57
-
58
- ## 0.13.4
59
-
60
- - Fix csp nonce
61
-
62
- ## 0.13.3
63
-
64
- - Add `media="all"` attribute to stylesheet link tag
65
-
66
- ## 0.13.2
67
-
68
- - Add `crossorigin="anonymous"` attribute to script tag
69
-
70
- ## 0.13.1
71
-
72
- - Add `type="module"` attribute to script tag
73
-
74
- ## 0.13.0
75
-
76
- - Drop support for ruby 2.6
77
-
78
- ## 0.12.0
79
-
80
- - Add `config.ignored_keys` and `config.ignored_key_handler`
81
-
82
- ## 0.11.0
83
-
84
- - Remove deprecated rescue_format option
85
- - Fix ruby@2.7 keyword warning
86
-
87
- ## 0.10.0
88
-
89
- - Add copy_tuner:detect_html_incompatible_keys task
90
-
91
- ## 0.9.0
92
-
93
- - Do not upload invalid type keys
94
-
95
- ## 0.8.1
96
-
97
- - Fix bug in `CopyrayMiddleware`
98
-
99
- ## 0.8.0
100
-
101
- - Change the default value of config.upload_disabled_environments
102
-
103
- ## 0.7.0
104
-
105
- - Add config.upload_disabled_environments
106
-
107
- ## 0.6.2
108
-
109
- - Add arguments to export task
110
-
111
- ## 0.6.1
112
-
113
- - Fix ruby@2.7 keyword warning
114
-
115
- ## 0.6.0
116
-
117
- - Drop support for ruby 2.4
118
- - Drop support for rails 5.1
119
-
120
- ## 0.5.2
121
-
122
- - Do not upload invalid keys
123
-
124
- ## 0.5.1
125
-
126
- - Do not upload downloaded keys
127
-
128
- ## 0.5.0
129
-
130
- - Drop support for ruby 2.3
131
- - Add tt helper
132
- - Add copy_tuner:detect_conflict_keys task
133
- - Do not re-upload empty keys
134
- - Fix dual loading tasks
135
- - Remove config.copyray_js_injection_regexp_for_debug
136
- - Remove config.copyray_js_injection_regexp_for_precompiled
137
- - Download translation when initialization
138
-
139
- ## 0.4.11
140
-
141
- - changes
142
- - Fix hide toggle button on mobile device.
143
-
144
- ## 0.4.10
145
-
146
- - changes
147
- - Hide copyray bar on all media.
148
-
149
- ## 0.4.9
150
-
151
- - changes
152
- - Smaller toggle button.
153
- - Hide toggle button on mobile device.
154
-
155
- ## 0.4.8
156
-
157
- - changes
158
- - Support passenger 5.3.x
159
-
160
- ## 0.4.7
161
-
162
- - changes
163
- - Compatibile with bullet gem (rewrap response with ActionDispatch::Response::RackBody)
164
-
165
- ## 0.4.6
166
-
167
- - changes
168
- - Performance imporovement (sync with server asynchronously)
169
- - Add config.middleware_position
170
-
171
- ## 0.4.5
172
-
173
- - changes
174
- - Fix deprecated css.
175
-
176
- ## 0.4.4
177
-
178
- - bug fix
179
- - Don't upload resolved default values.
180
-
181
- ## 0.4.3
182
-
183
- - bug fix
184
- - Start poller thread regardless of puma mode. #39
185
-
186
- ## 0.4.2
187
-
188
- - changes
189
- - span tag is no longer added to translation text.
190
-
191
- ## 0.4.1
192
-
193
- - bug fixes
194
-
195
- - js injection failed if jquery is not used. #33
196
- - Fix some js error. #34
197
- - Wrong key is displayed if scoped option is used. #35
198
-
199
- - deprecation
200
- - config.copyray_js_injection_regexp_for_debug is no longer needed.
201
- - config.copyray_js_injection_regexp_for_precompiled is no longer needed.
202
-
203
- ## 0.4.0
204
-
205
- - Remove jQuery dependency.
206
-
207
- ## 0.3.5
208
-
209
- - Support Rails 5.1
210
-
211
- ## 0.3.4
212
-
213
- - Use Logger to /dev/null as default when rails console
214
-
215
- ## 0.3.3
216
-
217
- - Add config.locales. (#24)
218
- - Fix initialization order bug. (#25)
219
-
220
- ## 0.3.2
221
-
222
- - Support I18n.t :scope option.
223
- - Update copyray_js_injection_regexp_for_debug.
224
-
225
- ## 0.3.1
226
-
227
- - Add search box to copyray bar.
228
- - Add disable_copyray_comment_injection to configuration.
229
-
230
- ## 0.3.0
231
-
232
- - Use https as default.
233
- - Download blurbs from S3.
234
- - Add toolbar.
235
- - "Translations in this page" menu.
1
+ > [!NOTE]
2
+ > 変更履歴は [GitHub Releases](https://github.com/SonicGarden/copy-tuner-ruby-client/releases) に移行済みです。
data/README.md CHANGED
@@ -62,20 +62,57 @@ CopyTuner で一元管理している翻訳を、`views.*` のような単位で
62
62
 
63
63
  アプリ独自の `number.*` キー(例 `number.gift_amount`)は対象外で、従来どおり CopyTuner で管理できます。
64
64
 
65
- ## Claude Code スキル
65
+ ## Middleware の挿入位置
66
66
 
67
- `skills/copy-tuner/` Claude Code 向けのスキルが含まれています。
67
+ CopyTuner は開発環境で `RequestSync` / `CopyrayMiddleware` Rack の middleware スタックに挿入します。`RequestSync` はリクエスト毎に CopyTuner サーバと同期し、`CopyrayMiddleware` はページ内のマーカー(`⟦CT:key⟧`)を除去・変換します。
68
68
 
69
- ### copy-tuner スキル
69
+ 挿入位置は自動で決まるため、通常は設定不要です。Devise(Warden)を使っているアプリでは `Warden::Manager` の直前、それ以外の環境ではスタック末尾に挿入されます。
70
70
 
71
- i18n キーの操作を支援するスキルです。翻訳キーの検索・登録・確認などの依頼に自動的に使用されます。
71
+ Devise 併用時に Warden の直前へ寄せるのは `throw :warden` の挙動に対応するためです。`throw :warden` は `Warden::Manager` の `catch(:warden)` までスタックを巻き戻すため、CopyTuner の middleware が Warden より内側にあると、認証エラー時のレスポンス(`Devise::FailureApp` が返す HTML)を受け取れず、Copyray のマーカーがページに残ってしまいます。
72
+
73
+ 位置を変えたい場合は `config.middleware_position` に `{ before: SomeMiddleware }` または `{ after: SomeMiddleware }` を指定すると、このデフォルトを上書きできます。
74
+
75
+ ```ruby
76
+ CopyTunerClient.configure do |config|
77
+ # ...
78
+ config.middleware_position = { after: Rack::Runtime }
79
+ end
80
+ ```
81
+
82
+ 指定した middleware がスタックに存在しない場合、Rails の起動時に例外(`No such middleware to insert before: ...` / `... insert after: ...`)が発生します。
83
+
84
+ ## Claude Code スキル
85
+
86
+ `skills/` 以下に Claude Code 向けのスキルが含まれています。
72
87
 
73
88
  ```
74
- gh skill install SonicGarden/copy-tuner-ruby-client copy-tuner --scope project
89
+ gh skill install SonicGarden/copy-tuner-ruby-client <スキル名> --scope project
75
90
  ```
76
91
 
92
+ ### copy-tuner スキル
93
+
94
+ i18n キーの操作を支援するスキルです。翻訳キーの検索・登録・確認などの依頼に自動的に使用されます。
95
+
77
96
  詳細: [skills/copy-tuner/SKILL.md](skills/copy-tuner/SKILL.md)
78
97
 
98
+ ### copy-tuner-to-locales-migrate-prefix スキル
99
+
100
+ copy_tuner が集中管理する i18n キーを、prefix(正規表現)単位で `config/locales` のローカル YAML 管理へ移行するスキルです。gem は残したまま特定 prefix だけをローカル化する「部分ローカル化」と、全 prefix を移して完全撤去する「全移行」の両方に使えます。明示的に呼び出したときのみ動作します。
101
+
102
+ 詳細: [skills/copy-tuner-to-locales-migrate-prefix/SKILL.md](skills/copy-tuner-to-locales-migrate-prefix/SKILL.md)
103
+
104
+ ### copy-tuner-to-locales-cleanup スキル
105
+
106
+ `copy-tuner-to-locales-migrate-prefix` で全 prefix の移行が完了した後に、gem・初期化子・CI・deploy・ドキュメント・MCP 設定を一括撤去し、copy_tuner 依存を完全に取り除くスキルです。明示的に呼び出したときのみ動作します。
107
+
108
+ 詳細: [skills/copy-tuner-to-locales-cleanup/SKILL.md](skills/copy-tuner-to-locales-cleanup/SKILL.md)
109
+
110
+ ### copy-tuner-to-t-migrate スキル
111
+
112
+ copy_tuner_client v2.0.0 で削除された独自ヘルパー `tt` の呼び出しを、Rails 標準の `t`(`translate`)へ置換するスキルです。機械的に安全な箇所は一括変換し、文字列加工や `label` の第一引数に渡している箇所は 1 件ずつ確認しながら置換します。破壊的な一括書き換えを含むため、明示的に呼び出したときのみ動作します。
113
+
114
+ 詳細: [skills/copy-tuner-to-t-migrate/SKILL.md](skills/copy-tuner-to-t-migrate/SKILL.md)
115
+
79
116
  Development
80
117
  =================
81
118
 
@@ -184,6 +184,7 @@ module CopyTunerClient
184
184
  self.local_first_key_regexp = nil
185
185
  self.project_id = nil
186
186
  self.download_cache_dir = Pathname.new(Dir.pwd).join('tmp', 'cache', 'copy_tuner_client')
187
+ self.middleware_position = default_middleware_position
187
188
 
188
189
  @applied = false
189
190
  end
@@ -357,6 +358,20 @@ module CopyTunerClient
357
358
  end
358
359
  end
359
360
 
361
+ # throw :warden は Warden::Manager の catch(:warden) までスタックを巻き戻すため、
362
+ # Warden より内側の middleware は @app.call の戻り値を受け取れず、Devise::FailureApp の
363
+ # 応答がマーカー除去を経ずにブラウザへ届いてしまう。これを避けるため Warden の直前を既定位置にする。
364
+ #
365
+ # 判定に Devise の有無も見るのは、warden を require するだけで Warden::Manager を
366
+ # スタックに積まない gem(authtrail 等)が存在するため。定数の有無だけで決めると
367
+ # そうしたアプリで insert_before が対象を見つけられず起動時例外になる。
368
+ # スタックへ積むのは Devise の railtie(config.app_middleware.use)である。
369
+ def default_middleware_position
370
+ return nil unless defined?(::Warden::Manager) && defined?(::Devise)
371
+
372
+ { before: ::Warden::Manager }
373
+ end
374
+
360
375
  def setup_middleware
361
376
  if enable_middleware?
362
377
  logger.info 'Using copytuner sync middleware'
@@ -372,23 +387,33 @@ module CopyTunerClient
372
387
  logger.info "Available locales: #{locales.join(' ')}"
373
388
  end
374
389
 
375
- def insert_middleware # rubocop:disable Metrics/AbcSize
376
- request_sync_options = {
390
+ def insert_middleware
391
+ # NOTE: 値の nil を除外するのは、{ before: SomeClass if cond } のように条件次第で nil が入る
392
+ # 書き方で従来は末尾 use にフォールバックしていた挙動を保つため(キーの有無だけで分岐すると
393
+ # insert_before(nil) が対象を見つけられず例外になる)。
394
+ case middleware_position
395
+ in { before: target } if target
396
+ middleware.insert_before(target, RequestSync, request_sync_options)
397
+ middleware.insert_before(target, CopyTunerClient::CopyrayMiddleware)
398
+ in { after: target } if target
399
+ # NOTE: insert_after(index, *) は insert(index + 1, *) を呼ぶため、同じ対象へ 2 回挿入すると
400
+ # 後から挿入した方が対象に近い位置に来て順序が反転する。逆順で呼ぶことで
401
+ # 外側→内側が RequestSync → CopyrayMiddleware に揃う。
402
+ middleware.insert_after(target, CopyTunerClient::CopyrayMiddleware)
403
+ middleware.insert_after(target, RequestSync, request_sync_options)
404
+ else
405
+ middleware.use(RequestSync, request_sync_options)
406
+ middleware.use(CopyTunerClient::CopyrayMiddleware)
407
+ end
408
+ end
409
+
410
+ def request_sync_options
411
+ {
377
412
  poller: @poller,
378
413
  cache:,
379
414
  interval: sync_interval,
380
415
  ignore_regex: sync_ignore_path_regex,
381
416
  }
382
- if middleware_position.is_a?(Hash) && middleware_position[:before]
383
- middleware.insert_before(middleware_position[:before], RequestSync, request_sync_options)
384
- middleware.insert_before(middleware_position[:before], CopyTunerClient::CopyrayMiddleware)
385
- elsif middleware_position.is_a?(Hash) && middleware_position[:after]
386
- middleware.insert_after(middleware_position[:after], RequestSync, request_sync_options)
387
- middleware.insert_after(middleware_position[:after], CopyTunerClient::CopyrayMiddleware)
388
- else
389
- middleware.use(RequestSync, request_sync_options)
390
- middleware.use(CopyTunerClient::CopyrayMiddleware)
391
- end
392
417
  end
393
418
 
394
419
  # project_id は必須。未設定なら明示的に失敗させる。
@@ -1,6 +1,6 @@
1
1
  module CopyTunerClient
2
2
  # Client version
3
- VERSION = '2.2.0'.freeze
3
+ VERSION = '2.2.1'.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 を経由しないため、この最終化は適用されない。
@@ -8,6 +8,10 @@
8
8
  # bin/rails runner .claude/skills/copy-tuner-to-locales-migrate-prefix/scripts/migrate_prefix.rb \
9
9
  # -- --prefix date --export tmp/copy_tuner_all.yml --out config/locales/0010_date.yml
10
10
  #
11
+ # このスクリプト自身の自己検証(gem メンテナ向け。Rails も bundler も不要):
12
+ #
13
+ # ruby skills/copy-tuner-to-locales-migrate-prefix/scripts/migrate_prefix.rb --self-test
14
+ #
11
15
  # What it does, in one pass:
12
16
  # (1) Place : extract the target prefix subtree from the 0000_original_*.yml
13
17
  # originals (load-order deep_merge), deep_merge the export subtree on top
@@ -29,6 +33,127 @@
29
33
  require 'optparse'
30
34
  require 'yaml'
31
35
 
36
+ # ---- self-test ----
37
+ # 引数パースより前に分岐する。このスクリプトはトップレベルで即実行する造りなので、`--self-test` の
38
+ # ときは本体へ進ませずテストだけ走らせてトップレベル return で抜ける。
39
+ if ARGV.delete('--self-test')
40
+ require 'minitest/autorun'
41
+ require 'tmpdir'
42
+ require 'fileutils'
43
+ require 'rbconfig'
44
+
45
+ # 本体はトップレベルで exit するため load では検証できない。fixture を tmpdir に作り、i18n だけを
46
+ # 直接設定する薄いランナー経由でサブプロセス実行する(スクリプトが触るのは I18n.load_path と
47
+ # Backend::Simple だけなので Rails は不要)。
48
+ class MigratePrefixSelfTest < Minitest::Test
49
+ SCRIPT = File.expand_path(__FILE__)
50
+ # 対象 prefix(date)を持つファイル。これだけが書き戻し対象になるのが期待挙動。
51
+ WITH_PREFIX = "ja:\n date:\n formats:\n default: \"%Y\"\n"
52
+ WITHOUT_PREFIX = "ja:\n greeting: \"hello\"\n"
53
+ # 片方だけが対象 prefix を持つ 2 ファイル構成。「1 ファイルだけ書き戻る」ことの検証に使う。
54
+ MIXED_FILES = { '0000_original_a.yml' => WITH_PREFIX, '0000_original_b.yml' => WITHOUT_PREFIX }.freeze
55
+ EXPORT_JA = "ja:\n date:\n formats:\n default: \"%Y/%m/%d\"\n"
56
+ EXPORT_JA_EN = "#{EXPORT_JA}en:\n date:\n formats:\n default: \"%m/%d/%Y\"\n".freeze
57
+ # to_yaml の既定 line_width(80)を確実に超える長さにして折り返しを誘発する。
58
+ LONG_VALUE = (['word'] * 60).join(' ').freeze
59
+
60
+ def test_untouched_file_keeps_bytes
61
+ # コメント・空行・ダブルクォートが 1 バイトも変わらないことを見る。
62
+ untouched = <<~YAML
63
+ ja:
64
+ # これは greeting のコメント
65
+ greeting: "hello"
66
+
67
+ farewell: "bye"
68
+ YAML
69
+ migrate({ '0000_original_a.yml' => WITH_PREFIX, '0000_original_b.yml' => untouched }) do |out, dir|
70
+ # binread は ASCII-8BIT を返すため、UTF-8 の期待値を b で揃えてバイト列として比較する。
71
+ assert_equal(untouched.b, read(dir, '0000_original_b.yml'), "無関係なファイルが書き換わった:\n#{out}")
72
+ end
73
+ end
74
+
75
+ def test_missing_locale_root_is_not_added
76
+ migrate(MIXED_FILES, locales: %w[ja en], export: EXPORT_JA_EN) do |_out, dir|
77
+ MIXED_FILES.each_key do |name|
78
+ refute_includes(read(dir, name), 'en:', "#{name} に元ファイルに無い en ルートが新設された")
79
+ end
80
+ end
81
+ end
82
+
83
+ def test_prefix_is_pruned_from_matching_file
84
+ migrate({ '0000_original_a.yml' => "#{WITH_PREFIX} greeting: \"hello\"\n" }) do |_out, dir|
85
+ assert_equal({ 'ja' => { 'greeting' => 'hello' } }, load_yaml(dir, '0000_original_a.yml'))
86
+ assert_equal(
87
+ { 'ja' => { 'date' => { 'formats' => { 'default' => '%Y/%m/%d' } } } },
88
+ load_yaml(dir, '0010_migrated.yml')
89
+ )
90
+ end
91
+ end
92
+
93
+ def test_long_value_is_not_wrapped
94
+ migrate({ '0000_original_a.yml' => "#{WITH_PREFIX} greeting: \"#{LONG_VALUE}\"\n" }) do |_out, dir|
95
+ body = read(dir, '0000_original_a.yml')
96
+ assert_includes(body, "greeting: #{LONG_VALUE}\n", "長い値が折り返された:\n#{body}")
97
+ end
98
+ end
99
+
100
+ def test_summary_counts_changed_files
101
+ migrate(MIXED_FILES) do |out, _dir|
102
+ assert_includes(out, 'オリジナル 2 ファイル中 1 ファイル')
103
+ end
104
+ end
105
+
106
+ private
107
+
108
+ # fixture を tmpdir に配置してサブプロセス実行し、標準出力と locales ディレクトリを yield する。
109
+ def migrate(files, locales: %w[ja], export: EXPORT_JA)
110
+ Dir.mktmpdir do |dir|
111
+ locales_dir = File.join(dir, 'config/locales')
112
+ FileUtils.mkdir_p(locales_dir)
113
+ FileUtils.mkdir_p(File.join(dir, 'tmp'))
114
+ files.each { |name, body| File.write(File.join(locales_dir, name), body) }
115
+ File.write(File.join(dir, 'tmp/export.yml'), export)
116
+ write_runner(dir, locales)
117
+
118
+ ok, out = spawn_script(dir, locales)
119
+ assert(ok, "スクリプトが異常終了した:\n#{out}")
120
+ yield(out, locales_dir)
121
+ end
122
+ end
123
+
124
+ def write_runner(dir, locales)
125
+ File.write(File.join(dir, 'runner.rb'), <<~RUBY)
126
+ require 'i18n'
127
+ I18n.load_path = Dir['config/locales/*.yml']
128
+ I18n.available_locales = #{locales.map(&:to_sym).inspect}
129
+ I18n.default_locale = #{locales.first.to_sym.inspect}
130
+ load #{SCRIPT.inspect}
131
+ RUBY
132
+ end
133
+
134
+ def spawn_script(dir, locales)
135
+ args = [
136
+ '--prefix', 'date', '--locales', locales.join(','),
137
+ '--export', 'tmp/export.yml', '--out', 'config/locales/0010_migrated.yml'
138
+ ]
139
+ Dir.chdir(dir) do
140
+ read_io, write_io = IO.pipe
141
+ pid = spawn(RbConfig.ruby, File.join(dir, 'runner.rb'), '--', *args, out: write_io, err: write_io)
142
+ write_io.close
143
+ out = read_io.read
144
+ _, status = Process.waitpid2(pid)
145
+ [status.success?, out]
146
+ end
147
+ end
148
+
149
+ def read(dir, name) = File.binread(File.join(dir, name))
150
+
151
+ def load_yaml(dir, name) = YAML.safe_load_file(File.join(dir, name))
152
+ end
153
+
154
+ return
155
+ end
156
+
32
157
  # NOTE: `bin/rails runner` は Kernel#abort が投げる SystemExit を握りつぶし終了コードが 0 になる
33
158
  # (実機確認済み)。中断を呼び出し側へ確実に伝えるため、abort ではなく warn + exit(1) を使う。
34
159
  def die(message)
@@ -160,7 +285,12 @@ end
160
285
  # locale ルートを持つ raw Hash(`{ "ja" => {...}, "en" => {...} }`)から、対象 prefix を全 locale で刈った
161
286
  # 新しい Hash を返す(非破壊)。移行漏れ検証のシミュレーションと実削除の両方で使う。
162
287
  def prune_prefix_all_locales(raw, locales, keys)
163
- locales.reduce(raw) { |acc, locale| acc.merge(locale => prune_prefix(acc[locale] || {}, keys)) }
288
+ locales.reduce(raw) do |acc, locale|
289
+ # 元ファイルに無い locale ルートを新設すると、ja だけのファイルに `en: {}` が追記されてしまう。
290
+ next acc unless acc.key?(locale)
291
+
292
+ acc.merge(locale => prune_prefix(acc[locale], keys))
293
+ end
164
294
  end
165
295
 
166
296
  # ---- (1) 配置 ----
@@ -277,10 +407,18 @@ unless leaks.empty?
277
407
  die('中断。--out の内容・採番・regexp を確認すること。')
278
408
  end
279
409
 
280
- original_files.each do |f|
281
- raw = YAML.safe_load_file(f, permitted_classes: [Symbol], aliases: true) || {}
282
- File.write(f, prune_prefix_all_locales(raw, LOCALES, prefix_keys).to_yaml)
283
- end
410
+ changed =
411
+ original_files.count do |f|
412
+ raw = YAML.safe_load_file(f, permitted_classes: [Symbol], aliases: true) || {}
413
+ pruned = prune_prefix_all_locales(raw, LOCALES, prefix_keys)
414
+ # 対象 prefix を含まないファイルを to_yaml で書き戻すと、クォート・アンカー名・折り返し・コメントの
415
+ # 無関係な整形差分が出る。実質的な変更があるファイルだけ書き戻す。
416
+ next false if pruned == raw
417
+
418
+ # 折り返しだけは line_width で無効化できる(アンカー名・クォートは Psych の仕様で制御できない)。
419
+ File.write(f, pruned.to_yaml(line_width: -1))
420
+ true
421
+ end
284
422
 
285
- puts "削除: prefix '#{PREFIX}' をオリジナル #{original_files.size} ファイルから刈り取った。"
423
+ puts "削除: prefix '#{PREFIX}' をオリジナル #{original_files.size} ファイル中 #{changed} ファイルから刈り取った。"
286
424
  puts '完了。手順7(local_first_key_regexp 追加)が未済なら次に実施すること。'
@@ -398,6 +398,179 @@ describe CopyTunerClient::Configuration do
398
398
  end
399
399
  end
400
400
 
401
+ describe 'middleware_position の初期値' do
402
+ let(:config) { described_class.new }
403
+
404
+ context 'Warden::Manager と Devise がどちらも定義済みのとき' do
405
+ before do
406
+ stub_const('Warden::Manager', Class.new)
407
+ stub_const('Devise', Module.new)
408
+ end
409
+
410
+ it '{ before: Warden::Manager } になる' do
411
+ expect(config.middleware_position).to eq({ before: Warden::Manager })
412
+ end
413
+ end
414
+
415
+ context 'Warden::Manager が未定義のとき' do
416
+ before { hide_const('Warden::Manager') }
417
+
418
+ it 'nil のままになる' do
419
+ expect(config.middleware_position).to be_nil
420
+ end
421
+ end
422
+
423
+ # NOTE: authtrail のように warden を require するだけで Warden::Manager をスタックへ積まない
424
+ # gem があるため、Warden 単独では既定位置にしない(insert_before が起動時例外になる)。
425
+ context 'Warden::Manager は定義済みだが Devise が未定義のとき' do
426
+ before do
427
+ stub_const('Warden::Manager', Class.new)
428
+ hide_const('Devise')
429
+ end
430
+
431
+ it 'nil のままになる' do
432
+ expect(config.middleware_position).to be_nil
433
+ end
434
+ end
435
+
436
+ context 'configure ブロック内で明示指定したとき' do
437
+ before do
438
+ stub_const('Warden::Manager', Class.new)
439
+ stub_const('Devise', Module.new)
440
+ end
441
+
442
+ it '明示指定した値が優先される' do
443
+ other = Class.new
444
+ CopyTunerClient.configure(apply: false) do |c|
445
+ c.middleware_position = { before: other }
446
+ end
447
+ expect(CopyTunerClient.configuration.middleware_position).to eq({ before: other })
448
+ end
449
+ end
450
+ end
451
+
452
+ context 'Warden::Manager がスタックにある状態で apply したとき' do
453
+ it_behaves_like 'applied configuration' do
454
+ it 'デフォルト値経由で Warden::Manager の直前に RequestSync → CopyrayMiddleware の順で入る' do
455
+ expect(middleware.classes).to eq(
456
+ [
457
+ Rack::ETag, Rack::TempfileReaper, CopyTunerClient::RequestSync, CopyTunerClient::CopyrayMiddleware,
458
+ Warden::Manager, Rack::Static
459
+ ]
460
+ )
461
+ end
462
+
463
+ it 'Warden::Manager より外側に配置される' do
464
+ expect(middleware.index(CopyTunerClient::RequestSync)).to be < middleware.index(Warden::Manager)
465
+ expect(middleware.index(CopyTunerClient::CopyrayMiddleware)).to be < middleware.index(Warden::Manager)
466
+ end
467
+
468
+ it 'RequestSync に poller / cache / interval / ignore_regex が渡る' do
469
+ args = middleware.args_for(CopyTunerClient::RequestSync)
470
+ expect(args.first).to include(
471
+ poller:, # rubocop:disable Sgcop/Rspec/NoMethodCallInExpectation
472
+ cache:,
473
+ interval: configuration.sync_interval,
474
+ ignore_regex: configuration.sync_ignore_path_regex
475
+ )
476
+ end
477
+ end
478
+
479
+ # NOTE: 実アプリの bin/rails middleware 出力を模した標準スタック。Warden::Manager / Devise は
480
+ # gem の依存にないため stub_const で fake の定数として定義する。
481
+ before do
482
+ stub_const('Warden::Manager', Class.new)
483
+ stub_const('Devise', Module.new)
484
+ end
485
+
486
+ let(:middleware) do
487
+ MiddlewareStack.new([Rack::ETag, Rack::TempfileReaper, Warden::Manager, Rack::Static])
488
+ end
489
+
490
+ def apply
491
+ configuration.middleware = middleware
492
+ configuration.environment_name = 'development'
493
+ configuration.apply
494
+ end
495
+ end
496
+
497
+ context 'Warden::Manager が未定義の状態で apply したとき' do
498
+ it_behaves_like 'applied configuration' do
499
+ it 'Warden::Manager 未定義時は従来どおりスタック末尾へ use される' do
500
+ expect(middleware.classes).to eq([CopyTunerClient::RequestSync, CopyTunerClient::CopyrayMiddleware])
501
+ end
502
+ end
503
+
504
+ before { hide_const('Warden::Manager') }
505
+
506
+ let(:middleware) { MiddlewareStack.new }
507
+
508
+ def apply
509
+ configuration.middleware = middleware
510
+ configuration.environment_name = 'development'
511
+ configuration.apply
512
+ end
513
+ end
514
+
515
+ context 'middleware_position に after を明示指定して apply したとき' do
516
+ it_behaves_like 'applied configuration' do
517
+ it '{after: X} 指定時は X の直後に RequestSync → CopyrayMiddleware の順で入る' do
518
+ expect(middleware.classes).to eq(
519
+ [:a, :x, CopyTunerClient::RequestSync, CopyTunerClient::CopyrayMiddleware, :b]
520
+ )
521
+ end
522
+ end
523
+
524
+ let(:middleware) { MiddlewareStack.new(%i[a x b]) }
525
+
526
+ def apply
527
+ configuration.middleware = middleware
528
+ configuration.middleware_position = { after: :x }
529
+ configuration.environment_name = 'development'
530
+ configuration.apply
531
+ end
532
+ end
533
+
534
+ context 'middleware_position に before を明示指定して apply したとき' do
535
+ it_behaves_like 'applied configuration' do
536
+ it '{before: X} 指定時は X の直前に RequestSync → CopyrayMiddleware の順で入る' do
537
+ expect(middleware.classes).to eq(
538
+ [:a, CopyTunerClient::RequestSync, CopyTunerClient::CopyrayMiddleware, :x, :b]
539
+ )
540
+ end
541
+ end
542
+
543
+ let(:middleware) { MiddlewareStack.new(%i[a x b]) }
544
+
545
+ def apply
546
+ configuration.middleware = middleware
547
+ configuration.middleware_position = { before: :x }
548
+ configuration.environment_name = 'development'
549
+ configuration.apply
550
+ end
551
+ end
552
+
553
+ # NOTE: { before: SomeClass if cond } のように条件次第で値が nil になる書き方を想定する。
554
+ # キーの有無だけで分岐すると insert_before(nil) が対象を見つけられず例外になる。
555
+ context 'middleware_position の値が nil のとき' do
556
+ it_behaves_like 'applied configuration' do
557
+ it '例外を投げずスタック末尾へ use する' do
558
+ expect(middleware.classes).to eq(
559
+ [:a, :x, :b, CopyTunerClient::RequestSync, CopyTunerClient::CopyrayMiddleware]
560
+ )
561
+ end
562
+ end
563
+
564
+ let(:middleware) { MiddlewareStack.new(%i[a x b]) }
565
+
566
+ def apply
567
+ configuration.middleware = middleware
568
+ configuration.middleware_position = { before: nil }
569
+ configuration.environment_name = 'development'
570
+ configuration.apply
571
+ end
572
+ end
573
+
401
574
  context 'applied without locale filter' do
402
575
  include_context 'stubbed configuration'
403
576
 
@@ -0,0 +1,95 @@
1
+ require 'spec_helper'
2
+ require 'copy_tuner_client/copyray_middleware'
3
+ require 'copy_tuner_client/copyray'
4
+ require 'copy_tuner_client/copyray/marker'
5
+ require 'copy_tuner_client/translation_log'
6
+
7
+ # Warden::Manager の catch(:warden) と failure app 呼び出しだけを模した fake(warden gem に依存しないため)
8
+ class FakeWarden
9
+ def initialize(app, failure_app)
10
+ @app = app
11
+ @failure_app = failure_app
12
+ end
13
+
14
+ def call(env)
15
+ result = catch(:warden) { @app.call(env) }
16
+ result.is_a?(Array) ? result : @failure_app.call(env)
17
+ end
18
+ end
19
+
20
+ # NOTE: CopyrayMiddleware 単体ではなく、Warden 相当の fake との組み合わせ位置を検証するため
21
+ # describe の第一引数はクラスではなく説明文にしている
22
+ describe 'throw :warden と CopyrayMiddleware の位置関係' do # rubocop:disable RSpec/DescribeClass
23
+ def marker(key)
24
+ CopyTunerClient::Copyray::Marker.encode(key)
25
+ end
26
+
27
+ # NOTE: authenticate_user! 失敗を模す。throw :warden するとこの @app.call(env) は正常リターンせず、
28
+ # CopyrayMiddleware が内側にあると Rewriter.rewrite が実行されない。
29
+ let(:throwing_app) { ->(_env) { throw :warden } }
30
+ let(:failure_status) { 200 }
31
+ let(:failure_app) do
32
+ ->(_env) do
33
+ [failure_status, { 'Content-Type' => 'text/html' }, [failure_body]]
34
+ end
35
+ end
36
+ let(:failure_body) { "<html><body><p>#{marker('devise.failure.unauthenticated')}Please sign in</p></body></html>" }
37
+
38
+ before do
39
+ CopyTunerClient.configure do |configuration|
40
+ configuration.project_id = 1
41
+ configuration.client = FakeClient.new
42
+ end
43
+ end
44
+
45
+ # NOTE: append_js は Rails の ActionController::Base.helpers に依存するため、
46
+ # copyray_middleware_spec.rb と同じ手法で no-op スタブに差し替え、Rewriter の効果だけを見る。
47
+ def stub_append_js(middleware)
48
+ allow(middleware).to receive(:append_js) { |html, *| html }
49
+ end
50
+
51
+ context '内側構成(CopyrayMiddleware が Warden より内側)のとき' do
52
+ it 'マーカー ⟦CT: がレスポンスに残る(不具合の記録)' do
53
+ copyray = CopyTunerClient::CopyrayMiddleware.new(throwing_app)
54
+ stub_append_js(copyray)
55
+ warden = FakeWarden.new(copyray, failure_app)
56
+
57
+ _status, _headers, response = warden.call({})
58
+ result = response.join
59
+
60
+ expect(result).to match(CopyTunerClient::Copyray::Marker::SCAN_REGEXP)
61
+ end
62
+ end
63
+
64
+ context '外側構成(CopyrayMiddleware が Warden より外側)のとき' do
65
+ it 'マーカーが消え data-copyray-key に変換される' do
66
+ warden = FakeWarden.new(throwing_app, failure_app)
67
+ copyray = CopyTunerClient::CopyrayMiddleware.new(warden)
68
+ stub_append_js(copyray)
69
+
70
+ _status, _headers, response = copyray.call({})
71
+ result = response.join
72
+
73
+ expect(result).to include('data-copyray-key="devise.failure.unauthenticated"')
74
+ expect(result).not_to match(CopyTunerClient::Copyray::Marker::SCAN_REGEXP)
75
+ end
76
+
77
+ context 'failure app が 422 を返すとき(Devise.responder.error_status = :unprocessable_entity 相当)' do
78
+ let(:unprocessable_failure_app) do
79
+ ->(_env) { [422, { 'Content-Type' => 'text/html' }, [failure_body]] }
80
+ end
81
+
82
+ it 'マーカーが消え data-copyray-key に変換される' do
83
+ warden = FakeWarden.new(throwing_app, unprocessable_failure_app)
84
+ copyray = CopyTunerClient::CopyrayMiddleware.new(warden)
85
+ stub_append_js(copyray)
86
+
87
+ _status, _headers, response = copyray.call({})
88
+ result = response.join
89
+
90
+ expect(result).to include('data-copyray-key="devise.failure.unauthenticated"')
91
+ expect(result).not_to match(CopyTunerClient::Copyray::Marker::SCAN_REGEXP)
92
+ end
93
+ end
94
+ end
95
+ end
@@ -1,13 +1,47 @@
1
+ # ActionDispatch::MiddlewareStack の挿入 API を模した fake(挿入順序と引数を検証するため)
1
2
  class MiddlewareStack
2
- def initialize
3
- @middlewares = []
3
+ Entry = Struct.new(:klass, :args)
4
+
5
+ def initialize(existing = [])
6
+ @middlewares = existing.map { |klass| Entry.new(klass, []) }
7
+ end
8
+
9
+ def use(klass, *args)
10
+ @middlewares << Entry.new(klass, args)
11
+ end
12
+
13
+ def insert_before(target, klass, *args)
14
+ index = index_of!(target, 'before')
15
+ @middlewares.insert(index, Entry.new(klass, args))
4
16
  end
5
17
 
6
- def use(klass, *)
7
- @middlewares << klass.new('fake_app', *)
18
+ def insert_after(target, klass, *args)
19
+ index = index_of!(target, 'after')
20
+ @middlewares.insert(index + 1, Entry.new(klass, args))
8
21
  end
9
22
 
10
23
  def include?(klass)
11
- @middlewares.any?(klass)
24
+ classes.include?(klass)
25
+ end
26
+
27
+ def classes
28
+ @middlewares.map(&:klass)
29
+ end
30
+
31
+ def index(klass)
32
+ classes.index(klass)
33
+ end
34
+
35
+ def args_for(klass)
36
+ @middlewares.find { |entry| entry.klass == klass }&.args
37
+ end
38
+
39
+ private
40
+
41
+ def index_of!(target, where)
42
+ index = classes.index(target)
43
+ raise "No such middleware to insert #{where}: #{target.inspect}" if index.nil?
44
+
45
+ index
12
46
  end
13
47
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: copy_tuner_client
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.2.0
4
+ version: 2.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - SonicGarden
@@ -258,6 +258,7 @@ files:
258
258
  - spec/copy_tuner_client/process_guard_spec.rb
259
259
  - spec/copy_tuner_client/request_sync_spec.rb
260
260
  - spec/copy_tuner_client/translation_log_spec.rb
261
+ - spec/copy_tuner_client/warden_integration_spec.rb
261
262
  - spec/copy_tuner_client_spec.rb
262
263
  - spec/spec_helper.rb
263
264
  - spec/support/client_spec_helpers.rb