copy_tuner_client 1.1.5 → 1.2.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 93652e0d1c83329df154658afb5d92a5ddeed5e7c417301ae0bc16d276cf3945
4
- data.tar.gz: 02a2e3c38c136d258aec06ab2a1d4d60a05b21b441cbe4d8ad90767ab1786ab4
3
+ metadata.gz: 28fdccccf21b4d18bd14a1179edeb3f878dc38ccd4f79e30514255be98286f16
4
+ data.tar.gz: 9afc9b5073c5a3b0f02c004080294f87a78ca7ed6f478ae6d13baf0284df954c
5
5
  SHA512:
6
- metadata.gz: ccd023595c35dd31542197803624dc52a93d5dabda5e49931b1d98f48406aef81fffe4a6131fa3538fa56375ae5b0247ee640ccfce8d174e1c6f46c7b5d35333
7
- data.tar.gz: 5fee43f3ccb648e9410064bf63512a7ca1ee8d16b1358f5bd2f7383271fd3261ab714c3b5f28058829566f344243561b3fed2555541b999de9cacef85306288d
6
+ metadata.gz: 031e2d70ce76dd689f085c028f6c7be116f7988434f250efc88674fca5131a4e73b6bca57b65275cbe750500751e5f44a49a429d672878045c466a5335ead337
7
+ data.tar.gz: a88877b5bcc4c31eca6ce56f7d486525b92c8a2a62e37b177ce0937a9eb08525765a2022a2937413891c0231de8763d49381538b1cd1fe7b31c3fb8e6c10ea1e
data/CLAUDE.md ADDED
@@ -0,0 +1,32 @@
1
+ # CLAUDE.md
2
+
3
+ CopyTuner の Ruby クライアント gem。Rails アプリの I18n を CopyTuner サーバと同期する。
4
+
5
+ ## コマンド
6
+ - テスト: `bundle exec rspec`(単一ファイル: `bundle exec rspec spec/copy_tuner_client/cache_spec.rb`)
7
+ - 特定の Rails バージョンでテスト: `BUNDLE_GEMFILE=gemfiles/8.0.gemfile bundle exec rspec`
8
+ - Lint: `bundle exec rubocop`(`sgcop` を継承)
9
+ - フロントエンドビルド: `yarn build`(開発: `yarn dev`)
10
+ - gem リリース: `bundle exec rake build|install|release`
11
+
12
+ ## アーキテクチャ
13
+ `Configuration#apply`(lib/copy_tuner_client/configuration.rb)が全コンポーネントを組み立てる起点:
14
+ - `Client` — CopyTuner サーバ / S3 との HTTP 通信
15
+ - `Cache` — Mutex で保護された blurb ストア。Hash のように振る舞う。アップロードキューを管理
16
+ - `I18nBackend` — デフォルトの I18n backend を置き換える(`I18n.backend = ...`)。lookup で Cache を参照
17
+ - `Poller` / `ProcessGuard` — バックグラウンド同期スレッド
18
+ - Rack middleware `RequestSync` / `CopyrayMiddleware` — 開発環境でのリクエスト毎同期とオーバーレイ
19
+ Rails 統合は engine.rb のイニシャライザ経由(ヘルパー/SimpleForm フック、アセット precompile)。
20
+
21
+ ## Gotchas
22
+ - **フロントエンドは `src/*.ts` を編集する。`app/assets/*` は Vite のビルド成果物なので直接編集しない**
23
+ (vite.config.ts が `src/main.ts` → `app/assets/javascripts/copytuner.js` を出力)。
24
+ - キー除外の 2 オプションは混同しやすい:
25
+ - `exclude_key_regexp` — locale 付きキー対象・アップロード時に作用
26
+ - `local_first_key_regexp` — locale を除いたキー対象・lookup 時に作用(ローカル YAML 優先)
27
+ local_first キーのアップロード抑止は `Cache#[]=` に集約されている。
28
+ - **アップロード抑止の新ルールは `Cache#[]=` に足す。`I18nBackend` の書き込み経路(`lookup` / `default` / `store_item`)ごとに個別ガードを足さない**
29
+ (理由: cache への書き込みは全経路が最終的に `Cache#[]=` を通る単一の関門。経路ごとにガードを足すと付け忘れの穴が生まれ、同じチェックが分散して保守負担になる。実際 local_first の抑止は当初 `default` 個別に足したが穴が残り、`Cache#[]=` への集約に作り直した)。
30
+
31
+ ## Claude Code スキル
32
+ `skills/copy-tuner/` に i18n キー操作支援スキルがある(SKILL.md 参照)。
data/README.md CHANGED
@@ -35,6 +35,48 @@ bundle exec rake copy_tuner:export
35
35
 
