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.
Files changed (52) hide show
  1. checksums.yaml +4 -4
  2. data/.gitattributes +2 -2
  3. data/.github/workflows/rspec.yml +1 -1
  4. data/CHANGELOG.md +27 -0
  5. data/CLAUDE.md +6 -4
  6. data/README.md +6 -27
  7. data/UPGRADING.md +201 -0
  8. data/app/assets/javascripts/copytuner.js +162 -250
  9. data/biome.json +39 -0
  10. data/index.html +9 -11
  11. data/lib/copy_tuner_client/cache.rb +0 -2
  12. data/lib/copy_tuner_client/configuration.rb +23 -37
  13. data/lib/copy_tuner_client/copyray/marker.rb +24 -0
  14. data/lib/copy_tuner_client/copyray/rewriter.rb +113 -0
  15. data/lib/copy_tuner_client/copyray.rb +11 -4
  16. data/lib/copy_tuner_client/copyray_middleware.rb +10 -8
  17. data/lib/copy_tuner_client/engine.rb +1 -1
  18. data/lib/copy_tuner_client/helper_extension.rb +0 -3
  19. data/lib/copy_tuner_client/i18n_backend.rb +5 -12
  20. data/lib/copy_tuner_client/version.rb +1 -1
  21. data/mise.toml +3 -0
  22. data/package.json +9 -13
  23. data/pnpm-lock.yaml +600 -0
  24. data/skills/copy-tuner/SKILL.md +37 -6
  25. data/skills/copy-tuner-to-locales-cleanup/SKILL.md +4 -4
  26. data/skills/copy-tuner-to-locales-migrate-prefix/references/local-first-regexp.md +4 -9
  27. data/skills/copy-tuner-to-t-migrate/SKILL.md +131 -0
  28. data/skills/copy-tuner-to-t-migrate/scripts/migrate_tt.rb +189 -0
  29. data/spec/copy_tuner_client/cache_spec.rb +2 -10
  30. data/spec/copy_tuner_client/client_spec.rb +1 -0
  31. data/spec/copy_tuner_client/configuration_spec.rb +16 -16
  32. data/spec/copy_tuner_client/copyray/marker_spec.rb +41 -0
  33. data/spec/copy_tuner_client/copyray/rewriter_spec.rb +216 -0
  34. data/spec/copy_tuner_client/copyray_middleware_spec.rb +88 -0
  35. data/spec/copy_tuner_client/copyray_spec.rb +22 -39
  36. data/spec/copy_tuner_client/helper_extension_spec.rb +18 -5
  37. data/spec/copy_tuner_client/i18n_backend_spec.rb +8 -15
  38. data/spec/support/client_spec_helpers.rb +0 -1
  39. data/src/copyray-overlay.ts +125 -0
  40. data/src/copytuner-bar.ts +153 -0
  41. data/src/main.ts +33 -26
  42. data/src/{copyray.css → styles.ts} +112 -136
  43. data/src/util.ts +9 -1
  44. data/tsconfig.json +5 -10
  45. data/vite.config.ts +0 -1
  46. metadata +15 -8
  47. data/.eslintrc.js +0 -12
  48. data/app/assets/stylesheets/copytuner.css +0 -1
  49. data/src/copyray.ts +0 -130
  50. data/src/copytuner_bar.ts +0 -115
  51. data/src/specimen.ts +0 -84
  52. data/yarn.lock +0 -2540
@@ -1,252 +1,164 @@
1
- var y = typeof globalThis < "u" ? globalThis : typeof window < "u" ? window : typeof global < "u" ? global : typeof self < "u" ? self : {}, A = "Expected a function", C = 0 / 0, _ = "[object Symbol]", $ = /^\s+|\s+$/g, R = /^[-+]0x[0-9a-f]+$/i, H = /^0b[01]+$/i, W = /^0o[0-7]+$/i, F = parseInt, D = typeof y == "object" && y && y.Object === Object && y, P = typeof self == "object" && self && self.Object === Object && self, q = D || P || Function("return this")(), K = Object.prototype, U = K.toString, Y = Math.max, V = Math.min, v = function() {
2
- return q.Date.now();
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
- function X(t, e, n) {
5
- var o, s, d, r, a, c, u = 0, x = !1, f = !1, b = !0;
6
- if (typeof t != "function")
7
- throw new TypeError(A);
8
- e = O(e) || 0, E(n) && (x = !!n.leading, f = "maxWait" in n, d = f ? Y(O(n.maxWait) || 0, e) : d, b = "trailing" in n ? !!n.trailing : b);
9
- function g(i) {
10
- var l = o, p = s;
11
- return o = s = void 0, u = i, r = t.apply(p, l), r;
12
- }
13
- function B(i) {
14
- return u = i, a = setTimeout(m, e), x ? g(i) : r;
15
- }
16
- function M(i) {
17
- var l = i - c, p = i - u, T = e - l;
18
- return f ? V(T, d - p) : T;
19
- }
20
- function w(i) {
21
- var l = i - c, p = i - u;
22
- return c === void 0 || l >= e || l < 0 || f && p >= d;
23
- }
24
- function m() {
25
- var i = v();
26
- if (w(i))
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
- class ie {
193
- // @ts-expect-error TS7006
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>Vite App</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
- <!--COPYRAY projects.index.locales-->言語
16
- </li>
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 exclude_key_regexp local_first_key_regexp s3_host locales ignored_keys ignored_key_handler
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
- attr_accessor :port
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
- attr_accessor :sync_interval
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 comment injection, set true
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
- send(option)
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 => send(option)
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 api_key
346
+ # @return [String] current project url by project_id
362
347
  def project_url
363
- path =
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