copy_tuner_client 0.10.0 → 0.13.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 (50) hide show
  1. checksums.yaml +4 -4
  2. data/.eslintrc.js +12 -0
  3. data/.gitattributes +2 -0
  4. data/.github/workflows/rspec.yml +22 -30
  5. data/.ruby-version +1 -1
  6. data/.vscode/settings.json +7 -0
  7. data/CHANGELOG.md +13 -0
  8. data/Gemfile +1 -1
  9. data/Gemfile.lock +168 -145
  10. data/README.md +8 -10
  11. data/app/assets/javascripts/main.js +388 -0
  12. data/app/assets/stylesheets/style.css +1 -0
  13. data/copy_tuner_client.gemspec +3 -3
  14. data/gemfiles/6.1.gemfile +5 -0
  15. data/gemfiles/7.0.gemfile +5 -0
  16. data/gemfiles/main.gemfile +5 -0
  17. data/index.html +34 -0
  18. data/lib/copy_tuner_client/configuration.rb +9 -1
  19. data/lib/copy_tuner_client/copyray_middleware.rb +15 -29
  20. data/lib/copy_tuner_client/engine.rb +6 -31
  21. data/lib/copy_tuner_client/errors.rb +3 -0
  22. data/lib/copy_tuner_client/helper_extension.rb +34 -0
  23. data/lib/copy_tuner_client/i18n_backend.rb +6 -0
  24. data/lib/copy_tuner_client/version.rb +1 -1
  25. data/package.json +14 -15
  26. data/spec/copy_tuner_client/helper_extension_spec.rb +40 -0
  27. data/spec/copy_tuner_client/i18n_backend_spec.rb +2 -0
  28. data/{app/assets/stylesheets → src}/copyray.css +0 -0
  29. data/src/copyray.ts +130 -0
  30. data/src/copytuner_bar.ts +115 -0
  31. data/src/main.ts +58 -0
  32. data/src/specimen.ts +84 -0
  33. data/src/util.ts +38 -0
  34. data/src/vite-env.d.ts +1 -0
  35. data/tsconfig.json +26 -0
  36. data/vite.config.ts +18 -0
  37. data/yarn.lock +2251 -0
  38. metadata +36 -27
  39. data/.eslintrc +0 -4
  40. data/app/assets/javascripts/copyray.js +0 -1069
  41. data/app/views/_copy_tuner_bar.html.erb +0 -6
  42. data/gemfiles/5.2.gemfile +0 -7
  43. data/gemfiles/6.0.gemfile +0 -7
  44. data/package-lock.json +0 -4341
  45. data/rollup.config.js +0 -16
  46. data/src/copyray.js +0 -112
  47. data/src/copytuner_bar.js +0 -96
  48. data/src/main.js +0 -42
  49. data/src/specimen.js +0 -63
  50. data/src/util.js +0 -32