36
36
  これで、`config/locales/copy_tuner.yml` に翻訳ファイルが作成されます。
37
37
 
38
+ ## 特定のキーをローカル YAML 優先にする(段階移行)
39
+
40
+ `config.local_first_key_regexp` を設定すると、**locale を除いたキー**(例 `views.foo.bar`)がその正規表現にマッチした場合、CopyTuner サーバのキャッシュをスキップして、ローカルの `config/locales/*.yml`(`I18n::Backend::Simple`)を優先的に参照します。
41
+
42
+ ```ruby
43
+ CopyTunerClient.configure do |config|
44
+ # ...
45
+ # views.* で始まるキーはローカル YAML を優先する
46
+ config.local_first_key_regexp = /\Aviews\./
47
+ end
48
+ ```
49
+
50
+ CopyTuner で一元管理している翻訳を、`views.*` のような単位で段階的にローカル YAML へ移行するためのオプションです。
51
+
52
+ - マッチしたキーは CopyTuner キャッシュを一切参照せず、ローカル YAML のみを引きます(完全分離)。
53
+ - ローカル YAML にも存在しない場合は未訳(`nil` / MissingTranslation)となります。CopyTuner へのフォールバックや新規キーのアップロードは行いません。これにより移行漏れを未訳として検知できます。
54
+ - マッチしたキーには、ビューヘルパー(`t` / `translate`)および SimpleForm のラベルで CopyRay オーバーレイマーカー(`<!--COPYRAY key-->`)を注入しません。これらのキーは CopyTuner 上で編集できないため、編集可能だと誤認させないためです。
55
+
56
+ `exclude_key_regexp` との違い:
57
+
58
+ | オプション | 対象 | 作用するタイミング |
59
+ | --- | --- | --- |
60
+ | `exclude_key_regexp` | locale 付きキー(例 `ja.views.foo`) | アップロード時(CopyTuner への送信を抑止) |
61
+ | `local_first_key_regexp` | locale を除いたキー(例 `views.foo`) | 読み込み時(lookup の優先順位) |
62
+
63
+ ### `exclude_key_regexp` は非推奨です
64
+
65
+ `exclude_key_regexp` は **非推奨**です(将来のリリースで削除予定)。設定すると deprecation 警告が出ます。代わりに `local_first_key_regexp` を使ってください。
66
+
67
+ ```ruby
68
+ # Before(非推奨)
69
+ config.exclude_key_regexp = /\Aja\.views\./
70
+
71
+ # After: locale プレフィックス(ja.)を外して指定する
72
+ config.local_first_key_regexp = /\Aviews\./
73
+ ```
74
+
75
+ 移行時の注意:
76
+
77
+ - 対象キーの形式が異なります。`exclude_key_regexp` は **locale 付き**(`ja.views.foo`)、`local_first_key_regexp` は **locale を除いた**形式(`views.foo`)でマッチします。正規表現から locale プレフィックスを外してください。
78
+ - 挙動も少し変わります。`exclude_key_regexp` はアップロードを抑止するだけで lookup 時は CopyTuner キャッシュを参照し続けますが、`local_first_key_regexp` は lookup 時に CopyTuner キャッシュをスキップしてローカル YAML を優先します(完全分離)。ローカル管理へ移行する用途では `local_first_key_regexp` のほうが適切です。
79
+
38
80
  ## Claude Code スキル
39
81
 
40
82
  `skills/copy-tuner/` に Claude Code 向けのスキルが含まれています。
