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.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/.gitattributes +2 -2
  3. data/.github/workflows/rspec.yml +1 -1
  4. data/CHANGELOG.md +27 -0
  5. data/CLAUDE.md +2 -3
  6. data/README.md +0 -25
  7. data/UPGRADING.md +201 -0
  8. data/app/assets/javascripts/copytuner.js +78 -73
  9. data/app/assets/stylesheets/copytuner.css +1 -1
  10. data/lib/copy_tuner_client/cache.rb +0 -2
  11. data/lib/copy_tuner_client/configuration.rb +23 -37
  12. data/lib/copy_tuner_client/copyray/marker.rb +24 -0
  13. data/lib/copy_tuner_client/copyray/rewriter.rb +113 -0
  14. data/lib/copy_tuner_client/copyray.rb +11 -4
  15. data/lib/copy_tuner_client/copyray_middleware.rb +10 -2
  16. data/lib/copy_tuner_client/helper_extension.rb +24 -3
  17. data/lib/copy_tuner_client/i18n_backend.rb +5 -12
  18. data/lib/copy_tuner_client/version.rb +1 -1
  19. data/skills/copy-tuner/SKILL.md +37 -6
  20. data/skills/copy-tuner-to-locales-cleanup/SKILL.md +4 -4
  21. data/skills/copy-tuner-to-locales-migrate-prefix/references/local-first-regexp.md +4 -9
  22. data/skills/copy-tuner-to-t-migrate/SKILL.md +131 -0
  23. data/skills/copy-tuner-to-t-migrate/scripts/migrate_tt.rb +189 -0
  24. data/spec/copy_tuner_client/cache_spec.rb +2 -10
  25. data/spec/copy_tuner_client/client_spec.rb +1 -0
  26. data/spec/copy_tuner_client/configuration_spec.rb +16 -16
  27. data/spec/copy_tuner_client/copyray/marker_spec.rb +41 -0
  28. data/spec/copy_tuner_client/copyray/rewriter_spec.rb +216 -0
  29. data/spec/copy_tuner_client/copyray_middleware_spec.rb +89 -0
  30. data/spec/copy_tuner_client/copyray_spec.rb +22 -39
  31. data/spec/copy_tuner_client/helper_extension_spec.rb +97 -5
  32. data/spec/copy_tuner_client/i18n_backend_spec.rb +8 -15
  33. data/spec/support/client_spec_helpers.rb +0 -1
  34. data/src/copyray.css +11 -0
  35. data/src/copyray.ts +10 -29
  36. data/src/copytuner_bar.ts +15 -1
  37. data/src/main.ts +5 -2
  38. data/src/specimen.ts +17 -7
  39. 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 exclude_key_regexp local_first_key_regexp s3_host locales ignored_keys ignored_key_handler
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
- attr_accessor :port
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
- attr_accessor :sync_interval
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 comment injection, set true
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
- send(option)
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 => send(option)
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 api_key
346
+ # @return [String] current project url by project_id
362
347
  def project_url
363
- path =
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
- # <!--COPYRAY views.home.index.message-->message
8
+ # ⟦CT:views.home.index.messagemessage
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
- escape = CopyTunerClient.configuration.html_escape && !source.html_safe?
15
- augmented = "<!--COPYRAY #{key}-->#{escape ? ERB::Util.html_escape(source) : source}"
16
- augmented.html_safe
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
- content = super
31
-
32
- return content if content.nil? || content.is_a?(Hash)
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.
@@ -1,6 +1,6 @@
1
1
  module CopyTunerClient
2
2
  # Client version
3
- VERSION = '1.4.0'.freeze
3
+ VERSION = '2.0.0'.freeze
4
4
 
5
5
  # API version being used to communicate with the server
6
6
  API_VERSION = '2.0'.freeze
@@ -71,13 +71,44 @@ license: MIT
71
71
  削除せず `config/initializers/copy_tuner.rb` の `ignored_keys` に追加する。
72
72
  一定期間どこからも参照されていないことを確認してから手動削除する。
73
73
 
74
- ## `t` と `tt` の使い分け
74
+ ## 翻訳ヘルパー(`t`)
75
75
 
76
- **HTML 要素の属性値には `tt` を使う**(それ以外の通常出力は `t`)。`tt` は copy_tuner_client gem の
77
- `HelperExtension` が提供するエイリアス。`t` は CopyTuner の都合で HTML コメント (`<!-- ... -->`) を
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
- <div title="<%= tt('views.tooltips.help_text') %>">...</div>
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 `ActionView` の `translate` をフックする際に **`tt` という独自エイリアス**を生やす
86
- (copyray コメント注入なしの純粋な翻訳。シグネチャは `tt(key, **options)`)。アプリのビューが `tt(...)`
87
- 使っていると、gem 撤去で `tt` が消えて `NoMethodError`(テンプレートで未定義ヘルパー)になる。**gem を抜く前に**
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
- ## deprecated exclude_key_regexp との違い
63
+ ## exclude_key_regexp について
64
64
 
65
- | | `local_first_key_regexp` | `exclude_key_regexp`(非推奨) |
66
- |---|---|---|
67
- | 対象キー | locale を**除いた**キー(`views.foo`) | locale を**含む**キー(`ja.views.foo`) |
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` の設定が残っている場合は削除する。