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.
- checksums.yaml +4 -4
- data/.eslintrc.js +12 -0
- data/.gitattributes +2 -0
- data/.github/workflows/rspec.yml +22 -30
- data/.ruby-version +1 -1
- data/.vscode/settings.json +7 -0
- data/CHANGELOG.md +13 -0
- data/Gemfile +1 -1
- data/Gemfile.lock +168 -145
- data/README.md +8 -10
- data/app/assets/javascripts/main.js +388 -0
- data/app/assets/stylesheets/style.css +1 -0
- data/copy_tuner_client.gemspec +3 -3
- data/gemfiles/6.1.gemfile +5 -0
- data/gemfiles/7.0.gemfile +5 -0
- data/gemfiles/main.gemfile +5 -0
- data/index.html +34 -0
- data/lib/copy_tuner_client/configuration.rb +9 -1
- data/lib/copy_tuner_client/copyray_middleware.rb +15 -29
- data/lib/copy_tuner_client/engine.rb +6 -31
- data/lib/copy_tuner_client/errors.rb +3 -0
- data/lib/copy_tuner_client/helper_extension.rb +34 -0
- data/lib/copy_tuner_client/i18n_backend.rb +6 -0
- data/lib/copy_tuner_client/version.rb +1 -1
- data/package.json +14 -15
- data/spec/copy_tuner_client/helper_extension_spec.rb +40 -0
- data/spec/copy_tuner_client/i18n_backend_spec.rb +2 -0
- data/{app/assets/stylesheets → src}/copyray.css +0 -0
- data/src/copyray.ts +130 -0
- data/src/copytuner_bar.ts +115 -0
- data/src/main.ts +58 -0
- data/src/specimen.ts +84 -0
- data/src/util.ts +38 -0
- data/src/vite-env.d.ts +1 -0
- data/tsconfig.json +26 -0
- data/vite.config.ts +18 -0
- data/yarn.lock +2251 -0
- metadata +36 -27
- data/.eslintrc +0 -4
- data/app/assets/javascripts/copyray.js +0 -1069
- data/app/views/_copy_tuner_bar.html.erb +0 -6
- data/gemfiles/5.2.gemfile +0 -7
- data/gemfiles/6.0.gemfile +0 -7
- data/package-lock.json +0 -4341
- data/rollup.config.js +0 -16
- data/src/copyray.js +0 -112
- data/src/copytuner_bar.js +0 -96
- data/src/main.js +0 -42
- data/src/specimen.js +0 -63
- 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}}
|
data/copy_tuner_client.gemspec
CHANGED
@@ -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.
|
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', '~>
|
10
|
+
s.add_development_dependency 'rails', '~> 6.1'
|
11
11
|
s.add_development_dependency 'rake'
|
12
|
-
s.add_development_dependency 'rspec'
|
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'
|
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
|
-
|
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 :
|
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
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
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 += [
|
28
|
+
app.config.assets.precompile += ['main.js', 'style.css']
|
54
29
|
end
|
55
30
|
end
|
56
31
|
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
|