@@ -21,6 +21,7 @@ module CopyTunerClient
21
21
  @logger = options[:logger]
22
22
  @mutex = Mutex.new
23
23
  @exclude_key_regexp = options[:exclude_key_regexp]
24
+ @local_first_key_regexp = options[:local_first_key_regexp]
24
25
  @upload_disabled = options[:upload_disabled]
25
26
  @ignored_keys = options.fetch(:ignored_keys, [])
26
27
  @ignored_key_handler = options.fetch(:ignored_key_handler, -> (e) { raise e })
@@ -51,6 +52,9 @@ module CopyTunerClient
51
52
 
52
53
  # NOTE: config/locales以下のファイルに除外キーが残っていた場合の対応
53
54
  key_without_locale = key.split('.')[1..].join('.')
55
+ # NOTE: local_first_key_regexp にマッチするキーは copy_tuner と完全分離するためアップロードしない
56
+ return if @local_first_key_regexp && key_without_locale.match?(@local_first_key_regexp)
57
+
54
58
  if @ignored_keys.include?(key_without_locale)
55
59
  @ignored_key_handler.call(IgnoredKey.new("Ignored key: #{key_without_locale}"))
56
60
  end
@@ -18,7 +18,7 @@ module CopyTunerClient
18
18
  proxy_port proxy_user secure polling_delay sync_interval
19
19
  sync_interval_staging sync_ignore_path_regex logger
20
20
  framework middleware disable_middleware disable_test_translation
21
- ca_file exclude_key_regexp s3_host locales ignored_keys ignored_key_handler
21
+ ca_file exclude_key_regexp local_first_key_regexp s3_host locales ignored_keys ignored_key_handler
22
22
  download_cache_dir].freeze
23
23
 
24
24
  # @return [String] The API key for your project, found on the project edit form.
@@ -114,7 +114,14 @@ module CopyTunerClient
114
114
  attr_accessor :poller
115
115
 
116
116
  # @return [Regexp] Regular expression to exclude keys.
117
- attr_accessor :exclude_key_regexp
117
+ # @deprecated Use {#local_first_key_regexp} instead.
118
+ attr_reader :exclude_key_regexp
119
+
120
+ # @return [Regexp] Keys (without locale) matching this regexp bypass the
121
+ # copy_tuner cache and are looked up from local config/locales
122
+ # (I18n::Backend::Simple) first. Used for gradual migration from
123
+ # copy_tuner to local YAML.
124
+ attr_accessor :local_first_key_regexp
118
125
 
119
126
  # @return [String] The S3 host to connect to (defaults to +copy-tuner-us.s3.amazonaws.com+).
120
127
  attr_accessor :s3_host
@@ -163,6 +170,7 @@ module CopyTunerClient
163
170
  self.html_escape = true
164
171
  self.ignored_keys = []
165
172
  self.ignored_key_handler = ->(e) { raise e }
173
+ self.local_first_key_regexp = nil
166
174
  self.project_id = nil
167
175
  self.download_cache_dir = Pathname.new(Dir.pwd).join('tmp', 'cache', 'copy_tuner_client')
168
176
 
@@ -314,6 +322,18 @@ module CopyTunerClient
314
322
  @api_key = api_key
315
323
  end
316
324
 
325
+ # @deprecated Use {#local_first_key_regexp} instead.
326
+ def exclude_key_regexp=(value)
327
+ unless value.nil?
328
+ ActiveSupport::Deprecation.new.warn(
329
+ 'exclude_key_regexp is deprecated and will be removed in a future release. ' \
330
+ 'Use local_first_key_regexp instead (note: it matches keys WITHOUT the locale prefix, ' \
331
+ 'e.g. /\Aviews\./ instead of /\Aja\.views\./).'
332
+ )
333
+ end
334
+ @exclude_key_regexp = value
335
+ end
336
+
317
337
  # Sync interval for Rack Middleware
318
338
  def sync_interval
319
339
  if environment_name == 'staging'
@@ -336,6 +356,18 @@ module CopyTunerClient
336
356
  URI::Generic.build(scheme: self.protocol, host: self.host, port: self.port.to_i, path:).to_s
