From 9a916e6a74eda725821c428ea590715aa1292c7a Mon Sep 17 00:00:00 2001 From: Tomasz Janusz <424941+TomaszJanusz@users.noreply.github.com> Date: Wed, 23 Sep 2026 19:44:04 +0200 Subject: [PATCH 1/2] feat(i18n): add Russian and Ukrainian translations --- .../scripts/audit-public-boundary.mjs | 1 + CHANGELOG.md | 2 + src/shared/extension-notifications.json | 70 ++++- src/shared/release-notification.test.ts | 19 ++ src/ui/i18n/en-sections/advanced.ts | 2 + src/ui/i18n/es-sections/advanced.ts | 2 + src/ui/i18n/i18n-usage.test.ts | 7 + src/ui/i18n/index.test.ts | 8 + src/ui/i18n/index.ts | 13 +- src/ui/i18n/localized-copy.test.ts | 48 ++++ src/ui/i18n/pt-sections/advanced.ts | 2 + src/ui/i18n/ru-sections/about.ts | 169 ++++++++++++ src/ui/i18n/ru-sections/advanced.ts | 207 +++++++++++++++ src/ui/i18n/ru-sections/common.ts | 42 +++ src/ui/i18n/ru-sections/demo.ts | 152 +++++++++++ src/ui/i18n/ru-sections/firefox-containers.ts | 101 +++++++ src/ui/i18n/ru-sections/locations.ts | 109 ++++++++ src/ui/i18n/ru-sections/options.ts | 125 +++++++++ src/ui/i18n/ru-sections/osm.ts | 12 + src/ui/i18n/ru-sections/popup.ts | 246 +++++++++++++++++ src/ui/i18n/ru-sections/rules.ts | 232 ++++++++++++++++ src/ui/i18n/ru-sections/shared-worker.ts | 32 +++ src/ui/i18n/ru-sections/sidebar.ts | 174 ++++++++++++ src/ui/i18n/ru-sections/trusted-sites.ts | 54 ++++ src/ui/i18n/ru-sections/welcome.ts | 139 ++++++++++ src/ui/i18n/ru.ts | 30 +++ src/ui/i18n/slavic-plural.test.ts | 25 ++ src/ui/i18n/uk-sections/about.ts | 168 ++++++++++++ src/ui/i18n/uk-sections/advanced.ts | 206 +++++++++++++++ src/ui/i18n/uk-sections/common.ts | 42 +++ src/ui/i18n/uk-sections/demo.ts | 151 +++++++++++ src/ui/i18n/uk-sections/firefox-containers.ts | 101 +++++++ src/ui/i18n/uk-sections/locations.ts | 108 ++++++++ src/ui/i18n/uk-sections/options.ts | 125 +++++++++ src/ui/i18n/uk-sections/osm.ts | 12 + src/ui/i18n/uk-sections/popup.ts | 248 ++++++++++++++++++ src/ui/i18n/uk-sections/rules.ts | 238 +++++++++++++++++ src/ui/i18n/uk-sections/shared-worker.ts | 32 +++ src/ui/i18n/uk-sections/sidebar.ts | 174 ++++++++++++ src/ui/i18n/uk-sections/trusted-sites.ts | 54 ++++ src/ui/i18n/uk-sections/welcome.ts | 139 ++++++++++ src/ui/i18n/uk.ts | 30 +++ .../tabs/options-preference-sections.tsx | 6 + src/ui/shared/slavic-plural.ts | 19 ++ 44 files changed, 3865 insertions(+), 11 deletions(-) create mode 100644 src/ui/i18n/ru-sections/about.ts create mode 100644 src/ui/i18n/ru-sections/advanced.ts create mode 100644 src/ui/i18n/ru-sections/common.ts create mode 100644 src/ui/i18n/ru-sections/demo.ts create mode 100644 src/ui/i18n/ru-sections/firefox-containers.ts create mode 100644 src/ui/i18n/ru-sections/locations.ts create mode 100644 src/ui/i18n/ru-sections/options.ts create mode 100644 src/ui/i18n/ru-sections/osm.ts create mode 100644 src/ui/i18n/ru-sections/popup.ts create mode 100644 src/ui/i18n/ru-sections/rules.ts create mode 100644 src/ui/i18n/ru-sections/shared-worker.ts create mode 100644 src/ui/i18n/ru-sections/sidebar.ts create mode 100644 src/ui/i18n/ru-sections/trusted-sites.ts create mode 100644 src/ui/i18n/ru-sections/welcome.ts create mode 100644 src/ui/i18n/ru.ts create mode 100644 src/ui/i18n/slavic-plural.test.ts create mode 100644 src/ui/i18n/uk-sections/about.ts create mode 100644 src/ui/i18n/uk-sections/advanced.ts create mode 100644 src/ui/i18n/uk-sections/common.ts create mode 100644 src/ui/i18n/uk-sections/demo.ts create mode 100644 src/ui/i18n/uk-sections/firefox-containers.ts create mode 100644 src/ui/i18n/uk-sections/locations.ts create mode 100644 src/ui/i18n/uk-sections/options.ts create mode 100644 src/ui/i18n/uk-sections/osm.ts create mode 100644 src/ui/i18n/uk-sections/popup.ts create mode 100644 src/ui/i18n/uk-sections/rules.ts create mode 100644 src/ui/i18n/uk-sections/shared-worker.ts create mode 100644 src/ui/i18n/uk-sections/sidebar.ts create mode 100644 src/ui/i18n/uk-sections/trusted-sites.ts create mode 100644 src/ui/i18n/uk-sections/welcome.ts create mode 100644 src/ui/i18n/uk.ts create mode 100644 src/ui/shared/slavic-plural.ts diff --git a/.agents/skills/public-boundary-audit/scripts/audit-public-boundary.mjs b/.agents/skills/public-boundary-audit/scripts/audit-public-boundary.mjs index c2df15e..e740e09 100644 --- a/.agents/skills/public-boundary-audit/scripts/audit-public-boundary.mjs +++ b/.agents/skills/public-boundary-audit/scripts/audit-public-boundary.mjs @@ -348,6 +348,7 @@ const buildAllowedRetiredLines = () => { "src/shared/extension-notifications.json", new Set([ `"en": ${JSON.stringify(renameTitle)}`, + `"en": ${JSON.stringify(renameTitle)},`, `${JSON.stringify(renameParagraph)},`, ]), ], diff --git a/CHANGELOG.md b/CHANGELOG.md index edaa63c..741b96e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,8 @@ The format is based on Keep a Changelog and the project follows Semantic Version - Added complete Spanish and Portuguese interface translations, selected automatically from the browser interface language with English as the fallback. +- Added complete Russian and Ukrainian interface translations, selected + automatically from the browser interface language. - X-Ray now counts BatteryManager property reads (`charging`, `chargingTime`, `dischargingTime`, `level`) in addition to `navigator.getBattery`. diff --git a/src/shared/extension-notifications.json b/src/shared/extension-notifications.json index 2256388..d6ed813 100644 --- a/src/shared/extension-notifications.json +++ b/src/shared/extension-notifications.json @@ -7,8 +7,10 @@ "delivery": "upgrades-only", "title": { "en": "GeoWarp is now {{brand}}", - "es": "GeoWarp ahora es {{brand}}", - "pt": "GeoWarp agora é {{brand}}" + "es": "La extensión ahora se llama {{brand}}", + "pt": "A extensão agora se chama {{brand}}", + "ru": "Теперь расширение называется {{brand}}", + "uk": "Тепер розширення називається {{brand}}" }, "message": { "en": [ @@ -16,12 +18,20 @@ "Thank you for using it and being part of the journey so far. Building this extension has been an extraordinary journey for me, and I’m excited to see what the future brings." ], "es": [ - "GeoWarp tiene un nuevo nombre: {{brand}}. La extensión que conoces sigue aquí y mantiene su propósito: darte el control sobre la ubicación y los datos del navegador que pueden ver los sitios web.", + "La extensión tiene un nuevo nombre: {{brand}}. Sigue siendo la extensión que conoces y mantiene su propósito: darte el control sobre la ubicación y los datos del navegador que pueden ver los sitios web.", "Gracias por usarla y por formar parte de este recorrido. Crear esta extensión ha sido una experiencia extraordinaria para mí y me ilusiona descubrir lo que traerá el futuro." ], "pt": [ - "GeoWarp tem um novo nome: {{brand}}. A extensão que você conhece continua aqui, com o mesmo objetivo de dar a você controle sobre a localização e as informações do navegador que os sites podem ver.", + "A extensão tem um novo nome: {{brand}}. Ela continua a mesma, com o objetivo de dar a você controle sobre a localização e as informações do navegador que os sites podem ver.", "Agradecemos por usá-la e por fazer parte desta jornada. Criar esta extensão tem sido uma experiência extraordinária para mim, e estou animado para ver o que o futuro reserva." + ], + "ru": [ + "У расширения новое имя: {{brand}}. Знакомое вам расширение осталось прежним. Оно по-прежнему помогает управлять тем, какие сведения о вашем местоположении и браузере видят сайты.", + "Спасибо, что пользуетесь расширением и прошли с ним этот путь. Работа над ним стала для меня необыкновенным опытом, и я с интересом жду того, что будет дальше." + ], + "uk": [ + "Розширення має нову назву: {{brand}}. Знайоме вам розширення залишається тим самим. Воно й далі допомагає керувати тим, які відомості про ваше місцеперебування та браузер бачать сайти.", + "Дякую, що користуєтеся розширенням і пройшли з ним цей шлях. Робота над ним стала для мене надзвичайним досвідом, і я з цікавістю чекаю на те, що буде далі." ] } }, @@ -33,7 +43,9 @@ "title": { "en": "Notifications are now in the popup", "es": "Las notificaciones ya están en la ventana emergente", - "pt": "As notificações agora estão no pop-up" + "pt": "As notificações agora estão no pop-up", + "ru": "Уведомления теперь в окне расширения", + "uk": "Сповіщення тепер у вікні розширення" }, "message": { "en": [ @@ -47,6 +59,14 @@ "pt": [ "{{brand}} agora mostra atualizações importantes e avisos de compatibilidade no pop-up.", "Ao abrir uma notificação, ela é marcada como lida e permanece disponível até que você a descarte." + ], + "ru": [ + "{{brand}} теперь показывает важные обновления и предупреждения о совместимости в окне расширения.", + "Когда вы открываете уведомление, оно отмечается как прочитанное. Оно остаётся доступным, пока вы его не закроете." + ], + "uk": [ + "{{brand}} тепер показує важливі оновлення та попередження про сумісність у вікні розширення.", + "Після відкриття сповіщення позначається як прочитане. Воно залишається доступним, доки ви його не закриєте." ] } }, @@ -58,7 +78,9 @@ "title": { "en": "Temporal API protection is ready to try", "es": "Ya puedes probar la protección de la API Temporal", - "pt": "A proteção da API Temporal está pronta para testes" + "pt": "A proteção da API Temporal está pronta para testes", + "ru": "Защиту Temporal API уже можно попробовать", + "uk": "Захист Temporal API вже можна спробувати" }, "message": { "en": [ @@ -75,6 +97,16 @@ "{{brand}} agora pode proteger chamadas nativas da API Temporal, incluindo o comportamento padrão de data, hora, configuração regional e fuso horário, junto com o restante da sua configuração.", "O recurso é experimental e vem desativado por padrão. Para testá-lo, abra Configurações → Avançado → Experimental e ative a API Temporal. Ele só funciona quando o navegador já oferece a API Temporal — {{brand}} não instala um polyfill.", "Quer saber mais? Use o link abaixo para consultar a documentação da API Temporal na Mozilla Developer Network." + ], + "ru": [ + "{{brand}} теперь может защищать вызовы встроенного Temporal API: даты, время, региональные настройки и часовой пояс по умолчанию — вместе с остальными параметрами защиты.", + "Функция экспериментальная и по умолчанию выключена. Чтобы попробовать её, откройте Настройки → Дополнительно → Экспериментальные функции и включите Temporal API. Это работает только в браузерах, где Temporal уже доступен: {{brand}} не добавляет полифил.", + "Хотите узнать больше? Откройте документацию по Temporal API на Mozilla Developer Network по ссылке ниже." + ], + "uk": [ + "{{brand}} тепер може захищати виклики вбудованого Temporal API: дати, час, регіональні налаштування та часовий пояс за замовчуванням — разом з іншими параметрами захисту.", + "Функція експериментальна й за замовчуванням вимкнена. Щоб спробувати її, відкрийте Налаштування → Додатково → Експериментальні функції та ввімкніть Temporal API. Це працює лише в браузерах, де Temporal уже доступний: {{brand}} не додає поліфіл.", + "Хочете дізнатися більше? Відкрийте документацію Temporal API на Mozilla Developer Network за посиланням нижче." ] }, "actionUrl": "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal" @@ -87,7 +119,9 @@ "title": { "en": "{{brand}} is now open source!", "es": "¡{{brand}} ya es de código abierto!", - "pt": "{{brand}} agora é de código aberto!" + "pt": "{{brand}} agora é de código aberto!", + "ru": "Исходный код {{brand}} открыт!", + "uk": "Вихідний код {{brand}} відкрито!" }, "message": { "en": [ @@ -101,6 +135,14 @@ "pt": [ "{{brand}} agora é de código aberto. O código está disponível no GitHub sob a licença AGPL-3.0-or-later e os termos adicionais do projeto. Explore o repositório para entender como tudo funciona nos bastidores.", "Encontrou um bug comum? Abra uma issue e conte o que aconteceu. Problemas de segurança ou detecção devem continuar sendo enviados pelo formulário privado do GitHub." + ], + "ru": [ + "Исходный код {{brand}} теперь открыт. Он доступен на GitHub по лицензии AGPL-3.0-or-later с дополнительными условиями проекта. Загляните в репозиторий, чтобы увидеть, как всё устроено.", + "Нашли обычную ошибку? Создайте issue и расскажите, что произошло. О проблемах безопасности или обнаружения защиты по-прежнему сообщайте через закрытую форму GitHub." + ], + "uk": [ + "Вихідний код {{brand}} тепер відкрито. Він доступний на GitHub за ліцензією AGPL-3.0-or-later з додатковими умовами проєкту. Зазирніть до репозиторію, щоб побачити, як усе працює.", + "Знайшли звичайну помилку? Створіть issue та розкажіть, що сталося. Про проблеми безпеки або виявлення захисту й надалі повідомляйте через приватну форму GitHub." ] }, "actionUrl": "https://github.com/Privacy-Thing/browser-extension" @@ -113,7 +155,9 @@ "title": { "en": "Domain fencing is ready to try", "es": "Ya puedes probar el aislamiento por dominio", - "pt": "O isolamento por domínio está pronto para testes" + "pt": "O isolamento por domínio está pronto para testes", + "ru": "Изоляцию по доменам уже можно попробовать", + "uk": "Ізоляцію за доменами вже можна спробувати" }, "message": { "en": [ @@ -130,6 +174,16 @@ "No Chrome e no Firefox, o isolamento por domínio atribui a cada site uma variante estável da impressão digital gerada quando a Regra Padrão se aplica. No Firefox, ele também separa as identidades atribuídas por meio de contêineres.", "Um site e seus subdomínios compartilham uma identidade, enquanto sites não relacionados recebem variantes distintas. O perfil regional escolhido não muda, e as Regras de Domínio configuradas manualmente não são isoladas.", "O isolamento por domínio é experimental e vem desativado por padrão. Para testá-lo, abra Configurações → Avançado → Experimental e ative Isolamento por domínio." + ], + "ru": [ + "В Chrome и Firefox изоляция по доменам даёт каждому сайту собственный стабильный вариант созданного цифрового отпечатка, когда действует правило по умолчанию. В Firefox она также разделяет идентичности, назначенные через контейнеры.", + "Сайт и его поддомены используют одну идентичность, а несвязанные сайты получают разные варианты. Выбранный региональный профиль не меняется. Правила для доменов, настроенные вручную, не изолируются.", + "Изоляция по доменам — экспериментальная функция, по умолчанию выключенная. Чтобы попробовать её, откройте Настройки → Дополнительно → Экспериментальные функции и включите изоляцию по доменам." + ], + "uk": [ + "У Chrome і Firefox ізоляція за доменами надає кожному сайту власний стабільний варіант створеного цифрового відбитка, коли діє правило за замовчуванням. У Firefox вона також розділяє ідентичності, призначені через контейнери.", + "Сайт і його піддомени використовують одну ідентичність, а не пов’язані між собою сайти отримують різні варіанти. Обраний регіональний профіль не змінюється. Правила для доменів, налаштовані вручну, не ізолюються.", + "Ізоляція за доменами — експериментальна функція, яку за замовчуванням вимкнено. Щоб спробувати її, відкрийте Налаштування → Додатково → Експериментальні функції та ввімкніть ізоляцію за доменами." ] } } diff --git a/src/shared/release-notification.test.ts b/src/shared/release-notification.test.ts index e335be8..0caabba 100644 --- a/src/shared/release-notification.test.ts +++ b/src/shared/release-notification.test.ts @@ -60,6 +60,25 @@ describe("extension notification catalog", () => { ]); }); + it.each([ + ["ru-RU", "Уведомления теперь в окне расширения"], + ["uk-UA", "Сповіщення тепер у вікні розширення"], + ])("provides %s release notices", (locale, title) => { + expect(getReleaseNotice("notification-center-intro", locale)?.title).toBe(title); + }); + + it("keeps Russian and Ukrainian notices complete with brand tokens", () => { + for (const notice of notificationCatalog.notifications) { + for (const locale of ["ru", "uk"] as const) { + expect(notice.title[locale]).toBeTruthy(); + expect(notice.message[locale]).toHaveLength(notice.message.en.length); + expect(getReleaseNotice(notice.id, locale)?.message).toHaveLength( + notice.message.en.length, + ); + } + } + }); + it("includes the 0.9.0 rename announcement", () => { const source = notificationCatalog.notifications.find( (notification) => notification.id === "privacy-thing-rename", diff --git a/src/ui/i18n/en-sections/advanced.ts b/src/ui/i18n/en-sections/advanced.ts index bd4fd5c..8ff6c3f 100644 --- a/src/ui/i18n/en-sections/advanced.ts +++ b/src/ui/i18n/en-sections/advanced.ts @@ -107,6 +107,8 @@ export const advanced = { optionEnglish: "English", optionSpanish: "Spanish", optionPortuguese: "Portuguese", + optionRussian: "Russian", + optionUkrainian: "Ukrainian", soon: "AUTO", copyLinkLabel: "language", }, diff --git a/src/ui/i18n/es-sections/advanced.ts b/src/ui/i18n/es-sections/advanced.ts index f5ea0a5..87c9411 100644 --- a/src/ui/i18n/es-sections/advanced.ts +++ b/src/ui/i18n/es-sections/advanced.ts @@ -108,6 +108,8 @@ export const advanced = { optionEnglish: "Inglés", optionSpanish: "Español", optionPortuguese: "Portugués", + optionRussian: "Ruso", + optionUkrainian: "Ucraniano", soon: "AUTOMÁTICO", copyLinkLabel: "idioma", }, diff --git a/src/ui/i18n/i18n-usage.test.ts b/src/ui/i18n/i18n-usage.test.ts index 23e9f46..5e8711d 100644 --- a/src/ui/i18n/i18n-usage.test.ts +++ b/src/ui/i18n/i18n-usage.test.ts @@ -376,6 +376,13 @@ describe("UI i18n coverage", () => { it("keeps all locale packs aligned with the base locale shape", async () => { const baseKeys = collectLocaleLeafPaths(en); const localeModules = await collectLocaleModules(); + expect(localeModules.map(({ code }) => code).sort()).toEqual([ + "en", + "es", + "pt", + "ru", + "uk", + ]); for (const localeModule of localeModules) { const localeKeys = collectLocaleLeafPaths(localeModule.value); diff --git a/src/ui/i18n/index.test.ts b/src/ui/i18n/index.test.ts index e478d38..fac6c82 100644 --- a/src/ui/i18n/index.test.ts +++ b/src/ui/i18n/index.test.ts @@ -14,6 +14,14 @@ describe("resolveUiLocale", () => { }, ); + it.each(["ru", "ru-RU", "RU-kz"])("selects Russian for %s", (language) => { + expect(resolveUiLocale(language)).toBe("ru"); + }); + + it.each(["uk", "uk-UA", "UK-ua"])("selects Ukrainian for %s", (language) => { + expect(resolveUiLocale(language)).toBe("uk"); + }); + it.each([undefined, "", "en", "en-US", "pl-PL"])( "falls back to English for %s", (language) => { diff --git a/src/ui/i18n/index.ts b/src/ui/i18n/index.ts index 3844dc3..2c96b81 100644 --- a/src/ui/i18n/index.ts +++ b/src/ui/i18n/index.ts @@ -1,16 +1,23 @@ import { en } from "./en"; import { es } from "./es"; import { pt } from "./pt"; +import { ru } from "./ru"; +import { uk } from "./uk"; -export type UiLocale = "en" | "es" | "pt"; +export type UiLocale = "en" | "es" | "pt" | "ru" | "uk"; export const resolveUiLocale = (language: string | undefined): UiLocale => { const baseLanguage = language?.toLowerCase().split("-")[0]; - return baseLanguage === "es" || baseLanguage === "pt" ? baseLanguage : "en"; + return baseLanguage === "es" || + baseLanguage === "pt" || + baseLanguage === "ru" || + baseLanguage === "uk" + ? baseLanguage + : "en"; }; export const UI_LOCALE = resolveUiLocale(globalThis.navigator?.language); -export const t = { en, es, pt }[UI_LOCALE]; +export const t = { en, es, pt, ru, uk }[UI_LOCALE]; if (typeof document !== "undefined") { document.documentElement.lang = UI_LOCALE; diff --git a/src/ui/i18n/localized-copy.test.ts b/src/ui/i18n/localized-copy.test.ts index ade7fb1..f2254f0 100644 --- a/src/ui/i18n/localized-copy.test.ts +++ b/src/ui/i18n/localized-copy.test.ts @@ -1,8 +1,11 @@ import type { SurfacePresentationState } from "@privacy-brand/xray-protocol"; import { describe, expect, it } from "vitest"; +import { en } from "./en"; import { es } from "./es"; import { pt } from "./pt"; +import { ru } from "./ru"; +import { uk } from "./uk"; const pluralProtectionCounts: Record = { "not-applicable": 2, @@ -17,6 +20,23 @@ const pluralProtectionCounts: Record = { }; describe("localized dynamic copy", () => { + it("keeps diagnostic JavaScript method names unchanged", () => { + expect(ru.sidebar.accessed.methods).toEqual(en.sidebar.accessed.methods); + expect(uk.sidebar.accessed.methods).toEqual(en.sidebar.accessed.methods); + }); + + it("joins Russian and Ukrainian interface fragments into complete sentences", () => { + for (const catalog of [ru, uk]) { + const mapAttribution = `${catalog.about.assets.mapPreviewsPrefix}OpenFreeMap${catalog.about.assets.mapPreviewsMiddle}OpenStreetMap${catalog.about.assets.mapPreviewsSuffix}`; + const privacyLink = `${catalog.welcome.steps.privacy.descriptionBeforePolicy} ${catalog.welcome.steps.privacy.policyLink}${catalog.welcome.steps.privacy.descriptionAfterPolicy}`; + const websiteLink = `${catalog.about.website.prefix}${catalog.about.website.linkLabel}${catalog.about.website.suffix}`; + expect(mapAttribution).not.toMatch(/ {2,}/); + expect(privacyLink).not.toMatch(/ {2,}/); + expect(websiteLink).not.toMatch(/\s+[,.;!?]/); + expect(privacyLink).toMatch(/[.!?]$/); + } + }); + it("uses complete Spanish plural phrases", () => { expect(es.popup.notificationsBadgeLabel(2)).toBe("2 notificaciones no leídas"); expect(es.popup.protectionCounts(pluralProtectionCounts)).toBe( @@ -33,4 +53,32 @@ describe("localized dynamic copy", () => { "2 configurações personalizadas", ); }); + + it.each([ + [1, "1 непрочитанное уведомление", "1 непрочитане сповіщення"], + [2, "2 непрочитанных уведомления", "2 непрочитані сповіщення"], + [5, "5 непрочитанных уведомлений", "5 непрочитаних сповіщень"], + [11, "11 непрочитанных уведомлений", "11 непрочитаних сповіщень"], + [21, "21 непрочитанное уведомление", "21 непрочитане сповіщення"], + [22, "22 непрочитанных уведомления", "22 непрочитані сповіщення"], + [25, "25 непрочитанных уведомлений", "25 непрочитаних сповіщень"], + ])( + "uses the right Russian and Ukrainian badge forms for %i", + (count, russian, ukrainian) => { + expect(ru.popup.notificationsBadgeLabel(count)).toBe(russian); + expect(uk.popup.notificationsBadgeLabel(count)).toBe(ukrainian); + }, + ); + + it("declines rule and preset counts in both languages", () => { + expect(ru.rules.globalFallback.overridesBadge(21)).toBe("21 особая настройка"); + expect(ru.rules.globalFallback.overridesBadge(22)).toBe("22 особые настройки"); + expect(ru.rules.globalFallback.overridesBadge(25)).toBe("25 особых настроек"); + expect(uk.locations.viewAssignedRulesAriaLabel("Київ", 22)).toBe( + "Показати 22 правила для профілю «Київ»", + ); + expect(uk.trustedSites.rulesCta.activeRulesOnly(11)).toContain( + "11 активних правил", + ); + }); }); diff --git a/src/ui/i18n/pt-sections/advanced.ts b/src/ui/i18n/pt-sections/advanced.ts index a5acd78..3feaad3 100644 --- a/src/ui/i18n/pt-sections/advanced.ts +++ b/src/ui/i18n/pt-sections/advanced.ts @@ -108,6 +108,8 @@ export const advanced = { optionEnglish: "Inglês", optionSpanish: "Espanhol", optionPortuguese: "Português", + optionRussian: "Russo", + optionUkrainian: "Ucraniano", soon: "AUTO", copyLinkLabel: "idioma", }, diff --git a/src/ui/i18n/ru-sections/about.ts b/src/ui/i18n/ru-sections/about.ts new file mode 100644 index 0000000..95dceec --- /dev/null +++ b/src/ui/i18n/ru-sections/about.ts @@ -0,0 +1,169 @@ +import { BRAND_DISPLAY_NAME } from "@/shared/brand"; + +const ABOUT_PRODUCT = `О ${BRAND_DISPLAY_NAME}`; +const ABOUT_PRODUCT_LOWER = `о ${BRAND_DISPLAY_NAME}`; +const HOW_TO_USE_PRODUCT = `Как использовать ${BRAND_DISPLAY_NAME}`; +const HOW_TO_USE_PRODUCT_LOWER = `как использовать ${BRAND_DISPLAY_NAME}`; +const WHAT_PRODUCT_IS_NOT = `Чем ${BRAND_DISPLAY_NAME} не является`; +const PRODUCT_NOT_LOWER = `раздел об ограничениях ${BRAND_DISPLAY_NAME}`; +const TERMS_OF_USE = "Условия использования"; + +export const about = { + title: ABOUT_PRODUCT, + description: `${BRAND_DISPLAY_NAME} помогает вам контролировать, как веб-сайты видят ваше местоположение, регион, часовой пояс и выбранные данные браузера.`, + body1: + "Каждый сохраненный региональный профиль объединяет координаты, языковые настройки и целевой часовой пояс в профиль, который можно назначать разным доменам.", + body2: `Ваши изменения сохраняются автоматически. Обновлённые профили и правила вступают в силу при следующей загрузке страницы, поэтому ${BRAND_DISPLAY_NAME} может применить новые настройки с самого начала.`, + body3Prefix: "Общие настройки защиты браузера находятся на вкладке", + body3LinkLabel: "«Настройки»", + body3Suffix: ".", + website: { + prefix: "Посетите ", + linkLabel: `Сайт ${BRAND_DISPLAY_NAME}`, + url: "https://privacything.com", + suffix: + ", чтобы узнать новости проекта, скачать расширение и найти другую информацию.", + }, + versionLabel: "Версия", + browserTargetLabel: "Браузер", + releaseChannelLabel: "Канал выпуска", + copyLinkLabel: ABOUT_PRODUCT_LOWER, + copyLinkTermsLabel: "Условия использования", + copyLinkPrivacyLabel: "о конфиденциальности", + copyLinkLimitationsLabel: PRODUCT_NOT_LOWER, + copyLinkLicenseLabel: "лицензия", + copyLinkAssetsLabel: "сторонние компоненты", + copyLinkUsageLabel: HOW_TO_USE_PRODUCT_LOWER, + releaseChannels: { + local: "Локальный", + beta: "Бета", + stable: "Стабильный", + }, + + support: { + url: "https://webh.pl", + logoLinkAriaLabel: "Открыть webh.pl", + bodyPrefix: `${BRAND_DISPLAY_NAME} растет благодаря поддержке со стороны `, + linkLabel: "webh.pl", + bodySuffix: " — быстрая, гибкая инфраструктура для амбициозных проектов.", + }, + + terms: { + title: TERMS_OF_USE, + body1: `${BRAND_DISPLAY_NAME} предоставляется «как есть», без каких-либо гарантий.`, + body2: `${BRAND_DISPLAY_NAME} помогает пользователям локально управлять выбранным местоположением, языковым стандартом, часовым поясом и соответствующими данными, отображаемыми в браузере. Это не VPN, прокси, инструмент анонимности, продукт безопасности или гарантия необнаружимости.`, + body3: `Используйте ${BRAND_DISPLAY_NAME} на свой страх и риск. Вы несете ответственность за соблюдение действующего законодательства, условий веб-сайта, политики на рабочем месте и правил платформы.`, + }, + + privacy: { + title: "Конфиденциальность", + body: `${BRAND_DISPLAY_NAME} сохраняет настройки локально в браузере. Запросы внешних карт не являются обязательными: OpenStreetMap Nominatim используется для поиска местоположения, а OpenFreeMap используется для интерактивного предварительного просмотра карт после вашего согласия.`, + openPolicyButton: "Открыть политику конфиденциальности", + }, + + limitations: { + title: WHAT_PRODUCT_IS_NOT, + intro: `${BRAND_DISPLAY_NAME} изменяет видимые в браузере данные, такие как геолокация, локаль, часовой пояс и выбранные данные цифрового отпечатка. Расширение не направляет трафик через другую сеть.`, + body1: "Расширение не скрывает и не заменяет ваш IP-адрес.", + body2: "Оно не заменяет VPN, прокси или настройки DNS.", + body3: + "Само по себе расширение не заставит сайты считать, что вы подключаетесь из другой страны.", + outro: `Используйте ${BRAND_DISPLAY_NAME} для подмены на уровне браузера. Используйте инструменты VPN, прокси-сервера или DNS, когда вам нужно изменить местоположение на уровне сети.`, + }, + + license: { + title: "Лицензия", + creatorPrefix: "Автор проекта — ", + creatorLabel: "Tomasz Janusz", + creatorUrl: "https://tomaszjanusz.dev", + creatorSuffix: ".", + copyright: "© 2025 — настоящее время.", + body: `${BRAND_DISPLAY_NAME} доступен по лицензии GNU Affero General Public License v3.0 или более поздней версии с дополнительными условиями.`, + openLicenseButton: "Открыть лицензию", + }, + + assets: { + title: "Сторонние компоненты", + body: `Просмотрите сторонние компоненты и откройте тексты лицензий, поставляемые с ${BRAND_DISPLAY_NAME}.`, + openNoticesButton: "Открыть сторонние уведомления", + fontAwesome: { + label: "Font Awesome Free 7.2.0", + url: "https://fontawesome.com", + body: " от Font Awesome / Fonticons, Inc. имеет лицензию CC BY 4.0, SIL OFL 1.1 и MIT.", + }, + mapLibre: { + label: "MapLibre GL JS", + url: "https://maplibre.org/maplibre-gl-js/docs/", + body: " поставляется локально для рендеринга векторных карт.", + }, + openFreeMap: { + label: "OpenFreeMap", + url: "https://openfreemap.org/", + }, + openStreetMap: { + label: "OpenStreetMap", + url: "https://www.openstreetmap.org/copyright", + }, + osmWikiCountryCodes: { + label: "OpenStreetMap Wiki: Nominatim/Country Codes", + url: "https://wiki.openstreetmap.org/wiki/Nominatim/Country_Codes", + body: " содержит типичные сочетания стран и языков для создания региональных профилей.", + licenseLabel: "CC BY-SA 2.0", + licenseUrl: "https://wiki.openstreetmap.org/wiki/Wiki_content_license", + licenseBody: + " относится к этому содержимому вики и соответственно упоминается здесь.", + }, + mapPreviewsPrefix: "Для интерактивного просмотра карт используются сервисы ", + mapPreviewsMiddle: "; сведения об участниках ", + mapPreviewsSuffix: " показаны на карте.", + + localData: { + title: "Локальные наборы данных", + body: `${BRAND_DISPLAY_NAME} содержит небольшие локальные наборы данных из открытых источников, чтобы подменённые значения оставались статистически правдоподобными. Во время работы данные не загружаются из Интернета: локальные копии обновляются вместе с расширением.`, + steam: { + label: "Опрос оборудования и программного обеспечения Steam", + url: "https://store.steampowered.com/hwsurvey/", + body: " (Valve) предоставляет статистику разрешения экрана, числа ядер процессора и объёма памяти для Windows, Linux и macOS. Эти данные используются для локальных профилей оборудования.", + }, + chromiumDash: { + label: "ChromiumDash", + url: "https://chromiumdash.appspot.com/", + body: " предоставляет реальные версии выпуска Chrome, используемые для User-Agent и Client Hints.", + }, + localeCatalog: { + prefix: "Каталоги ", + mozilla: { + label: "Mozilla", + url: "https://github.com/mozilla-firefox/firefox/blob/main/intl/locale/language.properties", + }, + middle: " и ", + chromium: { + label: "Chromium", + url: "https://github.com/chromium/chromium/blob/main/ui/base/l10n/l10n_util.cc", + }, + body: " предоставляют названия языков, согласованные с соответствующими браузерными движками.", + }, + }, + }, + + projectLinks: { + description: `У ${BRAND_DISPLAY_NAME} есть сайт и открытый исходный код. Посетите сайт, изучите репозиторий или сообщите об ошибке.`, + websiteLabel: "Сайт", + websiteUrl: "https://privacything.com", + sourceLabel: "Источник", + sourceUrl: "https://github.com/Privacy-Thing/browser-extension", + reportBugLabel: "Сообщить об ошибке", + reportBugUrl: + "https://github.com/Privacy-Thing/browser-extension/issues/new?template=bug_report.yml", + }, + + usage: { + title: HOW_TO_USE_PRODUCT, + body1: + "Если вам нужна быстрая и реалистичная отправная точка, начните с создания регионального профиля на основе искомого места.", + body2: `Создайте отдельный региональный профиль для каждого места, которое нужно имитировать, а затем назначьте сайтам подходящие профили.`, + body3: + "Используйте всплывающее окно для быстрого назначения домена. Откройте «Настройки», если вам нужна полная панель управления для региональных профилей и правил.", + body4: `Изучите «Настройки» и «Дополнительно», если вам нужен больший контроль над поведением ${BRAND_DISPLAY_NAME}.`, + }, +} as const; diff --git a/src/ui/i18n/ru-sections/advanced.ts b/src/ui/i18n/ru-sections/advanced.ts new file mode 100644 index 0000000..32a5d71 --- /dev/null +++ b/src/ui/i18n/ru-sections/advanced.ts @@ -0,0 +1,207 @@ +import { BRAND_DISPLAY_NAME } from "@/shared/brand"; +import { externalMapConsentCopy } from "@/ui/i18n/ru-sections/osm"; + +const TURN_OFF_PRODUCT = `Отключить ${BRAND_DISPLAY_NAME}`; + +export const advanced = { + runtimeTitle: "Расширенные параметры", + runtimeDescription: "Настройте подробное журналирование работы расширения.", + copyLinkRuntimeLabel: "расширенные настройки", + copyLinkHelpLabel: "расширенная помощь", + + noiseRadius: { + title: "Максимальный радиус координат по умолчанию", + description: + "Установите максимальный радиус координат по умолчанию в метрах для новых региональных профилей.", + copyLinkLabel: "максимальный радиус координат по умолчанию", + }, + + generatedLocationRandomization: { + enabled: { + title: "Рандомизировать координаты при создании новых региональных профилей", + description: + "Этот параметр включён по умолчанию для новых профилей. При создании профиля его можно отключить, чтобы использовать точные координаты.", + readWhy: "Зачем это нужно?", + tooltipPrivacy: `${BRAND_DISPLAY_NAME} помогает защищать приватность. Если все пользователи выбирают одни и те же точные координаты из каталога или поиска, сайты могут легче их распознавать.`, + tooltipExact: + "Вам нужно конкретное место? Выключите этот переключатель, и в профиле будут использоваться точные координаты.", + copyLinkLabel: "случайное смещение координат новых профилей", + }, + radius: { + title: "Радиус случайного смещения координат по умолчанию", + description: + "Выберите радиус по умолчанию, используемый при включенной рандомизации координат. Введите целое число от 1 до 99 км.", + inputLabel: "Радиус случайного смещения координат по умолчанию в километрах", + copyLinkLabel: "радиус случайного смещения координат по умолчанию", + }, + }, + + themeMode: { + title: "Тема", + description: `Выберите системную, светлую или тёмную тему для ${BRAND_DISPLAY_NAME}.`, + label: "Внешний вид", + options: { + system: "Система", + light: "Светлая", + dark: "Тёмная", + }, + copyLinkLabel: "тема", + }, + + watchPositionDelay: { + title: "Интервал обновления геолокации (с)", + description: + "Установите самую короткую и самую длинную задержку между обновлениями местоположения на веб-сайтах.", + copyLinkLabel: "интервал обновления геолокации", + }, + + debugMode: { + title: "Режим отладки", + description: `Выводить подробные сведения о работе ${BRAND_DISPLAY_NAME} в консоль разработчика браузера.`, + copyLinkLabel: "режим отладки", + }, + + experimental: { + title: "Экспериментальные функции", + description: + "Попробуйте защиту для API-интерфейсов браузера, которые все еще внедряются. Эти переключатели доступны в каждом канале выпуска.", + copyLinkLabel: "экспериментальные настройки", + temporalApi: { + title: "Temporal API", + description: + "Подменять дату, время, локаль и часовой пояс Temporal, если браузер поддерживает этот API. Полифил не устанавливается.", + copyLinkLabel: "Temporal API", + }, + domainFencing: { + title: "Изоляция по доменам", + description: + "Когда применяется правило по умолчанию или контейнер Firefox, каждый сайт получает свой собственный стабильный отпечаток. Ваш региональный профиль останется прежним.", + copyLinkLabel: "изоляция по доменам", + }, + }, + + privacy: { + title: "Конфиденциальность", + description: `${BRAND_DISPLAY_NAME} работает на вашем устройстве и никуда не отправляет данные о вашем просмотре. Единственные дополнительные сетевые запросы происходят, когда вы используете поиск местоположения или предварительный просмотр карты при настройке региональных профилей.`, + copyLinkLabel: "конфиденциальность", + + osmConsent: { + title: externalMapConsentCopy.title, + description: externalMapConsentCopy.description, + stateUnknown: `Разрешение ещё не выбрано. ${BRAND_DISPLAY_NAME} спросит перед обращением к внешнему картографическому сервису.`, + statePrefix: "Доступ к внешним картам", + stateEnabled: "разрешён — поиск мест и просмотр карты доступны", + stateDisabled: `запрещён — ${BRAND_DISPLAY_NAME} не будет обращаться к картографическим сервисам при настройке мест`, + copyLinkLabel: "доступ к внешней карте", + }, + }, + + display: { + title: "Внешний вид", + description: "Настройте параметры визуального представления и доступности.", + copyLinkLabel: "внешний вид", + + language: { + title: "Язык", + description: `${BRAND_DISPLAY_NAME} автоматически следует языку интерфейса вашего браузера.`, + optionEnglish: "Английский", + optionSpanish: "Испанский", + optionPortuguese: "Португальский", + optionRussian: "Русский", + optionUkrainian: "Украинский", + soon: "АВТО", + copyLinkLabel: "язык", + }, + + reduceMotion: { + title: "Уменьшение анимации", + description: `Отключите анимацию интерфейса в ${BRAND_DISPLAY_NAME}.`, + systemOverride: + "Ограничение движения включено настройкой специальных возможностей вашей системы.", + copyLinkLabel: "уменьшить движение", + }, + + accentColor: { + title: "Акцентный цвет", + description: `Выберите основной акцент ${BRAND_DISPLAY_NAME} из палитры по мотивам цветов контейнеров Firefox.`, + copyLinkLabel: "акцентный цвет", + optionAriaLabel: (label: string) => + `Используйте ${label} в качестве акцентного цвета`, + options: { + teal: "Бирюзовый", + blue: "Синий", + green: "Зеленый", + yellow: "Желтый", + orange: "Оранжевый", + red: "Красный", + pink: "Розовый", + purple: "Фиолетовый", + gray: "Серый", + }, + }, + + highContrast: { + title: "Режим высокой контрастности", + description: + "Увеличьте контрастность текста и видимость границ для улучшения читаемости.", + copyLinkLabel: "режим высокой контрастности", + }, + }, + + danger: { + title: "Опасная зона", + description: `Эти действия могут заменить, удалить или восстановить локально сохраненные настройки и данные ${BRAND_DISPLAY_NAME}.`, + copyLinkLabel: "опасная зона", + + spoofing: { + title: TURN_OFF_PRODUCT, + description: `Отключить все механизмы защиты ${BRAND_DISPLAY_NAME}, пока не включите их снова.`, + copyLinkLabel: TURN_OFF_PRODUCT, + }, + + export: { + title: "Экспорт", + description: + "Сохраните региональные профили, правила и другие настройки в виде резервной копии JSON.", + button: "Экспорт настроек", + copyLinkLabel: "настройки экспорта", + }, + + import: { + title: "Импорт", + description: + "Замените текущие локальные настройки ранее экспортированной резервной копией JSON.", + button: "Импортировать настройки", + copyLinkLabel: "импортировать настройки", + }, + + reload: { + title: "Загрузить заново", + description: + "Загрузите настройки из хранилища расширения заново и отмените несохранённые изменения.", + button: "Обновить настройки", + copyLinkLabel: "перезагрузить настройки", + }, + + reset: { + title: "Сбросить", + description: + "Верните исходные настройки и удалите сохранённые профили и правила.", + button: "Сбросить настройки", + copyLinkLabel: "сбросить настройки", + confirmTitle: "Сбросить настройки?", + confirmBody: + "При этом сохраненные профили, правила и пользовательские настройки будут удалены без возможности восстановления. Это невозможно отменить.", + onboardingToggleLabel: "Повторно открыть первоначальную настройку после сброса", + onboardingToggleDescription: + "После сброса снова откроется мастер первоначальной настройки.", + }, + }, + + help: { + title: "Дополнительно", + body1: `Используйте «Дополнительно», чтобы настроить защиту, временно отключить ${BRAND_DISPLAY_NAME} или управлять локально сохраненными настройками и данными.`, + body2: + "Импорт и экспорт помогают переносить настройки между браузерами. Повторная загрузка отменяет несохранённые изменения, а сброс возвращает исходные настройки.", + }, +} as const; diff --git a/src/ui/i18n/ru-sections/common.ts b/src/ui/i18n/ru-sections/common.ts new file mode 100644 index 0000000..841c31d --- /dev/null +++ b/src/ui/i18n/ru-sections/common.ts @@ -0,0 +1,42 @@ +import { BRAND_DISPLAY_NAME } from "@/shared/brand"; + +export const common = { + copyLinkTo: (section: string) => `Скопировать ссылку: ${section}`, + actions: { + cancel: "Отмена", + reset: "Сбросить", + close: "Закрыть", + create: "Создать", + save: "Сохранить", + back: "Назад", + continue: "Продолжить", + edit: "Изменить", + delete: "Удалить", + duplicate: "Дублировать", + search: "Поиск", + clear: "Очистить", + deleteSelected: "Удалить выбранное", + clearSelection: "Снять выделение", + openPrivacyPolicy: "Открыть политику конфиденциальности", + allowOpenStreetMap: "Разрешить доступ к карте", + notNow: "Не сейчас", + openPlayground: "Открыть песочницу", + }, + selectionCount: (count: number) => `Выбрано: ${count}`, + fields: { + name: "Название", + latitude: "Широта", + longitude: "Долгота", + accuracy: "Точность", + noiseRadius: "Макс. радиус (м)", + timeZone: "Часовой пояс", + }, + coordinateRandomization: { + labelBefore: "Случайное смещение координат в пределах", + labelAfter: "км", + radiusInputLabel: "Радиус случайного смещения координат в километрах", + readWhy: "Зачем это нужно?", + tooltipPrivacy: `${BRAND_DISPLAY_NAME} защищает приватность. Профилями должно быть удобно пользоваться, но точное повторение координат из каталога или поиска может выделить пользователей ${BRAND_DISPLAY_NAME} среди других.`, + tooltipExact: `Нужна точная точка? Выключите этот переключатель, и ${BRAND_DISPLAY_NAME} будет использовать сохранённые координаты без смещения. Значения по умолчанию можно изменить в разделе «Настройки» → «Геолокация» → «Дополнительно».`, + }, +} as const; diff --git a/src/ui/i18n/ru-sections/demo.ts b/src/ui/i18n/ru-sections/demo.ts new file mode 100644 index 0000000..8ef810e --- /dev/null +++ b/src/ui/i18n/ru-sections/demo.ts @@ -0,0 +1,152 @@ +import { BRAND_DISPLAY_NAME } from "@/shared/brand"; + +const SPOOFED_PRODUCT = `Подменено — ${BRAND_DISPLAY_NAME}`; + +export const demo = { + loadingSettings: "Загрузка настроек…", + noLocationsTitle: "Пока нет региональных профилей", + noLocationsBody: + "Создайте хотя бы один региональный профиль в настройках, чтобы открыть песочницу.", + openSettingsButton: "Открыть настройки", + + locationPreview: { + title: "Предпросмотр профиля", + activeLocationLabel: "Активный профиль", + activeLocationDescription: + "Выберите сохранённый региональный профиль, чтобы увидеть его значения. При смене профиля путь сбрасывается, и геолокация начинает обновляться для нового места.", + activeLocationPlaceholder: "Выберите профиль…", + playgroundCadenceLabel: "Интервал обновления в песочнице", + realSiteCadenceLabel: "Интервал обновления на сайте", + configuredDelayLabel: "Задержка watchPosition", + callbackDelayLabel: "Задержка ответа", + runtimeModeLabel: "Режим работы", + runtimeModeSimple: "Стандартный интервал", + realLocationTitleIdle: "Сравните с текущим местоположением вашего браузера", + realLocationTitleLoading: "Запрос вашего текущего местоположения в браузере", + realLocationTitleGranted: "Текущее местоположение браузера готово", + realLocationTitleDenied: + "Разрешение на определение местоположения браузера отклонено", + realLocationTitleUnavailable: "Местоположение браузера недоступно", + realLocationDescription: + "Запросите реальную геолокацию браузера один раз, чтобы сравнить ее с выбранным предварительным просмотром ниже.", + realLocationGrantedDescription: + "Текущее местоположение вашего браузера теперь доступно в строках геолокации ниже для параллельного сравнения.", + realLocationDeniedDescription: + "Браузер запретил доступ к реальной геолокации. Вы можете попробовать еще раз, если разрешение изменится.", + realLocationUnavailable: + "Сравнение реального местоположения недоступно, поскольку этот браузер не отображает здесь геолокацию.", + realLocationRefresh: "Обновить реальное местоположение", + }, + + localMachineTitle: "Этот браузер", + spoofedTitle: SPOOFED_PRODUCT, + waitingForPermission: "Ожидание разрешения браузера…", + permissionDenied: "Разрешение отклонено браузером", + geolocationUnavailable: "Геолокация недоступна", + requestRealLocation: "Запросить реальное местоположение", + requestRealLocationHintTitle: "Реальное местоположение еще не загружено", + requestRealLocationTableHint: + "Запросите реальное местоположение вашего браузера выше, чтобы сравнить его здесь.", + waitingForFix: "Ожидание первых координат…", + selectLocationPrompt: + "Выберите региональный профиль выше, чтобы увидеть его значения.", + + comparison: { + language: "navigator.language", + languages: "navigator.languages", + timeZone: "Intl…resolvedOptions().timeZone", + acceptLanguage: "Заголовок Accept-Language", + timeZoneOffset: "new Date().getTimezoneOffset()", + dateToString: "new Date().toString()", + dateToDateString: "new Date().toDateString()", + dateToTimeString: "new Date().toTimeString()", + dateLocaleString: "new Date().toLocaleString()", + dateLocaleDateString: "new Date().toLocaleDateString()", + dateLocaleTimeString: "new Date().toLocaleTimeString()", + currentPosition: "navigator.geolocation.getCurrentPosition()", + coords: "geolocation.coords", + timestamp: "geolocation.timestamp", + timestampInfoLabel: "Об отображении метки времени геолокации", + timestampTooltip: `${BRAND_DISPLAY_NAME} возвращает необработанную временную метку эпохи Unix в миллисекундах. Удобочитаемая дата, показанная здесь, предназначена только для удобства и отформатирована в соответствии с часовым поясом отображаемого столбца.`, + userAgent: "navigator.userAgent", + appVersion: "navigator.appVersion", + vendor: "navigator.vendor", + hardwareConcurrency: "navigator.hardwareConcurrency", + deviceMemory: "navigator.deviceMemory", + platform: "navigator.platform", + pixelDepth: "screen.pixelDepth", + screenMetrics: "screen.width/height/avail*/colorDepth", + devicePixelRatio: "window.devicePixelRatio", + canvas2d: "Результат проверки Canvas 2D", + webglRenderer: "Проверка рендерера WebGL", + webglDebugExtension: "WEBGL_debug_renderer_info", + webglReadPixels: "Проверка WebGL readPixels()", + audioFingerprint: "Проверка AnalyserNode и AudioBuffer", + clientHintBrands: "navigator.userAgentData.brands", + clientHintPlatform: "navigator.userAgentData.platform", + clientHintPlatformVersion: "navigator.userAgentData.platformVersion", + clientHintArchitecture: "navigator.userAgentData.architecture", + clientHintBitness: "navigator.userAgentData.bitness", + clientHintModel: "navigator.userAgentData.model", + clientHintMobile: "navigator.userAgentData.mobile", + clientHintFullVersionList: "navigator.userAgentData.fullVersionList", + secChUa: "Заголовок Sec-CH-UA", + secChUaPlatform: "Заголовок Sec-CH-UA-Platform", + secChUaMobile: "Заголовок Sec-CH-UA-Mobile", + secChUaFullVersionList: "Заголовок Sec-CH-UA-Full-Version-List", + webRTCIcePolicy: "Политика RTCPeerConnection ICE", + probePending: "Сбор данных…", + notAvailable: "Н/Д", + spoofedMatchesLocal: + "Для этой идентичности значение предпросмотра совпадает с данными вашего браузера.", + browserVersionNote: (versionToken: string) => + `${BRAND_DISPLAY_NAME} сохраняет стандартные обозначения версии браузера и не изменяет заполнители вроде ${versionToken}.`, + }, + + previewSeed: { + title: "Предварительный просмотр идентичности", + description: "Выберите идентичность браузера для этого предварительного просмотра.", + inputAriaLabel: "Код идентичности для предварительного просмотра", + placeholder: "Код идентичности", + hint: "Используйте 6 строчных букв или цифр. Измените код, чтобы просмотреть другую идентичность.", + randomize: "Создать новую идентичность", + }, + + sections: { + localeDate: "Локаль и Date", + networkHeaders: "Сетевые заголовки", + geolocation: "Геолокация", + browserFingerprint: "Цифровой отпечаток браузера", + webglCanvas: "Canvas и WebGL", + screen: "Экран", + audio: "Аудио", + webRTC: "WebRTC", + }, + + map: { + title: "Предварительный просмотр карты", + noLocationTitle: "Сначала выберите региональный профиль", + noLocationDescription: + "Выберите сохраненный региональный профиль выше, чтобы включить элементы управления картой и путевыми точками.", + osmRequired: "Требуется доступ к карте", + osmRequiredDescription: + "Разрешите загрузку внешних карт, чтобы увидеть подменённое положение на интерактивной карте.", + demoIntervalLabel: "Демонстрационный интервал (2–5 с)", + clearButton: "Очистить", + }, + + disclaimer: { + title: "Это предварительный просмотр", + body: `Здесь можно проверить, какие данные ${BRAND_DISPLAY_NAME} покажет сайтам для выбранного регионального профиля. Язык, локаль, время, заголовки и другие значения отображаются как предварительный просмотр на основе данных профиля. Геолокация работает так же, как на защищённых сайтах, но в песочнице координаты обновляются чаще, чтобы изменения было легче заметить.`, + }, + + howItWorks: { + title: "Как пользоваться предварительным просмотром", + body1: + "Язык, локаль и время — здесь показаны значения, которые увидят сайты для выбранного профиля. Сама страница песочницы не меняется.", + body2: `Геолокация — карта и координаты показывают движение и частоту обновления, которые будут действовать на защищённых сайтах.`, + body3: `Данные браузера и заголовки — сравните настоящие данные браузера со значениями, которые ${BRAND_DISPLAY_NAME} покажет для выбранного профиля с учётом ваших настроек защиты.`, + body4: + "В песочнице данные обновляются чаще, чем на обычных сайтах, чтобы изменения было легче заметить.", + }, +} as const; diff --git a/src/ui/i18n/ru-sections/firefox-containers.ts b/src/ui/i18n/ru-sections/firefox-containers.ts new file mode 100644 index 0000000..ba0b1c3 --- /dev/null +++ b/src/ui/i18n/ru-sections/firefox-containers.ts @@ -0,0 +1,101 @@ +import { BRAND_DISPLAY_NAME } from "@/shared/brand"; + +export const firefoxContainers = { + copyLinkLabel: "контейнеры", + copyLinkHelpLabel: "контейнеры помогают", + title: "Контейнеры", + description: `Управляйте контейнерами Firefox и при необходимости назначьте каждому региональный профиль ${BRAND_DISPLAY_NAME}.`, + refreshTitle: "Обновить список контейнеров", + refreshButton: "Обновить список", + createButton: "Новый контейнер", + deleteButton: "Удалить", + removeAssignmentButton: "Удалить назначение", + editActionTitle: (name: string) => `Редактировать контейнер «${name}»`, + deleteActionTitle: (name: string) => `Удалить контейнер «${name}»`, + removeAssignmentActionTitle: (name: string) => + `Удалить назначение профиля для удаленного контейнера «${name}»`, + showInactiveLabel: "Показать неактивные контейнеры", + filterPlaceholder: "Искать контейнеры...", + loading: "Загрузка контейнеров...", + noResults: "Ни один контейнер не соответствует текущему фильтру.", + columns: { + container: "Контейнер", + location: "Профиль", + actions: "Действия", + }, + noLocationAssigned: "Профиль не назначен", + missingLocation: "Назначенный профиль отсутствует", + inactiveBadge: "Удалено", + deletedContainerLabel: "Удаленный контейнер", + emptyTitle: "Контейнеров Firefox пока нет", + emptyBody: `Создайте свой первый контейнер здесь или в настройках Firefox, затем назначьте ему профиль ${BRAND_DISPLAY_NAME}.`, + unavailableTitle: "Контейнеры Firefox недоступны", + unavailableBody: + "Включите Firefox Containers, затем обновите список. Если проблема не исчезнет, эта сборка Firefox может не позволить расширению управлять ими.", + unavailableHint: `${BRAND_DISPLAY_NAME} может управлять контейнерами только тогда, когда включены контейнеры Firefox.`, + editor: { + createTitle: "Создать контейнер", + editTitlePrefix: "Редактировать контейнер", + createDescription: `Выберите имя, цвет, значок и дополнительные настройки ${BRAND_DISPLAY_NAME} для нового контейнера Firefox.`, + editDescription: `Обновите свойства контейнера Firefox и его дополнительные настройки ${BRAND_DISPLAY_NAME} за одно сохранение.`, + saveButton: "Сохранить", + previewUntitled: "Контейнер без названия", + nameRequired: "Имя контейнера не может быть пустым.", + defaultRulePreviewTitle: "Сохраненных настроек пока нет.", + defaultRulePreviewLocation: (label: string) => + `У этого контейнера пока нет собственных настроек ${BRAND_DISPLAY_NAME}, поэтому он использует профиль правила по умолчанию: ${label}. Сохраните настройки, чтобы задать контейнеру собственный профиль.`, + defaultRulePreviewProtections: `У этого контейнера пока нет собственных настроек ${BRAND_DISPLAY_NAME}, поэтому он использует только защиту по правилу по умолчанию, для которой не требуется региональный профиль. Сохраните настройки, чтобы задать контейнеру собственный профиль.`, + defaultRulePreviewUnconfigured: `У этого контейнера пока нет собственных настроек ${BRAND_DISPLAY_NAME}. Правило по умолчанию включено, но ему ещё не назначены профиль или особые настройки защиты. Сохраните настройки контейнера, чтобы задать контейнеру собственные настройки, или завершите настройку правила по умолчанию.`, + defaultRulePreviewDisabled: `У этого контейнера пока нет собственных настроек ${BRAND_DISPLAY_NAME}. Правило по умолчанию отключено, поэтому ${BRAND_DISPLAY_NAME} здесь остается отключенным. Сохраните настройки контейнера, чтобы задать контейнеру собственные настройки, или включите правило по умолчанию.`, + defaultRulePreviewMissing: `У этого контейнера пока нет собственных настроек ${BRAND_DISPLAY_NAME}. Сохраненная настройка правила по умолчанию недоступна, поэтому ${BRAND_DISPLAY_NAME} здесь остается отключенным. Сохраните настройки контейнера, чтобы задать контейнеру собственные настройки, или установите правило по умолчанию.`, + fields: { + name: "Имя", + color: "Цвет", + icon: "Значок", + enabled: "Включено", + location: "Профиль", + locationProfile: "региональный профиль", + }, + unassignedLocation: "Профиль не назначен", + enabledHint: + "Если этот параметр отключен, это назначение контейнера не применяется. Его настройки сохраняются.", + locationProfileHint: + "Выберите профиль, который должен использовать этот контейнер. Оставьте его неназначенным, чтобы использовать только параметры защиты этого контейнера и правило по умолчанию, если он доступен.", + identity: { + sectionTitle: "Цифровая идентичность", + sectionDescription: + "Этот контейнер сохраняет собственную цифровую идентичность для подмены. Меняйте её только тогда, когда нужен новый цифровой отпечаток и чистое состояние связанных сайтов.", + actionDescription: + "Удаляет связанные данные сайтов и создаёт для контейнера новую цифровую идентичность.", + actionLabel: "Новая идентичность", + confirmTitle: (name: string) => `Создать новую идентичность для «${name}»?`, + confirmDescription: + "При этом будут очищены файлы cookie, хранилище, Service Workers и кеши сайтов, привязанных к этому контейнеру. Затем будет создана новая цифровая идентичность.", + confirmDomainsLabel: `${BRAND_DISPLAY_NAME} удалит данные браузера для этих доменов:`, + confirmNoDomains: `Для этого контейнера пока не записано никаких данных браузера. ${BRAND_DISPLAY_NAME} по-прежнему будет создавать новую идентичность для подмены.`, + confirmLabel: "Создать новую идентичность", + rotateSuccess: "Для контейнера создана новая идентичность.", + rotateError: "Не удалось создать новую идентичность для контейнера.", + }, + }, + confirmations: { + deleteTitle: (name: string) => `Удалить контейнер «${name}»?`, + deleteBody: `Это удалит контейнер Firefox и очистит его назначение профиля ${BRAND_DISPLAY_NAME}.`, + removeOrphanTitle: "Удалить устаревшее назначение контейнера?", + removeOrphanBody: `Контейнер Firefox больше не существует. Это очистит только устаревшее назначение профиля ${BRAND_DISPLAY_NAME}.`, + }, + messages: { + created: (name: string) => `Создан контейнер «${name}».`, + updated: (name: string) => `Сохранён контейнер «${name}».`, + deleted: (name: string) => `Удалён контейнер «${name}».`, + orphanRemoved: "Удалено назначение устаревшего контейнера.", + saveFailed: "Не удалось сохранить контейнер.", + deleteFailed: "Удаление контейнера не удалось.", + }, + help: { + title: "Контейнеры", + body1: `Контейнеры Firefox позволяют выбирать настройки для отдельных групп вкладок. ${BRAND_DISPLAY_NAME} сначала проверяет правила для доменов, затем назначения контейнеров.`, + body2: `Редактор контейнеров сохраняет сведения о контейнере Firefox вместе с настройками ${BRAND_DISPLAY_NAME}, такими как включенное состояние, необязательный региональный профиль и особые настройки защиты.`, + body3: `Неактивные строки — это назначения для контейнеров Firefox, удалённых вне ${BRAND_DISPLAY_NAME}. Проверьте эти записи и удалите ненужные вручную.`, + }, +} as const; diff --git a/src/ui/i18n/ru-sections/locations.ts b/src/ui/i18n/ru-sections/locations.ts new file mode 100644 index 0000000..bc5454a --- /dev/null +++ b/src/ui/i18n/ru-sections/locations.ts @@ -0,0 +1,109 @@ +import { BRAND_DISPLAY_NAME } from "@/shared/brand"; +import { slavicCount } from "@/ui/shared/slavic-plural"; + +const PRODUCT_NOT_LOWER = `чем ${BRAND_DISPLAY_NAME} не является`; + +export const locations = { + title: "Региональные профили", + description: + "Региональные профили объединяют координаты, локаль и часовой пояс для подмены.", + addManualButton: "Добавить вручную", + actionsMenuLabel: "Действия с профилями", + generateButton: "Создать профиль", + searchPlaceholder: "Поиск профилей...", + unused: "Не используется", + assigned: (count: number) => + slavicCount(count, ["назначение", "назначения", "назначений"]), + viewAssignedRulesAriaLabel: (locationLabel: string, count: number) => + `Показать ${slavicCount(count, ["правило", "правила", "правил"])} для профиля «${locationLabel}»`, + copyLinkLabel: "региональные профили", + copyLinkHelpLabel: "справка по региональным профилям", + + playgroundCard: { + title: "Песочница", + body1: + "Проверьте сохранённый профиль перед назначением сайтам. Песочница показывает регион, часовой пояс, формат дат и подменённые координаты так же, как их увидят защищённые сайты.", + body2: + "В песочнице координаты обновляются каждые 2–5 секунд, чтобы движение было легко проверить. На обычных сайтах действует заданный вами интервал.", + }, + + help: { + title: "Региональные профили", + body1: + "Региональный профиль должен описывать одно правдоподобное место. Локаль, список языков и часовой пояс должны соответствовать региону сохранённых координат.", + body2: `Изменения сохраняются автоматически. Когда вы редактируете поле, ${BRAND_DISPLAY_NAME} записывает обновление после небольшой паузы.`, + body3: + "Сохраненный региональный профиль изменяет значения на уровне браузера, такие как геолокация, локаль и часовой пояс. Он не меняет ваш IP-адрес или маршрут сетевого трафика.", + privacyTitle: "Конфиденциальность", + privacyBody: `Поиск и предварительный просмотр карты не являются обязательными. ${BRAND_DISPLAY_NAME} связывается с OpenStreetMap Nominatim для поиска и с OpenFreeMap для интерактивного предварительного просмотра карт только после того, как вы разрешите это.`, + networkTitle: "Сетевые ограничения", + networkBodyPrefix: + "Нужно изменить местоположение на уровне сети? Используйте VPN, прокси или DNS. Узнайте, ", + networkBodyLinkLabel: PRODUCT_NOT_LOWER, + networkBodySuffix: ".", + }, + + editor: { + title: "Редактировать профиль", + description: + "Измените координаты, региональные форматы и поведение геолокации в сохранённом профиле.", + deleteBlockedTitle: "Этот профиль все еще назначен", + deleteBlockedDescription: + "Прежде чем удалять профиль, измените или удалите каждое назначение ниже.", + deleteBlockedButtonTitle: + "Прежде чем удалять этот профиль, удалите все назначения.", + disabledDependencySuffix: "(отключено)", + mapDisabledTitle: "Карта отключена", + mapDisabledBody: + "Предварительный просмотр карты не загружается, поскольку вы не разрешили внешние запросы карт.", + geolocationSectionTitle: "Геолокация", + geolocationSectionDescription: + "Координаты, точность и максимальный разброс, разрешенный для подменённых позиций.", + localeSectionTitle: "Время и язык", + localeSectionDescription: + "Локаль и языковые настройки должны соответствовать региону сохранённых координат.", + primaryLocaleLabel: "Основная локаль", + languageDescription: + "Основная локаль определяет формат дат, чисел и других региональных значений.", + languageBehaviorDescription: + "Когда настройка «Предпочитать английский на сайтах» выключена, сайты видят это значение в navigator.language. Даже если английский стоит первым в списке языков, этот профиль по-прежнему определяет формат дат и чисел по умолчанию.", + preferredLanguagesLabel: "Предпочитаемые языки", + languagesDescription: + "Упорядоченный список языковых настроек браузера для этого профиля.", + languagesBehaviorDescription: `${BRAND_DISPLAY_NAME} отображает этот порядок как navigator.languages и связанные с ним языковые настройки. Сначала сохраните основной языковой стандарт, а затем добавьте реалистичные альтернативные языки, которые сайт мог бы видеть для этого профиля.`, + preferEnglishContentLabel: "Предпочитать английский на сайтах", + preferEnglishContentDescriptionPrefix: `${BRAND_DISPLAY_NAME} сохраняет выбранный регион, но ставит`, + preferEnglishContentDescriptionSuffix: + "на первое место в списке языков браузера, чтобы сайты чаще показывали содержимое на английском.", + preferEnglishContentLockedTagTitle: (locale: string) => + `${locale} добавляется из-за приоритета английского языка, поэтому удалить его здесь нельзя.`, + accuracyDescription: + "Управляет значением точности, которое веб-сайты видят в результатах геолокации.", + noiseRadiusDescription: + "Максимально допустимое расстояние между подменёнными координатами и сохраненной точкой этого профиля.", + }, + + generator: { + title: "Создать профиль", + searchStepDescription: + "Найдите город, адрес или название места, чтобы создать новый региональный профиль.", + resultStepDescription: `Выберите подходящий результат, прежде чем ${BRAND_DISPLAY_NAME} создаст профиль.`, + languageStepDescription: + "Сначала выберите основную локаль браузера для этого профиля.", + confirmStepDescription: "Проверьте место на карте, прежде чем добавлять профиль.", + locationLabel: "Место", + locationPlaceholder: "Варшава, Польша", + osmDisclaimer: + "Поисковые запросы используют внешний API OpenStreetMap Nominatim. В интерактивном предварительном просмотре после согласия используются векторные плитки и шрифты OpenFreeMap.", + resultSelectLabel: "Результат поиска", + resultStepBody: + "OpenStreetMap вернул более одного возможного совпадения. Выберите место, которое вы имели в виду, и продолжайте.", + resultStepHint: "Выберите результат, чтобы продолжить.", + languageSelectLabel: "Язык браузера", + languageStepBody: + "Для этого места доступно несколько языков браузера. Выберите нужный для профиля.", + languageStepHint: + "Если нужный вам язык не указан ниже, вы можете выбрать другой на следующем шаге.", + resultPrefix: "Результат: ", + }, +} as const; diff --git a/src/ui/i18n/ru-sections/options.ts b/src/ui/i18n/ru-sections/options.ts new file mode 100644 index 0000000..74b366b --- /dev/null +++ b/src/ui/i18n/ru-sections/options.ts @@ -0,0 +1,125 @@ +import { workerHandlingModeCopy } from "./shared-worker"; + +import { BRAND_DISPLAY_NAME } from "@/shared/brand"; +import { slavicCount } from "@/ui/shared/slavic-plural"; + +export const options = { + title: "Настройки", + tabsAriaLabel: "Разделы настроек", + spoofingOffBannerTitle: "Подмена отключена", + spoofingOffBannerBody: `${BRAND_DISPLAY_NAME} сейчас не применяет подменённое местоположение или значения браузера.`, + spoofingOffBannerAction: "Включить подмену", + spoofingTurnedOffToast: `${BRAND_DISPLAY_NAME} отключён.`, + spoofingTurnedOnToast: `${BRAND_DISPLAY_NAME} включён.`, + + tabs: { + locations: "Региональные профили", + rules: "Правила для доменов", + trustedSites: "Доверенные сайты", + playground: "Песочница", + options: "Настройки", + advanced: "Дополнительно", + about: "О расширении", + }, +} as const; + +export const optionsPage = { + browserFingerprintSpoofing: { + title: "Глобальные настройки защиты", + description: `Выберите, какие средства защиты браузера ${BRAND_DISPLAY_NAME} использует по умолчанию. Более конкретные настройки могут переопределять отдельные средства защиты, а «Доверенные сайты» могут отключать их для соответствующих сайтов.`, + disabledNote: `Защита браузера на всех сайтах отключена. Включите её, чтобы настроить подмену данных по умолчанию.`, + copyLinkLabel: "защита браузера", + items: { + geolocation: { + label: "Геолокация", + description: `Управляет Geolocation API. Когда этот параметр включен, ${BRAND_DISPLAY_NAME} отвечает на запросы геолокации на основе активного регионального профиля. Если отключить защиту, сайты получат реальные результаты геолокации браузера.`, + advancedButton: "Дополнительно", + advancedModal: { + title: "Расширенные настройки геолокации", + description: + "Настройте радиус координат по умолчанию и частоту обновления местоположения.", + }, + }, + timeLocale: { + label: "Время и локаль", + description: `Управляет Date, Intl, navigator.language, navigator.languages и языковыми заголовками запросов. Когда эта опция включена, ${BRAND_DISPLAY_NAME} согласует эти значения с вашим активным профилем. Если отключить защиту, сайты увидят реальные региональные настройки вашего браузера.`, + }, + canvas: { + label: "Canvas", + description: `Сайты могут отображать скрытое изображение и использовать небольшие различия в рендеринге для распознавания вашего браузера. Когда эта опция включена, ${BRAND_DISPLAY_NAME} добавляет контролируемый шум, поэтому результат сложнее повторно использовать в качестве цифрового отпечатка. Если отключить защиту, сайты получат исходный результат Canvas.`, + }, + webGL: { + label: "WebGL", + description: `WebGL раскрывает детали графики, такие как модель видеокарты, рендерер и поведение драйвера. Когда эта функция включена, ${BRAND_DISPLAY_NAME} заменяет эти подсказки согласованным профилем, соответствующим вашей текущей платформе. Если отключить защиту, сайты получат графические данные, более близкие к реальным.`, + }, + audio: { + label: "Аудио", + description: `Аудио API выдают едва заметные различия, связанные с оборудованием, которые сайты могут измерять в фоновом режиме. Когда эта опция включена, ${BRAND_DISPLAY_NAME} слегка изменяет эти значения, поэтому они менее пригодны для отслеживания. Если отключить защиту, параметры звука останутся без изменений.`, + }, + navigator: { + label: "Navigator", + description: `Поля Navigator раскрывают такие сведения, как ваша платформа, языковые настройки, сведения о процессоре и другие идентификационные данные браузера. Когда этот параметр включен, ${BRAND_DISPLAY_NAME} подменяет отдельные идентифицирующие поля, сохраняя при этом их внутреннюю согласованность. Если отключить защиту, сайты увидят больше информации о вашем реальном браузере.`, + }, + screen: { + label: "Экран", + description: `Размер экрана, плотность пикселей и глубина цвета помогают отличить ваше устройство от других. Когда эта функция включена, ${BRAND_DISPLAY_NAME} маскирует именно эти детали отображения. Если отключить защиту, сайты получат настоящие параметры экрана.`, + }, + clientHints: { + label: "Client Hints", + description: `Client Hints передаёт данные о версии браузера, платформе и устройстве через заголовки запросов и API JavaScript. Когда этот параметр включен, ${BRAND_DISPLAY_NAME} согласует эти значения с вашими подменёнными данными браузера. Если отключить защиту, сайты получат информацию о вашем реальном браузере и платформе.`, + }, + battery: { + label: "Аккумулятор", + description: `Изменения состояния аккумулятора могут помочь сайтам связать разные посещения с одним устройством. Когда эта функция включена, ${BRAND_DISPLAY_NAME} сообщает о полностью заряженном аккумуляторе, подключённом к питанию, а не о реальном состоянии устройства. Если отключить защиту, сайты смогут считывать исходный Battery Status API.`, + }, + clientHintsVersionRotation: { + label: "Менять номера сборки и патча", + description: (count: number) => + `Меняет номера сборки и патча Chromium, передаваемые через Client Hints, сохраняя основную версию установленного браузера. В каталоге ${BRAND_DISPLAY_NAME} есть недавние сборки Chromium; для этой версии браузера доступно ${slavicCount(count, ["подходящее значение", "подходящих значения", "подходящих значений"])}. Сокращённая строка User-Agent остаётся в стандартном для браузера формате .0.0.0.`, + hintPrefix: "например:", + hint: "139.0.[build].[patch]", + }, + webRTC: { + label: "WebRTC", + description: `WebRTC может раскрывать локальные или публичные IP-адреса, даже если вы используете VPN или прокси. Когда этот параметр включен, ${BRAND_DISPLAY_NAME} просит браузер использовать самый строгий режим обработки IP-адресов. Если отключить защиту, WebRTC вернётся к обычному режиму и может раскрывать больше сетевых данных.`, + }, + serviceWorker: { + label: "Service Workers", + description: `Service Workers работают в фоновом режиме и позволяют сайтам хранить долговременные данные и идентификаторы. Включите этот параметр, чтобы ${BRAND_DISPLAY_NAME} по умолчанию блокировал регистрацию Service Workers на всех сайтах.`, + warning: + "Блокировка Service Workers может нарушить работу устанавливаемых веб-приложений (PWA), автономного режима, push-уведомлений и фоновой синхронизации. Некоторые приложения также могут работать медленнее или терять функции.", + defaultState: + "По умолчанию блокировка отключена. Её можно включить для всех сайтов, а затем разрешить регистрацию Service Workers для отдельных доменов с помощью правил.", + allow: "Разрешить", + block: "Блокировать", + }, + sharedWorker: { + label: "Dedicated и Shared Workers", + descriptionLead: + "Выберите способ запуска Dedicated и Shared Workers по умолчанию. Правила для доменов и контейнеры Firefox могут переопределять эту политику для соответствующих сайтов.", + copyLinkLabel: "обработка Dedicated и Shared Workers", + native: workerHandlingModeCopy.native.label, + nativeDescription: workerHandlingModeCopy.native.description, + spoof: workerHandlingModeCopy.spoof.label, + spoofDescription: workerHandlingModeCopy.spoof.description, + strict: workerHandlingModeCopy.strict.label, + strictDescription: workerHandlingModeCopy.strict.description, + }, + }, + }, + badgeQueryCount: { + label: "Показывать число вызовов на значке расширения", + description: `Показывать на значке расширения число вызовов API браузера, обработанных ${BRAND_DISPLAY_NAME}, вместо текстовой метки.`, + includeDateCalls: { + label: "Учитывать вызовы Date и Temporal", + description: + "Учитывать вызовы Date.* и Temporal.* на значке. Отключите этот параметр, чтобы частые проверки времени не завышали счётчик.", + }, + }, + copyLinkHelpLabel: "справка по настройкам", + help: { + title: "Настройки", + body1: `Защита браузера позволяет вам выбирать, какие значения ${BRAND_DISPLAY_NAME} подменяет по умолчанию. Эти настройки влияют на каждый сайт, если в правиле для домена или контейнере Firefox не указано иное.`, + body2: `Настройки приватности управляют единственными необязательными сетевыми запросами, которые ${BRAND_DISPLAY_NAME} делает во время настройки местоположений. Если оставить эти запросы выключенными, ${BRAND_DISPLAY_NAME} продолжит работать — места можно будет добавлять вручную, а не использовать поиск или предварительный просмотр карты.`, + }, +} as const; diff --git a/src/ui/i18n/ru-sections/osm.ts b/src/ui/i18n/ru-sections/osm.ts new file mode 100644 index 0000000..3cf20ac --- /dev/null +++ b/src/ui/i18n/ru-sections/osm.ts @@ -0,0 +1,12 @@ +import { BRAND_DISPLAY_NAME } from "@/shared/brand"; + +export const externalMapConsentCopy = { + title: "Разрешить поиск мест и загрузку карт", + description: `Разрешите ${BRAND_DISPLAY_NAME} отправлять поисковые запросы в OpenStreetMap Nominatim, а при показе карты загружать векторные тайлы и шрифты из OpenFreeMap. Эти сервисы получат текст поиска, запросы карты и шрифтов, а также ваш IP-адрес. Сохранённые правила, региональные профили и история посещений им не передаются. Если вы предпочитаете вводить координаты вручную без обращений к картографическим сервисам, оставьте эту настройку выключенной.`, +} as const; + +export const osm = { + modalTitle: "Разрешить запросы к картам и поиску?", + body1: `Доступ к внешним картам необязателен. ${BRAND_DISPLAY_NAME} не отправляет этим сервисам ваши профили, правила или историю посещений.`, + body2: `Если вы разрешите доступ, ${BRAND_DISPLAY_NAME} сможет обращаться к OpenStreetMap Nominatim для поиска мест и к OpenFreeMap для интерактивного просмотра карты.`, +} as const; diff --git a/src/ui/i18n/ru-sections/popup.ts b/src/ui/i18n/ru-sections/popup.ts new file mode 100644 index 0000000..69643f7 --- /dev/null +++ b/src/ui/i18n/ru-sections/popup.ts @@ -0,0 +1,246 @@ +import type { SurfacePresentationState } from "@privacy-brand/xray-protocol"; + +import { sharedWorkerModeCopy } from "./shared-worker"; + +import { BRAND_DIAGNOSTICS_NAME, BRAND_DISPLAY_NAME } from "@/shared/brand"; +import { slavicCount } from "@/ui/shared/slavic-plural"; + +export const popup = { + loading: "Загрузка…", + protectionProtected: "Защищено", + protectionOff: "Защита выключена", + protectionDisabled: "Защита отключена", + protectionNeedsAttention: "Требует внимания", + protectionUnknown: "Статус защиты недоступен", + protectionSourceSiteRule: "Правило для домена", + protectionSourceContainer: "Контейнер Firefox", + protectionSourceDefaultRule: "Правило по умолчанию", + protectionSourceTrustedSite: "Доверенный сайт", + protectionSourceNone: "Нет активного правила", + protectionSourceGlobalSetting: "Глобальная настройка", + languagePriorityTooltip: (languages: readonly string[]) => + [ + "Языковой приоритет:", + ...languages.map((language, index) => `${index + 1}. ${language}`), + ].join("\n"), + protectionViewDetails: "Посмотреть детали", + protectionViewPageActivity: "Просмотр активности страницы", + protectionDetailsTitle: "Детали защиты", + protectionDetailsLead: `Посмотрите, как ${BRAND_DISPLAY_NAME} обрабатывает каждую функцию браузера на этом сайте.`, + protectionGroupLocationLocale: "Местоположение, время и локаль", + protectionGroupBrowserIdentity: "Данные браузера", + protectionGroupRenderingMedia: "Графика и медиа", + protectionGroupWorkers: "Воркеры", + protectionStateProtected: "Защищено", + protectionStateDegraded: "Защита ослаблена", + protectionStateNative: "Без изменений", + protectionStateMixed: "Смешанное состояние", + protectionStatePending: "Проверка…", + protectionStateRepaired: "Защита восстановлена", + protectionStateBrowserEnforced: "Защищено браузером", + protectionStateUnrecoverable: "Защита не сработала", + protectionStateNotApplicable: "Неприменимо", + protectionStateCompatibility: "Проблема совместимости", + protectionSurfaceUnknown: (surface: string) => `Статус ${surface} неизвестен.`, + protectionPageMayNotWork: "Эта страница может работать некорректно", + protectionServiceWorkerBlockTitle: "Service Workers заблокированы", + protectionServiceWorkerBlockSummary: `${BRAND_DISPLAY_NAME} блокирует регистрацию Service Worker на этом сайте.`, + protectionServiceWorkerBlockDescription: + "Автономный режим, push-уведомления, фоновая синхронизация и другие функции Service Worker могут не работать, пока включена блокировка.", + protectionSharedWorkerStrictTitle: "Включён строгий режим Shared Worker", + protectionSharedWorkerStrictSummary: `${BRAND_DISPLAY_NAME} блокирует Shared Worker до запуска, если не может подтвердить подмену его данных.`, + protectionSharedWorkerStrictDescription: + "Синхронизация между вкладками, общие подключения и совместная работа в реальном времени могут не работать при включённом строгом режиме.", + notificationsTitle: "Уведомления", + notificationsThisSite: "Этот сайт", + notificationsExtension: BRAND_DISPLAY_NAME, + notificationsPreviousUpdates: "Предыдущие обновления", + notificationsDismissed: "Закрытые", + notificationsResolved: "Решено", + notificationsNew: "Новое", + notificationsAcknowledged: "Прочитано", + notificationsStillActive: "Требует внимания", + notificationsEmpty: "Нет активных уведомлений.", + notificationsDismiss: "Закрыть", + notificationsKeepStrictMode: "Оставить строгий режим", + notificationsWhatThisAffects: "На что это влияет", + notificationsBeforeYouContinue: "Прежде чем продолжить", + notificationsChooseSharedWorkerMode: "Выберите режим Shared Workers для этого сайта", + notificationsCspTitle: "Сайт заблокировал подмену данных воркера", + notificationsCspSummary: `Не удалось защитить воркер на этом сайте с помощью ${BRAND_DISPLAY_NAME}.`, + notificationsSharedWorkerTitle: "Не удалось защитить Shared Worker", + notificationsSharedWorkerSummary: `Не удалось применить подменённые значения до запуска Shared Worker с помощью ${BRAND_DISPLAY_NAME}.`, + notificationsRelaxWorkerPolicy: "Разрешить защиту воркеров", + notificationsAllowServiceWorkers: "Разрешить Service Workers", + notificationsSharedWorkerNative: "Без подмены", + notificationsSharedWorkerNativeDescription: sharedWorkerModeCopy.native.description, + notificationsSharedWorkerSpoof: "Подмена", + notificationsSharedWorkerSpoofDescription: sharedWorkerModeCopy.spoof.description, + notificationsUpdateTitle: `Обновление ${BRAND_DISPLAY_NAME}`, + notificationsUpdateSummary: "Ознакомьтесь с изменениями в этой версии.", + notificationsVersionLabel: "Версия", + notificationsOpenLink: "Открыть ссылку", + notificationsBadgeLabel: (count: number) => + slavicCount(count, [ + "непрочитанное уведомление", + "непрочитанных уведомления", + "непрочитанных уведомлений", + ]), + protectionCounts: (counts: Record) => { + // Roll the 9 presentation states up into the buckets a one-line summary + // can carry without becoming unreadable. `browser-enforced`/`repaired` + // are protected-like; `unrecoverable` folds into the degraded bucket. + const protectedCount = + counts.protected + counts["browser-enforced"] + counts.repaired; + const degradedCount = counts.degraded + counts.unrecoverable; + return [ + ...(protectedCount > 0 ? [`Защищено: ${protectedCount}`] : []), + ...(degradedCount > 0 ? [`Защита ослаблена: ${degradedCount}`] : []), + ...(counts.pending > 0 ? [`Проверяется: ${counts.pending}`] : []), + ...(counts["native-by-policy"] > 0 + ? [`Без изменений: ${counts["native-by-policy"]}`] + : []), + ...(counts["not-applicable"] > 0 + ? [`Неприменимо: ${counts["not-applicable"]}`] + : []), + ...(counts.unknown > 0 ? [`Неизвестно: ${counts.unknown}`] : []), + ].join(" · "); + }, + protectionException: (_surface: string) => "Эта страница может работать некорректно", + unsupportedTab: "Ограниченная страница", + noLocationYet: "Нет активного профиля", + noPresetAssigned: "Профиль не назначен", + regularPageRequired: `${BRAND_DISPLAY_NAME} не может получить доступ к этой странице.`, + openXRay: `Посмотреть активность этого сайта в ${BRAND_DIAGNOSTICS_NAME}`, + saveLabelCreate: "Создать", + saveLabelSave: "Сохранить", + ruleConflictConfirm: (pattern: string) => + `Правило для домена «${pattern}» уже существует. Заменить его этими настройками?`, + ruleConflictTitle: "Заменить правило для домена?", + ruleConflictReplace: "Заменить", + popupDataUnavailable: "Не удалось загрузить статус этого сайта.", + retryLabel: "Повторить попытку", + dismissLabel: "Закрыть", + mutationFailed: `${BRAND_DISPLAY_NAME} не смог выполнить это действие. Попробуйте еще раз.`, + enableExtensionFailed: `Не удалось включить ${BRAND_DISPLAY_NAME}.`, + trustSiteFailed: "Не удалось добавить этот сайт в список доверенных сайтов.", + firefoxPermissionFailed: "Не удалось предоставить разрешение Firefox userScripts.", + powerControlExtension: BRAND_DISPLAY_NAME, + powerControlGlobalProtections: "Общая защита", + powerTargetLoading: "Проверяем настройки этого сайта…", + powerTargetGlobal: `${BRAND_DISPLAY_NAME} отключен на всех сайтах.`, + powerTargetGlobalProtectionsDisabled: + "Включите защиту в настройках, чтобы использовать сохранённые правила и профили.", + powerTargetUnsupported: `${BRAND_DISPLAY_NAME} не может получить доступ к этой странице.`, + powerTargetTrustedSite: `${BRAND_DISPLAY_NAME} отключен, поскольку этот сайт соответствует доверенным сайтам.`, + powerTargetSiteRule: "Включает или выключает это правило для домена.", + powerTargetContainer: "Включает или выключает назначение контейнера Firefox.", + powerTargetContainerSetup: `Настройте ${BRAND_DISPLAY_NAME} для этого контейнера Firefox.`, + powerTargetContainerDefaultRule: + "Этот контейнер Firefox использует правило по умолчанию.", + powerTargetDefaultRule: + "Управляет сайтами без правила для домена или доверенного сайта.", + powerAriaLoading: "Проверяем настройки этого сайта", + powerAriaGlobalOff: `${BRAND_DISPLAY_NAME} отключен на всех сайтах.`, + powerAriaGlobalProtectionsDisabled: + "Общая защита отключена. Включите её в настройках.", + powerAriaRestricted: `${BRAND_DISPLAY_NAME} не может получить доступ к этой странице`, + powerAriaTrustedSite: `Включить ${BRAND_DISPLAY_NAME} для этого сайта.`, + powerAriaDomainRuleOn: "Включите это правило для домена", + powerAriaDomainRuleOff: "Отключите это правило для домена", + powerAriaContainerOn: "Включите это назначение контейнера Firefox.", + powerAriaContainerOff: "Отключите это назначение контейнера Firefox.", + powerAriaContainerSetup: `Настройте ${BRAND_DISPLAY_NAME} для этого контейнера Firefox.`, + powerAriaDefaultRuleOn: "Включите правило по умолчанию", + powerAriaDefaultRuleOff: "Отключить правило по умолчанию", + + sheetTitle: "Правило для домена", + currentProfileLabel: "Региональный профиль", + noPresetLabel: "Нет профиля", + inheritedDefaultRuleProfileLabel: "Использовать профиль правила по умолчанию", + inheritedContainerProfileLabel: "Использовать профиль контейнера Firefox", + inheritedDomainRuleProfileLabel: "Использовать профиль подходящего правила", + ruleTypeLabel: "Применяется к", + ruleTypeExact: "Точный хост", + ruleTypeSuffix: "Хост + субдомены", + workerHandlingLabel: "Dedicated и Shared Workers", + workerHandlingHint: + "Выберите режим для этого правила. «Наследовать» использует общую настройку. «Без подмены» оставляет воркерам реальные данные браузера. «Подмена» пытается защитить их. «Строгий» блокирует воркер, если защиту нельзя подтвердить до запуска.", + workerHandlingInherit: "Наследовать", + workerHandlingNative: "Без подмены", + workerHandlingSpoof: "Подмена", + workerHandlingStrict: "Строгий", + viewXRay: BRAND_DIAGNOSTICS_NAME, + deleteButtonLabel: "Удалить", + sheetLead: + "Выберите, где применяется это правило для домена и какой региональный профиль оно использует. При изменении профиля страница перезагружается.", + cleanupSheetTitle: "Новая идентичность", + cleanupConfirmTitle: "Создать новую идентичность?", + cleanupPlanLoading: "Проверяем, что может очистить этот браузер…", + cleanupPlanDescription: (available: string, unavailable: string) => + unavailable + ? `${BRAND_DISPLAY_NAME} очистит ${available}. Этот браузер не может очистить ${unavailable} здесь.` + : `${BRAND_DISPLAY_NAME} очистит ${available}.`, + cleanupResultTitle: "Новая идентичность создана", + cleanupResultComplete: `Данные браузера, доступные ${BRAND_DISPLAY_NAME}, были удалены.`, + cleanupResultPartial: + "Часть данных браузера удалена, но некоторые элементы оказались недоступны.", + cleanupResultFailed: `Идентичность изменена, но ${BRAND_DISPLAY_NAME} не смог очистить связанные данные браузера. Вы можете повторить попытку без повторной смены идентичности.`, + cleanupSurfaceCookies: "файлы cookie", + cleanupSurfaceLocalStorage: "локальное хранилище", + cleanupSurfaceIndexedDb: "IndexedDB", + cleanupSurfaceCacheStorage: "Cache Storage", + cleanupSurfaceServiceWorkers: "Service Workers", + cleanupSurfacePageStorage: "данные открытых страниц", + cleanupStatusCleaned: "Очищено", + cleanupStatusSkipped: "Недоступно", + cleanupStatusFailed: "Ошибка", + cleanupConfirmLabel: "Создать новую идентичность", + deleteConfirmTitle: "Удалить правило для домена?", + deleteConfirmDescription: (pattern: string) => + `Это удалит сохранённое правило для домена «${pattern}».`, + cleanDomainAriaLabel: "Создать новую идентичность для этого сайта", + cleanDomainLabel: "Новая идентичность", + cleanDomainDefaultRuleDisabled: "Недоступно для правила по умолчанию", + cleanDomainProductOffDisabled: `Недоступно, пока ${BRAND_DISPLAY_NAME} отключен для этого сайта.`, + cleanDomainContainerSetupDisabled: "Сначала настройте этот контейнер Firefox.", + cleanDomainPageDisabled: "Недоступно на этой странице", + cleanDomainLoadingDisabled: "Доступно после завершения загрузки сайта", + cleanDomainAvailabilityHint: + "Создать новую идентичность можно для правила домена или настроенного контейнера Firefox.", + openSettingsAriaLabel: "Открыть настройки", + openFullRuleSettings: "Открыть в правилах домена", + settingsLabel: "Настройки", + closeSheetAriaLabel: "Закрыть правило для домена", + trustedSiteCta: "Изменить доверенный сайт", + disableOnSiteCta: "Добавить в доверенные сайты", + enableTrustCta: "Добавить в доверенные сайты", + extensionOffCardAction: `Включить ${BRAND_DISPLAY_NAME}`, + surfaceGeolocation: "Геолокация", + surfaceTimeLocale: "Время и локаль", + surfaceNavigator: "Navigator", + surfaceScreen: "Экран", + surfaceClientHints: "Client Hints", + surfaceBattery: "Аккумулятор", + surfaceCanvas: "Canvas", + surfaceWebGL: "WebGL", + surfaceAudio: "Аудио", + surfaceWebRTC: "WebRTC", + surfaceWorker: "Dedicated Workers", + surfaceServiceWorker: "Service Workers", + surfaceSharedWorker: "Shared Workers", + addExactOverrideCta: "Добавить правило сайта", + containerSetupCta: "Настроить контейнер", + globalFallbackRuleCta: "Изменить правило по умолчанию", + editDomainRuleLabel: "Изменить правило для домена", + editContainerLabel: "Редактировать контейнер", + firefoxFirstInlinePermissionTitle: "Улучшить защиту при первой загрузке в Firefox", + firefoxFirstInlinePermissionDescription: `Firefox может раскрыть ваше реальное время и локаль для первого скрипта страницы. Предоставьте дополнительное разрешение userScripts, чтобы ${BRAND_DISPLAY_NAME} мог начать подмену раньше.`, + firefoxFirstInlinePermissionEnableLabel: "Предоставить разрешение", + detailsAbout: (label: string) => `Подробности о ${label}`, + relaxCspHint: `Удаляет заголовки Content Security Policy этого сайта, если они мешают ${BRAND_DISPLAY_NAME} защищать воркеров. Это ослабляет защиту сайта от вредоносных скриптов; включайте только для доверенного сайта.`, + advancedSectionTitle: "Дополнительно", + suggestionWorkerCspDescription: + "Content Security Policy (CSP) этого сайта заблокировала подмену данных воркера. Разрешение на защиту воркеров ослабит эту политику и защиту сайта от вредоносных скриптов. Продолжайте, только если доверяете этому сайту.", + suggestionSharedWorkerInjectionDescription: `${BRAND_DISPLAY_NAME} не смог защитить этот Shared Worker до его запуска. Выберите, что важнее на этом сайте: совместимость или защита.`, +} as const; diff --git a/src/ui/i18n/ru-sections/rules.ts b/src/ui/i18n/ru-sections/rules.ts new file mode 100644 index 0000000..29e4ebc --- /dev/null +++ b/src/ui/i18n/ru-sections/rules.ts @@ -0,0 +1,232 @@ +import { BRAND_DISPLAY_NAME } from "@/shared/brand"; +import { slavicCount } from "@/ui/shared/slavic-plural"; + +const PRODUCT_NOT_LOWER = `чем ${BRAND_DISPLAY_NAME} не является`; + +export const rules = { + title: "Правила для доменов", + hint: "Правила для доменов сопоставляют шаблоны хостов с региональными профилями.", + addButton: "Добавить правило", + filterLabel: "Фильтр правил", + filterPlaceholder: "Поиск по профилям, доменам и предупреждениям", + locationFilterLabel: "Фильтровать по профилю", + locationFilterPlaceholder: "Все профили", + assignLocationLabel: "Назначить профиль", + tableHeadRule: "Правило", + tableHeadProfile: "Профиль", + tableHeadActions: "Действия", + selectAllAriaLabel: "Выбрать все видимые правила", + selectMenuAriaLabel: "Открыть меню выбора правил", + selectRuleAriaLabel: (pattern: string) => `Выберите правило ${pattern}`, + editRuleAriaLabel: (pattern: string) => `Изменить правило ${pattern}`, + editRuleTitle: "Изменить правило", + deleteRuleAriaLabel: (pattern: string) => `Удалить правило ${pattern}`, + deleteRuleTitle: "Удалить правило", + inactiveBadge: "неактивный", + selectionMenuAllVisible: "Все видимые", + selectionMenuAll: "Все", + selectionMenuNone: "Нет", + selectionMenuActive: "Только активные", + selectionMenuInactive: "Только неактивные", + noRulesFiltered: "Ни одно правило не соответствует текущему фильтру.", + noRulesEmpty: `Правил для доменов пока нет. Добавьте правило, чтобы выбрать, как ${BRAND_DISPLAY_NAME} будет обрабатывать соответствующие сайты.`, + copyLinkLabel: "правила для доменов", + copyLinkHelpLabel: "справка по правилам домена", + copyLinkInspectorLabel: "проверка имени хоста", + copyLinkRuleAriaLabel: (pattern: string) => `Скопируйте ссылку на правило ${pattern}`, + + help: { + title: "Правила для доменов", + body1: + "Используйте example.com для одного конкретного хоста. Используйте *example.com для этого хоста и любого поддомена, например www.example.com. Используйте *.example.com только для субдоменов.", + body2: `Побеждает наиболее конкретный шаблон соответствия. Если два правила пересекаются и указывают на разные региональные профили, ${BRAND_DISPLAY_NAME} предупреждает об этом.`, + }, + + globalFallback: { + title: "Правило по умолчанию", + description: `Настройте защиту и при необходимости выберите региональный профиль, который ${BRAND_DISPLAY_NAME} использует, когда нет более конкретных правил.`, + copyLinkLabel: "Правило по умолчанию", + overridesBadge: (count: number) => + slavicCount(count, ["особая настройка", "особые настройки", "особых настроек"]), + openInRules: "Изменить в правилах домена", + editAriaLabel: "Изменить правило по умолчанию", + editTitle: "Изменить правило по умолчанию", + noPresetLabel: "Профиль не назначен", + setupHint: "Профиль и особые настройки защиты пока не заданы.", + tableHint: "Настройки для сайтов без более конкретного правила.", + dialog: { + title: "Правило по умолчанию", + description: + "Выберите защиту и необязательный профиль для сайтов, к которым не применяются более конкретные правила.", + identityDescription: + "У правила по умолчанию есть собственная постоянная цифровая идентичность для подмены. Её нельзя изменить вручную.", + enabledLabel: "Включено", + enabledHint: + "Если отключено, это правило не применяется. Его настройки сохраняются.", + enabledAriaLabel: "Переключить правило по умолчанию", + locationProfileLabel: "Региональный профиль", + locationProfileHint: + "Выберите профиль для правила по умолчанию. Если профиль не назначен, будут действовать только настройки защиты ниже.", + locationProfileWarningPrefix: `${BRAND_DISPLAY_NAME} не заменяет инструменты VPN, прокси-сервера или DNS. `, + locationProfileWarningLinkLabel: `Узнайте, ${PRODUCT_NOT_LOWER}`, + locationProfileWarningSuffix: ".", + locationLabel: "Профиль", + locationPlaceholder: "Выбрать профиль", + submit: "Сохранить правило", + }, + }, + + inspector: { + title: "Проверка хоста", + hint: `Прежде чем сохранять изменения, проверьте, как ${BRAND_DISPLAY_NAME} обрабатывает имя хоста. Здесь видно, соответствует ли хост правилу домена или доверенному сайту, а также какой региональный профиль будет применяться.`, + hostnameLabel: "Имя хоста", + hostnameHint: + "Используйте именно тот хост, который вы хотите проверить, например shop.example.com.", + hostnamePlaceholder: "например shop.example.com", + noMatchTitle: + "Ни одно сохраненное правило для домена или доверенный сайт не соответствуют этому имени хоста.", + noMatchDescription: + "Ни одно сохраненное правило не применяется, а правило по умолчанию отключено или не настроено.", + trustedSiteWinsTitle: "Этот хост отключён через доверенные сайты", + trustedSiteWinsDescription: `Он соответствует вашему списку доверенных сайтов, поэтому ${BRAND_DISPLAY_NAME} не действует на этом сайте, пока вы не удалите или не отключите эту запись.`, + trustedSiteOverridesRuleTitle: + "Доверенные сайты имеют приоритет над правилом домена", + trustedSiteOverridesRuleDescription: `Это имя хоста соответствует как доверенному сайту, так и правилу домена. Доверенные сайты имеют приоритет, поэтому ${BRAND_DISPLAY_NAME} остается выключенным, а приведенное ниже правило для домена игнорируется.`, + fallbackWinsTitle: "Здесь применяется правило по умолчанию", + fallbackWinsDescription: `Ни одно правило для домена или доверенный сайт не соответствовали этому имени хоста, поэтому ${BRAND_DISPLAY_NAME} здесь применит правило по умолчанию.`, + ruleMatchTitle: (locationLabel: string) => + `${locationLabel} — активный профиль здесь.`, + ruleMatchDescription: `Это имя хоста соответствует приведенному ниже правилу домена, поэтому ${BRAND_DISPLAY_NAME} будет использовать этот профиль на сайте.`, + hostnameDetailLabel: "Имя хоста", + trustedSiteDetailLabel: "Доверенный сайт", + ruleDetailLabel: "Правило", + defaultRuleDetailLabel: "Правило по умолчанию", + ignoredRuleDetailLabel: "Игнорируемое правило", + profileDetailLabel: "Региональный профиль", + geolocationDetailLabel: "Геолокация", + localeDetailLabel: "Локаль", + timeZoneDetailLabel: "Часовой пояс", + geolocationOn: "Вкл.", + geolocationOff: "Выкл.", + }, + + dialog: { + titleAdd: "Добавить правило", + titleEdit: "Изменить правило", + description: + "Выберите хосты, на которых действует правило, а затем назначьте региональный профиль, особые настройки защиты или оба варианта.", + patternLabel: "Шаблон", + patternInfo: + "Используйте example.com для одного конкретного хоста. Используйте *example.com для этого хоста и любого поддомена, например www.example.com. Используйте *.example.com только для субдоменов.", + patternInfoAriaLabel: "Узнайте, как работают шаблоны правил", + patternPlaceholder: "Введите шаблон домена", + locationLabel: "Профиль", + locationProfileLabel: "Региональный профиль", + locationProfileHint: `Выберите региональный профиль для этого правила. Если ничего не назначено, ${BRAND_DISPLAY_NAME} использует следующий подходящий профиль, сохраняя при этом настройки защиты этого правила.`, + bulkAssignSearchPlaceholder: "Поиск профилей...", + enabledLabel: "Включено", + enabledHint: + "Если отключено, это правило не применяется. Его настройки сохраняются.", + enabledAriaLabel: (pattern: string) => `Включить или отключить правило ${pattern}`, + advancedModal: { + trigger: "Дополнительно", + title: (pattern: string) => `Расширенные настройки для ${pattern}`, + description: "Изменения останутся в черновике, пока вы не сохраните правило.", + confirm: "ОК", + patternFallback: "это правило", + }, + relaxCspLabel: "Ослабить CSP для защиты воркеров", + relaxCspHint: `Удаляет заголовки Content Security Policy этого сайта, когда они мешают ${BRAND_DISPLAY_NAME} защищать воркеров.`, + relaxCspRiskHint: + "Предупреждение безопасности: это облегчает запуск вредоносных скриптов на сайте. Включайте его только для сайта, которому вы доверяете, и только в том случае, если в противном случае защита воркеров не сработает.", + relaxCspAriaLabel: (pattern: string) => + `Переключить ослабление CSP для правила ${pattern}`, + surfaceOverrides: { + title: "Настройки защиты", + description: + "Выберите другие настройки защиты для этого правила. Оставьте настройку «Наследовать», чтобы следовать глобальной настройке.", + stateOn: "Вкл.", + stateInherit: "Наследовать", + stateOff: "Выкл.", + stateNative: "Без подмены", + stateSpoof: "Подмена", + stateStrict: "Строгий", + stateBlock: "Блокировать", + stateAllow: "Разрешить", + helpAriaLabel: (label: string) => `Узнайте, чем управляет ${label}`, + geolocation: { + label: "Геолокация", + info: "Управляет Geolocation API. Отключите подмену геолокации, если сайт должен получать настоящее местоположение, или включите её, чтобы правило подменяло ответы на запросы геолокации.", + }, + timeLocale: { + label: "Время и локаль", + info: "Управляет Date, Intl, navigator.language, navigator.languages и языковыми заголовками, чтобы сайты видели регион из активного регионального профиля.", + }, + canvas: { + label: "Canvas", + info: "Управляет результатом скрытого рисунка Canvas, по которому сайты могут распознавать браузер.", + }, + webGL: { + label: "WebGL", + info: "Управляет графическими данными WebGL: рендерером, сведениями о GPU и другими признаками цифрового отпечатка.", + }, + audio: { + label: "Аудио", + info: "Управляет измеряемым выводом AudioContext, который сайты используют для цифрового отпечатка.", + }, + navigator: { + label: "Navigator", + info: "Управляет полями идентификации браузера, такими как платформа, подсказки об оборудовании и другие свойства навигатора.", + }, + screen: { + label: "Экран", + info: "Управляет размером экрана, плотностью пикселей и соответствующими деталями дисплея.", + }, + clientHints: { + label: "Client Hints", + info: "Управляет сведениями о браузере и устройстве, передаваемыми через заголовки и API Client Hints.", + }, + battery: { + label: "Аккумулятор", + info: "Определяет, получает ли сайт данные о полностью заряженном аккумуляторе вместо сведений о фактическом состоянии аккумулятора устройства.", + }, + webRTC: { + label: "WebRTC", + info: "Управляет защитой IP-адресов в WebRTC, которая может уменьшить утечки локальных и публичных адресов.", + }, + serviceWorker: { + label: "Service Workers", + info: "Определяет, может ли этот сайт регистрировать Service Workers, которые могут работать в фоновом режиме и хранить долговременные данные. Блокировка может нарушить работу PWA, автономного режима, push-уведомлений и фоновой синхронизации; «Разрешить» позволяет браузеру регистрировать их как обычно; «Наследовать» использует глобальную настройку.", + }, + sharedWorker: { + label: "Dedicated и Shared Workers", + info: "Задаёт режим Dedicated и Shared Workers для этого правила. «Без подмены» оставляет воркеры без защиты, «Подмена» пытается заменить значения, а «Строгий» блокирует воркер, если защиту нельзя подтвердить до запуска.", + }, + }, + identity: { + sectionTitle: "Цифровая идентичность", + sectionDescription: + "Это правило использует постоянную цифровую идентичность для подмены. Меняйте её только тогда, когда нужен новый цифровой отпечаток и чистое состояние связанных сайтов.", + actionDescription: + "Удаляет связанные данные сайтов и создаёт для правила новую цифровую идентичность.", + actionLabel: "Новая идентичность", + confirmTitle: (pattern: string) => `Создать новую идентичность для «${pattern}»?`, + confirmDescription: + "Это удалит файлы cookie, данные хранилищ, Service Workers и кеши сайтов, связанных с правилом. Затем будет создана новая цифровая идентичность.", + confirmDomainsLabel: `${BRAND_DISPLAY_NAME} удалит данные браузера для этих доменов:`, + confirmNoDomains: `Для этого правила пока нет сохранённых данных браузера. ${BRAND_DISPLAY_NAME} всё равно создаст новую цифровую идентичность.`, + confirmLabel: "Создать новую идентичность", + rotateSuccess: "Для правила создана новая идентичность.", + rotateError: "Не удалось создать новую идентичность для правила.", + }, + submitAdd: "Добавить правило", + submitEdit: "Сохранить", + duplicateAlertTitle: "Перезаписать существующее правило?", + duplicateAlertDescription: (pattern: string) => + `Правило для «${pattern}» уже существует. Перезаписать его с этими настройками?`, + duplicateAlertConfirm: "Перезаписать", + duplicateAlertClose: "Нет", + trustedSiteOverrideWarning: (pattern: string) => + `Этот домен соответствует записи доверенных сайтов «${pattern}». ${BRAND_DISPLAY_NAME} здесь останется выключенным независимо от этих настроек.`, + }, +} as const; diff --git a/src/ui/i18n/ru-sections/shared-worker.ts b/src/ui/i18n/ru-sections/shared-worker.ts new file mode 100644 index 0000000..92975aa --- /dev/null +++ b/src/ui/i18n/ru-sections/shared-worker.ts @@ -0,0 +1,32 @@ +import { BRAND_DISPLAY_NAME } from "@/shared/brand"; + +export const sharedWorkerModeCopy = { + native: { + label: "Без подмены", + description: `Shared Workers работают обычным образом, без защиты ${BRAND_DISPLAY_NAME}. Это обеспечивает наилучшую совместимость, но они могут считывать реальные данные браузера.`, + }, + spoof: { + label: "Подмена", + description: + "Применяет к Shared Workers другой способ защиты. Некоторые из них всё же могут не запуститься.", + }, + strict: { + label: "Строгий", + description: `Блокирует Shared Worker, если ${BRAND_DISPLAY_NAME} не может подтвердить подмену до его запуска. Так незащищённый Shared Worker не получит реальные данные браузера, но зависящие от него функции сайта могут перестать работать.`, + }, +} as const; + +export const workerHandlingModeCopy = { + native: { + label: "Без подмены", + description: `Dedicated и Shared Workers работают обычным образом, без защиты ${BRAND_DISPLAY_NAME}. Это обеспечивает наилучшую совместимость, но они могут считывать реальные данные браузера.`, + }, + spoof: { + label: "Подмена", + description: `Пытается применить подменённые ${BRAND_DISPLAY_NAME} значения до запуска воркеров. Если защиту применить не удастся, воркер может получить реальные данные браузера или не запуститься.`, + }, + strict: { + label: "Строгий", + description: `Блокирует воркер до запуска, если ${BRAND_DISPLAY_NAME} не может подтвердить подмену. Если сбой происходит после запуска, ${BRAND_DISPLAY_NAME} покажет уведомление. Зависящие от этих воркеров функции сайта могут перестать работать.`, + }, +} as const; diff --git a/src/ui/i18n/ru-sections/sidebar.ts b/src/ui/i18n/ru-sections/sidebar.ts new file mode 100644 index 0000000..f020eda --- /dev/null +++ b/src/ui/i18n/ru-sections/sidebar.ts @@ -0,0 +1,174 @@ +import { BRAND_DIAGNOSTICS_NAME, BRAND_DISPLAY_NAME } from "@/shared/brand"; + +export const sidebar = { + xRayTitle: BRAND_DIAGNOSTICS_NAME, + openSettings: "Открыть настройки", + openRule: "Изменить правило домена", + openLocation: "Редактировать профиль", + openLogs: "Просмотр журналов", + region: "Регион", + + errorPrefix: `${BRAND_DISPLAY_NAME} столкнулся с проблемой:`, + loading: "Проверка страницы…", + unsupportedTab: + "Страницы браузера невозможно подменить. Перейдите на веб-сайт, чтобы проверить покрытие.", + noSpoofing: `${BRAND_DISPLAY_NAME} неактивен на этой странице. Откройте всплывающее окно, чтобы настроить правило для этого сайта.`, + trustedSite: "Этот сайт отмечен как доверенный. Подмена данных выключена.", + trustedSiteHint: "Откройте «Доверенные сайты» в настройках, чтобы изменить это.", + + surfaces: { + title: "Подменённые данные", + timezone: "Часовой пояс", + language: "Язык", + languages: "Языки", + acceptLanguage: "Accept-Language", + geolocation: "Геолокация", + accuracyNoise: "Точность / шум", + platform: "Платформа", + userAgent: "User-Agent", + cpuRam: "ЦП/ОЗУ", + screen: "Экран", + noise: "Шум", + canvas: "Canvas", + audio: "Аудио", + gpu: "GPU", + sharedWorker: "Shared Worker", + sharedWorkerNativeCompatibility: "Без подмены ради совместимости", + sharedWorkerBlobSpoofing: "Подмена через обёртку Blob", + sharedWorkerResponseRewriteSpoofing: "Подмена через изменение ответа", + sharedWorkerResponseRewriteCacheSensitive: + "Резервный способ изменения ответа; зависит от кеша", + sharedWorkerModuleUnsupported: "Перезапись модуля не поддерживается", + sharedWorkerIdentityConflict: "Конфликт идентичности", + sharedWorkerRewriteUnavailable: "Перезапись ответа недоступна", + sharedWorkerBlockedStrict: "Заблокировано строгим режимом", + sharedWorkerStrictRewriteRequired: + "Строгий режим требует подтвержденной перезаписи", + sharedWorkerStrictBlockedCacheSensitive: + "Заблокировано: резервный способ зависел от кеша", + sharedWorkerCompatibilityHelp: + "Обработка Shared Worker зависит от выбранного режима: «Без подмены», «Подмена» или «Строгий».", + gpuDebug: "Диагностика GPU", + clientHintsPlatform: "платформа", + clientHintsBrands: "бренды", + fingerprint: "Цифровой отпечаток", + off: "Выкл.", + yes: "Активно", + blocked: "Заблокировано", + m: "м", + GB: "ГБ", + expand: "Показать полное значение", + collapse: "Свернуть", + }, + + accessed: { + title: "Активность страницы", + description: "Категории, запрошенные страницей с момента последней навигации.", + accessed: "Запрошено", + blocked: "Заблокировано", + armed: "Ожидание", + connecting: "Подключение…", + failed: "Ошибка", + off: "Выкл.", + categories: { + geolocation: "Геолокация", + timeLocale: "Время и локаль", + canvas: "Canvas", + webGL: "WebGL", + audio: "Аудио", + navigator: "Navigator", + screen: "Экран", + clientHints: "Client Hints", + battery: "Батарея", + webRTC: "WebRTC", + worker: "Dedicated Workers", + serviceWorker: "Service Workers", + sharedWorker: "Shared Workers", + }, + methods: { + geolocationGetCurrentPosition: "getCurrentPosition", + geolocationWatchPosition: "watchPosition", + geolocationClearWatch: "clearWatch", + geolocationPermissionsQuery: "permissions.query", + dateConstructor: "Date()", + dateNow: "Date.now", + dateParse: "Date.parse", + dateGetTimezoneOffset: "Date.getTimezoneOffset", + dateToString: "Date.toString", + dateToLocaleString: "Date.toLocaleString", + intlConstructor: "Intl constructor", + intlResolvedOptions: "Intl.resolvedOptions", + intlDateTimeFormatFormat: "DateTimeFormat.format", + intlDateTimeFormatFormatToParts: "DateTimeFormat.formatToParts", + temporalNowInstant: "Temporal.Now.instant", + temporalNowTimeZoneId: "Temporal.Now.timeZoneId", + temporalNowPlainDateTimeISO: "Temporal.Now.plainDateTimeISO", + temporalNowZonedDateTimeISO: "Temporal.Now.zonedDateTimeISO", + temporalNowPlainDateISO: "Temporal.Now.plainDateISO", + temporalNowPlainTimeISO: "Temporal.Now.plainTimeISO", + temporalDurationToLocaleString: "Temporal.Duration.toLocaleString", + temporalInstantToLocaleString: "Temporal.Instant.toLocaleString", + temporalPlainDateToLocaleString: "Temporal.PlainDate.toLocaleString", + temporalPlainDateTimeToLocaleString: "Temporal.PlainDateTime.toLocaleString", + temporalPlainMonthDayToLocaleString: "Temporal.PlainMonthDay.toLocaleString", + temporalPlainTimeToLocaleString: "Temporal.PlainTime.toLocaleString", + temporalPlainYearMonthToLocaleString: "Temporal.PlainYearMonth.toLocaleString", + temporalZonedDateTimeToLocaleString: "Temporal.ZonedDateTime.toLocaleString", + canvasGetImageData: "ctx.getImageData", + canvasToDataURL: "canvas.toDataURL", + canvasToBlob: "canvas.toBlob", + webGLReadPixels: "gl.readPixels", + webGLGetExtension: "GPU renderer info", + webGLGetSupportedExtensions: "gl.getSupportedExtensions", + webGLGetParameter: "gl.getParameter", + audioGetFloatFrequencyData: "analyser.getFloatFrequencyData", + audioGetByteFrequencyData: "analyser.getByteFrequencyData", + audioGetFloatTimeDomainData: "analyser.getFloatTimeDomainData", + audioGetByteTimeDomainData: "analyser.getByteTimeDomainData", + audioGetChannelData: "analyser.getChannelData", + navigatorWebdriver: "navigator.webdriver", + navigatorHardwareConcurrency: "navigator.hardwareConcurrency", + navigatorDeviceMemory: "navigator.deviceMemory", + navigatorMaxTouchPoints: "navigator.maxTouchPoints", + navigatorPlatform: "navigator.platform", + navigatorUserAgent: "navigator.userAgent", + navigatorVendor: "navigator.vendor", + navigatorAppVersion: "navigator.appVersion", + screenWidth: "screen.width", + screenHeight: "screen.height", + screenAvailWidth: "screen.availWidth", + screenAvailHeight: "screen.availHeight", + screenColorDepth: "screen.colorDepth", + screenPixelDepth: "screen.pixelDepth", + screenDevicePixelRatio: "screen.devicePixelRatio", + clientHintsBrands: "ua.brands", + clientHintsMobile: "ua.mobile", + clientHintsPlatform: "ua.platform", + clientHintsToJSON: "ua.toJSON", + clientHintsGetHighEntropyValues: "ua.getHighEntropyValues", + batteryGetBattery: "navigator.getBattery", + batteryManagerCharging: "BatteryManager.charging", + batteryManagerChargingTime: "BatteryManager.chargingTime", + batteryManagerDischargingTime: "BatteryManager.dischargingTime", + batteryManagerLevel: "BatteryManager.level", + webRTCConstructor: "RTCPeerConnection", + webRTCCreateOffer: "RTCPeerConnection.createOffer", + webRTCCreateAnswer: "RTCPeerConnection.createAnswer", + workerConstructor: "Worker()", + serviceWorkerRegister: "serviceWorker.register", + sharedWorkerConstructor: "SharedWorker()", + }, + }, + + why: { + title: "Почему применяются эти настройки", + sourcesShort: { + trustedSite: "Доверенный", + domainRule: "Правило домена", + container: "Контейнер", + fallback: "По умолчанию", + none: "Нет", + }, + noExplanation: "Объяснение правил отсутствует.", + }, +} as const; diff --git a/src/ui/i18n/ru-sections/trusted-sites.ts b/src/ui/i18n/ru-sections/trusted-sites.ts new file mode 100644 index 0000000..ce559eb --- /dev/null +++ b/src/ui/i18n/ru-sections/trusted-sites.ts @@ -0,0 +1,54 @@ +import { BRAND_DISPLAY_NAME } from "@/shared/brand"; +import { slavicCount } from "@/ui/shared/slavic-plural"; + +export const trustedSites = { + title: "Доверенные сайты", + hint: `Сайты, на которых ${BRAND_DISPLAY_NAME} остается выключенным, даже если в противном случае применяется правило, назначение контейнера или правило по умолчанию.`, + patternLabel: "Шаблон домена", + patternPlaceholder: "Введите шаблон домена", + filterLabel: "Фильтровать доверенные сайты", + filterPlaceholder: "Поиск домена", + addButton: "Добавить сайт", + copyLinkLabel: "доверенные сайты", + copyLinkHelpLabel: "справка по доверенным сайтам", + copyLinkInspectorLabel: "инспектор имен хостов", + tableHeadPattern: "Домен", + tableHeadStatus: "Статус", + tableHeadActions: "Действия", + empty: "Доверенных сайтов пока нет.", + filteredEmpty: "Ни один доверенный сайт не соответствует текущему фильтру.", + inactiveBadge: "неактивный", + toggleSiteAriaLabel: (pattern: string, enabled: boolean) => + `${enabled ? "Отключить" : "Включить"} доверенный сайт ${pattern}`, + deleteSiteAriaLabel: (pattern: string) => `Удалить доверенный сайт ${pattern}`, + deleteSiteTitle: "Удалить доверенный сайт", + duplicateWarning: "Этот доверенный сайт уже существует.", + patternRequired: "Введите шаблон домена.", + saved: "Доверенный сайт сохранен.", + updated: "Доверенный сайт обновлен.", + deleted: "Доверенный сайт удален.", + help: { + title: "Когда использовать доверенные сайты", + body1: + "Используйте доверенные сайты для доменов, которым мешает подмена, например банковские операции, оформление покупок или восстановление учётной записи, где изменение данных браузера может вызвать подозрение.", + body2: `Доверенные сайты переопределяют правила домена, назначения контейнеров Firefox и правило по умолчанию. Используйте example.com для одного хоста, *example.com для этого хоста и его субдоменов или *.example.com только для субдоменов.`, + }, + rulesCta: { + title: "Защита на других сайтах", + activeRulesOnly: (count: number) => + `За пределами доверенных сайтов: ${slavicCount(count, ["активное правило", "активных правила", "активных правил"])}. Откройте правила для доменов, чтобы проверить, где они применяются.`, + activeRulesWithDefault: (count: number) => + `За пределами доверенных сайтов: ${slavicCount(count, ["активное правило", "активных правила", "активных правил"])}. Правило по умолчанию также охватывает остальные сайты без совпадений.`, + defaultRuleOnly: `Правило по умолчанию по-прежнему применяется к несовпадающим сайтам. Доверенные сайты отключают ${BRAND_DISPLAY_NAME} только на соответствующих хостах.`, + openRules: "Открыть правила для доменов", + openDefaultRule: "Открыть правило по умолчанию", + }, + dialog: { + title: "Добавить сайт", + description: `Отключите ${BRAND_DISPLAY_NAME} на подходящих сайтах, если подмена мешает их работе.`, + patternInfo: + "Используйте example.com для одного конкретного хоста. Используйте *example.com для этого хоста и любого поддомена, например www.example.com. Используйте *.example.com только для субдоменов.", + patternInfoAriaLabel: "Узнайте, как работают шаблоны доверенных сайтов", + submit: "Добавить сайт", + }, +} as const; diff --git a/src/ui/i18n/ru-sections/welcome.ts b/src/ui/i18n/ru-sections/welcome.ts new file mode 100644 index 0000000..4e523df --- /dev/null +++ b/src/ui/i18n/ru-sections/welcome.ts @@ -0,0 +1,139 @@ +import { BRAND_DISPLAY_NAME } from "@/shared/brand"; +import { externalMapConsentCopy } from "@/ui/i18n/ru-sections/osm"; +import { slavicCount } from "@/ui/shared/slavic-plural"; + +const WELCOME_PRODUCT = `Добро пожаловать в ${BRAND_DISPLAY_NAME}`; + +export const welcome = { + title: WELCOME_PRODUCT, + loading: "Загрузка мастера настройки…", + progressLabel: "Ход настройки", + previous: "Предыдущий", + next: "Следующий", + privacyPolicy: "Политика конфиденциальности", + privacyDialog: { + title: "Политика конфиденциальности", + description: `Как ${BRAND_DISPLAY_NAME} обрабатывает настройки, диагностику и сетевые запросы.`, + close: "Закрыть политику конфиденциальности", + }, + saving: "Сохранение…", + unexpectedError: "Что-то пошло не так", + importSuccess: "Настройки импортированы", + importError: "Не удалось импортировать настройки", + importParseError: "Не удалось прочитать файл настроек", + steps: { + welcome: { + title: "Спасибо, что установили расширение", + description: + "Выберите, какие данные увидят сайты, вместо того чтобы раскрывать настоящее местоположение и данные браузера. Этот короткий необязательный мастер поможет принять первые решения. Любое из них можно изменить позже в настройках. Уже есть конфигурация, которую вы хотите использовать?", + importInline: "Вместо этого импортируйте настройки", + advancedTitle: "Я знаю, чего хочу", + advancedDescription: + "Пропустите руководство и откройте «Настройки» с чистыми настройками по умолчанию. Никакие региональные профили или правила домена не добавляются, правило по умолчанию остается отключенным, а защита браузера остается доступной для правил, которые вы создадите позже.", + guidedTitle: "Перейти к настройке", + guidedDescription: + "Пройдите основные шаги по порядку. Мастер объяснит последствия каждого выбора и сохранит его в настройках, которые можно изменить позже.", + advancedCta: "Пропустить настройку", + guidedCta: "Начать настройку", + }, + privacy: { + title: "История посещений остаётся в браузере", + description: `${BRAND_DISPLAY_NAME} хранит правила и региональные профили локально. Этот шаг касается только дополнительных внешних картографических сервисов: включите их, если вам нужен поиск и предварительный просмотр во время настройки, или оставьте их отключенными и введите координаты вручную.`, + descriptionBeforePolicy: `${BRAND_DISPLAY_NAME} хранит правила и региональные профили локально. Дополнительный поиск по карте и предварительный просмотр используют внешние сервисы только с вашего разрешения. Прочтите`, + policyLink: "политику конфиденциальности", + descriptionAfterPolicy: ", прежде чем решать, подходят ли вам эти запросы.", + consentTitle: externalMapConsentCopy.title, + consentDescription: externalMapConsentCopy.description, + }, + presets: { + title: "Добавить региональные профили", + description: + "Региональные профили содержат готовые координаты, язык и часовой пояс. Вместе эти данные создают согласованное представление о регионе для сайтов. Выберите места, с которых хотите начать. В конце настройки мастер импортирует выбранные профили; каждый из них можно будет изменить или удалить позже.", + selectAll: "Выбрать все", + selectNone: "Очистить все", + selectedCount: (count: number) => `${count} выбрано`, + }, + scope: { + title: "Выберите, с чего начинается защита", + description: + "Правило по умолчанию действует, если нет более конкретного правила. Оставьте его выключенным для спокойного начала или включите, чтобы защищать остальные сайты.", + defaultRuleDescription: + "Если вы назначаете региональный профиль правилу по умолчанию, оставьте этот профиль выбранным, чтобы программа установки могла импортировать его перед сохранением.", + enableEverywhereTitle: `Используйте ${BRAND_DISPLAY_NAME} на всех сайтах.`, + enableEverywhereDescription: `Включите правило по умолчанию сейчас, если вы хотите, чтобы ${BRAND_DISPLAY_NAME} был активен на сайтах без собственных правил. Вы по-прежнему можете сузить поведение позже с помощью правил домена, назначить разные настройки конкретным хостам или исключить конфиденциальные сайты с помощью доверенных сайтов.`, + editDefaultRuleTitle: "Настройки правила по умолчанию", + editDefaultRule: "Изменить правило по умолчанию", + editDefaultRuleDescription: + "Перед завершением установки выберите профиль для правила по умолчанию и пользовательские настройки защиты.", + defaultRuleDialogDescription: + "Во время установки в этом диалоговом окне могут использоваться выбранные вами региональные профили, даже если они еще не были импортированы. Если назначите профиль здесь, оставьте его выбранным, чтобы мастер импортировал его.", + presetMismatch: (label: string) => + `Правилу по умолчанию назначен профиль «${label}», но этот профиль не выбран для импорта. Выберите этот профиль или выберите другой профиль для правила по умолчанию.`, + }, + chromium: { + title: "Менять данные сборки Chromium", + description: (count: number) => + `${BRAND_DISPLAY_NAME} может менять номера сборки и патча Chromium, передаваемые через Client Hints, сохраняя основную версию установленного браузера. В комплектном каталоге есть недавние сборки Chromium; для вашей версии браузера доступно ${slavicCount(count, ["подходящее значение", "подходящих значения", "подходящих значений"])}.`, + switchTitle: "Менять номера сборки и патча", + switchDescription: + "Использовать недавние номера сборки и патча Chromium при подмене Client Hints. Выключите эту функцию, если хотите сохранить эти номера неизменными.", + }, + firefox: { + title: "Разрешение Firefox userScripts (необязательно)", + description: `Вы можете использовать ${BRAND_DISPLAY_NAME} в Firefox без этого разрешения. Если вы предоставите userScripts, ${BRAND_DISPLAY_NAME} сможет применять подменённые значения раньше на поддерживаемых сайтах, улучшая защиту во время первой загрузки страницы. Вы можете пропустить это сейчас и изменить это позже в настройках.`, + action: "Предоставьте разрешение userScripts", + granted: "Разрешение получено", + skipped: `Вы можете продолжать использовать ${BRAND_DISPLAY_NAME} без этого разрешения.`, + }, + appearance: { + title: "Сделайте это удобным", + description: `Выберите, как должен выглядеть интерфейс расширения, прежде чем начать его использовать. Эти настройки влияют только на экраны ${BRAND_DISPLAY_NAME}; они не меняют поведение подмены на веб-сайтах, в региональных профилях или правилах. Вы можете настроить те же параметры отображения позже в настройках.`, + themeTitle: "Тема", + themeDescription: `Следуйте настройкам вашей системы или выберите фиксированный светлый или темный интерфейс для всех экранов ${BRAND_DISPLAY_NAME}.`, + reduceMotionTitle: "Уменьшите движение", + reduceMotionDescription: `Отключите анимацию интерфейса в ${BRAND_DISPLAY_NAME}.`, + reduceMotionSystemOverride: + "Ограничение движения включено настройкой специальных возможностей вашей системы.", + accentTitle: "Акцентный цвет", + accentDescription: + "Выберите цвет выделения, используемый для элементов управления, активных состояний, акцентов фокуса и выбранных элементов.", + contrastTitle: "Режим высокой контрастности", + contrastDescription: + "Увеличьте контрастность текста, рамок и элементов управления, если обычное оформление недостаточно выразительно.", + }, + done: { + title: "Настройка готова", + description: `${BRAND_DISPLAY_NAME} сохранит эти настройки и откроет настройки. Здесь вы можете просмотреть импортированные настройки, настроить правило по умолчанию и изменить настройки внешнего вида или конфиденциальности.`, + cta: "Открыть настройки", + }, + }, + presetNames: { + spfWarsaw: "Варшава", + spfParis: "Париж", + spfLondon: "Лондон", + spfOttawa: "Оттава", + spfNewYork: "Нью-Йорк", + spfLasVegas: "Лас-Вегас", + spfSanFrancisco: "Сан-Франциско", + spfSydney: "Сидней", + spfBeijing: "Пекин", + spfHongKong: "Гонконг", + spfNewDelhi: "Нью-Дели", + spfCairo: "Каир", + spfLagos: "Лагос", + spfKyiv: "Киев", + spfKinshasa: "Киншаса", + spfSaoPaulo: "Сан-Паулу", + spfBuenosAires: "Буэнос-Айрес", + spfLima: "Лима", + spfRioDeJaneiro: "Рио-де-Жанейро", + spfCaracas: "Каракас", + spfBerlin: "Берлин", + spfMadrid: "Мадрид", + }, + themeOptions: { + system: "Системная", + light: "Светлая", + dark: "Тёмная", + }, +} as const; diff --git a/src/ui/i18n/ru.ts b/src/ui/i18n/ru.ts new file mode 100644 index 0000000..41efd13 --- /dev/null +++ b/src/ui/i18n/ru.ts @@ -0,0 +1,30 @@ +import { about } from "./ru-sections/about"; +import { advanced } from "./ru-sections/advanced"; +import { common } from "./ru-sections/common"; +import { demo } from "./ru-sections/demo"; +import { firefoxContainers } from "./ru-sections/firefox-containers"; +import { locations } from "./ru-sections/locations"; +import { options, optionsPage } from "./ru-sections/options"; +import { osm } from "./ru-sections/osm"; +import { popup } from "./ru-sections/popup"; +import { rules } from "./ru-sections/rules"; +import { sidebar } from "./ru-sections/sidebar"; +import { trustedSites } from "./ru-sections/trusted-sites"; +import { welcome } from "./ru-sections/welcome"; + +export const ru = { + common, + popup, + options, + optionsPage, + locations, + rules, + trustedSites, + advanced, + about, + osm, + demo, + firefoxContainers, + sidebar, + welcome, +} as const; diff --git a/src/ui/i18n/slavic-plural.test.ts b/src/ui/i18n/slavic-plural.test.ts new file mode 100644 index 0000000..0f3a234 --- /dev/null +++ b/src/ui/i18n/slavic-plural.test.ts @@ -0,0 +1,25 @@ +import { describe, expect, it } from "vitest"; + +import { slavicCount, slavicPluralForm } from "@/ui/shared/slavic-plural"; + +describe("Russian and Ukrainian count forms", () => { + it.each([ + [1, "one"], + [2, "few"], + [5, "many"], + [11, "many"], + [21, "one"], + [22, "few"], + [25, "many"], + [111, "many"], + [121, "one"], + ] as const)("selects %s as %s", (count, form) => { + expect(slavicPluralForm(count)).toBe(form); + }); + + it("selects complete noun phrases", () => { + expect(slavicCount(21, ["правило", "правила", "правил"])).toBe("21 правило"); + expect(slavicCount(22, ["правило", "правила", "правил"])).toBe("22 правила"); + expect(slavicCount(25, ["правило", "правила", "правил"])).toBe("25 правил"); + }); +}); diff --git a/src/ui/i18n/uk-sections/about.ts b/src/ui/i18n/uk-sections/about.ts new file mode 100644 index 0000000..bc824b4 --- /dev/null +++ b/src/ui/i18n/uk-sections/about.ts @@ -0,0 +1,168 @@ +import { BRAND_DISPLAY_NAME } from "@/shared/brand"; + +const ABOUT_PRODUCT = `Про ${BRAND_DISPLAY_NAME}`; +const ABOUT_PRODUCT_LOWER = `про ${BRAND_DISPLAY_NAME}`; +const HOW_TO_USE_PRODUCT = `Як використовувати ${BRAND_DISPLAY_NAME}`; +const HOW_TO_USE_PRODUCT_LOWER = `як використовувати ${BRAND_DISPLAY_NAME}`; +const WHAT_PRODUCT_IS_NOT = `Чим не є ${BRAND_DISPLAY_NAME}`; +const PRODUCT_NOT_LOWER = `розділ про обмеження ${BRAND_DISPLAY_NAME}`; +const TERMS_OF_USE = "Умови використання"; + +export const about = { + title: ABOUT_PRODUCT, + description: `${BRAND_DISPLAY_NAME} допомагає контролювати, як веб-сайти бачать ваше місцезнаходження, регіон, часовий пояс і вибрані відомості про браузер.`, + body1: + "Кожен збережений регіональний профіль об’єднує координати, мовні параметри та цільовий часовий пояс у профіль, який можна призначати різним доменам.", + body2: `Ваші зміни зберігаються автоматично. Оновлені профілі та правила набувають чинності під час наступного завантаження сторінки, тому ${BRAND_DISPLAY_NAME} може застосувати нові налаштування з самого початку.`, + body3Prefix: "Загальні налаштування захисту браузера є на вкладці", + body3LinkLabel: "«Налаштування»", + body3Suffix: ".", + website: { + prefix: "Відвідайте ", + linkLabel: `Веб-сайт ${BRAND_DISPLAY_NAME}`, + url: "https://privacything.com", + suffix: ", щоб знайти новини про проєкт, завантаження та іншу інформацію.", + }, + versionLabel: "Версія", + browserTargetLabel: "Цільовий браузер", + releaseChannelLabel: "Канал випуску", + copyLinkLabel: ABOUT_PRODUCT_LOWER, + copyLinkTermsLabel: "умови використання", + copyLinkPrivacyLabel: "про конфіденційність", + copyLinkLimitationsLabel: PRODUCT_NOT_LOWER, + copyLinkLicenseLabel: "ліцензія", + copyLinkAssetsLabel: "сторонні компоненти", + copyLinkUsageLabel: HOW_TO_USE_PRODUCT_LOWER, + releaseChannels: { + local: "Локальний", + beta: "Бета", + stable: "Стабільний", + }, + + support: { + url: "https://webh.pl", + logoLinkAriaLabel: "Відкрити webh.pl", + bodyPrefix: `${BRAND_DISPLAY_NAME} розвивається за підтримки `, + linkLabel: "webh.pl", + bodySuffix: " — швидка, гнучка інфраструктура для амбітних проєктів.", + }, + + terms: { + title: TERMS_OF_USE, + body1: `${BRAND_DISPLAY_NAME} надається «як є», без будь-яких гарантій.`, + body2: `${BRAND_DISPLAY_NAME} допомагає користувачам локально контролювати вибране місцезнаходження, регіон, часовий пояс і пов’язані дані, які браузер надає сайтам. Це не VPN, проксі-сервер, інструмент анонімності, продукт безпеки чи гарантія неможливості виявлення.`, + body3: `Використовуйте ${BRAND_DISPLAY_NAME} на власний ризик. Ви несете відповідальність за дотримання відповідних законів, умов використання сайтів, правил робочого місця і правил платформи.`, + }, + + privacy: { + title: "Конфіденційність", + body: `${BRAND_DISPLAY_NAME} зберігає налаштування локально у вашому браузері. Зовнішні запити карт необов’язкові: OpenStreetMap Nominatim використовується для пошуку місцезнаходження, а OpenFreeMap використовується для інтерактивного попереднього перегляду карти після вашої згоди.`, + openPolicyButton: "Відкрити політику конфіденційності", + }, + + limitations: { + title: WHAT_PRODUCT_IS_NOT, + intro: `${BRAND_DISPLAY_NAME} змінює видимі у браузері дані, такі як геолокація, локаль, часовий пояс і вибрані дані цифрових відбитків. Розширення не спрямовує трафік через іншу мережу.`, + body1: "Воно не приховує та не замінює вашу IP-адресу.", + body2: "Воно не замінює VPN, проксі чи налаштування DNS.", + body3: + "Саме по собі розширення не змусить сайти вважати, що ви підключаєтеся з іншої країни.", + outro: `Використовуйте ${BRAND_DISPLAY_NAME} для підміни на рівні браузера. Використовуйте інструменти VPN, проксі або DNS, коли потрібно змінити розташування на рівні мережі.`, + }, + + license: { + title: "Ліцензія", + creatorPrefix: "Автор проєкту — ", + creatorLabel: "Tomasz Janusz", + creatorUrl: "https://tomaszjanusz.dev", + creatorSuffix: ".", + copyright: "© 2025 — дотепер.", + body: `${BRAND_DISPLAY_NAME} доступний за GNU Affero General Public License v3.0 або новішої версії з додатковими умовами.`, + openLicenseButton: "Відкрити ліцензію", + }, + + assets: { + title: "Сторонні активи", + body: `Ознайомтеся зі сторонніми компонентами, що входять у комплект, і відкрийте тексти ліцензій, що постачаються разом із ${BRAND_DISPLAY_NAME}.`, + openNoticesButton: "Відкрити відомості про сторонні компоненти", + fontAwesome: { + label: "Font Awesome Free 7.2.0", + url: "https://fontawesome.com", + body: " від Font Awesome / Fonticons, Inc. ліцензовано відповідно до CC BY 4.0, SIL OFL 1.1 і MIT.", + }, + mapLibre: { + label: "MapLibre GL JS", + url: "https://maplibre.org/maplibre-gl-js/docs/", + body: " входить до складу розширення для відображення векторних карт.", + }, + openFreeMap: { + label: "OpenFreeMap", + url: "https://openfreemap.org/", + }, + openStreetMap: { + label: "OpenStreetMap", + url: "https://www.openstreetmap.org/copyright", + }, + osmWikiCountryCodes: { + label: "OpenStreetMap Wiki: Nominatim/Country Codes", + url: "https://wiki.openstreetmap.org/wiki/Nominatim/Country_Codes", + body: " надає типові пари країн і мов для створення регіональних профілів.", + licenseLabel: "CC BY-SA 2.0", + licenseUrl: "https://wiki.openstreetmap.org/wiki/Wiki_content_license", + licenseBody: + " поширюється на вміст вікі та зазначена тут відповідно до вимог ліцензії.", + }, + mapPreviewsPrefix: "Для інтерактивного перегляду карт використовуються сервіси ", + mapPreviewsMiddle: "; відомості про учасників ", + mapPreviewsSuffix: " показано на карті.", + + localData: { + title: "Локальні набори даних", + body: `${BRAND_DISPLAY_NAME} містить невеликі локальні набори даних, створені на основі оброблених відкритих даних, тому підмінені значення залишаються статистично реалістичними. Нічого не завантажується з Інтернету — це локальні знімки, які оновлюються з кожним оновленням розширення.`, + steam: { + label: "Опитування апаратного та програмного забезпечення Steam", + url: "https://store.steampowered.com/hwsurvey/", + body: " (Valve) надає статистику роздільної здатності екрана, кількості ядер процесора й обсягу пам’яті для Windows, Linux і macOS, які використовуються для профілів локального обладнання.", + }, + chromiumDash: { + label: "ChromiumDash", + url: "https://chromiumdash.appspot.com/", + body: " надає справжні номери версій Chrome, які використовуються для User-Agent і Client Hints.", + }, + localeCatalog: { + prefix: "Каталоги ", + mozilla: { + label: "Mozilla", + url: "https://github.com/mozilla-firefox/firefox/blob/main/intl/locale/language.properties", + }, + middle: " та ", + chromium: { + label: "Chromium", + url: "https://github.com/chromium/chromium/blob/main/ui/base/l10n/l10n_util.cc", + }, + body: " надають назви мов, узгоджені з відповідними браузерними рушіями.", + }, + }, + }, + + projectLinks: { + description: `${BRAND_DISPLAY_NAME} має власний сайт і відкритий код. Перейдіть на сайт, перегляньте репозиторій або повідомте про помилку.`, + websiteLabel: "Веб-сайт", + websiteUrl: "https://privacything.com", + sourceLabel: "Джерело", + sourceUrl: "https://github.com/Privacy-Thing/browser-extension", + reportBugLabel: "Повідомити про помилку", + reportBugUrl: + "https://github.com/Privacy-Thing/browser-extension/issues/new?template=bug_report.yml", + }, + + usage: { + title: HOW_TO_USE_PRODUCT, + body1: + "Почніть зі створення регіонального профілю з місця пошуку, якщо вам потрібна швидка, реалістична відправна точка.", + body2: `Зберігайте окремий профіль для кожного місця, яке має імітувати ${BRAND_DISPLAY_NAME}, а потім призначте веб-сайти відповідному профілю.`, + body3: + "Використовуйте вікно розширення для швидкого призначення домену. Відкрийте «Налаштування», якщо вам потрібна повна панель керування профілями та правилами.", + body4: `Відкрийте «Налаштування» та «Додатково», якщо вам потрібно більше контролювати поведінку ${BRAND_DISPLAY_NAME}.`, + }, +} as const; diff --git a/src/ui/i18n/uk-sections/advanced.ts b/src/ui/i18n/uk-sections/advanced.ts new file mode 100644 index 0000000..13dae2b --- /dev/null +++ b/src/ui/i18n/uk-sections/advanced.ts @@ -0,0 +1,206 @@ +import { BRAND_DISPLAY_NAME } from "@/shared/brand"; +import { externalMapConsentCopy } from "@/ui/i18n/uk-sections/osm"; + +const TURN_OFF_PRODUCT = `Вимкніть ${BRAND_DISPLAY_NAME}`; + +export const advanced = { + runtimeTitle: "Розширені параметри", + runtimeDescription: "Налаштуйте докладне журналювання роботи розширення.", + copyLinkRuntimeLabel: "розширені налаштування", + copyLinkHelpLabel: "розширена допомога", + + noiseRadius: { + title: "Максимальний радіус координат за замовчуванням", + description: + "Установіть максимальний радіус координат за замовчуванням у метрах для нових регіональних профілів.", + copyLinkLabel: "максимальний радіус координат за замовчуванням", + }, + + generatedLocationRandomization: { + enabled: { + title: + "Випадково зміщувати координати під час створення нових регіональних профілів", + description: + "Цей параметр увімкнено за замовчуванням для нових профілів. Під час створення профілю його можна вимкнути, щоб використати точні координати.", + readWhy: "Навіщо це потрібно?", + tooltipPrivacy: `${BRAND_DISPLAY_NAME} допомагає захищати приватність. Якщо всі користувачі вибирають однакові точні координати з каталогу чи пошуку, сайтам легше їх розпізнавати.`, + tooltipExact: + "Потрібна точна точка? Вимкніть цей параметр, щоб використати збережені координати без зміщення.", + copyLinkLabel: "випадкове зміщення координат нових профілів", + }, + radius: { + title: "Радіус випадкового зміщення координат за замовчуванням", + description: + "Виберіть радіус за замовчуванням, який використовується, коли ввімкнено рандомізацію координат. Введіть ціле число від 1 до 99 км.", + inputLabel: "Радіус випадкового зміщення координат за замовчуванням у кілометрах", + copyLinkLabel: "радіус випадкового зміщення координат за замовчуванням", + }, + }, + + themeMode: { + title: "Тема", + description: `Виберіть системну, світлу або темну тему для ${BRAND_DISPLAY_NAME}.`, + label: "Зовнішній вигляд", + options: { + system: "Системна", + light: "Світла", + dark: "Темна", + }, + copyLinkLabel: "тема", + }, + + watchPositionDelay: { + title: "Інтервал оновлення геолокації (с)", + description: + "Установіть найкоротшу та найдовшу затримку між оновленнями місцезнаходження на веб-сайтах.", + copyLinkLabel: "інтервал оновлення геолокації", + }, + + debugMode: { + title: "Режим налагодження", + description: `Виводити докладні відомості про роботу ${BRAND_DISPLAY_NAME} у консоль розробника браузера.`, + copyLinkLabel: "режим налагодження", + }, + + experimental: { + title: "Експериментальні функції", + description: + "Спробуйте засоби захисту для API браузера, які ще розгортаються. Ці перемикачі доступні в кожному каналі випуску.", + copyLinkLabel: "експериментальні функції", + temporalApi: { + title: "Temporal API", + description: + "Підміняти дату, час, локаль і часовий пояс Temporal, якщо браузер підтримує цей API. Поліфіл не встановлюється.", + copyLinkLabel: "Temporal API", + }, + domainFencing: { + title: "Ізоляція за доменами", + description: + "Коли застосовується правило за замовчуванням або контейнер Firefox, кожен сайт отримує власний стабільний відбиток. Ваш регіональний профіль залишається незмінним.", + copyLinkLabel: "ізоляція за доменами", + }, + }, + + privacy: { + title: "Конфіденційність", + description: `${BRAND_DISPLAY_NAME} працює на вашому пристрої та нікуди не надсилає дані веб-перегляду. Єдині необов’язкові мережеві запити надходять, коли ви використовуєте пошук розташування або попередній перегляд карти під час налаштування регіональних профілів.`, + copyLinkLabel: "конфіденційність", + + osmConsent: { + title: externalMapConsentCopy.title, + description: externalMapConsentCopy.description, + stateUnknown: `Дозвіл ще не вибрано. ${BRAND_DISPLAY_NAME} запитає перед зверненням до зовнішнього картографічного сервісу.`, + statePrefix: "Доступ до зовнішніх карт", + stateEnabled: "дозволений — пошук місць і перегляд карти доступні", + stateDisabled: `заборонений — ${BRAND_DISPLAY_NAME} не звертатиметься до картографічних сервісів під час налаштування місць`, + copyLinkLabel: "зовнішній доступ до карти", + }, + }, + + display: { + title: "Зовнішній вигляд", + description: "Налаштуйте параметри візуального представлення та доступності.", + copyLinkLabel: "зовнішній вигляд", + + language: { + title: "Мова", + description: `${BRAND_DISPLAY_NAME} автоматично відповідає мові інтерфейсу вашого браузера.`, + optionEnglish: "Англійська", + optionSpanish: "Іспанська", + optionPortuguese: "Португальська", + optionRussian: "Російська", + optionUkrainian: "Українська", + soon: "АВТО", + copyLinkLabel: "мова", + }, + + reduceMotion: { + title: "Зменшення анімації", + description: `Вимкніть анімацію інтерфейсу в ${BRAND_DISPLAY_NAME}.`, + systemOverride: "Зменшення руху ввімкнено системними налаштуваннями доступності.", + copyLinkLabel: "зменшити рух", + }, + + accentColor: { + title: "Акцентний колір", + description: `Виберіть основний акцент ${BRAND_DISPLAY_NAME} із палітри за мотивами кольорів контейнерів Firefox.`, + copyLinkLabel: "колір акценту", + optionAriaLabel: (label: string) => `Використати ${label} як акцентний колір`, + options: { + teal: "Бірюзовий", + blue: "Синій", + green: "Зелений", + yellow: "Жовтий", + orange: "Помаранчевий", + red: "Червоний", + pink: "Рожевий", + purple: "Фіолетовий", + gray: "Сірий", + }, + }, + + highContrast: { + title: "Режим високої контрастності", + description: + "Збільште контрастність тексту та видимість меж для кращої читабельності.", + copyLinkLabel: "режим високої контрастності", + }, + }, + + danger: { + title: "Небезпечна зона", + description: `Ці дії можуть замінити, видалити або відновити ваші локально збережені налаштування та дані ${BRAND_DISPLAY_NAME}.`, + copyLinkLabel: "Небезпечна зона", + + spoofing: { + title: TURN_OFF_PRODUCT, + description: `Вимкнути всі механізми захисту ${BRAND_DISPLAY_NAME}, доки їх знову не ввімкнуть.`, + copyLinkLabel: TURN_OFF_PRODUCT, + }, + + export: { + title: "Експорт", + description: + "Збережіть регіональні профілі, правила й інші налаштування в резервній копії JSON.", + button: "Експорт налаштувань", + copyLinkLabel: "параметри експорту", + }, + + import: { + title: "Імпорт", + description: + "Замініть поточні локальні налаштування попередньо експортованою резервною копією JSON.", + button: "Імпорт налаштувань", + copyLinkLabel: "налаштування імпорту", + }, + + reload: { + title: "Перезавантажити", + description: + "Завантажте налаштування зі сховища розширення заново та скасуйте незбережені зміни.", + button: "Перезавантажити налаштування", + copyLinkLabel: "перезавантажити налаштування", + }, + + reset: { + title: "Скинути", + description: + "Поверніть початкові налаштування й видаліть збережені профілі та правила.", + button: "Скидання налаштувань", + copyLinkLabel: "скинути налаштування", + confirmTitle: "Скинути налаштування?", + confirmBody: + "Це назавжди видалить ваші збережені профілі, правила та спеціальні налаштування. Цю дію не можна скасувати.", + onboardingToggleLabel: "Повторно відкрити початкове налаштування після скидання", + onboardingToggleDescription: + "Після скидання знову відкриється майстер початкового налаштування.", + }, + }, + + help: { + title: "Додатково", + body1: `Використовуйте Додатково, щоб точно налаштувати захист, тимчасово вимкнути ${BRAND_DISPLAY_NAME} або керувати своїми локально збереженими налаштуваннями та даними.`, + body2: + "Імпорт і експорт допомагають переносити налаштування між браузерами. Повторне завантаження скасовує незбережені зміни, а скидання повертає початкові налаштування.", + }, +} as const; diff --git a/src/ui/i18n/uk-sections/common.ts b/src/ui/i18n/uk-sections/common.ts new file mode 100644 index 0000000..e9d5bbd --- /dev/null +++ b/src/ui/i18n/uk-sections/common.ts @@ -0,0 +1,42 @@ +import { BRAND_DISPLAY_NAME } from "@/shared/brand"; + +export const common = { + copyLinkTo: (section: string) => `Скопіювати посилання: ${section}`, + actions: { + cancel: "Скасувати", + reset: "Скинути", + close: "Закрити", + create: "Створити", + save: "Зберегти", + back: "Назад", + continue: "Продовжити", + edit: "Редагувати", + delete: "Видалити", + duplicate: "Дублювати", + search: "Пошук", + clear: "Очистити", + deleteSelected: "Видалити вибране", + clearSelection: "Зняти виділення", + openPrivacyPolicy: "Відкрити політику конфіденційності", + allowOpenStreetMap: "Дозволити доступ до карти", + notNow: "Не зараз", + openPlayground: "Відкрити пісочницю", + }, + selectionCount: (count: number) => `Вибрано: ${count}`, + fields: { + name: "Назва", + latitude: "Широта", + longitude: "Довгота", + accuracy: "Точність", + noiseRadius: "Макс. радіус (м)", + timeZone: "Часовий пояс", + }, + coordinateRandomization: { + labelBefore: "Випадкове зміщення координат у межах", + labelAfter: "км", + radiusInputLabel: "Радіус випадкового зміщення координат у кілометрах", + readWhy: "Навіщо це потрібно?", + tooltipPrivacy: `${BRAND_DISPLAY_NAME} захищає приватність. Профілями має бути зручно користуватися, але точне повторення координат із каталогу або пошуку може вирізняти користувачів ${BRAND_DISPLAY_NAME} серед інших.`, + tooltipExact: `Потрібна точна точка? Вимкніть цей перемикач, і ${BRAND_DISPLAY_NAME} використовуватиме збережені координати без зміщення. Типові значення можна змінити в розділі «Налаштування» → «Геолокація» → «Додатково».`, + }, +} as const; diff --git a/src/ui/i18n/uk-sections/demo.ts b/src/ui/i18n/uk-sections/demo.ts new file mode 100644 index 0000000..f871b8e --- /dev/null +++ b/src/ui/i18n/uk-sections/demo.ts @@ -0,0 +1,151 @@ +import { BRAND_DISPLAY_NAME } from "@/shared/brand"; + +const SPOOFED_PRODUCT = `Підмінено — ${BRAND_DISPLAY_NAME}`; + +export const demo = { + loadingSettings: "Завантаження налаштувань…", + noLocationsTitle: "Регіональних профілів ще немає", + noLocationsBody: + "Створіть принаймні один регіональний профіль у налаштуваннях, щоб користуватися пісочницею.", + openSettingsButton: "Відкрити налаштування", + + locationPreview: { + title: "Попередній перегляд", + activeLocationLabel: "Активний профіль", + activeLocationDescription: + "Виберіть збережений регіональний профіль, щоб переглянути його значення в браузері. Зміна профілю скидає маршрут і оновлює попередній перегляд геолокації.", + activeLocationPlaceholder: "Вибрати профіль…", + playgroundCadenceLabel: "Інтервал оновлення в пісочниці", + realSiteCadenceLabel: "Інтервал оновлення на сайті", + configuredDelayLabel: "Задана затримка координат", + callbackDelayLabel: "Затримка відповіді", + runtimeModeLabel: "Режим роботи", + runtimeModeSimple: "Типовий інтервал", + realLocationTitleIdle: "Порівняйте з поточним розташуванням веб-переглядача", + realLocationTitleLoading: "Запит поточного розташування браузера", + realLocationTitleGranted: "Поточне розташування браузера готове", + realLocationTitleDenied: "Доступ до місцезнаходження відхилено", + realLocationTitleUnavailable: "Розташування веб-переглядача недоступне", + realLocationDescription: + "Запитайте справжнє місцезнаходження браузера один раз, щоб порівняти його з попереднім переглядом нижче.", + realLocationGrantedDescription: + "Поточне місцезнаходження вашого браузера тепер доступне в рядках геолокації нижче для порівняння.", + realLocationDeniedDescription: + "Браузер відмовив у доступі до реальної геолокації. Ви можете спробувати ще раз, якщо дозвіл зміниться.", + realLocationUnavailable: + "Порівняння реального місцезнаходження недоступне, оскільки цей веб-переглядач не надає тут геолокації.", + realLocationRefresh: "Оновити реальне розташування", + }, + + localMachineTitle: "Цей браузер", + spoofedTitle: SPOOFED_PRODUCT, + waitingForPermission: "Очікування дозволу браузера…", + permissionDenied: "Браузер відхилив дозвіл", + geolocationUnavailable: "Геолокація недоступна", + requestRealLocation: "Запит реального місцезнаходження", + requestRealLocationHintTitle: "Справжнє місцезнаходження ще не завантажено", + requestRealLocationTableHint: + "Надішліть запит на справжнє розташування вашого браузера, щоб порівняти його тут.", + waitingForFix: "Очікування перших координат…", + selectLocationPrompt: + "Виберіть регіональний профіль вище, щоб переглянути його значення.", + + comparison: { + language: "navigator.language", + languages: "navigator.languages", + timeZone: "Intl…resolvedOptions().timeZone", + acceptLanguage: "Заголовок Accept-Language", + timeZoneOffset: "new Date().getTimezoneOffset()", + dateToString: "new Date().toString()", + dateToDateString: "new Date().toDateString()", + dateToTimeString: "new Date().toTimeString()", + dateLocaleString: "new Date().toLocaleString()", + dateLocaleDateString: "new Date().toLocaleDateString()", + dateLocaleTimeString: "new Date().toLocaleTimeString()", + currentPosition: "navigator.geolocation.getCurrentPosition()", + coords: "geolocation.coords", + timestamp: "geolocation.timestamp", + timestampInfoLabel: "Про відображення позначки часу геолокації", + timestampTooltip: `${BRAND_DISPLAY_NAME} повертає необроблену позначку часу Unix у мілісекундах. Показана тут зрозуміла для людини дата лише для зручності та відформатована відповідно до часового поясу стовпця, що відображається.`, + userAgent: "navigator.userAgent", + appVersion: "navigator.appVersion", + vendor: "navigator.vendor", + hardwareConcurrency: "navigator.hardwareConcurrency", + deviceMemory: "navigator.deviceMemory", + platform: "navigator.platform", + pixelDepth: "screen.pixelDepth", + screenMetrics: "screen.width/height/avail*/colorDepth", + devicePixelRatio: "window.devicePixelRatio", + canvas2d: "Результат перевірки Canvas 2D", + webglRenderer: "Перевірка рендерера WebGL", + webglDebugExtension: "WEBGL_debug_renderer_info", + webglReadPixels: "Перевірка WebGL readPixels()", + audioFingerprint: "Перевірка AnalyserNode і AudioBuffer", + clientHintBrands: "navigator.userAgentData.brands", + clientHintPlatform: "navigator.userAgentData.platform", + clientHintPlatformVersion: "navigator.userAgentData.platformVersion", + clientHintArchitecture: "navigator.userAgentData.architecture", + clientHintBitness: "navigator.userAgentData.bitness", + clientHintModel: "navigator.userAgentData.model", + clientHintMobile: "navigator.userAgentData.mobile", + clientHintFullVersionList: "navigator.userAgentData.fullVersionList", + secChUa: "Заголовок Sec-CH-UA", + secChUaPlatform: "Заголовок Sec-CH-UA-Platform", + secChUaMobile: "Заголовок Sec-CH-UA-Mobile", + secChUaFullVersionList: "Заголовок Sec-CH-UA-Full-Version-List", + webRTCIcePolicy: "Політика ICE RTCPeerConnection", + probePending: "Збирання даних…", + notAvailable: "N/A", + spoofedMatchesLocal: + "Для цієї ідентичності значення попереднього перегляду збігається з даними вашого браузера.", + browserVersionNote: (versionToken: string) => + `${BRAND_DISPLAY_NAME} зберігає нормалізовані маркери версії браузера та не рандомізує варіанти заповнювачів, як-от ${versionToken}.`, + }, + + previewSeed: { + title: "Попередній перегляд ідентичності", + description: "Виберіть ідентичність браузера для цього попереднього перегляду.", + inputAriaLabel: "Код ідентичності для попереднього перегляду", + placeholder: "Код ідентичності", + hint: "Введіть 6 малих літер або цифр. Змініть код, щоб переглянути іншу ідентичність.", + randomize: "Створити нову ідентичність", + }, + + sections: { + localeDate: "Локаль і Date", + networkHeaders: "Мережні заголовки", + geolocation: "Геолокація", + browserFingerprint: "Ідентичність браузера", + webglCanvas: "Canvas і WebGL", + screen: "Екран", + audio: "Аудіо", + webRTC: "WebRTC", + }, + + map: { + title: "Попередній перегляд карти", + noLocationTitle: "Спочатку виберіть профіль", + noLocationDescription: + "Виберіть збережений регіональний профіль вище, щоб увімкнути керування картою та маршрутними точками.", + osmRequired: "Потрібен доступ до карти", + osmRequiredDescription: + "Дозвольте зовнішній доступ до карти для попереднього перегляду підміненої позиції на інтерактивній карті.", + demoIntervalLabel: "Демонстраційний інтервал (2–5 с)", + clearButton: "Очистити", + }, + + disclaimer: { + title: "Це попередній перегляд", + body: `Тут можна перевірити, як ${BRAND_DISPLAY_NAME} показуватиме вибраний регіональний профіль на сайтах. Мова, локаль, час, заголовки та інші значення відображаються як попередній перегляд на основі даних профілю. Геолокація працює так само, як на захищених сайтах, але в пісочниці координати оновлюються частіше, щоб зміни було легше помітити.`, + }, + + howItWorks: { + title: "Як читати цей попередній перегляд", + body1: + "Мова, локаль і час — тут показано, які значення побачать сайти для вибраного профілю. Саму сторінку пісочниці ці дані не змінюють.", + body2: `Геолокація — карта й координати відтворюють переміщення та частоту оновлення, які діятимуть на захищених сайтах.`, + body3: `Дані браузера й заголовки — порівняйте справжні дані браузера зі значеннями, які ${BRAND_DISPLAY_NAME} показуватиме для вибраного профілю з урахуванням ваших налаштувань захисту.`, + body4: + "Пісочниця оновлюється швидше, ніж звичайний перегляд, тому зміни легше помітити під час тестування.", + }, +} as const; diff --git a/src/ui/i18n/uk-sections/firefox-containers.ts b/src/ui/i18n/uk-sections/firefox-containers.ts new file mode 100644 index 0000000..288d06c --- /dev/null +++ b/src/ui/i18n/uk-sections/firefox-containers.ts @@ -0,0 +1,101 @@ +import { BRAND_DISPLAY_NAME } from "@/shared/brand"; + +export const firefoxContainers = { + copyLinkLabel: "контейнери", + copyLinkHelpLabel: "довідка про контейнери", + title: "Контейнери", + description: `Керуйте контейнерами Firefox і надайте кожному з них необов’язковий регіональний профіль ${BRAND_DISPLAY_NAME} з одного місця.`, + refreshTitle: "Оновити список контейнерів", + refreshButton: "Оновити список", + createButton: "Новий контейнер", + deleteButton: "Видалити", + removeAssignmentButton: "Видалити призначення", + editActionTitle: (name: string) => `Редагувати контейнер «${name}»`, + deleteActionTitle: (name: string) => `Видалити контейнер «${name}»`, + removeAssignmentActionTitle: (name: string) => + `Видалити призначення профілю для видаленого контейнера «${name}»`, + showInactiveLabel: "Показати неактивні контейнери", + filterPlaceholder: "Пошук контейнерів...", + loading: "Завантаження контейнерів...", + noResults: "Жоден контейнер не відповідає поточному фільтру.", + columns: { + container: "Контейнер", + location: "Профіль", + actions: "Дії", + }, + noLocationAssigned: "Профіль не призначено", + missingLocation: "Призначений регіональний профіль відсутній", + inactiveBadge: "Видалено", + deletedContainerLabel: "Видалений контейнер", + emptyTitle: "Контейнерів Firefox ще немає", + emptyBody: `Створіть свій перший контейнер тут або в налаштуваннях Firefox, а потім призначте йому профіль ${BRAND_DISPLAY_NAME}.`, + unavailableTitle: "Контейнери Firefox недоступні", + unavailableBody: + "Увімкніть контейнери Firefox, а потім оновіть список. Якщо проблема не зникне, ця збірка Firefox може не дозволяти розширенню керувати ними.", + unavailableHint: `${BRAND_DISPLAY_NAME} може керувати контейнерами, лише коли Firefox Containers увімкнено.`, + editor: { + createTitle: "Створити контейнер", + editTitlePrefix: "Редагувати контейнер", + createDescription: `Виберіть назву, колір, піктограму та додаткові параметри ${BRAND_DISPLAY_NAME} для нового контейнера Firefox.`, + editDescription: `Оновіть властивості контейнера Firefox і його додаткові параметри ${BRAND_DISPLAY_NAME} за одне збереження.`, + saveButton: "Зберегти", + previewUntitled: "Контейнер без назви", + nameRequired: "Назва контейнера не може бути порожньою.", + defaultRulePreviewTitle: "Ще немає збережених налаштувань", + defaultRulePreviewLocation: (label: string) => + `Цей контейнер ще не має власних налаштувань ${BRAND_DISPLAY_NAME}, тому він використовує профіль правила за замовчуванням: ${label}. Збережіть налаштування контейнера, щоб надати йому власні налаштування.`, + defaultRulePreviewProtections: `Цей контейнер ще не має власних налаштувань ${BRAND_DISPLAY_NAME}, тому він використовує лише засоби захисту за замовчуванням, які не потребують профілю. Збережіть налаштування контейнера, щоб надати йому власні налаштування.`, + defaultRulePreviewUnconfigured: `Цей контейнер ще не має власних налаштувань ${BRAND_DISPLAY_NAME}. Правило за замовчуванням увімкнено, але воно ще не має призначеного профілю чи особливих налаштувань захисту. Збережіть налаштування контейнера, щоб надати цьому контейнеру власні налаштування, або завершіть налаштування правила за замовчуванням.`, + defaultRulePreviewDisabled: `Цей контейнер ще не має власних налаштувань ${BRAND_DISPLAY_NAME}. Правило за замовчуванням вимкнено, тому ${BRAND_DISPLAY_NAME} тут залишається вимкненим. Збережіть налаштування контейнера, щоб надати цьому контейнеру власні налаштування, або ввімкніть правило за замовчуванням.`, + defaultRulePreviewMissing: `Цей контейнер ще не має власних налаштувань ${BRAND_DISPLAY_NAME}. Немає збережених налаштувань правила за замовчуванням, тому ${BRAND_DISPLAY_NAME} тут не працює. Збережіть налаштування контейнера, щоб надати цьому контейнеру власні налаштування, або встановіть правило за замовчуванням.`, + fields: { + name: "Назва", + color: "Колір", + icon: "Значок", + enabled: "Увімкнено", + location: "Профіль", + locationProfile: "регіональний профіль", + }, + unassignedLocation: "Профіль не призначено", + enabledHint: + "Якщо вимкнено, це призначення контейнера не застосовується. Його налаштування залишаються збереженими.", + locationProfileHint: + "Виберіть профіль для контейнера. Якщо залишити поле порожнім, діятимуть його налаштування захисту й, за наявності, правило за замовчуванням.", + identity: { + sectionTitle: "Цифрова ідентичність", + sectionDescription: + "Цей контейнер зберігає власну цифрову ідентичність для підміни. Змінюйте її лише тоді, коли потрібен новий цифровий відбиток і чистий стан пов’язаних сайтів.", + actionDescription: + "Видаляє пов’язані дані сайтів і створює для контейнера нову цифрову ідентичність.", + actionLabel: "Нова ідентичність", + confirmTitle: (name: string) => `Створити нову ідентичність для «${name}»?`, + confirmDescription: + "Це видалить файли cookie, дані сховищ, Service Workers і кеші сайтів, пов’язаних із контейнером. Потім буде створено нову цифрову ідентичність.", + confirmDomainsLabel: `${BRAND_DISPLAY_NAME} очистить дані браузера для цих доменів:`, + confirmNoDomains: `Дані браузера для цього контейнера ще не записані. ${BRAND_DISPLAY_NAME} однаково створить нову ідентичність для підміни.`, + confirmLabel: "Створити нову ідентичність", + rotateSuccess: "Для контейнера створено нову ідентичність.", + rotateError: "Не вдалося створити нову ідентичність для контейнера.", + }, + }, + confirmations: { + deleteTitle: (name: string) => `Видалити контейнер «${name}»?`, + deleteBody: `Це видаляє контейнер Firefox і очищає його призначення профілю ${BRAND_DISPLAY_NAME}.`, + removeOrphanTitle: "Видалити застаріле призначення контейнера?", + removeOrphanBody: `Контейнер Firefox більше не існує. Буде видалено лише застаріле призначення профілю ${BRAND_DISPLAY_NAME}.`, + }, + messages: { + created: (name: string) => `Створено контейнер «${name}».`, + updated: (name: string) => `Збережено контейнер «${name}».`, + deleted: (name: string) => `Видалено контейнер «${name}».`, + orphanRemoved: "Видалено застаріле призначення контейнера.", + saveFailed: "Не вдалося зберегти контейнер.", + deleteFailed: "Не вдалося видалити контейнер.", + }, + help: { + title: "Контейнери", + body1: `Контейнери Firefox дають змогу вибирати налаштування для окремих груп вкладок. ${BRAND_DISPLAY_NAME} спочатку перевіряє правила для доменів, а потім призначення контейнерів.`, + body2: `Редактор контейнерів зберігає деталі контейнера Firefox разом із налаштуваннями ${BRAND_DISPLAY_NAME}, такими як увімкнений стан, необов’язковий регіональний профіль і спеціальні налаштування захисту.`, + body3: `Неактивні рядки — це призначення для контейнерів Firefox, видалених поза ${BRAND_DISPLAY_NAME}. Перевірте ці записи й видаліть непотрібні вручну.`, + }, +} as const; diff --git a/src/ui/i18n/uk-sections/locations.ts b/src/ui/i18n/uk-sections/locations.ts new file mode 100644 index 0000000..b330d78 --- /dev/null +++ b/src/ui/i18n/uk-sections/locations.ts @@ -0,0 +1,108 @@ +import { BRAND_DISPLAY_NAME } from "@/shared/brand"; +import { slavicCount } from "@/ui/shared/slavic-plural"; + +const PRODUCT_NOT_LOWER = `чим не є ${BRAND_DISPLAY_NAME}`; + +export const locations = { + title: "Регіональні профілі", + description: + "Регіональні профілі об’єднують координати, локаль і часовий пояс для підміни.", + addManualButton: "Додати вручну", + actionsMenuLabel: "Дії з профілями", + generateButton: "Створити профіль", + searchPlaceholder: "Пошук профілів…", + unused: "Не використовується", + assigned: (count: number) => + slavicCount(count, ["призначення", "призначення", "призначень"]), + viewAssignedRulesAriaLabel: (locationLabel: string, count: number) => + `Показати ${slavicCount(count, ["правило", "правила", "правил"])} для профілю «${locationLabel}»`, + copyLinkLabel: "Регіональні профілі", + copyLinkHelpLabel: "довідка про регіональні профілі", + + playgroundCard: { + title: "Пісочниця", + body1: + "Перевірте збережений профіль перед призначенням сайтам. Пісочниця показує регіон, часовий пояс, формат дат і підмінені координати так само, як їх побачать захищені сайти.", + body2: + "У пісочниці координати оновлюються кожні 2–5 секунд, щоб рух було легко перевірити. На звичайних сайтах діє заданий вами інтервал.", + }, + + help: { + title: "Регіональні профілі", + body1: + "Регіональний профіль має описувати одне правдоподібне місце. Локаль, список мов і часовий пояс мають відповідати регіону збережених координат.", + body2: `Зміни зберігаються автоматично. Коли ви редагуєте поле, ${BRAND_DISPLAY_NAME} записує оновлення після короткої паузи.`, + body3: + "Збережений регіональний профіль змінює значення на рівні браузера, такі як геолокація, локаль і часовий пояс. Це не змінює IP-адресу чи маршрут мережевого трафіку.", + privacyTitle: "Конфіденційність", + privacyBody: `Пошук і попередній перегляд карти необов’язкові. ${BRAND_DISPLAY_NAME} зв’язується лише з OpenStreetMap Nominatim для пошуку та OpenFreeMap для попереднього перегляду інтерактивної карти після того, як ви це дозволите.`, + networkTitle: "Обмеження мережі", + networkBodyPrefix: + "Потрібно змінити місцеперебування на рівні мережі? Скористайтеся VPN, проксі або DNS. Дізнайтеся, ", + networkBodyLinkLabel: PRODUCT_NOT_LOWER, + networkBodySuffix: ".", + }, + + editor: { + title: "Редагувати профіль", + description: + "Оновіть збережений регіональний профіль, включаючи координати, регіональні формати та поведінку звітів про місцезнаходження.", + deleteBlockedTitle: "Цей профіль ще призначено", + deleteBlockedDescription: + "Змініть або видаліть усі призначення нижче, перш ніж видаляти профіль.", + deleteBlockedButtonTitle: "Спочатку видаліть усі призначення цього профілю.", + disabledDependencySuffix: "(вимкнено)", + mapDisabledTitle: "Карту вимкнено", + mapDisabledBody: + "Попередній перегляд карти не завантажується, оскільки ви не дозволили зовнішні запити карти.", + geolocationSectionTitle: "Геолокація", + geolocationSectionDescription: + "Координати, точність і максимальний розкид, дозволений для підмінених позицій.", + localeSectionTitle: "Час і мова", + localeSectionDescription: + "Зберігайте значення локалі у відповідності з тим самим регіоном, що й збережені координати.", + primaryLocaleLabel: "Основна локаль", + languageDescription: + "Це основна локаль профілю для дат, чисел та інших локалізованих значень.", + languageBehaviorDescription: + "Якщо параметр «Віддавати перевагу англійській на веб-сайтах» вимкнено, веб-сайти також бачать це значення як navigator.language. Навіть якщо спочатку англійська, ця мова все ще контролює форматування дати та чисел за замовчуванням.", + preferredLanguagesLabel: "Бажані мови", + languagesDescription: + "Упорядкований список налаштувань мови браузера для цього профілю.", + languagesBehaviorDescription: `${BRAND_DISPLAY_NAME} розкриває цей порядок як navigator.languages і відповідні мовні налаштування. Спершу збережіть основну мову, а потім додайте реалістичні альтернативні мови, які сайт міг би вірогідно побачити для цього профілю.`, + preferEnglishContentLabel: "Віддавати перевагу англійській на сайтах", + preferEnglishContentDescriptionPrefix: `${BRAND_DISPLAY_NAME} зберігає вибраний регіон, але ставить`, + preferEnglishContentDescriptionSuffix: + "на перше місце в списку мов браузера, щоб сайти частіше показували вміст англійською.", + preferEnglishContentLockedTagTitle: (locale: string) => + `${locale} додається через пріоритет англійської мови, тому тут його не можна видалити.`, + accuracyDescription: + "Контролює значення точності, яке веб-сайти бачать у результатах геолокації.", + noiseRadiusDescription: + "Максимально дозволена відстань між підміненими координатами та збереженою точкою цього профілю.", + }, + + generator: { + title: "Створити профіль", + searchStepDescription: + "Знайдіть місто, адресу чи назву місця, щоб створити новий профіль.", + resultStepDescription: `Виберіть відповідний результат, перш ніж ${BRAND_DISPLAY_NAME} створить профіль.`, + languageStepDescription: + "Спочатку виберіть основну локаль браузера для цього профілю.", + confirmStepDescription: "Перевірте місце на карті, перш ніж додавати профіль.", + locationLabel: "Розташування", + locationPlaceholder: "Варшава, Польща", + osmDisclaimer: + "Для пошуку використовується зовнішній API OpenStreetMap Nominatim. Інтерактивний попередній перегляд використовує векторні плитки та шрифти OpenFreeMap після згоди.", + resultSelectLabel: "Результат пошуку", + resultStepBody: + "OpenStreetMap повернув більше одного можливого збігу. Виберіть місце, яке ви мали на увазі, а потім продовжуйте.", + resultStepHint: "Виберіть результат, щоб продовжити.", + languageSelectLabel: "Мова браузера", + languageStepBody: + "Для цього місця доступно кілька мов браузера. Виберіть потрібну для профілю.", + languageStepHint: + "Якщо потрібної вам мови немає в списку нижче, ви можете вибрати іншу на наступному кроці.", + resultPrefix: "Результат: ", + }, +} as const; diff --git a/src/ui/i18n/uk-sections/options.ts b/src/ui/i18n/uk-sections/options.ts new file mode 100644 index 0000000..f3ea280 --- /dev/null +++ b/src/ui/i18n/uk-sections/options.ts @@ -0,0 +1,125 @@ +import { workerHandlingModeCopy } from "./shared-worker"; + +import { BRAND_DISPLAY_NAME } from "@/shared/brand"; +import { slavicCount } from "@/ui/shared/slavic-plural"; + +export const options = { + title: "Налаштування", + tabsAriaLabel: "Розділи налаштувань", + spoofingOffBannerTitle: "Підміну вимкнено", + spoofingOffBannerBody: `${BRAND_DISPLAY_NAME} зараз не застосовує підмінені значення місцезнаходження або браузера.`, + spoofingOffBannerAction: "Увімкнути підміну", + spoofingTurnedOffToast: `${BRAND_DISPLAY_NAME} вимкнено.`, + spoofingTurnedOnToast: `${BRAND_DISPLAY_NAME} увімкнуто.`, + + tabs: { + locations: "Регіональні профілі", + rules: "Правила для доменів", + trustedSites: "Довірені сайти", + playground: "Пісочниця", + options: "Налаштування", + advanced: "Додатково", + about: "Про розширення", + }, +} as const; + +export const optionsPage = { + browserFingerprintSpoofing: { + title: "Глобальні налаштування захисту", + description: `Виберіть, який захист браузера ${BRAND_DISPLAY_NAME} використовує за замовчуванням. Конкретніші налаштування можуть перевизначити окремі механізми захисту, тоді як довірені сайти можуть вимкнути їх для відповідних сайтів.`, + disabledNote: `Захист браузера скрізь вимкнено. Увімкніть його, щоб налаштувати підміну даних за замовчуванням.`, + copyLinkLabel: "механізми захисту браузера", + items: { + geolocation: { + label: "Геолокація", + description: `Керує Geolocation API. Коли це ввімкнено, ${BRAND_DISPLAY_NAME} відповідає на запити про місцезнаходження за допомогою вашого активного регіонального профілю. Якщо вимкнути захист, сайти отримають справжній результат геолокації браузера.`, + advancedButton: "Додатково", + advancedModal: { + title: "Розширені налаштування геолокації", + description: + "Налаштуйте радіус координат за замовчуванням і час оновлення місцезнаходження.", + }, + }, + timeLocale: { + label: "Час і локаль", + description: `Керує Date, Intl, navigator.language, navigator.languages і заголовками мови. Коли це ввімкнено, ${BRAND_DISPLAY_NAME} зберігає ці значення узгодженими з вашим активним профілем. Якщо вимкнути захист, сайти бачитимуть реальні регіональні налаштування вашого браузера.`, + }, + canvas: { + label: "Canvas", + description: `Сайти можуть малювати приховане зображення та використовувати невеликі відмінності у візуалізації, щоб розпізнати ваш браузер. Коли цей параметр увімкнено, ${BRAND_DISPLAY_NAME} додає контрольований шум, тому результат важче повторно використати як цифровий відбиток. Якщо вимкнути захист, сайти отримають справжні дані Canvas.`, + }, + webGL: { + label: "WebGL", + description: `WebGL розкриває такі графічні деталі, як модель графічного процесора, рендерер і поведінка драйвера. Коли це ввімкнено, ${BRAND_DISPLAY_NAME} замінює ці підказки узгодженим профілем, який відповідає вашій поточній платформі. Якщо вимкнути захист, сайти зможуть читати графічні дані, ближчі до справжніх даних пристрою.`, + }, + audio: { + label: "Аудіо", + description: `Аудіо API створюють крихітні апаратні відмінності, які сайти можуть вимірювати у фоновому режимі. Коли це ввімкнено, ${BRAND_DISPLAY_NAME} дещо змінює ці значення, щоб вони були менш корисними для стеження. Якщо вимкнути захист, аудіовихід залишиться без змін.`, + }, + navigator: { + label: "Navigator", + description: `Поля Navigator показують такі деталі, як ваша платформа, налаштування мови, дані процесора та інші ідентифікаційні дані браузера. Коли це ввімкнено, ${BRAND_DISPLAY_NAME} маскує вибрані ідентифікаційні поля, зберігаючи їх внутрішню узгодженість. Якщо вимкнути захист, сайти бачитимуть більше даних вашого справжнього браузера.`, + }, + screen: { + label: "Екран", + description: `Розмір екрана, щільність пікселів і глибина кольору допомагають вирізнити пристрій серед інших. Коли це ввімкнено, ${BRAND_DISPLAY_NAME} маскує саме ці деталі дисплея. Якщо вимкнути захист, сайти отримають справжні параметри екрана.`, + }, + clientHints: { + label: "Client Hints", + description: `Client Hints передає дані версії браузера, платформи та пристрою через заголовки запитів і JavaScript API. Коли це ввімкнено, ${BRAND_DISPLAY_NAME} зберігає ці значення узгодженими з вашою підміненою ідентифікацією браузера. Якщо вимкнути захист, сайти отримають інформацію про ваш справжній браузер і платформу.`, + }, + battery: { + label: "Акумулятор", + description: `Зміни стану акумулятора можуть допомогти сайтам пов’язувати різні відвідування з одним пристроєм. Коли цей параметр увімкнено, ${BRAND_DISPLAY_NAME} повідомляє про повністю заряджений акумулятор, підключений до живлення, а не про реальний стан пристрою. Якщо вимкнути захист, сайти зможуть читати оригінальний Battery Status API.`, + }, + clientHintsVersionRotation: { + label: "Змінювати номери збірки й патча", + description: (count: number) => + `Змінює номери збірки й патча Chromium, передані через Client Hints, зберігаючи основну версію встановленого браузера. У каталозі ${BRAND_DISPLAY_NAME} є нещодавні збірки Chromium; для цієї версії браузера можна вибрати ${slavicCount(count, ["відповідне значення", "відповідні значення", "відповідних значень"])}. Скорочений рядок User-Agent залишається у стандартному для браузера форматі .0.0.0.`, + hintPrefix: "наприклад:", + hint: "139.0.[build].[patch]", + }, + webRTC: { + label: "WebRTC", + description: `WebRTC може розкривати локальні або зовнішні IP-адреси, навіть якщо ви використовуєте VPN або проксі. Коли це ввімкнено, ${BRAND_DISPLAY_NAME} просить браузер використовувати найсуворіший режим обробки IP. Якщо вимкнути захист, WebRTC працюватиме нормально, що може розкрити більше деталей мережі.`, + }, + serviceWorker: { + label: "Service Workers", + description: `Service Workers працюють у фоновому режимі та дозволяють сайтам зберігати довгострокові дані та ідентифікатори. Увімкніть цей параметр, щоб ${BRAND_DISPLAY_NAME} за замовчуванням блокував реєстрацію Service Workers на всіх сайтах.`, + warning: + "Блокування Service Workers може порушити роботу встановлених вебзастосунків (PWA), офлайн-режим, push-сповіщення та фонову синхронізацію. Деякі застосунки також можуть працювати повільніше або втрачати функції.", + defaultState: + "За замовчуванням блокування вимкнено. Його можна ввімкнути для всіх сайтів, а потім дозволити реєстрацію Service Workers для окремих доменів за допомогою правил.", + allow: "Дозволити", + block: "Блокувати", + }, + sharedWorker: { + label: "Dedicated і Shared Workers", + descriptionLead: + "Виберіть спосіб запуску Dedicated і Shared Workers за замовчуванням. Правила для доменів та контейнери Firefox можуть замінити цю політику для відповідних сайтів.", + copyLinkLabel: "обробка Dedicated і Shared Workers", + native: workerHandlingModeCopy.native.label, + nativeDescription: workerHandlingModeCopy.native.description, + spoof: workerHandlingModeCopy.spoof.label, + spoofDescription: workerHandlingModeCopy.spoof.description, + strict: workerHandlingModeCopy.strict.label, + strictDescription: workerHandlingModeCopy.strict.description, + }, + }, + }, + badgeQueryCount: { + label: "Показувати кількість викликів на значку розширення", + description: `Показувати на значку розширення кількість викликів API браузера, оброблених ${BRAND_DISPLAY_NAME}, замість текстової мітки.`, + includeDateCalls: { + label: "Враховувати виклики Date і Temporal", + description: + "Враховувати виклики Date.* і Temporal.* на значку. Вимкніть цей параметр, щоб часті перевірки часу не завищували лічильник.", + }, + }, + copyLinkHelpLabel: "довідка з налаштувань", + help: { + title: "Налаштування", + body1: `Захист веб-переглядача дозволяє вибрати, які значення ${BRAND_DISPLAY_NAME} підмінює за замовчуванням. Ці параметри впливають на кожен сайт, якщо правило для домену або контейнер Firefox не вказує інше.`, + body2: `Налаштування приватності керують єдиними необов’язковими мережевими запитами, які ${BRAND_DISPLAY_NAME} робить під час налаштування місцезнаходження. Якщо залишити ці запити вимкненими, ${BRAND_DISPLAY_NAME} працюватиме й далі — місця можна буде додавати вручну, без пошуку й перегляду карти.`, + }, +} as const; diff --git a/src/ui/i18n/uk-sections/osm.ts b/src/ui/i18n/uk-sections/osm.ts new file mode 100644 index 0000000..f91d3b5 --- /dev/null +++ b/src/ui/i18n/uk-sections/osm.ts @@ -0,0 +1,12 @@ +import { BRAND_DISPLAY_NAME } from "@/shared/brand"; + +export const externalMapConsentCopy = { + title: "Дозволити пошук місць і завантаження карт", + description: `Дозвольте ${BRAND_DISPLAY_NAME} надсилати пошукові запити до OpenStreetMap Nominatim, а під час показу карти завантажувати векторні тайли та шрифти з OpenFreeMap. Ці сервіси отримають текст пошуку, запити карти й шрифтів, а також вашу IP-адресу. Збережені правила, регіональні профілі та історія відвідувань їм не передаються. Якщо ви волієте вводити координати вручну без звернень до картографічних сервісів, залиште це налаштування вимкненим.`, +} as const; + +export const osm = { + modalTitle: "Дозволити запити до карт і пошуку?", + body1: `Доступ до зовнішніх карт необов’язковий. ${BRAND_DISPLAY_NAME} не надсилає цим сервісам ваші профілі, правила чи історію відвідувань.`, + body2: `Якщо ви дозволите доступ, ${BRAND_DISPLAY_NAME} зможе звертатися до OpenStreetMap Nominatim для пошуку місць і до OpenFreeMap для інтерактивного перегляду карти.`, +} as const; diff --git a/src/ui/i18n/uk-sections/popup.ts b/src/ui/i18n/uk-sections/popup.ts new file mode 100644 index 0000000..d5c95ef --- /dev/null +++ b/src/ui/i18n/uk-sections/popup.ts @@ -0,0 +1,248 @@ +import type { SurfacePresentationState } from "@privacy-brand/xray-protocol"; + +import { sharedWorkerModeCopy } from "./shared-worker"; + +import { BRAND_DIAGNOSTICS_NAME, BRAND_DISPLAY_NAME } from "@/shared/brand"; +import { slavicCount } from "@/ui/shared/slavic-plural"; + +export const popup = { + loading: "Завантаження…", + protectionProtected: "Захищено", + protectionOff: "Захист вимкнено", + protectionDisabled: "Захист вимкнено", + protectionNeedsAttention: "Потребує уваги", + protectionUnknown: "Статус захисту недоступний", + protectionSourceSiteRule: "Правило для домену", + protectionSourceContainer: "Контейнер Firefox", + protectionSourceDefaultRule: "Правило за замовчуванням", + protectionSourceTrustedSite: "Довірений сайт", + protectionSourceNone: "Немає активного правила", + protectionSourceGlobalSetting: "Глобальне налаштування", + languagePriorityTooltip: (languages: readonly string[]) => + [ + "Порядок мов:", + ...languages.map((language, index) => `${index + 1}. ${language}`), + ].join("\n"), + protectionViewDetails: "Переглянути деталі", + protectionViewPageActivity: "Переглянути активність сторінки", + protectionDetailsTitle: "Стан захисту", + protectionDetailsLead: `Перегляньте, як ${BRAND_DISPLAY_NAME} обробляє кожну функцію браузера на цьому сайті.`, + protectionGroupLocationLocale: "Місцеперебування, час і локаль", + protectionGroupBrowserIdentity: "Дані браузера", + protectionGroupRenderingMedia: "Графіка й медіа", + protectionGroupWorkers: "Воркери", + protectionStateProtected: "Захищено", + protectionStateDegraded: "Захист ослаблено", + protectionStateNative: "Не змінено", + protectionStateMixed: "Змішаний стан", + protectionStatePending: "Перевірка…", + protectionStateRepaired: "Захист відновлено", + protectionStateBrowserEnforced: "Захищено браузером", + protectionStateUnrecoverable: "Збій захисту", + protectionStateNotApplicable: "Не застосовується", + protectionStateCompatibility: "Проблема сумісності", + protectionSurfaceUnknown: (surface: string) => `Статус ${surface} невідомий.`, + protectionPageMayNotWork: "Ця сторінка може працювати некоректно", + protectionServiceWorkerBlockTitle: "Service Workers заблоковано", + protectionServiceWorkerBlockSummary: `${BRAND_DISPLAY_NAME} блокує реєстрацію Service Worker на цьому сайті.`, + protectionServiceWorkerBlockDescription: + "Офлайн-режим, push-сповіщення, фонова синхронізація та інші функції Service Worker можуть не працювати, коли ввімкнено блокування.", + protectionSharedWorkerStrictTitle: "Увімкнено суворий режим Shared Worker", + protectionSharedWorkerStrictSummary: `${BRAND_DISPLAY_NAME} блокує Shared Worker до запуску, якщо не може підтвердити підміну його даних.`, + protectionSharedWorkerStrictDescription: + "Синхронізація між вкладками, спільні з’єднання та співпраця в реальному часі можуть не працювати, коли ввімкнено суворий режим.", + notificationsTitle: "Сповіщення", + notificationsThisSite: "Цей сайт", + notificationsExtension: BRAND_DISPLAY_NAME, + notificationsPreviousUpdates: "Попередні оновлення", + notificationsDismissed: "Приховані", + notificationsResolved: "Усунено", + notificationsNew: "Нове", + notificationsAcknowledged: "Прочитано", + notificationsStillActive: "Потребує уваги", + notificationsEmpty: "Немає активних сповіщень.", + notificationsDismiss: "Закрити", + notificationsKeepStrictMode: "Залишити суворий режим", + notificationsWhatThisAffects: "На що це впливає", + notificationsBeforeYouContinue: "Перш ніж продовжити", + notificationsChooseSharedWorkerMode: + "Виберіть спосіб запуску Shared Workers на цьому сайті", + notificationsCspTitle: "Сайт заблокував підміну даних воркера", + notificationsCspSummary: `Не вдалося захистити воркер на цьому сайті за допомогою ${BRAND_DISPLAY_NAME}.`, + notificationsSharedWorkerTitle: "Не вдалося захистити Shared Worker", + notificationsSharedWorkerSummary: `Не вдалося застосувати підмінені значення до запуску Shared Worker за допомогою ${BRAND_DISPLAY_NAME}.`, + notificationsRelaxWorkerPolicy: "Дозволити захист воркерів", + notificationsAllowServiceWorkers: "Дозволити Service Workers", + notificationsSharedWorkerNative: "Без підміни", + notificationsSharedWorkerNativeDescription: sharedWorkerModeCopy.native.description, + notificationsSharedWorkerSpoof: "Підміна", + notificationsSharedWorkerSpoofDescription: sharedWorkerModeCopy.spoof.description, + notificationsUpdateTitle: `${BRAND_DISPLAY_NAME} оновлено`, + notificationsUpdateSummary: "Перегляньте зміни в цій версії.", + notificationsVersionLabel: "Версія", + notificationsOpenLink: "Відкрити посилання", + notificationsBadgeLabel: (count: number) => + slavicCount(count, [ + "непрочитане сповіщення", + "непрочитані сповіщення", + "непрочитаних сповіщень", + ]), + protectionCounts: (counts: Record) => { + // Roll the 9 presentation states up into the buckets a one-line summary + // can carry without becoming unreadable. `browser-enforced`/`repaired` + // are protected-like; `unrecoverable` folds into the degraded bucket. + const protectedCount = + counts.protected + counts["browser-enforced"] + counts.repaired; + const degradedCount = counts.degraded + counts.unrecoverable; + return [ + ...(protectedCount > 0 ? [`Захищено: ${protectedCount}`] : []), + ...(degradedCount > 0 ? [`Захист ослаблено: ${degradedCount}`] : []), + ...(counts.pending > 0 ? [`Перевіряється: ${counts.pending}`] : []), + ...(counts["native-by-policy"] > 0 + ? [`Без змін: ${counts["native-by-policy"]}`] + : []), + ...(counts["not-applicable"] > 0 + ? [`Не застосовується: ${counts["not-applicable"]}`] + : []), + ...(counts.unknown > 0 ? [`Невідомо: ${counts.unknown}`] : []), + ].join(" · "); + }, + protectionException: (_surface: string) => "Ця сторінка може працювати некоректно", + unsupportedTab: "Обмежена сторінка", + noLocationYet: "Немає активного профілю", + noPresetAssigned: "Профіль не призначено", + regularPageRequired: `${BRAND_DISPLAY_NAME} не може отримати доступ до цієї сторінки.`, + openXRay: `Переглянути активність цього сайту в ${BRAND_DIAGNOSTICS_NAME}`, + saveLabelCreate: "Створити", + saveLabelSave: "Зберегти", + ruleConflictConfirm: (pattern: string) => + `Правило для домену «${pattern}» уже існує. Замінити його цими налаштуваннями?`, + ruleConflictTitle: "Замінити правило для домену?", + ruleConflictReplace: "Замінити", + popupDataUnavailable: "Не вдалося завантажити статус цього сайту.", + retryLabel: "Повторити", + dismissLabel: "Відхилити", + mutationFailed: `${BRAND_DISPLAY_NAME} не зміг виконати цю дію. Спробуйте знову.`, + enableExtensionFailed: `Не вдалося ввімкнути ${BRAND_DISPLAY_NAME}.`, + trustSiteFailed: "Не вдалося додати цей сайт до довірених сайтів.", + firefoxPermissionFailed: "Не вдалося надати дозвіл Firefox userScripts.", + powerControlExtension: BRAND_DISPLAY_NAME, + powerControlGlobalProtections: "Глобальні засоби захисту", + powerTargetLoading: "Перевірка налаштувань цього сайту…", + powerTargetGlobal: `${BRAND_DISPLAY_NAME} вимкнено на всіх сайтах.`, + powerTargetGlobalProtectionsDisabled: + "Увімкніть захист у налаштуваннях, щоб використовувати збережені правила й профілі.", + powerTargetUnsupported: `${BRAND_DISPLAY_NAME} не може отримати доступ до цієї сторінки.`, + powerTargetTrustedSite: `${BRAND_DISPLAY_NAME} вимкнено, оскільки цей сайт відповідає довіреному сайту.`, + powerTargetSiteRule: "Вмикає або вимикає це правило для домену.", + powerTargetContainer: "Вмикає або вимикає це призначення контейнера Firefox.", + powerTargetContainerSetup: `Налаштуйте ${BRAND_DISPLAY_NAME} для цього контейнера Firefox.`, + powerTargetContainerDefaultRule: + "Цей контейнер Firefox використовує правило за замовчуванням.", + powerTargetDefaultRule: + "Контролює сайти без правила для домену або довіреного сайту.", + powerAriaLoading: "Перевірка налаштувань цього сайту", + powerAriaGlobalOff: `${BRAND_DISPLAY_NAME} вимкнено на всіх сайтах`, + powerAriaGlobalProtectionsDisabled: + "Глобальні засоби захисту вимкнено. Увімкніть їх у налаштуваннях.", + powerAriaRestricted: `${BRAND_DISPLAY_NAME} не може отримати доступ до цієї сторінки`, + powerAriaTrustedSite: `Увімкнути ${BRAND_DISPLAY_NAME} для цього сайту`, + powerAriaDomainRuleOn: "Увімкніть це правило для домену", + powerAriaDomainRuleOff: "Вимкніть це правило для домену", + powerAriaContainerOn: "Увімкніть це призначення контейнера Firefox", + powerAriaContainerOff: "Вимкніть це призначення контейнера Firefox", + powerAriaContainerSetup: `Налаштуйте ${BRAND_DISPLAY_NAME} для цього контейнера Firefox`, + powerAriaDefaultRuleOn: "Увімкніть правило за замовчуванням", + powerAriaDefaultRuleOff: "Вимкніть правило за замовчуванням", + + sheetTitle: "Правило для домену", + currentProfileLabel: "Регіональний профіль", + noPresetLabel: "Немає профілю", + inheritedDefaultRuleProfileLabel: "Використати профіль правила за замовчуванням", + inheritedContainerProfileLabel: "Використати профіль контейнера Firefox", + inheritedDomainRuleProfileLabel: "Використати профіль відповідного правила", + ruleTypeLabel: "Застосовується до", + ruleTypeExact: "Точний хост", + ruleTypeSuffix: "Хост + субдомени", + workerHandlingLabel: "Dedicated і Shared Workers", + workerHandlingHint: + "Виберіть режим для цього правила. «Успадкувати» використовує загальне налаштування. «Без підміни» залишає воркерам справжні дані браузера. «Підміна» намагається захистити їх. «Суворий» блокує воркер, якщо захист не можна підтвердити до запуску.", + workerHandlingInherit: "Успадкувати", + workerHandlingNative: "Без підміни", + workerHandlingSpoof: "Підміна", + workerHandlingStrict: "Суворий", + viewXRay: BRAND_DIAGNOSTICS_NAME, + deleteButtonLabel: "Видалити", + sheetLead: + "Виберіть, де застосовується це правило для домену та який регіональний профіль воно використовує. Зміна профілю перезавантажує сторінку.", + cleanupSheetTitle: "Нова ідентичність", + cleanupConfirmTitle: "Створити нову ідентичність?", + cleanupPlanLoading: "Перевірка доступних для очищення даних…", + cleanupPlanDescription: (available: string, unavailable: string) => + unavailable + ? `${BRAND_DISPLAY_NAME} очистить ${available}. Цей браузер не може очистити тут ${unavailable}.` + : `${BRAND_DISPLAY_NAME} очистить ${available}.`, + cleanupResultTitle: "Створено нову ідентичність", + cleanupResultComplete: `Дані веб-переглядача, доступні для ${BRAND_DISPLAY_NAME}, видалено.`, + cleanupResultPartial: + "Частину даних браузера видалено, але деякі елементи виявилися недоступними.", + cleanupResultFailed: `Ідентичність змінилася, але ${BRAND_DISPLAY_NAME} не зміг очистити пов’язані дані браузера. Ви можете повторити спробу без повторної зміни ідентичності.`, + cleanupSurfaceCookies: "файли cookie", + cleanupSurfaceLocalStorage: "локальне сховище", + cleanupSurfaceIndexedDb: "IndexedDB", + cleanupSurfaceCacheStorage: "Cache Storage", + cleanupSurfaceServiceWorkers: "Service Workers", + cleanupSurfacePageStorage: "дані відкритих сторінок", + cleanupStatusCleaned: "Очищено", + cleanupStatusSkipped: "Недоступно", + cleanupStatusFailed: "Не вдалося", + cleanupConfirmLabel: "Створити нову ідентичність", + deleteConfirmTitle: "Видалити правило для домену?", + deleteConfirmDescription: (pattern: string) => + `Буде видалено збережене правило для домену «${pattern}».`, + cleanDomainAriaLabel: "Створити нову ідентичність для цього сайту", + cleanDomainLabel: "Нова ідентичність", + cleanDomainDefaultRuleDisabled: "Недоступно для правила за замовчуванням", + cleanDomainProductOffDisabled: `Недоступно, поки ${BRAND_DISPLAY_NAME} вимкнено для цього сайту`, + cleanDomainContainerSetupDisabled: "Спочатку налаштуйте цей контейнер Firefox", + cleanDomainPageDisabled: "Недоступно на цій сторінці", + cleanDomainLoadingDisabled: "Доступно після завершення завантаження сайту", + cleanDomainAvailabilityHint: + "Нову ідентичність можна створити для правила домену або налаштованого контейнера Firefox.", + openSettingsAriaLabel: "Відкрити налаштування", + openFullRuleSettings: "Відкрити в правилах домену", + settingsLabel: "Налаштування", + closeSheetAriaLabel: "Закрити правило для домену", + trustedSiteCta: "Редагувати довірений сайт", + disableOnSiteCta: "Додати до довірених сайтів", + enableTrustCta: "Додати до довірених сайтів", + extensionOffCardAction: `Увімкнути ${BRAND_DISPLAY_NAME}`, + surfaceGeolocation: "Геолокація", + surfaceTimeLocale: "Час і локаль", + surfaceNavigator: "Navigator", + surfaceScreen: "Екран", + surfaceClientHints: "Client Hints", + surfaceBattery: "Акумулятор", + surfaceCanvas: "Canvas", + surfaceWebGL: "WebGL", + surfaceAudio: "Аудіо", + surfaceWebRTC: "WebRTC", + surfaceWorker: "Dedicated Workers", + surfaceServiceWorker: "Service Workers", + surfaceSharedWorker: "Shared Workers", + addExactOverrideCta: "Додати правило сайту", + containerSetupCta: "Налаштувати контейнер", + globalFallbackRuleCta: "Редагувати правило за замовчуванням", + editDomainRuleLabel: "Редагувати правило для домену", + editContainerLabel: "Редагувати контейнер", + firefoxFirstInlinePermissionTitle: + "Поліпшити захист під час першого завантаження у Firefox", + firefoxFirstInlinePermissionDescription: `Firefox може показувати ваш реальний час і локаль першому сценарію сторінки. Надайте необов’язковий дозвіл userScripts, щоб ${BRAND_DISPLAY_NAME} міг почати підміну раніше.`, + firefoxFirstInlinePermissionEnableLabel: "Надати дозвіл", + detailsAbout: (label: string) => `Подробиці про ${label}`, + relaxCspHint: `Це видаляє заголовки сайту Content Security Policy, якщо вони заважають ${BRAND_DISPLAY_NAME} захищати воркерів. Це послаблює захист сайту від шкідливих скриптів; увімкніть його лише для сайту, якому ви довіряєте.`, + advancedSectionTitle: "Додатково", + suggestionWorkerCspDescription: + "Налаштування Content Security Policy (CSP) на цьому сайті заблокували підміну даних воркера. Дозвіл на захист воркерів послабить цю політику й захист сайту від шкідливих скриптів. Продовжуйте, лише якщо ви довіряєте цьому сайту.", + suggestionSharedWorkerInjectionDescription: `${BRAND_DISPLAY_NAME} не зміг захистити цей Shared Worker до запуску. На цьому сайті виберіть, що важливіше: сумісність чи захист.`, +} as const; diff --git a/src/ui/i18n/uk-sections/rules.ts b/src/ui/i18n/uk-sections/rules.ts new file mode 100644 index 0000000..e59e538 --- /dev/null +++ b/src/ui/i18n/uk-sections/rules.ts @@ -0,0 +1,238 @@ +import { BRAND_DISPLAY_NAME } from "@/shared/brand"; +import { slavicCount } from "@/ui/shared/slavic-plural"; + +const PRODUCT_NOT_LOWER = `чим не є ${BRAND_DISPLAY_NAME}`; + +export const rules = { + title: "Правила для доменів", + hint: "Правила для доменів зіставляють шаблони хостів із регіональними профілями.", + addButton: "Додати правило", + filterLabel: "Фільтр правил", + filterPlaceholder: "Пошук за профілями, доменами й попередженнями", + locationFilterLabel: "Фільтр за профілем", + locationFilterPlaceholder: "Усі профілі", + assignLocationLabel: "Призначити профіль", + tableHeadRule: "Правило", + tableHeadProfile: "Профіль", + tableHeadActions: "Дії", + selectAllAriaLabel: "Виберіть усі видимі правила", + selectMenuAriaLabel: "Відкрити меню вибору правил", + selectRuleAriaLabel: (pattern: string) => `Виберіть правило ${pattern}`, + editRuleAriaLabel: (pattern: string) => `Редагувати правило ${pattern}`, + editRuleTitle: "Редагувати правило", + deleteRuleAriaLabel: (pattern: string) => `Видалити правило ${pattern}`, + deleteRuleTitle: "Видалити правило", + inactiveBadge: "неактивний", + selectionMenuAllVisible: "Усі видимі", + selectionMenuAll: "Усі", + selectionMenuNone: "Жодного", + selectionMenuActive: "Лише активні", + selectionMenuInactive: "Лише неактивні", + noRulesFiltered: "Жодне правило не відповідає поточному фільтру.", + noRulesEmpty: `Правил для доменів ще немає. Додайте правило, щоб вибрати, як ${BRAND_DISPLAY_NAME} обробляє відповідні сайти.`, + copyLinkLabel: "правила для доменів", + copyLinkHelpLabel: "довідка про правила для доменів", + copyLinkInspectorLabel: "інспектор імен хостів", + copyLinkRuleAriaLabel: (pattern: string) => + `Скопіюйте посилання на правило ${pattern}`, + + help: { + title: "Правила для доменів", + body1: + "Використовуйте example.com для одного точного хоста. Використовуйте *example.com для цього хоста та будь-якого субдомену, наприклад www.example.com. Використовуйте *.example.com лише для субдоменів.", + body2: `Перевагу має найточніший відповідний шаблон. Якщо два правила збігаються та вказують на різні регіональні профілі, ${BRAND_DISPLAY_NAME} попереджає про це.`, + }, + + globalFallback: { + title: "Правило за замовчуванням", + description: `Налаштуйте захист і за потреби виберіть регіональний профіль, які ${BRAND_DISPLAY_NAME} використовуватиме за відсутності конкретнішого правила.`, + copyLinkLabel: "Правило за замовчуванням", + overridesBadge: (count: number) => + slavicCount(count, [ + "особливе налаштування", + "особливі налаштування", + "особливих налаштувань", + ]), + openInRules: "Редагувати в правилах домену", + editAriaLabel: "Редагувати правило за замовчуванням", + editTitle: "Редагувати правило за замовчуванням", + noPresetLabel: "Профіль не призначено", + setupHint: "Профіль і особливі налаштування захисту ще не задано.", + tableHint: "Налаштування за замовчуванням, якщо немає нічого більш конкретного.", + dialog: { + title: "Правило за замовчуванням", + description: + "Виберіть захист і необов’язковий профіль для сайтів без конкретнішого правила.", + identityDescription: + "Правило за замовчуванням зберігає власну сталу цифрову ідентичність для підміни. Її не можна змінити вручну.", + enabledLabel: "Увімкнено", + enabledHint: + "Якщо вимкнено, це правило не застосовується. Його налаштування залишаються збереженими.", + enabledAriaLabel: "Перемкніть правило за замовчуванням", + locationProfileLabel: "Регіональний профіль", + locationProfileHint: + "Виберіть профіль, який має використовувати правило за замовчуванням. Залиште його непризначеним, щоб використовувати лише наведені нижче налаштування захисту.", + locationProfileWarningPrefix: `${BRAND_DISPLAY_NAME} не замінює інструменти VPN, проксі чи DNS. `, + locationProfileWarningLinkLabel: `Дізнайтеся, ${PRODUCT_NOT_LOWER}`, + locationProfileWarningSuffix: ".", + locationLabel: "Профіль", + locationPlaceholder: "Вибрати профіль", + submit: "Зберегти правило", + }, + }, + + inspector: { + title: "Перевірка хоста", + hint: `Перш ніж зберегти зміни, перевірте, як ${BRAND_DISPLAY_NAME} обробляє ім’я хоста. Тут видно, чи хост відповідає правилу для домену чи довіреному сайту, і який регіональний профіль буде застосовано.`, + hostnameLabel: "Ім’я хоста", + hostnameHint: + "Використовуйте саме той хост, який ви хочете перевірити, наприклад shop.example.com.", + hostnamePlaceholder: "напр. shop.example.com", + noMatchTitle: + "Жодне збережене правило для домену чи довірений сайт не відповідає цьому імені хоста", + noMatchDescription: + "Жодне збережене правило не застосовується, а правило за замовчуванням вимкнено або не налаштовано.", + trustedSiteWinsTitle: "Це ім’я хоста вимкнено довіреними сайтами", + trustedSiteWinsDescription: `Хост є у списку довірених сайтів, тому ${BRAND_DISPLAY_NAME} не діятиме тут, доки ви не видалите або не вимкнете цей запис.`, + trustedSiteOverridesRuleTitle: + "Довірені сайти мають пріоритет над правилом для домену", + trustedSiteOverridesRuleDescription: `Це ім’я хоста відповідає як довіреному сайту, так і правилу домену. Довірені сайти мають пріоритет, тому ${BRAND_DISPLAY_NAME} залишається вимкненим, а наведене нижче правило для домену ігнорується.`, + fallbackWinsTitle: "Тут застосовується правило за замовчуванням", + fallbackWinsDescription: `Жодне правило для домену чи довірений сайт не відповідає цьому імені хоста, тому ${BRAND_DISPLAY_NAME} застосує тут правило за замовчуванням.`, + ruleMatchTitle: (locationLabel: string) => + `${locationLabel} — активний профіль для цього сайту`, + ruleMatchDescription: `Це ім’я хоста відповідає наведеному нижче правилу домену, тому ${BRAND_DISPLAY_NAME} використовуватиме цей профіль на сайті.`, + hostnameDetailLabel: "Ім’я хоста", + trustedSiteDetailLabel: "Довірений сайт", + ruleDetailLabel: "Правило", + defaultRuleDetailLabel: "Правило за замовчуванням", + ignoredRuleDetailLabel: "Правило, яке ігнорується", + profileDetailLabel: "Регіональний профіль", + geolocationDetailLabel: "Геолокація", + localeDetailLabel: "Локаль", + timeZoneDetailLabel: "Часовий пояс", + geolocationOn: "Увімкнено", + geolocationOff: "Вимкнено", + }, + + dialog: { + titleAdd: "Додати правило", + titleEdit: "Редагувати правило", + description: + "Виберіть хости, на яких діє правило, а потім призначте регіональний профіль, особливі налаштування захисту або обидва варіанти.", + patternLabel: "Шаблон", + patternInfo: + "Використовуйте example.com для одного точного хоста. Використовуйте *example.com для цього хоста та будь-якого субдомену, наприклад www.example.com. Використовуйте *.example.com лише для субдоменів.", + patternInfoAriaLabel: "Дізнайтеся, як працюють шаблони правил", + patternPlaceholder: "Введіть шаблон домену", + locationLabel: "Профіль", + locationProfileLabel: "Регіональний профіль", + locationProfileHint: `Виберіть профіль, який має використовувати це правило. Якщо жодного не призначено, ${BRAND_DISPLAY_NAME} використовує наступний доступний профіль, зберігаючи налаштування захисту цього правила.`, + bulkAssignSearchPlaceholder: "Пошук профілів…", + enabledLabel: "Увімкнено", + enabledHint: + "Якщо вимкнено, це правило не застосовується. Його налаштування залишаються збереженими.", + enabledAriaLabel: (pattern: string) => `Увімкнути або вимкнути правило ${pattern}`, + advancedModal: { + trigger: "Додатково", + title: (pattern: string) => `Розширені налаштування для ${pattern}`, + description: + "Ці зміни залишаються в поточній чернетці, доки ви не збережете правило.", + confirm: "Гаразд", + patternFallback: "це правило", + }, + relaxCspLabel: "Послабити CSP для захисту воркерів", + relaxCspHint: `Видаляє заголовки Content Security Policy цього сайту, якщо вони заважають ${BRAND_DISPLAY_NAME} захищати воркерів.`, + relaxCspRiskHint: + "Попередження безпеки: це полегшує запуск шкідливих сценаріїв на сайті. Увімкніть його лише для сайту, якому ви довіряєте, і лише тоді, коли захист воркерів іншим чином не працює.", + relaxCspAriaLabel: (pattern: string) => + `Перемкнути послаблення CSP для правила ${pattern}`, + surfaceOverrides: { + title: "Налаштування захисту", + description: + "Виберіть інші параметри захисту для цього правила. Залиште «Успадкувати», щоб застосувати глобальні налаштування.", + stateOn: "Увімкнено", + stateInherit: "Успадкувати", + stateOff: "Вимкнено", + stateNative: "Без підміни", + stateSpoof: "Підміна", + stateStrict: "Суворий", + stateBlock: "Блокувати", + stateAllow: "Дозволити", + helpAriaLabel: (label: string) => `Дізнайтеся, чим керує ${label}`, + geolocation: { + label: "Геолокація", + info: "Керує Geolocation API. Вимкніть підміну геолокації, якщо сайт має отримувати справжнє місцезнаходження, або ввімкніть її, щоб правило підміняло відповіді на запити геолокації.", + }, + timeLocale: { + label: "Час і локаль", + info: "Керує Date, Intl, navigator.language, navigator.languages і заголовками мови, щоб сайти бачили регіон із вашого активного профілю.", + }, + canvas: { + label: "Canvas", + info: "Керує результатом прихованого малюнка Canvas, за яким сайти можуть розпізнавати браузер.", + }, + webGL: { + label: "WebGL", + info: "Контролює графічні деталі, такі як рендерер, дані GPU та пов’язані дані цифрових відбитків WebGL.", + }, + audio: { + label: "Аудіо", + info: "Керує вихідними сигналами AudioContext, які сайти можуть вимірювати для відбитків аудіо.", + }, + navigator: { + label: "Navigator", + info: "Керує полями ідентичності веб-переглядача, такими як платформа, відомості про обладнання та інші властивості Navigator.", + }, + screen: { + label: "Екран", + info: "Керує розміром екрана, щільністю пікселів і відповідними деталями відображення.", + }, + clientHints: { + label: "Client Hints", + info: "Керує деталями веб-переглядача та пристрою, наданими через заголовки та API Client Hints.", + }, + battery: { + label: "Акумулятор", + info: "Контролює, чи отримує сайт фіксований стан повністю зарядженого акумулятора замість реального стану акумулятора пристрою.", + }, + webRTC: { + label: "WebRTC", + info: "Керує захистом IP-адрес у WebRTC, який може зменшити витоки локальних і публічних адрес.", + }, + serviceWorker: { + label: "Service Workers", + info: "Визначає, чи може цей сайт реєструвати Service Workers, які можуть працювати у фоновому режимі та зберігати дані надовго. Блокування може порушити роботу PWA, автономного режиму, push-сповіщень і фонової синхронізації; «Дозволити» дозволяє браузеру реєструвати їх як зазвичай; «Успадкувати» застосовує глобальне налаштування.", + }, + sharedWorker: { + label: "Dedicated і Shared Workers", + info: "Замінює обробку Dedicated і Shared Worker для цього правила. «Без підміни» залишає воркери без змін, «Підміна» намагається застосувати підмінені значення, а «Суворий» блокує воркер, якщо підміну не можна підтвердити перед запуском.", + }, + }, + identity: { + sectionTitle: "Цифрова ідентичність", + sectionDescription: + "Це правило використовує сталу цифрову ідентичність для підміни. Змінюйте її лише тоді, коли потрібен новий цифровий відбиток і чистий стан пов’язаних сайтів.", + actionDescription: + "Видаляє пов’язані дані сайтів і створює для правила нову цифрову ідентичність.", + actionLabel: "Нова ідентичність", + confirmTitle: (pattern: string) => `Створити нову ідентичність для «${pattern}»?`, + confirmDescription: + "Це видалить файли cookie, дані сховищ, Service Workers і кеші сайтів, пов’язаних із правилом. Після цього буде створено нову цифрову ідентичність.", + confirmDomainsLabel: `${BRAND_DISPLAY_NAME} видалить дані браузера для цих доменів:`, + confirmNoDomains: `Для цього правила ще немає збережених даних браузера. ${BRAND_DISPLAY_NAME} однаково створить нову цифрову ідентичність.`, + confirmLabel: "Створити нову ідентичність", + rotateSuccess: "Для правила створено нову ідентичність.", + rotateError: "Не вдалося створити нову ідентичність для правила.", + }, + submitAdd: "Додати правило", + submitEdit: "Зберегти", + duplicateAlertTitle: "Перезаписати існуюче правило?", + duplicateAlertDescription: (pattern: string) => + `Правило для «${pattern}» уже існує. Перезаписати його цими налаштуваннями?`, + duplicateAlertConfirm: "Перезаписати", + duplicateAlertClose: "Ні", + trustedSiteOverrideWarning: (pattern: string) => + `Цей домен відповідає запису довірених сайтів «${pattern}». ${BRAND_DISPLAY_NAME} залишатиметься вимкненим тут незалежно від цих налаштувань.`, + }, +} as const; diff --git a/src/ui/i18n/uk-sections/shared-worker.ts b/src/ui/i18n/uk-sections/shared-worker.ts new file mode 100644 index 0000000..4c4adee --- /dev/null +++ b/src/ui/i18n/uk-sections/shared-worker.ts @@ -0,0 +1,32 @@ +import { BRAND_DISPLAY_NAME } from "@/shared/brand"; + +export const sharedWorkerModeCopy = { + native: { + label: "Без підміни", + description: `Shared Workers працюють звичайним чином, без захисту ${BRAND_DISPLAY_NAME}. Це забезпечує найкращу сумісність, але вони можуть зчитувати справжні дані браузера.`, + }, + spoof: { + label: "Підміна", + description: + "Застосовує до Shared Workers інший спосіб захисту. Деякі з них усе одно можуть не запуститися.", + }, + strict: { + label: "Суворий", + description: `Блокує Shared Worker, якщо ${BRAND_DISPLAY_NAME} не може підтвердити підміну до його запуску. Так незахищений Shared Worker не отримає справжні дані браузера, але залежні від нього функції сайту можуть перестати працювати.`, + }, +} as const; + +export const workerHandlingModeCopy = { + native: { + label: "Без підміни", + description: `Dedicated і Shared Workers працюють звичайним чином, без захисту ${BRAND_DISPLAY_NAME}. Це забезпечує найкращу сумісність, але вони можуть зчитувати справжні дані браузера.`, + }, + spoof: { + label: "Підміна", + description: `Намагається застосувати підмінені ${BRAND_DISPLAY_NAME} значення до запуску воркерів. Якщо захист застосувати не вдасться, воркер може отримати справжні дані браузера або не запуститися.`, + }, + strict: { + label: "Суворий", + description: `Блокує воркер до запуску, якщо ${BRAND_DISPLAY_NAME} не може підтвердити підміну. Якщо збій стається після запуску, ${BRAND_DISPLAY_NAME} покаже сповіщення. Залежні від цих воркерів функції сайту можуть перестати працювати.`, + }, +} as const; diff --git a/src/ui/i18n/uk-sections/sidebar.ts b/src/ui/i18n/uk-sections/sidebar.ts new file mode 100644 index 0000000..436ed2c --- /dev/null +++ b/src/ui/i18n/uk-sections/sidebar.ts @@ -0,0 +1,174 @@ +import { BRAND_DIAGNOSTICS_NAME, BRAND_DISPLAY_NAME } from "@/shared/brand"; + +export const sidebar = { + xRayTitle: BRAND_DIAGNOSTICS_NAME, + openSettings: "Відкрити налаштування", + openRule: "Редагувати правило домену", + openLocation: "Редагувати профіль", + openLogs: "Переглянути журнали", + region: "Регіон", + + errorPrefix: `${BRAND_DISPLAY_NAME} зіткнувся з проблемою:`, + loading: "Перевірка цієї сторінки…", + unsupportedTab: + "На службових сторінках браузера підміна недоступна. Відкрийте звичайний сайт, щоб перевірити захист.", + noSpoofing: `${BRAND_DISPLAY_NAME} не активний на цій сторінці. Відкрийте спливаюче вікно, щоб налаштувати правило для цього сайту.`, + trustedSite: "Цей сайт позначено як довірений. Підміну даних вимкнено.", + trustedSiteHint: "Відкрийте «Довірені сайти» в налаштуваннях, щоб змінити це.", + + surfaces: { + title: "Підмінені дані", + timezone: "Часовий пояс", + language: "Мова", + languages: "Мови", + acceptLanguage: "Accept-Language", + geolocation: "Геолокація", + accuracyNoise: "Точність / Шум", + platform: "Платформа", + userAgent: "User-Agent", + cpuRam: "ЦП/ОЗП", + screen: "Екран", + noise: "Шум", + canvas: "Canvas", + audio: "Аудіо", + gpu: "GPU", + sharedWorker: "Shared Worker", + sharedWorkerNativeCompatibility: "Без підміни заради сумісності", + sharedWorkerBlobSpoofing: "Підміна через обгортку Blob", + sharedWorkerResponseRewriteSpoofing: "Підміна через зміну відповіді", + sharedWorkerResponseRewriteCacheSensitive: + "Резервний спосіб зміни відповіді; залежить від кешу", + sharedWorkerModuleUnsupported: "Перезапис модуля не підтримується", + sharedWorkerIdentityConflict: "Конфлікт ідентичності", + sharedWorkerRewriteUnavailable: "Перезапис відповіді недоступний", + sharedWorkerBlockedStrict: "Заблоковано суворим режимом", + sharedWorkerStrictRewriteRequired: + "Суворий режим потребує підтвердженої зміни відповіді", + sharedWorkerStrictBlockedCacheSensitive: + "Заблоковано: резервний спосіб залежав від кешу", + sharedWorkerCompatibilityHelp: + "Обробка Shared Worker залежить від вибраного режиму: «Без підміни», «Підміна» або «Суворий».", + gpuDebug: "Налагодження GPU", + clientHintsPlatform: "платформа", + clientHintsBrands: "бренди", + fingerprint: "Цифровий відбиток", + off: "Вимкнено", + yes: "Активно", + blocked: "Заблоковано", + m: "м", + GB: "GB", + expand: "Показати повне значення", + collapse: "Згорнути", + }, + + accessed: { + title: "Активність сторінки", + description: "Категорії, запитувані сторінкою з часу останньої навігації.", + accessed: "Запитано", + blocked: "Заблоковано", + armed: "Очікування", + connecting: "Підключення…", + failed: "Не вдалося", + off: "Вимкнено", + categories: { + geolocation: "Геолокація", + timeLocale: "Час і локаль", + canvas: "Canvas", + webGL: "WebGL", + audio: "Аудіо", + navigator: "Navigator", + screen: "Екран", + clientHints: "Client Hints", + battery: "Акумулятор", + webRTC: "WebRTC", + worker: "Dedicated Workers", + serviceWorker: "Service Workers", + sharedWorker: "Shared Workers", + }, + methods: { + geolocationGetCurrentPosition: "getCurrentPosition", + geolocationWatchPosition: "watchPosition", + geolocationClearWatch: "clearWatch", + geolocationPermissionsQuery: "permissions.query", + dateConstructor: "Date()", + dateNow: "Date.now", + dateParse: "Date.parse", + dateGetTimezoneOffset: "Date.getTimezoneOffset", + dateToString: "Date.toString", + dateToLocaleString: "Date.toLocaleString", + intlConstructor: "Intl constructor", + intlResolvedOptions: "Intl.resolvedOptions", + intlDateTimeFormatFormat: "DateTimeFormat.format", + intlDateTimeFormatFormatToParts: "DateTimeFormat.formatToParts", + temporalNowInstant: "Temporal.Now.instant", + temporalNowTimeZoneId: "Temporal.Now.timeZoneId", + temporalNowPlainDateTimeISO: "Temporal.Now.plainDateTimeISO", + temporalNowZonedDateTimeISO: "Temporal.Now.zonedDateTimeISO", + temporalNowPlainDateISO: "Temporal.Now.plainDateISO", + temporalNowPlainTimeISO: "Temporal.Now.plainTimeISO", + temporalDurationToLocaleString: "Temporal.Duration.toLocaleString", + temporalInstantToLocaleString: "Temporal.Instant.toLocaleString", + temporalPlainDateToLocaleString: "Temporal.PlainDate.toLocaleString", + temporalPlainDateTimeToLocaleString: "Temporal.PlainDateTime.toLocaleString", + temporalPlainMonthDayToLocaleString: "Temporal.PlainMonthDay.toLocaleString", + temporalPlainTimeToLocaleString: "Temporal.PlainTime.toLocaleString", + temporalPlainYearMonthToLocaleString: "Temporal.PlainYearMonth.toLocaleString", + temporalZonedDateTimeToLocaleString: "Temporal.ZonedDateTime.toLocaleString", + canvasGetImageData: "ctx.getImageData", + canvasToDataURL: "canvas.toDataURL", + canvasToBlob: "canvas.toBlob", + webGLReadPixels: "gl.readPixels", + webGLGetExtension: "GPU renderer info", + webGLGetSupportedExtensions: "gl.getSupportedExtensions", + webGLGetParameter: "gl.getParameter", + audioGetFloatFrequencyData: "analyser.getFloatFrequencyData", + audioGetByteFrequencyData: "analyser.getByteFrequencyData", + audioGetFloatTimeDomainData: "analyser.getFloatTimeDomainData", + audioGetByteTimeDomainData: "analyser.getByteTimeDomainData", + audioGetChannelData: "analyser.getChannelData", + navigatorWebdriver: "navigator.webdriver", + navigatorHardwareConcurrency: "navigator.hardwareConcurrency", + navigatorDeviceMemory: "navigator.deviceMemory", + navigatorMaxTouchPoints: "navigator.maxTouchPoints", + navigatorPlatform: "navigator.platform", + navigatorUserAgent: "navigator.userAgent", + navigatorVendor: "navigator.vendor", + navigatorAppVersion: "navigator.appVersion", + screenWidth: "screen.width", + screenHeight: "screen.height", + screenAvailWidth: "screen.availWidth", + screenAvailHeight: "screen.availHeight", + screenColorDepth: "screen.colorDepth", + screenPixelDepth: "screen.pixelDepth", + screenDevicePixelRatio: "screen.devicePixelRatio", + clientHintsBrands: "ua.brands", + clientHintsMobile: "ua.mobile", + clientHintsPlatform: "ua.platform", + clientHintsToJSON: "ua.toJSON", + clientHintsGetHighEntropyValues: "ua.getHighEntropyValues", + batteryGetBattery: "navigator.getBattery", + batteryManagerCharging: "BatteryManager.charging", + batteryManagerChargingTime: "BatteryManager.chargingTime", + batteryManagerDischargingTime: "BatteryManager.dischargingTime", + batteryManagerLevel: "BatteryManager.level", + webRTCConstructor: "RTCPeerConnection", + webRTCCreateOffer: "RTCPeerConnection.createOffer", + webRTCCreateAnswer: "RTCPeerConnection.createAnswer", + workerConstructor: "Worker()", + serviceWorkerRegister: "serviceWorker.register", + sharedWorkerConstructor: "SharedWorker()", + }, + }, + + why: { + title: "Чому застосовуються ці налаштування", + sourcesShort: { + trustedSite: "Довірений", + domainRule: "Правило домену", + container: "Контейнер", + fallback: "За замовчуванням", + none: "Жодного", + }, + noExplanation: "Пояснення правила недоступне.", + }, +} as const; diff --git a/src/ui/i18n/uk-sections/trusted-sites.ts b/src/ui/i18n/uk-sections/trusted-sites.ts new file mode 100644 index 0000000..ea56fa5 --- /dev/null +++ b/src/ui/i18n/uk-sections/trusted-sites.ts @@ -0,0 +1,54 @@ +import { BRAND_DISPLAY_NAME } from "@/shared/brand"; +import { slavicCount } from "@/ui/shared/slavic-plural"; + +export const trustedSites = { + title: "Довірені сайти", + hint: `Сайти, де ${BRAND_DISPLAY_NAME} залишається вимкненим, навіть якщо правило, призначення контейнера або правило за замовчуванням застосовуються інакше.`, + patternLabel: "Шаблон домену", + patternPlaceholder: "Введіть шаблон домену", + filterLabel: "Фільтрувати довірені сайти", + filterPlaceholder: "Пошук домену", + addButton: "Додати сайт", + copyLinkLabel: "довірені сайти", + copyLinkHelpLabel: "довідка про довірені сайти", + copyLinkInspectorLabel: "інспектор імен хостів", + tableHeadPattern: "Домен", + tableHeadStatus: "Статус", + tableHeadActions: "Дії", + empty: "Ще немає довірених сайтів.", + filteredEmpty: "Жоден довірений сайт не відповідає поточному фільтру.", + inactiveBadge: "неактивний", + toggleSiteAriaLabel: (pattern: string, enabled: boolean) => + `${enabled ? "Вимкнути" : "Увімкнути"} довірений сайт ${pattern}`, + deleteSiteAriaLabel: (pattern: string) => `Видалити довірений сайт ${pattern}`, + deleteSiteTitle: "Видалити довірений сайт", + duplicateWarning: "Цей довірений сайт уже існує.", + patternRequired: "Введіть шаблон домену.", + saved: "Довірений сайт збережено.", + updated: "Довірений сайт оновлено.", + deleted: "Довірений сайт видалено.", + help: { + title: "Коли використовувати довірені сайти", + body1: + "Використовуйте довірені сайти для доменів, де підміна заважає, як-от банківські послуги, оформлення покупок або відновлення облікового запису, де зміна даних браузера може викликати підозру.", + body2: `Довірені сайти мають пріоритет над правилами для доменів, призначеннями контейнерів Firefox і правилом за замовчуванням. Використовуйте example.com для одного конкретного хоста, *example.com для цього хоста та його субдоменів або *.example.com лише для субдоменів.`, + }, + rulesCta: { + title: "Захист на інших сайтах", + activeRulesOnly: (count: number) => + `Поза довіреними сайтами: ${slavicCount(count, ["активне правило", "активні правила", "активних правил"])}. Відкрийте правила для доменів, щоб перевірити, де вони застосовуються.`, + activeRulesWithDefault: (count: number) => + `Поза довіреними сайтами: ${slavicCount(count, ["активне правило", "активні правила", "активних правил"])}. Правило за замовчуванням також охоплює решту сайтів без збігів.`, + defaultRuleOnly: `Правило за замовчуванням все ще застосовується до сайтів без відповідного правила. Довірені сайти вимикають ${BRAND_DISPLAY_NAME} лише на відповідних хостах.`, + openRules: "Відкрити правила для доменів", + openDefaultRule: "Відкрити правило за замовчуванням", + }, + dialog: { + title: "Додати сайт", + description: `Вимкніть ${BRAND_DISPLAY_NAME} на відповідних сайтах, якщо підміна заважає їхній роботі.`, + patternInfo: + "Використовуйте example.com для одного точного хоста. Використовуйте *example.com для цього хоста та будь-якого субдомену, наприклад www.example.com. Використовуйте *.example.com лише для субдоменів.", + patternInfoAriaLabel: "Дізнайтеся, як працюють шаблони довірених сайтів", + submit: "Додати сайт", + }, +} as const; diff --git a/src/ui/i18n/uk-sections/welcome.ts b/src/ui/i18n/uk-sections/welcome.ts new file mode 100644 index 0000000..08a3871 --- /dev/null +++ b/src/ui/i18n/uk-sections/welcome.ts @@ -0,0 +1,139 @@ +import { BRAND_DISPLAY_NAME } from "@/shared/brand"; +import { externalMapConsentCopy } from "@/ui/i18n/uk-sections/osm"; +import { slavicCount } from "@/ui/shared/slavic-plural"; + +const WELCOME_PRODUCT = `Ласкаво просимо до ${BRAND_DISPLAY_NAME}`; + +export const welcome = { + title: WELCOME_PRODUCT, + loading: "Завантаження майстра налаштування…", + progressLabel: "Хід налаштування", + previous: "Назад", + next: "Далі", + privacyPolicy: "Політика конфіденційності", + privacyDialog: { + title: "Політика конфіденційності", + description: `Як ${BRAND_DISPLAY_NAME} обробляє налаштування, діагностику та мережеві запити.`, + close: "Закрити політику конфіденційності", + }, + saving: "Збереження…", + unexpectedError: "Щось пішло не так", + importSuccess: "Налаштування імпортовано", + importError: "Не вдалося імпортувати налаштування", + importParseError: "Не вдалося прочитати файл налаштувань", + steps: { + welcome: { + title: "Дякуємо, що встановили розширення", + description: + "Виберіть, які дані бачитимуть сайти, замість того щоб розкривати справжнє місцеперебування та дані браузера. Цей короткий необов’язковий майстер допоможе зробити перші налаштування. Кожне з них можна змінити пізніше. Уже маєте збережені налаштування?", + importInline: "Імпортувати налаштування", + advancedTitle: "Самостійне налаштування", + advancedDescription: + "Пропустіть майстер і відкрийте початкові налаштування. Регіональні профілі чи правила для доменів не додаються, правило за замовчуванням залишається вимкненим, а захист браузера залишається доступним для правил, які ви створите пізніше.", + guidedTitle: "Покрокове налаштування", + guidedDescription: + "Пройдіть основні кроки по черзі. Майстер пояснить наслідки кожного вибору та збереже його в налаштуваннях, які можна змінити пізніше.", + advancedCta: "Пропустити налаштування", + guidedCta: "Почати налаштування", + }, + privacy: { + title: "Історія відвідувань залишається у браузері", + description: `${BRAND_DISPLAY_NAME} зберігає правила та профілі локально. Цей крок стосується лише додаткових зовнішніх картографічних служб: увімкніть їх, якщо ви хочете шукати та переглядати під час налаштування, або залиште вимкненими й введіть координати вручну.`, + descriptionBeforePolicy: `${BRAND_DISPLAY_NAME} зберігає правила та профілі локально. Додатковий пошук на карті та попередній перегляд використовують зовнішні служби лише з вашого дозволу. Прочитайте`, + policyLink: "політику конфіденційності", + descriptionAfterPolicy: ", перш ніж вирішувати, чи підходять вам такі запити.", + consentTitle: externalMapConsentCopy.title, + consentDescription: externalMapConsentCopy.description, + }, + presets: { + title: "Додайте регіональні профілі", + description: + "Регіональні профілі містять готові координати, мову та часовий пояс. Разом ці дані створюють узгоджене уявлення про регіон для сайтів. Виберіть місця, з яких хочете почати. Наприкінці налаштування майстер імпортує обрані профілі; кожен із них можна буде змінити або видалити пізніше.", + selectAll: "Вибрати все", + selectNone: "Очистити все", + selectedCount: (count: number) => `Вибрано ${count}`, + }, + scope: { + title: "Виберіть, де діятиме захист", + description: + "Правило за замовчуванням діє, якщо немає конкретнішого правила. Залиште його вимкненим, щоб почати з вибраних сайтів, або ввімкніть, щоб захищати решту сайтів.", + defaultRuleDescription: + "Якщо ви призначаєте регіональний профіль правилу за замовчуванням, залиште цей профіль вибраним, щоб програма могла імпортувати його перед збереженням.", + enableEverywhereTitle: `Використовуйте ${BRAND_DISPLAY_NAME} на всіх сайтах`, + enableEverywhereDescription: `Увімкніть правило за замовчуванням зараз, якщо ви хочете, щоб ${BRAND_DISPLAY_NAME} діяв на сайтах без власних правил. Пізніше ви все одно можете звузити поведінку за допомогою правил домену, призначити окремим хостам різні профілі або виключити чутливі сайти за допомогою довірених сайтів.`, + editDefaultRuleTitle: "Параметри правила за замовчуванням", + editDefaultRule: "Редагувати правило за замовчуванням", + editDefaultRuleDescription: + "Перш ніж завершити налаштування, виберіть профіль для правила за замовчуванням і особливі налаштування захисту.", + defaultRuleDialogDescription: + "Під час налаштування це діалогове вікно може використовувати регіональні профілі, які ви вибрали, навіть якщо їх ще не імпортовано. Якщо призначите профіль тут, залиште його вибраним, щоб майстер імпортував його.", + presetMismatch: (label: string) => + `Правилу за замовчуванням призначено профіль «${label}», але його не вибрано для імпорту. Виберіть цей профіль або призначте інший.`, + }, + chromium: { + title: "Змінювати дані збірки Chromium", + description: (count: number) => + `${BRAND_DISPLAY_NAME} може змінювати номери збірки й патча Chromium, передані через Client Hints, зберігаючи основну версію встановленого браузера. У вбудованому каталозі є нещодавні збірки Chromium; для вашої версії браузера можна вибрати ${slavicCount(count, ["відповідне значення", "відповідні значення", "відповідних значень"])}.`, + switchTitle: "Змінювати номери збірки й патча", + switchDescription: + "Використовувати нещодавні номери збірки й патча Chromium під час підміни Client Hints. Вимкніть цю функцію, якщо хочете залишити ці номери незмінними.", + }, + firefox: { + title: "Дозвіл Firefox userScripts (необов’язково)", + description: `Ви можете використовувати ${BRAND_DISPLAY_NAME} у Firefox без цього дозволу. Якщо ви надасте userScripts, ${BRAND_DISPLAY_NAME} зможе раніше застосовувати підмінені значення на підтримуваних сайтах, покращуючи захист під час першого завантаження сторінки. Ви можете пропустити це зараз і змінити це пізніше в налаштуваннях.`, + action: "Надати дозвіл userScripts", + granted: "Дозвіл надано", + skipped: `Ви можете продовжувати використовувати ${BRAND_DISPLAY_NAME} без цього дозволу.`, + }, + appearance: { + title: "Налаштуйте вигляд", + description: `Виберіть, як має виглядати інтерфейс розширення, перш ніж почати його використовувати. Ці налаштування впливають лише на екрани ${BRAND_DISPLAY_NAME}; вони не змінюють роботу підміни на сайтах і не впливають на регіональні профілі чи правила. Ви можете налаштувати ті самі параметри відображення пізніше в налаштуваннях.`, + themeTitle: "Тема", + themeDescription: `Використовуйте системну тему або виберіть фіксований світлий або темний інтерфейс для всіх екранів ${BRAND_DISPLAY_NAME}.`, + reduceMotionTitle: "Зменшення анімації", + reduceMotionDescription: `Вимкніть анімацію інтерфейсу в ${BRAND_DISPLAY_NAME}.`, + reduceMotionSystemOverride: + "Зменшення руху ввімкнено системними налаштуваннями доступності.", + accentTitle: "Акцентний колір", + accentDescription: + "Виберіть колір виділення для елементів керування, активних станів, акцентів у фокусі та вибраних елементів.", + contrastTitle: "Режим високої контрастності", + contrastDescription: + "Збільште контрастність тексту, рамок і елементів керування, якщо звичайне оформлення недостатньо виразне.", + }, + done: { + title: "Налаштування завершено", + description: `${BRAND_DISPLAY_NAME} збереже ці параметри та відкриє налаштування. Ви можете переглянути імпортовані профілі, налаштувати правило за замовчуванням і змінити параметри зовнішнього вигляду чи конфіденційності.`, + cta: "Відкрити налаштування", + }, + }, + presetNames: { + spfWarsaw: "Варшава", + spfParis: "Париж", + spfLondon: "Лондон", + spfOttawa: "Оттава", + spfNewYork: "Нью-Йорк", + spfLasVegas: "Лас Вегас", + spfSanFrancisco: "Сан-Франциско", + spfSydney: "Сідней", + spfBeijing: "Пекін", + spfHongKong: "Гонконг", + spfNewDelhi: "Нью-Делі", + spfCairo: "Каїр", + spfLagos: "Лагос", + spfKyiv: "Київ", + spfKinshasa: "Кіншаса", + spfSaoPaulo: "Сан-Паулу", + spfBuenosAires: "Буенос-Айрес", + spfLima: "Ліма", + spfRioDeJaneiro: "Ріо де Жанейро", + spfCaracas: "Каракас", + spfBerlin: "Берлін", + spfMadrid: "Мадрид", + }, + themeOptions: { + system: "Системна", + light: "Світла", + dark: "Темна", + }, +} as const; diff --git a/src/ui/i18n/uk.ts b/src/ui/i18n/uk.ts new file mode 100644 index 0000000..5f1738d --- /dev/null +++ b/src/ui/i18n/uk.ts @@ -0,0 +1,30 @@ +import { about } from "./uk-sections/about"; +import { advanced } from "./uk-sections/advanced"; +import { common } from "./uk-sections/common"; +import { demo } from "./uk-sections/demo"; +import { firefoxContainers } from "./uk-sections/firefox-containers"; +import { locations } from "./uk-sections/locations"; +import { options, optionsPage } from "./uk-sections/options"; +import { osm } from "./uk-sections/osm"; +import { popup } from "./uk-sections/popup"; +import { rules } from "./uk-sections/rules"; +import { sidebar } from "./uk-sections/sidebar"; +import { trustedSites } from "./uk-sections/trusted-sites"; +import { welcome } from "./uk-sections/welcome"; + +export const uk = { + common, + popup, + options, + optionsPage, + locations, + rules, + trustedSites, + advanced, + about, + osm, + demo, + firefoxContainers, + sidebar, + welcome, +} as const; diff --git a/src/ui/options/components/tabs/options-preference-sections.tsx b/src/ui/options/components/tabs/options-preference-sections.tsx index 1e729ce..f5b09d9 100644 --- a/src/ui/options/components/tabs/options-preference-sections.tsx +++ b/src/ui/options/components/tabs/options-preference-sections.tsx @@ -167,6 +167,12 @@ const LanguageCard = () => ( {t.advanced.display.language.optionPortuguese} + + {t.advanced.display.language.optionRussian} + + + {t.advanced.display.language.optionUkrainian} + } diff --git a/src/ui/shared/slavic-plural.ts b/src/ui/shared/slavic-plural.ts new file mode 100644 index 0000000..ff435bb --- /dev/null +++ b/src/ui/shared/slavic-plural.ts @@ -0,0 +1,19 @@ +export type SlavicPluralForm = "one" | "few" | "many"; + +export const slavicPluralForm = (count: number): SlavicPluralForm => { + const integer = Math.abs(Math.trunc(count)); + const lastTwo = integer % 100; + const last = integer % 10; + if (last === 1 && lastTwo !== 11) return "one"; + if (last >= 2 && last <= 4 && (lastTwo < 12 || lastTwo > 14)) return "few"; + return "many"; +}; + +export const slavicCount = ( + count: number, + forms: readonly [one: string, few: string, many: string], +): string => { + const form = slavicPluralForm(count); + const index = { one: 0, few: 1, many: 2 }[form]; + return `${count} ${forms[index]}`; +}; From 8ff363794fe6b9092f21d6d6d34f1eb9eb6bb05a Mon Sep 17 00:00:00 2001 From: Tomasz Janusz <424941+TomaszJanusz@users.noreply.github.com> Date: Wed, 23 Sep 2026 20:01:52 +0200 Subject: [PATCH 2/2] fix(i18n): hyphenate Ukrainian city names --- src/ui/i18n/uk-sections/welcome.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ui/i18n/uk-sections/welcome.ts b/src/ui/i18n/uk-sections/welcome.ts index 08a3871..3848784 100644 --- a/src/ui/i18n/uk-sections/welcome.ts +++ b/src/ui/i18n/uk-sections/welcome.ts @@ -113,7 +113,7 @@ export const welcome = { spfLondon: "Лондон", spfOttawa: "Оттава", spfNewYork: "Нью-Йорк", - spfLasVegas: "Лас Вегас", + spfLasVegas: "Лас-Вегас", spfSanFrancisco: "Сан-Франциско", spfSydney: "Сідней", spfBeijing: "Пекін", @@ -126,7 +126,7 @@ export const welcome = { spfSaoPaulo: "Сан-Паулу", spfBuenosAires: "Буенос-Айрес", spfLima: "Ліма", - spfRioDeJaneiro: "Ріо де Жанейро", + spfRioDeJaneiro: "Ріо-де-Жанейро", spfCaracas: "Каракас", spfBerlin: "Берлін", spfMadrid: "Мадрид",