@@ -0,0 +1,388 @@
1
+ var commonjsGlobal = typeof globalThis !== "undefined" ? globalThis : typeof window !== "undefined" ? window : typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : {};
2
+ var FUNC_ERROR_TEXT = "Expected a function";
3
+ var NAN = 0 / 0;
4
+ var symbolTag = "[object Symbol]";
5
+ var reTrim = /^\s+|\s+$/g;
6
+ var reIsBadHex = /^[-+]0x[0-9a-f]+$/i;
7
+ var reIsBinary = /^0b[01]+$/i;
8
+ var reIsOctal = /^0o[0-7]+$/i;
9
+ var freeParseInt = parseInt;
10
+ var freeGlobal = typeof commonjsGlobal == "object" && commonjsGlobal && commonjsGlobal.Object === Object && commonjsGlobal;
11
+ var freeSelf = typeof self == "object" && self && self.Object === Object && self;
12
+ var root = freeGlobal || freeSelf || Function("return this")();
13
+ var objectProto = Object.prototype;
14
+ var objectToString = objectProto.toString;
15
+ var nativeMax = Math.max, nativeMin = Math.min;
16
+ var now = function() {
17
+ return root.Date.now();
18
+ };
19
+ function debounce(func, wait, options) {
20
+ var lastArgs, lastThis, maxWait, result, timerId, lastCallTime, lastInvokeTime = 0, leading = false, maxing = false, trailing = true;
21
+ if (typeof func != "function") {
22
+ throw new TypeError(FUNC_ERROR_TEXT);
23
+ }
24
+ wait = toNumber(wait) || 0;
25
+ if (isObject(options)) {
26
+ leading = !!options.leading;
27
+ maxing = "maxWait" in options;
28
+ maxWait = maxing ? nativeMax(toNumber(options.maxWait) || 0, wait) : maxWait;
29
+ trailing = "trailing" in options ? !!options.trailing : trailing;
30
+ }
31
+ function invokeFunc(time) {
32
+ var args = lastArgs, thisArg = lastThis;
33
+ lastArgs = lastThis = void 0;
34
+ lastInvokeTime = time;
35
+ result = func.apply(thisArg, args);
36
+ return result;
37
+ }
38
+ function leadingEdge(time) {
39
+ lastInvokeTime = time;
40
+ timerId = setTimeout(timerExpired, wait);
41
+ return leading ? invokeFunc(time) : result;
42
+ }
43
+ function remainingWait(time) {
44
+ var timeSinceLastCall = time - lastCallTime, timeSinceLastInvoke = time - lastInvokeTime, result2 = wait - timeSinceLastCall;
45
+ return maxing ? nativeMin(result2, maxWait - timeSinceLastInvoke) : result2;
46
+ }
47
+ function shouldInvoke(time) {
48
+ var timeSinceLastCall = time - lastCallTime, timeSinceLastInvoke = time - lastInvokeTime;
49
+ return lastCallTime === void 0 || timeSinceLastCall >= wait || timeSinceLastCall < 0 || maxing && timeSinceLastInvoke >= maxWait;
50
+ }
51
+ function timerExpired() {
52
+ var time = now();
53
+ if (shouldInvoke(time)) {
54
+ return trailingEdge(time);
55
+ }
56
+ timerId = setTimeout(timerExpired, remainingWait(time));
57
+ }
58
+ function trailingEdge(time) {
59
+ timerId = void 0;
60
+ if (trailing && lastArgs) {
61
+ return invokeFunc(time);
62
+ }
63
+ lastArgs = lastThis = void 0;
64
+ return result;
65
+ }
66
+ function cancel() {
67
+ if (timerId !== void 0) {
68
+ clearTimeout(timerId);
69
+ }
70
+ lastInvokeTime = 0;
71
+ lastArgs = lastCallTime = lastThis = timerId = void 0;
72
+ }
73
+ function flush() {
74
+ return timerId === void 0 ? result : trailingEdge(now());
75
+ }
76
+ function debounced() {
77
+ var time = now(), isInvoking = shouldInvoke(time);
78
+ lastArgs = arguments;
79
+ lastThis = this;
80
+ lastCallTime = time;
81
+ if (isInvoking) {
82
+ if (timerId === void 0) {
83
+ return leadingEdge(lastCallTime);
84
+ }
85
+ if (maxing) {
86
+ timerId = setTimeout(timerExpired, wait);
87
+ return invokeFunc(lastCallTime);
88
+ }
89
+ }
90
+ if (timerId === void 0) {
91
+ timerId = setTimeout(timerExpired, wait);
92
+ }
93
+ return result;
94
+ }
95
+ debounced.cancel = cancel;
96
+ debounced.flush = flush;
97
+ return debounced;
98
+ }
99
+ function isObject(value) {
100
+ var type = typeof value;
101
+ return !!value && (type == "object" || type == "function");
102
+ }
103
+ function isObjectLike(value) {
104
+ return !!value && typeof value == "object";
105
+ }
106
+ function isSymbol(value) {
107
+ return typeof value == "symbol" || isObjectLike(value) && objectToString.call(value) == symbolTag;
108
+ }
109
+ function toNumber(value) {
110
+ if (typeof value == "number") {
111
+ return value;
112
+ }
113
+ if (isSymbol(value)) {
114
+ return NAN;
115
+ }
116
+ if (isObject(value)) {
117
+ var other = typeof value.valueOf == "function" ? value.valueOf() : value;
118
+ value = isObject(other) ? other + "" : other;
119
+ }
120
+ if (typeof value != "string") {
121
+ return value === 0 ? value : +value;
122
+ }
123
+ value = value.replace(reTrim, "");
124
+ var isBinary = reIsBinary.test(value);
125
+ return isBinary || reIsOctal.test(value) ? freeParseInt(value.slice(2), isBinary ? 2 : 8) : reIsBadHex.test(value) ? NAN : +value;
126
+ }
127
+ var lodash_debounce = debounce;
128
+ const HIDDEN_CLASS = "copy-tuner-hidden";
129
+ class CopytunerBar {
130
+ constructor(element, data, callback) {
131
+ this.element = element;
132
+ this.data = data;
133
+ this.callback = callback;
134
+ this.searchBoxElement = element.querySelector(".js-copy-tuner-bar-search");
135
+ this.logMenuElement = this.makeLogMenu();
136
+ this.element.append(this.logMenuElement);
137
+ this.addHandler();
138
+ }
139
+ addHandler() {
140
+ const openLogButton = this.element.querySelector(".js-copy-tuner-bar-open-log");
141
+ openLogButton.addEventListener("click", (event) => {
142
+ event.preventDefault();
143
+ this.toggleLogMenu();
144
+ });
145
+ this.searchBoxElement.addEventListener("input", lodash_debounce(this.onKeyup.bind(this), 250));
146
+ }
147
+ show() {
148
+ this.element.classList.remove(HIDDEN_CLASS);
149
+ this.searchBoxElement.focus();
150
+ }
151
+ hide() {
152
+ this.element.classList.add(HIDDEN_CLASS);
153
+ }
154
+ showLogMenu() {
155
+ this.logMenuElement.classList.remove(HIDDEN_CLASS);
156
+ }
157
+ toggleLogMenu() {
158
+ this.logMenuElement.classList.toggle(HIDDEN_CLASS);
159
+ }
160
+ makeLogMenu() {
161
+ const div = document.createElement("div");
162
+ div.setAttribute("id", "copy-tuner-bar-log-menu");
163
+ div.classList.add(HIDDEN_CLASS);
164
+ const table = document.createElement("table");
165
+ const tbody = document.createElement("tbody");
166
+ tbody.classList.remove("is-not-initialized");
167
+ for (const key of Object.keys(this.data).sort()) {
168
+ const value = this.data[key];
169
+ if (value === "") {
170
+ continue;
171
+ }
172
+ const td1 = document.createElement("td");
173
+ td1.textContent = key;
174
+ const td2 = document.createElement("td");
175
+ td2.textContent = value;
176
+ const tr = document.createElement("tr");
177
+ tr.classList.add("copy-tuner-bar-log-menu__row");
178
+ tr.dataset.key = key;
179
+ tr.addEventListener("click", ({ currentTarget }) => {
180
+ this.callback(currentTarget.dataset.key);
181
+ });
182
+ tr.append(td1);
183
+ tr.append(td2);
184
+ tbody.append(tr);
185
+ }
186
+ table.append(tbody);
187
+ div.append(table);
188
+ return div;
189
+ }
190
+ onKeyup({ target }) {
191
+ const keyword = target.value.trim();
192
+ this.showLogMenu();
193
+ const rows = [...this.logMenuElement.querySelectorAll("tr")];
194
+ for (const row of rows) {
195
+ const isShow = keyword === "" || [...row.querySelectorAll("td")].some((td) => td.textContent.includes(keyword));
196
+ row.classList.toggle(HIDDEN_CLASS, !isShow);
197
+ }
198
+ }
199
+ }
200
+ const isMac = navigator.platform.toUpperCase().includes("MAC");
201
+ const isVisible = (element) => !!(element.offsetWidth || element.offsetHeight || element.getClientRects().length > 0);
202
+ const getOffset = (elment) => {
203
+ const box = elment.getBoundingClientRect();
204
+ return {
205
+ top: box.top + (window.pageYOffset - document.documentElement.clientTop),
206
+ left: box.left + (window.pageXOffset - document.documentElement.clientLeft)
207
+ };
208
+ };
209
+ const computeBoundingBox = (element) => {
210
+ if (!isVisible(element)) {
211
+ return null;
212
+ }
213
+ const boxFrame = getOffset(element);
214
+ boxFrame.right = boxFrame.left + element.offsetWidth;
215
+ boxFrame.bottom = boxFrame.top + element.offsetHeight;
216
+ return {
217
+ left: boxFrame.left,
218
+ top: boxFrame.top,
219
+ width: boxFrame.right - boxFrame.left,
220
+ height: boxFrame.bottom - boxFrame.top
221
+ };
222
+ };
223
+ const ZINDEX = 2e9;
224
+ class Specimen {
225
+ constructor(element, key, callback) {
226
+ this.element = element;
227
+ this.key = key;
228
+ this.callback = callback;
229
+ }
230
+ show() {
231
+ this.box = this.makeBox();
232
+ if (this.box === null)
233
+ return;
234
+ this.box.addEventListener("click", () => {
235
+ this.callback(this.key);
236
+ });
237
+ document.body.append(this.box);
238
+ }
239
+ remove() {
240
+ if (!this.box) {
241
+ return;
242
+ }
243
+ this.box.remove();
244
+ this.box = null;
245
+ }
246
+ makeBox() {
247
+ const box = document.createElement("div");
248
+ box.classList.add("copyray-specimen");
249
+ box.classList.add("Specimen");
250
+ const bounds = computeBoundingBox(this.element);
251
+ if (bounds === null)
252
+ return null;
253
+ for (const key of Object.keys(bounds)) {
254
+ const value = bounds[key];
255
+ box.style[key] = `${value}px`;
256
+ }
257
+ box.style.zIndex = ZINDEX;
258
+ const { position, top, left } = getComputedStyle(this.element);
259
+ if (position === "fixed") {
260
+ this.box.style.position = "fixed";
261
+ this.box.style.top = `${top}px`;
262
+ this.box.style.left = `${left}px`;
263
+ }
264
+ box.append(this.makeLabel());
265
+ return box;
266
+ }
267
+ makeLabel() {
268
+ const div = document.createElement("div");
269
+ div.classList.add("copyray-specimen-handle");
270
+ div.classList.add("Specimen");
271
+ div.textContent = this.key;
272
+ return div;
273
+ }
274
+ }
275
+ const findBlurbs = () => {
276
+ const filterNone = () => NodeFilter.FILTER_ACCEPT;
277
+ const iterator = document.createNodeIterator(document.body, NodeFilter.SHOW_COMMENT, filterNone, false);
278
+ const comments = [];
279
+ let curNode;
280
+ while (curNode = iterator.nextNode()) {
281
+ comments.push(curNode);
282
+ }
283
+ return comments.filter((comment) => comment.nodeValue.startsWith("COPYRAY")).map((comment) => {
284
+ const [, key] = comment.nodeValue.match(/^COPYRAY (\S*)$/);
285
+ const element = comment.parentNode;
286
+ return { key, element };
287
+ });
288
+ };
289
+ class Copyray {
290
+ constructor(baseUrl, data) {
291
+ this.baseUrl = baseUrl;
292
+ this.data = data;
293
+ this.isShowing = false;
294
+ this.specimens = [];
295
+ this.overlay = this.makeOverlay();
296
+ this.toggleButton = this.makeToggleButton();
297
+ this.boundOpen = this.open.bind(this);
298
+ this.copyTunerBar = new CopytunerBar(document.querySelector("#copy-tuner-bar"), this.data, this.boundOpen);
299
+ }
300
+ show() {
301
+ this.reset();
302
+ document.body.append(this.overlay);
303
+ this.makeSpecimens();
304
+ for (const specimen of this.specimens) {
305
+ specimen.show();
306
+ }
307
+ this.copyTunerBar.show();
308
+ this.isShowing = true;
309
+ }
310
+ hide() {
311
+ this.overlay.remove();
312
+ this.reset();
313
+ this.copyTunerBar.hide();
314
+ this.isShowing = false;
315
+ }
316
+ toggle() {
317
+ if (this.isShowing) {
318
+ this.hide();
319
+ } else {
320
+ this.show();
321
+ }
322
+ }
323
+ open(key) {
324
+ window.open(`${this.baseUrl}/blurbs/${key}/edit`);
325
+ }
326
+ makeSpecimens() {
327
+ for (const { element, key } of findBlurbs()) {
328
+ this.specimens.push(new Specimen(element, key, this.boundOpen));
329
+ }
330
+ }
331
+ makeToggleButton() {
332
+ const element = document.createElement("a");
333
+ element.addEventListener("click", () => {
334
+ this.show();
335
+ });
336
+ element.classList.add("copyray-toggle-button");
337
+ element.classList.add("hidden-on-mobile");
338
+ element.textContent = "Open CopyTuner";
339
+ document.body.append(element);
340
+ return element;
341
+ }
342
+ makeOverlay() {
343
+ const div = document.createElement("div");
344
+ div.setAttribute("id", "copyray-overlay");
345
+ div.addEventListener("click", () => this.hide());
346
+ return div;
347
+ }
348
+ reset() {
349
+ for (const specimen of this.specimens) {
350
+ specimen.remove();
351
+ }
352
+ }
353
+ }
354
+ var copyray = "";
355
+ const appendCopyTunerBar = (url) => {
356
+ const bar = document.createElement("div");
357
+ bar.id = "copy-tuner-bar";
358
+ bar.classList.add("copy-tuner-hidden");
359
+ bar.innerHTML = `
360
+ <a class="copy-tuner-bar-button" target="_blank" href="${url}">CopyTuner</a>
361
+ <a href="/copytuner" target="_blank" class="copy-tuner-bar-button">Sync</a>
362
+ <a href="javascript:void(0)" class="copy-tuner-bar-open-log copy-tuner-bar-button js-copy-tuner-bar-open-log">Translations in this page</a>
363
+ <input type="text" class="copy-tuner-bar__search js-copy-tuner-bar-search" placeholder="search">
364
+ `;
365
+ document.body.append(bar);
366
+ };
367
+ const start = () => {
368
+ const { url, data } = window.CopyTuner;
369
+ appendCopyTunerBar(url);
370
+ const copyray2 = new Copyray(url, data);
371
+ document.addEventListener("keydown", (event) => {
372
+ if (copyray2.isShowing && ["Escape", "Esc"].includes(event.key)) {
373
+ copyray2.hide();
374
+ return;
375
+ }
376
+ if ((isMac && event.metaKey || !isMac && event.ctrlKey) && event.shiftKey && event.key === "k") {
377
+ copyray2.toggle();
378
+ }
379
+ });
380
+ if (console) {
381
+ console.log(`Ready to Copyray. Press ${isMac ? "cmd+shift+k" : "ctrl+shift+k"} to scan your UI.`);
382
+ }
383
+ };
384
+ if (document.readyState === "complete" || document.readyState !== "loading") {
385
+ start();
386
+ } else {
387
+ document.addEventListener("DOMContentLoaded", () => start());
388
+ }
@@ -0,0 +1 @@
1
+ @charset "UTF-8";#copyray-overlay,#copyray-overlay *,#copyray-overlay a:hover,#copyray-overlay a:visited,#copyray-overlay a:active{background:none;border:none;bottom:auto;clear:none;cursor:default;float:none;font-family:Arial,Helvetica,sans-serif;font-size:medium;font-style:normal;font-weight:400;height:auto;left:auto;letter-spacing:normal;line-height:normal;max-height:none;max-width:none;min-height:0;min-width:0;overflow:visible;position:static;right:auto;text-align:left;text-decoration:none;text-indent:0;text-transform:none;top:auto;visibility:visible;white-space:normal;width:auto;z-index:auto}#copyray-overlay{position:fixed;left:0;top:0;bottom:0;right:0;background-image:radial-gradient(ellipse farthest-corner at center,rgba(0,0,0,.4) 10%,rgba(0,0,0,.8) 100%);z-index:9000}.copyray-specimen{position:absolute;background:rgba(255,255,255,.15);outline:1px solid rgba(255,255,255,.8);outline-offset:-1px;color:#666;font-family:Helvetica Neue,sans-serif;font-size:13px;box-shadow:0 1px 3px #000000b3}.copyray-specimen:hover{cursor:pointer;background:rgba(255,255,255,.4)}.copyray-specimen.Specimen{outline:1px solid rgba(255,50,50,.8);background:rgba(255,50,50,.1)}.copyray-specimen.Specimen:hover{background:rgba(255,50,50,.4)}.copyray-specimen-handle{float:left;background:#fff;padding:0 3px;color:#333;font-size:10px}.copyray-specimen-handle.Specimen{background:rgba(255,50,50,.8);color:#fff}a.copyray-toggle-button{display:block;position:fixed;left:0;bottom:0;color:#fff;background:black;padding:12px 16px;border-radius:0 10px 0 0;opacity:0;transition:opacity .6s ease-in-out;z-index:10000;font-size:12px;cursor:pointer}a.copyray-toggle-button:hover{opacity:1}#copy-tuner-bar{position:fixed;left:0;right:0;bottom:0;height:40px;padding:0 8px;background:#222;font-family:Helvetica Neue,Helvetica,Arial,sans-serif;font-weight:200;color:#fff;z-index:2147483647;box-shadow:0 -1px #ffffff1a,inset 0 2px 6px #000c;background-image:linear-gradient(rgba(0,0,0,0),rgba(0,0,0,.3))}#copy-tuner-bar-log-menu{position:fixed;left:0;right:0;bottom:40px;max-height:calc(100vh - 40px);background:#222;font-family:Helvetica Neue,Helvetica,Arial,sans-serif;color:#fff;z-index:2147483647;overflow-y:auto}#copy-tuner-bar-log-menu tbody td{padding:2px 8px}#copy-tuner-bar-log-menu tbody tr{cursor:pointer}#copy-tuner-bar-log-menu tbody tr:hover{background:#444}#copy-tuner-bar-log-menu tbody a{color:#fff}#copy-tuner-bar-log-menu tbody a:hover,#copy-tuner-bar-log-menu tbody a:focus{color:#fff;text-decoration:underline}.copy-tuner-bar-button{position:relative;display:inline-block;color:#fff;margin:8px 1px;height:24px;line-height:24px;padding:0 8px;font-size:14px;cursor:pointer;vertical-align:middle;background-color:#444;background-image:linear-gradient(rgba(0,0,0,0),rgba(0,0,0,.2));border-radius:2px;box-shadow:1px 1px 1px #00000080,inset 0 1px #fff3,inset 0 0 2px #fff3;text-shadow:0 -1px 0 rgba(0,0,0,.4)}.copy-tuner-bar-button:hover,.copy-tuner-bar-button:focus{color:#fff;text-decoration:none;background-color:#555}input[type=text].copy-tuner-bar__search{-webkit-appearance:none;-moz-appearance:none;appearance:none;border:none;border-radius:2px;background-image:linear-gradient(rgba(0,0,0,.2),rgba(0,0,0,0));box-shadow:inset 0 1px #0003,inset 0 0 2px #0003;padding:2px 8px;margin:0;line-height:20px;vertical-align:middle;color:#000;width:auto;height:auto;font-size:14px}.copy-tuner-hidden{display:none!important}@media screen and (max-width: 480px){.hidden-on-mobile{display:none!important}}
@@ -3,13 +3,13 @@ $LOAD_PATH.push File.expand_path('../lib', __FILE__)
3
3
  require 'copy_tuner_client/version'