337
357
  end
338
358
 
359
+ # locale を除いたキーが local_first_key_regexp にマッチするかを返す。
360
+ # マッチするキーはローカル config/locales(CopyTuner 管理外)で管理されるため、
361
+ # オーバーレイマーカー注入やキャッシュ参照をスキップする必要がある。
362
+ #
363
+ # @param key_without_locale [String, Symbol, nil] locale prefix を除いたキー(例: "views.foo.bar")
364
+ # @return [Boolean]
365
+ def local_first_key?(key_without_locale)
366
+ return false if local_first_key_regexp.nil? || key_without_locale.nil?
367
+
368
+ key_without_locale.to_s.match?(local_first_key_regexp)
369
+ end
370
+
339
371
  private
340
372
 
341
373
  def default_port
@@ -7,6 +7,10 @@ module CopyTunerClient
7
7
  def self.augment_template(source, key)
8
8
  return source if source.blank? || !source.is_a?(String)
9
9
 
10
+ # NOTE: local_first(CopyTuner 管理外でローカル config/locales 優先)のキーには
11
+ # オーバーレイマーカーを出さない。編集できないキーを編集可能だと誤認させないため。
12
+ return source if CopyTunerClient.configuration.local_first_key?(key)
13
+
10
14
  escape = CopyTunerClient.configuration.html_escape && !source.html_safe?
11
15
  augmented = "<!--COPYRAY #{key}-->#{escape ? ERB::Util.html_escape(source) : source}"
12
16
  augmented.html_safe
@@ -73,6 +73,13 @@ module CopyTunerClient
73
73
  CopyTunerClient::configuration.ignored_key_handler.call(IgnoredKey.new("Ignored key: #{key_without_locale}"))
74
74
  end
75
75
 
76
+ # NOTE: local_first_key_regexp にマッチするキーは copy_tuner キャッシュをスキップし、
77
+ # ローカル config/locales(I18n::Backend::Simple)を優先する。段階的にローカルへ移行するための仕組み。
78
+ # ローカルに無い場合は nil(未訳)のまま返し、copy_tuner へのフォールバックも空キー登録も行わない(完全分離)。
79
+ if local_first_key?(key_without_locale)
80
+ return super
81
+ end
82
+
76
83
  # NOTE: ハッシュ化した場合に削除されるキーに対応するため、最初に完全一致をチェック(旧クライアントの動作を維持)
77
84
  # 例: `en.test.key` が `en.test.key.conflict` のように別のキーで上書きされている場合の対応
78
85
  exact_match = cache[key_with_locale]
@@ -134,6 +141,10 @@ module CopyTunerClient
134
141
  cache.wait_for_download
135
142
  end
136
143
 
144
+ def local_first_key?(key_without_locale)
145
+ CopyTunerClient.configuration.local_first_key?(key_without_locale)
146
+ end
147
+
137
148
  def default(locale, object, subject, options = {})
138
149
  content = super(locale, object, subject, options)
139
150
  return content if !object.is_a?(String) && !object.is_a?(Symbol)
@@ -141,6 +152,7 @@ module CopyTunerClient
141
152
  if content.respond_to?(:to_str)
142
153
  parts = I18n.normalize_keys(locale, object, options[:scope], options[:separator])
143
154
  # NOTE: ActionView::Helpers::TranslationHelper#translate wraps default String in an Array
155
+ # NOTE: local_first キーのアップロード抑止は Cache#[]= 側に集約している
144
156
  if subject.is_a?(String) || (subject.is_a?(Array) && subject.size == 1 && subject.first.is_a?(String))
145
157
  key = parts.join('.')
146
158
  cache[key] = content.to_str
@@ -13,6 +13,11 @@ module CopyTunerClient
13
13
  end
14
14
 
15
15
  def self.add(key, result)
16
+ # local_first(CopyTuner 管理外でローカル config/locales 優先)のキーは記録しない。
17
+ # ここに集約することで、Copyray オーバーレイの JSON(window.CopyTuner.data)にも
18
+ # local_first キーが混入しない。key は I18n.normalize_keys(nil, ...) 由来の locale なし形式。
19
+ return if CopyTunerClient.configuration.local_first_key?(key)
20
+
16
21
  translations[key] = result if initialized? && !translations.key?(key)
