copy_tuner_client 1.5.0 → 2.1.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/.gitattributes +2 -2
- data/.github/workflows/rspec.yml +1 -1
- data/CHANGELOG.md +27 -0
- data/CLAUDE.md +6 -4
- data/README.md +6 -27
- data/UPGRADING.md +201 -0
- data/app/assets/javascripts/copytuner.js +162 -250
- data/biome.json +39 -0
- data/index.html +9 -11
- data/lib/copy_tuner_client/cache.rb +0 -2
- data/lib/copy_tuner_client/configuration.rb +23 -37
- data/lib/copy_tuner_client/copyray/marker.rb +24 -0
- data/lib/copy_tuner_client/copyray/rewriter.rb +113 -0
- data/lib/copy_tuner_client/copyray.rb +11 -4
- data/lib/copy_tuner_client/copyray_middleware.rb +10 -8
- data/lib/copy_tuner_client/engine.rb +1 -1
- data/lib/copy_tuner_client/helper_extension.rb +0 -3
- data/lib/copy_tuner_client/i18n_backend.rb +5 -12
- data/lib/copy_tuner_client/version.rb +1 -1
- data/mise.toml +3 -0
- data/package.json +9 -13
- data/pnpm-lock.yaml +600 -0
- data/skills/copy-tuner/SKILL.md +37 -6
- data/skills/copy-tuner-to-locales-cleanup/SKILL.md +4 -4
- data/skills/copy-tuner-to-locales-migrate-prefix/references/local-first-regexp.md +4 -9
- data/skills/copy-tuner-to-t-migrate/SKILL.md +131 -0
- data/skills/copy-tuner-to-t-migrate/scripts/migrate_tt.rb +189 -0
- data/spec/copy_tuner_client/cache_spec.rb +2 -10
- data/spec/copy_tuner_client/client_spec.rb +1 -0
- data/spec/copy_tuner_client/configuration_spec.rb +16 -16
- data/spec/copy_tuner_client/copyray/marker_spec.rb +41 -0
- data/spec/copy_tuner_client/copyray/rewriter_spec.rb +216 -0
- data/spec/copy_tuner_client/copyray_middleware_spec.rb +88 -0
- data/spec/copy_tuner_client/copyray_spec.rb +22 -39
- data/spec/copy_tuner_client/helper_extension_spec.rb +18 -5
- data/spec/copy_tuner_client/i18n_backend_spec.rb +8 -15
- data/spec/support/client_spec_helpers.rb +0 -1
- data/src/copyray-overlay.ts +125 -0
- data/src/copytuner-bar.ts +153 -0
- data/src/main.ts +33 -26
- data/src/{copyray.css → styles.ts} +112 -136
- data/src/util.ts +9 -1
- data/tsconfig.json +5 -10
- data/vite.config.ts +0 -1
- metadata +15 -8
- data/.eslintrc.js +0 -12
- data/app/assets/stylesheets/copytuner.css +0 -1
- data/src/copyray.ts +0 -130
- data/src/copytuner_bar.ts +0 -115
- data/src/specimen.ts +0 -84
- data/yarn.lock +0 -2540
|
@@ -1,252 +1,164 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
//#region src/styles.ts
|
|
2
|
+
var e = navigator.platform.toUpperCase().includes("MAC"), t = (e) => !!(e.offsetWidth || e.offsetHeight || e.getClientRects().length > 0), n = (e) => {
|
|
3
|
+
let t = e.getBoundingClientRect();
|
|
4
|
+
return {
|
|
5
|
+
top: t.top + (window.pageYOffset - document.documentElement.clientTop),
|
|
6
|
+
left: t.left + (window.pageXOffset - document.documentElement.clientLeft)
|
|
7
|
+
};
|
|
8
|
+
}, r = (e) => {
|
|
9
|
+
if (!t(e)) return null;
|
|
10
|
+
let r = n(e);
|
|
11
|
+
return r.right = r.left + e.offsetWidth, r.bottom = r.top + e.offsetHeight, {
|
|
12
|
+
left: r.left,
|
|
13
|
+
top: r.top,
|
|
14
|
+
width: r.right - r.left,
|
|
15
|
+
height: r.bottom - r.top
|
|
16
|
+
};
|
|
17
|
+
}, i = (e, t) => {
|
|
18
|
+
let n;
|
|
19
|
+
return (...r) => {
|
|
20
|
+
clearTimeout(n), n = setTimeout(() => e(...r), t);
|
|
21
|
+
};
|
|
22
|
+
}, a = () => Array.from(document.querySelectorAll("[data-copyray-key]")).map((e) => ({
|
|
23
|
+
keys: (e.getAttribute("data-copyray-key") ?? "").split(",").filter(Boolean),
|
|
24
|
+
element: e
|
|
25
|
+
})), o = class extends HTMLElement {
|
|
26
|
+
#e = () => {};
|
|
27
|
+
#t = () => {};
|
|
28
|
+
#n;
|
|
29
|
+
#r;
|
|
30
|
+
#i;
|
|
31
|
+
constructor() {
|
|
32
|
+
super();
|
|
33
|
+
let e = this.attachShadow({ mode: "open" }), t = document.createElement("style");
|
|
34
|
+
t.textContent = "\n:host {\n position: absolute;\n top: 0;\n left: 0;\n width: 0;\n height: 0;\n}\n\n:host([hidden]) {\n display: none;\n}\n\n.backdrop {\n position: fixed;\n inset: 0;\n background-image: radial-gradient(\n ellipse farthest-corner at center,\n rgba(0, 0, 0, 0.4) 10%,\n rgba(0, 0, 0, 0.8) 100%\n );\n z-index: 9000;\n}\n\n.specimen {\n position: absolute;\n background: rgba(255, 50, 50, 0.1);\n outline: 1px solid rgba(255, 50, 50, 0.8);\n outline-offset: -1px;\n color: #666;\n font-family: 'Helvetica Neue', sans-serif;\n font-size: 13px;\n box-shadow: 0 1px 3px rgba(0, 0, 0, 0.7);\n z-index: 2000000000;\n}\n\n.specimen:hover {\n cursor: pointer;\n background: rgba(255, 50, 50, 0.4);\n}\n\n.specimen-handle {\n float: left;\n margin: 0 2px 2px 0;\n background: rgba(255, 50, 50, 0.8);\n padding: 0 3px;\n color: #fff;\n font-size: 10px;\n cursor: pointer;\n}\n\n.toggle-button {\n display: block;\n position: fixed;\n left: 0;\n bottom: 0;\n color: white;\n background: black;\n padding: 12px 16px;\n border-radius: 0 10px 0 0;\n opacity: 0;\n transition: opacity 0.6s ease-in-out;\n z-index: 10000;\n font-size: 12px;\n cursor: pointer;\n text-decoration: none;\n}\n\n.toggle-button:hover {\n opacity: 1;\n}\n\n@media screen and (max-width: 480px) {\n .toggle-button {\n display: none;\n }\n}\n", e.append(t), this.#n = document.createElement("div"), this.#n.classList.add("backdrop"), this.#n.addEventListener("click", () => this.hide()), this.#r = document.createElement("div"), this.#r.classList.add("specimens"), this.#i = document.createElement("a"), this.#i.classList.add("toggle-button"), this.#i.textContent = "Open CopyTuner", this.#i.addEventListener("click", () => this.#t()), e.append(this.#n, this.#r, this.#i), this.hide();
|
|
35
|
+
}
|
|
36
|
+
set onOpen(e) {
|
|
37
|
+
this.#e = e;
|
|
38
|
+
}
|
|
39
|
+
set onToggle(e) {
|
|
40
|
+
this.#t = e;
|
|
41
|
+
}
|
|
42
|
+
get isShowing() {
|
|
43
|
+
return !this.#n.hidden;
|
|
44
|
+
}
|
|
45
|
+
show() {
|
|
46
|
+
this.reset(), this.#n.hidden = !1;
|
|
47
|
+
for (let { element: e, keys: t } of a()) {
|
|
48
|
+
let n = this.makeBox(e, t);
|
|
49
|
+
n && this.#r.append(n);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
hide() {
|
|
53
|
+
this.reset(), this.#n.hidden = !0;
|
|
54
|
+
}
|
|
55
|
+
reset() {
|
|
56
|
+
this.#r.replaceChildren();
|
|
57
|
+
}
|
|
58
|
+
makeBox(e, t) {
|
|
59
|
+
let n = r(e);
|
|
60
|
+
if (n === null) return null;
|
|
61
|
+
let i = document.createElement("div");
|
|
62
|
+
i.classList.add("specimen"), i.style.left = `${n.left}px`, i.style.top = `${n.top}px`, i.style.width = `${n.width}px`, i.style.height = `${n.height}px`;
|
|
63
|
+
let { position: a, top: o, left: s } = getComputedStyle(e);
|
|
64
|
+
a === "fixed" && (i.style.position = "fixed", i.style.top = o, i.style.left = s), i.addEventListener("click", () => this.#e(t[0]));
|
|
65
|
+
for (let e of t) i.append(this.makeLabel(e));
|
|
66
|
+
return i;
|
|
67
|
+
}
|
|
68
|
+
makeLabel(e) {
|
|
69
|
+
let t = document.createElement("div");
|
|
70
|
+
return t.classList.add("specimen-handle"), t.textContent = e, t.addEventListener("click", (t) => {
|
|
71
|
+
t.stopPropagation(), this.#e(e);
|
|
72
|
+
}), t;
|
|
73
|
+
}
|
|
74
|
+
}, s = class extends HTMLElement {
|
|
75
|
+
#e = () => {};
|
|
76
|
+
#t;
|
|
77
|
+
#n;
|
|
78
|
+
constructor() {
|
|
79
|
+
super(), this.attachShadow({ mode: "open" });
|
|
80
|
+
}
|
|
81
|
+
connectedCallback() {
|
|
82
|
+
this.hidden = !0;
|
|
83
|
+
}
|
|
84
|
+
init({ url: e, data: t, keysSkipped: n, onOpen: r }) {
|
|
85
|
+
this.#e = r;
|
|
86
|
+
let a = this.shadowRoot, o = document.createElement("style");
|
|
87
|
+
o.textContent = "\n:host {\n position: fixed;\n left: 0;\n right: 0;\n bottom: 0;\n height: 40px;\n padding: 0 8px;\n background: #222;\n font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;\n font-weight: 200;\n color: #fff;\n z-index: 2147483647;\n box-shadow: 0 -1px 0 rgba(255, 255, 255, 0.1), inset 0 2px 6px rgba(0, 0, 0, 0.8);\n background-image: linear-gradient(rgba(0, 0, 0, 0), rgba(0, 0, 0, 0.3));\n box-sizing: border-box;\n}\n\n:host([hidden]) {\n display: none;\n}\n\n.log-menu {\n position: fixed;\n left: 0;\n right: 0;\n bottom: 40px;\n max-height: calc(100vh - 40px);\n background: #222;\n color: #fff;\n overflow-y: auto;\n}\n\n.log-menu[hidden] {\n display: none;\n}\n\n.log-menu tbody td {\n padding: 2px 8px;\n}\n\n.log-menu tbody tr {\n cursor: pointer;\n}\n\n.log-menu tbody tr:hover {\n background: #444;\n}\n\n.log-menu tbody tr[hidden] {\n display: none;\n}\n\n.button {\n position: relative;\n display: inline-block;\n color: #fff;\n margin: 8px 1px;\n height: 24px;\n line-height: 24px;\n padding: 0 8px;\n font-size: 14px;\n cursor: pointer;\n vertical-align: middle;\n background-color: #444;\n background-image: linear-gradient(rgba(0, 0, 0, 0), rgba(0, 0, 0, 0.2));\n border-radius: 2px;\n box-shadow: 1px 1px 1px rgba(0, 0, 0, 0.5), inset 0 1px 0 rgba(255, 255, 255, 0.2),\n inset 0 0 2px rgba(255, 255, 255, 0.2);\n text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.4);\n text-decoration: none;\n}\n\n.button:hover,\n.button:focus {\n color: #fff;\n text-decoration: none;\n background-color: #555;\n}\n\n.notice {\n display: inline-block;\n margin: 8px;\n font-size: 13px;\n line-height: 24px;\n vertical-align: middle;\n color: #ffd24d;\n}\n\n.search {\n appearance: none;\n border: none;\n border-radius: 2px;\n background-image: linear-gradient(rgba(0, 0, 0, 0.2), rgba(0, 0, 0, 0));\n box-shadow: inset 0 1px 0 rgba(0, 0, 0, 0.2), inset 0 0 2px rgba(0, 0, 0, 0.2);\n padding: 2px 8px;\n margin: 0;\n line-height: 20px;\n vertical-align: middle;\n color: black;\n width: auto;\n height: auto;\n font-size: 14px;\n}\n", a.append(o);
|
|
88
|
+
let s = this.makeButton("CopyTuner", e, "_blank"), c = this.makeButton("Sync", "/copytuner", "_blank"), l = this.makeButton("Translations in this page", "javascript:void(0)");
|
|
89
|
+
this.#t = document.createElement("input"), this.#t.type = "text", this.#t.classList.add("search"), this.#t.placeholder = "search", a.append(s, c, l, this.#t), this.#n = this.makeLogMenu(t), a.append(this.#n), n && this.appendSkippedNotice(), l.addEventListener("click", (e) => {
|
|
90
|
+
e.preventDefault(), this.toggleLogMenu();
|
|
91
|
+
}), this.#t.addEventListener("input", i(this.onSearch.bind(this), 250));
|
|
92
|
+
}
|
|
93
|
+
show() {
|
|
94
|
+
this.hidden = !1, this.#t.focus();
|
|
95
|
+
}
|
|
96
|
+
hide() {
|
|
97
|
+
this.hidden = !0;
|
|
98
|
+
}
|
|
99
|
+
makeButton(e, t, n) {
|
|
100
|
+
let r = document.createElement("a");
|
|
101
|
+
return r.classList.add("button"), r.textContent = e, r.href = t, n && (r.target = n), r;
|
|
102
|
+
}
|
|
103
|
+
appendSkippedNotice() {
|
|
104
|
+
let e = document.createElement("span");
|
|
105
|
+
e.classList.add("notice"), e.textContent = "⚠ This page is too large for the overlay. Use \"Translations in this page\" to edit.", this.shadowRoot.append(e);
|
|
106
|
+
}
|
|
107
|
+
showLogMenu() {
|
|
108
|
+
this.#n.hidden = !1;
|
|
109
|
+
}
|
|
110
|
+
toggleLogMenu() {
|
|
111
|
+
this.#n.hidden = !this.#n.hidden;
|
|
112
|
+
}
|
|
113
|
+
makeLogMenu(e) {
|
|
114
|
+
let t = document.createElement("div");
|
|
115
|
+
t.classList.add("log-menu"), t.hidden = !0;
|
|
116
|
+
let n = document.createElement("table"), r = document.createElement("tbody");
|
|
117
|
+
for (let t of Object.keys(e).sort()) {
|
|
118
|
+
let n = e[t];
|
|
119
|
+
if (n === "") continue;
|
|
120
|
+
let i = document.createElement("td");
|
|
121
|
+
i.textContent = t;
|
|
122
|
+
let a = document.createElement("td");
|
|
123
|
+
a.textContent = n;
|
|
124
|
+
let o = document.createElement("tr");
|
|
125
|
+
o.dataset.key = t, o.addEventListener("click", ({ currentTarget: e }) => {
|
|
126
|
+
let t = e;
|
|
127
|
+
t.dataset.key && this.#e(t.dataset.key);
|
|
128
|
+
}), o.append(i, a), r.append(o);
|
|
129
|
+
}
|
|
130
|
+
return n.append(r), t.append(n), t;
|
|
131
|
+
}
|
|
132
|
+
onSearch() {
|
|
133
|
+
let e = this.#t.value.trim();
|
|
134
|
+
this.showLogMenu();
|
|
135
|
+
let t = [...this.#n.querySelectorAll("tr")];
|
|
136
|
+
for (let n of t) n.hidden = !(e === "" || [...n.querySelectorAll("td")].some((t) => (t.textContent ?? "").includes(e)));
|
|
137
|
+
}
|
|
3
138
|
};
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
return S(i);
|
|
28
|
-
a = setTimeout(m, M(i));
|
|
29
|
-
}
|
|
30
|
-
function S(i) {
|
|
31
|
-
return a = void 0, b && o ? g(i) : (o = s = void 0, r);
|
|
32
|
-
}
|
|
33
|
-
function I() {
|
|
34
|
-
a !== void 0 && clearTimeout(a), u = 0, o = c = s = a = void 0;
|
|
35
|
-
}
|
|
36
|
-
function N() {
|
|
37
|
-
return a === void 0 ? r : S(v());
|
|
38
|
-
}
|
|
39
|
-
function k() {
|
|
40
|
-
var i = v(), l = w(i);
|
|
41
|
-
if (o = arguments, s = this, c = i, l) {
|
|
42
|
-
if (a === void 0)
|
|
43
|
-
return B(c);
|
|
44
|
-
if (f)
|
|
45
|
-
return a = setTimeout(m, e), g(c);
|
|
46
|
-
}
|
|
47
|
-
return a === void 0 && (a = setTimeout(m, e)), r;
|
|
48
|
-
}
|
|
49
|
-
return k.cancel = I, k.flush = N, k;
|
|
50
|
-
}
|
|
51
|
-
function E(t) {
|
|
52
|
-
var e = typeof t;
|
|
53
|
-
return !!t && (e == "object" || e == "function");
|
|
54
|
-
}
|
|
55
|
-
function z(t) {
|
|
56
|
-
return !!t && typeof t == "object";
|
|
57
|
-
}
|
|
58
|
-
function G(t) {
|
|
59
|
-
return typeof t == "symbol" || z(t) && U.call(t) == _;
|
|
60
|
-
}
|
|
61
|
-
function O(t) {
|
|
62
|
-
if (typeof t == "number")
|
|
63
|
-
return t;
|
|
64
|
-
if (G(t))
|
|
65
|
-
return C;
|
|
66
|
-
if (E(t)) {
|
|
67
|
-
var e = typeof t.valueOf == "function" ? t.valueOf() : t;
|
|
68
|
-
t = E(e) ? e + "" : e;
|
|
69
|
-
}
|
|
70
|
-
if (typeof t != "string")
|
|
71
|
-
return t === 0 ? t : +t;
|
|
72
|
-
t = t.replace($, "");
|
|
73
|
-
var n = H.test(t);
|
|
74
|
-
return n || W.test(t) ? F(t.slice(2), n ? 2 : 8) : R.test(t) ? C : +t;
|
|
75
|
-
}
|
|
76
|
-
var Z = X;
|
|
77
|
-
const h = "copy-tuner-hidden";
|
|
78
|
-
class J {
|
|
79
|
-
// @ts-expect-error TS7006
|
|
80
|
-
constructor(e, n, o) {
|
|
81
|
-
this.element = e, this.data = n, this.callback = o, this.searchBoxElement = e.querySelector(".js-copy-tuner-bar-search"), this.logMenuElement = this.makeLogMenu(), this.element.append(this.logMenuElement), this.addHandler();
|
|
82
|
-
}
|
|
83
|
-
addHandler() {
|
|
84
|
-
this.element.querySelector(".js-copy-tuner-bar-open-log").addEventListener("click", (n) => {
|
|
85
|
-
n.preventDefault(), this.toggleLogMenu();
|
|
86
|
-
}), this.searchBoxElement.addEventListener("input", Z(this.onKeyup.bind(this), 250));
|
|
87
|
-
}
|
|
88
|
-
show() {
|
|
89
|
-
this.element.classList.remove(h), this.searchBoxElement.focus();
|
|
90
|
-
}
|
|
91
|
-
hide() {
|
|
92
|
-
this.element.classList.add(h);
|
|
93
|
-
}
|
|
94
|
-
showLogMenu() {
|
|
95
|
-
this.logMenuElement.classList.remove(h);
|
|
96
|
-
}
|
|
97
|
-
toggleLogMenu() {
|
|
98
|
-
this.logMenuElement.classList.toggle(h);
|
|
99
|
-
}
|
|
100
|
-
makeLogMenu() {
|
|
101
|
-
const e = document.createElement("div");
|
|
102
|
-
e.setAttribute("id", "copy-tuner-bar-log-menu"), e.classList.add(h);
|
|
103
|
-
const n = document.createElement("table"), o = document.createElement("tbody");
|
|
104
|
-
o.classList.remove("is-not-initialized");
|
|
105
|
-
for (const s of Object.keys(this.data).sort()) {
|
|
106
|
-
const d = this.data[s];
|
|
107
|
-
if (d === "")
|
|
108
|
-
continue;
|
|
109
|
-
const r = document.createElement("td");
|
|
110
|
-
r.textContent = s;
|
|
111
|
-
const a = document.createElement("td");
|
|
112
|
-
a.textContent = d;
|
|
113
|
-
const c = document.createElement("tr");
|
|
114
|
-
c.classList.add("copy-tuner-bar-log-menu__row"), c.dataset.key = s, c.addEventListener("click", ({ currentTarget: u }) => {
|
|
115
|
-
this.callback(u.dataset.key);
|
|
116
|
-
}), c.append(r), c.append(a), o.append(c);
|
|
117
|
-
}
|
|
118
|
-
return n.append(o), e.append(n), e;
|
|
119
|
-
}
|
|
120
|
-
// @ts-expect-error TS7031
|
|
121
|
-
onKeyup({ target: e }) {
|
|
122
|
-
const n = e.value.trim();
|
|
123
|
-
this.showLogMenu();
|
|
124
|
-
const o = [...this.logMenuElement.querySelectorAll("tr")];
|
|
125
|
-
for (const s of o) {
|
|
126
|
-
const d = n === "" || [...s.querySelectorAll("td")].some((r) => r.textContent.includes(n));
|
|
127
|
-
s.classList.toggle(h, !d);
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
const L = navigator.platform.toUpperCase().includes("MAC"), Q = (t) => !!(t.offsetWidth || t.offsetHeight || t.getClientRects().length > 0), ee = (t) => {
|
|
132
|
-
const e = t.getBoundingClientRect();
|
|
133
|
-
return {
|
|
134
|
-
top: e.top + (window.pageYOffset - document.documentElement.clientTop),
|
|
135
|
-
left: e.left + (window.pageXOffset - document.documentElement.clientLeft)
|
|
136
|
-
};
|
|
137
|
-
}, te = (t) => {
|
|
138
|
-
if (!Q(t))
|
|
139
|
-
return null;
|
|
140
|
-
const e = ee(t);
|
|
141
|
-
return e.right = e.left + t.offsetWidth, e.bottom = e.top + t.offsetHeight, {
|
|
142
|
-
left: e.left,
|
|
143
|
-
top: e.top,
|
|
144
|
-
// @ts-expect-error TS2339
|
|
145
|
-
width: e.right - e.left,
|
|
146
|
-
// @ts-expect-error TS2339
|
|
147
|
-
height: e.bottom - e.top
|
|
148
|
-
};
|
|
149
|
-
}, ne = 2e9;
|
|
150
|
-
class oe {
|
|
151
|
-
// @ts-expect-error TS7006
|
|
152
|
-
constructor(e, n, o) {
|
|
153
|
-
this.element = e, this.key = n, this.callback = o;
|
|
154
|
-
}
|
|
155
|
-
show() {
|
|
156
|
-
this.box = this.makeBox(), this.box !== null && (this.box.addEventListener("click", () => {
|
|
157
|
-
this.callback(this.key);
|
|
158
|
-
}), document.body.append(this.box));
|
|
159
|
-
}
|
|
160
|
-
remove() {
|
|
161
|
-
this.box && (this.box.remove(), this.box = null);
|
|
162
|
-
}
|
|
163
|
-
makeBox() {
|
|
164
|
-
const e = document.createElement("div");
|
|
165
|
-
e.classList.add("copyray-specimen"), e.classList.add("Specimen");
|
|
166
|
-
const n = te(this.element);
|
|
167
|
-
if (n === null)
|
|
168
|
-
return null;
|
|
169
|
-
for (const r of Object.keys(n)) {
|
|
170
|
-
const a = n[r];
|
|
171
|
-
e.style[r] = `${a}px`;
|
|
172
|
-
}
|
|
173
|
-
e.style.zIndex = ne;
|
|
174
|
-
const { position: o, top: s, left: d } = getComputedStyle(this.element);
|
|
175
|
-
return o === "fixed" && (this.box.style.position = "fixed", this.box.style.top = `${s}px`, this.box.style.left = `${d}px`), e.append(this.makeLabel()), e;
|
|
176
|
-
}
|
|
177
|
-
makeLabel() {
|
|
178
|
-
const e = document.createElement("div");
|
|
179
|
-
return e.classList.add("copyray-specimen-handle"), e.classList.add("Specimen"), e.textContent = this.key, e;
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
const se = () => {
|
|
183
|
-
const t = () => NodeFilter.FILTER_ACCEPT, e = document.createNodeIterator(document.body, NodeFilter.SHOW_COMMENT, t, !1), n = [];
|
|
184
|
-
let o;
|
|
185
|
-
for (; o = e.nextNode(); )
|
|
186
|
-
n.push(o);
|
|
187
|
-
return n.filter((s) => s.nodeValue.startsWith("COPYRAY")).map((s) => {
|
|
188
|
-
const [, d] = s.nodeValue.match(/^COPYRAY (\S*)$/), r = s.parentNode;
|
|
189
|
-
return { key: d, element: r };
|
|
190
|
-
});
|
|
139
|
+
customElements.define("copytuner-bar", s), customElements.define("copyray-overlay", o);
|
|
140
|
+
var c = () => {
|
|
141
|
+
let { url: t, data: n, keysSkipped: r } = window.CopyTuner, i = (e) => window.open(`${t}/blurbs/${e}/edit`), a = document.createElement("copytuner-bar");
|
|
142
|
+
document.body.append(a), a.init({
|
|
143
|
+
url: t,
|
|
144
|
+
data: n,
|
|
145
|
+
keysSkipped: !!r,
|
|
146
|
+
onOpen: i
|
|
147
|
+
});
|
|
148
|
+
let o = document.createElement("copyray-overlay");
|
|
149
|
+
o.onOpen = i, document.body.append(o);
|
|
150
|
+
let s = () => {
|
|
151
|
+
o.show(), a.show();
|
|
152
|
+
}, c = () => {
|
|
153
|
+
o.hide(), a.hide();
|
|
154
|
+
}, l = () => o.isShowing ? c() : s();
|
|
155
|
+
o.onToggle = l, window.CopyTuner.toggle = l, document.addEventListener("keydown", (t) => {
|
|
156
|
+
if (o.isShowing && ["Escape", "Esc"].includes(t.key)) {
|
|
157
|
+
c();
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
(e && t.metaKey || !e && t.ctrlKey) && t.shiftKey && t.key.toLowerCase() === "k" && l();
|
|
161
|
+
}), console && console.log(`Ready to Copyray. Press ${e ? "cmd+shift+k" : "ctrl+shift+k"} to scan your UI.`);
|
|
191
162
|
};
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
constructor(e, n) {
|
|
195
|
-
this.baseUrl = e, this.data = n, this.isShowing = !1, this.specimens = [], this.overlay = this.makeOverlay(), this.toggleButton = this.makeToggleButton(), this.boundOpen = this.open.bind(this), this.copyTunerBar = new J(document.querySelector("#copy-tuner-bar"), this.data, this.boundOpen);
|
|
196
|
-
}
|
|
197
|
-
show() {
|
|
198
|
-
this.reset(), document.body.append(this.overlay), this.makeSpecimens();
|
|
199
|
-
for (const e of this.specimens)
|
|
200
|
-
e.show();
|
|
201
|
-
this.copyTunerBar.show(), this.isShowing = !0;
|
|
202
|
-
}
|
|
203
|
-
hide() {
|
|
204
|
-
this.overlay.remove(), this.reset(), this.copyTunerBar.hide(), this.isShowing = !1;
|
|
205
|
-
}
|
|
206
|
-
toggle() {
|
|
207
|
-
this.isShowing ? this.hide() : this.show();
|
|
208
|
-
}
|
|
209
|
-
// @ts-expect-error TS7006
|
|
210
|
-
open(e) {
|
|
211
|
-
window.open(`${this.baseUrl}/blurbs/${e}/edit`);
|
|
212
|
-
}
|
|
213
|
-
makeSpecimens() {
|
|
214
|
-
for (const { element: e, key: n } of se())
|
|
215
|
-
this.specimens.push(new oe(e, n, this.boundOpen));
|
|
216
|
-
}
|
|
217
|
-
makeToggleButton() {
|
|
218
|
-
const e = document.createElement("a");
|
|
219
|
-
return e.addEventListener("click", () => {
|
|
220
|
-
this.show();
|
|
221
|
-
}), e.classList.add("copyray-toggle-button"), e.classList.add("hidden-on-mobile"), e.textContent = "Open CopyTuner", document.body.append(e), e;
|
|
222
|
-
}
|
|
223
|
-
makeOverlay() {
|
|
224
|
-
const e = document.createElement("div");
|
|
225
|
-
return e.setAttribute("id", "copyray-overlay"), e.addEventListener("click", () => this.hide()), e;
|
|
226
|
-
}
|
|
227
|
-
reset() {
|
|
228
|
-
for (const e of this.specimens)
|
|
229
|
-
e.remove();
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
const re = (t) => {
|
|
233
|
-
const e = document.createElement("div");
|
|
234
|
-
e.id = "copy-tuner-bar", e.classList.add("copy-tuner-hidden"), e.innerHTML = `
|
|
235
|
-
<a class="copy-tuner-bar-button" target="_blank" href="${t}">CopyTuner</a>
|
|
236
|
-
<a href="/copytuner" target="_blank" class="copy-tuner-bar-button">Sync</a>
|
|
237
|
-
<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>
|
|
238
|
-
<input type="text" class="copy-tuner-bar__search js-copy-tuner-bar-search" placeholder="search">
|
|
239
|
-
`, document.body.append(e);
|
|
240
|
-
}, j = () => {
|
|
241
|
-
const { url: t, data: e } = window.CopyTuner;
|
|
242
|
-
re(t);
|
|
243
|
-
const n = new ie(t, e);
|
|
244
|
-
window.CopyTuner.toggle = () => n.toggle(), document.addEventListener("keydown", (o) => {
|
|
245
|
-
if (n.isShowing && ["Escape", "Esc"].includes(o.key)) {
|
|
246
|
-
n.hide();
|
|
247
|
-
return;
|
|
248
|
-
}
|
|
249
|
-
(L && o.metaKey || !L && o.ctrlKey) && o.shiftKey && o.key.toLowerCase() === "k" && n.toggle();
|
|
250
|
-
}), console && console.log(`Ready to Copyray. Press ${L ? "cmd+shift+k" : "ctrl+shift+k"} to scan your UI.`);
|
|
251
|
-
};
|
|
252
|
-
document.readyState === "complete" || document.readyState !== "loading" ? j() : document.addEventListener("DOMContentLoaded", () => j());
|
|
163
|
+
document.readyState === "complete" || document.readyState !== "loading" ? c() : document.addEventListener("DOMContentLoaded", () => c());
|
|
164
|
+
//#endregion
|
data/biome.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://biomejs.dev/schemas/2.5.0/schema.json",
|
|
3
|
+
"vcs": {
|
|
4
|
+
"enabled": true,
|
|
5
|
+
"clientKind": "git",
|
|
6
|
+
"useIgnoreFile": true
|
|
7
|
+
},
|
|
8
|
+
"files": {
|
|
9
|
+
"ignoreUnknown": false,
|
|
10
|
+
"includes": ["src/**", "*.ts", "*.json"]
|
|
11
|
+
},
|
|
12
|
+
"formatter": {
|
|
13
|
+
"enabled": true,
|
|
14
|
+
"indentStyle": "space",
|
|
15
|
+
"indentWidth": 2,
|
|
16
|
+
"lineWidth": 120
|
|
17
|
+
},
|
|
18
|
+
"linter": {
|
|
19
|
+
"enabled": true,
|
|
20
|
+
"rules": {
|
|
21
|
+
"preset": "recommended"
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"javascript": {
|
|
25
|
+
"formatter": {
|
|
26
|
+
"quoteStyle": "single",
|
|
27
|
+
"semicolons": "asNeeded",
|
|
28
|
+
"trailingCommas": "all"
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
"assist": {
|
|
32
|
+
"enabled": true,
|
|
33
|
+
"actions": {
|
|
34
|
+
"source": {
|
|
35
|
+
"organizeImports": "on"
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
data/index.html
CHANGED
|
@@ -5,27 +5,25 @@
|
|
|
5
5
|
<meta charset="UTF-8" />
|
|
6
6
|
<link rel="icon" type="image/svg+xml" href="/src/favicon.svg" />
|
|
7
7
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
8
|
-
<title>
|
|
8
|
+
<title>CopyTuner Dev</title>
|
|
9
9
|
</head>
|
|
10
10
|
|
|
11
11
|
<body>
|
|
12
12
|
<div id="app">
|
|
13
13
|
<ul>
|
|
14
|
-
<li>
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
<li>
|
|
18
|
-
<!--COPYRAY projects.index.blurbs-->翻訳キー
|
|
19
|
-
</li>
|
|
20
|
-
<li>
|
|
21
|
-
<!--COPYRAY projects.index.members-->メンバー数
|
|
22
|
-
</li>
|
|
14
|
+
<li data-copyray-key="projects.index.locales">言語</li>
|
|
15
|
+
<li data-copyray-key="projects.index.blurbs">翻訳キー</li>
|
|
16
|
+
<li data-copyray-key="projects.index.members">メンバー数</li>
|
|
23
17
|
</ul>
|
|
24
18
|
</div>
|
|
25
19
|
<script>
|
|
26
20
|
window.CopyTuner = {
|
|
27
21
|
url: 'https://copy-tuner.herokuapp.com/projects/xxx',
|
|
28
|
-
data: {
|
|
22
|
+
data: {
|
|
23
|
+
'projects.index.locales': '言語',
|
|
24
|
+
'projects.index.blurbs': '翻訳キー',
|
|
25
|
+
'projects.index.members': 'メンバー数',
|
|
26
|
+
},
|
|
29
27
|
}
|
|
30
28
|
</script>
|
|
31
29
|
<script type="module" src="/src/main.ts"></script>
|
|
@@ -20,7 +20,6 @@ module CopyTunerClient
|
|
|
20
20
|
@client = client
|
|
21
21
|
@logger = options[:logger]
|
|
22
22
|
@mutex = Mutex.new
|
|
23
|
-
@exclude_key_regexp = options[:exclude_key_regexp]
|
|
24
23
|
@local_first_key_regexp = options[:local_first_key_regexp]
|
|
25
24
|
@upload_disabled = options[:upload_disabled]
|
|
26
25
|
@ignored_keys = options.fetch(:ignored_keys, [])
|
|
@@ -49,7 +48,6 @@ module CopyTunerClient
|
|
|
49
48
|
# @param key [String] the key of the blurb to update
|
|
50
49
|
# @param value [String] the new contents of the blurb
|
|
51
50
|
def []=(key, value)
|
|
52
|
-
return if @exclude_key_regexp && key.match?(@exclude_key_regexp)
|
|
53
51
|
return unless key.include?('.')
|
|
54
52
|
return if @locales.present? && !@locales.member?(key.split('.').first)
|
|
55
53
|
return if @upload_disabled
|
|
@@ -10,7 +10,7 @@ require 'copy_tuner_client/copyray_middleware'
|
|
|
10
10
|
|
|
11
11
|
module CopyTunerClient
|
|
12
12
|
# Used to set up and modify settings for the client.
|
|
13
|
-
class Configuration
|
|
13
|
+
class Configuration # rubocop:disable Metrics/ClassLength
|
|
14
14
|
# These options will be present in the Hash returned by {#to_hash}.
|
|
15
15
|
OPTIONS = %i[api_key development_environments environment_name host
|
|
16
16
|
http_open_timeout http_read_timeout client_name client_url
|
|
@@ -18,7 +18,7 @@ module CopyTunerClient
|
|
|
18
18
|
proxy_port proxy_user secure polling_delay sync_interval
|
|
19
19
|
sync_interval_staging sync_ignore_path_regex logger
|
|
20
20
|
framework middleware disable_middleware disable_test_translation
|
|
21
|
-
ca_file
|
|
21
|
+
ca_file local_first_key_regexp s3_host locales ignored_keys ignored_key_handler
|
|
22
22
|
download_cache_dir].freeze
|
|
23
23
|
|
|
24
24
|
# NOTE: Rails 標準ロケールで非文字列値(precision: Integer, significant: Boolean,
|
|
@@ -43,7 +43,8 @@ module CopyTunerClient
|
|
|
43
43
|
attr_accessor :host
|
|
44
44
|
|
|
45
45
|
# @return [Fixnum] The port on which your CopyTuner server runs (defaults to +443+ for secure connections, +80+ for insecure connections).
|
|
46
|
-
|
|
46
|
+
# NOTE: reader は default_port フォールバック付きの明示定義(#port)があるため attr_accessor を使わない
|
|
47
|
+
attr_writer :port
|
|
47
48
|
|
|
48
49
|
# @return [Boolean] +true+ for https connections, +false+ for http connections.
|
|
49
50
|
attr_accessor :secure
|
|
@@ -94,7 +95,8 @@ module CopyTunerClient
|
|
|
94
95
|
attr_accessor :polling_delay
|
|
95
96
|
|
|
96
97
|
# @return [Integer] The time, in seconds, in between each sync to the server in development. Defaults to +60+.
|
|
97
|
-
|
|
98
|
+
# NOTE: reader は environment で分岐する明示定義(#sync_interval)があるため attr_accessor を使わない
|
|
99
|
+
attr_writer :sync_interval
|
|
98
100
|
|
|
99
101
|
# @return [Integer] The time, in seconds, in between each sync to the server in development. Defaults to +60+.
|
|
100
102
|
attr_accessor :sync_interval_staging
|
|
@@ -128,10 +130,6 @@ module CopyTunerClient
|
|
|
128
130
|
|
|
129
131
|
attr_accessor :poller
|
|
130
132
|
|
|
131
|
-
# @return [Regexp] Regular expression to exclude keys.
|
|
132
|
-
# @deprecated Use {#local_first_key_regexp} instead.
|
|
133
|
-
attr_reader :exclude_key_regexp
|
|
134
|
-
|
|
135
133
|
# @return [Regexp] Keys (without locale) matching this regexp bypass the
|
|
136
134
|
# copy_tuner cache and are looked up from local config/locales
|
|
137
135
|
# (I18n::Backend::Simple) first. Used for gradual migration from
|
|
@@ -141,15 +139,12 @@ module CopyTunerClient
|
|
|
141
139
|
# @return [String] The S3 host to connect to (defaults to +copy-tuner-us.s3.amazonaws.com+).
|
|
142
140
|
attr_accessor :s3_host
|
|
143
141
|
|
|
144
|
-
# @return [Boolean] To disable Copyray
|
|
142
|
+
# @return [Boolean] To disable Copyray marker injection, set true
|
|
145
143
|
attr_accessor :disable_copyray_comment_injection
|
|
146
144
|
|
|
147
145
|
# @return [Array<Symbol>] Restrict blurb locales to upload
|
|
148
146
|
attr_accessor :locales
|
|
149
147
|
|
|
150
|
-
# @return [Boolean] Html escape
|
|
151
|
-
attr_accessor :html_escape
|
|
152
|
-
|
|
153
148
|
# @return [Array<String>] A list of ignored keys
|
|
154
149
|
attr_accessor :ignored_keys
|
|
155
150
|
|
|
@@ -165,7 +160,7 @@ module CopyTunerClient
|
|
|
165
160
|
alias secure? secure
|
|
166
161
|
|
|
167
162
|
# Instantiated from {CopyTunerClient.configure}. Sets defaults.
|
|
168
|
-
def initialize
|
|
163
|
+
def initialize # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
|
169
164
|
self.client_name = 'CopyTuner Client'
|
|
170
165
|
self.client_url = 'https://rubygems.org/gems/copy_tuner_client'
|
|
171
166
|
self.client_version = VERSION
|
|
@@ -182,7 +177,6 @@ module CopyTunerClient
|
|
|
182
177
|
self.upload_disabled_environments = %w[production staging]
|
|
183
178
|
self.s3_host = 'copy-tuner.sg-apps.com' # NOTE: cloudfront host
|
|
184
179
|
self.disable_copyray_comment_injection = false
|
|
185
|
-
self.html_escape = true
|
|
186
180
|
self.ignored_keys = []
|
|
187
181
|
self.ignored_key_handler = ->(e) { raise e }
|
|
188
182
|
self.local_first_key_regexp = nil
|
|
@@ -197,7 +191,7 @@ module CopyTunerClient
|
|
|
197
191
|
# @param [Symbol] option Key for a given attribute
|
|
198
192
|
# @return [Object] the given attribute
|
|
199
193
|
def [](option)
|
|
200
|
-
|
|
194
|
+
public_send(option)
|
|
201
195
|
end
|
|
202
196
|
|
|
203
197
|
# Returns a hash of all configurable options
|
|
@@ -206,7 +200,7 @@ module CopyTunerClient
|
|
|
206
200
|
base_options = { public: public?, upload_disabled: upload_disabled? }
|
|
207
201
|
|
|
208
202
|
OPTIONS.inject(base_options) do |hash, option|
|
|
209
|
-
hash.merge option.to_sym =>
|
|
203
|
+
hash.merge option.to_sym => public_send(option)
|
|
210
204
|
end
|
|
211
205
|
end
|
|
212
206
|
|
|
@@ -258,7 +252,10 @@ module CopyTunerClient
|
|
|
258
252
|
# This creates the {I18nBackend} and puts them together.
|
|
259
253
|
#
|
|
260
254
|
# When {#test?} returns +false+, the poller will be started.
|
|
261
|
-
def apply
|
|
255
|
+
def apply # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
256
|
+
# NOTE: project_id は必須。未設定なら apply 時点で明示的に失敗させる
|
|
257
|
+
validate_project_id!
|
|
258
|
+
|
|
262
259
|
self.locales ||= self.locales = if defined?(::Rails)
|
|
263
260
|
::Rails.application.config.i18n.available_locales.presence || Array(::Rails.application.config.i18n.default_locale)
|
|
264
261
|
else
|
|
@@ -337,18 +334,6 @@ module CopyTunerClient
|
|
|
337
334
|
@api_key = api_key
|
|
338
335
|
end
|
|
339
336
|
|
|
340
|
-
# @deprecated Use {#local_first_key_regexp} instead.
|
|
341
|
-
def exclude_key_regexp=(value)
|
|
342
|
-
unless value.nil?
|
|
343
|
-
ActiveSupport::Deprecation.new.warn(
|
|
344
|
-
'exclude_key_regexp is deprecated and will be removed in a future release. ' \
|
|
345
|
-
'Use local_first_key_regexp instead (note: it matches keys WITHOUT the locale prefix, ' \
|
|
346
|
-
'e.g. /\Aviews\./ instead of /\Aja\.views\./).'
|
|
347
|
-
)
|
|
348
|
-
end
|
|
349
|
-
@exclude_key_regexp = value
|
|
350
|
-
end
|
|
351
|
-
|
|
352
337
|
# Sync interval for Rack Middleware
|
|
353
338
|
def sync_interval
|
|
354
339
|
if environment_name == 'staging'
|
|
@@ -358,16 +343,11 @@ module CopyTunerClient
|
|
|
358
343
|
end
|
|
359
344
|
end
|
|
360
345
|
|
|
361
|
-
# @return [String] current project url by
|
|
346
|
+
# @return [String] current project url by project_id
|
|
362
347
|
def project_url
|
|
363
|
-
|
|
364
|
-
if project_id
|
|
365
|
-
"/projects/#{project_id}"
|
|
366
|
-
else
|
|
367
|
-
ActiveSupport::Deprecation.new.warn('Please set project_id.')
|
|
368
|
-
"/projects/#{api_key}"
|
|
369
|
-
end
|
|
348
|
+
validate_project_id!
|
|
370
349
|
|
|
350
|
+
path = "/projects/#{project_id}"
|
|
371
351
|
URI::Generic.build(scheme: self.protocol, host: self.host, port: self.port.to_i, path:).to_s
|
|
372
352
|
end
|
|
373
353
|
|
|
@@ -389,6 +369,12 @@ module CopyTunerClient
|
|
|
389
369
|
|
|
390
370
|
private
|
|
391
371
|
|
|
372
|
+
# project_id は必須。未設定なら明示的に失敗させる。
|
|
373
|
+
# apply(起動時の全体検証)と project_url(apply を経ない経路へのセーフネット)の両方から呼ぶ。
|
|
374
|
+
def validate_project_id!
|
|
375
|
+
raise ArgumentError, 'project_id is required' if project_id.nil?
|
|
376
|
+
end
|
|
377
|
+
|
|
392
378
|
def default_port
|
|
393
379
|
if secure?
|
|
394
380
|
443
|