4
4
 
5
5
  Gem::Specification.new do |s|
6
- s.required_ruby_version = '>= 2.5.0'
6
+ s.required_ruby_version = '>= 2.7.0'
7
7
  s.add_dependency 'i18n', '>= 0.5.0'
8
8
  s.add_dependency 'json'
9
9
  s.add_dependency 'nokogiri'
10
- s.add_development_dependency 'rails', '~> 5.2.4.2'
10
+ s.add_development_dependency 'rails', '~> 6.1'
11
11
  s.add_development_dependency 'rake'
12
- s.add_development_dependency 'rspec', '3.8.0' # NOTE: 3.9.0だとundefined method `synchronize'でコケるテストがある
12
+ s.add_development_dependency 'rspec'
13
13
  s.add_development_dependency 'sham_rack'
14
14
  s.add_development_dependency 'sinatra'
15
15
  s.add_development_dependency 'sqlite3'
@@ -0,0 +1,5 @@
1
+ source "http://rubygems.org"
2
+
3
+ gem "rails", "~> 6.1"
4
+
5
+ gemspec :path => "../"
@@ -0,0 +1,5 @@
1
+ source "http://rubygems.org"
2
+
3
+ gem "rails", "~> 7.0"
4
+
5
+ gemspec :path => "../"
@@ -0,0 +1,5 @@
1
+ source "http://rubygems.org"
2
+
3
+ gem "rails", github: "rails/rails", branch: "main"
4
+
5
+ gemspec :path => "../"
data/index.html ADDED
@@ -0,0 +1,34 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+
4
+ <head>
5
+ <meta charset="UTF-8" />
6
+ <link rel="icon" type="image/svg+xml" href="/src/favicon.svg" />
7
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
8
+ <title>Vite App</title>
9
+ </head>
10
+
11
+ <body>
12
+ <div id="app">
13
+ <ul>
14
+ <li>
15
+ <!--COPYRAY projects.index.locales-->言語
16
+ </li>
17
+ <li>
18
+ <!--COPYRAY projects.index.blurbs-->翻訳キー
19
+ </li>
20
+ <li>
21
+ <!--COPYRAY projects.index.members-->メンバー数
22
+ </li>
23
+ </ul>
24
+ </div>
25
+ <script>
26
+ window.CopyTuner = {
27
+ url: 'https://copy-tuner.herokuapp.com/projects/xxx',
28
+ data: {},
29
+ }
30
+ </script>
31
+ <script type="module" src="/src/main.ts"></script>
32
+ </body>
33
+
34
+ </html>
@@ -19,7 +19,7 @@ module CopyTunerClient
19
19
  :proxy_port, :proxy_user, :secure, :polling_delay, :sync_interval,
