feat(i18n): add localized desktop UI

Refs #17
This commit is contained in:
NimBold
2026-07-18 13:44:29 +03:30
parent e4d7d5ecf0
commit 93ddf427b4
27 changed files with 5014 additions and 835 deletions
+1
View File
@@ -31,6 +31,7 @@
"dev": "vite",
"bindings": "cd src-tauri && cargo test export_bindings --lib",
"build": "tsc && vite build",
"check:i18n": "vitest run src/i18n/resources.test.ts",
"check:updates": "node scripts/check-updates.js",
"verify:macos-signing": "node scripts/verify-macos-signing.js",
"preview": "vite preview",
+6
View File
@@ -6,6 +6,10 @@ fn default_speed_limit_unit() -> String {
"MB/s".to_string()
}
fn default_language_preference() -> String {
"system".to_string()
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize, TS)]
#[serde(rename_all = "lowercase")]
#[ts(export, export_to = "../../src/bindings/")]
@@ -257,6 +261,8 @@ pub struct SchedulerSettings {
#[ts(export, export_to = "../../src/bindings/")]
pub struct PersistedSettings {
pub theme: Theme,
#[serde(default = "default_language_preference")]
pub language: String,
pub base_download_folder: String,
pub category_subfolders_enabled: bool,
pub category_subfolders: HashMap<String, String>,
+6
View File
@@ -192,6 +192,11 @@ fn sanitize_persisted_setting_values(state: &mut Value) {
"theme",
&["system", "light", "dark", "dracula", "nord"],
);
sanitize_allowed_string(
state,
"language",
&["system", "en", "zh-CN", "he", "fa", "uk", "ru"],
);
sanitize_allowed_string(state, "appFontSize", &["small", "standard", "large"]);
sanitize_allowed_string(state, "listRowDensity", &["compact", "standard", "relaxed"]);
sanitize_allowed_string(state, "activeSettingsTab", &[
@@ -410,6 +415,7 @@ fn derived_location_path(base: &str, subfolder: &str) -> String {
fn default_settings() -> PersistedSettings {
PersistedSettings {
theme: Theme::System,
language: "system".to_string(),
base_download_folder: "~/Downloads".to_string(),
category_subfolders_enabled: true,
category_subfolders: default_category_subfolders(),
+46 -20
View File
@@ -1,12 +1,8 @@
import { initMediaDomains, isActiveDownloadStatus, isTransferActiveStatus } from './utils/downloads';
import { schedulerCompletionState } from './utils/schedulerCompletion';
import { useCallback, useEffect, useRef, useState } from "react";
import { lazy, Suspense, useCallback, useEffect, useRef, useState } from "react";
import { Sidebar, SidebarFilter } from "./components/Sidebar";
import { DownloadTable } from "./components/DownloadTable";
import { AddDownloadsModal } from "./components/AddDownloadsModal";
import SettingsView from "./components/SettingsView";
import { PropertiesModal } from "./components/PropertiesModal";
import { DeleteConfirmationModal } from "./components/DeleteConfirmationModal";
import { extractValidDownloadUrls } from './utils/url';
import { readClipboardDownloadUrls } from './utils/clipboard';
import { listenEvent as listen, invokeCommand as invoke } from "./ipc";
@@ -14,10 +10,6 @@ import { useDownloadStore, MAIN_QUEUE_ID, type ExtensionDownloadRequest } from '
import { initDownloadListener } from './store/downloadStore';
import { subscribeToSettingsPersistenceErrors, useSettingsStore } from "./store/useSettingsStore";
import { isPermissionGranted, requestPermission, sendNotification } from '@tauri-apps/plugin-notification';
import SchedulerView from "./components/SchedulerView";
import SpeedLimiterView from "./components/SpeedLimiterView";
import LogsView from "./components/LogsView";
import { KeychainPermissionModal } from "./components/KeychainPermissionModal";
import { WindowControls } from "./components/WindowControls";
import { useToast } from "./contexts/ToastContext";
import { setLogStreamActive } from './utils/logger';
@@ -32,9 +24,26 @@ import { getVersion } from '@tauri-apps/api/app';
import type { PostQueueAction } from './bindings/PostQueueAction';
import { PanelLeft } from 'lucide-react';
import { isTrustedFirelinkReleaseUrl } from './utils/releaseUrls';
import { syncDocumentLocale } from './i18n';
import { changeAppLocale, resolveAppLocale, syncDocumentLocale } from './i18n';
import { useTranslation } from 'react-i18next';
const SettingsView = lazy(() => import('./components/SettingsView'));
const SchedulerView = lazy(() => import('./components/SchedulerView'));
const SpeedLimiterView = lazy(() => import('./components/SpeedLimiterView'));
const LogsView = lazy(() => import('./components/LogsView'));
const AddDownloadsModal = lazy(() => import('./components/AddDownloadsModal').then(module => ({
default: module.AddDownloadsModal,
})));
const PropertiesModal = lazy(() => import('./components/PropertiesModal').then(module => ({
default: module.PropertiesModal,
})));
const DeleteConfirmationModal = lazy(() => import('./components/DeleteConfirmationModal').then(module => ({
default: module.DeleteConfirmationModal,
})));
const KeychainPermissionModal = lazy(() => import('./components/KeychainPermissionModal').then(module => ({
default: module.KeychainPermissionModal,
})));
let automaticUpdateCheckStarted = false;
const processingScheduleKeys = new Set<string>();
@@ -123,11 +132,21 @@ function App() {
});
const theme = useSettingsStore(state => state.theme);
const languagePreference = useSettingsStore(state => state.language);
const isSidebarVisible = useSettingsStore(state => state.isSidebarVisible);
const toggleSidebar = useSettingsStore(state => state.toggleSidebar);
const activeView = useSettingsStore(state => state.activeView);
const appFontSize = useSettingsStore(state => state.appFontSize);
const listRowDensity = useSettingsStore(state => state.listRowDensity);
useEffect(() => {
const locale = languagePreference === 'system'
? resolveAppLocale(typeof navigator === 'undefined' ? undefined : navigator.language)
: languagePreference;
if (i18n.language !== locale) {
void changeAppLocale(locale);
}
}, [i18n, languagePreference]);
const autoCheckUpdates = useSettingsStore(state => state.autoCheckUpdates);
const autoAddClipboardLinks = useSettingsStore(state => state.autoAddClipboardLinks);
const showNotifications = useSettingsStore(state => state.showNotifications);
@@ -135,6 +154,9 @@ function App() {
const showMenuBarIcon = useSettingsStore(state => state.showMenuBarIcon);
const extensionPairingToken = useSettingsStore(state => state.extensionPairingToken);
const showKeychainModal = useSettingsStore(state => state.showKeychainModal);
const isAddModalOpen = useDownloadStore(state => state.isAddModalOpen);
const selectedPropertiesDownloadId = useDownloadStore(state => state.selectedPropertiesDownloadId);
const isDeleteModalOpen = useDownloadStore(state => state.deleteModalState.isOpen);
const downloads = useDownloadStore(state => state.downloads);
const activeDownloadCount = downloads.filter(download => isTransferActiveStatus(download.status)).length;
const queuedCount = downloads.filter(download =>
@@ -920,7 +942,7 @@ function App() {
}`}
style={{
width: sidebarWidth,
marginLeft: isSidebarVisible ? 0 : -sidebarWidth
marginInlineStart: isSidebarVisible ? 0 : -sidebarWidth
}}
>
<div className="app-sidebar-panel h-full w-full">
@@ -956,11 +978,13 @@ function App() {
</button>
)}
<div className="flex-1 flex flex-col overflow-hidden relative">
{activeView === 'downloads' && <DownloadTable filter={filter} />}
{activeView === 'settings' && <SettingsView />}
{activeView === 'scheduler' && <SchedulerView />}
{activeView === 'speedLimiter' && <SpeedLimiterView />}
{activeView === 'logs' && <LogsView />}
<Suspense fallback={null}>
{activeView === 'downloads' && <DownloadTable filter={filter} />}
{activeView === 'settings' && <SettingsView />}
{activeView === 'scheduler' && <SchedulerView />}
{activeView === 'speedLimiter' && <SpeedLimiterView />}
{activeView === 'logs' && <LogsView />}
</Suspense>
</div>
{/* Status Bar */}
@@ -974,10 +998,12 @@ function App() {
</div>
</div>
<AddDownloadsModal />
<PropertiesModal />
<DeleteConfirmationModal />
<KeychainPermissionModal consentVersion={keychainConsentVersion} />
<Suspense fallback={null}>
{isAddModalOpen && <AddDownloadsModal />}
{selectedPropertiesDownloadId !== null && <PropertiesModal />}
{isDeleteModalOpen && <DeleteConfirmationModal />}
{showKeychainModal && <KeychainPermissionModal consentVersion={keychainConsentVersion} />}
</Suspense>
</div>
);
+1 -1
View File
@@ -8,4 +8,4 @@ import type { SettingsTab } from "./SettingsTab";
import type { SiteLogin } from "./SiteLogin";
import type { Theme } from "./Theme";
export type PersistedSettings = { theme: Theme, baseDownloadFolder: string, categorySubfoldersEnabled: boolean, categorySubfolders: { [key in string]: string }, categoryDirectoryOverrides: { [key in string]: string }, approvedDownloadRoots: Array<string>, maxConcurrentDownloads: number, globalSpeedLimit: string, speedLimitPresetValues: Array<number>, logsEnabled: boolean, isSidebarVisible: boolean, activeSettingsTab: SettingsTab, scheduler: SchedulerSettings, schedulerRunning: boolean, schedulerActiveDownloadIds: Array<string>, schedulerLastStartKey: string, schedulerLastStopKey: string, lastCustomSpeedLimitKiB: number, lastCustomSpeedLimitUnit: string, perServerConnections: number, maxAutomaticRetries: number, showNotifications: boolean, playCompletionSound: boolean, autoAddClipboardLinks: boolean, appFontSize: AppFontSize, listRowDensity: ListRowDensity, showDockBadge: boolean, showMenuBarIcon: boolean, proxyMode: ProxyMode, proxyHost: string, proxyPort: number, customUserAgent: string, askWhereToSaveEachFile: boolean, rememberLastUsedDownloadDirectory: boolean, preventsSleepWhileDownloading: boolean, mediaCookieSource: MediaCookieSource, siteLogins: Array<SiteLogin>, autoCheckUpdates: boolean, keychainAccessGranted: boolean, };
export type PersistedSettings = { theme: Theme, language: string, baseDownloadFolder: string, categorySubfoldersEnabled: boolean, categorySubfolders: { [key in string]: string }, categoryDirectoryOverrides: { [key in string]: string }, approvedDownloadRoots: Array<string>, maxConcurrentDownloads: number, globalSpeedLimit: string, speedLimitPresetValues: Array<number>, logsEnabled: boolean, isSidebarVisible: boolean, activeSettingsTab: SettingsTab, scheduler: SchedulerSettings, schedulerRunning: boolean, schedulerActiveDownloadIds: Array<string>, schedulerLastStartKey: string, schedulerLastStopKey: string, lastCustomSpeedLimitKiB: number, lastCustomSpeedLimitUnit: string, perServerConnections: number, maxAutomaticRetries: number, showNotifications: boolean, playCompletionSound: boolean, autoAddClipboardLinks: boolean, appFontSize: AppFontSize, listRowDensity: ListRowDensity, showDockBadge: boolean, showMenuBarIcon: boolean, proxyMode: ProxyMode, proxyHost: string, proxyPort: number, customUserAgent: string, askWhereToSaveEachFile: boolean, rememberLastUsedDownloadDirectory: boolean, preventsSleepWhileDownloading: boolean, mediaCookieSource: MediaCookieSource, siteLogins: Array<SiteLogin>, autoCheckUpdates: boolean, keychainAccessGranted: boolean, };
+61 -19
View File
@@ -25,6 +25,7 @@ import { getPlatformInfo } from '../utils/platform';
import { isTransferLocked } from '../utils/downloadActions';
import { useToast } from '../contexts/ToastContext';
import { useTranslation } from 'react-i18next';
import { localePluralVariant } from '../i18n/locales';
import {
canSubmitMetadataRows,
appendRequestUrlsAfterVersion,
@@ -114,7 +115,7 @@ const extensionHeaders = (context: PendingAddRequestContext | undefined) => [
].filter(Boolean).join('\n');
export const AddDownloadsModal = () => {
const { t } = useTranslation();
const { t, i18n } = useTranslation();
const { addToast } = useToast();
const {
isAddModalOpen,
@@ -1132,19 +1133,60 @@ export const AddDownloadsModal = () => {
);
const playlistSummaries = Object.entries(playlistExpansions)
.filter(([sourceUrl]) => activePlaylistUrls.has(sourceUrl));
const pluralMessage = (
count: number,
one: () => string,
few: () => string,
many: () => string
): string => {
switch (localePluralVariant(i18n.language, count)) {
case 'one': return one();
case 'few': return few();
case 'many': return many();
}
};
const metadataSummary = (() => {
const summary = metadataSummaryState(parsedItems);
const plural = (count: number) => count === 1 ? '' : 's';
switch (summary.type) {
case 'empty': return t($ => $.addDownloads.pasteOneOrMore);
case 'none-selected': return t($ => $.addDownloads.selectAtLeastOne);
case 'invalid': return t($ => $.addDownloads.correctInvalid, { count: summary.count, plural: plural(summary.count) });
case 'loading': return t($ => $.addDownloads.waitingForMetadata, { count: summary.count, plural: plural(summary.count) });
case 'unsafe': return t($ => $.addDownloads.removeUnsafe, { count: summary.count, plural: plural(summary.count) });
case 'media-error': return t($ => $.addDownloads.mediaMetadataUnavailableSummary, { count: summary.count, plural: plural(summary.count) });
case 'invalid': return pluralMessage(
summary.count,
() => t($ => $.addDownloads.correctInvalidOne, { count: summary.count }),
() => t($ => $.addDownloads.correctInvalidFew, { count: summary.count }),
() => t($ => $.addDownloads.correctInvalidMany, { count: summary.count })
);
case 'loading': return pluralMessage(
summary.count,
() => t($ => $.addDownloads.waitingForMetadataOne, { count: summary.count }),
() => t($ => $.addDownloads.waitingForMetadataFew, { count: summary.count }),
() => t($ => $.addDownloads.waitingForMetadataMany, { count: summary.count })
);
case 'unsafe': return pluralMessage(
summary.count,
() => t($ => $.addDownloads.removeUnsafeOne, { count: summary.count }),
() => t($ => $.addDownloads.removeUnsafeFew, { count: summary.count }),
() => t($ => $.addDownloads.removeUnsafeMany, { count: summary.count })
);
case 'media-error': return pluralMessage(
summary.count,
() => t($ => $.addDownloads.mediaMetadataUnavailableSummaryOne, { count: summary.count }),
() => t($ => $.addDownloads.mediaMetadataUnavailableSummaryFew, { count: summary.count }),
() => t($ => $.addDownloads.mediaMetadataUnavailableSummaryMany, { count: summary.count })
);
case 'all-error': return t($ => $.addDownloads.metadataUnavailableFallback);
case 'fallback': return t($ => $.addDownloads.fallbackReady, { ready: summary.ready, readyPlural: plural(summary.ready), failed: summary.failed });
case 'ready': return t($ => $.addDownloads.readyToAdd, { count: summary.count, plural: plural(summary.count) });
case 'fallback': return pluralMessage(
summary.ready,
() => t($ => $.addDownloads.fallbackReadyOne, { ready: summary.ready, failed: summary.failed }),
() => t($ => $.addDownloads.fallbackReadyFew, { ready: summary.ready, failed: summary.failed }),
() => t($ => $.addDownloads.fallbackReadyMany, { ready: summary.ready, failed: summary.failed })
);
case 'ready': return pluralMessage(
summary.count,
() => t($ => $.addDownloads.readyToAddOne, { count: summary.count }),
() => t($ => $.addDownloads.readyToAddFew, { count: summary.count }),
() => t($ => $.addDownloads.readyToAddMany, { count: summary.count })
);
}
})();
@@ -1194,7 +1236,7 @@ export const AddDownloadsModal = () => {
<div className="flex flex-1 min-h-0 overflow-hidden">
{/* Left Column: URLs and Preview */}
<div className="add-download-left w-[55%] border-r border-border-modal flex flex-col">
<div className="add-download-left w-[55%] flex flex-col">
<div className="add-download-pane p-5 flex-1 min-h-0 min-w-0 flex flex-col gap-5">
<div className="flex flex-col gap-2">
@@ -1245,7 +1287,7 @@ export const AddDownloadsModal = () => {
type="button"
onClick={toggleAllRows}
disabled={parsedItems.length === 0}
className="add-download-link-button ml-3 text-[11px] font-medium"
className="add-download-link-button ms-3 text-[11px] font-medium"
>
{allRowsSelected ? t($ => $.addDownloads.clearSelection) : t($ => $.addDownloads.selectAll)}
</button>
@@ -1302,9 +1344,9 @@ export const AddDownloadsModal = () => {
onChange={() => toggleRowSelection(i)}
onClick={event => event.stopPropagation()}
aria-label={t($ => $.addDownloads.selectItem, { file: item.file })}
className="mr-2 shrink-0 accent-purple-500"
className="me-2 shrink-0 accent-purple-500"
/>
<div className="flex-[2] text-text-primary font-medium truncate pr-2" title={item.file}>{item.file}</div>
<div className="flex-[2] text-text-primary font-medium truncate pe-2" title={item.file}>{item.file}</div>
<div className={`flex-1 font-mono ${item.status === 'loading' ? 'text-text-muted/50' : 'text-text-muted'}`}>{item.size || t($ => $.addDownloads.unknown)}</div>
<div className={`flex-[1.5] font-medium ${item.status === 'metadata-error' || item.status === 'invalid' ? 'text-red-500' : item.status === 'loading' ? 'text-orange-400' : 'text-blue-500'}`}>
{item.status === 'loading' ? (
@@ -1337,7 +1379,7 @@ export const AddDownloadsModal = () => {
{/* Media Format (Dynamic) */}
{selectedItemIndex !== null && parsedItems[selectedItemIndex]?.isMedia && (
<section className="add-download-section add-download-media-section relative overflow-hidden p-4">
<div className="absolute top-0 right-0 p-2 opacity-10">
<div className="absolute top-0 end-0 p-2 opacity-10">
<Video size={48} />
</div>
<div className="add-download-section-title flex items-center gap-2 mb-3 relative z-10">
@@ -1358,7 +1400,7 @@ export const AddDownloadsModal = () => {
<div className="space-y-3 relative z-10">
<div className="flex flex-col gap-1.5">
<label className="text-[10px] uppercase font-bold tracking-wider text-text-muted">{t($ => $.addDownloads.availableStreams)}</label>
<div className="flex flex-col gap-1 max-h-48 overflow-y-auto pr-1" role="radiogroup" aria-label={t($ => $.addDownloads.availableMediaStreams)}>
<div className="flex flex-col gap-1 max-h-48 overflow-y-auto pe-1" role="radiogroup" aria-label={t($ => $.addDownloads.availableMediaStreams)}>
{parsedItems[selectedItemIndex].formats!.map((f, idx) => {
const isSelected = parsedItems[selectedItemIndex].selectedFormat === idx;
const Icon = f.type === 'Audio' ? Music : Film;
@@ -1474,7 +1516,7 @@ export const AddDownloadsModal = () => {
</label>
{useAuth && (
<div className="space-y-2.5 pl-5 border-l-2 border-border-modal/50">
<div className="add-download-nested-fields space-y-2.5">
<input type="text" value={username} onChange={e=>setUsername(e.target.value)} placeholder={t($ => $.addDownloads.username)} className="add-download-control w-full px-3 py-1.5 text-xs" />
<input type="password" value={password} onChange={e=>setPassword(e.target.value)} placeholder={t($ => $.addDownloads.password)} className="add-download-control w-full px-3 py-1.5 text-xs" />
</div>
@@ -1493,7 +1535,7 @@ export const AddDownloadsModal = () => {
</button>
{advancedExpanded && (
<div className="mt-4 space-y-4 pl-6">
<div className="add-download-advanced-fields mt-4 space-y-4">
<label className="flex items-center gap-2 text-xs text-text-secondary font-medium cursor-pointer">
<input type="checkbox" checked={checksumEnabled} onChange={e=>setChecksumEnabled(e.target.checked)} className="add-download-checkbox" />
{t($ => $.addDownloads.verifyChecksum)}
@@ -1573,12 +1615,12 @@ export const AddDownloadsModal = () => {
aria-haspopup="menu"
aria-expanded={isQueueMenuOpen}
>
{t($ => $.addDownloads.addToQueue)} <ChevronDown size={14} className="ml-1" />
{t($ => $.addDownloads.addToQueue)} <ChevronDown size={14} className="ms-1" />
</button>
{isQueueMenuOpen && (
<div
role="menu"
className="app-modal absolute bottom-full right-0 z-[70] mb-2 min-w-[200px] overflow-visible py-1.5 text-xs"
className="add-download-queue-menu app-modal absolute bottom-full z-[70] mb-2 min-w-[200px] overflow-visible py-1.5 text-xs"
>
{queues.map(queue => (
<button
@@ -1589,7 +1631,7 @@ export const AddDownloadsModal = () => {
setIsQueueMenuOpen(false);
void handleAction({ type: 'add-to-queue', queueId: queue.id });
}}
className="flex w-full items-center gap-2 px-3 py-2 text-left hover:bg-item-hover"
className="flex w-full items-center gap-2 px-3 py-2 text-start hover:bg-item-hover"
>
<span className="truncate">{queue.name}</span>
</button>
+4 -4
View File
@@ -692,9 +692,9 @@ export const DownloadTable: React.FC<DownloadTableProps> = ({ filter }) => {
<div className="group relative">
<button className="w-full text-left px-3 py-2 hover:bg-item-hover transition-colors flex justify-between items-center">
{t($ => $.downloadTable.addToQueue)}
<ChevronRight size={14} />
<ChevronRight size={14} className="download-context-menu-chevron" />
</button>
<div className="absolute left-full top-0 hidden group-hover:block ml-1 min-w-[150px] bg-bg-modal border border-border-modal rounded-lg shadow-lg py-1.5 z-50">
<div className="download-context-submenu absolute top-0 hidden group-hover:block min-w-[150px] bg-bg-modal border border-border-modal rounded-lg shadow-lg py-1.5 z-50">
{queues.map(q => (
<button key={q.id} onClick={() => {
setContextMenu(null);
@@ -811,9 +811,9 @@ export const DownloadTable: React.FC<DownloadTableProps> = ({ filter }) => {
<div className="group relative">
<button className="w-full text-left px-3 py-2 hover:bg-item-hover transition-colors flex justify-between items-center">
{t($ => $.downloadTable.addToQueue)}
<ChevronRight size={14} />
<ChevronRight size={14} className="download-context-menu-chevron" />
</button>
<div className="absolute left-full top-0 hidden group-hover:block ml-1 min-w-[150px] bg-bg-modal border border-border-modal rounded-lg shadow-lg py-1.5 z-50">
<div className="download-context-submenu absolute top-0 hidden group-hover:block min-w-[150px] bg-bg-modal border border-border-modal rounded-lg shadow-lg py-1.5 z-50">
{queues.map(q => (
<button key={q.id} onClick={() => {
setContextMenu(null);
+3 -3
View File
@@ -328,7 +328,7 @@ export default function LogsView() {
<div className="bg-black/10 border-y border-border-modal px-4 py-2 shrink-0 flex items-center gap-2 text-text-muted text-[10px] select-none">
<Info size={12} className="text-text-muted opacity-80 shrink-0" />
<span className="opacity-90 leading-tight">
<strong className="font-medium text-text-primary mr-1">{t($ => $.logs.localDiagnostics)}</strong>
<strong className="font-medium text-text-primary me-1">{t($ => $.logs.localDiagnostics)}</strong>
{t($ => $.logs.diagnosticsDescription)}
</span>
</div>
@@ -362,10 +362,10 @@ export default function LogsView() {
onClick={(e) => e.stopPropagation()}
>
<button
className="w-full text-left px-3 py-2 flex items-center hover:bg-item-hover transition-colors"
className="w-full text-start px-3 py-2 flex items-center hover:bg-item-hover transition-colors"
onClick={handleCopy}
>
<Copy size={13} className="mr-2 text-text-secondary" />
<Copy size={13} className="me-2 text-text-secondary" />
{t($ => $.logs.copy)}
</button>
</div>
+28
View File
@@ -644,6 +644,15 @@ runEngineChecks(false);
default: return t($ => $.navigation.categories.other);
}
};
const languageOptions = [
{ value: 'system', label: t($ => $.settings.lookAndFeel.languageSystem) },
{ value: 'en', label: t($ => $.settings.lookAndFeel.languageEnglish) },
{ value: 'zh-CN', label: t($ => $.settings.lookAndFeel.languageChinese) },
{ value: 'he', label: t($ => $.settings.lookAndFeel.languageHebrew) },
{ value: 'fa', label: t($ => $.settings.lookAndFeel.languagePersian) },
{ value: 'uk', label: t($ => $.settings.lookAndFeel.languageUkrainian) },
{ value: 'ru', label: t($ => $.settings.lookAndFeel.languageRussian) },
] as const;
const TabButton = ({ type, icon: Icon, label }: { type: SettingsTab; icon: typeof Download; label: string }) => {
const active = activeTab === type;
@@ -784,6 +793,25 @@ runEngineChecks(false);
{/* Look & Feel Pane */}
{activeTab === 'lookandfeel' && (
<div className="settings-pane max-w-[720px]">
<h2 className="settings-section-title">{t($ => $.settings.lookAndFeel.language)}</h2>
<div className="mac-settings-group">
<div className="mac-settings-row">
<div className="settings-row-label">
<span>{t($ => $.settings.lookAndFeel.language)}</span>
<small>{t($ => $.settings.lookAndFeel.languageDescription)}</small>
</div>
<select
value={settings.language}
onChange={(event) => settings.setLanguage(event.target.value as typeof settings.language)}
className="app-control w-48"
>
{languageOptions.map(option => (
<option key={option.value} value={option.value}>{option.label}</option>
))}
</select>
</div>
</div>
<h2 className="settings-section-title">{t($ => $.settings.lookAndFeel.appTheme)}</h2>
<div className="mac-settings-group">
<div className="mac-settings-row settings-choice-row">
+13 -13
View File
@@ -122,13 +122,13 @@ export const Sidebar: React.FC<SidebarProps> = (props) => {
<button
type="button"
data-active={isSelected}
className="sidebar-nav-item group flex w-full items-center text-[13px] text-left cursor-default font-medium"
className="sidebar-nav-item group flex w-full items-center text-[13px] text-start cursor-default font-medium"
onClick={() => onSelectFilter(filter)}
>
<Icon className="w-[18px] h-[18px] mr-3 shrink-0" strokeWidth={isSelected ? 2.5 : 2} />
<Icon className="w-[18px] h-[18px] me-3 shrink-0" strokeWidth={isSelected ? 2.5 : 2} />
<span className="truncate">{label}</span>
{getCount(filter) > 0 && (
<span className="sidebar-count ml-auto min-w-5 px-1.5 py-0.5 rounded-full text-center text-[10px] leading-none font-bold">
<span className="sidebar-count ms-auto min-w-5 px-1.5 py-0.5 rounded-full text-center text-[10px] leading-none font-bold">
{getCount(filter)}
</span>
)}
@@ -182,7 +182,7 @@ export const Sidebar: React.FC<SidebarProps> = (props) => {
if (isRenaming) {
return (
<div className="flex items-center px-2.5 py-1 rounded-lg mb-0.5 bg-item-hover">
<List className="w-4 h-4 mr-2 text-text-secondary" strokeWidth={2} />
<List className="w-4 h-4 me-2 text-text-secondary" strokeWidth={2} />
<input
ref={renameInputRef}
type="text"
@@ -205,12 +205,12 @@ export const Sidebar: React.FC<SidebarProps> = (props) => {
data-active={isSelected}
onContextMenu={e => handleQueueContextMenu(e, queue.id)}
onClick={() => onSelectFilter(filterId)}
className="sidebar-nav-item group flex w-full items-center text-[13px] text-left cursor-default font-medium"
className="sidebar-nav-item group flex w-full items-center text-[13px] text-start cursor-default font-medium"
>
<List className="w-[18px] h-[18px] mr-3 shrink-0" strokeWidth={isSelected ? 2.5 : 2} />
<List className="w-[18px] h-[18px] me-3 shrink-0" strokeWidth={isSelected ? 2.5 : 2} />
<span className="truncate">{queue.name}</span>
{getCount(filterId) > 0 && (
<span className="sidebar-count ml-auto min-w-5 px-1.5 py-0.5 rounded-full text-center text-[10px] leading-none font-bold shrink-0">
<span className="sidebar-count ms-auto min-w-5 px-1.5 py-0.5 rounded-full text-center text-[10px] leading-none font-bold shrink-0">
{getCount(filterId)}
</span>
)}
@@ -225,9 +225,9 @@ export const Sidebar: React.FC<SidebarProps> = (props) => {
type="button"
data-active={isSelected}
onClick={() => setActiveView(view)}
className="sidebar-nav-item group flex w-full items-center text-[13px] text-left cursor-default font-medium"
className="sidebar-nav-item group flex w-full items-center text-[13px] text-start cursor-default font-medium"
>
<Icon className="w-[18px] h-[18px] mr-3 shrink-0" strokeWidth={isSelected ? 2.5 : 2} />
<Icon className="w-[18px] h-[18px] me-3 shrink-0" strokeWidth={isSelected ? 2.5 : 2} />
<span>{label}</span>
</button>
);
@@ -298,7 +298,7 @@ export const Sidebar: React.FC<SidebarProps> = (props) => {
))}
{isAddingQueue ? (
<div className="flex items-center px-3.5 py-1.5 rounded-lg bg-item-hover mb-1">
<Plus className="w-4 h-4 mr-2 text-text-secondary shrink-0" strokeWidth={2} />
<Plus className="w-4 h-4 me-2 text-text-secondary shrink-0" strokeWidth={2} />
<input
ref={addInputRef}
type="text"
@@ -319,7 +319,7 @@ export const Sidebar: React.FC<SidebarProps> = (props) => {
onClick={() => { addQueueSubmitRef.current = false; setIsAddingQueue(true); setNewQueueName(''); }}
className="flex w-full items-center px-3.5 py-1.5 rounded-lg text-[13px] text-text-muted hover:bg-item-hover hover:text-text-secondary cursor-default transition-colors mb-1"
>
<Plus className="w-4 h-4 mr-2 shrink-0" strokeWidth={2} />
<Plus className="w-4 h-4 me-2 shrink-0" strokeWidth={2} />
<span className="truncate">{t($ => $.actions.addNewQueue)}</span>
</button>
)}
@@ -338,9 +338,9 @@ export const Sidebar: React.FC<SidebarProps> = (props) => {
type="button"
data-active={activeView === 'settings'}
onClick={() => setActiveView('settings')}
className="sidebar-nav-item sidebar-settings-button group flex w-full items-center text-[13px] text-left cursor-default font-medium transition-colors"
className="sidebar-nav-item sidebar-settings-button group flex w-full items-center text-[13px] text-start cursor-default font-medium transition-colors"
>
<Settings className={`w-[18px] h-[18px] mr-3 shrink-0 ${activeView === 'settings' ? 'text-white' : 'text-text-muted'}`} strokeWidth={activeView === 'settings' ? 2.5 : 2} />
<Settings className={`w-[18px] h-[18px] me-3 shrink-0 ${activeView === 'settings' ? 'text-white' : 'text-text-muted'}`} strokeWidth={activeView === 'settings' ? 2.5 : 2} />
<span>{t($ => $.navigation.settings)}</span>
</button>
</div>
+4 -4
View File
@@ -218,7 +218,7 @@ export default function SpeedLimiterView() {
}`}>
{enabled ? `${currentDisplayValue} ${unit}` : t($ => $.speedLimiter.unlimited)}
</span>
<button onClick={() => void save()} disabled={isSaving} className="app-button app-button-primary ml-auto px-3 text-[11px] disabled:opacity-50">
<button onClick={() => void save()} disabled={isSaving} className="app-button app-button-primary ms-auto px-3 text-[11px] disabled:opacity-50">
<Save size={14} /> {t($ => $.speedLimiter.saveLimit)}
</button>
</div>
@@ -275,7 +275,7 @@ export default function SpeedLimiterView() {
type="button"
disabled={!enabled || isSaving}
onClick={() => preset(presetValue)}
className="h-full flex-1 px-3 text-left disabled:opacity-50"
className="h-full flex-1 px-3 text-start disabled:opacity-50"
>
{formatPresetValue(displayValue)} {unit}
</button>
@@ -283,7 +283,7 @@ export default function SpeedLimiterView() {
type="button"
disabled={!enabled || isSaving}
onClick={() => removePreset(presetValue)}
className="flex h-full w-7 items-center justify-center border-l border-border-modal text-text-muted transition-colors hover:bg-red-500/10 hover:text-red-400 focus-visible:bg-red-500/10 focus-visible:text-red-400 disabled:opacity-50"
className="flex h-full w-7 items-center justify-center border-s border-border-modal text-text-muted transition-colors hover:bg-red-500/10 hover:text-red-400 focus-visible:bg-red-500/10 focus-visible:text-red-400 disabled:opacity-50"
title={t($ => $.speedLimiter.removePreset, { value: formatPresetValue(displayValue), unit })}
aria-label={t($ => $.speedLimiter.removePreset, { value: formatPresetValue(displayValue), unit })}
>
@@ -292,7 +292,7 @@ export default function SpeedLimiterView() {
</div>
);
})}
<div className="ml-1 flex h-8 items-center gap-1.5 rounded-md border border-border-modal bg-bg-input px-2">
<div className="ms-1 flex h-8 items-center gap-1.5 rounded-md border border-border-modal bg-bg-input px-2">
<input
type="number"
min={speedValueFromKiB(1, unit)}
+728
View File
@@ -0,0 +1,728 @@
const common = {
navigation: {
library: 'Library',
filters: {
all: 'All',
active: 'Active',
completed: 'Completed',
unfinished: 'Unfinished',
},
categories: {
musics: 'Music',
movies: 'Movies',
compressed: 'Compressed',
documents: 'Documents',
pictures: 'Pictures',
applications: 'Applications',
other: 'Other',
},
folders: 'Folders',
queues: 'Queues',
tools: 'Tools',
scheduler: 'Scheduler',
speedLimiter: 'Speed Limiter',
logs: 'Logs',
settings: 'Settings',
},
actions: {
hideSidebar: 'Hide Sidebar',
showSidebar: 'Show Sidebar',
resizeSidebar: 'Resize Sidebar',
cancel: 'Cancel',
continue: 'Continue',
dismissNotification: 'Dismiss notification',
queueName: 'Queue name',
addNewQueue: 'Add new queue',
startQueue: 'Start Queue',
pauseQueue: 'Pause Queue',
renameQueue: 'Rename Queue',
deleteQueue: 'Delete Queue',
},
dialogs: {
errorBoundary: {
title: 'Something went wrong.',
description: 'A critical error occurred in the React component tree. The error details below can help identify the root cause.',
reload: 'Reload App',
},
duplicateDownloads: {
title: 'Duplicate Downloads Detected',
description: 'Some of the downloads you are adding already exist in the queue or on disk. Please choose how to resolve these conflicts.',
rename: 'Rename',
replace: 'Replace',
skip: 'Skip',
},
removeDownload: {
title: 'Remove Download',
confirmationSingle: 'Are you sure you want to remove this item from the list? You can also choose to delete the underlying file from your hard drive.',
confirmationMultiple: 'Are you sure you want to remove these {{count}} items from the list? You can also choose to delete the underlying files from your hard drive.',
errorSummary: '{{succeeded}} removed, {{failed}} failed: {{detail}}',
remove: 'Remove',
deleteFile: 'Delete file',
},
},
window: {
controls: 'Window controls',
close: 'Close',
minimize: 'Minimize',
maximize: 'Maximize',
},
downloads: {
actions: {
moveUp: 'Move Up',
moveDown: 'Move Down',
pause: 'Pause',
start: 'Start',
resume: 'Resume',
options: 'Options',
},
size: {
downloadedOf: '{{downloaded}} downloaded of {{total}} {{unit}}',
downloadedOfApproximate: '{{downloaded}} downloaded of approximately {{total}} {{unit}}',
},
status: {
ready: 'Ready',
staged: 'In queue',
queued: 'Queued',
downloading: 'Downloading',
processing: 'Processing',
paused: 'Paused',
completed: 'Completed',
failed: 'Failed',
retrying: 'Retrying',
},
values: {
processing: 'Processing...',
muxing: 'Muxing...',
},
},
speedLimiter: {
title: 'Speed Limiter',
unlimited: 'Unlimited',
saveLimit: 'Save Limit',
globalSpeedLimit: 'Global Speed Limit',
description: 'Applies to new and active aria2 transfers and new yt-dlp media downloads. Explicit per-download limits remain attached to those transfers.',
quickPresets: 'Quick Presets',
removePreset: 'Remove {{value}} {{unit}} preset',
customPresetIn: 'Custom preset in {{unit}}',
addQuickPreset: 'Add quick preset',
globalLimitSaved: 'Global limit saved at {{value}} {{unit}}',
globalLimitDisabled: 'Global speed limit disabled',
saveFailed: 'Could not save global speed limit: {{detail}}',
presetAlreadyExists: '{{value}} {{unit}} is already in quick presets',
presetAdded: 'Added {{value}} {{unit}} quick preset',
presetRemoved: 'Removed {{value}} {{unit}} quick preset',
},
logs: {
title: 'Logs',
entries: '({{count}} entries)',
collecting: 'Collecting',
off: 'Off',
allLevels: 'All Levels',
levels: {
error: 'Error',
warn: 'Warn',
info: 'Info',
debug: 'Debug',
trace: 'Trace',
},
pauseLogging: 'Pause diagnostic logging',
enableLogging: 'Enable diagnostic logging',
clearDisplayed: 'Clear displayed logs',
export: 'Export logs',
exportButton: 'Export Logs',
logFiles: 'Log Files',
localDiagnostics: 'Local diagnostics:',
diagnosticsDescription: 'Diagnostic collection is opt-in, bounded, and local. Common secrets, URL queries, and home-directory paths are redacted during display and export. Nothing is uploaded automatically.',
noEntries: 'No persisted log entries are available yet.',
disabledDescription: 'Diagnostic logging is off. Existing support logs will appear here when available.',
copy: 'Copy',
copied: 'Copied to clipboard',
copyFailed: 'Failed to copy to clipboard',
exported: 'Support logs exported',
exportFailed: 'Could not export logs: {{detail}}',
cleared: 'Logs cleared',
clearFailed: 'Could not clear logs: {{detail}}',
enabled: 'Diagnostic logging enabled',
disabled: 'Diagnostic logging disabled',
updateFailed: 'Could not update diagnostic logging: {{detail}}',
},
scheduler: {
title: 'Scheduler',
running: 'Running',
disabled: 'Scheduler is disabled',
noScheduledDay: 'No scheduled day selected',
unsavedChanges: 'Unsaved changes',
runNow: 'Run Now',
pause: 'Pause',
saveSettings: 'Save Settings',
timing: 'Timing',
startTime: 'Start Time',
stopTime: 'Stop Time',
timingDescription: 'If Firelink is asleep at the start time, it starts the selected queues when it returns later that day, unless the stop time has already passed.',
runEveryDay: 'Run Every Day',
queuesToSchedule: 'Queues to Schedule',
defaultQueue: 'Default queue',
afterCompletion: 'After Completion',
afterCompletionDescription: 'Choose what happens after downloads started by the scheduler finish.',
actionWarning: 'This action can interrupt other work on the computer. Firelink invokes it immediately after the scheduled queue finishes.',
systemPermissions: 'System Permissions',
macPermissionDescription: 'Sleep, restart, and shut down require macOS Automation permission for System Events.',
permissionGranted: 'Automation permission granted',
permissionChecking: 'Checking Automation permission...',
permissionMissing: 'Automation permission missing',
revokePermission: 'Revoke permission',
grantPermission: 'Grant permission',
permissionAvailable: 'Automation permission is available.',
permissionMissingDetails: 'Automation permission is missing. Enable Firelink under Automation for System Events in System Settings.',
revokePermissionDetails: 'macOS does not allow Firelink to revoke Automation permission directly. Revoke System Events access in System Settings, then return to Firelink.',
requestingPermission: 'Requesting Automation permission...',
enablePermissionDetails: 'Enable Firelink under Automation for System Events in System Settings, then return to Firelink.',
systemActions: 'System Actions',
windowsActionsDescription: 'Sleep, restart, and shut down use Windows system privileges. Firelink reports any rejected action when it runs; no permanent permission is claimed in advance.',
linuxActionsDescription: 'Sleep, restart, and shut down use your Linux desktop and system policy. Firelink reports any rejected action when it runs; no permanent permission is claimed in advance.',
validationDay: 'Select at least one day for the scheduler',
validationQueue: 'Select at least one queue for the scheduler',
validationStopTime: 'Stop time must be later than start time',
saved: 'Scheduler settings saved',
trackingOne: 'Tracking 1 scheduled download',
trackingMany: 'Tracking {{count}} scheduled downloads',
noStartableDownloads: 'No downloads in the selected queues can be started',
pausedOne: 'Paused 1 active download',
pausedMany: 'Paused {{count}} active downloads',
noActiveDownloads: 'No active downloads',
days: {
su: 'Su',
mo: 'Mo',
tu: 'Tu',
we: 'We',
th: 'Th',
fr: 'Fr',
sa: 'Sa',
},
postActions: {
none: 'Do nothing',
sleep: 'Sleep',
restart: 'Restart',
shutdown: 'Shut down',
},
},
properties: {
progress: 'Progress',
size: 'Size',
speed: 'Speed',
eta: 'ETA',
connections: 'Connections',
speedCap: 'Speed cap',
category: 'Category',
lastTry: 'Last try',
dateAdded: 'Date added',
destination: 'Destination',
lastError: 'Last error',
saved: ' (saved)',
defaultValue: ' (default)',
savedTooltip: 'Saved for this download; Settings changes apply to new downloads.',
defaultTooltip: 'Using the current default for new downloads.',
identityReadOnly: 'File identity is read-only. Transfer settings are saved for redownload.',
transferSettings: 'Transfer settings can be changed after stopping or pausing. Current transfers keep their existing backend options.',
download: 'Download',
url: 'URL',
fileName: 'File name',
saveLocation: 'Save location',
select: 'Select',
perFile: 'per file',
useCurrentDefault: 'Use current default ({{count}})',
savedPerDownload: 'Saved per download. The Settings default applies to new downloads.',
limit: 'Limit',
siteLogin: 'Site Login',
siteLoginRedownload: 'Site Login for Redownload',
matchingSiteLogin: 'Matching site login',
customCredentials: 'Custom credentials',
noLogin: 'No login',
useSavedLogin: 'Will use saved login if available.',
advancedTransfer: 'Advanced Transfer',
advancedTransferRedownload: 'Advanced Transfer for Redownload',
checksum: 'Checksum',
verify: 'Verify',
algorithm: 'Algorithm',
digest: 'Digest',
expectedDigest: 'Expected digest',
cookies: 'Cookies',
headers: 'Headers',
mirrors: 'Mirrors',
username: 'Username',
password: 'Password',
enterValidUrl: 'Enter a valid URL.',
fileNameEmpty: 'File name cannot be empty.',
cancel: 'Cancel',
save: 'Save',
},
keychain: {
title: 'Credential Storage Access Needed',
stores: {
portable: 'the portable Firelink data folder',
windows: 'Windows Credential Manager',
linux: 'your Linux credential store',
macos: 'macOS Keychain',
system: "this system's credential store",
siteCredentials: "the system's credential store",
},
grantLabelPortable: 'Continue',
grantLabelMacos: 'Grant Access',
grantLabelDefault: 'Enable Secure Storage',
description: 'Firelink uses the browser extension to capture downloads. To keep the extension paired after restarts, Firelink stores its pairing token in {{pairingStore}}. Optional site credentials are stored in {{siteCredentialStore}}.',
portableExplanation: 'The pairing token is portable with this folder. Site credentials remain in the system credential store; a system prompt may appear after you grant access.',
macosExplanation: 'macOS may show a Keychain prompt after you grant access.',
defaultExplanation: 'This usually completes silently. If the credential service is unavailable, Firelink will show the error here and the extension will stay paired for this session only.',
note: 'Note:',
portableNote: 'Portable mode stores only the pairing token in this folder. It does not copy site passwords or browser credentials.',
defaultNote: 'Firelink only writes its own dedicated credential entry. It cannot access other saved passwords or credential items on your system.',
hint: 'Hint:',
portableHint: 'The portable pairing token is already stored with this folder; you can enable it here or select Later.',
defaultHint: 'If you select Later, the extension will only work for this session.',
enableFromSettings: 'You can enable storage anytime from Settings > Integrations.',
later: 'Later',
enabling: 'Enabling...',
timeout: 'Credential storage request timed out. You can select Later and try again.',
unavailable: '{{store}} is unavailable.',
accessRequired: 'Grant credential-store access before regenerating the pairing token.',
storeUnavailable: 'Credential store access is unavailable.',
},
app: {
settingsSaveFailed: 'Could not save settings. Check storage permissions and try again.',
systemActionCountdown: '{{action}} in 10 seconds.',
systemActionCancelled: 'System action cancelled because another download is active or queued.',
systemActionFailed: 'Scheduled system action failed: {{detail}}',
downloadCompleteTitle: 'Download Complete',
downloadCompleteBody: '{{fileName}} has finished downloading.',
unknownFile: 'A file',
downloadFailedTitle: 'Download Failed',
downloadFailedBody: '{{fileName}} failed to download.',
initializeDownloadsFailed: 'Could not initialize saved downloads: {{detail}}',
extensionDisconnected: 'Browser extension disconnected because its pairing token changed.',
copyToken: 'Copy token',
integrations: 'Integrations',
copyTokenFailed: 'Could not copy pairing token: {{detail}}',
credentialPersistenceFailed: 'Secure credential persistence is unavailable. Browser pairing works for this session only: {{detail}}',
resumeDownloadsFailed: 'Could not resume saved downloads: {{detail}}',
updateAvailable: 'Firelink {{version}} is available.',
viewRelease: 'View release',
sleepPreventionFailed: 'Could not update sleep prevention: {{detail}}',
schedulerNoQueues: 'Scheduler has no valid queues selected. Update Scheduler settings.',
schedulerPauseOneFailed: 'Scheduler could not pause 1 download.',
schedulerPauseManyFailed: 'Scheduler could not pause {{count}} downloads.',
scheduledIncomplete: 'Scheduled downloads did not all complete. The post-queue system action was skipped.',
scheduledActionSkippedActive: 'Scheduled system action skipped because another download is active or queued.',
notificationsDisabled: 'System notifications are disabled for Firelink.',
notificationsFailed: 'Could not configure notifications: {{detail}}',
},
sidebar: {
deleteQueueConfirm: 'Delete queue "{{name}}"? Its unfinished downloads will move to Main Queue.',
deleteQueueFailed: 'Could not delete queue: {{detail}}',
queueNameEmpty: 'Queue name cannot be empty',
queueNameExists: 'A queue with this name already exists',
startQueueFailed: 'Could not start queue: {{detail}}',
pauseQueueFailed: 'Could not pause queue: {{detail}}',
},
downloadTable: {
unknownQueue: 'Unknown Queue',
allDownloads: 'All Downloads',
active: 'Active',
completed: 'Completed',
unfinished: 'Unfinished',
headers: {
fileName: 'File Name',
size: 'Size',
status: 'Status',
speed: 'Speed',
eta: 'ETA',
dateAdded: 'Date Added',
},
addDownload: 'Add Download',
resumeAll: 'Resume All',
pauseAll: 'Pause All',
queueEmpty: 'Queue is empty',
noCompletedDownloads: 'No Completed Downloads',
noDownloads: 'No Downloads',
queueEmptyDescription: 'Add downloads to this queue from an item menu or the Add window.',
completedDescription: 'Completed downloads will appear here.',
clickToAdd: 'Click',
addButtonOr: 'button or',
toAddDownloads: 'to add downloads',
nonResumableOne: '1 download does not support resuming. If you pause it, you will have to start over again later. Are you sure you want to pause?',
nonResumableMany: '{{count}} downloads do not support resuming. If you pause them, you will have to start over again later. Are you sure you want to pause?',
interactionError: '{{message}}: {{detail}}',
openFileFailed: 'Could not open downloaded file',
revealFileFailed: 'Could not show download in Finder',
pauseFailed: 'Could not pause download',
resumeFailed: 'Could not resume {{fileName}}',
moveManyFailed: 'Could not move downloads to queue',
moveOneFailed: 'Could not move download to queue',
copyAddressesFailed: 'Could not copy addresses',
copyAddressFailed: 'Could not copy address',
copyPathFailed: 'Could not copy file path',
missingFileName: 'File name is missing',
redownloadFailed: 'Redownload failed',
startResume: 'Start/Resume',
addToQueue: 'Add to Queue',
copyAddress: 'Copy Address',
remove: 'Remove',
open: 'Open',
showInFolder: 'Show in Folder',
pause: 'Pause',
start: 'Start',
resume: 'Resume',
redownload: 'Redownload',
copyFilePath: 'Copy File Path',
properties: 'Properties',
backendRejectedStart: 'The backend rejected the start/resume request.',
transferActive: 'Cannot change properties while transfer is active. Pause it first.',
redownloadNotFound: 'Cannot redownload: download was not found.',
redownloadActive: 'Cannot redownload a {{status}} download. Pause or wait for it to finish first.',
originalUrlMissing: 'Cannot redownload: original URL is missing.',
pauseBeforeMove: 'Pause {{file}} before moving it to another queue.',
},
addDownloads: {
unknownSize: 'Unknown size',
unknown: 'Unknown',
discardSetup: 'Discard this download setup?',
speedInvalid: 'Speed limit must be greater than zero',
duplicateResolveFailed: 'Could not resolve duplicate downloads: {{detail}}',
addedWithFailures: '{{added}} added, {{failed}} failed. {{detail}}',
addedOne: '1 download added',
addedMany: '{{count}} downloads added',
urlAlreadyQueued: 'URL already in queue',
destinationConflict: 'Another selected download uses this destination',
existingDownloadDestination: 'Existing Firelink download uses this destination',
fileExistsOnDisk: 'File exists on disk; rename or skip to avoid deleting unrelated data',
backendRejectedStart: 'Backend rejected download start.',
playlistNoEntries: 'Playlist contains no downloadable entries',
invalidMediaMetadata: 'Invalid media metadata or no formats found',
noAvailableName: 'Could not find an available name for {{file}}.',
pauseBeforeReplace: 'Pause {{file}} before replacing it.',
cannotReplace: 'Cannot replace {{file}}: file is not owned by a Firelink download.',
downloadLinks: 'Download Links',
pastePlaceholder: 'Paste HTTP, HTTPS, FTP, or SFTP URLs here...\n\nFor media downloads, paste links from YouTube, X, TikTok, Instagram, Reddit, etc.',
playlistSummary: 'Playlist “{{title}}”: {{loaded}}{{total}} entries loaded{{truncated}}{{skipped}}',
safeEntryLimit: ' (safe entry limit reached)',
selectedSummary: '{{ready}} selected ready, {{fallback}} fallback, {{mediaRetry}} media retry, {{blocked}} blocked',
clearSelection: 'Clear selection',
selectAll: 'Select all',
refreshMetadata: 'Refresh Metadata',
files: 'Files',
required: 'Required',
free: 'Free',
preview: 'Preview',
file: 'File',
size: 'Size',
status: 'Status',
downloadPreview: 'Download preview',
noLinks: 'No links added yet.',
selectItem: 'Select {{file}}',
fetchingPlaylist: 'Fetching playlist...',
fetching: 'Fetching...',
unsafeUrl: 'Unsafe URL',
playlistFailed: 'Playlist failed',
metadataFailed: 'Metadata failed',
fallback: 'Fallback',
invalid: 'Invalid',
ready: 'Ready',
mediaFormat: 'Media Format',
playlistItem: 'Playlist item {{index}}',
fetchingMediaStreams: 'Fetching media streams...',
availableStreams: 'Available Streams',
availableMediaStreams: 'Available media streams',
metadataUnavailable: 'Metadata unavailable. Refresh metadata before adding this media.',
saveLocation: 'Save Location',
browse: 'Browse',
categoryFolders: 'Files will be organized into category folders.',
sharedFolder: 'All selected downloads will use this folder.',
transferSettings: 'Transfer Settings',
connectionsPerFile: 'Connections per File',
connectionsPerFileAria: 'Connections per file',
limitSpeedPerFile: 'Limit speed per file',
speedLimitPerFileAria: 'Speed limit per file',
authorization: 'Authorization',
useAuthorization: 'Use authorization',
username: 'Username',
password: 'Password',
advancedTransfer: 'Advanced Transfer',
verifyChecksum: 'Verify Checksum',
checksumAlgorithm: 'Checksum algorithm',
expectedDigest: 'Expected digest',
headers: 'Headers',
requestHeaders: 'Request headers',
cookies: 'Cookies',
cookiePlaceholder: 'name=value; other=value',
mirrors: 'Mirrors',
cancel: 'Cancel',
startDownloads: 'Start Downloads',
addToQueue: 'Add to queue',
pasteOneOrMore: 'Paste one or more links.',
selectAtLeastOne: 'Select at least one download.',
correctInvalidOne: 'Correct or remove {{count}} invalid URL before continuing.',
correctInvalidFew: 'Correct or remove {{count}} invalid URLs before continuing.',
correctInvalidMany: 'Correct or remove {{count}} invalid URLs before continuing.',
waitingForMetadataOne: 'Waiting for metadata for {{count}} download.',
waitingForMetadataFew: 'Waiting for metadata for {{count}} downloads.',
waitingForMetadataMany: 'Waiting for metadata for {{count}} downloads.',
removeUnsafeOne: 'Remove {{count}} unsafe URL before continuing.',
removeUnsafeFew: 'Remove {{count}} unsafe URLs before continuing.',
removeUnsafeMany: 'Remove {{count}} unsafe URLs before continuing.',
mediaMetadataUnavailableSummaryOne: 'Media metadata is unavailable for {{count}} item. Refresh metadata before adding.',
mediaMetadataUnavailableSummaryFew: 'Media metadata is unavailable for {{count}} items. Refresh metadata before adding.',
mediaMetadataUnavailableSummaryMany: 'Media metadata is unavailable for {{count}} items. Refresh metadata before adding.',
metadataUnavailableFallback: 'Metadata is unavailable. Downloads can still be added using fallback details.',
fallbackReadyOne: '{{ready}} download ready; {{failed}} will use fallback filename and unknown size.',
fallbackReadyFew: '{{ready}} downloads ready; {{failed}} will use fallback filename and unknown size.',
fallbackReadyMany: '{{ready}} downloads ready; {{failed}} will use fallback filename and unknown size.',
readyToAddOne: 'Ready to add {{count}} download.',
readyToAddFew: 'Ready to add {{count}} downloads.',
readyToAddMany: 'Ready to add {{count}} downloads.',
},
settings: {
tabs: {
downloads: 'Downloads',
lookAndFeel: 'Look and feel',
network: 'Network',
locations: 'Locations',
siteLogins: 'Site Logins',
power: 'Power',
engine: 'Engine',
integrations: 'Integrations',
about: 'About',
},
common: {
thisOs: 'this OS',
checking: 'Checking...',
ready: 'Ready',
errorMissing: 'Error / Missing',
hideTechnicalDetails: 'Hide technical details',
showTechnicalDetails: 'Show technical details',
binary: 'Binary',
expected: 'Expected',
error: 'Error',
tip: 'Tip',
stderr: 'stderr',
daemonProcessAlive: 'Daemon process alive',
rpcReady: 'RPC ready',
rpcPort: 'RPC port',
daemonStderr: 'daemon stderr',
packaging: 'Packaging: PyInstaller onedir (_internal required)',
internalDirectoryFound: '_internal directory found',
pythonRuntimeFound: 'Python runtime found',
externalOpenFailed: 'Could not open {{label}}: {{detail}}',
updateUnexpectedResponse: 'The update check returned an unexpected response.',
updateUntrustedReleaseUrl: 'The update check returned an untrusted release URL.',
updateFailed: 'Update check failed: {{detail}}',
},
downloads: {
defaultConnections: 'Default connections:',
defaultConnectionsDescription: 'New downloads; existing items keep their saved value',
parallelDownloads: 'Parallel downloads:',
parallelDownloadsDescription: 'Max simultaneous active files',
automaticRetries: 'Automatic retries:',
automaticRetriesDescription: 'If a connection fails',
systemNotification: 'Show system notification when download completes',
systemNotificationDescription: 'Uses your operating system notification settings',
completionChime: 'Play in-app completion chime',
completionChimeDescription: 'Optional Firelink sound, independent of system notifications',
clipboardLinks: 'Add clipboard links when Firelink becomes active',
clipboardLinksDescription: 'Opens supported copied links in the Add window. Off by default.',
},
lookAndFeel: {
language: 'Language',
languageDescription: 'Choose the Firelink interface language.',
languageSystem: 'System default',
languageEnglish: 'English',
languageChinese: 'Simplified Chinese',
languageHebrew: 'Hebrew',
languagePersian: 'Persian',
languageUkrainian: 'Ukrainian',
languageRussian: 'Russian',
appTheme: 'App Theme',
theme: 'Theme',
ariaLabel: 'App theme',
system: 'System',
light: 'Light',
dark: 'Dark',
dracula: 'Dracula',
nord: 'Nord',
systemAppearance: 'System follows the current {{platform}} light or dark appearance.',
display: 'Display',
fontSize: 'Font size',
fontSizeDescription: 'Scales the download list and compact controls.',
small: 'Small',
standard: 'Standard',
large: 'Large',
listDensity: 'List density',
listDensityDescription: 'Changes row height, spacing, and progress bar scale.',
compact: 'Compact',
comfortable: 'Comfortable',
relaxed: 'Relaxed',
osIntegration: 'OS Integration',
dockBadge: 'Show badge on Dock icon',
dockBadgeDescription: 'Displays active download count on Firelink Dock icon.',
menuBarIcon: 'Show menu bar icon',
statusIndicatorIcon: 'Show status indicator icon',
systemTrayIcon: 'Show system tray icon',
macosTrayDescription: 'Provides quick access from the macOS menu bar.',
windowsTrayDescription: 'Provides quick access from the Windows notification area.',
linuxTrayDescription: 'Provides quick access from the desktop tray or status area when available.',
defaultTrayDescription: 'Provides quick access from the OS tray area when available.',
},
network: {
proxy: 'Proxy',
mode: 'Mode',
modeDescription: 'Controls proxy use for new download requests.',
noProxy: 'No Proxy',
systemProxy: 'System Proxy',
customProxy: 'Custom Proxy',
proxyHost: 'Proxy host',
proxyHostDescription: 'Host name, IP address, or HTTP proxy URL.',
proxyHostPlaceholder: '127.0.0.1 or http://127.0.0.1',
proxyPort: 'Proxy port',
proxyPortDescription: 'Valid range is 1 to 65535.',
noProxyDescription: 'Downloads ignore configured proxies.',
systemProxyDescription: 'Downloads use the detected {{platform}} system proxy. Normal file downloads require an HTTP or HTTPS proxy endpoint; media downloads can use SOCKS.',
customProxyDescription: 'Downloads use the configured HTTP proxy endpoint for metadata and download engines.',
invalidCustomProxy: 'Enter a valid HTTP proxy host and port to enable the custom proxy.',
incompleteCustomProxy: 'Enter a proxy host and port to enable the custom proxy.',
checkingSystemProxy: 'Checking system proxy configuration…',
detectedSystemProxy: 'A system proxy was detected. Normal file downloads require an HTTP or HTTPS endpoint; media downloads can use SOCKS.',
noSystemProxy: 'No usable system proxy was detected. Downloads will use no proxy.',
systemProxyReadFailed: 'System proxy configuration could not be read. Choose No Proxy or try again when it is available.',
identity: 'Identity',
customUserAgent: 'Custom User-Agent',
userAgentDescription: 'Applied to metadata fetches and download engines.',
userAgentPlaceholder: 'Leave blank for Firelink default',
userAgentOverrides: 'Overrides the outbound User-Agent header. Leave blank for Firelink defaults.',
chromeWindows: 'Chrome (Windows)',
chromeMacos: 'Chrome (macOS)',
edgeWindows: 'Edge (Windows)',
firefoxWindows: 'Firefox (Windows)',
firefoxMacos: 'Firefox (macOS)',
safariMacos: 'Safari (macOS)',
windowsDesktop: 'Windows desktop',
macosDesktop: 'macOS desktop',
},
locations: {
baseDownloadFolder: 'Base Download Folder',
baseDownloadFolderDescription: 'Automatic category folders are created inside this folder.',
browse: 'Browse',
baseFolderCreateFailed: 'Base folder saved, but category folders could not be created: {{detail}}',
baseFolderUpdated: 'Base download folder updated',
askWhereToSave: 'Ask where to save when adding downloads',
rememberLastUsedDownloadDirectory: 'Remember the last used download directory',
rememberLastUsedDownloadDirectoryDescription: 'Use the last folder selected in the Add window for the next download until the app restarts.',
automaticCategorySubfolders: 'Automatically save files to category subfolders',
automaticCategorySubfoldersDescription: 'When off, automatic downloads use the base folder.',
categorySubfolders: 'Category Subfolders',
relativeToBase: 'Relative to the base folder',
disabled: 'Disabled',
categorySubfolder: '{{category}} subfolder',
customFolder: 'Custom folder',
useAutomatic: 'Use automatic',
resetDefaults: 'Reset Defaults',
resetToast: 'Reset category locations to default',
},
siteLogins: {
title: 'Site Credentials',
noSavedLogins: 'No saved logins.',
user: 'User: {{username}}',
deleteCredential: 'Delete credential',
deletedCredential: 'Deleted credential',
deleteFailed: 'Could not delete credential: {{detail}}',
addTitle: 'Add Site Credentials',
urlPattern: 'URL Pattern:',
urlPatternPlaceholder: 'e.g. *.example.com or example.com/downloads',
username: 'Username:',
password: 'Password:',
usernamePlaceholder: 'Username',
passwordPlaceholder: 'Password',
saving: 'Saving…',
addLogin: 'Add Login',
urlPatternRequired: 'URL pattern is required.',
urlPatternWhitespace: 'URL pattern cannot contain whitespace.',
usernameRequired: 'Username is required.',
passwordRequired: 'Password is required.',
accessBeforeSave: 'Grant credential-store access before saving a site login.',
saveFailed: 'Failed to save site credential securely.',
addedCredential: 'Added site credential',
},
power: {
title: 'Power Management',
preventSleep: 'Prevent system sleep while downloads are active',
preventSleepDescription: 'The display may still turn off. Firelink only keeps the device awake enough to complete active transfers.',
},
engine: {
title: 'Media Downloader & Engines',
description: 'Successful results are reused for this app session. Recheck runs real validation again.',
recheck: 'Recheck engines',
coreDownloader: 'Core Downloader (aria2)',
version: 'Version:',
status: 'Status:',
mediaExtractors: 'Media Extractors',
browserCookiesSource: 'Browser Cookies Source:',
cookieDescription: 'yt-dlp reads browser cookies to bypass video download limits or access restricted media. Firelink does not save browser cookies.',
none: 'None',
},
integrations: {
title: 'Connect Browser Extension',
description: 'Capture downloads directly from your browser in three easy steps.',
portablePairingEnabled: 'Portable Pairing Enabled',
pairingTokenPersisted: 'Pairing Token Persisted',
portablePersistedDescription: 'Your pairing token is stored with this portable Firelink folder and will persist when the folder is moved. Treat the folder as sensitive.',
systemPersistedDescription: 'Your pairing token is stored in the system credential store and will persist across restarts.',
portablePairingAvailable: 'Portable Pairing Available',
credentialStorageNeeded: 'Credential Storage Needed',
portableAvailableDescription: 'Your pairing token is stored with this portable Firelink folder and will persist across restarts. Enable it here to review the portable-storage warning.',
credentialStorageDescription: "Firelink needs access to this system's credential store to securely save your pairing token across app restarts. Currently, your extension will only stay connected for this session.",
reviewPortablePairing: 'Review Portable Pairing',
grantCredentialAccess: 'Grant Credential Access',
copyToken: 'Copy Token',
tokenDescription: 'This secure token authorizes your browser extension.',
tokenCopied: 'Token copied to clipboard!',
tokenCopyFailed: 'Could not copy token: {{detail}}',
pairingTokenRegenerated: 'Pairing token regenerated',
regenerateFailed: 'Could not regenerate pairing token: {{detail}}',
getExtension: 'Get Extension',
extensionDescription: 'Install Firelink Companion for Firefox or Chromium browsers.',
firefoxAddons: 'Firefox Add-ons',
chromiumZip: 'Chromium ZIP',
pasteConnect: 'Paste & Connect',
pasteConnectDescription: "Click the Firelink icon in your browser's toolbar and paste the copied token.",
extensionServerStatus: 'Extension Server Status:',
listening: '● Listening on 127.0.0.1:{{port}}',
unavailable: '● Server unavailable',
},
about: {
firelinkIcon: 'Firelink Icon',
unknown: 'Unknown',
version: 'Version {{version}}',
description: 'Cross-platform download manager for macOS, Windows, and Linux.',
sourceCode: 'Source Code',
source: 'Source',
upToDate: 'Firelink {{version}} is up to date.',
updateAvailable: 'Firelink {{version}} is available.',
viewRelease: 'View release',
updates: 'Updates',
checkForUpdates: 'Check for Updates',
updateDescription: 'Firelink checks GitHub Releases for new versions.',
checking: 'Checking...',
checkNow: 'Check Now',
automaticUpdates: 'Automatically check for updates',
createdBy: 'Created by NimBold',
mitLicense: 'MIT License',
credits: 'Credits: aria2, yt-dlp, FFmpeg, and Deno.',
copyright: 'Copyright © 2026 NimBold. All rights reserved.',
},
},
status: {
ready: 'Ready',
active: '{{count}} active',
queued: '{{count}} queued',
done: '{{count}} done',
},
} as const;
export default common;
+728
View File
@@ -0,0 +1,728 @@
const fa = {
navigation: {
library: 'کتابخانه',
filters: {
all: 'همه',
active: 'فعال',
completed: 'تکمیل‌شده',
unfinished: 'ناتمام',
},
categories: {
musics: 'موسیقی‌ها',
movies: 'فیلم‌ها',
compressed: 'فشرده',
documents: 'اسناد',
pictures: 'تصاویر',
applications: 'برنامه‌ها',
other: 'سایر',
},
folders: 'پوشه‌ها',
queues: 'صف‌ها',
tools: 'ابزارها',
scheduler: 'زمان‌بند',
speedLimiter: 'محدودکننده سرعت',
logs: 'گزارش‌ها',
settings: 'تنظیمات',
},
actions: {
hideSidebar: 'پنهان کردن نوار کناری',
showSidebar: 'نمایش نوار کناری',
resizeSidebar: 'تغییر اندازه نوار کناری',
cancel: 'لغو',
continue: 'ادامه',
dismissNotification: 'رد کردن اعلان',
queueName: 'نام صف',
addNewQueue: 'افزودن صف جدید',
startQueue: 'شروع صف',
pauseQueue: 'توقف صف',
renameQueue: 'تغییر نام صف',
deleteQueue: 'حذف صف',
},
dialogs: {
errorBoundary: {
title: 'مشکلی پیش آمد.',
description: 'یک خطای بحرانی در درخت اجزای React رخ داده است. جزئیات خطای زیر می‌تواند به شناسایی علت اصلی کمک کند.',
reload: 'بارگذاری مجدد برنامه',
},
duplicateDownloads: {
title: 'شناسایی دانلودهای تکراری',
description: 'برخی از دانلودهایی که اضافه می‌کنید از قبل در صف یا روی دیسک وجود دارند. لطفاً نحوه حل این تداخل‌ها را انتخاب کنید.',
rename: 'تغییر نام',
replace: 'جایگزینی',
skip: 'رد شدن',
},
removeDownload: {
title: 'حذف دانلود',
confirmationSingle: 'آیا مطمئن هستید که می‌خواهید این مورد را از لیست حذف کنید؟ همچنین می‌توانید فایل اصلی را از هارد دیسک خود حذف کنید.',
confirmationMultiple: 'آیا مطمئن هستید که می‌خواهید این {{count}} مورد را از لیست حذف کنید؟ همچنین می‌توانید فایل‌های اصلی را از هارد دیسک خود حذف کنید.',
errorSummary: '{{succeeded}} مورد حذف شد، {{failed}} مورد ناموفق: {{detail}}',
remove: 'حذف',
deleteFile: 'حذف فایل',
},
},
window: {
controls: 'کنترل‌های پنجره',
close: 'بستن',
minimize: 'کوچک کردن',
maximize: 'بزرگ کردن',
},
downloads: {
actions: {
moveUp: 'انتقال به بالا',
moveDown: 'انتقال به پایین',
pause: 'توقف',
start: 'شروع',
resume: 'ادامه',
options: 'گزینه‌ها',
},
size: {
downloadedOf: '{{downloaded}} دانلود شده از {{total}} {{unit}}',
downloadedOfApproximate: '{{downloaded}} دانلود شده از حدود {{total}} {{unit}}',
},
status: {
ready: 'آماده',
staged: 'در صف',
queued: 'در صف',
downloading: 'در حال دانلود',
processing: 'در حال پردازش',
paused: 'متوقف شده',
completed: 'تکمیل شده',
failed: 'ناموفق',
retrying: 'در حال تلاش مجدد',
},
values: {
processing: 'در حال پردازش...',
muxing: 'ترکیب...',
},
},
speedLimiter: {
title: 'محدودکننده سرعت',
unlimited: 'نامحدود',
saveLimit: 'ذخیره محدودیت',
globalSpeedLimit: 'محدودیت سرعت کلی',
description: 'در مورد انتقال‌های جدید و فعال aria2 و دانلودهای رسانه جدید yt-dlp اعمال می‌شود. محدودیت‌های خاص هر دانلود برای آن انتقال‌ها به قوت خود باقی می‌ماند.',
quickPresets: 'پیش‌تنظیم‌های سریع',
removePreset: 'حذف پیش‌تنظیم {{value}} {{unit}}',
customPresetIn: 'پیش‌تنظیم سفارشی در {{unit}}',
addQuickPreset: 'افزودن پیش‌تنظیم سریع',
globalLimitSaved: 'محدودیت کلی در {{value}} {{unit}} ذخیره شد',
globalLimitDisabled: 'محدودیت سرعت کلی غیرفعال شد',
saveFailed: 'محدودیت سرعت کلی ذخیره نشد: {{detail}}',
presetAlreadyExists: '{{value}} {{unit}} از قبل در پیش‌تنظیم‌های سریع وجود دارد',
presetAdded: 'پیش‌تنظیم سریع {{value}} {{unit}} اضافه شد',
presetRemoved: 'پیش‌تنظیم سریع {{value}} {{unit}} حذف شد',
},
logs: {
title: 'گزارش‌ها',
entries: '({{count}} ورودی)',
collecting: 'در حال جمع‌آوری',
off: 'خاموش',
allLevels: 'همه سطوح',
levels: {
error: 'خطا',
warn: 'هشدار',
info: 'اطلاعات',
debug: 'دیباگ',
trace: 'ردیابی',
},
pauseLogging: 'توقف گزارش‌گیری تشخیصی',
enableLogging: 'فعال‌سازی گزارش‌گیری تشخیصی',
clearDisplayed: 'پاک کردن گزارش‌های نمایش داده شده',
export: 'خروجی گزارش‌ها',
exportButton: 'خروجی گزارش‌ها',
logFiles: 'فایل‌های گزارش',
localDiagnostics: 'تشخیص‌های محلی:',
diagnosticsDescription: 'جمع‌آوری تشخیص‌ها انتخابی، محدود و محلی است. اسرار رایج، درخواست‌های URL و مسیرهای پوشه خانگی هنگام نمایش و خروجی پنهان می‌شوند. هیچ چیز به طور خودکار بارگذاری نمی‌شود.',
noEntries: 'هنوز هیچ گزارش ذخیره‌شده‌ای در دسترس نیست.',
disabledDescription: 'گزارش‌گیری تشخیصی خاموش است. گزارش‌های پشتیبانی موجود در صورت در دسترس بودن در اینجا ظاهر می‌شوند.',
copy: 'کپی',
copied: 'در کلیپ‌بورد کپی شد',
copyFailed: 'کپی در کلیپ‌بورد ناموفق بود',
exported: 'گزارش‌های پشتیبانی صادر شدند',
exportFailed: 'گزارش‌ها صادر نشدند: {{detail}}',
cleared: 'گزارش‌ها پاک شدند',
clearFailed: 'گزارش‌ها پاک نشدند: {{detail}}',
enabled: 'گزارش‌گیری تشخیصی فعال شد',
disabled: 'گزارش‌گیری تشخیصی غیرفعال شد',
updateFailed: 'گزارش‌گیری تشخیصی به‌روزرسانی نشد: {{detail}}',
},
scheduler: {
title: 'زمان‌بند',
running: 'در حال اجرا',
disabled: 'زمان‌بند غیرفعال است',
noScheduledDay: 'هیچ روز زمان‌بندی‌شده‌ای انتخاب نشده است',
unsavedChanges: 'تغییرات ذخیره‌نشده',
runNow: 'اجرای فوری',
pause: 'توقف',
saveSettings: 'ذخیره تنظیمات',
timing: 'زمان‌بندی',
startTime: 'زمان شروع',
stopTime: 'زمان پایان',
timingDescription: 'اگر Firelink در زمان شروع در حالت خواب باشد، صف‌های انتخاب‌شده را هنگام بازگشت در ادامه آن روز شروع می‌کند، مگر اینکه زمان پایان گذشته باشد.',
runEveryDay: 'اجرای هر روز',
queuesToSchedule: 'صف‌ها برای زمان‌بندی',
defaultQueue: 'صف پیش‌فرض',
afterCompletion: 'پس از تکمیل',
afterCompletionDescription: 'انتخاب کنید پس از پایان دانلودهای شروع‌شده توسط زمان‌بند، چه اتفاقی بیفتد.',
actionWarning: 'این اقدام می‌تواند سایر کارهای کامپیوتر را مختل کند. Firelink بلافاصله پس از اتمام صف زمان‌بندی‌شده، آن را فراخوانی می‌کند.',
systemPermissions: 'مجوزهای سیستم',
macPermissionDescription: 'خواب، راه‌اندازی مجدد و خاموش کردن به مجوز macOS Automation برای رویدادهای سیستم نیاز دارد.',
permissionGranted: 'مجوز اتوماسیون داده شد',
permissionChecking: 'در حال بررسی مجوز اتوماسیون...',
permissionMissing: 'مجوز اتوماسیون وجود ندارد',
revokePermission: 'لغو مجوز',
grantPermission: 'دادن مجوز',
permissionAvailable: 'مجوز اتوماسیون در دسترس است.',
permissionMissingDetails: 'مجوز اتوماسیون وجود ندارد. Firelink را در زیر Automation برای System Events در System Settings فعال کنید.',
revokePermissionDetails: 'macOS اجازه نمی‌دهد Firelink مجوز اتوماسیون را مستقیماً لغو کند. دسترسی System Events را در System Settings لغو کنید و سپس به Firelink برگردید.',
requestingPermission: 'در حال درخواست مجوز اتوماسیون...',
enablePermissionDetails: 'Firelink را در زیر Automation برای System Events در System Settings فعال کنید و سپس به Firelink برگردید.',
systemActions: 'اقدامات سیستم',
windowsActionsDescription: 'خواب، راه‌اندازی مجدد و خاموش کردن از امتیازات سیستم Windows استفاده می‌کنند. Firelink هنگام اجرا هر اقدام رد شده‌ای را گزارش می‌دهد؛ هیچ مجوز دائمی از قبل دریافت نمی‌شود.',
linuxActionsDescription: 'خواب، راه‌اندازی مجدد و خاموش کردن از دسکتاپ و سیاست سیستم Linux شما استفاده می‌کنند. Firelink هنگام اجرا هر اقدام رد شده‌ای را گزارش می‌دهد؛ هیچ مجوز دائمی از قبل دریافت نمی‌شود.',
validationDay: 'حداقل یک روز برای زمان‌بند انتخاب کنید',
validationQueue: 'حداقل یک صف برای زمان‌بند انتخاب کنید',
validationStopTime: 'زمان پایان باید بعد از زمان شروع باشد',
saved: 'تنظیمات زمان‌بند ذخیره شد',
trackingOne: 'در حال ردیابی ۱ دانلود زمان‌بندی‌شده',
trackingMany: 'در حال ردیابی {{count}} دانلود زمان‌بندی‌شده',
noStartableDownloads: 'هیچ دانلودی در صف‌های انتخاب‌شده قابل شروع نیست',
pausedOne: '۱ دانلود فعال متوقف شد',
pausedMany: '{{count}} دانلود فعال متوقف شد',
noActiveDownloads: 'هیچ دانلود فعالی وجود ندارد',
days: {
su: 'ی',
mo: 'د',
tu: 'س',
we: 'چ',
th: 'پ',
fr: 'ج',
sa: 'ش',
},
postActions: {
none: 'هیچ کاری انجام نده',
sleep: 'خواب',
restart: 'راه‌اندازی مجدد',
shutdown: 'خاموش کردن',
},
},
properties: {
progress: 'پیشرفت',
size: 'اندازه',
speed: 'سرعت',
eta: 'زمان تخمینی',
connections: 'اتصالات',
speedCap: 'سقف سرعت',
category: 'دسته',
lastTry: 'آخرین تلاش',
dateAdded: 'تاریخ اضافه شدن',
destination: 'مقصد',
lastError: 'آخرین خطا',
saved: ' (ذخیره شده)',
defaultValue: ' (پیش‌فرض)',
savedTooltip: 'برای این دانلود ذخیره شد؛ تغییرات تنظیمات در دانلودهای جدید اعمال می‌شود.',
defaultTooltip: 'استفاده از پیش‌فرض کنونی برای دانلودهای جدید.',
identityReadOnly: 'هویت فایل فقط-خواندنی است. تنظیمات انتقال برای دانلود مجدد ذخیره می‌شود.',
transferSettings: 'تنظیمات انتقال را می‌توان پس از توقف تغییر داد. انتقال‌های کنونی گزینه‌های فعلی خود را حفظ می‌کنند.',
download: 'دانلود',
url: 'URL',
fileName: 'نام فایل',
saveLocation: 'محل ذخیره',
select: 'انتخاب',
perFile: 'در هر فایل',
useCurrentDefault: 'استفاده از پیش‌فرض کنونی ({{count}})',
savedPerDownload: 'برای هر دانلود ذخیره شد. پیش‌فرض تنظیمات در دانلودهای جدید اعمال می‌شود.',
limit: 'محدودیت',
siteLogin: 'ورود به سایت',
siteLoginRedownload: 'ورود به سایت برای دانلود مجدد',
matchingSiteLogin: 'ورود به سایت منطبق',
customCredentials: 'اعتبارنامه‌های سفارشی',
noLogin: 'بدون ورود',
useSavedLogin: 'در صورت وجود از ورود ذخیره‌شده استفاده خواهد کرد.',
advancedTransfer: 'انتقال پیشرفته',
advancedTransferRedownload: 'انتقال پیشرفته برای دانلود مجدد',
checksum: 'چک‌سام',
verify: 'تأیید',
algorithm: 'الگوریتم',
digest: 'هش',
expectedDigest: 'هش موردانتظار',
cookies: 'کوکی‌ها',
headers: 'هدرها',
mirrors: 'آینه‌ها',
username: 'نام کاربری',
password: 'رمز عبور',
enterValidUrl: 'یک URL معتبر وارد کنید.',
fileNameEmpty: 'نام فایل نمی‌تواند خالی باشد.',
cancel: 'لغو',
save: 'ذخیره',
},
keychain: {
title: 'نیاز به دسترسی ذخیره‌سازی اعتبارنامه',
stores: {
portable: 'پوشه داده‌های قابل‌حمل Firelink',
windows: 'Windows Credential Manager',
linux: 'مخزن اعتبارنامه‌های Linux شما',
macos: 'macOS Keychain',
system: "مخزن اعتبارنامه‌های این سیستم",
siteCredentials: "مخزن اعتبارنامه‌های سیستم",
},
grantLabelPortable: 'ادامه',
grantLabelMacos: 'اعطای دسترسی',
grantLabelDefault: 'فعال‌سازی ذخیره‌سازی امن',
description: 'Firelink از افزونه مرورگر برای ثبت دانلودها استفاده می‌کند. برای جفت ماندن افزونه پس از راه‌اندازی مجدد، Firelink توکن جفت‌سازی خود را در {{pairingStore}} ذخیره می‌کند. اعتبارنامه‌های سایت دلخواه در {{siteCredentialStore}} ذخیره می‌شوند.',
portableExplanation: 'توکن جفت‌سازی با این پوشه قابل حمل است. اعتبارنامه‌های سایت در مخزن اعتبارنامه‌های سیستم باقی می‌مانند؛ ممکن است پس از اعطای دسترسی، یک اعلان سیستم ظاهر شود.',
macosExplanation: 'macOS ممکن است پس از اعطای دسترسی، یک اعلان Keychain را نشان دهد.',
defaultExplanation: 'این کار معمولاً بی‌صدا انجام می‌شود. اگر سرویس اعتبارنامه در دسترس نباشد، Firelink در اینجا خطا را نشان می‌دهد و افزونه فقط برای این جلسه جفت باقی می‌ماند.',
note: 'توجه:',
portableNote: 'حالت قابل حمل فقط توکن جفت‌سازی را در این پوشه ذخیره می‌کند. رمزهای عبور سایت یا اعتبارنامه‌های مرورگر را کپی نمی‌کند.',
defaultNote: 'Firelink فقط ورودی اعتبارنامه اختصاصی خود را می‌نویسد. نمی‌تواند به سایر رمزهای عبور ذخیره‌شده یا موارد اعتبارنامه در سیستم شما دسترسی پیدا کند.',
hint: 'راهنمایی:',
portableHint: 'توکن جفت‌سازی قابل حمل از قبل با این پوشه ذخیره شده است؛ می‌توانید آن را در اینجا فعال کنید یا بعداً را انتخاب کنید.',
defaultHint: 'اگر بعداً را انتخاب کنید، افزونه فقط برای این جلسه کار خواهد کرد.',
enableFromSettings: 'در هر زمان می‌توانید ذخیره‌سازی را از تنظیمات > یکپارچه‌سازی فعال کنید.',
later: 'بعداً',
enabling: 'در حال فعال‌سازی...',
timeout: 'درخواست ذخیره‌سازی اعتبارنامه مهلت زمانی‌اش تمام شد. می‌توانید بعداً را انتخاب کنید و دوباره امتحان کنید.',
unavailable: '{{store}} در دسترس نیست.',
accessRequired: 'پیش از ایجاد دوباره توکن جفت‌سازی، به مخزن اعتبارنامه‌ها دسترسی بدهید.',
storeUnavailable: 'دسترسی به مخزن اعتبارنامه در دسترس نیست.',
},
app: {
settingsSaveFailed: 'تنظیمات ذخیره نشدند. مجوزهای ذخیره‌سازی را بررسی کنید و دوباره امتحان کنید.',
systemActionCountdown: '{{action}} در ۱۰ ثانیه.',
systemActionCancelled: 'اقدام سیستم لغو شد زیرا دانلود دیگری فعال یا در صف است.',
systemActionFailed: 'اقدام سیستم زمان‌بندی‌شده ناموفق بود: {{detail}}',
downloadCompleteTitle: 'دانلود تکمیل شد',
downloadCompleteBody: 'دانلود {{fileName}} به پایان رسید.',
unknownFile: 'یک فایل',
downloadFailedTitle: 'دانلود ناموفق بود',
downloadFailedBody: 'دانلود {{fileName}} با شکست مواجه شد.',
initializeDownloadsFailed: 'راه‌اندازی دانلودهای ذخیره‌شده ناموفق بود: {{detail}}',
extensionDisconnected: 'اتصال افزونه مرورگر قطع شد زیرا توکن جفت‌سازی آن تغییر کرده است.',
copyToken: 'کپی توکن',
integrations: 'یکپارچه‌سازی',
copyTokenFailed: 'کپی توکن جفت‌سازی ناموفق بود: {{detail}}',
credentialPersistenceFailed: 'تداوم اعتبارنامه امن در دسترس نیست. جفت‌سازی مرورگر فقط برای این جلسه کار می‌کند: {{detail}}',
resumeDownloadsFailed: 'ادامه دانلودهای ذخیره‌شده ناموفق بود: {{detail}}',
updateAvailable: 'Firelink {{version}} در دسترس است.',
viewRelease: 'مشاهده انتشار',
sleepPreventionFailed: 'جلوگیری از خواب سیستم به‌روزرسانی نشد: {{detail}}',
schedulerNoQueues: 'زمان‌بند هیچ صف معتبری انتخاب نکرده است. تنظیمات زمان‌بند را به‌روزرسانی کنید.',
schedulerPauseOneFailed: 'زمان‌بند نتوانست ۱ دانلود را متوقف کند.',
schedulerPauseManyFailed: 'زمان‌بند نتوانست {{count}} دانلود را متوقف کند.',
scheduledIncomplete: 'همه دانلودهای زمان‌بندی‌شده تکمیل نشدند. اقدام سیستم پس از صف رد شد.',
scheduledActionSkippedActive: 'اقدام سیستم زمان‌بندی‌شده رد شد زیرا دانلود دیگری فعال یا در صف است.',
notificationsDisabled: 'اعلان‌های سیستم برای Firelink غیرفعال شده‌اند.',
notificationsFailed: 'پیکربندی اعلان‌ها ناموفق بود: {{detail}}',
},
sidebar: {
deleteQueueConfirm: 'حذف صف "{{name}}"؟ دانلودهای ناتمام آن به صف اصلی منتقل خواهند شد.',
deleteQueueFailed: 'حذف صف ناموفق بود: {{detail}}',
queueNameEmpty: 'نام صف نمی‌تواند خالی باشد',
queueNameExists: 'یک صف با این نام از قبل وجود دارد',
startQueueFailed: 'شروع صف ناموفق بود: {{detail}}',
pauseQueueFailed: 'توقف صف ناموفق بود: {{detail}}',
},
downloadTable: {
unknownQueue: 'صف نامشخص',
allDownloads: 'همه دانلودها',
active: 'فعال',
completed: 'تکمیل‌شده',
unfinished: 'ناتمام',
headers: {
fileName: 'نام فایل',
size: 'اندازه',
status: 'وضعیت',
speed: 'سرعت',
eta: 'زمان تخمینی',
dateAdded: 'تاریخ اضافه شدن',
},
addDownload: 'افزودن دانلود',
resumeAll: 'ادامه همه',
pauseAll: 'توقف همه',
queueEmpty: 'صف خالی است',
noCompletedDownloads: 'بدون دانلود تکمیل‌شده',
noDownloads: 'بدون دانلود',
queueEmptyDescription: 'دانلودها را از منوی یک مورد یا پنجره افزودن به این صف اضافه کنید.',
completedDescription: 'دانلودهای تکمیل‌شده در اینجا ظاهر می‌شوند.',
clickToAdd: 'برای افزودن دانلودها، روی دکمه',
addButtonOr: 'یا',
toAddDownloads: 'کلیک کنید',
nonResumableOne: '۱ دانلود از ادامه پشتیبانی نمی‌کند. اگر آن را متوقف کنید، بعداً باید دوباره شروع کنید. آیا مطمئن هستید که می‌خواهید متوقف کنید؟',
nonResumableMany: '{{count}} دانلود از ادامه پشتیبانی نمی‌کنند. اگر آنها را متوقف کنید، بعداً باید دوباره شروع کنید. آیا مطمئن هستید که می‌خواهید متوقف کنید؟',
interactionError: '{{message}}: {{detail}}',
openFileFailed: 'فایل دانلود شده باز نشد',
revealFileFailed: 'نمایش دانلود در Finder ناموفق بود',
pauseFailed: 'توقف دانلود ناموفق بود',
resumeFailed: 'ادامه {{fileName}} ناموفق بود',
moveManyFailed: 'انتقال دانلودها به صف ناموفق بود',
moveOneFailed: 'انتقال دانلود به صف ناموفق بود',
copyAddressesFailed: 'آدرس‌ها کپی نشدند',
copyAddressFailed: 'آدرس کپی نشد',
copyPathFailed: 'مسیر فایل کپی نشد',
missingFileName: 'نام فایل وجود ندارد',
redownloadFailed: 'دانلود مجدد ناموفق بود',
startResume: 'شروع/ادامه',
addToQueue: 'افزودن به صف',
copyAddress: 'کپی آدرس',
remove: 'حذف',
open: 'باز کردن',
showInFolder: 'نمایش در پوشه',
pause: 'توقف',
start: 'شروع',
resume: 'ادامه',
redownload: 'دانلود مجدد',
copyFilePath: 'کپی مسیر فایل',
properties: 'خصوصیات',
backendRejectedStart: 'درخواست شروع/ادامه توسط موتور دانلود رد شد.',
transferActive: 'نمی‌توانید خصوصیات را در حالی که انتقال فعال است تغییر دهید. ابتدا آن را متوقف کنید.',
redownloadNotFound: 'دانلود مجدد ناموفق: دانلود پیدا نشد.',
redownloadActive: 'نمی‌توانید یک دانلود {{status}} را دوباره دانلود کنید. ابتدا آن را متوقف کنید یا منتظر بمانید تا تمام شود.',
originalUrlMissing: 'دانلود مجدد ناموفق: URL اصلی وجود ندارد.',
pauseBeforeMove: 'قبل از انتقال {{file}} به صف دیگر، آن را متوقف کنید.',
},
addDownloads: {
unknownSize: 'اندازه نامشخص',
unknown: 'نامشخص',
discardSetup: 'حذف این راه‌اندازی دانلود؟',
speedInvalid: 'محدودیت سرعت باید بیشتر از صفر باشد',
duplicateResolveFailed: 'دانلودهای تکراری برطرف نشدند: {{detail}}',
addedWithFailures: '{{added}} اضافه شد، {{failed}} ناموفق بود. {{detail}}',
addedOne: '۱ دانلود اضافه شد',
addedMany: '{{count}} دانلود اضافه شد',
urlAlreadyQueued: 'URL در حال حاضر در صف است',
destinationConflict: 'یک دانلود انتخاب‌شده دیگر از این مقصد استفاده می‌کند',
existingDownloadDestination: 'دانلود Firelink موجود از این مقصد استفاده می‌کند',
fileExistsOnDisk: 'فایل در دیسک وجود دارد؛ برای جلوگیری از حذف داده‌های نامرتبط، تغییر نام دهید یا رد شوید',
backendRejectedStart: 'شروع دانلود توسط موتور دانلود رد شد.',
playlistNoEntries: 'لیست پخش شامل هیچ ورودی قابل دانلودی نیست',
invalidMediaMetadata: 'متادیتا رسانه نامعتبر است یا هیچ فرمتی یافت نشد',
noAvailableName: 'یک نام در دسترس برای {{file}} پیدا نشد.',
pauseBeforeReplace: 'قبل از جایگزینی {{file}}، آن را متوقف کنید.',
cannotReplace: 'نمی‌توان {{file}} را جایگزین کرد: فایل متعلق به یک دانلود Firelink نیست.',
downloadLinks: 'پیوندهای دانلود',
pastePlaceholder: 'URLهای HTTP، HTTPS، FTP یا SFTP را در اینجا جای‌گذاری کنید...\n\nبرای دانلود رسانه، پیوندهایی از YouTube، X، TikTok، Instagram، Reddit و غیره جای‌گذاری کنید.',
playlistSummary: 'لیست پخش "{{title}}": {{loaded}}{{total}} ورودی بارگیری شد{{truncated}}{{skipped}}',
safeEntryLimit: ' (به حد نصاب ایمن ورودی‌ها رسیدیم)',
selectedSummary: '{{ready}} انتخاب‌شده آماده، {{fallback}} بازگشتی، {{mediaRetry}} تلاش مجدد رسانه، {{blocked}} مسدودشده',
clearSelection: 'پاک کردن انتخاب',
selectAll: 'انتخاب همه',
refreshMetadata: 'تازه‌سازی متادیتا',
files: 'فایل‌ها',
required: 'الزامی',
free: 'آزاد',
preview: 'پیش‌نمایش',
file: 'فایل',
size: 'اندازه',
status: 'وضعیت',
downloadPreview: 'پیش‌نمایش دانلود',
noLinks: 'هنوز هیچ پیوندی اضافه نشده است.',
selectItem: 'انتخاب {{file}}',
fetchingPlaylist: 'در حال دریافت لیست پخش...',
fetching: 'در حال دریافت...',
unsafeUrl: 'URL ناامن',
playlistFailed: 'لیست پخش ناموفق بود',
metadataFailed: 'متادیتا ناموفق بود',
fallback: 'بازگشتی',
invalid: 'نامعتبر',
ready: 'آماده',
mediaFormat: 'فرمت رسانه',
playlistItem: 'مورد لیست پخش {{index}}',
fetchingMediaStreams: 'در حال دریافت جریان‌های رسانه...',
availableStreams: 'جریان‌های موجود',
availableMediaStreams: 'جریان‌های رسانه موجود',
metadataUnavailable: 'متادیتا در دسترس نیست. پیش از افزودن این رسانه، متادیتا را تازه‌سازی کنید.',
saveLocation: 'محل ذخیره',
browse: 'مرور',
categoryFolders: 'فایل‌ها در پوشه‌های دسته سازماندهی خواهند شد.',
sharedFolder: 'تمام دانلودهای انتخاب‌شده از این پوشه استفاده خواهند کرد.',
transferSettings: 'تنظیمات انتقال',
connectionsPerFile: 'اتصالات در هر فایل',
connectionsPerFileAria: 'اتصالات در هر فایل',
limitSpeedPerFile: 'محدودیت سرعت در هر فایل',
speedLimitPerFileAria: 'محدودیت سرعت در هر فایل',
authorization: 'احراز هویت',
useAuthorization: 'استفاده از احراز هویت',
username: 'نام کاربری',
password: 'رمز عبور',
advancedTransfer: 'انتقال پیشرفته',
verifyChecksum: 'تأیید چک‌سام',
checksumAlgorithm: 'الگوریتم چک‌سام',
expectedDigest: 'هش موردانتظار',
headers: 'هدرها',
requestHeaders: 'هدرهای درخواست',
cookies: 'کوکی‌ها',
cookiePlaceholder: 'name=value; other=value',
mirrors: 'آینه‌ها',
cancel: 'لغو',
startDownloads: 'شروع دانلودها',
addToQueue: 'افزودن به صف',
pasteOneOrMore: 'یک یا چند پیوند را جای‌گذاری کنید.',
selectAtLeastOne: 'حداقل یک دانلود را انتخاب کنید.',
correctInvalidOne: 'قبل از ادامه، {{count}} URL نامعتبر را اصلاح یا حذف کنید.',
correctInvalidFew: 'قبل از ادامه، {{count}} URL نامعتبر را اصلاح یا حذف کنید.',
correctInvalidMany: 'قبل از ادامه، {{count}} URL نامعتبر را اصلاح یا حذف کنید.',
waitingForMetadataOne: 'در انتظار متادیتای {{count}} دانلود.',
waitingForMetadataFew: 'در انتظار متادیتای {{count}} دانلود.',
waitingForMetadataMany: 'در انتظار متادیتای {{count}} دانلود.',
removeUnsafeOne: 'قبل از ادامه، {{count}} URL ناامن را حذف کنید.',
removeUnsafeFew: 'قبل از ادامه، {{count}} URL ناامن را حذف کنید.',
removeUnsafeMany: 'قبل از ادامه، {{count}} URL ناامن را حذف کنید.',
mediaMetadataUnavailableSummaryOne: 'متادیتای رسانه برای {{count}} مورد در دسترس نیست. پیش از افزودن، متادیتا را تازه‌سازی کنید.',
mediaMetadataUnavailableSummaryFew: 'متادیتای رسانه برای {{count}} مورد در دسترس نیست. پیش از افزودن، متادیتا را تازه‌سازی کنید.',
mediaMetadataUnavailableSummaryMany: 'متادیتای رسانه برای {{count}} مورد در دسترس نیست. پیش از افزودن، متادیتا را تازه‌سازی کنید.',
metadataUnavailableFallback: 'متادیتا در دسترس نیست. دانلودها همچنان می‌توانند با استفاده از جزئیات بازگشتی اضافه شوند.',
fallbackReadyOne: '{{ready}} دانلود آماده؛ برای {{failed}} از نام فایل بازگشتی و اندازه نامشخص استفاده خواهد شد.',
fallbackReadyFew: '{{ready}} دانلود آماده؛ برای {{failed}} از نام فایل بازگشتی و اندازه نامشخص استفاده خواهد شد.',
fallbackReadyMany: '{{ready}} دانلود آماده؛ برای {{failed}} از نام فایل بازگشتی و اندازه نامشخص استفاده خواهد شد.',
readyToAddOne: 'آماده برای افزودن {{count}} دانلود.',
readyToAddFew: 'آماده برای افزودن {{count}} دانلود.',
readyToAddMany: 'آماده برای افزودن {{count}} دانلود.',
},
settings: {
tabs: {
downloads: 'دانلودها',
lookAndFeel: 'ظاهر',
network: 'شبکه',
locations: 'مکان‌ها',
siteLogins: 'ورود به سایت‌ها',
power: 'انرژی',
engine: 'موتور',
integrations: 'یکپارچه‌سازی‌ها',
about: 'درباره',
},
common: {
thisOs: 'این سیستم‌عامل',
checking: 'در حال بررسی...',
ready: 'آماده',
errorMissing: 'خطا / ناموجود',
hideTechnicalDetails: 'پنهان کردن جزئیات فنی',
showTechnicalDetails: 'نمایش جزئیات فنی',
binary: 'باینری',
expected: 'موردانتظار',
error: 'خطا',
tip: 'نکته',
stderr: 'stderr',
daemonProcessAlive: 'فرآیند دیمون زنده است',
rpcReady: 'RPC آماده است',
rpcPort: 'پورت RPC',
daemonStderr: 'دیمون stderr',
packaging: 'بسته‌بندی: PyInstaller onedir (نیاز به _internal دارد)',
internalDirectoryFound: 'پوشه _internal پیدا شد',
pythonRuntimeFound: 'زمان‌اجرای Python پیدا شد',
externalOpenFailed: 'باز نشد {{label}}: {{detail}}',
updateUnexpectedResponse: 'بررسی به‌روزرسانی با یک پاسخ غیرمنتظره مواجه شد.',
updateUntrustedReleaseUrl: 'بررسی به‌روزرسانی با یک URL انتشار نامعتبر مواجه شد.',
updateFailed: 'بررسی به‌روزرسانی ناموفق بود: {{detail}}',
},
downloads: {
defaultConnections: 'اتصالات پیش‌فرض:',
defaultConnectionsDescription: 'دانلودهای جدید؛ موارد موجود مقادیر ذخیره‌شده خود را حفظ می‌کنند',
parallelDownloads: 'دانلودهای موازی:',
parallelDownloadsDescription: 'حداکثر فایل‌های فعال همزمان',
automaticRetries: 'تلاش‌های مجدد خودکار:',
automaticRetriesDescription: 'اگر اتصالی ناموفق باشد',
systemNotification: 'نمایش اعلان سیستم هنگام تکمیل دانلود',
systemNotificationDescription: 'از تنظیمات اعلان سیستم‌عامل شما استفاده می‌کند',
completionChime: 'پخش صدای تکمیل درون برنامه‌ای',
completionChimeDescription: 'صدای اختیاری Firelink، مستقل از اعلان‌های سیستم',
clipboardLinks: 'افزودن پیوندهای کلیپ‌بورد وقتی Firelink فعال می‌شود',
clipboardLinksDescription: 'پیوندهای کپی‌شده پشتیبانی‌شده را در پنجره افزودن باز می‌کند. به‌طور پیش‌فرض خاموش است.',
},
lookAndFeel: {
language: 'زبان',
languageDescription: 'زبان رابط Firelink را انتخاب کنید.',
languageSystem: 'زبان سیستم',
languageEnglish: 'انگلیسی',
languageChinese: 'چینی ساده‌شده',
languageHebrew: 'عبری',
languagePersian: 'فارسی',
languageUkrainian: 'اوکراینی',
languageRussian: 'روسی',
appTheme: 'پوسته برنامه',
theme: 'پوسته',
ariaLabel: 'پوسته برنامه',
system: 'سیستم',
light: 'روشن',
dark: 'تاریک',
dracula: 'Dracula',
nord: 'Nord',
systemAppearance: 'سیستم از ظاهر روشن یا تاریک فعلی {{platform}} پیروی می‌کند.',
display: 'نمایش',
fontSize: 'اندازه قلم',
fontSizeDescription: 'لیست دانلودها و کنترل‌های فشرده را تغییر مقیاس می‌دهد.',
small: 'کوچک',
standard: 'استاندارد',
large: 'بزرگ',
listDensity: 'تراکم لیست',
listDensityDescription: 'ارتفاع سطر، فاصله‌گذاری و مقیاس نوار پیشرفت را تغییر می‌دهد.',
compact: 'فشرده',
comfortable: 'راحت',
relaxed: 'بسیار راحت',
osIntegration: 'یکپارچه‌سازی سیستم‌عامل',
dockBadge: 'نمایش نشان روی آیکون Dock',
dockBadgeDescription: 'تعداد دانلودهای فعال را روی آیکون Firelink در Dock نشان می‌دهد.',
menuBarIcon: 'نمایش آیکون نوار منو',
statusIndicatorIcon: 'نمایش آیکون نشانگر وضعیت',
systemTrayIcon: 'نمایش آیکون System Tray',
macosTrayDescription: 'دسترسی سریع از نوار منوی macOS فراهم می‌کند.',
windowsTrayDescription: 'دسترسی سریع از ناحیه اعلان‌های Windows فراهم می‌کند.',
linuxTrayDescription: 'در صورت وجود، دسترسی سریع از سینی دسکتاپ یا ناحیه وضعیت فراهم می‌کند.',
defaultTrayDescription: 'در صورت وجود، دسترسی سریع از ناحیه سینی سیستم‌عامل فراهم می‌کند.',
},
network: {
proxy: 'پروکسی',
mode: 'حالت',
modeDescription: 'استفاده از پروکسی را برای درخواست‌های دانلود جدید کنترل می‌کند.',
noProxy: 'بدون پروکسی',
systemProxy: 'پروکسی سیستم',
customProxy: 'پروکسی سفارشی',
proxyHost: 'میزبان پروکسی',
proxyHostDescription: 'نام میزبان، آدرس IP یا URL پروکسی HTTP.',
proxyHostPlaceholder: '127.0.0.1 یا http://127.0.0.1',
proxyPort: 'پورت پروکسی',
proxyPortDescription: 'محدوده معتبر بین ۱ تا ۶۵۵۳۵ است.',
noProxyDescription: 'دانلودها پروکسی‌های پیکربندی‌شده را نادیده می‌گیرند.',
systemProxyDescription: 'دانلودها از پروکسی سیستم شناسایی‌شده {{platform}} استفاده می‌کنند. دانلودهای فایل عادی به یک نقطه پایانی پروکسی HTTP یا HTTPS نیاز دارند؛ دانلودهای رسانه می‌توانند از SOCKS استفاده کنند.',
customProxyDescription: 'دانلودها از نقطه پایانی پروکسی HTTP پیکربندی‌شده برای متادیتا و موتورهای دانلود استفاده می‌کنند.',
invalidCustomProxy: 'برای فعال کردن پروکسی سفارشی یک میزبان و پورت پروکسی HTTP معتبر وارد کنید.',
incompleteCustomProxy: 'برای فعال کردن پروکسی سفارشی یک میزبان و پورت پروکسی وارد کنید.',
checkingSystemProxy: 'در حال بررسی پیکربندی پروکسی سیستم...',
detectedSystemProxy: 'یک پروکسی سیستم شناسایی شد. دانلودهای فایل عادی به یک نقطه پایانی HTTP یا HTTPS نیاز دارند؛ دانلودهای رسانه می‌توانند از SOCKS استفاده کنند.',
noSystemProxy: 'هیچ پروکسی سیستم قابل استفاده‌ای شناسایی نشد. دانلودها از هیچ پروکسی‌ای استفاده نخواهند کرد.',
systemProxyReadFailed: 'پیکربندی پروکسی سیستم قابل خواندن نیست. بدون پروکسی را انتخاب کنید یا هنگامی که در دسترس است دوباره امتحان کنید.',
identity: 'هویت',
customUserAgent: 'User-Agent سفارشی',
userAgentDescription: 'در دریافت‌های متادیتا و موتورهای دانلود اعمال می‌شود.',
userAgentPlaceholder: 'برای پیش‌فرض Firelink خالی بگذارید',
userAgentOverrides: 'هدر User-Agent خروجی را بازنویسی می‌کند. برای پیش‌فرض‌های Firelink خالی بگذارید.',
chromeWindows: 'Chrome (Windows)',
chromeMacos: 'Chrome (macOS)',
edgeWindows: 'Edge (Windows)',
firefoxWindows: 'Firefox (Windows)',
firefoxMacos: 'Firefox (macOS)',
safariMacos: 'Safari (macOS)',
windowsDesktop: 'Windows دسکتاپ',
macosDesktop: 'macOS دسکتاپ',
},
locations: {
baseDownloadFolder: 'پوشه پایه دانلود',
baseDownloadFolderDescription: 'پوشه‌های دسته خودکار در داخل این پوشه ایجاد می‌شوند.',
browse: 'مرور',
baseFolderCreateFailed: 'پوشه پایه ذخیره شد، اما ایجاد پوشه‌های دسته ناموفق بود: {{detail}}',
baseFolderUpdated: 'پوشه پایه دانلود به‌روزرسانی شد',
askWhereToSave: 'هنگام افزودن دانلود بپرس کجا ذخیره شود',
rememberLastUsedDownloadDirectory: 'پوشه دانلود اخیراً استفاده‌شده را به خاطر بسپار',
rememberLastUsedDownloadDirectoryDescription: 'از آخرین پوشه انتخاب‌شده در پنجره افزودن برای دانلود بعدی تا زمان راه‌اندازی مجدد برنامه استفاده کنید.',
automaticCategorySubfolders: 'فایل‌ها را به‌طور خودکار در زیرپوشه‌های دسته ذخیره کن',
automaticCategorySubfoldersDescription: 'وقتی خاموش است، دانلودهای خودکار از پوشه پایه استفاده می‌کنند.',
categorySubfolders: 'زیرپوشه‌های دسته',
relativeToBase: 'نسبت به پوشه پایه',
disabled: 'غیرفعال',
categorySubfolder: 'زیرپوشه {{category}}',
customFolder: 'پوشه سفارشی',
useAutomatic: 'استفاده از حالت خودکار',
resetDefaults: 'بازنشانی به پیش‌فرض‌ها',
resetToast: 'مکان‌های دسته‌ها به پیش‌فرض بازنشانی شد',
},
siteLogins: {
title: 'اعتبارنامه‌های سایت',
noSavedLogins: 'هیچ ورودی ذخیره‌شده‌ای وجود ندارد.',
user: 'کاربر: {{username}}',
deleteCredential: 'حذف اعتبارنامه',
deletedCredential: 'اعتبارنامه حذف شد',
deleteFailed: 'اعتبارنامه حذف نشد: {{detail}}',
addTitle: 'افزودن اعتبارنامه‌های سایت',
urlPattern: 'الگوی URL:',
urlPatternPlaceholder: 'مثلاً *.example.com یا example.com/downloads',
username: 'نام کاربری:',
password: 'رمز عبور:',
usernamePlaceholder: 'نام کاربری',
passwordPlaceholder: 'رمز عبور',
saving: 'در حال ذخیره...',
addLogin: 'افزودن ورود',
urlPatternRequired: 'الگوی URL الزامی است.',
urlPatternWhitespace: 'الگوی URL نمی‌تواند شامل فاصله باشد.',
usernameRequired: 'نام کاربری الزامی است.',
passwordRequired: 'رمز عبور الزامی است.',
accessBeforeSave: 'قبل از ذخیره ورود به سایت، به مخزن اعتبارنامه دسترسی بدهید.',
saveFailed: 'ذخیره ایمن اعتبارنامه سایت ناموفق بود.',
addedCredential: 'اعتبارنامه سایت اضافه شد',
},
power: {
title: 'مدیریت انرژی',
preventSleep: 'جلوگیری از خواب سیستم در حالی که دانلودها فعال هستند',
preventSleepDescription: 'نمایشگر همچنان می‌تواند خاموش شود. Firelink تنها به اندازه‌ای دستگاه را بیدار نگه می‌دارد تا انتقال‌های فعال را تکمیل کند.',
},
engine: {
title: 'دانلودکننده رسانه و موتورها',
description: 'نتایج موفقیت‌آمیز برای این جلسه برنامه استفاده مجدد می‌شوند. بررسی مجدد باعث اعتبارسنجی واقعی دوباره می‌شود.',
recheck: 'بررسی مجدد موتورها',
coreDownloader: 'دانلودکننده اصلی (aria2)',
version: 'نسخه:',
status: 'وضعیت:',
mediaExtractors: 'استخراج‌کننده‌های رسانه',
browserCookiesSource: 'منبع کوکی‌های مرورگر:',
cookieDescription: 'yt-dlp کوکی‌های مرورگر را می‌خواند تا محدودیت‌های دانلود ویدیو را دور بزند یا به رسانه‌های محدود دسترسی پیدا کند. Firelink کوکی‌های مرورگر را ذخیره نمی‌کند.',
none: 'هیچ‌کدام',
},
integrations: {
title: 'اتصال افزونه مرورگر',
description: 'ثبت مستقیم دانلودها از مرورگر خود در سه مرحله آسان.',
portablePairingEnabled: 'جفت‌سازی قابل‌حمل فعال شد',
pairingTokenPersisted: 'توکن جفت‌سازی ذخیره شد',
portablePersistedDescription: 'توکن جفت‌سازی شما با این پوشه قابل‌حمل Firelink ذخیره می‌شود و هنگام انتقال پوشه باقی می‌ماند. با پوشه به عنوان داده حساس برخورد کنید.',
systemPersistedDescription: 'توکن جفت‌سازی شما در مخزن اعتبارنامه‌های سیستم ذخیره شده و پس از راه‌اندازی مجدد باقی می‌ماند.',
portablePairingAvailable: 'جفت‌سازی قابل‌حمل در دسترس است',
credentialStorageNeeded: 'نیاز به ذخیره‌سازی اعتبارنامه',
portableAvailableDescription: 'توکن جفت‌سازی شما با این پوشه قابل‌حمل Firelink ذخیره می‌شود و پس از راه‌اندازی مجدد باقی می‌ماند. برای مشاهده هشدار ذخیره‌سازی قابل‌حمل آن را اینجا فعال کنید.',
credentialStorageDescription: "Firelink به دسترسی به مخزن اعتبارنامه‌های این سیستم نیاز دارد تا توکن جفت‌سازی شما را پس از راه‌اندازی مجدد به‌طور امن حفظ کند. در حال حاضر، افزونه شما فقط برای این جلسه متصل باقی می‌ماند.",
reviewPortablePairing: 'بررسی جفت‌سازی قابل‌حمل',
grantCredentialAccess: 'اعطای دسترسی اعتبارنامه',
copyToken: 'کپی توکن',
tokenDescription: 'این توکن امن افزونه مرورگر شما را مجاز می‌سازد.',
tokenCopied: 'توکن در کلیپ‌بورد کپی شد!',
tokenCopyFailed: 'توکن کپی نشد: {{detail}}',
pairingTokenRegenerated: 'توکن جفت‌سازی دوباره ایجاد شد',
regenerateFailed: 'ایجاد دوباره توکن جفت‌سازی ناموفق بود: {{detail}}',
getExtension: 'دریافت افزونه',
extensionDescription: 'Firelink Companion را برای مرورگرهای Firefox یا Chromium نصب کنید.',
firefoxAddons: 'افزونه‌های Firefox',
chromiumZip: 'Chromium ZIP',
pasteConnect: 'جای‌گذاری و اتصال',
pasteConnectDescription: "روی آیکون Firelink در نوار ابزار مرورگر خود کلیک کنید و توکن کپی‌شده را جای‌گذاری کنید.",
extensionServerStatus: 'وضعیت سرور افزونه:',
listening: '● در حال گوش دادن به 127.0.0.1:{{port}}',
unavailable: '● سرور در دسترس نیست',
},
about: {
firelinkIcon: 'آیکون Firelink',
unknown: 'نامشخص',
version: 'نسخه {{version}}',
description: 'مدیریت دانلود بین پلتفرمی برای macOS، Windows و Linux.',
sourceCode: 'کد منبع',
source: 'منبع',
upToDate: 'Firelink {{version}} به‌روز است.',
updateAvailable: 'Firelink {{version}} در دسترس است.',
viewRelease: 'مشاهده انتشار',
updates: 'به‌روزرسانی‌ها',
checkForUpdates: 'بررسی به‌روزرسانی‌ها',
updateDescription: 'Firelink انتشار‌های GitHub را برای نسخه‌های جدید بررسی می‌کند.',
checking: 'در حال بررسی...',
checkNow: 'اکنون بررسی کن',
automaticUpdates: 'بررسی خودکار برای به‌روزرسانی‌ها',
createdBy: 'ایجاد شده توسط NimBold',
mitLicense: 'مجوز MIT',
credits: 'اعتبارات: aria2، yt-dlp، FFmpeg و Deno.',
copyright: 'حق کپی‌رایت © 2026 NimBold. تمامی حقوق محفوظ است.',
},
},
status: {
ready: 'آماده',
active: '{{count}} فعال',
queued: '{{count}} در صف',
done: '{{count}} تکمیل شد',
},
} as const;
export default fa;
+728
View File
@@ -0,0 +1,728 @@
const he = {
navigation: {
library: 'ספרייה',
filters: {
all: 'הכל',
active: 'פעיל',
completed: 'הושלם',
unfinished: 'לא הושלם',
},
categories: {
musics: 'מוזיקה',
movies: 'סרטים',
compressed: 'דחוס',
documents: 'מסמכים',
pictures: 'תמונות',
applications: 'יישומים',
other: 'אחר',
},
folders: 'תיקיות',
queues: 'תורים',
tools: 'כלים',
scheduler: 'מתזמן',
speedLimiter: 'הגבלת מהירות',
logs: 'יומנים',
settings: 'הגדרות',
},
actions: {
hideSidebar: 'הסתרת סרגל צד',
showSidebar: 'הצגת סרגל צד',
resizeSidebar: 'שינוי גודל סרגל צד',
cancel: 'ביטול',
continue: 'המשך',
dismissNotification: 'סגירת התראה',
queueName: 'שם התור',
addNewQueue: 'הוספת תור חדש',
startQueue: 'התחלת תור',
pauseQueue: 'השהיית תור',
renameQueue: 'שינוי שם תור',
deleteQueue: 'מחיקת תור',
},
dialogs: {
errorBoundary: {
title: 'משהו השתבש.',
description: 'אירעה שגיאה קריטית בעץ הרכיבים של React. פרטי השגיאה למטה עשויים לעזור באיתור הגורם לה.',
reload: 'טעינה מחדש של האפליקציה',
},
duplicateDownloads: {
title: 'זוהו הורדות כפולות',
description: 'חלק מההורדות שאתה מוסיף כבר קיימות בתור או בדיסק. אנא בחר כיצד לפתור התנגשויות אלו.',
rename: 'שינוי שם',
replace: 'החלפה',
skip: 'דילוג',
},
removeDownload: {
title: 'הסרת הורדה',
confirmationSingle: 'האם אתה בטוח שברצונך להסיר פריט זה מהרשימה? תוכל גם לבחור למחוק את הקובץ המקורי מהכונן הקשיח.',
confirmationMultiple: 'האם אתה בטוח שברצונך להסיר {{count}} פריטים אלו מהרשימה? תוכל גם לבחור למחוק את הקבצים המקוריים מהכונן הקשיח.',
errorSummary: '{{succeeded}} הוסרו, {{failed}} נכשלו: {{detail}}',
remove: 'הסרה',
deleteFile: 'מחיקת קובץ',
},
},
window: {
controls: 'פקדי חלון',
close: 'סגירה',
minimize: 'מזעור',
maximize: 'הגדלה',
},
downloads: {
actions: {
moveUp: 'הזזה למעלה',
moveDown: 'הזזה למטה',
pause: 'השהייה',
start: 'התחלה',
resume: 'המשך',
options: 'אפשרויות',
},
size: {
downloadedOf: '{{downloaded}} הורדו מתוך {{total}} {{unit}}',
downloadedOfApproximate: '{{downloaded}} הורדו מתוך כ-{{total}} {{unit}}',
},
status: {
ready: 'מוכן',
staged: 'בתור',
queued: 'בתור',
downloading: 'מוריד',
processing: 'מעבד',
paused: 'מושהה',
completed: 'הושלם',
failed: 'נכשל',
retrying: 'מנסה שוב',
},
values: {
processing: 'מעבד...',
muxing: 'ממזג...',
},
},
speedLimiter: {
title: 'הגבלת מהירות',
unlimited: 'ללא הגבלה',
saveLimit: 'שמירת הגבלה',
globalSpeedLimit: 'הגבלת מהירות כללית',
description: 'חל על העברות חדשות ופעילות של aria2 והורדות מדיה חדשות של yt-dlp. הגבלות פרטניות מפורשות להורדה נשארות צמודות להעברות אלה.',
quickPresets: 'הגדרות מהירות',
removePreset: 'הסרת הגדרה של {{value}} {{unit}}',
customPresetIn: 'הגדרה מותאמת אישית ב-{{unit}}',
addQuickPreset: 'הוספת הגדרה מהירה',
globalLimitSaved: 'הגבלה כללית נשמרה כ-{{value}} {{unit}}',
globalLimitDisabled: 'הגבלת מהירות כללית מושבתת',
saveFailed: 'לא ניתן לשמור את הגבלת המהירות הכללית: {{detail}}',
presetAlreadyExists: '{{value}} {{unit}} כבר נמצא בהגדרות המהירות',
presetAdded: 'נוספה הגדרה מהירה של {{value}} {{unit}}',
presetRemoved: 'הוסרה הגדרה מהירה של {{value}} {{unit}}',
},
logs: {
title: 'יומנים',
entries: '({{count}} רשומות)',
collecting: 'אוסף',
off: 'כבוי',
allLevels: 'כל הרמות',
levels: {
error: 'שגיאה',
warn: 'אזהרה',
info: 'מידע',
debug: 'ניפוי באגים',
trace: 'מעקב',
},
pauseLogging: 'השהיית רישום אבחון',
enableLogging: 'הפעלת רישום אבחון',
clearDisplayed: 'ניקוי יומנים מוצגים',
export: 'ייצוא יומנים',
exportButton: 'ייצוא יומנים',
logFiles: 'קבצי יומן',
localDiagnostics: 'אבחון מקומי:',
diagnosticsDescription: 'איסוף אבחון נעשה מבחירה, מוגבל ומקומי. סודות נפוצים, שאילתות URL ונתיבי תיקיית בית מוסתרים במהלך הצגה וייצוא. שום דבר אינו מועלה באופן אוטומטי.',
noEntries: 'אין עדיין רשומות יומן שמורות זמינות.',
disabledDescription: 'רישום אבחון כבוי. יומני תמיכה קיימים יופיעו כאן כאשר יהיו זמינים.',
copy: 'העתקה',
copied: 'הועתק ללוח',
copyFailed: 'העתקה ללוח נכשלה',
exported: 'יומני תמיכה יוצאו',
exportFailed: 'לא ניתן לייצא יומנים: {{detail}}',
cleared: 'היומנים נוקו',
clearFailed: 'לא ניתן לנקות יומנים: {{detail}}',
enabled: 'רישום אבחון מופעל',
disabled: 'רישום אבחון מושבת',
updateFailed: 'לא ניתן לעדכן רישום אבחון: {{detail}}',
},
scheduler: {
title: 'מתזמן',
running: 'פועל',
disabled: 'המתזמן מושבת',
noScheduledDay: 'לא נבחר יום מתוזמן',
unsavedChanges: 'שינויים שלא נשמרו',
runNow: 'הפעל כעת',
pause: 'השהייה',
saveSettings: 'שמירת הגדרות',
timing: 'תזמון',
startTime: 'שעת התחלה',
stopTime: 'שעת סיום',
timingDescription: 'אם Firelink ישן בשעת ההתחלה, הוא יפעיל את התורים שנבחרו כשיחזור מאוחר יותר באותו יום, אלא אם כן שעת הסיום כבר עברה.',
runEveryDay: 'הפעל כל יום',
queuesToSchedule: 'תורים לתזמון',
defaultQueue: 'תור ברירת מחדל',
afterCompletion: 'לאחר השלמה',
afterCompletionDescription: 'בחר מה יקרה לאחר סיום ההורדות שהופעלו על ידי המתזמן.',
actionWarning: 'פעולה זו עשויה להפריע לעבודה אחרת במחשב. Firelink מפעיל אותה מיד לאחר שהתור המתוזמן מסתיים.',
systemPermissions: 'הרשאות מערכת',
macPermissionDescription: 'שינה, הפעלה מחדש וכיבוי דורשים הרשאת Automation ל-System Events ב-macOS.',
permissionGranted: 'ניתנה הרשאת Automation',
permissionChecking: 'בודק הרשאת Automation...',
permissionMissing: 'חסרה הרשאת Automation',
revokePermission: 'שלילת הרשאה',
grantPermission: 'מתן הרשאה',
permissionAvailable: 'הרשאת Automation זמינה.',
permissionMissingDetails: 'חסרה הרשאת Automation. הפעל את Firelink תחת Automation עבור System Events בהגדרות המערכת.',
revokePermissionDetails: 'macOS אינה מאפשרת ל-Firelink לשלול הרשאת Automation ישירות. שלול גישה ל-System Events בהגדרות המערכת, ולאחר מכן חזור ל-Firelink.',
requestingPermission: 'מבקש הרשאת Automation...',
enablePermissionDetails: 'הפעל את Firelink תחת Automation עבור System Events בהגדרות המערכת, ולאחר מכן חזור ל-Firelink.',
systemActions: 'פעולות מערכת',
windowsActionsDescription: 'שינה, הפעלה מחדש וכיבוי משתמשים בהרשאות מערכת של Windows. Firelink מדווח על כל פעולה שנדחתה כאשר היא פועלת; לא נדרשת הרשאה קבועה מראש.',
linuxActionsDescription: 'שינה, הפעלה מחדש וכיבוי משתמשים בשולחן העבודה שלך ובמדיניות המערכת של Linux. Firelink מדווח על כל פעולה שנדחתה כאשר היא פועלת; לא נדרשת הרשאה קבועה מראש.',
validationDay: 'בחר לפחות יום אחד למתזמן',
validationQueue: 'בחר לפחות תור אחד למתזמן',
validationStopTime: 'שעת הסיום חייבת להיות מאוחרת משעת ההתחלה',
saved: 'הגדרות המתזמן נשמרו',
trackingOne: 'עוקב אחר הורדה מתוזמנת אחת',
trackingMany: 'עוקב אחר {{count}} הורדות מתוזמנות',
noStartableDownloads: 'אין הורדות בתורים שנבחרו שניתן להתחיל',
pausedOne: 'הושהתה הורדה פעילה אחת',
pausedMany: 'הושהתו {{count}} הורדות פעילות',
noActiveDownloads: 'אין הורדות פעילות',
days: {
su: 'א\'',
mo: 'ב\'',
tu: 'ג\'',
we: 'ד\'',
th: 'ה\'',
fr: 'ו\'',
sa: 'ש\'',
},
postActions: {
none: 'אל תעשה כלום',
sleep: 'שינה',
restart: 'הפעלה מחדש',
shutdown: 'כיבוי',
},
},
properties: {
progress: 'התקדמות',
size: 'גודל',
speed: 'מהירות',
eta: 'זמן נותר',
connections: 'חיבורים',
speedCap: 'הגבלת מהירות',
category: 'קטגוריה',
lastTry: 'ניסיון אחרון',
dateAdded: 'תאריך הוספה',
destination: 'יעד',
lastError: 'שגיאה אחרונה',
saved: ' (נשמר)',
defaultValue: ' (ברירת מחדל)',
savedTooltip: 'נשמר עבור הורדה זו; שינויי הגדרות חלים על הורדות חדשות.',
defaultTooltip: 'שימוש בברירת המחדל הנוכחית להורדות חדשות.',
identityReadOnly: 'זהות הקובץ לקריאה בלבד. הגדרות ההעברה נשמרות להורדה מחדש.',
transferSettings: 'ניתן לשנות את הגדרות ההעברה לאחר עצירה או השהייה. העברות נוכחיות שומרות על אפשרויות ההעברה הקיימות שלהן.',
download: 'הורדה',
url: 'URL',
fileName: 'שם קובץ',
saveLocation: 'מיקום שמירה',
select: 'בחירה',
perFile: 'לקובץ',
useCurrentDefault: 'השתמש בברירת המחדל הנוכחית ({{count}})',
savedPerDownload: 'נשמר לכל הורדה. ברירת המחדל של ההגדרות חלה על הורדות חדשות.',
limit: 'הגבלה',
siteLogin: 'התחברות לאתר',
siteLoginRedownload: 'התחברות לאתר להורדה מחדש',
matchingSiteLogin: 'התחברות תואמת לאתר',
customCredentials: 'אישורים מותאמים אישית',
noLogin: 'ללא התחברות',
useSavedLogin: 'ישתמש בהתחברות שמורה אם זמינה.',
advancedTransfer: 'העברה מתקדמת',
advancedTransferRedownload: 'העברה מתקדמת להורדה מחדש',
checksum: 'סכום ביקורת',
verify: 'אימות',
algorithm: 'אלגוריתם',
digest: 'תקציר',
expectedDigest: 'תקציר צפוי',
cookies: 'עוגיות',
headers: 'כותרות (Headers)',
mirrors: 'מראות',
username: 'שם משתמש',
password: 'סיסמה',
enterValidUrl: 'הזן URL תקין.',
fileNameEmpty: 'שם קובץ אינו יכול להיות ריק.',
cancel: 'ביטול',
save: 'שמירה',
},
keychain: {
title: 'נדרשת גישה לאחסון אישורים',
stores: {
portable: 'תיקיית הנתונים הניידת של Firelink',
windows: 'מנהל האישורים של Windows',
linux: 'אחסון האישורים של Linux שלך',
macos: 'Keychain של macOS',
system: 'אחסון האישורים של מערכת זו',
siteCredentials: 'אחסון האישורים של המערכת',
},
grantLabelPortable: 'המשך',
grantLabelMacos: 'הענקת גישה',
grantLabelDefault: 'הפעלת אחסון מאובטח',
description: 'Firelink משתמש בתוסף הדפדפן כדי ללכוד הורדות. כדי לשמור על חיבור התוסף לאחר הפעלה מחדש, Firelink שומר את אסימון הצימוד שלו ב-{{pairingStore}}. אישורי אתר אופציונליים נשמרים ב-{{siteCredentialStore}}.',
portableExplanation: 'אסימון הצימוד נייד יחד עם תיקייה זו. אישורי אתרים נשארים באחסון האישורים של המערכת; ייתכן שתופיע בקשת מערכת לאחר שתעניק גישה.',
macosExplanation: 'macOS עשויה להציג בקשת Keychain לאחר שתעניק גישה.',
defaultExplanation: 'לרוב זה מסתיים בשקט. אם שירות האישורים אינו זמין, Firelink יציג את השגיאה כאן והתוסף יישאר מצומד עבור הפעלה זו בלבד.',
note: 'הערה:',
portableNote: 'מצב נייד שומר רק את אסימון הצימוד בתיקייה זו. הוא אינו מעתיק סיסמאות אתרים או אישורי דפדפן.',
defaultNote: 'Firelink כותב רק רשומת אישור ייעודית משלו. אין לו גישה לסיסמאות שמורות אחרות או לפריטי אישורים במערכת שלך.',
hint: 'רמז:',
portableHint: 'אסימון הצימוד הנייד כבר שמור בתיקייה זו; תוכל להפעיל אותו כאן או לבחור "מאוחר יותר".',
defaultHint: 'אם תבחר "מאוחר יותר", התוסף יפעל עבור הפעלה זו בלבד.',
enableFromSettings: 'תוכל להפעיל את האחסון בכל עת דרך הגדרות > שילובים.',
later: 'מאוחר יותר',
enabling: 'מפעיל...',
timeout: 'בקשת האחסון של האישורים הסתיימה. תוכל לבחור "מאוחר יותר" ולנסות שוב.',
unavailable: '{{store}} אינו זמין.',
accessRequired: 'הענק גישה לאחסון האישורים לפני יצירה מחדש של אסימון הצימוד.',
storeUnavailable: 'הגישה לאחסון האישורים אינה זמינה.',
},
app: {
settingsSaveFailed: 'לא ניתן לשמור הגדרות. בדוק הרשאות אחסון ונסה שוב.',
systemActionCountdown: '{{action}} בעוד 10 שניות.',
systemActionCancelled: 'פעולת המערכת בוטלה מכיוון שהורדה אחרת פעילה או בתור.',
systemActionFailed: 'פעולת מערכת מתוזמנת נכשלה: {{detail}}',
downloadCompleteTitle: 'ההורדה הושלמה',
downloadCompleteBody: 'הורדת {{fileName}} הסתיימה.',
unknownFile: 'קובץ',
downloadFailedTitle: 'ההורדה נכשלה',
downloadFailedBody: 'הורדת {{fileName}} נכשלה.',
initializeDownloadsFailed: 'לא ניתן לאתחל הורדות שמורות: {{detail}}',
extensionDisconnected: 'תוסף הדפדפן נותק מכיוון שאסימון הצימוד שלו השתנה.',
copyToken: 'העתקת אסימון',
integrations: 'שילובים',
copyTokenFailed: 'לא ניתן להעתיק את אסימון הצימוד: {{detail}}',
credentialPersistenceFailed: 'שמירת אישורים מאובטחת אינה זמינה. צימוד הדפדפן פועל עבור הפעלה זו בלבד: {{detail}}',
resumeDownloadsFailed: 'לא ניתן להמשיך הורדות שמורות: {{detail}}',
updateAvailable: 'Firelink {{version}} זמין.',
viewRelease: 'הצגת שחרור',
sleepPreventionFailed: 'לא ניתן לעדכן את מניעת השינה: {{detail}}',
schedulerNoQueues: 'למתזמן אין תורים חוקיים שנבחרו. עדכן את הגדרות המתזמן.',
schedulerPauseOneFailed: 'המתזמן לא הצליח להשהות הורדה אחת.',
schedulerPauseManyFailed: 'המתזמן לא הצליח להשהות {{count}} הורדות.',
scheduledIncomplete: 'ההורדות המתוזמנות לא הושלמו במלואן. פעולת המערכת לאחר התור דולגה.',
scheduledActionSkippedActive: 'פעולת המערכת המתוזמנת דולגה מכיוון שהורדה אחרת פעילה או בתור.',
notificationsDisabled: 'התראות המערכת מושבתות עבור Firelink.',
notificationsFailed: 'לא ניתן להגדיר התראות: {{detail}}',
},
sidebar: {
deleteQueueConfirm: 'למחוק את התור "{{name}}"? ההורדות שלא הושלמו בו יועברו לתור הראשי.',
deleteQueueFailed: 'לא ניתן למחוק את התור: {{detail}}',
queueNameEmpty: 'שם התור אינו יכול להיות ריק',
queueNameExists: 'כבר קיים תור בשם זה',
startQueueFailed: 'לא ניתן להתחיל את התור: {{detail}}',
pauseQueueFailed: 'לא ניתן להשהות את התור: {{detail}}',
},
downloadTable: {
unknownQueue: 'תור לא ידוע',
allDownloads: 'כל ההורדות',
active: 'פעיל',
completed: 'הושלם',
unfinished: 'לא הושלם',
headers: {
fileName: 'שם קובץ',
size: 'גודל',
status: 'סטטוס',
speed: 'מהירות',
eta: 'זמן נותר',
dateAdded: 'תאריך הוספה',
},
addDownload: 'הוספת הורדה',
resumeAll: 'המשך הכל',
pauseAll: 'השהיית הכל',
queueEmpty: 'התור ריק',
noCompletedDownloads: 'אין הורדות שהושלמו',
noDownloads: 'אין הורדות',
queueEmptyDescription: 'הוסף הורדות לתור זה מתפריט הפריט או מחלון ההוספה.',
completedDescription: 'הורדות שהושלמו יופיעו כאן.',
clickToAdd: 'לחץ על לחצן',
addButtonOr: 'או',
toAddDownloads: 'כדי להוסיף הורדות',
nonResumableOne: 'הורדה אחת אינה תומכת בהמשך. אם תשהה אותה, תצטרך להתחיל מהתחלה מאוחר יותר. האם אתה בטוח שברצונך להשהות?',
nonResumableMany: '{{count}} הורדות אינן תומכות בהמשך. אם תשהה אותן, תצטרך להתחיל מהתחלה מאוחר יותר. האם אתה בטוח שברצונך להשהות?',
interactionError: '{{message}}: {{detail}}',
openFileFailed: 'לא ניתן לפתוח את הקובץ שהורד',
revealFileFailed: 'לא ניתן להציג את ההורדה ב-Finder',
pauseFailed: 'לא ניתן להשהות את ההורדה',
resumeFailed: 'לא ניתן להמשיך את {{fileName}}',
moveManyFailed: 'לא ניתן להעביר הורדות לתור',
moveOneFailed: 'לא ניתן להעביר הורדה לתור',
copyAddressesFailed: 'לא ניתן להעתיק כתובות',
copyAddressFailed: 'לא ניתן להעתיק כתובת',
copyPathFailed: 'לא ניתן להעתיק נתיב קובץ',
missingFileName: 'שם הקובץ חסר',
redownloadFailed: 'הורדה מחדש נכשלה',
startResume: 'התחלה/המשך',
addToQueue: 'הוספה לתור',
copyAddress: 'העתקת כתובת',
remove: 'הסרה',
open: 'פתיחה',
showInFolder: 'הצגה בתיקייה',
pause: 'השהייה',
start: 'התחלה',
resume: 'המשך',
redownload: 'הורדה מחדש',
copyFilePath: 'העתקת נתיב קובץ',
properties: 'מאפיינים',
backendRejectedStart: 'העורף דחה את בקשת ההתחלה/המשך.',
transferActive: 'לא ניתן לשנות מאפיינים בזמן שההעברה פעילה. השהה אותה תחילה.',
redownloadNotFound: 'לא ניתן להוריד מחדש: ההורדה לא נמצאה.',
redownloadActive: 'לא ניתן להוריד מחדש הורדה בסטטוס {{status}}. השהה או המתן שתסתיים תחילה.',
originalUrlMissing: 'לא ניתן להוריד מחדש: ה-URL המקורי חסר.',
pauseBeforeMove: 'השהה את {{file}} לפני העברתו לתור אחר.',
},
addDownloads: {
unknownSize: 'גודל לא ידוע',
unknown: 'לא ידוע',
discardSetup: 'לבטל את הגדרת ההורדה הזו?',
speedInvalid: 'הגבלת המהירות חייבת להיות גדולה מאפס',
duplicateResolveFailed: 'לא ניתן לפתור הורדות כפולות: {{detail}}',
addedWithFailures: '{{added}} נוספו, {{failed}} נכשלו. {{detail}}',
addedOne: 'הורדה אחת נוספה',
addedMany: '{{count}} הורדות נוספו',
urlAlreadyQueued: 'URL כבר בתור',
destinationConflict: 'הורדה אחרת שנבחרה משתמשת ביעד זה',
existingDownloadDestination: 'הורדת Firelink קיימת משתמשת ביעד זה',
fileExistsOnDisk: 'הקובץ קיים בדיסק; שנה שם או דלג כדי למנוע מחיקת נתונים שאינם קשורים',
backendRejectedStart: 'העורף דחה את התחלת ההורדה.',
playlistNoEntries: 'הפלייליסט אינו מכיל רשומות הניתנות להורדה',
invalidMediaMetadata: 'מטא נתונים של מדיה לא חוקיים או שלא נמצאו פורמטים',
noAvailableName: 'לא נמצא שם פנוי עבור {{file}}.',
pauseBeforeReplace: 'השהה את {{file}} לפני החלפתו.',
cannotReplace: 'לא ניתן להחליף את {{file}}: הקובץ אינו שייך להורדת Firelink.',
downloadLinks: 'קישורי הורדה',
pastePlaceholder: 'הדבק כתובות HTTP, HTTPS, FTP או SFTP כאן...\n\nעבור הורדות מדיה, הדבק קישורים מ-YouTube, X, TikTok, Instagram, Reddit וכו\'.',
playlistSummary: 'פלייליסט "{{title}}": {{loaded}} מתוך {{total}} רשומות נטענו{{truncated}}{{skipped}}',
safeEntryLimit: ' (הגיע למגבלת רשומות בטוחה)',
selectedSummary: '{{ready}} נבחרו ומוכנים, {{fallback}} לגיבוי, {{mediaRetry}} מדיה לניסיון חוזר, {{blocked}} חסומים',
clearSelection: 'ניקוי בחירה',
selectAll: 'בחירת הכל',
refreshMetadata: 'רענון מטא נתונים',
files: 'קבצים',
required: 'נדרש',
free: 'פנוי',
preview: 'תצוגה מקדימה',
file: 'קובץ',
size: 'גודל',
status: 'סטטוס',
downloadPreview: 'תצוגה מקדימה של הורדה',
noLinks: 'טרם נוספו קישורים.',
selectItem: 'בחר {{file}}',
fetchingPlaylist: 'מביא פלייליסט...',
fetching: 'מביא...',
unsafeUrl: 'URL לא בטוח',
playlistFailed: 'טעינת הפלייליסט נכשלה',
metadataFailed: 'אחזור המטא-נתונים נכשל',
fallback: 'גיבוי',
invalid: 'לא חוקי',
ready: 'מוכן',
mediaFormat: 'פורמט מדיה',
playlistItem: 'פריט פלייליסט {{index}}',
fetchingMediaStreams: 'מביא זרמי מדיה...',
availableStreams: 'זרמים זמינים',
availableMediaStreams: 'זרמי מדיה זמינים',
metadataUnavailable: 'מטא נתונים אינם זמינים. רענן מטא נתונים לפני הוספת מדיה זו.',
saveLocation: 'מיקום שמירה',
browse: 'עיון',
categoryFolders: 'הקבצים יאורגנו בתיקיות קטגוריה.',
sharedFolder: 'כל ההורדות שנבחרו ישתמשו בתיקייה זו.',
transferSettings: 'הגדרות העברה',
connectionsPerFile: 'חיבורים לקובץ',
connectionsPerFileAria: 'חיבורים לקובץ',
limitSpeedPerFile: 'הגבלת מהירות לקובץ',
speedLimitPerFileAria: 'הגבלת מהירות לקובץ',
authorization: 'הרשאה',
useAuthorization: 'שימוש בהרשאה',
username: 'שם משתמש',
password: 'סיסמה',
advancedTransfer: 'העברה מתקדמת',
verifyChecksum: 'אימות סכום ביקורת',
checksumAlgorithm: 'אלגוריתם סכום ביקורת',
expectedDigest: 'תקציר צפוי',
headers: 'כותרות (Headers)',
requestHeaders: 'כותרות בקשה',
cookies: 'עוגיות',
cookiePlaceholder: 'name=value; other=value',
mirrors: 'מראות',
cancel: 'ביטול',
startDownloads: 'התחלת הורדות',
addToQueue: 'הוספה לתור',
pasteOneOrMore: 'הדבק קישור אחד או יותר.',
selectAtLeastOne: 'בחר לפחות הורדה אחת.',
correctInvalidOne: 'תקן או הסר {{count}} כתובת URL לא חוקית לפני שתמשיך.',
correctInvalidFew: 'תקן או הסר {{count}} כתובות URL לא חוקיות לפני שתמשיך.',
correctInvalidMany: 'תקן או הסר {{count}} כתובות URL לא חוקיות לפני שתמשיך.',
waitingForMetadataOne: 'ממתין למטא נתונים עבור {{count}} הורדה.',
waitingForMetadataFew: 'ממתין למטא נתונים עבור {{count}} הורדות.',
waitingForMetadataMany: 'ממתין למטא נתונים עבור {{count}} הורדות.',
removeUnsafeOne: 'הסר {{count}} כתובת URL לא בטוחה לפני שתמשיך.',
removeUnsafeFew: 'הסר {{count}} כתובות URL לא בטוחות לפני שתמשיך.',
removeUnsafeMany: 'הסר {{count}} כתובות URL לא בטוחות לפני שתמשיך.',
mediaMetadataUnavailableSummaryOne: 'מטא נתונים של מדיה אינם זמינים עבור {{count}} פריט. רענן מטא נתונים לפני ההוספה.',
mediaMetadataUnavailableSummaryFew: 'מטא נתונים של מדיה אינם זמינים עבור {{count}} פריטים. רענן מטא נתונים לפני ההוספה.',
mediaMetadataUnavailableSummaryMany: 'מטא נתונים של מדיה אינם זמינים עבור {{count}} פריטים. רענן מטא נתונים לפני ההוספה.',
metadataUnavailableFallback: 'מטא נתונים אינם זמינים. עדיין ניתן להוסיף הורדות באמצעות פרטי הגיבוי.',
fallbackReadyOne: '{{ready}} הורדה מוכנה; עבור {{failed}} ייעשה שימוש בשם קובץ גיבוי ובגודל לא ידוע.',
fallbackReadyFew: '{{ready}} הורדות מוכנות; עבור {{failed}} ייעשה שימוש בשם קובץ גיבוי ובגודל לא ידוע.',
fallbackReadyMany: '{{ready}} הורדות מוכנות; עבור {{failed}} ייעשה שימוש בשם קובץ גיבוי ובגודל לא ידוע.',
readyToAddOne: 'מוכן להוסיף {{count}} הורדה.',
readyToAddFew: 'מוכן להוסיף {{count}} הורדות.',
readyToAddMany: 'מוכן להוסיף {{count}} הורדות.',
},
settings: {
tabs: {
downloads: 'הורדות',
lookAndFeel: 'מראה ותחושה',
network: 'רשת',
locations: 'מיקומים',
siteLogins: 'התחברויות לאתרים',
power: 'צריכת חשמל',
engine: 'מנוע',
integrations: 'שילובים',
about: 'אודות',
},
common: {
thisOs: 'מערכת הפעלה זו',
checking: 'בודק...',
ready: 'מוכן',
errorMissing: 'שגיאה / חסר',
hideTechnicalDetails: 'הסתרת פרטים טכניים',
showTechnicalDetails: 'הצגת פרטים טכניים',
binary: 'קובץ בינארי (Binary)',
expected: 'צפוי',
error: 'שגיאה',
tip: 'טיפ',
stderr: 'stderr',
daemonProcessAlive: 'תהליך רקע פעיל',
rpcReady: 'RPC מוכן',
rpcPort: 'פורט RPC',
daemonStderr: 'stderr של תהליך רקע',
packaging: 'אריזה: PyInstaller onedir (נדרש _internal)',
internalDirectoryFound: 'תיקיית _internal נמצאה',
pythonRuntimeFound: 'סביבת ריצה של Python נמצאה',
externalOpenFailed: 'לא ניתן לפתוח את {{label}}: {{detail}}',
updateUnexpectedResponse: 'בדיקת העדכון החזירה תגובה בלתי צפויה.',
updateUntrustedReleaseUrl: 'בדיקת העדכון החזירה URL שחרור לא מהימן.',
updateFailed: 'בדיקת עדכון נכשלה: {{detail}}',
},
downloads: {
defaultConnections: 'חיבורי ברירת מחדל:',
defaultConnectionsDescription: 'הורדות חדשות; פריטים קיימים שומרים על הערך השמור שלהם',
parallelDownloads: 'הורדות במקביל:',
parallelDownloadsDescription: 'מקסימום קבצים פעילים בו-זמנית',
automaticRetries: 'ניסיונות חוזרים אוטומטיים:',
automaticRetriesDescription: 'אם חיבור נכשל',
systemNotification: 'הצג התראת מערכת כאשר ההורדה מסתיימת',
systemNotificationDescription: 'משתמש בהגדרות ההתראות של מערכת ההפעלה שלך',
completionChime: 'השמע צליל סיום בתוך האפליקציה',
completionChimeDescription: 'צליל Firelink אופציונלי, ללא קשר להתראות המערכת',
clipboardLinks: 'הוסף קישורים מהלוח כאשר Firelink הופך לפעיל',
clipboardLinksDescription: 'פותח קישורים מועתקים נתמכים בחלון ההוספה. כבוי כברירת מחדל.',
},
lookAndFeel: {
language: 'שפה',
languageDescription: 'בחר את שפת הממשק של Firelink.',
languageSystem: 'שפת המערכת',
languageEnglish: 'אנגלית',
languageChinese: 'סינית מפושטת',
languageHebrew: 'עברית',
languagePersian: 'פרסית',
languageUkrainian: 'אוקראינית',
languageRussian: 'רוסית',
appTheme: 'ערכת נושא לאפליקציה',
theme: 'ערכת נושא',
ariaLabel: 'ערכת נושא לאפליקציה',
system: 'מערכת',
light: 'בהירה',
dark: 'כהה',
dracula: 'Dracula',
nord: 'Nord',
systemAppearance: 'מערכת עוקבת אחר המראה הנוכחי (בהיר או כהה) של {{platform}}.',
display: 'תצוגה',
fontSize: 'גודל גופן',
fontSizeDescription: 'משנה את קנה המידה של רשימת ההורדות והפקדים הקומפקטיים.',
small: 'קטן',
standard: 'רגיל',
large: 'גדול',
listDensity: 'צפיפות הרשימה',
listDensityDescription: 'משנה את גובה השורה, הריווח וקנה המידה של פס ההתקדמות.',
compact: 'קומפקטי',
comfortable: 'נוח',
relaxed: 'מרווח',
osIntegration: 'שילוב במערכת ההפעלה',
dockBadge: 'הצג תג על סמל ה-Dock',
dockBadgeDescription: 'מציג את ספירת ההורדות הפעילות על סמל Firelink ב-Dock.',
menuBarIcon: 'הצג סמל בשורת התפריטים',
statusIndicatorIcon: 'הצג סמל מחוון סטטוס',
systemTrayIcon: 'הצג סמל במגש המערכת',
macosTrayDescription: 'מספק גישה מהירה משורת התפריטים של macOS.',
windowsTrayDescription: 'מספק גישה מהירה מאזור ההתראות של Windows.',
linuxTrayDescription: 'מספק גישה מהירה ממגש שולחן העבודה או מאזור הסטטוס כאשר הם זמינים.',
defaultTrayDescription: 'מספק גישה מהירה מאזור המגש של מערכת ההפעלה כאשר הוא זמין.',
},
network: {
proxy: 'פרוקסי',
mode: 'מצב',
modeDescription: 'שולט בשימוש בפרוקסי עבור בקשות הורדה חדשות.',
noProxy: 'ללא פרוקסי',
systemProxy: 'פרוקסי של המערכת',
customProxy: 'פרוקסי מותאם אישית',
proxyHost: 'מארח פרוקסי',
proxyHostDescription: 'שם מארח, כתובת IP או URL של פרוקסי HTTP.',
proxyHostPlaceholder: '127.0.0.1 או http://127.0.0.1',
proxyPort: 'פורט פרוקסי',
proxyPortDescription: 'טווח חוקי הוא 1 עד 65535.',
noProxyDescription: 'הורדות מתעלמות מפרוקסי שהוגדרו.',
systemProxyDescription: 'הורדות משתמשות בפרוקסי המערכת {{platform}} שזוהה. הורדות קבצים רגילות דורשות נקודת קצה של HTTP או HTTPS; הורדות מדיה יכולות להשתמש ב-SOCKS.',
customProxyDescription: 'הורדות משתמשות בנקודת הקצה המוגדרת של פרוקסי HTTP עבור מטא-נתונים ומנועי הורדה.',
invalidCustomProxy: 'הזן מארח ופורט חוקיים של פרוקסי HTTP כדי להפעיל את הפרוקסי המותאם אישית.',
incompleteCustomProxy: 'הזן מארח ופורט של פרוקסי כדי להפעיל את הפרוקסי המותאם אישית.',
checkingSystemProxy: 'בודק את תצורת פרוקסי המערכת…',
detectedSystemProxy: 'זוהה פרוקסי מערכת. הורדות קבצים רגילות דורשות נקודת קצה של HTTP או HTTPS; הורדות מדיה יכולות להשתמש ב-SOCKS.',
noSystemProxy: 'לא זוהה פרוקסי מערכת שימושי. ההורדות לא ישתמשו בפרוקסי.',
systemProxyReadFailed: 'לא ניתן לקרוא את תצורת פרוקסי המערכת. בחר "ללא פרוקסי" או נסה שוב כשהיא תהיה זמינה.',
identity: 'זהות',
customUserAgent: 'User-Agent מותאם אישית',
userAgentDescription: 'מוחל על משיכות מטא נתונים ומנועי הורדה.',
userAgentPlaceholder: 'השאר ריק עבור ברירת המחדל של Firelink',
userAgentOverrides: 'דורס את כותרת ה-User-Agent היוצאת. השאר ריק עבור ברירות המחדל של Firelink.',
chromeWindows: 'Chrome (Windows)',
chromeMacos: 'Chrome (macOS)',
edgeWindows: 'Edge (Windows)',
firefoxWindows: 'Firefox (Windows)',
firefoxMacos: 'Firefox (macOS)',
safariMacos: 'Safari (macOS)',
windowsDesktop: 'שולחן עבודה Windows',
macosDesktop: 'שולחן עבודה macOS',
},
locations: {
baseDownloadFolder: 'תיקיית הורדות בסיסית',
baseDownloadFolderDescription: 'תיקיות קטגוריה אוטומטיות נוצרות בתוך תיקייה זו.',
browse: 'עיון',
baseFolderCreateFailed: 'תיקיית הבסיס נשמרה, אך לא ניתן היה ליצור תיקיות קטגוריה: {{detail}}',
baseFolderUpdated: 'תיקיית הורדות בסיסית עודכנה',
askWhereToSave: 'שאל היכן לשמור בעת הוספת הורדות',
rememberLastUsedDownloadDirectory: 'זכור את ספריית ההורדות שהייתה בשימוש אחרון',
rememberLastUsedDownloadDirectoryDescription: 'השתמש בתיקייה האחרונה שנבחרה בחלון ההוספה עבור ההורדה הבאה עד שהאפליקציה תופעל מחדש.',
automaticCategorySubfolders: 'שמור קבצים אוטומטית לתיקיות משנה של קטגוריות',
automaticCategorySubfoldersDescription: 'כאשר כבוי, הורדות אוטומטיות משתמשות בתיקיית הבסיס.',
categorySubfolders: 'תיקיות משנה של קטגוריות',
relativeToBase: 'יחסית לתיקיית הבסיס',
disabled: 'מושבת',
categorySubfolder: 'תיקיית משנה {{category}}',
customFolder: 'תיקייה מותאמת אישית',
useAutomatic: 'השתמש באוטומטי',
resetDefaults: 'שחזור ברירות מחדל',
resetToast: 'מיקומי הקטגוריות אופסו לברירת המחדל',
},
siteLogins: {
title: 'אישורי אתרים',
noSavedLogins: 'אין התחברויות שמורות.',
user: 'משתמש: {{username}}',
deleteCredential: 'מחיקת אישור',
deletedCredential: 'אישור נמחק',
deleteFailed: 'לא ניתן למחוק אישור: {{detail}}',
addTitle: 'הוספת אישורי אתר',
urlPattern: 'תבנית URL:',
urlPatternPlaceholder: 'לדוגמה *.example.com או example.com/downloads',
username: 'שם משתמש:',
password: 'סיסמה:',
usernamePlaceholder: 'שם משתמש',
passwordPlaceholder: 'סיסמה',
saving: 'שומר…',
addLogin: 'הוספת התחברות',
urlPatternRequired: 'נדרשת תבנית URL.',
urlPatternWhitespace: 'תבנית URL אינה יכולה להכיל רווחים.',
usernameRequired: 'נדרש שם משתמש.',
passwordRequired: 'נדרשת סיסמה.',
accessBeforeSave: 'הענק גישה לאחסון האישורים לפני שמירת התחברות לאתר.',
saveFailed: 'שמירת אישור האתר באופן מאובטח נכשלה.',
addedCredential: 'אישור אתר נוסף',
},
power: {
title: 'ניהול צריכת חשמל',
preventSleep: 'מנע שינה של המערכת בזמן שהורדות פעילות',
preventSleepDescription: 'התצוגה עדיין עשויה להכבות. Firelink משאיר את המכשיר ער רק במידה המספקת להשלמת העברות פעילות.',
},
engine: {
title: 'מוריד מדיה ומנועים',
description: 'תוצאות מוצלחות נמצאות בשימוש חוזר עבור הפעלה זו של האפליקציה. בדיקה מחדש מפעילה אימות אמיתי מחדש.',
recheck: 'בדיקה מחדש של מנועים',
coreDownloader: 'מנוע ההורדה הראשי (aria2)',
version: 'גרסה:',
status: 'סטטוס:',
mediaExtractors: 'מחלצי מדיה',
browserCookiesSource: 'מקור עוגיות דפדפן (Browser Cookies):',
cookieDescription: 'yt-dlp קורא עוגיות דפדפן כדי לעקוף הגבלות הורדת וידאו או כדי לגשת למדיה מוגבלת. Firelink אינו שומר עוגיות דפדפן.',
none: 'ללא',
},
integrations: {
title: 'חיבור תוסף דפדפן',
description: 'לכוד הורדות ישירות מהדפדפן שלך בשלושה שלבים פשוטים.',
portablePairingEnabled: 'צימוד נייד מופעל',
pairingTokenPersisted: 'אסימון צימוד נשמר',
portablePersistedDescription: 'אסימון הצימוד שלך נשמר יחד עם תיקיית Firelink ניידת זו ויישאר בעת הזזת התיקייה. התייחס לתיקייה כרגישה.',
systemPersistedDescription: 'אסימון הצימוד שלך נשמר באחסון האישורים של המערכת ויישאר לאחר הפעלות מחדש.',
portablePairingAvailable: 'צימוד נייד זמין',
credentialStorageNeeded: 'נדרש אחסון אישורים',
portableAvailableDescription: 'אסימון הצימוד שלך נשמר יחד עם תיקיית Firelink ניידת זו ויישאר לאחר הפעלות מחדש. הפעל אותו כאן כדי לעיין באזהרת האחסון הנייד.',
credentialStorageDescription: 'Firelink זקוק לגישה לאחסון האישורים של מערכת זו כדי לשמור בבטחה את אסימון הצימוד שלך לאחר הפעלות מחדש של האפליקציה. כרגע, התוסף שלך יישאר מחובר עבור הפעלה זו בלבד.',
reviewPortablePairing: 'עיון בצימוד נייד',
grantCredentialAccess: 'הענקת גישה לאישורים',
copyToken: 'העתקת אסימון',
tokenDescription: 'אסימון מאובטח זה מאשר את תוסף הדפדפן שלך.',
tokenCopied: 'האסימון הועתק ללוח!',
tokenCopyFailed: 'לא ניתן להעתיק את האסימון: {{detail}}',
pairingTokenRegenerated: 'אסימון הצימוד נוצר מחדש',
regenerateFailed: 'לא ניתן ליצור מחדש אסימון צימוד: {{detail}}',
getExtension: 'קבלת התוסף',
extensionDescription: 'התקן את Firelink Companion עבור דפדפני Firefox או Chromium.',
firefoxAddons: 'הרחבות Firefox',
chromiumZip: 'Chromium ZIP',
pasteConnect: 'הדבקה וחיבור',
pasteConnectDescription: 'לחץ על סמל Firelink בסרגל הכלים של הדפדפן שלך והדבק את האסימון שהועתק.',
extensionServerStatus: 'סטטוס שרת התוסף:',
listening: '● מאזין ב-127.0.0.1:{{port}}',
unavailable: '● שרת לא זמין',
},
about: {
firelinkIcon: 'סמל Firelink',
unknown: 'לא ידוע',
version: 'גרסה {{version}}',
description: 'מנהל הורדות חוצה-פלטפורמות עבור macOS, Windows ו-Linux.',
sourceCode: 'קוד מקור',
source: 'מקור',
upToDate: 'Firelink {{version}} מעודכן.',
updateAvailable: 'Firelink {{version}} זמין.',
viewRelease: 'הצגת שחרור',
updates: 'עדכונים',
checkForUpdates: 'בדוק עדכונים',
updateDescription: 'Firelink בודק מהדורות ב-GitHub עבור גרסאות חדשות.',
checking: 'בודק...',
checkNow: 'בדוק כעת',
automaticUpdates: 'בדוק עדכונים אוטומטית',
createdBy: 'נוצר על ידי NimBold',
mitLicense: 'רישיון MIT',
credits: 'קרדיטים: aria2, yt-dlp, FFmpeg ו-Deno.',
copyright: 'כל הזכויות שמורות © 2026 NimBold.',
},
},
status: {
ready: 'מוכן',
active: '{{count}} פעילים',
queued: '{{count}} בתור',
done: '{{count}} הושלמו',
},
} as const;
export default he;
+728
View File
@@ -0,0 +1,728 @@
const ru = {
navigation: {
library: 'Библиотека',
filters: {
all: 'Все',
active: 'Активные',
completed: 'Завершенные',
unfinished: 'Незавершенные',
},
categories: {
musics: 'Музыка',
movies: 'Фильмы',
compressed: 'Архивы',
documents: 'Документы',
pictures: 'Изображения',
applications: 'Приложения',
other: 'Другое',
},
folders: 'Папки',
queues: 'Очереди',
tools: 'Инструменты',
scheduler: 'Планировщик',
speedLimiter: 'Ограничение скорости',
logs: 'Журналы',
settings: 'Настройки',
},
actions: {
hideSidebar: 'Скрыть боковую панель',
showSidebar: 'Показать боковую панель',
resizeSidebar: 'Изменить размер боковой панели',
cancel: 'Отмена',
continue: 'Продолжить',
dismissNotification: 'Скрыть уведомление',
queueName: 'Имя очереди',
addNewQueue: 'Добавить новую очередь',
startQueue: 'Запустить очередь',
pauseQueue: 'Приостановить очередь',
renameQueue: 'Переименовать очередь',
deleteQueue: 'Удалить очередь',
},
dialogs: {
errorBoundary: {
title: 'Что-то пошло не так.',
description: 'Произошла критическая ошибка в дереве компонентов React. Нижеприведенные сведения об ошибке могут помочь определить ее причину.',
reload: 'Перезагрузить приложение',
},
duplicateDownloads: {
title: 'Обнаружены дубликаты загрузок',
description: 'Некоторые из добавляемых загрузок уже существуют в очереди или на диске. Пожалуйста, выберите, как разрешить эти конфликты.',
rename: 'Переименовать',
replace: 'Заменить',
skip: 'Пропустить',
},
removeDownload: {
title: 'Удалить загрузку',
confirmationSingle: 'Вы уверены, что хотите удалить этот элемент из списка? Вы также можете удалить сам файл с жесткого диска.',
confirmationMultiple: 'Вы уверены, что хотите удалить эти {{count}} элементов из списка? Вы также можете удалить сами файлы с жесткого диска.',
errorSummary: '{{succeeded}} удалено, {{failed}} с ошибкой: {{detail}}',
remove: 'Удалить',
deleteFile: 'Удалить файл',
},
},
window: {
controls: 'Элементы управления окном',
close: 'Закрыть',
minimize: 'Свернуть',
maximize: 'Развернуть',
},
downloads: {
actions: {
moveUp: 'Вверх',
moveDown: 'Вниз',
pause: 'Пауза',
start: 'Старт',
resume: 'Возобновить',
options: 'Опции',
},
size: {
downloadedOf: '{{downloaded}} загружено из {{total}} {{unit}}',
downloadedOfApproximate: '{{downloaded}} загружено из примерно {{total}} {{unit}}',
},
status: {
ready: 'Готово',
staged: 'В очереди',
queued: 'В очереди',
downloading: 'Загрузка',
processing: 'Обработка',
paused: 'Приостановлено',
completed: 'Завершено',
failed: 'Ошибка',
retrying: 'Повторная попытка',
},
values: {
processing: 'Обработка...',
muxing: 'Мультиплексирование...',
},
},
speedLimiter: {
title: 'Ограничение скорости',
unlimited: 'Без ограничений',
saveLimit: 'Сохранить лимит',
globalSpeedLimit: 'Глобальное ограничение скорости',
description: 'Применяется к новым и активным загрузкам aria2 и новым загрузкам мультимедиа yt-dlp. Явные ограничения для отдельных загрузок остаются привязанными к этим передачам.',
quickPresets: 'Быстрые пресеты',
removePreset: 'Удалить пресет {{value}} {{unit}}',
customPresetIn: 'Пользовательский пресет в {{unit}}',
addQuickPreset: 'Добавить быстрый пресет',
globalLimitSaved: 'Глобальный лимит сохранен на уровне {{value}} {{unit}}',
globalLimitDisabled: 'Глобальное ограничение скорости отключено',
saveFailed: 'Не удалось сохранить глобальное ограничение скорости: {{detail}}',
presetAlreadyExists: '{{value}} {{unit}} уже есть в быстрых пресетах',
presetAdded: 'Добавлен быстрый пресет {{value}} {{unit}}',
presetRemoved: 'Удален быстрый пресет {{value}} {{unit}}',
},
logs: {
title: 'Журналы',
entries: '({{count}} записей)',
collecting: 'Сбор',
off: 'Выкл',
allLevels: 'Все уровни',
levels: {
error: 'Ошибка',
warn: 'Предупреждение',
info: 'Информация',
debug: 'Отладка',
trace: 'Трассировка',
},
pauseLogging: 'Приостановить диагностическое логирование',
enableLogging: 'Включить диагностическое логирование',
clearDisplayed: 'Очистить отображаемые журналы',
export: 'Экспорт журналов',
exportButton: 'Экспортировать журналы',
logFiles: 'Файлы журналов',
localDiagnostics: 'Локальная диагностика:',
diagnosticsDescription: 'Сбор диагностики осуществляется по согласию, ограничен по объему и локален. Распространенные секреты, параметры URL-адресов и пути к домашним каталогам скрываются при отображении и экспорте. Ничего не загружается автоматически.',
noEntries: 'Сохраненных записей журнала пока нет.',
disabledDescription: 'Диагностическое логирование отключено. Существующие журналы поддержки появятся здесь, когда будут доступны.',
copy: 'Копировать',
copied: 'Скопировано в буфер обмена',
copyFailed: 'Не удалось скопировать в буфер обмена',
exported: 'Журналы поддержки экспортированы',
exportFailed: 'Не удалось экспортировать журналы: {{detail}}',
cleared: 'Журналы очищены',
clearFailed: 'Не удалось очистить журналы: {{detail}}',
enabled: 'Диагностическое логирование включено',
disabled: 'Диагностическое логирование отключено',
updateFailed: 'Не удалось обновить диагностическое логирование: {{detail}}',
},
scheduler: {
title: 'Планировщик',
running: 'Запущен',
disabled: 'Планировщик отключен',
noScheduledDay: 'Не выбран ни один день расписания',
unsavedChanges: 'Несохраненные изменения',
runNow: 'Запустить сейчас',
pause: 'Пауза',
saveSettings: 'Сохранить настройки',
timing: 'Время',
startTime: 'Время начала',
stopTime: 'Время окончания',
timingDescription: 'Если Firelink находится в спящем режиме во время начала, он запускает выбранные очереди при возобновлении работы позже в этот день, если время окончания еще не прошло.',
runEveryDay: 'Запускать каждый день',
queuesToSchedule: 'Очереди для планирования',
defaultQueue: 'Очередь по умолчанию',
afterCompletion: 'После завершения',
afterCompletionDescription: 'Выберите, что произойдет после завершения загрузок, начатых планировщиком.',
actionWarning: 'Это действие может прервать другую работу на компьютере. Firelink вызывает его сразу после завершения запланированной очереди.',
systemPermissions: 'Системные разрешения',
macPermissionDescription: 'Для спящего режима, перезагрузки и выключения требуются разрешения macOS Automation для System Events.',
permissionGranted: 'Разрешение Automation предоставлено',
permissionChecking: 'Проверка разрешения Automation...',
permissionMissing: 'Отсутствует разрешение Automation',
revokePermission: 'Отозвать разрешение',
grantPermission: 'Предоставить разрешение',
permissionAvailable: 'Разрешение Automation доступно.',
permissionMissingDetails: 'Отсутствует разрешение Automation. Включите Firelink в разделе Automation для System Events в System Settings.',
revokePermissionDetails: 'macOS не позволяет Firelink отзывать разрешение Automation напрямую. Отзовите доступ к System Events в System Settings, затем вернитесь в Firelink.',
requestingPermission: 'Запрос разрешения Automation...',
enablePermissionDetails: 'Включите Firelink в разделе Automation для System Events в System Settings, затем вернитесь в Firelink.',
systemActions: 'Системные действия',
windowsActionsDescription: 'Для спящего режима, перезагрузки и выключения используются системные привилегии Windows. Firelink сообщает о любом отклоненном действии при запуске; постоянное разрешение заранее не запрашивается.',
linuxActionsDescription: 'Для спящего режима, перезагрузки и выключения используется политика вашего рабочего стола и системы Linux. Firelink сообщает о любом отклоненном действии при запуске; постоянное разрешение заранее не запрашивается.',
validationDay: 'Выберите хотя бы один день для планировщика',
validationQueue: 'Выберите хотя бы одну очередь для планировщика',
validationStopTime: 'Время окончания должно быть позже времени начала',
saved: 'Настройки планировщика сохранены',
trackingOne: 'Отслеживание 1 запланированной загрузки',
trackingMany: 'Отслеживание {{count}} запланированных загрузок',
noStartableDownloads: 'Нет загрузок в выбранных очередях, которые можно запустить',
pausedOne: 'Приостановлена 1 активная загрузка',
pausedMany: 'Приостановлено {{count}} активных загрузок',
noActiveDownloads: 'Нет активных загрузок',
days: {
su: 'Вс',
mo: 'Пн',
tu: 'Вт',
we: 'Ср',
th: 'Чт',
fr: 'Пт',
sa: 'Сб',
},
postActions: {
none: 'Ничего не делать',
sleep: 'Спящий режим',
restart: 'Перезагрузка',
shutdown: 'Выключение',
},
},
properties: {
progress: 'Прогресс',
size: 'Размер',
speed: 'Скорость',
eta: 'Осталось',
connections: 'Соединения',
speedCap: 'Ограничение скорости',
category: 'Категория',
lastTry: 'Последняя попытка',
dateAdded: 'Дата добавления',
destination: 'Место назначения',
lastError: 'Последняя ошибка',
saved: ' (сохранено)',
defaultValue: ' (по умолчанию)',
savedTooltip: 'Сохранено для этой загрузки; изменения в настройках применяются к новым загрузкам.',
defaultTooltip: 'Используется текущее значение по умолчанию для новых загрузок.',
identityReadOnly: 'Идентификатор файла доступен только для чтения. Настройки передачи сохранены для повторной загрузки.',
transferSettings: 'Настройки передачи можно изменить после остановки или приостановки. Текущие передачи сохраняют свои существующие параметры бэкенда.',
download: 'Загрузка',
url: 'URL',
fileName: 'Имя файла',
saveLocation: 'Место сохранения',
select: 'Выбрать',
perFile: 'на файл',
useCurrentDefault: 'Использовать текущее значение по умолчанию ({{count}})',
savedPerDownload: 'Сохраняется для каждой загрузки. Значение по умолчанию из Настроек применяется к новым загрузкам.',
limit: 'Лимит',
siteLogin: 'Вход на сайт',
siteLoginRedownload: 'Вход на сайт для повторной загрузки',
matchingSiteLogin: 'Соответствующий вход на сайт',
customCredentials: 'Пользовательские учетные данные',
noLogin: 'Без входа',
useSavedLogin: 'Будет использовать сохраненный логин, если доступен.',
advancedTransfer: 'Расширенная передача',
advancedTransferRedownload: 'Расширенная передача для повторной загрузки',
checksum: 'Контрольная сумма',
verify: 'Проверить',
algorithm: 'Алгоритм',
digest: 'Хэш',
expectedDigest: 'Ожидаемый хэш',
cookies: 'Файлы cookie',
headers: 'Заголовки',
mirrors: 'Зеркала',
username: 'Имя пользователя',
password: 'Пароль',
enterValidUrl: 'Введите действительный URL.',
fileNameEmpty: 'Имя файла не может быть пустым.',
cancel: 'Отмена',
save: 'Сохранить',
},
keychain: {
title: 'Требуется доступ к хранилищу учетных данных',
stores: {
portable: 'портативная папка данных Firelink',
windows: 'Диспетчер учетных данных Windows',
linux: 'ваше хранилище учетных данных Linux',
macos: 'Связка ключей macOS',
system: "хранилище учетных данных этой системы",
siteCredentials: "системное хранилище учетных данных",
},
grantLabelPortable: 'Продолжить',
grantLabelMacos: 'Предоставить доступ',
grantLabelDefault: 'Включить безопасное хранилище',
description: 'Firelink использует расширение браузера для перехвата загрузок. Чтобы расширение оставалось связанным после перезапуска, Firelink сохраняет свой токен сопряжения в {{pairingStore}}. Дополнительные учетные данные сайтов сохраняются в {{siteCredentialStore}}.',
portableExplanation: 'Токен сопряжения переносится вместе с этой папкой. Учетные данные сайтов остаются в системном хранилище учетных данных; после предоставления вами доступа может появиться системный запрос.',
macosExplanation: 'macOS может показать запрос Связки ключей после того, как вы предоставите доступ.',
defaultExplanation: 'Обычно это происходит незаметно. Если служба учетных данных недоступна, Firelink покажет ошибку здесь, и расширение останется связанным только для этого сеанса.',
note: 'Примечание:',
portableNote: 'В портативном режиме в этой папке сохраняется только токен сопряжения. В нее не копируются пароли сайтов или учетные данные браузера.',
defaultNote: 'Firelink записывает только свою собственную выделенную запись учетных данных. Он не может получить доступ к другим сохраненным паролям или элементам учетных данных в вашей системе.',
hint: 'Подсказка:',
portableHint: 'Портативный токен сопряжения уже сохранен с этой папкой; вы можете включить его здесь или выбрать "Позже".',
defaultHint: 'Если вы выберете "Позже", расширение будет работать только для этого сеанса.',
enableFromSettings: 'Вы можете включить хранилище в любое время в Настройки > Интеграции.',
later: 'Позже',
enabling: 'Включение...',
timeout: 'Время ожидания запроса к хранилищу учетных данных истекло. Вы можете выбрать "Позже" и повторить попытку.',
unavailable: '{{store}} недоступно.',
accessRequired: 'Предоставьте доступ к хранилищу учетных данных перед повторным созданием токена сопряжения.',
storeUnavailable: 'Доступ к хранилищу учетных данных недоступен.',
},
app: {
settingsSaveFailed: 'Не удалось сохранить настройки. Проверьте права доступа к хранилищу и повторите попытку.',
systemActionCountdown: '{{action}} через 10 секунд.',
systemActionCancelled: 'Системное действие отменено, так как другая загрузка активна или находится в очереди.',
systemActionFailed: 'Запланированное системное действие не выполнено: {{detail}}',
downloadCompleteTitle: 'Загрузка завершена',
downloadCompleteBody: 'Загрузка {{fileName}} завершена.',
unknownFile: 'Файл',
downloadFailedTitle: 'Ошибка загрузки',
downloadFailedBody: 'Не удалось загрузить {{fileName}}.',
initializeDownloadsFailed: 'Не удалось инициализировать сохраненные загрузки: {{detail}}',
extensionDisconnected: 'Расширение браузера отключено, так как его токен сопряжения изменился.',
copyToken: 'Копировать токен',
integrations: 'Интеграции',
copyTokenFailed: 'Не удалось скопировать токен сопряжения: {{detail}}',
credentialPersistenceFailed: 'Безопасное сохранение учетных данных недоступно. Сопряжение с браузером работает только для этого сеанса: {{detail}}',
resumeDownloadsFailed: 'Не удалось возобновить сохраненные загрузки: {{detail}}',
updateAvailable: 'Доступна версия Firelink {{version}}.',
viewRelease: 'Посмотреть релиз',
sleepPreventionFailed: 'Не удалось обновить предотвращение спящего режима: {{detail}}',
schedulerNoQueues: 'Для планировщика не выбраны действительные очереди. Обновите настройки планировщика.',
schedulerPauseOneFailed: 'Планировщик не смог приостановить 1 загрузку.',
schedulerPauseManyFailed: 'Планировщик не смог приостановить {{count}} загрузок.',
scheduledIncomplete: 'Запланированные загрузки не были завершены полностью. Системное действие после очереди было пропущено.',
scheduledActionSkippedActive: 'Запланированное системное действие пропущено, так как другая загрузка активна или находится в очереди.',
notificationsDisabled: 'Системные уведомления отключены для Firelink.',
notificationsFailed: 'Не удалось настроить уведомления: {{detail}}',
},
sidebar: {
deleteQueueConfirm: 'Удалить очередь "{{name}}"? Незавершенные загрузки будут перемещены в Главную Очередь.',
deleteQueueFailed: 'Не удалось удалить очередь: {{detail}}',
queueNameEmpty: 'Имя очереди не может быть пустым',
queueNameExists: 'Очередь с таким именем уже существует',
startQueueFailed: 'Не удалось запустить очередь: {{detail}}',
pauseQueueFailed: 'Не удалось приостановить очередь: {{detail}}',
},
downloadTable: {
unknownQueue: 'Неизвестная очередь',
allDownloads: 'Все загрузки',
active: 'Активные',
completed: 'Завершенные',
unfinished: 'Незавершенные',
headers: {
fileName: 'Имя файла',
size: 'Размер',
status: 'Статус',
speed: 'Скорость',
eta: 'Осталось',
dateAdded: 'Дата добавления',
},
addDownload: 'Добавить загрузку',
resumeAll: 'Возобновить все',
pauseAll: 'Приостановить все',
queueEmpty: 'Очередь пуста',
noCompletedDownloads: 'Нет завершенных загрузок',
noDownloads: 'Нет загрузок',
queueEmptyDescription: 'Добавьте загрузки в эту очередь из меню элемента или окна Добавить.',
completedDescription: 'Завершенные загрузки появятся здесь.',
clickToAdd: 'Нажмите кнопку',
addButtonOr: 'или',
toAddDownloads: 'чтобы добавить загрузки',
nonResumableOne: '1 загрузка не поддерживает возобновление. Если вы приостановите ее, вам придется начать все сначала позже. Вы уверены, что хотите приостановить?',
nonResumableMany: '{{count}} загрузок не поддерживают возобновление. Если вы приостановите их, вам придется начать все сначала позже. Вы уверены, что хотите приостановить?',
interactionError: '{{message}}: {{detail}}',
openFileFailed: 'Не удалось открыть загруженный файл',
revealFileFailed: 'Не удалось показать загрузку в Finder',
pauseFailed: 'Не удалось приостановить загрузку',
resumeFailed: 'Не удалось возобновить {{fileName}}',
moveManyFailed: 'Не удалось переместить загрузки в очередь',
moveOneFailed: 'Не удалось переместить загрузку в очередь',
copyAddressesFailed: 'Не удалось скопировать адреса',
copyAddressFailed: 'Не удалось скопировать адрес',
copyPathFailed: 'Не удалось скопировать путь к файлу',
missingFileName: 'Имя файла отсутствует',
redownloadFailed: 'Ошибка повторной загрузки',
startResume: 'Старт/Возобновить',
addToQueue: 'Добавить в очередь',
copyAddress: 'Копировать адрес',
remove: 'Удалить',
open: 'Открыть',
showInFolder: 'Показать в папке',
pause: 'Пауза',
start: 'Старт',
resume: 'Возобновить',
redownload: 'Повторная загрузка',
copyFilePath: 'Копировать путь к файлу',
properties: 'Свойства',
backendRejectedStart: 'Бэкенд отклонил запрос на запуск/возобновление.',
transferActive: 'Невозможно изменить свойства во время активной передачи. Сначала приостановите ее.',
redownloadNotFound: 'Невозможно повторно загрузить: загрузка не найдена.',
redownloadActive: 'Невозможно повторно загрузить {{status}} загрузку. Сначала приостановите ее или дождитесь завершения.',
originalUrlMissing: 'Невозможно повторно загрузить: отсутствует исходный URL.',
pauseBeforeMove: 'Приостановите {{file}} перед перемещением в другую очередь.',
},
addDownloads: {
unknownSize: 'Неизвестный размер',
unknown: 'Неизвестно',
discardSetup: 'Отменить настройку этой загрузки?',
speedInvalid: 'Ограничение скорости должно быть больше нуля',
duplicateResolveFailed: 'Не удалось разрешить дубликаты загрузок: {{detail}}',
addedWithFailures: '{{added}} добавлено, {{failed}} с ошибкой. {{detail}}',
addedOne: 'Добавлена 1 загрузка',
addedMany: 'Добавлено {{count}} загрузок',
urlAlreadyQueued: 'URL уже в очереди',
destinationConflict: 'Другая выбранная загрузка использует это место назначения',
existingDownloadDestination: 'Существующая загрузка Firelink использует это место назначения',
fileExistsOnDisk: 'Файл существует на диске; переименуйте или пропустите, чтобы избежать удаления не связанных данных',
backendRejectedStart: 'Бэкенд отклонил запуск загрузки.',
playlistNoEntries: 'Плейлист не содержит доступных для загрузки записей',
invalidMediaMetadata: 'Недействительные метаданные мультимедиа или форматы не найдены',
noAvailableName: 'Не удалось найти доступное имя для {{file}}.',
pauseBeforeReplace: 'Приостановите {{file}} перед его заменой.',
cannotReplace: 'Невозможно заменить {{file}}: файл не принадлежит загрузке Firelink.',
downloadLinks: 'Ссылки на скачивание',
pastePlaceholder: 'Вставьте URL-адреса HTTP, HTTPS, FTP или SFTP здесь...\n\nДля загрузки мультимедиа вставляйте ссылки с YouTube, X, TikTok, Instagram, Reddit и т.д.',
playlistSummary: 'Плейлист “{{title}}”: {{loaded}} из {{total}} записей загружено{{truncated}}{{skipped}}',
safeEntryLimit: ' (достигнут безопасный лимит записей)',
selectedSummary: '{{ready}} выбрано готовых, {{fallback}} резервных, {{mediaRetry}} повторных медиа, {{blocked}} заблокированных',
clearSelection: 'Очистить выбор',
selectAll: 'Выбрать все',
refreshMetadata: 'Обновить метаданные',
files: 'Файлы',
required: 'Обязательно',
free: 'Свободно',
preview: 'Предпросмотр',
file: 'Файл',
size: 'Размер',
status: 'Статус',
downloadPreview: 'Предпросмотр загрузки',
noLinks: 'Ссылки еще не добавлены.',
selectItem: 'Выбрать {{file}}',
fetchingPlaylist: 'Получение плейлиста...',
fetching: 'Получение...',
unsafeUrl: 'Небезопасный URL',
playlistFailed: 'Ошибка плейлиста',
metadataFailed: 'Ошибка метаданных',
fallback: 'Резервный вариант',
invalid: 'Недействительно',
ready: 'Готово',
mediaFormat: 'Медиаформат',
playlistItem: 'Элемент плейлиста {{index}}',
fetchingMediaStreams: 'Получение медиапотоков...',
availableStreams: 'Доступные потоки',
availableMediaStreams: 'Доступные медиапотоки',
metadataUnavailable: 'Метаданные недоступны. Обновите метаданные перед добавлением этого мультимедиа.',
saveLocation: 'Место сохранения',
browse: 'Обзор',
categoryFolders: 'Файлы будут организованы в папки категорий.',
sharedFolder: 'Все выбранные загрузки будут использовать эту папку.',
transferSettings: 'Настройки передачи',
connectionsPerFile: 'Соединений на файл',
connectionsPerFileAria: 'Соединений на файл',
limitSpeedPerFile: 'Ограничить скорость на файл',
speedLimitPerFileAria: 'Ограничение скорости на файл',
authorization: 'Авторизация',
useAuthorization: 'Использовать авторизацию',
username: 'Имя пользователя',
password: 'Пароль',
advancedTransfer: 'Расширенная передача',
verifyChecksum: 'Проверить контрольную сумму',
checksumAlgorithm: 'Алгоритм контрольной суммы',
expectedDigest: 'Ожидаемый хэш',
headers: 'Заголовки',
requestHeaders: 'Заголовки запроса',
cookies: 'Файлы cookie',
cookiePlaceholder: 'имя=значение; другое=значение',
mirrors: 'Зеркала',
cancel: 'Отмена',
startDownloads: 'Начать загрузки',
addToQueue: 'Добавить в очередь',
pasteOneOrMore: 'Вставьте одну или несколько ссылок.',
selectAtLeastOne: 'Выберите хотя бы одну загрузку.',
correctInvalidOne: 'Исправьте или удалите {{count}} недействительный URL перед продолжением.',
correctInvalidFew: 'Исправьте или удалите {{count}} недействительных URL перед продолжением.',
correctInvalidMany: 'Исправьте или удалите {{count}} недействительных URL перед продолжением.',
waitingForMetadataOne: 'Ожидание метаданных для {{count}} загрузки.',
waitingForMetadataFew: 'Ожидание метаданных для {{count}} загрузок.',
waitingForMetadataMany: 'Ожидание метаданных для {{count}} загрузок.',
removeUnsafeOne: 'Удалите {{count}} небезопасный URL перед продолжением.',
removeUnsafeFew: 'Удалите {{count}} небезопасных URL перед продолжением.',
removeUnsafeMany: 'Удалите {{count}} небезопасных URL перед продолжением.',
mediaMetadataUnavailableSummaryOne: 'Метаданные мультимедиа недоступны для {{count}} элемента. Обновите метаданные перед добавлением.',
mediaMetadataUnavailableSummaryFew: 'Метаданные мультимедиа недоступны для {{count}} элементов. Обновите метаданные перед добавлением.',
mediaMetadataUnavailableSummaryMany: 'Метаданные мультимедиа недоступны для {{count}} элементов. Обновите метаданные перед добавлением.',
metadataUnavailableFallback: 'Метаданные недоступны. Загрузки все еще могут быть добавлены с использованием резервных сведений.',
fallbackReadyOne: '{{ready}} загрузка готова; для {{failed}} будет использовано резервное имя файла и неизвестный размер.',
fallbackReadyFew: '{{ready}} загрузки готовы; для {{failed}} будет использовано резервное имя файла и неизвестный размер.',
fallbackReadyMany: '{{ready}} загрузок готово; для {{failed}} будет использовано резервное имя файла и неизвестный размер.',
readyToAddOne: 'Готово к добавлению: {{count}} загрузка.',
readyToAddFew: 'Готово к добавлению: {{count}} загрузки.',
readyToAddMany: 'Готово к добавлению {{count}} загрузок.',
},
settings: {
tabs: {
downloads: 'Загрузки',
lookAndFeel: 'Внешний вид',
network: 'Сеть',
locations: 'Местоположения',
siteLogins: 'Учетные записи сайтов',
power: 'Питание',
engine: 'Движок',
integrations: 'Интеграции',
about: 'О программе',
},
common: {
thisOs: 'этой ОС',
checking: 'Проверка...',
ready: 'Готово',
errorMissing: 'Ошибка / Отсутствует',
hideTechnicalDetails: 'Скрыть технические детали',
showTechnicalDetails: 'Показать технические детали',
binary: 'Бинарный файл',
expected: 'Ожидается',
error: 'Ошибка',
tip: 'Подсказка',
stderr: 'stderr',
daemonProcessAlive: 'Процесс демона активен',
rpcReady: 'RPC готов',
rpcPort: 'Порт RPC',
daemonStderr: 'stderr демона',
packaging: 'Упаковка: PyInstaller onedir (требуется _internal)',
internalDirectoryFound: 'Директория _internal найдена',
pythonRuntimeFound: 'Среда выполнения Python найдена',
externalOpenFailed: 'Не удалось открыть {{label}}: {{detail}}',
updateUnexpectedResponse: 'Проверка обновлений вернула неожиданный ответ.',
updateUntrustedReleaseUrl: 'Проверка обновлений вернула ненадежный URL релиза.',
updateFailed: 'Ошибка проверки обновлений: {{detail}}',
},
downloads: {
defaultConnections: 'Соединения по умолчанию:',
defaultConnectionsDescription: 'Для новых загрузок; существующие элементы сохраняют свое значение',
parallelDownloads: 'Параллельные загрузки:',
parallelDownloadsDescription: 'Максимальное количество одновременных активных файлов',
automaticRetries: 'Автоматические повторы:',
automaticRetriesDescription: 'При сбое соединения',
systemNotification: 'Показывать системное уведомление по завершении загрузки',
systemNotificationDescription: 'Использует настройки уведомлений вашей операционной системы',
completionChime: 'Воспроизводить звуковой сигнал приложения по завершении',
completionChimeDescription: 'Дополнительный звук Firelink, независимый от системных уведомлений',
clipboardLinks: 'Добавлять ссылки из буфера обмена, когда Firelink становится активным',
clipboardLinksDescription: 'Открывает поддерживаемые скопированные ссылки в окне Добавить. По умолчанию отключено.',
},
lookAndFeel: {
language: 'Язык',
languageDescription: 'Выберите язык интерфейса Firelink.',
languageSystem: 'Язык системы',
languageEnglish: 'Английский',
languageChinese: 'Упрощённый китайский',
languageHebrew: 'Иврит',
languagePersian: 'Персидский',
languageUkrainian: 'Украинский',
languageRussian: 'Русский',
appTheme: 'Тема приложения',
theme: 'Тема',
ariaLabel: 'Тема приложения',
system: 'Системная',
light: 'Светлая',
dark: 'Темная',
dracula: 'Дракула',
nord: 'Норд',
systemAppearance: 'Система следует текущему светлому или темному оформлению {{platform}}.',
display: 'Отображение',
fontSize: 'Размер шрифта',
fontSizeDescription: 'Масштабирует список загрузок и компактные элементы управления.',
small: 'Мелкий',
standard: 'Стандартный',
large: 'Крупный',
listDensity: 'Плотность списка',
listDensityDescription: 'Изменяет высоту строки, интервалы и масштаб индикатора прогресса.',
compact: 'Компактный',
comfortable: 'Удобный',
relaxed: 'Свободный',
osIntegration: 'Интеграция с ОС',
dockBadge: 'Показывать значок на иконке Dock',
dockBadgeDescription: 'Отображает количество активных загрузок на иконке Firelink в Dock.',
menuBarIcon: 'Показывать иконку в строке меню',
statusIndicatorIcon: 'Показывать иконку индикатора состояния',
systemTrayIcon: 'Показывать иконку в системном трее',
macosTrayDescription: 'Обеспечивает быстрый доступ из строки меню macOS.',
windowsTrayDescription: 'Обеспечивает быстрый доступ из области уведомлений Windows.',
linuxTrayDescription: 'Обеспечивает быстрый доступ из системного трея рабочего стола или области состояния, если доступно.',
defaultTrayDescription: 'Обеспечивает быстрый доступ из области системного трея ОС, если доступно.',
},
network: {
proxy: 'Прокси',
mode: 'Режим',
modeDescription: 'Управляет использованием прокси для новых запросов загрузки.',
noProxy: 'Без прокси',
systemProxy: 'Системный прокси',
customProxy: 'Пользовательский прокси',
proxyHost: 'Хост прокси',
proxyHostDescription: 'Имя хоста, IP-адрес или URL-адрес HTTP-прокси.',
proxyHostPlaceholder: '127.0.0.1 или http://127.0.0.1',
proxyPort: 'Порт прокси',
proxyPortDescription: 'Допустимый диапазон от 1 до 65535.',
noProxyDescription: 'Загрузки игнорируют настроенные прокси.',
systemProxyDescription: 'Загрузки используют обнаруженный системный прокси {{platform}}. Для обычных загрузок файлов требуется конечная точка HTTP или HTTPS; загрузки мультимедиа могут использовать SOCKS.',
customProxyDescription: 'Загрузки используют настроенную конечную точку HTTP-прокси для получения метаданных и движков загрузки.',
invalidCustomProxy: 'Введите действительный хост и порт HTTP-прокси, чтобы включить пользовательский прокси.',
incompleteCustomProxy: 'Введите хост и порт прокси, чтобы включить пользовательский прокси.',
checkingSystemProxy: 'Проверка конфигурации системного прокси…',
detectedSystemProxy: 'Обнаружен системный прокси. Для обычных загрузок файлов требуется конечная точка HTTP или HTTPS; загрузки мультимедиа могут использовать SOCKS.',
noSystemProxy: 'Используемый системный прокси не обнаружен. Загрузки будут выполняться без прокси.',
systemProxyReadFailed: 'Не удалось прочитать конфигурацию системного прокси. Выберите Без прокси или повторите попытку, когда он будет доступен.',
identity: 'Идентификация',
customUserAgent: 'Пользовательский User-Agent',
userAgentDescription: 'Применяется к получению метаданных и движкам загрузки.',
userAgentPlaceholder: 'Оставьте пустым для значения по умолчанию Firelink',
userAgentOverrides: 'Переопределяет исходящий заголовок User-Agent. Оставьте пустым для значений по умолчанию Firelink.',
chromeWindows: 'Chrome (Windows)',
chromeMacos: 'Chrome (macOS)',
edgeWindows: 'Edge (Windows)',
firefoxWindows: 'Firefox (Windows)',
firefoxMacos: 'Firefox (macOS)',
safariMacos: 'Safari (macOS)',
windowsDesktop: 'Рабочий стол Windows',
macosDesktop: 'Рабочий стол macOS',
},
locations: {
baseDownloadFolder: 'Базовая папка загрузок',
baseDownloadFolderDescription: 'В этой папке создаются автоматические папки категорий.',
browse: 'Обзор',
baseFolderCreateFailed: 'Базовая папка сохранена, но папки категорий создать не удалось: {{detail}}',
baseFolderUpdated: 'Базовая папка загрузок обновлена',
askWhereToSave: 'Спрашивать, где сохранить, при добавлении загрузок',
rememberLastUsedDownloadDirectory: 'Запомнить последний использованный каталог загрузок',
rememberLastUsedDownloadDirectoryDescription: 'Использовать последнюю выбранную папку в окне Добавить для следующей загрузки до перезапуска приложения.',
automaticCategorySubfolders: 'Автоматически сохранять файлы во вложенные папки категорий',
automaticCategorySubfoldersDescription: 'Если выключено, автоматические загрузки используют базовую папку.',
categorySubfolders: 'Вложенные папки категорий',
relativeToBase: 'Относительно базовой папки',
disabled: 'Отключено',
categorySubfolder: 'Вложенная папка {{category}}',
customFolder: 'Пользовательская папка',
useAutomatic: 'Использовать автоматическую',
resetDefaults: 'Сбросить по умолчанию',
resetToast: 'Расположения категорий сброшены по умолчанию',
},
siteLogins: {
title: 'Учетные данные сайтов',
noSavedLogins: 'Нет сохраненных логинов.',
user: 'Пользователь: {{username}}',
deleteCredential: 'Удалить учетные данные',
deletedCredential: 'Учетные данные удалены',
deleteFailed: 'Не удалось удалить учетные данные: {{detail}}',
addTitle: 'Добавить учетные данные сайта',
urlPattern: 'Шаблон URL:',
urlPatternPlaceholder: 'напр. *.example.com или example.com/downloads',
username: 'Имя пользователя:',
password: 'Пароль:',
usernamePlaceholder: 'Имя пользователя',
passwordPlaceholder: 'Пароль',
saving: 'Сохранение…',
addLogin: 'Добавить логин',
urlPatternRequired: 'Шаблон URL обязателен.',
urlPatternWhitespace: 'Шаблон URL не может содержать пробелы.',
usernameRequired: 'Имя пользователя обязательно.',
passwordRequired: 'Пароль обязателен.',
accessBeforeSave: 'Предоставьте доступ к хранилищу учетных данных перед сохранением входа на сайт.',
saveFailed: 'Не удалось безопасно сохранить учетные данные сайта.',
addedCredential: 'Учетные данные сайта добавлены',
},
power: {
title: 'Управление питанием',
preventSleep: 'Предотвращать переход системы в спящий режим во время активных загрузок',
preventSleepDescription: 'Дисплей все еще может выключаться. Firelink только поддерживает активность устройства, достаточную для завершения активных передач.',
},
engine: {
title: 'Загрузчик мультимедиа и движки',
description: 'Успешные результаты повторно используются для этого сеанса приложения. Повторная проверка запускает реальную валидацию заново.',
recheck: 'Перепроверить движки',
coreDownloader: 'Ядро загрузчика (aria2)',
version: 'Версия:',
status: 'Статус:',
mediaExtractors: 'Медиаэкстракторы',
browserCookiesSource: 'Источник файлов cookie браузера:',
cookieDescription: 'yt-dlp читает файлы cookie браузера для обхода ограничений загрузки видео или доступа к ограниченному мультимедиа. Firelink не сохраняет файлы cookie браузера.',
none: 'Нет',
},
integrations: {
title: 'Подключить расширение браузера',
description: 'Перехватывайте загрузки прямо из вашего браузера за три простых шага.',
portablePairingEnabled: 'Портативное сопряжение включено',
pairingTokenPersisted: 'Токен сопряжения сохранен',
portablePersistedDescription: 'Ваш токен сопряжения сохранен с этой портативной папкой Firelink и останется при перемещении папки. Относитесь к папке как к конфиденциальной информации.',
systemPersistedDescription: 'Ваш токен сопряжения сохранен в системном хранилище учетных данных и останется после перезапуска.',
portablePairingAvailable: 'Доступно портативное сопряжение',
credentialStorageNeeded: 'Требуется хранилище учетных данных',
portableAvailableDescription: 'Ваш токен сопряжения сохранен с этой портативной папкой Firelink и останется после перезапуска. Включите его здесь, чтобы ознакомиться с предупреждением о портативном хранилище.',
credentialStorageDescription: "Firelink требуется доступ к системному хранилищу учетных данных для безопасного сохранения вашего токена сопряжения между перезапусками приложения. В настоящее время ваше расширение будет оставаться подключенным только для этого сеанса.",
reviewPortablePairing: 'Просмотреть портативное сопряжение',
grantCredentialAccess: 'Предоставить доступ к учетным данным',
copyToken: 'Копировать токен',
tokenDescription: 'Этот безопасный токен авторизует расширение вашего браузера.',
tokenCopied: 'Токен скопирован в буфер обмена!',
tokenCopyFailed: 'Не удалось скопировать токен: {{detail}}',
pairingTokenRegenerated: 'Токен сопряжения пересоздан',
regenerateFailed: 'Не удалось пересоздать токен сопряжения: {{detail}}',
getExtension: 'Получить расширение',
extensionDescription: 'Установите Firelink Companion для браузеров Firefox или Chromium.',
firefoxAddons: 'Дополнения Firefox',
chromiumZip: 'ZIP-архив Chromium',
pasteConnect: 'Вставить и подключить',
pasteConnectDescription: "Нажмите на иконку Firelink на панели инструментов браузера и вставьте скопированный токен.",
extensionServerStatus: 'Статус сервера расширения:',
listening: '● Прослушивание на 127.0.0.1:{{port}}',
unavailable: '● Сервер недоступен',
},
about: {
firelinkIcon: 'Иконка Firelink',
unknown: 'Неизвестно',
version: 'Версия {{version}}',
description: 'Кроссплатформенный менеджер загрузок для macOS, Windows и Linux.',
sourceCode: 'Исходный код',
source: 'Источник',
upToDate: 'У вас последняя версия Firelink {{version}}.',
updateAvailable: 'Доступна новая версия Firelink {{version}}.',
viewRelease: 'Посмотреть релиз',
updates: 'Обновления',
checkForUpdates: 'Проверить обновления',
updateDescription: 'Firelink проверяет GitHub Releases на наличие новых версий.',
checking: 'Проверка...',
checkNow: 'Проверить сейчас',
automaticUpdates: 'Автоматически проверять наличие обновлений',
createdBy: 'Создано NimBold',
mitLicense: 'Лицензия MIT',
credits: 'Благодарности: aria2, yt-dlp, FFmpeg и Deno.',
copyright: 'Авторские права © 2026 NimBold. Все права защищены.',
},
},
status: {
ready: 'Готово',
active: '{{count}} активных',
queued: '{{count}} в очереди',
done: '{{count}} завершено',
},
} as const;
export default ru;
+728
View File
@@ -0,0 +1,728 @@
const uk = {
navigation: {
library: 'Бібліотека',
filters: {
all: 'Усі',
active: 'Активні',
completed: 'Завершені',
unfinished: 'Незавершені',
},
categories: {
musics: 'Музика',
movies: 'Відео',
compressed: 'Архіви',
documents: 'Документи',
pictures: 'Зображення',
applications: 'Програми',
other: 'Інше',
},
folders: 'Папки',
queues: 'Черги',
tools: 'Інструменти',
scheduler: 'Планувальник',
speedLimiter: 'Обмеження швидкості',
logs: 'Журнали',
settings: 'Налаштування',
},
actions: {
hideSidebar: 'Сховати бічну панель',
showSidebar: 'Показати бічну панель',
resizeSidebar: 'Змінити розмір бічної панелі',
cancel: 'Скасувати',
continue: 'Продовжити',
dismissNotification: 'Закрити сповіщення',
queueName: 'Назва черги',
addNewQueue: 'Додати нову чергу',
startQueue: 'Запустити чергу',
pauseQueue: 'Призупинити чергу',
renameQueue: 'Перейменувати чергу',
deleteQueue: 'Видалити чергу',
},
dialogs: {
errorBoundary: {
title: 'Щось пішло не так.',
description: 'Сталася критична помилка в дереві компонентів React. Наведені нижче деталі помилки можуть допомогти виявити першопричину.',
reload: 'Перезавантажити програму',
},
duplicateDownloads: {
title: 'Виявлено дублікати',
description: 'Деякі із завантажень, які ви додаєте, вже існують у черзі або на диску. Будь ласка, виберіть, як вирішити ці конфлікти.',
rename: 'Перейменувати',
replace: 'Замінити',
skip: 'Пропустити',
},
removeDownload: {
title: 'Видалити завантаження',
confirmationSingle: 'Ви впевнені, що хочете видалити цей елемент зі списку? Ви також можете видалити сам файл з вашого жорсткого диска.',
confirmationMultiple: 'Ви впевнені, що хочете видалити ці {{count}} елементів зі списку? Ви також можете видалити самі файли з вашого жорсткого диска.',
errorSummary: '{{succeeded}} видалено, {{failed}} не вдалося: {{detail}}',
remove: 'Видалити',
deleteFile: 'Видалити файл',
},
},
window: {
controls: 'Керування вікном',
close: 'Закрити',
minimize: 'Згорнути',
maximize: 'Розгорнути',
},
downloads: {
actions: {
moveUp: 'Перемістити вгору',
moveDown: 'Перемістити вниз',
pause: 'Призупинити',
start: 'Запустити',
resume: 'Відновити',
options: 'Опції',
},
size: {
downloadedOf: '{{downloaded}} завантажено з {{total}} {{unit}}',
downloadedOfApproximate: '{{downloaded}} завантажено з приблизно {{total}} {{unit}}',
},
status: {
ready: 'Готово',
staged: 'У черзі',
queued: 'У черзі',
downloading: 'Завантаження',
processing: 'Обробка',
paused: 'Призупинено',
completed: 'Завершено',
failed: 'Помилка',
retrying: 'Повторна спроба',
},
values: {
processing: 'Обробка...',
muxing: 'Мультиплексування...',
},
},
speedLimiter: {
title: 'Обмеження швидкості',
unlimited: 'Необмежено',
saveLimit: 'Зберегти обмеження',
globalSpeedLimit: 'Глобальне обмеження швидкості',
description: 'Застосовується до нових і активних завантажень aria2 та нових медіазавантажень yt-dlp. Індивідуальні обмеження для кожного завантаження залишаються прикріпленими до цих завантажень.',
quickPresets: 'Швидкі пресети',
removePreset: 'Видалити пресет {{value}} {{unit}}',
customPresetIn: 'Свій пресет в {{unit}}',
addQuickPreset: 'Додати швидкий пресет',
globalLimitSaved: 'Глобальне обмеження збережено на рівні {{value}} {{unit}}',
globalLimitDisabled: 'Глобальне обмеження швидкості вимкнено',
saveFailed: 'Не вдалося зберегти глобальне обмеження швидкості: {{detail}}',
presetAlreadyExists: '{{value}} {{unit}} вже є у швидких пресетах',
presetAdded: 'Додано швидкий пресет {{value}} {{unit}}',
presetRemoved: 'Видалено швидкий пресет {{value}} {{unit}}',
},
logs: {
title: 'Журнали',
entries: '({{count}} записів)',
collecting: 'Збір даних',
off: 'Вимкнено',
allLevels: 'Усі рівні',
levels: {
error: 'Помилка',
warn: 'Попередження',
info: 'Інфо',
debug: 'Дебаг',
trace: 'Трейс',
},
pauseLogging: 'Призупинити діагностичне логування',
enableLogging: 'Увімкнути діагностичне логування',
clearDisplayed: 'Очистити відображені журнали',
export: 'Експортувати журнали',
exportButton: 'Експортувати журнали',
logFiles: 'Файли журналів',
localDiagnostics: 'Локальна діагностика:',
diagnosticsDescription: 'Збір діагностичних даних є добровільним, обмеженим і локальним. Звичайні секрети, запити URL та шляхи до домашнього каталогу приховуються під час відображення та експорту. Нічого не завантажується автоматично.',
noEntries: 'Збережених записів журналів поки немає.',
disabledDescription: 'Діагностичне логування вимкнено. Існуючі журнали підтримки з\'являться тут, коли будуть доступні.',
copy: 'Копіювати',
copied: 'Скопійовано в буфер обміну',
copyFailed: 'Не вдалося скопіювати в буфер обміну',
exported: 'Журнали підтримки експортовано',
exportFailed: 'Не вдалося експортувати журнали: {{detail}}',
cleared: 'Журнали очищено',
clearFailed: 'Не вдалося очистити журнали: {{detail}}',
enabled: 'Діагностичне логування увімкнено',
disabled: 'Діагностичне логування вимкнено',
updateFailed: 'Не вдалося оновити діагностичне логування: {{detail}}',
},
scheduler: {
title: 'Планувальник',
running: 'Працює',
disabled: 'Планувальник вимкнено',
noScheduledDay: 'Не вибрано запланованого дня',
unsavedChanges: 'Незбережені зміни',
runNow: 'Запустити зараз',
pause: 'Призупинити',
saveSettings: 'Зберегти налаштування',
timing: 'Час',
startTime: 'Час початку',
stopTime: 'Час зупинки',
timingDescription: 'Якщо Firelink знаходиться в режимі сну на момент початку, він запустить вибрані черги після повернення пізніше того ж дня, якщо час зупинки ще не минув.',
runEveryDay: 'Запускати щодня',
queuesToSchedule: 'Черги для планування',
defaultQueue: 'Черга за замовчуванням',
afterCompletion: 'Після завершення',
afterCompletionDescription: 'Виберіть, що станеться після завершення завантажень, розпочатих планувальником.',
actionWarning: 'Ця дія може перервати іншу роботу на комп\'ютері. Firelink викликає її відразу після завершення запланованої черги.',
systemPermissions: 'Системні дозволи',
macPermissionDescription: 'Сон, перезавантаження та вимкнення вимагають дозволу на автоматизацію macOS для системних подій (System Events).',
permissionGranted: 'Дозвіл на автоматизацію надано',
permissionChecking: 'Перевірка дозволу на автоматизацію...',
permissionMissing: 'Відсутній дозвіл на автоматизацію',
revokePermission: 'Відкликати дозвіл',
grantPermission: 'Надати дозвіл',
permissionAvailable: 'Дозвіл на автоматизацію доступний.',
permissionMissingDetails: 'Відсутній дозвіл на автоматизацію. Увімкніть Firelink у розділі Автоматизація для системних подій у Налаштуваннях системи.',
revokePermissionDetails: 'macOS не дозволяє Firelink безпосередньо відкликати дозвіл на автоматизацію. Відкличте доступ до системних подій у Налаштуваннях системи, а потім поверніться до Firelink.',
requestingPermission: 'Запит дозволу на автоматизацію...',
enablePermissionDetails: 'Увімкніть Firelink у розділі Автоматизація для системних подій у Налаштуваннях системи, а потім поверніться до Firelink.',
systemActions: 'Системні дії',
windowsActionsDescription: 'Сон, перезавантаження та вимкнення використовують системні привілеї Windows. Firelink повідомляє про будь-яку відхилену дію під час її виконання; жодні постійні дозволи заздалегідь не вимагаються.',
linuxActionsDescription: 'Сон, перезавантаження та вимкнення використовують ваш робочий стіл Linux та системну політику. Firelink повідомляє про будь-яку відхилену дію під час її виконання; жодні постійні дозволи заздалегідь не вимагаються.',
validationDay: 'Виберіть принаймні один день для планувальника',
validationQueue: 'Виберіть принаймні одну чергу для планувальника',
validationStopTime: 'Час зупинки має бути пізнішим за час початку',
saved: 'Налаштування планувальника збережено',
trackingOne: 'Відстеження 1 запланованого завантаження',
trackingMany: 'Відстеження {{count}} запланованих завантажень',
noStartableDownloads: 'Жодне завантаження у вибраних чергах не може бути запущено',
pausedOne: 'Призупинено 1 активне завантаження',
pausedMany: 'Призупинено {{count}} активних завантажень',
noActiveDownloads: 'Немає активних завантажень',
days: {
su: 'Нд',
mo: 'Пн',
tu: 'Вв',
we: 'Ср',
th: 'Чт',
fr: 'Пт',
sa: 'Сб',
},
postActions: {
none: 'Нічого не робити',
sleep: 'Сон',
restart: 'Перезавантажити',
shutdown: 'Вимкнути',
},
},
properties: {
progress: 'Прогрес',
size: 'Розмір',
speed: 'Швидкість',
eta: 'Залишилось',
connections: 'З\'єднання',
speedCap: 'Обмеження швидкості',
category: 'Категорія',
lastTry: 'Остання спроба',
dateAdded: 'Дата додавання',
destination: 'Призначення',
lastError: 'Остання помилка',
saved: ' (збережено)',
defaultValue: ' (за замовчуванням)',
savedTooltip: 'Збережено для цього завантаження; зміни в налаштуваннях застосовуються до нових завантажень.',
defaultTooltip: 'Використовується поточне значення за замовчуванням для нових завантажень.',
identityReadOnly: 'Ідентифікатор файлу доступний лише для читання. Налаштування передачі збережено для повторного завантаження.',
transferSettings: 'Налаштування передачі можна змінити після зупинки або призупинення. Поточні передачі зберігають свої існуючі налаштування бекенду.',
download: 'Завантаження',
url: 'URL',
fileName: 'Ім\'я файлу',
saveLocation: 'Місце збереження',
select: 'Вибрати',
perFile: 'на файл',
useCurrentDefault: 'Використовувати поточне за замовчуванням ({{count}})',
savedPerDownload: 'Збережено для кожного завантаження. Значення за замовчуванням з Налаштувань застосовується до нових завантажень.',
limit: 'Обмеження',
siteLogin: 'Вхід на сайт',
siteLoginRedownload: 'Вхід на сайт для повторного завантаження',
matchingSiteLogin: 'Відповідний логін сайту',
customCredentials: 'Власні облікові дані',
noLogin: 'Без входу',
useSavedLogin: 'Буде використано збережений логін, якщо доступний.',
advancedTransfer: 'Розширені параметри передачі',
advancedTransferRedownload: 'Розширені параметри передачі для повторного завантаження',
checksum: 'Контрольна сума',
verify: 'Перевірити',
algorithm: 'Алгоритм',
digest: 'Хеш',
expectedDigest: 'Очікуваний хеш',
cookies: 'Файли cookie',
headers: 'Заголовки',
mirrors: 'Дзеркала',
username: 'Ім\'я користувача',
password: 'Пароль',
enterValidUrl: 'Введіть дійсну URL-адресу.',
fileNameEmpty: 'Ім\'я файлу не може бути порожнім.',
cancel: 'Скасувати',
save: 'Зберегти',
},
keychain: {
title: 'Потрібен доступ до сховища облікових даних',
stores: {
portable: 'портативна папка даних Firelink',
windows: 'Диспетчер облікових даних Windows',
linux: 'ваше сховище облікових даних Linux',
macos: 'В\'язка ключів macOS (Keychain)',
system: "сховище облікових даних цієї системи",
siteCredentials: "сховище облікових даних системи",
},
grantLabelPortable: 'Продовжити',
grantLabelMacos: 'Надати доступ',
grantLabelDefault: 'Увімкнути безпечне сховище',
description: 'Firelink використовує розширення браузера для захоплення завантажень. Щоб зберегти розширення підключеним після перезапуску, Firelink зберігає свій токен підключення у {{pairingStore}}. Необов\'язкові облікові дані сайтів зберігаються в {{siteCredentialStore}}.',
portableExplanation: 'Токен підключення є портативним і зберігається в цій папці. Облікові дані сайтів залишаються в системному сховищі; після надання доступу може з\'явитися системний запит.',
macosExplanation: 'macOS може показати запит до В\'язки ключів після того, як ви надасте доступ.',
defaultExplanation: 'Зазвичай це виконується у фоновому режимі. Якщо служба облікових даних недоступна, Firelink покаже помилку тут, а розширення залишиться підключеним лише для цього сеансу.',
note: 'Примітка:',
portableNote: 'У портативному режимі в цій папці зберігається лише токен підключення. Він не копіює паролі сайтів або облікові дані браузера.',
defaultNote: 'Firelink записує лише свій власний запис облікових даних. Він не має доступу до інших збережених паролів або елементів облікових даних у вашій системі.',
hint: 'Підказка:',
portableHint: 'Портативний токен підключення вже збережено в цій папці; ви можете увімкнути його тут або вибрати "Пізніше".',
defaultHint: 'Якщо ви виберете "Пізніше", розширення працюватиме лише для цього сеансу.',
enableFromSettings: 'Ви можете увімкнути сховище будь-коли в Налаштування > Інтеграції.',
later: 'Пізніше',
enabling: 'Увімкнення...',
timeout: 'Час очікування запиту до сховища облікових даних вичерпано. Ви можете вибрати "Пізніше" і спробувати ще раз.',
unavailable: '{{store}} недоступно.',
accessRequired: 'Надайте доступ до сховища облікових даних перед повторною генерацією токена підключення.',
storeUnavailable: 'Доступ до сховища облікових даних недоступний.',
},
app: {
settingsSaveFailed: 'Не вдалося зберегти налаштування. Перевірте дозволи сховища та спробуйте ще раз.',
systemActionCountdown: '{{action}} через 10 секунд.',
systemActionCancelled: 'Системна дія скасована, оскільки активне або в черзі інше завантаження.',
systemActionFailed: 'Не вдалося виконати заплановану системну дію: {{detail}}',
downloadCompleteTitle: 'Завантаження завершено',
downloadCompleteBody: 'Завантаження {{fileName}} завершено.',
unknownFile: 'Файл',
downloadFailedTitle: 'Помилка завантаження',
downloadFailedBody: 'Не вдалося завантажити {{fileName}}.',
initializeDownloadsFailed: 'Не вдалося ініціалізувати збережені завантаження: {{detail}}',
extensionDisconnected: 'Розширення браузера відключено, оскільки його токен підключення змінився.',
copyToken: 'Скопіювати токен',
integrations: 'Інтеграції',
copyTokenFailed: 'Не вдалося скопіювати токен підключення: {{detail}}',
credentialPersistenceFailed: 'Безпечне збереження облікових даних недоступне. Підключення браузера працює лише для цього сеансу: {{detail}}',
resumeDownloadsFailed: 'Не вдалося відновити збережені завантаження: {{detail}}',
updateAvailable: 'Доступна нова версія Firelink {{version}}.',
viewRelease: 'Переглянути реліз',
sleepPreventionFailed: 'Не вдалося оновити запобігання переходу в сплячий режим: {{detail}}',
schedulerNoQueues: 'Для планувальника не вибрано жодної дійсної черги. Оновіть налаштування Планувальника.',
schedulerPauseOneFailed: 'Планувальник не зміг призупинити 1 завантаження.',
schedulerPauseManyFailed: 'Планувальник не зміг призупинити {{count}} завантажень.',
scheduledIncomplete: 'Заплановані завантаження не були завершені повністю. Системну дію після виконання черги пропущено.',
scheduledActionSkippedActive: 'Заплановану системну дію пропущено, оскільки інше завантаження активне або перебуває в черзі.',
notificationsDisabled: 'Системні сповіщення вимкнено для Firelink.',
notificationsFailed: 'Не вдалося налаштувати сповіщення: {{detail}}',
},
sidebar: {
deleteQueueConfirm: 'Видалити чергу "{{name}}"? Її незавершені завантаження перемістяться до Головної черги (Main Queue).',
deleteQueueFailed: 'Не вдалося видалити чергу: {{detail}}',
queueNameEmpty: 'Назва черги не може бути порожньою',
queueNameExists: 'Черга з такою назвою вже існує',
startQueueFailed: 'Не вдалося запустити чергу: {{detail}}',
pauseQueueFailed: 'Не вдалося призупинити чергу: {{detail}}',
},
downloadTable: {
unknownQueue: 'Невідома черга',
allDownloads: 'Усі завантаження',
active: 'Активні',
completed: 'Завершені',
unfinished: 'Незавершені',
headers: {
fileName: 'Ім\'я файлу',
size: 'Розмір',
status: 'Статус',
speed: 'Швидкість',
eta: 'Залишилось',
dateAdded: 'Дата додавання',
},
addDownload: 'Додати завантаження',
resumeAll: 'Відновити всі',
pauseAll: 'Призупинити всі',
queueEmpty: 'Черга порожня',
noCompletedDownloads: 'Немає завершених завантажень',
noDownloads: 'Немає завантажень',
queueEmptyDescription: 'Додайте завантаження до цієї черги через меню елемента або у вікні "Додати".',
completedDescription: 'Тут з\'являтимуться завершені завантаження.',
clickToAdd: 'Натисніть',
addButtonOr: 'або',
toAddDownloads: 'щоб додати завантаження',
nonResumableOne: '1 завантаження не підтримує відновлення. Якщо ви призупините його, вам доведеться почати спочатку пізніше. Ви впевнені, що хочете призупинити?',
nonResumableMany: '{{count}} завантажень не підтримують відновлення. Якщо ви призупините їх, вам доведеться почати спочатку пізніше. Ви впевнені, що хочете призупинити?',
interactionError: '{{message}}: {{detail}}',
openFileFailed: 'Не вдалося відкрити завантажений файл',
revealFileFailed: 'Не вдалося показати завантаження в Finder / Провіднику',
pauseFailed: 'Не вдалося призупинити завантаження',
resumeFailed: 'Не вдалося відновити {{fileName}}',
moveManyFailed: 'Не вдалося перемістити завантаження до черги',
moveOneFailed: 'Не вдалося перемістити завантаження до черги',
copyAddressesFailed: 'Не вдалося скопіювати адреси',
copyAddressFailed: 'Не вдалося скопіювати адресу',
copyPathFailed: 'Не вдалося скопіювати шлях до файлу',
missingFileName: 'Ім\'я файлу відсутнє',
redownloadFailed: 'Не вдалося повторно завантажити',
startResume: 'Запустити/Відновити',
addToQueue: 'Додати до черги',
copyAddress: 'Скопіювати адресу',
remove: 'Видалити',
open: 'Відкрити',
showInFolder: 'Показати в папці',
pause: 'Призупинити',
start: 'Запустити',
resume: 'Відновити',
redownload: 'Завантажити повторно',
copyFilePath: 'Скопіювати шлях до файлу',
properties: 'Властивості',
backendRejectedStart: 'Бекенд відхилив запит на запуск/відновлення.',
transferActive: 'Неможливо змінити властивості під час активної передачі. Спочатку призупиніть її.',
redownloadNotFound: 'Неможливо завантажити повторно: завантаження не знайдено.',
redownloadActive: 'Неможливо повторно завантажити файл у статусі {{status}}. Спочатку призупиніть або дочекайтеся завершення.',
originalUrlMissing: 'Неможливо завантажити повторно: оригінальна URL-адреса відсутня.',
pauseBeforeMove: 'Призупиніть {{file}} перед переміщенням його до іншої черги.',
},
addDownloads: {
unknownSize: 'Невідомий розмір',
unknown: 'Невідомо',
discardSetup: 'Відкинути це налаштування завантаження?',
speedInvalid: 'Обмеження швидкості має бути більшим за нуль',
duplicateResolveFailed: 'Не вдалося вирішити конфлікт дублікатів завантажень: {{detail}}',
addedWithFailures: '{{added}} додано, {{failed}} не вдалося. {{detail}}',
addedOne: '1 завантаження додано',
addedMany: '{{count}} завантажень додано',
urlAlreadyQueued: 'URL-адреса вже в черзі',
destinationConflict: 'Інше вибране завантаження використовує це місце призначення',
existingDownloadDestination: 'Існуюче завантаження Firelink використовує це місце призначення',
fileExistsOnDisk: 'Файл існує на диску; перейменуйте або пропустіть, щоб уникнути видалення незв\'язаних даних',
backendRejectedStart: 'Бекенд відхилив початок завантаження.',
playlistNoEntries: 'Плейлист не містить елементів для завантаження',
invalidMediaMetadata: 'Недійсні метадані медіа або не знайдено форматів',
noAvailableName: 'Не вдалося знайти доступне ім\'я для {{file}}.',
pauseBeforeReplace: 'Призупиніть {{file}} перед заміною.',
cannotReplace: 'Неможливо замінити {{file}}: файл не належить до завантажень Firelink.',
downloadLinks: 'Посилання для завантаження',
pastePlaceholder: 'Вставте URL-адреси HTTP, HTTPS, FTP або SFTP сюди...\n\nДля медіазавантажень вставте посилання з YouTube, X, TikTok, Instagram, Reddit тощо.',
playlistSummary: 'Плейлист “{{title}}”: {{loaded}}{{total}} елементів завантажено{{truncated}}{{skipped}}',
safeEntryLimit: ' (досягнуто безпечного ліміту елементів)',
selectedSummary: '{{ready}} вибрано готових, {{fallback}} резервних, {{mediaRetry}} повторних медіа, {{blocked}} заблоковано',
clearSelection: 'Очистити вибір',
selectAll: 'Вибрати всі',
refreshMetadata: 'Оновити метадані',
files: 'Файли',
required: 'Обов\'язково',
free: 'Вільно',
preview: 'Попередній перегляд',
file: 'Файл',
size: 'Розмір',
status: 'Статус',
downloadPreview: 'Попередній перегляд завантаження',
noLinks: 'Поки що немає доданих посилань.',
selectItem: 'Вибрати {{file}}',
fetchingPlaylist: 'Отримання плейлиста...',
fetching: 'Отримання...',
unsafeUrl: 'Небезпечна URL-адреса',
playlistFailed: 'Помилка плейлиста',
metadataFailed: 'Помилка метаданих',
fallback: 'Резервний варіант',
invalid: 'Недійсно',
ready: 'Готово',
mediaFormat: 'Формат медіа',
playlistItem: 'Елемент плейлиста {{index}}',
fetchingMediaStreams: 'Отримання медіапотоків...',
availableStreams: 'Доступні потоки',
availableMediaStreams: 'Доступні медіапотоки',
metadataUnavailable: 'Метадані недоступні. Оновіть метадані перед додаванням цього медіа.',
saveLocation: 'Місце збереження',
browse: 'Огляд',
categoryFolders: 'Файли будуть організовані в папки категорій.',
sharedFolder: 'Усі вибрані завантаження використовуватимуть цю папку.',
transferSettings: 'Налаштування передачі',
connectionsPerFile: 'З\'єднань на файл',
connectionsPerFileAria: 'З\'єднань на файл',
limitSpeedPerFile: 'Обмеження швидкості на файл',
speedLimitPerFileAria: 'Обмеження швидкості на файл',
authorization: 'Авторизація',
useAuthorization: 'Використовувати авторизацію',
username: 'Ім\'я користувача',
password: 'Пароль',
advancedTransfer: 'Розширені параметри передачі',
verifyChecksum: 'Перевірити контрольну суму',
checksumAlgorithm: 'Алгоритм контрольної суми',
expectedDigest: 'Очікуваний хеш',
headers: 'Заголовки',
requestHeaders: 'Заголовки запиту',
cookies: 'Файли cookie',
cookiePlaceholder: 'name=value; other=value',
mirrors: 'Дзеркала',
cancel: 'Скасувати',
startDownloads: 'Почати завантаження',
addToQueue: 'Додати до черги',
pasteOneOrMore: 'Вставте одне або кілька посилань.',
selectAtLeastOne: 'Виберіть хоча б одне завантаження.',
correctInvalidOne: 'Виправте або видаліть {{count}} недійсну URL-адресу перед продовженням.',
correctInvalidFew: 'Виправте або видаліть {{count}} недійсні URL-адреси перед продовженням.',
correctInvalidMany: 'Виправте або видаліть {{count}} недійсні URL-адреси перед продовженням.',
waitingForMetadataOne: 'Очікування метаданих для {{count}} завантаження.',
waitingForMetadataFew: 'Очікування метаданих для {{count}} завантажень.',
waitingForMetadataMany: 'Очікування метаданих для {{count}} завантажень.',
removeUnsafeOne: 'Видаліть {{count}} небезпечну URL-адресу перед продовженням.',
removeUnsafeFew: 'Видаліть {{count}} небезпечні URL-адреси перед продовженням.',
removeUnsafeMany: 'Видаліть {{count}} небезпечні URL-адреси перед продовженням.',
mediaMetadataUnavailableSummaryOne: 'Метадані медіа недоступні для {{count}} елемента. Оновіть метадані перед додаванням.',
mediaMetadataUnavailableSummaryFew: 'Метадані медіа недоступні для {{count}} елементів. Оновіть метадані перед додаванням.',
mediaMetadataUnavailableSummaryMany: 'Метадані медіа недоступні для {{count}} елементів. Оновіть метадані перед додаванням.',
metadataUnavailableFallback: 'Метадані недоступні. Завантаження все ще можна додати, використовуючи резервні дані.',
fallbackReadyOne: '{{ready}} завантаження готове; для {{failed}} буде використано резервне ім\'я файлу та невідомий розмір.',
fallbackReadyFew: '{{ready}} завантаження готові; для {{failed}} буде використано резервне ім\'я файлу та невідомий розмір.',
fallbackReadyMany: '{{ready}} завантажень готово; для {{failed}} буде використано резервне ім\'я файлу та невідомий розмір.',
readyToAddOne: 'Готово до додавання: {{count}} завантаження.',
readyToAddFew: 'Готово до додавання: {{count}} завантаження.',
readyToAddMany: 'Готово до додавання {{count}} завантажень.',
},
settings: {
tabs: {
downloads: 'Завантаження',
lookAndFeel: 'Вигляд та інтерфейс',
network: 'Мережа',
locations: 'Розташування',
siteLogins: 'Вхід на сайти',
power: 'Живлення',
engine: 'Рушій',
integrations: 'Інтеграції',
about: 'Про програму',
},
common: {
thisOs: 'ця ОС',
checking: 'Перевірка...',
ready: 'Готово',
errorMissing: 'Помилка / Відсутнє',
hideTechnicalDetails: 'Сховати технічні деталі',
showTechnicalDetails: 'Показати технічні деталі',
binary: 'Виконуваний файл',
expected: 'Очікується',
error: 'Помилка',
tip: 'Підказка',
stderr: 'stderr',
daemonProcessAlive: 'Процес демона працює',
rpcReady: 'RPC готовий',
rpcPort: 'Порт RPC',
daemonStderr: 'stderr демона',
packaging: 'Пакування: PyInstaller onedir (потрібен _internal)',
internalDirectoryFound: 'Директорію _internal знайдено',
pythonRuntimeFound: 'Середовище виконання Python знайдено',
externalOpenFailed: 'Не вдалося відкрити {{label}}: {{detail}}',
updateUnexpectedResponse: 'Перевірка оновлень повернула неочікувану відповідь.',
updateUntrustedReleaseUrl: 'Перевірка оновлень повернула ненадійну URL-адресу релізу.',
updateFailed: 'Не вдалося перевірити наявність оновлень: {{detail}}',
},
downloads: {
defaultConnections: 'З\'єднання за замовчуванням:',
defaultConnectionsDescription: 'Нові завантаження; існуючі елементи зберігають своє збережене значення',
parallelDownloads: 'Паралельні завантаження:',
parallelDownloadsDescription: 'Макс. одночасних активних файлів',
automaticRetries: 'Автоматичні повторні спроби:',
automaticRetriesDescription: 'Якщо з\'єднання перерветься',
systemNotification: 'Показувати системне сповіщення по завершенні завантаження',
systemNotificationDescription: 'Використовує налаштування сповіщень вашої операційної системи',
completionChime: 'Відтворювати звуковий сигнал по завершенні в програмі',
completionChimeDescription: 'Додатковий звук Firelink, незалежно від системних сповіщень',
clipboardLinks: 'Додавати посилання з буфера обміну, коли Firelink стає активним',
clipboardLinksDescription: 'Відкриває підтримувані скопійовані посилання у вікні "Додати". Вимкнено за замовчуванням.',
},
lookAndFeel: {
language: 'Мова',
languageDescription: 'Виберіть мову інтерфейсу Firelink.',
languageSystem: 'Мова системи',
languageEnglish: 'Англійська',
languageChinese: 'Спрощена китайська',
languageHebrew: 'Іврит',
languagePersian: 'Перська',
languageUkrainian: 'Українська',
languageRussian: 'Російська',
appTheme: 'Тема програми',
theme: 'Тема',
ariaLabel: 'Тема програми',
system: 'Системна',
light: 'Світла',
dark: 'Темна',
dracula: 'Dracula',
nord: 'Nord',
systemAppearance: 'Система слідкує за поточним світлим або темним виглядом {{platform}}.',
display: 'Відображення',
fontSize: 'Розмір шрифту',
fontSizeDescription: 'Масштабує список завантажень та компактні елементи керування.',
small: 'Дрібний',
standard: 'Стандартний',
large: 'Великий',
listDensity: 'Щільність списку',
listDensityDescription: 'Змінює висоту рядка, інтервали та масштаб індикатора прогресу.',
compact: 'Компактний',
comfortable: 'Комфортний',
relaxed: 'Вільний',
osIntegration: 'Інтеграція з ОС',
dockBadge: 'Показувати значок на іконці Dock',
dockBadgeDescription: 'Відображає кількість активних завантажень на іконці Firelink у Dock.',
menuBarIcon: 'Показувати іконку в рядку меню',
statusIndicatorIcon: 'Показувати іконку індикатора стану',
systemTrayIcon: 'Показувати іконку в системному треї',
macosTrayDescription: 'Надає швидкий доступ з рядка меню macOS.',
windowsTrayDescription: 'Надає швидкий доступ з області сповіщень Windows.',
linuxTrayDescription: 'Надає швидкий доступ із системного трею або області стану, якщо доступно.',
defaultTrayDescription: 'Надає швидкий доступ із системного трею ОС, якщо доступно.',
},
network: {
proxy: 'Проксі',
mode: 'Режим',
modeDescription: 'Керує використанням проксі для нових запитів на завантаження.',
noProxy: 'Без проксі',
systemProxy: 'Системний проксі',
customProxy: 'Власний проксі',
proxyHost: 'Хост проксі',
proxyHostDescription: 'Ім\'я хоста, IP-адреса або URL-адреса HTTP-проксі.',
proxyHostPlaceholder: '127.0.0.1 або http://127.0.0.1',
proxyPort: 'Порт проксі',
proxyPortDescription: 'Дійсний діапазон від 1 до 65535.',
noProxyDescription: 'Завантаження ігнорують налаштовані проксі-сервери.',
systemProxyDescription: 'Завантаження використовують виявлений системний проксі {{platform}}. Звичайні завантаження файлів вимагають HTTP або HTTPS проксі; медіазавантаження можуть використовувати SOCKS.',
customProxyDescription: 'Завантаження використовують налаштовану кінцеву точку HTTP-проксі для метаданих і рушіїв завантаження.',
invalidCustomProxy: 'Введіть дійсний хост і порт HTTP-проксі, щоб увімкнути власний проксі.',
incompleteCustomProxy: 'Введіть хост і порт проксі, щоб увімкнути власний проксі.',
checkingSystemProxy: 'Перевірка конфігурації системного проксі…',
detectedSystemProxy: 'Виявлено системний проксі. Звичайні завантаження файлів вимагають HTTP або HTTPS проксі; медіазавантаження можуть використовувати SOCKS.',
noSystemProxy: 'Не виявлено придатного системного проксі. Завантаження використовуватимуть пряме підключення (без проксі).',
systemProxyReadFailed: 'Не вдалося прочитати конфігурацію системного проксі. Виберіть "Без проксі" або спробуйте ще раз, коли він стане доступним.',
identity: 'Ідентифікація',
customUserAgent: 'Власний User-Agent',
userAgentDescription: 'Застосовується до запитів метаданих та рушіїв завантаження.',
userAgentPlaceholder: 'Залиште порожнім для Firelink за замовчуванням',
userAgentOverrides: 'Перевизначає вихідний заголовок User-Agent. Залиште порожнім для типових значень Firelink.',
chromeWindows: 'Chrome (Windows)',
chromeMacos: 'Chrome (macOS)',
edgeWindows: 'Edge (Windows)',
firefoxWindows: 'Firefox (Windows)',
firefoxMacos: 'Firefox (macOS)',
safariMacos: 'Safari (macOS)',
windowsDesktop: 'Робочий стіл Windows',
macosDesktop: 'Робочий стіл macOS',
},
locations: {
baseDownloadFolder: 'Базова папка завантажень',
baseDownloadFolderDescription: 'Автоматичні папки категорій створюються всередині цієї папки.',
browse: 'Огляд',
baseFolderCreateFailed: 'Базова папка збережена, але папки категорій не вдалося створити: {{detail}}',
baseFolderUpdated: 'Базову папку завантажень оновлено',
askWhereToSave: 'Запитувати, де зберігати під час додавання завантажень',
rememberLastUsedDownloadDirectory: 'Запам\'ятати останню використану папку завантажень',
rememberLastUsedDownloadDirectoryDescription: 'Використовувати останню папку, вибрану у вікні "Додати", для наступного завантаження до перезапуску програми.',
automaticCategorySubfolders: 'Автоматично зберігати файли у вкладених папках категорій',
automaticCategorySubfoldersDescription: 'Якщо вимкнено, автоматичні завантаження використовують базову папку.',
categorySubfolders: 'Вкладені папки категорій',
relativeToBase: 'Відносно базової папки',
disabled: 'Вимкнено',
categorySubfolder: 'Вкладена папка {{category}}',
customFolder: 'Власна папка',
useAutomatic: 'Використовувати автоматичну',
resetDefaults: 'Скинути за замовчуванням',
resetToast: 'Розташування категорій скинуто до стандартних',
},
siteLogins: {
title: 'Облікові дані сайтів',
noSavedLogins: 'Немає збережених логінів.',
user: 'Користувач: {{username}}',
deleteCredential: 'Видалити облікові дані',
deletedCredential: 'Видалено облікові дані',
deleteFailed: 'Не вдалося видалити облікові дані: {{detail}}',
addTitle: 'Додати облікові дані сайту',
urlPattern: 'Шаблон URL:',
urlPatternPlaceholder: 'наприклад, *.example.com або example.com/downloads',
username: 'Ім\'я користувача:',
password: 'Пароль:',
usernamePlaceholder: 'Ім\'я користувача',
passwordPlaceholder: 'Пароль',
saving: 'Збереження…',
addLogin: 'Додати вхід',
urlPatternRequired: 'Необхідно вказати шаблон URL.',
urlPatternWhitespace: 'Шаблон URL не може містити пробілів.',
usernameRequired: 'Необхідно вказати ім\'я користувача.',
passwordRequired: 'Необхідно вказати пароль.',
accessBeforeSave: 'Надайте доступ до сховища облікових даних перед збереженням логіна сайту.',
saveFailed: 'Не вдалося безпечно зберегти облікові дані сайту.',
addedCredential: 'Додано облікові дані сайту',
},
power: {
title: 'Керування живленням',
preventSleep: 'Запобігати переходу системи в сплячий режим під час активних завантажень',
preventSleepDescription: 'Дисплей може все одно вимкнутися. Firelink лише не дає пристрою заснути, щоб завершити активні передачі.',
},
engine: {
title: 'Завантажувач медіа та рушії',
description: 'Успішні результати повторно використовуються для цього сеансу програми. Повторна перевірка запускає справжню валідацію знову.',
recheck: 'Перевірити рушії знову',
coreDownloader: 'Ядро завантаження (aria2)',
version: 'Версія:',
status: 'Статус:',
mediaExtractors: 'Екстрактори медіа',
browserCookiesSource: 'Джерело Cookies з браузера:',
cookieDescription: 'yt-dlp читає файли cookie з браузера, щоб обійти обмеження на завантаження відео або отримати доступ до медіа з обмеженим доступом. Firelink не зберігає файли cookie з браузера.',
none: 'Немає',
},
integrations: {
title: 'Підключення розширення браузера',
description: 'Захоплюйте завантаження безпосередньо з вашого браузера у три простих кроки.',
portablePairingEnabled: 'Портативне підключення увімкнено',
pairingTokenPersisted: 'Токен підключення збережено',
portablePersistedDescription: 'Ваш токен підключення збережено разом із цією портативною папкою Firelink, і він залишатиметься дійсним при переміщенні папки. Ставтеся до цієї папки як до конфіденційної.',
systemPersistedDescription: 'Ваш токен підключення зберігається в системному сховищі облікових даних і зберігатиметься після перезапусків.',
portablePairingAvailable: 'Доступно портативне підключення',
credentialStorageNeeded: 'Потрібне сховище облікових даних',
portableAvailableDescription: 'Ваш токен підключення збережено разом із цією портативною папкою Firelink, і він зберігатиметься після перезапусків. Увімкніть його тут, щоб переглянути попередження про портативне сховище.',
credentialStorageDescription: "Firelink потребує доступу до сховища облікових даних цієї системи, щоб безпечно зберегти ваш токен підключення після перезапусків програми. Наразі ваше розширення залишатиметься підключеним лише для цього сеансу.",
reviewPortablePairing: 'Переглянути портативне підключення',
grantCredentialAccess: 'Надати доступ до облікових даних',
copyToken: 'Скопіювати токен',
tokenDescription: 'Цей безпечний токен авторизує ваше розширення браузера.',
tokenCopied: 'Токен скопійовано в буфер обміну!',
tokenCopyFailed: 'Не вдалося скопіювати токен: {{detail}}',
pairingTokenRegenerated: 'Токен підключення згенеровано наново',
regenerateFailed: 'Не вдалося згенерувати токен підключення наново: {{detail}}',
getExtension: 'Отримати розширення',
extensionDescription: 'Встановіть Firelink Companion для браузерів Firefox або Chromium.',
firefoxAddons: 'Додатки Firefox',
chromiumZip: 'Chromium ZIP',
pasteConnect: 'Вставити та підключити',
pasteConnectDescription: "Натисніть на іконку Firelink на панелі інструментів вашого браузера та вставте скопійований токен.",
extensionServerStatus: 'Статус сервера розширень:',
listening: '● Прослуховування на 127.0.0.1:{{port}}',
unavailable: '● Сервер недоступний',
},
about: {
firelinkIcon: 'Іконка Firelink',
unknown: 'Невідомо',
version: 'Версія {{version}}',
description: 'Кросплатформний менеджер завантажень для macOS, Windows та Linux.',
sourceCode: 'Вихідний код',
source: 'Джерело',
upToDate: 'У вас остання версія Firelink {{version}}.',
updateAvailable: 'Доступна нова версія Firelink {{version}}.',
viewRelease: 'Переглянути реліз',
updates: 'Оновлення',
checkForUpdates: 'Перевірити наявність оновлень',
updateDescription: 'Firelink перевіряє GitHub Releases на наявність нових версій.',
checking: 'Перевірка...',
checkNow: 'Перевірити зараз',
automaticUpdates: 'Автоматично перевіряти наявність оновлень',
createdBy: 'Створено NimBold',
mitLicense: 'Ліцензія MIT',
credits: 'Подяки: aria2, yt-dlp, FFmpeg та Deno.',
copyright: 'Авторське право © 2026 NimBold. Всі права захищені.',
},
},
status: {
ready: 'Готово',
active: '{{count}} активних',
queued: '{{count}} в черзі',
done: '{{count}} завершено',
},
} as const;
export default uk;
+728
View File
@@ -0,0 +1,728 @@
const zhCN = {
navigation: {
library: '库',
filters: {
all: '全部',
active: '活动',
completed: '已完成',
unfinished: '未完成',
},
categories: {
musics: '音乐',
movies: '视频',
compressed: '压缩包',
documents: '文档',
pictures: '图片',
applications: '应用程序',
other: '其他',
},
folders: '文件夹',
queues: '队列',
tools: '工具',
scheduler: '计划任务',
speedLimiter: '速度限制',
logs: '日志',
settings: '设置',
},
actions: {
hideSidebar: '隐藏侧边栏',
showSidebar: '显示侧边栏',
resizeSidebar: '调整侧边栏大小',
cancel: '取消',
continue: '继续',
dismissNotification: '关闭通知',
queueName: '队列名称',
addNewQueue: '添加新队列',
startQueue: '开始队列',
pauseQueue: '暂停队列',
renameQueue: '重命名队列',
deleteQueue: '删除队列',
},
dialogs: {
errorBoundary: {
title: '发生错误。',
description: 'React 组件树中发生了严重错误。下面的错误详情可以帮助确定根本原因。',
reload: '重新加载应用',
},
duplicateDownloads: {
title: '检测到重复下载',
description: '您要添加的一些下载已存在于队列或磁盘上。请选择如何解决这些冲突。',
rename: '重命名',
replace: '替换',
skip: '跳过',
},
removeDownload: {
title: '移除下载',
confirmationSingle: '您确定要从列表中移除此项目吗?您也可以选择从硬盘中删除底层文件。',
confirmationMultiple: '您确定要从列表中移除这 {{count}} 个项目吗?您也可以选择从硬盘中删除底层文件。',
errorSummary: '成功移除 {{succeeded}} 个,失败 {{failed}} 个:{{detail}}',
remove: '移除',
deleteFile: '删除文件',
},
},
window: {
controls: '窗口控制',
close: '关闭',
minimize: '最小化',
maximize: '最大化',
},
downloads: {
actions: {
moveUp: '上移',
moveDown: '下移',
pause: '暂停',
start: '开始',
resume: '继续',
options: '选项',
},
size: {
downloadedOf: '已下载 {{downloaded}} / 共 {{total}} {{unit}}',
downloadedOfApproximate: '已下载 {{downloaded}} / 约 {{total}} {{unit}}',
},
status: {
ready: '就绪',
staged: '在队列中',
queued: '已排队',
downloading: '下载中',
processing: '处理中',
paused: '已暂停',
completed: '已完成',
failed: '失败',
retrying: '重试中',
},
values: {
processing: '处理中...',
muxing: '混流中...',
},
},
speedLimiter: {
title: '速度限制',
unlimited: '无限制',
saveLimit: '保存限制',
globalSpeedLimit: '全局速度限制',
description: '适用于新的和活动的 aria2 传输以及新的 yt-dlp 媒体下载。每个下载的显式限制仍然附加到这些传输上。',
quickPresets: '快速预设',
removePreset: '移除 {{value}} {{unit}} 预设',
customPresetIn: '自定义预设 (单位:{{unit}})',
addQuickPreset: '添加快速预设',
globalLimitSaved: '全局限制已保存为 {{value}} {{unit}}',
globalLimitDisabled: '全局速度限制已禁用',
saveFailed: '无法保存全局速度限制:{{detail}}',
presetAlreadyExists: '{{value}} {{unit}} 已在快速预设中',
presetAdded: '已添加 {{value}} {{unit}} 快速预设',
presetRemoved: '已移除 {{value}} {{unit}} 快速预设',
},
logs: {
title: '日志',
entries: '({{count}} 条)',
collecting: '收集...',
off: '关闭',
allLevels: '所有级别',
levels: {
error: '错误',
warn: '警告',
info: '信息',
debug: '调试',
trace: '追踪',
},
pauseLogging: '暂停诊断日志记录',
enableLogging: '启用诊断日志记录',
clearDisplayed: '清除显示的日志',
export: '导出日志',
exportButton: '导出日志',
logFiles: '日志文件',
localDiagnostics: '本地诊断:',
diagnosticsDescription: '诊断收集需要主动开启,受范围限制并且在本地进行。常见的密钥、URL 查询参数和主目录路径在显示和导出时都会被隐藏。不会自动上传任何数据。',
noEntries: '尚无持久化的日志条目。',
disabledDescription: '诊断日志记录已关闭。现有的支持日志将在可用时显示在此处。',
copy: '复制',
copied: '已复制到剪贴板',
copyFailed: '复制到剪贴板失败',
exported: '支持日志已导出',
exportFailed: '无法导出日志:{{detail}}',
cleared: '日志已清除',
clearFailed: '无法清除日志:{{detail}}',
enabled: '已启用诊断日志记录',
disabled: '已禁用诊断日志记录',
updateFailed: '无法更新诊断日志记录:{{detail}}',
},
scheduler: {
title: '计划任务',
running: '运行中',
disabled: '计划任务已禁用',
noScheduledDay: '未选择计划日期',
unsavedChanges: '未保存的更改',
runNow: '立即运行',
pause: '暂停',
saveSettings: '保存设置',
timing: '时间',
startTime: '开始时间',
stopTime: '停止时间',
timingDescription: '如果在开始时间 Firelink 处于休眠状态,它将在当天晚些时候恢复时启动所选队列,除非停止时间已过。',
runEveryDay: '每天运行',
queuesToSchedule: '要调度的队列',
defaultQueue: '默认队列',
afterCompletion: '完成之后',
afterCompletionDescription: '选择计划任务启动的下载完成后的操作。',
actionWarning: '此操作可能会中断计算机上的其他工作。Firelink 会在计划的队列完成后立即执行它。',
systemPermissions: '系统权限',
macPermissionDescription: '睡眠、重启和关机需要 macOS 系统事件的自动化权限。',
permissionGranted: '已授予自动化权限',
permissionChecking: '正在检查自动化权限...',
permissionMissing: '缺少自动化权限',
revokePermission: '撤销权限',
grantPermission: '授予权限',
permissionAvailable: '自动化权限可用。',
permissionMissingDetails: '缺少自动化权限。请在“系统设置”的“系统事件”的“自动化”下启用 Firelink。',
revokePermissionDetails: 'macOS 不允许 Firelink 直接撤销自动化权限。请在“系统设置”中撤销“系统事件”访问权限,然后返回 Firelink。',
requestingPermission: '正在请求自动化权限...',
enablePermissionDetails: '请在“系统设置”的“系统事件”的“自动化”下启用 Firelink,然后返回 Firelink。',
systemActions: '系统操作',
windowsActionsDescription: '睡眠、重启和关机使用 Windows 系统权限。Firelink 在运行时会报告任何被拒绝的操作;不会提前声明任何永久权限。',
linuxActionsDescription: '睡眠、重启和关机使用您的 Linux 桌面和系统策略。Firelink 在运行时会报告任何被拒绝的操作;不会提前声明任何永久权限。',
validationDay: '至少为计划任务选择一天',
validationQueue: '至少为计划任务选择一个队列',
validationStopTime: '停止时间必须晚于开始时间',
saved: '计划任务设置已保存',
trackingOne: '正在跟踪 1 个计划的下载',
trackingMany: '正在跟踪 {{count}} 个计划的下载',
noStartableDownloads: '所选队列中没有可开始的下载',
pausedOne: '已暂停 1 个活动的下载',
pausedMany: '已暂停 {{count}} 个活动的下载',
noActiveDownloads: '没有活动的下载',
days: {
su: '日',
mo: '一',
tu: '二',
we: '三',
th: '四',
fr: '五',
sa: '六',
},
postActions: {
none: '无操作',
sleep: '睡眠',
restart: '重启',
shutdown: '关机',
},
},
properties: {
progress: '进度',
size: '大小',
speed: '速度',
eta: '剩余时间',
connections: '连接数',
speedCap: '速度上限',
category: '类别',
lastTry: '上次尝试',
dateAdded: '添加日期',
destination: '目标路径',
lastError: '上次错误',
saved: ' (已保存)',
defaultValue: ' (默认)',
savedTooltip: '已为此下载保存;设置的更改将应用于新的下载。',
defaultTooltip: '对新的下载使用当前默认值。',
identityReadOnly: '文件标识为只读。传输设置会保存以备重新下载使用。',
transferSettings: '停止或暂停后可以更改传输设置。当前的传输会保留其现有的后端选项。',
download: '下载',
url: 'URL',
fileName: '文件名',
saveLocation: '保存位置',
select: '选择',
perFile: '每个文件',
useCurrentDefault: '使用当前默认值 ({{count}})',
savedPerDownload: '按下载保存。设置中的默认值将应用于新下载。',
limit: '限制',
siteLogin: '站点登录',
siteLoginRedownload: '重新下载的站点登录',
matchingSiteLogin: '匹配的站点登录',
customCredentials: '自定义凭据',
noLogin: '无登录',
useSavedLogin: '如果可用,将使用保存的登录信息。',
advancedTransfer: '高级传输',
advancedTransferRedownload: '重新下载的高级传输',
checksum: '校验和',
verify: '验证',
algorithm: '算法',
digest: '摘要',
expectedDigest: '预期摘要',
cookies: 'Cookie',
headers: '请求头',
mirrors: '镜像',
username: '用户名',
password: '密码',
enterValidUrl: '请输入有效的 URL。',
fileNameEmpty: '文件名不能为空。',
cancel: '取消',
save: '保存',
},
keychain: {
title: '需要凭据存储访问权限',
stores: {
portable: '便携式 Firelink 数据文件夹',
windows: 'Windows 凭据管理器',
linux: '您的 Linux 凭据存储',
macos: 'macOS 钥匙串',
system: '此系统的凭据存储',
siteCredentials: '系统的凭据存储',
},
grantLabelPortable: '继续',
grantLabelMacos: '授予访问权限',
grantLabelDefault: '启用安全存储',
description: 'Firelink 使用浏览器扩展来捕获下载。要在重启后保持扩展配对,Firelink 会将其配对令牌存储在{{pairingStore}}中。可选的站点凭据存储在{{siteCredentialStore}}中。',
portableExplanation: '配对令牌可与此文件夹一起移植。站点凭据保留在系统凭据存储中;授予访问权限后可能会出现系统提示。',
macosExplanation: '授予访问权限后,macOS 可能会显示钥匙串提示。',
defaultExplanation: '这通常会在后台静默完成。如果凭据服务不可用,Firelink 将在此处显示错误,并且扩展仅在本次会话期间保持配对状态。',
note: '注意:',
portableNote: '便携模式仅在此文件夹中存储配对令牌。它不会复制站点密码或浏览器凭据。',
defaultNote: 'Firelink 仅写入其自己的专用凭据条目。它无法访问您系统上保存的其他密码或凭据项。',
hint: '提示:',
portableHint: '便携配对令牌已与此文件夹一起存储;您可以在此处启用它,也可以选择“稍后”。',
defaultHint: '如果您选择“稍后”,扩展仅在本次会话中工作。',
enableFromSettings: '您可以随时从“设置 > 集成”中启用存储。',
later: '稍后',
enabling: '正在启用...',
timeout: '凭据存储请求超时。您可以选择“稍后”并重试。',
unavailable: '{{store}}不可用。',
accessRequired: '在重新生成配对令牌之前,请授予凭据存储访问权限。',
storeUnavailable: '凭据存储访问不可用。',
},
app: {
settingsSaveFailed: '无法保存设置。请检查存储权限并重试。',
systemActionCountdown: '10 秒后{{action}}。',
systemActionCancelled: '系统操作已取消,因为有其他下载正在进行或已排队。',
systemActionFailed: '计划的系统操作失败:{{detail}}',
downloadCompleteTitle: '下载完成',
downloadCompleteBody: '{{fileName}} 已下载完成。',
unknownFile: '未知文件',
downloadFailedTitle: '下载失败',
downloadFailedBody: '{{fileName}} 下载失败。',
initializeDownloadsFailed: '无法初始化已保存的下载:{{detail}}',
extensionDisconnected: '浏览器扩展已断开连接,因为其配对令牌已更改。',
copyToken: '复制令牌',
integrations: '集成',
copyTokenFailed: '无法复制配对令牌:{{detail}}',
credentialPersistenceFailed: '安全凭据持久化不可用。浏览器配对仅适用于本次会话:{{detail}}',
resumeDownloadsFailed: '无法恢复已保存的下载:{{detail}}',
updateAvailable: 'Firelink {{version}} 可用。',
viewRelease: '查看发布版本',
sleepPreventionFailed: '无法更新睡眠阻止状态:{{detail}}',
schedulerNoQueues: '计划任务未选择有效的队列。请更新计划任务设置。',
schedulerPauseOneFailed: '计划任务无法暂停 1 个下载。',
schedulerPauseManyFailed: '计划任务无法暂停 {{count}} 个下载。',
scheduledIncomplete: '并非所有计划的下载都已完成。已跳过队列后系统操作。',
scheduledActionSkippedActive: '已跳过计划的系统操作,因为有其他下载正在进行或已排队。',
notificationsDisabled: '系统的 Firelink 通知已被禁用。',
notificationsFailed: '无法配置通知:{{detail}}',
},
sidebar: {
deleteQueueConfirm: '删除队列“{{name}}”?其未完成的下载将被移至主队列。',
deleteQueueFailed: '无法删除队列:{{detail}}',
queueNameEmpty: '队列名称不能为空',
queueNameExists: '同名队列已存在',
startQueueFailed: '无法开始队列:{{detail}}',
pauseQueueFailed: '无法暂停队列:{{detail}}',
},
downloadTable: {
unknownQueue: '未知队列',
allDownloads: '所有下载',
active: '活动',
completed: '已完成',
unfinished: '未完成',
headers: {
fileName: '文件名',
size: '大小',
status: '状态',
speed: '速度',
eta: '剩余时间',
dateAdded: '添加日期',
},
addDownload: '添加下载',
resumeAll: '全部继续',
pauseAll: '全部暂停',
queueEmpty: '队列为空',
noCompletedDownloads: '没有已完成的下载',
noDownloads: '没有下载',
queueEmptyDescription: '通过项目菜单或“添加”窗口将下载添加到此队列。',
completedDescription: '已完成的下载将显示在此处。',
clickToAdd: '点击',
addButtonOr: '按钮或',
toAddDownloads: '来添加下载',
nonResumableOne: '1 个下载不支持断点续传。如果您暂停它,稍后您将不得不重新开始。确定要暂停吗?',
nonResumableMany: '{{count}} 个下载不支持断点续传。如果您暂停它们,稍后您将不得不重新开始。确定要暂停吗?',
interactionError: '{{message}}{{detail}}',
openFileFailed: '无法打开下载的文件',
revealFileFailed: '无法在文件管理器中显示下载',
pauseFailed: '无法暂停下载',
resumeFailed: '无法继续 {{fileName}}',
moveManyFailed: '无法将下载移动到队列',
moveOneFailed: '无法将下载移动到队列',
copyAddressesFailed: '无法复制地址',
copyAddressFailed: '无法复制地址',
copyPathFailed: '无法复制文件路径',
missingFileName: '缺少文件名',
redownloadFailed: '重新下载失败',
startResume: '开始/继续',
addToQueue: '添加到队列',
copyAddress: '复制地址',
remove: '移除',
open: '打开',
showInFolder: '在文件夹中显示',
pause: '暂停',
start: '开始',
resume: '继续',
redownload: '重新下载',
copyFilePath: '复制文件路径',
properties: '属性',
backendRejectedStart: '后端拒绝了开始/继续请求。',
transferActive: '传输处于活动状态时无法更改属性。请先暂停它。',
redownloadNotFound: '无法重新下载:未找到下载。',
redownloadActive: '无法重新下载{{status}}的下载。请先暂停或等待其完成。',
originalUrlMissing: '无法重新下载:缺少原始 URL。',
pauseBeforeMove: '请在将 {{file}} 移动到另一个队列前暂停它。',
},
addDownloads: {
unknownSize: '未知大小',
unknown: '未知',
discardSetup: '放弃此下载设置吗?',
speedInvalid: '速度限制必须大于零',
duplicateResolveFailed: '无法解决重复的下载:{{detail}}',
addedWithFailures: '添加了 {{added}} 个,失败了 {{failed}} 个。{{detail}}',
addedOne: '添加了 1 个下载',
addedMany: '添加了 {{count}} 个下载',
urlAlreadyQueued: 'URL 已在队列中',
destinationConflict: '其他选定的下载已使用此目标路径',
existingDownloadDestination: '现有的 Firelink 下载已使用此目标路径',
fileExistsOnDisk: '磁盘上已存在该文件;请重命名或跳过以避免删除无关数据',
backendRejectedStart: '后端拒绝开始下载。',
playlistNoEntries: '播放列表不包含可下载的条目',
invalidMediaMetadata: '媒体元数据无效或未找到格式',
noAvailableName: '无法为 {{file}} 找到可用的名称。',
pauseBeforeReplace: '请在替换 {{file}} 前暂停它。',
cannotReplace: '无法替换 {{file}}:文件不属于 Firelink 下载。',
downloadLinks: '下载链接',
pastePlaceholder: '在此粘贴 HTTP、HTTPS、FTP 或 SFTP URL...\n\n对于媒体下载,请粘贴来自 YouTube、X、TikTok、Instagram、Reddit 等的链接。',
playlistSummary: '播放列表“{{title}}”:已加载 {{loaded}} / {{total}} 个条目{{truncated}}{{skipped}}',
safeEntryLimit: ' (达到安全条目限制)',
selectedSummary: '准备就绪 {{ready}} 个,后备项 {{fallback}} 个,媒体重试 {{mediaRetry}} 个,已屏蔽 {{blocked}} 个',
clearSelection: '清除选择',
selectAll: '全选',
refreshMetadata: '刷新元数据',
files: '文件',
required: '必需',
free: '空闲',
preview: '预览',
file: '文件',
size: '大小',
status: '状态',
downloadPreview: '下载预览',
noLinks: '尚未添加任何链接。',
selectItem: '选择 {{file}}',
fetchingPlaylist: '正在获取播放列表...',
fetching: '正在获取...',
unsafeUrl: '不安全的 URL',
playlistFailed: '播放列表获取失败',
metadataFailed: '元数据获取失败',
fallback: '后备项',
invalid: '无效',
ready: '就绪',
mediaFormat: '媒体格式',
playlistItem: '播放列表项目 {{index}}',
fetchingMediaStreams: '正在获取媒体流...',
availableStreams: '可用流',
availableMediaStreams: '可用的媒体流',
metadataUnavailable: '元数据不可用。在添加此媒体之前,请刷新元数据。',
saveLocation: '保存位置',
browse: '浏览',
categoryFolders: '文件将被组织到类别文件夹中。',
sharedFolder: '所有选定的下载都将使用此文件夹。',
transferSettings: '传输设置',
connectionsPerFile: '每个文件的连接数',
connectionsPerFileAria: '每个文件的连接数 (Aria)',
limitSpeedPerFile: '限制每个文件的速度',
speedLimitPerFileAria: '每个文件的速度限制 (Aria)',
authorization: '授权',
useAuthorization: '使用授权',
username: '用户名',
password: '密码',
advancedTransfer: '高级传输',
verifyChecksum: '验证校验和',
checksumAlgorithm: '校验和算法',
expectedDigest: '预期摘要',
headers: '请求头',
requestHeaders: '请求头',
cookies: 'Cookie',
cookiePlaceholder: 'name=value; other=value',
mirrors: '镜像',
cancel: '取消',
startDownloads: '开始下载',
addToQueue: '添加到队列',
pasteOneOrMore: '粘贴一个或多个链接。',
selectAtLeastOne: '请至少选择一个下载。',
correctInvalidOne: '在继续之前,请更正或移除 {{count}} 个无效的 URL。',
correctInvalidFew: '在继续之前,请更正或移除 {{count}} 个无效的 URL。',
correctInvalidMany: '在继续之前,请更正或移除 {{count}} 个无效的 URL。',
waitingForMetadataOne: '正在等待 {{count}} 个下载的元数据。',
waitingForMetadataFew: '正在等待 {{count}} 个下载的元数据。',
waitingForMetadataMany: '正在等待 {{count}} 个下载的元数据。',
removeUnsafeOne: '在继续之前,请移除 {{count}} 个不安全的 URL。',
removeUnsafeFew: '在继续之前,请移除 {{count}} 个不安全的 URL。',
removeUnsafeMany: '在继续之前,请移除 {{count}} 个不安全的 URL。',
mediaMetadataUnavailableSummaryOne: '{{count}} 个项目的媒体元数据不可用。在添加之前请刷新元数据。',
mediaMetadataUnavailableSummaryFew: '{{count}} 个项目的媒体元数据不可用。在添加之前请刷新元数据。',
mediaMetadataUnavailableSummaryMany: '{{count}} 个项目的媒体元数据不可用。在添加之前请刷新元数据。',
metadataUnavailableFallback: '元数据不可用。下载仍可以使用后备详细信息添加。',
fallbackReadyOne: '{{ready}} 个下载已就绪;{{failed}} 个将使用后备文件名和未知大小。',
fallbackReadyFew: '{{ready}} 个下载已就绪;{{failed}} 个将使用后备文件名和未知大小。',
fallbackReadyMany: '{{ready}} 个下载已就绪;{{failed}} 个将使用后备文件名和未知大小。',
readyToAddOne: '准备添加 {{count}} 个下载。',
readyToAddFew: '准备添加 {{count}} 个下载。',
readyToAddMany: '准备添加 {{count}} 个下载。',
},
settings: {
tabs: {
downloads: '下载',
lookAndFeel: '外观',
network: '网络',
locations: '位置',
siteLogins: '站点登录',
power: '电源',
engine: '引擎',
integrations: '集成',
about: '关于',
},
common: {
thisOs: '此操作系统',
checking: '正在检查...',
ready: '就绪',
errorMissing: '错误 / 缺失',
hideTechnicalDetails: '隐藏技术详细信息',
showTechnicalDetails: '显示技术详细信息',
binary: '二进制文件',
expected: '预期',
error: '错误',
tip: '提示',
stderr: '标准错误',
daemonProcessAlive: '守护进程处于活动状态',
rpcReady: 'RPC 已就绪',
rpcPort: 'RPC 端口',
daemonStderr: '守护进程标准错误',
packaging: '打包:PyInstaller onedir(需要 _internal',
internalDirectoryFound: '找到 _internal 目录',
pythonRuntimeFound: '找到 Python 运行时',
externalOpenFailed: '无法打开 {{label}}{{detail}}',
updateUnexpectedResponse: '更新检查返回了意外的响应。',
updateUntrustedReleaseUrl: '更新检查返回了不受信任的发布版本 URL。',
updateFailed: '更新检查失败:{{detail}}',
},
downloads: {
defaultConnections: '默认连接数:',
defaultConnectionsDescription: '新下载;现有项目保持其保存的值',
parallelDownloads: '并行下载:',
parallelDownloadsDescription: '最大同时活动文件数',
automaticRetries: '自动重试:',
automaticRetriesDescription: '如果连接失败',
systemNotification: '下载完成时显示系统通知',
systemNotificationDescription: '使用操作系统的通知设置',
completionChime: '播放应用内完成提示音',
completionChimeDescription: '可选的 Firelink 声音,独立于系统通知',
clipboardLinks: '当 Firelink 处于活动状态时添加剪贴板链接',
clipboardLinksDescription: '在“添加”窗口中打开支持的已复制链接。默认关闭。',
},
lookAndFeel: {
language: '语言',
languageDescription: '选择 Firelink 的界面语言。',
languageSystem: '系统默认',
languageEnglish: '英语',
languageChinese: '简体中文',
languageHebrew: '希伯来语',
languagePersian: '波斯语',
languageUkrainian: '乌克兰语',
languageRussian: '俄语',
appTheme: '应用主题',
theme: '主题',
ariaLabel: '应用主题',
system: '系统',
light: '浅色',
dark: '深色',
dracula: 'Dracula',
nord: 'Nord',
systemAppearance: '系统跟随当前的 {{platform}} 浅色或深色外观。',
display: '显示',
fontSize: '字体大小',
fontSizeDescription: '缩放下载列表和紧凑型控件。',
small: '小',
standard: '标准',
large: '大',
listDensity: '列表密度',
listDensityDescription: '更改行高、间距和进度条的缩放。',
compact: '紧凑',
comfortable: '舒适',
relaxed: '宽松',
osIntegration: '系统集成',
dockBadge: '在 Dock 图标上显示角标',
dockBadgeDescription: '在 Firelink 的 Dock 图标上显示活动下载计数。',
menuBarIcon: '显示菜单栏图标',
statusIndicatorIcon: '显示状态指示器图标',
systemTrayIcon: '显示系统托盘图标',
macosTrayDescription: '提供从 macOS 菜单栏的快速访问。',
windowsTrayDescription: '提供从 Windows 通知区域的快速访问。',
linuxTrayDescription: '在可用时,提供从桌面托盘或状态区域的快速访问。',
defaultTrayDescription: '在可用时,提供从操作系统托盘区域的快速访问。',
},
network: {
proxy: '代理',
mode: '模式',
modeDescription: '控制新下载请求的代理使用。',
noProxy: '无代理',
systemProxy: '系统代理',
customProxy: '自定义代理',
proxyHost: '代理主机',
proxyHostDescription: '主机名、IP 地址或 HTTP 代理 URL。',
proxyHostPlaceholder: '127.0.0.1 或 http://127.0.0.1',
proxyPort: '代理端口',
proxyPortDescription: '有效范围是 1 到 65535。',
noProxyDescription: '下载忽略已配置的代理。',
systemProxyDescription: '下载使用检测到的 {{platform}} 系统代理。普通文件下载需要 HTTP 或 HTTPS 代理端点;媒体下载可以使用 SOCKS。',
customProxyDescription: '下载使用配置的 HTTP 代理端点来获取元数据和进行引擎下载。',
invalidCustomProxy: '输入有效的 HTTP 代理主机和端口以启用自定义代理。',
incompleteCustomProxy: '输入代理主机和端口以启用自定义代理。',
checkingSystemProxy: '正在检查系统代理配置...',
detectedSystemProxy: '检测到系统代理。普通文件下载需要 HTTP 或 HTTPS 端点;媒体下载可以使用 SOCKS。',
noSystemProxy: '未检测到可用的系统代理。下载将不使用代理。',
systemProxyReadFailed: '无法读取系统代理配置。请选择“无代理”,或者在其可用时重试。',
identity: '身份',
customUserAgent: '自定义 User-Agent',
userAgentDescription: '应用于元数据获取和下载引擎。',
userAgentPlaceholder: '留空以使用 Firelink 默认值',
userAgentOverrides: '覆盖传出的 User-Agent 请求头。留空以使用 Firelink 默认值。',
chromeWindows: 'Chrome (Windows)',
chromeMacos: 'Chrome (macOS)',
edgeWindows: 'Edge (Windows)',
firefoxWindows: 'Firefox (Windows)',
firefoxMacos: 'Firefox (macOS)',
safariMacos: 'Safari (macOS)',
windowsDesktop: 'Windows 桌面',
macosDesktop: 'macOS 桌面',
},
locations: {
baseDownloadFolder: '基本下载文件夹',
baseDownloadFolderDescription: '在此文件夹中创建自动类别文件夹。',
browse: '浏览',
baseFolderCreateFailed: '基本文件夹已保存,但无法创建类别文件夹:{{detail}}',
baseFolderUpdated: '基本下载文件夹已更新',
askWhereToSave: '在添加下载时询问保存位置',
rememberLastUsedDownloadDirectory: '记住上次使用的下载目录',
rememberLastUsedDownloadDirectoryDescription: '在下一次下载时,使用在“添加”窗口中最后选择的文件夹,直到应用程序重启。',
automaticCategorySubfolders: '自动将文件保存到类别子文件夹',
automaticCategorySubfoldersDescription: '关闭时,自动下载使用基本文件夹。',
categorySubfolders: '类别子文件夹',
relativeToBase: '相对于基本文件夹',
disabled: '已禁用',
categorySubfolder: '{{category}}子文件夹',
customFolder: '自定义文件夹',
useAutomatic: '使用自动',
resetDefaults: '重置为默认值',
resetToast: '已将类别位置重置为默认值',
},
siteLogins: {
title: '站点凭据',
noSavedLogins: '没有保存的登录信息。',
user: '用户:{{username}}',
deleteCredential: '删除凭据',
deletedCredential: '已删除凭据',
deleteFailed: '无法删除凭据:{{detail}}',
addTitle: '添加站点凭据',
urlPattern: 'URL 模式:',
urlPatternPlaceholder: '例如:*.example.com 或 example.com/downloads',
username: '用户名:',
password: '密码:',
usernamePlaceholder: '用户名',
passwordPlaceholder: '密码',
saving: '正在保存...',
addLogin: '添加登录',
urlPatternRequired: 'URL 模式是必需的。',
urlPatternWhitespace: 'URL 模式不能包含空格。',
usernameRequired: '用户名是必需的。',
passwordRequired: '密码是必需的。',
accessBeforeSave: '在保存站点登录之前,请授予凭据存储访问权限。',
saveFailed: '无法安全地保存站点凭据。',
addedCredential: '已添加站点凭据',
},
power: {
title: '电源管理',
preventSleep: '下载活动时阻止系统睡眠',
preventSleepDescription: '显示器可能仍会关闭。Firelink 仅保持设备唤醒足以完成活动的传输。',
},
engine: {
title: '媒体下载器和引擎',
description: '成功的结果将在此应用会话中重用。重新检查将再次运行真正的验证。',
recheck: '重新检查引擎',
coreDownloader: '核心下载器 (aria2)',
version: '版本:',
status: '状态:',
mediaExtractors: '媒体提取器',
browserCookiesSource: '浏览器 Cookies 来源:',
cookieDescription: 'yt-dlp 读取浏览器 cookies 以绕过视频下载限制或访问受限媒体。Firelink 不会保存浏览器 cookies。',
none: '无',
},
integrations: {
title: '连接浏览器扩展',
description: '通过三个简单的步骤直接从浏览器捕获下载。',
portablePairingEnabled: '便携配对已启用',
pairingTokenPersisted: '配对令牌已持久化',
portablePersistedDescription: '您的配对令牌与此便携式 Firelink 文件夹一起存储,并在文件夹移动时保留。请将该文件夹视为敏感内容。',
systemPersistedDescription: '您的配对令牌已存储在系统凭据存储中,并在重启后保留。',
portablePairingAvailable: '便携配对可用',
credentialStorageNeeded: '需要凭据存储',
portableAvailableDescription: '您的配对令牌与此便携式 Firelink 文件夹一起存储,并在重启后保留。在此处启用它以查看便携存储警告。',
credentialStorageDescription: 'Firelink 需要访问此系统的凭据存储以在应用重启时安全地保存您的配对令牌。目前,您的扩展仅在此会话中保持连接。',
reviewPortablePairing: '查看便携配对',
grantCredentialAccess: '授予凭据访问权限',
copyToken: '复制令牌',
tokenDescription: '此安全令牌用于授权您的浏览器扩展。',
tokenCopied: '令牌已复制到剪贴板!',
tokenCopyFailed: '无法复制令牌:{{detail}}',
pairingTokenRegenerated: '配对令牌已重新生成',
regenerateFailed: '无法重新生成配对令牌:{{detail}}',
getExtension: '获取扩展',
extensionDescription: '安装适用于 Firefox 或 Chromium 浏览器的 Firelink Companion。',
firefoxAddons: 'Firefox 附加组件',
chromiumZip: 'Chromium ZIP',
pasteConnect: '粘贴并连接',
pasteConnectDescription: '点击浏览器工具栏中的 Firelink 图标,然后粘贴复制的令牌。',
extensionServerStatus: '扩展服务器状态:',
listening: '● 正在 127.0.0.1:{{port}} 监听',
unavailable: '● 服务器不可用',
},
about: {
firelinkIcon: 'Firelink 图标',
unknown: '未知',
version: '版本 {{version}}',
description: '适用于 macOS、Windows 和 Linux 的跨平台下载管理器。',
sourceCode: '源代码',
source: '源码',
upToDate: 'Firelink {{version}} 已是最新版本。',
updateAvailable: 'Firelink {{version}} 可用。',
viewRelease: '查看发布版本',
updates: '更新',
checkForUpdates: '检查更新',
updateDescription: 'Firelink 会在 GitHub Releases 检查新版本。',
checking: '正在检查...',
checkNow: '立即检查',
automaticUpdates: '自动检查更新',
createdBy: '由 NimBold 创建',
mitLicense: 'MIT 许可证',
credits: '致谢:aria2、yt-dlp、FFmpeg 和 Deno。',
copyright: '版权所有 © 2026 NimBold。保留所有权利。',
},
},
status: {
ready: '就绪',
active: '{{count}} 个活动',
queued: '{{count}} 个排队',
done: '{{count}} 个完成',
},
};
export default zhCN;
+54 -6
View File
@@ -1,16 +1,64 @@
import { describe, expect, it } from 'vitest';
import i18n, { i18nReady } from './index';
import { describe, expect, it, vi } from 'vitest';
import i18n, { changeAppLocale, i18nReady, syncDocumentLocale } from './index';
import { APP_LOCALES, localeDirection, resolveAppLocale } from './locales';
describe('i18n runtime', () => {
it('falls back to bundled English while planned catalogs are untranslated', async () => {
it('selects every bundled catalog instead of falling back to English', async () => {
await i18nReady;
const originalLanguage = i18n.language;
try {
await i18n.changeLanguage('fa');
expect(i18n.t($ => $.navigation.library)).toBe('Library');
const expectedLibraryLabels = {
en: 'Library',
'zh-CN': '库',
he: 'ספרייה',
fa: 'کتابخانه',
uk: 'Бібліотека',
ru: 'Библиотека',
} as const;
for (const locale of APP_LOCALES) {
await changeAppLocale(locale);
expect(i18n.language).toBe(locale);
expect(i18n.t($ => $.navigation.library)).toBe(expectedLibraryLabels[locale]);
}
} finally {
await i18n.changeLanguage(originalLanguage);
await changeAppLocale(resolveAppLocale(originalLanguage));
}
});
it('syncs document language and direction for every supported locale', async () => {
const documentElement = { lang: '', dir: '' };
vi.stubGlobal('document', { documentElement });
try {
for (const locale of APP_LOCALES) {
await changeAppLocale(locale);
expect(documentElement).toEqual({ lang: locale, dir: localeDirection(locale) });
}
expect(syncDocumentLocale('de-DE')).toBe('en');
expect(documentElement).toEqual({ lang: 'en', dir: 'ltr' });
} finally {
vi.unstubAllGlobals();
}
});
it('uses English resources for an unknown i18next language', async () => {
await i18nReady;
const originalLanguage = i18n.language;
try {
await i18n.changeLanguage('de-DE');
expect(i18n.t($ => $.navigation.library)).toBe('Library');
expect(resolveAppLocale('de-DE')).toBe('en');
expect(resolveAppLocale('ru-RU')).toBe('ru');
expect(resolveAppLocale('fa-IR')).toBe('fa');
expect(resolveAppLocale('he-IL')).toBe('he');
expect(resolveAppLocale('uk-UA')).toBe('uk');
expect(resolveAppLocale('zh-Hans-CN')).toBe('zh-CN');
} finally {
await changeAppLocale(resolveAppLocale(originalLanguage));
}
});
});
+72 -8
View File
@@ -1,5 +1,6 @@
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import englishCommon from './catalogs/en';
import {
APP_LOCALES,
DEFAULT_LOCALE,
@@ -7,36 +8,99 @@ import {
resolveAppLocale,
type AppLocale,
} from './locales';
import { defaultNS, resources } from './resources';
export { APP_LOCALES, APP_LOCALE_METADATA, DEFAULT_LOCALE, isAppLocale, localeDirection, resolveAppLocale } from './locales';
export type { AppLocale, AppLocaleDirection } from './locales';
export { APP_LOCALES, APP_LOCALE_METADATA, APP_LOCALE_PREFERENCES, DEFAULT_LOCALE, isAppLocale, isAppLocalePreference, localeDirection, localePluralVariant, resolveAppLocale } from './locales';
export type { AppLocale, AppLocaleDirection, AppLocalePreference, AppPluralVariant } from './locales';
const defaultNS = 'common' as const;
const initialLocale = resolveAppLocale(
typeof navigator === 'undefined' ? undefined : navigator.language
);
type CommonCatalog = Parameters<typeof i18n.addResourceBundle>[2];
type LocaleLoader = () => Promise<CommonCatalog>;
const localeLoaders: Record<AppLocale, LocaleLoader> = {
en: async () => englishCommon,
'zh-CN': async () => (await import('./catalogs/zh-CN')).default,
he: async () => (await import('./catalogs/he')).default,
fa: async () => (await import('./catalogs/fa')).default,
uk: async () => (await import('./catalogs/uk')).default,
ru: async () => (await import('./catalogs/ru')).default,
};
const loadedLocales = new Set<AppLocale>([DEFAULT_LOCALE]);
const localeLoadPromises = new Map<AppLocale, Promise<void>>();
let localeChangeRequest = 0;
const loadLocale = (locale: AppLocale): Promise<void> => {
if (loadedLocales.has(locale)) return Promise.resolve();
const existingLoad = localeLoadPromises.get(locale);
if (existingLoad) return existingLoad;
const load = localeLoaders[locale]().then(catalog => {
i18n.addResourceBundle(locale, defaultNS, catalog, true, true);
loadedLocales.add(locale);
localeLoadPromises.delete(locale);
}).catch(error => {
localeLoadPromises.delete(locale);
throw error;
});
localeLoadPromises.set(locale, load);
return load;
};
const fallbackToEnglish = async (requestedLocale: AppLocale, error: unknown): Promise<void> => {
console.error(`Failed to load locale "${requestedLocale}"; falling back to English.`, error);
await i18n.changeLanguage(DEFAULT_LOCALE);
syncDocumentLocale(DEFAULT_LOCALE);
};
export const i18nReady = i18n
.use(initReactI18next)
.init({
defaultNS,
fallbackLng: DEFAULT_LOCALE,
initAsync: false,
lng: initialLocale,
lng: DEFAULT_LOCALE,
interpolation: {
escapeValue: false,
},
ns: [defaultNS],
resources,
resources: {
[DEFAULT_LOCALE]: { [defaultNS]: englishCommon },
},
returnNull: false,
supportedLngs: APP_LOCALES,
})
.then(async () => {
try {
await loadLocale(initialLocale);
await i18n.changeLanguage(initialLocale);
syncDocumentLocale(initialLocale);
} catch (error) {
await fallbackToEnglish(initialLocale, error);
}
});
void i18nReady.then(() => syncDocumentLocale(initialLocale));
export const changeAppLocale = async (locale: AppLocale): Promise<void> => {
await i18n.changeLanguage(locale);
syncDocumentLocale(locale);
const request = ++localeChangeRequest;
await i18nReady;
if (request !== localeChangeRequest) return;
try {
await loadLocale(locale);
if (request !== localeChangeRequest) return;
await i18n.changeLanguage(locale);
if (request === localeChangeRequest) syncDocumentLocale(locale);
} catch (error) {
if (request === localeChangeRequest) {
await fallbackToEnglish(locale, error);
}
}
};
export const syncDocumentLocale = (value: string | null | undefined): AppLocale => {
+18 -1
View File
@@ -2,14 +2,16 @@ import { describe, expect, it } from 'vitest';
import {
APP_LOCALES,
DEFAULT_LOCALE,
isAppLocalePreference,
isAppLocale,
localeDirection,
localePluralVariant,
resolveAppLocale,
} from './locales';
describe('app locale metadata', () => {
it('keeps the planned locale set stable', () => {
expect(APP_LOCALES).toEqual(['en', 'zh-CN', 'he', 'fa', 'uk']);
expect(APP_LOCALES).toEqual(['en', 'zh-CN', 'he', 'fa', 'uk', 'ru']);
expect(DEFAULT_LOCALE).toBe('en');
});
@@ -19,6 +21,7 @@ describe('app locale metadata', () => {
expect(resolveAppLocale('iw-IL')).toBe('he');
expect(resolveAppLocale('fa-IR')).toBe('fa');
expect(resolveAppLocale('uk-UA')).toBe('uk');
expect(resolveAppLocale('ru-RU')).toBe('ru');
expect(resolveAppLocale('de-DE')).toBe('en');
});
@@ -28,12 +31,26 @@ describe('app locale metadata', () => {
expect(localeDirection('he')).toBe('rtl');
expect(localeDirection('fa')).toBe('rtl');
expect(localeDirection('uk')).toBe('ltr');
expect(localeDirection('ru')).toBe('ltr');
});
it('selects locale-aware plural categories', () => {
expect(localePluralVariant('en', 1)).toBe('one');
expect(localePluralVariant('en', 2)).toBe('many');
expect(localePluralVariant('ru-RU', 1)).toBe('one');
expect(localePluralVariant('ru-RU', 2)).toBe('few');
expect(localePluralVariant('ru-RU', 5)).toBe('many');
expect(localePluralVariant('uk-UA', 2)).toBe('few');
});
it('guards locale values before they reach i18next', () => {
expect(isAppLocale('en')).toBe(true);
expect(isAppLocale('fa')).toBe(true);
expect(isAppLocale('ru')).toBe(true);
expect(isAppLocale('de')).toBe(false);
expect(isAppLocale(null)).toBe(false);
expect(isAppLocalePreference('system')).toBe(true);
expect(isAppLocalePreference('fa')).toBe(true);
expect(isAppLocalePreference('de')).toBe(false);
});
});
+25 -8
View File
@@ -1,7 +1,10 @@
export const APP_LOCALES = ['en', 'zh-CN', 'he', 'fa', 'uk'] as const;
export const APP_LOCALES = ['en', 'zh-CN', 'he', 'fa', 'uk', 'ru'] as const;
export const APP_LOCALE_PREFERENCES = ['system', ...APP_LOCALES] as const;
export type AppLocale = typeof APP_LOCALES[number];
export type AppLocalePreference = typeof APP_LOCALE_PREFERENCES[number];
export type AppLocaleDirection = 'ltr' | 'rtl';
export type AppPluralVariant = 'one' | 'few' | 'many';
export const DEFAULT_LOCALE: AppLocale = 'en';
@@ -10,19 +13,22 @@ export const APP_LOCALE_METADATA: Record<AppLocale, {
isTranslated: boolean;
}> = {
en: { direction: 'ltr', isTranslated: true },
'zh-CN': { direction: 'ltr', isTranslated: false },
he: { direction: 'rtl', isTranslated: false },
fa: { direction: 'rtl', isTranslated: false },
uk: { direction: 'ltr', isTranslated: false },
'zh-CN': { direction: 'ltr', isTranslated: true },
he: { direction: 'rtl', isTranslated: true },
fa: { direction: 'rtl', isTranslated: true },
uk: { direction: 'ltr', isTranslated: true },
ru: { direction: 'ltr', isTranslated: true },
};
export const isAppLocale = (value: unknown): value is AppLocale =>
typeof value === 'string' && (APP_LOCALES as readonly string[]).includes(value);
export const isAppLocalePreference = (value: unknown): value is AppLocalePreference =>
typeof value === 'string' && (APP_LOCALE_PREFERENCES as readonly string[]).includes(value);
/**
* Resolve a system or persisted BCP 47 language tag to a planned Firelink
* locale. Non-English locales intentionally fall back to English resources
* until their translations are added.
* Resolve a system or persisted BCP 47 language tag to a bundled Firelink
* locale. Unknown languages fall back to English.
*/
export const resolveAppLocale = (value: string | null | undefined): AppLocale => {
const normalized = value?.trim().replace(/_/g, '-').toLowerCase();
@@ -32,10 +38,21 @@ export const resolveAppLocale = (value: string | null | undefined): AppLocale =>
if (normalized === 'he' || normalized.startsWith('he-') || normalized === 'iw' || normalized.startsWith('iw-')) return 'he';
if (normalized === 'fa' || normalized.startsWith('fa-')) return 'fa';
if (normalized === 'uk' || normalized.startsWith('uk-')) return 'uk';
if (normalized === 'ru' || normalized.startsWith('ru-')) return 'ru';
if (normalized === 'en' || normalized.startsWith('en-')) return 'en';
return DEFAULT_LOCALE;
};
export const localePluralVariant = (
value: string | null | undefined,
count: number
): AppPluralVariant => {
const category = new Intl.PluralRules(resolveAppLocale(value)).select(Math.abs(count));
if (category === 'one') return 'one';
if (category === 'few') return 'few';
return 'many';
};
export const localeDirection = (locale: AppLocale): AppLocaleDirection =>
APP_LOCALE_METADATA[locale].direction;
+111
View File
@@ -0,0 +1,111 @@
import { describe, expect, it } from 'vitest';
import { APP_LOCALES } from './locales';
import { resources } from './resources';
type CatalogNode = string | { readonly [key: string]: CatalogNode };
const flattenCatalog = (
node: CatalogNode,
prefix = ''
): Map<string, string> => {
const entries = new Map<string, string>();
if (typeof node === 'string') {
entries.set(prefix, node);
return entries;
}
for (const [key, value] of Object.entries(node)) {
const path = prefix ? `${prefix}.${key}` : key;
for (const [leafPath, leafValue] of flattenCatalog(value, path)) {
entries.set(leafPath, leafValue);
}
}
return entries;
};
const interpolationTokens = (value: string): string[] =>
[...value.matchAll(/\{\{([A-Za-z0-9_]+)\}\}/g)]
.map((match) => match[1])
.sort();
const catalogFor = (locale: keyof typeof resources): Map<string, string> =>
flattenCatalog(resources[locale].common);
describe('translation catalogs', () => {
it('registers a catalog for every supported locale', () => {
expect(Object.keys(resources).sort()).toEqual([...APP_LOCALES].sort());
});
it('keeps the English catalog free of translated-script contamination', () => {
const suspiciousValues = [...catalogFor('en').entries()]
.filter(([, value]) => /[\u0590-\u05FF\u0600-\u06FF\u0400-\u04FF\u4E00-\u9FFF]/u.test(value))
.map(([key, value]) => `${key} = ${JSON.stringify(value)}`);
expect(suspiciousValues).toEqual([]);
});
it('keeps every locale structurally identical to English', () => {
const englishKeys = [...catalogFor('en').keys()].sort();
for (const locale of APP_LOCALES) {
const localeKeys = [...catalogFor(locale).keys()].sort();
expect({ locale, missing: englishKeys.filter((key) => !localeKeys.includes(key)), extra: localeKeys.filter((key) => !englishKeys.includes(key)) }).toEqual({
locale,
missing: [],
extra: [],
});
}
});
it('preserves interpolation tokens exactly in every translation', () => {
const english = catalogFor('en');
const allMismatches = APP_LOCALES.flatMap((locale) => {
const catalog = catalogFor(locale);
return [...english.entries()]
.map(([key, englishValue]) => ({
locale,
key,
expected: interpolationTokens(englishValue),
actual: interpolationTokens(catalog.get(key) ?? ''),
}))
.filter(({ expected, actual }) => JSON.stringify(expected) !== JSON.stringify(actual));
});
expect(allMismatches).toEqual([]);
});
it('reports exact English duplicates for translation review', () => {
const english = catalogFor('en');
const duplicates = APP_LOCALES.filter((locale) => locale !== 'en').flatMap((locale) => {
const catalog = catalogFor(locale);
return [...english.entries()]
.filter(([key, englishValue]) => englishValue.length >= 4 && catalog.get(key) === englishValue)
.map(([key, value]) => ({ locale, key, value }));
});
const allowedExactKeys = new Set([
'addDownloads.cookiePlaceholder',
'downloadTable.interactionError',
'keychain.stores.windows',
'keychain.stores.macos',
'settings.common.stderr',
'settings.integrations.chromiumZip',
'settings.lookAndFeel.dracula',
'settings.lookAndFeel.nord',
'settings.network.chromeWindows',
'settings.network.chromeMacos',
'settings.network.edgeWindows',
'settings.network.firefoxWindows',
'settings.network.firefoxMacos',
'settings.network.safariMacos',
]);
const unexpectedDuplicates = duplicates
.filter(({ key }) => !allowedExactKeys.has(key))
.map(({ locale, key, value }) => `${locale}:${key} = ${JSON.stringify(value)}`);
expect(unexpectedDuplicates).toEqual([]);
});
});
+13 -709
View File
@@ -1,713 +1,17 @@
const common = {
navigation: {
library: 'Library',
filters: {
all: 'All',
active: 'Active',
completed: 'Completed',
unfinished: 'Unfinished',
},
categories: {
musics: 'Musics',
movies: 'Movies',
compressed: 'Compressed',
documents: 'Documents',
pictures: 'Pictures',
applications: 'Applications',
other: 'Other',
},
folders: 'Folders',
queues: 'Queues',
tools: 'Tools',
scheduler: 'Scheduler',
speedLimiter: 'Speed Limiter',
logs: 'Logs',
settings: 'Settings',
},
actions: {
hideSidebar: 'Hide Sidebar',
showSidebar: 'Show Sidebar',
resizeSidebar: 'Resize Sidebar',
cancel: 'Cancel',
continue: 'Continue',
dismissNotification: 'Dismiss notification',
queueName: 'Queue name',
addNewQueue: 'Add new queue',
startQueue: 'Start Queue',
pauseQueue: 'Pause Queue',
renameQueue: 'Rename Queue',
deleteQueue: 'Delete Queue',
},
dialogs: {
errorBoundary: {
title: 'Something went wrong.',
description: 'A critical error occurred in the React component tree. The error details below can help identify the root cause.',
reload: 'Reload App',
},
duplicateDownloads: {
title: 'Duplicate Downloads Detected',
description: 'Some of the downloads you are adding already exist in the queue or on disk. Please choose how to resolve these conflicts.',
rename: 'Rename',
replace: 'Replace',
skip: 'Skip',
},
removeDownload: {
title: 'Remove Download',
confirmationSingle: 'Are you sure you want to remove this item from the list? You can also choose to delete the underlying file from your hard drive.',
confirmationMultiple: 'Are you sure you want to remove these {{count}} items from the list? You can also choose to delete the underlying files from your hard drive.',
errorSummary: '{{succeeded}} removed, {{failed}} failed: {{detail}}',
remove: 'Remove',
deleteFile: 'Delete file',
},
},
window: {
controls: 'Window controls',
close: 'Close',
minimize: 'Minimize',
maximize: 'Maximize',
},
downloads: {
actions: {
moveUp: 'Move Up',
moveDown: 'Move Down',
pause: 'Pause',
start: 'Start',
resume: 'Resume',
options: 'Options',
},
size: {
downloadedOf: '{{downloaded}} downloaded of {{total}} {{unit}}',
downloadedOfApproximate: '{{downloaded}} downloaded of approximately {{total}} {{unit}}',
},
status: {
ready: 'Ready',
staged: 'In queue',
queued: 'Queued',
downloading: 'Downloading',
processing: 'Processing',
paused: 'Paused',
completed: 'Completed',
failed: 'Failed',
retrying: 'Retrying',
},
values: {
processing: 'Processing...',
muxing: 'Muxing...',
},
},
speedLimiter: {
title: 'Speed Limiter',
unlimited: 'Unlimited',
saveLimit: 'Save Limit',
globalSpeedLimit: 'Global Speed Limit',
description: 'Applies to new and active aria2 transfers and new yt-dlp media downloads. Explicit per-download limits remain attached to those transfers.',
quickPresets: 'Quick Presets',
removePreset: 'Remove {{value}} {{unit}} preset',
customPresetIn: 'Custom preset in {{unit}}',
addQuickPreset: 'Add quick preset',
globalLimitSaved: 'Global limit saved at {{value}} {{unit}}',
globalLimitDisabled: 'Global speed limit disabled',
saveFailed: 'Could not save global speed limit: {{detail}}',
presetAlreadyExists: '{{value}} {{unit}} is already in quick presets',
presetAdded: 'Added {{value}} {{unit}} quick preset',
presetRemoved: 'Removed {{value}} {{unit}} quick preset',
},
logs: {
title: 'Logs',
entries: '({{count}} entries)',
collecting: 'Collecting',
off: 'Off',
allLevels: 'All Levels',
levels: {
error: 'Error',
warn: 'Warn',
info: 'Info',
debug: 'Debug',
trace: 'Trace',
},
pauseLogging: 'Pause diagnostic logging',
enableLogging: 'Enable diagnostic logging',
clearDisplayed: 'Clear displayed logs',
export: 'Export logs',
exportButton: 'Export Logs',
logFiles: 'Log Files',
localDiagnostics: 'Local diagnostics:',
diagnosticsDescription: 'Diagnostic collection is opt-in, bounded, and local. Common secrets, URL queries, and home-directory paths are redacted during display and export. Nothing is uploaded automatically.',
noEntries: 'No persisted log entries are available yet.',
disabledDescription: 'Diagnostic logging is off. Existing support logs will appear here when available.',
copy: 'Copy',
copied: 'Copied to clipboard',
copyFailed: 'Failed to copy to clipboard',
exported: 'Support logs exported',
exportFailed: 'Could not export logs: {{detail}}',
cleared: 'Logs cleared',
clearFailed: 'Could not clear logs: {{detail}}',
enabled: 'Diagnostic logging enabled',
disabled: 'Diagnostic logging disabled',
updateFailed: 'Could not update diagnostic logging: {{detail}}',
},
scheduler: {
title: 'Scheduler',
running: 'Running',
disabled: 'Scheduler is disabled',
noScheduledDay: 'No scheduled day selected',
unsavedChanges: 'Unsaved changes',
runNow: 'Run Now',
pause: 'Pause',
saveSettings: 'Save Settings',
timing: 'Timing',
startTime: 'Start Time',
stopTime: 'Stop Time',
timingDescription: 'If Firelink is asleep at the start time, it starts the selected queues when it returns later that day, unless the stop time has already passed.',
runEveryDay: 'Run Every Day',
queuesToSchedule: 'Queues to Schedule',
defaultQueue: 'Default queue',
afterCompletion: 'After Completion',
afterCompletionDescription: 'Choose what happens after downloads started by the scheduler finish.',
actionWarning: 'This action can interrupt other work on the computer. Firelink invokes it immediately after the scheduled queue finishes.',
systemPermissions: 'System Permissions',
macPermissionDescription: 'Sleep, restart, and shut down require macOS Automation permission for System Events.',
permissionGranted: 'Automation permission granted',
permissionChecking: 'Checking Automation permission...',
permissionMissing: 'Automation permission missing',
revokePermission: 'Revoke permission',
grantPermission: 'Grant permission',
permissionAvailable: 'Automation permission is available.',
permissionMissingDetails: 'Automation permission is missing. Enable Firelink under Automation for System Events in System Settings.',
revokePermissionDetails: 'macOS does not allow Firelink to revoke Automation permission directly. Revoke System Events access in System Settings, then return to Firelink.',
requestingPermission: 'Requesting Automation permission...',
enablePermissionDetails: 'Enable Firelink under Automation for System Events in System Settings, then return to Firelink.',
systemActions: 'System Actions',
windowsActionsDescription: 'Sleep, restart, and shut down use Windows system privileges. Firelink reports any rejected action when it runs; no permanent permission is claimed in advance.',
linuxActionsDescription: 'Sleep, restart, and shut down use your Linux desktop and system policy. Firelink reports any rejected action when it runs; no permanent permission is claimed in advance.',
validationDay: 'Select at least one day for the scheduler',
validationQueue: 'Select at least one queue for the scheduler',
validationStopTime: 'Stop time must be later than start time',
saved: 'Scheduler settings saved',
trackingOne: 'Tracking 1 scheduled download',
trackingMany: 'Tracking {{count}} scheduled downloads',
noStartableDownloads: 'No downloads in the selected queues can be started',
pausedOne: 'Paused 1 active download',
pausedMany: 'Paused {{count}} active downloads',
noActiveDownloads: 'No active downloads',
days: {
su: 'Su',
mo: 'Mo',
tu: 'Tu',
we: 'We',
th: 'Th',
fr: 'Fr',
sa: 'Sa',
},
postActions: {
none: 'Do nothing',
sleep: 'Sleep',
restart: 'Restart',
shutdown: 'Shut down',
},
},
properties: {
progress: 'Progress',
size: 'Size',
speed: 'Speed',
eta: 'ETA',
connections: 'Connections',
speedCap: 'Speed cap',
category: 'Category',
lastTry: 'Last try',
dateAdded: 'Date added',
destination: 'Destination',
lastError: 'Last error',
saved: ' (saved)',
defaultValue: ' (default)',
savedTooltip: 'Saved for this download; Settings changes apply to new downloads.',
defaultTooltip: 'Using the current default for new downloads.',
identityReadOnly: 'File identity is read-only. Transfer settings are saved for redownload.',
transferSettings: 'Transfer settings can be changed after stopping or pausing. Current transfers keep their existing backend options.',
download: 'Download',
url: 'URL',
fileName: 'File name',
saveLocation: 'Save location',
select: 'Select',
perFile: 'per file',
useCurrentDefault: 'Use current default ({{count}})',
savedPerDownload: 'Saved per download. The Settings default applies to new downloads.',
limit: 'Limit',
siteLogin: 'Site Login',
siteLoginRedownload: 'Site Login for Redownload',
matchingSiteLogin: 'Matching site login',
customCredentials: 'Custom credentials',
noLogin: 'No login',
useSavedLogin: 'Will use saved login if available.',
advancedTransfer: 'Advanced Transfer',
advancedTransferRedownload: 'Advanced Transfer for Redownload',
checksum: 'Checksum',
verify: 'Verify',
algorithm: 'Algorithm',
digest: 'Digest',
expectedDigest: 'Expected digest',
cookies: 'Cookies',
headers: 'Headers',
mirrors: 'Mirrors',
username: 'Username',
password: 'Password',
enterValidUrl: 'Enter a valid URL.',
fileNameEmpty: 'File name cannot be empty.',
cancel: 'Cancel',
save: 'Save',
},
keychain: {
title: 'Credential Storage Access Needed',
stores: {
portable: 'the portable Firelink data folder',
windows: 'Windows Credential Manager',
linux: 'your Linux credential store',
macos: 'macOS Keychain',
system: "this system's credential store",
siteCredentials: "the system's credential store",
},
grantLabelPortable: 'Continue',
grantLabelMacos: 'Grant Access',
grantLabelDefault: 'Enable Secure Storage',
description: 'Firelink uses the browser extension to capture downloads. To keep the extension paired after restarts, Firelink stores its pairing token in {{pairingStore}}. Optional site credentials are stored in {{siteCredentialStore}}.',
portableExplanation: 'The pairing token is portable with this folder. Site credentials remain in the system credential store; a system prompt may appear after you grant access.',
macosExplanation: 'macOS may show a Keychain prompt after you grant access.',
defaultExplanation: 'This usually completes silently. If the credential service is unavailable, Firelink will show the error here and the extension will stay paired for this session only.',
note: 'Note:',
portableNote: 'Portable mode stores only the pairing token in this folder. It does not copy site passwords or browser credentials.',
defaultNote: 'Firelink only writes its own dedicated credential entry. It cannot access other saved passwords or credential items on your system.',
hint: 'Hint:',
portableHint: 'The portable pairing token is already stored with this folder; you can enable it here or select Later.',
defaultHint: 'If you select Later, the extension will only work for this session.',
enableFromSettings: 'You can enable storage anytime from Settings > Integrations.',
later: 'Later',
enabling: 'Enabling...',
timeout: 'Credential storage request timed out. You can select Later and try again.',
unavailable: '{{store}} is unavailable.',
accessRequired: 'Grant credential-store access before regenerating the pairing token.',
storeUnavailable: 'Credential store access is unavailable.',
},
app: {
settingsSaveFailed: 'Could not save settings. Check storage permissions and try again.',
systemActionCountdown: '{{action}} in 10 seconds.',
systemActionCancelled: 'System action cancelled because another download is active or queued.',
systemActionFailed: 'Scheduled system action failed: {{detail}}',
downloadCompleteTitle: 'Download Complete',
downloadCompleteBody: '{{fileName}} has finished downloading.',
unknownFile: 'A file',
downloadFailedTitle: 'Download Failed',
downloadFailedBody: '{{fileName}} failed to download.',
initializeDownloadsFailed: 'Could not initialize saved downloads: {{detail}}',
extensionDisconnected: 'Browser extension disconnected because its pairing token changed.',
copyToken: 'Copy token',
integrations: 'Integrations',
copyTokenFailed: 'Could not copy pairing token: {{detail}}',
credentialPersistenceFailed: 'Secure credential persistence is unavailable. Browser pairing works for this session only: {{detail}}',
resumeDownloadsFailed: 'Could not resume saved downloads: {{detail}}',
updateAvailable: 'Firelink {{version}} is available.',
viewRelease: 'View release',
sleepPreventionFailed: 'Could not update sleep prevention: {{detail}}',
schedulerNoQueues: 'Scheduler has no valid queues selected. Update Scheduler settings.',
schedulerPauseOneFailed: 'Scheduler could not pause 1 download.',
schedulerPauseManyFailed: 'Scheduler could not pause {{count}} downloads.',
scheduledIncomplete: 'Scheduled downloads did not all complete. The post-queue system action was skipped.',
scheduledActionSkippedActive: 'Scheduled system action skipped because another download is active or queued.',
notificationsDisabled: 'System notifications are disabled for Firelink.',
notificationsFailed: 'Could not configure notifications: {{detail}}',
},
sidebar: {
deleteQueueConfirm: 'Delete queue "{{name}}"? Its unfinished downloads will move to Main Queue.',
deleteQueueFailed: 'Could not delete queue: {{detail}}',
queueNameEmpty: 'Queue name cannot be empty',
queueNameExists: 'A queue with this name already exists',
startQueueFailed: 'Could not start queue: {{detail}}',
pauseQueueFailed: 'Could not pause queue: {{detail}}',
},
downloadTable: {
unknownQueue: 'Unknown Queue',
allDownloads: 'All Downloads',
active: 'Active',
completed: 'Completed',
unfinished: 'Unfinished',
headers: {
fileName: 'File Name',
size: 'Size',
status: 'Status',
speed: 'Speed',
eta: 'ETA',
dateAdded: 'Date Added',
},
addDownload: 'Add Download',
resumeAll: 'Resume All',
pauseAll: 'Pause All',
queueEmpty: 'Queue is empty',
noCompletedDownloads: 'No Completed Downloads',
noDownloads: 'No Downloads',
queueEmptyDescription: 'Add downloads to this queue from an item menu or the Add window.',
completedDescription: 'Completed downloads will appear here.',
clickToAdd: 'Click',
addButtonOr: 'button or',
toAddDownloads: 'to add downloads',
nonResumableOne: '1 download does not support resuming. If you pause it, you will have to start over again later. Are you sure you want to pause?',
nonResumableMany: '{{count}} downloads do not support resuming. If you pause them, you will have to start over again later. Are you sure you want to pause?',
interactionError: '{{message}}: {{detail}}',
openFileFailed: 'Could not open downloaded file',
revealFileFailed: 'Could not show download in Finder',
pauseFailed: 'Could not pause download',
resumeFailed: 'Could not resume {{fileName}}',
moveManyFailed: 'Could not move downloads to queue',
moveOneFailed: 'Could not move download to queue',
copyAddressesFailed: 'Could not copy addresses',
copyAddressFailed: 'Could not copy address',
copyPathFailed: 'Could not copy file path',
missingFileName: 'File name is missing',
redownloadFailed: 'Redownload failed',
startResume: 'Start/Resume',
addToQueue: 'Add to Queue',
copyAddress: 'Copy Address',
remove: 'Remove',
open: 'Open',
showInFolder: 'Show in Folder',
pause: 'Pause',
start: 'Start',
resume: 'Resume',
redownload: 'Redownload',
copyFilePath: 'Copy File Path',
properties: 'Properties',
backendRejectedStart: 'The backend rejected the start/resume request.',
transferActive: 'Cannot change properties while transfer is active. Pause it first.',
redownloadNotFound: 'Cannot redownload: download was not found.',
redownloadActive: 'Cannot redownload a {{status}} download. Pause or wait for it to finish first.',
originalUrlMissing: 'Cannot redownload: original URL is missing.',
pauseBeforeMove: 'Pause {{file}} before moving it to another queue.',
},
addDownloads: {
unknownSize: 'Unknown size',
unknown: 'Unknown',
discardSetup: 'Discard this download setup?',
speedInvalid: 'Speed limit must be greater than zero',
duplicateResolveFailed: 'Could not resolve duplicate downloads: {{detail}}',
addedWithFailures: '{{added}} added, {{failed}} failed. {{detail}}',
addedOne: '1 download added',
addedMany: '{{count}} downloads added',
urlAlreadyQueued: 'URL already in queue',
destinationConflict: 'Another selected download uses this destination',
existingDownloadDestination: 'Existing Firelink download uses this destination',
fileExistsOnDisk: 'File exists on disk; rename or skip to avoid deleting unrelated data',
backendRejectedStart: 'Backend rejected download start.',
playlistNoEntries: 'Playlist contains no downloadable entries',
invalidMediaMetadata: 'Invalid media metadata or no formats found',
noAvailableName: 'Could not find an available name for {{file}}.',
pauseBeforeReplace: 'Pause {{file}} before replacing it.',
cannotReplace: 'Cannot replace {{file}}: file is not owned by a Firelink download.',
downloadLinks: 'Download Links',
pastePlaceholder: 'Paste HTTP, HTTPS, FTP, or SFTP URLs here...\n\nFor media downloads, paste links from Youtube, X, TikTok, Instagram, Reddit, etc.',
playlistSummary: 'Playlist “{{title}}”: {{loaded}}{{total}} entries loaded{{truncated}}{{skipped}}',
safeEntryLimit: ' (safe entry limit reached)',
selectedSummary: '{{ready}} selected ready, {{fallback}} fallback, {{mediaRetry}} media retry, {{blocked}} blocked',
clearSelection: 'Clear selection',
selectAll: 'Select all',
refreshMetadata: 'Refresh Metadata',
files: 'Files',
required: 'Required',
free: 'Free',
preview: 'Preview',
file: 'File',
size: 'Size',
status: 'Status',
downloadPreview: 'Download preview',
noLinks: 'No links added yet.',
selectItem: 'Select {{file}}',
fetchingPlaylist: 'Fetching playlist...',
fetching: 'Fetching...',
unsafeUrl: 'Unsafe URL',
playlistFailed: 'Playlist failed',
metadataFailed: 'Metadata failed',
fallback: 'Fallback',
invalid: 'Invalid',
ready: 'Ready',
mediaFormat: 'Media Format',
playlistItem: 'Playlist item {{index}}',
fetchingMediaStreams: 'Fetching media streams...',
availableStreams: 'Available Streams',
availableMediaStreams: 'Available media streams',
metadataUnavailable: 'Metadata unavailable. Refresh metadata before adding this media.',
saveLocation: 'Save Location',
browse: 'Browse',
categoryFolders: 'Files will be organized into category folders.',
sharedFolder: 'All selected downloads will use this folder.',
transferSettings: 'Transfer Settings',
connectionsPerFile: 'Connections per File',
connectionsPerFileAria: 'Connections per file',
limitSpeedPerFile: 'Limit speed per file',
speedLimitPerFileAria: 'Speed limit per file',
authorization: 'Authorization',
useAuthorization: 'Use authorization',
username: 'Username',
password: 'Password',
advancedTransfer: 'Advanced Transfer',
verifyChecksum: 'Verify Checksum',
checksumAlgorithm: 'Checksum algorithm',
expectedDigest: 'Expected digest',
headers: 'Headers',
requestHeaders: 'Request headers',
cookies: 'Cookies',
cookiePlaceholder: 'name=value; other=value',
mirrors: 'Mirrors',
cancel: 'Cancel',
startDownloads: 'Start Downloads',
addToQueue: 'Add to queue',
pasteOneOrMore: 'Paste one or more links.',
selectAtLeastOne: 'Select at least one download.',
correctInvalid: 'Correct or remove {{count}} invalid URL{{plural}} before continuing.',
waitingForMetadata: 'Waiting for metadata for {{count}} download{{plural}}.',
removeUnsafe: 'Remove {{count}} unsafe URL{{plural}} before continuing.',
mediaMetadataUnavailableSummary: 'Media metadata is unavailable for {{count}} item{{plural}}. Refresh metadata before adding.',
metadataUnavailableFallback: 'Metadata is unavailable. Downloads can still be added using fallback details.',
fallbackReady: '{{ready}} download{{readyPlural}} ready; {{failed}} will use fallback filename and unknown size.',
readyToAdd: 'Ready to add {{count}} download{{plural}}.',
},
settings: {
tabs: {
downloads: 'Downloads',
lookAndFeel: 'Look and feel',
network: 'Network',
locations: 'Locations',
siteLogins: 'Site Logins',
power: 'Power',
engine: 'Engine',
integrations: 'Integrations',
about: 'About',
},
common: {
thisOs: 'this OS',
checking: 'Checking...',
ready: 'Ready',
errorMissing: 'Error / Missing',
hideTechnicalDetails: 'Hide technical details',
showTechnicalDetails: 'Show technical details',
binary: 'Binary',
expected: 'Expected',
error: 'Error',
tip: 'Tip',
stderr: 'stderr',
daemonProcessAlive: 'Daemon process alive',
rpcReady: 'RPC ready',
rpcPort: 'RPC port',
daemonStderr: 'daemon stderr',
packaging: 'Packaging: PyInstaller onedir (_internal required)',
internalDirectoryFound: '_internal directory found',
pythonRuntimeFound: 'Python runtime found',
externalOpenFailed: 'Could not open {{label}}: {{detail}}',
updateUnexpectedResponse: 'The update check returned an unexpected response.',
updateUntrustedReleaseUrl: 'The update check returned an untrusted release URL.',
updateFailed: 'Update check failed: {{detail}}',
},
downloads: {
defaultConnections: 'Default connections:',
defaultConnectionsDescription: 'New downloads; existing items keep their saved value',
parallelDownloads: 'Parallel downloads:',
parallelDownloadsDescription: 'Max simultaneous active files',
automaticRetries: 'Automatic retries:',
automaticRetriesDescription: 'If a connection fails',
systemNotification: 'Show system notification when download completes',
systemNotificationDescription: 'Uses your operating system notification settings',
completionChime: 'Play in-app completion chime',
completionChimeDescription: 'Optional Firelink sound, independent of system notifications',
clipboardLinks: 'Add clipboard links when Firelink becomes active',
clipboardLinksDescription: 'Opens supported copied links in the Add window. Off by default.',
},
lookAndFeel: {
appTheme: 'App Theme',
theme: 'Theme',
ariaLabel: 'App theme',
system: 'System',
light: 'Light',
dark: 'Dark',
dracula: 'Dracula',
nord: 'Nord',
systemAppearance: 'System follows the current {{platform}} light or dark appearance.',
display: 'Display',
fontSize: 'Font size',
fontSizeDescription: 'Scales the download list and compact controls.',
small: 'Small',
standard: 'Standard',
large: 'Large',
listDensity: 'List density',
listDensityDescription: 'Changes row height, spacing, and progress bar scale.',
compact: 'Compact',
comfortable: 'Comfortable',
relaxed: 'Relaxed',
osIntegration: 'OS Integration',
dockBadge: 'Show badge on Dock icon',
dockBadgeDescription: 'Displays active download count on Firelink Dock icon.',
menuBarIcon: 'Show menu bar icon',
statusIndicatorIcon: 'Show status indicator icon',
systemTrayIcon: 'Show system tray icon',
macosTrayDescription: 'Provides quick access from the macOS menu bar.',
windowsTrayDescription: 'Provides quick access from the Windows notification area.',
linuxTrayDescription: 'Provides quick access from the desktop tray or status area when available.',
defaultTrayDescription: 'Provides quick access from the OS tray area when available.',
},
network: {
proxy: 'Proxy',
mode: 'Mode',
modeDescription: 'Controls proxy use for new download requests.',
noProxy: 'No Proxy',
systemProxy: 'System Proxy',
customProxy: 'Custom Proxy',
proxyHost: 'Proxy host',
proxyHostDescription: 'Host name, IP address, or HTTP proxy URL.',
proxyHostPlaceholder: '127.0.0.1 or http://127.0.0.1',
proxyPort: 'Proxy port',
proxyPortDescription: 'Valid range is 1 to 65535.',
noProxyDescription: 'Downloads ignore configured proxies.',
systemProxyDescription: 'Downloads use the detected {{platform}} system proxy. Normal file downloads require an HTTP or HTTPS proxy endpoint; media downloads can use SOCKS.',
customProxyDescription: 'Downloads use the configured HTTP proxy endpoint for metadata and download engines.',
invalidCustomProxy: 'Enter a valid HTTP proxy host and port to enable the custom proxy.',
incompleteCustomProxy: 'Enter a proxy host and port to enable the custom proxy.',
checkingSystemProxy: 'Checking system proxy configuration…',
detectedSystemProxy: 'A system proxy was detected. Normal file downloads require an HTTP or HTTPS endpoint; media downloads can use SOCKS.',
noSystemProxy: 'No usable system proxy was detected. Downloads will use no proxy.',
systemProxyReadFailed: 'System proxy configuration could not be read. Choose No Proxy or try again when it is available.',
identity: 'Identity',
customUserAgent: 'Custom User-Agent',
userAgentDescription: 'Applied to metadata fetches and download engines.',
userAgentPlaceholder: 'Leave blank for Firelink default',
userAgentOverrides: 'Overrides the outbound User-Agent header. Leave blank for Firelink defaults.',
chromeWindows: 'Chrome (Windows)',
chromeMacos: 'Chrome (macOS)',
edgeWindows: 'Edge (Windows)',
firefoxWindows: 'Firefox (Windows)',
firefoxMacos: 'Firefox (macOS)',
safariMacos: 'Safari (macOS)',
windowsDesktop: 'Windows desktop',
macosDesktop: 'macOS desktop',
},
locations: {
baseDownloadFolder: 'Base Download Folder',
baseDownloadFolderDescription: 'Automatic category folders are created inside this folder.',
browse: 'Browse',
baseFolderCreateFailed: 'Base folder saved, but category folders could not be created: {{detail}}',
baseFolderUpdated: 'Base download folder updated',
askWhereToSave: 'Ask where to save when adding downloads',
rememberLastUsedDownloadDirectory: 'Remember the last used download directory',
rememberLastUsedDownloadDirectoryDescription: 'Use the last folder selected in the Add window for the next download until the app restarts.',
automaticCategorySubfolders: 'Automatically save files to category subfolders',
automaticCategorySubfoldersDescription: 'When off, automatic downloads use the base folder.',
categorySubfolders: 'Category Subfolders',
relativeToBase: 'Relative to the base folder',
disabled: 'Disabled',
categorySubfolder: '{{category}} subfolder',
customFolder: 'Custom folder',
useAutomatic: 'Use automatic',
resetDefaults: 'Reset Defaults',
resetToast: 'Reset category locations to default',
},
siteLogins: {
title: 'Site Credentials',
noSavedLogins: 'No saved logins.',
user: 'User: {{username}}',
deleteCredential: 'Delete credential',
deletedCredential: 'Deleted credential',
deleteFailed: 'Could not delete credential: {{detail}}',
addTitle: 'Add Site Credentials',
urlPattern: 'URL Pattern:',
urlPatternPlaceholder: 'e.g. *.example.com or example.com/downloads',
username: 'Username:',
password: 'Password:',
usernamePlaceholder: 'Username',
passwordPlaceholder: 'Password',
saving: 'Saving…',
addLogin: 'Add Login',
urlPatternRequired: 'URL pattern is required.',
urlPatternWhitespace: 'URL pattern cannot contain whitespace.',
usernameRequired: 'Username is required.',
passwordRequired: 'Password is required.',
accessBeforeSave: 'Grant credential-store access before saving a site login.',
saveFailed: 'Failed to save site credential securely.',
addedCredential: 'Added site credential',
},
power: {
title: 'Power Management',
preventSleep: 'Prevent system sleep while downloads are active',
preventSleepDescription: 'The display may still turn off. Firelink only keeps the device awake enough to complete active transfers.',
},
engine: {
title: 'Media Downloader & Engines',
description: 'Successful results are reused for this app session. Recheck runs real validation again.',
recheck: 'Recheck engines',
coreDownloader: 'Core Downloader (Aria2)',
version: 'Version:',
status: 'Status:',
mediaExtractors: 'Media Extractors',
browserCookiesSource: 'Browser Cookies Source:',
cookieDescription: 'yt-dlp reads browser cookies to bypass video download limits or access restricted media. Firelink does not save browser cookies.',
none: 'None',
},
integrations: {
title: 'Connect Browser Extension',
description: 'Capture downloads directly from your browser in three easy steps.',
portablePairingEnabled: 'Portable Pairing Enabled',
pairingTokenPersisted: 'Pairing Token Persisted',
portablePersistedDescription: 'Your pairing token is stored with this portable Firelink folder and will persist when the folder is moved. Treat the folder as sensitive.',
systemPersistedDescription: 'Your pairing token is stored in the system credential store and will persist across restarts.',
portablePairingAvailable: 'Portable Pairing Available',
credentialStorageNeeded: 'Credential Storage Needed',
portableAvailableDescription: 'Your pairing token is stored with this portable Firelink folder and will persist across restarts. Enable it here to review the portable-storage warning.',
credentialStorageDescription: "Firelink needs access to this system's credential store to securely save your pairing token across app restarts. Currently, your extension will only stay connected for this session.",
reviewPortablePairing: 'Review Portable Pairing',
grantCredentialAccess: 'Grant Credential Access',
copyToken: 'Copy Token',
tokenDescription: 'This secure token authorizes your browser extension.',
tokenCopied: 'Token copied to clipboard!',
tokenCopyFailed: 'Could not copy token: {{detail}}',
pairingTokenRegenerated: 'Pairing token regenerated',
regenerateFailed: 'Could not regenerate pairing token: {{detail}}',
getExtension: 'Get Extension',
extensionDescription: 'Install Firelink Companion for Firefox or Chromium browsers.',
firefoxAddons: 'Firefox Add-ons',
chromiumZip: 'Chromium ZIP',
pasteConnect: 'Paste & Connect',
pasteConnectDescription: "Click the Firelink icon in your browser's toolbar and paste the copied token.",
extensionServerStatus: 'Extension Server Status:',
listening: '● Listening on 127.0.0.1:{{port}}',
unavailable: '● Server unavailable',
},
about: {
firelinkIcon: 'Firelink Icon',
unknown: 'Unknown',
version: 'Version {{version}}',
description: 'Cross-platform download manager for macOS, Windows, and Linux.',
sourceCode: 'Source Code',
source: 'Source',
upToDate: 'Firelink {{version}} is up to date.',
updateAvailable: 'Firelink {{version}} is available.',
viewRelease: 'View release',
updates: 'Updates',
checkForUpdates: 'Check for Updates',
updateDescription: 'Firelink checks GitHub Releases for new versions.',
checking: 'Checking...',
checkNow: 'Check Now',
automaticUpdates: 'Automatically check for updates',
createdBy: 'Created by NimBold',
mitLicense: 'MIT License',
credits: 'Credits: aria2, yt-dlp, FFmpeg, and Deno.',
copyright: 'Copyright © 2026 NimBold. All rights reserved.',
},
},
status: {
ready: 'Ready',
active: '{{count}} active',
queued: '{{count}} queued',
done: '{{count}} done',
},
} as const;
import en from "./catalogs/en";
import fa from "./catalogs/fa";
import he from "./catalogs/he";
import ru from "./catalogs/ru";
import uk from "./catalogs/uk";
import zhCN from "./catalogs/zh-CN";
export const defaultNS = 'common' as const;
export const defaultNS = "common" as const;
export const resources = {
en: {
common,
},
en: { common: en },
"zh-CN": { common: zhCN },
fa: { common: fa },
he: { common: he },
uk: { common: uk },
ru: { common: ru },
} as const;
+103 -31
View File
@@ -548,6 +548,7 @@ html[data-list-density="relaxed"] {
min-width: 0;
min-height: 0;
overflow: hidden;
border-inline-end: 1px solid hsl(var(--border-modal));
background:
radial-gradient(circle at 18% 0%, hsl(var(--accent-color) / 0.055), transparent 34%),
hsl(var(--main-bg) / 0.56);
@@ -619,6 +620,8 @@ html[data-list-density="relaxed"] {
.add-download-links-input {
border-radius: 10px;
line-height: 1.55;
direction: ltr;
text-align: start;
}
.add-download-link-button {
@@ -1003,14 +1006,15 @@ html[data-list-density="relaxed"] {
}
.app-sidebar-shell {
padding: 9px 0 9px 9px;
padding-block: 9px;
padding-inline-start: 9px;
background: transparent;
}
.sidebar-resize-handle {
position: absolute;
top: 18px;
right: -4px;
inset-inline-end: -4px;
bottom: 18px;
z-index: 60;
width: 8px;
@@ -1022,7 +1026,7 @@ html[data-list-density="relaxed"] {
content: '';
position: absolute;
top: 0;
right: 3px;
inset-inline-end: 3px;
bottom: 0;
width: 1px;
background: transparent;
@@ -1043,10 +1047,10 @@ html[data-list-density="relaxed"] {
background: hsl(var(--sidebar-panel-bg));
border: 1px solid hsl(var(--sidebar-border));
border-top-left-radius: 18px;
border-bottom-left-radius: 18px;
border-top-right-radius: 17px;
border-bottom-right-radius: 17px;
border-start-start-radius: 18px;
border-end-start-radius: 18px;
border-start-end-radius: 17px;
border-end-end-radius: 17px;
box-shadow:
inset 0 1px 0 hsl(0 0% 100% / 0.045),
@@ -1061,13 +1065,13 @@ html[data-list-density="relaxed"] {
.app-sidebar-reveal-button {
position: absolute;
top: 12px;
left: 88px;
inset-inline-start: 88px;
z-index: 80;
-webkit-app-region: no-drag;
}
.app-workspace--custom-window-controls .app-sidebar-reveal-button {
left: 124px;
inset-inline-start: 124px;
}
.app-statusbar {
@@ -1240,7 +1244,7 @@ html[data-list-density="relaxed"] {
.sidebar-toggle-button {
position: absolute;
top: 11px;
right: 12px;
inset-inline-end: 12px;
z-index: 50;
width: 24px;
@@ -1445,7 +1449,7 @@ html[data-list-density="relaxed"] {
background: transparent;
color: hsl(var(--text-primary));
font: inherit;
text-align: left;
text-align: start;
}
.settings-combobox-option:last-child {
@@ -1639,11 +1643,11 @@ html[data-list-density="relaxed"] {
}
.app-workspace--sidebar-collapsed .main-titlebar {
padding-left: 124px;
padding-inline-start: 124px;
}
.app-workspace--sidebar-collapsed.app-workspace--custom-window-controls .main-titlebar {
padding-left: 160px;
padding-inline-start: 160px;
}
.main-titlebar-title {
@@ -1654,7 +1658,7 @@ html[data-list-density="relaxed"] {
}
.main-control-group {
margin-left: auto;
margin-inline-start: auto;
height: 28px;
display: inline-flex;
align-items: center;
@@ -1672,7 +1676,7 @@ html[data-list-density="relaxed"] {
align-items: center;
justify-content: center;
color: hsl(var(--text-secondary));
border-right: 1px solid hsl(var(--border-color));
border-inline-end: 1px solid hsl(var(--border-color));
-webkit-app-region: no-drag;
}
@@ -1682,7 +1686,7 @@ html[data-list-density="relaxed"] {
}
.main-control-button:last-child {
border-right: 0;
border-inline-end: 0;
}
.main-control-button:hover:not(:disabled) {
@@ -1819,11 +1823,11 @@ html[data-list-density="relaxed"] {
}
.download-table-header > div:first-child {
padding-left: 0;
padding-inline-start: 0;
}
.download-table-header > div:last-child {
padding-right: 0;
padding-inline-end: 0;
}
.download-table-header > div > span {
@@ -1834,13 +1838,13 @@ html[data-list-density="relaxed"] {
}
.download-table-header > div:not(:first-child) {
padding-left: var(--download-column-padding-x);
padding-inline-start: var(--download-column-padding-x);
}
.column-resize-handle {
position: absolute;
top: 0;
right: -4px;
inset-inline-end: -4px;
bottom: 0;
z-index: 5;
width: 8px;
@@ -1922,11 +1926,11 @@ html[data-list-density="relaxed"] .download-ghost-row {
}
.download-row > div:first-child {
padding-left: 0;
padding-inline-start: 0;
}
.download-row > div:last-child {
padding-right: 0;
padding-inline-end: 0;
}
@@ -1973,7 +1977,7 @@ html[data-list-density="relaxed"] .download-ghost-row {
.download-size-cell {
justify-content: flex-start;
text-align: left;
text-align: start;
}
.download-size-content {
@@ -1982,17 +1986,18 @@ html[data-list-density="relaxed"] .download-ghost-row {
width: 100%;
min-width: 0;
overflow: hidden;
text-align: left;
direction: ltr;
text-align: start;
}
.download-size-content > .download-size-progress {
flex: 0 1 auto;
text-align: left;
text-align: start;
}
.download-size-content > .download-size-total {
flex: 0 1 auto;
text-align: left;
text-align: start;
}
.download-size-content > .download-size-progress,
@@ -2005,17 +2010,17 @@ html[data-list-density="relaxed"] .download-ghost-row {
}
.download-row > .download-size-cell {
padding-left: calc(var(--download-column-padding-x) + 8px);
padding-inline-start: calc(var(--download-column-padding-x) + 8px);
}
.download-table-header > div:nth-child(2) {
padding-left: calc(var(--download-column-padding-x) + 8px);
padding-inline-start: calc(var(--download-column-padding-x) + 8px);
}
.download-cell-truncate > span,
.download-cell-right > span {
display: block;
text-align: left;
text-align: start;
max-width: 100%;
min-width: 0;
overflow: hidden;
@@ -2028,7 +2033,7 @@ html[data-list-density="relaxed"] .download-ghost-row {
}
.download-cell-right {
text-align: left;
text-align: start;
display: flex;
justify-content: flex-start;
align-items: center;
@@ -2036,6 +2041,28 @@ html[data-list-density="relaxed"] .download-ghost-row {
overflow: hidden;
}
.add-download-nested-fields {
padding-inline-start: 1.25rem;
border-inline-start: 2px solid hsl(var(--border-modal) / 0.5);
}
.add-download-advanced-fields {
padding-inline-start: 1.5rem;
}
.add-download-queue-menu {
inset-inline-end: 0;
}
.download-context-submenu {
inset-inline-start: 100%;
margin-inline-start: 0.25rem;
}
html[dir="rtl"] .download-context-menu-chevron {
transform: scaleX(-1);
}
.download-empty-row {
color: hsl(var(--text-muted));
}
@@ -2070,7 +2097,7 @@ html[data-list-density="relaxed"] .download-ghost-row {
min-width: 0;
max-width: 100%;
flex: 0 0 auto;
text-align: left;
text-align: start;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
@@ -2286,3 +2313,48 @@ html[data-list-density="relaxed"] .download-ghost-row {
transition-duration: 0.01ms !important;
}
}
html[dir="rtl"] .text-left {
text-align: right;
}
html[dir="rtl"] .font-mono,
html[dir="rtl"] input[type="number"] {
direction: ltr;
unicode-bidi: isolate;
}
html[dir="rtl"] .sidebar-nav-item > svg {
margin-right: 0;
margin-left: 0.75rem;
}
html[dir="rtl"] .sidebar-nav-item .sidebar-count {
margin-left: 0;
margin-right: auto;
}
html[dir="rtl"] .sidebar-inner .mr-2 {
margin-right: 0;
margin-left: 0.5rem;
}
html[dir="rtl"] .sidebar-inner .mr-3 {
margin-right: 0;
margin-left: 0.75rem;
}
html[dir="rtl"] .app-sidebar-panel {
box-shadow:
inset 0 1px 0 hsl(0 0% 100% / 0.045),
-2px 0 10px hsl(0 0% 0% / 0.20);
}
html[dir="rtl"] .mac-switch::after {
left: auto;
right: 2px;
}
html[dir="rtl"] .mac-switch:checked::after {
transform: translateX(-16px);
}
+11 -1
View File
@@ -20,6 +20,7 @@ import {
} from '../utils/downloadLocations';
import { normalizeSpeedLimitForBackend } from '../utils/downloads';
import i18n from '../i18n';
import { isAppLocalePreference, type AppLocalePreference } from '../i18n/locales';
let settingsQueue: Promise<void> = Promise.resolve();
let pairingTokenHydrationRequest: Promise<PairingTokenHydration> | null = null;
@@ -78,6 +79,7 @@ const SETTINGS_TAB_VALUES = [
] as const;
type PersistedSettingsSnapshot = PersistedSettings & {
language: AppLocalePreference;
keychainPromptDismissed: boolean;
keychainAccessVersion: string;
};
@@ -164,6 +166,7 @@ export type {
export interface SettingsState {
theme: Theme;
language: AppLocalePreference;
baseDownloadFolder: string;
categorySubfoldersEnabled: boolean;
categorySubfolders: Record<string, string>;
@@ -215,6 +218,7 @@ export interface SettingsState {
showKeychainModal: boolean;
setTheme: (theme: Theme) => void;
setLanguage: (language: AppLocalePreference) => void;
setBaseDownloadFolder: (path: string) => void;
approveDownloadRoot: (path: string) => Promise<string>;
setMaxConcurrentDownloads: (count: number) => void;
@@ -269,6 +273,7 @@ export const useSettingsStore = create<SettingsState>()(
persist(
(set, get) => ({
theme: 'system',
language: 'system',
baseDownloadFolder: '~/Downloads',
categorySubfoldersEnabled: true,
categorySubfolders: { ...DEFAULT_CATEGORY_SUBFOLDERS },
@@ -328,6 +333,7 @@ export const useSettingsStore = create<SettingsState>()(
showKeychainModal: false,
setTheme: (theme) => { info('Settings updated: theme'); set({ theme }); },
setLanguage: (language) => { info('Settings updated: language'); set({ language }); },
setBaseDownloadFolder: (path) => {
info('Settings updated: baseDownloadFolder');
set({ baseDownloadFolder: path });
@@ -494,7 +500,7 @@ export const useSettingsStore = create<SettingsState>()(
{
name: 'firelink-settings',
storage: createJSONStorage(() => tauriStorage),
version: 3,
version: 4,
migrate: (persistedState) => {
if (!persistedState || typeof persistedState !== 'object') {
return persistedState as SettingsState;
@@ -530,6 +536,7 @@ export const useSettingsStore = create<SettingsState>()(
},
partialize: (state): PersistedSettingsSnapshot => ({
theme: state.theme,
language: state.language,
baseDownloadFolder: state.baseDownloadFolder,
categorySubfoldersEnabled: state.categorySubfoldersEnabled,
categorySubfolders: state.categorySubfolders,
@@ -588,6 +595,9 @@ export const useSettingsStore = create<SettingsState>()(
theme: isAllowedSetting(THEME_VALUES, persisted.theme)
? persisted.theme
: currentState.theme,
language: isAppLocalePreference(persisted.language)
? persisted.language
: currentState.language,
appFontSize: isAllowedSetting(APP_FONT_SIZE_VALUES, persisted.appFontSize)
? persisted.appFontSize
: currentState.appFontSize,
+16
View File
@@ -12,6 +12,7 @@ import {
updateRowIfCurrent,
type AddDownloadDraftRow
} from './addDownloadMetadata';
import i18n, { changeAppLocale } from '../i18n';
const row = (
overrides: Partial<AddDownloadDraftRow> = {}
@@ -537,4 +538,19 @@ describe('add download metadata workflow', () => {
row({ status: 'invalid' })
])).toContain('Correct or remove 1 invalid URL');
});
it('uses few forms for Russian and Ukrainian metadata summaries', async () => {
const originalLanguage = i18n.language;
const twoReadyRows = [row(), row({ id: 'row-2' })];
try {
await changeAppLocale('ru');
expect(metadataSummaryMessage(twoReadyRows)).toBe('Готово к добавлению: 2 загрузки.');
await changeAppLocale('uk');
expect(metadataSummaryMessage(twoReadyRows)).toBe('Готово до додавання: 2 завантаження.');
} finally {
await changeAppLocale(originalLanguage === 'uk' || originalLanguage === 'ru' ? originalLanguage : 'en');
}
});
});
+50 -7
View File
@@ -5,6 +5,7 @@ import {
} from './downloads';
import type { MediaPlaylistMetadata } from '../bindings/MediaPlaylistMetadata';
import i18n from '../i18n';
import { localePluralVariant } from '../i18n/locales';
export type MetadataStatus = 'loading' | 'ready' | 'metadata-error' | 'invalid';
@@ -397,20 +398,42 @@ export const metadataSummaryState = (rows: AddDownloadDraftRow[]): MetadataSumma
};
export const metadataSummaryMessage = (rows: AddDownloadDraftRow[]): string => {
const pluralMessage = (
count: number,
one: () => string,
few: () => string,
many: () => string
): string => {
switch (localePluralVariant(i18n.language, count)) {
case 'one': return one();
case 'few': return few();
case 'many': return many();
}
};
if (rows.length === 0) return i18n.t($ => $.addDownloads.pasteOneOrMore);
const selectedRows = rows.filter(row => row.selected !== false);
if (selectedRows.length === 0) return i18n.t($ => $.addDownloads.selectAtLeastOne);
const plural = (count: number) => count === 1 ? '' : 's';
const invalid = selectedRows.filter(row => row.status === 'invalid').length;
if (invalid > 0) {
return i18n.t($ => $.addDownloads.correctInvalid, { count: invalid, plural: plural(invalid) });
return pluralMessage(
invalid,
() => i18n.t($ => $.addDownloads.correctInvalidOne, { count: invalid }),
() => i18n.t($ => $.addDownloads.correctInvalidFew, { count: invalid }),
() => i18n.t($ => $.addDownloads.correctInvalidMany, { count: invalid })
);
}
const loading = selectedRows.filter(row => row.status === 'loading').length;
if (loading > 0) {
return i18n.t($ => $.addDownloads.waitingForMetadata, { count: loading, plural: plural(loading) });
return pluralMessage(
loading,
() => i18n.t($ => $.addDownloads.waitingForMetadataOne, { count: loading }),
() => i18n.t($ => $.addDownloads.waitingForMetadataFew, { count: loading }),
() => i18n.t($ => $.addDownloads.waitingForMetadataMany, { count: loading })
);
}
const failed = selectedRows.filter(row => row.status === 'metadata-error').length;
@@ -418,16 +441,36 @@ export const metadataSummaryMessage = (rows: AddDownloadDraftRow[]): string => {
const blocked = selectedRows.filter(row => row.metadataBlockedReason === 'unsafe-url').length;
const ready = selectedRows.filter(row => row.status === 'ready').length;
if (blocked > 0) {
return i18n.t($ => $.addDownloads.removeUnsafe, { count: blocked, plural: plural(blocked) });
return pluralMessage(
blocked,
() => i18n.t($ => $.addDownloads.removeUnsafeOne, { count: blocked }),
() => i18n.t($ => $.addDownloads.removeUnsafeFew, { count: blocked }),
() => i18n.t($ => $.addDownloads.removeUnsafeMany, { count: blocked })
);
}
if (failedMedia > 0) {
return i18n.t($ => $.addDownloads.mediaMetadataUnavailableSummary, { count: failedMedia, plural: plural(failedMedia) });
return pluralMessage(
failedMedia,
() => i18n.t($ => $.addDownloads.mediaMetadataUnavailableSummaryOne, { count: failedMedia }),
() => i18n.t($ => $.addDownloads.mediaMetadataUnavailableSummaryFew, { count: failedMedia }),
() => i18n.t($ => $.addDownloads.mediaMetadataUnavailableSummaryMany, { count: failedMedia })
);
}
if (failed === selectedRows.length) {
return i18n.t($ => $.addDownloads.metadataUnavailableFallback);
}
if (failed > 0) {
return i18n.t($ => $.addDownloads.fallbackReady, { ready, readyPlural: plural(ready), failed });
return pluralMessage(
ready,
() => i18n.t($ => $.addDownloads.fallbackReadyOne, { ready, failed }),
() => i18n.t($ => $.addDownloads.fallbackReadyFew, { ready, failed }),
() => i18n.t($ => $.addDownloads.fallbackReadyMany, { ready, failed })
);
}
return i18n.t($ => $.addDownloads.readyToAdd, { count: ready, plural: plural(ready) });
return pluralMessage(
ready,
() => i18n.t($ => $.addDownloads.readyToAddOne, { count: ready }),
() => i18n.t($ => $.addDownloads.readyToAddFew, { count: ready }),
() => i18n.t($ => $.addDownloads.readyToAddMany, { count: ready })
);
};