17
22
  end
18
23
 
@@ -1,6 +1,6 @@
1
1
  module CopyTunerClient
2
2
  # Client version
3
- VERSION = '1.1.5'.freeze
3
+ VERSION = '1.2.0'.freeze
4
4
 
5
5
  # API version being used to communicate with the server
6
6
  API_VERSION = '2.0'.freeze
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: copy-tuner
3
- description: copy_tuner を使った i18n キーの操作スキル。「翻訳キーを調べて」「i18nキーを追加して」「このテキストのキーは?」「i18nキーを探して」「翻訳を確認して」「copy_tunerで調べて」のような依頼に必ず使用する。新しい翻訳キーが必要な実装時、既存キーを参照する時、不要になったキーを処理する時にも積極的に使用する。
3
+ description: "copy_tuner を使った i18n キーの操作スキル。「翻訳キーを調べて」「i18nキーを追加して」「このテキストのキーは?」「i18nキーを探して」「翻訳を確認して」「copy_tunerで調べて」のような依頼に必ず使用する。新しい翻訳キーが必要な実装時、既存キーを参照する時、不要になったキーを処理する時にも積極的に使用する。重要: config/locales ディレクトリや .yml 翻訳ファイルを読もうとしたとき・参照しようとしたときは、必ずこのスキルを使うこと。このプロジェクトの i18n は config/locales ではなく copy_tuner で管理されているため、config/locales を読んでも翻訳は見つからない。"
4
4
  license: MIT
5
5
  ---
6
6
 
@@ -25,10 +25,23 @@ license: MIT
25
25
 
26
26
  1. `search_key` で機能名・画面名を英語キーワードで検索し、命名パターンを把握する(例: `search_key("move_out")` で退去関連のキー名の構造を確認する)
27
27
  2. パターンに合わせたキー名を決める
28
- 3. ja は必須。他に必要なロケールは `get_locales` で確認し、日本語以外は文脈に応じて翻訳して登録する
29
- 4. コード内で `t("登録したキー名")` として使用する
28
+ 3. ja は必須。他に必要なロケールは `get_locales` で確認し、存在するロケール分を翻訳して登録する
29
+ 4. コード内で翻訳キーを使用する(`t` `tt` の使い分けは「I18nメソッドの使い分け」セクション参照)
30
30
 
31
31
  ## 不要なキーの処理
32
32
 
33
33
  コード削除等で不要になったキーは削除せず `config/initializers/copy_tuner.rb` の `ignored_keys` に追加する。
34
34
  一定期間どこからも参照されていないことを確認してから手動削除する。
35
+
36
+ ## I18nメソッドの使い分け
37
+
38
+ - **`t`メソッド**: 通常のテキスト出力に使用
39
+ - **`tt`メソッド**: **HTML要素の属性値**に使用(必須)。copy_tuner_client gem の `HelperExtension` が提供するエイリアス。
40
+
41
+ **理由**: CopyTunerの影響で `t` はHTMLコメント (`<!-- ... -->`) を埋め込みます。属性値に含まれると意図しない表示や動作不良の原因となります。
42
+
43
+ ```erb
44
+ <div title="<%= tt('views.tooltips.help_text') %>">...</div>
45
+ <input placeholder="<%= tt('views.forms.enter_name') %>">
46
+ <h1><%= t('views.simulation.show.title') %></h1>
47
+ ```
@@ -34,6 +34,27 @@ describe 'CopyTunerClient::Cache' do
34
34
  expect(cache.queued.keys).to match_array(%w[en.test.key])
35
35
  end
36
36
 