20
20
  :sync_interval_staging, :sync_ignore_path_regex, :logger,
21
21
  :framework, :middleware, :disable_middleware, :disable_test_translation,
22
- :ca_file, :exclude_key_regexp, :s3_host, :locales].freeze
22
+ :ca_file, :exclude_key_regexp, :s3_host, :locales, :ignored_keys, :ignored_key_handler].freeze
23
23
 
24
24
  # @return [String] The API key for your project, found on the project edit form.
25
25
  attr_accessor :api_key
@@ -131,6 +131,12 @@ module CopyTunerClient
131
131
  # @return [Boolean] Html escape
132
132
  attr_accessor :html_escape
133
133
 
134
+ # @return [Array<String>] A list of ignored keys
135
+ attr_accessor :ignored_keys
136
+
137
+ # @return [Proc]
138
+ attr_accessor :ignored_key_handler
139
+
134
140
  alias_method :secure?, :secure
135
141
 
136
142
  # Instantiated from {CopyTunerClient.configure}. Sets defaults.
@@ -152,6 +158,8 @@ module CopyTunerClient
152
158
  self.s3_host = 'copy-tuner-data-prod.s3.amazonaws.com'
153
159
  self.disable_copyray_comment_injection = false
154
160
  self.html_escape = false
161
+ self.ignored_keys = []
162
+ self.ignored_key_handler = -> (e) { raise e }
155
163
 
