refactor: profile imports

This commit is contained in:
zhom
2026-08-10 09:19:25 +04:00
parent a6b79341b3
commit 32a1728dee
33 changed files with 4917 additions and 387 deletions
+233 -89
View File
@@ -53,10 +53,88 @@ import type {
ImportProfileItem,
ProfileImportBatchResult,
ProfileImportProgress,
ProfileImportReport,
WayfernConfig,
} from "@/types";
import { RippleButton } from "./ui/ripple";
/**
* What an import actually carried, and what it could not.
*
* The counts matter more than they look: an import that reports zero of
* everything is the exact symptom of the bug where copied data landed where
* the browser never reads it, and it used to be indistinguishable from success.
*/
function ImportReportSummary({ report }: { report: ProfileImportReport }) {
const { t } = useTranslation();
// Label-then-value rather than "{{count}} cookies": it keeps the row scannable
// and sidesteps needing correct plural forms in ten languages.
const carried = (
[
["importProfile.reportCookies", report.cookies_migrated],
["importProfile.reportPasswords", report.passwords_migrated],
["importProfile.reportAutofill", report.payment_methods_migrated],
["importProfile.reportExtensions", report.extensions_migrated],
["importProfile.reportHistory", report.history_entries],
["importProfile.reportBookmarks", report.bookmarks],
["importProfile.reportLocalStorage", report.local_storage_origins],
] as const
)
.filter(([, count]) => count > 0)
.map(([key, count]) => `${t(key)} ${count.toLocaleString()}`);
const unrecoverable =
report.cookies_unrecoverable +
report.passwords_unrecoverable +
report.payment_methods_unrecoverable;
return (
<div className="mt-0.5 space-y-0.5 pl-1 text-xs text-muted-foreground">
<p>
{carried.length > 0
? carried.join(" · ")
: t("importProfile.reportNothingCarried")}
</p>
{unrecoverable > 0 && (
<p>
{t("importProfile.reportUnrecoverable", { count: unrecoverable })}
</p>
)}
{report.warnings.map((code) => (
<p key={code} className="text-warning-text">
{t(`importProfile.warnings.${code}`)}
</p>
))}
</div>
);
}
/**
* Fold a retry's results back into the batch it came from.
*
* A retry only resubmits the items that failed, so the previous batch is still
* authoritative for every other row. Replacing it wholesale would make the
* successful imports disappear from the summary.
*/
function mergeImportResults(
previous: ProfileImportBatchResult,
retry: ProfileImportBatchResult,
): ProfileImportBatchResult {
const byPath = new Map(retry.results.map((item) => [item.source_path, item]));
const results = previous.results.map(
(item) => byPath.get(item.source_path) ?? item,
);
const count = (status: string) =>
results.filter((item) => item.status === status).length;
return {
imported_count: count("imported"),
skipped_count: count("skipped"),
failed_count: count("failed"),
results,
};
}
interface ImportProfileDialogProps {
isOpen: boolean;
onClose: () => void;
@@ -283,69 +361,99 @@ export function ImportProfileDialog({
}
};
const handleImport = useCallback(async () => {
if (selectedProfiles.length === 0) {
toast.error(t("importProfile.selectAtLeastOne"));
return;
}
if (
selectedProfiles.some((p) => !(profileNames[p.path] ?? p.name).trim())
) {
toast.error(t("importProfile.emptyNames"));
return;
}
const items: ImportProfileItem[] = selectedProfiles.map((p, index) => ({
source_path: p.path,
browser_type: p.browser,
new_profile_name: (profileNames[p.path] ?? p.name).trim(),
proxy_id: proxyIdForIndex(index),
vpn_id: vpnAssignment === "none" ? null : vpnAssignment,
}));
setCurrentStep("importing");
setIsImporting(true);
setProgress(null);
setResult(null);
try {
const batchResult = await invoke<ProfileImportBatchResult>(
"import_browser_profiles",
{
items,
groupId: selectedGroupId === "none" ? null : selectedGroupId,
duplicateStrategy: duplicateStrategy,
wayfernConfig,
},
);
setResult(batchResult);
toast.success(
t("importProfile.resultsSummary", {
imported: batchResult.imported_count,
skipped: batchResult.skipped_count,
failed: batchResult.failed_count,
}),
);
if (batchResult.imported_count > 0 && !reducedMotion) {
fireSprinkleConfetti();
const handleImport = useCallback(
async (allowRunning = false, retryPaths?: ReadonlySet<string>) => {
if (selectedProfiles.length === 0) {
toast.error(t("importProfile.selectAtLeastOne"));
return;
}
} catch (error) {
console.error("Failed to import profiles:", error);
toast.error(translateBackendError(t, error));
setCurrentStep("configure");
} finally {
setIsImporting(false);
}
}, [
selectedProfiles,
profileNames,
proxyIdForIndex,
vpnAssignment,
selectedGroupId,
duplicateStrategy,
wayfernConfig,
reducedMotion,
t,
]);
if (
selectedProfiles.some((p) => !(profileNames[p.path] ?? p.name).trim())
) {
toast.error(t("importProfile.emptyNames"));
return;
}
// Filter AFTER the map, so a retry keeps the proxy each profile was
// originally assigned by the index-based round-robin.
const items: ImportProfileItem[] = selectedProfiles
.map((p, index) => ({
source_path: p.path,
browser_type: p.browser,
new_profile_name: (profileNames[p.path] ?? p.name).trim(),
proxy_id: proxyIdForIndex(index),
vpn_id: vpnAssignment === "none" ? null : vpnAssignment,
allow_running: allowRunning,
}))
.filter((item) => !retryPaths || retryPaths.has(item.source_path));
if (items.length === 0) {
return;
}
setCurrentStep("importing");
setIsImporting(true);
setProgress(null);
// A retry covers only the failed subset, so the earlier results are still
// the truth for everything else and must not be thrown away.
const previous = retryPaths ? result : null;
setResult(null);
try {
const batchResult = await invoke<ProfileImportBatchResult>(
"import_browser_profiles",
{
items,
groupId: selectedGroupId === "none" ? null : selectedGroupId,
duplicateStrategy: duplicateStrategy,
wayfernConfig,
},
);
setResult(
previous ? mergeImportResults(previous, batchResult) : batchResult,
);
toast.success(
t("importProfile.resultsSummary", {
imported: batchResult.imported_count,
skipped: batchResult.skipped_count,
failed: batchResult.failed_count,
}),
);
if (batchResult.imported_count > 0 && !reducedMotion) {
fireSprinkleConfetti();
}
} catch (error) {
console.error("Failed to import profiles:", error);
toast.error(translateBackendError(t, error));
setCurrentStep("configure");
} finally {
setIsImporting(false);
}
},
[
selectedProfiles,
profileNames,
proxyIdForIndex,
vpnAssignment,
selectedGroupId,
duplicateStrategy,
wayfernConfig,
reducedMotion,
result,
t,
],
);
// A source browser that is still running is the one failure the user can fix
// without starting over, so offer the override right where it happened.
const hasRunningBrowserFailure = useMemo(
() =>
(result?.results ?? []).some(
(item) =>
item.status === "failed" &&
item.error?.includes("IMPORT_SOURCE_BROWSER_RUNNING"),
),
[result],
);
const handleClose = () => {
void cleanupExtractedDir(extractedDir);
@@ -840,38 +948,74 @@ export function ImportProfileDialog({
</h3>
<div className="max-h-64 space-y-1 overflow-y-auto rounded-lg border border-border p-2">
{result.results.map((item) => (
<div
key={item.source_path}
className="flex items-center gap-2 p-1 text-sm"
>
<span
className={cn(
"shrink-0 text-xs font-medium",
item.status === "imported" && "text-success-text",
item.status === "skipped" &&
"text-muted-foreground",
item.status === "failed" &&
"text-destructive-text",
)}
>
{item.status === "imported" &&
t("importProfile.statusImported")}
{item.status === "skipped" &&
t("importProfile.statusSkipped")}
{item.status === "failed" &&
t("importProfile.statusFailed")}
</span>
<span className="min-w-0 flex-1 truncate">
{item.name || item.source_path}
</span>
{item.error && (
<span className="min-w-0 flex-1 truncate text-xs text-destructive-text">
{translateBackendError(t, new Error(item.error))}
<div key={item.source_path} className="p-1 text-sm">
<div className="flex items-center gap-2">
<span
className={cn(
"shrink-0 text-xs font-medium",
item.status === "imported" &&
"text-success-text",
item.status === "skipped" &&
"text-muted-foreground",
item.status === "failed" &&
"text-destructive-text",
)}
>
{item.status === "imported" &&
t("importProfile.statusImported")}
{item.status === "skipped" &&
t("importProfile.statusSkipped")}
{item.status === "failed" &&
t("importProfile.statusFailed")}
</span>
<span className="min-w-0 flex-1 truncate">
{item.name || item.source_path}
</span>
{item.error && (
<span className="min-w-0 flex-1 truncate text-xs text-destructive-text">
{translateBackendError(
t,
new Error(item.error),
)}
</span>
)}
</div>
{item.report && (
<ImportReportSummary report={item.report} />
)}
</div>
))}
</div>
{hasRunningBrowserFailure && (
<Alert>
<AlertDescription className="space-y-2">
<p>{t("importProfile.closeSourceBrowserHint")}</p>
<Button
variant="outline"
size="sm"
onClick={() => {
void handleImport(
true,
new Set(
result.results
.filter(
(item) =>
item.status === "failed" &&
item.error?.includes(
"IMPORT_SOURCE_BROWSER_RUNNING",
),
)
.map((item) => item.source_path),
),
);
}}
>
{t("importProfile.importAnyway")}
</Button>
</AlertDescription>
</Alert>
)}
</div>
)}
</div>
+16
View File
@@ -1855,10 +1855,18 @@
"name": "ntapi",
"license": "Apache-2.0 OR MIT"
},
{
"name": "num",
"license": "Apache-2.0 OR MIT"
},
{
"name": "num-bigint",
"license": "Apache-2.0 OR MIT"
},
{
"name": "num-complex",
"license": "Apache-2.0 OR MIT"
},
{
"name": "num-conv",
"license": "Apache-2.0 OR MIT"
@@ -1871,6 +1879,10 @@
"name": "num-integer",
"license": "Apache-2.0 OR MIT"
},
{
"name": "num-iter",
"license": "Apache-2.0 OR MIT"
},
{
"name": "num-rational",
"license": "Apache-2.0 OR MIT"
@@ -2407,6 +2419,10 @@
"name": "sealed",
"license": "Apache-2.0 OR MIT"
},
{
"name": "secret-service",
"license": "Apache-2.0 OR MIT"
},
{
"name": "security-framework",
"license": "Apache-2.0 OR MIT"
+26 -2
View File
@@ -1467,7 +1467,28 @@
"vpnOptional": "VPN (Optional)",
"noVpn": "No VPN",
"advancedOptions": "Advanced options",
"configureFingerprint": "Configure fingerprint (optional)"
"configureFingerprint": "Configure fingerprint (optional)",
"reportCookies": "Cookies",
"reportPasswords": "Passwords",
"reportAutofill": "Payment methods",
"reportExtensions": "Extensions",
"reportHistory": "History",
"reportBookmarks": "Bookmarks",
"reportLocalStorage": "Site data",
"reportNothingCarried": "No readable data was carried over",
"reportUnrecoverable": "Could not be decrypted: {{count}}",
"closeSourceBrowserHint": "Close the source browser and try again for a complete copy, or import now and accept that site data may be incomplete.",
"importAnyway": "Import anyway",
"warnings": {
"secretsNotMigrated": "Cookies and passwords could not be unlocked, so you will need to sign in again.",
"appBoundEncrypted": "Chrome 127+ on Windows locks cookies to the browser itself; those cookies cannot be migrated by any other app.",
"storeTooOld": "A database was too old for this browser to open and was skipped.",
"storeTooNew": "A database came from a newer browser than this one and was skipped.",
"sourceBrowserRunning": "The source browser was running, so site data may be incomplete.",
"securePreferencesReset": "Protected settings such as the homepage and search engine reset to defaults.",
"extensionsPartial": "Some extensions belonged to the source browser and were not carried over.",
"storeUnreadable": "A database could not be read and was skipped rather than copied damaged."
}
},
"syncTooltips": {
"syncing": "Syncing...",
@@ -1914,7 +1935,10 @@
"malformed": "The VLESS URI is invalid."
},
"camoufoxRemoved": "Camoufox is no longer supported. Recreate this profile with Wayfern.",
"noE2ePasswordSet": "No end-to-end encryption password is set. Set one before syncing encrypted data."
"noE2ePasswordSet": "No end-to-end encryption password is set. Set one before syncing encrypted data.",
"importSourceNotChromium": "This folder is not a Chromium browser profile",
"importSourceNotChromiumNamed": "{{family}} profiles cannot be imported; only Chromium-based browsers are supported",
"importSourceBrowserRunning": "Close {{browser}} first, or choose to import anyway"
},
"rail": {
"profiles": "Profiles",
+26 -2
View File
@@ -1471,7 +1471,28 @@
"vpnOptional": "VPN (opcional)",
"noVpn": "Sin VPN",
"advancedOptions": "Opciones avanzadas",
"configureFingerprint": "Configurar huella digital (opcional)"
"configureFingerprint": "Configurar huella digital (opcional)",
"reportCookies": "Cookies",
"reportPasswords": "Contraseñas",
"reportAutofill": "Métodos de pago",
"reportExtensions": "Extensiones",
"reportHistory": "Historial",
"reportBookmarks": "Marcadores",
"reportLocalStorage": "Datos de sitios",
"reportNothingCarried": "No se transfirió ningún dato legible",
"reportUnrecoverable": "No se pudo descifrar: {{count}}",
"closeSourceBrowserHint": "Cierra el navegador de origen y vuelve a intentarlo para obtener una copia completa, o importa ahora aceptando que los datos de sitios pueden quedar incompletos.",
"importAnyway": "Importar de todos modos",
"warnings": {
"secretsNotMigrated": "No se pudieron desbloquear las cookies ni las contraseñas, así que tendrás que iniciar sesión de nuevo.",
"appBoundEncrypted": "Chrome 127+ en Windows vincula las cookies al propio navegador; ninguna otra aplicación puede migrarlas.",
"storeTooOld": "Una base de datos era demasiado antigua para este navegador y se omitió.",
"storeTooNew": "Una base de datos procede de un navegador más reciente que este y se omitió.",
"sourceBrowserRunning": "El navegador de origen estaba en ejecución, por lo que los datos de sitios pueden estar incompletos.",
"securePreferencesReset": "Los ajustes protegidos, como la página de inicio y el buscador, volvieron a sus valores predeterminados.",
"extensionsPartial": "Algunas extensiones pertenecían al navegador de origen y no se transfirieron.",
"storeUnreadable": "No se pudo leer una base de datos y se omitió en lugar de copiarla dañada."
}
},
"syncTooltips": {
"syncing": "Sincronizando...",
@@ -1921,7 +1942,10 @@
"malformed": "La URI VLESS no es válida."
},
"camoufoxRemoved": "Camoufox ya no es compatible. Vuelve a crear este perfil con Wayfern.",
"noE2ePasswordSet": "No hay contraseña de cifrado de extremo a extremo. Establece una antes de sincronizar datos cifrados."
"noE2ePasswordSet": "No hay contraseña de cifrado de extremo a extremo. Establece una antes de sincronizar datos cifrados.",
"importSourceNotChromium": "Esta carpeta no es un perfil de navegador Chromium",
"importSourceNotChromiumNamed": "Los perfiles de {{family}} no se pueden importar; solo se admiten navegadores basados en Chromium",
"importSourceBrowserRunning": "Cierra {{browser}} primero o elige importar de todos modos"
},
"rail": {
"profiles": "Perfiles",
+26 -2
View File
@@ -1471,7 +1471,28 @@
"vpnOptional": "VPN (facultatif)",
"noVpn": "Sans VPN",
"advancedOptions": "Options avancées",
"configureFingerprint": "Configurer l'empreinte (facultatif)"
"configureFingerprint": "Configurer l'empreinte (facultatif)",
"reportCookies": "Cookies",
"reportPasswords": "Mots de passe",
"reportAutofill": "Moyens de paiement",
"reportExtensions": "Extensions",
"reportHistory": "Historique",
"reportBookmarks": "Favoris",
"reportLocalStorage": "Données de sites",
"reportNothingCarried": "Aucune donnée lisible n'a été transférée",
"reportUnrecoverable": "Déchiffrement impossible : {{count}}",
"closeSourceBrowserHint": "Fermez le navigateur source et réessayez pour obtenir une copie complète, ou importez maintenant en acceptant que les données de sites soient incomplètes.",
"importAnyway": "Importer quand même",
"warnings": {
"secretsNotMigrated": "Les cookies et les mots de passe n'ont pas pu être déverrouillés : vous devrez vous reconnecter.",
"appBoundEncrypted": "Chrome 127+ sous Windows lie les cookies au navigateur lui-même ; aucune autre application ne peut les migrer.",
"storeTooOld": "Une base de données était trop ancienne pour ce navigateur et a été ignorée.",
"storeTooNew": "Une base de données provient d'un navigateur plus récent que celui-ci et a été ignorée.",
"sourceBrowserRunning": "Le navigateur source était en cours d'exécution, les données de sites peuvent donc être incomplètes.",
"securePreferencesReset": "Les réglages protégés, comme la page d'accueil et le moteur de recherche, sont revenus aux valeurs par défaut.",
"extensionsPartial": "Certaines extensions appartenaient au navigateur source et n'ont pas été transférées.",
"storeUnreadable": "Une base de données n'a pas pu être lue et a été ignorée plutôt que copiée endommagée."
}
},
"syncTooltips": {
"syncing": "Synchronisation...",
@@ -1921,7 +1942,10 @@
"malformed": "L'URI VLESS n'est pas valide."
},
"camoufoxRemoved": "Camoufox n'est plus pris en charge. Recréez ce profil avec Wayfern.",
"noE2ePasswordSet": "Aucun mot de passe de chiffrement de bout en bout n'est défini. Définissez-en un avant de synchroniser des données chiffrées."
"noE2ePasswordSet": "Aucun mot de passe de chiffrement de bout en bout n'est défini. Définissez-en un avant de synchroniser des données chiffrées.",
"importSourceNotChromium": "Ce dossier n'est pas un profil de navigateur Chromium",
"importSourceNotChromiumNamed": "Les profils {{family}} ne peuvent pas être importés ; seuls les navigateurs basés sur Chromium sont pris en charge",
"importSourceBrowserRunning": "Fermez d'abord {{browser}}, ou choisissez d'importer quand même"
},
"rail": {
"profiles": "Profils",
+26 -2
View File
@@ -1467,7 +1467,28 @@
"vpnOptional": "VPN(任意)",
"noVpn": "VPNなし",
"advancedOptions": "詳細オプション",
"configureFingerprint": "フィンガープリントを設定(任意)"
"configureFingerprint": "フィンガープリントを設定(任意)",
"reportCookies": "Cookie",
"reportPasswords": "パスワード",
"reportAutofill": "お支払い方法",
"reportExtensions": "拡張機能",
"reportHistory": "履歴",
"reportBookmarks": "ブックマーク",
"reportLocalStorage": "サイトデータ",
"reportNothingCarried": "読み取り可能なデータは引き継がれませんでした",
"reportUnrecoverable": "復号できませんでした: {{count}}",
"closeSourceBrowserHint": "完全にコピーするには、元のブラウザーを閉じてからもう一度お試しください。サイトデータが不完全になることを承知のうえで、このままインポートすることもできます。",
"importAnyway": "このままインポート",
"warnings": {
"secretsNotMigrated": "Cookie とパスワードのロックを解除できなかったため、再度サインインが必要です。",
"appBoundEncrypted": "Windows の Chrome 127 以降は Cookie をブラウザー自体に紐付けるため、他のアプリからは移行できません。",
"storeTooOld": "このブラウザーでは開けない古いデータベースがあったため、スキップしました。",
"storeTooNew": "このブラウザーより新しいブラウザーのデータベースだったため、スキップしました。",
"sourceBrowserRunning": "元のブラウザーが実行中だったため、サイトデータが不完全な可能性があります。",
"securePreferencesReset": "ホームページや検索エンジンなど、保護された設定は既定値に戻りました。",
"extensionsPartial": "一部の拡張機能は元のブラウザー付属のもので、引き継がれませんでした。",
"storeUnreadable": "読み取れないデータベースがあったため、破損したままコピーせずスキップしました。"
}
},
"syncTooltips": {
"syncing": "同期中...",
@@ -1914,7 +1935,10 @@
"malformed": "VLESS URIが無効です。"
},
"camoufoxRemoved": "Camoufoxはサポートされなくなりました。Wayfernでこのプロファイルを作り直してください。",
"noE2ePasswordSet": "エンドツーエンド暗号化のパスワードが設定されていません。暗号化データを同期する前に設定してください。"
"noE2ePasswordSet": "エンドツーエンド暗号化のパスワードが設定されていません。暗号化データを同期する前に設定してください。",
"importSourceNotChromium": "このフォルダーは Chromium ブラウザーのプロファイルではありません",
"importSourceNotChromiumNamed": "{{family}} のプロファイルはインポートできません。Chromium 系ブラウザーのみ対応しています",
"importSourceBrowserRunning": "先に {{browser}} を閉じるか、このままインポートを選択してください"
},
"rail": {
"profiles": "プロファイル",
+26 -2
View File
@@ -1467,7 +1467,28 @@
"vpnOptional": "VPN (선택 사항)",
"noVpn": "VPN 없음",
"advancedOptions": "고급 옵션",
"configureFingerprint": "핑거프린트 구성 (선택 사항)"
"configureFingerprint": "핑거프린트 구성 (선택 사항)",
"reportCookies": "쿠키",
"reportPasswords": "비밀번호",
"reportAutofill": "결제 수단",
"reportExtensions": "확장 프로그램",
"reportHistory": "방문 기록",
"reportBookmarks": "북마크",
"reportLocalStorage": "사이트 데이터",
"reportNothingCarried": "읽을 수 있는 데이터가 이전되지 않았습니다",
"reportUnrecoverable": "복호화할 수 없음: {{count}}",
"closeSourceBrowserHint": "완전하게 복사하려면 원본 브라우저를 닫고 다시 시도하세요. 사이트 데이터가 불완전할 수 있음을 감수하고 지금 가져올 수도 있습니다.",
"importAnyway": "그래도 가져오기",
"warnings": {
"secretsNotMigrated": "쿠키와 비밀번호를 잠금 해제하지 못해 다시 로그인해야 합니다.",
"appBoundEncrypted": "Windows의 Chrome 127 이상은 쿠키를 브라우저 자체에 묶어 두므로 다른 앱에서는 이전할 수 없습니다.",
"storeTooOld": "이 브라우저가 열 수 없을 만큼 오래된 데이터베이스가 있어 건너뛰었습니다.",
"storeTooNew": "이 브라우저보다 최신 브라우저의 데이터베이스여서 건너뛰었습니다.",
"sourceBrowserRunning": "원본 브라우저가 실행 중이어서 사이트 데이터가 불완전할 수 있습니다.",
"securePreferencesReset": "홈페이지와 검색 엔진 같은 보호된 설정이 기본값으로 초기화되었습니다.",
"extensionsPartial": "일부 확장 프로그램은 원본 브라우저의 것이어서 이전되지 않았습니다.",
"storeUnreadable": "읽을 수 없는 데이터베이스가 있어 손상된 채로 복사하지 않고 건너뛰었습니다."
}
},
"syncTooltips": {
"syncing": "동기화 중...",
@@ -1914,7 +1935,10 @@
"malformed": "VLESS URI가 올바르지 않습니다."
},
"camoufoxRemoved": "Camoufox는 더 이상 지원되지 않습니다. Wayfern으로 이 프로필을 다시 만드세요.",
"noE2ePasswordSet": "종단 간 암호화 비밀번호가 설정되지 않았습니다. 암호화된 데이터를 동기화하기 전에 설정하세요."
"noE2ePasswordSet": "종단 간 암호화 비밀번호가 설정되지 않았습니다. 암호화된 데이터를 동기화하기 전에 설정하세요.",
"importSourceNotChromium": "이 폴더는 Chromium 브라우저 프로필이 아닙니다",
"importSourceNotChromiumNamed": "{{family}} 프로필은 가져올 수 없습니다. Chromium 기반 브라우저만 지원합니다",
"importSourceBrowserRunning": "{{browser}}을(를) 먼저 닫거나 그래도 가져오기를 선택하세요"
},
"rail": {
"profiles": "프로필",
+26 -2
View File
@@ -1471,7 +1471,28 @@
"vpnOptional": "VPN (opcional)",
"noVpn": "Sem VPN",
"advancedOptions": "Opções avançadas",
"configureFingerprint": "Configurar impressão digital (opcional)"
"configureFingerprint": "Configurar impressão digital (opcional)",
"reportCookies": "Cookies",
"reportPasswords": "Senhas",
"reportAutofill": "Formas de pagamento",
"reportExtensions": "Extensões",
"reportHistory": "Histórico",
"reportBookmarks": "Favoritos",
"reportLocalStorage": "Dados de sites",
"reportNothingCarried": "Nenhum dado legível foi transferido",
"reportUnrecoverable": "Não foi possível descriptografar: {{count}}",
"closeSourceBrowserHint": "Feche o navegador de origem e tente de novo para obter uma cópia completa, ou importe agora aceitando que os dados de sites podem ficar incompletos.",
"importAnyway": "Importar mesmo assim",
"warnings": {
"secretsNotMigrated": "Não foi possível desbloquear cookies e senhas, então você precisará entrar novamente.",
"appBoundEncrypted": "O Chrome 127+ no Windows vincula os cookies ao próprio navegador; nenhum outro aplicativo consegue migrá-los.",
"storeTooOld": "Um banco de dados era antigo demais para este navegador e foi ignorado.",
"storeTooNew": "Um banco de dados veio de um navegador mais novo que este e foi ignorado.",
"sourceBrowserRunning": "O navegador de origem estava aberto, então os dados de sites podem estar incompletos.",
"securePreferencesReset": "Configurações protegidas, como página inicial e mecanismo de busca, voltaram ao padrão.",
"extensionsPartial": "Algumas extensões pertenciam ao navegador de origem e não foram transferidas.",
"storeUnreadable": "Não foi possível ler um banco de dados, que foi ignorado em vez de copiado danificado."
}
},
"syncTooltips": {
"syncing": "Sincronizando...",
@@ -1921,7 +1942,10 @@
"malformed": "A URI VLESS é inválida."
},
"camoufoxRemoved": "O Camoufox não é mais compatível. Recrie este perfil com o Wayfern.",
"noE2ePasswordSet": "Nenhuma senha de criptografia de ponta a ponta foi definida. Defina uma antes de sincronizar dados criptografados."
"noE2ePasswordSet": "Nenhuma senha de criptografia de ponta a ponta foi definida. Defina uma antes de sincronizar dados criptografados.",
"importSourceNotChromium": "Esta pasta não é um perfil de navegador Chromium",
"importSourceNotChromiumNamed": "Perfis do {{family}} não podem ser importados; apenas navegadores baseados em Chromium são compatíveis",
"importSourceBrowserRunning": "Feche o {{browser}} primeiro ou escolha importar mesmo assim"
},
"rail": {
"profiles": "Perfis",
+26 -2
View File
@@ -1475,7 +1475,28 @@
"vpnOptional": "VPN (необязательно)",
"noVpn": "Без VPN",
"advancedOptions": "Дополнительные параметры",
"configureFingerprint": "Настроить отпечаток (необязательно)"
"configureFingerprint": "Настроить отпечаток (необязательно)",
"reportCookies": "Файлы cookie",
"reportPasswords": "Пароли",
"reportAutofill": "Способы оплаты",
"reportExtensions": "Расширения",
"reportHistory": "История",
"reportBookmarks": "Закладки",
"reportLocalStorage": "Данные сайтов",
"reportNothingCarried": "Читаемые данные не перенесены",
"reportUnrecoverable": "Не удалось расшифровать: {{count}}",
"closeSourceBrowserHint": "Закройте исходный браузер и повторите попытку, чтобы получить полную копию, либо импортируйте сейчас, приняв, что данные сайтов могут оказаться неполными.",
"importAnyway": "Всё равно импортировать",
"warnings": {
"secretsNotMigrated": "Не удалось разблокировать файлы cookie и пароли, поэтому потребуется войти заново.",
"appBoundEncrypted": "Chrome 127+ в Windows привязывает файлы cookie к самому браузеру, и другое приложение не может их перенести.",
"storeTooOld": "База данных оказалась слишком старой для этого браузера и была пропущена.",
"storeTooNew": "База данных создана более новым браузером и была пропущена.",
"sourceBrowserRunning": "Исходный браузер был запущен, поэтому данные сайтов могут быть неполными.",
"securePreferencesReset": "Защищённые настройки, например домашняя страница и поисковая система, сброшены до значений по умолчанию.",
"extensionsPartial": "Некоторые расширения принадлежали исходному браузеру и не были перенесены.",
"storeUnreadable": "База данных не читалась и была пропущена, а не скопирована повреждённой."
}
},
"syncTooltips": {
"syncing": "Синхронизация...",
@@ -1928,7 +1949,10 @@
"malformed": "VLESS URI недействителен."
},
"camoufoxRemoved": "Camoufox больше не поддерживается. Создайте этот профиль заново с Wayfern.",
"noE2ePasswordSet": "Пароль сквозного шифрования не задан. Задайте его перед синхронизацией зашифрованных данных."
"noE2ePasswordSet": "Пароль сквозного шифрования не задан. Задайте его перед синхронизацией зашифрованных данных.",
"importSourceNotChromium": "Эта папка не является профилем браузера на Chromium",
"importSourceNotChromiumNamed": "Профили {{family}} импортировать нельзя: поддерживаются только браузеры на Chromium",
"importSourceBrowserRunning": "Сначала закройте {{browser}} или выберите импорт всё равно"
},
"rail": {
"profiles": "Профили",
+26 -2
View File
@@ -1467,7 +1467,28 @@
"vpnOptional": "VPN (isteğe bağlı)",
"noVpn": "VPN yok",
"advancedOptions": "Gelişmiş seçenekler",
"configureFingerprint": "Parmak izini yapılandır (isteğe bağlı)"
"configureFingerprint": "Parmak izini yapılandır (isteğe bağlı)",
"reportCookies": "Çerezler",
"reportPasswords": "Parolalar",
"reportAutofill": "Ödeme yöntemleri",
"reportExtensions": "Uzantılar",
"reportHistory": "Geçmiş",
"reportBookmarks": "Yer imleri",
"reportLocalStorage": "Site verileri",
"reportNothingCarried": "Okunabilir hiçbir veri aktarılmadı",
"reportUnrecoverable": "Şifresi çözülemedi: {{count}}",
"closeSourceBrowserHint": "Tam bir kopya için kaynak tarayıcıyı kapatıp yeniden deneyin ya da site verilerinin eksik olabileceğini kabul ederek şimdi içe aktarın.",
"importAnyway": "Yine de içe aktar",
"warnings": {
"secretsNotMigrated": "Çerezlerin ve parolaların kilidi açılamadı, bu yüzden yeniden oturum açmanız gerekecek.",
"appBoundEncrypted": "Windows'ta Chrome 127+ çerezleri tarayıcının kendisine bağlar; başka hiçbir uygulama bunları taşıyamaz.",
"storeTooOld": "Bir veritabanı bu tarayıcının açamayacağı kadar eskiydi ve atlandı.",
"storeTooNew": "Bir veritabanı bundan daha yeni bir tarayıcıdan geldi ve atlandı.",
"sourceBrowserRunning": "Kaynak tarayıcı çalışıyordu, bu yüzden site verileri eksik olabilir.",
"securePreferencesReset": "Ana sayfa ve arama motoru gibi korumalı ayarlar varsayılana döndü.",
"extensionsPartial": "Bazı uzantılar kaynak tarayıcıya aitti ve aktarılmadı.",
"storeUnreadable": "Bir veritabanı okunamadı ve bozuk şekilde kopyalanmak yerine atlandı."
}
},
"syncTooltips": {
"syncing": "Eşitleniyor...",
@@ -1914,7 +1935,10 @@
"malformed": "VLESS URI'si geçersiz."
},
"camoufoxRemoved": "Camoufox artık desteklenmiyor. Bu profili Wayfern ile yeniden oluşturun.",
"noE2ePasswordSet": "Uçtan uca şifreleme parolası ayarlanmamış. Şifreli veriyi eşitlemeden önce bir parola belirleyin."
"noE2ePasswordSet": "Uçtan uca şifreleme parolası ayarlanmamış. Şifreli veriyi eşitlemeden önce bir parola belirleyin.",
"importSourceNotChromium": "Bu klasör bir Chromium tarayıcı profili değil",
"importSourceNotChromiumNamed": "{{family}} profilleri içe aktarılamaz; yalnızca Chromium tabanlı tarayıcılar desteklenir",
"importSourceBrowserRunning": "Önce {{browser}} uygulamasını kapatın veya yine de içe aktarmayı seçin"
},
"rail": {
"profiles": "Profiller",
+26 -2
View File
@@ -1467,7 +1467,28 @@
"vpnOptional": "VPN (tùy chọn)",
"noVpn": "Không dùng VPN",
"advancedOptions": "Tùy chọn nâng cao",
"configureFingerprint": "Cấu hình vân tay (tùy chọn)"
"configureFingerprint": "Cấu hình vân tay (tùy chọn)",
"reportCookies": "Cookie",
"reportPasswords": "Mật khẩu",
"reportAutofill": "Phương thức thanh toán",
"reportExtensions": "Tiện ích mở rộng",
"reportHistory": "Lịch sử",
"reportBookmarks": "Dấu trang",
"reportLocalStorage": "Dữ liệu trang web",
"reportNothingCarried": "Không có dữ liệu đọc được nào được chuyển sang",
"reportUnrecoverable": "Không giải mã được: {{count}}",
"closeSourceBrowserHint": "Hãy đóng trình duyệt nguồn rồi thử lại để có bản sao đầy đủ, hoặc nhập ngay và chấp nhận rằng dữ liệu trang web có thể chưa đầy đủ.",
"importAnyway": "Vẫn nhập",
"warnings": {
"secretsNotMigrated": "Không mở khóa được cookie và mật khẩu, nên bạn sẽ phải đăng nhập lại.",
"appBoundEncrypted": "Chrome 127 trở lên trên Windows gắn cookie với chính trình duyệt; không ứng dụng nào khác có thể chuyển được.",
"storeTooOld": "Một cơ sở dữ liệu quá cũ để trình duyệt này mở nên đã bị bỏ qua.",
"storeTooNew": "Một cơ sở dữ liệu đến từ trình duyệt mới hơn nên đã bị bỏ qua.",
"sourceBrowserRunning": "Trình duyệt nguồn đang chạy nên dữ liệu trang web có thể chưa đầy đủ.",
"securePreferencesReset": "Các cài đặt được bảo vệ như trang chủ và công cụ tìm kiếm đã trở về mặc định.",
"extensionsPartial": "Một số tiện ích thuộc về trình duyệt nguồn nên không được chuyển sang.",
"storeUnreadable": "Một cơ sở dữ liệu không đọc được nên đã bị bỏ qua thay vì sao chép hỏng."
}
},
"syncTooltips": {
"syncing": "Đang đồng bộ...",
@@ -1914,7 +1935,10 @@
"malformed": "URI VLESS không hợp lệ."
},
"camoufoxRemoved": "Camoufox không còn được hỗ trợ. Hãy tạo lại hồ sơ này bằng Wayfern.",
"noE2ePasswordSet": "Chưa đặt mật khẩu mã hóa đầu cuối. Hãy đặt trước khi đồng bộ dữ liệu đã mã hóa."
"noE2ePasswordSet": "Chưa đặt mật khẩu mã hóa đầu cuối. Hãy đặt trước khi đồng bộ dữ liệu đã mã hóa.",
"importSourceNotChromium": "Thư mục này không phải hồ sơ trình duyệt Chromium",
"importSourceNotChromiumNamed": "Không thể nhập hồ sơ {{family}}; chỉ hỗ trợ các trình duyệt nền Chromium",
"importSourceBrowserRunning": "Hãy đóng {{browser}} trước, hoặc chọn vẫn nhập"
},
"rail": {
"profiles": "Profile",
+26 -2
View File
@@ -1467,7 +1467,28 @@
"vpnOptional": "VPN(可选)",
"noVpn": "不使用 VPN",
"advancedOptions": "高级选项",
"configureFingerprint": "配置指纹(可选)"
"configureFingerprint": "配置指纹(可选)",
"reportCookies": "Cookie",
"reportPasswords": "密码",
"reportAutofill": "付款方式",
"reportExtensions": "扩展程序",
"reportHistory": "历史记录",
"reportBookmarks": "书签",
"reportLocalStorage": "网站数据",
"reportNothingCarried": "没有可读取的数据被迁移",
"reportUnrecoverable": "无法解密:{{count}}",
"closeSourceBrowserHint": "关闭源浏览器后重试可获得完整副本;也可以现在导入,但网站数据可能不完整。",
"importAnyway": "仍要导入",
"warnings": {
"secretsNotMigrated": "无法解锁 Cookie 和密码,你需要重新登录。",
"appBoundEncrypted": "Windows 上的 Chrome 127+ 会把 Cookie 绑定到浏览器本身,其他任何应用都无法迁移。",
"storeTooOld": "某个数据库过旧,此浏览器无法打开,已跳过。",
"storeTooNew": "某个数据库来自更新版本的浏览器,已跳过。",
"sourceBrowserRunning": "源浏览器正在运行,网站数据可能不完整。",
"securePreferencesReset": "主页、搜索引擎等受保护的设置已恢复为默认值。",
"extensionsPartial": "部分扩展属于源浏览器,未被迁移。",
"storeUnreadable": "某个数据库无法读取,已跳过而不是复制损坏的副本。"
}
},
"syncTooltips": {
"syncing": "同步中...",
@@ -1914,7 +1935,10 @@
"malformed": "VLESS URI 无效。"
},
"camoufoxRemoved": "Camoufox 已不再受支持。请使用 Wayfern 重新创建此配置文件。",
"noE2ePasswordSet": "尚未设置端到端加密密码。请先设置后再同步加密数据。"
"noE2ePasswordSet": "尚未设置端到端加密密码。请先设置后再同步加密数据。",
"importSourceNotChromium": "该文件夹不是 Chromium 浏览器配置文件",
"importSourceNotChromiumNamed": "无法导入 {{family}} 配置文件;仅支持基于 Chromium 的浏览器",
"importSourceBrowserRunning": "请先关闭 {{browser}},或选择仍要导入"
},
"rail": {
"profiles": "配置文件",
+12
View File
@@ -43,6 +43,8 @@ export type BackendErrorCode =
| "UPDATE_PREPARATION_FAILED"
| "PROFILE_NAME_EXISTS"
| "IMPORT_SOURCE_NOT_FOUND"
| "IMPORT_SOURCE_NOT_CHROMIUM"
| "IMPORT_SOURCE_BROWSER_RUNNING"
| "IMPORT_NO_ITEMS"
| "BROWSER_NOT_DOWNLOADED"
| "ARCHIVE_EXTRACTION_FAILED"
@@ -253,6 +255,16 @@ export function translateBackendError(t: TFunction, err: unknown): string {
});
case "IMPORT_SOURCE_NOT_FOUND":
return t("backendErrors.importSourceNotFound");
case "IMPORT_SOURCE_NOT_CHROMIUM":
return parsed.params?.family
? t("backendErrors.importSourceNotChromiumNamed", {
family: parsed.params.family,
})
: t("backendErrors.importSourceNotChromium");
case "IMPORT_SOURCE_BROWSER_RUNNING":
return t("backendErrors.importSourceBrowserRunning", {
browser: parsed.params?.browser ?? "",
});
case "IMPORT_NO_ITEMS":
return t("backendErrors.importNoItems");
case "BROWSER_NOT_DOWNLOADED":
+35
View File
@@ -275,11 +275,44 @@ export interface DetectedProfile {
export interface ImportProfileItem {
source_path: string;
/**
* Source browser family. Selects which OS keychain entry holds the key that
* unlocks the source's cookies and passwords, so it decides whether secrets
* survive the import.
*/
browser_type?: string;
new_profile_name: string;
/** Mutually exclusive with `vpn_id`; the importer rejects setting both. */
proxy_id?: string | null;
vpn_id?: string | null;
/** Import even though the source browser is still running. */
allow_running?: boolean;
}
/** Stable warning codes; each maps to `importProfile.warnings.*`. */
export type ProfileImportWarning =
| "secretsNotMigrated"
| "appBoundEncrypted"
| "storeTooOld"
| "storeTooNew"
| "sourceBrowserRunning"
| "securePreferencesReset"
| "extensionsPartial"
| "storeUnreadable";
export interface ProfileImportReport {
cookies_migrated: number;
cookies_unrecoverable: number;
passwords_migrated: number;
passwords_unrecoverable: number;
payment_methods_migrated: number;
payment_methods_unrecoverable: number;
extensions_migrated: number;
history_entries: number;
bookmarks: number;
local_storage_origins: number;
bytes_copied: number;
warnings: ProfileImportWarning[];
}
export interface ProfileImportItemResult {
@@ -288,6 +321,8 @@ export interface ProfileImportItemResult {
status: "imported" | "skipped" | "failed";
profile_id: string | null;
error: string | null;
/** What actually came across. Present when status is "imported". */
report?: ProfileImportReport | null;
}
export interface ProfileImportBatchResult {