diff --git a/CHANGELOG.md b/CHANGELOG.md
index dd30d3e..90201aa 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,6 +8,8 @@ The format is based on Keep a Changelog and the project follows Semantic Version
### Added
+- Added complete Spanish and Portuguese interface translations, selected
+ automatically from the browser interface language with English as the fallback.
- X-Ray now counts BatteryManager property reads (`charging`, `chargingTime`,
`dischargingTime`, `level`) in addition to `navigator.getBattery`.
diff --git a/docs/project-structure.md b/docs/project-structure.md
index dcdcad2..76d2b3a 100644
--- a/docs/project-structure.md
+++ b/docs/project-structure.md
@@ -179,7 +179,7 @@ src/
├── components/
│ ├── ui/ # shadcn/ui components (@/ui/components/ui/)
│ └── stories/ # Storybook stories for all UI components
- ├── i18n/ # UI string constants (English only)
+ ├── i18n/ # UI string constants (English, Spanish, Portuguese)
├── logs/ # Extension event log viewer
├── options/ # Main options page (tabs: Rules, Locations, Containers, etc.)
│ ├── components/
diff --git a/src/shared/extension-notifications.json b/src/shared/extension-notifications.json
index 83786d4..2256388 100644
--- a/src/shared/extension-notifications.json
+++ b/src/shared/extension-notifications.json
@@ -6,12 +6,22 @@
"introducedInVersion": "0.9.0",
"delivery": "upgrades-only",
"title": {
- "en": "GeoWarp is now {{brand}}"
+ "en": "GeoWarp is now {{brand}}",
+ "es": "GeoWarp ahora es {{brand}}",
+ "pt": "GeoWarp agora é {{brand}}"
},
"message": {
"en": [
"GeoWarp has a new name: {{brand}}. The extension you know is still here, with the same focus on giving you control over the location and browser information websites can see.",
"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.",
+ "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.",
+ "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."
]
}
},
@@ -21,12 +31,22 @@
"introducedInVersion": "0.9.0",
"delivery": "upgrades-only",
"title": {
- "en": "Notifications are now in the popup"
+ "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"
},
"message": {
"en": [
"{{brand}} now shows important updates and compatibility notices in the popup.",
"Opening a notification marks it as read. It remains available until you dismiss it."
+ ],
+ "es": [
+ "{{brand}} ahora muestra las novedades importantes y los avisos de compatibilidad en la ventana emergente.",
+ "Al abrir una notificación se marca como leída. Permanecerá disponible hasta que la descartes."
+ ],
+ "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."
]
}
},
@@ -36,13 +56,25 @@
"introducedInVersion": "0.9.2",
"delivery": "upgrades-only",
"title": {
- "en": "Temporal API protection is ready to try"
+ "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"
},
"message": {
"en": [
"{{brand}} can now protect native Temporal calls, covering date, time, locale, and default time-zone behavior alongside the rest of your setup.",
"It’s experimental and off by default. To try it, open Settings → Advanced → Experimental and turn on Temporal API. It only works when your browser already provides Temporal—{{brand}} doesn’t add a polyfill.",
"Want to know more? You can read about the Temporal API on Mozilla Developer Network using the link below."
+ ],
+ "es": [
+ "{{brand}} ya puede proteger las llamadas nativas a Temporal y su comportamiento de fecha, hora, configuración regional y zona horaria predeterminada, junto con el resto de tu configuración.",
+ "Es una función experimental y está desactivada de forma predeterminada. Para probarla, abre Configuración → Avanzado → Experimental y activa la API Temporal. Solo funciona si el navegador ya ofrece Temporal; {{brand}} no añade ningún polyfill.",
+ "¿Quieres saber más? Usa el siguiente enlace para consultar la documentación de la API Temporal en Mozilla Developer Network."
+ ],
+ "pt": [
+ "{{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."
]
},
"actionUrl": "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal"
@@ -53,12 +85,22 @@
"introducedInVersion": "0.9.2",
"delivery": "upgrades-only",
"title": {
- "en": "{{brand}} is now open source!"
+ "en": "{{brand}} is now open source!",
+ "es": "¡{{brand}} ya es de código abierto!",
+ "pt": "{{brand}} agora é de código aberto!"
},
"message": {
"en": [
"{{brand}} is open source now. The code lives on GitHub under AGPL-3.0-or-later, with the project’s additional terms. Have a look around and see how it all works under the hood.",
"Found a regular bug? Open an issue and tell me what happened. Security or detection problems should still go through GitHub’s private reporting form."
+ ],
+ "es": [
+ "{{brand}} ya es de código abierto. El código está disponible en GitHub bajo la licencia AGPL-3.0-or-later y las condiciones adicionales del proyecto. Explóralo para descubrir cómo funciona por dentro.",
+ "¿Has encontrado un error común? Abre una incidencia y cuéntame qué ha ocurrido. Los problemas de seguridad o detección deben seguir enviándose mediante el formulario privado de GitHub."
+ ],
+ "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."
]
},
"actionUrl": "https://github.com/Privacy-Thing/browser-extension"
@@ -69,13 +111,25 @@
"introducedInVersion": "0.9.3",
"delivery": "all-current-users",
"title": {
- "en": "Domain fencing is ready to try"
+ "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"
},
"message": {
"en": [
"On Chrome and Firefox, Domain fencing gives each site its own stable variation of the generated fingerprint when the Default Rule applies. On Firefox, it also separates identities assigned through containers.",
"The same site and its subdomains share one identity, while unrelated sites receive separate variations. Your chosen regional preset stays unchanged, and manually configured Domain Rules are not fenced.",
"Domain fencing is experimental and off by default. To try it, open Settings → Advanced → Experimental and turn on Domain fencing."
+ ],
+ "es": [
+ "En Chrome y Firefox, el aislamiento por dominio asigna a cada sitio una variante estable de la huella digital generada cuando se aplica la Regla predeterminada. En Firefox también separa las identidades asignadas mediante contenedores.",
+ "Un sitio y sus subdominios comparten una identidad, mientras que los sitios no relacionados reciben variantes distintas. El perfil regional elegido no cambia y las reglas de dominio configuradas manualmente no se aíslan.",
+ "El aislamiento por dominio es experimental y está desactivado de forma predeterminada. Para probarlo, abre Configuración → Avanzado → Experimental y activa Aislamiento por dominio."
+ ],
+ "pt": [
+ "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."
]
}
}
diff --git a/src/shared/release-notification.test.ts b/src/shared/release-notification.test.ts
index e47a82b..e335be8 100644
--- a/src/shared/release-notification.test.ts
+++ b/src/shared/release-notification.test.ts
@@ -38,6 +38,28 @@ describe("extension notification catalog", () => {
]);
});
+ it("provides Spanish release notices without falling back to English", () => {
+ const notification = getReleaseNotice("notification-center-intro", "es-MX");
+
+ expect(notification?.title).toBe(
+ "Las notificaciones ya están en la ventana emergente",
+ );
+ expect(notification?.message).toEqual([
+ "Privacy Thing ahora muestra las novedades importantes y los avisos de compatibilidad en la ventana emergente.",
+ "Al abrir una notificación se marca como leída. Permanecerá disponible hasta que la descartes.",
+ ]);
+ });
+
+ it("provides Portuguese release notices without falling back to English", () => {
+ const notification = getReleaseNotice("notification-center-intro", "pt-BR");
+
+ expect(notification?.title).toBe("As notificações agora estão no pop-up");
+ expect(notification?.message).toEqual([
+ "Privacy Thing 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.",
+ ]);
+ });
+
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 72fda4f..bd4fd5c 100644
--- a/src/ui/i18n/en-sections/advanced.ts
+++ b/src/ui/i18n/en-sections/advanced.ts
@@ -103,9 +103,11 @@ export const advanced = {
language: {
title: "Language",
- description: `More languages are on the way. We’re working on new translations for ${BRAND_DISPLAY_NAME}.`,
- option: "English",
- soon: "SOON",
+ description: `${BRAND_DISPLAY_NAME} automatically follows your browser interface language.`,
+ optionEnglish: "English",
+ optionSpanish: "Spanish",
+ optionPortuguese: "Portuguese",
+ soon: "AUTO",
copyLinkLabel: "language",
},
diff --git a/src/ui/i18n/es-sections/about.ts b/src/ui/i18n/es-sections/about.ts
new file mode 100644
index 0000000..62a16a7
--- /dev/null
+++ b/src/ui/i18n/es-sections/about.ts
@@ -0,0 +1,168 @@
+import { BRAND_DISPLAY_NAME } from "@/shared/brand";
+
+const ABOUT_PRODUCT = `Acerca de ${BRAND_DISPLAY_NAME}`;
+const ABOUT_PRODUCT_LOWER = `acerca de ${BRAND_DISPLAY_NAME}`;
+const HOW_TO_USE_PRODUCT = `Cómo usar ${BRAND_DISPLAY_NAME}`;
+const HOW_TO_USE_PRODUCT_LOWER = `cómo usar ${BRAND_DISPLAY_NAME}`;
+const WHAT_PRODUCT_IS_NOT = `Qué ${BRAND_DISPLAY_NAME} no es`;
+const PRODUCT_NOT_LOWER = `qué ${BRAND_DISPLAY_NAME} no es`;
+const TERMS_OF_USE = "Términos de uso";
+
+export const about = {
+ title: ABOUT_PRODUCT,
+ description: `${BRAND_DISPLAY_NAME} te ayuda a controlar cómo los sitios web ven tu ubicación, idioma, zona horaria y detalles de identidad del navegador seleccionados.`,
+ body1:
+ "Cada perfil regional guardado reúne coordenadas, preferencias de idioma y una zona horaria en una configuración reutilizable que puedes aplicar a distintos dominios sin tener que crearla de nuevo.",
+ body2: `Los cambios se guardan automáticamente. Los perfiles y las reglas actualizados surten efecto al volver a cargar la página, para que ${BRAND_DISPLAY_NAME} pueda aplicar la nueva configuración desde el principio.`,
+ body3Prefix:
+ "Puedes encontrar los controles de protección del navegador compartidos en la",
+ body3LinkLabel: "pestaña de Opciones",
+ body3Suffix: ".",
+ website: {
+ prefix: "Puedes visitar el ",
+ linkLabel: `sitio web de ${BRAND_DISPLAY_NAME}`,
+ url: "https://privacything.com",
+ suffix: " para noticias del proyecto, descargas y más información.",
+ },
+ versionLabel: "Versión",
+ browserTargetLabel: "Objetivo del navegador",
+ releaseChannelLabel: "Canal de lanzamiento",
+ copyLinkLabel: ABOUT_PRODUCT_LOWER,
+ copyLinkTermsLabel: "términos de uso",
+ copyLinkPrivacyLabel: "sobre la privacidad",
+ copyLinkLimitationsLabel: PRODUCT_NOT_LOWER,
+ copyLinkLicenseLabel: "licencia",
+ copyLinkAssetsLabel: "recursos de terceros",
+ copyLinkUsageLabel: HOW_TO_USE_PRODUCT_LOWER,
+ releaseChannels: {
+ local: "Local",
+ beta: "Beta",
+ stable: "Estable",
+ },
+
+ support: {
+ url: "https://webh.pl",
+ logoLinkAriaLabel: "Abrir webh.pl",
+ bodyPrefix: `${BRAND_DISPLAY_NAME} crece con el apoyo de `,
+ linkLabel: "webh.pl",
+ bodySuffix: " — infraestructura rápida y flexible para proyectos ambiciosos.",
+ },
+
+ terms: {
+ title: TERMS_OF_USE,
+ body1: `${BRAND_DISPLAY_NAME} se proporciona “tal cual”, sin garantía de ningún tipo.`,
+ body2: `${BRAND_DISPLAY_NAME} ayuda a los usuarios a controlar localmente la ubicación, la configuración regional, la zona horaria y los datos relacionados expuestos por el navegador. No es una VPN, proxy, herramienta de anonimato, producto de seguridad ni garantía de indetectabilidad.`,
+ body3: `Usa ${BRAND_DISPLAY_NAME} bajo tu propia responsabilidad. Debes cumplir las leyes aplicables, las condiciones de los sitios web, las políticas de tu lugar de trabajo y las normas de las plataformas.`,
+ },
+
+ privacy: {
+ title: "Privacidad",
+ body: `${BRAND_DISPLAY_NAME} guarda localmente en tu navegador tus opciones y datos de configuración. Las solicitudes externas de mapas son opcionales: tras obtener tu consentimiento, usa OpenStreetMap Nominatim para buscar ubicaciones y OpenFreeMap para mostrar vistas previas interactivas.`,
+ openPolicyButton: "Abrir política de privacidad",
+ },
+
+ limitations: {
+ title: WHAT_PRODUCT_IS_NOT,
+ intro: `${BRAND_DISPLAY_NAME} cambia los datos visibles en el navegador como la geolocalización, la configuración regional, la zona horaria y los detalles de la huella digital seleccionados. No enruta tu tráfico a través de una red diferente.`,
+ body1: "No oculta ni reemplaza tu dirección IP.",
+ body2: "No reemplaza una VPN, proxy o configuración de enrutamiento basada en DNS.",
+ body3:
+ "No hace que los sitios web vean tu conexión como proveniente de otro país por sí solo.",
+ outro: `Usa ${BRAND_DISPLAY_NAME} para simular datos en el navegador. Usa una VPN, un proxy o herramientas DNS cuando necesites cambiar la ubicación a nivel de red.`,
+ },
+
+ license: {
+ title: "Licencia",
+ creatorPrefix: "Este proyecto fue creado por ",
+ creatorLabel: "Tomasz Janusz",
+ creatorUrl: "https://tomaszjanusz.dev",
+ creatorSuffix: ".",
+ copyright: "Copyright © 2025-presente.",
+ body: `${BRAND_DISPLAY_NAME} está disponible bajo la Licencia Pública General Affero de GNU v3.0 o posterior, con términos adicionales.`,
+ openLicenseButton: "Abrir licencia",
+ },
+
+ assets: {
+ title: "Recursos de terceros",
+ body: `Consulta los componentes de terceros incluidos y los textos de sus licencias distribuidos con ${BRAND_DISPLAY_NAME}.`,
+ openNoticesButton: "Abrir avisos de terceros",
+ fontAwesome: {
+ label: "Font Awesome Free 7.2.0",
+ url: "https://fontawesome.com",
+ body: " por Font Awesome / Fonticons, Inc. está licenciado bajo CC BY 4.0, SIL OFL 1.1 y MIT.",
+ },
+ mapLibre: {
+ label: "MapLibre GL JS",
+ url: "https://maplibre.org/maplibre-gl-js/docs/",
+ body: " se incluye localmente para la renderización de mapas vectoriales.",
+ },
+ openFreeMap: {
+ label: "OpenFreeMap",
+ url: "https://openfreemap.org/",
+ },
+ openStreetMap: {
+ label: "OpenStreetMap",
+ url: "https://www.openstreetmap.org/copyright",
+ },
+ osmWikiCountryCodes: {
+ label: "Wiki de OpenStreetMap: Nominatim/Country Codes",
+ url: "https://wiki.openstreetmap.org/wiki/Nominatim/Country_Codes",
+ body: " proporciona los valores predeterminados de país a idioma usados durante la generación de perfiles.",
+ licenseLabel: "CC BY-SA 2.0",
+ licenseUrl: "https://wiki.openstreetmap.org/wiki/Wiki_content_license",
+ licenseBody: " se aplica al contenido de esa wiki, que se atribuye aquí.",
+ },
+ mapPreviewsPrefix: "Las vistas previas de mapas interactivos usan ",
+ mapPreviewsMiddle: " servicios con ",
+ mapPreviewsSuffix: " atribución de los colaboradores mostrada en el mapa.",
+
+ localData: {
+ title: "Conjuntos de datos locales",
+ body: `${BRAND_DISPLAY_NAME} incluye pequeñas bases de datos locales creadas a partir de datos públicos procesados para que los valores simulados sean estadísticamente realistas. No se descarga nada de Internet: son instantáneas incluidas en la extensión y renovadas con cada actualización.`,
+ steam: {
+ label: "Encuesta de hardware y software de Steam",
+ url: "https://store.steampowered.com/hwsurvey/",
+ body: " (Valve) proporciona las distribuciones de resolución de pantalla, núcleos de CPU y RAM en Windows, Linux y macOS utilizadas para los perfiles de hardware locales.",
+ },
+ chromiumDash: {
+ label: "ChromiumDash",
+ url: "https://chromiumdash.appspot.com/",
+ body: " proporciona las versiones reales de lanzamiento de Chrome utilizadas para User-Agent y Client Hints.",
+ },
+ localeCatalog: {
+ prefix: "Los catálogos de configuración regional de ",
+ mozilla: {
+ label: "Mozilla",
+ url: "https://github.com/mozilla-firefox/firefox/blob/main/intl/locale/language.properties",
+ },
+ middle: " y ",
+ chromium: {
+ label: "Chromium",
+ url: "https://github.com/chromium/chromium/blob/main/ui/base/l10n/l10n_util.cc",
+ },
+ body: " proporcionan nombres de idioma adaptados a cada motor de navegador.",
+ },
+ },
+ },
+
+ projectLinks: {
+ description: `${BRAND_DISPLAY_NAME} también vive fuera de la extensión. Visita el sitio web, explora el código o avísame cuando algo no funcione.`,
+ websiteLabel: "Sitio web",
+ websiteUrl: "https://privacything.com",
+ sourceLabel: "Código fuente",
+ sourceUrl: "https://github.com/Privacy-Thing/browser-extension",
+ reportBugLabel: "Informar de un error",
+ reportBugUrl:
+ "https://github.com/Privacy-Thing/browser-extension/issues/new?template=bug_report.yml",
+ },
+
+ usage: {
+ title: HOW_TO_USE_PRODUCT,
+ body1:
+ "Para obtener un punto de partida rápido y realista, comienza generando un perfil regional a partir de un lugar buscado.",
+ body2: `Guarda un perfil regional por cada lugar real que quieras que ${BRAND_DISPLAY_NAME} imite y asigna los sitios web al perfil correspondiente.`,
+ body3:
+ "Usa la ventana emergente para asignar dominios rápidamente. Abre Configuración para acceder al panel completo de perfiles y reglas.",
+ body4: `Explora Opciones y Avanzado cuando quieras más control sobre cómo ${BRAND_DISPLAY_NAME} se comporta.`,
+ },
+} as const;
diff --git a/src/ui/i18n/es-sections/advanced.ts b/src/ui/i18n/es-sections/advanced.ts
new file mode 100644
index 0000000..f5ea0a5
--- /dev/null
+++ b/src/ui/i18n/es-sections/advanced.ts
@@ -0,0 +1,205 @@
+import { BRAND_DISPLAY_NAME } from "@/shared/brand";
+import { externalMapConsentCopy } from "@/ui/i18n/es-sections/osm";
+
+const TURN_OFF_PRODUCT = `Desactivar ${BRAND_DISPLAY_NAME}`;
+
+export const advanced = {
+ runtimeTitle: "Opciones avanzadas",
+ runtimeDescription: "Gestiona el registro detallado de la actividad interna.",
+ copyLinkRuntimeLabel: "configuración avanzada",
+ copyLinkHelpLabel: "ayuda avanzada",
+
+ noiseRadius: {
+ title: "Radio máximo de coordenadas predeterminado",
+ description:
+ "Establece el radio máximo predeterminado, en metros, para los perfiles nuevos.",
+ copyLinkLabel: "radio máximo de coordenadas predeterminado",
+ },
+
+ generatedLocationRandomization: {
+ enabled: {
+ title: "Aleatorizar coordenadas al crear nuevos perfiles regionales",
+ description:
+ "Usa esta opción de forma predeterminada en los perfiles generados. Podrás desactivarla en cada operación cuando necesites coordenadas exactas.",
+ readWhy: "Leer por qué.",
+ tooltipPrivacy: `${BRAND_DISPLAY_NAME} está orientado a la privacidad. Los perfiles deben ser fáciles de usar, pero reutilizar las coordenadas exactas de un catálogo o una búsqueda podría facilitar que se reconozca a sus usuarios.`,
+ tooltipExact:
+ "¿Necesitas una ubicación concreta? Desactiva esta opción y el perfil usará las coordenadas exactas.",
+ copyLinkLabel: "aleatorización de coordenadas de perfiles nuevos",
+ },
+ radius: {
+ title: "Radio de aleatorización de coordenadas por defecto",
+ description:
+ "Elige el radio predeterminado que se usará al aleatorizar las coordenadas. Introduce un número entero entre 1 y 99 km.",
+ inputLabel: "Radio de aleatorización de coordenadas por defecto en kilómetros",
+ copyLinkLabel: "radio de aleatorización de coordenadas por defecto",
+ },
+ },
+
+ themeMode: {
+ title: "Tema",
+ description: `Elige si ${BRAND_DISPLAY_NAME} sigue la apariencia del sistema o siempre utiliza un tema fijo claro u oscuro.`,
+ label: "Apariencia",
+ options: {
+ system: "Sistema",
+ light: "Claro",
+ dark: "Oscuro",
+ },
+ copyLinkLabel: "tema",
+ },
+
+ watchPositionDelay: {
+ title: "Intervalo de actualización de la posición (s)",
+ description:
+ "Establece los tiempos mínimo y máximo entre actualizaciones de ubicación en los sitios web.",
+ copyLinkLabel: "intervalo de actualización de la posición",
+ },
+
+ debugMode: {
+ title: "Modo de depuración",
+ description: `Registra la actividad interna detallada de ${BRAND_DISPLAY_NAME} en la consola para desarrolladores del navegador.`,
+ copyLinkLabel: "modo de depuración",
+ },
+
+ experimental: {
+ title: "Experimental",
+ description:
+ "Prueba protecciones para API del navegador que aún se están implementando. Estas opciones están disponibles en todos los canales de publicación.",
+ copyLinkLabel: "opciones experimentales",
+ temporalApi: {
+ title: "Temporal API",
+ description:
+ "Protege el comportamiento de fecha, hora, configuración regional y zona horaria predeterminada de Temporal cuando el navegador ofrece la API nativa. No instala ningún polyfill.",
+ copyLinkLabel: "Temporal API",
+ },
+ domainFencing: {
+ title: "Aislamiento por dominio",
+ description:
+ "Cuando se aplica la Regla predeterminada o un contenedor de Firefox, cada sitio recibe su propia huella digital estable. El perfil regional no cambia.",
+ copyLinkLabel: "aislamiento por dominio",
+ },
+ },
+
+ privacy: {
+ title: "Privacidad",
+ description: `${BRAND_DISPLAY_NAME} se ejecuta en tu dispositivo y no envía a ningún sitio tus datos de navegación. Las únicas solicitudes de red opcionales se realizan al buscar ubicaciones o mostrar vistas previas del mapa mientras configuras perfiles regionales.`,
+ copyLinkLabel: "privacidad",
+
+ osmConsent: {
+ title: externalMapConsentCopy.title,
+ description: externalMapConsentCopy.description,
+ stateUnknown: `Aún no has elegido esto. ${BRAND_DISPLAY_NAME} preguntará antes de conectarse a un servicio de mapas externo.`,
+ statePrefix: "El acceso externo a mapas está",
+ stateEnabled:
+ "activado: se pueden cargar la búsqueda de ubicaciones y las vistas previas del mapa",
+ stateDisabled: `desactivado: ${BRAND_DISPLAY_NAME} no se conectará a Internet mientras configuras ubicaciones`,
+ copyLinkLabel: "acceso a mapas externos",
+ },
+ },
+
+ display: {
+ title: "Apariencia",
+ description: "Ajustar la presentación visual y la configuración de accesibilidad.",
+ copyLinkLabel: "apariencia",
+
+ language: {
+ title: "Idioma",
+ description: `${BRAND_DISPLAY_NAME} usa automáticamente el idioma de la interfaz del navegador.`,
+ optionEnglish: "Inglés",
+ optionSpanish: "Español",
+ optionPortuguese: "Portugués",
+ soon: "AUTOMÁTICO",
+ copyLinkLabel: "idioma",
+ },
+
+ reduceMotion: {
+ title: "Reducir movimiento",
+ description: `Desactivar animaciones de la interfaz en ${BRAND_DISPLAY_NAME}.`,
+ systemOverride:
+ "La reducción de movimiento está habilitada por la configuración de accesibilidad de tu sistema.",
+ copyLinkLabel: "reducir movimiento",
+ },
+
+ accentColor: {
+ title: "Color de acento",
+ description: `Elige el color principal de ${BRAND_DISPLAY_NAME} en una paleta inspirada en los contenedores de Firefox.`,
+ copyLinkLabel: "color de acento",
+ optionAriaLabel: (label: string) => `Usar ${label} como color de acento`,
+ options: {
+ teal: "Verde azulado",
+ blue: "Azul",
+ green: "Verde",
+ yellow: "Amarillo",
+ orange: "Naranja",
+ red: "Rojo",
+ pink: "Rosa",
+ purple: "Morado",
+ gray: "Gris",
+ },
+ },
+
+ highContrast: {
+ title: "Modo de alto contraste",
+ description:
+ "Aumenta el contraste del texto y la visibilidad de los bordes para mejorar la legibilidad.",
+ copyLinkLabel: "modo de alto contraste",
+ },
+ },
+
+ danger: {
+ title: "Zona de peligro",
+ description: `Estas acciones pueden sustituir, eliminar o restaurar la configuración y los datos de ${BRAND_DISPLAY_NAME} guardados en tu dispositivo.`,
+ copyLinkLabel: "zona de peligro",
+
+ spoofing: {
+ title: TURN_OFF_PRODUCT,
+ description: `Desactiva todas las protecciones de ${BRAND_DISPLAY_NAME} hasta que vuelvas a activarlas.`,
+ copyLinkLabel: TURN_OFF_PRODUCT,
+ },
+
+ export: {
+ title: "Exportar",
+ description:
+ "Descarga tus perfiles, reglas y opciones actuales como copia de seguridad en formato JSON.",
+ button: "Exportar configuración",
+ copyLinkLabel: "exportar configuración",
+ },
+
+ import: {
+ title: "Importar",
+ description:
+ "Sustituye la configuración local actual por una copia de seguridad JSON exportada anteriormente.",
+ button: "Importar configuración",
+ copyLinkLabel: "importar configuración",
+ },
+
+ reload: {
+ title: "Recargar",
+ description:
+ "Vuelve a cargar la configuración guardada por la extensión y descarta los cambios sin guardar de la vista actual.",
+ button: "Recargar configuración",
+ copyLinkLabel: "recargar configuración",
+ },
+
+ reset: {
+ title: "Restablecer",
+ description:
+ "Restaura los valores predeterminados y elimina tus perfiles y reglas personalizados.",
+ button: "Restablecer configuración",
+ copyLinkLabel: "restablecer configuración",
+ confirmTitle: "¿Restablecer configuración?",
+ confirmBody:
+ "Esta acción elimina de forma permanente tus perfiles, reglas y opciones personalizadas. No se puede deshacer.",
+ onboardingToggleLabel: "Volver a ejecutar el asistente después de restablecer",
+ onboardingToggleDescription:
+ "Vuelve a abrir la guía inicial una vez borrada la configuración.",
+ },
+ },
+
+ help: {
+ title: "Avanzado",
+ body1: `Usa Avanzado para ajustar las protecciones, desactivar temporalmente ${BRAND_DISPLAY_NAME} o gestionar la configuración y los datos guardados en tu dispositivo.`,
+ body2:
+ "Importar, exportar, recargar y restablecer te permiten trasladar la configuración entre navegadores, corregir errores o recuperar los datos guardados cuando algo deja de estar sincronizado.",
+ },
+} as const;
diff --git a/src/ui/i18n/es-sections/common.ts b/src/ui/i18n/es-sections/common.ts
new file mode 100644
index 0000000..afd1044
--- /dev/null
+++ b/src/ui/i18n/es-sections/common.ts
@@ -0,0 +1,46 @@
+import { BRAND_DISPLAY_NAME } from "@/shared/brand";
+
+export const common = {
+ copyLinkTo: (section: string) => `Copiar enlace a ${section}`,
+
+ actions: {
+ cancel: "Cancelar",
+ reset: "Restablecer",
+ close: "Cerrar",
+ create: "Crear",
+ save: "Guardar",
+ back: "Atrás",
+ continue: "Continuar",
+ edit: "Editar",
+ delete: "Eliminar",
+ duplicate: "Duplicar",
+ search: "Buscar",
+ clear: "Borrar",
+ deleteSelected: "Eliminar seleccionado",
+ clearSelection: "Borrar selección",
+ openPrivacyPolicy: "Abrir política de privacidad",
+ allowOpenStreetMap: "Permitir acceso al mapa",
+ notNow: "Ahora no",
+ openPlayground: "Abrir Área de pruebas",
+ },
+ selectionCount: (count: number) =>
+ `${count} ${count === 1 ? "seleccionado" : "seleccionados"}`,
+
+ fields: {
+ name: "Nombre",
+ latitude: "Latitud",
+ longitude: "Longitud",
+ accuracy: "Precisión",
+ noiseRadius: "Radio máximo (m)",
+ timeZone: "Zona horaria",
+ },
+
+ coordinateRandomization: {
+ labelBefore: "Aleatorizar coordenadas dentro de",
+ labelAfter: "km.",
+ radiusInputLabel: "Radio de aleatorización de coordenadas en kilómetros",
+ readWhy: "Leer por qué.",
+ tooltipPrivacy: `${BRAND_DISPLAY_NAME} está orientado a la privacidad. Los perfiles deben ser fáciles de usar, pero reutilizar las coordenadas exactas de un catálogo o una búsqueda podría facilitar que se reconozca a sus usuarios.`,
+ tooltipExact: `¿Necesitas una ubicación concreta? Desactiva este interruptor y ${BRAND_DISPLAY_NAME} usará las coordenadas exactas. Puedes cambiar los valores predeterminados en Opciones > Geolocalización > Avanzado.`,
+ },
+} as const;
diff --git a/src/ui/i18n/es-sections/demo.ts b/src/ui/i18n/es-sections/demo.ts
new file mode 100644
index 0000000..06b5862
--- /dev/null
+++ b/src/ui/i18n/es-sections/demo.ts
@@ -0,0 +1,152 @@
+import { BRAND_DISPLAY_NAME } from "@/shared/brand";
+
+const SPOOFED_PRODUCT = `Simulado — ${BRAND_DISPLAY_NAME}`;
+
+export const demo = {
+ loadingSettings: "Cargando configuración…",
+ noLocationsTitle: "Aún no hay perfiles regionales",
+ noLocationsBody:
+ "Crea al menos un perfil regional en Configuración para usar el Área de pruebas.",
+ openSettingsButton: "Abrir Configuración",
+
+ locationPreview: {
+ title: "Vista previa del perfil",
+ activeLocationLabel: "Perfil activo",
+ activeLocationDescription:
+ "Selecciona un perfil regional guardado para previsualizar los valores que verá el navegador. Al cambiar de perfil se reinicia la ruta y se aplica la nueva vista previa de geolocalización.",
+ activeLocationPlaceholder: "Elige un perfil…",
+ playgroundCadenceLabel: "Intervalo del Área de pruebas",
+ realSiteCadenceLabel: "Tiempo de actualización del sitio web",
+ configuredDelayLabel: "Intervalo de actualización de la posición",
+ callbackDelayLabel: "Retardo de la respuesta",
+ runtimeModeLabel: "Modo de ejecución",
+ runtimeModeSimple: "Tiempo por defecto",
+ realLocationTitleIdle: "Comparar con la ubicación actual del navegador",
+ realLocationTitleLoading: "Solicitando la ubicación actual del navegador",
+ realLocationTitleGranted: "La ubicación actual del navegador está lista",
+ realLocationTitleDenied: "Permiso de ubicación del navegador denegado",
+ realLocationTitleUnavailable: "Ubicación del navegador no disponible",
+ realLocationDescription:
+ "Solicita una vez la geolocalización real del navegador para compararla con la vista previa del perfil.",
+ realLocationGrantedDescription:
+ "La ubicación actual del navegador ya está disponible en las filas de geolocalización para compararla en paralelo.",
+ realLocationDeniedDescription:
+ "El navegador ha denegado el acceso a la geolocalización real. Puedes volver a intentarlo si cambia el permiso.",
+ realLocationUnavailable:
+ "La comparación de ubicación real no está disponible porque este navegador no expone la geolocalización aquí.",
+ realLocationRefresh: "Actualizar ubicación real",
+ },
+
+ localMachineTitle: "Dispositivo local",
+ spoofedTitle: SPOOFED_PRODUCT,
+ waitingForPermission: "Esperando permiso del navegador…",
+ permissionDenied: "Permiso denegado por el navegador",
+ geolocationUnavailable: "Geolocalización no disponible",
+ requestRealLocation: "Solicitar ubicación real",
+ requestRealLocationHintTitle: "Ubicación real aún no cargada",
+ requestRealLocationTableHint:
+ "Solicita arriba la ubicación real del navegador para compararla aquí.",
+ waitingForFix: "Esperando la primera corrección…",
+ selectLocationPrompt: "Selecciona arriba un perfil regional para ver sus valores.",
+
+ comparison: {
+ language: "navigator.language",
+ languages: "navigator.languages",
+ timeZone: "Intl…resolvedOptions().timeZone",
+ acceptLanguage: "Encabezado 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:
+ "Acerca de la visualización de la marca de tiempo de geolocalización",
+ timestampTooltip: `${BRAND_DISPLAY_NAME} devuelve la marca de tiempo Unix sin procesar en milisegundos. La fecha legible para humanos que se muestra aquí es solo por conveniencia y está formateada en la zona horaria de la columna mostrada.`,
+ 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: "Resumen de la prueba Canvas 2D",
+ webglRenderer: "Prueba del renderizador WebGL",
+ webglDebugExtension: "WEBGL_debug_renderer_info",
+ webglReadPixels: "Sonda readPixels() de WebGL",
+ audioFingerprint: "Sonda 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: "Encabezado Sec-CH-UA",
+ secChUaPlatform: "Encabezado Sec-CH-UA-Platform",
+ secChUaMobile: "Encabezado Sec-CH-UA-Mobile",
+ secChUaFullVersionList: "Encabezado Sec-CH-UA-Full-Version-List",
+ webRTCIcePolicy: "RTCPeerConnection ICE policy",
+ probePending: "Recopilando…",
+ notAvailable: "No disponible",
+ spoofedMatchesLocal:
+ "El valor de vista previa coincide con tu navegador para esta identidad.",
+ browserVersionNote: (versionToken: string) =>
+ `${BRAND_DISPLAY_NAME} mantiene tokens de versión del navegador normalizados y no aleatoriza variantes de marcador de posición como ${versionToken}.`,
+ },
+
+ previewSeed: {
+ title: "Identidad de vista previa",
+ description:
+ "Elige la identidad de navegador consistente que se muestra en esta vista previa del Área de pruebas.",
+ inputAriaLabel: "Código de identidad de vista previa",
+ placeholder: "Código de identidad",
+ hint: "Usa 6 letras minúsculas o dígitos. Cambia el código para previsualizar una identidad diferente.",
+ randomize: "Generar nueva identidad",
+ },
+
+ sections: {
+ localeDate: "Configuración regional y fecha",
+ networkHeaders: "Encabezados de red",
+ geolocation: "Geolocalización",
+ browserFingerprint: "Identidad del navegador",
+ webglCanvas: "Canvas y WebGL",
+ screen: "Pantalla",
+ audio: "Audio",
+ webRTC: "WebRTC",
+ },
+
+ map: {
+ title: "Vista previa del mapa",
+ noLocationTitle: "Selecciona primero un perfil",
+ noLocationDescription:
+ "Elige arriba un perfil regional guardado para activar el mapa y los controles de puntos de referencia.",
+ osmRequired: "Se requiere acceso al mapa",
+ osmRequiredDescription:
+ "Permite el acceso externo al mapa para previsualizar la posición simulada en un mapa interactivo.",
+ demoIntervalLabel: "Intervalo de demostración (2–5 s)",
+ clearButton: "Borrar",
+ },
+
+ disclaimer: {
+ title: "Modo de vista previa, no un sitio web en vivo",
+ body: `Usa esta página para comprobar cómo ${BRAND_DISPLAY_NAME} presentaría el perfil regional seleccionado antes de aplicarlo en sitios web. El idioma, la configuración regional, la hora, las cabeceras y otros valores se muestran como una vista previa de los mismos datos del perfil. La geolocalización utiliza el mismo comportamiento de movimiento y actualización que los sitios protegidos. En el Área de pruebas, la ubicación se actualiza más rápido que en sitios normales para que los cambios resulten más visibles.`,
+ },
+
+ howItWorks: {
+ title: "Cómo leer esta vista previa",
+ body1:
+ "Idioma, configuración regional y hora — estas filas muestran cómo se presentaría el perfil seleccionado a los sitios web, sin modificar la propia página del Área de pruebas.",
+ body2: `Geolocalización — las filas de mapa y ubicación usan el mismo comportamiento de movimiento y actualización que los sitios web protegidos, por lo que puedes previsualizar el tiempo y la variación de coordenadas antes de asignar el perfil.`,
+ body3: `Valores y cabeceras del navegador — estas comparaciones colocan los valores actuales del navegador junto a los que ${BRAND_DISPLAY_NAME} mostraría para el perfil seleccionado, con las mismas protecciones definidas en Configuración.`,
+ body4:
+ "El Área de pruebas se actualiza más rápido que la navegación normal, por lo que los cambios son más fáciles de detectar.",
+ },
+} as const;
diff --git a/src/ui/i18n/es-sections/firefox-containers.ts b/src/ui/i18n/es-sections/firefox-containers.ts
new file mode 100644
index 0000000..a2e6d11
--- /dev/null
+++ b/src/ui/i18n/es-sections/firefox-containers.ts
@@ -0,0 +1,101 @@
+import { BRAND_DISPLAY_NAME } from "@/shared/brand";
+
+export const firefoxContainers = {
+ copyLinkLabel: "contenedores",
+ copyLinkHelpLabel: "ayuda de contenedores",
+ title: "Contenedores",
+ description: `Gestiona los contenedores de Firefox y asigna a cada uno un perfil de ${BRAND_DISPLAY_NAME} desde un único lugar.`,
+ refreshTitle: "Actualizar lista de contenedores",
+ refreshButton: "Actualizar lista",
+ createButton: "Nuevo contenedor",
+ deleteButton: "Eliminar",
+ removeAssignmentButton: "Eliminar asignación",
+ editActionTitle: (name: string) => `Editar contenedor “${name}”`,
+ deleteActionTitle: (name: string) => `Eliminar contenedor “${name}”`,
+ removeAssignmentActionTitle: (name: string) =>
+ `Eliminar la asignación del contenedor eliminado «${name}»`,
+ showInactiveLabel: "Mostrar contenedores inactivos",
+ filterPlaceholder: "Buscar contenedores…",
+ loading: "Cargando contenedores…",
+ noResults: "Ningún contenedor coincide con el filtro actual.",
+ columns: {
+ container: "Contenedor",
+ location: "Perfil",
+ actions: "Acciones",
+ },
+ noLocationAssigned: "No hay perfil asignado",
+ missingLocation: "Falta el perfil asignado",
+ inactiveBadge: "Eliminado",
+ deletedContainerLabel: "Contenedor eliminado",
+ emptyTitle: "Aún no hay contenedores de Firefox",
+ emptyBody: `Crea tu primer contenedor aquí o en la configuración de Firefox y asígnale un perfil de ${BRAND_DISPLAY_NAME}.`,
+ unavailableTitle: "Los Contenedores de Firefox no están disponibles",
+ unavailableBody:
+ "Activa los Contenedores de Firefox, luego actualiza la lista. Si el problema continúa, es posible que esta versión de Firefox no permita que la extensión los gestione.",
+ unavailableHint: `${BRAND_DISPLAY_NAME} solo puede gestionar contenedores cuando los Contenedores de Firefox están activados.`,
+ editor: {
+ createTitle: "Crear contenedor",
+ editTitlePrefix: "Editar contenedor",
+ createDescription: `Elige el nombre, el color, el icono y, si quieres, la configuración de ${BRAND_DISPLAY_NAME} del nuevo contenedor de Firefox.`,
+ editDescription: `Actualiza a la vez las propiedades del contenedor de Firefox y su configuración opcional de ${BRAND_DISPLAY_NAME}.`,
+ saveButton: "Guardar",
+ previewUntitled: "Contenedor sin título",
+ nameRequired: "El nombre del contenedor no puede estar vacío.",
+ defaultRulePreviewTitle: "Aún no hay configuración guardada",
+ defaultRulePreviewLocation: (label: string) =>
+ `Este contenedor aún no tiene configuración propia de ${BRAND_DISPLAY_NAME}, por lo que usa el perfil de la Regla predeterminada: ${label}. Guarda la configuración del contenedor para asignarle sus propias opciones.`,
+ defaultRulePreviewProtections: `Este contenedor aún no tiene configuración propia de ${BRAND_DISPLAY_NAME}, por lo que solo usa las protecciones de la Regla predeterminada que no requieren un perfil. Guarda la configuración del contenedor para asignarle sus propias opciones.`,
+ defaultRulePreviewUnconfigured: `Este contenedor aún no tiene configuración propia de ${BRAND_DISPLAY_NAME}. La Regla predeterminada está activada, pero no tiene un perfil ni protecciones personalizadas. Configura el contenedor o termina de configurar la Regla predeterminada.`,
+ defaultRulePreviewDisabled: `Este contenedor aún no tiene configuración propia de ${BRAND_DISPLAY_NAME}. La Regla predeterminada está desactivada, por lo que ${BRAND_DISPLAY_NAME} permanece desactivado aquí. Configura el contenedor o activa la Regla predeterminada.`,
+ defaultRulePreviewMissing: `Este contenedor aún no tiene configuración propia de ${BRAND_DISPLAY_NAME} y no hay una Regla predeterminada guardada, por lo que ${BRAND_DISPLAY_NAME} permanece desactivado aquí. Configura el contenedor o crea una Regla predeterminada.`,
+ fields: {
+ name: "Nombre",
+ color: "Color",
+ icon: "Icono",
+ enabled: "Activado",
+ location: "Perfil",
+ locationProfile: "Perfil regional",
+ },
+ unassignedLocation: "No hay perfil asignado",
+ enabledHint:
+ "Cuando está desactivada, esta asignación de contenedor no se aplica. Sus opciones permanecen guardadas.",
+ locationProfileHint:
+ "Elige el perfil que debe usar este contenedor. Déjalo sin asignar para usar solo las protecciones del contenedor y, cuando esté disponible, el perfil de la Regla predeterminada.",
+ identity: {
+ sectionTitle: "Identidad",
+ sectionDescription:
+ "Este contenedor conserva su propia identidad simulada. Rótala únicamente cuando quieras una huella digital nueva y borrar el estado de los sitios asociados al contenedor.",
+ actionDescription:
+ "Borra los datos de sitios relacionados y comienza este contenedor con una identidad nueva.",
+ actionLabel: "Nueva identidad",
+ confirmTitle: (name: string) => `Nueva identidad para “${name}”?`,
+ confirmDescription:
+ "Esto borra cookies, almacenamiento, Service Workers y cachés de los sitios vinculados a este contenedor. Luego crea una nueva identidad de simulación.",
+ confirmDomainsLabel: `${BRAND_DISPLAY_NAME} borrará datos del navegador para estos dominios:`,
+ confirmNoDomains: `Aún no se han registrado datos del navegador para este contenedor. ${BRAND_DISPLAY_NAME} todavía creará una nueva identidad de simulación.`,
+ confirmLabel: "Crear nueva identidad",
+ rotateSuccess: "Se ha guardado una nueva identidad de contenedor.",
+ rotateError: "No se pudo crear una nueva identidad de contenedor.",
+ },
+ },
+ confirmations: {
+ deleteTitle: (name: string) => `Eliminar contenedor “${name}”?`,
+ deleteBody: `Esta acción elimina el contenedor de Firefox y borra su asignación de ${BRAND_DISPLAY_NAME}.`,
+ removeOrphanTitle: "¿Eliminar la asignación del contenedor eliminado?",
+ removeOrphanBody: `El contenedor de Firefox ya no existe. Esta acción solo borrará su asignación obsoleta de ${BRAND_DISPLAY_NAME}.`,
+ },
+ messages: {
+ created: (name: string) => `Se ha creado el contenedor «${name}».`,
+ updated: (name: string) => `Se ha guardado el contenedor «${name}».`,
+ deleted: (name: string) => `Se ha eliminado el contenedor «${name}».`,
+ orphanRemoved: "Se eliminó la asignación de contenedor obsoleta.",
+ saveFailed: "Error al guardar el contenedor.",
+ deleteFailed: "Error al eliminar el contenedor.",
+ },
+ help: {
+ title: "Contenedores",
+ body1: `Los contenedores ofrecen otra forma de elegir la configuración en Firefox. ${BRAND_DISPLAY_NAME} comprueba primero las reglas de dominio y después la asignación del contenedor.`,
+ body2: `El editor guarda los datos del contenedor de Firefox junto con opciones de ${BRAND_DISPLAY_NAME}, como el estado, un perfil opcional y protecciones personalizadas.`,
+ body3: `Las filas inactivas representan asignaciones cuyo contenedor de Firefox se eliminó fuera de ${BRAND_DISPLAY_NAME}. Revísalas y bórralas de forma explícita para evitar perderlas sin aviso.`,
+ },
+} as const;
diff --git a/src/ui/i18n/es-sections/locations.ts b/src/ui/i18n/es-sections/locations.ts
new file mode 100644
index 0000000..2e72081
--- /dev/null
+++ b/src/ui/i18n/es-sections/locations.ts
@@ -0,0 +1,108 @@
+import { BRAND_DISPLAY_NAME } from "@/shared/brand";
+
+const PRODUCT_NOT_LOWER = `qué ${BRAND_DISPLAY_NAME} no es`;
+
+export const locations = {
+ title: "Perfiles regionales",
+ description:
+ "Los perfiles regionales agrupan coordenadas, configuración regional y zona horaria para simularlas.",
+ addManualButton: "Agregar manualmente",
+ actionsMenuLabel: "Acciones del perfil",
+ generateButton: "Generar perfil",
+ searchPlaceholder: "Buscar perfiles…",
+ unused: "Sin usar",
+ assigned: (count: number) =>
+ `${count} ${count === 1 ? "asignación" : "asignaciones"}`,
+ viewAssignedRulesAriaLabel: (locationLabel: string, count: number) =>
+ `Mostrar ${count} ${count === 1 ? "regla de dominio asignada" : "reglas de dominio asignadas"} a ${locationLabel}`,
+ copyLinkLabel: "perfiles regionales",
+ copyLinkHelpLabel: "ayuda de perfiles regionales",
+
+ playgroundCard: {
+ title: "Área de pruebas",
+ body1:
+ "Previsualiza un perfil guardado antes de asignarlo a sitios. El Área de pruebas muestra su configuración regional, zona horaria, formato de fecha y coordenadas simuladas con el mismo comportamiento de geolocalización que los sitios protegidos.",
+ body2:
+ "De forma predeterminada, el Área de pruebas actualiza la geolocalización simulada cada 2-5 segundos para facilitar la inspección del movimiento. Los sitios reales usan el intervalo de actualización configurado.",
+ },
+
+ help: {
+ title: "Perfiles regionales",
+ body1:
+ "Un perfil regional debe representar un lugar creíble. Mantén su configuración regional, sus idiomas y su zona horaria alineados con la región de las coordenadas.",
+ body2: `Los cambios se guardan automáticamente. Cuando editas un campo, ${BRAND_DISPLAY_NAME} escribe la actualización después de una breve pausa.`,
+ body3:
+ "Un perfil regional guardado cambia valores del navegador como la geolocalización, la configuración regional y la zona horaria. No cambia tu dirección IP ni la ruta de tu tráfico.",
+ privacyTitle: "Privacidad",
+ privacyBody: `La búsqueda y las vistas previas del mapa son opcionales. ${BRAND_DISPLAY_NAME} solo contacta a OpenStreetMap Nominatim para búsqueda y a OpenFreeMap para vistas previas interactivas del mapa después de que lo permitas.`,
+ networkTitle: "Límites de red",
+ networkBodyPrefix:
+ "¿Necesitas cambiar la ubicación a nivel de red? Usa una VPN, un proxy o una herramienta DNS. Consulta ",
+ networkBodyLinkLabel: PRODUCT_NOT_LOWER,
+ networkBodySuffix: ".",
+ },
+
+ editor: {
+ title: "Editar perfil",
+ description:
+ "Actualiza el perfil regional guardado, incluidas sus coordenadas, formatos regionales y forma de informar de la ubicación.",
+ deleteBlockedTitle: "Este perfil aún está asignado",
+ deleteBlockedDescription:
+ "Cambia o elimina todas las asignaciones indicadas a continuación antes de eliminar el perfil.",
+ deleteBlockedButtonTitle:
+ "Elimina todas las asignaciones antes de borrar este perfil.",
+ disabledDependencySuffix: "(desactivado)",
+ mapDisabledTitle: "Mapa desactivado",
+ mapDisabledBody:
+ "La vista previa del mapa no se ha cargado porque no permitiste solicitudes externas de mapas.",
+ geolocationSectionTitle: "Geolocalización",
+ geolocationSectionDescription:
+ "Coordenadas, precisión y el máximo rango permitido para posiciones simuladas.",
+ localeSectionTitle: "Hora y idioma",
+ localeSectionDescription:
+ "Mantén los valores regionales alineados con la región de las coordenadas guardadas.",
+ primaryLocaleLabel: "Configuración regional principal",
+ languageDescription:
+ "Es el formato regional principal del perfil para fechas, números y otros valores localizados.",
+ languageBehaviorDescription:
+ "Cuando «Preferir inglés en los sitios web» está desactivado, los sitios también ven este valor como navigator.language. Aunque el inglés aparezca primero, esta configuración regional sigue controlando el formato predeterminado de fechas y números.",
+ preferredLanguagesLabel: "Idiomas preferidos",
+ languagesDescription:
+ "Lista ordenada de preferencias de idioma del navegador para este perfil.",
+ languagesBehaviorDescription: `${BRAND_DISPLAY_NAME} expone este orden como navigator.languages y en las preferencias de idioma relacionadas. Mantén primero la configuración regional principal y añade después idiomas alternativos verosímiles para este perfil.`,
+ preferEnglishContentLabel: "Preferir inglés en sitios web",
+ preferEnglishContentDescriptionPrefix: `${BRAND_DISPLAY_NAME} mantiene la configuración regional guardada como tu referencia regional, pero expone`,
+ preferEnglishContentDescriptionSuffix:
+ "como primera opción en las preferencias de idioma del navegador para aumentar la probabilidad de que los sitios permanezcan en inglés.",
+ preferEnglishContentLockedTagTitle: (locale: string) =>
+ `${locale} se añade debido a la preferencia de usar el navegador en inglés y no se puede eliminar aquí.`,
+ accuracyDescription:
+ "Controla el valor de precisión que los sitios web ven en los resultados de geolocalización.",
+ noiseRadiusDescription:
+ "Distancia máxima permitida entre las coordenadas simuladas y el punto guardado de este perfil.",
+ },
+
+ generator: {
+ title: "Generar perfil",
+ searchStepDescription:
+ "Busca una ciudad, una dirección o un lugar para crear un perfil.",
+ resultStepDescription: `Elige el resultado correcto antes de que ${BRAND_DISPLAY_NAME} cree el perfil.`,
+ languageStepDescription:
+ "Elige el idioma del navegador que servirá de base para este perfil antes de revisar el resto.",
+ confirmStepDescription: "Confirma el perfil en el mapa antes de añadirlo.",
+ locationLabel: "Ubicación",
+ locationPlaceholder: "Varsovia, Polonia",
+ osmDisclaimer:
+ "Las consultas de búsqueda utilizan la API externa Nominatim de OpenStreetMap. Las vistas previas interactivas usan las teselas vectoriales y fuentes de OpenFreeMap después del consentimiento.",
+ resultSelectLabel: "Resultado de búsqueda",
+ resultStepBody:
+ "OpenStreetMap devolvió más de una coincidencia posible. Selecciona el lugar que querías, luego continúa.",
+ resultStepHint: "Elige un resultado para continuar.",
+ languageSelectLabel: "Idioma del navegador",
+ languageStepBody:
+ "Hay más de un idioma del navegador disponible para este lugar. Elige el que quieras usar en el perfil.",
+ languageStepHint:
+ "Si el idioma que deseas no aparece a continuación, puedes elegir otro en el siguiente paso.",
+ resultPrefix: "Resultado: ",
+ },
+} as const;
diff --git a/src/ui/i18n/es-sections/options.ts b/src/ui/i18n/es-sections/options.ts
new file mode 100644
index 0000000..2ecad92
--- /dev/null
+++ b/src/ui/i18n/es-sections/options.ts
@@ -0,0 +1,124 @@
+import { workerHandlingModeCopy } from "./shared-worker";
+
+import { BRAND_DISPLAY_NAME } from "@/shared/brand";
+
+export const options = {
+ title: "Configuración",
+ tabsAriaLabel: "Secciones de configuración",
+ spoofingOffBannerTitle: "La simulación está desactivada",
+ spoofingOffBannerBody: `${BRAND_DISPLAY_NAME} no está aplicando valores simulados de ubicación o del navegador en este momento.`,
+ spoofingOffBannerAction: "Volver a activar la simulación",
+ spoofingTurnedOffToast: `${BRAND_DISPLAY_NAME} desactivado.`,
+ spoofingTurnedOnToast: `${BRAND_DISPLAY_NAME} activado.`,
+
+ tabs: {
+ locations: "Perfiles regionales",
+ rules: "Reglas de dominio",
+ trustedSites: "Sitios de confianza",
+ playground: "Área de pruebas",
+ options: "Opciones",
+ advanced: "Avanzado",
+ about: "Acerca de",
+ },
+} as const;
+
+export const optionsPage = {
+ browserFingerprintSpoofing: {
+ title: "Configuración global de protección",
+ description: `Elige qué protecciones del navegador usa ${BRAND_DISPLAY_NAME} de forma predeterminada. Las opciones más específicas pueden sustituir protecciones concretas, mientras que Sitios de confianza puede desactivarlas en los sitios coincidentes.`,
+ disabledNote: `Las protecciones del navegador están desactivadas en todas partes. Vuelve a activarlas para elegir qué ${BRAND_DISPLAY_NAME} debería simular por defecto.`,
+ copyLinkLabel: "protecciones del navegador",
+ items: {
+ geolocation: {
+ label: "Geolocalización",
+ description: `Controla la API de geolocalización. Cuando está activada, ${BRAND_DISPLAY_NAME} responde a las solicitudes de ubicación con el perfil activo. Desactívala y los sitios recibirán el resultado real de geolocalización del navegador.`,
+ advancedButton: "Avanzado",
+ advancedModal: {
+ title: "Configuración avanzada de geolocalización",
+ description:
+ "Ajusta el radio de coordenadas predeterminado y el tiempo de actualización de la ubicación.",
+ },
+ },
+ timeLocale: {
+ label: "Hora y configuración regional",
+ description: `Controla Date, Intl, navigator.language, navigator.languages y las cabeceras de idioma. Cuando está activada, ${BRAND_DISPLAY_NAME} mantiene estos valores alineados con el perfil activo. Desactívala y los sitios verán la configuración regional real del navegador.`,
+ },
+ canvas: {
+ label: "Canvas",
+ description: `Los sitios pueden dibujar una imagen oculta y usar pequeñas diferencias de renderizado para reconocer tu navegador. Cuando está activado, ${BRAND_DISPLAY_NAME} añade ruido controlado para que el resultado sea más difícil de reutilizar como huella digital. Desactívalo y los sitios recibirán la salida real del canvas.`,
+ },
+ webGL: {
+ label: "WebGL",
+ description: `WebGL revela detalles gráficos como el modelo de tu GPU, el renderizador y el comportamiento del controlador. Cuando esto está activado, ${BRAND_DISPLAY_NAME} reemplaza esas pistas con un perfil consistente que se ajusta a tu plataforma actual. Desactívalo y los sitios pueden leer datos gráficos más cercanos a tu máquina real.`,
+ },
+ audio: {
+ label: "Audio",
+ description: `Las API de audio producen pequeñas diferencias específicas del hardware que los sitios pueden medir en segundo plano. Cuando esto está activado, ${BRAND_DISPLAY_NAME} cambia ligeramente esos valores para que sean menos útiles para el seguimiento. Desactívalo y la salida de audio permanece sin cambios.`,
+ },
+ navigator: {
+ label: "Navigator",
+ description: `Los campos del navegador revelan detalles como tu plataforma, configuración de idioma, pistas del CPU y otros datos de identidad del navegador. Cuando esto está activado, ${BRAND_DISPLAY_NAME} enmascara campos de identidad seleccionados mientras los mantiene internamente consistentes. Apágalo y los sitios verán más de tu identidad real del navegador.`,
+ },
+ screen: {
+ label: "Pantalla",
+ description: `El tamaño de la pantalla, la relación de píxeles y la profundidad de color ayudan a reducirte a un pequeño grupo de dispositivos. Cuando esto está activado, ${BRAND_DISPLAY_NAME} enmascara esos detalles exactos de la pantalla. Apágalo y los sitios obtendrán las propiedades reales de tu pantalla.`,
+ },
+ clientHints: {
+ label: "Client Hints",
+ description: `Client Hints comparte la versión del navegador, la plataforma y datos del dispositivo mediante cabeceras de solicitud y API de JavaScript. Cuando está activado, ${BRAND_DISPLAY_NAME} mantiene estos valores coherentes con la identidad simulada del navegador. Desactívalo y los sitios recibirán los datos reales del navegador y la plataforma.`,
+ },
+ battery: {
+ label: "Batería",
+ description: `El estado de la batería puede revelar información cambiante del dispositivo que los sitios pueden usar para vincular visitas. Cuando esto está activado, ${BRAND_DISPLAY_NAME} informa de una batería llena y cargando en lugar del estado real del dispositivo. Apágalo y los sitios pueden leer la API de Estado de Batería nativa.`,
+ },
+ clientHintsVersionRotation: {
+ label: "Rotar números de compilación y parche",
+ description: (count: number) =>
+ `Rota los números de compilación y parche del motor Chromium expuestos mediante Client Hints, manteniendo la versión principal del navegador instalado. ${BRAND_DISPLAY_NAME} incluye un catálogo de compilaciones recientes de Chromium; para esta versión puede elegir entre ${count} ${count === 1 ? "entrada compatible" : "entradas compatibles"}. La cadena reducida de User-Agent conserva el formato .0.0.0 que el navegador usa para proteger la privacidad.`,
+ hintPrefix: "p. ej.:",
+ hint: "139.0.[compilación].[parche]",
+ },
+ webRTC: {
+ label: "WebRTC",
+ description: `WebRTC puede revelar direcciones IP locales o públicas, incluso cuando usas una VPN o un proxy. Cuando esto está activado, ${BRAND_DISPLAY_NAME} pide al navegador que use su modo más estricto de manejo de IP. Apágalo y WebRTC se comporta normalmente, lo que puede exponer más detalles de la red.`,
+ },
+ serviceWorker: {
+ label: "Service Workers",
+ description: `Los Service Workers se ejecutan en segundo plano y pueden permitir que los sitios conserven datos e identificadores durante mucho tiempo. Activa esta opción para que ${BRAND_DISPLAY_NAME} impida de forma predeterminada que los sitios registren Service Workers.`,
+ warning:
+ "Bloquear los Service Workers puede romper aplicaciones web instalables (PWAs), el modo sin conexión, las notificaciones push y la sincronización en segundo plano. Algunas aplicaciones también pueden funcionar más lentamente o perder funciones.",
+ defaultState:
+ "El bloqueo está desactivado de forma predeterminada. Puedes activarlo globalmente y permitir después los Service Workers en dominios concretos mediante una regla de dominio.",
+ allow: "Permitir",
+ block: "Bloquear",
+ },
+ sharedWorker: {
+ label: "Dedicated y Shared Workers",
+ descriptionLead:
+ "Elige cómo se ejecutan los Dedicated y Shared Workers de forma predeterminada. Las reglas de dominio y los contenedores de Firefox pueden sustituir esta política en los sitios coincidentes.",
+ copyLinkLabel: "Manejo de Dedicated y 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: "Mostrar recuento de llamadas en el icono de la extensión",
+ description: `Muestra en el icono de la extensión cuántas llamadas a API del navegador ha gestionado ${BRAND_DISPLAY_NAME}, en lugar de la etiqueta de texto.`,
+ includeDateCalls: {
+ label: "Incluir llamadas a Date y a la API Temporal",
+ description:
+ "Incluir llamadas a Date.* y Temporal.* en el número del icono. Desactiva esto para evitar que verificaciones frecuentes de tiempo inflen el recuento.",
+ },
+ },
+ copyLinkHelpLabel: "ayuda de opciones",
+ help: {
+ title: "Opciones",
+ body1: `Las protecciones del navegador te permiten elegir qué valores simula ${BRAND_DISPLAY_NAME} de forma predeterminada. Afectan a todos los sitios salvo que una regla de dominio o un contenedor de Firefox indique lo contrario.`,
+ body2: `Los controles de privacidad abarcan las únicas solicitudes de red opcionales que ${BRAND_DISPLAY_NAME} realiza al configurar ubicaciones. Si los dejas desactivados, ${BRAND_DISPLAY_NAME} seguirá funcionando; simplemente añadirás las ubicaciones de forma manual, sin búsquedas ni vistas previas del mapa.`,
+ },
+} as const;
diff --git a/src/ui/i18n/es-sections/osm.ts b/src/ui/i18n/es-sections/osm.ts
new file mode 100644
index 0000000..ceda413
--- /dev/null
+++ b/src/ui/i18n/es-sections/osm.ts
@@ -0,0 +1,12 @@
+import { BRAND_DISPLAY_NAME } from "@/shared/brand";
+
+export const externalMapConsentCopy = {
+ title: "Permitir búsqueda de ubicación y solicitudes de mapa",
+ description: `Permite que ${BRAND_DISPLAY_NAME} envíe consultas a OpenStreetMap Nominatim cuando busques ubicaciones y solicite teselas vectoriales y fuentes a OpenFreeMap cuando se muestren vistas previas del mapa. Estos servicios reciben el texto de búsqueda, las solicitudes de mapas y fuentes, y tu dirección IP, pero no tus reglas, perfiles ni historial de navegación guardados. Mantén esta opción desactivada si prefieres introducir las coordenadas manualmente sin recurrir a servicios de mapas durante la configuración.`,
+} as const;
+
+export const osm = {
+ modalTitle: "¿Permitir solicitudes externas de mapas y búsquedas?",
+ body1: `El acceso externo a mapas es opcional. ${BRAND_DISPLAY_NAME} no envía por sí solo tus perfiles, reglas ni datos de navegación a estos servicios.`,
+ body2: `Si permites esto, ${BRAND_DISPLAY_NAME} puede contactar a OpenStreetMap Nominatim para la búsqueda de ubicaciones y a OpenFreeMap para vistas previas interactivas del mapa.`,
+} as const;
diff --git a/src/ui/i18n/es-sections/popup.ts b/src/ui/i18n/es-sections/popup.ts
new file mode 100644
index 0000000..3bf2766
--- /dev/null
+++ b/src/ui/i18n/es-sections/popup.ts
@@ -0,0 +1,252 @@
+import type { SurfacePresentationState } from "@privacy-brand/xray-protocol";
+
+import { BRAND_DIAGNOSTICS_NAME, BRAND_DISPLAY_NAME } from "@/shared/brand";
+
+const formatCount = (count: number, singular: string, plural: string): string =>
+ `${count} ${count === 1 ? singular : plural}`;
+
+export const popup = {
+ loading: "Cargando…",
+ protectionProtected: "Protegido",
+ protectionOff: "Protecciones desactivadas",
+ protectionDisabled: "Protecciones desactivadas",
+ protectionNeedsAttention: "Necesita atención",
+ protectionUnknown: "Estado de protección no disponible",
+ protectionSourceSiteRule: "Regla de dominio",
+ protectionSourceContainer: "Contenedor de Firefox",
+ protectionSourceDefaultRule: "Regla predeterminada",
+ protectionSourceTrustedSite: "Sitio de confianza",
+ protectionSourceNone: "Sin regla activa",
+ protectionSourceGlobalSetting: "Configuración global",
+ languagePriorityTooltip: (languages: readonly string[]) =>
+ [
+ "Prioridad de idioma:",
+ ...languages.map((language, index) => `${index + 1}. ${language}`),
+ ].join("\n"),
+ protectionViewDetails: "Ver detalles",
+ protectionViewPageActivity: "Ver actividad de la página",
+ protectionDetailsTitle: "Detalles de protección",
+ protectionDetailsLead: `Consulta cómo gestiona ${BRAND_DISPLAY_NAME} cada función del navegador en este sitio.`,
+ protectionGroupLocationLocale: "Ubicación, hora y configuración regional",
+ protectionGroupBrowserIdentity: "Identidad del navegador",
+ protectionGroupRenderingMedia: "Renderizado y medios",
+ protectionGroupWorkers: "Web Workers",
+ protectionStateProtected: "Protegido",
+ protectionStateDegraded: "Degradado",
+ protectionStateNative: "No modificado",
+ protectionStateMixed: "Mixto",
+ protectionStatePending: "Confirmando…",
+ protectionStateRepaired: "Protección restaurada",
+ protectionStateBrowserEnforced: "Protegido por el navegador",
+ protectionStateUnrecoverable: "Protección fallida",
+ protectionStateNotApplicable: "No aplicable",
+ protectionStateCompatibility: "Problema de compatibilidad",
+ protectionSurfaceUnknown: (surface: string) => `Estado desconocido de ${surface}.`,
+ protectionPageMayNotWork: "Esta página puede no funcionar correctamente",
+ protectionServiceWorkerBlockTitle: "Los Service Workers están bloqueados",
+ protectionServiceWorkerBlockSummary: `${BRAND_DISPLAY_NAME} está bloqueando el registro de Service Worker en este sitio.`,
+ protectionServiceWorkerBlockDescription:
+ "El modo fuera de línea, las notificaciones push, la sincronización en segundo plano y otras funciones de Service Worker pueden no funcionar mientras el bloqueo esté activado.",
+ protectionSharedWorkerStrictTitle: "El modo estricto de Shared Worker está activado",
+ protectionSharedWorkerStrictSummary: `${BRAND_DISPLAY_NAME} bloquea cualquier Shared Worker cuya protección no pueda confirmar antes de iniciarlo.`,
+ protectionSharedWorkerStrictDescription:
+ "La sincronización entre pestañas, las conexiones compartidas y la colaboración en vivo pueden no funcionar mientras el modo estricto esté activado.",
+ notificationsTitle: "Notificaciones",
+ notificationsThisSite: "Este sitio",
+ notificationsExtension: BRAND_DISPLAY_NAME,
+ notificationsPreviousUpdates: "Actualizaciones anteriores",
+ notificationsDismissed: "Descartado",
+ notificationsResolved: "Resuelto",
+ notificationsNew: "Nuevo",
+ notificationsAcknowledged: "Leído",
+ notificationsStillActive: "Necesita atención",
+ notificationsEmpty: "No hay notificaciones activas.",
+ notificationsDismiss: "Descartar",
+ notificationsKeepStrictMode: "Mantener el modo estricto",
+ notificationsWhatThisAffects: "Lo que esto afecta",
+ notificationsBeforeYouContinue: "Antes de continuar",
+ notificationsChooseSharedWorkerMode:
+ "Elige cómo se ejecutan los Shared Workers en este sitio",
+ notificationsCspTitle: "El sitio ha bloqueado la protección de workers",
+ notificationsCspSummary: `${BRAND_DISPLAY_NAME} no ha podido proteger un worker en este sitio.`,
+ notificationsSharedWorkerTitle: "No se ha podido proteger un Shared Worker",
+ notificationsSharedWorkerSummary: `${BRAND_DISPLAY_NAME} no ha podido aplicar los valores simulados antes de que se iniciara un Shared Worker.`,
+ notificationsRelaxWorkerPolicy: "Permitir la simulación de workers",
+ notificationsAllowServiceWorkers: "Permitir Service Workers",
+ notificationsSharedWorkerNative: "Nativo",
+ notificationsSharedWorkerNativeDescription: `Ejecuta los Shared Workers con normalidad, sin la protección de ${BRAND_DISPLAY_NAME}. Ofrece la máxima compatibilidad, pero un worker puede leer los valores reales del navegador.`,
+ notificationsSharedWorkerSpoof: "Simular",
+ notificationsSharedWorkerSpoofDescription: `Intenta un método de protección alternativo para Shared Workers. Algunos workers aún pueden fallar al iniciarse.`,
+ notificationsUpdateTitle: `${BRAND_DISPLAY_NAME} fue actualizado`,
+ notificationsUpdateSummary: "Revisa los cambios en esta versión.",
+ notificationsVersionLabel: "Versión",
+ notificationsOpenLink: "Abrir enlace",
+ notificationsBadgeLabel: (count: number) =>
+ formatCount(count, "notificación no leída", "notificaciones no leídas"),
+ 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
+ ? [formatCount(protectedCount, "protegido", "protegidos")]
+ : []),
+ ...(degradedCount > 0
+ ? [formatCount(degradedCount, "degradado", "degradados")]
+ : []),
+ ...(counts.pending > 0 ? [`${counts.pending} confirmando`] : []),
+ ...(counts["native-by-policy"] > 0
+ ? [formatCount(counts["native-by-policy"], "no modificado", "no modificados")]
+ : []),
+ ...(counts["not-applicable"] > 0
+ ? [formatCount(counts["not-applicable"], "no aplicable", "no aplicables")]
+ : []),
+ ...(counts.unknown > 0
+ ? [formatCount(counts.unknown, "desconocido", "desconocidos")]
+ : []),
+ ].join(" · ");
+ },
+ protectionException: (_surface: string) =>
+ "Esta página puede no funcionar correctamente",
+ unsupportedTab: "Página restringida",
+ noLocationYet: "No hay ningún perfil activo",
+ noPresetAssigned: "No hay perfil asignado",
+ regularPageRequired: `${BRAND_DISPLAY_NAME} no puede acceder a esta página.`,
+ openXRay: `Ver la actividad de este sitio en ${BRAND_DIAGNOSTICS_NAME}`,
+ saveLabelCreate: "Crear",
+ saveLabelSave: "Guardar",
+ ruleConflictConfirm: (pattern: string) =>
+ `Ya existe una regla de dominio para «${pattern}». ¿Quieres sustituirla por esta configuración?`,
+ ruleConflictTitle: "¿Sustituir la regla de dominio?",
+ ruleConflictReplace: "Reemplazar",
+ popupDataUnavailable: "No se pudo cargar el estado de este sitio.",
+ retryLabel: "Reintentar",
+ dismissLabel: "Descartar",
+ mutationFailed: `${BRAND_DISPLAY_NAME} no ha podido completar la acción. Inténtalo de nuevo.`,
+ enableExtensionFailed: `No se ha podido activar ${BRAND_DISPLAY_NAME}.`,
+ trustSiteFailed: "No se ha podido añadir este sitio a Sitios de confianza.",
+ firefoxPermissionFailed:
+ "No se ha podido conceder el permiso userScripts de Firefox.",
+ powerControlExtension: BRAND_DISPLAY_NAME,
+ powerControlGlobalProtections: "Protecciones globales",
+ powerTargetLoading: "Comprobando los ajustes de este sitio…",
+ powerTargetGlobal: `${BRAND_DISPLAY_NAME} está desactivado en todos los sitios.`,
+ powerTargetGlobalProtectionsDisabled:
+ "Actívalos en Configuración para usar tus reglas y ajustes guardados.",
+ powerTargetUnsupported: `${BRAND_DISPLAY_NAME} no puede acceder a esta página.`,
+ powerTargetTrustedSite: `${BRAND_DISPLAY_NAME} está desactivado porque este sitio coincide con Sitios de confianza.`,
+ powerTargetSiteRule: "Activa o desactiva esta regla de dominio.",
+ powerTargetContainer: "Activa o desactiva esta asignación de contenedor de Firefox.",
+ powerTargetContainerSetup: `Configura ${BRAND_DISPLAY_NAME} para este contenedor de Firefox.`,
+ powerTargetContainerDefaultRule:
+ "Este contenedor de Firefox usa la Regla predeterminada.",
+ powerTargetDefaultRule:
+ "Controla los sitios sin una regla de dominio ni una entrada en Sitios de confianza.",
+ powerAriaLoading: "Verificando la configuración de este sitio",
+ powerAriaGlobalOff: `${BRAND_DISPLAY_NAME} está desactivado en todos los sitios`,
+ powerAriaGlobalProtectionsDisabled:
+ "Las protecciones globales están deshabilitadas. Actívalas en Configuración.",
+ powerAriaRestricted: `${BRAND_DISPLAY_NAME} no puede acceder a esta página`,
+ powerAriaTrustedSite: `Activar ${BRAND_DISPLAY_NAME} para este sitio`,
+ powerAriaDomainRuleOn: "Activar esta regla de dominio",
+ powerAriaDomainRuleOff: "Desactivar esta regla de dominio",
+ powerAriaContainerOn: "Activar esta asignación de contenedor de Firefox",
+ powerAriaContainerOff: "Desactivar esta asignación de contenedor de Firefox",
+ powerAriaContainerSetup: `Configurar ${BRAND_DISPLAY_NAME} para este contenedor de Firefox`,
+ powerAriaDefaultRuleOn: "Activar la regla predeterminada",
+ powerAriaDefaultRuleOff: "Desactivar la regla predeterminada",
+
+ sheetTitle: "Regla de dominio",
+ currentProfileLabel: "Perfil regional",
+ noPresetLabel: "Sin perfil",
+ inheritedDefaultRuleProfileLabel: "Usar el perfil de la Regla predeterminada",
+ inheritedContainerProfileLabel: "Usar perfil de contenedor de Firefox",
+ inheritedDomainRuleProfileLabel: "Usar perfil de regla de dominio coincidente",
+ ruleTypeLabel: "Se aplica a",
+ ruleTypeExact: "Host exacto",
+ ruleTypeSuffix: "Host + subdominios",
+ workerHandlingLabel: "Dedicated y Shared Workers",
+ workerHandlingHint:
+ "Elige un modo para esta regla. Heredar usa la configuración global. Nativo ejecuta los workers con los valores reales del navegador. Simular intenta protegerlos. Estricto bloquea un worker cuando no puede confirmar la protección antes de iniciarlo.",
+ workerHandlingInherit: "Heredar",
+ workerHandlingNative: "Nativo",
+ workerHandlingSpoof: "Simular",
+ workerHandlingStrict: "Estricto",
+ viewXRay: BRAND_DIAGNOSTICS_NAME,
+ deleteButtonLabel: "Eliminar",
+ sheetLead:
+ "Elige dónde se aplica esta regla de dominio y qué perfil regional utiliza. Al cambiar la configuración se vuelve a cargar la página.",
+ cleanupSheetTitle: "Nueva identidad",
+ cleanupConfirmTitle: "¿Crear nueva identidad?",
+ cleanupPlanLoading: "Comprobando qué puede borrar este navegador…",
+ cleanupPlanDescription: (available: string, unavailable: string) =>
+ unavailable
+ ? `${BRAND_DISPLAY_NAME} borrará ${available}. Este navegador no puede borrar ${unavailable} aquí.`
+ : `${BRAND_DISPLAY_NAME} borrará ${available}.`,
+ cleanupResultTitle: "Nueva identidad creada",
+ cleanupResultComplete: `Se han borrado los datos del navegador disponibles para ${BRAND_DISPLAY_NAME}.`,
+ cleanupResultPartial:
+ "Algunos datos del navegador fueron borrados, pero este navegador no puso a disposición todos los elementos.",
+ cleanupResultFailed: `La identidad cambió, pero ${BRAND_DISPLAY_NAME} no pudo borrar los datos asociados del navegador. Puedes intentarlo de nuevo sin cambiar la identidad.`,
+ cleanupSurfaceCookies: "cookies",
+ cleanupSurfaceLocalStorage: "almacenamiento local",
+ cleanupSurfaceIndexedDb: "IndexedDB",
+ cleanupSurfaceCacheStorage: "almacenamiento en caché",
+ cleanupSurfaceServiceWorkers: "Service Workers",
+ cleanupSurfacePageStorage: "almacenamiento en páginas abiertas",
+ cleanupStatusCleaned: "Borrado",
+ cleanupStatusSkipped: "No disponible",
+ cleanupStatusFailed: "Fallido",
+ cleanupConfirmLabel: "Crear nueva identidad",
+ deleteConfirmTitle: "¿Eliminar regla de dominio?",
+ deleteConfirmDescription: (pattern: string) =>
+ `Esto elimina la regla de dominio guardada para “${pattern}”.`,
+ cleanDomainAriaLabel: "Crear una nueva identidad para este sitio",
+ cleanDomainLabel: "Nueva identidad",
+ cleanDomainDefaultRuleDisabled: "No disponible para la regla predeterminada",
+ cleanDomainProductOffDisabled: `No disponible mientras ${BRAND_DISPLAY_NAME} está desactivado para este sitio`,
+ cleanDomainContainerSetupDisabled: "Configura este contenedor de Firefox primero",
+ cleanDomainPageDisabled: "No disponible en esta página",
+ cleanDomainLoadingDisabled: "Disponible después de que el sitio termine de cargar",
+ cleanDomainAvailabilityHint:
+ "La nueva identidad funciona con una regla de dominio o con un contenedor de Firefox configurado.",
+ openSettingsAriaLabel: "Abrir Configuración",
+ openFullRuleSettings: "Abrir en Reglas de dominio",
+ settingsLabel: "Configuración",
+ closeSheetAriaLabel: "Cerrar regla de dominio",
+ trustedSiteCta: "Editar sitio de confianza",
+ disableOnSiteCta: "Agregar a sitios de confianza",
+ enableTrustCta: "Agregar a sitios de confianza",
+ extensionOffCardAction: `Activar ${BRAND_DISPLAY_NAME}`,
+ surfaceGeolocation: "Geolocalización",
+ surfaceTimeLocale: "Hora y configuración regional",
+ surfaceNavigator: "Navigator",
+ surfaceScreen: "Pantalla",
+ surfaceClientHints: "Client Hints",
+ surfaceBattery: "Batería",
+ surfaceCanvas: "Canvas",
+ surfaceWebGL: "WebGL",
+ surfaceAudio: "Audio",
+ surfaceWebRTC: "WebRTC",
+ surfaceWorker: "Dedicated Workers",
+ surfaceServiceWorker: "Service Workers",
+ surfaceSharedWorker: "Shared Workers",
+ addExactOverrideCta: "Agregar regla del sitio",
+ containerSetupCta: "Configurar contenedor",
+ globalFallbackRuleCta: "Editar regla predeterminada",
+ editDomainRuleLabel: "Editar regla de dominio",
+ editContainerLabel: "Editar contenedor",
+ firefoxFirstInlinePermissionTitle:
+ "Mejorar la simulación en la primera carga en Firefox",
+ firefoxFirstInlinePermissionDescription: `Firefox puede exponer tu hora y configuración regional reales al primer script de la página. Otorga el permiso opcional de userScripts para que ${BRAND_DISPLAY_NAME} pueda comenzar a simular antes.`,
+ firefoxFirstInlinePermissionEnableLabel: "Conceder permiso",
+ detailsAbout: (label: string) => `Detalles sobre ${label}`,
+ relaxCspHint: `Elimina las cabeceras de la Política de Seguridad de Contenido del sitio cuando impiden que ${BRAND_DISPLAY_NAME} proteja sus workers. Reduce la protección contra scripts maliciosos; actívalo únicamente en un sitio de confianza.`,
+ advancedSectionTitle: "Avanzado",
+ suggestionWorkerCspDescription:
+ "La Política de Seguridad de Contenido (CSP) del sitio ha bloqueado la protección de workers. Permitirla relajará esa política y reducirá la protección contra scripts inyectados. Continúa solo si confías en este sitio.",
+ suggestionSharedWorkerInjectionDescription: `${BRAND_DISPLAY_NAME} no ha podido proteger este Shared Worker antes de iniciarlo. Elige si en este sitio importa más la compatibilidad o la protección.`,
+} as const;
diff --git a/src/ui/i18n/es-sections/rules.ts b/src/ui/i18n/es-sections/rules.ts
new file mode 100644
index 0000000..f22dce2
--- /dev/null
+++ b/src/ui/i18n/es-sections/rules.ts
@@ -0,0 +1,233 @@
+import { BRAND_DISPLAY_NAME } from "@/shared/brand";
+
+const PRODUCT_NOT_LOWER = `qué ${BRAND_DISPLAY_NAME} no es`;
+
+export const rules = {
+ title: "Reglas de dominio",
+ hint: "Las reglas de dominio asignan patrones de host a perfiles regionales.",
+ addButton: "Agregar regla",
+ filterLabel: "Filtrar reglas",
+ filterPlaceholder: "Buscar perfiles, patrones de dominio o advertencias",
+ locationFilterLabel: "Filtrar por perfil",
+ locationFilterPlaceholder: "Todos los perfiles",
+ assignLocationLabel: "Asignar perfil",
+ tableHeadRule: "Regla",
+ tableHeadProfile: "Perfil",
+ tableHeadActions: "Acciones",
+ selectAllAriaLabel: "Seleccionar todas las reglas visibles",
+ selectMenuAriaLabel: "Abrir menú de selección de reglas",
+ selectRuleAriaLabel: (pattern: string) => `Seleccionar regla ${pattern}`,
+ editRuleAriaLabel: (pattern: string) => `Editar regla ${pattern}`,
+ editRuleTitle: "Editar regla",
+ deleteRuleAriaLabel: (pattern: string) => `Eliminar regla ${pattern}`,
+ deleteRuleTitle: "Eliminar regla",
+ inactiveBadge: "inactivo",
+ selectionMenuAllVisible: "Todas las visibles",
+ selectionMenuAll: "Todas",
+ selectionMenuNone: "Ninguna",
+ selectionMenuActive: "Solo activas",
+ selectionMenuInactive: "Solo inactivas",
+ noRulesFiltered: "Ninguna regla coincide con el filtro actual.",
+ noRulesEmpty: `Aún no hay reglas de dominio. Añade una para elegir cómo gestiona ${BRAND_DISPLAY_NAME} los sitios coincidentes.`,
+ copyLinkLabel: "reglas de dominio",
+ copyLinkHelpLabel: "ayuda de reglas de dominio",
+ copyLinkInspectorLabel: "inspector de nombres de host",
+ copyLinkRuleAriaLabel: (pattern: string) => `Copiar enlace de la regla ${pattern}`,
+
+ help: {
+ title: "Reglas de dominio",
+ body1:
+ "Usa example.com para un solo host exacto. Usa *example.com para ese host y cualquier subdominio, como www.example.com. Usa *.example.com solo para subdominios.",
+ body2: `El patrón de coincidencia más específico gana. Si dos reglas se superponen y apuntan a diferentes perfiles regionales, ${BRAND_DISPLAY_NAME} advierte al respecto.`,
+ },
+
+ globalFallback: {
+ title: "Regla predeterminada",
+ description: `Define las protecciones predeterminadas y el perfil opcional que ${BRAND_DISPLAY_NAME} usa cuando no tiene prioridad ninguna opción más específica.`,
+ copyLinkLabel: "Regla predeterminada",
+ overridesBadge: (count: number) =>
+ `${count} configuración personalizada${count === 1 ? "" : "s"}`,
+ openInRules: "Editar en Reglas de dominio",
+ editAriaLabel: "Editar regla predeterminada",
+ editTitle: "Editar regla predeterminada",
+ noPresetLabel: "No hay perfil asignado",
+ setupHint: "Aún no hay ningún perfil ni protección personalizada.",
+ tableHint: "Configuración predeterminada cuando no se aplica nada más específico.",
+ dialog: {
+ title: "Regla predeterminada",
+ description:
+ "Define las protecciones y el perfil opcional que usará la Regla predeterminada cuando no tenga prioridad ninguna opción más específica.",
+ identityDescription:
+ "La Regla predeterminada mantiene su propia identidad simulada y fija. No se puede cambiar manualmente.",
+ enabledLabel: "Activada",
+ enabledHint:
+ "Cuando está desactivada, esta regla no se aplica. Sus opciones permanecen guardadas.",
+ enabledAriaLabel: "Activar o desactivar la Regla predeterminada",
+ locationProfileLabel: "Perfil regional",
+ locationProfileHint:
+ "Elige el perfil que debe usar la Regla predeterminada. Déjalo sin asignar para usar únicamente las protecciones indicadas a continuación.",
+ locationProfileWarningPrefix: `${BRAND_DISPLAY_NAME} no reemplaza VPN, proxy o herramientas DNS. `,
+ locationProfileWarningLinkLabel: `Ver ${PRODUCT_NOT_LOWER}`,
+ locationProfileWarningSuffix: ".",
+ locationLabel: "Perfil",
+ locationPlaceholder: "Elegir perfil",
+ submit: "Guardar regla",
+ },
+ },
+
+ inspector: {
+ title: "Inspector de nombres de host",
+ hint: `Comprueba cómo ${BRAND_DISPLAY_NAME} tratará un nombre de host antes de guardar los cambios. Indica si coincide con una regla de dominio o un sitio de confianza, y qué perfil regional se aplicaría.`,
+ hostnameLabel: "Nombre de host",
+ hostnameHint:
+ "Usa el host exacto que quieras inspeccionar, por ejemplo shop.example.com.",
+ hostnamePlaceholder: "p. ej., shop.example.com",
+ noMatchTitle:
+ "Ninguna regla de dominio ni sitio de confianza coincide con este nombre de host",
+ noMatchDescription:
+ "No se aplica ninguna regla guardada y la Regla predeterminada está desactivada o sin configurar.",
+ trustedSiteWinsTitle:
+ "Este nombre de host está desactivado mediante Sitios de confianza",
+ trustedSiteWinsDescription: `Coincide con tu lista de Sitios de confianza, por lo que ${BRAND_DISPLAY_NAME} permanecerá desactivado aquí hasta que elimines o desactives esa entrada.`,
+ trustedSiteOverridesRuleTitle:
+ "Sitios de confianza anula una regla de dominio coincidente",
+ trustedSiteOverridesRuleDescription: `Este nombre de host coincide tanto con un sitio de confianza como con una regla de dominio. Sitios de confianza tiene prioridad, por lo que ${BRAND_DISPLAY_NAME} permanece desactivado y se ignora la regla indicada a continuación.`,
+ fallbackWinsTitle: "Aquí se aplica la Regla predeterminada",
+ fallbackWinsDescription: `Ninguna regla de dominio ni sitio de confianza coincide con este nombre de host, por lo que ${BRAND_DISPLAY_NAME} recurriría aquí a la Regla predeterminada.`,
+ ruleMatchTitle: (locationLabel: string) =>
+ `${locationLabel} es el perfil activo aquí`,
+ ruleMatchDescription: `Este nombre de host coincide con la regla de dominio indicada a continuación, por lo que ${BRAND_DISPLAY_NAME} usaría este perfil en el sitio.`,
+ hostnameDetailLabel: "Nombre de host",
+ trustedSiteDetailLabel: "Sitio de confianza",
+ ruleDetailLabel: "Regla",
+ defaultRuleDetailLabel: "Regla predeterminada",
+ ignoredRuleDetailLabel: "Regla ignorada",
+ profileDetailLabel: "Perfil regional",
+ geolocationDetailLabel: "Geolocalización",
+ localeDetailLabel: "Configuración regional",
+ timeZoneDetailLabel: "Zona horaria",
+ geolocationOn: "Activado",
+ geolocationOff: "Desactivado",
+ },
+
+ dialog: {
+ titleAdd: "Agregar regla",
+ titleEdit: "Editar regla",
+ description:
+ "Elige dónde se aplica esta regla y decide si debe usar un perfil, protecciones personalizadas o ambos.",
+ patternLabel: "Patrón",
+ patternInfo:
+ "Usa example.com para un solo host exacto. Usa *example.com para ese host y cualquier subdominio, como www.example.com. Usa *.example.com solo para subdominios.",
+ patternInfoAriaLabel: "Aprende cómo funcionan los patrones de regla",
+ patternPlaceholder: "Introduce un patrón de dominio",
+ locationLabel: "Perfil",
+ locationProfileLabel: "Perfil regional",
+ locationProfileHint: `Elige el perfil que debe usar esta regla. Si no asignas ninguno, ${BRAND_DISPLAY_NAME} usa el siguiente perfil disponible y conserva las protecciones de la regla.`,
+ bulkAssignSearchPlaceholder: "Buscar perfiles…",
+ enabledLabel: "Activada",
+ enabledHint:
+ "Cuando está desactivada, esta regla no se aplica. Sus opciones permanecen guardadas.",
+ enabledAriaLabel: (pattern: string) => `Activar o desactivar la regla ${pattern}`,
+ advancedModal: {
+ trigger: "Avanzado",
+ title: (pattern: string) => `Configuración avanzada de ${pattern}`,
+ description:
+ "Estos cambios permanecen en el borrador actual hasta que guardes la regla.",
+ confirm: "Aceptar",
+ patternFallback: "esta regla",
+ },
+ relaxCspLabel: "Relajar la CSP para proteger workers",
+ relaxCspHint: `Elimina las cabeceras de la Política de Seguridad de Contenido del sitio cuando impiden que ${BRAND_DISPLAY_NAME} proteja sus workers.`,
+ relaxCspRiskHint:
+ "Advertencia de seguridad: esto facilita la ejecución de scripts maliciosos en el sitio. Actívalo únicamente en un sitio de confianza y cuando la protección de workers no funcione de otro modo.",
+ relaxCspAriaLabel: (pattern: string) =>
+ `Alternar relajación de CSP para la regla ${pattern}`,
+ surfaceOverrides: {
+ title: "Configuración de protección",
+ description:
+ "Elige protecciones distintas para esta regla. Deja una opción en Heredar para usar la configuración global.",
+ stateOn: "Activado",
+ stateInherit: "Heredar",
+ stateOff: "Desactivado",
+ stateNative: "Nativo",
+ stateSpoof: "Simular",
+ stateStrict: "Estricto",
+ stateBlock: "Bloquear",
+ stateAllow: "Permitir",
+ helpAriaLabel: (label: string) => `Aprende qué ${label} controla`,
+ geolocation: {
+ label: "Geolocalización",
+ info: "Controla la API de geolocalización. Desactívala cuando un sitio deba leer la ubicación real del navegador o actívala cuando esta regla deba simular las consultas de ubicación.",
+ },
+ timeLocale: {
+ label: "Hora y configuración regional",
+ info: "Controla Date, Intl, navigator.language, navigator.languages y las cabeceras de idioma para que los sitios vean la región del perfil activo.",
+ },
+ canvas: {
+ label: "Canvas",
+ info: "Controla la salida de imágenes ocultas que los sitios usan para la huella digital de canvas.",
+ },
+ webGL: {
+ label: "WebGL",
+ info: "Controla detalles gráficos como el renderer, pistas de GPU y datos relacionados con la huella digital de WebGL.",
+ },
+ audio: {
+ label: "Audio",
+ info: "Controla la salida de AudioContext que los sitios pueden medir para la huella digital de audio.",
+ },
+ navigator: {
+ label: "Navigator",
+ info: "Controla campos de identidad del navegador como plataforma, pistas de hardware y otras propiedades de navigator.",
+ },
+ screen: {
+ label: "Pantalla",
+ info: "Controla el tamaño de pantalla, la proporción de píxeles y detalles de visualización relacionados.",
+ },
+ clientHints: {
+ label: "Client Hints",
+ info: "Controla los datos del navegador y del dispositivo compartidos mediante cabeceras y API de Client Hints.",
+ },
+ battery: {
+ label: "Batería",
+ info: "Controla si el sitio recibe un perfil de batería llena y en carga en lugar del estado real de la batería del dispositivo.",
+ },
+ webRTC: {
+ label: "WebRTC",
+ info: "Controla la protección de manejo de IP de WebRTC que puede reducir las filtraciones de IP locales y públicas.",
+ },
+ serviceWorker: {
+ label: "Service Workers",
+ info: "Controla si este sitio puede registrar Service Workers, que pueden ejecutarse en segundo plano y mantener datos a largo plazo. Bloquear puede romper las PWAs, el modo offline, las notificaciones push y la sincronización en segundo plano; Permitir deja que el navegador maneje el registro normalmente; Heredar sigue la configuración global.",
+ },
+ sharedWorker: {
+ label: "Dedicated y Shared Workers",
+ info: "Sustituye el modo de los Dedicated y Shared Workers para esta regla. Nativo no modifica los workers, Simular intenta aplicar valores simulados y Estricto bloquea un worker cuando no puede confirmar la protección antes de iniciarlo.",
+ },
+ },
+ identity: {
+ sectionTitle: "Identidad",
+ sectionDescription:
+ "Esta regla conserva su propia identidad simulada. Rótala únicamente cuando quieras una huella digital nueva y borrar el estado de los sitios asociados a la regla.",
+ actionDescription:
+ "Borra los datos relacionados del sitio y comienza esta regla con una identidad nueva.",
+ actionLabel: "Nueva identidad",
+ confirmTitle: (pattern: string) => `Nueva identidad para ${pattern}?`,
+ confirmDescription:
+ "Esto borra cookies, almacenamiento, Service Workers y cachés de los sitios vinculados a esta regla. Luego crea una nueva identidad de simulación.",
+ confirmDomainsLabel: `${BRAND_DISPLAY_NAME} borrará datos del navegador para estos dominios:`,
+ confirmNoDomains: `Aún no se ha registrado ningún dato del navegador para esta regla. ${BRAND_DISPLAY_NAME} todavía creará una nueva identidad de simulación.`,
+ confirmLabel: "Crear nueva identidad",
+ rotateSuccess: "Se ha guardado una nueva identidad para la regla.",
+ rotateError: "Falló la creación de una nueva identidad de regla.",
+ },
+ submitAdd: "Agregar regla",
+ submitEdit: "Guardar",
+ duplicateAlertTitle: "¿Sobrescribir la regla existente?",
+ duplicateAlertDescription: (pattern: string) =>
+ `Ya existe una regla para «${pattern}». ¿Quieres sustituirla por esta configuración?`,
+ duplicateAlertConfirm: "Sobrescribir",
+ duplicateAlertClose: "No",
+ trustedSiteOverrideWarning: (pattern: string) =>
+ `Este dominio coincide con la entrada «${pattern}» de Sitios de confianza. ${BRAND_DISPLAY_NAME} permanecerá desactivado aquí independientemente de estas opciones.`,
+ },
+} as const;
diff --git a/src/ui/i18n/es-sections/shared-worker.ts b/src/ui/i18n/es-sections/shared-worker.ts
new file mode 100644
index 0000000..e7908d8
--- /dev/null
+++ b/src/ui/i18n/es-sections/shared-worker.ts
@@ -0,0 +1,31 @@
+import { BRAND_DISPLAY_NAME } from "@/shared/brand";
+
+export const sharedWorkerModeCopy = {
+ native: {
+ label: "Nativo",
+ description: `Ejecuta los Shared Workers con normalidad, sin la protección de ${BRAND_DISPLAY_NAME}. Ofrece la máxima compatibilidad, pero un worker puede leer los valores reales de tu navegador.`,
+ },
+ spoof: {
+ label: "Simular",
+ description: `Intenta un método de protección alternativo para Shared Workers. Algunos workers aún pueden fallar al iniciarse.`,
+ },
+ strict: {
+ label: "Estricto",
+ description: `Bloquea un Shared Worker salvo que ${BRAND_DISPLAY_NAME} pueda confirmar la simulación antes de que se inicie. Así se evita que un Shared Worker sin proteger vea los valores reales del navegador, pero es posible que las funciones que dependen de él no funcionen.`,
+ },
+} as const;
+
+export const workerHandlingModeCopy = {
+ native: {
+ label: "Nativo",
+ description: `Ejecuta los Dedicated y Shared Workers con normalidad, sin la protección de ${BRAND_DISPLAY_NAME}. Ofrece la máxima compatibilidad, pero los workers pueden leer los valores reales de tu navegador.`,
+ },
+ spoof: {
+ label: "Simular",
+ description: `Intenta aplicar los valores simulados de ${BRAND_DISPLAY_NAME} antes de que se inicien los workers. Si no puede aplicar la protección, un worker podría usar los valores reales del navegador o no iniciarse.`,
+ },
+ strict: {
+ label: "Estricto",
+ description: `Bloquea un worker antes de que se inicie cuando ${BRAND_DISPLAY_NAME} determina que no puede confirmar la simulación. Si el fallo ocurre después del inicio, ${BRAND_DISPLAY_NAME} muestra una notificación. Es posible que las funciones que dependen de estos workers no funcionen.`,
+ },
+} as const;
diff --git a/src/ui/i18n/es-sections/sidebar.ts b/src/ui/i18n/es-sections/sidebar.ts
new file mode 100644
index 0000000..b4f4b4c
--- /dev/null
+++ b/src/ui/i18n/es-sections/sidebar.ts
@@ -0,0 +1,175 @@
+import { BRAND_DIAGNOSTICS_NAME, BRAND_DISPLAY_NAME } from "@/shared/brand";
+
+export const sidebar = {
+ xRayTitle: BRAND_DIAGNOSTICS_NAME,
+ openSettings: "Abrir Configuración",
+ openRule: "Editar regla de dominio",
+ openLocation: "Editar perfil",
+ openLogs: "Ver registros",
+ region: "Región",
+
+ errorPrefix: `${BRAND_DISPLAY_NAME} ha detectado un problema:`,
+ loading: "Comprobando esta página…",
+ unsupportedTab:
+ "No se pueden simular datos en las páginas internas del navegador. Abre un sitio web para comprobar la cobertura.",
+ noSpoofing: `${BRAND_DISPLAY_NAME} no está activo en esta página. Abre la ventana emergente para configurar una regla para este sitio.`,
+ trustedSite:
+ "Has marcado este sitio como de confianza. La simulación está desactivada.",
+ trustedSiteHint: "Abre Sitios de confianza en Configuración para cambiarlo.",
+
+ surfaces: {
+ title: "Superficies simuladas",
+ timezone: "Zona horaria",
+ language: "Idioma",
+ languages: "Idiomas",
+ acceptLanguage: "Accept-Language",
+ geolocation: "Geolocalización",
+ accuracyNoise: "Precisión / Ruido",
+ platform: "Plataforma",
+ userAgent: "Agente de usuario",
+ cpuRam: "CPU / RAM",
+ screen: "Pantalla",
+ noise: "Ruido",
+ canvas: "Canvas",
+ audio: "Audio",
+ gpu: "GPU",
+ sharedWorker: "Shared Worker",
+ sharedWorkerNativeCompatibility: "Nativo para compatibilidad",
+ sharedWorkerBlobSpoofing: "Simulado mediante envoltorio de blob",
+ sharedWorkerResponseRewriteSpoofing: "Simulado mediante reescritura de respuesta",
+ sharedWorkerResponseRewriteCacheSensitive:
+ "Reescritura de respaldo; sensible a la caché",
+ sharedWorkerModuleUnsupported: "Reescritura de módulo no compatible",
+ sharedWorkerIdentityConflict: "Conflicto de identidad",
+ sharedWorkerRewriteUnavailable: "Reescritura de respuesta no disponible",
+ sharedWorkerBlockedStrict: "Bloqueado por modo estricto",
+ sharedWorkerStrictRewriteRequired:
+ "El modo estricto requiere reescritura confirmada",
+ sharedWorkerStrictBlockedCacheSensitive:
+ "Bloqueado; la reescritura de respaldo era sensible a la caché",
+ sharedWorkerCompatibilityHelp:
+ "El modo de Shared Worker sigue la opción seleccionada: Nativo, Simular o Estricto.",
+ gpuDebug: "Depuración de GPU",
+ clientHintsPlatform: "plataforma",
+ clientHintsBrands: "marcas",
+ fingerprint: "Huella digital",
+ off: "Desactivado",
+ yes: "Activo",
+ blocked: "Bloqueado",
+ m: "m",
+ GB: "GB",
+ expand: "Mostrar valor completo",
+ collapse: "Contraer",
+ },
+
+ accessed: {
+ title: "Actividad de la página",
+ description: "Categorías consultadas por la página desde la última navegación.",
+ accessed: "Consultado",
+ blocked: "Bloqueado",
+ armed: "Esperando",
+ connecting: "Conectando…",
+ failed: "Fallido",
+ off: "Desactivado",
+ categories: {
+ geolocation: "Geolocalización",
+ timeLocale: "Hora y configuración regional",
+ canvas: "Canvas",
+ webGL: "WebGL",
+ audio: "Audio",
+ navigator: "Navigator",
+ screen: "Pantalla",
+ clientHints: "Client Hints",
+ battery: "Batería",
+ 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: "Información del renderizador GPU",
+ 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: "Por qué se aplica esta configuración",
+ sourcesShort: {
+ trustedSite: "De confianza",
+ domainRule: "Regla de dominio",
+ container: "Contenedor",
+ fallback: "Predeterminado",
+ none: "Ninguna",
+ },
+ noExplanation: "No hay explicación de la regla disponible.",
+ },
+} as const;
diff --git a/src/ui/i18n/es-sections/trusted-sites.ts b/src/ui/i18n/es-sections/trusted-sites.ts
new file mode 100644
index 0000000..9485a2d
--- /dev/null
+++ b/src/ui/i18n/es-sections/trusted-sites.ts
@@ -0,0 +1,53 @@
+import { BRAND_DISPLAY_NAME } from "@/shared/brand";
+
+export const trustedSites = {
+ title: "Sitios de confianza",
+ hint: `Sitios en los que ${BRAND_DISPLAY_NAME} permanece desactivado aunque se aplicara una regla, una asignación de contenedor o la Regla predeterminada.`,
+ patternLabel: "Patrón de dominio",
+ patternPlaceholder: "Introduce un patrón de dominio",
+ filterLabel: "Filtrar sitios de confianza",
+ filterPlaceholder: "Buscar dominio",
+ addButton: "Agregar sitio web",
+ copyLinkLabel: "sitios de confianza",
+ copyLinkHelpLabel: "ayuda de sitios de confianza",
+ copyLinkInspectorLabel: "inspector de nombres de host",
+ tableHeadPattern: "Dominio",
+ tableHeadStatus: "Estado",
+ tableHeadActions: "Acciones",
+ empty: "Aún no hay sitios de confianza.",
+ filteredEmpty: "Ningún sitio de confianza coincide con el filtro actual.",
+ inactiveBadge: "inactivo",
+ toggleSiteAriaLabel: (pattern: string, enabled: boolean) =>
+ `${enabled ? "Desactivar" : "Activar"} el sitio de confianza ${pattern}`,
+ deleteSiteAriaLabel: (pattern: string) => `Eliminar el sitio de confianza ${pattern}`,
+ deleteSiteTitle: "Eliminar sitio de confianza",
+ duplicateWarning: "Ese sitio de confianza ya existe.",
+ patternRequired: "Introduce un patrón de dominio.",
+ saved: "Sitio de confianza guardado.",
+ updated: "Sitio de confianza actualizado.",
+ deleted: "Sitio de confianza eliminado.",
+ help: {
+ title: "Cuándo usar Sitios de confianza",
+ body1:
+ "Usa Sitios de confianza en dominios donde la simulación cause problemas, como procesos bancarios, pagos o recuperación de cuentas que puedan considerar sospechosos los cambios del navegador.",
+ body2: `Sitios de confianza tiene prioridad sobre las reglas de dominio, las asignaciones de contenedores de Firefox y la Regla predeterminada. Usa example.com para un host exacto, *example.com para ese host y sus subdominios, o *.example.com solo para los subdominios.`,
+ },
+ rulesCta: {
+ title: "Protección en otros sitios",
+ activeRulesOnly: (count: number) =>
+ `${count} ${count === 1 ? "regla de dominio activada se aplica" : "reglas de dominio activadas se aplican"} fuera de Sitios de confianza. Abre Reglas de dominio para revisar dónde están activas.`,
+ activeRulesWithDefault: (count: number) =>
+ `${count} ${count === 1 ? "regla de dominio activada se aplica" : "reglas de dominio activadas se aplican"} fuera de Sitios de confianza. La Regla predeterminada también cubre otros sitios que no coinciden.`,
+ defaultRuleOnly: `La Regla predeterminada sigue aplicándose a los sitios que no coinciden. Sitios de confianza solo desactiva ${BRAND_DISPLAY_NAME} en los hosts coincidentes.`,
+ openRules: "Abrir Reglas de dominio",
+ openDefaultRule: "Abrir regla predeterminada",
+ },
+ dialog: {
+ title: "Agregar sitio web",
+ description: `Mantén ${BRAND_DISPLAY_NAME} desactivado en las páginas coincidentes cuando un sitio funcione mejor con el estado normal del navegador.`,
+ patternInfo:
+ "Usa example.com para un solo host exacto. Usa *example.com para ese host y cualquier subdominio, como www.example.com. Usa *.example.com solo para subdominios.",
+ patternInfoAriaLabel: "Cómo funcionan los patrones de Sitios de confianza",
+ submit: "Agregar sitio web",
+ },
+} as const;
diff --git a/src/ui/i18n/es-sections/welcome.ts b/src/ui/i18n/es-sections/welcome.ts
new file mode 100644
index 0000000..d479101
--- /dev/null
+++ b/src/ui/i18n/es-sections/welcome.ts
@@ -0,0 +1,140 @@
+import { BRAND_DISPLAY_NAME } from "@/shared/brand";
+import { externalMapConsentCopy } from "@/ui/i18n/es-sections/osm";
+
+const WELCOME_PRODUCT = `Bienvenido a ${BRAND_DISPLAY_NAME}`;
+
+export const welcome = {
+ title: WELCOME_PRODUCT,
+ loading: "Cargando configuración…",
+ progressLabel: "Progreso de configuración",
+ previous: "Anterior",
+ next: "Siguiente",
+ privacyPolicy: "Política de privacidad",
+ privacyDialog: {
+ title: "Política de privacidad",
+ description: `Cómo gestiona ${BRAND_DISPLAY_NAME} la configuración, los diagnósticos y las solicitudes de red.`,
+ close: "Cerrar política de privacidad",
+ },
+ saving: "Guardando…",
+ unexpectedError: "Algo salió mal",
+ importSuccess: "Configuración importada",
+ importError: "No se pudo importar la configuración",
+ importParseError: "No se pudo leer el archivo de configuración",
+ steps: {
+ welcome: {
+ title: "Gracias por instalar esta extensión",
+ description:
+ "Elige qué verán los sitios en lugar de exponer tu ubicación real y la identidad de tu navegador. Esta breve guía opcional explica las primeras decisiones —todas reversibles— y las guarda en la misma Configuración que podrás revisar en cualquier momento. ¿Ya tienes una configuración que quieras reutilizar?",
+ importInline: "Importar una configuración",
+ advancedTitle: "Sé lo que quiero",
+ advancedDescription:
+ "Omite la guía y abre Configuración con los valores predeterminados. No se añadirán perfiles regionales ni reglas de dominio; la Regla predeterminada permanecerá desactivada y las protecciones del navegador estarán disponibles para las reglas que crees más adelante.",
+ guidedTitle: "Ir a la configuración",
+ guidedDescription:
+ "Toma las primeras decisiones paso a paso. La guía limita las opciones, explica sus implicaciones y guarda todo en las mismas pantallas de Configuración que podrás editar después.",
+ advancedCta: "Omitir configuración",
+ guidedCta: "Iniciar configuración",
+ },
+ privacy: {
+ title: "Tu navegación se mantiene local",
+ description: `${BRAND_DISPLAY_NAME} guarda las reglas y los perfiles en tu dispositivo. Este paso solo se refiere a los servicios externos de mapas: actívalos si quieres usar la búsqueda y las vistas previas durante la configuración, o déjalos desactivados e introduce las coordenadas manualmente.`,
+ descriptionBeforePolicy: `${BRAND_DISPLAY_NAME} guarda las reglas y los perfiles en tu dispositivo. La búsqueda y las vistas previas del mapa solo usan servicios externos después de que los autorices. Lee la`,
+ policyLink: "política de privacidad",
+ descriptionAfterPolicy:
+ " antes de decidir si esas solicitudes se ajustan a tu configuración.",
+ consentTitle: externalMapConsentCopy.title,
+ consentDescription: externalMapConsentCopy.description,
+ },
+ presets: {
+ title: "Agregar perfiles regionales",
+ description:
+ "Los perfiles regionales reúnen ubicaciones con sus idiomas y zonas horarias correspondientes, de modo que los sitios vean un conjunto de datos regionales coherente. Selecciona únicamente los lugares que quieras incluir al principio. El asistente importará los perfiles elegidos al finalizar y podrás editarlos o eliminarlos más adelante.",
+ selectAll: "Seleccionar todo",
+ selectNone: "Borrar todo",
+ selectedCount: (count: number) =>
+ `${count} ${count === 1 ? "seleccionado" : "seleccionados"}`,
+ },
+ scope: {
+ title: "Elegir dónde comienza la protección",
+ description:
+ "La Regla predeterminada se aplica cuando no tiene prioridad ninguna opción más específica. Déjala desactivada para comenzar de forma más discreta o actívala para proteger los sitios que no coincidan con otras reglas.",
+ defaultRuleDescription:
+ "Si asignas un perfil regional a la Regla predeterminada, mantenlo seleccionado para que el asistente pueda importarlo antes de guardar.",
+ enableEverywhereTitle: `Usar ${BRAND_DISPLAY_NAME} en todos los sitios`,
+ enableEverywhereDescription: `Activa ahora la Regla predeterminada si quieres que ${BRAND_DISPLAY_NAME} proteja los sitios que no tengan una regla propia. Más adelante podrás limitar su comportamiento con reglas de dominio, asignar perfiles distintos a hosts concretos o excluir sitios sensibles mediante Sitios de confianza.`,
+ editDefaultRuleTitle: "Configuración de la Regla predeterminada",
+ editDefaultRule: "Editar regla predeterminada",
+ editDefaultRuleDescription:
+ "Elige el perfil y las protecciones personalizadas de la Regla predeterminada antes de finalizar la configuración.",
+ defaultRuleDialogDescription:
+ "Durante la configuración, este cuadro puede usar los perfiles regionales seleccionados aunque aún no se hayan importado. Si asignas uno aquí, mantenlo seleccionado para que el asistente pueda crearlo.",
+ presetMismatch: (label: string) =>
+ `La Regla predeterminada tiene asignado el perfil ${label}, pero no está seleccionado para importarlo. Vuelve a seleccionar ese perfil o elige otro para la Regla predeterminada.`,
+ },
+ chromium: {
+ title: "Rotar detalles de la compilación de Chromium",
+ description: (count: number) =>
+ `${BRAND_DISPLAY_NAME} puede rotar los números de compilación y parche del motor Chromium expuestos mediante Client Hints, manteniendo la versión principal igual a la del navegador instalado. El catálogo incluido contiene compilaciones recientes de Chromium; para esta versión del navegador, ${BRAND_DISPLAY_NAME} puede elegir entre ${count} ${count === 1 ? "entrada compatible" : "entradas compatibles"}.`,
+ switchTitle: "Rotar números de compilación y parche",
+ switchDescription:
+ "Usa números recientes de compilación y parche de Chromium del catálogo incluido al simular Client Hints. Desactiva esta opción si prefieres mantener fijos esos detalles de la versión.",
+ },
+ firefox: {
+ title: "Permiso de userScripts de Firefox (opcional)",
+ description: `Puedes usar ${BRAND_DISPLAY_NAME} en Firefox sin este permiso. Si concedes userScripts, ${BRAND_DISPLAY_NAME} puede aplicar valores simulados antes en sitios compatibles, mejorando la protección durante la primera carga de la página. Puedes omitirlo ahora y cambiarlo más tarde en Configuración.`,
+ action: "Conceder permiso a userScripts",
+ granted: "Permiso concedido",
+ skipped: `Puedes seguir usando ${BRAND_DISPLAY_NAME} sin este permiso.`,
+ },
+ appearance: {
+ title: "Hazlo cómodo",
+ description: `Elige el aspecto de la interfaz antes de comenzar. Estos ajustes solo afectan a las pantallas de ${BRAND_DISPLAY_NAME}; no cambian la simulación en los sitios web, los perfiles regionales ni las reglas. Podrás modificar las mismas opciones más adelante desde Configuración.`,
+ themeTitle: "Tema",
+ themeDescription: `Usa el ajuste del sistema o elige una interfaz clara u oscura fija para todas las pantallas de ${BRAND_DISPLAY_NAME}.`,
+ reduceMotionTitle: "Reducir movimiento",
+ reduceMotionDescription: `Desactivar animaciones de la interfaz en ${BRAND_DISPLAY_NAME}.`,
+ reduceMotionSystemOverride:
+ "La reducción de movimiento está habilitada por la configuración de accesibilidad de tu sistema.",
+ accentTitle: "Color de acento",
+ accentDescription:
+ "Elige el color de resaltado que se usará para los controles, estados activos, acentos de enfoque y elementos seleccionados.",
+ contrastTitle: "Modo de alto contraste",
+ contrastDescription:
+ "Aumenta el contraste para texto, bordes y controles en toda la interfaz de la extensión cuando el aspecto predeterminado sea demasiado sutil.",
+ },
+ done: {
+ title: "La configuración está lista",
+ description: `${BRAND_DISPLAY_NAME} guardará estas opciones y abrirá Configuración. Allí podrás revisar los perfiles importados, ajustar la Regla predeterminada y cambiar la apariencia o las opciones de privacidad.`,
+ cta: "Abrir Configuración",
+ },
+ },
+ presetNames: {
+ spfWarsaw: "Varsovia",
+ spfParis: "París",
+ spfLondon: "Londres",
+ spfOttawa: "Ottawa",
+ spfNewYork: "Nueva York",
+ spfLasVegas: "Las Vegas",
+ spfSanFrancisco: "San Francisco",
+ spfSydney: "Sydney",
+ spfBeijing: "Pekín",
+ spfHongKong: "Hong Kong",
+ spfNewDelhi: "Nueva Delhi",
+ spfCairo: "El Cairo",
+ spfLagos: "Lagos",
+ spfKyiv: "Kiev",
+ spfKinshasa: "Kinsasa",
+ spfSaoPaulo: "São Paulo",
+ spfBuenosAires: "Buenos Aires",
+ spfLima: "Lima",
+ spfRioDeJaneiro: "Rio de Janeiro",
+ spfCaracas: "Caracas",
+ spfBerlin: "Berlín",
+ spfMadrid: "Madrid",
+ },
+ themeOptions: {
+ system: "Sistema",
+ light: "Claro",
+ dark: "Oscuro",
+ },
+} as const;
diff --git a/src/ui/i18n/es.ts b/src/ui/i18n/es.ts
new file mode 100644
index 0000000..c7a44c7
--- /dev/null
+++ b/src/ui/i18n/es.ts
@@ -0,0 +1,30 @@
+import { about } from "./es-sections/about";
+import { advanced } from "./es-sections/advanced";
+import { common } from "./es-sections/common";
+import { demo } from "./es-sections/demo";
+import { firefoxContainers } from "./es-sections/firefox-containers";
+import { locations } from "./es-sections/locations";
+import { options, optionsPage } from "./es-sections/options";
+import { osm } from "./es-sections/osm";
+import { popup } from "./es-sections/popup";
+import { rules } from "./es-sections/rules";
+import { sidebar } from "./es-sections/sidebar";
+import { trustedSites } from "./es-sections/trusted-sites";
+import { welcome } from "./es-sections/welcome";
+
+export const es = {
+ common,
+ popup,
+ options,
+ optionsPage,
+ locations,
+ rules,
+ trustedSites,
+ advanced,
+ about,
+ osm,
+ demo,
+ firefoxContainers,
+ sidebar,
+ welcome,
+} as const;
diff --git a/src/ui/i18n/i18n-usage.test.ts b/src/ui/i18n/i18n-usage.test.ts
index c4471e5..23e9f46 100644
--- a/src/ui/i18n/i18n-usage.test.ts
+++ b/src/ui/i18n/i18n-usage.test.ts
@@ -13,7 +13,10 @@ const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const uiRoot = path.resolve(__dirname, "..");
const i18nRoot = __dirname;
-const localeSectionsRoot = path.join(i18nRoot, "en-sections");
+const localeSectionRoots = fs
+ .readdirSync(i18nRoot, { withFileTypes: true })
+ .filter((entry) => entry.isDirectory() && entry.name.endsWith("-sections"))
+ .map((entry) => path.join(i18nRoot, entry.name));
const collectLocaleLeafPaths = (
value: unknown,
@@ -322,8 +325,8 @@ const collectLocaleModules = async (): Promise<
describe("UI i18n coverage", () => {
it("keeps the product name behind the shared branding token", () => {
- const hardCodedProductNames = collectSourceFiles(localeSectionsRoot).flatMap(
- (filePath) => {
+ const hardCodedProductNames = localeSectionRoots.flatMap((localeSectionsRoot) =>
+ collectSourceFiles(localeSectionsRoot).flatMap((filePath) => {
const sourceFile = ts.createSourceFile(
filePath,
fs.readFileSync(filePath, "utf8"),
@@ -344,7 +347,7 @@ describe("UI i18n coverage", () => {
};
visit(sourceFile);
return matches;
- },
+ }),
);
expect(hardCodedProductNames).toEqual([]);
diff --git a/src/ui/i18n/index.test.ts b/src/ui/i18n/index.test.ts
new file mode 100644
index 0000000..e478d38
--- /dev/null
+++ b/src/ui/i18n/index.test.ts
@@ -0,0 +1,23 @@
+import { describe, expect, it } from "vitest";
+
+import { resolveUiLocale } from "./index";
+
+describe("resolveUiLocale", () => {
+ it.each(["es", "es-ES", "es-MX", "ES-ar"])("selects Spanish for %s", (language) => {
+ expect(resolveUiLocale(language)).toBe("es");
+ });
+
+ it.each(["pt", "pt-BR", "pt-PT", "PT-ao"])(
+ "selects Portuguese for %s",
+ (language) => {
+ expect(resolveUiLocale(language)).toBe("pt");
+ },
+ );
+
+ it.each([undefined, "", "en", "en-US", "pl-PL"])(
+ "falls back to English for %s",
+ (language) => {
+ expect(resolveUiLocale(language)).toBe("en");
+ },
+ );
+});
diff --git a/src/ui/i18n/index.ts b/src/ui/i18n/index.ts
index 0d7db38..3844dc3 100644
--- a/src/ui/i18n/index.ts
+++ b/src/ui/i18n/index.ts
@@ -1,5 +1,19 @@
import { en } from "./en";
+import { es } from "./es";
+import { pt } from "./pt";
+
+export type UiLocale = "en" | "es" | "pt";
+
+export const resolveUiLocale = (language: string | undefined): UiLocale => {
+ const baseLanguage = language?.toLowerCase().split("-")[0];
+ return baseLanguage === "es" || baseLanguage === "pt" ? baseLanguage : "en";
+};
+
+export const UI_LOCALE = resolveUiLocale(globalThis.navigator?.language);
+export const t = { en, es, pt }[UI_LOCALE];
+
+if (typeof document !== "undefined") {
+ document.documentElement.lang = UI_LOCALE;
+}
-export const UI_LOCALE = "en";
-export const t = en;
export type { Translations } from "./types";
diff --git a/src/ui/i18n/localized-copy.test.ts b/src/ui/i18n/localized-copy.test.ts
new file mode 100644
index 0000000..ade7fb1
--- /dev/null
+++ b/src/ui/i18n/localized-copy.test.ts
@@ -0,0 +1,36 @@
+import type { SurfacePresentationState } from "@privacy-brand/xray-protocol";
+import { describe, expect, it } from "vitest";
+
+import { es } from "./es";
+import { pt } from "./pt";
+
+const pluralProtectionCounts: Record = {
+ "not-applicable": 2,
+ "native-by-policy": 2,
+ unrecoverable: 1,
+ degraded: 1,
+ pending: 2,
+ repaired: 1,
+ "browser-enforced": 1,
+ protected: 1,
+ unknown: 2,
+};
+
+describe("localized dynamic copy", () => {
+ it("uses complete Spanish plural phrases", () => {
+ expect(es.popup.notificationsBadgeLabel(2)).toBe("2 notificaciones no leídas");
+ expect(es.popup.protectionCounts(pluralProtectionCounts)).toBe(
+ "3 protegidos · 2 degradados · 2 confirmando · 2 no modificados · 2 no aplicables · 2 desconocidos",
+ );
+ });
+
+ it("uses complete Portuguese plural phrases", () => {
+ expect(pt.popup.notificationsBadgeLabel(2)).toBe("2 notificações não lidas");
+ expect(pt.popup.protectionCounts(pluralProtectionCounts)).toBe(
+ "3 protegidos · 2 degradados · 2 confirmando · 2 não modificados · 2 não aplicáveis · 2 desconhecidos",
+ );
+ expect(pt.rules.globalFallback.overridesBadge(2)).toBe(
+ "2 configurações personalizadas",
+ );
+ });
+});
diff --git a/src/ui/i18n/pt-sections/about.ts b/src/ui/i18n/pt-sections/about.ts
new file mode 100644
index 0000000..d4c6947
--- /dev/null
+++ b/src/ui/i18n/pt-sections/about.ts
@@ -0,0 +1,168 @@
+import { BRAND_DISPLAY_NAME } from "@/shared/brand";
+
+const ABOUT_PRODUCT = `Sobre ${BRAND_DISPLAY_NAME}`;
+const ABOUT_PRODUCT_LOWER = `sobre ${BRAND_DISPLAY_NAME}`;
+const HOW_TO_USE_PRODUCT = `Como usar ${BRAND_DISPLAY_NAME}`;
+const HOW_TO_USE_PRODUCT_LOWER = `como usar ${BRAND_DISPLAY_NAME}`;
+const WHAT_PRODUCT_IS_NOT = `O que ${BRAND_DISPLAY_NAME} não é`;
+const PRODUCT_NOT_LOWER = `o que ${BRAND_DISPLAY_NAME} não é`;
+const TERMS_OF_USE = "Termos de Uso";
+
+export const about = {
+ title: ABOUT_PRODUCT,
+ description: `${BRAND_DISPLAY_NAME} ajuda você a controlar como os sites veem sua localização, idioma, fuso horário e detalhes de identidade do navegador selecionado.`,
+ body1:
+ "Cada perfil regional salvo reúne coordenadas, preferências de idioma e um fuso horário em uma configuração reutilizável que você pode aplicar aos domínios escolhidos sem precisar recriá-la.",
+ body2: `Suas alterações são salvas automaticamente. Perfis regionais e regras atualizadas entram em vigor no próximo carregamento da página, então o ${BRAND_DISPLAY_NAME} pode aplicar a nova configuração desde o início.`,
+ body3Prefix: "Você encontra os controles globais de proteção do navegador na",
+ body3LinkLabel: "aba Opções",
+ body3Suffix: ".",
+ website: {
+ prefix: "Você pode visitar o ",
+ linkLabel: `site ${BRAND_DISPLAY_NAME}`,
+ url: "https://privacything.com",
+ suffix: " para notícias do projeto, downloads e mais informações.",
+ },
+ versionLabel: "Versão",
+ browserTargetLabel: "Destino do navegador",
+ releaseChannelLabel: "Canal de lançamento",
+ copyLinkLabel: ABOUT_PRODUCT_LOWER,
+ copyLinkTermsLabel: "termos de uso",
+ copyLinkPrivacyLabel: "sobre privacidade",
+ copyLinkLimitationsLabel: PRODUCT_NOT_LOWER,
+ copyLinkLicenseLabel: "licença",
+ copyLinkAssetsLabel: "recursos de terceiros",
+ copyLinkUsageLabel: HOW_TO_USE_PRODUCT_LOWER,
+ releaseChannels: {
+ local: "Local",
+ beta: "Beta",
+ stable: "Estável",
+ },
+
+ support: {
+ url: "https://webh.pl",
+ logoLinkAriaLabel: "Abrir webh.pl",
+ bodyPrefix: `${BRAND_DISPLAY_NAME} cresce com o suporte de `,
+ linkLabel: "webh.pl",
+ bodySuffix: " — infraestrutura rápida e flexível para projetos ambiciosos.",
+ },
+
+ terms: {
+ title: TERMS_OF_USE,
+ body1: `${BRAND_DISPLAY_NAME} é fornecido “como está”, sem garantia de qualquer tipo.`,
+ body2: `${BRAND_DISPLAY_NAME} ajuda os usuários a controlar localmente o local, o idioma, o fuso horário e os dados relacionados expostos pelo navegador. Não é uma VPN, proxy, ferramenta de anonimato, produto de segurança ou garantia de indetectabilidade.`,
+ body3: `Use o ${BRAND_DISPLAY_NAME} por sua própria conta e risco. Você é responsável por cumprir as leis aplicáveis, os termos dos sites, as políticas do local de trabalho e as regras da plataforma.`,
+ },
+
+ privacy: {
+ title: "Privacidade",
+ body: `O ${BRAND_DISPLAY_NAME} armazena suas configurações e seus perfis regionais localmente no navegador. As solicitações externas de mapas são opcionais: após o seu consentimento, o OpenStreetMap Nominatim é usado para pesquisar locais e o OpenFreeMap para exibir mapas interativos.`,
+ openPolicyButton: "Abrir política de privacidade",
+ },
+
+ limitations: {
+ title: WHAT_PRODUCT_IS_NOT,
+ intro: `${BRAND_DISPLAY_NAME} altera dados visíveis no navegador, como geolocalização, localidade, fuso horário e detalhes de impressão digital selecionados. Ele não redireciona seu tráfego por uma rede diferente.`,
+ body1: "Ele não oculta nem substitui seu endereço IP.",
+ body2:
+ "Ele não substitui uma VPN, proxy ou configuração de roteamento baseada em DNS.",
+ body3:
+ "Ele não faz com que os sites vejam sua conexão como vinda de outro país por conta própria.",
+ outro: `Use ${BRAND_DISPLAY_NAME} para simulação no nível do navegador. Use ferramentas de VPN, proxy ou DNS quando precisar de alterações de localização no nível da rede.`,
+ },
+
+ license: {
+ title: "Licença",
+ creatorPrefix: "Este projeto foi criado por ",
+ creatorLabel: "Tomasz Janusz",
+ creatorUrl: "https://tomaszjanusz.dev",
+ creatorSuffix: ".",
+ copyright: "Copyright © 2025-presente.",
+ body: `${BRAND_DISPLAY_NAME} está disponível sob a Licença Pública Geral Affero GNU v3.0 ou posterior, com termos adicionais.`,
+ openLicenseButton: "Abrir licença",
+ },
+
+ assets: {
+ title: "Recursos de terceiros",
+ body: `Revise os componentes de terceiros incluídos e os textos de licença aberta fornecidos com ${BRAND_DISPLAY_NAME}.`,
+ openNoticesButton: "Abrir avisos de terceiros",
+ fontAwesome: {
+ label: "Font Awesome Free 7.2.0",
+ url: "https://fontawesome.com",
+ body: " por Font Awesome / Fonticons, Inc. é licenciado sob CC BY 4.0, SIL OFL 1.1 e MIT.",
+ },
+ mapLibre: {
+ label: "MapLibre GL JS",
+ url: "https://maplibre.org/maplibre-gl-js/docs/",
+ body: " é incluído localmente para renderização de mapas vetoriais.",
+ },
+ openFreeMap: {
+ label: "OpenFreeMap",
+ url: "https://openfreemap.org/",
+ },
+ openStreetMap: {
+ label: "OpenStreetMap",
+ url: "https://www.openstreetmap.org/copyright",
+ },
+ osmWikiCountryCodes: {
+ label: "Wiki OpenStreetMap: Nominatim/Country Codes",
+ url: "https://wiki.openstreetmap.org/wiki/Nominatim/Country_Codes",
+ body: " fornece as configurações padrão de país para idioma usadas durante a geração de perfis regionais.",
+ licenseLabel: "CC BY-SA 2.0",
+ licenseUrl: "https://wiki.openstreetmap.org/wiki/Wiki_content_license",
+ licenseBody: " aplica-se a esse conteúdo da wiki e é atribuído aqui de acordo.",
+ },
+ mapPreviewsPrefix: "Visualizações interativas de mapas usam ",
+ mapPreviewsMiddle: " serviços com ",
+ mapPreviewsSuffix: " atribuição de contribuinte exibida no mapa.",
+
+ localData: {
+ title: "Conjuntos de dados locais",
+ body: `${BRAND_DISPLAY_NAME} inclui pequenos bancos de dados locais criados a partir de dados públicos processados, para manter os valores simulados estatisticamente realistas. Nada é baixado da internet — são cópias empacotadas localmente e atualizadas com cada versão da extensão.`,
+ steam: {
+ label: "Pesquisa de Hardware & Software do Steam",
+ url: "https://store.steampowered.com/hwsurvey/",
+ body: " (Valve) fornece as distribuições de resolução de tela, núcleos de CPU e RAM do Windows, Linux e macOS usadas para perfis de hardware locais.",
+ },
+ chromiumDash: {
+ label: "ChromiumDash",
+ url: "https://chromiumdash.appspot.com/",
+ body: " fornece as versões reais do Chrome usadas para User-Agent e Client Hints.",
+ },
+ localeCatalog: {
+ prefix: "O ",
+ mozilla: {
+ label: "Mozilla",
+ url: "https://github.com/mozilla-firefox/firefox/blob/main/intl/locale/language.properties",
+ },
+ middle: " e ",
+ chromium: {
+ label: "Chromium",
+ url: "https://github.com/chromium/chromium/blob/main/ui/base/l10n/l10n_util.cc",
+ },
+ body: " catálogos de localização fornecem nomes de exibição de idioma alinhados com cada mecanismo de navegador.",
+ },
+ },
+ },
+
+ projectLinks: {
+ description: `${BRAND_DISPLAY_NAME} também vive fora da extensão. Visite o site, navegue pelo código ou me avise quando algo não estiver funcionando.`,
+ websiteLabel: "Site",
+ websiteUrl: "https://privacything.com",
+ sourceLabel: "Código fonte",
+ sourceUrl: "https://github.com/Privacy-Thing/browser-extension",
+ reportBugLabel: "Reportar bug",
+ reportBugUrl:
+ "https://github.com/Privacy-Thing/browser-extension/issues/new?template=bug_report.yml",
+ },
+
+ usage: {
+ title: HOW_TO_USE_PRODUCT,
+ body1:
+ "Comece gerando um perfil regional a partir de um lugar pesquisado quando quiser um ponto de partida rápido e realista.",
+ body2: `Mantenha um perfil regional salvo por lugar do mundo real que você deseja que ${BRAND_DISPLAY_NAME} imite, depois atribua sites ao perfil regional correspondente.`,
+ body3:
+ "Use o popup para atribuição rápida de domínios. Abra Configurações quando quiser o painel completo de perfis regionais e regras.",
+ body4: `Explore Opções e Avançado quando quiser mais controle sobre como ${BRAND_DISPLAY_NAME} se comporta.`,
+ },
+} as const;
diff --git a/src/ui/i18n/pt-sections/advanced.ts b/src/ui/i18n/pt-sections/advanced.ts
new file mode 100644
index 0000000..a5acd78
--- /dev/null
+++ b/src/ui/i18n/pt-sections/advanced.ts
@@ -0,0 +1,205 @@
+import { BRAND_DISPLAY_NAME } from "@/shared/brand";
+import { externalMapConsentCopy } from "@/ui/i18n/pt-sections/osm";
+
+const TURN_OFF_PRODUCT = `Desligar ${BRAND_DISPLAY_NAME}`;
+
+export const advanced = {
+ runtimeTitle: "Opções avançadas",
+ runtimeDescription: "Gerenciar registro detalhado em tempo de execução.",
+ copyLinkRuntimeLabel: "configurações avançadas",
+ copyLinkHelpLabel: "ajuda avançada",
+
+ noiseRadius: {
+ title: "Raio máximo de coordenadas padrão",
+ description:
+ "Defina, em metros, o raio máximo padrão das coordenadas de novos perfis regionais.",
+ copyLinkLabel: "raio máximo de coordenadas padrão",
+ },
+
+ generatedLocationRandomization: {
+ enabled: {
+ title: "Aleatorizar coordenadas ao criar novos perfis regionais",
+ description:
+ "Use esta opção como padrão para perfis regionais gerados. Você ainda pode desativá-la individualmente quando precisar de coordenadas exatas.",
+ readWhy: "Leia por quê.",
+ tooltipPrivacy: `${BRAND_DISPLAY_NAME} é focado em privacidade. Os perfis regionais devem ser fáceis de usar, mas reutilizar coordenadas exatas de catálogos ou buscas pode tornar os usuários do ${BRAND_DISPLAY_NAME} mais fáceis de reconhecer.`,
+ tooltipExact:
+ "Precisa de um local específico? Desative este interruptor e o perfil regional usará a coordenada exata.",
+ copyLinkLabel: "nova randomização de coordenadas do perfil regional",
+ },
+ radius: {
+ title: "Raio de randomização de coordenadas padrão",
+ description:
+ "Escolha o raio padrão usado quando a randomização de coordenadas estiver ativada. Digite um número inteiro de 1 a 99 km.",
+ inputLabel: "Raio de randomização de coordenadas padrão em quilômetros",
+ copyLinkLabel: "raio de randomização de coordenadas padrão",
+ },
+ },
+
+ themeMode: {
+ title: "Tema",
+ description: `Escolha se ${BRAND_DISPLAY_NAME} seguirá a aparência do sistema ou sempre usará um tema claro ou escuro fixo.`,
+ label: "Aparência",
+ options: {
+ system: "Sistema",
+ light: "Claro",
+ dark: "Escuro",
+ },
+ copyLinkLabel: "tema",
+ },
+
+ watchPositionDelay: {
+ title: "Intervalo de atualização da posição (s)",
+ description:
+ "Defina o menor e o maior atraso entre atualizações de localização em sites.",
+ copyLinkLabel: "intervalo de atraso de posição de observação",
+ },
+
+ debugMode: {
+ title: "Modo de Depuração",
+ description: `Registre a atividade detalhada em tempo de execução para ${BRAND_DISPLAY_NAME} no console de desenvolvedor do navegador.`,
+ copyLinkLabel: "modo de depuração",
+ },
+
+ experimental: {
+ title: "Experimental",
+ description:
+ "Teste proteções para APIs do navegador que ainda estão sendo lançadas. Esses recursos estão disponíveis em todos os canais de lançamento.",
+ copyLinkLabel: "configurações experimentais",
+ temporalApi: {
+ title: "Temporal API",
+ description:
+ "Proteja o comportamento de data, hora, localidade e fuso horário padrão do Temporal quando este navegador fornece a API nativa. Nenhum polyfill está instalado.",
+ copyLinkLabel: "Temporal API",
+ },
+ domainFencing: {
+ title: "Isolamento por domínio",
+ description:
+ "Quando a Regra Padrão ou um contêiner do Firefox se aplica, cada site recebe sua própria impressão digital estável. Sua configuração regional permanece a mesma.",
+ copyLinkLabel: "isolamento por domínio",
+ },
+ },
+
+ privacy: {
+ title: "Privacidade",
+ description: `${BRAND_DISPLAY_NAME} roda no seu dispositivo e não envia seus dados de navegação para lugar nenhum. As únicas solicitações de rede opcionais ocorrem quando você usa a pesquisa de localização ou visualizações de mapas ao configurar perfis regionais.`,
+ copyLinkLabel: "privacidade",
+
+ osmConsent: {
+ title: externalMapConsentCopy.title,
+ description: externalMapConsentCopy.description,
+ stateUnknown: `Você ainda não escolheu isso. ${BRAND_DISPLAY_NAME} perguntará antes de se conectar a um serviço de mapas externo.`,
+ statePrefix: "O acesso a mapas externos está",
+ stateEnabled:
+ "ativado — a pesquisa de localização e visualizações de mapas podem carregar",
+ stateDisabled: `desativado — ${BRAND_DISPLAY_NAME} permanecerá offline enquanto você configura os locais`,
+ copyLinkLabel: "acesso a mapas externos",
+ },
+ },
+
+ display: {
+ title: "Aparência",
+ description: "Ajuste a apresentação visual e as configurações de acessibilidade.",
+ copyLinkLabel: "aparência",
+
+ language: {
+ title: "Idioma",
+ description: `${BRAND_DISPLAY_NAME} segue automaticamente o idioma da interface do seu navegador.`,
+ optionEnglish: "Inglês",
+ optionSpanish: "Espanhol",
+ optionPortuguese: "Português",
+ soon: "AUTO",
+ copyLinkLabel: "idioma",
+ },
+
+ reduceMotion: {
+ title: "Reduzir movimento",
+ description: `Desativar animações da interface em todo o ${BRAND_DISPLAY_NAME}.`,
+ systemOverride:
+ "Movimento reduzido está ativado pelas configurações de acessibilidade do seu sistema.",
+ copyLinkLabel: "reduzir movimento",
+ },
+
+ accentColor: {
+ title: "Cor de destaque",
+ description: `Escolha a principal cor de destaque do ${BRAND_DISPLAY_NAME} em uma paleta inspirada nas cores dos Contêineres do Firefox.`,
+ copyLinkLabel: "cor de destaque",
+ optionAriaLabel: (label: string) => `Usar ${label} como cor de destaque`,
+ options: {
+ teal: "Verde-azulado",
+ blue: "Azul",
+ green: "Verde",
+ yellow: "Amarelo",
+ orange: "Laranja",
+ red: "Vermelho",
+ pink: "Rosa",
+ purple: "Roxo",
+ gray: "Cinza",
+ },
+ },
+
+ highContrast: {
+ title: "Modo de alto contraste",
+ description:
+ "Aumente o contraste do texto e a visibilidade das bordas para melhorar a legibilidade.",
+ copyLinkLabel: "modo de alto contraste",
+ },
+ },
+
+ danger: {
+ title: "Zona de perigo",
+ description: `Estas ações podem substituir, excluir ou restaurar as configurações e os dados do ${BRAND_DISPLAY_NAME} salvos localmente.`,
+ copyLinkLabel: "zona de perigo",
+
+ spoofing: {
+ title: TURN_OFF_PRODUCT,
+ description: `Desative todas as proteções do ${BRAND_DISPLAY_NAME} até que você as ative novamente.`,
+ copyLinkLabel: TURN_OFF_PRODUCT,
+ },
+
+ export: {
+ title: "Exportar",
+ description:
+ "Baixe seus perfis regionais, regras e configurações atuais como um backup JSON.",
+ button: "Exportar configurações",
+ copyLinkLabel: "exportar configurações",
+ },
+
+ import: {
+ title: "Importar",
+ description:
+ "Substitua as configurações locais atuais por um backup JSON exportado anteriormente.",
+ button: "Importar configurações",
+ copyLinkLabel: "importar configurações",
+ },
+
+ reload: {
+ title: "Recarregar",
+ description:
+ "Recarregar as configurações salvas do armazenamento da extensão e descartar alterações não salvas na visualização atual.",
+ button: "Recarregar configurações",
+ copyLinkLabel: "recarregar configurações",
+ },
+
+ reset: {
+ title: "Redefinir",
+ description:
+ "Restaure os padrões iniciais e remova seus perfis regionais e regras personalizados.",
+ button: "Redefinir configurações",
+ copyLinkLabel: "redefinir configurações",
+ confirmTitle: "Redefinir configurações?",
+ confirmBody:
+ "Isso exclui permanentemente seus perfis regionais, regras e configurações personalizadas salvos. Isso não pode ser desfeito.",
+ onboardingToggleLabel: "Executar configuração novamente após redefinir",
+ onboardingToggleDescription:
+ "Reabra o guia de configuração inicial assim que suas configurações forem limpas.",
+ },
+ },
+
+ help: {
+ title: "Avançado",
+ body1: `Use Avançado para ajustar as proteções, desativar temporariamente o ${BRAND_DISPLAY_NAME} ou gerenciar suas configurações e dados salvos localmente.`,
+ body2:
+ "Importar, exportar, recarregar e redefinir ajudam você a mover configurações entre navegadores, corrigir erros ou restaurar configurações salvas quando algo fica fora de sincronia.",
+ },
+} as const;
diff --git a/src/ui/i18n/pt-sections/common.ts b/src/ui/i18n/pt-sections/common.ts
new file mode 100644
index 0000000..c16ee83
--- /dev/null
+++ b/src/ui/i18n/pt-sections/common.ts
@@ -0,0 +1,46 @@
+import { BRAND_DISPLAY_NAME } from "@/shared/brand";
+
+export const common = {
+ copyLinkTo: (section: string) => `Copiar link para ${section}`,
+
+ actions: {
+ cancel: "Cancelar",
+ reset: "Redefinir",
+ close: "Fechar",
+ create: "Criar",
+ save: "Salvar",
+ back: "Voltar",
+ continue: "Continuar",
+ edit: "Editar",
+ delete: "Excluir",
+ duplicate: "Duplicar",
+ search: "Pesquisar",
+ clear: "Limpar",
+ deleteSelected: "Excluir selecionados",
+ clearSelection: "Limpar seleção",
+ openPrivacyPolicy: "Abrir política de privacidade",
+ allowOpenStreetMap: "Permitir acesso ao mapa",
+ notNow: "Agora não",
+ openPlayground: "Abrir Área de testes",
+ },
+ selectionCount: (count: number) =>
+ `${count} ${count === 1 ? "selecionado" : "selecionados"}`,
+
+ fields: {
+ name: "Nome",
+ latitude: "Latitude",
+ longitude: "Longitude",
+ accuracy: "Precisão",
+ noiseRadius: "Raio máximo (m)",
+ timeZone: "Fuso horário",
+ },
+
+ coordinateRandomization: {
+ labelBefore: "Aleatorizar coordenadas dentro de",
+ labelAfter: "km.",
+ radiusInputLabel: "Raio de aleatorização de coordenadas em quilômetros",
+ readWhy: "Leia por quê.",
+ tooltipPrivacy: `${BRAND_DISPLAY_NAME} é focado em privacidade. Os perfis regionais devem ser fáceis de usar, mas reutilizar coordenadas exatas de catálogos ou buscas pode tornar os usuários do ${BRAND_DISPLAY_NAME} mais fáceis de reconhecer.`,
+ tooltipExact: `Precisa de um local específico? Desative este interruptor e o ${BRAND_DISPLAY_NAME} usará a coordenada exata. Você pode alterar os padrões em Opções > Geolocalização > Avançado.`,
+ },
+} as const;
diff --git a/src/ui/i18n/pt-sections/demo.ts b/src/ui/i18n/pt-sections/demo.ts
new file mode 100644
index 0000000..43b669e
--- /dev/null
+++ b/src/ui/i18n/pt-sections/demo.ts
@@ -0,0 +1,152 @@
+import { BRAND_DISPLAY_NAME } from "@/shared/brand";
+
+const SPOOFED_PRODUCT = `Simulado — ${BRAND_DISPLAY_NAME}`;
+
+export const demo = {
+ loadingSettings: "Carregando configurações…",
+ noLocationsTitle: "Ainda não há perfis regionais",
+ noLocationsBody:
+ "Crie pelo menos um perfil regional em Configurações para usar a Área de testes.",
+ openSettingsButton: "Abrir Configurações",
+
+ locationPreview: {
+ title: "Pré-visualização do perfil regional",
+ activeLocationLabel: "Perfil regional ativo",
+ activeLocationDescription:
+ "Selecione um perfil regional salvo para visualizar seus valores exibidos no navegador. Mudar o perfil regional reinicia a rota e aplica a nova visualização de geolocalização.",
+ activeLocationPlaceholder: "Escolher um perfil regional…",
+ playgroundCadenceLabel: "Tempo de atualização da Área de testes",
+ realSiteCadenceLabel: "Tempo de atualização do site",
+ configuredDelayLabel: "Intervalo do watchPosition",
+ callbackDelayLabel: "Atraso de retorno de chamada",
+ runtimeModeLabel: "Modo de execução",
+ runtimeModeSimple: "Tempo padrão",
+ realLocationTitleIdle: "Comparar com a localização atual do seu navegador",
+ realLocationTitleLoading: "Solicitando a localização atual do seu navegador",
+ realLocationTitleGranted: "Localização atual do navegador está pronta",
+ realLocationTitleDenied: "Permissão de localização do navegador negada",
+ realLocationTitleUnavailable: "Localização do navegador indisponível",
+ realLocationDescription:
+ "Solicite a geolocalização real do navegador uma vez para compará-la com a pré-visualização definida abaixo.",
+ realLocationGrantedDescription:
+ "A localização atual do seu navegador agora está disponível nas linhas de geolocalização abaixo para comparação lado a lado.",
+ realLocationDeniedDescription:
+ "O navegador negou o acesso à geolocalização real. Você pode tentar novamente se a permissão mudar.",
+ realLocationUnavailable:
+ "A comparação com a localização real não está disponível porque este navegador não expõe geolocalização aqui.",
+ realLocationRefresh: "Atualizar localização real",
+ },
+
+ localMachineTitle: "Computador local",
+ spoofedTitle: SPOOFED_PRODUCT,
+ waitingForPermission: "Aguardando permissão do navegador…",
+ permissionDenied: "Permissão negada pelo navegador",
+ geolocationUnavailable: "Geolocalização não disponível",
+ requestRealLocation: "Solicitar localização real",
+ requestRealLocationHintTitle: "Localização real ainda não carregada",
+ requestRealLocationTableHint:
+ "Solicite a localização real do seu navegador acima para compará-la aqui.",
+ waitingForFix: "Aguardando a primeira posição...",
+ selectLocationPrompt:
+ "Selecione um perfil regional acima para ver os valores de pré-visualização.",
+
+ comparison: {
+ language: "navigator.language",
+ languages: "navigator.languages",
+ timeZone: "Intl…resolvedOptions().timeZone",
+ acceptLanguage: "Cabeçalho 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: "Sobre a exibição de timestamp da geolocalização",
+ timestampTooltip: `${BRAND_DISPLAY_NAME} retorna o timestamp Unix bruto em milissegundos. A data legível exibida aqui serve apenas para facilitar a consulta e usa o fuso horário da respectiva coluna.`,
+ 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: "Resumo da sonda Canvas 2D",
+ webglRenderer: "Sonda do renderizador WebGL",
+ webglDebugExtension: "WEBGL_debug_renderer_info",
+ webglReadPixels: "Sonda readPixels() do WebGL",
+ audioFingerprint: "Sonda 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: "Cabeçalho Sec-CH-UA",
+ secChUaPlatform: "Cabeçalho Sec-CH-UA-Platform",
+ secChUaMobile: "Cabeçalho Sec-CH-UA-Mobile",
+ secChUaFullVersionList: "Cabeçalho Sec-CH-UA-Full-Version-List",
+ webRTCIcePolicy: "Política ICE do RTCPeerConnection",
+ probePending: "Coletando…",
+ notAvailable: "N/D",
+ spoofedMatchesLocal:
+ "O valor de visualização corresponde ao seu navegador para esta identidade.",
+ browserVersionNote: (versionToken: string) =>
+ `${BRAND_DISPLAY_NAME} mantém os tokens de versão do navegador normalizados e não randomiza variantes de espaço reservado como ${versionToken}.`,
+ },
+
+ previewSeed: {
+ title: "Identidade de pré-visualização",
+ description:
+ "Escolha a identidade de navegador consistente mostrada nesta pré-visualização da Área de testes.",
+ inputAriaLabel: "Código de identidade de pré-visualização",
+ placeholder: "Código de identidade",
+ hint: "Use 6 letras minúsculas ou dígitos. Altere o código para visualizar uma identidade diferente.",
+ randomize: "Gerar nova identidade",
+ },
+
+ sections: {
+ localeDate: "Localidade e Data",
+ networkHeaders: "Cabeçalhos de Rede",
+ geolocation: "Geolocalização",
+ browserFingerprint: "Identidade do navegador",
+ webglCanvas: "Canvas e WebGL",
+ screen: "Tela",
+ audio: "Áudio",
+ webRTC: "WebRTC",
+ },
+
+ map: {
+ title: "Visualização do mapa",
+ noLocationTitle: "Selecione um perfil regional primeiro",
+ noLocationDescription:
+ "Escolha um perfil regional salvo acima para ativar os controles de mapa e waypoint.",
+ osmRequired: "Acesso ao mapa necessário",
+ osmRequiredDescription:
+ "Permita o acesso externo ao mapa para visualizar a posição simulada em um mapa interativo.",
+ demoIntervalLabel: "Intervalo de demonstração (2–5 s)",
+ clearButton: "Limpar",
+ },
+
+ disclaimer: {
+ title: "Modo de visualização, não um site ao vivo",
+ body: `Use esta página para inspecionar como ${BRAND_DISPLAY_NAME} apresentaria o perfil regional selecionado antes de aplicá-lo em sites. Idioma, localidade, hora, cabeçalhos e valores similares são exibidos como uma pré-visualização a partir dos mesmos dados do perfil regional. A geolocalização usa o mesmo comportamento de movimento e atualização de sites protegidos. Na Área de testes, as atualizações de localização são mais rápidas do que em sites normais, facilitando ver as mudanças.`,
+ },
+
+ howItWorks: {
+ title: "Como ler esta pré-visualização",
+ body1:
+ "Idioma, localidade e hora — estas linhas mostram como o perfil regional selecionado se apresentaria aos sites, sem alterar a própria página da Área de testes.",
+ body2: `Geolocalização — as linhas de mapa e localização usam o mesmo comportamento de movimento e atualização que sites protegidos, para que você possa visualizar a sincronização e a variação de coordenadas antes de atribuir o perfil regional.`,
+ body3: `Valores e cabeçalhos do navegador — essas comparações colocam os valores atuais do seu navegador ao lado dos valores que ${BRAND_DISPLAY_NAME} exporia para o perfil regional selecionado, seguindo as mesmas configurações de proteção que você usa em Configurações.`,
+ body4:
+ "A Área de testes atualiza mais rápido que a navegação normal, então mudanças são mais fáceis de perceber durante os testes.",
+ },
+} as const;
diff --git a/src/ui/i18n/pt-sections/firefox-containers.ts b/src/ui/i18n/pt-sections/firefox-containers.ts
new file mode 100644
index 0000000..5ab47ba
--- /dev/null
+++ b/src/ui/i18n/pt-sections/firefox-containers.ts
@@ -0,0 +1,101 @@
+import { BRAND_DISPLAY_NAME } from "@/shared/brand";
+
+export const firefoxContainers = {
+ copyLinkLabel: "contêineres",
+ copyLinkHelpLabel: "ajuda de contêineres",
+ title: "Contêineres",
+ description: `Gerencie os Contêineres do Firefox e atribua a cada um um perfil regional opcional do ${BRAND_DISPLAY_NAME}.`,
+ refreshTitle: "Atualizar lista de contêineres",
+ refreshButton: "Atualizar lista",
+ createButton: "Novo contêiner",
+ deleteButton: "Excluir",
+ removeAssignmentButton: "Remover atribuição",
+ editActionTitle: (name: string) => `Editar contêiner “${name}”`,
+ deleteActionTitle: (name: string) => `Excluir contêiner “${name}”`,
+ removeAssignmentActionTitle: (name: string) =>
+ `Remover atribuição de perfil regional para o contêiner excluído “${name}”`,
+ showInactiveLabel: "Mostrar contêineres inativos",
+ filterPlaceholder: "Pesquisar contêineres...",
+ loading: "Carregando contêineres...",
+ noResults: "Nenhum contêiner corresponde ao filtro atual.",
+ columns: {
+ container: "Contêiner",
+ location: "Perfil regional",
+ actions: "Ações",
+ },
+ noLocationAssigned: "Nenhum perfil regional atribuído",
+ missingLocation: "O perfil regional atribuído não está disponível",
+ inactiveBadge: "Excluído",
+ deletedContainerLabel: "Contêiner excluído",
+ emptyTitle: "Ainda não há Contêineres do Firefox",
+ emptyBody: `Crie seu primeiro contêiner aqui ou nas configurações do Firefox e atribua a ele um perfil regional do ${BRAND_DISPLAY_NAME}.`,
+ unavailableTitle: "Contêineres do Firefox não disponíveis",
+ unavailableBody:
+ "Ative os Contêineres do Firefox e depois atualize a lista. Se o problema continuar, esta versão do Firefox pode não permitir que a extensão os gerencie.",
+ unavailableHint: `${BRAND_DISPLAY_NAME} pode gerenciar contêineres apenas quando os Contêineres do Firefox estiverem ativados.`,
+ editor: {
+ createTitle: "Criar contêiner",
+ editTitlePrefix: "Editar contêiner",
+ createDescription: `Escolha um nome, cor, ícone e configurações opcionais ${BRAND_DISPLAY_NAME} para o novo contêiner do Firefox.`,
+ editDescription: `Atualize as propriedades do contêiner do Firefox e suas configurações opcionais ${BRAND_DISPLAY_NAME} em uma única gravação.`,
+ saveButton: "Salvar",
+ previewUntitled: "Contêiner sem título",
+ nameRequired: "O nome do contêiner não pode estar vazio.",
+ defaultRulePreviewTitle: "Nenhuma configuração salva ainda",
+ defaultRulePreviewLocation: (label: string) =>
+ `Este contêiner ainda não possui configurações próprias do ${BRAND_DISPLAY_NAME}, por isso usa o perfil da Regra Padrão: ${label}. Salve as configurações do contêiner para criar uma configuração própria.`,
+ defaultRulePreviewProtections: `Este contêiner ainda não possui configurações próprias do ${BRAND_DISPLAY_NAME}, por isso usa apenas as proteções da Regra Padrão que não exigem um perfil regional. Salve as configurações do contêiner para criar uma configuração própria.`,
+ defaultRulePreviewUnconfigured: `Este contêiner ainda não possui configurações próprias do ${BRAND_DISPLAY_NAME}. A Regra Padrão está ativada, mas ainda não tem um perfil regional nem proteções personalizadas. Salve as configurações do contêiner ou termine de configurar a Regra Padrão.`,
+ defaultRulePreviewDisabled: `Este contêiner ainda não possui configurações próprias do ${BRAND_DISPLAY_NAME}. A Regra Padrão está desativada, portanto ${BRAND_DISPLAY_NAME} permanece desativado aqui. Salve as configurações do contêiner ou ative a Regra Padrão.`,
+ defaultRulePreviewMissing: `Este contêiner ainda não possui configurações próprias do ${BRAND_DISPLAY_NAME}. Não há uma Regra Padrão salva, portanto ${BRAND_DISPLAY_NAME} permanece desativado aqui. Salve as configurações do contêiner ou defina uma Regra Padrão.`,
+ fields: {
+ name: "Nome",
+ color: "Cor",
+ icon: "Ícone",
+ enabled: "Ativado",
+ location: "Perfil regional",
+ locationProfile: "Perfil regional",
+ },
+ unassignedLocation: "Nenhum perfil regional atribuído",
+ enabledHint:
+ "Quando desabilitado, esta atribuição de contêiner não se aplica. Suas configurações permanecem salvas.",
+ locationProfileHint:
+ "Escolha o perfil regional que este contêiner deve usar. Deixe sem atribuição para usar apenas as configurações de proteção do contêiner e o perfil da Regra Padrão, quando disponível.",
+ identity: {
+ sectionTitle: "Identidade",
+ sectionDescription:
+ "Este contêiner mantém sua própria identidade de simulação. Gere outra apenas quando quiser uma nova impressão digital e um estado limpo dos sites deste contêiner.",
+ actionDescription:
+ "Limpa os dados relacionados do site e inicia este contêiner com uma identidade nova.",
+ actionLabel: "Nova identidade",
+ confirmTitle: (name: string) => `Nova identidade para “${name}”?`,
+ confirmDescription:
+ "Isso limpa cookies, armazenamento, Service Workers e caches para sites vinculados a este contêiner. Em seguida, cria uma nova identidade de simulação.",
+ confirmDomainsLabel: `${BRAND_DISPLAY_NAME} limpará os dados do navegador para estes domínios:`,
+ confirmNoDomains: `Nenhum dado do navegador foi registrado para este contêiner ainda. ${BRAND_DISPLAY_NAME} ainda criará uma nova identidade de simulação.`,
+ confirmLabel: "Criar nova identidade",
+ rotateSuccess: "Nova identidade de contêiner salva.",
+ rotateError: "Falha ao criar uma nova identidade de contêiner.",
+ },
+ },
+ confirmations: {
+ deleteTitle: (name: string) => `Excluir contêiner “${name}”?`,
+ deleteBody: `Isso remove o contêiner do Firefox e limpa sua atribuição de perfil regional do ${BRAND_DISPLAY_NAME}.`,
+ removeOrphanTitle: "Remover atribuição do contêiner excluído?",
+ removeOrphanBody: `O contêiner do Firefox não existe mais. Isso limpará apenas a atribuição desatualizada do perfil regional do ${BRAND_DISPLAY_NAME}.`,
+ },
+ messages: {
+ created: (name: string) => `Contêiner “${name}” criado.`,
+ updated: (name: string) => `Contêiner “${name}” salvo.`,
+ deleted: (name: string) => `Contêiner “${name}” excluído.`,
+ orphanRemoved: "Removida a atribuição do contêiner obsoleto.",
+ saveFailed: "Falha ao salvar o contêiner.",
+ deleteFailed: "Falha ao excluir o contêiner.",
+ },
+ help: {
+ title: "Contêineres",
+ body1: `Os Contêineres do Firefox adicionam outra maneira de escolher configurações no Firefox. ${BRAND_DISPLAY_NAME} verifica primeiro as Regras de Domínio, depois a atribuição do contêiner.`,
+ body2: `O editor salva os detalhes do Contêiner do Firefox junto com as configurações do ${BRAND_DISPLAY_NAME}, como o estado de ativação, um perfil regional opcional e proteções personalizadas.`,
+ body3: `Linhas inativas representam atribuições cujo contêiner do Firefox foi removido fora do ${BRAND_DISPLAY_NAME}. Revise-as e limpe-as deliberadamente em vez de perdê-las silenciosamente.`,
+ },
+} as const;
diff --git a/src/ui/i18n/pt-sections/locations.ts b/src/ui/i18n/pt-sections/locations.ts
new file mode 100644
index 0000000..49101d9
--- /dev/null
+++ b/src/ui/i18n/pt-sections/locations.ts
@@ -0,0 +1,107 @@
+import { BRAND_DISPLAY_NAME } from "@/shared/brand";
+
+const PRODUCT_NOT_LOWER = `o que ${BRAND_DISPLAY_NAME} não é`;
+
+export const locations = {
+ title: "Perfis regionais",
+ description:
+ "Perfis regionais agrupam coordenadas, configuração regional e fuso horário para simulação.",
+ addManualButton: "Adicionar manualmente",
+ actionsMenuLabel: "Ações do perfil regional",
+ generateButton: "Gerar perfil regional",
+ searchPlaceholder: "Pesquisar perfis regionais...",
+ unused: "Não utilizado",
+ assigned: (count: number) => `${count} ${count === 1 ? "atribuição" : "atribuições"}`,
+ viewAssignedRulesAriaLabel: (locationLabel: string, count: number) =>
+ `Mostrar ${count} ${count === 1 ? "regra de domínio atribuída" : "regras de domínio atribuídas"} a ${locationLabel}`,
+ copyLinkLabel: "perfis regionais",
+ copyLinkHelpLabel: "ajuda de perfis regionais",
+
+ playgroundCard: {
+ title: "Área de testes",
+ body1:
+ "Visualize um perfil regional salvo antes de atribuí-lo a sites. A Área de testes mostra a configuração regional, o fuso horário, a formatação de data e as coordenadas simuladas com o mesmo comportamento de geolocalização usado em sites protegidos.",
+ body2:
+ "Por padrão, a Área de testes atualiza a geolocalização simulada a cada 2-5 segundos, tornando fácil inspecionar o movimento. Sites reais usam o Atraso de Observação de Posição configurado por você.",
+ },
+
+ help: {
+ title: "Perfis regionais",
+ body1:
+ "Um perfil regional deve descrever um lugar plausível. Mantenha a configuração regional, os idiomas e o fuso horário coerentes com a região das coordenadas.",
+ body2: `As alterações são salvas automaticamente. Quando você edita um campo, ${BRAND_DISPLAY_NAME} grava a atualização após uma breve pausa.`,
+ body3:
+ "Um perfil regional salvo altera valores no nível do navegador, como geolocalização, localidade e fuso horário. Ele não altera seu endereço IP nem para onde seu tráfego é roteado.",
+ privacyTitle: "Privacidade",
+ privacyBody: `Pesquisas e pré-visualizações de mapas são opcionais. ${BRAND_DISPLAY_NAME} só entra em contato com o OpenStreetMap Nominatim para pesquisa e com o OpenFreeMap para pré-visualizações de mapas interativos após você permitir.`,
+ networkTitle: "Limites de rede",
+ networkBodyPrefix:
+ "Precisa de alterações de localização em nível de rede? Use uma VPN, proxy ou ferramenta de DNS. Veja ",
+ networkBodyLinkLabel: PRODUCT_NOT_LOWER,
+ networkBodySuffix: ".",
+ },
+
+ editor: {
+ title: "Editar perfil regional",
+ description:
+ "Atualize o perfil regional salvo, incluindo coordenadas, formatos regionais e comportamento de relatórios de localização.",
+ deleteBlockedTitle: "Este perfil regional ainda está atribuído",
+ deleteBlockedDescription:
+ "Altere ou remova todas as atribuições abaixo antes de excluir o perfil regional.",
+ deleteBlockedButtonTitle:
+ "Remova todas as atribuições antes de excluir este perfil regional.",
+ disabledDependencySuffix: "(desligado)",
+ mapDisabledTitle: "Mapa desativado",
+ mapDisabledBody:
+ "A visualização do mapa não foi carregada porque você não permitiu solicitações externas de mapa.",
+ geolocationSectionTitle: "Geolocalização",
+ geolocationSectionDescription:
+ "Coordenadas, precisão e a dispersão máxima permitida para posições simuladas.",
+ localeSectionTitle: "Hora e idioma",
+ localeSectionDescription:
+ "Mantenha os valores regionais coerentes com a região das coordenadas salvas.",
+ primaryLocaleLabel: "Configuração regional principal",
+ languageDescription:
+ "Este é o formato regional principal do perfil regional para datas, números e outros valores localizados.",
+ languageBehaviorDescription:
+ 'Quando "Preferir inglês em sites" está desativado, os sites também veem este valor como navigator.language. Mesmo com o inglês em primeiro lugar, esta configuração regional ainda controla a formatação padrão de datas e números.',
+ preferredLanguagesLabel: "Idiomas preferidos",
+ languagesDescription:
+ "Lista ordenada das preferências de idioma do navegador para este perfil regional.",
+ languagesBehaviorDescription: `${BRAND_DISPLAY_NAME} expõe esta ordem como navigator.languages e preferências de idioma relacionadas. Mantenha a configuração regional principal em primeiro lugar e adicione apenas idiomas alternativos plausíveis para este perfil regional.`,
+ preferEnglishContentLabel: "Preferir inglês em sites",
+ preferEnglishContentDescriptionPrefix: `${BRAND_DISPLAY_NAME} mantém o locale salvo como sua base regional, mas expõe`,
+ preferEnglishContentDescriptionSuffix:
+ "primeiro nas preferências de idioma do navegador, para que os sites tenham mais probabilidade de permanecer em inglês.",
+ preferEnglishContentLockedTagTitle: (locale: string) =>
+ `${locale} é inserido pela preferência de idioma do navegador em inglês e não pode ser removido aqui.`,
+ accuracyDescription:
+ "Controla o valor de precisão que os sites veem nos resultados de geolocalização.",
+ noiseRadiusDescription:
+ "Distância máxima permitida entre as coordenadas simuladas e o ponto salvo deste perfil regional.",
+ },
+
+ generator: {
+ title: "Gerar perfil regional",
+ searchStepDescription:
+ "Pesquise uma cidade, endereço ou nome de lugar para criar um novo perfil regional.",
+ resultStepDescription: `Escolha o resultado correspondente antes que ${BRAND_DISPLAY_NAME} crie o perfil regional.`,
+ languageStepDescription:
+ "Escolha o idioma do navegador que deve ancorar este perfil regional antes de revisar o restante.",
+ confirmStepDescription: "Confirme o perfil regional no mapa antes de adicioná-lo.",
+ locationLabel: "Localização",
+ locationPlaceholder: "Varsóvia, Polônia",
+ osmDisclaimer:
+ "As consultas de pesquisa usam a API externa Nominatim do OpenStreetMap. Visualizações interativas usam tiles vetoriais e fontes do OpenFreeMap após consentimento.",
+ resultSelectLabel: "Resultado da pesquisa",
+ resultStepBody:
+ "O OpenStreetMap retornou mais de uma correspondência possível. Selecione o local que você quis dizer e depois continue.",
+ resultStepHint: "Escolha um resultado para continuar.",
+ languageSelectLabel: "Idioma do navegador",
+ languageStepBody:
+ "Mais de um idioma do navegador está disponível para este local. Escolha o que você quer que este perfil regional use.",
+ languageStepHint:
+ "Se o idioma que você deseja não estiver listado abaixo, você pode escolher um diferente na próxima etapa.",
+ resultPrefix: "Resultado: ",
+ },
+} as const;
diff --git a/src/ui/i18n/pt-sections/options.ts b/src/ui/i18n/pt-sections/options.ts
new file mode 100644
index 0000000..214c0fd
--- /dev/null
+++ b/src/ui/i18n/pt-sections/options.ts
@@ -0,0 +1,124 @@
+import { workerHandlingModeCopy } from "./shared-worker";
+
+import { BRAND_DISPLAY_NAME } from "@/shared/brand";
+
+export const options = {
+ title: "Configurações",
+ tabsAriaLabel: "Seções de configurações",
+ spoofingOffBannerTitle: "Simulação está desativada",
+ spoofingOffBannerBody: `${BRAND_DISPLAY_NAME} não está aplicando valores de localização ou de navegador simulados neste momento.`,
+ spoofingOffBannerAction: "Ativar simulação novamente",
+ spoofingTurnedOffToast: `${BRAND_DISPLAY_NAME} desativado.`,
+ spoofingTurnedOnToast: `${BRAND_DISPLAY_NAME} ativado.`,
+
+ tabs: {
+ locations: "Perfis regionais",
+ rules: "Regras de Domínio",
+ trustedSites: "Sites Confiáveis",
+ playground: "Área de testes",
+ options: "Opções",
+ advanced: "Avançado",
+ about: "Sobre",
+ },
+} as const;
+
+export const optionsPage = {
+ browserFingerprintSpoofing: {
+ title: "Configurações de proteção global",
+ description: `Escolha quais proteções do navegador o ${BRAND_DISPLAY_NAME} usa por padrão. Configurações mais específicas podem substituir proteções individuais, enquanto Sites Confiáveis podem desativá-las para sites correspondentes.`,
+ disabledNote: `As proteções do navegador estão desativadas em todos os lugares. Ative-as novamente para escolher o que o ${BRAND_DISPLAY_NAME} deve simular por padrão.`,
+ copyLinkLabel: "proteções do navegador",
+ items: {
+ geolocation: {
+ label: "Geolocalização",
+ description: `Controla a API de Geolocalização. Quando isto está ligado, ${BRAND_DISPLAY_NAME} responde a solicitações de localização com seu perfil regional ativo. Desative e os sites obtêm o resultado real de geolocalização do navegador.`,
+ advancedButton: "Avançado",
+ advancedModal: {
+ title: "Configurações avançadas de geolocalização",
+ description:
+ "Ajuste o raio de coordenadas padrão e o tempo de atualização da localização.",
+ },
+ },
+ timeLocale: {
+ label: "Hora e Localidade",
+ description: `Controla Date, Intl, navigator.language, navigator.languages e cabeçalhos de idioma. Quando isto está ligado, ${BRAND_DISPLAY_NAME} mantém esses valores alinhados com seu perfil regional ativo. Desative e os sites veem as configurações regionais reais do seu navegador.`,
+ },
+ canvas: {
+ label: "Tela de desenho (Canvas)",
+ description: `Sites podem desenhar uma imagem oculta e usar pequenas diferenças de renderização para reconhecer seu navegador. Quando isso está ativado, ${BRAND_DISPLAY_NAME} adiciona ruído controlado para que o resultado seja mais difícil de reutilizar como uma impressão digital. Desative e os sites receberão a saída real do seu canvas.`,
+ },
+ webGL: {
+ label: "WebGL",
+ description: `WebGL revela detalhes gráficos, como o modelo da sua GPU, o renderizador e o comportamento do driver. Quando isso está ativado, ${BRAND_DISPLAY_NAME} substitui essas pistas por um perfil consistente que se adapta à sua plataforma atual. Desative e os sites poderão ler dados gráficos mais próximos da sua máquina real.`,
+ },
+ audio: {
+ label: "Áudio",
+ description: `APIs de áudio produzem pequenas diferenças específicas do hardware que os sites podem medir em segundo plano. Quando isso está ativado, ${BRAND_DISPLAY_NAME} altera levemente esses valores para que sejam menos úteis para rastreamento. Desative e a saída de áudio permanece inalterada.`,
+ },
+ navigator: {
+ label: "navigator",
+ description: `Os campos do navegador revelam detalhes como sua plataforma, configuração de idioma, dicas de CPU e outros dados de identidade do navegador. Quando isto está ativado, ${BRAND_DISPLAY_NAME} oculta campos de identidade selecionados enquanto mantém a consistência interna. Desative-o e os sites verão mais da sua identidade real do navegador.`,
+ },
+ screen: {
+ label: "Tela",
+ description: `Tamanho da tela, proporção de pixels e profundidade de cor ajudam a restringir você a um pequeno grupo de dispositivos. Quando isto está ativado, ${BRAND_DISPLAY_NAME} oculta esses detalhes exatos de exibição. Desative-o e os sites obterão suas propriedades reais da tela.`,
+ },
+ clientHints: {
+ label: "Client Hints",
+ description: `Os Client Hints compartilham a versão do navegador, a plataforma e dados do dispositivo por meio de cabeçalhos de requisição e APIs JavaScript. Quando esta proteção está ativada, ${BRAND_DISPLAY_NAME} mantém esses valores coerentes com sua identidade de navegador simulada. Desative-a e os sites receberão os dados reais do navegador e da plataforma.`,
+ },
+ battery: {
+ label: "Bateria",
+ description: `O status da bateria pode revelar informações do dispositivo em mudança que os sites podem usar para vincular visitas. Quando isto está ativado, ${BRAND_DISPLAY_NAME} relata uma bateria cheia, carregando, em vez do estado real do dispositivo. Desative-o e os sites podem ler a API nativa de Status da Bateria.`,
+ },
+ clientHintsVersionRotation: {
+ label: "Rotacionar números de build e patch",
+ description: (count: number) =>
+ `Alterna os números de compilação e patch do Chromium expostos pelos Client Hints, mantendo a versão principal do navegador instalado. ${BRAND_DISPLAY_NAME} inclui um catálogo de compilações recentes do Chromium; para esta versão do navegador, pode escolher entre ${count} ${count === 1 ? "opção compatível" : "opções compatíveis"}. A string reduzida do User-Agent permanece no formato nativo do navegador, .0.0.0.`,
+ hintPrefix: "ex.:",
+ hint: "139.0.[build].[patch]",
+ },
+ webRTC: {
+ label: "WebRTC",
+ description: `O WebRTC pode revelar endereços IP locais ou públicos, mesmo quando você usa uma VPN ou proxy. Quando isso está ativo, ${BRAND_DISPLAY_NAME} solicita ao navegador que use seu modo mais restrito de gerenciamento de IP. Desative-o e o WebRTC se comporta normalmente, o que pode expor mais detalhes da rede.`,
+ },
+ serviceWorker: {
+ label: "Service Workers",
+ description: `Os Service Workers são executados em segundo plano e podem permitir que sites mantenham dados e identificadores de longa duração. Ative isto quando ${BRAND_DISPLAY_NAME} deve bloquear sites de registrar Service Workers em todos os lugares por padrão.`,
+ warning:
+ "Bloquear Service Workers pode impedir que aplicativos web instaláveis (PWAs), modo offline, notificações push e sincronização em segundo plano funcionem. Alguns aplicativos também podem funcionar mais lentamente ou perder recursos.",
+ defaultState:
+ "O bloqueio está desativado por padrão — você pode ativá-lo globalmente e depois permitir para domínios específicos com uma Regra de Domínio.",
+ allow: "Permitir",
+ block: "Bloquear",
+ },
+ sharedWorker: {
+ label: "Workers Dedicados & Compartilhados",
+ descriptionLead:
+ "Escolha como os Workers Dedicados e Compartilhados são executados por padrão. Regras de Domínio e Contêineres do Firefox podem substituir esta política para sites correspondentes.",
+ copyLinkLabel: "Manipulação de Workers Dedicados e Compartilhados",
+ 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: "Mostrar contagem de chamadas no badge da extensão",
+ description: `Mostrar quantas chamadas da API do navegador ${BRAND_DISPLAY_NAME} lidou no badge da extensão em vez do rótulo de texto.`,
+ includeDateCalls: {
+ label: "Incluir chamadas de API Date e Temporal",
+ description:
+ "Incluir chamadas Date.* e Temporal.* no número do badge. Desative isto para evitar que verificações de tempo frequentes aumentem a contagem.",
+ },
+ },
+ copyLinkHelpLabel: "ajuda de opções",
+ help: {
+ title: "Opções",
+ body1: `As proteções do navegador permitem escolher quais valores ${BRAND_DISPLAY_NAME} simula por padrão. Essas configurações afetam todos os sites, a menos que uma Regra de Domínio ou um Contêiner do Firefox determine o contrário.`,
+ body2: `Os controles de privacidade cobrem apenas as solicitações de rede opcionais que ${BRAND_DISPLAY_NAME} faz enquanto você configura locais. Deixe-os desativados e ${BRAND_DISPLAY_NAME} ainda funcionará — você apenas adicionará locais manualmente em vez de usar a pesquisa ou pré-visualizações de mapas.`,
+ },
+} as const;
diff --git a/src/ui/i18n/pt-sections/osm.ts b/src/ui/i18n/pt-sections/osm.ts
new file mode 100644
index 0000000..6bca4f0
--- /dev/null
+++ b/src/ui/i18n/pt-sections/osm.ts
@@ -0,0 +1,12 @@
+import { BRAND_DISPLAY_NAME } from "@/shared/brand";
+
+export const externalMapConsentCopy = {
+ title: "Permitir busca de localização e solicitações de mapa",
+ description: `Permitir que ${BRAND_DISPLAY_NAME} envie consultas de pesquisa para o OpenStreetMap Nominatim quando você pesquisar locais e solicite tiles vetoriais e fontes do OpenFreeMap quando pré-visualizações de mapas forem exibidas. Esses serviços recebem seu texto de pesquisa, solicitações de mapa e fontes, e seu endereço IP, mas não suas regras salvas, perfis regionais ou histórico de navegação. Mantenha isso desativado se preferir inserir coordenadas manualmente sem fazer solicitações de serviços de mapas durante a configuração.`,
+} as const;
+
+export const osm = {
+ modalTitle: "Permitir solicitações externas de mapa e pesquisa?",
+ body1: `O acesso externo a mapas é opcional. O próprio ${BRAND_DISPLAY_NAME} não envia seus perfis regionais, regras nem dados de navegação a esses serviços.`,
+ body2: `Se você permitir isso, ${BRAND_DISPLAY_NAME} poderá contatar o OpenStreetMap Nominatim para pesquisa de localização e o OpenFreeMap para pré-visualizações de mapa interativas.`,
+} as const;
diff --git a/src/ui/i18n/pt-sections/popup.ts b/src/ui/i18n/pt-sections/popup.ts
new file mode 100644
index 0000000..6dc26f7
--- /dev/null
+++ b/src/ui/i18n/pt-sections/popup.ts
@@ -0,0 +1,252 @@
+import type { SurfacePresentationState } from "@privacy-brand/xray-protocol";
+
+import { BRAND_DIAGNOSTICS_NAME, BRAND_DISPLAY_NAME } from "@/shared/brand";
+
+const formatCount = (count: number, singular: string, plural: string): string =>
+ `${count} ${count === 1 ? singular : plural}`;
+
+export const popup = {
+ loading: "Carregando…",
+ protectionProtected: "Protegido",
+ protectionOff: "Desativado",
+ protectionDisabled: "Proteções desabilitadas",
+ protectionNeedsAttention: "Precisa de atenção",
+ protectionUnknown: "Status da proteção indisponível",
+ protectionSourceSiteRule: "Regra de Domínio",
+ protectionSourceContainer: "Contêiner Firefox",
+ protectionSourceDefaultRule: "Regra Padrão",
+ protectionSourceTrustedSite: "Site Confiável",
+ protectionSourceNone: "Nenhuma regra ativa",
+ protectionSourceGlobalSetting: "Configuração global",
+ languagePriorityTooltip: (languages: readonly string[]) =>
+ [
+ "Prioridade de idioma:",
+ ...languages.map((language, index) => `${index + 1}. ${language}`),
+ ].join("\n"),
+ protectionViewDetails: "Ver detalhes",
+ protectionViewPageActivity: "Ver atividade da página",
+ protectionDetailsTitle: "Detalhes de proteção",
+ protectionDetailsLead: `Veja como ${BRAND_DISPLAY_NAME} lida com cada recurso do navegador neste site.`,
+ protectionGroupLocationLocale: "Localização, hora e idioma",
+ protectionGroupBrowserIdentity: "Identidade do navegador",
+ protectionGroupRenderingMedia: "Renderização e mídia",
+ protectionGroupWorkers: "Web Workers",
+ protectionStateProtected: "Protegido",
+ protectionStateDegraded: "Degradado",
+ protectionStateNative: "Não modificado",
+ protectionStateMixed: "Misto",
+ protectionStatePending: "Confirmando…",
+ protectionStateRepaired: "Proteção restaurada",
+ protectionStateBrowserEnforced: "Protegido pelo navegador",
+ protectionStateUnrecoverable: "Falha na proteção",
+ protectionStateNotApplicable: "Não aplicável",
+ protectionStateCompatibility: "Problema de compatibilidade",
+ protectionSurfaceUnknown: (surface: string) =>
+ `O status de ${surface} é desconhecido.`,
+ protectionPageMayNotWork: "Esta página pode não funcionar corretamente",
+ protectionServiceWorkerBlockTitle: "Service Workers estão bloqueados",
+ protectionServiceWorkerBlockSummary: `${BRAND_DISPLAY_NAME} está bloqueando o registro de Service Worker neste site.`,
+ protectionServiceWorkerBlockDescription:
+ "O modo offline, notificações push, sincronização em segundo plano e outros recursos do Service Worker podem não funcionar enquanto o bloqueio estiver ativado.",
+ protectionSharedWorkerStrictTitle: "O modo estrito para Shared Workers está ativado",
+ protectionSharedWorkerStrictSummary: `${BRAND_DISPLAY_NAME} bloqueia qualquer Shared Worker que não possa simular antes de começar.`,
+ protectionSharedWorkerStrictDescription:
+ "Sincronização entre abas, conexões compartilhadas e colaboração ao vivo podem não funcionar enquanto o modo estrito estiver ativado.",
+ notificationsTitle: "Notificações",
+ notificationsThisSite: "Este site",
+ notificationsExtension: BRAND_DISPLAY_NAME,
+ notificationsPreviousUpdates: "Atualizações anteriores",
+ notificationsDismissed: "Descartadas",
+ notificationsResolved: "Resolvidas",
+ notificationsNew: "Novas",
+ notificationsAcknowledged: "Lidas",
+ notificationsStillActive: "Precisa de atenção",
+ notificationsEmpty: "Nenhuma notificação ativa.",
+ notificationsDismiss: "Descartar",
+ notificationsKeepStrictMode: "Manter modo estrito",
+ notificationsWhatThisAffects: "O que isso afeta",
+ notificationsBeforeYouContinue: "Antes de continuar",
+ notificationsChooseSharedWorkerMode:
+ "Escolha como os Workers Compartilhados são executados neste site",
+ notificationsCspTitle: "Este site bloqueou a simulação do worker",
+ notificationsCspSummary: `${BRAND_DISPLAY_NAME} não conseguiu proteger um worker neste site.`,
+ notificationsSharedWorkerTitle: "Falha na simulação do Worker Compartilhado",
+ notificationsSharedWorkerSummary: `${BRAND_DISPLAY_NAME} não conseguiu aplicar valores simulados antes que um Worker Compartilhado fosse iniciado.`,
+ notificationsRelaxWorkerPolicy: "Permitir simulação de worker",
+ notificationsAllowServiceWorkers: "Permitir Service Workers",
+ notificationsSharedWorkerNative: "Nativo",
+ notificationsSharedWorkerNativeDescription: `Executa Workers Compartilhados normalmente, sem a proteção do ${BRAND_DISPLAY_NAME}. Isso oferece a melhor compatibilidade, mas um worker pode ler os valores reais do seu navegador.`,
+ notificationsSharedWorkerSpoof: "Simular",
+ notificationsSharedWorkerSpoofDescription: `Tenta um método alternativo de proteção para Workers Compartilhados. Alguns workers ainda podem falhar ao iniciar.`,
+ notificationsUpdateTitle: `${BRAND_DISPLAY_NAME} foi atualizado`,
+ notificationsUpdateSummary: "Revise as alterações nesta versão.",
+ notificationsVersionLabel: "Versão",
+ notificationsOpenLink: "Abrir link",
+ notificationsBadgeLabel: (count: number) =>
+ formatCount(count, "notificação não lida", "notificações não lidas"),
+ 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
+ ? [formatCount(protectedCount, "protegido", "protegidos")]
+ : []),
+ ...(degradedCount > 0
+ ? [formatCount(degradedCount, "degradado", "degradados")]
+ : []),
+ ...(counts.pending > 0 ? [`${counts.pending} confirmando`] : []),
+ ...(counts["native-by-policy"] > 0
+ ? [formatCount(counts["native-by-policy"], "não modificado", "não modificados")]
+ : []),
+ ...(counts["not-applicable"] > 0
+ ? [formatCount(counts["not-applicable"], "não aplicável", "não aplicáveis")]
+ : []),
+ ...(counts.unknown > 0
+ ? [formatCount(counts.unknown, "desconhecido", "desconhecidos")]
+ : []),
+ ].join(" · ");
+ },
+ protectionException: (_surface: string) =>
+ "Esta página pode não funcionar corretamente",
+ unsupportedTab: "Página restrita",
+ noLocationYet: "Nenhum perfil regional ativo",
+ noPresetAssigned: "Nenhum perfil regional atribuído",
+ regularPageRequired: `${BRAND_DISPLAY_NAME} não pode acessar esta página.`,
+ openXRay: `Veja a atividade deste site em ${BRAND_DIAGNOSTICS_NAME}`,
+ saveLabelCreate: "Criar",
+ saveLabelSave: "Salvar",
+ ruleConflictConfirm: (pattern: string) =>
+ `Uma Regra de Domínio para “${pattern}” já existe. Substituí-la por estas configurações?`,
+ ruleConflictTitle: "Substituir Regra de Domínio?",
+ ruleConflictReplace: "Substituir",
+ popupDataUnavailable: "Não foi possível carregar o status deste site.",
+ retryLabel: "Tentar novamente",
+ dismissLabel: "Descartar",
+ mutationFailed: `${BRAND_DISPLAY_NAME} não pôde completar essa ação. Tente novamente.`,
+ enableExtensionFailed: `Não foi possível ativar o ${BRAND_DISPLAY_NAME}.`,
+ trustSiteFailed: "Não foi possível adicionar este site aos Sites Confiáveis.",
+ firefoxPermissionFailed:
+ "Não foi possível conceder permissão ao userScripts do Firefox.",
+ powerControlExtension: BRAND_DISPLAY_NAME,
+ powerControlGlobalProtections: "Proteções globais",
+ powerTargetLoading: "Verificando as configurações deste site…",
+ powerTargetGlobal: `${BRAND_DISPLAY_NAME} está desativado em todos os sites.`,
+ powerTargetGlobalProtectionsDisabled:
+ "Ative-as nas Configurações para usar suas regras e seus perfis regionais salvos.",
+ powerTargetUnsupported: `${BRAND_DISPLAY_NAME} não pode acessar esta página.`,
+ powerTargetTrustedSite: `${BRAND_DISPLAY_NAME} está desativado porque este site corresponde aos Sites Confiáveis.`,
+ powerTargetSiteRule: "Ativa ou desativa esta Regra de Domínio.",
+ powerTargetContainer: "Ativa ou desativa esta atribuição de Contêiner do Firefox.",
+ powerTargetContainerSetup: `Configure o ${BRAND_DISPLAY_NAME} para este Contêiner do Firefox.`,
+ powerTargetContainerDefaultRule: "Este Contêiner do Firefox usa a Regra Padrão.",
+ powerTargetDefaultRule: "Controla sites sem uma Regra de Domínio ou Site Confiável.",
+ powerAriaLoading: "Verificando as configurações deste site",
+ powerAriaGlobalOff: `${BRAND_DISPLAY_NAME} está desativado em todos os sites`,
+ powerAriaGlobalProtectionsDisabled:
+ "As proteções globais estão desativadas. Ative-as nas Configurações.",
+ powerAriaRestricted: `${BRAND_DISPLAY_NAME} não pode acessar esta página`,
+ powerAriaTrustedSite: `Ativar ${BRAND_DISPLAY_NAME} para este site`,
+ powerAriaDomainRuleOn: "Ativar esta Regra de Domínio",
+ powerAriaDomainRuleOff: "Desativar esta Regra de Domínio",
+ powerAriaContainerOn: "Ativar esta atribuição de Contêiner do Firefox",
+ powerAriaContainerOff: "Desativar esta atribuição de Contêiner do Firefox",
+ powerAriaContainerSetup: `Configurar ${BRAND_DISPLAY_NAME} para este Contêiner do Firefox`,
+ powerAriaDefaultRuleOn: "Ativar a Regra Padrão",
+ powerAriaDefaultRuleOff: "Desativar a Regra Padrão",
+
+ sheetTitle: "Regra de Domínio",
+ currentProfileLabel: "Perfil regional",
+ noPresetLabel: "Sem perfil regional",
+ inheritedDefaultRuleProfileLabel: "Usar perfil regional da Regra Padrão",
+ inheritedContainerProfileLabel: "Usar perfil regional de Contêiner do Firefox",
+ inheritedDomainRuleProfileLabel:
+ "Usar perfil regional de Regra de Domínio correspondente",
+ ruleTypeLabel: "Aplica-se a",
+ ruleTypeExact: "Host exato",
+ ruleTypeSuffix: "Host + subdomínios",
+ workerHandlingLabel: "Workers Dedicados & Compartilhados",
+ workerHandlingHint:
+ "Escolha um modo para esta regra. Herdar usa a configuração global. Nativo permite que os workers sejam executados com valores reais do navegador. Simular tenta protegê-los. Rigoroso bloqueia um worker quando a proteção não pode ser confirmada antes da inicialização.",
+ workerHandlingInherit: "Herdar",
+ workerHandlingNative: "Nativo",
+ workerHandlingSpoof: "Simular",
+ workerHandlingStrict: "Rigoroso",
+ viewXRay: BRAND_DIAGNOSTICS_NAME,
+ deleteButtonLabel: "Excluir",
+ sheetLead:
+ "Escolha onde esta Regra de Domínio se aplica e qual perfil regional ela usa. Alterar o perfil regional recarrega a página.",
+ cleanupSheetTitle: "Nova identidade",
+ cleanupConfirmTitle: "Criar nova identidade?",
+ cleanupPlanLoading: "Verificando o que este navegador pode limpar…",
+ cleanupPlanDescription: (available: string, unavailable: string) =>
+ unavailable
+ ? `${BRAND_DISPLAY_NAME} irá limpar ${available}. Este navegador não pode limpar ${unavailable} aqui.`
+ : `${BRAND_DISPLAY_NAME} limpará ${available}.`,
+ cleanupResultTitle: "Nova identidade criada",
+ cleanupResultComplete: `Os dados do navegador disponíveis para ${BRAND_DISPLAY_NAME} foram limpos.`,
+ cleanupResultPartial:
+ "Alguns dados do navegador foram limpos, mas este navegador não disponibilizou todos os itens.",
+ cleanupResultFailed: `A identidade mudou, mas ${BRAND_DISPLAY_NAME} não conseguiu limpar os dados do navegador associados. Você pode tentar novamente sem mudar a identidade.`,
+ cleanupSurfaceCookies: "cookies",
+ cleanupSurfaceLocalStorage: "armazenamento local",
+ cleanupSurfaceIndexedDb: "IndexedDB",
+ cleanupSurfaceCacheStorage: "Cache Storage",
+ cleanupSurfaceServiceWorkers: "Service Workers",
+ cleanupSurfacePageStorage: "armazenamento em páginas abertas",
+ cleanupStatusCleaned: "Limpo",
+ cleanupStatusSkipped: "Indisponível",
+ cleanupStatusFailed: "Falhou",
+ cleanupConfirmLabel: "Criar nova identidade",
+ deleteConfirmTitle: "Excluir regra de domínio?",
+ deleteConfirmDescription: (pattern: string) =>
+ `Isso remove a regra de domínio salva para “${pattern}”.`,
+ cleanDomainAriaLabel: "Crie uma nova identidade para este site",
+ cleanDomainLabel: "Nova identidade",
+ cleanDomainDefaultRuleDisabled: "Indisponível para a Regra Padrão",
+ cleanDomainProductOffDisabled: `Indisponível enquanto ${BRAND_DISPLAY_NAME} estiver desativado para este site`,
+ cleanDomainContainerSetupDisabled: "Configure este Contêiner do Firefox primeiro",
+ cleanDomainPageDisabled: "Indisponível nesta página",
+ cleanDomainLoadingDisabled: "Disponível após o site terminar de carregar",
+ cleanDomainAvailabilityHint:
+ "Nova identidade funciona com uma Regra de Domínio ou um Contêiner do Firefox configurado.",
+ openSettingsAriaLabel: "Abrir configurações",
+ openFullRuleSettings: "Abrir em Regras de Domínio",
+ settingsLabel: "Configurações",
+ closeSheetAriaLabel: "Fechar Regra de Domínio",
+ trustedSiteCta: "Editar Site Confiável",
+ disableOnSiteCta: "Adicionar a Sites Confiáveis",
+ enableTrustCta: "Adicionar a Sites Confiáveis",
+ extensionOffCardAction: `Ativar ${BRAND_DISPLAY_NAME}`,
+ surfaceGeolocation: "Geolocalização",
+ surfaceTimeLocale: "Hora e Localidade",
+ surfaceNavigator: "navigator",
+ surfaceScreen: "Tela",
+ surfaceClientHints: "Client Hints",
+ surfaceBattery: "Bateria",
+ surfaceCanvas: "Tela de desenho (Canvas)",
+ surfaceWebGL: "WebGL",
+ surfaceAudio: "Áudio",
+ surfaceWebRTC: "WebRTC",
+ surfaceWorker: "Workers Dedicados",
+ surfaceServiceWorker: "Service Workers",
+ surfaceSharedWorker: "Workers Compartilhados",
+ addExactOverrideCta: "Adicionar Regra de Site",
+ containerSetupCta: "Configurar Contêiner",
+ globalFallbackRuleCta: "Editar Regra Padrão",
+ editDomainRuleLabel: "Editar Regra de Domínio",
+ editContainerLabel: "Editar Contêiner",
+ firefoxFirstInlinePermissionTitle:
+ "Melhorar a simulação no primeiro carregamento no Firefox",
+ firefoxFirstInlinePermissionDescription: `O Firefox pode expor sua hora real e local para o primeiro script da página. Conceda a permissão opcional userScripts para que ${BRAND_DISPLAY_NAME} possa começar a simular mais cedo.`,
+ firefoxFirstInlinePermissionEnableLabel: "Conceder permissão",
+ detailsAbout: (label: string) => `Detalhes sobre ${label}`,
+ relaxCspHint: `Isso remove os cabeçalhos da Política de Segurança de Conteúdo do site quando eles impedem que ${BRAND_DISPLAY_NAME} proteja os workers. Isso enfraquece a proteção do site contra scripts maliciosos; habilite apenas para um site em que você confia.`,
+ advancedSectionTitle: "Avançado",
+ suggestionWorkerCspDescription:
+ "A Política de Segurança de Conteúdo (CSP) deste site bloqueou a simulação de worker. Permitir a simulação de worker irá relaxar essa política e enfraquecer a proteção do site contra scripts injetados. Continue apenas se você confiar neste site.",
+ suggestionSharedWorkerInjectionDescription: `${BRAND_DISPLAY_NAME} não pôde proteger este Worker Compartilhado antes de ele começar. Escolha se a compatibilidade ou a proteção é mais importante neste site.`,
+} as const;
diff --git a/src/ui/i18n/pt-sections/rules.ts b/src/ui/i18n/pt-sections/rules.ts
new file mode 100644
index 0000000..d9d2027
--- /dev/null
+++ b/src/ui/i18n/pt-sections/rules.ts
@@ -0,0 +1,233 @@
+import { BRAND_DISPLAY_NAME } from "@/shared/brand";
+
+const PRODUCT_NOT_LOWER = `o que ${BRAND_DISPLAY_NAME} não é`;
+
+export const rules = {
+ title: "Regras de Domínio",
+ hint: "Regras de Domínio mapeiam padrões de host para perfis regionais.",
+ addButton: "Adicionar regra",
+ filterLabel: "Filtrar regras",
+ filterPlaceholder: "Pesquisar perfis regionais, padrões de domínio, avisos",
+ locationFilterLabel: "Filtrar por perfil regional",
+ locationFilterPlaceholder: "Todos os perfis regionais",
+ assignLocationLabel: "Atribuir perfil regional",
+ tableHeadRule: "Regra",
+ tableHeadProfile: "Perfil regional",
+ tableHeadActions: "Ações",
+ selectAllAriaLabel: "Selecionar todas as regras visíveis",
+ selectMenuAriaLabel: "Abrir menu de seleção de regras",
+ selectRuleAriaLabel: (pattern: string) => `Selecionar regra ${pattern}`,
+ editRuleAriaLabel: (pattern: string) => `Editar regra ${pattern}`,
+ editRuleTitle: "Editar regra",
+ deleteRuleAriaLabel: (pattern: string) => `Excluir regra ${pattern}`,
+ deleteRuleTitle: "Excluir regra",
+ inactiveBadge: "inativo",
+ selectionMenuAllVisible: "Todos visíveis",
+ selectionMenuAll: "Todos",
+ selectionMenuNone: "Nenhum",
+ selectionMenuActive: "Apenas ativos",
+ selectionMenuInactive: "Apenas inativos",
+ noRulesFiltered: "Nenhuma regra corresponde ao filtro atual.",
+ noRulesEmpty: `Ainda não há Regras de Domínio. Adicione uma para escolher como ${BRAND_DISPLAY_NAME} trata os sites correspondentes.`,
+ copyLinkLabel: "regras de domínio",
+ copyLinkHelpLabel: "ajuda de regras de domínio",
+ copyLinkInspectorLabel: "inspetor de hostname",
+ copyLinkRuleAriaLabel: (pattern: string) => `Copiar link da regra ${pattern}`,
+
+ help: {
+ title: "Regras de Domínio",
+ body1:
+ "Use example.com para um host exato. Use *example.com para esse host e qualquer subdomínio, como www.example.com. Use *.example.com apenas para subdomínios.",
+ body2: `O padrão de correspondência mais específico tem prioridade. Se duas regras se sobrepuserem e apontarem para perfis regionais diferentes, ${BRAND_DISPLAY_NAME} mostrará um aviso.`,
+ },
+
+ globalFallback: {
+ title: "Regra Padrão",
+ description: `Defina as proteções padrão e o perfil regional opcional que ${BRAND_DISPLAY_NAME} usa quando nada mais específico tem prioridade.`,
+ copyLinkLabel: "Regra Padrão",
+ overridesBadge: (count: number) =>
+ `${count} ${count === 1 ? "configuração personalizada" : "configurações personalizadas"}`,
+ openInRules: "Editar em Regras de Domínio",
+ editAriaLabel: "Editar Regra Padrão",
+ editTitle: "Editar Regra Padrão",
+ noPresetLabel: "Nenhum perfil regional atribuído",
+ setupHint: "Nenhuma configuração de proteção pré-definida ou personalizada ainda.",
+ tableHint: "Configurações padrão quando nada mais específico se aplica.",
+ dialog: {
+ title: "Regra Padrão",
+ description:
+ "Defina as proteções e o perfil regional opcional que a Regra Padrão deve usar quando nada mais específico tiver prioridade.",
+ identityDescription:
+ "A Regra Padrão mantém sua própria identidade de simulação fixa. Não pode ser alterada manualmente.",
+ enabledLabel: "Ativado",
+ enabledHint:
+ "Quando desativada, esta regra não se aplica. Suas configurações permanecem salvas.",
+ enabledAriaLabel: "Alternar a Regra Padrão",
+ locationProfileLabel: "Perfil regional",
+ locationProfileHint:
+ "Escolha o perfil regional que a Regra Padrão deve usar. Deixe sem atribuição para usar apenas as configurações de proteção abaixo.",
+ locationProfileWarningPrefix: `${BRAND_DISPLAY_NAME} não substitui VPN, proxy ou ferramentas de DNS. `,
+ locationProfileWarningLinkLabel: `Veja ${PRODUCT_NOT_LOWER}`,
+ locationProfileWarningSuffix: ".",
+ locationLabel: "Perfil regional",
+ locationPlaceholder: "Escolher perfil regional",
+ submit: "Salvar regra",
+ },
+ },
+
+ inspector: {
+ title: "Inspetor de hostname",
+ hint: `Verifique como o ${BRAND_DISPLAY_NAME} trata um nome de host antes de salvar as alterações. Ele mostra se o nome de host corresponde a uma Regra de Domínio ou Site Confiável, e qual configuração regional se aplicaria.`,
+ hostnameLabel: "Nome do Host",
+ hostnameHint:
+ "Use o host exato que deseja inspecionar, por exemplo, shop.example.com.",
+ hostnamePlaceholder: "ex.: shop.example.com",
+ noMatchTitle:
+ "Nenhuma Regra de Domínio ou Site Confiável salvo corresponde a este nome de host",
+ noMatchDescription:
+ "Nenhuma regra salva se aplica, e a Regra Padrão está desligada ou não configurada.",
+ trustedSiteWinsTitle: "Este nome de host está desativado pelos Sites Confiáveis",
+ trustedSiteWinsDescription: `Ele corresponde à sua lista de Sites Confiáveis, então o ${BRAND_DISPLAY_NAME} permanece desligado aqui até que você remova ou desative essa entrada.`,
+ trustedSiteOverridesRuleTitle:
+ "Sites Confiáveis estão substituindo uma Regra de Domínio correspondente",
+ trustedSiteOverridesRuleDescription: `Este nome de host corresponde tanto a um Site Confiável quanto a uma Regra de Domínio. Sites Confiáveis têm prioridade, portanto ${BRAND_DISPLAY_NAME} permanece desligado e a Regra de Domínio abaixo é ignorada.`,
+ fallbackWinsTitle: "Regra Padrão se aplica aqui",
+ fallbackWinsDescription: `Nenhuma Regra de Domínio ou Site Confiável correspondeu a este nome de host, então ${BRAND_DISPLAY_NAME} cairia de volta para a Regra Padrão aqui.`,
+ ruleMatchTitle: (locationLabel: string) =>
+ `${locationLabel} é o perfil regional ativo aqui`,
+ ruleMatchDescription: `Este nome de host corresponde à Regra de Domínio abaixo, então ${BRAND_DISPLAY_NAME} usaria este perfil regional no site.`,
+ hostnameDetailLabel: "Nome do Host",
+ trustedSiteDetailLabel: "Site Confiável",
+ ruleDetailLabel: "Regra",
+ defaultRuleDetailLabel: "Regra Padrão",
+ ignoredRuleDetailLabel: "Regra Ignorada",
+ profileDetailLabel: "Perfil regional",
+ geolocationDetailLabel: "Geolocalização",
+ localeDetailLabel: "Localidade",
+ timeZoneDetailLabel: "Fuso horário",
+ geolocationOn: "Ligado",
+ geolocationOff: "Desligado",
+ },
+
+ dialog: {
+ titleAdd: "Adicionar regra",
+ titleEdit: "Editar regra",
+ description:
+ "Escolha onde esta regra se aplica, e então decida se deve usar um perfil regional, configurações de proteção personalizadas ou ambos.",
+ patternLabel: "Padrão",
+ patternInfo:
+ "Use example.com para um host exato. Use *example.com para esse host e qualquer subdomínio, como www.example.com. Use *.example.com apenas para subdomínios.",
+ patternInfoAriaLabel: "Aprenda como os padrões de regra funcionam",
+ patternPlaceholder: "Insira um padrão de domínio",
+ locationLabel: "Perfil regional",
+ locationProfileLabel: "Perfil regional",
+ locationProfileHint: `Escolha o perfil regional que esta regra deve usar. Se nenhum estiver atribuído, ${BRAND_DISPLAY_NAME} usa o próximo perfil regional disponível enquanto mantém as configurações de proteção desta regra.`,
+ bulkAssignSearchPlaceholder: "Pesquisar perfis regionais...",
+ enabledLabel: "Ativado",
+ enabledHint:
+ "Quando desativada, esta regra não se aplica. Suas configurações permanecem salvas.",
+ enabledAriaLabel: (pattern: string) =>
+ `Alternar estado ativado para a regra ${pattern}`,
+ advancedModal: {
+ trigger: "Avançado",
+ title: (pattern: string) => `Configurações avançadas para ${pattern}`,
+ description:
+ "Essas alterações permanecem no rascunho atual até você salvar a regra.",
+ confirm: "Ok",
+ patternFallback: "esta regra",
+ },
+ relaxCspLabel: "Relaxar CSP para simulação de worker",
+ relaxCspHint: `Remove os cabeçalhos de Política de Segurança de Conteúdo deste site quando eles impedem que ${BRAND_DISPLAY_NAME} proteja os workers.`,
+ relaxCspRiskHint:
+ "Aviso de segurança: isso facilita a execução de scripts maliciosos no site. Ative-o apenas para um site em que você confie e apenas quando a proteção do worker falhar.",
+ relaxCspAriaLabel: (pattern: string) =>
+ `Alternar relaxamento de CSP para a regra ${pattern}`,
+ surfaceOverrides: {
+ title: "Configurações de proteção",
+ description:
+ "Escolha diferentes configurações de proteção para esta regra. Deixe uma configuração em Herdar para seguir a configuração global.",
+ stateOn: "Ligado",
+ stateInherit: "Herdar",
+ stateOff: "Desligado",
+ stateNative: "Nativo",
+ stateSpoof: "Simular",
+ stateStrict: "Rigoroso",
+ stateBlock: "Bloquear",
+ stateAllow: "Permitir",
+ helpAriaLabel: (label: string) => `Saiba o que ${label} controla`,
+ geolocation: {
+ label: "Geolocalização",
+ info: "Controla a API de Geolocalização. Desative quando um site deve ler sua localização real no navegador ou ative quando esta regra deve simular consultas de localização.",
+ },
+ timeLocale: {
+ label: "Hora e Localidade",
+ info: "Controla Date, Intl, navigator.language, navigator.languages e cabeçalhos de idioma para que os sites vejam a região do seu perfil regional ativo.",
+ },
+ canvas: {
+ label: "Tela de desenho (Canvas)",
+ info: "Controla os sites de saída de imagens ocultas usados para impressão digital de canvas.",
+ },
+ webGL: {
+ label: "WebGL",
+ info: "Controla detalhes gráficos como renderer, pistas de GPU e dados relacionados de impressão digital WebGL.",
+ },
+ audio: {
+ label: "Áudio",
+ info: "Controla a saída do AudioContext que os sites podem medir para impressão digital de áudio.",
+ },
+ navigator: {
+ label: "navigator",
+ info: "Controla campos de identidade do navegador, como plataforma, dicas de hardware e outras propriedades do navegador.",
+ },
+ screen: {
+ label: "Tela",
+ info: "Controla o tamanho da tela, a proporção de pixels e detalhes relacionados à exibição.",
+ },
+ clientHints: {
+ label: "Client Hints",
+ info: "Controla detalhes do navegador e do dispositivo compartilhados através de cabeçalhos e APIs do Client Hints.",
+ },
+ battery: {
+ label: "Bateria",
+ info: "Controla se o site recebe um perfil de bateria completo e cheio fixo em vez do estado real da bateria do dispositivo.",
+ },
+ webRTC: {
+ label: "WebRTC",
+ info: "Controla a proteção no gerenciamento de IP do WebRTC que pode reduzir vazamentos de IP local e público.",
+ },
+ serviceWorker: {
+ label: "Service Workers",
+ info: "Controla se este site pode registrar Service Workers, que podem rodar em segundo plano e manter dados de longa duração. Bloquear pode impedir PWAs, modo offline, notificações push e sincronização em segundo plano; Permitir deixa o navegador lidar com o registro normalmente; Herdar segue a configuração global.",
+ },
+ sharedWorker: {
+ label: "Workers Dedicados & Compartilhados",
+ info: "Substitui o gerenciamento de Dedicated e Shared Workers para esta regra. Nativo mantém os workers inalterados, Simular tenta aplicar valores simulados, e Estrito bloqueia um worker quando a simulação não pode ser confirmada antes da inicialização.",
+ },
+ },
+ identity: {
+ sectionTitle: "Identidade",
+ sectionDescription:
+ "Esta regra mantém sua própria identidade de simulação. Gere outra apenas quando quiser uma nova impressão digital e um estado limpo do site para esta regra.",
+ actionDescription:
+ "Limpa os dados relacionados ao site e inicia esta regra com uma identidade nova.",
+ actionLabel: "Nova identidade",
+ confirmTitle: (pattern: string) => `Nova identidade para ${pattern}?`,
+ confirmDescription:
+ "Isso limpa cookies, armazenamento, Service Workers e caches dos sites vinculados a esta regra. Em seguida, cria uma nova identidade de simulação.",
+ confirmDomainsLabel: `${BRAND_DISPLAY_NAME} limpará os dados do navegador para estes domínios:`,
+ confirmNoDomains: `Ainda não há dados do navegador registrados para esta regra. Mesmo assim, ${BRAND_DISPLAY_NAME} criará uma nova identidade de simulação.`,
+ confirmLabel: "Criar nova identidade",
+ rotateSuccess: "Nova identidade de regra salva.",
+ rotateError: "Falha ao criar uma nova identidade de regra.",
+ },
+ submitAdd: "Adicionar regra",
+ submitEdit: "Salvar",
+ duplicateAlertTitle: "Sobrescrever regra existente?",
+ duplicateAlertDescription: (pattern: string) =>
+ `Uma regra para "${pattern}" já existe. Quer sobrescrevê-la com estas configurações?`,
+ duplicateAlertConfirm: "Sobrescrever",
+ duplicateAlertClose: "Não",
+ trustedSiteOverrideWarning: (pattern: string) =>
+ `Este domínio corresponde à entrada Sites Confiáveis "${pattern}". ${BRAND_DISPLAY_NAME} permanecerá desativado aqui independentemente destas configurações.`,
+ },
+} as const;
diff --git a/src/ui/i18n/pt-sections/shared-worker.ts b/src/ui/i18n/pt-sections/shared-worker.ts
new file mode 100644
index 0000000..fcb8669
--- /dev/null
+++ b/src/ui/i18n/pt-sections/shared-worker.ts
@@ -0,0 +1,31 @@
+import { BRAND_DISPLAY_NAME } from "@/shared/brand";
+
+export const sharedWorkerModeCopy = {
+ native: {
+ label: "Nativo",
+ description: `Executa Workers Compartilhados normalmente, sem a proteção do ${BRAND_DISPLAY_NAME}. Isso oferece a melhor compatibilidade, mas um worker pode ler os valores reais do seu navegador.`,
+ },
+ spoof: {
+ label: "Simular",
+ description: `Tenta um método alternativo de proteção para Workers Compartilhados. Alguns workers ainda podem falhar ao iniciar.`,
+ },
+ strict: {
+ label: "Rigoroso",
+ description: `Bloqueia um Worker Compartilhado a menos que ${BRAND_DISPLAY_NAME} possa confirmar a simulação antes de ele começar. Isso impede que um Worker Compartilhado não simulado veja os valores nativos do navegador, mas recursos que dependem desse worker podem não funcionar.`,
+ },
+} as const;
+
+export const workerHandlingModeCopy = {
+ native: {
+ label: "Nativo",
+ description: `Executa Workers Dedicados e Compartilhados normalmente, sem a proteção de ${BRAND_DISPLAY_NAME}. Isso oferece a melhor compatibilidade, mas os workers podem ler seus valores reais do navegador.`,
+ },
+ spoof: {
+ label: "Simular",
+ description: `Tenta aplicar os valores simulados de ${BRAND_DISPLAY_NAME} antes que os workers comecem. Se a proteção não puder ser aplicada, um worker pode usar valores reais do navegador ou falhar ao iniciar.`,
+ },
+ strict: {
+ label: "Rigoroso",
+ description: `Bloqueia um worker antes da inicialização quando ${BRAND_DISPLAY_NAME} pode determinar que a simulação não pode ser confirmada. Se ocorrer uma falha após a inicialização, ${BRAND_DISPLAY_NAME} mostra uma notificação em vez disso. Recursos que dependem desses workers podem não funcionar.`,
+ },
+} as const;
diff --git a/src/ui/i18n/pt-sections/sidebar.ts b/src/ui/i18n/pt-sections/sidebar.ts
new file mode 100644
index 0000000..19ce0e4
--- /dev/null
+++ b/src/ui/i18n/pt-sections/sidebar.ts
@@ -0,0 +1,173 @@
+import { BRAND_DIAGNOSTICS_NAME, BRAND_DISPLAY_NAME } from "@/shared/brand";
+
+export const sidebar = {
+ xRayTitle: BRAND_DIAGNOSTICS_NAME,
+ openSettings: "Abrir configurações",
+ openRule: "Editar regra de domínio",
+ openLocation: "Editar perfil regional",
+ openLogs: "Ver logs",
+ region: "Região",
+
+ errorPrefix: `${BRAND_DISPLAY_NAME} encontrou um problema:`,
+ loading: "Verificando esta página…",
+ unsupportedTab:
+ "Páginas do navegador não podem ser simuladas. Navegue até um site para inspecionar a cobertura.",
+ noSpoofing: `${BRAND_DISPLAY_NAME} não está ativo nesta página. Abra o popup para configurar uma regra para este site.`,
+ trustedSite: "Você marcou este site como confiável. Nenhuma simulação ativa.",
+ trustedSiteHint: "Abra Sites Confiáveis nas configurações para alterar isso.",
+
+ surfaces: {
+ title: "Superfícies simuladas",
+ timezone: "Fuso horário",
+ language: "Idioma",
+ languages: "Idiomas",
+ acceptLanguage: "Accept-Language",
+ geolocation: "Geolocalização",
+ accuracyNoise: "Precisão / Ruído",
+ platform: "Plataforma",
+ userAgent: "Agente do Usuário",
+ cpuRam: "CPU / RAM",
+ screen: "Tela",
+ noise: "Ruído",
+ canvas: "Tela de desenho (Canvas)",
+ audio: "Áudio",
+ gpu: "GPU",
+ sharedWorker: "Shared Worker",
+ sharedWorkerNativeCompatibility: "Nativo para compatibilidade",
+ sharedWorkerBlobSpoofing: "Simulado via wrapper de blob",
+ sharedWorkerResponseRewriteSpoofing: "Simulado via reescrita de resposta",
+ sharedWorkerResponseRewriteCacheSensitive:
+ "Fallback de reescrita; sensível ao cache",
+ sharedWorkerModuleUnsupported: "Reescrita de módulo não suportada",
+ sharedWorkerIdentityConflict: "Conflito de identidade",
+ sharedWorkerRewriteUnavailable: "Reescrita de resposta indisponível",
+ sharedWorkerBlockedStrict: "Bloqueado pelo modo estrito",
+ sharedWorkerStrictRewriteRequired: "Modo estrito requer reescrita confirmada",
+ sharedWorkerStrictBlockedCacheSensitive:
+ "Bloqueado; fallback de reescrita era sensível ao cache",
+ sharedWorkerCompatibilityHelp:
+ "O manuseio do Shared Worker segue a política Nativa, Simulada ou Estrita selecionada.",
+ gpuDebug: "Depuração de GPU",
+ clientHintsPlatform: "plataforma",
+ clientHintsBrands: "marcas",
+ fingerprint: "Impressão digital",
+ off: "Desligado",
+ yes: "Ativo",
+ blocked: "Bloqueado",
+ m: "m",
+ GB: "GB",
+ expand: "Mostrar valor completo",
+ collapse: "Colapsar",
+ },
+
+ accessed: {
+ title: "Atividade da página",
+ description: "Categorias consultadas pela página desde a última navegação.",
+ accessed: "Consultado",
+ blocked: "Bloqueado",
+ armed: "Aguardando",
+ connecting: "Conectando…",
+ failed: "Falhou",
+ off: "Desligado",
+ categories: {
+ geolocation: "Geolocalização",
+ timeLocale: "Hora & local",
+ canvas: "Tela de desenho (Canvas)",
+ webGL: "WebGL",
+ audio: "Áudio",
+ navigator: "navigator",
+ screen: "Tela",
+ clientHints: "Client Hints",
+ battery: "Bateria",
+ webRTC: "WebRTC",
+ worker: "Workers Dedicados",
+ serviceWorker: "Service Workers",
+ sharedWorker: "Workers Compartilhados",
+ },
+ 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: "Informações do renderizador da GPU",
+ 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: "Por que essas configurações se aplicam",
+ sourcesShort: {
+ trustedSite: "Confiável",
+ domainRule: "Regra de domínio",
+ container: "Contêiner",
+ fallback: "Padrão",
+ none: "Nenhum",
+ },
+ noExplanation: "Nenhuma explicação de regra está disponível.",
+ },
+} as const;
diff --git a/src/ui/i18n/pt-sections/trusted-sites.ts b/src/ui/i18n/pt-sections/trusted-sites.ts
new file mode 100644
index 0000000..6b58246
--- /dev/null
+++ b/src/ui/i18n/pt-sections/trusted-sites.ts
@@ -0,0 +1,53 @@
+import { BRAND_DISPLAY_NAME } from "@/shared/brand";
+
+export const trustedSites = {
+ title: "Sites Confiáveis",
+ hint: `Sites onde ${BRAND_DISPLAY_NAME} permanece desativado mesmo quando uma regra, atribuição de contêiner ou a Regra Padrão normalmente se aplicaria.`,
+ patternLabel: "Padrão de domínio",
+ patternPlaceholder: "Insira um padrão de domínio",
+ filterLabel: "Filtrar sites confiáveis",
+ filterPlaceholder: "Pesquisar domínio",
+ addButton: "Adicionar site",
+ copyLinkLabel: "sites confiáveis",
+ copyLinkHelpLabel: "ajuda de sites confiáveis",
+ copyLinkInspectorLabel: "inspetor de hostname",
+ tableHeadPattern: "Domínio",
+ tableHeadStatus: "Status",
+ tableHeadActions: "Ações",
+ empty: "Ainda não há sites confiáveis.",
+ filteredEmpty: "Nenhum site confiável corresponde ao filtro atual.",
+ inactiveBadge: "inativo",
+ toggleSiteAriaLabel: (pattern: string, enabled: boolean) =>
+ `${enabled ? "Desativar" : "Ativar"} o site confiável ${pattern}`,
+ deleteSiteAriaLabel: (pattern: string) => `Excluir site confiável ${pattern}`,
+ deleteSiteTitle: "Excluir site confiável",
+ duplicateWarning: "Esse site confiável já existe.",
+ patternRequired: "Digite um padrão de domínio.",
+ saved: "Site confiável salvo.",
+ updated: "Site confiável atualizado.",
+ deleted: "Site confiável excluído.",
+ help: {
+ title: "Quando usar Sites Confiáveis",
+ body1:
+ "Use Sites Confiáveis para domínios onde simulação atrapalha, como fluxos de banco, checkout ou recuperação de conta que tratam mudanças no navegador como suspeitas.",
+ body2: `Sites Confiáveis substituem Regras de Domínio, atribuições de contêiner do Firefox e a Regra Padrão. Use example.com para um host exato, *example.com para esse host mais seus subdomínios, ou *.example.com apenas para subdomínios.`,
+ },
+ rulesCta: {
+ title: "Proteção em outros sites",
+ activeRulesOnly: (count: number) =>
+ `${count} ${count === 1 ? "Regra de Domínio ativa se aplica" : "Regras de Domínio ativas se aplicam"} fora dos Sites Confiáveis. Abra Regras de Domínio para revisar onde ${count === 1 ? "ela está ativa" : "elas estão ativas"}.`,
+ activeRulesWithDefault: (count: number) =>
+ `${count} ${count === 1 ? "Regra de Domínio ativa se aplica" : "Regras de Domínio ativas se aplicam"} fora dos Sites Confiáveis. A Regra Padrão também cobre outros sites sem correspondência.`,
+ defaultRuleOnly: `A Regra Padrão ainda se aplica a sites não correspondentes. Sites Confiáveis desativam ${BRAND_DISPLAY_NAME} apenas em hosts correspondentes.`,
+ openRules: "Abrir Regras de Domínio",
+ openDefaultRule: "Abrir Regra Padrão",
+ },
+ dialog: {
+ title: "Adicionar site",
+ description: `Mantenha ${BRAND_DISPLAY_NAME} desativado em páginas correspondentes quando um site funcionar melhor com o estado normal do seu navegador.`,
+ patternInfo:
+ "Use example.com para um host exato. Use *example.com para esse host e qualquer subdomínio, como www.example.com. Use *.example.com apenas para subdomínios.",
+ patternInfoAriaLabel: "Saiba como funcionam os padrões de sites confiáveis",
+ submit: "Adicionar site",
+ },
+} as const;
diff --git a/src/ui/i18n/pt-sections/welcome.ts b/src/ui/i18n/pt-sections/welcome.ts
new file mode 100644
index 0000000..8528d13
--- /dev/null
+++ b/src/ui/i18n/pt-sections/welcome.ts
@@ -0,0 +1,140 @@
+import { BRAND_DISPLAY_NAME } from "@/shared/brand";
+import { externalMapConsentCopy } from "@/ui/i18n/pt-sections/osm";
+
+const WELCOME_PRODUCT = `Bem-vindo ao ${BRAND_DISPLAY_NAME}`;
+
+export const welcome = {
+ title: WELCOME_PRODUCT,
+ loading: "Carregando configuração...",
+ progressLabel: "Progresso da configuração",
+ previous: "Anterior",
+ next: "Próximo",
+ privacyPolicy: "Política de privacidade",
+ privacyDialog: {
+ title: "Política de Privacidade",
+ description: `Como ${BRAND_DISPLAY_NAME} lida com configurações, diagnósticos e solicitações de rede.`,
+ close: "Fechar política de privacidade",
+ },
+ saving: "Salvando...",
+ unexpectedError: "Algo deu errado",
+ importSuccess: "Configurações importadas",
+ importError: "Não foi possível importar as configurações",
+ importParseError: "Não foi possível ler o arquivo de configurações",
+ steps: {
+ welcome: {
+ title: "Obrigado por instalar esta extensão",
+ description:
+ "Escolha o que os sites veem em vez de expor sua localização real e a identidade do navegador. Este breve guia opcional orienta você pelas primeiras escolhas de configuração — todas reversíveis — e as salva nas mesmas Configurações que você pode revisar a qualquer momento. Já tem uma configuração para reutilizar?",
+ importInline: "Importar configurações em vez disso",
+ advancedTitle: "Eu sei o que quero",
+ advancedDescription:
+ "Pule o guia e abra as Configurações com os padrões iniciais. Nenhum perfil regional nem Regra de Domínio será adicionado, a Regra Padrão permanecerá desativada e as proteções do navegador continuarão disponíveis para as regras que você criar depois.",
+ guidedTitle: "Ir para a configuração",
+ guidedDescription:
+ "Percorra as primeiras decisões em ordem. O guia limita as escolhas, explica as implicações e salva tudo nas mesmas telas de Configurações que você poderá editar depois.",
+ advancedCta: "Pular configuração",
+ guidedCta: "Iniciar configuração",
+ },
+ privacy: {
+ title: "Sua navegação permanece local",
+ description: `${BRAND_DISPLAY_NAME} armazena regras e perfis regionais localmente. Esta etapa é apenas sobre serviços de mapa externos opcionais: ative-os se quiser pesquisa e pré-visualizações durante a configuração, ou deixe desativado e insira as coordenadas manualmente.`,
+ descriptionBeforePolicy: `${BRAND_DISPLAY_NAME} armazena regras e perfis regionais localmente. A pesquisa e pré-visualizações de mapas opcionais usam serviços externos somente depois que você os permitir. Leia a`,
+ policyLink: "política de privacidade",
+ descriptionAfterPolicy:
+ " antes de decidir se essas solicitações se encaixam na sua configuração.",
+ consentTitle: externalMapConsentCopy.title,
+ consentDescription: externalMapConsentCopy.description,
+ },
+ presets: {
+ title: "Adicionar perfis regionais",
+ description:
+ "Perfis regionais fornecem locais prontos com configurações de idioma e fuso horário correspondentes, para que os sites vejam um perfil regional coerente. Selecione apenas os lugares que deseja na biblioteca inicial. A configuração importará os perfis selecionados no final, e você poderá editar ou excluir cada um deles depois.",
+ selectAll: "Selecionar tudo",
+ selectNone: "Limpar tudo",
+ selectedCount: (count: number) =>
+ `${count} ${count === 1 ? "selecionado" : "selecionados"}`,
+ },
+ scope: {
+ title: "Escolher onde a proteção começa",
+ description:
+ "A Regra Padrão se aplica quando nenhuma configuração mais específica tem prioridade. Mantenha-a desativada para um início mais silencioso ou ative para proteger sites não correspondentes.",
+ defaultRuleDescription:
+ "Se você atribuir um perfil regional à Regra Padrão, mantenha esse perfil selecionado para que a configuração possa importá-lo antes de salvar.",
+ enableEverywhereTitle: `Usar ${BRAND_DISPLAY_NAME} em todos os sites`,
+ enableEverywhereDescription: `Ative a Regra Padrão agora se você quiser que o ${BRAND_DISPLAY_NAME} esteja ativo em sites sem suas próprias regras. Você ainda pode restringir o comportamento posteriormente com Regras de Domínio, atribuir diferentes perfis regionais a hosts específicos ou excluir sites sensíveis com Sites Confiáveis.`,
+ editDefaultRuleTitle: "Configurações da Regra Padrão",
+ editDefaultRule: "Editar Regra Padrão",
+ editDefaultRuleDescription:
+ "Escolha o perfil regional da Regra Padrão e as configurações de proteção personalizada antes de concluir a configuração.",
+ defaultRuleDialogDescription:
+ "Durante a configuração, esta janela pode usar os perfis regionais selecionados, mesmo que ainda não tenham sido importados. Se você atribuir um deles aqui, mantenha-o selecionado para que a configuração possa criá-lo.",
+ presetMismatch: (label: string) =>
+ `A Regra Padrão usa ${label}, mas esse perfil regional não está selecionado para importação. Selecione-o novamente ou escolha outro perfil para a Regra Padrão.`,
+ },
+ chromium: {
+ title: "Detalhes da compilação do Chromium para rotação",
+ description: (count: number) =>
+ `${BRAND_DISPLAY_NAME} pode alternar os números de build e patch do Chromium expostos pelos Client Hints, mantendo a versão principal alinhada ao navegador instalado. O catálogo incluído contém builds recentes do Chromium; para esta versão do navegador, ${BRAND_DISPLAY_NAME} pode escolher entre ${count} ${count === 1 ? "opção compatível" : "opções compatíveis"}.`,
+ switchTitle: "Rotacionar números de build e patch",
+ switchDescription:
+ "Use valores recentes de build e patch do Chromium do catálogo incluído quando os Client Hints forem simulados. Desative esta opção se você quiser que esses detalhes de versão menor permaneçam fixos.",
+ },
+ firefox: {
+ title: "Permissão do userScripts do Firefox (opcional)",
+ description: `Você pode usar ${BRAND_DISPLAY_NAME} no Firefox sem esta permissão. Se você conceder userScripts, ${BRAND_DISPLAY_NAME} pode aplicar valores simulados mais cedo nos sites suportados, melhorando a proteção durante o primeiro carregamento da página. Você pode pular isso agora e alterar depois nas Configurações.`,
+ action: "Conceder permissão ao userScripts",
+ granted: "Permissão concedida",
+ skipped: `Você pode continuar usando ${BRAND_DISPLAY_NAME} sem esta permissão.`,
+ },
+ appearance: {
+ title: "Ajuste a aparência",
+ description: `Escolha como a interface da extensão deve aparecer antes de começar a usá-la. Essas configurações afetam apenas as telas do ${BRAND_DISPLAY_NAME}; elas não alteram o comportamento de simulação em sites, perfis regionais ou regras. Você pode ajustar as mesmas opções de exibição depois nas Configurações.`,
+ themeTitle: "Tema",
+ themeDescription: `Siga a configuração do seu sistema ou escolha uma interface clara ou escura fixa para todas as telas do ${BRAND_DISPLAY_NAME}.`,
+ reduceMotionTitle: "Reduzir movimento",
+ reduceMotionDescription: `Desativar animações da interface em todo o ${BRAND_DISPLAY_NAME}.`,
+ reduceMotionSystemOverride:
+ "Movimento reduzido está ativado pelas configurações de acessibilidade do seu sistema.",
+ accentTitle: "Cor de destaque",
+ accentDescription:
+ "Escolha a cor de destaque usada para controles, estados ativos, acentos de foco e itens selecionados.",
+ contrastTitle: "Modo de alto contraste",
+ contrastDescription:
+ "Aumente o contraste para texto, bordas e controles em toda a interface da extensão quando a aparência padrão parecer muito sutil.",
+ },
+ done: {
+ title: "Configuração pronta",
+ description: `${BRAND_DISPLAY_NAME} salvará essas escolhas e abrirá as Configurações. Você pode revisar os perfis regionais importados, ajustar a Regra Padrão e alterar configurações de aparência ou privacidade lá.`,
+ cta: "Abrir Configurações",
+ },
+ },
+ presetNames: {
+ spfWarsaw: "Varsóvia",
+ spfParis: "Paris",
+ spfLondon: "Londres",
+ spfOttawa: "Ottawa",
+ spfNewYork: "Nova York",
+ spfLasVegas: "Las Vegas",
+ spfSanFrancisco: "San Francisco",
+ spfSydney: "Sydney",
+ spfBeijing: "Pequim",
+ spfHongKong: "Hong Kong",
+ spfNewDelhi: "Nova Délhi",
+ spfCairo: "Cairo",
+ spfLagos: "Lagos",
+ spfKyiv: "Kyiv",
+ spfKinshasa: "Kinshasa",
+ spfSaoPaulo: "São Paulo",
+ spfBuenosAires: "Buenos Aires",
+ spfLima: "Lima",
+ spfRioDeJaneiro: "Rio de Janeiro",
+ spfCaracas: "Caracas",
+ spfBerlin: "Berlim",
+ spfMadrid: "Madri",
+ },
+ themeOptions: {
+ system: "Sistema",
+ light: "Claro",
+ dark: "Escuro",
+ },
+} as const;
diff --git a/src/ui/i18n/pt.ts b/src/ui/i18n/pt.ts
new file mode 100644
index 0000000..7a6d7d0
--- /dev/null
+++ b/src/ui/i18n/pt.ts
@@ -0,0 +1,30 @@
+import { about } from "./pt-sections/about";
+import { advanced } from "./pt-sections/advanced";
+import { common } from "./pt-sections/common";
+import { demo } from "./pt-sections/demo";
+import { firefoxContainers } from "./pt-sections/firefox-containers";
+import { locations } from "./pt-sections/locations";
+import { options, optionsPage } from "./pt-sections/options";
+import { osm } from "./pt-sections/osm";
+import { popup } from "./pt-sections/popup";
+import { rules } from "./pt-sections/rules";
+import { sidebar } from "./pt-sections/sidebar";
+import { trustedSites } from "./pt-sections/trusted-sites";
+import { welcome } from "./pt-sections/welcome";
+
+export const pt = {
+ common,
+ popup,
+ options,
+ optionsPage,
+ locations,
+ rules,
+ trustedSites,
+ advanced,
+ about,
+ osm,
+ demo,
+ firefoxContainers,
+ sidebar,
+ welcome,
+} as const;
diff --git a/src/ui/options/components/tabs/OptionsTab.render.target.test.tsx b/src/ui/options/components/tabs/OptionsTab.render.target.test.tsx
index 3f4c2dc..27a0031 100644
--- a/src/ui/options/components/tabs/OptionsTab.render.target.test.tsx
+++ b/src/ui/options/components/tabs/OptionsTab.render.target.test.tsx
@@ -212,7 +212,7 @@ describe("OptionsTab Shared Worker handling", () => {
);
});
- it("shows the disabled upcoming Language selector without saving settings", async () => {
+ it("shows the automatically selected UI language without saving settings", async () => {
root = await renderWithRoot();
const languageTrigger = document.getElementById("language-trigger");
@@ -221,7 +221,7 @@ describe("OptionsTab Shared Worker handling", () => {
expect((languageTrigger as HTMLButtonElement).disabled).toBe(true);
expect(languageTrigger?.textContent).toContain("English");
expect(document.body.textContent).toContain("Language");
- expect(document.body.textContent).toContain("SOON");
+ expect(document.body.textContent).toContain("AUTO");
expect(scheduleAutosaveMock).not.toHaveBeenCalled();
});
diff --git a/src/ui/options/components/tabs/options-preference-sections.tsx b/src/ui/options/components/tabs/options-preference-sections.tsx
index e5bb36e..1e729ce 100644
--- a/src/ui/options/components/tabs/options-preference-sections.tsx
+++ b/src/ui/options/components/tabs/options-preference-sections.tsx
@@ -20,7 +20,7 @@ import {
SelectValue,
} from "@/ui/components/ui/select";
import { Switch } from "@/ui/components/ui/switch";
-import { t } from "@/ui/i18n";
+import { t, UI_LOCALE } from "@/ui/i18n";
import type { OptionsModel } from "@/ui/options/components/tabs/options-model";
import { renderOsmConsentState } from "@/ui/options/components/tabs/options-surface-data";
import {
@@ -150,7 +150,7 @@ const LanguageCard = () => (
description={t.advanced.display.language.description}
actionClassName="w-full sm:w-44"
action={
-