37
+ it 'local_first_key_regexpにマッチするキー(locale除く)はアップロードキューに入れないこと' do
38
+ cache = build_cache(local_first_key_regexp: /\Aviews\./)
39
+ cache['ja.views.foo'] = 'local value'
40
+ cache['ja.messages.bar'] = 'copy tuner value'
41
+
42
+ cache.download
43
+
44
+ # 完全分離: views.* は copy_tuner へアップロードしない
45
+ expect(cache.queued.keys).to match_array(%w[ja.messages.bar])
46
+ end
47
+
48
+ it 'local_first_key_regexpがnil(デフォルト)の場合は全キーをアップロードすること' do
49
+ cache = build_cache(local_first_key_regexp: nil)
50
+ cache['ja.views.foo'] = 'local value'
51
+ cache['ja.messages.bar'] = 'copy tuner value'
52
+
53
+ cache.download
54
+
55
+ expect(cache.queued.keys).to match_array(%w[ja.views.foo ja.messages.bar])
56
+ end
57
+
37
58
  it '変更がない場合はアップロードしないこと' do
38
59
  cache = build_cache
39
60
  cache.flush
@@ -45,6 +45,7 @@ describe CopyTunerClient::Configuration do
45
45
  it { is_expected.to have_config_option(:middleware).overridable }
46
46
  it { is_expected.to have_config_option(:client).overridable }
47
47
  it { is_expected.to have_config_option(:cache).overridable }
48
+ it { is_expected.to have_config_option(:local_first_key_regexp).overridable.default(nil) }
48
49
 
49
50
  it 'should provide default values for secure connections' do
50
51
  config = CopyTunerClient::Configuration.new
@@ -189,6 +190,58 @@ describe CopyTunerClient::Configuration do
189
190
  expect(prefixed_logger).to be_a(CopyTunerClient::PrefixedLogger)
190
191
  expect(prefixed_logger.original_logger).to eq(logger)
191
192
  end
193
+
194
+ describe '#local_first_key?' do
195
+ let(:config) { CopyTunerClient::Configuration.new }
196
+
197
+ it 'returns false when local_first_key_regexp is nil (default)' do
198
+ expect(config.local_first_key?('views.foo.bar')).to eq false
199
+ end
200
+
201
+ context 'when local_first_key_regexp is set' do
202
+ before { config.local_first_key_regexp = /\Aviews\./ }
203
+
204
+ it 'returns true for a matching key' do
205
+ expect(config.local_first_key?('views.foo.bar')).to eq true
206
+ end
207
+
208
+ it 'returns false for a non-matching key' do
209
+ expect(config.local_first_key?('models.foo.bar')).to eq false
210
+ end
211
+
212
+ it 'returns false for a nil key' do
213
+ expect(config.local_first_key?(nil)).to eq false
214
+ end
215
+
216
+ it 'coerces a Symbol key before matching' do
217
+ expect(config.local_first_key?(:'views.foo')).to eq true
218
+ end
219
+ end
220
+ end
221
+
222
+ describe '#exclude_key_regexp= (deprecated)' do
223
+ let(:config) { CopyTunerClient::Configuration.new }
224
+ let(:deprecator) { instance_double(ActiveSupport::Deprecation, warn: nil) }
225
+
226
+ before { allow(ActiveSupport::Deprecation).to receive(:new).and_return(deprecator) }
227
+
228
+ it 'warns when a value is set' do
229
+ expect(deprecator).to receive(:warn).with(/exclude_key_regexp is deprecated/)
230
+
231
+ config.exclude_key_regexp = /\Aja\.views\./
232
+ end
233
+
234
+ it 'stores the assigned value' do
235
+ config.exclude_key_regexp = /\Aja\.views\./
236
+ expect(config.exclude_key_regexp).to eq(/\Aja\.views\./)
237
+ end
238
+
239
+ it 'does not warn when set to nil' do
240
+ expect(deprecator).not_to receive(:warn)
241
+
242
+ config.exclude_key_regexp = nil
243
+ end
244
+ end
192
245
  end
193
246
 
194
247
  shared_context 'stubbed configuration' do
@@ -54,5 +54,16 @@ describe CopyTunerClient::Copyray do
54
54
  it_behaves_like 'Not escaped'
55
55
  end
56
56
  end
