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.
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 +0 -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 +18 -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
@@ -7,62 +7,45 @@ describe CopyTunerClient::Copyray do
7
7
 
8
8
  let(:key) { 'en.test.key' }
9
9
 
10
- shared_examples 'Not escaped' do
11
- it { is_expected.to be_html_safe }
12
- it { is_expected.to eq "<!--COPYRAY #{key}--><b>Hello</b>" }
13
- end
14
-
15
- context 'html_escape option is false' do
16
- before do
17
- CopyTunerClient.configure do |configuration|
18
- configuration.html_escape = false
19
- configuration.client = FakeClient.new
20
- end
10
+ before do
11
+ CopyTunerClient.configure do |configuration|
12
+ configuration.project_id = 1
13
+ configuration.client = FakeClient.new
21
14
  end
15
+ end
22
16
 
23
- context 'string not marked as html safe' do
24
- let(:source) { FakeHtmlSafeString.new('<b>Hello</b>') }
17
+ context 'when the source is html_safe (e.g. _html key)' do
18
+ let(:source) { FakeHtmlSafeString.new('<b>Hello</b>').html_safe }
25
19
 
26
- it_behaves_like 'Not escaped'
20
+ it 'keeps the html_safe flag so the translation is not re-escaped' do
21
+ is_expected.to be_html_safe
27
22
  end
28
23
 
29
- context 'string marked as html safe' do
30
- let(:source) { FakeHtmlSafeString.new('<b>Hello</b>').html_safe }
31
-
32
- it_behaves_like 'Not escaped'
24
+ it 'prepends the visible marker token without escaping' do
25
+ is_expected.to eq '⟦CT:en.test.key⟧<b>Hello</b>'
33
26
  end
34
27
  end
35
28
 
36
- context 'html_escape option is true' do
37
- before do
38
- CopyTunerClient.configure do |configuration|
39
- configuration.html_escape = true
40
- configuration.client = FakeClient.new
41
- end
42
- end
43
-
44
- context 'string not marked as html safe' do
45
- let(:source) { FakeHtmlSafeString.new('<b>Hello</b>') }
29
+ context 'when the source is plain text (not html_safe)' do
30
+ let(:source) { FakeHtmlSafeString.new('Hello & <World>') }
46
31
 
47
- it { is_expected.to be_html_safe }
48
- it { is_expected.to eq "<!--COPYRAY #{key}-->&lt;b&gt;Hello&lt;/b&gt;" }
49
- end
50
-
51
- context 'string marked as html safe' do
52
- let(:source) { FakeHtmlSafeString.new('<b>Hello</b>').html_safe }
53
-
54
- it_behaves_like 'Not escaped'
32
+ it 'prepends the marker but keeps the source non html_safe so ActionView still escapes the body' do
33
+ is_expected.to eq '⟦CT:en.test.keyHello & <World>'
34
+ is_expected.not_to be_html_safe
55
35
  end
56
36
  end
57
37
 
58
38
  context 'when the key matches local_first_key_regexp' do
59
- let(:source) { 'Hello' }
60
39
  let(:key) { 'views.foo' }
61
40
 
62
41
  before { CopyTunerClient.configuration.local_first_key_regexp = /\Aviews\./ }
63
42
 
64
- it 'does not inject the overlay marker' do
65
- is_expected.to eq 'Hello'
43
+ it 'does not inject the marker into a plain source' do
44
+ expect(CopyTunerClient::Copyray.augment_template('Hello', key)).to eq 'Hello'
45
+ end
46
+
47
+ it 'does not inject the marker into an html_safe source' do
48
+ expect(CopyTunerClient::Copyray.augment_template('Hello'.html_safe, key)).to eq 'Hello'
66
49
  end
67
50
  end
68
51
  end
@@ -26,8 +26,11 @@ describe CopyTunerClient::HelperExtension do
26
26
  module KeywordArgumentsHelper
27
27
  attr_writer :controller
28
28
 
