copy_tuner_client 1.5.0 → 2.0.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 +2 -3
- data/README.md +0 -25
- data/UPGRADING.md +201 -0
- data/app/assets/javascripts/copytuner.js +78 -73
- data/app/assets/stylesheets/copytuner.css +1 -1
- 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 -2
- 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/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 +89 -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.css +11 -0
- data/src/copyray.ts +10 -29
- data/src/copytuner_bar.ts +15 -1
- data/src/main.ts +5 -2
- data/src/specimen.ts +17 -7
- metadata +9 -1
|
@@ -10,7 +10,7 @@ require 'copy_tuner_client/copyray_middleware'
|
|
|
10
10
|
|
|
11
11
|
module CopyTunerClient
|
|
12
12
|
# Used to set up and modify settings for the client.
|
|
13
|
-
class Configuration
|
|
13
|
+
class Configuration # rubocop:disable Metrics/ClassLength
|
|
14
14
|
# These options will be present in the Hash returned by {#to_hash}.
|
|
15
15
|
OPTIONS = %i[api_key development_environments environment_name host
|
|
16
16
|
http_open_timeout http_read_timeout client_name client_url
|
|
@@ -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
|
|
21
|
+
ca_file local_first_key_regexp s3_host locales ignored_keys ignored_key_handler
|
|
22
22
|
download_cache_dir].freeze
|
|
23
23
|
|
|
24
24
|
# NOTE: Rails 標準ロケールで非文字列値(precision: Integer, significant: Boolean,
|
|
@@ -43,7 +43,8 @@ module CopyTunerClient
|
|
|
43
43
|
attr_accessor :host
|
|
44
44
|
|
|
45
45
|
# @return [Fixnum] The port on which your CopyTuner server runs (defaults to +443+ for secure connections, +80+ for insecure connections).
|
|
46
|
-
|
|
46
|
+
# NOTE: reader は default_port フォールバック付きの明示定義(#port)があるため attr_accessor を使わない
|
|
47
|
+
attr_writer :port
|
|
47
48
|
|
|
48
49
|
# @return [Boolean] +true+ for https connections, +false+ for http connections.
|
|
49
50
|
attr_accessor :secure
|
|
@@ -94,7 +95,8 @@ module CopyTunerClient
|
|
|
94
95
|
attr_accessor :polling_delay
|
|
95
96
|
|
|
96
97
|
# @return [Integer] The time, in seconds, in between each sync to the server in development. Defaults to +60+.
|
|
97
|
-
|
|
98
|
+
# NOTE: reader は environment で分岐する明示定義(#sync_interval)があるため attr_accessor を使わない
|
|
99
|
+
attr_writer :sync_interval
|
|
98
100
|
|
|
99
101
|
# @return [Integer] The time, in seconds, in between each sync to the server in development. Defaults to +60+.
|
|
100
102
|
attr_accessor :sync_interval_staging
|
|
@@ -128,10 +130,6 @@ module CopyTunerClient
|
|
|
128
130
|
|
|
129
131
|
attr_accessor :poller
|
|
130
132
|
|
|
131
|
-
# @return [Regexp] Regular expression to exclude keys.
|
|
132
|
-
# @deprecated Use {#local_first_key_regexp} instead.
|
|
133
|
-
attr_reader :exclude_key_regexp
|
|
134
|
-
|
|
135
133
|
# @return [Regexp] Keys (without locale) matching this regexp bypass the
|
|
136
134
|
# copy_tuner cache and are looked up from local config/locales
|
|
137
135
|
# (I18n::Backend::Simple) first. Used for gradual migration from
|
|
@@ -141,15 +139,12 @@ module CopyTunerClient
|
|
|
141
139
|
# @return [String] The S3 host to connect to (defaults to +copy-tuner-us.s3.amazonaws.com+).
|
|
142
140
|
attr_accessor :s3_host
|
|
143
141
|
|
|
144
|
-
# @return [Boolean] To disable Copyray
|
|
142
|
+
# @return [Boolean] To disable Copyray marker injection, set true
|
|
145
143
|
attr_accessor :disable_copyray_comment_injection
|
|
146
144
|
|
|
147
145
|
# @return [Array<Symbol>] Restrict blurb locales to upload
|
|
148
146
|
attr_accessor :locales
|
|
149
147
|
|
|
150
|
-
# @return [Boolean] Html escape
|
|
151
|
-
attr_accessor :html_escape
|
|
152
|
-
|
|
153
148
|
# @return [Array<String>] A list of ignored keys
|
|
154
149
|
attr_accessor :ignored_keys
|
|
155
150
|
|
|
@@ -165,7 +160,7 @@ module CopyTunerClient
|
|
|
165
160
|
alias secure? secure
|
|
166
161
|
|
|
167
162
|
# Instantiated from {CopyTunerClient.configure}. Sets defaults.
|
|
168
|
-
def initialize
|
|
163
|
+
def initialize # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
|
169
164
|
self.client_name = 'CopyTuner Client'
|
|
170
165
|
self.client_url = 'https://rubygems.org/gems/copy_tuner_client'
|
|
171
166
|
self.client_version = VERSION
|
|
@@ -182,7 +177,6 @@ module CopyTunerClient
|
|
|
182
177
|
self.upload_disabled_environments = %w[production staging]
|
|
183
178
|
self.s3_host = 'copy-tuner.sg-apps.com' # NOTE: cloudfront host
|
|
184
179
|
self.disable_copyray_comment_injection = false
|
|
185
|
-
self.html_escape = true
|
|
186
180
|
self.ignored_keys = []
|
|
187
181
|
self.ignored_key_handler = ->(e) { raise e }
|
|
188
182
|
self.local_first_key_regexp = nil
|
|
@@ -197,7 +191,7 @@ module CopyTunerClient
|
|
|
197
191
|
# @param [Symbol] option Key for a given attribute
|
|
198
192
|
# @return [Object] the given attribute
|
|
199
193
|
def [](option)
|
|
200
|
-
|
|
194
|
+
public_send(option)
|
|
201
195
|
end
|
|
202
196
|
|
|
203
197
|
# Returns a hash of all configurable options
|
|
@@ -206,7 +200,7 @@ module CopyTunerClient
|
|
|
206
200
|
base_options = { public: public?, upload_disabled: upload_disabled? }
|
|
207
201
|
|
|
208
202
|
OPTIONS.inject(base_options) do |hash, option|
|
|
209
|
-
hash.merge option.to_sym =>
|
|
203
|
+
hash.merge option.to_sym => public_send(option)
|
|
210
204
|
end
|
|
211
205
|
end
|
|
212
206
|
|
|
@@ -258,7 +252,10 @@ module CopyTunerClient
|
|
|
258
252
|
# This creates the {I18nBackend} and puts them together.
|
|
259
253
|
#
|
|
260
254
|
# When {#test?} returns +false+, the poller will be started.
|
|
261
|
-
def apply
|
|
255
|
+
def apply # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
256
|
+
# NOTE: project_id は必須。未設定なら apply 時点で明示的に失敗させる
|
|
257
|
+
validate_project_id!
|
|
258
|
+
|
|
262
259
|
self.locales ||= self.locales = if defined?(::Rails)
|
|
263
260
|
::Rails.application.config.i18n.available_locales.presence || Array(::Rails.application.config.i18n.default_locale)
|
|
264
261
|
else
|
|
@@ -337,18 +334,6 @@ module CopyTunerClient
|
|
|
337
334
|
@api_key = api_key
|
|
338
335
|
end
|
|
339
336
|
|
|
340
|
-
# @deprecated Use {#local_first_key_regexp} instead.
|
|
341
|
-
def exclude_key_regexp=(value)
|
|
342
|
-
unless value.nil?
|
|
343
|
-
ActiveSupport::Deprecation.new.warn(
|
|
344
|
-
'exclude_key_regexp is deprecated and will be removed in a future release. ' \
|
|
345
|
-
'Use local_first_key_regexp instead (note: it matches keys WITHOUT the locale prefix, ' \
|
|
346
|
-
'e.g. /\Aviews\./ instead of /\Aja\.views\./).'
|
|
347
|
-
)
|
|
348
|
-
end
|
|
349
|
-
@exclude_key_regexp = value
|
|
350
|
-
end
|
|
351
|
-
|
|
352
337
|
# Sync interval for Rack Middleware
|
|
353
338
|
def sync_interval
|
|
354
339
|
if environment_name == 'staging'
|
|
@@ -358,16 +343,11 @@ module CopyTunerClient
|
|
|
358
343
|
end
|
|
359
344
|
end
|
|
360
345
|
|
|
361
|
-
# @return [String] current project url by
|
|
346
|
+
# @return [String] current project url by project_id
|
|
362
347
|
def project_url
|
|
363
|
-
|
|
364
|
-
if project_id
|
|
365
|
-
"/projects/#{project_id}"
|
|
366
|
-
else
|
|
367
|
-
ActiveSupport::Deprecation.new.warn('Please set project_id.')
|
|
368
|
-
"/projects/#{api_key}"
|
|
369
|
-
end
|
|
348
|
+
validate_project_id!
|
|
370
349
|
|
|
350
|
+
path = "/projects/#{project_id}"
|
|
371
351
|
URI::Generic.build(scheme: self.protocol, host: self.host, port: self.port.to_i, path:).to_s
|
|
372
352
|
end
|
|
373
353
|
|
|
@@ -389,6 +369,12 @@ module CopyTunerClient
|
|
|
389
369
|
|
|
390
370
|
private
|
|
391
371
|
|
|
372
|
+
# project_id は必須。未設定なら明示的に失敗させる。
|
|
373
|
+
# apply(起動時の全体検証)と project_url(apply を経ない経路へのセーフネット)の両方から呼ぶ。
|
|
374
|
+
def validate_project_id!
|
|
375
|
+
raise ArgumentError, 'project_id is required' if project_id.nil?
|
|
376
|
+
end
|
|
377
|
+
|
|
392
378
|
def default_port
|
|
393
379
|
if secure?
|
|
394
380
|
443
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
module CopyTunerClient
|
|
2
|
+
class Copyray
|
|
3
|
+
# 訳文に埋め込むキーマーカーの可視テキストトークン。
|
|
4
|
+
# サーバ側(CopyrayMiddleware の Rewriter)だけが encode/scan/strip すればよく、
|
|
5
|
+
# フロントは data 属性化された後の DOM を見るので decode は不要。
|
|
6
|
+
#
|
|
7
|
+
# 例: encode('views.home.message') #=> "⟦CT:views.home.message⟧"
|
|
8
|
+
module Marker
|
|
9
|
+
# NOTE: 通常の本文・属性値に出現しない記号(U+27E6 / U+27E7)と固定プレフィックス CT: で
|
|
10
|
+
# 偶発衝突を二重に下げる。除去漏れても可読なので人間が原因に気づける(不可視文字は不採用)。
|
|
11
|
+
PREFIX = '⟦CT:'.freeze
|
|
12
|
+
SUFFIX = '⟧'.freeze
|
|
13
|
+
|
|
14
|
+
# NOTE: PREFIX/SUFFIX から動的構築し、区切り変更時に encode と SCAN_REGEXP がズレないようにする。
|
|
15
|
+
SCAN_REGEXP = /#{Regexp.escape(PREFIX)}(.*?)#{Regexp.escape(SUFFIX)}/
|
|
16
|
+
|
|
17
|
+
module_function
|
|
18
|
+
|
|
19
|
+
def encode(key)
|
|
20
|
+
"#{PREFIX}#{key}#{SUFFIX}"
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
require 'nokogiri'
|
|
2
|
+
require 'copy_tuner_client/copyray/marker'
|
|
3
|
+
|
|
4
|
+
module CopyTunerClient
|
|
5
|
+
class Copyray
|
|
6
|
+
# 完成 HTML を走査し、マーカートークンを含むテキストノードの親要素・属性値を持つ要素に
|
|
7
|
+
# data-copyray-key 属性を付与し、トークンを完全に除去する。
|
|
8
|
+
# CopyrayMiddleware の HTML 後処理(CSS/JS 挿入)の前段で呼ばれる。
|
|
9
|
+
module Rewriter
|
|
10
|
+
DATA_ATTR = 'data-copyray-key'.freeze
|
|
11
|
+
|
|
12
|
+
# NOTE: マーカーを含む巨大 HTML を Nokogiri で parse/traverse/再シリアライズすると
|
|
13
|
+
# サイズに比例して重くなる(実測 ~1MB で 167ms, ~3MB で 514ms)。development 限定機能で
|
|
14
|
+
# レスポンスを大きく悪化させないため、この閾値超では Nokogiri を通さず可視トークン除去のみ行う。
|
|
15
|
+
# 失うのは data-copyray-key(オーバーレイ編集導線)だけで、フォールバックの gsub は巨大ページでも数ms。
|
|
16
|
+
MAX_REWRITE_BYTESIZE = 1_000_000
|
|
17
|
+
|
|
18
|
+
class << self
|
|
19
|
+
# NOTE: 戻り値は [html, skipped]。skipped は data-copyray-key を付与できなかったことを表す
|
|
20
|
+
# (巨大DOMでのスキップ・Nokogiri 例外の双方で true)。ミドルウェアがこれを JS に伝え、
|
|
21
|
+
# オーバーレイ非対応である旨をツールバーで案内する。
|
|
22
|
+
def rewrite(html)
|
|
23
|
+
# NOTE: ボディが ASCII-8BIT に転落していると UTF-8 の Marker::PREFIX との include? 比較が
|
|
24
|
+
# Encoding::CompatibilityError を投げる(ミドルウェアのボディ連結で非ASCIIバイトを含む
|
|
25
|
+
# ASCII-8BIT チャンクが混じると発生)。実バイト列は本来 UTF-8 なので判定用に UTF-8 とみなす。
|
|
26
|
+
# String.new でエンコーディングだけ付け替える(元オブジェクトを破壊せずバッファもコピーしない)。
|
|
27
|
+
scannable = html.encoding == Encoding::UTF_8 ? html : String.new(html, encoding: Encoding::UTF_8)
|
|
28
|
+
|
|
29
|
+
# NOTE: マーカーが無ければ Copyray 無効時・マーカーの無い通常ページなので一切変形しない(高速パス)。
|
|
30
|
+
# これによりマーカー非注入の HTML は完全に無傷で、Nokogiri の正規化も通らない。
|
|
31
|
+
# (そもそも CopyrayMiddleware は development 限定で本番のスタックには登録されず、本番では呼ばれない。)
|
|
32
|
+
# 判定は正規表現より安い部分文字列検索で行う(プレフィックスがあれば必ずマーカー候補)。
|
|
33
|
+
return [html, false] unless scannable.include?(Marker::PREFIX)
|
|
34
|
+
|
|
35
|
+
# NOTE: 閾値超は Nokogiri を通さず可視トークン除去のみ。skipped=true で編集導線を諦めた旨を伝える。
|
|
36
|
+
return [strip_markers(scannable), true] if scannable.bytesize > MAX_REWRITE_BYTESIZE
|
|
37
|
+
|
|
38
|
+
[rewrite_with_nokogiri(scannable), false]
|
|
39
|
+
rescue StandardError => e
|
|
40
|
+
# NOTE: Copyray は開発支援機能なので、壊れた HTML 等で Nokogiri 処理が落ちても
|
|
41
|
+
# ページを 500 にしない。data-copyray-key 付与(編集導線)は諦め、最低限可視トークンだけ除去する。
|
|
42
|
+
warn_rewrite_failure(e)
|
|
43
|
+
[strip_markers(scannable), true]
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
private
|
|
47
|
+
|
|
48
|
+
# NOTE: gsub 対象が html ではなく scannable なのは、ASCII-8BIT のままだと UTF-8 の
|
|
49
|
+
# SCAN_REGEXP との比較で Encoding::CompatibilityError が再発しうるため。
|
|
50
|
+
def strip_markers(scannable)
|
|
51
|
+
scannable.gsub(Marker::SCAN_REGEXP, '')
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def rewrite_with_nokogiri(scannable)
|
|
55
|
+
doc = Nokogiri::HTML(scannable)
|
|
56
|
+
|
|
57
|
+
doc.traverse do |node|
|
|
58
|
+
if node.text?
|
|
59
|
+
annotate_text_node(node)
|
|
60
|
+
elsif node.element?
|
|
61
|
+
annotate_attributes(node)
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# NOTE: Nokogiri 走査はテキストノード親要素・属性値への属性付与とノード単位の除去を担うが、
|
|
66
|
+
# serialize 時の正規化(エンティティ復元等)でトークンが復活しうる縁を塞ぐため、
|
|
67
|
+
# 最終出力にもう一度 gsub をかけて残留トークンを保険で全除去する(可視トークンの除去漏れは画面に出るため)。
|
|
68
|
+
strip_markers(doc.to_html)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# NOTE: logger 未設定の構成でもフォールバック自体が落ちないよう nil ガードする。
|
|
72
|
+
def warn_rewrite_failure(error)
|
|
73
|
+
logger = CopyTunerClient.configuration.logger
|
|
74
|
+
logger&.warn("CopyTuner Copyray::Rewriter failed: #{error.class.name}: #{error.message}")
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def annotate_text_node(node)
|
|
78
|
+
keys = scan_keys(node.content)
|
|
79
|
+
return if keys.empty?
|
|
80
|
+
|
|
81
|
+
set_keys(node.parent, keys)
|
|
82
|
+
node.content = node.content.gsub(Marker::SCAN_REGEXP, '')
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def annotate_attributes(element)
|
|
86
|
+
element.attribute_nodes.each do |attr|
|
|
87
|
+
keys = scan_keys(attr.value)
|
|
88
|
+
next if keys.empty?
|
|
89
|
+
|
|
90
|
+
set_keys(element, keys)
|
|
91
|
+
attr.value = attr.value.gsub(Marker::SCAN_REGEXP, '')
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# NOTE: 空キーは除く(JS 側も split 後に空要素を捨てるため、表現を両端で揃える)。
|
|
96
|
+
def scan_keys(text)
|
|
97
|
+
text.scan(Marker::SCAN_REGEXP).flatten.reject(&:empty?)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# NOTE: 同一テキストノード/属性値に複数マーカーが連結されると全キーをここで受け取り、
|
|
101
|
+
# 1 要素に複数キーをカンマ区切りで保持する(1 要素 1 キーだと 2 個目以降の編集導線が消える)。
|
|
102
|
+
# 既存値がある場合(同じ要素を複数経路で踏むケース)はマージして重複排除し、出現順を保つ。
|
|
103
|
+
# I18n キーに ',' は通常含まれないため区切り文字として安全。JS 側もカンマ区切りを前提に読む。
|
|
104
|
+
def set_keys(element, keys)
|
|
105
|
+
return if element.nil? || !element.element?
|
|
106
|
+
|
|
107
|
+
existing = element[DATA_ATTR]&.split(',') || []
|
|
108
|
+
element[DATA_ATTR] = (existing + keys).uniq.join(',')
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
@@ -1,9 +1,12 @@
|
|
|
1
|
+
require 'copy_tuner_client/copyray/marker'
|
|
2
|
+
|
|
1
3
|
module CopyTunerClient
|
|
2
4
|
class Copyray
|
|
3
5
|
# This:
|
|
4
6
|
# message
|
|
5
7
|
# Becomes:
|
|
6
|
-
#
|
|
8
|
+
# ⟦CT:views.home.index.message⟧message
|
|
9
|
+
# マーカートークンは CopyrayMiddleware の Rewriter で data-copyray-key 属性に変換され、HTML から除去される。
|
|
7
10
|
def self.augment_template(source, key)
|
|
8
11
|
return source if source.blank? || !source.is_a?(String)
|
|
9
12
|
|
|
@@ -11,9 +14,13 @@ module CopyTunerClient
|
|
|
11
14
|
# オーバーレイマーカーを出さない。編集できないキーを編集可能だと誤認させないため。
|
|
12
15
|
return source if CopyTunerClient.configuration.local_first_key?(key)
|
|
13
16
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
+
# NOTE: マーカーは平文・html_safe どちらの訳文にも埋め込む(画面に出る全テキストをオーバーレイ対象にする)。
|
|
18
|
+
# トークンの区切り記号 ⟦⟧ は HTML 特殊文字ではないため、平文が ActionView でエスケープされても無傷で残り、
|
|
19
|
+
# Rewriter の走査は崩れない。
|
|
20
|
+
# html_safe フラグは source のものを引き継ぐ。html_safe を勝手に立てると平文訳文の本体(& < >)が
|
|
21
|
+
# エスケープされず XSS になり、逆に html_safe を落とすと _html 訳文がエスケープされて壊れるため。
|
|
22
|
+
augmented = Marker.encode(key) + source
|
|
23
|
+
source.html_safe? ? augmented.html_safe : augmented
|
|
17
24
|
end
|
|
18
25
|
end
|
|
19
26
|
end
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# cf) xray-rails : xray/middleware.rb
|
|
2
2
|
|
|
3
|
+
require 'copy_tuner_client/copyray/rewriter'
|
|
4
|
+
|
|
3
5
|
module CopyTunerClient
|
|
4
6
|
class CopyrayMiddleware
|
|
5
7
|
def initialize(app)
|
|
@@ -11,8 +13,13 @@ module CopyTunerClient
|
|
|
11
13
|
status, headers, response = @app.call(env)
|
|
12
14
|
if html_headers?(status, headers) && body = response_body(response)
|
|
13
15
|
csp_nonce = env['action_dispatch.content_security_policy_nonce'] || env['secure_headers_content_security_policy_nonce']
|
|
16
|
+
# NOTE: CSS/JS 挿入の前に Rewriter を通す。serialize 後も </body> は必ず出力されるので
|
|
17
|
+
# append_to_html_body の rindex は機能し、CSS/JS タグはトークン非含有なので二重処理も起きない。
|
|
18
|
+
# NOTE: skipped は data-copyray-key を付与できなかったこと(巨大DOM/Nokogiri例外)を表す。
|
|
19
|
+
# JS にこれを伝え、オーバーレイ非対応である旨をツールバーで案内させる。
|
|
20
|
+
body, skipped = CopyTunerClient::Copyray::Rewriter.rewrite(body)
|
|
14
21
|
body = append_css(body, csp_nonce)
|
|
15
|
-
body = append_js(body, csp_nonce)
|
|
22
|
+
body = append_js(body, csp_nonce, skipped: skipped)
|
|
16
23
|
content_length = body.bytesize.to_s
|
|
17
24
|
headers['Content-Length'] = content_length
|
|
18
25
|
# maintains compatibility with other middlewares
|
|
@@ -37,7 +44,7 @@ module CopyTunerClient
|
|
|
37
44
|
append_to_html_body(html, css_tag)
|
|
38
45
|
end
|
|
39
46
|
|
|
40
|
-
def append_js(html, csp_nonce)
|
|
47
|
+
def append_js(html, csp_nonce, skipped: false)
|
|
41
48
|
json =
|
|
42
49
|
if CopyTunerClient::TranslationLog.initialized?
|
|
43
50
|
CopyTunerClient::TranslationLog.translations.to_json
|
|
@@ -49,6 +56,7 @@ module CopyTunerClient
|
|
|
49
56
|
window.CopyTuner = {
|
|
50
57
|
url: '#{CopyTunerClient.configuration.project_url}',
|
|
51
58
|
data: #{json},
|
|
59
|
+
keysSkipped: #{skipped},
|
|
52
60
|
}
|
|
53
61
|
SCRIPT
|
|
54
62
|
append_to_html_body(html, helpers.javascript_include_tag('copytuner', type: 'module', crossorigin: 'anonymous', nonce: csp_nonce))
|
|
@@ -60,9 +60,6 @@ module CopyTunerClient
|
|
|
60
60
|
alias_method :translate_without_copyray_comment, :translate
|
|
61
61
|
alias_method :translate, :translate_with_copyray_comment
|
|
62
62
|
alias :t :translate
|
|
63
|
-
alias :tt :translate_without_copyray_comment
|
|
64
|
-
else
|
|
65
|
-
alias :tt :translate
|
|
66
63
|
end
|
|
67
64
|
end
|
|
68
65
|
end
|
|
@@ -26,18 +26,11 @@ module CopyTunerClient
|
|
|
26
26
|
#
|
|
27
27
|
# @return [Object] the translated key (usually a String)
|
|
28
28
|
def translate(locale, key, options = {})
|
|
29
|
-
# I18nの標準処理に任せる(内部でlookup
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
# HTML escapeの処理(ツリー構造のHashは除く)
|
|
35
|
-
if CopyTunerClient.configuration.html_escape
|
|
36
|
-
content
|
|
37
|
-
else
|
|
38
|
-
# Backward compatible
|
|
39
|
-
content.respond_to?(:html_safe) ? content.html_safe : content
|
|
40
|
-
end
|
|
29
|
+
# I18nの標準処理に任せる(内部でlookupが呼ばれる)。
|
|
30
|
+
# NOTE: html_safe 化は backend では行わない。.html/_html キーの html_safe 化は
|
|
31
|
+
# ActionView の TranslationHelper(ActiveSupport::HtmlSafeTranslation)が担うため、
|
|
32
|
+
# backend は I18n 標準どおり素の content を返すだけにする(旧 html_escape 分岐は廃止)。
|
|
33
|
+
super
|
|
41
34
|
end
|
|
42
35
|
|
|
43
36
|
# Returns locales available for this CopyTuner project.
|
data/skills/copy-tuner/SKILL.md
CHANGED
|
@@ -71,13 +71,44 @@ license: MIT
|
|
|
71
71
|
削除せず `config/initializers/copy_tuner.rb` の `ignored_keys` に追加する。
|
|
72
72
|
一定期間どこからも参照されていないことを確認してから手動削除する。
|
|
73
73
|
|
|
74
|
-
##
|
|
74
|
+
## 翻訳ヘルパー(`t`)
|
|
75
75
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
76
|
+
**通常出力・属性値ともに `t`(`translate`)を使う。** 属性値も含めて区別は不要。
|
|
77
|
+
|
|
78
|
+
**`tt` は削除済み。** PR #122 で存在理由を喪失したため削除した。既存の `tt` 呼び出しは `t` へ置き換える(属性値含め)。
|
|
79
|
+
|
|
80
|
+
## 落とし穴: 開発環境で訳文にマーカートークンが混入する
|
|
81
|
+
|
|
82
|
+
PR #122 の可視トークン方式では、development(`middleware` 有効時)の `t('key')` の戻り値は
|
|
83
|
+
`⟦CT:key⟧訳文本体`(記号は U+27E6 / U+27E7)のように、訳文先頭に Copyray マーカートークンが付いた
|
|
84
|
+
文字列になる。トークンは配信直前に `CopyrayMiddleware` → `Rewriter` が `data-copyray-key` 属性へ変換
|
|
85
|
+
して HTML から完全除去するため画面表示は正常だが、**ビューで訳文を文字列として加工するコードは、
|
|
86
|
+
除去前のトークン込み文字列に作用してしまう。**
|
|
87
|
+
|
|
88
|
+
**本番では再現しない。** 本番では `CopyrayMiddleware` 自体が登録されずマーカー注入もされないため、
|
|
89
|
+
戻り値は素の訳文。**開発環境だけ挙動が違う**のが見落としやすい点。
|
|
90
|
+
|
|
91
|
+
危険なコード例:
|
|
92
|
+
|
|
93
|
+
```erb
|
|
94
|
+
<%= truncate(t('views.foo.body')) %> <%# ⟦CT:...⟧ 込みの長さで切られ、トークン途中で切れる/本文が短く切られる %>
|
|
95
|
+
<%= t('views.foo.title').length %> <%# トークン長が加算され想定外の値になる %>
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
一般化すると、`t(...)` の戻り値を `truncate` / `length` / スライス(`[0..n]`)/ 正規表現マッチ /
|
|
99
|
+
文字数バリデーション等で**文字列処理する全般**が対象。
|
|
100
|
+
|
|
101
|
+
**対処**: 訳文を文字列加工する場合、マーカーを注入しない `I18n.t`(`translate` ヘルパーではなく
|
|
102
|
+
`I18n` モジュールを直接呼ぶ)で訳文を取得してから加工する。`I18n.t` は HelperExtension のラッパーを
|
|
103
|
+
通らないためトークンが付かない。
|
|
79
104
|
|
|
80
105
|
```erb
|
|
81
|
-
|
|
82
|
-
<h1><%= t('views.simulation.show.title') %></h1>
|
|
106
|
+
<%= truncate(I18n.t('views.foo.body')) %>
|
|
83
107
|
```
|
|
108
|
+
|
|
109
|
+
注意:
|
|
110
|
+
- `I18n.t` は partial 相対キー(先頭ドットの `t('.body')`)を解決しないので、`I18n.t('views.foo.body')`
|
|
111
|
+
のように**絶対キーで書く**こと。
|
|
112
|
+
|
|
113
|
+
**このスキルでの注意**: キー追加・参照・コード差し替えを行う際、上記のように訳文を文字列加工して
|
|
114
|
+
いるコードを書く/触る場合はこの罠を念頭に置くこと。
|
|
@@ -82,10 +82,10 @@ migrate-prefix 側で配置を直してから戻る。
|
|
|
82
82
|
|
|
83
83
|
### 3. `tt` ヘルパーをアプリ側へ退避
|
|
84
84
|
|
|
85
|
-
copy_tuner_client
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
85
|
+
copy_tuner_client はかつて `ActionView` の `translate` をフックする際に **`tt` という独自エイリアス**を生やして
|
|
86
|
+
いた(シグネチャは `tt(key, **options)`)。PR #122 で存在理由を喪失したため gem からは削除済みだが、過去の
|
|
87
|
+
copy_tuner_client を使っていたアプリのビューに `tt(...)` 呼び出しが残っていると `NoMethodError`(テンプレートで
|
|
88
|
+
未定義ヘルパー)になる。**gem を抜く前に**アプリ側へ移しておく(既に gem 側に `tt` が無い場合も手順は同じ)。
|
|
89
89
|
|
|
90
90
|
1. 利用箇所を洗い出す(残っていれば移行対象):
|
|
91
91
|
|
|
@@ -60,13 +60,8 @@ config.local_first_key_regexp = Regexp.union(
|
|
|
60
60
|
(`/\Aviews\./`。`Regexp.union` に文字列を渡す場合は自動エスケープされるが、Regexp リテラルを渡すときは自分で
|
|
61
61
|
書く)。
|
|
62
62
|
|
|
63
|
-
##
|
|
63
|
+
## 旧 exclude_key_regexp について
|
|
64
64
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
| 作用タイミング | lookup(読み込み)時 | upload(送信)時 |
|
|
69
|
-
| 効果 | ローカル YAML を優先(完全分離) | サーバへのアップロードを抑止するだけ |
|
|
70
|
-
|
|
71
|
-
`exclude_key_regexp` は PR #110 で非推奨化された(設定すると `ActiveSupport::Deprecation` 警告が出る)。
|
|
72
|
-
移行では使わない。もし既存設定に `exclude_key_regexp` があれば、cleanup スキルで gem ごと撤去される。
|
|
65
|
+
かつて存在した `exclude_key_regexp` オプションは v2 で削除済み。`local_first_key_regexp` を使うこと
|
|
66
|
+
(対象は locale を**除いた**キー `views.foo`、lookup 時に作用しローカル YAML を優先=完全分離)。
|
|
67
|
+
既存 initializer に `exclude_key_regexp` の設定が残っている場合は削除する。
|
|
@@ -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 件ずつ確認する。
|