57
+
58
+ context 'when the key matches local_first_key_regexp' do
59
+ let(:source) { 'Hello' }
60
+ let(:key) { 'views.foo' }
61
+
62
+ before { CopyTunerClient.configuration.local_first_key_regexp = /\Aviews\./ }
63
+
64
+ it 'does not inject the overlay marker' do
65
+ is_expected.to eq 'Hello'
66
+ end
67
+ end
57
68
  end
58
69
  end
@@ -23,4 +23,10 @@ describe CopyTunerClient::HelperExtension do
23
23
  view = KeywordArgumentsView.new
24
24
  expect(view.translate('some.key', name: 'World')).to eq '<!--COPYRAY some.key-->Hello, World'
25
25
  end
26
+
27
+ it 'does not inject the overlay marker for a local_first key' do
28
+ CopyTunerClient.configuration.local_first_key_regexp = /\Aviews\./
29
+ view = KeywordArgumentsView.new
30
+ expect(view.translate('views.foo', name: 'World')).to eq 'Hello, World'
31
+ end
26
32
  end
@@ -427,4 +427,85 @@ describe 'CopyTunerClient::I18nBackend' do
427
427
  end
428
428
  end
429
429
  end
430
+
431
+ describe 'local_first_key_regexp(ローカル優先キー)' do
432
+ after { CopyTunerClient.configuration.local_first_key_regexp = nil }
433
+
434
+ # ローカル config/locales 相当のデータを I18n::Backend::Simple 側だけに格納する。
435
+ # I18nBackend#store_translations は cache にも書き込んでしまうため、
436
+ # Simple の store_translations を直接呼んでローカル YAML のみを再現する。
437
+ def store_local(backend, locale, data)
438
+ I18n::Backend::Simple.instance_method(:store_translations).bind(backend).call(locale, data)
439
+ end
440
+
441
+ # アップロード抑止(Cache#[]=)の検証用に、現在の configuration を反映した実 Cache を作る。
442
+ def build_real_cache
443
+ CopyTunerClient::Cache.new(FakeClient.new, CopyTunerClient.configuration.to_hash)
444
+ end
445
+
446
+ context 'local_first_key_regexp が nil(デフォルト)のとき' do
447
+ it 'views.* でも従来どおり copy_tuner(cache)を優先すること' do
448
+ CopyTunerClient.configuration.local_first_key_regexp = nil
449
+ cache['ja.views.foo'] = 'copy tuner value'
450
+ store_local(subject, :ja, views: { foo: 'local value' })
451
+
452
+ expect(subject.translate('ja', 'views.foo')).to eq('copy tuner value')
453
+ end
454
+ end
455
+
456
+ context 'local_first_key_regexp = /\Aviews\./ のとき' do
457
+ before { CopyTunerClient.configuration.local_first_key_regexp = /\Aviews\./ }
458
+
459
+ it 'views.* は cache に値があってもローカル YAML を優先すること' do
460
+ cache['ja.views.foo'] = 'copy tuner value'
461
+ store_local(subject, :ja, views: { foo: 'local value' })
462
+
463
+ expect(subject.translate('ja', 'views.foo')).to eq('local value')
464
+ end
465
+
466
+ it 'views.* 以外のキーは従来どおり copy_tuner を優先すること' do
467
+ cache['ja.messages.foo'] = 'copy tuner value'
468
+ store_local(subject, :ja, messages: { foo: 'local value' })
469
+
470
+ expect(subject.translate('ja', 'messages.foo')).to eq('copy tuner value')
471
+ end
472
+
473
+ it 'views.* がローカルにも cache にも無いとき nil を返し、空キー登録(アップロード)をしないこと' do
474
+ spy_cache = TestCache.new
475
+ allow(spy_cache).to receive(:[]=).and_call_original
476
+ backend = CopyTunerClient::I18nBackend.new(spy_cache)
477
+ I18n.backend = backend
478
+
479
+ result = backend.translate('ja', 'views.missing', default: nil)
480
+
481
+ expect(result).to be_nil
482
+ # 案1(完全分離): local_first キーは空キー登録(アップロードキュー投入)を行わない
483
+ expect(spy_cache).not_to have_received(:[]=).with('ja.views.missing', nil)
484
+ end
485
+
486
+ it 'views.* を default: 付きで翻訳しても copy_tuner へアップロードしないこと' do
487
+ real_cache = build_real_cache
488
+ backend = CopyTunerClient::I18nBackend.new(real_cache)
489
+ I18n.backend = backend
490
+
491
+ result = backend.translate('ja', 'views.bar', default: 'literal default')
492
+
493
+ expect(result).to eq('literal default')
494
+ # 完全分離: local_first キーは default: 経由でもアップロードキューに入らない(Cache#[]= が弾く)
495
+ expect(real_cache.queued.keys).not_to include('ja.views.bar')
496
+ end
497
+
498
+ it 'store_translations 経由でも local_first キーはアップロードキューに入らないこと' do
499
+ real_cache = build_real_cache
500
+ backend = CopyTunerClient::I18nBackend.new(real_cache)
501
+ I18n.backend = backend
502
+
503
+ backend.store_translations(:ja, views: { foo: 'local value' }, messages: { bar: 'msg value' })
504
+
505
+ # 完全分離: views.* はキューに入らず、それ以外(messages.*)は従来どおり入る
506
+ expect(real_cache.queued.keys).not_to include('ja.views.foo')
507
+ expect(real_cache.queued.keys).to include('ja.messages.bar')
508
+ end
509
+ end
510
+ end
430
511
  end