29
+ # NOTE: ActionView の TranslationHelper を模し、.html/_html キーのみ html_safe な訳文を返す。
30
+ # マーカーは平文・html_safe どちらにも注入されるが、html_safe フラグの引き継ぎを検証できるよう両方返し分ける。
29
31
  def translate(key, **options)
30
- "Hello, #{options[:name]}"
32
+ source = "Hello, #{options[:name]}"
33
+ key.to_s.end_with?('.html', '_html') ? source.html_safe : source
31
34
  end
32
35
 
33
36
  def controller
@@ -54,7 +57,17 @@ describe CopyTunerClient::HelperExtension do
54
57
  end
55
58
 
56
59
  it 'works with keyword argument method' do
57
- expect(view.translate('some.key', name: 'World')).to eq '<!--COPYRAY some.key-->Hello, World'
60
+ expect(view.translate('some.key_html', name: 'World')).to eq '⟦CT:some.key_html⟧Hello, World'
61
+ end
62
+
63
+ it 'injects the marker into a plain (non html_safe) translation, keeping it non html_safe' do
64
+ result = view.translate('some.key', name: 'World')
65
+ expect(result).to eq '⟦CT:some.key⟧Hello, World'
66
+ expect(result).not_to be_html_safe
67
+ end
68
+
69
+ it 'keeps the html_safe flag for an _html key so the body is not re-escaped' do
70
+ expect(view.translate('some.key_html', name: 'World')).to be_html_safe
58
71
  end
59
72
 
60
73
  it 'does not inject the overlay marker for a local_first key' do
@@ -62,9 +75,9 @@ describe CopyTunerClient::HelperExtension do
62
75
  expect(view.translate('views.foo', name: 'World')).to eq 'Hello, World'
63
76
  end
64
77
 
65
- context 'injection guard by request format' do
66
- it 'injects the marker when request.format is html' do
67
- expect(view.translate('some.key', name: 'World')).to eq '<!--COPYRAY some.key-->Hello, World'
78
+ context 'injection guard by rendering context' do
79
+ it 'injects the marker when request.format is :html' do
80
+ expect(view.translate('some.key', name: 'World')).to eq '⟦CT:some.keyHello, World'
68
81
  end
69
82
 
70
83
  %i[json text csv pdf].each do |format|
@@ -138,12 +138,20 @@ describe 'CopyTunerClient::I18nBackend' do
138
138
  expect(cache['en.test.key']).to eq 'default %{interpolate}'
139
139
  end
140
140
 
141
+ # NOTE: backend は html_safe 化をしない(.html/_html キーの html_safe 化は ActionView の
142
+ # TranslationHelper が担う)。html_escape 設定の有無に関わらず素の content を返す i18n 標準準拠の挙動。
141
143
  it 'html safeを付与しないこと' do
142
144
  cache['en.test.key'] = FakeHtmlSafeString.new("Hello")
143
145
  backend = build_backend
144
146
  expect(backend.translate('en', 'test.key')).to_not be_html_safe
145
147
  end
146
148
 
149
+ it 'html_safe な値を渡しても backend が独自に html_safe 化しないこと' do
150
+ cache['en.test.key'] = FakeHtmlSafeString.new("Hello").html_safe
151
+ backend = build_backend
152
+ expect(backend.translate('en', 'test.key')).to be_html_safe
153
+ end
154
+
147
155
  it 'defaultが配列の場合に順に検索できること' do
148
156
  cache['en.key.one'] = "Expected"
149
157
  backend = build_backend
@@ -151,21 +159,6 @@ describe 'CopyTunerClient::I18nBackend' do
151
159
  to eq('Expected')
152
160
  end
153
161
 
154
- context 'html_escapeオプションがtrueの場合' do
155
- before do
156
- CopyTunerClient.configure do |configuration|
157
- configuration.html_escape = true
158
- configuration.client = FakeClient.new
159
- end
160
- end
161
-
162
- it 'html safeを付与しないこと' do
163
- cache['en.test.key'] = FakeHtmlSafeString.new("Hello")
164
- backend = build_backend
165
- expect(backend.translate('en', 'test.key')).not_to be_html_safe
166
- end
167
- end
168
-
169
162
  context '非文字列キーの場合' do
