copy_tuner_client 1.5.0 → 2.1.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/.gitattributes +2 -2
- data/.github/workflows/rspec.yml +1 -1
- data/CHANGELOG.md +27 -0
- data/CLAUDE.md +6 -4
- data/README.md +6 -27
- data/UPGRADING.md +201 -0
- data/app/assets/javascripts/copytuner.js +162 -250
- data/biome.json +39 -0
- data/index.html +9 -11
- data/lib/copy_tuner_client/cache.rb +0 -2
- data/lib/copy_tuner_client/configuration.rb +23 -37
- data/lib/copy_tuner_client/copyray/marker.rb +24 -0
- data/lib/copy_tuner_client/copyray/rewriter.rb +113 -0
- data/lib/copy_tuner_client/copyray.rb +11 -4
- data/lib/copy_tuner_client/copyray_middleware.rb +10 -8
- data/lib/copy_tuner_client/engine.rb +1 -1
- data/lib/copy_tuner_client/helper_extension.rb +0 -3
- data/lib/copy_tuner_client/i18n_backend.rb +5 -12
- data/lib/copy_tuner_client/version.rb +1 -1
- data/mise.toml +3 -0
- data/package.json +9 -13
- data/pnpm-lock.yaml +600 -0
- data/skills/copy-tuner/SKILL.md +37 -6
- data/skills/copy-tuner-to-locales-cleanup/SKILL.md +4 -4
- data/skills/copy-tuner-to-locales-migrate-prefix/references/local-first-regexp.md +4 -9
- data/skills/copy-tuner-to-t-migrate/SKILL.md +131 -0
- data/skills/copy-tuner-to-t-migrate/scripts/migrate_tt.rb +189 -0
- data/spec/copy_tuner_client/cache_spec.rb +2 -10
- data/spec/copy_tuner_client/client_spec.rb +1 -0
- data/spec/copy_tuner_client/configuration_spec.rb +16 -16
- data/spec/copy_tuner_client/copyray/marker_spec.rb +41 -0
- data/spec/copy_tuner_client/copyray/rewriter_spec.rb +216 -0
- data/spec/copy_tuner_client/copyray_middleware_spec.rb +88 -0
- data/spec/copy_tuner_client/copyray_spec.rb +22 -39
- data/spec/copy_tuner_client/helper_extension_spec.rb +18 -5
- data/spec/copy_tuner_client/i18n_backend_spec.rb +8 -15
- data/spec/support/client_spec_helpers.rb +0 -1
- data/src/copyray-overlay.ts +125 -0
- data/src/copytuner-bar.ts +153 -0
- data/src/main.ts +33 -26
- data/src/{copyray.css → styles.ts} +112 -136
- data/src/util.ts +9 -1
- data/tsconfig.json +5 -10
- data/vite.config.ts +0 -1
- metadata +15 -8
- data/.eslintrc.js +0 -12
- data/app/assets/stylesheets/copytuner.css +0 -1
- data/src/copyray.ts +0 -130
- data/src/copytuner_bar.ts +0 -115
- data/src/specimen.ts +0 -84
- data/yarn.lock +0 -2540
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: copy-tuner-to-t-migrate
|
|
3
|
+
description: >-
|
|
4
|
+
copy_tuner_client v2.0.0 への移行で、削除された独自ヘルパー tt の呼び出しを Rails 標準の t(translate)へ
|
|
5
|
+
置換するスキル。tt は PR #122 で存在理由を失い gem から削除されたため、残った tt(...) は NoMethodError に
|
|
6
|
+
なる。大半は機械的に tt( → t( で済むが、訳文を文字列加工している箇所だけは t ではなく I18n.t へ移す必要があり
|
|
7
|
+
(開発環境のマーカートークン混入バグ対策)、そこは 1 件ずつ確認する。対象は app 配下の .rb / .haml / .erb。
|
|
8
|
+
disable-model-invocation: true
|
|
9
|
+
license: MIT
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# tt → t 移行スキル(copy_tuner_client v2.0.0)
|
|
13
|
+
|
|
14
|
+
copy_tuner_client がかつて `ActionView` にフックして生やしていた独自ヘルパー **`tt`**(シグネチャは
|
|
15
|
+
`tt(key, **options)`、引数は `t` と完全同一)を、Rails 標準の `t`(`translate`)へ置き換えるワークフロー。
|
|
16
|
+
gem を v2.0.0 に上げたアプリのビューに `tt(...)` が残っていると **`NoMethodError`(未定義ヘルパー)** になる。
|
|
17
|
+
|
|
18
|
+
このスキルは**破壊的な一括書き換え**を含むため、ユーザが明示的に呼んだときだけ動く(`disable-model-invocation`)。
|
|
19
|
+
|
|
20
|
+
## なぜ tt を t へ置換するのか
|
|
21
|
+
|
|
22
|
+
`tt` はもともと「**copyray マーカーを注入しない生の訳文を取る**」ためのヘルパーだった。旧方式では `t('key')` の
|
|
23
|
+
戻り値そのものにマーカーが埋め込まれ、`truncate` / `length` 等で**訳文を文字列加工する箇所**でマーカー長が
|
|
24
|
+
混入して壊れる問題があり、その対策が `tt` だった。
|
|
25
|
+
|
|
26
|
+
PR #122 のマーカー方式刷新で、マーカー注入は戻り値ではなく middleware(`CopyrayMiddleware` → `Rewriter`)で
|
|
27
|
+
行い HTML 配信前に完全除去するようになった。これにより通常の `t` がどこでも安全に使えるようになり、`tt` は
|
|
28
|
+
存在理由を失って削除された。背景の詳細は `skills/copy-tuner/SKILL.md` の「翻訳ヘルパー(`t`)」と
|
|
29
|
+
「落とし穴: 開発環境で訳文にマーカートークンが混入する」を参照。
|
|
30
|
+
|
|
31
|
+
**ただし罠が一つ残る。** マーカー除去は「最終的に HTML へ出力される訳文」に対してのみ効く。development で
|
|
32
|
+
`t('key')` の戻り値を**ビュー内で文字列加工する**コードは、除去前のマーカー込み文字列に作用してしまう(本番では
|
|
33
|
+
middleware 自体が無いので再現しない=開発環境だけ壊れる)。
|
|
34
|
+
|
|
35
|
+
→ つまり **`tt` を素朴に `t` へ変えてよいのは「文字列加工していない箇所」だけ**。文字列加工している `tt` を
|
|
36
|
+
`t` にすると、`tt` がもともと潰していたバグをそのまま復活させてしまう。そこは middleware のラッパーを通らない
|
|
37
|
+
**`I18n.t`(絶対キー)** へ移すのが正解で、機械的には決められないため 1 件ずつ確認する。
|
|
38
|
+
|
|
39
|
+
## 変換の 3 分類
|
|
40
|
+
|
|
41
|
+
| 分類 | 例 | 扱い |
|
|
42
|
+
|---|---|---|
|
|
43
|
+
| **safe** | `tt('views.foo')` / `= tt '.title'` / `tt(key, default: x)` | `tt(` → `t(` に決定論的一括変換 |
|
|
44
|
+
| **suspicious** | `truncate(tt('k'))` / `tt('k').length` / `tt('k')[0..n]` / `tt('k') =~ /re/` / `tt('k').gsub(...)` 等 | `I18n.t('絶対キー')` へ。**1 件ずつ確認**。スクリプトは触らない |
|
|
45
|
+
| **other** | `def tt` / `alias tt`(定義側)/ app 外(`lib/` 等)/ `tt` を含む別識別子の誤検出 | 最後にまとめて提示。自動変換しない |
|
|
46
|
+
|
|
47
|
+
**重要(定義側の罠):** アプリが v2.0.0 を待つ間の後方互換として `ApplicationHelper` に `def tt(key, **) = t(key, **)`
|
|
48
|
+
のような **`tt` の定義**を生やしていることがある。これは「呼び出し」ではないので `t(` へ機械置換すると Rails の `t`
|
|
49
|
+
を再定義して破滅する。スクリプトは `def tt` / `alias tt` を **other** に隔離して自動変換しない。**呼び出しを全て
|
|
50
|
+
`t` / `I18n.t` へ移し終えた後**に、この定義を手で削除する(順序を逆にすると呼び出しが `NoMethodError` になる)。
|
|
51
|
+
|
|
52
|
+
判定はヒューリスティック(完全な AST ではない)。**誤判定のコストが非対称**なので疑わしきは suspicious に倒す:
|
|
53
|
+
safe を取りこぼす(本当は怪しいのに safe)とマーカー混入バグが再発して重大、suspicious を過剰検出しても人間が
|
|
54
|
+
1 件確認するだけで軽微。
|
|
55
|
+
|
|
56
|
+
## ワークフロー
|
|
57
|
+
|
|
58
|
+
### 1. 利用箇所の洗い出しと 3 分類
|
|
59
|
+
|
|
60
|
+
同梱スクリプトで `app/` 配下の `.rb` / `.haml` / `.erb` を走査し、3 分類でレポートする(変更は加えない)。
|
|
61
|
+
Rails context は不要なので素の `ruby` で動く。
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
ruby skills/copy-tuner-to-t-migrate/scripts/migrate_tt.rb --report
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
safe / suspicious / other の件数と該当箇所(`ファイル:行: コード`)が出る。まず全体像をユーザと共有する。
|
|
68
|
+
|
|
69
|
+
### 2. safe な箇所を一括変換
|
|
70
|
+
|
|
71
|
+
safe 分類は引数がそのまま通る(`tt(key, **options)` → `t(key, **options)`)ので決定論的に置換できる。
|
|
72
|
+
**件数をユーザに伝え、承認を得てから**適用する(`--apply-safe` はファイルを直接書き換えるため)。
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
ruby skills/copy-tuner-to-t-migrate/scripts/migrate_tt.rb --apply-safe
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
suspicious / other の行は**一切触らない**(行番号で限定しているため)。適用後に `git diff` を見せて、safe 行
|
|
79
|
+
だけが `t(` になったことを確認してもらう。
|
|
80
|
+
|
|
81
|
+
### 3. suspicious な箇所を 1 件ずつ確認
|
|
82
|
+
|
|
83
|
+
ここが人間の判断が要る核心。手順 1 の suspicious 各件について、次を 1 件ずつ提示して確認を取る:
|
|
84
|
+
|
|
85
|
+
- 該当コード(`ファイル:行`)と、なぜ怪しいか(戻り値を文字列加工している=マーカー混入の危険)
|
|
86
|
+
- 提案する置換: `t(...)` ではなく **`I18n.t('絶対キー')`**(`I18n` モジュール直呼びはラッパーを通らずマーカーが付かない)
|
|
87
|
+
|
|
88
|
+
注意点:
|
|
89
|
+
- **たとえユーザが「全部まとめて I18n.t にして」と言っても、suspicious は必ず 1 件ずつ提示する。** 相対キーを含む件は絶対キーをユーザと確認しないと置換できないためで、省略すると誤ったキーを書き込む危険がある。
|
|
90
|
+
- **相対キー(先頭ドット `tt('.foo')`)は `I18n.t` では解決できない。** 絶対キー(`I18n.t('views.foo.bar')`)へ
|
|
91
|
+
書き換える必要があり、partial のパスからキーを補う判断が要る。スクリプトはこの相対キーの suspicious を
|
|
92
|
+
`相対キー(.foo)は絶対キーへ書き換えが必要` と強調表示するので、特に丁寧に確認する。ヘルパーや
|
|
93
|
+
コントローラ(`app/helpers/`, `app/controllers/`)に相対キーがある場合は、呼び出し元のビューのパスを
|
|
94
|
+
調べて基点キーを特定するか、ユーザに直接絶対キーを確認する。絶対キーが不明なときは
|
|
95
|
+
`grep -r "キー末尾部分" config/locales/` でロケールファイルを検索して候補を絞り込む。
|
|
96
|
+
- 本当に文字列加工していない(誤検出)なら、その場合は `t(...)` でよい。ユーザの判断に従う。
|
|
97
|
+
|
|
98
|
+
確認が取れた箇所だけ Edit で個別に置換する(スクリプトでは変換しない)。
|
|
99
|
+
|
|
100
|
+
### 4. その他ヒット(定義側・app 外・誤検出)を提示
|
|
101
|
+
|
|
102
|
+
手順 1 の other をまとめて提示する。
|
|
103
|
+
|
|
104
|
+
- **`def tt` / `alias tt`(定義側)**: 後方互換シムが残っていることが多い。**呼び出しを全て移し終えてから手で削除**する
|
|
105
|
+
(3 分類表の「定義側の罠」参照)。早く消すと残った呼び出しが `NoMethodError` になる。
|
|
106
|
+
- **app 外(`lib/` のヘルパーやコントローラ等)**: ビューヘルパー `tt` がそのスコープから見えるかは別問題なので、
|
|
107
|
+
自動変換せずユーザに判断を委ねる。
|
|
108
|
+
|
|
109
|
+
念のため、スクリプトの単語境界判定をすり抜けた可能性に備えて素朴な grep でも最終確認する:
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
git grep -nwI tt -- app # -I でバイナリ(画像等)の誤マッチを除外
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### 5. 検証
|
|
116
|
+
|
|
117
|
+
- `git grep -nwI tt -- app` の残りが「未対応の suspicious + まだ消していない定義側」だけであること
|
|
118
|
+
(safe は 0 件、`I18n.t` 化した suspicious も `tt` としては消える)。
|
|
119
|
+
- 対象アプリで `bundle exec rspec`(または該当アプリのテスト)を回し、`NoMethodError` が出ないこと。
|
|
120
|
+
- development で実画面を開き、文字列加工していた箇所にマーカートークン(`⟦CT:...⟧`)が混入していないこと。
|
|
121
|
+
|
|
122
|
+
## スクリプトの責務(決定論的な範囲のみ)
|
|
123
|
+
|
|
124
|
+
`scripts/migrate_tt.rb` は**機械的に確定できる部分だけ**を担う:
|
|
125
|
+
|
|
126
|
+
- `--report`(既定): 3 分類を出力。`--json` で機械可読出力も可能。**ファイルは変更しない**。
|
|
127
|
+
- `--apply-safe`: safe 分類のみ `tt(` → `t(` を適用。suspicious / other は触らない。
|
|
128
|
+
- `--root DIR`: リポジトリルート指定(既定はカレント)。
|
|
129
|
+
|
|
130
|
+
suspicious の確定変換(`I18n.t` 化・相対→絶対キー)は機械的に決められないため**スクリプトは行わない**。
|
|
131
|
+
必ず手順 3 で人間が 1 件ずつ確認する。
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# copy_tuner_client v2.0.0 移行: tt → t 置換スクリプト(SKILL.md 手順 1〜2)。
|
|
4
|
+
#
|
|
5
|
+
# copy_tuner_client が生やしていた独自ヘルパー tt は PR #122 で削除された。gem を上げた
|
|
6
|
+
# アプリのビューに tt(...) が残ると NoMethodError になるため t へ移す。大半は機械的に
|
|
7
|
+
# tt( → t( で済むが、訳文を文字列加工している箇所だけは素朴に t へ変えると開発環境で
|
|
8
|
+
# マーカートークン混入バグが再発する(skills/copy-tuner/SKILL.md の罠を参照)。そこは
|
|
9
|
+
# I18n.t(絶対キー)へ変換すべきなので、このスクリプトは自動変換せず「怪しい」として抽出する。
|
|
10
|
+
#
|
|
11
|
+
# Rails context は不要なので素の ruby で動く:
|
|
12
|
+
#
|
|
13
|
+
# ruby skills/copy-tuner-to-t-migrate/scripts/migrate_tt.rb --report
|
|
14
|
+
# ruby skills/copy-tuner-to-t-migrate/scripts/migrate_tt.rb --apply-safe
|
|
15
|
+
#
|
|
16
|
+
# 出力する 3 分類:
|
|
17
|
+
# (1) safe : 文字列加工されていない tt 呼び出し。tt( → t( に決定論的変換できる。
|
|
18
|
+
# (2) suspicious : 戻り値が同一行で文字列加工されている tt 呼び出し。I18n.t 提案・要確認。
|
|
19
|
+
# (3) other : app 外 / tt を含む別識別子の誤検出など。自動変換せず目視で確認。
|
|
20
|
+
#
|
|
21
|
+
# 判定はヒューリスティック(完全な AST 解析ではない)。誤判定のコストは
|
|
22
|
+
# safe 誤分類(怪しいのに safe)= マーカー混入バグ再発 → 重大
|
|
23
|
+
# suspicious 誤分類(安全なのに suspicious)= 人間が 1 件確認するだけ → 軽微
|
|
24
|
+
# なので、疑わしきは suspicious に倒す。
|
|
25
|
+
|
|
26
|
+
require 'optparse'
|
|
27
|
+
|
|
28
|
+
# NOTE: 中断を呼び出し側(Bash 手順)へ確実に伝えるため warn + exit(1) を使う。
|
|
29
|
+
def die(message)
|
|
30
|
+
warn message
|
|
31
|
+
exit(1)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# ---- 引数 ----
|
|
35
|
+
options = { mode: :report, root: '.' }
|
|
36
|
+
parser =
|
|
37
|
+
OptionParser.new do |opts|
|
|
38
|
+
opts.banner = 'Usage: ruby migrate_tt.rb [--report | --apply-safe] [--root DIR]'
|
|
39
|
+
opts.on('--report', '3 分類を出力する(既定)。ファイルは変更しない') { options[:mode] = :report }
|
|
40
|
+
opts.on('--apply-safe', 'safe 分類のみ tt( → t( を実ファイルに適用する') { options[:mode] = :apply_safe }
|
|
41
|
+
opts.on('--root DIR', 'リポジトリルート(既定: カレントディレクトリ)') { |v| options[:root] = v }
|
|
42
|
+
opts.on('--json', 'report を JSON で出力する(機械処理用)') { options[:json] = true }
|
|
43
|
+
opts.on('-h', '--help', 'ヘルプ') do
|
|
44
|
+
puts opts
|
|
45
|
+
exit(0)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
parser.parse!(ARGV)
|
|
49
|
+
|
|
50
|
+
ROOT = File.expand_path(options[:root])
|
|
51
|
+
die("ルートが見つからない: #{ROOT}") unless File.directory?(ROOT)
|
|
52
|
+
|
|
53
|
+
# 対象: app 配下の .rb / .haml / .erb。それ以外のヒットは other 扱いで提示のみ。
|
|
54
|
+
APP_GLOB = File.join(ROOT, 'app', '**', '*.{rb,haml,erb}')
|
|
55
|
+
# app 外も走査して other に入れる(lib/ 等の取りこぼし提示用)。
|
|
56
|
+
ALL_GLOB = File.join(ROOT, '**', '*.{rb,haml,erb}')
|
|
57
|
+
|
|
58
|
+
# 単語境界の tt 呼び出し。tt( だけでなく haml の `= tt '...'`(括弧なし)も拾う。
|
|
59
|
+
# 直前が識別子文字(attr/http/setting 等の一部や `.tt` のメソッド呼び出し)でないことを (?<![\w.]) で担保。
|
|
60
|
+
TT_CALL = /(?<![\w.])tt(\s*\(|\s+['":@])/
|
|
61
|
+
|
|
62
|
+
# tt の「定義」側(呼び出しではない)。アプリが後方互換で `def tt` を生やしている場合がある。
|
|
63
|
+
# これを safe として t( へ機械置換すると Rails の t を再定義してしまい破滅的なので、定義は別扱いにする。
|
|
64
|
+
TT_DEFINITION = /\b(?:def\s+(?:self\.)?tt\b|alias(?:_method)?\s+:?tt\b|alias(?:_method)?\s+['"]tt['"])/
|
|
65
|
+
|
|
66
|
+
# tt(...) の戻り値が同一行で文字列加工されているかの判定に使う語彙。
|
|
67
|
+
STRING_HELPERS = %w[truncate simple_format strip_tags highlight excerpt word_wrap].freeze
|
|
68
|
+
STRING_METHODS = %w[
|
|
69
|
+
length size bytesize slice first last truncate truncate_words
|
|
70
|
+
gsub sub gsub! sub! strip lstrip rstrip chomp chop chars bytes lines
|
|
71
|
+
scan match match? =~ start_with? end_with? include? index rindex
|
|
72
|
+
upcase downcase capitalize ljust rjust center delete squeeze tr
|
|
73
|
+
to_i to_f to_sym html_safe
|
|
74
|
+
].freeze
|
|
75
|
+
|
|
76
|
+
# (a) truncate( ... tt( ... ) のように文字列加工ヘルパーの引数になっている
|
|
77
|
+
STRING_HELPER_RE = /\b(?:#{STRING_HELPERS.join('|')})\s*\(\s*[^)]*\btt\b/
|
|
78
|
+
# (b) tt(...) または tt '...' の直後に .method / =~ / [ が続く。引数の閉じ括弧をまたぐ単純近似。
|
|
79
|
+
STRING_METHOD_RE = /\btt\b\s*(?:\([^\n]*?\)|['":@][^\n,]*)\s*(?:\.\s*(?:#{STRING_METHODS.join('|')})\b|=~|\[)/
|
|
80
|
+
|
|
81
|
+
# 先頭ドットの相対キー tt('.foo') は I18n.t では解決できず絶対キー化が要る → 強調表示用。
|
|
82
|
+
RELATIVE_KEY_RE = /\btt\b\s*(?:\(\s*)?['":]\s*\./
|
|
83
|
+
|
|
84
|
+
def relative(path)
|
|
85
|
+
path.sub("#{ROOT}/", '')
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
safe = []
|
|
89
|
+
suspicious = []
|
|
90
|
+
other = []
|
|
91
|
+
|
|
92
|
+
app_set = Dir.glob(APP_GLOB).to_set
|
|
93
|
+
|
|
94
|
+
# 1 ファイル分を走査し、ヒット行を safe / suspicious / other へ振り分ける。
|
|
95
|
+
def scan_file(path, in_app:)
|
|
96
|
+
rel = relative(path)
|
|
97
|
+
File.readlines(path).each_with_index do |line, idx|
|
|
98
|
+
next unless line =~ TT_CALL
|
|
99
|
+
|
|
100
|
+
entry = { file: rel, lineno: idx + 1, code: line.rstrip }
|
|
101
|
+
yield(classify_line(line, entry, in_app: in_app))
|
|
102
|
+
end
|
|
103
|
+
rescue StandardError
|
|
104
|
+
nil
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# ヒット 1 行を分類し、[:safe | :suspicious | :other, entry] を返す。
|
|
108
|
+
def classify_line(line, entry, in_app:)
|
|
109
|
+
# tt の定義(def/alias)は呼び出しではない。t( へ置換すると Rails の t を壊すので絶対に自動変換しない。
|
|
110
|
+
if line.match?(TT_DEFINITION)
|
|
111
|
+
entry[:reason] = 'tt の定義(def/alias)。自動変換禁止。呼び出しを全て t へ移した後に手動で削除する'
|
|
112
|
+
return [:other, entry]
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
unless in_app
|
|
116
|
+
entry[:reason] = 'app 外(要目視)'
|
|
117
|
+
return [:other, entry]
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
return [:safe, entry] unless line.match?(STRING_HELPER_RE) || line.match?(STRING_METHOD_RE)
|
|
121
|
+
|
|
122
|
+
entry[:relative_key] = line.match?(RELATIVE_KEY_RE)
|
|
123
|
+
entry[:hint] = if entry[:relative_key]
|
|
124
|
+
'I18n.t へ。相対キー(.foo)は絶対キーへ書き換えが必要'
|
|
125
|
+
else
|
|
126
|
+
'I18n.t(絶対キー) へ置換を検討'
|
|
127
|
+
end
|
|
128
|
+
[:suspicious, entry]
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
buckets = { safe: safe, suspicious: suspicious, other: other }
|
|
132
|
+
Dir.glob(ALL_GLOB).each do |path|
|
|
133
|
+
# vendor / node_modules / tmp 等のノイズを除外
|
|
134
|
+
next if relative(path).start_with?('vendor/', 'node_modules/', 'tmp/', '.git/')
|
|
135
|
+
|
|
136
|
+
scan_file(path, in_app: app_set.include?(path)) do |bucket, entry|
|
|
137
|
+
buckets[bucket] << entry
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
if options[:json]
|
|
142
|
+
require 'json'
|
|
143
|
+
puts JSON.pretty_generate(safe: safe, suspicious: suspicious, other: other)
|
|
144
|
+
exit(0)
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def print_section(title, entries)
|
|
148
|
+
puts "\n== #{title} (#{entries.size}) =="
|
|
149
|
+
entries.each do |e|
|
|
150
|
+
suffix = if e[:hint]
|
|
151
|
+
" # #{e[:hint]}"
|
|
152
|
+
else
|
|
153
|
+
(e[:reason] ? " # #{e[:reason]}" : '')
|
|
154
|
+
end
|
|
155
|
+
puts "#{e[:file]}:#{e[:lineno]}: #{e[:code].strip}#{suffix}"
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
if options[:mode] == :report
|
|
160
|
+
print_section('safe(tt( → t( に決定論的変換できる)', safe)
|
|
161
|
+
print_section('suspicious(文字列加工あり・I18n.t 提案・要 1 件ずつ確認)', suspicious)
|
|
162
|
+
print_section('other(app 外・誤検出・要目視)', other)
|
|
163
|
+
puts "\n合計: safe=#{safe.size} suspicious=#{suspicious.size} other=#{other.size}"
|
|
164
|
+
exit(0)
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# ---- --apply-safe: safe 分類のみ tt( → t( を適用 ----
|
|
168
|
+
# 同一ファイル内の safe 行だけを対象に、単語境界の tt を t へ置換する。
|
|
169
|
+
# suspicious / other 行は触らない(行番号で限定)。
|
|
170
|
+
by_file = safe.group_by { |e| e[:file] }
|
|
171
|
+
changed = 0
|
|
172
|
+
by_file.each do |rel, entries|
|
|
173
|
+
path = File.join(ROOT, rel)
|
|
174
|
+
lines = File.readlines(path)
|
|
175
|
+
target_linenos = entries.to_set { |e| e[:lineno] }
|
|
176
|
+
target_linenos.each do |lineno|
|
|
177
|
+
line = lines[lineno - 1]
|
|
178
|
+
# safe 行内の tt 呼び出しのみ t へ。tt('...') や tt(... の tt を t に。
|
|
179
|
+
new_line = line.gsub(TT_CALL) { "t#{Regexp.last_match(1)}" }
|
|
180
|
+
if new_line != line
|
|
181
|
+
lines[lineno - 1] = new_line
|
|
182
|
+
changed += 1
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
File.write(path, lines.join)
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
puts "safe 置換を適用: #{changed} 箇所 / #{by_file.size} ファイル"
|
|
189
|
+
puts "残った suspicious=#{suspicious.size} other=#{other.size} は手作業で対応すること(--report で再確認)"
|
|
@@ -24,16 +24,6 @@ describe 'CopyTunerClient::Cache' do
|
|
|
24
24
|
expect(cache.keys).to match_array(%w[en.test.key en.test.other_key])
|
|
25
25
|
end
|
|
26
26
|
|
|
27
|
-
it 'exclude_key_regexpが設定されている場合、該当データを除外すること' do
|
|
28
|
-
cache = build_cache(exclude_key_regexp: /^en\.test\.other_key$/)
|
|
29
|
-
cache['en.test.key'] = 'expected'
|
|
30
|
-
cache['en.test.other_key'] = 'expected'
|
|
31
|
-
|
|
32
|
-
cache.download
|
|
33
|
-
|
|
34
|
-
expect(cache.queued.keys).to match_array(%w[en.test.key])
|
|
35
|
-
end
|
|
36
|
-
|
|
37
27
|
it 'local_first_key_regexpにマッチするキー(locale除く)はアップロードキューに入れないこと' do
|
|
38
28
|
cache = build_cache(local_first_key_regexp: /\Aviews\./)
|
|
39
29
|
cache['ja.views.foo'] = 'local value'
|
|
@@ -284,6 +274,7 @@ describe 'CopyTunerClient::Cache' do
|
|
|
284
274
|
it 'トップレベルからflushできること' do
|
|
285
275
|
cache = build_cache
|
|
286
276
|
CopyTunerClient.configure do |config|
|
|
277
|
+
config.project_id = 1
|
|
287
278
|
config.cache = cache
|
|
288
279
|
end
|
|
289
280
|
expect(cache).to receive(:flush).at_least(:once)
|
|
@@ -410,6 +401,7 @@ describe 'CopyTunerClient::Cache' do
|
|
|
410
401
|
|
|
411
402
|
it 'トップレベル定数から呼び出せること' do
|
|
412
403
|
CopyTunerClient.configure do |config|
|
|
404
|
+
config.project_id = 1
|
|
413
405
|
config.cache = cache
|
|
414
406
|
end
|
|
415
407
|
expect(cache).to receive(:export)
|
|
@@ -253,27 +253,24 @@ describe CopyTunerClient::Configuration do
|
|
|
253
253
|
end
|
|
254
254
|
end
|
|
255
255
|
|
|
256
|
-
describe '
|
|
257
|
-
let(:config)
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
it 'warns when a value is set' do
|
|
263
|
-
expect(deprecator).to receive(:warn).with(/exclude_key_regexp is deprecated/)
|
|
264
|
-
|
|
265
|
-
config.exclude_key_regexp = /\Aja\.views\./
|
|
256
|
+
describe 'project_id の必須化' do
|
|
257
|
+
let(:config) do
|
|
258
|
+
config = CopyTunerClient::Configuration.new
|
|
259
|
+
config.api_key = 'abc123'
|
|
260
|
+
config
|
|
266
261
|
end
|
|
267
262
|
|
|
268
|
-
it '
|
|
269
|
-
config.
|
|
270
|
-
expect(config.exclude_key_regexp).to eq(/\Aja\.views\./)
|
|
263
|
+
it 'project_id が未設定のとき apply が ArgumentError を出すこと' do
|
|
264
|
+
expect { config.apply }.to raise_error(ArgumentError, 'project_id is required')
|
|
271
265
|
end
|
|
272
266
|
|
|
273
|
-
it '
|
|
274
|
-
expect
|
|
267
|
+
it 'project_id が未設定のとき project_url が ArgumentError を出すこと' do
|
|
268
|
+
expect { config.project_url }.to raise_error(ArgumentError, 'project_id is required')
|
|
269
|
+
end
|
|
275
270
|
|
|
276
|
-
|
|
271
|
+
it 'project_id を設定すると project_url がそれを使った URL を返すこと' do
|
|
272
|
+
config.project_id = 77
|
|
273
|
+
expect(config.project_url).to include('/projects/77')
|
|
277
274
|
end
|
|
278
275
|
end
|
|
279
276
|
end
|
|
@@ -294,6 +291,9 @@ shared_context 'stubbed configuration' do
|
|
|
294
291
|
allow(CopyTunerClient::Poller).to receive(:new).and_return(poller)
|
|
295
292
|
allow(CopyTunerClient::ProcessGuard).to receive(:new).and_return(process_guard)
|
|
296
293
|
subject.logger = logger
|
|
294
|
+
# NOTE: apply は project_id 必須になったため、未設定だと raise する。applied 系テストは
|
|
295
|
+
# project_id 自体を検証しないので適当な値を補っておく
|
|
296
|
+
subject.project_id ||= 1
|
|
297
297
|
apply
|
|
298
298
|
end
|
|
299
299
|
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
require 'copy_tuner_client/copyray/marker'
|
|
3
|
+
|
|
4
|
+
describe CopyTunerClient::Copyray::Marker do
|
|
5
|
+
describe '.encode' do
|
|
6
|
+
it 'キーを可視トークンの区切り記号で囲む' do
|
|
7
|
+
expect(described_class.encode('views.home.index.message')).to eq '⟦CT:views.home.index.message⟧'
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
it 'ドット・アンダースコア・スラッシュを含むキーをそのまま保持する' do
|
|
11
|
+
expect(described_class.encode('en.foo_bar.baz/qux')).to eq '⟦CT:en.foo_bar.baz/qux⟧'
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
it 'マルチバイトキーをそのまま保持する' do
|
|
15
|
+
expect(described_class.encode('ja.見出し')).to eq '⟦CT:ja.見出し⟧'
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
describe '::SCAN_REGEXP' do
|
|
20
|
+
it 'エンコード済みトークンからキーを取り出す' do
|
|
21
|
+
key = 'views.home.index.message'
|
|
22
|
+
match = described_class::SCAN_REGEXP.match(described_class.encode(key))
|
|
23
|
+
expect(match[1]).to eq key
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
it 'ドット・アンダースコア・スラッシュ・マルチバイトを含むキーを取り出す' do
|
|
27
|
+
key = 'ja.foo_bar.baz/qux.見出し'
|
|
28
|
+
match = described_class::SCAN_REGEXP.match(described_class.encode(key))
|
|
29
|
+
expect(match[1]).to eq key
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
it '非貪欲マッチで隣接するトークンを分離する' do
|
|
33
|
+
text = "#{described_class.encode('a.b')}hello#{described_class.encode('c.d')}"
|
|
34
|
+
expect(text.scan(described_class::SCAN_REGEXP).flatten).to eq ['a.b', 'c.d']
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
it 'プレーンテキストにはマッチしない' do
|
|
38
|
+
expect('just a normal sentence with CT: and brackets [x]').not_to match described_class::SCAN_REGEXP
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|