From f8ce56481fa072026c65a41690c6169c6e0c2559 Mon Sep 17 00:00:00 2001 From: zhom <2717306+zhom@users.noreply.github.com> Date: Tue, 28 Apr 2026 23:50:56 +0400 Subject: [PATCH] chore: i18n --- AGENTS.md | 8 + src/app/page.tsx | 227 +++--- src/components/camoufox-config-dialog.tsx | 28 +- src/components/clone-profile-dialog.tsx | 2 +- src/components/commercial-trial-modal.tsx | 20 +- src/components/cookie-copy-dialog.tsx | 84 ++- src/components/cookie-management-dialog.tsx | 109 ++- src/components/create-group-dialog.tsx | 22 +- src/components/create-profile-dialog.tsx | 276 ++++--- src/components/custom-toast.tsx | 4 +- src/components/data-table-action-bar.tsx | 10 +- src/components/delete-confirmation-dialog.tsx | 10 +- src/components/delete-group-dialog.tsx | 40 +- src/components/edit-group-dialog.tsx | 22 +- src/components/group-assignment-dialog.tsx | 38 +- src/components/group-badges.tsx | 2 +- src/components/group-management-dialog.tsx | 75 +- src/components/import-profile-dialog.tsx | 110 +-- src/components/integrations-dialog.tsx | 72 +- src/components/launch-on-login-dialog.tsx | 26 +- src/components/location-proxy-dialog.tsx | 69 +- src/components/permission-dialog.tsx | 35 +- src/components/profile-data-table.tsx | 82 +- src/components/profile-selector-dialog.tsx | 33 +- src/components/proxy-assignment-dialog.tsx | 49 +- src/components/proxy-check-button.tsx | 29 +- src/components/proxy-export-dialog.tsx | 40 +- src/components/proxy-form-dialog.tsx | 17 +- src/components/proxy-import-dialog.tsx | 107 ++- src/components/proxy-management-dialog.tsx | 279 ++++--- src/components/release-type-selector.tsx | 26 +- src/components/settings-dialog.tsx | 161 ++-- .../shared-camoufox-config-form.tsx | 13 +- src/components/sync-config-dialog.tsx | 22 +- src/components/traffic-details-dialog.tsx | 125 +++- src/components/ui/color-picker.tsx | 4 +- src/components/ui/combobox.tsx | 90 +-- src/components/ui/command.tsx | 13 +- src/components/ui/dialog.tsx | 4 +- src/components/vpn-check-button.tsx | 26 +- src/components/vpn-form-dialog.tsx | 86 ++- src/components/vpn-import-dialog.tsx | 96 +-- src/components/wayfern-config-form.tsx | 14 +- src/components/wayfern-terms-dialog.tsx | 23 +- src/components/window-drag-area.tsx | 6 +- src/hooks/use-app-update-notifications.tsx | 59 +- src/hooks/use-browser-download.ts | 73 +- src/hooks/use-browser-support.ts | 3 +- src/hooks/use-extension-events.ts | 5 +- src/hooks/use-group-events.ts | 9 +- src/hooks/use-profile-events.ts | 9 +- src/hooks/use-proxy-events.ts | 9 +- src/hooks/use-update-notifications.tsx | 6 +- src/hooks/use-version-updater.ts | 101 ++- src/hooks/use-vpn-events.ts | 11 +- src/i18n/locales/en.json | 699 ++++++++++++++++- src/i18n/locales/es.json | 703 ++++++++++++++++- src/i18n/locales/fr.json | 703 ++++++++++++++++- src/i18n/locales/ja.json | 705 +++++++++++++++++- src/i18n/locales/pt.json | 703 ++++++++++++++++- src/i18n/locales/ru.json | 705 +++++++++++++++++- src/i18n/locales/zh.json | 705 +++++++++++++++++- 62 files changed, 6520 insertions(+), 1322 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index d83c008..afbc338 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -60,6 +60,14 @@ donutbrowser/ - Don't duplicate code unless there's a very good reason; keep the same logic in one place - Anytime you make changes that affect copy or add new text, it has to be reflected in all translation files +## Translations (mandatory) + +- Never write user-facing strings as raw English literals in JSX, toast messages, dialog titles/descriptions, button labels, placeholders, table headers, tooltips, or empty-state text. Always go through `t("namespace.key")` from `useTranslation()`. +- This applies to every component under `src/` — including new ones. If a component doesn't already import `useTranslation`, add it. +- Adding a new string means adding the key to ALL seven locale files in `src/i18n/locales/` (en, es, fr, ja, pt, ru, zh) — not just `en.json`. The English version alone is incomplete work. +- Reuse existing keys (`common.buttons.*`, `common.labels.*`, `createProfile.*`, etc.) before creating new namespaces. Check `en.json` first. +- Strings excluded from this rule: `console.log/warn/error`, dev-only debug labels, internal IDs, CSS class names, type names. If unsure whether a string renders to the user, assume it does and translate it. + ## Singletons - If there is a global singleton of a struct, only use it inside a method while properly initializing it, unless explicitly specified otherwise diff --git a/src/app/page.tsx b/src/app/page.tsx index 63be864..fb30c74 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -4,6 +4,7 @@ import { invoke } from "@tauri-apps/api/core"; import { listen } from "@tauri-apps/api/event"; import { getCurrent } from "@tauri-apps/plugin-deep-link"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { useTranslation } from "react-i18next"; import { CamoufoxConfigDialog } from "@/components/camoufox-config-dialog"; import { CloneProfileDialog } from "@/components/clone-profile-dialog"; import { CommercialTrialModal } from "@/components/commercial-trial-modal"; @@ -67,6 +68,7 @@ interface PendingUrl { } export default function Home() { + const { t } = useTranslation(); // Mount global version update listener/toasts useVersionUpdater(); @@ -428,9 +430,7 @@ export default function Home() { "Received show create profile dialog request:", event.payload, ); - showErrorToast( - "No profiles available. Please create a profile first before opening URLs.", - ); + showErrorToast(t("errors.noProfilesForUrl")); setCreateProfileDialogOpen(true); }); @@ -455,7 +455,7 @@ export default function Home() { } catch (error) { console.error("Failed to setup URL listener:", error); } - }, [handleUrlOpen]); + }, [handleUrlOpen, t]); const handleConfigureCamoufox = useCallback((profile: BrowserProfile) => { setCurrentProfileForCamoufoxConfig(profile); @@ -474,12 +474,14 @@ export default function Home() { } catch (err: unknown) { console.error("Failed to update camoufox config:", err); showErrorToast( - `Failed to update camoufox config: ${JSON.stringify(err)}`, + t("errors.updateCamoufoxConfigFailed", { + error: JSON.stringify(err), + }), ); throw err; } }, - [], + [t], ); const handleSaveWayfernConfig = useCallback( @@ -494,12 +496,12 @@ export default function Home() { } catch (err: unknown) { console.error("Failed to update wayfern config:", err); showErrorToast( - `Failed to update wayfern config: ${JSON.stringify(err)}`, + t("errors.updateWayfernConfigFailed", { error: JSON.stringify(err) }), ); throw err; } }, - [], + [t], ); const handleCreateProfile = useCallback( @@ -553,84 +555,92 @@ export default function Home() { // No need to manually reload - useProfileEvents will handle the update } catch (error) { showErrorToast( - `Failed to create profile: ${ - error instanceof Error ? error.message : String(error) - }`, + t("errors.createProfileFailed", { + error: error instanceof Error ? error.message : String(error), + }), ); } }, - [selectedGroupId], + [selectedGroupId, t], ); - const launchProfile = useCallback(async (profile: BrowserProfile) => { - console.log("Starting launch for profile:", profile.name); + const launchProfile = useCallback( + async (profile: BrowserProfile) => { + console.log("Starting launch for profile:", profile.name); - // Show one-time warning about window resizing for fingerprinted browsers - if (profile.browser === "camoufox" || profile.browser === "wayfern") { - try { - const dismissed = await invoke( - "get_window_resize_warning_dismissed", - ); - if (!dismissed) { - const proceed = await new Promise((resolve) => { - windowResizeWarningResolver.current = resolve; - setWindowResizeWarningBrowserType(profile.browser); - setWindowResizeWarningOpen(true); - }); - if (!proceed) { - return; + // Show one-time warning about window resizing for fingerprinted browsers + if (profile.browser === "camoufox" || profile.browser === "wayfern") { + try { + const dismissed = await invoke( + "get_window_resize_warning_dismissed", + ); + if (!dismissed) { + const proceed = await new Promise((resolve) => { + windowResizeWarningResolver.current = resolve; + setWindowResizeWarningBrowserType(profile.browser); + setWindowResizeWarningOpen(true); + }); + if (!proceed) { + return; + } } + } catch (error) { + console.error("Failed to check window resize warning:", error); } - } catch (error) { - console.error("Failed to check window resize warning:", error); } - } - try { - const result = await invoke("launch_browser_profile", { - profile, - }); - console.log("Successfully launched profile:", result.name); - } catch (err: unknown) { - console.error("Failed to launch browser:", err); - const errorMessage = err instanceof Error ? err.message : String(err); - showErrorToast(`Failed to launch browser: ${errorMessage}`); - throw err; - } - }, []); + try { + const result = await invoke("launch_browser_profile", { + profile, + }); + console.log("Successfully launched profile:", result.name); + } catch (err: unknown) { + console.error("Failed to launch browser:", err); + const errorMessage = err instanceof Error ? err.message : String(err); + showErrorToast( + t("errors.launchBrowserFailed", { error: errorMessage }), + ); + throw err; + } + }, + [t], + ); const handleCloneProfile = useCallback((profile: BrowserProfile) => { setCloneProfile(profile); }, []); - const handleDeleteProfile = useCallback(async (profile: BrowserProfile) => { - console.log("Attempting to delete profile:", profile.name); + const handleDeleteProfile = useCallback( + async (profile: BrowserProfile) => { + console.log("Attempting to delete profile:", profile.name); - try { - // First check if the browser is running for this profile - const isRunning = await invoke("check_browser_status", { - profile, - }); + try { + // First check if the browser is running for this profile + const isRunning = await invoke("check_browser_status", { + profile, + }); - if (isRunning) { + if (isRunning) { + showErrorToast(t("errors.cannotDeleteRunningProfile")); + return; + } + + // Attempt to delete the profile + await invoke("delete_profile", { profileId: profile.id }); + console.log("Profile deletion command completed successfully"); + + // No need to manually reload - useProfileEvents will handle the update + console.log("Profile deleted successfully"); + } catch (err: unknown) { + console.error("Failed to delete profile:", err); + const errorMessage = err instanceof Error ? err.message : String(err); showErrorToast( - "Cannot delete profile while browser is running. Please stop the browser first.", + t("errors.deleteProfileFailed", { error: errorMessage }), ); - return; } - - // Attempt to delete the profile - await invoke("delete_profile", { profileId: profile.id }); - console.log("Profile deletion command completed successfully"); - - // No need to manually reload - useProfileEvents will handle the update - console.log("Profile deleted successfully"); - } catch (err: unknown) { - console.error("Failed to delete profile:", err); - const errorMessage = err instanceof Error ? err.message : String(err); - showErrorToast(`Failed to delete profile: ${errorMessage}`); - } - }, []); + }, + [t], + ); const handleRenameProfile = useCallback( async (profileId: string, newName: string) => { @@ -639,28 +649,33 @@ export default function Home() { // No need to manually reload - useProfileEvents will handle the update } catch (err: unknown) { console.error("Failed to rename profile:", err); - showErrorToast(`Failed to rename profile: ${JSON.stringify(err)}`); + showErrorToast( + t("errors.renameProfileFailed", { error: JSON.stringify(err) }), + ); throw err; } }, - [], + [t], ); - const handleKillProfile = useCallback(async (profile: BrowserProfile) => { - console.log("Starting kill for profile:", profile.name); + const handleKillProfile = useCallback( + async (profile: BrowserProfile) => { + console.log("Starting kill for profile:", profile.name); - try { - await invoke("kill_browser_profile", { profile }); - console.log("Successfully killed profile:", profile.name); - // No need to manually reload - useProfileEvents will handle the update - } catch (err: unknown) { - console.error("Failed to kill browser:", err); - const errorMessage = err instanceof Error ? err.message : String(err); - showErrorToast(`Failed to kill browser: ${errorMessage}`); - // Re-throw the error so the table component can handle loading state cleanup - throw err; - } - }, []); + try { + await invoke("kill_browser_profile", { profile }); + console.log("Successfully killed profile:", profile.name); + // No need to manually reload - useProfileEvents will handle the update + } catch (err: unknown) { + console.error("Failed to kill browser:", err); + const errorMessage = err instanceof Error ? err.message : String(err); + showErrorToast(t("errors.killBrowserFailed", { error: errorMessage })); + // Re-throw the error so the table component can handle loading state cleanup + throw err; + } + }, + [t], + ); const handleDeleteSelectedProfiles = useCallback( async (profileIds: string[]) => { @@ -670,11 +685,13 @@ export default function Home() { } catch (err: unknown) { console.error("Failed to delete selected profiles:", err); showErrorToast( - `Failed to delete selected profiles: ${JSON.stringify(err)}`, + t("errors.deleteSelectedProfilesFailed", { + error: JSON.stringify(err), + }), ); } }, - [], + [t], ); const handleAssignProfilesToGroup = useCallback((profileIds: string[]) => { @@ -701,12 +718,14 @@ export default function Home() { } catch (error) { console.error("Failed to delete selected profiles:", error); showErrorToast( - `Failed to delete selected profiles: ${JSON.stringify(error)}`, + t("errors.deleteSelectedProfilesFailed", { + error: JSON.stringify(error), + }), ); } finally { setIsBulkDeleting(false); } - }, [selectedProfiles]); + }, [selectedProfiles, t]); const handleBulkGroupAssignment = useCallback(() => { if (selectedProfiles.length === 0) return; @@ -749,14 +768,12 @@ export default function Home() { (p.browser === "wayfern" || p.browser === "camoufox"), ); if (eligibleProfiles.length === 0) { - showErrorToast( - "Cookie copy only works with Wayfern and Camoufox profiles", - ); + showErrorToast(t("errors.cookieCopyUnsupportedBrowser")); return; } setSelectedProfilesForCookies(eligibleProfiles.map((p) => p.id)); setCookieCopyDialogOpen(true); - }, [selectedProfiles, profiles]); + }, [selectedProfiles, profiles, t]); const handleCopyCookiesToProfile = useCallback((profile: BrowserProfile) => { setSelectedProfilesForCookies([profile.id]); @@ -804,10 +821,10 @@ export default function Home() { }); } catch (error) { console.error("Failed to toggle sync:", error); - showErrorToast("Failed to update sync settings"); + showErrorToast(t("errors.updateSyncSettingsFailed")); } }, - [], + [t], ); useEffect(() => { @@ -825,19 +842,22 @@ export default function Home() { const { profile_id, status, error, profile_name } = event.payload; const toastId = `sync-${profile_id}`; const profile = profiles.find((p) => p.id === profile_id); - const name = profile_name || profile?.name || "Unknown"; + const name = + profile_name || profile?.name || t("common.labels.unknownProfile"); if (status === "synced") { dismissToast(toastId); if (profilesWithTransfer.has(profile_id)) { profilesWithTransfer.delete(profile_id); - showSuccessToast(`Profile '${name}' synced successfully`); + showSuccessToast(t("sync.toast.profileSynced", { name })); } } else if (status === "error") { dismissToast(toastId); profilesWithTransfer.delete(profile_id); showErrorToast( - `Failed to sync profile '${name}'${error ? `: ${error}` : ""}`, + error + ? t("sync.toast.profileSyncFailedWithError", { name, error }) + : t("sync.toast.profileSyncFailed", { name }), ); } }); @@ -857,7 +877,10 @@ export default function Home() { const payload = event.payload; const toastId = `sync-${payload.profile_id}`; const profile = profiles.find((p) => p.id === payload.profile_id); - const name = payload.profile_name || profile?.name || "Unknown"; + const name = + payload.profile_name || + profile?.name || + t("common.labels.unknownProfile"); if ( payload.phase === "started" || @@ -889,7 +912,7 @@ export default function Home() { if (unlistenStatus) unlistenStatus(); if (unlistenProgress) unlistenProgress(); }; - }, [profiles]); + }, [profiles, t]); useEffect(() => { // Check for startup default browser prompt @@ -1272,9 +1295,13 @@ export default function Home() { setShowBulkDeleteConfirmation(false); }} onConfirm={confirmBulkDelete} - title="Delete Selected Profiles" - description={`This action cannot be undone. This will permanently delete ${selectedProfiles.length} profile${selectedProfiles.length !== 1 ? "s" : ""} and all associated data.`} - confirmButtonText={`Delete ${selectedProfiles.length} Profile${selectedProfiles.length !== 1 ? "s" : ""}`} + title={t("profiles.bulkDelete.title")} + description={t("profiles.bulkDelete.description", { + count: selectedProfiles.length, + })} + confirmButtonText={t("profiles.bulkDelete.confirmButton", { + count: selectedProfiles.length, + })} isLoading={isBulkDeleting} profileIds={selectedProfiles} profiles={profiles.map((p) => ({ id: p.id, name: p.name }))} diff --git a/src/components/camoufox-config-dialog.tsx b/src/components/camoufox-config-dialog.tsx index 5dfa5b8..fe93043 100644 --- a/src/components/camoufox-config-dialog.tsx +++ b/src/components/camoufox-config-dialog.tsx @@ -1,6 +1,7 @@ "use client"; import { useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; import { SharedCamoufoxConfigForm } from "@/components/shared-camoufox-config-form"; import { Dialog, @@ -51,6 +52,7 @@ export function CamoufoxConfigDialog({ isRunning = false, crossOsUnlocked = false, }: CamoufoxConfigDialogProps) { + const { t } = useTranslation(); // Use union type to support both Camoufox and Wayfern configs const [config, setConfig] = useState(() => ({ geoip: true, @@ -93,9 +95,8 @@ export function CamoufoxConfigDialog({ JSON.parse(config.fingerprint); } catch (_error) { const { toast } = await import("sonner"); - toast.error("Invalid fingerprint configuration", { - description: - "The fingerprint configuration contains invalid JSON. Please check your advanced settings.", + toast.error(t("camoufoxDialog.invalidFingerprint"), { + description: t("camoufoxDialog.invalidFingerprintDescription"), }); return; } @@ -112,9 +113,11 @@ export function CamoufoxConfigDialog({ } catch (error) { console.error("Failed to save config:", error); const { toast } = await import("sonner"); - toast.error("Failed to save configuration", { + toast.error(t("camoufoxDialog.saveFailed"), { description: - error instanceof Error ? error.message : "Unknown error occurred", + error instanceof Error + ? error.message + : t("camoufoxDialog.unknownError"), }); } finally { setIsSaving(false); @@ -149,8 +152,15 @@ export function CamoufoxConfigDialog({ - {isRunning ? "View" : "Configure"} Fingerprint Settings -{" "} - {profile.name} ({browserName}) + {isRunning + ? t("camoufoxDialog.titleView", { + name: profile.name, + browser: browserName, + }) + : t("camoufoxDialog.titleConfigure", { + name: profile.name, + browser: browserName, + })} @@ -185,7 +195,7 @@ export function CamoufoxConfigDialog({ - {isRunning ? "Close" : "Cancel"} + {isRunning ? t("common.buttons.close") : t("common.buttons.cancel")} {!isRunning && ( - Save + {t("common.buttons.save")} )} diff --git a/src/components/clone-profile-dialog.tsx b/src/components/clone-profile-dialog.tsx index ae27cae..cdb39c0 100644 --- a/src/components/clone-profile-dialog.tsx +++ b/src/components/clone-profile-dialog.tsx @@ -62,7 +62,7 @@ export function CloneProfileDialog({ onCloneComplete?.(); } catch (err: unknown) { const errorMessage = err instanceof Error ? err.message : String(err); - showErrorToast(`Failed to clone profile: ${errorMessage}`); + showErrorToast(t("errors.cloneProfileFailed", { error: errorMessage })); } finally { setIsLoading(false); } diff --git a/src/components/commercial-trial-modal.tsx b/src/components/commercial-trial-modal.tsx index 41e1757..bce8748 100644 --- a/src/components/commercial-trial-modal.tsx +++ b/src/components/commercial-trial-modal.tsx @@ -2,6 +2,7 @@ import { invoke } from "@tauri-apps/api/core"; import { useCallback, useState } from "react"; +import { useTranslation } from "react-i18next"; import { LoadingButton } from "@/components/loading-button"; import { Dialog, @@ -22,6 +23,7 @@ export function CommercialTrialModal({ isOpen, onClose, }: CommercialTrialModalProps) { + const { t } = useTranslation(); const [isAcknowledging, setIsAcknowledging] = useState(false); const handleAcknowledge = useCallback(async () => { @@ -31,14 +33,16 @@ export function CommercialTrialModal({ onClose(); } catch (error) { console.error("Failed to acknowledge trial expiration:", error); - showErrorToast("Failed to save acknowledgment", { + showErrorToast(t("commercialTrial.failed"), { description: - error instanceof Error ? error.message : "Please try again", + error instanceof Error + ? error.message + : t("commercialTrial.tryAgain"), }); } finally { setIsAcknowledging(false); } - }, [onClose]); + }, [onClose, t]); return ( @@ -55,17 +59,15 @@ export function CommercialTrialModal({ }} > - Commercial Trial Expired + {t("commercialTrial.title")} - Your 2-week commercial trial period has ended. + {t("commercialTrial.description")}

