copy_tuner_client 1.4.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 +24 -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 +97 -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))
|
|
@@ -17,6 +17,12 @@ module CopyTunerClient
|
|
|
17
17
|
I18n.t(key.to_s.first == '.' ? scope_key_by_partial(key) : key, **options)
|
|
18
18
|
end
|
|
19
19
|
|
|
20
|
+
# NOTE: マーカーは HTML コメントとしてブラウザに無視されつつ Copyray オーバーレイのキー特定に
|
|
21
|
+
# 使われる。HTML 以外の経路(メール本文・render :json・CSV/PDF など)ではコメントが文字列として
|
|
22
|
+
# 出力に混入してしまうため、それらの経路には注入しない。default 引数による初期値登録は維持する
|
|
23
|
+
# 必要があるため、このガードは初期値登録(上の I18n.t 呼び出し)より後に置く。
|
|
24
|
+
return source unless copyray_injectable?
|
|
25
|
+
|
|
20
26
|
if CopyTunerClient.configuration.disable_copyray_comment_injection
|
|
21
27
|
source
|
|
22
28
|
else
|
|
@@ -32,13 +38,28 @@ module CopyTunerClient
|
|
|
32
38
|
CopyTunerClient::Copyray.augment_template(source, scope_key)
|
|
33
39
|
end
|
|
34
40
|
end
|
|
41
|
+
|
|
42
|
+
# NOTE: HTML 以外の経路(メール本文・render :json・CSV/PDF など)ではマーカーが文字列として
|
|
43
|
+
# 出力に混入するため注入しない。判定には controller.request.format を使い、@current_template.format /
|
|
44
|
+
# lookup_context.formats のような ActionView の内部実装には依存させない(Rails バージョン間で壊れうるため)。
|
|
45
|
+
def copyray_injectable?
|
|
46
|
+
current_controller = controller
|
|
47
|
+
return false if current_controller.nil?
|
|
48
|
+
|
|
49
|
+
# NOTE: mailer は request を持たず request.format で判定できない。かつメール本文への
|
|
50
|
+
# マーカー混入は実害が大きいため、controller の型で明示除外する。
|
|
51
|
+
return false if defined?(ActionMailer::Base) && current_controller.is_a?(ActionMailer::Base)
|
|
52
|
+
|
|
53
|
+
# NOTE: request が無い/format が html でない経路には注入しない。&. と || false で
|
|
54
|
+
# request 不在時も安全に false を返す。
|
|
55
|
+
current_controller.request&.format&.html? || false
|
|
56
|
+
end
|
|
57
|
+
private :copyray_injectable?
|
|
58
|
+
|
|
35
59
|
if middleware_enabled
|
|
36
60
|
alias_method :translate_without_copyray_comment, :translate
|
|
37
61
|
alias_method :translate, :translate_with_copyray_comment
|
|
38
62
|
alias :t :translate
|
|
39
|
-
alias :tt :translate_without_copyray_comment
|
|
40
|
-
else
|
|
41
|
-
alias :tt :translate
|
|
42
63
|
end
|
|
43
64
|
end
|
|
44
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` の設定が残っている場合は削除する。
|