commit 8f11c5af43b9ccaad26b8fb6cc850a10b0d6381d Author: emil Date: Wed May 13 00:25:09 2026 +0300 feat: add favorites panel to meal generator, remove redundant Another one button - Add clickable favorites counter with expandable panel - Render saved recipes list with remove button - Remove duplicate 'Another one' button (Generate already covers it) - Sync favorite button state when removing from panel diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..beed316 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,5 @@ +node_modules +dist +.git +.env +*.log diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000..df3e3f3 --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,33 @@ +name: Deploy + +on: + push: + branches: [main] + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: 20 + cache: npm + + - run: npm ci + - run: npm run build + + - name: Setup SSH key + run: | + mkdir -p ~/.ssh + printf '%s\n' "${{ secrets.DEPLOY_KEY }}" > ~/.ssh/deploy_key + chmod 600 ~/.ssh/deploy_key + ssh-keyscan -H ${{ secrets.DEPLOY_HOST }} >> ~/.ssh/known_hosts + + - name: Deploy via rsync + run: | + rsync -avz --delete \ + -e "ssh -i ~/.ssh/deploy_key -o StrictHostKeyChecking=no" \ + dist/ \ + ${{ secrets.DEPLOY_USER }}@${{ secrets.DEPLOY_HOST }}:~/www/randify.pro/ diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..eaed124 --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +node_modules/ +dist/ +.env +*.log +.astro/ +.kimi/ +node_modules_old/ +node_modules/ +package-lock.json +plan.md diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..ad52979 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,6 @@ +dist/ +node_modules/ +.astro/ +package-lock.json +CLAUDE.md +.prettierignore diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..d87faa5 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,11 @@ +{ + "plugins": ["prettier-plugin-astro"], + "overrides": [ + { + "files": "*.astro", + "options": { + "parser": "astro" + } + } + ] +} diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..c869123 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,75 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## What this is + +Randify is a bilingual (EN/RU) static site (Astro 4 + Tailwind CSS 4) hosting random-value generators. All 10 generators are live. Deployed to randify.pro on reg.ru via GitHub Actions + rsync on push to `main`. + +**i18n is handled via Astro's built-in i18n routing.** English pages live at `src/pages/`, Russian pages at `src/pages/ru/`. Both share identical templates because `BaseLayout`, `GeneratorLayout`, and components derive the active locale from `Astro.currentLocale`. + +## Commands + +```bash +npm run dev # Dev server (localhost:4321) +npm run build # Static build → dist/ +npm run preview # Serve the built dist/ locally +``` + +**Docker (production):** multi-stage build with Node 20, served via `nginx:alpine` with pretty URLs. + +```bash +docker build -t randify . +docker run -p 8080:80 randify +``` + +No test runner or linter is configured. + +## Adding a new generator + +Every new generator must be created in **both English and Russian simultaneously**. + +1. **`src/content/generators/.json`** — create a JSON file with all fields: `slug`, `title`, `description`, `icon`, `status`, `seoTitle`, `seoDescription`, `ruTitle`, `ruDescription`, `ruSeoTitle`, `ruSeoDescription`, `pageTitle`, `ruPageTitle`, `howTo`, `whenTo`, `ruHowTo`, `ruWhenTo`. The schema is defined in `src/lib/generator-schema.ts` and validated automatically at build time via Zod (`.strict()` — unknown fields will fail the build). + +2. **`src/components/generators/Generator.astro`** — the interactive component. Language detection pattern: + - Frontmatter: `const isRu = Astro.url.pathname.startsWith('/ru'); const T = useT(isRu ? 'ru' : 'en');` + - Client script: `const isRu = document.documentElement.lang === 'ru';` + - Use `T.*` keys for all user-visible strings in the template; use `isRu` ternaries in the ` + + + + + + + diff --git a/src/components/FaqBlock.astro b/src/components/FaqBlock.astro new file mode 100644 index 0000000..5f71e0a --- /dev/null +++ b/src/components/FaqBlock.astro @@ -0,0 +1,33 @@ +--- +import { useT } from "../i18n/translations"; +import type { Lang } from "../i18n/translations"; + +interface Props { + questions: Array<{ q: string; a: string }>; + lang?: Lang; +} + +const { questions, lang = "en" } = Astro.props; +const T = useT(lang); +--- + +
+
+

+ + {T.faq} +

+
+ { + questions.map(({ q, a }) => ( +
+

{q}

+

{a}

+
+ )) + } +
+
+
diff --git a/src/components/GeneratorCard.astro b/src/components/GeneratorCard.astro new file mode 100644 index 0000000..74bc01b --- /dev/null +++ b/src/components/GeneratorCard.astro @@ -0,0 +1,88 @@ +--- +import type { Generator } from "../data/generators"; +import { useT } from "../i18n/translations"; +import type { Lang } from "../i18n/translations"; + +interface Props { + generator: Generator; + lang?: Lang; +} + +const { generator, lang: langProp } = Astro.props; +const lang = langProp || (Astro.currentLocale as Lang) || "en"; +const T = useT(lang); +const { slug, icon, status } = generator; +const isLive = status === "live"; +const isRu = lang === "ru"; +const title = isRu ? generator.ruTitle : generator.title; +const description = isRu ? generator.ruDescription : generator.description; +const href = isRu ? `/ru/generators/${slug}/` : `/generators/${slug}/`; + +const icons: Record = { + hash: ``, + palette: ``, + lock: ``, + ticket: ``, + "dice-6": ``, + "square-stack": ``, + "circle-dollar-sign": ``, + list: ``, + fingerprint: ``, + "pie-chart": ``, + "help-circle": ``, + user: ``, + users: ``, + type: ``, + calendar: ``, + hand: ``, + smile: ``, + paintbrush: ``, + shuffle: ``, + sparkles: ``, + font: ``, + clock: ``, + utensils: ``, +}; +--- + +{ + isLive ? ( + +
+ +

+ {title} +

+

{description}

+
+ ) : ( +
+
+ +

{title}

+

{description}

+
+ ) +} diff --git a/src/components/LanguageSwitcher.astro b/src/components/LanguageSwitcher.astro new file mode 100644 index 0000000..dfd9206 --- /dev/null +++ b/src/components/LanguageSwitcher.astro @@ -0,0 +1,57 @@ +--- +const currentPath = Astro.url.pathname; +const isRu = currentPath.startsWith("/ru"); +const alternatePath = isRu + ? currentPath.replace(/^\/ru/, "") || "/" + : "/ru" + currentPath; +--- + + + + diff --git a/src/components/SeoBlock.astro b/src/components/SeoBlock.astro new file mode 100644 index 0000000..f801848 --- /dev/null +++ b/src/components/SeoBlock.astro @@ -0,0 +1,40 @@ +--- +import { useT } from "../i18n/translations"; +import type { Lang } from "../i18n/translations"; + +interface Props { + howTo: string[]; + whenTo: string[]; + lang?: Lang; +} + +const { howTo, whenTo, lang = "en" } = Astro.props; +const T = useT(lang); +--- + +
+
+

+ + {T.howToUse} +

+
    + {howTo.map((step) =>
  1. {step}
  2. )} +
+
+
+

+ + {T.whenToUse} +

+
    + {whenTo.map((item) =>
  • {item}
  • )} +
+
+
diff --git a/src/components/Starfield.astro b/src/components/Starfield.astro new file mode 100644 index 0000000..76aecaa --- /dev/null +++ b/src/components/Starfield.astro @@ -0,0 +1,97 @@ + + + diff --git a/src/components/YandexRTB.astro b/src/components/YandexRTB.astro new file mode 100644 index 0000000..0958b87 --- /dev/null +++ b/src/components/YandexRTB.astro @@ -0,0 +1,17 @@ + +
+ Advertisement +
+ diff --git a/src/components/generators/CardGenerator.astro b/src/components/generators/CardGenerator.astro new file mode 100644 index 0000000..bcba2f8 --- /dev/null +++ b/src/components/generators/CardGenerator.astro @@ -0,0 +1,264 @@ +--- +import { useT } from "../../i18n/translations"; +const isRu = Astro.url.pathname.startsWith("/ru"); +const T = useT(isRu ? "ru" : "en"); +--- + +
+
+
+
+ + +
+ +
+
+ + + +
+
+
+ + +
+ +
+ +
+
+ + diff --git a/src/components/generators/CoinGenerator.astro b/src/components/generators/CoinGenerator.astro new file mode 100644 index 0000000..64bcba6 --- /dev/null +++ b/src/components/generators/CoinGenerator.astro @@ -0,0 +1,185 @@ +--- +import { useT } from "../../i18n/translations"; +const isRu = Astro.url.pathname.startsWith("/ru"); +const T = useT(isRu ? "ru" : "en"); +--- + +
+
+
+ + 1 +
+ +
+ 120 +
+
+ +
+
+
+ + + + +
+ +
+ +
+
+ + diff --git a/src/components/generators/ColorGenerator.astro b/src/components/generators/ColorGenerator.astro new file mode 100644 index 0000000..963e030 --- /dev/null +++ b/src/components/generators/ColorGenerator.astro @@ -0,0 +1,184 @@ +--- +import { useT } from "../../i18n/translations"; +const isRu = Astro.url.pathname.startsWith("/ru"); +const T = useT(isRu ? "ru" : "en"); +--- + +
+
+ +
+ +
+ + +
+ { + [ + { id: "cg-hex", label: "HEX" }, + { id: "cg-rgb", label: "RGB" }, + { id: "cg-hsl", label: "HSL" }, + ].map(({ id, label }) => ( +
+ + {label} + + + +
+ )) + } +
+ +
+ + +
+ +
+
+ + diff --git a/src/components/generators/CountryGenerator.astro b/src/components/generators/CountryGenerator.astro new file mode 100644 index 0000000..7da02e3 --- /dev/null +++ b/src/components/generators/CountryGenerator.astro @@ -0,0 +1,249 @@ +--- +import { useT } from "../../i18n/translations"; +const isRu = Astro.url.pathname.startsWith("/ru"); +const T = useT(isRu ? "ru" : "en"); + +const regions = isRu + ? [ + { value: "all", label: "Все" }, + { value: "europe", label: "Европа" }, + { value: "asia", label: "Азия" }, + { value: "americas", label: "Америка" }, + { value: "africa", label: "Африка" }, + { value: "oceania", label: "Океания" }, + ] + : [ + { value: "all", label: "All" }, + { value: "europe", label: "Europe" }, + { value: "asia", label: "Asia" }, + { value: "americas", label: "Americas" }, + { value: "africa", label: "Africa" }, + { value: "oceania", label: "Oceania" }, + ]; +--- + +
+ +
+ + +
+ + +
+
+ +
+ + +
+ +
+
+ + diff --git a/src/components/generators/DateGenerator.astro b/src/components/generators/DateGenerator.astro new file mode 100644 index 0000000..ed5d272 --- /dev/null +++ b/src/components/generators/DateGenerator.astro @@ -0,0 +1,193 @@ +--- +import { useT } from "../../i18n/translations"; +const isRu = Astro.url.pathname.startsWith("/ru"); +const T = useT(isRu ? "ru" : "en"); +--- + +
+
+
+
+ + +
+
+ + +
+
+
+ + + +
+
+ + {T.copied} +
+
+ +
+ +
+
+ + diff --git a/src/components/generators/DiceGenerator.astro b/src/components/generators/DiceGenerator.astro new file mode 100644 index 0000000..e884424 --- /dev/null +++ b/src/components/generators/DiceGenerator.astro @@ -0,0 +1,958 @@ +--- +import { useT } from "../../i18n/translations"; +const isRu = Astro.url.pathname.startsWith("/ru"); +const T = useT(isRu ? "ru" : "en"); + +const notationPlaceholder = isRu + ? "напр. 2d6+3, 1d20, 4d6dl1" + : "e.g. 2d6+3, 1d20, 4d6dl1"; +const modeNormal = isRu ? "Обычный" : "Normal"; +const modeAdvantage = isRu ? "Преимущество" : "Advantage"; +const modeDisadvantage = isRu ? "Помеха" : "Disadvantage"; +const advOnly = isRu + ? "Преимущество работает только с одним кубиком" + : "Advantage only works with a single die"; +const historyTitle = isRu ? "История бросков" : "Roll history"; +const clearHistory = isRu ? "Очистить историю" : "Clear history"; +const presetsLabel = isRu ? "Пресеты" : "Presets"; + +const presets = [ + { label: isRu ? "Атака" : "Attack", notation: "1d20" }, + { label: isRu ? "Урон" : "Damage", notation: "1d8+3" }, + { label: isRu ? "Огненный шар" : "Fireball", notation: "8d6" }, + { label: isRu ? "Скрытая атака" : "Sneak Attack", notation: "3d6" }, + { label: isRu ? "Характеристика" : "Stat Roll", notation: "4d6dl1" }, + { label: isRu ? "Яростный удар" : "Savage Strike", notation: "2d6!" }, +]; +--- + +
+
+ +
+ + +
+ + +
+ + +
+ + +
+
+ + +
+
+ +
+ { + ["4", "6", "8", "10", "12", "20", "100"].map((s) => ( + + )) + } +
+
+
+ + +
+ +
+ { + [ + { value: "normal", label: modeNormal }, + { value: "advantage", label: modeAdvantage }, + { value: "disadvantage", label: modeDisadvantage }, + ].map(({ value, label }, i) => ( + + )) + } +
+ +
+ + + +
+ + +
+
+
+ + + + +
+ + +
+ +
+ + + +
+ + + + diff --git a/src/components/generators/EmojiGenerator.astro b/src/components/generators/EmojiGenerator.astro new file mode 100644 index 0000000..28b64ef --- /dev/null +++ b/src/components/generators/EmojiGenerator.astro @@ -0,0 +1,675 @@ +--- +import { useT } from "../../i18n/translations"; +const isRu = Astro.url.pathname.startsWith("/ru"); +const T = useT(isRu ? "ru" : "en"); + +const categoryLabel = isRu ? "Категория" : "Category"; +const countLabel = isRu ? "Количество" : "Count"; +const allLabel = isRu ? "Все" : "All"; +const smileysLabel = isRu ? "Смайлики" : "Smileys"; +const gesturesLabel = isRu ? "Жесты и тело" : "Gestures"; +const animalsLabel = isRu ? "Животные" : "Animals"; +const foodLabel = isRu ? "Еда и напитки" : "Food"; +const travelLabel = isRu ? "Путешествия" : "Travel"; +const activitiesLabel = isRu ? "Активности" : "Activities"; +const objectsLabel = isRu ? "Предметы" : "Objects"; +const symbolsLabel = isRu ? "Символы" : "Symbols"; +const clickToCopyLabel = isRu ? "Нажмите, чтобы скопировать" : "Click to copy"; +--- + +
+ +
+
+
+ + +
+
+ + +
+
+
+ + + + + +
+
+
+ +
+ +
+
+ + +
+ +
+
+ + diff --git a/src/components/generators/FontPairGenerator.astro b/src/components/generators/FontPairGenerator.astro new file mode 100644 index 0000000..0f24171 --- /dev/null +++ b/src/components/generators/FontPairGenerator.astro @@ -0,0 +1,483 @@ +--- +import { useT } from "../../i18n/translations"; +const isRu = Astro.url.pathname.startsWith("/ru"); +const T = useT(isRu ? "ru" : "en"); + +const i18n = { + generate: T.generate, + copied: T.copied, + copy: T.copy, + headingLabel: isRu ? "Заголовок" : "Heading", + bodyLabel: isRu ? "Основной текст" : "Body Text", + copyHtmlCss: isRu ? "Копировать HTML и CSS" : "Copy HTML & CSS", + shuffleHeading: isRu ? "Перемешать заголовок" : "Shuffle heading", + shuffleBody: isRu ? "Перемешать текст" : "Shuffle body", + lockHeading: isRu ? "Закрепить заголовок" : "Lock heading", + lockBody: isRu ? "Закрепить текст" : "Lock body", + unlockHeading: isRu ? "Открепить заголовок" : "Unlock heading", + unlockBody: isRu ? "Открепить текст" : "Unlock body", + googleFonts: isRu ? "Открыть в Google Fonts" : "Open in Google Fonts", + previewHeading: isRu ? "Быстрая коричневая лиса прыгает через ленивого пса" : "The quick brown fox jumps over the lazy dog", + previewBody: isRu + ? "Lorem ipsum — это текст-рыба, часто используемый в печати и веб-дизайне. Он демонстрирует, как выглядит шрифт в реальном абзаце." + : "Lorem ipspsum dolor sit amet, consectetur adipiscing elit. This sample text shows how the body font looks in a real paragraph with multiple lines of content.", + categories: { + serif: isRu ? "С засечками" : "Serif", + sans: isRu ? "Без засечек" : "Sans-Serif", + display: isRu ? "Декоративный" : "Display", + handwriting: isRu ? "Рукописный" : "Handwriting", + monospace: isRu ? "Моноширинный" : "Monospace", + }, +}; +--- + +
+ +
+ + +
+ + +
+ +
+ +
+
+ {i18n.headingLabel} + +
+ + — + + — +
+ + +
+
+ {i18n.bodyLabel} + +
+ + — + + — +
+
+ + +
+

+ {i18n.previewHeading} +

+

+ {i18n.previewBody} +

+
+ + +
+ +
+
+ + + + + +
+ +
+
+ + diff --git a/src/components/generators/GradientGenerator.astro b/src/components/generators/GradientGenerator.astro new file mode 100644 index 0000000..eb63d75 --- /dev/null +++ b/src/components/generators/GradientGenerator.astro @@ -0,0 +1,300 @@ +--- +import { useT } from "../../i18n/translations"; +const isRu = Astro.url.pathname.startsWith("/ru"); +const T = useT(isRu ? "ru" : "en"); + +const i18n = { + typeLabel: isRu ? "Тип градиента" : "Gradient type", + stopsLabel: isRu ? "Количество цветов" : "Color stops", + generate: T.generate, + copied: T.copied, + copy: T.copy, + cssCode: isRu ? "CSS-код" : "CSS code", + colorStops: isRu ? "Цветовые точки" : "Color stops", + types: { + linear: isRu ? "Линейный" : "Linear", + radial: isRu ? "Радиальный" : "Radial", + conic: isRu ? "Конический" : "Conic", + }, +}; +--- + +
+ +
+
+ + +
+
+ + +
+
+ + +
+ +
+ +
+ + +
+
+ {i18n.cssCode} + +
+
+ +
+
+ + +
+ {i18n.colorStops} +
+ +
+
+
+ + +
+ +
+
+ + diff --git a/src/components/generators/HashGenerator.astro b/src/components/generators/HashGenerator.astro new file mode 100644 index 0000000..eb3f5d9 --- /dev/null +++ b/src/components/generators/HashGenerator.astro @@ -0,0 +1,592 @@ +--- +import { useT } from "../../i18n/translations"; +const isRu = Astro.url.pathname.startsWith("/ru"); +const T = useT(isRu ? "ru" : "en"); +--- + +
+ +
+