156
164
  @applied = false
157
165
  end
@@ -12,8 +12,6 @@ module CopyTunerClient
12
12
  if html_headers?(status, headers) && body = response_body(response)
13
13
  body = append_css(body)
14
14
  body = append_js(body)
15
- body = append_translation_logs(body)
16
- body = inject_copy_tuner_bar(body)
17
15
  content_length = body.bytesize.to_s
18
16
  headers['Content-Length'] = content_length
19
17
  # maintains compatibility with other middlewares
@@ -33,41 +31,29 @@ module CopyTunerClient
33
31
  ActionController::Base.helpers
34
32
  end
35
33
 
36
- def append_translation_logs(html)
37
- if CopyTunerClient::TranslationLog.initialized?
38
- json = CopyTunerClient::TranslationLog.translations.to_json
39
- # Use block to avoid back reference \?
40
- append_to_html_body(html, "<div id='copy-tuner-data' data-copy-tuner-translation-log='#{ERB::Util.html_escape json}' data-copy-tuner-url='#{CopyTunerClient.configuration.project_url}'></div>")
41
- else
42
- html
43
- end
44
- end
45
-
46
- def inject_copy_tuner_bar(html)
47
- append_to_html_body(html, render_copy_tuner_bar)
48
- end
49
-
50
- def render_copy_tuner_bar
51
- if ApplicationController.respond_to?(:render)
52
- # Rails 5
53
- ApplicationController.render(:partial => "/copy_tuner_bar").html_safe
54
- else
55
- # Rails <= 4.2
56
- ac = ActionController::Base.new
57
- ac.render_to_string(:partial => '/copy_tuner_bar').html_safe
58
- end
59
- end
60
-
61
34
  def append_css(html)
