Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -348,6 +348,7 @@ const buildAllowedRetiredLines = () => {
"src/shared/extension-notifications.json",
new Set([
`"en": ${JSON.stringify(renameTitle)}`,
`"en": ${JSON.stringify(renameTitle)},`,
`${JSON.stringify(renameParagraph)},`,
]),
],
Expand Down
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ The format is based on Keep a Changelog and the project follows Semantic Version

- Added complete Spanish and Portuguese interface translations, selected
automatically from the browser interface language with English as the fallback.
- Added complete Russian and Ukrainian interface translations, selected
automatically from the browser interface language.
- X-Ray now counts BatteryManager property reads (`charging`, `chargingTime`,
`dischargingTime`, `level`) in addition to `navigator.getBattery`.

Expand Down
70 changes: 62 additions & 8 deletions src/shared/extension-notifications.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,21 +7,31 @@
"delivery": "upgrades-only",
"title": {
"en": "GeoWarp is now {{brand}}",
"es": "GeoWarp ahora es {{brand}}",
"pt": "GeoWarp agora é {{brand}}"
"es": "La extensión ahora se llama {{brand}}",
"pt": "A extensão agora se chama {{brand}}",
"ru": "Теперь расширение называется {{brand}}",
"uk": "Тепер розширення називається {{brand}}"
},
"message": {
"en": [
"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.",
"La extensión tiene un nuevo nombre: {{brand}}. Sigue siendo la extensión que conoces y mantiene su propósito: darte el control sobre la ubicación y los datos del navegador que pueden ver los sitios web.",
"Gracias por usarla y por formar parte de este recorrido. Crear esta extensión ha sido una experiencia extraordinaria para mí y me ilusiona descubrir lo que traerá el futuro."
],
"pt": [
"GeoWarp tem um novo nome: {{brand}}. A extensão que você conhece continua aqui, com o mesmo objetivo de dar a você controle sobre a localização e as informações do navegador que os sites podem ver.",
"A extensão tem um novo nome: {{brand}}. Ela continua a mesma, com o objetivo de dar a você controle sobre a localização e as informações do navegador que os sites podem ver.",
"Agradecemos por usá-la e por fazer parte desta jornada. Criar esta extensão tem sido uma experiência extraordinária para mim, e estou animado para ver o que o futuro reserva."
],
"ru": [
"У расширения новое имя: {{brand}}. Знакомое вам расширение осталось прежним. Оно по-прежнему помогает управлять тем, какие сведения о вашем местоположении и браузере видят сайты.",
"Спасибо, что пользуетесь расширением и прошли с ним этот путь. Работа над ним стала для меня необыкновенным опытом, и я с интересом жду того, что будет дальше."
],
"uk": [
"Розширення має нову назву: {{brand}}. Знайоме вам розширення залишається тим самим. Воно й далі допомагає керувати тим, які відомості про ваше місцеперебування та браузер бачать сайти.",
"Дякую, що користуєтеся розширенням і пройшли з ним цей шлях. Робота над ним стала для мене надзвичайним досвідом, і я з цікавістю чекаю на те, що буде далі."
]
}
},
Expand All @@ -33,7 +43,9 @@
"title": {
"en": "Notifications are now in the popup",
"es": "Las notificaciones ya están en la ventana emergente",
"pt": "As notificações agora estão no pop-up"
"pt": "As notificações agora estão no pop-up",
"ru": "Уведомления теперь в окне расширения",
"uk": "Сповіщення тепер у вікні розширення"
},
"message": {
"en": [
Expand All @@ -47,6 +59,14 @@
"pt": [
"{{brand}} agora mostra atualizações importantes e avisos de compatibilidade no pop-up.",
"Ao abrir uma notificação, ela é marcada como lida e permanece disponível até que você a descarte."
],
"ru": [
"{{brand}} теперь показывает важные обновления и предупреждения о совместимости в окне расширения.",
"Когда вы открываете уведомление, оно отмечается как прочитанное. Оно остаётся доступным, пока вы его не закроете."
],
"uk": [
"{{brand}} тепер показує важливі оновлення та попередження про сумісність у вікні розширення.",
"Після відкриття сповіщення позначається як прочитане. Воно залишається доступним, доки ви його не закриєте."
]
}
},
Expand All @@ -58,7 +78,9 @@
"title": {
"en": "Temporal API protection is ready to try",
"es": "Ya puedes probar la protección de la API Temporal",
"pt": "A proteção da API Temporal está pronta para testes"
"pt": "A proteção da API Temporal está pronta para testes",
"ru": "Защиту Temporal API уже можно попробовать",
"uk": "Захист Temporal API вже можна спробувати"
},
"message": {
"en": [
Expand All @@ -75,6 +97,16 @@
"{{brand}} agora pode proteger chamadas nativas da API Temporal, incluindo o comportamento padrão de data, hora, configuração regional e fuso horário, junto com o restante da sua configuração.",
"O recurso é experimental e vem desativado por padrão. Para testá-lo, abra Configurações → Avançado → Experimental e ative a API Temporal. Ele só funciona quando o navegador já oferece a API Temporal — {{brand}} não instala um polyfill.",
"Quer saber mais? Use o link abaixo para consultar a documentação da API Temporal na Mozilla Developer Network."
],
"ru": [
"{{brand}} теперь может защищать вызовы встроенного Temporal API: даты, время, региональные настройки и часовой пояс по умолчанию — вместе с остальными параметрами защиты.",
"Функция экспериментальная и по умолчанию выключена. Чтобы попробовать её, откройте Настройки → Дополнительно → Экспериментальные функции и включите Temporal API. Это работает только в браузерах, где Temporal уже доступен: {{brand}} не добавляет полифил.",
"Хотите узнать больше? Откройте документацию по Temporal API на Mozilla Developer Network по ссылке ниже."
],
"uk": [
"{{brand}} тепер може захищати виклики вбудованого Temporal API: дати, час, регіональні налаштування та часовий пояс за замовчуванням — разом з іншими параметрами захисту.",
"Функція експериментальна й за замовчуванням вимкнена. Щоб спробувати її, відкрийте Налаштування → Додатково → Експериментальні функції та ввімкніть Temporal API. Це працює лише в браузерах, де Temporal уже доступний: {{brand}} не додає поліфіл.",
"Хочете дізнатися більше? Відкрийте документацію Temporal API на Mozilla Developer Network за посиланням нижче."
]
},
"actionUrl": "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal"
Expand All @@ -87,7 +119,9 @@
"title": {
"en": "{{brand}} is now open source!",
"es": "¡{{brand}} ya es de código abierto!",
"pt": "{{brand}} agora é de código aberto!"
"pt": "{{brand}} agora é de código aberto!",
"ru": "Исходный код {{brand}} открыт!",
"uk": "Вихідний код {{brand}} відкрито!"
},
"message": {
"en": [
Expand All @@ -101,6 +135,14 @@
"pt": [
"{{brand}} agora é de código aberto. O código está disponível no GitHub sob a licença AGPL-3.0-or-later e os termos adicionais do projeto. Explore o repositório para entender como tudo funciona nos bastidores.",
"Encontrou um bug comum? Abra uma issue e conte o que aconteceu. Problemas de segurança ou detecção devem continuar sendo enviados pelo formulário privado do GitHub."
],
"ru": [
"Исходный код {{brand}} теперь открыт. Он доступен на GitHub по лицензии AGPL-3.0-or-later с дополнительными условиями проекта. Загляните в репозиторий, чтобы увидеть, как всё устроено.",
"Нашли обычную ошибку? Создайте issue и расскажите, что произошло. О проблемах безопасности или обнаружения защиты по-прежнему сообщайте через закрытую форму GitHub."
],
"uk": [
"Вихідний код {{brand}} тепер відкрито. Він доступний на GitHub за ліцензією AGPL-3.0-or-later з додатковими умовами проєкту. Зазирніть до репозиторію, щоб побачити, як усе працює.",
"Знайшли звичайну помилку? Створіть issue та розкажіть, що сталося. Про проблеми безпеки або виявлення захисту й надалі повідомляйте через приватну форму GitHub."
]
},
"actionUrl": "https://github.com/Privacy-Thing/browser-extension"
Expand All @@ -113,7 +155,9 @@
"title": {
"en": "Domain fencing is ready to try",
"es": "Ya puedes probar el aislamiento por dominio",
"pt": "O isolamento por domínio está pronto para testes"
"pt": "O isolamento por domínio está pronto para testes",
"ru": "Изоляцию по доменам уже можно попробовать",
"uk": "Ізоляцію за доменами вже можна спробувати"
},
"message": {
"en": [
Expand All @@ -130,6 +174,16 @@
"No Chrome e no Firefox, o isolamento por domínio atribui a cada site uma variante estável da impressão digital gerada quando a Regra Padrão se aplica. No Firefox, ele também separa as identidades atribuídas por meio de contêineres.",
"Um site e seus subdomínios compartilham uma identidade, enquanto sites não relacionados recebem variantes distintas. O perfil regional escolhido não muda, e as Regras de Domínio configuradas manualmente não são isoladas.",
"O isolamento por domínio é experimental e vem desativado por padrão. Para testá-lo, abra Configurações → Avançado → Experimental e ative Isolamento por domínio."
],
"ru": [
"В Chrome и Firefox изоляция по доменам даёт каждому сайту собственный стабильный вариант созданного цифрового отпечатка, когда действует правило по умолчанию. В Firefox она также разделяет идентичности, назначенные через контейнеры.",
"Сайт и его поддомены используют одну идентичность, а несвязанные сайты получают разные варианты. Выбранный региональный профиль не меняется. Правила для доменов, настроенные вручную, не изолируются.",
"Изоляция по доменам — экспериментальная функция, по умолчанию выключенная. Чтобы попробовать её, откройте Настройки → Дополнительно → Экспериментальные функции и включите изоляцию по доменам."
],
"uk": [
"У Chrome і Firefox ізоляція за доменами надає кожному сайту власний стабільний варіант створеного цифрового відбитка, коли діє правило за замовчуванням. У Firefox вона також розділяє ідентичності, призначені через контейнери.",
"Сайт і його піддомени використовують одну ідентичність, а не пов’язані між собою сайти отримують різні варіанти. Обраний регіональний профіль не змінюється. Правила для доменів, налаштовані вручну, не ізолюються.",
"Ізоляція за доменами — експериментальна функція, яку за замовчуванням вимкнено. Щоб спробувати її, відкрийте Налаштування → Додатково → Експериментальні функції та ввімкніть ізоляцію за доменами."
]
}
}
Expand Down
19 changes: 19 additions & 0 deletions src/shared/release-notification.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,25 @@ describe("extension notification catalog", () => {
]);
});