+ {isRu ? "Режим ввода" : "Input mode"} +

+
+ + +
+ + +
+ +
+ + + +
+ + +
+

+ {isRu ? "Алгоритмы хеширования" : "Hash algorithms"} +

+
+ + + + +
+ + +
+ + +
+
+ + +
+ + +

+ {isRu + ? "Вставьте хеш, чтобы проверить совпадение с результатами." + : "Paste a hash to check if it matches any result."} +

+
+ + +
+
+ + diff --git a/src/components/generators/LetterGenerator.astro b/src/components/generators/LetterGenerator.astro new file mode 100644 index 0000000..90b5752 --- /dev/null +++ b/src/components/generators/LetterGenerator.astro @@ -0,0 +1,120 @@ +--- +import { useT } from "../../i18n/translations"; +const isRu = Astro.url.pathname.startsWith("/ru"); +const T = useT(isRu ? "ru" : "en"); +--- + +
+
+
+ + {T.copied} +
+
+ +
+ +
+
+ + diff --git a/src/components/generators/ListGenerator.astro b/src/components/generators/ListGenerator.astro new file mode 100644 index 0000000..d16fdc7 --- /dev/null +++ b/src/components/generators/ListGenerator.astro @@ -0,0 +1,242 @@ +--- +import { useT } from "../../i18n/translations"; +const isRu = Astro.url.pathname.startsWith("/ru"); +const T = useT(isRu ? "ru" : "en"); +--- + +
+
+
+
+ + +
+ +
+
+ + +
+ +
+
+ + +
+ +
+
+
+ + +
+ +
+ +
+
+ + diff --git a/src/components/generators/LoremGenerator.astro b/src/components/generators/LoremGenerator.astro new file mode 100644 index 0000000..244e80a --- /dev/null +++ b/src/components/generators/LoremGenerator.astro @@ -0,0 +1,252 @@ +--- +import { useT } from "../../i18n/translations"; +const isRu = Astro.url.pathname.startsWith("/ru"); +const T = useT(isRu ? "ru" : "en"); + +const lengthOptions = isRu + ? [ + { value: "short", label: "Короткие" }, + { value: "medium", label: "Средние" }, + { value: "long", label: "Длинные" }, + ] + : [ + { value: "short", label: "Short" }, + { value: "medium", label: "Medium" }, + { value: "long", label: "Long" }, + ]; + +const startLabel = isRu + ? "Начать с Lorem ipsum dolor sit amet" + : "Start with Lorem ipsum dolor sit amet"; +--- + +
+ +
+
+ +
+ + +
+ + +
+ + +
+
+ + +
+ + +
+ + +
+ +
+
+ + +
+
+ + {isRu ? "Результат" : "Result"} + + +
+ +
+
+
+ + diff --git a/src/components/generators/LotteryGenerator.astro b/src/components/generators/LotteryGenerator.astro new file mode 100644 index 0000000..fe6b25e --- /dev/null +++ b/src/components/generators/LotteryGenerator.astro @@ -0,0 +1,211 @@ +--- +import { useT } from "../../i18n/translations"; +const isRu = Astro.url.pathname.startsWith("/ru"); +const T = useT(isRu ? "ru" : "en"); +--- + +
+
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ +
+
+
+ +
+ +
+ +
+
+ + diff --git a/src/components/generators/Magic8BallGenerator.astro b/src/components/generators/Magic8BallGenerator.astro new file mode 100644 index 0000000..8cfb752 --- /dev/null +++ b/src/components/generators/Magic8BallGenerator.astro @@ -0,0 +1,456 @@ +--- +import { useT } from "../../i18n/translations"; +const isRu = Astro.url.pathname.startsWith("/ru"); +const T = useT(isRu ? "ru" : "en"); + +const askPlaceholder = isRu + ? "Задайте вопрос с ответом да/нет..." + : "Ask a yes/no question..."; +const askButtonLabel = isRu ? "Спросить шар" : "Ask the Ball"; +const historyTitle = isRu ? "История ответов" : "Answer History"; +const clearHistoryLabel = isRu ? "Очистить" : "Clear"; +const clickToAsk = isRu ? "Нажмите, чтобы спросить" : "Click to ask"; +const copyLabel = isRu ? "Копировать" : "Copy"; +const copiedLabel = isRu ? "Скопировано" : "Copied"; +--- + +
+ +
+ + +
+ + +
+
+ +
+
+ + + + +
+
+ +
+ +
+
+
+ + + + {clickToAsk} + +
+
+
+
+ + + + + + {copiedLabel} +
+
+ + +
+ +
+ + + +
+ + + + diff --git a/src/components/generators/MealGenerator.astro b/src/components/generators/MealGenerator.astro new file mode 100644 index 0000000..8301036 --- /dev/null +++ b/src/components/generators/MealGenerator.astro @@ -0,0 +1,678 @@ +--- +import { useT } from "../../i18n/translations"; +const isRu = Astro.url.pathname.startsWith("/ru"); +const T = useT(isRu ? "ru" : "en"); + +const mealTypes = isRu + ? [ + { value: "all", label: "Все" }, + { value: "breakfast", label: "Завтрак" }, + { value: "lunch", label: "Обед" }, + { value: "dinner", label: "Ужин" }, + { value: "snack", label: "Перекус" }, + { value: "dessert", label: "Десерт" }, + ] + : [ + { value: "all", label: "All" }, + { value: "breakfast", label: "Breakfast" }, + { value: "lunch", label: "Lunch" }, + { value: "dinner", label: "Dinner" }, + { value: "snack", label: "Snack" }, + { value: "dessert", label: "Dessert" }, + ]; + +const cuisines = isRu + ? [ + { value: "all", label: "Все" }, + { value: "italian", label: "Итальянская" }, + { value: "asian", label: "Азиатская" }, + { value: "mexican", label: "Мексиканская" }, + { value: "american", label: "Американская" }, + { value: "mediterranean", label: "Средиземноморская" }, + { value: "french", label: "Французская" }, + { value: "indian", label: "Индийская" }, + { value: "russian", label: "Русская" }, + { value: "middle-eastern", label: "Ближневосточная" }, + ] + : [ + { value: "all", label: "All" }, + { value: "italian", label: "Italian" }, + { value: "asian", label: "Asian" }, + { value: "mexican", label: "Mexican" }, + { value: "american", label: "American" }, + { value: "mediterranean", label: "Mediterranean" }, + { value: "french", label: "French" }, + { value: "indian", label: "Indian" }, + { value: "russian", label: "Russian" }, + { value: "middle-eastern", label: "Middle Eastern" }, + ]; + +const difficulties = isRu + ? [ + { value: "all", label: "Все" }, + { value: "easy", label: "Легко" }, + { value: "medium", label: "Средне" }, + { value: "hard", label: "Сложно" }, + ] + : [ + { value: "all", label: "All" }, + { value: "easy", label: "Easy" }, + { value: "medium", label: "Medium" }, + { value: "hard", label: "Hard" }, + ]; + +const timeFilters = isRu + ? [ + { value: "all", label: "Все" }, + { value: "15", label: "До 15 мин" }, + { value: "30", label: "До 30 мин" }, + { value: "60", label: "До 1 часа" }, + { value: "slow", label: "Долгая готовка" }, + ] + : [ + { value: "all", label: "All" }, + { value: "15", label: "Under 15 min" }, + { value: "30", label: "Under 30 min" }, + { value: "60", label: "Under 1 hour" }, + { value: "slow", label: "Slow cook" }, + ]; + +const cuisineLabelMap: Record = { + italian: { en: "Italian", ru: "Итальянская" }, + asian: { en: "Asian", ru: "Азиатская" }, + mexican: { en: "Mexican", ru: "Мексиканская" }, + american: { en: "American", ru: "Американская" }, + mediterranean: { en: "Mediterranean", ru: "Средиземноморская" }, + french: { en: "French", ru: "Французская" }, + indian: { en: "Indian", ru: "Индийская" }, + russian: { en: "Russian", ru: "Русская" }, + "middle-eastern": { en: "Middle Eastern", ru: "Ближневосточная" }, +}; + +const mealTypeLabelMap: Record = { + breakfast: { en: "Breakfast", ru: "Завтрак" }, + lunch: { en: "Lunch", ru: "Обед" }, + dinner: { en: "Dinner", ru: "Ужин" }, + snack: { en: "Snack", ru: "Перекус" }, + dessert: { en: "Dessert", ru: "Десерт" }, +}; + +const difficultyLabelMap: Record = { + easy: { en: "Easy", ru: "Легко", color: "bg-emerald-500/15 text-emerald-400 border-emerald-500/25" }, + medium: { en: "Medium", ru: "Средне", color: "bg-amber-500/15 text-amber-400 border-amber-500/25" }, + hard: { en: "Hard", ru: "Сложно", color: "bg-red-500/15 text-red-400 border-red-500/25" }, +}; + +const dietaryLabelMap: Record = { + vegetarian: { en: "Vegetarian", ru: "Вегетарианское", emoji: "🌿" }, + vegan: { en: "Vegan", ru: "Веганское", emoji: "🌱" }, + "gluten-free": { en: "Gluten-Free", ru: "Без глютена", emoji: "🌾" }, + spicy: { en: "Spicy", ru: "Острое", emoji: "🌶" }, +}; +--- + +
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ +
+ + + + + +
+
+ +
+ + + + + + + +
+ + +

+ + +

+ + +

+ + {isRu ? "Ингредиенты" : "Ingredients"} + +
+
+ + +
+ +
+
+ + +
+ + + + + + + +
+
+
+ + +
+ +
+

+ + diff --git a/src/components/generators/NamesGenerator.astro b/src/components/generators/NamesGenerator.astro new file mode 100644 index 0000000..0486f41 --- /dev/null +++ b/src/components/generators/NamesGenerator.astro @@ -0,0 +1,217 @@ +--- +import { useT } from "../../i18n/translations"; +const isRu = Astro.url.pathname.startsWith("/ru"); +const T = useT(isRu ? "ru" : "en"); + +const genderOptions = [ + { value: "male", label: isRu ? "Мужские" : "Male" }, + { value: "female", label: isRu ? "Женские" : "Female" }, + { value: "any", label: isRu ? "Любые" : "Any" }, +]; +--- + +
+
+ +
+ { + genderOptions.map(({ value, label }, i) => ( + + )) + } +
+
+ +
+
+ + + {T.copied} +
+
+ +
+ +
+
+ + diff --git a/src/components/generators/NumberGenerator.astro b/src/components/generators/NumberGenerator.astro new file mode 100644 index 0000000..29ad637 --- /dev/null +++ b/src/components/generators/NumberGenerator.astro @@ -0,0 +1,178 @@ +--- +import { useT } from "../../i18n/translations"; +const isRu = Astro.url.pathname.startsWith("/ru"); +const T = useT(isRu ? "ru" : "en"); +--- + +
+
+
+
+ + +
+
+ + +
+
+
+ + + +
+
+ + {T.copied} +
+
+ +
+ +
+
+ + diff --git a/src/components/generators/PaletteGenerator.astro b/src/components/generators/PaletteGenerator.astro new file mode 100644 index 0000000..8ce7b9b --- /dev/null +++ b/src/components/generators/PaletteGenerator.astro @@ -0,0 +1,346 @@ +--- +import { useT } from "../../i18n/translations"; +const isRu = Astro.url.pathname.startsWith("/ru"); +const T = useT(isRu ? "ru" : "en"); + +const i18n = { + harmonyLabel: isRu ? "Гармония" : "Harmony", + countLabel: isRu ? "Количество цветов" : "Number of colors", + copyAll: T.copyAll, + generate: T.generate, + copied: T.copied, + copy: T.copy, + modes: { + random: isRu ? "Случайная" : "Random", + analogous: isRu ? "Аналоговая" : "Analogous", + complementary: isRu ? "Комплементарная" : "Complementary", + triadic: isRu ? "Триадная" : "Triadic", + monochromatic: isRu ? "Монохромная" : "Monochromatic", + splitComplementary: isRu ? "Сплит-комплементарная" : "Split-Complementary", + tetradic: isRu ? "Тетрадная" : "Tetradic", + }, +}; +--- + +
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+ +
+ +
+
+ + + + + +
+ +
+
+ + diff --git a/src/components/generators/PasswordGenerator.astro b/src/components/generators/PasswordGenerator.astro new file mode 100644 index 0000000..f0cab8e --- /dev/null +++ b/src/components/generators/PasswordGenerator.astro @@ -0,0 +1,215 @@ +--- +import { useT } from "../../i18n/translations"; +const isRu = Astro.url.pathname.startsWith("/ru"); +const T = useT(isRu ? "ru" : "en"); +--- + +
+
+
+
+
+ + 16 +
+ +
+ 464 +
+
+
+ { + [ + { id: "pg-upper", label: T.uppercase }, + { id: "pg-lower", label: T.lowercase }, + { id: "pg-digits", label: T.digits }, + { id: "pg-symbols", label: T.symbols }, + ].map(({ id, label }) => ( + + )) + } +
+
+
+ + + +
+ + {T.copied} +
+ +
+ +
+
+ + diff --git a/src/components/generators/RpsGenerator.astro b/src/components/generators/RpsGenerator.astro new file mode 100644 index 0000000..25f565b --- /dev/null +++ b/src/components/generators/RpsGenerator.astro @@ -0,0 +1,145 @@ +--- +import { useT } from "../../i18n/translations"; +const isRu = Astro.url.pathname.startsWith("/ru"); +const T = useT(isRu ? "ru" : "en"); +--- + +
+
+

+ {isRu ? "Выберите ваш вариант:" : "Pick your move:"} +

+
+ + + +
+
+ +
+
+

+ {isRu ? "Выберите ход, чтобы начать игру" : "Pick a move to start the game"} +