62
35
  append_to_html_body(html, css_tag)
63
36
  end
64
37
 
65
38
  def append_js(html)
66
- append_to_html_body(html, helpers.javascript_include_tag(:copyray))
39
+ json =
40
+ if CopyTunerClient::TranslationLog.initialized?
41
+ CopyTunerClient::TranslationLog.translations.to_json
42
+ else
43
+ '{}'
44
+ end
45
+
46
+ append_to_html_body(html, helpers.javascript_tag(<<~SCRIPT))
47
+ window.CopyTuner = {
48
+ url: '#{CopyTunerClient.configuration.project_url}',
49
+ data: #{json},
50
+ }
51
+ SCRIPT
52
+ append_to_html_body(html, helpers.javascript_include_tag(:main))
67
53
  end
68
54
 
69
55
  def css_tag
70
- helpers.stylesheet_link_tag :copyray, media: :all
56
+ helpers.stylesheet_link_tag :style, media: :screen
71
57
  end
72
58
 
73
59
  def append_to_html_body(html, content)
@@ -1,5 +1,6 @@
1
1
  require 'copy_tuner_client/copyray'
2
2
  require 'copy_tuner_client/translation_log'
3
+ require 'copy_tuner_client/helper_extension'
3
4
 
4
5
  module CopyTunerClient