- If you are using Donut Browser for business purposes, you need to - purchase a commercial license to continue. You can still use it for - personal use for free. + {t("commercialTrial.body")}

@@ -74,7 +76,7 @@ export function CommercialTrialModal({ onClick={handleAcknowledge} isLoading={isAcknowledging} > - I Understand + {t("commercialTrial.understandButton")}
diff --git a/src/components/cookie-copy-dialog.tsx b/src/components/cookie-copy-dialog.tsx index 5cb1816..d7b3a7c 100644 --- a/src/components/cookie-copy-dialog.tsx +++ b/src/components/cookie-copy-dialog.tsx @@ -2,6 +2,7 @@ import { invoke } from "@tauri-apps/api/core"; import { useCallback, useEffect, useMemo, useState } from "react"; +import { useTranslation } from "react-i18next"; import { LuChevronDown, LuChevronRight, @@ -66,6 +67,7 @@ export function CookieCopyDialog({ runningProfiles, onCopyComplete, }: CookieCopyDialogProps) { + const { t } = useTranslation(); const [sourceProfileId, setSourceProfileId] = useState(null); const [cookieData, setCookieData] = useState(null); const [isLoadingCookies, setIsLoadingCookies] = useState(false); @@ -243,10 +245,11 @@ export function CookieCopyDialog({ runningProfiles.has(p.id), ); if (runningTargets.length > 0) { + const names = runningTargets.map((p) => p.name).join(", "); toast.error( - `Cannot copy cookies: ${runningTargets.map((p) => p.name).join(", ")} ${ - runningTargets.length === 1 ? "is" : "are" - } still running`, + runningTargets.length === 1 + ? t("cookies.copy.cannotCopyRunningOne", { names }) + : t("cookies.copy.cannotCopyRunningMany", { names }), ); return; } @@ -277,10 +280,15 @@ export function CookieCopyDialog({ } if (errors.length > 0) { - toast.error(`Some errors occurred: ${errors.join(", ")}`); + toast.error( + t("cookies.copy.someErrors", { errors: errors.join(", ") }), + ); } else { toast.success( - `Successfully copied ${totalCopied + totalReplaced} cookies (${totalReplaced} replaced)`, + t("cookies.copy.successMessage", { + copied: totalCopied + totalReplaced, + replaced: totalReplaced, + }), ); onCopyComplete?.(); onClose(); @@ -288,7 +296,9 @@ export function CookieCopyDialog({ } catch (err) { console.error("Failed to copy cookies:", err); toast.error( - `Failed to copy cookies: ${err instanceof Error ? err.message : String(err)}`, + t("cookies.copy.failedMessage", { + error: err instanceof Error ? err.message : String(err), + }), ); } finally { setIsCopying(false); @@ -300,6 +310,7 @@ export function CookieCopyDialog({ buildSelectedCookies, onCopyComplete, onClose, + t, ]); useEffect(() => { @@ -325,23 +336,30 @@ export function CookieCopyDialog({ - Copy Cookies + {t("cookies.copy.title")} - Copy cookies from a source profile to {selectedProfiles.length}{" "} - selected profile{selectedProfiles.length !== 1 ? "s" : ""}. + {selectedProfiles.length === 1 + ? t("cookies.copy.dialogDescription_one", { + count: selectedProfiles.length, + }) + : t("cookies.copy.dialogDescription_other", { + count: selectedProfiles.length, + })}
- + { setSearchQuery(e.target.value); @@ -435,8 +459,8 @@ export function CookieCopyDialog({ ) : filteredDomains.length === 0 ? (
{searchQuery - ? "No matching cookies found" - : "No cookies found"} + ? t("cookies.copy.noMatching") + : t("cookies.copy.noFound")}
) : ( @@ -457,8 +481,7 @@ export function CookieCopyDialog({ )}

- Existing cookies with the same name and domain will be replaced. - Other cookies will be kept. + {t("cookies.copy.replaceNote")}

)} @@ -470,15 +493,22 @@ export function CookieCopyDialog({ onClick={onClose} disabled={isCopying} > - Cancel + {t("common.buttons.cancel")} void handleCopy()} disabled={!canCopy} > - Copy {selectedCookieCount > 0 ? `${selectedCookieCount} ` : ""} - Cookie{selectedCookieCount !== 1 ? "s" : ""} + {selectedCookieCount === 0 + ? t("cookies.copy.copyButtonEmpty") + : selectedCookieCount === 1 + ? t("cookies.copy.copyButton_one", { + count: selectedCookieCount, + }) + : t("cookies.copy.copyButton_other", { + count: selectedCookieCount, + })} diff --git a/src/components/cookie-management-dialog.tsx b/src/components/cookie-management-dialog.tsx index 55b70e5..bc14219 100644 --- a/src/components/cookie-management-dialog.tsx +++ b/src/components/cookie-management-dialog.tsx @@ -4,6 +4,7 @@ import { invoke } from "@tauri-apps/api/core"; import { save } from "@tauri-apps/plugin-dialog"; import { writeTextFile } from "@tauri-apps/plugin-fs"; import { useCallback, useEffect, useMemo, useState } from "react"; +import { useTranslation } from "react-i18next"; import { LuChevronDown, LuChevronRight, LuUpload } from "react-icons/lu"; import { toast } from "sonner"; import { LoadingButton } from "@/components/loading-button"; @@ -122,6 +123,7 @@ export function CookieManagementDialog({ profile, initialTab = "import", }: CookieManagementDialogProps) { + const { t } = useTranslation(); // Import state const [fileContent, setFileContent] = useState(null); const [fileName, setFileName] = useState(null); @@ -171,13 +173,15 @@ export function CookieManagementDialog({ setExportSelection(initSelectionFromCookieData(result)); } catch (err) { toast.error( - `Failed to load cookies: ${err instanceof Error ? err.message : String(err)}`, + t("cookies.management.loadFailed", { + error: err instanceof Error ? err.message : String(err), + }), ); } finally { setIsLoadingExportCookies(false); } }, - [exportCookieData], + [exportCookieData, t], ); useEffect(() => { @@ -220,19 +224,22 @@ export function CookieManagementDialog({ [resetImportState, resetExportState], ); - const handleFileRead = useCallback((file: File) => { - const reader = new FileReader(); - reader.onload = (e) => { - const content = e.target?.result as string; - setFileContent(content); - setFileName(file.name); - setCookieCount(countCookies(content)); - }; - reader.onerror = () => { - toast.error("Failed to read file"); - }; - reader.readAsText(file); - }, []); + const handleFileRead = useCallback( + (file: File) => { + const reader = new FileReader(); + reader.onload = (e) => { + const content = e.target?.result as string; + setFileContent(content); + setFileName(file.name); + setCookieCount(countCookies(content)); + }; + reader.onerror = () => { + toast.error(t("cookies.management.fileReadError")); + }; + reader.readAsText(file); + }, + [t], + ); const handleImport = useCallback(async () => { if (!fileContent || !profile) return; @@ -297,14 +304,14 @@ export function CookieManagementDialog({ } await writeTextFile(filePath, content); - toast.success("Cookies exported successfully"); + toast.success(t("cookies.export.success")); handleClose(); } catch (error) { toast.error(error instanceof Error ? error.message : String(error)); } finally { setIsExporting(false); } - }, [profile, format, getSelectedCookies, handleClose]); + }, [profile, format, getSelectedCookies, handleClose, t]); const toggleDomain = useCallback( (domain: string, cookies: UnifiedCookie[]) => { @@ -385,7 +392,7 @@ export function CookieManagementDialog({ - Cookie Management + {t("cookies.management.title")} - Import - Export + + {t("cookies.management.tabImport")} + + + {t("cookies.management.tabExport")} + {!fileContent && (

- Import cookies from a Netscape or JSON format file. + {t("cookies.management.importDescription")}

- Click to choose a cookie file + {t("cookies.management.dropPrompt")}
- (.txt, .cookies, or .json) + + {t("cookies.management.fileFormats")} +

{fileName}
- {cookieCount} cookies found + {t("cookies.management.cookiesFound", { + count: cookieCount, + })}
- Back + {t("cookies.management.backButton")} void handleImport()} disabled={cookieCount === 0} > - Import + {t("cookies.management.importButton")}
@@ -468,17 +483,23 @@ export function CookieManagementDialog({
- Successfully imported {importResult.cookies_imported}{" "} - cookies ({importResult.cookies_replaced} replaced) + {t("cookies.management.importedSuccess", { + imported: importResult.cookies_imported, + replaced: importResult.cookies_replaced, + })}
{importResult.errors.length > 0 && (
- {importResult.errors.length} line(s) skipped + {t("cookies.management.linesSkipped", { + count: importResult.errors.length, + })}
)}
- Done + + {t("cookies.management.doneButton")} +
)} @@ -486,7 +507,7 @@ export function CookieManagementDialog({
- +
@@ -506,11 +531,13 @@ export function CookieManagementDialog({
@@ -521,8 +548,8 @@ export function CookieManagementDialog({ onClick={toggleSelectAll} > {selectedExportCount === exportCookieData.total_count - ? "Deselect all" - : "Select all"} + ? t("cookies.management.deselectAll") + : t("cookies.management.selectAll")} )}
@@ -533,7 +560,7 @@ export function CookieManagementDialog({
) : !exportCookieData || exportCookieData.domains.length === 0 ? (
- No cookies found in this profile + {t("cookies.management.noCookies")}
) : ( @@ -556,14 +583,14 @@ export function CookieManagementDialog({
- Cancel + {t("common.buttons.cancel")} void handleExport()} disabled={selectedExportCount === 0} > - Export + {t("cookies.management.exportButton")}
diff --git a/src/components/create-group-dialog.tsx b/src/components/create-group-dialog.tsx index 058289a..96d3784 100644 --- a/src/components/create-group-dialog.tsx +++ b/src/components/create-group-dialog.tsx @@ -2,6 +2,7 @@ import { invoke } from "@tauri-apps/api/core"; import { useCallback, useState } from "react"; +import { useTranslation } from "react-i18next"; import { toast } from "sonner"; import { LoadingButton } from "@/components/loading-button"; import { @@ -28,6 +29,7 @@ export function CreateGroupDialog({ onClose, onGroupCreated, }: CreateGroupDialogProps) { + const { t } = useTranslation(); const [groupName, setGroupName] = useState(""); const [isCreating, setIsCreating] = useState(false); const [error, setError] = useState(null); @@ -42,20 +44,20 @@ export function CreateGroupDialog({ name: groupName.trim(), }); - toast.success("Group created successfully"); + toast.success(t("groups.createSuccess")); onGroupCreated(newGroup); setGroupName(""); onClose(); } catch (err) { console.error("Failed to create group:", err); const errorMessage = - err instanceof Error ? err.message : "Failed to create group"; + err instanceof Error ? err.message : t("groups.createFailed"); setError(errorMessage); toast.error(errorMessage); } finally { setIsCreating(false); } - }, [groupName, onGroupCreated, onClose]); + }, [groupName, onGroupCreated, onClose, t]); const handleClose = useCallback(() => { setGroupName(""); @@ -67,18 +69,16 @@ export function CreateGroupDialog({ - Create New Group - - Create a new group to organize your browser profiles. - + {t("groups.createTitle")} + {t("groups.createDescription")}
- + { setGroupName(e.target.value); @@ -105,14 +105,14 @@ export function CreateGroupDialog({ onClick={handleClose} disabled={isCreating} > - Cancel + {t("common.buttons.cancel")} void handleCreate()} disabled={!groupName.trim()} > - Create + {t("common.buttons.create")} diff --git a/src/components/create-profile-dialog.tsx b/src/components/create-profile-dialog.tsx index 380ee7e..4a16b71 100644 --- a/src/components/create-profile-dialog.tsx +++ b/src/components/create-profile-dialog.tsx @@ -625,10 +625,10 @@ export function CreateProfileDialog({

- Regular Browsers + {t("createProfile.regular.title")}

- Choose from supported regular browsers + {t("createProfile.regular.description")}

@@ -655,7 +655,7 @@ export function CreateProfileDialog({ {browser.label}
- Regular Browser + {t("createProfile.regular.badge")}
@@ -672,7 +672,9 @@ export function CreateProfileDialog({
{/* Profile Name */}
- +
@@ -722,7 +726,7 @@ export function CreateProfileDialog({

- Fetching available versions... + {t("createProfile.version.fetching")}

)} @@ -739,7 +743,7 @@ export function CreateProfileDialog({ size="sm" variant="outline" > - Retry + {t("common.buttons.retry")}
)} @@ -748,8 +752,9 @@ export function CreateProfileDialog({ !getBestAvailableVersion("wayfern") && (

- Wayfern is not available on your platform - yet. + {t("createProfile.platformUnavailable", { + browser: "Wayfern", + })}

)} @@ -760,11 +765,12 @@ export function CreateProfileDialog({ getBestAvailableVersion("wayfern") && (

- {(() => { - const bestVersion = - getBestAvailableVersion("wayfern"); - return `Wayfern version (${bestVersion?.version}) needs to be downloaded`; - })()} + {t("createProfile.version.needsDownload", { + browser: "Wayfern", + version: + getBestAvailableVersion("wayfern") + ?.version, + })}

{ @@ -779,8 +785,8 @@ export function CreateProfileDialog({ )} > {isBrowserCurrentlyDownloading("wayfern") - ? "Downloading..." - : "Download"} + ? t("common.buttons.downloading") + : t("common.buttons.download")}
)} @@ -789,20 +795,22 @@ export function CreateProfileDialog({ !isBrowserCurrentlyDownloading("wayfern") && isBrowserVersionAvailable("wayfern") && (
- {(() => { - const bestVersion = - getBestAvailableVersion("wayfern"); - return `✓ Wayfern version (${bestVersion?.version}) is available`; - })()} + ✓{" "} + {t("createProfile.version.available", { + browser: "Wayfern", + version: + getBestAvailableVersion("wayfern") + ?.version, + })}
)} {isBrowserCurrentlyDownloading("wayfern") && (
- {(() => { - const bestVersion = - getBestAvailableVersion("wayfern"); - return `Downloading Wayfern version (${bestVersion?.version})...`; - })()} + {t("createProfile.version.downloading", { + browser: "Wayfern", + version: + getBestAvailableVersion("wayfern")?.version, + })}
)} @@ -826,7 +834,7 @@ export function CreateProfileDialog({

- Fetching available versions... + {t("createProfile.version.fetching")}

)} @@ -843,7 +851,7 @@ export function CreateProfileDialog({ size="sm" variant="outline" > - Retry + {t("common.buttons.retry")}
)} @@ -852,8 +860,9 @@ export function CreateProfileDialog({ !getBestAvailableVersion("camoufox") && (

- Camoufox is not available on your platform - yet. + {t("createProfile.platformUnavailable", { + browser: "Camoufox", + })}

)} @@ -864,11 +873,12 @@ export function CreateProfileDialog({ getBestAvailableVersion("camoufox") && (

- {(() => { - const bestVersion = - getBestAvailableVersion("camoufox"); - return `Camoufox version (${bestVersion?.version}) needs to be downloaded`; - })()} + {t("createProfile.version.needsDownload", { + browser: "Camoufox", + version: + getBestAvailableVersion("camoufox") + ?.version, + })}

{ @@ -883,8 +893,8 @@ export function CreateProfileDialog({ )} > {isBrowserCurrentlyDownloading("camoufox") - ? "Downloading..." - : "Download"} + ? t("common.buttons.downloading") + : t("common.buttons.download")}
)} @@ -893,20 +903,23 @@ export function CreateProfileDialog({ !isBrowserCurrentlyDownloading("camoufox") && isBrowserVersionAvailable("camoufox") && (
- {(() => { - const bestVersion = - getBestAvailableVersion("camoufox"); - return `✓ Camoufox version (${bestVersion?.version}) is available`; - })()} + ✓{" "} + {t("createProfile.version.available", { + browser: "Camoufox", + version: + getBestAvailableVersion("camoufox") + ?.version, + })}
)} {isBrowserCurrentlyDownloading("camoufox") && (
- {(() => { - const bestVersion = - getBestAvailableVersion("camoufox"); - return `Downloading Camoufox version (${bestVersion?.version})...`; - })()} + {t("createProfile.version.downloading", { + browser: "Camoufox", + version: + getBestAvailableVersion("camoufox") + ?.version, + })}
)} @@ -971,13 +984,15 @@ export function CreateProfileDialog({ getBestAvailableVersion(selectedBrowser) && (

- {(() => { - const bestVersion = - getBestAvailableVersion( - selectedBrowser, - ); - return `Latest version (${bestVersion?.version}) needs to be downloaded`; - })()} + {t( + "createProfile.version.latestNeedsDownload", + { + version: + getBestAvailableVersion( + selectedBrowser, + )?.version, + }, + )}

{ @@ -992,7 +1007,7 @@ export function CreateProfileDialog({ selectedBrowser, )} > - Download + {t("common.buttons.download")}
)} @@ -1005,26 +1020,31 @@ export function CreateProfileDialog({ selectedBrowser, ) && (
- {(() => { - const bestVersion = - getBestAvailableVersion( - selectedBrowser, - ); - return `✓ Latest version (${bestVersion?.version}) is available`; - })()} + ✓{" "} + {t( + "createProfile.version.latestAvailable", + { + version: + getBestAvailableVersion( + selectedBrowser, + )?.version, + }, + )}
)} {isBrowserCurrentlyDownloading( selectedBrowser, ) && (
- {(() => { - const bestVersion = - getBestAvailableVersion( - selectedBrowser, - ); - return `Downloading version (${bestVersion?.version})...`; - })()} + {t( + "createProfile.version.latestDownloading", + { + version: + getBestAvailableVersion( + selectedBrowser, + )?.version, + }, + )}
)}
@@ -1035,7 +1055,7 @@ export function CreateProfileDialog({ {/* Proxy / VPN Selection - Always visible */}
- + - Add Proxy + {" "} + {t("createProfile.proxy.addProxy")}
{storedProxies.length > 0 || vpnConfigs.length > 0 ? ( @@ -1061,7 +1082,7 @@ export function CreateProfileDialog({ > {(() => { if (!selectedProxyId) - return "No proxy / VPN"; + return t("createProfile.proxy.noProxy"); if (selectedProxyId.startsWith("vpn-")) { const vpn = vpnConfigs.find( (v) => @@ -1069,12 +1090,15 @@ export function CreateProfileDialog({ ); return vpn ? `WG — ${vpn.name}` - : "No proxy / VPN"; + : t("createProfile.proxy.noProxy"); } const proxy = storedProxies.find( (p) => p.id === selectedProxyId, ); - return proxy?.name ?? "No proxy / VPN"; + return ( + proxy?.name ?? + t("createProfile.proxy.noProxy") + ); })()} @@ -1084,10 +1108,14 @@ export function CreateProfileDialog({ sideOffset={8} > - + - No proxies or VPNs found. + {t("createProfile.proxy.notFound")} - None + {t("common.labels.none")} {storedProxies.map((proxy) => ( ) : (
- No proxies or VPNs available. Add one to route - this profile's traffic. + {t("createProfile.proxy.noProxiesAvailable")}
)}
@@ -1265,7 +1292,9 @@ export function CreateProfileDialog({
{/* Profile Name */}
- +
@@ -1310,7 +1341,7 @@ export function CreateProfileDialog({ size="sm" variant="outline" > - Retry + {t("common.buttons.retry")}
)} @@ -1323,13 +1354,15 @@ export function CreateProfileDialog({ getBestAvailableVersion(selectedBrowser) && (

- {(() => { - const bestVersion = - getBestAvailableVersion( - selectedBrowser, - ); - return `Latest version (${bestVersion?.version}) needs to be downloaded`; - })()} + {t( + "createProfile.version.latestNeedsDownload", + { + version: + getBestAvailableVersion( + selectedBrowser, + )?.version, + }, + )}

{ @@ -1344,7 +1377,7 @@ export function CreateProfileDialog({ selectedBrowser, )} > - Download + {t("common.buttons.download")}
)} @@ -1355,24 +1388,30 @@ export function CreateProfileDialog({ ) && isBrowserVersionAvailable(selectedBrowser) && (
- {(() => { - const bestVersion = - getBestAvailableVersion( - selectedBrowser, - ); - return `✓ Latest version (${bestVersion?.version}) is available`; - })()} + ✓{" "} + {t( + "createProfile.version.latestAvailable", + { + version: + getBestAvailableVersion( + selectedBrowser, + )?.version, + }, + )}
)} {isBrowserCurrentlyDownloading( selectedBrowser, ) && (
- {(() => { - const bestVersion = - getBestAvailableVersion(selectedBrowser); - return `Downloading version (${bestVersion?.version})...`; - })()} + {t( + "createProfile.version.latestDownloading", + { + version: + getBestAvailableVersion(selectedBrowser) + ?.version, + }, + )}
)}
@@ -1382,7 +1421,7 @@ export function CreateProfileDialog({ {/* Proxy / VPN Selection - Always visible */}
- + - Add Proxy + {" "} + {t("createProfile.proxy.addProxy")}
{storedProxies.length > 0 || vpnConfigs.length > 0 ? ( @@ -1408,7 +1448,7 @@ export function CreateProfileDialog({ > {(() => { if (!selectedProxyId) - return "No proxy / VPN"; + return t("createProfile.proxy.noProxy"); if (selectedProxyId.startsWith("vpn-")) { const vpn = vpnConfigs.find( (v) => @@ -1416,12 +1456,15 @@ export function CreateProfileDialog({ ); return vpn ? `WG — ${vpn.name}` - : "No proxy / VPN"; + : t("createProfile.proxy.noProxy"); } const proxy = storedProxies.find( (p) => p.id === selectedProxyId, ); - return proxy?.name ?? "No proxy / VPN"; + return ( + proxy?.name ?? + t("createProfile.proxy.noProxy") + ); })()} @@ -1431,10 +1474,14 @@ export function CreateProfileDialog({ sideOffset={8} > - + - No proxies or VPNs found. + {t("createProfile.proxy.notFound")} - None + {t("common.labels.none")} {storedProxies.map((proxy) => ( ) : (
- No proxies or VPNs available. Add one to route - this profile's traffic. + {t("createProfile.proxy.noProxiesAvailable")}
)}
@@ -1549,19 +1595,19 @@ export function CreateProfileDialog({ {currentStep === "browser-config" ? ( <> - Back + {t("common.buttons.back")} - Create + {t("common.buttons.create")} ) : ( - Cancel + {t("common.buttons.cancel")} )} diff --git a/src/components/custom-toast.tsx b/src/components/custom-toast.tsx index eacc74f..b0f0a11 100644 --- a/src/components/custom-toast.tsx +++ b/src/components/custom-toast.tsx @@ -49,6 +49,7 @@ */ /** biome-ignore-all lint/suspicious/noExplicitAny: TODO */ +import { useTranslation } from "react-i18next"; import { LuCheckCheck, LuDownload, @@ -214,6 +215,7 @@ function getToastIcon(type: ToastProps["type"], stage?: string) { } export function UnifiedToast(props: ToastProps) { + const { t } = useTranslation(); const { title, description, type, action, onCancel } = props; const stage = "stage" in props ? props.stage : undefined; const progress = "progress" in props ? props.progress : undefined; @@ -231,7 +233,7 @@ export function UnifiedToast(props: ToastProps) { type="button" onClick={onCancel} className="ml-2 p-1 rounded hover:bg-muted text-muted-foreground hover:text-foreground transition-colors flex-shrink-0" - aria-label="Cancel" + aria-label={t("common.buttons.cancel")} > diff --git a/src/components/data-table-action-bar.tsx b/src/components/data-table-action-bar.tsx index d675fb4..e5110d9 100644 --- a/src/components/data-table-action-bar.tsx +++ b/src/components/data-table-action-bar.tsx @@ -4,6 +4,7 @@ import type { Table } from "@tanstack/react-table"; import { AnimatePresence, motion } from "motion/react"; import * as React from "react"; import * as ReactDOM from "react-dom"; +import { useTranslation } from "react-i18next"; import { LuX } from "react-icons/lu"; import { Button } from "@/components/ui/button"; import { @@ -134,6 +135,7 @@ interface DataTableActionBarSelectionProps { function DataTableActionBarSelection({ table, }: DataTableActionBarSelectionProps) { + const { t } = useTranslation(); const onClearSelection = React.useCallback(() => { table.toggleAllRowsSelected(false); }, [table]); @@ -141,7 +143,9 @@ function DataTableActionBarSelection({ return (
- {table.getFilteredSelectedRowModel().rows.length} selected + {t("dataTableActionBar.selected", { + count: table.getFilteredSelectedRowModel().rows.length, + })}
@@ -159,9 +163,9 @@ function DataTableActionBarSelection({ sideOffset={10} className="flex items-center gap-2 border bg-accent px-2 py-1 font-semibold text-foreground dark:bg-card [&>span]:hidden" > -

Clear selection

+

{t("dataTableActionBar.clearSelection")}

- + Esc diff --git a/src/components/delete-confirmation-dialog.tsx b/src/components/delete-confirmation-dialog.tsx index b27a994..342aabd 100644 --- a/src/components/delete-confirmation-dialog.tsx +++ b/src/components/delete-confirmation-dialog.tsx @@ -1,5 +1,6 @@ "use client"; +import { useTranslation } from "react-i18next"; import { Dialog, DialogContent, @@ -29,11 +30,12 @@ export function DeleteConfirmationDialog({ onConfirm, title, description, - confirmButtonText = "Delete", + confirmButtonText, isLoading = false, profileIds, profiles = [], }: DeleteConfirmationDialogProps) { + const { t } = useTranslation(); const handleConfirm = async () => { await onConfirm(); }; @@ -47,7 +49,7 @@ export function DeleteConfirmationDialog({ {profileIds && profileIds.length > 0 && (

- Profiles to be deleted: + {t("deleteDialog.profilesToDelete")}