+ + +
+
+
+ + diff --git a/src/components/generators/ShufflerGenerator.astro b/src/components/generators/ShufflerGenerator.astro new file mode 100644 index 0000000..dfbc15c --- /dev/null +++ b/src/components/generators/ShufflerGenerator.astro @@ -0,0 +1,225 @@ +--- +import { useT } from "../../i18n/translations"; +const isRu = Astro.url.pathname.startsWith("/ru"); +const T = useT(isRu ? "ru" : "en"); +--- + +
+
+
+
+ + +
+
+ + +
+ +
+ +
+ + +
+ + + + diff --git a/src/components/generators/TeamsGenerator.astro b/src/components/generators/TeamsGenerator.astro new file mode 100644 index 0000000..0f98249 --- /dev/null +++ b/src/components/generators/TeamsGenerator.astro @@ -0,0 +1,313 @@ +--- +import { useT } from "../../i18n/translations"; +const isRu = Astro.url.pathname.startsWith("/ru"); +const T = useT(isRu ? "ru" : "en"); + +const itemsPlaceholder = isRu + ? "Алиса\nБоб\nКарол\nДэвид" + : "Alice\nBob\nCarol\nDavid"; +const teamCountLabel = isRu ? "Количество команд" : "Number of teams"; +const teamPrefix = isRu ? "Команда" : "Team"; +const errAddTwo = isRu + ? "Добавьте хотя бы 2 участника." + : "Add at least 2 participants."; +const errTeamsMin = isRu + ? "Минимум 2 команды." + : "Minimum 2 teams."; +const errTeamsMax = isRu + ? "Максимум 20 команд." + : "Maximum 20 teams."; +const errMorePlayers = isRu + ? "Участников должно быть больше, чем команд." + : "Participants must outnumber teams."; +--- + +
+
+
+
+ + +
+ +
+ + +
+
+
+ + + +
+
+
+ + +
+ +
+ +
+
+ + diff --git a/src/components/generators/TimeGenerator.astro b/src/components/generators/TimeGenerator.astro new file mode 100644 index 0000000..29ef793 --- /dev/null +++ b/src/components/generators/TimeGenerator.astro @@ -0,0 +1,435 @@ +--- +import { useT } from "../../i18n/translations"; +const isRu = Astro.url.pathname.startsWith("/ru"); +const T = useT(isRu ? "ru" : "en"); +--- + +
+ +
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+ + +
+
+
+
+ + + + + +
+
+ +
+ + + + + + + + + + + + + + + + + + + +
+ + +
+ + {T.copied} + + + + + + +
+
+
+ + +
+ +
+
+ + diff --git a/src/components/generators/UuidGenerator.astro b/src/components/generators/UuidGenerator.astro new file mode 100644 index 0000000..2df80cf --- /dev/null +++ b/src/components/generators/UuidGenerator.astro @@ -0,0 +1,248 @@ +--- +import { useT } from "../../i18n/translations"; +const isRu = Astro.url.pathname.startsWith("/ru"); +const T = useT(isRu ? "ru" : "en"); +--- + +
+
+
+
+ +
+ { + [ + { value: "uuid", label: "UUID v4" }, + { value: "hex", label: "Hex" }, + { value: "base64", label: "Base64" }, + ].map(({ value, label }) => ( + + )) + } +
+
+ +
+ +
+ + +
+
+
+
+ +
+
+ + +
+ +
+ +
+
+ + diff --git a/src/components/generators/WeightedGenerator.astro b/src/components/generators/WeightedGenerator.astro new file mode 100644 index 0000000..0f01759 --- /dev/null +++ b/src/components/generators/WeightedGenerator.astro @@ -0,0 +1,365 @@ +--- +import { useT } from "../../i18n/translations"; +const isRu = Astro.url.pathname.startsWith("/ru"); +const T = useT(isRu ? "ru" : "en"); +--- + +
+
+
+ +
+ +
+ +
+ + +
+ + + + + +
+ +
+ + +
+
+
+ + diff --git a/src/components/generators/WheelSpinner.astro b/src/components/generators/WheelSpinner.astro new file mode 100644 index 0000000..8fb6815 --- /dev/null +++ b/src/components/generators/WheelSpinner.astro @@ -0,0 +1,422 @@ +--- +import { useT } from "../../i18n/translations"; +const isRu = Astro.url.pathname.startsWith("/ru"); +const T = useT(isRu ? "ru" : "en"); +--- + +
+ +
+
+
+ + +
+ +
+
+ + +
+
+ + + +
+ + +
+ + + +
+ + + + diff --git a/src/components/generators/YesNoGenerator.astro b/src/components/generators/YesNoGenerator.astro new file mode 100644 index 0000000..0d4a0dc --- /dev/null +++ b/src/components/generators/YesNoGenerator.astro @@ -0,0 +1,182 @@ +--- +import { useT } from "../../i18n/translations"; +const isRu = Astro.url.pathname.startsWith("/ru"); +const T = useT(isRu ? "ru" : "en"); + +const askPlaceholder = isRu ? "Задайте вопрос..." : "Ask anything..."; +const getAnswerLabel = isRu ? "Получить ответ" : "Get Answer"; +const confidenceLabel = isRu ? "Уверенность" : "Confidence"; +--- + +
+
+ + +
+ +
+
+ + + + + {T.copied} +
+
+ +
+ +
+
+ + diff --git a/src/content/config.ts b/src/content/config.ts new file mode 100644 index 0000000..6ac065d --- /dev/null +++ b/src/content/config.ts @@ -0,0 +1,11 @@ +import { defineCollection } from "astro:content"; +import { generatorSchema } from "@/lib/generator-schema"; + +const generators = defineCollection({ + type: "data", + schema: generatorSchema, +}); + +export const collections = { + generators, +}; diff --git a/src/content/generators/cards.json b/src/content/generators/cards.json new file mode 100644 index 0000000..8d4ad28 --- /dev/null +++ b/src/content/generators/cards.json @@ -0,0 +1,48 @@ +{ + "slug": "cards", + "title": "Card", + "description": "Draw a random playing card from a standard deck.", + "icon": "square-stack", + "status": "live", + "seoTitle": "Random Card Generator — Draw from a Deck | Randify", + "seoDescription": "Draw random playing cards from a standard 52-card deck. Pick any number of cards, with or without replacement. Free online card picker.", + "ruTitle": "Карта", + "ruDescription": "Вытащите случайную карту из стандартной колоды.", + "ruSeoTitle": "Генератор случайных карт | Randify", + "ruSeoDescription": "Тяните случайные карты из колоды 52 карт. С повторениями или без. Бесплатный онлайн-генератор карт.", + "pageTitle": "Card Generator", + "ruPageTitle": "Генератор карт", + "howTo": [ + "Set how many cards to draw (1–52).", + "Toggle Allow duplicates if you want cards to repeat.", + "Click Draw — cards are shown as mini card tiles.", + "Click Copy to copy the result (e.g. A♠, K♥) to your clipboard." + ], + "whenTo": [ + "Drawing a random hand for card games.", + "Practising card probability and statistics.", + "Simulating a random card pull for magic tricks or puzzles.", + "Running fair draws without a physical deck." + ], + "ruHowTo": [ + "Укажите количество карт для розыгрыша.", + "Включите «Разрешить повторения», если нужны дубли.", + "Нажмите «Тянуть» — карты появятся на экране.", + "Нажмите «Копировать», чтобы скопировать результат." + ], + "ruWhenTo": [ + "Случайная раздача карт для карточных игр.", + "Выбор случайных карт для гаданий.", + "Генерация случайных комбинаций карт для тестирования." + ], + "faq": [ + { "q": "What deck is used?", "a": "A standard 52-card deck with four suits: spades ♠, hearts ♥, diamonds ♦, and clubs ♣." }, + { "q": "Can I draw the same card twice?", "a": "Only if Allow duplicates is enabled. By default, each card is drawn at most once." }, + { "q": "Are jokers included?", "a": "No. Only the standard 52 playing cards are used." } + ], + "ruFaq": [ + { "q": "Какая колода используется?", "a": "Стандартная колода из 52 карт четырёх мастей: пики ♠, червы ♥, бубны ♦, трефы ♣." }, + { "q": "Можно вытащить одну карту дважды?", "a": "Только если включены повторения. По умолчанию каждая карта вытаскивается не более одного раза." }, + { "q": "Есть ли джокеры?", "a": "Нет. Используется только стандартная колода из 52 карт." } + ] +} diff --git a/src/content/generators/coin.json b/src/content/generators/coin.json new file mode 100644 index 0000000..d0ff7d0 --- /dev/null +++ b/src/content/generators/coin.json @@ -0,0 +1,50 @@ +{ + "slug": "coin", + "title": "Coin Flip", + "description": "Flip one or more coins and see heads or tails.", + "icon": "circle-dollar-sign", + "status": "live", + "seoTitle": "Coin Flip Online — Heads or Tails | Randify", + "seoDescription": "Flip a virtual coin online. Flip multiple coins at once and see how many heads and tails you get. Free random coin toss simulator.", + "ruTitle": "Монетка", + "ruDescription": "Подбросьте одну или несколько монет — орёл или решка.", + "ruSeoTitle": "Подбросить монетку онлайн — орёл или решка | Randify", + "ruSeoDescription": "Подбрасывайте виртуальную монету онлайн. Одну или несколько сразу. Бесплатный симулятор подбрасывания монеты.", + "pageTitle": "Coin Flip", + "ruPageTitle": "Подбросить монетку", + "howTo": [ + "Drag the slider to choose how many coins to flip (1–20).", + "Click Flip — each coin shows H for heads or T for tails.", + "When flipping more than one coin, the heads/tails count is shown below.", + "Click Copy to copy all results to the clipboard.", + "Use a single coin for a classic 50/50 decision." + ], + "whenTo": [ + "Making a quick heads-or-tails decision.", + "Simulating a coin toss for a sports match or game.", + "Demonstrating probability and the law of large numbers.", + "Settling a fair 50/50 choice between two options." + ], + "ruHowTo": [ + "Выберите количество монет с помощью ползунка.", + "Нажмите «Подбросить» — орёл или решка.", + "При нескольких монетах отображается итоговый счёт.", + "Нажмите «Копировать», чтобы сохранить результат.", + "Одна монетка — классическое решение «орёл или решка»." + ], + "faq": [ + { "q": "Is the coin flip fair?", "a": "Yes. Each flip uses your browser's random function, giving a true 50/50 chance for heads or tails." }, + { "q": "Can I flip multiple coins at once?", "a": "Yes. Use the slider to choose up to 20 coins, and see the total heads and tails count." }, + { "q": "What do H and T stand for?", "a": "H means Heads and T means Tails — the two sides of a coin." } + ], + "ruFaq": [ + { "q": "Подбрасывание честное?", "a": "Да. Каждый бросок использует функцию случайности браузера, давая истинный шанс 50/50 на орла или решку." }, + { "q": "Можно подбросить несколько монет сразу?", "a": "Да. С помощью ползунка выберите до 20 монет и увидите общий счёт орлов и решек." }, + { "q": "Что означают О и Р?", "a": "О — орёл, Р — решка. Это две стороны монеты." } + ], + "ruWhenTo": [ + "Принятие решений по принципу «орёл или решка».", + "Определение очерёдности в игре.", + "Симуляция случайных событий с равной вероятностью." + ] +} diff --git a/src/content/generators/colors.json b/src/content/generators/colors.json new file mode 100644 index 0000000..0812034 --- /dev/null +++ b/src/content/generators/colors.json @@ -0,0 +1,56 @@ +{ + "slug": "colors", + "title": "Color", + "description": "Generate a random color in HEX, RGB, or HSL format.", + "icon": "palette", + "status": "live", + "seoTitle": "Random Color Generator — HEX, RGB, HSL | Randify", + "seoDescription": "Get a random color in HEX, RGB or HSL format with one click. Free online color randomizer for designers and developers.", + "ruTitle": "Цвет", + "ruDescription": "Генерируйте случайный цвет в форматах HEX, RGB или HSL.", + "ruSeoTitle": "Генератор случайных цветов — HEX, RGB, HSL | Randify", + "ruSeoDescription": "Получайте случайные цвета в форматах HEX, RGB и HSL одним нажатием. Бесплатный рандомайзер цветов для дизайнеров.", + "pageTitle": "Color Generator", + "ruPageTitle": "Генератор цветов", + "howTo": [ + "Click Generate to produce a random color.", + "Switch between HEX, RGB, and HSL tabs to see the value in your preferred format.", + "Click the color value or the copy icon to copy it to the clipboard." + ], + "whenTo": [ + "Finding inspiration for a new design palette.", + "Picking a placeholder color while prototyping.", + "Selecting a random theme color for a project or presentation.", + "Teaching or exploring color theory and formats." + ], + "faq": [ + { "q": "What color formats are supported?", "a": "HEX, RGB, and HSL. All three are shown at once, so you can copy the one you need." }, + { "q": "Can I use the generated color in CSS?", "a": "Yes. Copy the HEX or RGB value and paste it directly into your stylesheet or design tool." }, + { "q": "Are the colors truly random?", "a": "Yes. Each color is generated using a cryptographically secure random number generator, covering the entire color space." } + ], + "ruFaq": [ + { "q": "Какие форматы цветов поддерживаются?", "a": "HEX, RGB и HSL. Все три формата отображаются одновременно — скопируйте тот, который нужен." }, + { "q": "Можно ли использовать сгенерированный цвет в CSS?", "a": "Да. Скопируйте значение HEX или RGB и вставьте его прямо в стили или графический редактор." }, + { "q": "Цвета действительно случайные?", "a": "Да. Каждый цвет генерируется с помощью криптографически стойкого генератора случайных чисел, покрывая всё цветовое пространство." } + ], + "ruHowTo": [ + "Нажмите «Сгенерировать», чтобы получить случайный цвет.", + "Цвет отображается в форматах HEX, RGB и HSL одновременно.", + "Нажмите на иконку копирования рядом с нужным форматом." + ], + "ruWhenTo": [ + "Поиск вдохновения для цветовой палитры.", + "Случайный выбор цвета для дизайна или иллюстрации.", + "Генерация тестовых данных с цветовыми значениями." + ], + "faq": [ + { "q": "What color formats are supported?", "a": "HEX, RGB, and HSL. All three are shown simultaneously for every generated color." }, + { "q": "Can I copy just one format?", "a": "Yes. Click the copy icon next to any format to copy only that value." }, + { "q": "Are the colors truly random?", "a": "Yes. Each color is generated using random values for hue, saturation, and lightness." } + ], + "ruFaq": [ + { "q": "Какие форматы цветов поддерживаются?", "a": "HEX, RGB и HSL. Все три отображаются одновременно для каждого сгенерированного цвета." }, + { "q": "Можно скопировать только один формат?", "a": "Да. Нажмите иконку копирования рядом с нужным форматом." }, + { "q": "Цвета действительно случайные?", "a": "Да. Каждый цвет генерируется из случайных значений оттенка, насыщенности и освещённости." } + ] +} diff --git a/src/content/generators/country.json b/src/content/generators/country.json new file mode 100644 index 0000000..e22ddb0 --- /dev/null +++ b/src/content/generators/country.json @@ -0,0 +1,49 @@ +{ + "slug": "country", + "title": "Country", + "description": "Generate a random country with flag, capital, region, and population.", + "icon": "user", + "status": "live", + "seoTitle": "Random Country Generator — Flag, Capital, Region | Randify", + "seoDescription": "Get a random country with flag emoji, capital, region, and population. Free online country randomizer for games, quizzes, and travel inspiration.", + "ruTitle": "Страна", + "ruDescription": "Генерируйте случайную страну с флагом, столицей, регионом и населением.", + "ruSeoTitle": "Генератор случайных стран — флаг, столица, регион | Randify", + "ruSeoDescription": "Получайте случайную страну с эмодзи флага, столицей, регионом и населением. Бесплатный рандомайзер стран для игр, викторин и вдохновения.", + "pageTitle": "Random Country Generator", + "ruPageTitle": "Генератор случайных стран", + "howTo": [ + "Select a region filter or leave it on All to include every country.", + "Click Generate to pick a random country from the filtered list.", + "View the result card with flag, name, capital, region, and population.", + "Click Copy to copy the country details to your clipboard." + ], + "whenTo": [ + "Choosing a random destination for a travel challenge or game.", + "Creating quiz questions about capitals, flags, or geography.", + "Picking a random country for a school project or presentation.", + "Settling a friendly debate with a random geographic draw." + ], + "ruHowTo": [ + "Выберите фильтр по региону или оставьте «Все», чтобы включить все страны.", + "Нажмите «Сгенерировать», чтобы выбрать случайную страну из отфильтрованного списка.", + "Посмотрите карточку результата с флагом, названием, столицей, регионом и населением.", + "Нажмите «Копировать», чтобы скопировать данные о стране в буфер обмена." + ], + "ruWhenTo": [ + "Выбор случайного направления для путешественнического челленджа или игры.", + "Создание вопросов для викторин о столицах, флагах или географии.", + "Выбор случайной страны для школьного проекта или презентации.", + "Решение спора случайным географическим выбором." + ], + "faq": [ + { "q": "How many countries are in the generator?", "a": "The generator includes 40 countries from all inhabited continents, covering a wide range of regions, populations, and cultures." }, + { "q": "Can I filter by region?", "a": "Yes. You can filter by Europe, Asia, Americas, Africa, or Oceania, or choose All to include every country." }, + { "q": "Is the population data accurate?", "a": "Population figures are approximate and based on recent estimates. They are intended for casual use rather than precise research." } + ], + "ruFaq": [ + { "q": "Сколько стран в генераторе?", "a": "Генератор включает 40 стран со всех обитаемых континентов, охватывая широкий спектр регионов, населения и культур." }, + { "q": "Можно ли фильтровать по региону?", "a": "Да. Вы можете фильтровать по Европе, Азии, Америке, Африке или Океании, либо выбрать «Все», чтобы включить все страны." }, + { "q": "Данные о населении актуальны?", "a": "Численность населения приблизительная и основана на недавних оценках. Она предназначена для повседневного использования, а не для точных исследований." } + ] +} diff --git a/src/content/generators/date.json b/src/content/generators/date.json new file mode 100644 index 0000000..801f28d --- /dev/null +++ b/src/content/generators/date.json @@ -0,0 +1,47 @@ +{ + "slug": "date", + "title": "Random Date", + "description": "Generate a random date between any two dates you choose.", + "icon": "calendar", + "status": "live", + "seoTitle": "Random Date Generator — Between Two Dates | Randify", + "seoDescription": "Generate a random date between any two dates instantly. Free online date picker for planning, games, and creative projects.", + "ruTitle": "Случайная дата", + "ruDescription": "Сгенерируйте случайную дату между любыми двумя датами.", + "ruSeoTitle": "Генератор случайных дат — между двумя датами | Randify", + "ruSeoDescription": "Сгенерируйте случайную дату в любом диапазоне мгновенно. Бесплатный инструмент для планирования, игр и творческих задач.", + "pageTitle": "Random Date Generator", + "ruPageTitle": "Генератор случайных дат", + "howTo": [ + "Select the start date in the From field.", + "Select the end date in the To field.", + "Click Generate — a random date within your range appears instantly.", + "Click the date to copy it to your clipboard." + ], + "whenTo": [ + "Picking a random deadline or milestone for a challenge.", + "Choosing a date for a hypothetical scenario or story setting.", + "Generating random historical dates for a quiz or trivia game." + ], + "ruHowTo": [ + "Выберите начальную дату в поле «От».", + "Выберите конечную дату в поле «До».", + "Нажмите «Сгенерировать» — случайная дата в диапазоне появится мгновенно.", + "Нажмите на дату, чтобы скопировать её в буфер обмена." + ], + "ruWhenTo": [ + "Выбор случайного дедлайна или вехи для челленджа.", + "Определение даты для гипотетического сценария или сюжета.", + "Генерация случайных исторических дат для викторины или игры." + ], + "faq": [ + { "q": "What date format is used?", "a": "Dates are displayed in a clear, locale-friendly format. You can copy the value and paste it wherever you need." }, + { "q": "Can I generate multiple random dates?", "a": "Currently, one date is generated per click. Click again to get another random date in the same range." }, + { "q": "Is the selection truly random?", "a": "Yes. The date is picked using a cryptographically secure random number generator, so every day in your range has an equal chance." } + ], + "ruFaq": [ + { "q": "Какой формат даты используется?", "a": "Даты отображаются в удобном локализованном формате. Вы можете скопировать значение и вставить его куда угодно." }, + { "q": "Можно ли сгенерировать несколько дат сразу?", "a": "Сейчас генерируется одна дата за раз. Нажмите снова, чтобы получить ещё одну случайную дату в том же диапазоне." }, + { "q": "Выбор действительно случайный?", "a": "Да. Дата выбирается с помощью криптографически стойкого генератора случайных чисел, поэтому каждый день в диапазоне имеет равный шанс." } + ] +} diff --git a/src/content/generators/dice.json b/src/content/generators/dice.json new file mode 100644 index 0000000..1a77993 --- /dev/null +++ b/src/content/generators/dice.json @@ -0,0 +1,63 @@ +{ + "slug": "dice", + "title": "Dice", + "description": "Roll one or more dice with any number of sides.", + "icon": "dice-6", + "status": "live", + "seoTitle": "Dice Roller — d4 d6 d20 + Full Notation | D&D Dice | Randify", + "seoDescription": "Free online dice roller with full XdY+Z notation, keep/drop (4d6dl1), exploding dice (3d6!), reroll, advantage/disadvantage, and roll history. Perfect for D&D, Pathfinder, and Savage Worlds.", + "ruTitle": "Кубик", + "ruDescription": "Бросьте один или несколько кубиков с любым числом граней.", + "ruSeoTitle": "Бросить кубик онлайн — d4 d6 d8 d10 d12 d20 | D&D | Randify", + "ruSeoDescription": "Бесплатный онлайн-бросок кубиков с полной нотацией XdY+Z, keep/drop (4d6dl1), взрывающимися кубиками (3d6!), перебросом, преимуществом/помехой и историей. Для D&D, Pathfinder и Savage Worlds.", + "pageTitle": "Dice Generator", + "ruPageTitle": "Бросить кубик онлайн", + "howTo": [ + "Type a notation like 2d6+3 or 1d20 — or use the die buttons and count.", + "Pick a mode: Normal, Advantage, or Disadvantage (single die only).", + "Click Roll — each die result appears with the total sum.", + "Maximum rolls are highlighted in purple; ones are highlighted in red." + ], + "whenTo": [ + "Rolling dice for D&D, Pathfinder, or any tabletop RPG.", + "Using dice notation shortcuts like 2d6+3 or 1d20 for fast rolling.", + "Rolling with advantage or disadvantage for D&D 5e mechanics.", + "Generating character stats with 4d6 drop-lowest method.", + "Rolling exploding dice for Savage Worlds or house rules (e.g. 3d6!).", + "Using keep/drop modifiers like 4d6kh3 to keep highest rolls." + ], + "faq": [ + { "q": "What dice notation can I use?", "a": "You can use standard XdY+Z notation, plus modifiers like advantage, disadvantage, keep/drop (kh/kl/dl/dh), and exploding dice (!)." }, + { "q": "Is this fair for D&D and other RPGs?", "a": "Yes. Randify uses the browser's cryptographically secure random number generator, so every roll is unbiased." }, + { "q": "Can I roll multiple dice at once?", "a": "Absolutely. Enter something like 5d6 or click the die buttons and set the count to roll up to 20 dice in a single throw." } + ], + "ruFaq": [ + { "q": "Какую нотацию кубиков поддерживает генератор?", "a": "Стандартную нотацию XdY+Z, а также модификаторы: преимущество, помеха, keep/drop (kh/kl/dl/dh) и взрывающиеся кубики (!)." }, + { "q": "Подходит ли генератор для D&D и других НРИ?", "a": "Да. Randify использует криптографически стойкий генератор случайных чисел браузера, поэтому каждый бросок честный и непредвзятый." }, + { "q": "Можно ли бросать несколько кубиков одновременно?", "a": "Конечно. Введите что-то вроде 5d6 или настройте кубики вручную — можно бросить до 20 кубиков за раз." } + ], + "ruHowTo": [ + "Введите нотацию, например 2d6+3 или 1d20 — или настройте кубики вручную.", + "Выберите режим: Обычный, Преимущество или Помеха (только для одного кубика).", + "Нажмите «Бросить» — результаты отобразятся мгновенно.", + "Максимумы подсвечены фиолетовым, единицы — красным." + ], + "ruWhenTo": [ + "Броски в настольных ролевых играх (D&D и другие).", + "Быстрые броски через нотацию: 2d6+3, 1d20 и любые другие.", + "Преимущество и помеха для механик D&D 5e.", + "Бросок характеристик: 4d6, отбросить наименьший (4d6dl1).", + "Взрывающиеся кубики для Savage Worlds или хаус-рулов (например, 3d6!).", + "Использование keep/drop модификаторов типа 4d6kh3 (оставить три лучших)." + ], + "faq": [ + { "q": "What dice notation is supported?", "a": "Standard XdY+Z, advantage/disadvantage, keep/drop (kh/kl/dh/dl), reroll (r), and exploding dice (!)." }, + { "q": "Can I roll more than one type of die at once?", "a": "Yes. Enter multiple notations separated by spaces, like 2d6 1d20." }, + { "q": "Is the roll history saved?", "a": "Yes. Your last rolls are shown below the results until you refresh the page." } + ], + "ruFaq": [ + { "q": "Какая нотация кубиков поддерживается?", "a": "Стандартная XdY+Z, преимущество/помеха, keep/drop (kh/kl/dh/dl), переброс (r) и взрывающиеся кубики (!)." }, + { "q": "Можно ли бросить несколько типов кубиков сразу?", "a": "Да. Введите несколько нотаций через пробел, например 2d6 1d20." }, + { "q": "История бросков сохраняется?", "a": "Да. Последние броски отображаются под результатами до обновления страницы." } + ] +} diff --git a/src/content/generators/emoji.json b/src/content/generators/emoji.json new file mode 100644 index 0000000..ce1bc5e --- /dev/null +++ b/src/content/generators/emoji.json @@ -0,0 +1,49 @@ +{ + "slug": "emoji", + "title": "Random Emoji", + "description": "Generate random emojis by category. Pick smileys, animals, food, travel, and more.", + "icon": "smile", + "status": "live", + "seoTitle": "Random Emoji Generator — Smileys, Animals, Food & More | Randify", + "seoDescription": "Generate random emojis by category or mix them all. Pick smileys, animals, food, travel, activities, objects, and symbols. Click to copy. Free and instant.", + "ruTitle": "Случайные эмодзи", + "ruDescription": "Генерируйте случайные эмодзи по категориям. Смайлики, животные, еда, путешествия и многое другое.", + "ruSeoTitle": "Генератор случайных эмодзи — смайлики, животные, еда и другое | Randify", + "ruSeoDescription": "Генерируйте случайные эмодзи по категории или смешивайте их все. Смайлики, животные, еда, путешествия, активности, предметы и символы. Нажмите, чтобы скопировать. Бесплатно и мгновенно.", + "pageTitle": "Random Emoji Generator", + "ruPageTitle": "Генератор случайных эмодзи", + "howTo": [ + "Select a category from the dropdown — or choose \"All\" to mix every category together.", + "Set how many emojis you want to generate (1–50).", + "Click Generate to get your random emojis. Click any individual emoji to copy it, or use Copy All to copy the entire set." + ], + "whenTo": [ + "Spicing up messages, bios, or social media posts with random emoji combos.", + "Picking a random emoji for games, quizzes, or ice-breakers.", + "Finding inspiration for emoji-based designs or creative projects.", + "Quickly copying a batch of emojis without scrolling through a picker." + ], + "ruHowTo": [ + "Выберите категорию из выпадающего списка — или «Все», чтобы смешать все категории.", + "Укажите, сколько эмодзи вы хотите сгенерировать (1–50).", + "Нажмите «Сгенерировать», чтобы получить случайные эмодзи. Нажмите на любой эмодзи, чтобы скопировать его, или используйте «Копировать всё», чтобы скопировать весь набор." + ], + "ruWhenTo": [ + "Разнообразие сообщений, биографий или постов в социальных сетях случайными комбинациями эмодзи.", + "Выбор случайного эмодзи для игр, викторин или разминок.", + "Поиск вдохновения для дизайнов на основе эмодзи или творческих проектов.", + "Быстрое копирование набора эмодзи без прокрутки через пикер." + ], + "faq": [ + { "q": "How many emojis can I generate at once?", "a": "You can generate between 1 and 50 emojis in a single click. If you pick more than the category contains, we'll shuffle and return as many unique ones as available." }, + { "q": "Can I mix categories together?", "a": "Yes. Select \"All\" from the category dropdown to get a random mix from every emoji category." }, + { "q": "Are the emojis truly random?", "a": "Yes. Each generation shuffles the emoji pool and picks completely at random using the browser's built-in random number generator." }, + { "q": "Can I copy a single emoji?", "a": "Absolutely. Click any individual emoji in the result grid to copy it instantly to your clipboard." } + ], + "ruFaq": [ + { "q": "Сколько эмодзи можно сгенерировать за раз?", "a": "Вы можете сгенерировать от 1 до 50 эмодзи одним нажатием. Если вы выберете больше, чем содержит категория, мы перемешаем и вернём столько уникальных, сколько доступно." }, + { "q": "Можно ли смешивать категории?", "a": "Да. Выберите «Все» в выпадающем списке категорий, чтобы получить случайную смесь из всех категорий эмодзи." }, + { "q": "Эмодзи действительно случайные?", "a": "Да. При каждой генерации набор эмодзи перемешивается и выбирается полностью случайным образом с помощью встроенного генератора случайных чисел браузера." }, + { "q": "Можно ли скопировать одно эмодзи?", "a": "Безусловно. Нажмите на любой эмодзи в сетке результатов, чтобы мгновенно скопировать его в буфер обмена." } + ] +} diff --git a/src/content/generators/fontpair.json b/src/content/generators/fontpair.json new file mode 100644 index 0000000..f12b4fa --- /dev/null +++ b/src/content/generators/fontpair.json @@ -0,0 +1,67 @@ +{ + "slug": "fontpair", + "title": "Random Font Pairing", + "description": "Generate a random pairing of heading and body fonts from Google Fonts.", + "icon": "font", + "status": "live", + "seoTitle": "Random Font Pairing Generator — Google Fonts Combinations | Randify", + "seoDescription": "Generate random font pairings from Google Fonts for headings and body text. Discover beautiful typography combinations with one click. Free online tool for designers and developers.", + "ruTitle": "Случайная пара шрифтов", + "ruDescription": "Сгенерируйте случайную пару шрифтов для заголовков и основного текста из Google Fonts.", + "ruSeoTitle": "Генератор случайных пар шрифтов — комбинации Google Fonts | Randify", + "ruSeoDescription": "Создавайте случайные пары шрифтов из Google Fonts для заголовков и основного текста. Находите красивые типографские сочетания одним нажатием. Бесплатный онлайн-инструмент для дизайнеров и разработчиков.", + "pageTitle": "Random Font Pairing", + "ruPageTitle": "Случайная пара шрифтов", + "howTo": [ + "Click Generate to create a new font pairing.", + "Preview shows a heading sample and a body text sample.", + "Use the lock icons to keep a font you like while shuffling the other.", + "Click Copy to get the HTML link tags and CSS font-family declarations." + ], + "whenTo": [ + "Choosing fonts for a new website or web app.", + "Exploring typography options for a design system.", + "Finding inspiration for a brand identity or logo.", + "Quickly prototyping a landing page or mockup." + ], + "ruHowTo": [ + "Нажмите «Сгенерировать», чтобы создать новую пару шрифтов.", + "Предпросмотр показывает образец заголовка и образец основного текста.", + "Используйте значки замка, чтобы сохранить понравившийся шрифт, меняя другой.", + "Нажмите «Копировать», чтобы получить HTML-теги link и CSS-объявления font-family." + ], + "ruWhenTo": [ + "Выбор шрифтов для нового сайта или веб-приложения.", + "Исследование типографских вариантов для дизайн-системы.", + "Поиск вдохновения для фирменного стиля или логотипа.", + "Быстрое прототипирование лендинга или макета." + ], + "faq": [ + { + "q": "What is a font pairing generator?", + "a": "A font pairing generator randomly combines two complementary fonts from Google Fonts — one for headings and one for body text — to help you discover beautiful typography combinations for your projects." + }, + { + "q": "How does font pairing work?", + "a": "Good font pairing typically combines fonts with contrasting styles (e.g., a decorative serif heading with a clean sans-serif body) while maintaining visual harmony. The generator selects from curated heading and body font categories to ensure balanced combinations." + }, + { + "q": "Can I use these fonts commercially?", + "a": "Yes. All fonts in this generator are from Google Fonts, which are licensed under open-source licenses (typically SIL Open Font License). You can use them freely in both personal and commercial projects." + } + ], + "ruFaq": [ + { + "q": "Что такое генератор пар шрифтов?", + "a": "Генератор пар шрифтов случайным образом объединяет два дополняющих друг друга шрифта из Google Fonts — один для заголовков и один для основного текста — чтобы помочь вам найти красивые типографские сочетания для ваших проектов." + }, + { + "q": "Как работает подбор пар шрифтов?", + "a": "Хороший подбор пар шрифтов обычно объединяет шрифты с контрастными стилями (например, декоративный заголовочный шрифт с засечками и чистый шрифт без засечек для основного текста), сохраняя при этом визуальную гармонию. Генератор выбирает из курируемых категорий заголовочных и текстовых шрифтов, обеспечивая сбалансированные комбинации." + }, + { + "q": "Можно ли использовать эти шрифты в коммерческих проектах?", + "a": "Да. Все шрифты в этом генераторе взяты из Google Fonts и лицензированы под открытыми лицензиями (обычно SIL Open Font License). Вы можете свободно использовать их как в личных, так и в коммерческих проектах." + } + ] +} diff --git a/src/content/generators/gradient.json b/src/content/generators/gradient.json new file mode 100644 index 0000000..371bd9c --- /dev/null +++ b/src/content/generators/gradient.json @@ -0,0 +1,47 @@ +{ + "slug": "gradient", + "title": "Gradient Generator", + "description": "Generate beautiful random CSS gradients — linear, radial, and conic — with harmonious colors.", + "icon": "paintbrush", + "status": "live", + "seoTitle": "Random Gradient Generator — CSS Linear, Radial & Conic | Randify", + "seoDescription": "Generate beautiful random CSS gradients instantly. Choose linear, radial, or conic type with 2–5 harmonious color stops. Copy ready-to-use CSS code for free.", + "ruTitle": "Генератор градиентов", + "ruDescription": "Создавайте красивые случайные CSS-градиенты — линейные, радиальные и конические — с гармоничными цветами.", + "ruSeoTitle": "Генератор случайных градиентов — CSS линейный, радиальный, конический | Randify", + "ruSeoDescription": "Создавайте красивые случайные CSS-градиенты мгновенно. Выбирайте тип: линейный, радиальный или конический, с 2–5 гармоничными цветами. Копируйте готовый CSS-код бесплатно.", + "pageTitle": "Random Gradient Generator", + "ruPageTitle": "Генератор случайных градиентов", + "howTo": [ + "Select the gradient type (Linear, Radial, or Conic) and the number of color stops (2–5).", + "Click Generate to create a new random gradient with harmonious colors.", + "Copy the CSS code to your clipboard, or click any individual color HEX to copy it." + ], + "whenTo": [ + "Designing website backgrounds, hero sections, or UI overlays.", + "Creating eye-catching social media graphics and banners.", + "Prototyping app interfaces that need vibrant color transitions.", + "Exploring color combinations and gradient styles for presentations." + ], + "ruHowTo": [ + "Выберите тип градиента (линейный, радиальный или конический) и количество цветовых точек (2–5).", + "Нажмите «Сгенерировать», чтобы создать новый случайный градиент с гармоничными цветами.", + "Скопируйте CSS-код в буфер обмена или нажмите на любой HEX-код цвета, чтобы скопировать его." + ], + "ruWhenTo": [ + "Дизайн фонов сайтов, шапок страниц или UI-оверлеев.", + "Создание яркой графики и баннеров для социальных сетей.", + "Прототипирование интерфейсов приложений с живыми цветовыми переходами.", + "Изучение сочетаний цветов и стилей градиентов для презентаций." + ], + "faq": [ + { "q": "What gradient types are supported?", "a": "Linear (with a random angle), Radial (circular from a random center point), and Conic (sweeping around a center point)." }, + { "q": "How are the colors generated?", "a": "Colors are generated using HSL harmony rules — analogous hue spreads, consistent saturation (50–90%), and balanced lightness (35–65%) — so gradients always look beautiful." }, + { "q": "Can I use the CSS code directly?", "a": "Yes. The generated CSS background value is ready to paste into your stylesheet or inline style attribute." } + ], + "ruFaq": [ + { "q": "Какие типы градиентов поддерживаются?", "a": "Линейный (со случайным углом), радиальный (круговой от случайной центральной точки) и конический (охватывающий вокруг центра)." }, + { "q": "Как генерируются цвета?", "a": "Цвета создаются с помощью правил гармонии HSL — распределение оттенков по аналоговой схеме, насыщенность 50–90% и сбалансированная светлота 35–65%, поэтому градиенты всегда выглядят красиво." }, + { "q": "Можно ли использовать CSS-код напрямую?", "a": "Да. Сгенерированное значение background готово для вставки в таблицу стилей или inline-атрибут style." } + ] +} diff --git a/src/content/generators/hash.json b/src/content/generators/hash.json new file mode 100644 index 0000000..b66c4ff --- /dev/null +++ b/src/content/generators/hash.json @@ -0,0 +1,87 @@ +{ + "slug": "hash", + "title": "Hash Generator", + "description": "Generate MD5, SHA-1, SHA-256, or SHA-512 hashes from any text or random string.", + "icon": "hash", + "status": "live", + "seoTitle": "Hash Generator — MD5, SHA-1, SHA-256, SHA-512 Online | Randify", + "seoDescription": "Free online hash generator. Compute MD5, SHA-1, SHA-256, and SHA-512 hashes from any text or random string. Secure, browser-side processing.", + "ruTitle": "Генератор хешей", + "ruDescription": "Генерируйте хеши MD5, SHA-1, SHA-256 или SHA-512 из любого текста или случайной строки.", + "ruSeoTitle": "Генератор хешей — MD5, SHA-1, SHA-256, SHA-512 онлайн | Randify", + "ruSeoDescription": "Бесплатный онлайн-генератор хешей. Вычисляйте хеши MD5, SHA-1, SHA-256 и SHA-512 из любого текста или случайной строки. Безопасная обработка в браузере.", + "pageTitle": "Hash Generator", + "ruPageTitle": "Генератор хешей", + "howTo": [ + "Choose input mode: type your own text or generate a random string.", + "Select one or more hash algorithms (MD5, SHA-1, SHA-256, SHA-512).", + "Click Generate to compute the hashes instantly in your browser.", + "Use the Compare field to check if a given hash matches any result.", + "Toggle uppercase/lowercase output format as needed." + ], + "whenTo": [ + "Verifying file integrity or data checksums.", + "Testing and debugging authentication or hashing logic.", + "Generating quick hash values for development workflows.", + "Comparing hashes to confirm data matches expected values.", + "Learning about different hash algorithms and their outputs." + ], + "ruHowTo": [ + "Выберите режим ввода: введите свой текст или сгенерируйте случайную строку.", + "Выберите один или несколько алгоритмов хеширования (MD5, SHA-1, SHA-256, SHA-512).", + "Нажмите «Сгенерировать» для мгновенного вычисления хешей в браузере.", + "Используйте поле сравнения, чтобы проверить, совпадает ли заданный хеш с результатом.", + "Переключайте формат вывода: заглавные или строчные буквы." + ], + "ruWhenTo": [ + "Проверка целостности файлов или контрольных сумм данных.", + "Тестирование и отладка логики аутентификации или хеширования.", + "Быстрое генерирование хешей для рабочих процессов разработки.", + "Сравнение хешей для подтверждения совпадения данных с ожидаемыми значениями.", + "Изучение различных алгоритмов хеширования и их результатов." + ], + "faq": [ + { + "q": "What is a hash function?", + "a": "A hash function is a mathematical algorithm that converts input data of any size into a fixed-size string of characters. It is deterministic (same input always produces the same output) and designed to be a one-way function." + }, + { + "q": "Is hashing reversible?", + "a": "No, hashing is a one-way process. You cannot recover the original input from its hash value. This is what makes hashing suitable for password storage and data integrity verification." + }, + { + "q": "Are my inputs sent to a server?", + "a": "No. All hash computations are performed entirely within your browser using the Web Crypto API. Your text never leaves your device, ensuring complete privacy and security." + }, + { + "q": "What is the difference between MD5 and SHA-256?", + "a": "MD5 produces a 128-bit hash and is considered cryptographically broken — suitable for checksums but not security. SHA-256 produces a 256-bit hash and is currently considered secure for cryptographic purposes." + }, + { + "q": "Why would I compare hashes?", + "a": "Hash comparison is commonly used to verify data integrity — for example, confirming a downloaded file matches the publisher's checksum, or verifying that a password matches a stored hash." + } + ], + "ruFaq": [ + { + "q": "Что такое хеш-функция?", + "a": "Хеш-функция — это математический алгоритм, который преобразует входные данные любого размера в строку фиксированной длины. Она детерминирована (одинаковый вход всегда даёт одинаковый выход) и работает только в одну сторону." + }, + { + "q": "Можно ли расшифровать хеш?", + "a": "Нет, хеширование — это односторонний процесс. Невозможно восстановить исходные данные из хеш-значения. Именно поэтому хеширование используется для хранения паролей и проверки целостности данных." + }, + { + "q": "Отправляются ли мои данные на сервер?", + "a": "Нет. Все вычисления хешей выполняются полностью в вашем браузере с помощью Web Crypto API. Ваш текст никогда не покидает устройство, что гарантирует полную конфиденциальность и безопасность." + }, + { + "q": "В чём разница между MD5 и SHA-256?", + "a": "MD5 генерирует 128-битный хеш и считается криптографически взломанным — подходит для контрольных сумм, но не для безопасности. SHA-256 генерирует 256-битный хеш и в настоящее время считается криптографически стойким." + }, + { + "q": "Зачем сравнивать хеши?", + "a": "Сравнение хешей обычно используется для проверки целостности данных — например, чтобы убедиться, что загруженный файл совпадает с контрольной суммой издателя, или чтобы проверить совпадение пароля с сохранённым хешем." + } + ] +} diff --git a/src/content/generators/letter.json b/src/content/generators/letter.json new file mode 100644 index 0000000..0c70188 --- /dev/null +++ b/src/content/generators/letter.json @@ -0,0 +1,47 @@ +{ + "slug": "letter", + "title": "Random Letter", + "description": "Pick a random letter from the alphabet.", + "icon": "type", + "status": "live", + "seoTitle": "Random Letter Generator — A to Z | Randify", + "seoDescription": "Pick a random letter from A to Z instantly. Free online letter randomizer for games, learning, and creative projects.", + "ruTitle": "Случайная буква", + "ruDescription": "Выберите случайную букву из алфавита.", + "ruSeoTitle": "Генератор случайных букв — А до Я | Randify", + "ruSeoDescription": "Выберите случайную букву алфавита мгновенно. Бесплатный рандомайзер букв для игр, обучения и творческих проектов.", + "pageTitle": "Random Letter Generator", + "ruPageTitle": "Генератор случайных букв", + "howTo": [ + "Click Generate to pick a random letter from A to Z.", + "The letter appears in large type for easy reading.", + "Click the letter to copy it to your clipboard.", + "Click again to generate a new random letter." + ], + "whenTo": [ + "Choosing a starting letter for a word game or alphabet challenge.", + "Picking a random category in a trivia or party game.", + "Teaching children letters and the alphabet in a fun way." + ], + "ruHowTo": [ + "Нажмите «Сгенерировать», чтобы выбрать случайную букву от А до Я.", + "Буква отображается крупным шрифтом для удобного чтения.", + "Нажмите на букву, чтобы скопировать её в буфер обмена.", + "Нажмите снова, чтобы сгенерировать новую случайную букву." + ], + "ruWhenTo": [ + "Выбор стартовой буквы для словесной игры или алфавитного челленджа.", + "Случайный выбор категории в викторине или настольной игре.", + "Обучение детей буквам и алфавиту в игровой форме." + ], + "faq": [ + { "q": "Does it include both uppercase and lowercase letters?", "a": "The generator produces uppercase letters by default. Lowercase is not currently supported." }, + { "q": "Can I exclude certain letters?", "a": "Not at the moment. Every letter from A to Z has an equal chance of being selected." }, + { "q": "Is this useful for language learning?", "a": "Yes. Teachers and students use it for alphabet drills, vocabulary games, and pronunciation practice." } + ], + "ruFaq": [ + { "q": "Включает ли генератор заглавные и строчные буквы?", "a": "Генератор выдаёт заглавные буквы по умолчанию. Строчные буквы пока не поддерживаются." }, + { "q": "Можно ли исключить определённые буквы?", "a": "Пока нет. Каждая буква от А до Я имеет равный шанс быть выбранной." }, + { "q": "Подходит ли для изучения языка?", "a": "Да. Учителя и ученики используют генератор для алфавитных упражнений, словарных игр и практики произношения." } + ] +} diff --git a/src/content/generators/list.json b/src/content/generators/list.json new file mode 100644 index 0000000..5d11007 --- /dev/null +++ b/src/content/generators/list.json @@ -0,0 +1,58 @@ +{ + "slug": "list", + "title": "List Picker", + "description": "Paste a list of items and pick random winners.", + "icon": "list", + "status": "live", + "seoTitle": "Random List Picker — Pick a Random Winner | Randify", + "seoDescription": "Paste any list of names or items and pick random winners instantly. Great for giveaways, choosing who goes first, or any random selection.", + "ruTitle": "Список", + "ruDescription": "Вставьте список элементов и выберите случайных победителей.", + "ruSeoTitle": "Случайный выбор из списка | Randify", + "ruSeoDescription": "Вставьте список имён или вариантов и выберите случайных победителей мгновенно. Идеально для розыгрышей и жеребьёвок.", + "pageTitle": "List Picker", + "ruPageTitle": "Случайный выбор из списка", + "howTo": [ + "Type or paste your items into the text area — one item per line.", + "Set how many items to pick in the Pick field.", + "Toggle Allow duplicates if the same item can be chosen more than once.", + "Click Pick (or press Ctrl+Enter) — the selected items appear as chips." + ], + "whenTo": [ + "Choosing a random winner for a giveaway or contest.", + "Picking who presents first in a meeting or class.", + "Organising a Secret Santa or gift exchange draw.", + "Selecting a random movie, restaurant, or activity from a list." + ], + "faq": [ + { "q": "Can the same item be picked more than once?", "a": "Only if you enable the 'Allow duplicates' toggle. By default, each item can be chosen only once per draw." }, + { "q": "Is there a limit to how many items I can add?", "a": "You can add up to 1,000 items. For most use cases — giveaways, raffles, or team assignments — this is more than enough." }, + { "q": "Can I pick multiple winners at once?", "a": "Yes. Set the 'Pick' field to any number up to the total count of items, and Randify will select that many winners in one go." } + ], + "ruFaq": [ + { "q": "Может ли один и тот же элемент выпасть несколько раз?", "a": "Только если включён переключатель «Разрешить повторения». По умолчанию каждый элемент выбирается не более одного раза." }, + { "q": "Есть ли ограничение на количество элементов в списке?", "a": "Можно добавить до 1 000 элементов. Для розыгрышей, жеребьёвок и распределения задач этого более чем достаточно." }, + { "q": "Можно ли выбрать несколько победителей сразу?", "a": "Да. Укажите в поле «Выбрать» любое число до общего количества элементов — Randify выберет нужное количество победителей за один раз." } + ], + "ruHowTo": [ + "Введите список элементов — по одному на строку.", + "Укажите, сколько элементов выбрать.", + "При необходимости разрешите повторения.", + "Нажмите «Выбрать» — результаты появятся на экране." + ], + "ruWhenTo": [ + "Розыгрыш победителей конкурса.", + "Случайное распределение задач между участниками команды.", + "Выбор случайного варианта из списка." + ], + "faq": [ + { "q": "Can the same item be picked more than once?", "a": "Only if you toggle Allow duplicates. By default, each item is picked at most once." }, + { "q": "Is there a limit to how many items I can add?", "a": "You can add up to 500 items, and pick up to the total number of unique items." }, + { "q": "Does the order of items matter?", "a": "No. The selection is completely random regardless of the order you enter items." } + ], + "ruFaq": [ + { "q": "Может ли один элемент выпасть несколько раз?", "a": "Только если включена опция «Разрешить повторения». По умолчанию каждый элемент выбирается не более одного раза." }, + { "q": "Есть ли ограничение на количество элементов?", "a": "Можно добавить до 500 элементов и выбрать до общего числа уникальных элементов." }, + { "q": "Имеет ли значение порядок элементов?", "a": "Нет. Выбор полностью случайный независимо от порядка ввода." } + ] +} diff --git a/src/content/generators/lorem.json b/src/content/generators/lorem.json new file mode 100644 index 0000000..77979f4 --- /dev/null +++ b/src/content/generators/lorem.json @@ -0,0 +1,47 @@ +{ + "slug": "lorem", + "title": "Lorem Ipsum", + "description": "Generate random Lorem Ipsum placeholder text in paragraphs.", + "icon": "type", + "status": "live", + "seoTitle": "Lorem Ipsum Generator — Free Placeholder Text | Randify", + "seoDescription": "Generate random Lorem Ipsum placeholder text instantly. Choose paragraph count and length. Free online dummy text generator for designers and developers.", + "ruTitle": "Lorem Ipsum", + "ruDescription": "Генерируйте случайный текст-заполнитель Lorem Ipsum абзацами.", + "ruSeoTitle": "Генератор Lorem Ipsum — бесплатный текст-заполнитель | Randify", + "ruSeoDescription": "Генерируйте текст-заполнитель Lorem Ipsum мгновенно. Выбирайте количество абзацев и длину. Бесплатный генератор текста для дизайнеров и разработчиков.", + "pageTitle": "Lorem Ipsum Generator", + "ruPageTitle": "Генератор Lorem Ipsum", + "howTo": [ + "Choose the number of paragraphs (1–20) and the desired length: Short, Medium, or Long.", + "Optionally check \"Start with Lorem ipsum...\" to begin the first paragraph with the classic opening.", + "Click Generate to create the text, then use the Copy button to copy it to your clipboard." + ], + "whenTo": [ + "Filling mockups and wireframes with realistic placeholder text.", + "Testing typography, line height, and readability in designs.", + "Creating demo content for websites and applications.", + "Needing neutral Latin text that won't distract from layout." + ], + "ruHowTo": [ + "Выберите количество абзацев (1–20) и желаемую длину: Короткие, Средние или Длинные.", + "При желании отметьте «Начать с Lorem ipsum...», чтобы первый абзац начинался классически.", + "Нажмите «Сгенерировать», а затем «Копировать», чтобы скопировать текст в буфер обмена." + ], + "ruWhenTo": [ + "Заполнение макетов и каркасов реалистичным текстом-заполнителем.", + "Тестирование типографики, межстрочного интервала и читаемости в дизайнах.", + "Создание демонстрационного контента для сайтов и приложений.", + "Необходимость в нейтральном латинском тексте, который не отвлекает от макета." + ], + "faq": [ + { "q": "What is Lorem Ipsum?", "a": "Lorem Ipsum is a classic placeholder text used in publishing and graphic design. It has been the industry's standard dummy text since the 1500s." }, + { "q": "Can I choose how long each paragraph is?", "a": "Yes. You can select Short (2–4 sentences), Medium (4–6 sentences), or Long (6–10 sentences) per paragraph." }, + { "q": "Is the generated text truly random?", "a": "Yes. Each paragraph is built from a shuffled pool of classic Latin words, so every generation produces unique text." } + ], + "ruFaq": [ + { "q": "Что такое Lorem Ipsum?", "a": "Lorem Ipsum — это классический текст-заполнитель, используемый в издательском деле и графическом дизайне. Он является стандартным текстом-рыбой с XVI века." }, + { "q": "Можно ли выбрать длину каждого абзаца?", "a": "Да. Вы можете выбрать Короткие (2–4 предложения), Средние (4–6 предложений) или Длинные (6–10 предложений) абзацы." }, + { "q": "Сгенерированный текст действительно случайный?", "a": "Да. Каждый абзац собирается из перемешанного набора классических латинских слов, поэтому каждая генерация уникальна." } + ] +} diff --git a/src/content/generators/lottery.json b/src/content/generators/lottery.json new file mode 100644 index 0000000..bc63d82 --- /dev/null +++ b/src/content/generators/lottery.json @@ -0,0 +1,48 @@ +{ + "slug": "lottery", + "title": "Lottery", + "description": "Draw a set of unique numbers for lottery-style picks.", + "icon": "ticket", + "status": "live", + "seoTitle": "Lottery Number Generator — Random Pick | Randify", + "seoDescription": "Draw a set of unique random lottery numbers from any range. Perfect for lotteries, raffles, and lucky draws. Free online lottery picker.", + "ruTitle": "Лотерея", + "ruDescription": "Вытащите набор уникальных чисел в стиле лотереи.", + "ruSeoTitle": "Генератор лотерейных номеров | Randify", + "ruSeoDescription": "Тяните уникальные случайные числа для лотереи, розыгрыша или жеребьёвки. Бесплатный онлайн-генератор лотерейных номеров.", + "pageTitle": "Lottery Generator", + "ruPageTitle": "Генератор лотереи", + "howTo": [ + "Set the From and To fields to define your number range (e.g. 1 to 49).", + "Enter how many numbers to pick in the Pick field.", + "Click Draw — a sorted set of unique numbers appears.", + "Click Copy to copy all numbers to the clipboard." + ], + "whenTo": [ + "Generating lottery or lotto ticket numbers.", + "Running a fair raffle draw with unique entries.", + "Selecting a random sample from a numbered list.", + "Choosing lottery numbers for Powerball, EuroMillions, or local draws." + ], + "ruHowTo": [ + "Укажите диапазон чисел в полях «От» и «До».", + "Задайте количество чисел для розыгрыша.", + "Нажмите «Тянуть» — выпавшие числа появятся на экране.", + "Нажмите «Копировать», чтобы сохранить результат." + ], + "ruWhenTo": [ + "Розыгрыш лотерейных номеров.", + "Жеребьёвка участников конкурса.", + "Случайный выбор нескольких уникальных чисел." + ], + "faq": [ + { "q": "Are the numbers guaranteed to be unique?", "a": "Yes. The lottery generator always picks unique numbers within your chosen range." }, + { "q": "Can I use this for real lottery tickets?", "a": "Yes, but remember that no generator can improve your odds — each combination has the same chance." }, + { "q": "What is the maximum range?", "a": "You can set any range where the difference between From and To is at least as large as the number of picks." } + ], + "ruFaq": [ + { "q": "Числа гарантированно уникальны?", "a": "Да. Генератор лотереи всегда выбирает уникальные числа в заданном диапазоне." }, + { "q": "Можно ли использовать для реальных лотерей?", "a": "Да, но помните: генератор не улучшает шансы — каждая комбинация имеет равную вероятность." }, + { "q": "Какой максимальный диапазон?", "a": "Любой диапазон, где разница между «От» и «До» не меньше количества выбираемых чисел." } + ] +} diff --git a/src/content/generators/magic8ball.json b/src/content/generators/magic8ball.json new file mode 100644 index 0000000..65e994d --- /dev/null +++ b/src/content/generators/magic8ball.json @@ -0,0 +1,89 @@ +{ + "slug": "magic8ball", + "title": "Magic 8 Ball", + "description": "Ask the Magic 8 Ball a yes/no question and get a mystical answer. Classic fortune-telling toy with 20 possible responses.", + "icon": "sparkles", + "status": "live", + "seoTitle": "Magic 8 Ball Online — Ask a Question | Randify", + "seoDescription": "Ask the online Magic 8 Ball any yes/no question and get a classic fortune-telling answer. 20 mystical responses — positive, neutral, and negative. Free and fun!", + "ruTitle": "Магический шар 8", + "ruDescription": "Задайте магическому шару вопрос с ответом да/нет и получите мистический ответ. Классическая игрушка-предсказатель с 20 вариантами ответов.", + "ruSeoTitle": "Магический шар 8 онлайн — задай вопрос | Randify", + "ruSeoDescription": "Задайте онлайн-шару вопрос с ответом да/нет и получите классический мистический ответ. 20 вариантов — положительные, нейтральные и отрицательные. Бесплатно и весело!", + "pageTitle": "Magic 8 Ball", + "ruPageTitle": "Магический шар 8", + "howTo": [ + "Type a yes/no question into the input field (optional — just for fun).", + "Click the \"Ask the Ball\" button to shake the Magic 8 Ball.", + "Watch the ball shake and reveal its mystical answer in the blue triangle window.", + "The answer color indicates the type: blue for positive, yellow for neutral, red for negative.", + "Click the copy button to save the answer to your clipboard.", + "Check the history below to see your last 5 questions and answers." + ], + "whenTo": [ + "Having fun with friends at a party or gathering.", + "Making light-hearted decisions when you are feeling indecisive.", + "Adding a mystical element to a game or role-playing session.", + "Breaking the ice with a fun interactive activity.", + "Recreating the nostalgic experience of the classic Magic 8 Ball toy." + ], + "ruHowTo": [ + "Введите вопрос с ответом да/нет в поле (по желанию — просто для развлечения).", + "Нажмите кнопку «Спросить шар», чтобы встряхнуть магический шар.", + "Наблюдайте, как шар встряхивается и раскрывает мистический ответ в синем треугольнике.", + "Цвет ответа указывает на тип: синий — положительный, жёлтый — нейтральный, красный — отрицательный.", + "Нажмите кнопку копирования, чтобы сохранить ответ в буфер обмена.", + "Посмотрите историю ниже, чтобы увидеть ваши последние 5 вопросов и ответов." + ], + "ruWhenTo": [ + "Развлечение с друзьями на вечеринке или собрании.", + "Принятие лёгких решений, когда вы сомневаетесь.", + "Добавление мистического элемента в игру или ролевую сессию.", + "Разрядка обстановки с помощью весёлой интерактивной активности.", + "Воспроизведение ностальгического опыта классической игрушки Магический шар 8." + ], + "faq": [ + { + "q": "How many answers does the Magic 8 Ball have?", + "a": "The Magic 8 Ball has 20 possible answers: 10 positive (blue), 5 neutral (yellow), and 5 negative (red). Each answer is chosen completely at random." + }, + { + "q": "Is the Magic 8 Ball really random?", + "a": "Yes! The answer is selected using a cryptographically secure random number generator, giving each of the 20 responses an equal chance." + }, + { + "q": "Do I need to type a question?", + "a": "No, the question field is purely optional and for fun. The ball does not read or process your question — it simply picks a random answer each time you shake it." + }, + { + "q": "What do the colors mean?", + "a": "Blue means a positive/affirmative answer, yellow means a neutral or uncertain answer, and red means a negative answer. This mirrors the classic Magic 8 Ball experience." + }, + { + "q": "Can I use this for serious decisions?", + "a": "The Magic 8 Ball is intended for entertainment purposes only. For important life decisions, please rely on careful thought and professional advice." + } + ], + "ruFaq": [ + { + "q": "Сколько ответов у магического шара?", + "a": "Магический шар имеет 20 возможных ответов: 10 положительных (синих), 5 нейтральных (жёлтых) и 5 отрицательных (красных). Каждый ответ выбирается полностью случайным образом." + }, + { + "q": "Шар действительно случайный?", + "a": "Да! Ответ выбирается с помощью криптографически стойкого генератора случайных чисел, давая каждому из 20 вариантов равный шанс." + }, + { + "q": "Нужно ли вводить вопрос?", + "a": "Нет, поле вопроса является полностью необязательным и существует для развлечения. Шар не читает и не обрабатывает ваш вопрос — он просто выбирает случайный ответ при каждом встряхивании." + }, + { + "q": "Что означают цвета?", + "a": "Синий означает положительный/утвердительный ответ, жёлтый — нейтральный или неопределённый ответ, красный — отрицательный ответ. Это повторяет классический опыт магического шара." + }, + { + "q": "Можно ли использовать шар для серьёзных решений?", + "a": "Магический шар предназначен только для развлечения. Для важных жизненных решений полагайтесь на внимательное обдумывание и профессиональные консультации." + } + ] +} diff --git a/src/content/generators/meal.json b/src/content/generators/meal.json new file mode 100644 index 0000000..5ae4a57 --- /dev/null +++ b/src/content/generators/meal.json @@ -0,0 +1,57 @@ +{ + "slug": "meal", + "title": "Random Meal Idea", + "description": "Get a random meal or recipe idea when you don't know what to cook.", + "icon": "utensils", + "status": "live", + "seoTitle": "Random Meal Idea Generator — Recipe Inspiration | Randify", + "seoDescription": "Discover random meal and recipe ideas with cuisine, difficulty, time, ingredients, and calories. Filters for type, cuisine, difficulty & cooking time.", + "ruTitle": "Идея для блюда", + "ruDescription": "Получите идею для блюда или рецепта, когда не знаете, что приготовить.", + "ruSeoTitle": "Генератор идей для блюд — вдохновение для рецептов | Randify", + "ruSeoDescription": "Открывайте случайные идеи для блюд и рецептов с кухней, сложностью, временем, ингредиентами и калориями. Фильтры по типу, кухне, сложности и времени приготовления.", + "pageTitle": "Random Meal Idea Generator", + "ruPageTitle": "Генератор идей для блюд", + "howTo": [ + "Use the filters to narrow down by meal type, cuisine, difficulty, or cooking time.", + "Click the Generate button to get a random meal idea matching your filters.", + "View the result card with description, ingredients, calories, and dietary info.", + "Click Copy to save the meal details to your clipboard.", + "Click the star icon to save meals to your favorites for later." + ], + "whenTo": [ + "When you don't know what to cook for breakfast, lunch, or dinner.", + "Meal planning for the week ahead.", + "Looking for new recipe ideas from different cuisines.", + "Challenging yourself to try a new dish or cooking technique.", + "Planning a themed dinner party with friends or family." + ], + "ruHowTo": [ + "Используйте фильтры, чтобы сузить выбор по типу блюда, кухне, сложности или времени приготовления.", + "Нажмите кнопку «Сгенерировать», чтобы получить случайную идею для блюда по вашим фильтрам.", + "Посмотрите карточку результата с описанием, ингредиентами, калориями и информацией о диете.", + "Нажмите «Копировать», чтобы сохранить детали блюда в буфер обмена.", + "Нажмите на иконку звезды, чтобы сохранить блюда в избранное на потом." + ], + "ruWhenTo": [ + "Когда вы не знаете, что приготовить на завтрак, обед или ужин.", + "Планирование питания на неделю вперед.", + "Поиск новых идей рецептов из разных кухонь мира.", + "Бросая себе вызов попробовать новое блюдо или технику приготовления.", + "Организация тематического ужина с друзьями или семьей." + ], + "faq": [ + { "q": "How many meals are in the generator?", "a": "The generator includes over 80 meals across 9 world cuisines, 5 meal types, and 3 difficulty levels. New meals are added regularly." }, + { "q": "Can I filter by dietary restrictions?", "a": "Yes. Meals are tagged with dietary info including Vegetarian, Vegan, Gluten-Free, and Spicy. You can see these badges on each result card." }, + { "q": "How do I save my favorite meals?", "a": "Click the star icon on any result card to save it to your favorites. Your saved meals are stored in your browser's localStorage and persist between visits." }, + { "q": "Can I get a list of ingredients?", "a": "Yes. Each meal result shows 4–6 key ingredients as tags. Click Copy to get the full meal name, description, and ingredient list." }, + { "q": "Are the calorie counts accurate?", "a": "Calorie estimates are approximate and based on standard serving sizes. They are intended as a general guide, not precise nutritional data." } + ], + "ruFaq": [ + { "q": "Сколько блюд в генераторе?", "a": "Генератор включает более 80 блюд из 9 кухонь мира, 5 типов приемов пищи и 3 уровней сложности. Новые блюда добавляются регулярно." }, + { "q": "Можно ли фильтровать по диетическим ограничениям?", "a": "Да. Блюда помечены диетической информацией, включая вегетарианские, веганские, безглютеновые и острые. Эти значки видны на каждой карточке результата." }, + { "q": "Как сохранить избранные блюда?", "a": "Нажмите на иконку звезды на любой карточке результата, чтобы сохранить ее в избранное. Сохраненные блюда хранятся в localStorage вашего браузера и сохраняются между визитами." }, + { "q": "Можно ли получить список ингредиентов?", "a": "Да. Каждый результат показывает 4–6 ключевых ингредиентов в виде тегов. Нажмите «Копировать», чтобы получить полное название блюда, описание и список ингредиентов." }, + { "q": "Насколько точны данные о калориях?", "a": "Оценки калорийности приблизительны и основаны на стандартных порциях. Они предназначены для общего ориентира, а не для точных диетических расчетов." } + ] +} diff --git a/src/content/generators/names.json b/src/content/generators/names.json new file mode 100644 index 0000000..2b6d23c --- /dev/null +++ b/src/content/generators/names.json @@ -0,0 +1,47 @@ +{ + "slug": "names", + "title": "Random Name", + "description": "Generate a random first name by gender or any category.", + "icon": "user", + "status": "live", + "seoTitle": "Random Name Generator — First Names | Randify", + "seoDescription": "Generate random first names instantly. Pick male, female, or any names for characters, babies, games, and more.", + "ruTitle": "Случайное имя", + "ruDescription": "Сгенерируйте случайное имя по полу или из общего списка.", + "ruSeoTitle": "Генератор случайных имён | Randify", + "ruSeoDescription": "Генератор случайных имён: мужские, женские, любые. Для персонажей, игр, вдохновения и не только.", + "pageTitle": "Random Name Generator", + "ruPageTitle": "Генератор случайных имён", + "howTo": [ + "Select a category: Male, Female, or Any.", + "Click Generate to produce a random name.", + "The name appears instantly on the screen.", + "Click the name to copy it to your clipboard." + ], + "whenTo": [ + "Naming a character in a story, game, or RPG campaign.", + "Looking for baby name inspiration.", + "Generating placeholder names for mockups or test data." + ], + "ruHowTo": [ + "Выберите категорию: Мужские, Женские или Любые.", + "Нажмите «Сгенерировать», чтобы получить случайное имя.", + "Имя появится на экране мгновенно.", + "Нажмите на имя, чтобы скопировать его в буфер обмена." + ], + "ruWhenTo": [ + "Придумывание имени персонажа для рассказа, игры или НРИ.", + "Поиск вдохновения для имени ребёнка.", + "Генерация тестовых данных с реалистичными именами." + ], + "faq": [ + { "q": "How many names are in the database?", "a": "Hundreds of popular first names are included, covering common choices in English and Russian." }, + { "q": "Can I generate multiple names at once?", "a": "Currently, one name is shown per click. You can click Generate as many times as you like." }, + { "q": "Are the names culturally specific?", "a": "The list includes widely used names. Some may be more common in certain regions, but all are familiar internationally." } + ], + "ruFaq": [ + { "q": "Сколько имён в базе?", "a": "В базе сотни популярных имён, включая распространённые варианты на английском и русском языках." }, + { "q": "Можно ли сгенерировать несколько имён сразу?", "a": "Сейчас показывается одно имя за раз. Вы можете нажимать «Сгенерировать» столько раз, сколько нужно." }, + { "q": "Имена привязаны к конкретной культуре?", "a": "В списке широко распространённые имена. Некоторые чаще встречаются в определённых регионах, но все они международно узнаваемы." } + ] +} diff --git a/src/content/generators/numbers.json b/src/content/generators/numbers.json new file mode 100644 index 0000000..310b4cc --- /dev/null +++ b/src/content/generators/numbers.json @@ -0,0 +1,51 @@ +{ + "slug": "numbers", + "title": "Number", + "description": "Pick a random number within any range you choose.", + "icon": "hash", + "status": "live", + "seoTitle": "Random Number Generator | Randify", + "seoDescription": "Generate a random number between any two values instantly. Free online random number picker — perfect for giveaways, games, and decisions.", + "ruTitle": "Число", + "ruDescription": "Выберите случайное число в любом диапазоне.", + "ruSeoTitle": "Генератор случайных чисел | Randify", + "ruSeoDescription": "Генерируйте случайное число в любом диапазоне мгновенно. Идеально для розыгрышей, игр и принятия решений.", + "pageTitle": "Number Generator", + "ruPageTitle": "Генератор чисел", + "howTo": [ + "Enter the minimum value in the From field.", + "Enter the maximum value in the To field.", + "Click Generate — a random number within your range appears instantly.", + "Click the number to copy it to the clipboard." + ], + "whenTo": [ + "Picking a winner in a giveaway or raffle.", + "Choosing a random starting player in a board game.", + "Making a quick decision between numbered options.", + "Generating test data with values in a specific range.", + "Assigning random IDs or order numbers to items." + ], + "ruHowTo": [ + "Введите минимальное значение в поле «От».", + "Введите максимальное значение в поле «До».", + "Нажмите «Сгенерировать» — случайное число появится мгновенно.", + "Нажмите на число, чтобы скопировать его в буфер обмена." + ], + "ruWhenTo": [ + "Выбор победителя в розыгрыше или конкурсе.", + "Определение случайного первого игрока в настольной игре.", + "Быстрое принятие решения между пронумерованными вариантами.", + "Генерация тестовых данных с числами в заданном диапазоне.", + "Назначение случайных номеров или порядковых идентификаторов." + ], + "faq": [ + { "q": "Is the number truly random?", "a": "Yes. Randify uses your browser's built-in random function, which produces unpredictable results every time." }, + { "q": "Can I generate negative numbers?", "a": "Absolutely. Just enter a negative value in the From field and a positive or negative value in the To field." }, + { "q": "Is there a limit to the range?", "a": "You can use any whole numbers that JavaScript supports — safely up to about 9 quadrillion." } + ], + "ruFaq": [ + { "q": "Число действительно случайное?", "a": "Да. Randify использует встроенную функцию случайности браузера, которая каждый раз выдаёт непредсказуемый результат." }, + { "q": "Можно ли генерировать отрицательные числа?", "a": "Конечно. Введите отрицательное значение в поле «От» и любое значение в поле «До»." }, + { "q": "Есть ли ограничение на диапазон?", "a": "Можно использовать любые целые числа, которые поддерживает JavaScript — безопасно до примерно 9 квадриллионов." } + ] +} diff --git a/src/content/generators/palette.json b/src/content/generators/palette.json new file mode 100644 index 0000000..12da908 --- /dev/null +++ b/src/content/generators/palette.json @@ -0,0 +1,47 @@ +{ + "slug": "palette", + "title": "Color Palette", + "description": "Generate harmonious color palettes with one click.", + "icon": "palette", + "status": "live", + "seoTitle": "Color Palette Generator — Random & Harmonic Palettes | Randify", + "seoDescription": "Generate beautiful color palettes instantly. Choose from Random, Analogous, Complementary, Triadic, and Monochromatic harmony modes. Free online tool for designers.", + "ruTitle": "Палитра цветов", + "ruDescription": "Генерируйте гармоничные палитры цветов одним нажатием.", + "ruSeoTitle": "Генератор палитры цветов — случайные и гармоничные палитры | Randify", + "ruSeoDescription": "Создавайте красивые палитры цветов мгновенно. Выбирайте режимы гармонии: случайный, аналоговый, комплементарный, триадный, монохромный. Бесплатный онлайн-инструмент для дизайнеров.", + "pageTitle": "Color Palette Generator", + "ruPageTitle": "Генератор палитры цветов", + "howTo": [ + "Select the number of colors and a harmony mode.", + "Click Generate to create a new palette.", + "Click any color swatch to copy its HEX code, or use Copy All to copy the entire palette." + ], + "whenTo": [ + "Designing a brand identity or logo color scheme.", + "Choosing colors for a website or app UI.", + "Creating illustrations, presentations, or social media graphics.", + "Exploring color theory and harmony relationships." + ], + "ruHowTo": [ + "Выберите количество цветов и режим гармонии.", + "Нажмите «Сгенерировать», чтобы создать новую палитру.", + "Нажмите на любой цвет, чтобы скопировать его HEX-код, или используйте «Копировать всё»." + ], + "ruWhenTo": [ + "Разработка фирменного стиля или цветовой схемы логотипа.", + "Выбор цветов для дизайна сайта или приложения.", + "Создание иллюстраций, презентаций или графики для соцсетей.", + "Изучение теории цвета и цветовых гармоний." + ], + "faq": [ + { "q": "What harmony modes are available?", "a": "Random, Analogous, Complementary, Triadic, and Monochromatic. Each mode creates colors based on a different relationship on the color wheel." }, + { "q": "Can I copy the whole palette at once?", "a": "Yes. Click the Copy All button to copy all HEX codes separated by commas to your clipboard." }, + { "q": "How many colors can I generate?", "a": "You can generate palettes from 3 to 8 colors using the count selector." } + ], + "ruFaq": [ + { "q": "Какие режимы гармонии доступны?", "a": "Случайный, аналоговый, комплементарный, триадный и монохромный. Каждый режим создаёт цвета на основе разных отношений на цветовом круге." }, + { "q": "Можно ли скопировать всю палитру сразу?", "a": "Да. Нажмите кнопку «Копировать всё», чтобы скопировать все HEX-коды через запятую в буфер обмена." }, + { "q": "Сколько цветов можно сгенерировать?", "a": "Вы можете создавать палитры от 3 до 8 цветов с помощью селектора количества." } + ] +} diff --git a/src/content/generators/password.json b/src/content/generators/password.json new file mode 100644 index 0000000..fbda23f --- /dev/null +++ b/src/content/generators/password.json @@ -0,0 +1,50 @@ +{ + "slug": "password", + "title": "Password", + "description": "Create a strong, random password with custom rules.", + "icon": "lock", + "status": "live", + "seoTitle": "Random Password Generator | Randify", + "seoDescription": "Create a strong random password with custom length, uppercase, lowercase, digits and symbols. Free and secure — runs entirely in your browser.", + "ruTitle": "Пароль", + "ruDescription": "Создайте надёжный случайный пароль с настраиваемыми правилами.", + "ruSeoTitle": "Генератор случайных паролей | Randify", + "ruSeoDescription": "Создавайте надёжные пароли с заглавными буквами, цифрами и символами. Бесплатно и безопасно — работает прямо в браузере.", + "pageTitle": "Password Generator", + "ruPageTitle": "Генератор паролей", + "howTo": [ + "Drag the length slider to set how many characters you need (4–64).", + "Toggle uppercase, lowercase, digits, and symbols on or off.", + "Click Generate — a new password appears instantly.", + "Click the password to copy it to your clipboard." + ], + "whenTo": [ + "Creating a strong password for a new account.", + "Generating a temporary access credential.", + "Producing a random secret key for development or testing.", + "Replacing a weak or reused password.", + "Setting up a secure Wi-Fi passphrase." + ], + "ruHowTo": [ + "Задайте длину пароля с помощью ползунка.", + "Выберите типы символов: заглавные, строчные, цифры, спецсимволы.", + "Нажмите «Сгенерировать» — готовый пароль появится на экране.", + "Нажмите на пароль, чтобы скопировать его." + ], + "ruWhenTo": [ + "Создание надёжного пароля для нового аккаунта.", + "Генерация секретного ключа или токена.", + "Регулярное обновление паролей для повышения безопасности.", + "Создание сложного пароля для Wi-Fi сети." + ], + "faq": [ + { "q": "Are the passwords stored anywhere?", "a": "No. Passwords are generated locally in your browser and never leave your device." }, + { "q": "How strong are the generated passwords?", "a": "Strength depends on length and character types. A 16-character password with all types enabled is considered very strong." }, + { "q": "Can I use these passwords for banking?", "a": "Yes, but always follow your bank's specific requirements and consider using a dedicated password manager." } + ], + "ruFaq": [ + { "q": "Пароли где-то сохраняются?", "a": "Нет. Пароли генерируются локально в браузере и никогда не покидают ваше устройство." }, + { "q": "Насколько надёжны сгенерированные пароли?", "a": "Надёжность зависит от длины и наборов символов. Пароль из 16 символов со всеми типами считается очень сильным." }, + { "q": "Можно ли использовать эти пароли для банкинга?", "a": "Да, но всегда учитывайте требования вашего банка и рассмотрите использование специального менеджера паролей." } + ] +} diff --git a/src/content/generators/rps.json b/src/content/generators/rps.json new file mode 100644 index 0000000..2a5b3ee --- /dev/null +++ b/src/content/generators/rps.json @@ -0,0 +1,47 @@ +{ + "slug": "rps", + "title": "Rock Paper Scissors", + "description": "Play Rock Paper Scissors against a random computer opponent.", + "icon": "hand", + "status": "live", + "seoTitle": "Rock Paper Scissors — Play Online | Randify", + "seoDescription": "Play Rock Paper Scissors online against a random computer opponent. Free, instant, and fair — no account needed.", + "ruTitle": "Камень Ножницы Бумага", + "ruDescription": "Сыграйте в Камень Ножницы Бумага против случайного компьютерного соперника.", + "ruSeoTitle": "Камень Ножницы Бумага — играть онлайн | Randify", + "ruSeoDescription": "Играйте в Камень Ножницы Бумага онлайн против компьютера. Бесплатно, мгновенно и честно — без регистрации.", + "pageTitle": "Rock Paper Scissors", + "ruPageTitle": "Камень Ножницы Бумага", + "howTo": [ + "Choose your move: Rock, Paper, or Scissors.", + "Click Play — the computer makes its random choice.", + "The result is shown instantly: win, lose, or draw.", + "Play again as many times as you like." + ], + "whenTo": [ + "Making a quick decision with a friend when neither wants to choose.", + "Killing time with a simple, familiar game.", + "Settling a tie or breaking a deadlock in any activity." + ], + "ruHowTo": [ + "Выберите свой ход: Камень, Ножницы или Бумага.", + "Нажмите «Играть» — компьютер делает случайный выбор.", + "Результат отображается мгновенно: победа, поражение или ничья.", + "Играйте снова сколько угодно раз." + ], + "ruWhenTo": [ + "Быстрое решение спора с другом, когда никто не хочет выбирать первым.", + "Коротание времени в простой и знакомой игре.", + "Разрешение ничьей или выход из тупика в любой активности." + ], + "faq": [ + { "q": "Is the computer choice truly random?", "a": "Yes. The computer's move is generated using a cryptographically secure random number generator, making every round fair and unpredictable." }, + { "q": "Can I play multiple rounds and track my score?", "a": "Currently, each round is independent. A score tracker may be added in a future update." }, + { "q": "Does the computer learn from my moves?", "a": "No. Each move is completely independent and random. The computer does not adapt or remember your previous choices." } + ], + "ruFaq": [ + { "q": "Выбор компьютера действительно случайный?", "a": "Да. Ход компьютера генерируется с помощью криптографически стойкого генератора случайных чисел, делая каждый раунд честным и непредсказуемым." }, + { "q": "Можно ли играть несколько раундов и вести счёт?", "a": "Сейчас каждый раунд независим. Счётчик побед может появиться в будущем обновлении." }, + { "q": "Учится ли компьютер на моих ходах?", "a": "Нет. Каждый ход полностью независимый и случайный. Компьютер не адаптируется и не запоминает ваши предыдущие выборы." } + ] +} diff --git a/src/content/generators/shuffler.json b/src/content/generators/shuffler.json new file mode 100644 index 0000000..8c435b7 --- /dev/null +++ b/src/content/generators/shuffler.json @@ -0,0 +1,67 @@ +{ + "slug": "shuffler", + "title": "Sequence Shuffler", + "description": "Shuffle any list of items into a random order instantly.", + "icon": "shuffle", + "status": "live", + "seoTitle": "Sequence Shuffler — Randomize Any List | Randify", + "seoDescription": "Paste a list of items and shuffle them into a completely random order. Perfect for randomizing names, tasks, playlist order, or any sequence.", + "ruTitle": "Перемешиватель", + "ruDescription": "Мгновенно перемешайте любой список элементов в случайном порядке.", + "ruSeoTitle": "Перемешиватель списка — случайный порядок | Randify", + "ruSeoDescription": "Вставьте список элементов и перемешайте их в случайном порядке. Идеально для случайного распределения имён, задач, порядка воспроизведения или любой последовательности.", + "pageTitle": "Sequence Shuffler", + "ruPageTitle": "Перемешиватель списка", + "howTo": [ + "Type or paste your items into the text area — one item per line.", + "Click Shuffle to randomize the order of all items.", + "The shuffled list appears as a numbered sequence.", + "Click Copy to copy the shuffled list back to your clipboard." + ], + "whenTo": [ + "Randomizing the order of presentation slides or talking points.", + "Shuffling a playlist, reading list, or queue of items.", + "Creating a random task order for a team or group activity.", + "Fairly deciding the speaking order in meetings or classes." + ], + "faq": [ + { + "q": "Is the shuffle truly random?", + "a": "Yes. We use the Fisher-Yates shuffle algorithm powered by the browser's cryptographically secure random number generator, giving every item an equal chance to end up in any position." + }, + { + "q": "Is there a limit to how many items I can shuffle?", + "a": "You can shuffle up to 1,000 items at once. For most everyday use cases this is more than enough." + }, + { + "q": "Can I shuffle the same list multiple times?", + "a": "Absolutely. Click Shuffle as many times as you like — each time you'll get a completely new random order." + } + ], + "ruHowTo": [ + "Введите или вставьте элементы в текстовое поле — по одному на строку.", + "Нажмите «Перемешать», чтобы случайным образом изменить порядок всех элементов.", + "Перемешанный список отобразится в виде пронумерованной последовательности.", + "Нажмите «Копировать», чтобы скопировать перемешанный список в буфер обмена." + ], + "ruWhenTo": [ + "Случайный порядок слайдов или тем для обсуждения.", + "Перемешивание плейлиста, списка для чтения или очереди.", + "Создание случайного порядка задач для команды или групповой активности.", + "Честное определение порядка выступлений на собраниях или в классе." + ], + "ruFaq": [ + { + "q": "Перемешивание действительно случайное?", + "a": "Да. Мы используем алгоритм Фишера-Йетса на основе криптографически стойкого генератора случайных чисел браузера, что даёт каждому элементу равные шансы оказаться на любой позиции." + }, + { + "q": "Есть ли ограничение на количество элементов?", + "a": "Можно перемешать до 1 000 элементов за раз. Для большинства задач этого более чем достаточно." + }, + { + "q": "Можно ли перемешать один и тот же список несколько раз?", + "a": "Конечно. Нажимайте «Перемешать» столько раз, сколько хотите — каждый раз вы получите совершенно новый случайный порядок." + } + ] +} diff --git a/src/content/generators/teams.json b/src/content/generators/teams.json new file mode 100644 index 0000000..9b629bf --- /dev/null +++ b/src/content/generators/teams.json @@ -0,0 +1,47 @@ +{ + "slug": "teams", + "title": "Team Splitter", + "description": "Split a list of people into random teams or groups.", + "icon": "users", + "status": "live", + "seoTitle": "Random Team Generator — Split into Groups | Randify", + "seoDescription": "Split any list of names into random teams instantly. Perfect for sports, workshops, classrooms, and party games.", + "ruTitle": "Разделение на команды", + "ruDescription": "Разделите список людей на случайные команды или группы.", + "ruSeoTitle": "Генератор случайных команд — разделение на группы | Randify", + "ruSeoDescription": "Разделите любой список имён на случайные команды мгновенно. Для спорта, занятий, воркшопов и вечеринок.", + "pageTitle": "Random Team Generator", + "ruPageTitle": "Генератор случайных команд", + "howTo": [ + "Enter the list of participants — one name per line.", + "Set the number of teams you want to create.", + "Click Generate — the tool shuffles and splits the list evenly.", + "Review the teams and copy or share the results." + ], + "whenTo": [ + "Organising random teams for a sports match or tournament.", + "Splitting a class or workshop into balanced groups.", + "Creating teams for a party game or team-building activity." + ], + "ruHowTo": [ + "Введите список участников — по одному имени на строку.", + "Укажите количество команд, которые нужно создать.", + "Нажмите «Сгенерировать» — инструмент перемешает и равномерно разделит список.", + "Просмотрите команды и скопируйте или поделитесь результатами." + ], + "ruWhenTo": [ + "Формирование случайных команд для спортивной игры или турнира.", + "Разделение класса или воркшопа на сбалансированные группы.", + "Создание команд для игры на вечеринке или тимбилдинга." + ], + "faq": [ + { "q": "What happens if the list cannot be divided evenly?", "a": "Teams are created as evenly as possible. If there is a remainder, some teams will have one extra member." }, + { "q": "Is there a limit to the number of participants?", "a": "You can add up to 500 names. For most events and activities, this limit is more than sufficient." }, + { "q": "Can I use this for non-name lists?", "a": "Yes. You can split any text items — tasks, topics, or objects — into random groups." } + ], + "ruFaq": [ + { "q": "Что если список нельзя разделить поровну?", "a": "Команды формируются максимально равномерно. Если остаётся остаток, некоторые команды будут на одного участника больше." }, + { "q": "Есть ли ограничение на количество участников?", "a": "Можно добавить до 500 имён. Для большинства мероприятий и активностей этого более чем достаточно." }, + { "q": "Можно ли использовать не только для имён?", "a": "Да. Вы можете разделить на группы любые текстовые элементы — задачи, темы или предметы." } + ] +} diff --git a/src/content/generators/time.json b/src/content/generators/time.json new file mode 100644 index 0000000..ade4b34 --- /dev/null +++ b/src/content/generators/time.json @@ -0,0 +1,53 @@ +{ + "slug": "time", + "title": "Random Time", + "description": "Generate a random time of day with a beautiful analog clock display.", + "icon": "clock", + "status": "live", + "seoTitle": "Random Time Generator — with Analog Clock | Randify", + "seoDescription": "Generate a random time of day between any hours. Choose 12h or 24h format, set intervals, and see a beautiful analog clock display. Free online tool.", + "ruTitle": "Случайное время", + "ruDescription": "Генерируйте случайное время суток с красивым аналоговым циферблатом.", + "ruSeoTitle": "Генератор случайного времени — с аналоговыми часами | Randify", + "ruSeoDescription": "Генерируйте случайное время суток в любом диапазоне. Выбирайте 12-часовой или 24-часовой формат, задавайте интервалы и смотрите на аналоговый циферблат. Бесплатный онлайн-инструмент.", + "pageTitle": "Random Time Generator", + "ruPageTitle": "Генератор случайного времени", + "howTo": [ + "Set the start time in the From field and the end time in the To field.", + "Choose the time interval (every minute, 5 min, 15 min, 30 min, or hourly).", + "Toggle between 12-hour and 24-hour format as needed.", + "Click Generate — a random time appears with an analog clock display.", + "Click the time to copy it to your clipboard." + ], + "whenTo": [ + "Picking a random meeting time or schedule slot.", + "Creating random scenarios for creative writing or games.", + "Generating a random alarm or reminder time for a challenge.", + "Simulating realistic timestamps for testing or prototyping." + ], + "ruHowTo": [ + "Задайте начальное время в поле «От» и конечное время в поле «До».", + "Выберите интервал времени (каждую минуту, 5 мин, 15 мин, 30 мин или каждый час).", + "Переключайтесь между 12-часовым и 24-часовым форматом при необходимости.", + "Нажмите «Сгенерировать» — случайное время появится с аналоговым циферблатом.", + "Нажмите на время, чтобы скопировать его в буфер обмена." + ], + "ruWhenTo": [ + "Выбор случайного времени для встречи или расписания.", + "Создание случайных сценариев для творческого письма или игр.", + "Генерация случайного времени будильника или напоминания для челленджа.", + "Имитация реалистичных временных меток для тестирования или прототипирования." + ], + "faq": [ + { "q": "Can I switch between 12-hour and 24-hour format?", "a": "Yes. Use the format toggle to switch between 12-hour (AM/PM) and 24-hour display at any time." }, + { "q": "What time intervals are available?", "a": "You can choose between every minute, every 5 minutes, every 15 minutes, every 30 minutes, or hourly intervals." }, + { "q": "Can I set any time range?", "a": "Yes. Set any From and To time, as long as From is earlier than To. The generator will pick a random time within that range." }, + { "q": "Is the generated time truly random?", "a": "Yes. Times are generated using cryptographically secure random numbers, giving every valid time in your range an equal chance." } + ], + "ruFaq": [ + { "q": "Можно ли переключаться между 12-часовым и 24-часовым форматом?", "a": "Да. Используйте переключатель формата для выбора между 12-часовым (AM/PM) и 24-часовым отображением в любое время." }, + { "q": "Какие интервалы времени доступны?", "a": "Вы можете выбрать интервалы: каждую минуту, каждые 5 минут, каждые 15 минут, каждые 30 минут или каждый час." }, + { "q": "Можно ли задать любой диапазон времени?", "a": "Да. Задайте любое время «От» и «До», главное чтобы «От» было раньше «До». Генератор выберет случайное время в этом диапазоне." }, + { "q": "Сгенерированное время действительно случайное?", "a": "Да. Время генерируется с помощью криптографически стойких случайных чисел, давая каждому допустимому времени в диапазоне равный шанс." } + ] +} diff --git a/src/content/generators/uuid.json b/src/content/generators/uuid.json new file mode 100644 index 0000000..c6ee06b --- /dev/null +++ b/src/content/generators/uuid.json @@ -0,0 +1,48 @@ +{ + "slug": "uuid", + "title": "UUID / Token", + "description": "Generate UUIDs, hex tokens, or random base64 strings.", + "icon": "fingerprint", + "status": "live", + "seoTitle": "UUID Generator — UUID v4, Hex & Base64 Tokens | Randify", + "seoDescription": "Generate random UUID v4, hex tokens, or base64 strings online. Cryptographically secure, runs in your browser. Free UUID and token generator.", + "ruTitle": "UUID / Токен", + "ruDescription": "Генерируйте UUID, hex-токены или случайные Base64-строки.", + "ruSeoTitle": "Генератор UUID и токенов | Randify", + "ruSeoDescription": "Генерируйте UUID v4, hex-токены и Base64-строки онлайн. Криптографически безопасно, работает в браузере.", + "pageTitle": "UUID / Token", + "ruPageTitle": "Генератор UUID и токенов", + "howTo": [ + "Choose a token type: UUID v4, Hex, or Base64.", + "For Hex and Base64, set the byte length (4–64 bytes).", + "Enter how many tokens to generate (1–20).", + "Click Generate, then Copy all to copy every token to the clipboard." + ], + "whenTo": [ + "Generating unique IDs for database records or API resources.", + "Creating random session tokens or API keys during development.", + "Producing test data with realistic-looking identifiers.", + "Generating a one-time secret for environment variables or config files." + ], + "ruHowTo": [ + "Выберите тип: UUID v4, Hex или Base64.", + "Для Hex и Base64 задайте длину в байтах.", + "Укажите количество токенов.", + "Нажмите «Сгенерировать» и скопируйте результат." + ], + "ruWhenTo": [ + "Генерация уникальных идентификаторов для баз данных.", + "Создание токенов для API и сессий.", + "Генерация случайных ключей шифрования." + ], + "faq": [ + { "q": "Are these UUIDs cryptographically secure?", "a": "Yes. UUID v4 is generated using the browser's crypto.getRandomValues, which is cryptographically secure." }, + { "q": "What is the difference between UUID, Hex, and Base64?", "a": "UUID v4 is a 128-bit standard format. Hex is raw random bytes in hexadecimal. Base64 is the same bytes encoded for compactness." }, + { "q": "Can I use these tokens in production?", "a": "UUID v4 tokens are safe for production. For Hex and Base64, the security depends on the byte length you choose." } + ], + "ruFaq": [ + { "q": "UUID криптографически безопасны?", "a": "Да. UUID v4 генерируется через crypto.getRandomValues браузера, что обеспечивает криптографическую стойкость." }, + { "q": "В чём разница между UUID, Hex и Base64?", "a": "UUID v4 — стандартный 128-битный формат. Hex — сырые случайные байты в шестнадцатеричном виде. Base64 — те же байты в компактном кодировании." }, + { "q": "Можно ли использовать токены в продакшене?", "a": "UUID v4 безопасны для продакшена. Для Hex и Base64 безопасность зависит от выбранной длины в байтах." } + ] +} diff --git a/src/content/generators/weighted.json b/src/content/generators/weighted.json new file mode 100644 index 0000000..38e00de --- /dev/null +++ b/src/content/generators/weighted.json @@ -0,0 +1,47 @@ +{ + "slug": "weighted", + "title": "Weighted Random", + "description": "Pick a random item from a list using custom weights for each option.", + "icon": "list", + "status": "live", + "seoTitle": "Weighted Random Generator — Weighted Choice Picker | Randify", + "seoDescription": "Pick random items with custom weights. Perfect for raffles, weighted decisions, and probability experiments. Free online weighted random selector.", + "ruTitle": "Взвешенный случайный выбор", + "ruDescription": "Выбирайте случайный элемент из списка с учётом заданных весов для каждого варианта.", + "ruSeoTitle": "Генератор взвешенного случайного выбора | Randify", + "ruSeoDescription": "Выбирайте случайные элементы с заданными весами. Идеально для розыгрышей, взвешенных решений и экспериментов с вероятностями.", + "pageTitle": "Weighted Random Generator", + "ruPageTitle": "Генератор взвешенного случайного выбора", + "howTo": [ + "Add items to the list and assign a weight to each one. Higher weight means higher chance of being picked.", + "Click Pick to select one random item based on the weights.", + "Use Pick N times to run multiple trials and see the distribution statistics." + ], + "whenTo": [ + "Running a raffle where some participants have more entries than others.", + "Making weighted decisions when options have different priorities.", + "Simulating probability distributions or testing randomness.", + "Creating balanced teams or assignments with different skill weights." + ], + "ruHowTo": [ + "Добавьте элементы в список и назначьте каждому вес. Чем выше вес, тем больше шанс быть выбранным.", + "Нажмите «Выбрать», чтобы случайно выбрать один элемент с учётом весов.", + "Используйте «Выбрать N раз» для множественных испытаний и просмотра статистики распределения." + ], + "ruWhenTo": [ + "Проведение розыгрыша, где у некоторых участников больше шансов, чем у других.", + "Принятие взвешенных решений, когда у вариантов разные приоритеты.", + "Моделирование распределений вероятностей или тестирование случайности.", + "Создание сбалансированных команд или назначений с разными весами навыков." + ], + "faq": [ + { "q": "How does weighted random selection work?", "a": "Each item's chance of being picked is proportional to its weight. If item A has weight 2 and item B has weight 8, B is 4 times more likely to be selected than A." }, + { "q": "Can I use decimal weights?", "a": "Yes, you can use any positive number as a weight, including decimals. The generator normalizes all weights automatically." }, + { "q": "Is the result truly random?", "a": "Yes. The generator uses a cryptographically secure random number generator to ensure fair and unpredictable results every time." } + ], + "ruFaq": [ + { "q": "Как работает взвешенный случайный выбор?", "a": "Шанс выбора каждого элемента пропорционален его весу. Если у элемента А вес 2, а у элемента Б вес 8, то Б в 4 раза чаще будет выбран, чем А." }, + { "q": "Можно ли использовать дробные веса?", "a": "Да, весом может быть любое положительное число, включая десятичные дроби. Генератор автоматически нормализует все веса." }, + { "q": "Результат действительно случайный?", "a": "Да. Генератор использует криптографически стойкий генератор случайных чисел, чтобы гарантировать честный и непредсказуемый результат каждый раз." } + ] +} diff --git a/src/content/generators/wheel.json b/src/content/generators/wheel.json new file mode 100644 index 0000000..61fcbef --- /dev/null +++ b/src/content/generators/wheel.json @@ -0,0 +1,48 @@ +{ + "slug": "wheel", + "title": "Spin the Wheel", + "description": "Add your items, spin the wheel, and let chance decide.", + "icon": "pie-chart", + "status": "live", + "seoTitle": "Spin the Wheel — Random Picker | Randify", + "seoDescription": "Spin a customizable wheel of fortune to pick a random winner, choice, or option. Add your own items and let the wheel decide.", + "ruTitle": "Колесо фортуны", + "ruDescription": "Добавьте элементы, крутите колесо и пусть случай решит.", + "ruSeoTitle": "Колесо фортуны онлайн — случайный выбор | Randify", + "ruSeoDescription": "Крутите колесо фортуны онлайн. Добавьте свои варианты и пусть случай решит победителя.", + "pageTitle": "Spin the Wheel", + "ruPageTitle": "Колесо фортуны онлайн", + "howTo": [ + "Type your items into the text box — one per line.", + "Click \"Update wheel\" to apply changes.", + "Hit \"Spin\" and watch the wheel decide.", + "Copy the result or spin again." + ], + "whenTo": [ + "Picking a winner for a giveaway or raffle.", + "Deciding whose turn it is in a game or meeting.", + "Choosing a random activity, movie, or restaurant.", + "Any decision you want to leave to chance." + ], + "ruHowTo": [ + "Введите варианты в поле — по одному на строку (от 2 до 24).", + "Колесо обновляется автоматически.", + "Нажмите «Крутить» и дождитесь результата.", + "Победитель выделяется — скопируйте его кнопкой «Копировать»." + ], + "ruWhenTo": [ + "Случайный выбор победителя розыгрыша.", + "Принятие решений в команде.", + "Распределение ролей или задач между участниками." + ], + "faq": [ + { "q": "How many items can I add?", "a": "Between 2 and 24 items. The wheel automatically resizes to fit all segments." }, + { "q": "Is the spin truly random?", "a": "Yes. The stopping position is determined by a cryptographically secure random value." }, + { "q": "Can I save my wheel?", "a": "Not yet. You can bookmark the page or copy your item list to reuse it later." } + ], + "ruFaq": [ + { "q": "Сколько элементов можно добавить?", "a": "От 2 до 24. Колесо автоматически подстраивается под количество сегментов." }, + { "q": "Вращение действительно случайное?", "a": "Да. Конечная позиция определяется криптографически стойким случайным значением." }, + { "q": "Можно ли сохранить колесо?", "a": "Пока нет. Можно добавить страницу в закладки или скопировать список элементов для повторного использования." } + ] +} diff --git a/src/content/generators/yesno.json b/src/content/generators/yesno.json new file mode 100644 index 0000000..a1735dd --- /dev/null +++ b/src/content/generators/yesno.json @@ -0,0 +1,47 @@ +{ + "slug": "yesno", + "title": "Yes or No", + "description": "Get a random Yes, No, or Maybe answer to any question.", + "icon": "help-circle", + "status": "live", + "seoTitle": "Yes or No Generator — Random Answer | Randify", + "seoDescription": "Get an instant random Yes, No, or Maybe answer to any question. Free online decision maker when you need a quick verdict.", + "ruTitle": "Да или Нет", + "ruDescription": "Получите случайный ответ Да, Нет или Возможно на любой вопрос.", + "ruSeoTitle": "Генератор Да или Нет — случайный ответ | Randify", + "ruSeoDescription": "Мгновенный случайный ответ Да, Нет или Возможно на любой вопрос. Бесплатный онлайн-помощник для быстрых решений.", + "pageTitle": "Yes or No Generator", + "ruPageTitle": "Генератор Да или Нет", + "howTo": [ + "Type your question in the field (optional — it is just for context).", + "Click the button to get a random answer.", + "The result appears instantly: Yes, No, or Maybe.", + "A confidence percentage is shown alongside the answer." + ], + "whenTo": [ + "Making a trivial decision when you are stuck.", + "Settling a friendly debate or bet.", + "Adding a fun random element to a game or challenge." + ], + "ruHowTo": [ + "Введите свой вопрос в поле (по желанию — это просто для контекста).", + "Нажмите кнопку, чтобы получить случайный ответ.", + "Результат появится мгновенно: Да, Нет или Возможно.", + "Рядом с ответом отображается процент уверенности." + ], + "ruWhenTo": [ + "Принятие простого решения, когда вы не можете выбрать.", + "Разрешение дружеского спора или пари.", + "Добавление случайного элемента в игру или челлендж." + ], + "faq": [ + { "q": "Is the answer really random?", "a": "Yes. The outcome is generated using a cryptographically secure random number generator, giving each option a fair chance." }, + { "q": "What does the confidence percentage mean?", "a": "It is a fun visual indicator showing how 'certain' the random choice feels — it does not affect the actual result." }, + { "q": "Can I use this for serious decisions?", "a": "It is best for light-hearted or trivial choices. For important matters, trust your own judgment." } + ], + "ruFaq": [ + { "q": "Ответ действительно случайный?", "a": "Да. Результат генерируется с помощью криптографически стойкого генератора случайных чисел, давая каждому варианту равный шанс." }, + { "q": "Что означает процент уверенности?", "a": "Это забавный визуальный индикатор, показывающий, насколько «уверен» случайный выбор — он не влияет на сам результат." }, + { "q": "Можно ли использовать для серьёзных решений?", "a": "Лучше всего подходит для лёгких или шуточных выборов. В важных вопросах доверяйте собственному мнению." } + ] +} diff --git a/src/data/config.ts b/src/data/config.ts new file mode 100644 index 0000000..83a780c --- /dev/null +++ b/src/data/config.ts @@ -0,0 +1,6 @@ +export const siteUrl = "https://randify.pro"; + +export const analytics = { + yandexMetrikaId: "109130319", + topMailRuId: "3765043", +} as const; diff --git a/src/data/generators.ts b/src/data/generators.ts new file mode 100644 index 0000000..ee8f26f --- /dev/null +++ b/src/data/generators.ts @@ -0,0 +1,21 @@ +import { generatorSchema } from "@/lib/generator-schema"; +import type { Generator } from "@/lib/generator-schema"; + +const modules = import.meta.glob<{ default: unknown }>( + "../content/generators/*.json", + { + eager: true, + }, +); + +export const generators: Generator[] = Object.values(modules).map((mod) => + generatorSchema.parse(mod.default), +); + +export function findGenerator(slug: string): Generator { + const g = generators.find((gen) => gen.slug === slug); + if (!g) throw new Error(`Generator not found: ${slug}`); + return g; +} + +export type { Generator }; diff --git a/src/env.d.ts b/src/env.d.ts new file mode 100644 index 0000000..e16c13c --- /dev/null +++ b/src/env.d.ts @@ -0,0 +1 @@ +/// diff --git a/src/i18n/translations.ts b/src/i18n/translations.ts new file mode 100644 index 0000000..1592a7b --- /dev/null +++ b/src/i18n/translations.ts @@ -0,0 +1,198 @@ +export type Lang = "en" | "ru"; + +export const translations = { + en: { + allGenerators: "All generators", + live: "Live", + comingSoon: "Coming soon", + copy: "Copy", + copied: "Copied", + copyAll: "Copy all", + howToUse: "How to use", + whenToUse: "When to use", + faq: "FAQ", + about: "About", + + generate: "Generate", + draw: "Draw", + roll: "Roll", + flip: "Flip", + pick: "Pick", + spin: "Spin", + + from: "From", + to: "To", + length: "Length", + count: "Count", + winner: "Winner", + total: "Total", + sides: "Sides", + numberOfDice: "Number of dice", + numberOfCoins: "Number of coins", + cardsToDraw: "Cards to draw", + allowDuplicates: "Allow duplicates", + uppercase: "Uppercase (A–Z)", + lowercase: "Lowercase (a–z)", + digits: "Digits (0–9)", + symbols: "Symbols (!@#…)", + type: "Type", + lengthBytes: "Length (bytes)", + itemsOneLine: "one per line", + itemsOneLine224: "one per line, 2–24", + items: "Items", + headsLabel: "H", + tailsLabel: "T", + headsWord: "heads", + tailsWord: "tails", + + errBothIntegers: "Both values must be whole numbers.", + errFromLessThanTo: '"From" must be less than "To".', + errSelectOneType: "Select at least one character type.", + errAllIntegers: "All values must be whole numbers.", + errPickAtLeastOne: '"Pick" must be at least 1.', + errDiceAtLeast: "Number of dice must be at least 1.", + errDiceMax: "Maximum 20 dice at once.", + errDrawAtLeast: "Draw at least 1 card.", + errMaxUniqueCards: "Cannot draw more than 52 unique cards.", + errMax52Cards: "Maximum 52 cards at once.", + errAddAtLeast2: "Add at least 2 items.", + errMax24: "Maximum 24 items.", + errAddAtLeast1: "Add at least one item to the list.", + errPickAtLeast1: "Pick at least 1 item.", + + homeTitle: "Random generators for every occasion", + homeSubtitle: + "Simple tools for raffles, games, and everything that needs randomness.", + defaultTitle: "Randify — Random Value Generators", + defaultDesc: + "Simple tools for raffles, games, and everything that needs randomness.", + brandLabel: "randify", + backToAll: "All generators", + + aboutTitle: "About Randify", + aboutMission: + "Randify exists to make randomness simple, fast, and accessible to everyone. Whether you are picking a winner, rolling dice, or generating a secure password, you should not need to install an app or create an account. Just open the page and get your result.", + aboutHistory: + "The idea for Randify came from a simple frustration: every time we needed a quick random number or a coin flip, we ended up on cluttered websites full of ads, pop-ups, and unnecessary steps. We wanted something cleaner — a tool that just works, without distractions.\n\nWe started with a single number generator and gradually expanded the collection based on what people actually use: dice for board games, wheels for raffles, passwords for security, and more. Each tool was built with the same philosophy — minimal interface, instant results, zero friction.", + aboutHowItWorks: + "Everything on Randify runs entirely in your browser. No data is sent to any server, no personal information is collected, and no account is required. The random values are generated locally using the Web Crypto API, which provides cryptographically secure randomness. This means your passwords, numbers, and choices stay private — we never see them.", + aboutContact: + "Have a suggestion, found a bug, or just want to say hello? Drop us a line at hello@randify.pro — we read every message.", + aboutCta: + "Ready to give it a spin? Explore the generators and let chance do the work.", + + privacyTitle: "Privacy Policy", + privacyIntro: + "This Privacy Policy explains how Randify handles information when you use our website. We believe in transparency: we collect as little data as possible and never ask for personal information.", + privacyData: + "Randify does not collect, store, or process any personal data. All random values — numbers, passwords, dice rolls, card draws, and every other result — are generated entirely inside your browser. Nothing you enter or generate is sent to our servers. We do not require registration, we do not ask for your name or email to use the generators, and we have no access to your inputs or outputs.", + privacyCookies: + "We do not use tracking cookies. The only information stored on your device is your language preference (English or Russian), which is saved in localStorage so the site remembers your choice on future visits. This data never leaves your browser and is not shared with anyone.", + privacyThirdParty: + "We use two third-party services to keep Randify free and improve the experience:\n\n• Yandex.RTB — displays advertisements on some pages. Yandex may use cookies and similar technologies to show relevant ads. You can manage ad preferences through your browser settings or Yandex's opt-out tools.\n\n• Yandex.Metrika — helps us understand how visitors use the site (for example, which generators are most popular). This is anonymous aggregate data: we cannot identify individual users.", + privacyContact: + "If you have any questions about this policy or how we handle data, please contact us at hello@randify.pro.", + privacyChanges: + "We may update this Privacy Policy from time to time. Any changes will be posted on this page with an updated date. Since we do not collect contact information, we cannot notify users individually — please check this page occasionally for updates.", + }, + ru: { + allGenerators: "Все генераторы", + live: "Активен", + comingSoon: "Скоро", + copy: "Копировать", + copied: "Скопировано", + copyAll: "Копировать всё", + howToUse: "Как пользоваться", + whenToUse: "Когда использовать", + faq: "Частые вопросы", + about: "О проекте", + + generate: "Сгенерировать", + draw: "Тянуть", + roll: "Бросить", + flip: "Подбросить", + pick: "Выбрать", + spin: "Крутить", + + from: "От", + to: "До", + length: "Длина", + count: "Количество", + winner: "Победитель", + total: "Итого", + sides: "Грани", + numberOfDice: "Количество кубиков", + numberOfCoins: "Количество монет", + cardsToDraw: "Карт вытащить", + allowDuplicates: "Разрешить повторения", + uppercase: "Заглавные (A–Z)", + lowercase: "Строчные (a–z)", + digits: "Цифры (0–9)", + symbols: "Символы (!@#…)", + type: "Тип", + lengthBytes: "Длина (байты)", + itemsOneLine: "по одному на строку", + itemsOneLine224: "по одному на строку, 2–24", + items: "Элементы", + headsLabel: "О", + tailsLabel: "Р", + headsWord: "орёл", + tailsWord: "решка", + + errBothIntegers: "Оба значения должны быть целыми числами.", + errFromLessThanTo: "«От» должно быть меньше «До».", + errSelectOneType: "Выберите хотя бы один тип символов.", + errAllIntegers: "Все значения должны быть целыми числами.", + errPickAtLeastOne: "«Выбрать» должно быть не менее 1.", + errDiceAtLeast: "Количество кубиков должно быть не менее 1.", + errDiceMax: "Максимум 20 кубиков одновременно.", + errDrawAtLeast: "Вытащите хотя бы 1 карту.", + errMaxUniqueCards: "Нельзя вытащить более 52 уникальных карт.", + errMax52Cards: "Максимум 52 карты одновременно.", + errAddAtLeast2: "Добавьте хотя бы 2 элемента.", + errMax24: "Максимум 24 элемента.", + errAddAtLeast1: "Добавьте хотя бы один элемент в список.", + errPickAtLeast1: "Выберите хотя бы 1 элемент.", + + homeTitle: "Генераторы случайных значений на любой случай", + homeSubtitle: + "Простые инструменты для розыгрышей, игр и всего, что требует случайности.", + defaultTitle: "Randify — Генераторы случайных значений", + defaultDesc: + "Простые инструменты для розыгрышей, игр и всего, что требует случайности.", + brandLabel: "randify", + backToAll: "Все генераторы", + + aboutTitle: "О проекте", + aboutMission: + "Randify создан, чтобы сделать случайность простой, быстрой и доступной каждому. Независимо от того, выбираете ли вы победителя, бросаете кубики или создаёте надёжный пароль — не нужно устанавливать приложение или регистрироваться. Просто откройте страницу и получите результат.", + aboutHistory: + "Идея Randify родилась из простого раздражения: каждый раз, когда нам нужно было быстро сгенерировать случайное число или подбросить монетку, мы попадали на перегруженные рекламой сайты с всплывающими окнами и лишними шагами. Мы хотели создать что-то чище — инструмент, который просто работает, без отвлекающих элементов.\n\nМы начали с одного генератора чисел и постепенно расширяли коллекцию, ориентируясь на реальные потребности: кубики для настольных игр, колесо фортуны для розыгрышей, генератор паролей для безопасности и многое другое. Каждый инструмент создавался по одному принципу — минимальный интерфейс, мгновенный результат, никаких препятствий.", + aboutHowItWorks: + "Всё на Randify работает полностью в вашем браузере. Данные не отправляются на сервер, личная информация не собирается, регистрация не требуется. Случайные значения генерируются локально с помощью Web Crypto API, который обеспечивает криптографически стойкую случайность. Это означает, что ваши пароли, числа и выбор остаются приватными — мы их не видим.", + aboutContact: + "Есть предложение, нашли ошибку или просто хотите поздороваться? Напишите нам на hello@randify.pro — мы читаем каждое сообщение.", + aboutCta: + "Готовы попробовать? Откройте генераторы и позвольте случайности сделать выбор за вас.", + + privacyTitle: "Политика конфиденциальности", + privacyIntro: + "Настоящая Политика конфиденциальности объясняет, как Randify работает с информацией при использовании сайта. Мы верим в прозрачность: собираем минимум данных и никогда не запрашиваем персональную информацию.", + privacyData: + "Randify не собирает, не хранит и не обрабатывает персональные данные. Все случайные значения — числа, пароли, броски кубиков, вытягивание карт и любые другие результаты — генерируются полностью внутри вашего браузера. Ничто из того, что вы вводите или генерируете, не отправляется на наши серверы. Мы не требуем регистрации, не просим имя или email для использования генераторов и не имеем доступа к вашим данным.", + privacyCookies: + "Мы не используем cookies для отслеживания. Единственная информация, которая сохраняется на вашем устройстве, — это предпочитаемый язык (английский или русский) в localStorage, чтобы сайт запоминал ваш выбор при следующем посещении. Эти данные не покидают браузер и никому не передаются.", + privacyThirdParty: + "Мы используем два сторонних сервиса, чтобы Randify оставался бесплатным и удобным:\n\n• Yandex.RTB — показывает рекламу на некоторых страницах. Яндекс может использовать cookies и аналогичные технологии для показа релевантной рекламы. Вы можете управлять настройками рекламы через параметры браузера или инструменты отказа Яндекса.\n\n• Яндекс.Метрика — помогает нам понимать, как посетители используют сайт (например, какие генераторы самые популярные). Это анонимные агрегированные данные: мы не можем идентифицировать отдельных пользователей.", + privacyContact: + "Если у вас есть вопросы об этой политике или о том, как мы работаем с данными, напишите нам на hello@randify.pro.", + privacyChanges: + "Мы можем обновлять эту Политику конфиденциальности время от времени. Любые изменения будут опубликованы на этой странице с обновлённой датой. Поскольку мы не собираем контактную информацию, мы не можем уведомлять пользователей индивидуально — пожалуйста, проверяйте эту страницу время от времени.", + }, +} as const; + +export type T = typeof translations.en; + +export function useT(lang: Lang): T { + return translations[lang] as T; +} diff --git a/src/layouts/BaseLayout.astro b/src/layouts/BaseLayout.astro new file mode 100644 index 0000000..439f3fd --- /dev/null +++ b/src/layouts/BaseLayout.astro @@ -0,0 +1,128 @@ +--- +import { getRelativeLocaleUrl } from "astro:i18n"; +import LanguageSwitcher from "../components/LanguageSwitcher.astro"; +import Starfield from "../components/Starfield.astro"; +import Analytics from "../components/Analytics.astro"; +import { useT } from "../i18n/translations"; +import type { Lang } from "../i18n/translations"; + +interface Props { + title?: string; + description?: string; +} + +const lang = (Astro.currentLocale as Lang) || "en"; +const T = useT(lang); + +const { title = T.defaultTitle, description = T.defaultDesc } = Astro.props; + +const pathWithoutLocale = Astro.url.pathname.replace(/^\/ru/, "") || "/"; +const enUrl = `https://randify.pro${getRelativeLocaleUrl("en", pathWithoutLocale)}`; +const ruUrl = `https://randify.pro${getRelativeLocaleUrl("ru", pathWithoutLocale)}`; +const canonicalUrl = `https://randify.pro${Astro.url.pathname}`; + +const webPageSchema = { + "@context": "https://schema.org", + "@type": "WebPage", + name: title, + description: description, + url: canonicalUrl, + inLanguage: lang, +}; +--- + + + + + + + + + + + + + + + + + + + + {title} + + + + + +
+ + +
+ + + + + + + diff --git a/src/layouts/GeneratorLayout.astro b/src/layouts/GeneratorLayout.astro new file mode 100644 index 0000000..067a8b0 --- /dev/null +++ b/src/layouts/GeneratorLayout.astro @@ -0,0 +1,137 @@ +--- +import BaseLayout from "./BaseLayout.astro"; +import YandexRTB from "../components/YandexRTB.astro"; +import SeoBlock from "../components/SeoBlock.astro"; +import FaqBlock from "../components/FaqBlock.astro"; +import { useT } from "../i18n/translations"; +import type { Lang } from "../i18n/translations"; +import type { Generator } from "../data/generators"; + +interface Props { + generator: Generator; +} + +const { generator } = Astro.props; +const lang = (Astro.currentLocale as Lang) || "en"; +const T = useT(lang); +const isRu = lang === "ru"; + +const canonicalUrl = `https://randify.pro${Astro.url.pathname}`; + +const appSchema = { + "@context": "https://schema.org", + "@type": "SoftwareApplication", + name: isRu ? generator.ruTitle : generator.title, + applicationCategory: "UtilityApplication", + operatingSystem: "Any", + offers: { + "@type": "Offer", + price: "0", + priceCurrency: "USD", + }, + inLanguage: ["en", "ru"], + description: isRu ? generator.ruDescription : generator.description, + url: canonicalUrl, +}; + +const breadcrumbSchema = { + "@context": "https://schema.org", + "@type": "BreadcrumbList", + itemListElement: [ + { + "@type": "ListItem", + position: 1, + name: "Randify.pro", + item: "https://randify.pro/", + }, + { + "@type": "ListItem", + position: 2, + name: isRu ? generator.ruTitle : generator.title, + item: canonicalUrl, + }, + ], +}; +--- + + + +