5
6
  # Connects to integration points for Rails 3 applications
@@ -10,36 +11,10 @@ module CopyTunerClient
10
11
 
11
12
  initializer :initialize_copy_tuner_hook_methods, :after => :load_config_initializers do |app|
12
13
  ActiveSupport.on_load(:action_view) do
13
- ActionView::Helpers::TranslationHelper.class_eval do
14
- def translate_with_copyray_comment(key, options = {})
15
- source = translate_without_copyray_comment(key, options)
16
- # TODO: 0.6.0以降で options[:rescue_format] == text を消す
17
- if CopyTunerClient.configuration.disable_copyray_comment_injection || options[:rescue_format] == :text
18
- if options[:rescue_format] == :text
19
- ActiveSupport::Deprecation.warn('rescue_format option is deprecated in copy_tuner_client@0.6.0')
20
- end
21
- source
22
- else
23
- separator = options[:separator] || I18n.default_separator
24
- scope = options[:scope]
25
- normalized_key =
26
- if key.to_s.first == '.'
27
- scope_key_by_partial(key)
28
- else
29
- I18n.normalize_keys(nil, key, scope, separator).join(separator)
30
- end
31
- CopyTunerClient::Copyray.augment_template(source, normalized_key)
32
- end
33
- end
34
- if CopyTunerClient.configuration.enable_middleware?
35
- alias_method :translate_without_copyray_comment, :translate
36
- alias_method :translate, :translate_with_copyray_comment
37
- alias :t :translate
38
- alias :tt :translate_without_copyray_comment
39
- else
40
- alias :tt :translate
41
- end
42
- end
14
+ CopyTunerClient::HelperExtension.hook_translation_helper(
15
+ ActionView::Helpers::TranslationHelper,
16
+ middleware_enabled: CopyTunerClient.configuration.enable_middleware?
17
+ )
43
18
  end