170
163
  it 'キャッシュに登録されないこと' do
171
164
  expect { subject.translate('en', {}) }.to throw_symbol(:exception)
@@ -4,7 +4,6 @@ module ClientSpecHelpers
4
4
  CopyTunerClient.configure(false) do |config|
5
5
  config.api_key = 'abc123'
6
6
  config.s3_host = 'copy-tuner.com'
7
- config.html_escape = true
8
7
  end
9
8
  end
10
9
  end
data/src/copyray.css CHANGED
@@ -79,10 +79,12 @@
79
79
 
80
80
  .copyray-specimen-handle {
81
81
  float: left;
82
+ margin: 0 2px 2px 0;
82
83
  background: #fff;
83
84
  padding: 0 3px;
84
85
  color: #333;
85
86
  font-size: 10px;
87
+ cursor: pointer;
86
88
  }
87
89
 
88
90
  .copyray-specimen-handle.Specimen {
@@ -187,6 +189,15 @@ a.copyray-toggle-button:hover {
187
189
  background-color: #555;
188
190
  }
189
191
 
192
+ .copy-tuner-bar__notice {
193
+ display: inline-block;
194
+ margin: 8px;
195
+ font-size: 13px;
196
+ line-height: 24px;
197
+ vertical-align: middle;
198
+ color: #ffd24d;
199
+ }
200
+
190
201
  input[type='text'].copy-tuner-bar__search {
191
202
  -webkit-appearance: none;
192
203
  -moz-appearance: none;
data/src/copyray.ts CHANGED
@@ -1,35 +1,16 @@
1
1
  import CopyTunerBar from './copytuner_bar'
2
2
  import Specimen from './specimen'
3
3
 
4
- const findBlurbs = () => {
5
- const filterNone = () => NodeFilter.FILTER_ACCEPT
6
-
7
- // @ts-expect-error TS2554
8
- const iterator = document.createNodeIterator(document.body, NodeFilter.SHOW_COMMENT, filterNone, false)
9
-
10
- const comments = []
11
- let curNode
12
-
13
- while ((curNode = iterator.nextNode())) {
14
- comments.push(curNode)
15
- }
16
-
17
- return (
18
- comments
19
- // @ts-expect-error TS2531
20
- .filter((comment) => comment.nodeValue.startsWith('COPYRAY'))
21
- .map((comment) => {
22
- // @ts-expect-error TS2488
23
- const [, key] = comment.nodeValue.match(/^COPYRAY (\S*)$/)
24
- const element = comment.parentNode
25
- return { key, element }
26
- })
27
- )
28
- }
4
+ const findBlurbs = () =>
5
+ Array.from(document.querySelectorAll('[data-copyray-key]')).map((element) => ({
6
+ // 1 要素に複数キーがカンマ区切りで入りうる(同一テキストノードに複数訳文が連結された場合)
7
+ keys: (element.getAttribute('data-copyray-key') ?? '').split(',').filter(Boolean),
8
+ element,
9
+ }))
29
10
 
30
11
  export default class Copyray {
31
12
  // @ts-expect-error TS7006
32
- constructor(baseUrl, data) {
13
+ constructor(baseUrl, data, keysSkipped = false) {
33
14
  // @ts-expect-error TS2339
34
15
  this.baseUrl = baseUrl
35
16
  // @ts-expect-error TS2339
@@ -46,7 +27,7 @@ export default class Copyray {
46
27
  this.boundOpen = this.open.bind(this)
47
28
 
48
29
  // @ts-expect-error TS2339
49
- this.copyTunerBar = new CopyTunerBar(document.querySelector('#copy-tuner-bar'), this.data, this.boundOpen)
30
+ this.copyTunerBar = new CopyTunerBar(document.querySelector('#copy-tuner-bar'), this.data, this.boundOpen, keysSkipped)
50
31
  }
51
32
 
52
33
  show() {
@@ -93,9 +74,9 @@ export default class Copyray {
93
74
  }
94
75
 
95
76
  makeSpecimens() {
96
- for (const { element, key } of findBlurbs()) {
77
+ for (const { element, keys } of findBlurbs()) {
97
78
  // @ts-expect-error TS2339
98
- this.specimens.push(new Specimen(element, key, this.boundOpen))
79
+ this.specimens.push(new Specimen(element, keys, this.boundOpen))
99
80
  }
100
81
  }
101
82
 
data/src/copytuner_bar.ts CHANGED
@@ -5,7 +5,7 @@ const HIDDEN_CLASS = 'copy-tuner-hidden'
5
5
 
6
6
  export default class CopytunerBar {
7
7
  // @ts-expect-error TS7006
8
- constructor(element, data, callback) {
8
+ constructor(element, data, callback, keysSkipped = false) {
9
9
  // @ts-expect-error TS2339
10
10
  this.element = element
11
11
  // @ts-expect-error TS2339
@@ -19,9 +19,23 @@ export default class CopytunerBar {
19
19
  // @ts-expect-error TS2339
20
20
  this.element.append(this.logMenuElement)
21
21
 
22
+ // 巨大DOM/Nokogiri例外でキー付与がスキップされた場合は、オーバーレイが使えないので
23
+ // ツールバー(Translations in this page)から編集する旨を案内する。
24
+ if (keysSkipped) {
25
+ this.appendSkippedNotice()
26
+ }
27
+
22
28
  this.addHandler()
23
29
  }
24
30
 
31
+ appendSkippedNotice() {
32
+ const notice = document.createElement('span')
33
+ notice.classList.add('copy-tuner-bar__notice')
34
+ notice.textContent = '⚠ This page is too large for the overlay. Use "Translations in this page" to edit.'
35
+ // @ts-expect-error TS2339
36
+ this.element.append(notice)
37
+ }
38
+
25
39
  addHandler() {
26
40
  // @ts-expect-error TS2339
27
41
  const openLogButton = this.element.querySelector('.js-copy-tuner-bar-open-log')
data/src/main.ts CHANGED
@@ -9,6 +9,9 @@ declare global {
9
9
  toggle?: () => void
10
10
  // TODO: type
11
11
  data: object
12
+ // 巨大DOM/Nokogiri例外で data-copyray-key 付与をスキップしたか。
13
+ // true のときオーバーレイは使えないのでツールバーから編集する旨を案内する。
14
+ keysSkipped?: boolean
12
15
  }
13
16
  }
14
17
  }
@@ -30,10 +33,10 @@ const appendCopyTunerBar = (url: string) => {
30
33
  }
31
34
 
32
35
  const start = () => {
33
- const { url, data } = window.CopyTuner
36
+ const { url, data, keysSkipped } = window.CopyTuner
34
37
 
35
38
  appendCopyTunerBar(url)
36
- const copyray = new Copyray(url, data)
39
+ const copyray = new Copyray(url, data, Boolean(keysSkipped))
37
40
  window.CopyTuner.toggle = () => copyray.toggle()
38
41
 
39
42
  document.addEventListener('keydown', (event) => {
data/src/specimen.ts CHANGED
@@ -4,11 +4,11 @@ const ZINDEX = 2_000_000_000
4
4
 
5
5
  export default class Specimen {
6
6
  // @ts-expect-error TS7006
7
- constructor(element, key, callback) {
7
+ constructor(element, keys, callback) {
8
8
  // @ts-expect-error TS2339
9
9
  this.element = element
10
10
  // @ts-expect-error TS2339
11
- this.key = key
11
+ this.keys = keys
12
12
  // @ts-expect-error TS2339
13
13
  this.callback = callback
14
14
  }
@@ -19,10 +19,11 @@ export default class Specimen {
19
19
  // @ts-expect-error TS2339
20
20
  if (this.box === null) return
21
21
 
22
+ // box 全体のクリックは先頭キーを開く(広いクリック領域を維持)。複数キー時は各ラベルから個別に開ける
22
23
  // @ts-expect-error TS2339
23
24
  this.box.addEventListener('click', () => {
24
25
  // @ts-expect-error TS2339
25
- this.callback(this.key)
26
+ this.callback(this.keys[0])
26
27
  })
27
28
 
28
29
  // @ts-expect-error TS2339
@@ -69,16 +70,25 @@ export default class Specimen {
69
70
  this.box.style.left = `${left}px`
70
71
  }
71
72
 
72
- box.append(this.makeLabel())
73
+ // @ts-expect-error TS2339
74
+ for (const key of this.keys) {
75
+ box.append(this.makeLabel(key))
76
+ }
73
77
  return box
74
78
  }
75
79
 
76
- makeLabel() {
80
+ // @ts-expect-error TS7006
81
+ makeLabel(key) {
77
82
  const div = document.createElement('div')
78
83
  div.classList.add('copyray-specimen-handle')
79
84
  div.classList.add('Specimen')
80
- // @ts-expect-error TS2339
81
- div.textContent = this.key
85
+ div.textContent = key
86
+ // ラベルのクリックはそのキーを開く。box への伝播を止めて先頭キーとの二重発火を防ぐ
87
+ div.addEventListener('click', (event) => {
88
+ event.stopPropagation()
89
+ // @ts-expect-error TS2339
90
+ this.callback(key)
91
+ })
82
92
  return div
83
93
  }
84
94
  }
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: copy_tuner_client
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.5.0
4
+ version: 2.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - SonicGarden
@@ -198,6 +198,7 @@ files:
198
198
  - LICENSE.txt
199
199
  - README.md
200
200
  - Rakefile
201
+ - UPGRADING.md
201
202
  - app/assets/javascripts/copytuner.js
202
203
  - app/assets/stylesheets/copytuner.css
203
204
  - copy_tuner_client.gemspec
@@ -210,6 +211,8 @@ files:
210
211
  - lib/copy_tuner_client/client.rb
211
212
  - lib/copy_tuner_client/configuration.rb
212
213
  - lib/copy_tuner_client/copyray.rb
214
+ - lib/copy_tuner_client/copyray/marker.rb
215
+ - lib/copy_tuner_client/copyray/rewriter.rb
213
216
  - lib/copy_tuner_client/copyray_middleware.rb
214
217
  - lib/copy_tuner_client/dotted_hash.rb
215
218
  - lib/copy_tuner_client/engine.rb
@@ -237,10 +240,15 @@ files:
237
240
  - skills/copy-tuner-to-locales-migrate-prefix/references/local-first-regexp.md
238
241
  - skills/copy-tuner-to-locales-migrate-prefix/references/verification-per-prefix.md
239
242
  - skills/copy-tuner-to-locales-migrate-prefix/scripts/migrate_prefix.rb
243
+ - skills/copy-tuner-to-t-migrate/SKILL.md
244
+ - skills/copy-tuner-to-t-migrate/scripts/migrate_tt.rb
240
245
  - skills/copy-tuner/SKILL.md
241
246
  - spec/copy_tuner_client/cache_spec.rb
242
247
  - spec/copy_tuner_client/client_spec.rb
243
248
  - spec/copy_tuner_client/configuration_spec.rb
249
+ - spec/copy_tuner_client/copyray/marker_spec.rb
250
+ - spec/copy_tuner_client/copyray/rewriter_spec.rb
251
+ - spec/copy_tuner_client/copyray_middleware_spec.rb
244
252
  - spec/copy_tuner_client/copyray_spec.rb
245
253
  - spec/copy_tuner_client/dotted_hash_spec.rb
246
254
  - spec/copy_tuner_client/helper_extension_spec.rb