it.each([
["ru-RU", "Уведомления теперь в окне расширения"],
["uk-UA", "Сповіщення тепер у вікні розширення"],
])("provides %s release notices", (locale, title) => {
expect(getReleaseNotice("notification-center-intro", locale)?.title).toBe(title);
});

it("keeps Russian and Ukrainian notices complete with brand tokens", () => {
for (const notice of notificationCatalog.notifications) {
for (const locale of ["ru", "uk"] as const) {
expect(notice.title[locale]).toBeTruthy();
expect(notice.message[locale]).toHaveLength(notice.message.en.length);
expect(getReleaseNotice(notice.id, locale)?.message).toHaveLength(
notice.message.en.length,
);
}
}
});

it("includes the 0.9.0 rename announcement", () => {
const source = notificationCatalog.notifications.find(
(notification) => notification.id === "privacy-thing-rename",
Expand Down
2 changes: 2 additions & 0 deletions src/ui/i18n/en-sections/advanced.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,8 @@ export const advanced = {
optionEnglish: "English",
optionSpanish: "Spanish",
optionPortuguese: "Portuguese",
optionRussian: "Russian",
optionUkrainian: "Ukrainian",
soon: "AUTO",
copyLinkLabel: "language",
},
Expand Down
2 changes: 2 additions & 0 deletions src/ui/i18n/es-sections/advanced.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,8 @@ export const advanced = {
optionEnglish: "Inglés",
optionSpanish: "Español",
optionPortuguese: "Portugués",
optionRussian: "Ruso",
optionUkrainian: "Ucraniano",
soon: "AUTOMÁTICO",
copyLinkLabel: "idioma",
},
Expand Down
7 changes: 7 additions & 0 deletions src/ui/i18n/i18n-usage.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -376,6 +376,13 @@ describe("UI i18n coverage", () => {
it("keeps all locale packs aligned with the base locale shape", async () => {
const baseKeys = collectLocaleLeafPaths(en);
const localeModules = await collectLocaleModules();
expect(localeModules.map(({ code }) => code).sort()).toEqual([
"en",
"es",
"pt",
"ru",
"uk",
]);

for (const localeModule of localeModules) {
const localeKeys = collectLocaleLeafPaths(localeModule.value);
Expand Down
8 changes: 8 additions & 0 deletions src/ui/i18n/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,14 @@ describe("resolveUiLocale", () => {
},
);

it.each(["ru", "ru-RU", "RU-kz"])("selects Russian for %s", (language) => {
expect(resolveUiLocale(language)).toBe("ru");
});

it.each(["uk", "uk-UA", "UK-ua"])("selects Ukrainian for %s", (language) => {
expect(resolveUiLocale(language)).toBe("uk");
});

it.each([undefined, "", "en", "en-US", "pl-PL"])(
"falls back to English for %s",
(language) => {
Expand Down
13 changes: 10 additions & 3 deletions src/ui/i18n/index.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,23 @@
import { en } from "./en";
import { es } from "./es";
import { pt } from "./pt";
import { ru } from "./ru";
import { uk } from "./uk";

export type UiLocale = "en" | "es" | "pt";
export type UiLocale = "en" | "es" | "pt" | "ru" | "uk";

export const resolveUiLocale = (language: string | undefined): UiLocale => {
const baseLanguage = language?.toLowerCase().split("-")[0];
return baseLanguage === "es" || baseLanguage === "pt" ? baseLanguage : "en";
return baseLanguage === "es" ||
baseLanguage === "pt" ||
baseLanguage === "ru" ||
baseLanguage === "uk"
? baseLanguage
: "en";
};

export const UI_LOCALE = resolveUiLocale(globalThis.navigator?.language);
export const t = { en, es, pt }[UI_LOCALE];
export const t = { en, es, pt, ru, uk }[UI_LOCALE];

if (typeof document !== "undefined") {
document.documentElement.lang = UI_LOCALE;
Expand Down
Loading