44
19
 
45
20
  if CopyTunerClient.configuration.enable_middleware?
@@ -50,7 +25,7 @@ module CopyTunerClient
50
25
  end
51
26
 
52
27
  initializer "copy_tuner.assets.precompile", group: :all do |app|
53
- app.config.assets.precompile += ["copyray.js", "copyray.css"]
28
+ app.config.assets.precompile += ['main.js', 'style.css']
54
29
  end
55
30
  end
56
31
  end
@@ -9,4 +9,7 @@ module CopyTunerClient
9
9
  # server does not recognize. Polling is aborted when this error is raised.
10
10
  class InvalidApiKey < StandardError
11
11
  end
12
+
13
+ class IgnoredKey < StandardError
14
+ end
12
15
  end
@@ -0,0 +1,34 @@
1
+ module CopyTunerClient
2
+ module HelperExtension
3
+ class << self
4
+ def hook_translation_helper(mod, middleware_enabled:)
5
+ mod.class_eval do
6
+ def translate_with_copyray_comment(key, **options)
7
+ source = translate_without_copyray_comment(key, **options)
8
+ if CopyTunerClient.configuration.disable_copyray_comment_injection
9
+ source
10
+ else
11
+ separator = options[:separator] || I18n.default_separator
12
+ scope = options[:scope]
13
+ normalized_key =
14
+ if key.to_s.first == '.'
15
+ scope_key_by_partial(key)
16
+ else
17
+ I18n.normalize_keys(nil, key, scope, separator).join(separator)
18
+ end
19
+ CopyTunerClient::Copyray.augment_template(source, normalized_key)
20
+ end
21
+ end
22
+ if middleware_enabled
23
+ alias_method :translate_without_copyray_comment, :translate
24
+ alias_method :translate, :translate_with_copyray_comment
25
+ alias :t :translate
26
+ alias :tt :translate_without_copyray_comment
27
+ else
28
+ alias :tt :translate
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -65,6 +65,12 @@ module CopyTunerClient
65
65
 
66
66
  parts = I18n.normalize_keys(locale, key, scope, options[:separator])
67
67
  key_with_locale = parts.join('.')
68
+ key_without_locale = parts[1..].join('.')
69
+
70
+ if CopyTunerClient::configuration.ignored_keys.include?(key_without_locale)
71
+ CopyTunerClient::configuration.ignored_key_handler.call(IgnoredKey.new("Ignored key: #{key_without_locale}"))
72
+ end
73
+
68
74
  content = cache[key_with_locale] || super
69
75
  cache[key_with_locale] = nil if content.nil?
70
76
  content