@@ -0,0 +1,51 @@
1
+ require 'spec_helper'
2
+ require 'copy_tuner_client/translation_log'
3
+
4
+ describe CopyTunerClient::TranslationLog do
5
+ before { described_class.clear }
6
+
7
+ describe '.add' do
8
+ context 'when initialized' do
9
+ it 'records the key' do
10
+ described_class.add('views.foo', 'Hello')
11
+ expect(described_class.translations).to eq('views.foo' => 'Hello')
12
+ end
13
+
14
+ it 'does not overwrite an existing key' do
15
+ described_class.add('views.foo', 'Hello')
16
+ described_class.add('views.foo', 'World')
17
+ expect(described_class.translations['views.foo']).to eq 'Hello'
18
+ end
19
+
20
+ context 'when the key matches local_first_key_regexp' do
21
+ before { CopyTunerClient.configuration.local_first_key_regexp = /\Aviews\./ }
22
+
23
+ it 'does not record the matching key' do
24
+ described_class.add('views.foo', 'Hello')
25
+ expect(described_class.translations).to be_empty
26
+ end
27
+
28
+ it 'records keys that do not match' do
29
+ described_class.add('messages.greeting', 'Hi')
30
+ expect(described_class.translations).to eq('messages.greeting' => 'Hi')
31
+ end
32
+ end
33
+
34
+ context 'when local_first_key_regexp is not set' do
35
+ it 'records all keys' do
36
+ described_class.add('views.foo', 'Hello')
37
+ expect(described_class.translations).to eq('views.foo' => 'Hello')
38
+ end
39
+ end
40
+ end
41
+
42
+ context 'when not initialized' do
43
+ before { Thread.current[:translations] = nil }
44
+
45
+ it 'ignores the key' do
46
+ described_class.add('views.foo', 'Hello')
47
+ expect(described_class.initialized?).to be false
48
+ end
49
+ end
50
+ end
51
+ 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: 1.1.5
4
+ version: 1.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - SonicGarden
@@ -192,6 +192,7 @@ files:
192
192
  - ".ruby-version"
193
193
  - ".vscode/settings.json"
194
194
  - CHANGELOG.md
195
+ - CLAUDE.md
195
196
  - Gemfile
196
197
  - LICENSE.txt
197
198
  - README.md
@@ -239,6 +240,7 @@ files:
239
240
  - spec/copy_tuner_client/prefixed_logger_spec.rb
240
241
  - spec/copy_tuner_client/process_guard_spec.rb
241
242
  - spec/copy_tuner_client/request_sync_spec.rb
243
+ - spec/copy_tuner_client/translation_log_spec.rb
242
244
  - spec/copy_tuner_client_spec.rb
243
245
  - spec/spec_helper.rb
244
246
  - spec/support/client_spec_helpers.rb