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 +4 -4
- data/CLAUDE.md +32 -0
- data/README.md +42 -0
- data/lib/copy_tuner_client/cache.rb +4 -0
- data/lib/copy_tuner_client/configuration.rb +34 -2
- data/lib/copy_tuner_client/copyray.rb +4 -0
- data/lib/copy_tuner_client/i18n_backend.rb +12 -0
- data/lib/copy_tuner_client/translation_log.rb +5 -0
- data/lib/copy_tuner_client/version.rb +1 -1
- data/skills/copy-tuner/SKILL.md +16 -3
- data/spec/copy_tuner_client/cache_spec.rb +21 -0
- data/spec/copy_tuner_client/configuration_spec.rb +53 -0
- data/spec/copy_tuner_client/copyray_spec.rb +11 -0
- data/spec/copy_tuner_client/helper_extension_spec.rb +6 -0
- data/spec/copy_tuner_client/i18n_backend_spec.rb +81 -0
- data/spec/copy_tuner_client/translation_log_spec.rb +51 -0
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 28fdccccf21b4d18bd14a1179edeb3f878dc38ccd4f79e30514255be98286f16
|
|
4
|
+
data.tar.gz: 9afc9b5073c5a3b0f02c004080294f87a78ca7ed6f478ae6d13baf0284df954c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
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
|
|
data/skills/copy-tuner/SKILL.md
CHANGED
|
@@ -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.
|
|
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.
|
|
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
|