Skip to content
Open
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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ The format is based on Keep a Changelog and the project follows Semantic Version

- The interface language can be chosen in Settings. Automatic still follows the
browser language. Popup, sidebar, and Settings update without a reload.
- Language choices show the English name and the name from the active translation.
The English interface keeps English names only.
- Accent colors, protection labels, and X-Ray labels follow the active interface
language. Long About link labels stay inside their buttons.
- Experimental: every subframe in a tab now uses the top-frame runtime snapshot
(locale, timezone, fingerprint, geolocation) so iframe and worker realms do
not diverge from the page. Per-tab header rules apply to all requests in that
Expand Down
2 changes: 2 additions & 0 deletions src/ui/i18n/en-sections/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ export const optionsPage = {
description: `Choose which browser protections ${BRAND_DISPLAY_NAME} uses by default. More specific settings can override individual protections, while Trusted Sites can disable them for matching sites.`,
disabledNote: `Browser protections are off everywhere. Turn them back on to choose what ${BRAND_DISPLAY_NAME} should spoof by default.`,
copyLinkLabel: "browser protections",
interceptsApis: (count: number) =>
`Intercepts ${count} browser ${count === 1 ? "API" : "APIs"}`,
items: {
geolocation: {
label: "Geolocation",
Expand Down
2 changes: 2 additions & 0 deletions src/ui/i18n/es-sections/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ export const optionsPage = {
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",
interceptsApis: (count: number) =>
`Intercepta ${count} ${count === 1 ? "API del navegador" : "APIs del navegador"}`,
items: {
geolocation: {
label: "Geolocalización",
Expand Down
2 changes: 2 additions & 0 deletions src/ui/i18n/pt-sections/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ export const optionsPage = {
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",
interceptsApis: (count: number) =>
`Intercepta ${count} ${count === 1 ? "API do navegador" : "APIs do navegador"}`,
items: {
geolocation: {
label: "Geolocalização",
Expand Down
1 change: 1 addition & 0 deletions src/ui/i18n/ru-sections/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export const optionsPage = {
description: `Выберите, какие средства защиты браузера ${BRAND_DISPLAY_NAME} использует по умолчанию. Более конкретные настройки могут переопределять отдельные средства защиты, а «Доверенные сайты» могут отключать их для соответствующих сайтов.`,
disabledNote: `Защита браузера на всех сайтах отключена. Включите её, чтобы настроить подмену данных по умолчанию.`,
copyLinkLabel: "защита браузера",
interceptsApis: (count: number) => `Перехватывает ${count} API браузера`,
items: {
geolocation: {
label: "Геолокация",
Expand Down
1 change: 1 addition & 0 deletions src/ui/i18n/uk-sections/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export const optionsPage = {
description: `Виберіть, який захист браузера ${BRAND_DISPLAY_NAME} використовує за замовчуванням. Конкретніші налаштування можуть перевизначити окремі механізми захисту, тоді як довірені сайти можуть вимкнути їх для відповідних сайтів.`,
disabledNote: `Захист браузера скрізь вимкнено. Увімкніть його, щоб налаштувати підміну даних за замовчуванням.`,
copyLinkLabel: "механізми захисту браузера",
interceptsApis: (count: number) => `Перехоплює ${count} API браузера`,
items: {
geolocation: {
label: "Геолокація",
Expand Down
28 changes: 15 additions & 13 deletions src/ui/options/components/onboarding/WelcomeWizard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import { PAGE_ANCHORS } from "@/ui/options/navigation";
import { useSettings } from "@/ui/options/state/SettingsContext";
import { sendMessageOrThrow } from "@/ui/shared/runtime-messaging";
import { getThemeAccentTokens } from "@/ui/shared/theme";
import { THEME_ACCENT_OPTIONS } from "@/ui/shared/theme-accent-options";
import { getThemeAccentOptions } from "@/ui/shared/theme-accent-options";
import { useTheme } from "@/ui/shared/ThemeProvider";

const DEFAULT_PRESET_IDS = [
Expand Down Expand Up @@ -371,18 +371,20 @@ const setWizardContrast = async (
await theme.setHighContrast(checked);
};

const getAccentView = (theme: Theme) => ({
color: `hsl(${getThemeAccentTokens(theme.accentPreset, theme.theme, theme.highContrast).primary})`,
label:
THEME_ACCENT_OPTIONS.find((option) => option.preset === theme.accentPreset)
?.label ??
THEME_ACCENT_OPTIONS[0]?.label ??
theme.accentPreset,
options: THEME_ACCENT_OPTIONS.map((option) => ({
...option,
color: `hsl(${getThemeAccentTokens(option.preset, theme.theme, theme.highContrast).primary})`,
})),
});
const getAccentView = (theme: Theme) => {
const accentOptions = getThemeAccentOptions();
return {
color: `hsl(${getThemeAccentTokens(theme.accentPreset, theme.theme, theme.highContrast).primary})`,
label:
accentOptions.find((option) => option.preset === theme.accentPreset)?.label ??
accentOptions[0]?.label ??
theme.accentPreset,
options: accentOptions.map((option) => ({
...option,
color: `hsl(${getThemeAccentTokens(option.preset, theme.theme, theme.highContrast).primary})`,
})),
};
};

export const WelcomeWizard = ({ onComplete }: { onComplete: () => void }) => {
const settings = useSettings();
Expand Down
4 changes: 2 additions & 2 deletions src/ui/options/components/tabs/AboutTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -479,14 +479,14 @@ const AboutSidebar = () => {
<p className="text-sm text-muted-foreground">
{t.about.projectLinks.description}
</p>
<div className="grid grid-cols-3 gap-2">
<div className="flex flex-wrap gap-2">
{projectLinks.map((link) => (
<Button
key={link.url}
asChild
size="sm"
variant="secondary"
className="w-full min-w-0 px-2"
className="h-auto max-w-full whitespace-normal px-2 py-1.5 text-center leading-snug"
>
<a href={link.url} target="_blank" rel="noopener noreferrer">
{icon(link.icon)}
Expand Down
45 changes: 35 additions & 10 deletions src/ui/options/components/tabs/options-preference-sections.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,16 +21,19 @@ import {
} from "@/ui/components/ui/select";
import { Switch } from "@/ui/components/ui/switch";
import { t } from "@/ui/i18n";
import { en } from "@/ui/i18n/en";
import { useUiLocale } from "@/ui/i18n/LocaleRefresh";
import type { OptionsModel } from "@/ui/options/components/tabs/options-model";
import { renderOsmConsentState } from "@/ui/options/components/tabs/options-surface-data";
import { languageOptionLabel } from "@/ui/options/interface-language-label";
import {
SECTION_ANCHORS,
SETTINGS_SUBPAGE_ANCHORS,
SETTING_ANCHORS,
} from "@/ui/options/navigation";
import { icon } from "@/ui/options/utils";
import { getThemeAccentTokens } from "@/ui/shared/theme";
import { THEME_ACCENT_OPTIONS } from "@/ui/shared/theme-accent-options";
import { getThemeAccentOptions } from "@/ui/shared/theme-accent-options";
import { useTheme } from "@/ui/shared/ThemeProvider";

export const PrivacySection = ({ model }: { model: OptionsModel }) => (
Expand Down Expand Up @@ -112,7 +115,7 @@ const AccentChip = ({
aria-pressed={selected}
disabled={disabled}
className={cn(
"h-auto justify-start rounded-xl px-3 py-2 text-left",
"h-auto justify-start whitespace-normal rounded-xl px-3 py-2 text-left",
selected
? "border-primary bg-primary/10 text-foreground shadow-sm ring-1 ring-primary/30 hover:bg-primary/15"
: "bg-card/35 text-foreground hover:bg-accent/70",
Expand Down Expand Up @@ -151,6 +154,10 @@ export const commitLanguageSelection = (

const LanguageCard = ({ model }: { model: OptionsModel }) => {
const { setUiLocale, uiLocale } = useTheme();
const activeLocale = useUiLocale();
const englishLanguage = en.advanced.display.language;
const labelFor = (englishName: string, localizedName: string) =>
languageOptionLabel(activeLocale, englishName, localizedName);
return (
<SettingsControlCard
anchorId={SETTING_ANCHORS.options.language}
Expand All @@ -172,7 +179,7 @@ const LanguageCard = ({ model }: { model: OptionsModel }) => {
</span>
}
focusControlOnTitleClick
actionClassName="w-full sm:w-52"
actionClassName="w-full sm:w-72"
action={
<Select
value={uiLocale}
Expand All @@ -189,22 +196,40 @@ const LanguageCard = ({ model }: { model: OptionsModel }) => {
</SelectTrigger>
<SelectContent>
<SelectItem value="auto">
{t.advanced.display.language.optionAutomatic}
{labelFor(
englishLanguage.optionAutomatic,
t.advanced.display.language.optionAutomatic,
)}
</SelectItem>
<SelectItem value="en">
{t.advanced.display.language.optionEnglish}
{labelFor(
englishLanguage.optionEnglish,
t.advanced.display.language.optionEnglish,
)}
</SelectItem>
<SelectItem value="es">
{t.advanced.display.language.optionSpanish}
{labelFor(
englishLanguage.optionSpanish,
t.advanced.display.language.optionSpanish,
)}
</SelectItem>
<SelectItem value="pt">
{t.advanced.display.language.optionPortuguese}
{labelFor(
englishLanguage.optionPortuguese,
t.advanced.display.language.optionPortuguese,
)}
</SelectItem>
<SelectItem value="ru">
{t.advanced.display.language.optionRussian}
{labelFor(
englishLanguage.optionRussian,
t.advanced.display.language.optionRussian,
)}
</SelectItem>
<SelectItem value="uk">
{t.advanced.display.language.optionUkrainian}
{labelFor(
englishLanguage.optionUkrainian,
t.advanced.display.language.optionUkrainian,
)}
</SelectItem>
</SelectContent>
</Select>
Expand Down Expand Up @@ -270,7 +295,7 @@ const AccentCard = ({ model }: { model: OptionsModel }) => (
aria-describedby={getSettingDescriptionId(SETTING_ANCHORS.advanced.accentColor)}
className="grid gap-2 sm:grid-cols-2 xl:grid-cols-3"
>
{THEME_ACCENT_OPTIONS.map(({ preset, label }) => (
{getThemeAccentOptions().map(({ preset, label }) => (
<AccentChip
key={preset}
preset={preset}
Expand Down
47 changes: 25 additions & 22 deletions src/ui/options/components/tabs/options-spoofing-section.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,30 +22,33 @@ import {
} from "@/ui/options/components/tabs/options-surface-data";
import { AnimatedVersionHint } from "@/ui/options/components/tabs/options-version-hint";
import { SECTION_ANCHORS, SETTING_ANCHORS } from "@/ui/options/navigation";
import { SURFACE_METHOD_LABELS } from "@/ui/shared/surface-method-labels";
import { getSurfaceMethodLabels } from "@/ui/shared/surface-method-labels";

const MethodList = ({ methods }: Pick<SpoofingSurface, "methods">) => (
<details className="group min-w-0">
<summary className="list-none cursor-pointer select-none [&::-webkit-details-marker]:hidden">
<span className="inline-flex items-center gap-1 text-xs text-muted-foreground transition-colors hover:text-foreground">
<span className="inline-block transition-transform group-open:rotate-90">
›
</span>
{`Intercepts ${methods.length} browser APIs`}
</span>
</summary>
<div className="mt-2 flex flex-wrap gap-1">
{methods.map((method) => (
<span
key={method.id}
className="rounded bg-muted px-1.5 py-0.5 font-mono text-[0.72rem] text-foreground"
>
{SURFACE_METHOD_LABELS[method.id as SpoofingSurfaceMethodId]}
const MethodList = ({ methods }: Pick<SpoofingSurface, "methods">) => {
const methodLabels = getSurfaceMethodLabels();
return (
<details className="group min-w-0">
<summary className="list-none cursor-pointer select-none [&::-webkit-details-marker]:hidden">
<span className="inline-flex items-center gap-1 text-xs text-muted-foreground transition-colors hover:text-foreground">
<span className="inline-block transition-transform group-open:rotate-90">
›
</span>
{t.optionsPage.browserFingerprintSpoofing.interceptsApis(methods.length)}
</span>
))}
</div>
</details>
);
</summary>
<div className="mt-2 flex flex-wrap gap-1">
{methods.map((method) => (
<span
key={method.id}
className="rounded bg-muted px-1.5 py-0.5 font-mono text-[0.72rem] text-foreground"
>
{methodLabels[method.id as SpoofingSurfaceMethodId]}
</span>
))}
</div>
</details>
);
};

const SharedWorkerCard = ({
model,
Expand Down
14 changes: 8 additions & 6 deletions src/ui/options/components/tabs/options-surface-data.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,10 @@ const SURFACE_ANCHORS: Record<ConfigurableSurfaceKey, string> = {
sharedWorker: SETTING_ANCHORS.options.sharedWorkerHandlingMode,
};

const SURFACE_COPY: Record<
const getSurfaceCopy = (): Record<
ConfigurableSurfaceKey,
{ label: string; description: ReactNode }
> = {
> => ({
geolocation: {
label: t.optionsPage.browserFingerprintSpoofing.items.geolocation.label,
description: t.optionsPage.browserFingerprintSpoofing.items.geolocation.description,
Expand Down Expand Up @@ -143,7 +143,7 @@ const SURFACE_COPY: Record<
</div>
),
},
};
});

export const buildSpoofingSurfaces = ({
browserTarget,
Expand All @@ -155,9 +155,10 @@ export const buildSpoofingSurfaces = ({
featureFlags?: FeatureFlags;
nativeTemporalApi?: boolean;
sharedSpoofing: SharedSpoofingConfig | undefined;
}): SpoofingSurface[] =>
CONFIGURABLE_SURFACES.map((surface) => {
const copy = SURFACE_COPY[surface.key];
}): SpoofingSurface[] => {
const surfaceCopy = getSurfaceCopy();
return CONFIGURABLE_SURFACES.map((surface) => {
const copy = surfaceCopy[surface.key];
return {
key: surface.key,
anchorId: SURFACE_ANCHORS[surface.key],
Expand All @@ -176,6 +177,7 @@ export const buildSpoofingSurfaces = ({
),
} satisfies SpoofingSurface;
}).filter((surface) => surface.supported !== false);
};

export const renderOsmConsentState = (osmConsent: "unknown" | "granted" | "denied") => {
if (osmConsent === "unknown") return t.advanced.privacy.osmConsent.stateUnknown;
Expand Down
20 changes: 20 additions & 0 deletions src/ui/options/interface-language-label.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { describe, expect, it } from "vitest";

import { languageOptionLabel } from "./interface-language-label";

describe("languageOptionLabel", () => {
it("keeps the English interface on English names", () => {
expect(languageOptionLabel("en", "Spanish", "Spanish")).toBe("Spanish");
expect(languageOptionLabel("en", "Automatic", "Automatic")).toBe("Automatic");
});

it("shows the English name and the active translation", () => {
expect(languageOptionLabel("es", "Spanish", "Español")).toBe("Spanish (Español)");
expect(languageOptionLabel("uk", "Ukrainian", "Українська")).toBe(
"Ukrainian (Українська)",
);
expect(languageOptionLabel("ru", "Automatic", "Автоматически")).toBe(
"Automatic (Автоматически)",
);
});
});
8 changes: 8 additions & 0 deletions src/ui/options/interface-language-label.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import type { UiLocale } from "@/shared/ui-locale";

export const languageOptionLabel = (
activeLocale: UiLocale,
englishName: string,
localizedName: string,
): string =>
activeLocale === "en" ? englishName : `${englishName} (${localizedName})`;
4 changes: 2 additions & 2 deletions src/ui/shared/surface-method-labels.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { SpoofingSurfaceMethodId } from "@/shared/spoofing-surfaces";
import { t } from "@/ui/i18n";

export const SURFACE_METHOD_LABELS: Record<SpoofingSurfaceMethodId, string> = {
export const getSurfaceMethodLabels = (): Record<SpoofingSurfaceMethodId, string> => ({
"geolocation.getCurrentPosition":
t.sidebar.accessed.methods.geolocationGetCurrentPosition,
"geolocation.watchPosition": t.sidebar.accessed.methods.geolocationWatchPosition,
Expand Down Expand Up @@ -91,4 +91,4 @@ export const SURFACE_METHOD_LABELS: Record<SpoofingSurfaceMethodId, string> = {
"worker.constructor": t.sidebar.accessed.methods.workerConstructor,
"serviceWorker.register": t.sidebar.accessed.methods.serviceWorkerRegister,
"sharedWorker.constructor": t.sidebar.accessed.methods.sharedWorkerConstructor,
};
});
18 changes: 18 additions & 0 deletions src/ui/shared/theme-accent-options.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { describe, expect, it } from "vitest";

import { accentOptionLabel } from "./theme-accent-options";

import { applyUiLocalePreference } from "@/ui/i18n";

describe("accentOptionLabel", () => {
it("follows the active interface language", () => {
applyUiLocalePreference("en");
expect(accentOptionLabel("teal")).toBe("Teal");

applyUiLocalePreference("es");
expect(accentOptionLabel("teal")).toBe("Verde azulado");
expect(accentOptionLabel("purple")).toBe("Morado");

applyUiLocalePreference("en");
});
});
Loading