copy_tuner_client 0.10.0 → 0.13.0

Sign up to get free protection for your applications and to get access to all the features.
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