Wave 4: refactor generators batch 2 to crypto random

- refactor(generators): migrate Gradient, Hash, Letter, List to crypto random
- refactor(generators): migrate Lorem, Lottery, Magic8Ball to crypto random
This commit is contained in:
emil
2026-05-13 20:25:39 +03:00
parent 3d94c99ff9
commit 49269f209d
9 changed files with 126 additions and 103 deletions
+54 -2
View File
@@ -8,7 +8,7 @@
"plan_name": "critical-refactor",
"status": "active",
"started_at": "2026-05-13T16:25:19.266Z",
"updated_at": "2026-05-13T17:16:10.363Z",
"updated_at": "2026-05-13T17:25:32.173Z",
"session_ids": [
"ses_1de6ede59ffexUwt5C0sddEWwR"
],
@@ -68,6 +68,32 @@
"status": "completed",
"ended_at": "2026-05-13T17:16:10.363Z",
"elapsed_ms": 1586504
},
"todo:13": {
"task_key": "todo:13",
"task_label": "13",
"task_title": "Refactor GradientGenerator.astro",
"session_id": "ses_1dda85f21ffeEGeOWrbqmWAk02",
"agent": "Sisyphus-Junior",
"category": "unspecified-high",
"updated_at": "2026-05-13T17:22:16.123Z",
"started_at": "2026-05-13T17:19:45.020Z",
"status": "completed",
"ended_at": "2026-05-13T17:22:16.123Z",
"elapsed_ms": 151103
},
"todo:17": {
"task_key": "todo:17",
"task_label": "17",
"task_title": "Refactor LoremGenerator.astro",
"session_id": "ses_1dda37bf2ffexATCZ3W4Z6EDu1",
"agent": "Sisyphus-Junior",
"category": "unspecified-high",
"updated_at": "2026-05-13T17:25:32.173Z",
"started_at": "2026-05-13T17:25:11.269Z",
"status": "completed",
"ended_at": "2026-05-13T17:25:32.173Z",
"elapsed_ms": 20904
}
}
}
@@ -75,7 +101,7 @@
"active_plan": "/home/emil/Desktop/Coding/AI/Randify.pro/.sisyphus/plans/critical-refactor.md",
"started_at": "2026-05-13T16:25:19.266Z",
"status": "active",
"updated_at": "2026-05-13T17:16:10.363Z",
"updated_at": "2026-05-13T17:25:32.173Z",
"session_ids": [
"ses_1de6ede59ffexUwt5C0sddEWwR"
],
@@ -135,6 +161,32 @@
"status": "completed",
"ended_at": "2026-05-13T17:16:10.363Z",
"elapsed_ms": 1586504
},
"todo:13": {
"task_key": "todo:13",
"task_label": "13",
"task_title": "Refactor GradientGenerator.astro",
"session_id": "ses_1dda85f21ffeEGeOWrbqmWAk02",
"agent": "Sisyphus-Junior",
"category": "unspecified-high",
"updated_at": "2026-05-13T17:22:16.123Z",
"started_at": "2026-05-13T17:19:45.020Z",
"status": "completed",
"ended_at": "2026-05-13T17:22:16.123Z",
"elapsed_ms": 151103
},
"todo:17": {
"task_key": "todo:17",
"task_label": "17",
"task_title": "Refactor LoremGenerator.astro",
"session_id": "ses_1dda37bf2ffexATCZ3W4Z6EDu1",
"agent": "Sisyphus-Junior",
"category": "unspecified-high",
"updated_at": "2026-05-13T17:25:32.173Z",
"started_at": "2026-05-13T17:25:11.269Z",
"status": "completed",
"ended_at": "2026-05-13T17:25:32.173Z",
"elapsed_ms": 20904
}
},
"agent": "atlas"
+7 -7
View File
@@ -519,7 +519,7 @@ Max Concurrent: 8 (Wave 3)
- Message: `refactor(generators): migrate batch 1 to crypto random (Card, Coin, Color, Country, Date, Emoji, FontPair)`
- Files: `src/components/generators/{Card,Coin,Color,Country,Date,Emoji,FontPair}Generator.astro`
- [ ] 13. Refactor GradientGenerator.astro
- [x] 13. Refactor GradientGenerator.astro
**What to do**: Same pattern as Task 6.
**Recommended Agent Profile**: `unspecified-high`
**Parallelization**: YES (with Tasks 14-19, 20-26, 27-32)
@@ -527,7 +527,7 @@ Max Concurrent: 8 (Wave 3)
**Acceptance Criteria**: No Math.random, uses random.ts and i18n.ts
**Commit**: NO
- [ ] 14. Refactor HashGenerator.astro
- [x] 14. Refactor HashGenerator.astro
**What to do**: Same pattern as Task 6.
**Recommended Agent Profile**: `unspecified-high`
**Parallelization**: YES (with Tasks 13, 15-19, 20-26, 27-32)
@@ -535,7 +535,7 @@ Max Concurrent: 8 (Wave 3)
**Acceptance Criteria**: No Math.random, uses random.ts and i18n.ts
**Commit**: NO
- [ ] 15. Refactor LetterGenerator.astro
- [x] 15. Refactor LetterGenerator.astro
**What to do**: Same pattern as Task 6.
**Recommended Agent Profile**: `unspecified-high`
**Parallelization**: YES (with Tasks 13-14, 16-19, 20-26, 27-32)
@@ -543,7 +543,7 @@ Max Concurrent: 8 (Wave 3)
**Acceptance Criteria**: No Math.random, uses random.ts and i18n.ts
**Commit**: NO
- [ ] 16. Refactor ListGenerator.astro
- [x] 16. Refactor ListGenerator.astro
**What to do**: Same pattern as Task 6.
**Recommended Agent Profile**: `unspecified-high`
**Parallelization**: YES (with Tasks 13-15, 17-19, 20-26, 27-32)
@@ -551,7 +551,7 @@ Max Concurrent: 8 (Wave 3)
**Acceptance Criteria**: No Math.random, uses random.ts and i18n.ts
**Commit**: NO
- [ ] 17. Refactor LoremGenerator.astro
- [x] 17. Refactor LoremGenerator.astro
**What to do**: Same pattern as Task 6.
**Recommended Agent Profile**: `unspecified-high`
**Parallelization**: YES (with Tasks 13-16, 18-19, 20-26, 27-32)
@@ -559,7 +559,7 @@ Max Concurrent: 8 (Wave 3)
**Acceptance Criteria**: No Math.random, uses random.ts and i18n.ts
**Commit**: NO
- [ ] 18. Refactor LotteryGenerator.astro
- [x] 18. Refactor LotteryGenerator.astro
**What to do**: Same pattern as Task 6.
**Recommended Agent Profile**: `unspecified-high`
**Parallelization**: YES (with Tasks 13-17, 19, 20-26, 27-32)
@@ -567,7 +567,7 @@ Max Concurrent: 8 (Wave 3)
**Acceptance Criteria**: No Math.random, uses random.ts and i18n.ts
**Commit**: NO
- [ ] 19. Refactor Magic8BallGenerator.astro
- [x] 19. Refactor Magic8BallGenerator.astro
**What to do**: Same pattern as Task 6.
**Recommended Agent Profile**: `unspecified-high`
**Parallelization**: YES (with Tasks 13-18, 20-26, 27-32)
@@ -113,7 +113,11 @@ const i18n = {
</div>
<script>
const isRu = document.documentElement.lang === "ru";
import { randomInt } from "@/lib/client/random";
import { getClientLang, getClientT } from "@/lib/client/i18n";
const lang = getClientLang();
const T = getClientT();
const typeSelect = document.getElementById("gg-type") as HTMLSelectElement;
const stopsSelect = document.getElementById("gg-stops") as HTMLSelectElement;
@@ -130,10 +134,6 @@ const i18n = {
const copyIconSvg = `<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="14" height="14" x="8" y="8" rx="2" ry="2"/><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"/></svg>`;
const checkIconSvg = `<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" class="text-accent"><path d="M20 6 9 17l-5-5"/></svg>`;
function randomInt(min: number, max: number): number {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
function hslToHex(h: number, s: number, l: number): string {
s /= 100;
l /= 100;
@@ -223,7 +223,7 @@ const i18n = {
chip.type = "button";
chip.className =
"group flex items-center gap-2 px-3 py-2 bg-zinc-950 border border-zinc-800 rounded-xl hover:border-zinc-700 focus:outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-2 focus-visible:ring-offset-zinc-950 transition-all duration-100 cursor-pointer";
chip.setAttribute("aria-label", isRu ? `Скопировать ${hex}` : `Copy ${hex}`);
chip.setAttribute("aria-label", `${T.copy} ${hex}`);
const swatch = document.createElement("span");
swatch.className = "w-5 h-5 rounded-full border border-zinc-700 shrink-0";
@@ -276,12 +276,12 @@ const i18n = {
copyCssIcon.classList.add("hidden");
checkCssIcon.classList.remove("hidden");
copyCssLabel.textContent = isRu ? "Скопировано!" : "Copied!";
copyCssLabel.textContent = T.copied;
if (cssCopyTimer) clearTimeout(cssCopyTimer);
cssCopyTimer = setTimeout(() => {
checkCssIcon.classList.add("hidden");
copyCssIcon.classList.remove("hidden");
copyCssLabel.textContent = isRu ? "Копировать" : "Copy";
copyCssLabel.textContent = T.copy;
}, 1500);
});
+14 -28
View File
@@ -213,6 +213,12 @@ const T = useT(isRu ? "ru" : "en");
</div>
<script>
import { randomInt } from "@/lib/client/random";
import { getClientLang, getClientT } from "@/lib/client/i18n";
const isRu = getClientLang() === "ru";
const T = getClientT();
// ===================== MD5 Implementation =====================
function md5(str: string): string {
const utf8 = new TextEncoder().encode(str);
@@ -300,33 +306,13 @@ const T = useT(isRu ? "ru" : "en");
function generateRandomString(length: number): string {
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
let result = "";
const randomValues = new Uint32Array(length);
crypto.getRandomValues(randomValues);
for (let i = 0; i < length; i++) {
result += chars[randomValues[i] % chars.length];
result += chars[randomInt(0, chars.length - 1)];
}
return result;
}
// ===================== DOM Elements =====================
const isRu = document.documentElement.lang === "ru";
const TEXTS = {
generate: isRu ? "Сгенерировать" : "Generate",
generated: isRu ? "Сгенерировано" : "Generated",
copy: isRu ? "Копировать" : "Copy",
copied: isRu ? "Скопировано" : "Copied",
match: isRu ? "Совпадение" : "Match",
noMatch: isRu ? "Нет совпадения" : "No match",
enterText: isRu ? "Ввести текст" : "Enter text",
generateRandom: isRu ? "Случайная строка" : "Generate random",
uppercase: isRu ? "ВЕРХНИЙ РЕГИСТР" : "UPPERCASE",
lowercase: isRu ? "нижний регистр" : "lowercase",
compareHint: isRu ? "Вставьте хеш, чтобы проверить совпадение с результатами." : "Paste a hash to check if it matches any result.",
refresh: isRu ? "Обновить" : "Refresh",
inputMode: isRu ? "Режим ввода" : "Input mode",
stringLength: isRu ? "Длина строки" : "String length",
};
const modeTextBtn = document.getElementById("hash-mode-text") as HTMLButtonElement;
const modeRandomBtn = document.getElementById("hash-mode-random") as HTMLButtonElement;
const textSection = document.getElementById("hash-text-section") as HTMLDivElement;
@@ -411,19 +397,19 @@ const T = useT(isRu ? "ru" : "en");
return {
matches,
display: matches
? `<span class="inline-flex items-center gap-1 text-green-400 text-xs font-medium"><svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5"/></svg> ${TEXTS.match}</span>`
: `<span class="inline-flex items-center gap-1 text-red-400 text-xs font-medium"><svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg> ${TEXTS.noMatch}</span>`,
? `<span class="inline-flex items-center gap-1 text-green-400 text-xs font-medium"><svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5"/></svg> ${isRu ? "Совпадение" : "Match"}</span>`
: `<span class="inline-flex items-center gap-1 text-red-400 text-xs font-medium"><svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg> ${isRu ? "Нет совпадения" : "No match"}</span>`,
};
}
async function copyToClipboard(text: string, iconEl: HTMLElement, checkEl: HTMLElement, labelEl: HTMLElement) {
try {
await navigator.clipboard.writeText(text);
labelEl.textContent = TEXTS.copied;
labelEl.textContent = T.copied;
iconEl.classList.add("hidden");
checkEl.classList.remove("hidden");
setTimeout(() => {
labelEl.textContent = TEXTS.copy;
labelEl.textContent = T.copy;
iconEl.classList.remove("hidden");
checkEl.classList.add("hidden");
}, 1500);
@@ -488,9 +474,9 @@ const T = useT(isRu ? "ru" : "en");
<button
type="button"
class="hash-copy-btn inline-flex items-center gap-1.5 px-3 py-1.5 bg-zinc-900/80 border border-zinc-700/50 rounded-lg text-xs font-medium text-zinc-400 hover:text-zinc-100 hover:bg-zinc-800 transition-colors focus:outline-none focus-visible:ring-2 ${colors.ring} focus-visible:ring-offset-2 focus-visible:ring-offset-zinc-950 flex-shrink-0 cursor-pointer"
aria-label="${TEXTS.copy} ${algo.toUpperCase()}"
aria-label="${T.copy} ${algo.toUpperCase()}"
>
<span class="hash-copy-label">${TEXTS.copy}</span>
<span class="hash-copy-label">${T.copy}</span>
<span class="hash-copy-icon" aria-hidden="true">
<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="14" height="14" x="8" y="8" rx="2" ry="2"/><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"/></svg>
</span>
@@ -572,7 +558,7 @@ const T = useT(isRu ? "ru" : "en");
caseToggleBtn.addEventListener("click", () => {
isUppercase = !isUppercase;
caseLabel.textContent = isUppercase ? TEXTS.uppercase : TEXTS.lowercase;
caseLabel.textContent = isUppercase ? (isRu ? "ВЕРХНИЙ РЕГИСТР" : "UPPERCASE") : (isRu ? "нижний регистр" : "lowercase");
updateCase();
});
@@ -79,8 +79,12 @@ const T = useT(isRu ? "ru" : "en");
<script>
import { CopyFeedback } from "@/lib/client/clipboard";
import { popElement } from "@/lib/client/animations";
import { randomInt } from "@/lib/client/random";
import { getClientLang, getClientT } from "@/lib/client/i18n";
const isRu = document.documentElement.lang === "ru";
const lang = getClientLang();
const isRu = lang === "ru";
const T = getClientT();
const ALPHABET = isRu
? "АБВГДЕЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯ"
: "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
@@ -97,7 +101,7 @@ const T = useT(isRu ? "ru" : "en");
const clipboard = new CopyFeedback(copyIcon, checkIcon);
function generate() {
const idx = Math.floor(Math.random() * ALPHABET.length);
const idx = randomInt(0, ALPHABET.length - 1);
const letter = ALPHABET[idx];
resultEl.textContent = letter;
copyBtn.classList.remove("invisible");
+10 -16
View File
@@ -135,16 +135,15 @@ David"
<script>
import { CopyFeedback } from "@/lib/client/clipboard";
import { createErrorDisplay } from "@/lib/client/validation";
import { randomInt, shuffleArray } from "@/lib/client/random";
import { getClientLang, getClientT } from "@/lib/client/i18n";
const isRu = document.documentElement.lang === "ru";
const COPY_LABEL = isRu ? "Копировать" : "Copy";
const COPIED_LABEL = isRu ? "Скопировано" : "Copied";
const ERR_ADD_ONE = isRu
? "Добавьте хотя бы один элемент в список."
: "Add at least one item to the list.";
const ERR_PICK_ONE = isRu
? "Выберите хотя бы 1 элемент."
: "Pick at least 1 item.";
const isRu = getClientLang() === "ru";
const T = getClientT();
const COPY_LABEL = T.copy;
const COPIED_LABEL = T.copied;
const ERR_ADD_ONE = T.errAddAtLeast1;
const ERR_PICK_ONE = T.errPickAtLeast1;
const itemsInput = document.getElementById(
"lst-items",
@@ -198,15 +197,10 @@ David"
if (withReplacement) {
lastPicked = Array.from(
{ length: count },
() => items[Math.floor(Math.random() * items.length)],
() => items[randomInt(0, items.length - 1)],
);
} else {
const pool = [...items];
for (let i = 0; i < count; i++) {
const j = i + Math.floor(Math.random() * (pool.length - i));
[pool[i], pool[j]] = [pool[j], pool[i]];
}
lastPicked = pool.slice(0, count);
lastPicked = shuffleArray(items).slice(0, count);
}
resultEl.innerHTML = "";
+8 -19
View File
@@ -138,6 +138,8 @@ const startLabel = isRu
</div>
<script>
import { randomInt, shuffleArray } from "@/lib/client/random";
import { getClientLang, getClientT } from "@/lib/client/i18n";
const WORDS = [
"lorem", "ipsum", "dolor", "sit", "amet", "consectetur", "adipiscing", "elit",
"sed", "do", "eiusmod", "tempor", "incididunt", "ut", "labore", "et", "dolore",
@@ -159,26 +161,13 @@ const startLabel = isRu
const copyIcon = document.getElementById("lg-copy-icon") as SVGElement;
const checkIcon = document.getElementById("lg-check-icon") as SVGElement;
function randInt(min: number, max: number) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
function shuffle<T>(arr: T[]): T[] {
const a = arr.slice();
for (let i = a.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[a[i], a[j]] = [a[j], a[i]];
}
return a;
}
function capitalize(word: string) {
return word.charAt(0).toUpperCase() + word.slice(1);
}
function generateSentence(): string {
const length = randInt(8, 18);
const words = shuffle(WORDS).slice(0, length);
const length = randomInt(8, 18);
const words = shuffleArray(WORDS).slice(0, length);
words[0] = capitalize(words[0]);
const sentence = words.join(" ") + ".";
return sentence;
@@ -218,7 +207,7 @@ const startLabel = isRu
const paragraphs: string[] = [];
for (let i = 0; i < count; i++) {
const sentenceCount = randInt(minSentences, maxSentences);
const sentenceCount = randomInt(minSentences, maxSentences);
paragraphs.push(generateParagraph(sentenceCount, i === 0, startClassic));
}
@@ -226,8 +215,8 @@ const startLabel = isRu
resultWrap.style.opacity = "1";
}
const isRu = document.documentElement.lang === "ru";
const copyLabel = copyBtn.getAttribute("aria-label") || (isRu ? "Копировать" : "Copy");
const T = getClientT();
const copyLabel = copyBtn.getAttribute("aria-label") || T.copy;
let copyTimer: ReturnType<typeof setTimeout> | null = null;
copyBtn.addEventListener("click", async () => {
@@ -236,7 +225,7 @@ const startLabel = isRu
await navigator.clipboard.writeText(text);
copyIcon.classList.add("hidden");
checkIcon.classList.remove("hidden");
copyBtn.setAttribute("aria-label", isRu ? "Скопировано" : "Copied");
copyBtn.setAttribute("aria-label", T.copied);
if (copyTimer) clearTimeout(copyTimer);
copyTimer = setTimeout(() => {
checkIcon.classList.add("hidden");
@@ -111,19 +111,17 @@ const T = useT(isRu ? "ru" : "en");
<script>
import { CopyFeedback } from "@/lib/client/clipboard";
import { createErrorDisplay, isInteger } from "@/lib/client/validation";
import { shuffleArray } from "@/lib/client/random";
import { getClientLang, getClientT } from "@/lib/client/i18n";
const isRu = document.documentElement.lang === "ru";
const ERR_INTEGERS = isRu
? "Все значения должны быть целыми числами."
: "All values must be whole numbers.";
const ERR_FROM_TO = isRu
? "«От» должно быть меньше «До»."
: '"From" must be less than "To".';
const ERR_PICK_MIN = isRu
? "«Выбрать» должно быть не менее 1."
: '"Pick" must be at least 1.';
const COPY_LABEL = isRu ? "Копировать" : "Copy";
const COPIED_LABEL = isRu ? "Скопировано" : "Copied";
const lang = getClientLang();
const T = getClientT();
const isRu = lang === "ru";
const ERR_INTEGERS = T.errAllIntegers;
const ERR_FROM_TO = T.errFromLessThanTo;
const ERR_PICK_MIN = T.errPickAtLeastOne;
const COPY_LABEL = T.copy;
const COPIED_LABEL = T.copied;
const fromInput = document.getElementById("lg-from") as HTMLInputElement;
const toInput = document.getElementById("lg-to") as HTMLInputElement;
@@ -170,11 +168,7 @@ const T = useT(isRu ? "ru" : "en");
}
errors.clear();
const nums = Array.from({ length: pool }, (_, i) => min + i);
for (let i = 0; i < pick; i++) {
const j = i + Math.floor(Math.random() * (pool - i));
[nums[i], nums[j]] = [nums[j], nums[i]];
}
const picked = nums.slice(0, pick).sort((a, b) => a - b);
const picked = shuffleArray(nums).slice(0, pick).sort((a, b) => a - b);
resultEl.innerHTML = "";
picked.forEach((n) => {
const ball = document.createElement("span");
@@ -214,8 +214,12 @@ const copiedLabel = isRu ? "Скопировано" : "Copied";
<script>
import { CopyFeedback } from "@/lib/client/clipboard";
import { randomInt } from "@/lib/client/random";
import { getClientLang, getClientT } from "@/lib/client/i18n";
const isRu = document.documentElement.lang === "ru";
const lang = getClientLang();
const T = getClientT();
const isRu = lang === "ru";
// ─── Answer pools ───────────────────────────────────────────────────────────
@@ -322,7 +326,7 @@ const copiedLabel = isRu ? "Скопировано" : "Copied";
// ─── Helpers ────────────────────────────────────────────────────────────────
function getRandomAnswer(): Answer {
return ANSWERS[Math.floor(Math.random() * ANSWERS.length)];
return ANSWERS[randomInt(0, ANSWERS.length - 1)];
}
function loadHistory(): { question: string; answer: string; type: AnswerType; time: number }[] {
@@ -430,11 +434,11 @@ const copiedLabel = isRu ? "Скопировано" : "Copied";
if (!currentAnswer) return;
await navigator.clipboard.writeText(currentAnswer.text);
clipboard.showCopied();
copyLabel.textContent = isRu ? "Скопировано" : "Copied";
copyLabel.textContent = T.copied;
copiedLabel.style.opacity = "1";
setTimeout(() => {
copiedLabel.style.opacity = "0";
copyLabel.textContent = isRu ? "Копировать" : "Copy";
copyLabel.textContent = T.copy;
}, 1500);
}