diff --git a/plugins/visage/visage.js b/plugins/visage/visage.js index 1b429461..d17f3240 100644 --- a/plugins/visage/visage.js +++ b/plugins/visage/visage.js @@ -1,8 +1,8 @@ (function () { 'use strict'; - const React$g = window.PluginApi.React; - const { createContext, useContext, useReducer, useCallback: useCallback$7 } = React$g; + const React$h = window.PluginApi.React; + const { createContext, useContext, useReducer, useCallback: useCallback$7 } = React$h; const initialState = { scenario: null, scenarioId: null, @@ -183,7 +183,7 @@ openSettings, closeSettings, }; - return React$g.createElement(VisageContext.Provider, { value }, children); + return React$h.createElement(VisageContext.Provider, { value }, children); } function useVisage() { const context = useContext(VisageContext); @@ -8676,6 +8676,23 @@ return null; return boxes[getStashBoxIndex(boxes, sourceName)].endpoint; } + // The active Stash UI locale, read from the server-side configuration the same + // way the Stash frontend does (App.tsx reads configuration.interface.language). + // Falls back to 'en'. Note: Stash does not reflect the locale in . + async function getStashLanguage() { + var _a, _b, _c; + try { + const result = await getClient().query({ + query: GQL.ConfigurationDocument, + }); + const lang = (_c = (_b = (_a = result.data) === null || _a === void 0 ? void 0 : _a.configuration) === null || _b === void 0 ? void 0 : _b.interface) === null || _c === void 0 ? void 0 : _c.language; + return typeof lang === 'string' && lang ? lang : 'en'; + } + catch (err) { + console.warn('[Visage] Failed to read Stash language:', err); + return 'en'; + } + } async function getStashboxStatus() { var _a, _b, _c; const result = await getClient().query({ @@ -8844,8 +8861,9 @@ toggle.setAttribute('aria-expanded', 'false'); } function isOpsMenu(menu) { - const text = menu.textContent || ''; - return text.includes('Delete') && (text.includes('Generate') || text.includes('Rescan') || text.includes('Submit')); + var _a; + return menu.getAttribute('aria-labelledby') === 'operation-menu' + || !!((_a = menu.closest('.dropdown')) === null || _a === void 0 ? void 0 : _a.querySelector('[id="operation-menu"]')); } function injectMenu(menu) { const divider = document.createElement('hr'); @@ -8861,6 +8879,251 @@ return error instanceof Error ? error.message : fallback; } + // English source dictionary. Every user-facing string in the plugin lives here. + // Keys are namespaced by component. `{var}` tokens are interpolated via t(). + // Adding a new string: add it here with an English value; every locale dict is + // Partial, so missing keys fall back to English automatically. + const en = { + // ---- BackendSettings.tsx ---- + 'backendSettings.title': 'Backend settings', + 'backendSettings.closeAria': 'Close settings', + 'backendSettings.backendAria': 'Backend settings', + 'backendSettings.changeBackend': 'Change backend', + 'backendSettings.backendLabel': 'Backend', + 'backendSettings.local': 'Local', + 'backendSettings.cloud': 'Cloud (Hugging Face)', + 'backendSettings.cloudNote': 'Images are sent to the Hugging Face cloud service.', + 'backendSettings.hintPrefix': 'Want your images to stay on your network?', + 'backendSettings.hintLink': 'Run a private server via Patreon', + 'backendSettings.urlLabel': 'URL', + 'backendSettings.csp1': "The browser's security policy (CSP) only allows", + 'backendSettings.csp2': 'by default. To reach a local backend on another address (e.g. your LAN IP), add it to the', + 'backendSettings.csp3': 'list in the', + 'backendSettings.csp4': 'file inside your Stash plugins folder, otherwise requests will be blocked. Note: updating Visage reinstalls', + 'backendSettings.csp5': ', so this must be reapplied after every update.', + 'backendSettings.testing': 'Testing connection…', + 'backendSettings.testConnection': 'Test connection', + 'backendSettings.testingShort': 'Testing…', + 'backendSettings.cancel': 'Cancel', + 'backendSettings.save': 'Save', + 'backendSettings.feedback.reachable': 'Connection successful. Backend is ready.', + 'backendSettings.feedback.degraded': 'Backend reachable but degraded (models or index not loaded).', + 'backendSettings.feedback.unreachable': 'Backend unreachable. Check the URL and that the backend is running.', + // ---- FaceMatchModal.tsx ---- + 'faceMatch.title': 'CURRENT FRAME', + 'faceMatch.close': 'Close', + 'faceMatch.facesSelected': '{faces} faces found · {selected} selected', + 'faceMatch.inScene': '· {count} in scene', + 'faceMatch.stashboxMissing': 'No stash-box configured.', + 'faceMatch.stashboxMissingBody': ' Add a stash-box provider in Settings → Metadata Providers to enable performer import.', + 'faceMatch.stashboxWrongName': 'No "StashDB" provider found.', + 'faceMatch.stashboxWrongNameBody': ' Performer import requires a provider named "StashDB". Rename your provider in Settings → Metadata Providers.', + 'faceMatch.learnMore': 'Learn more.', + 'faceMatch.scanning': 'Scanning • face recognition…', + 'faceMatch.faceAlt': 'Face {index}', + 'faceMatch.minConf': 'Min conf.', + 'faceMatch.minConfTitle': 'Minimum confidence: {percent}%', + 'faceMatch.detected': 'Detected', + 'faceMatch.detectedFaceAlt': 'Detected face', + 'faceMatch.vs': 'vs', + 'faceMatch.supportPatreon': 'Support on Patreon', + 'faceMatch.ofSelected': '{selected} of {total} selected', + 'faceMatch.allInScene': '{total} faces found · all in scene', + 'faceMatch.clickToSelect': '{total} faces found · click to select', + 'faceMatch.kbSwitch': 'Switch faces', + 'faceMatch.kbSelect': 'Select performers', + 'faceMatch.kbToggle': 'Toggle select', + 'faceMatch.kbAddInstant': 'Shift+click to add instantly', + 'faceMatch.selectBest': 'Select Best Matches', + 'faceMatch.adding': 'Adding...', + 'faceMatch.done': 'Done ({count})', + 'faceMatch.toast.added': 'Added performer to the {target}.', + 'faceMatch.toast.addError': 'Failed to add performer: {error}', + 'faceMatch.toast.noStashbox': 'No stash-box configured. Add a stash-box provider in Settings → Metadata Providers to enable performer import. See {url}', + 'faceMatch.toast.noProvider': 'No provider named "StashDB" found. Rename your provider to "StashDB" in Settings → Metadata Providers to enable performer import.', + 'faceMatch.toast.configureProvider': 'Configure a stash-box provider in Settings → Metadata Providers to enable performer import.', + 'faceMatch.toast.addedMultiple': 'Added {count} performer{s} to the {target}.', + // ---- SpriteResultModal.tsx ---- + 'sprite.title': 'SCENE PERFORMERS', + 'sprite.close': 'Close', + 'sprite.foundConfirmed': '{found} found · {confirmed} confirmed', + 'sprite.confidence': 'confidence', + 'sprite.name': 'name', + 'sprite.hits': 'hits', + 'sprite.minConf': 'Min conf.', + 'sprite.minConfTitle': 'Minimum confidence: {percent}%', + 'sprite.scanning': 'Visage Scanning…', + 'sprite.cancel': 'Cancel', + 'sprite.empty': 'No performers identified in this sprite.', + 'sprite.detectedFaceAlt': 'Detected face', + 'sprite.spriteLabel': 'SPRITE', + 'sprite.stashLabel': 'STASH', + 'sprite.vs': 'vs', + 'sprite.inScene': 'In scene', + 'sprite.hitsCount': '{count} hit{s}', + 'sprite.totalTime': '{time} total', + 'sprite.alreadyInScene': 'Already in scene', + 'sprite.clickToConfirm': 'Click to confirm', + 'sprite.confirmed': 'Confirmed', + 'sprite.supportPatreon': 'Support on Patreon', + 'sprite.confirmedCount': '{confirmed} of {total} confirmed', + 'sprite.shownHint': '{shown} shown ({total} total) · click to confirm · ←→ navigate · Enter confirm', + 'sprite.confirmHint': 'Click to confirm · ←→ navigate · Enter confirm', + 'sprite.adding': 'Adding...', + 'sprite.done': 'Done ({count})', + // ---- PerformerCard.tsx ---- + 'gender.male': 'Male', + 'gender.female': 'Female', + 'gender.transMale': 'Transgender male', + 'gender.transFemale': 'Transgender female', + 'gender.nonBinary': 'Non-binary', + 'gender.intersex': 'Intersex', + 'card.excellent': 'Excellent match', + 'card.good': 'Good match', + 'card.uncertain': 'Uncertain match', + 'card.select': 'Select {name}', + 'card.deselect': 'Deselect {name}', + 'card.openOn': 'Open on {source}', + // ---- FaceSearchButton.tsx ---- + 'search.overlayHint': 'Drag to select a face — Enter to scan the whole frame — Esc to cancel', + 'search.noFaces': 'No faces found in that selection. Try a tighter crop, or press Enter to scan the whole frame.', + 'search.captureMediaFail': 'Could not capture media. Please ensure the scene/image is fully loaded.', + 'search.healthBanner': 'Face recognition API is not reachable. Start the backend and try again.', + 'search.failed': 'Face search failed: {error}', + 'search.fetchImageFail': 'Could not fetch image from Stash.', + 'search.captureFail': 'Failed to capture image: {error}', + 'search.selectFaceImage': 'Select a face within the image.', + 'search.captureFrameFail': 'Could not capture the current frame.', + 'search.captureFrameFail2': 'Failed to capture the current frame.', + 'search.selectFaceVideo': 'Select a face within the video player area.', + 'search.menuItemTitle': 'Drag a box around a face, or press Enter to scan the whole frame, to search StashDB for matches', + 'search.currentFrame': 'Visage: Current Frame', + // ---- SceneScanButton.tsx ---- + 'scene.noSprite': 'No sprite sheet or preview video found for this scene. Generate them in Scene settings, then try again.', + 'scene.noFaces': 'No faces or performers found in this scene’s sprite sheet or preview video.', + 'scene.healthBanner': 'Face recognition API is not reachable. Start the backend and try again.', + 'scene.failed': 'Scene scan failed: {error}', + 'scene.menuItemTitle': 'Identify every performer in the scene (needs a generated sprite sheet or preview video)', + 'scene.wholeScene': 'Visage: Whole Scene', + // ---- BackendHealthBanner.tsx ---- + 'banner.changeBackend': 'Change backend', + 'banner.dismiss': 'Dismiss', + // ---- ErrorDialog.tsx ---- + 'error.dismiss': 'Dismiss', + // ---- FirstRunDialog.tsx ---- + 'firstRun.title': 'Set up your Visage backend', + 'firstRun.subtitle': 'Visage sends face images to a backend for recognition. Choose where to run it.', + 'firstRun.cloud': 'Use Hugging Face cloud', + 'firstRun.cloudNote': 'Zero setup. Images are sent to the Hugging Face cloud service.', + 'firstRun.local': 'Use my own server', + 'firstRun.localNote': 'Run the private binary on your own machine or network.', + 'firstRun.skip': 'Skip for now', + // ---- BackendBadge.tsx ---- + 'badge.local': 'Local', + 'badge.cloud': 'Cloud (Hugging Face)', + 'badge.title': 'Visage backend: {label}', + // ---- DonateFooter.tsx ---- + 'donate.enjoying': 'Enjoying Visage? Help keep it alive', + 'donate.supportPatreon': 'Support on Patreon', + // ---- FaceFrameSelector.tsx ---- + 'frame.close': 'Close frame selector', + 'frame.seekFail': 'Failed to seek video player.', + 'frame.selectAt': 'Select face frame at {time}s', + }; + + // All locale dicts registered by the loader. `en` is always present. + const dicts = { + en, + }; + function registerLocale(code, dict) { + dicts[code.toLowerCase()] = dict; + } + // The active locale, defaulting to English. Stash does NOT sync the + // attribute to the UI locale (it stays "en"), so the real locale is read from + // the Stash configuration via GraphQL (see services/stash.getStashLanguage) and + // applied here through setActiveLocale. + let currentLocale = { code: 'en', language: 'en' }; + // Returns the active locale. Defaults to English until setActiveLocale is called. + function getLocale() { + return currentLocale; + } + // Updates the active locale used by t(). Called once the real Stash language is + // resolved from the GraphQL configuration. + function setActiveLocale(locale) { + currentLocale = locale; + } + function normalizeLocale(lang) { + const trimmed = lang.trim().toLowerCase(); + if (!trimmed) + return { code: 'en', language: 'en' }; + // Stash uses full BCP-47 tags (e.g. "nl-NL"); also tolerate bare "nl". + const [language, region] = trimmed.split('-'); + return { code: trimmed, language, region }; + } + // Translate a key with optional {var} interpolation. Unknown keys fall back + // to English (or the raw key if even en lacks it) — never throws. + function t(key, vars) { + var _a, _b; + const locale = getLocale(); + let value = (_b = (_a = lookup(locale.code, key)) !== null && _a !== void 0 ? _a : en[key]) !== null && _b !== void 0 ? _b : key; + if (vars) { + for (const [k, v] of Object.entries(vars)) { + value = value.split(`{${k}}`).join(String(v)); + } + } + return value; + } + function lookup(code, key) { + var _a; + const dict = dicts[code]; + if (dict === null || dict === void 0 ? void 0 : dict[key]) + return dict[key]; + const { language } = normalizeLocale(code); + if (language && language !== code) { + const langDict = dicts[language]; + if (langDict === null || langDict === void 0 ? void 0 : langDict[key]) + return langDict[key]; + } + // If the active code is bare (e.g. "nl"), fall back to any dict whose + // registered region code shares that language prefix (e.g. "nl-NL"). + const prefixDict = Object.keys(dicts).find((k) => { var _a; return k.startsWith(language + '-') && ((_a = dicts[k]) === null || _a === void 0 ? void 0 : _a[key]); }); + if (prefixDict) + return (_a = dicts[prefixDict]) === null || _a === void 0 ? void 0 : _a[key]; + return undefined; + } + + // React via Stash PluginApi, matching the pattern used across the plugin. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const React$g = window.PluginApi.React; + // Re-renders the component when the active locale changes. Stash does not sync + // the attribute to the UI locale, so the real locale is resolved + // from the GraphQL configuration (configuration.interface.language) and applied + // via setActiveLocale. Stash reloads the page when the language changes, so a + // mount-time fetch is sufficient. + function useLocale() { + const [locale, setLocale] = React$g.useState(() => getLocale()); + React$g.useEffect(() => { + let cancelled = false; + (async () => { + const lang = await getStashLanguage(); + if (cancelled) + return; + const next = normalizeLocale(lang); + const current = getLocale(); + if (next.code !== current.code || next.language !== current.language) { + setActiveLocale(next); + setLocale(next); + } + })().catch(() => { + // Swallow: default to English if the locale can't be resolved. + }); + return () => { + cancelled = true; + }; + }, []); + return locale; + } + const React$f = window.PluginApi.React; const { useCallback: useCallback$6 } = React$f; let overlayDiv = null; @@ -8877,7 +9140,7 @@ overlayDiv = document.createElement('div'); overlayDiv.style.cssText = 'position:fixed;inset:0;z-index:2147483647;cursor:crosshair;background:rgba(0,0,0,0.12);'; const instructions = document.createElement('div'); - instructions.textContent = 'Drag to select a face \u2014 Enter to scan the whole frame \u2014 Esc to cancel'; + instructions.textContent = t('search.overlayHint'); instructions.style.cssText = 'position:fixed;top:16px;left:50%;transform:translateX(-50%);background:#07070f;color:rgba(255,255,255,0.8);padding:10px 24px;border-radius:8px;font:13px/1.4 system-ui,sans-serif;border:1px solid rgba(255,255,255,0.07);box-shadow:0 4px 20px rgba(0,0,0,0.5);pointer-events:none;z-index:2147483648;'; overlayDiv.appendChild(instructions); selectionDiv = document.createElement('div'); @@ -9057,7 +9320,7 @@ const result = await callGradioAPI('multiple_image_search', [blob, MAX_RESULTS]); const matchData = result.data; if (!matchData || typeof matchData !== 'object') { - showWarning('No faces found in that selection. Try a tighter crop, or press Enter to scan the whole frame.'); + showWarning(t('search.noFaces')); return; } const first = Array.isArray(matchData) ? matchData[0] : matchData; @@ -9066,7 +9329,7 @@ return; } if (!Array.isArray(matchData) || matchData.length === 0) { - showWarning('No faces found in that selection. Try a tighter crop, or press Enter to scan the whole frame.'); + showWarning(t('search.noFaces')); return; } setMatches(matchData); @@ -9075,13 +9338,13 @@ catch (error) { const message = getErrorMessage(error); if (message.includes('Could not find element')) { - showError('Could not capture media. Please ensure the scene/image is fully loaded.'); + showError(t('search.captureMediaFail')); } else if (message.includes('Failed to fetch') || message.includes('NetworkError')) { - showHealthBanner('Face recognition API is not reachable. Start the backend and try again.'); + showHealthBanner(t('search.healthBanner')); } else { - showError('Face search failed: ' + message); + showError(t('search.failed', { error: message })); } } finally { @@ -9091,6 +9354,7 @@ } } function FaceSearchButton({ menuItem }) { + useLocale(); const { state, setLoading, setMatches, setScenario, showWarning, showError, showHealthBanner, startMatchSearch, showMatchModal } = useVisage(); const loadingKey = 'face-search'; const isLoading = state.loading[loadingKey] || false; @@ -9105,14 +9369,14 @@ try { const blob = await fetchImageBlob(scenarioId); if (!blob) { - showWarning('Could not fetch image from Stash.'); + showWarning(t('search.fetchImageFail')); setLoading(loadingKey, false); return; } await dispatchBlob(blob, showWarning, showError, showHealthBanner, setLoading, setMatches, setScenario, loadingKey, startMatchSearch, showMatchModal); } catch (error) { - showError('Failed to capture image: ' + getErrorMessage(error)); + showError(t('search.captureFail', { error: getErrorMessage(error) })); setLoading(loadingKey, false); } return; @@ -9120,7 +9384,7 @@ makeOverlay((blob) => { dispatchBlob(blob, showWarning, showError, showHealthBanner, setLoading, setMatches, setScenario, loadingKey, startMatchSearch, showMatchModal); }, () => { - showWarning('Select a face within the image.'); + showWarning(t('search.selectFaceImage')); }, { region: async (bounds) => { const b = await fetchImageBlob(scenarioId); @@ -9137,7 +9401,7 @@ try { const blob = await captureWholeFrame(); if (!blob) { - showWarning('Could not capture the current frame.'); + showWarning(t('search.captureFrameFail')); setLoading(loadingKey, false); return; } @@ -9145,7 +9409,7 @@ } catch (error) { console.warn('[Visage] captureWholeFrame failed:', error); - showError('Failed to capture the current frame.'); + showError(t('search.captureFrameFail2')); setLoading(loadingKey, false); } return; @@ -9153,7 +9417,7 @@ makeOverlay((blob) => { dispatchBlob(blob, showWarning, showError, showHealthBanner, setLoading, setMatches, setScenario, loadingKey, startMatchSearch, showMatchModal); }, () => { - showWarning('Select a face within the video player area.'); + showWarning(t('search.selectFaceVideo')); }); }, [setLoading, setMatches, setScenario, isLoading, showWarning, showError, showHealthBanner, startMatchSearch, showMatchModal]); if (menuItem) { @@ -9161,16 +9425,16 @@ href: '#', className: 'bg-secondary text-white dropdown-item', role: 'button', - title: 'Drag a box around a face, or press Enter to scan the whole frame, to search StashDB for matches', + title: t('search.menuItemTitle'), onClick: (e) => { e.preventDefault(); closeDropdown(); recognize(); }, - }, 'Visage: Current Frame'); + }, t('search.currentFrame')); } return React$f.createElement('button', { id: 'visage-frame-search', className: `visage-toolbar-button${isLoading ? ' visage-scanning' : ''}`, onClick: recognize, disabled: isLoading, - title: 'Visage: Current Frame', + title: t('search.currentFrame'), }, React$f.createElement('svg', { width: 20, height: 20, viewBox: '0 0 24 24', fill: 'currentColor', xmlns: 'http://www.w3.org/2000/svg', @@ -9301,6 +9565,7 @@ const React$e = window.PluginApi.React; const { useCallback: useCallback$5 } = React$e; function SceneScanButton({ menuItem }) { + useLocale(); const { state, setLoading, setSpriteResult, showSpriteModal, setDetectionMode, setScanProgress, showWarning, showError, showHealthBanner } = useVisage(); const loadingKey = 'face-detection'; const isLoading = state.loading[loadingKey] || false; @@ -9334,10 +9599,10 @@ const spriteUrl = await getUrlSprite(sceneId); const previewUrl = await getUrlPreview(sceneId); if (!spriteUrl && !previewUrl) { - showWarning('No sprite sheet or preview video found for this scene. Generate them in Scene settings, then try again.'); + showWarning(t('scene.noSprite')); } else { - showWarning('No faces or performers found in this scene\u2019s sprite sheet or preview video.'); + showWarning(t('scene.noFaces')); } } catch (error) { @@ -9345,10 +9610,10 @@ showSpriteModal(false); const message = getErrorMessage(error); if (message.includes('Failed to fetch') || message.includes('NetworkError')) { - showHealthBanner('Face recognition API is not reachable. Start the backend and try again.'); + showHealthBanner(t('scene.healthBanner')); } else { - showError('Scene scan failed: ' + message); + showError(t('scene.failed', { error: message })); } } finally { @@ -9360,16 +9625,16 @@ href: '#', className: 'bg-secondary text-white dropdown-item', role: 'button', - title: 'Identify every performer in the scene (needs a generated sprite sheet or preview video)', + title: t('scene.menuItemTitle'), onClick: (e) => { e.preventDefault(); closeDropdown(); scanScene(); }, - }, 'Visage: Whole Scene'); + }, t('scene.wholeScene')); } return React$e.createElement('button', { id: 'visage-scene-scan', className: `visage-toolbar-button${isLoading ? ' visage-scanning' : ''}`, onClick: scanScene, disabled: isLoading, - title: 'Visage: Whole Scene', + title: t('scene.wholeScene'), }, React$e.createElement('svg', { width: 20, height: 20, viewBox: '0 0 24 24', fill: 'currentColor', xmlns: 'http://www.w3.org/2000/svg', @@ -9541,12 +9806,12 @@ return `${base}/performers/${id}`; }; const GENDER_CONFIG = { - MALE: { symbol: '♂', label: 'Male', className: 'visage-gender-male' }, - FEMALE: { symbol: '♀', label: 'Female', className: 'visage-gender-female' }, - TRANSGENDER_MALE: { symbol: '⚧', label: 'Transgender male', className: 'visage-gender-transgender-male' }, - TRANSGENDER_FEMALE: { symbol: '⚧', label: 'Transgender female', className: 'visage-gender-transgender-female' }, - NON_BINARY: { symbol: '⚦', label: 'Non-binary', className: 'visage-gender-non-binary' }, - INTERSEX: { symbol: '⚥', label: 'Intersex', className: 'visage-gender-intersex' }, + MALE: { symbol: '♂', label: 'gender.male', className: 'visage-gender-male' }, + FEMALE: { symbol: '♀', label: 'gender.female', className: 'visage-gender-female' }, + TRANSGENDER_MALE: { symbol: '⚧', label: 'gender.transMale', className: 'visage-gender-transgender-male' }, + TRANSGENDER_FEMALE: { symbol: '⚧', label: 'gender.transFemale', className: 'visage-gender-transgender-female' }, + NON_BINARY: { symbol: '⚦', label: 'gender.nonBinary', className: 'visage-gender-non-binary' }, + INTERSEX: { symbol: '⚥', label: 'gender.intersex', className: 'visage-gender-intersex' }, }; const genderBadge = (gender) => { if (!gender) @@ -9554,13 +9819,15 @@ const cfg = GENDER_CONFIG[gender]; if (!cfg) return null; + const label = t(cfg.label); return React$a.createElement('span', { className: `visage-gender-badge ${cfg.className}`, - title: cfg.label, - 'aria-label': cfg.label, + title: label, + 'aria-label': label, }, cfg.symbol); }; function PerformerCard({ performer, isLoading, isKeyboardSelected, isSelected, onToggle, onQuickAdd }) { + useLocale(); const imgRef = useSmoothLoad(); const confidence = performer.confidence || 0; const confidenceClass = confidence >= 90 @@ -9569,10 +9836,10 @@ ? 'visage-confidence-good' : 'visage-confidence-uncertain'; const confidenceLabel = confidence >= 90 - ? 'Excellent match' + ? t('card.excellent') : confidence >= 70 - ? 'Good match' - : 'Uncertain match'; + ? t('card.good') + : t('card.uncertain'); const cardClasses = [ 'visage-performer-card', isLoading ? 'visage-loading' : '', @@ -9605,7 +9872,7 @@ role: 'button', tabIndex: isKeyboardSelected ? 0 : -1, 'aria-pressed': isSelected, - 'aria-label': `${isSelected ? 'Deselect' : 'Select'} ${performer.name}`, + 'aria-label': t(isSelected ? 'card.deselect' : 'card.select', { name: performer.name }), }, // Portrait image area React$a.createElement('div', { className: 'visage-card-portrait' }, React$a.createElement('img', { @@ -9640,7 +9907,7 @@ // Text overlay: name + confidence React$a.createElement('div', { className: 'visage-card-meta' }, React$a.createElement('a', { href: performerUrl(performer.id, performer.source), - title: `Open on ${(performer.source || 'stashdb').toLowerCase()}`, + title: t('card.openOn', { source: (performer.source || 'stashdb').toLowerCase() }), target: '_blank', rel: 'noopener noreferrer', className: 'visage-performer-name', @@ -9665,26 +9932,29 @@ return host === 'localhost' || host === '127.0.0.1' || host === '::1'; } const TEST_FEEDBACK = { - reachable: 'Connection successful. Backend is ready.', - degraded: 'Backend reachable but degraded (models or index not loaded).', - unreachable: 'Backend unreachable. Check the URL and that the backend is running.', + reachable: 'backendSettings.feedback.reachable', + degraded: 'backendSettings.feedback.degraded', + unreachable: 'backendSettings.feedback.unreachable', }; function SettingsCloseButton({ onClick }) { + useLocale(); return React$9.createElement('button', { className: 'visage-backend-settings-close', onClick, - 'aria-label': 'Close settings', + 'aria-label': t('backendSettings.closeAria'), }, React$9.createElement('svg', { width: 16, height: 16, viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: 2 }, React$9.createElement('line', { x1: 18, y1: 6, x2: 6, y2: 18 }), React$9.createElement('line', { x1: 6, y1: 6, x2: 18, y2: 18 }))); } function SettingsButton({ onClick }) { + useLocale(); return React$9.createElement('button', { className: 'visage-modal-settings', onClick, - 'aria-label': 'Backend settings', - title: 'Change backend', + 'aria-label': t('backendSettings.backendAria'), + title: t('backendSettings.changeBackend'), }, React$9.createElement('svg', { width: 20, height: 20, viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: 2 }, React$9.createElement('path', { d: 'M12 15a3 3 0 1 0 0-6 3 3 0 0 0 0 6z' }), React$9.createElement('path', { d: 'M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z' }))); } function BackendSettings({ onClose, initialMode }) { + useLocale(); const current = getApiEndpoint(); const [mode, setMode] = useState$3(() => { if (initialMode) @@ -9755,8 +10025,8 @@ onClose(); } const statusText = testStatus === 'testing' - ? 'Testing connection…' - : (testStatus ? TEST_FEEDBACK[testStatus] : ''); + ? t('backendSettings.testing') + : (testStatus ? t(TEST_FEEDBACK[testStatus]) : ''); const showNonLocalhostWarn = mode === 'local' && url.trim() !== '' && !isLocalHost(url.trim()); return React$9.createElement('div', { className: 'visage-backend-settings-backdrop', @@ -9766,19 +10036,19 @@ className: 'visage-backend-settings', role: 'dialog', 'aria-modal': 'true', - 'aria-label': 'Backend settings', - }, React$9.createElement('div', { className: 'visage-backend-settings-header' }, React$9.createElement('h2', { className: 'visage-backend-settings-title' }, 'Backend settings'), React$9.createElement(SettingsCloseButton, { onClick: onClose })), React$9.createElement('div', { className: 'visage-backend-settings-body' }, React$9.createElement('div', { className: 'visage-backend-settings-field' }, React$9.createElement('span', { className: 'visage-backend-settings-label' }, 'Backend'), React$9.createElement('div', { className: 'visage-backend-settings-seg' }, React$9.createElement('button', { + 'aria-label': t('backendSettings.backendAria'), + }, React$9.createElement('div', { className: 'visage-backend-settings-header' }, React$9.createElement('h2', { className: 'visage-backend-settings-title' }, t('backendSettings.title')), React$9.createElement(SettingsCloseButton, { onClick: onClose })), React$9.createElement('div', { className: 'visage-backend-settings-body' }, React$9.createElement('div', { className: 'visage-backend-settings-field' }, React$9.createElement('span', { className: 'visage-backend-settings-label' }, t('backendSettings.backendLabel')), React$9.createElement('div', { className: 'visage-backend-settings-seg' }, React$9.createElement('button', { className: `visage-backend-seg-opt${mode === 'local' ? ' visage-backend-seg-active' : ''}`, onClick: () => selectMode('local'), - }, 'Local'), React$9.createElement('button', { + }, t('backendSettings.local')), React$9.createElement('button', { className: `visage-backend-seg-opt${mode === 'cloud' ? ' visage-backend-seg-active' : ''}`, onClick: () => selectMode('cloud'), - }, 'Cloud (Hugging Face)'))), mode === 'cloud' && React$9.createElement('div', { className: 'visage-backend-settings-note' }, 'Images are sent to the Hugging Face cloud service.'), mode === 'cloud' && React$9.createElement('p', { className: 'visage-backend-settings-hint' }, 'Want your images to stay on your network? ', React$9.createElement('a', { + }, t('backendSettings.cloud')))), mode === 'cloud' && React$9.createElement('div', { className: 'visage-backend-settings-note' }, t('backendSettings.cloudNote')), mode === 'cloud' && React$9.createElement('p', { className: 'visage-backend-settings-hint' }, t('backendSettings.hintPrefix'), React$9.createElement('a', { className: 'visage-backend-settings-hint-link', href: 'https://www.patreon.com/cw/cc12340', target: '_blank', rel: 'noopener noreferrer', - }, 'Run a private server via Patreon'), '.'), React$9.createElement('div', { className: 'visage-backend-settings-field' }, React$9.createElement('label', { className: 'visage-backend-settings-label', htmlFor: 'visage-backend-url' }, 'URL'), mode === 'local' + }, t('backendSettings.hintLink')), '.'), React$9.createElement('div', { className: 'visage-backend-settings-field' }, React$9.createElement('label', { className: 'visage-backend-settings-label', htmlFor: 'visage-backend-url' }, t('backendSettings.urlLabel')), mode === 'local' ? React$9.createElement('input', { id: 'visage-backend-url', className: 'visage-backend-settings-input', @@ -9787,20 +10057,20 @@ onChange: (e) => { setUrl(e.target.value); setTestStatus(null); }, placeholder: LOCAL_DEFAULT, }) - : React$9.createElement('div', { className: 'visage-backend-settings-cloud-url' }, url)), showNonLocalhostWarn && React$9.createElement('div', { className: 'visage-backend-settings-warn' }, 'The browser\'s security policy (CSP) only allows ', React$9.createElement('code', null, 'http://localhost:7860'), ' by default. To reach a local backend on another address (e.g. your LAN IP), add it to the ', React$9.createElement('code', null, 'connect-src'), ' list in the ', React$9.createElement('code', null, 'visage.yml'), ' file inside your Stash plugins folder, otherwise requests will be blocked. Note: updating Visage reinstalls ', React$9.createElement('code', null, 'visage.yml'), ', so this must be reapplied after every update.'), React$9.createElement('div', { + : React$9.createElement('div', { className: 'visage-backend-settings-cloud-url' }, url)), showNonLocalhostWarn && React$9.createElement('div', { className: 'visage-backend-settings-warn' }, t('backendSettings.csp1'), React$9.createElement('code', null, 'http://localhost:7860'), t('backendSettings.csp2'), React$9.createElement('code', null, 'connect-src'), t('backendSettings.csp3'), React$9.createElement('code', null, 'visage.yml'), t('backendSettings.csp4'), React$9.createElement('code', null, 'visage.yml'), t('backendSettings.csp5')), React$9.createElement('div', { className: `visage-backend-settings-status${testStatus ? ` visage-backend-${testStatus}` : ''}`, role: 'status', }, statusText)), React$9.createElement('div', { className: 'visage-backend-settings-actions' }, React$9.createElement('button', { className: 'visage-btn visage-btn-secondary', onClick: handleTest, disabled: testStatus === 'testing', - }, testStatus === 'testing' ? 'Testing…' : 'Test connection'), React$9.createElement('button', { + }, testStatus === 'testing' ? t('backendSettings.testingShort') : t('backendSettings.testConnection')), React$9.createElement('button', { className: 'visage-btn visage-btn-secondary', onClick: onClose, - }, 'Cancel'), React$9.createElement('button', { + }, t('backendSettings.cancel')), React$9.createElement('button', { className: 'visage-btn visage-btn-primary', onClick: handleSave, - }, 'Save')))); + }, t('backendSettings.save'))))); } const React$8 = window.PluginApi.React; @@ -9811,6 +10081,7 @@ * button opens the backend settings. */ function BackendBadge() { + useLocale(); let isLocal; try { isLocal = isLocalEndpoint(getApiEndpoint()); @@ -9818,12 +10089,13 @@ catch (_a) { isLocal = false; } - const label = isLocal ? 'Local' : 'Cloud (Hugging Face)'; + const label = isLocal ? t('badge.local') : t('badge.cloud'); const variant = isLocal ? 'local' : 'cloud'; + const title = t('badge.title', { label }); return React$8.createElement('span', { className: `visage-backend-badge visage-backend-${variant}`, - 'aria-label': `Visage backend: ${label}`, - title: `Visage backend: ${label}`, + 'aria-label': title, + title, }, label); } @@ -9831,6 +10103,7 @@ const { useCallback: useCallback$2, useEffect: useEffect$3, useRef: useRef$3, useState: useState$2 } = React$7; function FaceMatchModal() { var _a, _b, _c, _d, _e; + useLocale(); const { state, setLoading, showMatchModal, showError, showSuccess, showWarning, openSettings } = useVisage(); const { matches: rawMatches, loading: loadingState } = state; const matches = Array.isArray(rawMatches) ? rawMatches : []; @@ -9933,13 +10206,13 @@ try { const ok = await addSinglePerformer(stashId); if (ok) { - showSuccess(`Added performer to the ${scenario === 'scenes' ? 'scene' : 'image'}.`); + showSuccess(t('faceMatch.toast.added', { target: scenario === 'scenes' ? 'scene' : 'image' })); close(); } } catch (error) { console.warn('[Visage] quick-add error:', error); - showError('Failed to add performer: ' + getErrorMessage(error)); + showError(t('faceMatch.toast.addError', { error: getErrorMessage(error) })); } finally { setAdding(false); @@ -10013,19 +10286,19 @@ } if (addedCount === 0) { const msg = stashboxStatus === 'empty' - ? 'No stash-box configured. Add a stash-box provider in Settings \u2192 Metadata Providers to enable performer import. See ' + STASHBOX_DOCS_URL + ? t('faceMatch.toast.noStashbox', { url: STASHBOX_DOCS_URL }) : stashboxStatus === 'mismatch' - ? 'No provider named "StashDB" found. Rename your provider to "StashDB" in Settings \u2192 Metadata Providers to enable performer import.' - : 'Configure a stash-box provider in Settings \u2192 Metadata Providers to enable performer import.'; + ? t('faceMatch.toast.noProvider') + : t('faceMatch.toast.configureProvider'); showWarning(msg); return; } - showSuccess(`Added ${addedCount} performer${addedCount !== 1 ? 's' : ''} to the ${scenario === 'scenes' ? 'scene' : 'image'}.`); + showSuccess(t('faceMatch.toast.addedMultiple', { count: addedCount, s: addedCount !== 1 ? 's' : '', target: scenario === 'scenes' ? 'scene' : 'image' })); close(); } catch (error) { console.warn('[Visage] addConfirmed error:', error); - showError('Failed to add performers: ' + getErrorMessage(error)); + showError(t('faceMatch.toast.addError', { error: getErrorMessage(error) })); } finally { setAdding(false); @@ -10064,16 +10337,16 @@ role: 'dialog', 'aria-modal': 'true', tabIndex: -1, - }, React$7.createElement('div', { className: 'visage-modal-header' }, React$7.createElement('div', { className: 'visage-modal-title' }, React$7.createElement('h2', null, 'CURRENT FRAME'), !isSearching && React$7.createElement('span', { className: 'visage-match-count' }, `${matches.length} faces found \u00B7 ${selectedPerformer.size} selected` + (sceneStashIds.size > 0 ? ` \u00B7 ${sceneStashIds.size} in scene` : ''))), React$7.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: '8px' } }, React$7.createElement(BackendBadge, null), React$7.createElement(SettingsButton, { onClick: openSettings }), React$7.createElement('button', { + }, React$7.createElement('div', { className: 'visage-modal-header' }, React$7.createElement('div', { className: 'visage-modal-title' }, React$7.createElement('h2', null, t('faceMatch.title')), !isSearching && React$7.createElement('span', { className: 'visage-match-count' }, t('faceMatch.facesSelected', { faces: matches.length, selected: selectedPerformer.size }) + (sceneStashIds.size > 0 ? ' ' + t('faceMatch.inScene', { count: sceneStashIds.size }) : ''))), React$7.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: '8px' } }, React$7.createElement(BackendBadge, null), React$7.createElement(SettingsButton, { onClick: openSettings }), React$7.createElement('button', { className: 'visage-modal-close', onClick: close, - 'aria-label': 'Close', + 'aria-label': t('faceMatch.close'), }, React$7.createElement('svg', { width: 20, height: 20, viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: 2, - }, React$7.createElement('line', { x1: 18, y1: 6, x2: 6, y2: 18 }), React$7.createElement('line', { x1: 6, y1: 6, x2: 18, y2: 18 }))))), stashboxStatus !== null && stashboxStatus !== 'configured' && React$7.createElement('div', { className: 'visage-stashbox-banner' }, React$7.createElement('svg', { width: 16, height: 16, viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: 2 }, React$7.createElement('path', { d: 'M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z' }), React$7.createElement('line', { x1: 12, y1: 9, x2: 12, y2: 13 }), React$7.createElement('line', { x1: 12, y1: 17, x2: 12.01, y2: 17 })), React$7.createElement('span', { className: 'visage-stashbox-banner-text' }, React$7.createElement('strong', null, stashboxStatus === 'empty' ? 'No stash-box configured.' : 'No "StashDB" provider found.'), stashboxStatus === 'empty' - ? ' Add a stash-box provider in Settings \u2192 Metadata Providers to enable performer import. ' - : ' Performer import requires a provider named "StashDB". Rename your provider in Settings \u2192 Metadata Providers. ', React$7.createElement('a', { href: STASHBOX_DOCS_URL, target: '_blank', rel: 'noopener noreferrer' }, 'Learn more.'))), isSearching + }, React$7.createElement('line', { x1: 18, y1: 6, x2: 6, y2: 18 }), React$7.createElement('line', { x1: 6, y1: 6, x2: 18, y2: 18 }))))), stashboxStatus !== null && stashboxStatus !== 'configured' && React$7.createElement('div', { className: 'visage-stashbox-banner' }, React$7.createElement('svg', { width: 16, height: 16, viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: 2 }, React$7.createElement('path', { d: 'M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z' }), React$7.createElement('line', { x1: 12, y1: 9, x2: 12, y2: 13 }), React$7.createElement('line', { x1: 12, y1: 17, x2: 12.01, y2: 17 })), React$7.createElement('span', { className: 'visage-stashbox-banner-text' }, React$7.createElement('strong', null, stashboxStatus === 'empty' ? t('faceMatch.stashboxMissing') : t('faceMatch.stashboxWrongName')), stashboxStatus === 'empty' + ? t('faceMatch.stashboxMissingBody') + : t('faceMatch.stashboxWrongNameBody'), React$7.createElement('a', { href: STASHBOX_DOCS_URL, target: '_blank', rel: 'noopener noreferrer' }, t('faceMatch.learnMore')))), isSearching ? React$7.createElement('div', { className: 'visage-modal-loading' }, React$7.createElement('div', { className: 'visage-face-scan' }, React$7.createElement('span', { className: 'visage-face-scan-corner' }), React$7.createElement('span', { className: 'visage-face-scan-corner' }), React$7.createElement('span', { className: 'visage-face-scan-corner' }), React$7.createElement('span', { className: 'visage-face-scan-corner' }), React$7.createElement('svg', { viewBox: '0 0 100 120', fill: 'none', @@ -10084,7 +10357,7 @@ [25, 52], [75, 52], [35, 68], [65, 68], [60.5, 82], ].map(([cx, cy], i) => React$7.createElement('circle', { key: i, className: 'visage-face-scan-dot', cx, cy, r: i === 4 ? 1.5 : 1.8, stroke: 'none', - })))), React$7.createElement('div', { className: 'visage-face-scan-beam' })), React$7.createElement('span', { className: 'visage-loading-label' }, 'Scanning \u2022 face recognition\u2026')) + })))), React$7.createElement('div', { className: 'visage-face-scan-beam' })), React$7.createElement('span', { className: 'visage-loading-label' }, t('faceMatch.scanning'))) : React$7.createElement(React$7.Fragment, null, React$7.createElement('div', { className: 'visage-face-tabs' }, matches.map((face, idx) => { if (getPerformerCount(idx) === 0) return null; @@ -10094,22 +10367,22 @@ onClick: () => setActiveFaceIndex(idx), }, React$7.createElement('img', { src: `data:image/jpg;base64,${face.image}`, - alt: `Face ${idx + 1}`, + alt: t('faceMatch.faceAlt', { index: idx + 1 }), className: 'visage-face-tab-image', })); - })), React$7.createElement('div', { className: 'visage-sprite-toolbar', style: { padding: '0.5rem 1.5rem' } }, React$7.createElement('div', { className: 'visage-sprite-threshold' }, React$7.createElement('span', { className: 'visage-threshold-text' }, 'Min conf.'), React$7.createElement('input', { + })), React$7.createElement('div', { className: 'visage-sprite-toolbar', style: { padding: '0.5rem 1.5rem' } }, React$7.createElement('div', { className: 'visage-sprite-threshold' }, React$7.createElement('span', { className: 'visage-threshold-text' }, t('faceMatch.minConf')), React$7.createElement('input', { type: 'range', min: 0, max: 100, step: 5, value: minConfidence, onChange: (e) => { const value = Number(e.target.value); setConfidenceOverride(value); }, - title: `Minimum confidence: ${minConfidence}%`, - }), React$7.createElement('span', { className: 'visage-sprite-threshold-label' }, `${minConfidence}%`))), React$7.createElement('div', { className: 'visage-modal-body' }, React$7.createElement('div', { className: 'visage-left-panel' }, activeFace && React$7.createElement(React$7.Fragment, null, React$7.createElement('div', null, React$7.createElement('span', { className: 'visage-section-label' }, 'Detected'), React$7.createElement('div', { className: 'visage-detect-frame' }, React$7.createElement('img', { + title: t('faceMatch.minConfTitle', { percent: minConfidence }), + }), React$7.createElement('span', { className: 'visage-sprite-threshold-label' }, `${minConfidence}%`))), React$7.createElement('div', { className: 'visage-modal-body' }, React$7.createElement('div', { className: 'visage-left-panel' }, activeFace && React$7.createElement(React$7.Fragment, null, React$7.createElement('div', null, React$7.createElement('span', { className: 'visage-section-label' }, t('faceMatch.detected')), React$7.createElement('div', { className: 'visage-detect-frame' }, React$7.createElement('img', { src: `data:image/jpg;base64,${activeFace.image}`, - alt: 'Detected face', + alt: t('faceMatch.detectedFaceAlt'), className: 'visage-detect-image', - }))), activeFace.performers[previewIdx] && React$7.createElement(React$7.Fragment, null, React$7.createElement('div', { className: 'visage-vs-divider' }, 'vs'), React$7.createElement('div', null, React$7.createElement('span', { className: 'visage-section-label-name' }, activeFace.performers[previewIdx].name), React$7.createElement('div', { className: 'visage-match-frame' }, React$7.createElement('img', { + }))), activeFace.performers[previewIdx] && React$7.createElement(React$7.Fragment, null, React$7.createElement('div', { className: 'visage-vs-divider' }, t('faceMatch.vs')), React$7.createElement('div', null, React$7.createElement('span', { className: 'visage-section-label-name' }, activeFace.performers[previewIdx].name), React$7.createElement('div', { className: 'visage-match-frame' }, React$7.createElement('img', { src: activeFace.performers[previewIdx].image, alt: activeFace.performers[previewIdx].name, className: 'visage-match-image', @@ -10129,21 +10402,21 @@ href: 'https://www.patreon.com/cw/cc12340', target: '_blank', rel: 'noopener noreferrer', - }, React$7.createElement('svg', { width: 13, height: 13, viewBox: '0 0 24 24', fill: 'currentColor' }, React$7.createElement('circle', { cx: 15, cy: 9, r: 7 }), React$7.createElement('rect', { x: 2, y: 2, width: 4, height: 20, rx: 1 })), 'Support on Patreon'), React$7.createElement('div', { className: 'visage-progress-info' }, React$7.createElement('span', { className: 'visage-matches-assigned' }, selectedPerformer.size > 0 - ? `${selectedPerformer.size} of ${matches.length} selected` + }, React$7.createElement('svg', { width: 13, height: 13, viewBox: '0 0 24 24', fill: 'currentColor' }, React$7.createElement('circle', { cx: 15, cy: 9, r: 7 }), React$7.createElement('rect', { x: 2, y: 2, width: 4, height: 20, rx: 1 })), t('faceMatch.supportPatreon')), React$7.createElement('div', { className: 'visage-progress-info' }, React$7.createElement('span', { className: 'visage-matches-assigned' }, selectedPerformer.size > 0 + ? t('faceMatch.ofSelected', { selected: selectedPerformer.size, total: matches.length }) : sceneStashIds.size > 0 - ? `${matches.length} faces found \u00B7 all in scene` - : `${matches.length} faces found \u00B7 click to select`), React$7.createElement('div', { className: 'visage-keyboard-hints' }, React$7.createElement('kbd', null, '\u2191\u2193'), ' Switch faces \u2022 ', React$7.createElement('kbd', null, '\u2190\u2192'), ' Select performers \u2022 ', React$7.createElement('kbd', null, 'Enter'), ' Toggle select \u2022 ', 'Shift+click to add instantly')), React$7.createElement('div', { className: 'visage-button-group' }, hasHighConfidenceMatches() && React$7.createElement('button', { + ? t('faceMatch.allInScene', { total: matches.length }) + : t('faceMatch.clickToSelect', { total: matches.length })), React$7.createElement('div', { className: 'visage-keyboard-hints' }, React$7.createElement('kbd', null, '\u2191\u2193'), ' ' + t('faceMatch.kbSwitch') + ' \u2022 ', React$7.createElement('kbd', null, '\u2190\u2192'), ' ' + t('faceMatch.kbSelect') + ' \u2022 ', React$7.createElement('kbd', null, 'Enter'), ' ' + t('faceMatch.kbToggle') + ' \u2022 ', t('faceMatch.kbAddInstant'))), React$7.createElement('div', { className: 'visage-button-group' }, hasHighConfidenceMatches() && React$7.createElement('button', { className: 'visage-btn visage-btn-secondary', onClick: assignConfidentMatches, }, React$7.createElement('svg', { width: 16, height: 16, viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: 2, - }, React$7.createElement('path', { d: 'M9 11l3 3 8-8' })), 'Select Best Matches'), React$7.createElement('button', { + }, React$7.createElement('path', { d: 'M9 11l3 3 8-8' })), t('faceMatch.selectBest')), React$7.createElement('button', { className: 'visage-btn visage-btn-primary', onClick: addConfirmed, disabled: selectedPerformer.size === 0 || adding, - }, adding ? 'Adding...' : `Done (${selectedPerformer.size})`))))))); + }, adding ? t('faceMatch.adding') : t('faceMatch.done', { count: selectedPerformer.size })))))))); } function formatTime(seconds) { @@ -10311,7 +10584,16 @@ NON_BINARY: '\u26A6', INTERSEX: '\u26A5', }; + const GENDER_NAME_KEY = { + MALE: 'gender.male', + FEMALE: 'gender.female', + TRANSGENDER_MALE: 'gender.transMale', + TRANSGENDER_FEMALE: 'gender.transFemale', + NON_BINARY: 'gender.nonBinary', + INTERSEX: 'gender.intersex', + }; function SpriteResultModal() { + useLocale(); const { state, showSpriteModal, clearSpriteResult, setLoading, showError, showSuccess, showWarning, openSettings } = useVisage(); const { spriteResult, loading: loadingState, scanProgress, scanProgressDesc } = state; const [visible, setVisible] = useState(false); @@ -10417,12 +10699,12 @@ } } await addPerformerToContent(scenario, scenarioId, pid); - showSuccess(`Added performer to the ${scenario === 'scenes' ? 'scene' : 'image'}.`); + showSuccess(t('faceMatch.toast.added', { target: scenario === 'scenes' ? 'scene' : 'image' })); close(); } catch (e) { console.error('[Visage] sprite quick add error:', e); - showError('Failed to add performer: ' + getErrorMessage(e)); + showError(t('faceMatch.toast.addError', { error: getErrorMessage(e) })); } finally { setAdding(false); @@ -10479,19 +10761,19 @@ } if (addedCount === 0) { const msg = stashboxStatus === 'empty' - ? 'No stash-box configured. Add a stash-box provider in Settings \u2192 Metadata Providers to enable performer import. See ' + STASHBOX_DOCS_URL + ? t('faceMatch.toast.noStashbox', { url: STASHBOX_DOCS_URL }) : stashboxStatus === 'mismatch' - ? 'No provider named "StashDB" found. Rename your provider to "StashDB" in Settings \u2192 Metadata Providers to enable performer import.' - : 'Configure a stash-box provider in Settings \u2192 Metadata Providers to enable performer import.'; + ? t('faceMatch.toast.noProvider') + : t('faceMatch.toast.configureProvider'); showWarning(msg); return; } - showSuccess(`Added ${addedCount} performer${addedCount !== 1 ? 's' : ''} to the ${scenario === 'scenes' ? 'scene' : 'image'}.`); + showSuccess(t('faceMatch.toast.addedMultiple', { count: addedCount, s: addedCount !== 1 ? 's' : '', target: scenario === 'scenes' ? 'scene' : 'image' })); close(); } catch (e) { console.error('[Visage] add performers error:', e); - showError('Failed to add performers: ' + getErrorMessage(e)); + showError(t('faceMatch.toast.addError', { error: getErrorMessage(e) })); } finally { setAdding(false); @@ -10539,18 +10821,21 @@ className: `visage-modal visage-sprite-modal ${visible ? 'visage-visible' : ''}`, role: 'dialog', 'aria-modal': 'true', - }, React$4.createElement('div', { className: 'visage-modal-header' }, React$4.createElement('div', { className: 'visage-modal-title' }, React$4.createElement('h2', null, 'SCENE PERFORMERS'), !isProcessing && React$4.createElement('span', { className: 'visage-match-count' }, `${performers.length} found \u00B7 ${confirmedIds.size} confirmed`)), React$4.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: '8px' } }, React$4.createElement(BackendBadge, null), React$4.createElement(SettingsButton, { onClick: openSettings }), React$4.createElement('button', { + }, React$4.createElement('div', { className: 'visage-modal-header' }, React$4.createElement('div', { className: 'visage-modal-title' }, React$4.createElement('h2', null, t('sprite.title')), !isProcessing && React$4.createElement('span', { className: 'visage-match-count' }, t('sprite.foundConfirmed', { found: performers.length, confirmed: confirmedIds.size }))), React$4.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: '8px' } }, React$4.createElement(BackendBadge, null), React$4.createElement(SettingsButton, { onClick: openSettings }), React$4.createElement('button', { className: 'visage-modal-close', onClick: close, - 'aria-label': 'Close', + 'aria-label': t('sprite.close'), }, React$4.createElement('svg', { width: 20, height: 20, viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: 2, - }, React$4.createElement('line', { x1: 18, y1: 6, x2: 6, y2: 18 }), React$4.createElement('line', { x1: 6, y1: 6, x2: 18, y2: 18 })))), !isProcessing && React$4.createElement('div', { className: 'visage-sprite-toolbar' }, React$4.createElement('div', { className: 'visage-sprite-sort' }, ['confidence', 'name', 'hits'].map(s => React$4.createElement('button', { - key: s, - className: `visage-sprite-sort-btn${sortBy === s ? ' visage-active' : ''}`, - onClick: () => { setSortBy(s); setFocusedIndex(0); }, - }, s.charAt(0).toUpperCase() + s.slice(1)))), React$4.createElement('div', { className: 'visage-sprite-threshold' }, React$4.createElement('span', { className: 'visage-threshold-text' }, 'Min conf.'), React$4.createElement('input', { + }, React$4.createElement('line', { x1: 18, y1: 6, x2: 6, y2: 18 }), React$4.createElement('line', { x1: 6, y1: 6, x2: 18, y2: 18 })))), !isProcessing && React$4.createElement('div', { className: 'visage-sprite-toolbar' }, React$4.createElement('div', { className: 'visage-sprite-sort' }, ['confidence', 'name', 'hits'].map(s => { + const label = s === 'confidence' ? t('sprite.confidence') : s === 'name' ? t('sprite.name') : t('sprite.hits'); + return React$4.createElement('button', { + key: s, + className: `visage-sprite-sort-btn${sortBy === s ? ' visage-active' : ''}`, + onClick: () => { setSortBy(s); setFocusedIndex(0); }, + }, label); + })), React$4.createElement('div', { className: 'visage-sprite-threshold' }, React$4.createElement('span', { className: 'visage-threshold-text' }, t('sprite.minConf')), React$4.createElement('input', { type: 'range', min: 0, max: 100, step: 5, value: minConfidence, onChange: (e) => { @@ -10558,14 +10843,14 @@ setMinConfidence(value); localStorage.setItem('visage:minConfidence:sprite', String(value)); }, - title: `Minimum confidence: ${minConfidence}%`, - }), React$4.createElement('span', { className: 'visage-sprite-threshold-label' }, `${minConfidence}%`)))), stashboxStatus !== null && stashboxStatus !== 'configured' && React$4.createElement('div', { className: 'visage-stashbox-banner' }, React$4.createElement('svg', { width: 16, height: 16, viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: 2 }, React$4.createElement('path', { d: 'M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z' }), React$4.createElement('line', { x1: 12, y1: 9, x2: 12, y2: 13 }), React$4.createElement('line', { x1: 12, y1: 17, x2: 12.01, y2: 17 })), React$4.createElement('span', { className: 'visage-stashbox-banner-text' }, React$4.createElement('strong', null, stashboxStatus === 'empty' ? 'No stash-box configured.' : 'No "StashDB" provider found.'), stashboxStatus === 'empty' - ? ' Add a stash-box provider in Settings \u2192 Metadata Providers to enable performer import. ' - : ' Performer import requires a provider named "StashDB". Rename your provider in Settings \u2192 Metadata Providers. ', React$4.createElement('a', { href: STASHBOX_DOCS_URL, target: '_blank', rel: 'noopener noreferrer' }, 'Learn more.'))), isProcessing + title: t('sprite.minConfTitle', { percent: minConfidence }), + }), React$4.createElement('span', { className: 'visage-sprite-threshold-label' }, `${minConfidence}%`)))), stashboxStatus !== null && stashboxStatus !== 'configured' && React$4.createElement('div', { className: 'visage-stashbox-banner' }, React$4.createElement('svg', { width: 16, height: 16, viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: 2 }, React$4.createElement('path', { d: 'M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z' }), React$4.createElement('line', { x1: 12, y1: 9, x2: 12, y2: 13 }), React$4.createElement('line', { x1: 12, y1: 17, x2: 12.01, y2: 17 })), React$4.createElement('span', { className: 'visage-stashbox-banner-text' }, React$4.createElement('strong', null, stashboxStatus === 'empty' ? t('faceMatch.stashboxMissing') : t('faceMatch.stashboxWrongName')), stashboxStatus === 'empty' + ? t('faceMatch.stashboxMissingBody') + : t('faceMatch.stashboxWrongNameBody'), React$4.createElement('a', { href: STASHBOX_DOCS_URL, target: '_blank', rel: 'noopener noreferrer' }, t('faceMatch.learnMore')))), isProcessing ? React$4.createElement('div', { className: 'visage-modal-loading' }, React$4.createElement('div', { className: 'visage-conveyor' }, React$4.createElement('div', { className: 'visage-conveyor-track' }, Array.from({ length: 14 }, (_, i) => React$4.createElement('div', { key: i, className: 'visage-conveyor-frame' }))), React$4.createElement('div', { className: 'visage-conveyor-beam' }), React$4.createElement('div', { className: 'visage-conveyor-rail' })), (scanProgress > 0 || scanProgressDesc) && React$4.createElement('div', { className: 'visage-scan-progress' }, React$4.createElement('div', { className: 'visage-scan-progress-bar' }, React$4.createElement('div', { className: 'visage-scan-progress-fill', style: { width: `${Math.round(scanProgress * 100)}%` }, - }))), React$4.createElement('span', { className: 'visage-loading-label' }, scanProgressDesc || 'Visage Scanning\u2026'), React$4.createElement('button', { + }))), React$4.createElement('span', { className: 'visage-loading-label' }, scanProgressDesc || t('sprite.scanning')), React$4.createElement('button', { className: 'visage-btn visage-btn-secondary', onClick: () => { cancelDetection(); @@ -10573,9 +10858,9 @@ close(); }, style: { marginTop: 12 }, - }, 'Cancel')) + }, t('sprite.cancel'))) : React$4.createElement(React$4.Fragment, null, React$4.createElement('div', { className: 'visage-sprite-body' }, performers.length === 0 - ? React$4.createElement('div', { className: 'visage-sprite-empty' }, React$4.createElement('p', null, 'No performers identified in this sprite.')) + ? React$4.createElement('div', { className: 'visage-sprite-empty' }, React$4.createElement('p', null, t('sprite.empty'))) : React$4.createElement('div', { className: 'visage-sprite-grid' }, visiblePerformers.map((p, idx) => { const alreadyInScene = sceneStashIds.has(p.id); const isConfirmed = confirmedIds.has(p.id) || alreadyInScene; @@ -10611,30 +10896,32 @@ 'aria-pressed': isConfirmed, }, React$4.createElement('div', { className: 'visage-sprite-compare' }, React$4.createElement('div', { className: 'visage-sprite-face' }, React$4.createElement('img', { src: `data:image/jpeg;base64,${p.thumbnail}`, - alt: 'Detected face', + alt: t('sprite.detectedFaceAlt'), className: 'visage-sprite-face-img', - }), React$4.createElement('span', { className: 'visage-sprite-label' }, 'SPRITE')), React$4.createElement('div', { className: 'visage-sprite-vs' }, React$4.createElement('span', null, 'vs')), React$4.createElement('div', { className: 'visage-sprite-performer-img' }, React$4.createElement('div', { className: 'visage-sprite-img-placeholder' }, React$4.createElement('svg', { width: 24, height: 24, viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: 2 }, React$4.createElement('path', { d: 'M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2' }), React$4.createElement('circle', { cx: 12, cy: 7, r: 4 }))), isLoadingImg && React$4.createElement('div', { className: 'visage-sprite-img-loading' }), stashImg && React$4.createElement('img', { + }), React$4.createElement('span', { className: 'visage-sprite-label' }, t('sprite.spriteLabel'))), React$4.createElement('div', { className: 'visage-sprite-vs' }, React$4.createElement('span', null, t('sprite.vs'))), React$4.createElement('div', { className: 'visage-sprite-performer-img' }, React$4.createElement('div', { className: 'visage-sprite-img-placeholder' }, React$4.createElement('svg', { width: 24, height: 24, viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: 2 }, React$4.createElement('path', { d: 'M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2' }), React$4.createElement('circle', { cx: 12, cy: 7, r: 4 }))), isLoadingImg && React$4.createElement('div', { className: 'visage-sprite-img-loading' }), stashImg && React$4.createElement('img', { src: stashImg, alt: p.name, className: 'visage-sprite-performer-img-src', onError: (e) => { e.target.remove(); }, - }), React$4.createElement('span', { className: 'visage-sprite-label' }, 'STASH'), (cached === null || cached === void 0 ? void 0 : cached.gender) && React$4.createElement('span', { + }), React$4.createElement('span', { className: 'visage-sprite-label' }, t('sprite.stashLabel')), (cached === null || cached === void 0 ? void 0 : cached.gender) && React$4.createElement('span', { className: 'visage-sprite-gender-badge visage-gender-badge visage-gender-' + cached.gender.toLowerCase().replace(/_/g, '-'), - title: cached.gender.replace(/_/g, ' ').toLowerCase().replace(/\b\w/g, c => c.toUpperCase()), + title: GENDER_NAME_KEY[cached.gender] + ? t(GENDER_NAME_KEY[cached.gender]) + : cached.gender.replace(/_/g, ' ').toLowerCase().replace(/\b\w/g, c => c.toUpperCase()), }, GENDER_SYMBOLS[cached.gender] || '?'), (cached === null || cached === void 0 ? void 0 : cached.country) && React$4.createElement('span', { className: 'visage-sprite-country-flag fi fi-' + cached.country.toLowerCase(), - })), alreadyInScene && React$4.createElement('div', { className: 'visage-sprite-in-scene' }, 'In scene'), isConfirmed && !alreadyInScene && React$4.createElement('div', { className: 'visage-sprite-check-badge' }, React$4.createElement('svg', { width: 18, height: 18, viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: 3 }, React$4.createElement('polyline', { points: '20 6 9 17 4 12' })))), React$4.createElement('div', { className: 'visage-sprite-info' }, React$4.createElement('div', { className: 'visage-sprite-summary' }, ConfidenceRing({ confidence: p.confidence, confClass }), React$4.createElement('div', { className: 'visage-sprite-summary-text' }, React$4.createElement('a', { + })), alreadyInScene && React$4.createElement('div', { className: 'visage-sprite-in-scene' }, t('sprite.inScene')), isConfirmed && !alreadyInScene && React$4.createElement('div', { className: 'visage-sprite-check-badge' }, React$4.createElement('svg', { width: 18, height: 18, viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: 3 }, React$4.createElement('polyline', { points: '20 6 9 17 4 12' })))), React$4.createElement('div', { className: 'visage-sprite-info' }, React$4.createElement('div', { className: 'visage-sprite-summary' }, ConfidenceRing({ confidence: p.confidence, confClass }), React$4.createElement('div', { className: 'visage-sprite-summary-text' }, React$4.createElement('a', { href: p.performer_url, target: '_blank', rel: 'noopener noreferrer', className: 'visage-sprite-performer-link', onClick: (e) => e.stopPropagation(), - }, p.name), React$4.createElement('div', { className: 'visage-sprite-stats-row' }, React$4.createElement('span', { className: 'visage-sprite-stat' }, `${p.hit_count} hit${p.hit_count !== 1 ? 's' : ''}`), React$4.createElement('span', { className: 'visage-sprite-stat-sep' }, '\u00B7'), React$4.createElement('span', { className: 'visage-sprite-stat' }, `${formatTime(sortedRanges.reduce((sum, r) => { - var _a, _b, _c, _d; - const end = Number((_b = (_a = r === null || r === void 0 ? void 0 : r[1]) !== null && _a !== void 0 ? _a : r === null || r === void 0 ? void 0 : r.end) !== null && _b !== void 0 ? _b : 0); - const start = Number((_d = (_c = r === null || r === void 0 ? void 0 : r[0]) !== null && _c !== void 0 ? _c : r === null || r === void 0 ? void 0 : r.start) !== null && _d !== void 0 ? _d : 0); - return sum + (end - start); - }, 0))} total`)))), React$4.createElement('div', { className: 'visage-sprite-timeline-wrap' }, React$4.createElement('div', { className: 'visage-sprite-timeline' }, segments.map((seg, i) => { + }, p.name), React$4.createElement('div', { className: 'visage-sprite-stats-row' }, React$4.createElement('span', { className: 'visage-sprite-stat' }, t('sprite.hitsCount', { count: p.hit_count, s: p.hit_count !== 1 ? 's' : '' })), React$4.createElement('span', { className: 'visage-sprite-stat-sep' }, '\u00B7'), React$4.createElement('span', { className: 'visage-sprite-stat' }, t('sprite.totalTime', { time: formatTime(sortedRanges.reduce((sum, r) => { + var _a, _b, _c, _d; + const end = Number((_b = (_a = r === null || r === void 0 ? void 0 : r[1]) !== null && _a !== void 0 ? _a : r === null || r === void 0 ? void 0 : r.end) !== null && _b !== void 0 ? _b : 0); + const start = Number((_d = (_c = r === null || r === void 0 ? void 0 : r[0]) !== null && _c !== void 0 ? _c : r === null || r === void 0 ? void 0 : r.start) !== null && _d !== void 0 ? _d : 0); + return sum + (end - start); + }, 0)) }))))), React$4.createElement('div', { className: 'visage-sprite-timeline-wrap' }, React$4.createElement('div', { className: 'visage-sprite-timeline' }, segments.map((seg, i) => { var _a, _b, _c, _d; const range = sortedRanges[i]; const start = Number((_b = (_a = range === null || range === void 0 ? void 0 : range[0]) !== null && _a !== void 0 ? _a : range === null || range === void 0 ? void 0 : range.start) !== null && _b !== void 0 ? _b : 0); @@ -10646,21 +10933,21 @@ onClick: (e) => { e.stopPropagation(); jumpToTime(start); }, title: `${formatTime(start)}\u2013${formatTime(end)}`, }); - })), React$4.createElement('div', { className: 'visage-sprite-timeline-labels' }, React$4.createElement('span', null, '00:00'), React$4.createElement('span', null, formatTime(maxEndTime)))), alreadyInScene && React$4.createElement('div', { className: 'visage-sprite-hint visage-sprite-hint-done' }, React$4.createElement('span', null, 'Already in scene')), !isConfirmed && !alreadyInScene && React$4.createElement('div', { className: 'visage-sprite-hint' }, React$4.createElement('span', null, 'Click to confirm')), isConfirmed && !alreadyInScene && React$4.createElement('div', { className: 'visage-sprite-hint visage-sprite-hint-done' }, React$4.createElement('span', null, 'Confirmed')))); + })), React$4.createElement('div', { className: 'visage-sprite-timeline-labels' }, React$4.createElement('span', null, '00:00'), React$4.createElement('span', null, formatTime(maxEndTime)))), alreadyInScene && React$4.createElement('div', { className: 'visage-sprite-hint visage-sprite-hint-done' }, React$4.createElement('span', null, t('sprite.alreadyInScene'))), !isConfirmed && !alreadyInScene && React$4.createElement('div', { className: 'visage-sprite-hint' }, React$4.createElement('span', null, t('sprite.clickToConfirm'))), isConfirmed && !alreadyInScene && React$4.createElement('div', { className: 'visage-sprite-hint visage-sprite-hint-done' }, React$4.createElement('span', null, t('sprite.confirmed'))))); }))), React$4.createElement('div', { className: 'visage-modal-footer' }, React$4.createElement('div', { className: 'visage-footer-main' }, React$4.createElement('a', { className: 'visage-ext-patreon-btn', href: 'https://www.patreon.com/cw/cc12340', target: '_blank', rel: 'noopener noreferrer', - }, React$4.createElement('svg', { width: 13, height: 13, viewBox: '0 0 24 24', fill: 'currentColor' }, React$4.createElement('circle', { cx: 15, cy: 9, r: 7 }), React$4.createElement('rect', { x: 2, y: 2, width: 4, height: 20, rx: 1 })), 'Support on Patreon'), React$4.createElement('div', { className: 'visage-progress-info' }, confirmedIds.size > 0 - ? React$4.createElement('span', { className: 'visage-matches-assigned' }, `${confirmedIds.size} of ${performers.length} confirmed`) + }, React$4.createElement('svg', { width: 13, height: 13, viewBox: '0 0 24 24', fill: 'currentColor' }, React$4.createElement('circle', { cx: 15, cy: 9, r: 7 }), React$4.createElement('rect', { x: 2, y: 2, width: 4, height: 20, rx: 1 })), t('sprite.supportPatreon')), React$4.createElement('div', { className: 'visage-progress-info' }, confirmedIds.size > 0 + ? React$4.createElement('span', { className: 'visage-matches-assigned' }, t('sprite.confirmedCount', { confirmed: confirmedIds.size, total: performers.length })) : React$4.createElement('span', { className: 'visage-matches-assigned' }, visiblePerformers.length < performers.length - ? `${visiblePerformers.length} shown (${performers.length} total) \u00B7 click to confirm \u00B7 \u2190\u2192 navigate \u00B7 Enter confirm` - : `Click to confirm \u00B7 \u2190\u2192 navigate \u00B7 Enter confirm`)), React$4.createElement('div', { className: 'visage-button-group' }, React$4.createElement('button', { + ? t('sprite.shownHint', { shown: visiblePerformers.length, total: performers.length }) + : t('sprite.confirmHint'))), React$4.createElement('div', { className: 'visage-button-group' }, React$4.createElement('button', { className: 'visage-btn visage-btn-primary', onClick: addConfirmed, disabled: confirmedIds.size === 0 || adding, - }, adding ? 'Adding...' : `Done (${confirmedIds.size})`))))))); + }, adding ? t('sprite.adding') : t('sprite.done', { count: confirmedIds.size })))))))); } const React$3 = window.PluginApi.React; @@ -10671,6 +10958,7 @@ success: 'M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z', }; function ErrorDialog() { + useLocale(); const { state, hideErrorDialog } = useVisage(); const dialogRef = useRef(null); const timerRef = useRef(null); @@ -10716,7 +11004,7 @@ }, React$3.createElement('div', { className: 'visage-error-icon' }, React$3.createElement('svg', { width: 22, height: 22, viewBox: '0 0 24 24', fill: 'currentColor' }, React$3.createElement('path', { d: ICONS[variant] }))), React$3.createElement('div', { className: 'visage-error-message' }, message), React$3.createElement('button', { className: 'visage-error-close', onClick: hideErrorDialog, - 'aria-label': 'Dismiss', + 'aria-label': t('error.dismiss'), }, React$3.createElement('svg', { width: 16, height: 16, viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: 2 }, React$3.createElement('line', { x1: 18, y1: 6, x2: 6, y2: 18 }), React$3.createElement('line', { x1: 6, y1: 6, x2: 18, y2: 18 }))), React$3.createElement('div', { className: 'visage-error-timer' }))); } @@ -10730,6 +11018,7 @@ * "Change backend" link opens the backend settings panel via `onOpen`. */ function BackendHealthBanner({ onOpen }) { + useLocale(); const { state, hideHealthBanner } = useVisage(); if (!state.healthBanner) return null; @@ -10751,18 +11040,19 @@ }, React$2.createElement('span', null, state.healthBanner), onOpen && React$2.createElement('a', { className: 'visage-health-banner-action', onClick: openSettings, - }, 'Change backend'), React$2.createElement('button', { + }, t('banner.changeBackend')), React$2.createElement('button', { className: 'visage-health-banner-close', onClick: (e) => { e.stopPropagation(); hideHealthBanner(); }, - 'aria-label': 'Dismiss', + 'aria-label': t('banner.dismiss'), }, '×')); } const { React: React$1 } = PluginApi; function FirstRunDialog({ onComplete }) { + useLocale(); const [showSettings, setShowSettings] = React$1.useState(false); if (showSettings) { return React$1.createElement(BackendSettings, { onClose: onComplete, initialMode: 'local' }); @@ -10771,17 +11061,17 @@ className: 'visage-firstrun-dialog', role: 'dialog', 'aria-modal': 'true', - 'aria-label': 'Set up your Visage backend', - }, React$1.createElement('h2', { className: 'visage-firstrun-heading' }, 'Set up your Visage backend'), React$1.createElement('p', { className: 'visage-firstrun-sub' }, 'Visage sends face images to a backend for recognition. Choose where to run it.'), React$1.createElement('button', { + 'aria-label': t('firstRun.title'), + }, React$1.createElement('h2', { className: 'visage-firstrun-heading' }, t('firstRun.title')), React$1.createElement('p', { className: 'visage-firstrun-sub' }, t('firstRun.subtitle')), React$1.createElement('button', { className: 'visage-btn visage-btn-primary visage-firstrun-cloud', onClick: onComplete, - }, 'Use Hugging Face cloud'), React$1.createElement('div', { className: 'visage-firstrun-note' }, 'Zero setup. Images are sent to the Hugging Face cloud service.'), React$1.createElement('button', { + }, t('firstRun.cloud')), React$1.createElement('div', { className: 'visage-firstrun-note' }, t('firstRun.cloudNote')), React$1.createElement('button', { className: 'visage-btn visage-btn-secondary visage-firstrun-server', onClick: () => setShowSettings(true), - }, 'Use my own server'), React$1.createElement('div', { className: 'visage-firstrun-note' }, 'Run the private binary on your own machine or network.'), React$1.createElement('button', { + }, t('firstRun.local')), React$1.createElement('div', { className: 'visage-firstrun-note' }, t('firstRun.localNote')), React$1.createElement('button', { className: 'visage-firstrun-skip', onClick: onComplete, - }, 'Skip for now'))); + }, t('firstRun.skip')))); } function detectTheme() { @@ -10800,6 +11090,4754 @@ : '89, 145, 246'; } + // Dutch. Missing keys fall back to English automatically. + const nl = { + // ---- BackendSettings.tsx ---- + 'backendSettings.title': `Backend-instellingen`, + 'backendSettings.closeAria': `Instellingen sluiten`, + 'backendSettings.backendAria': `Backend-instellingen`, + 'backendSettings.changeBackend': `Backend wijzigen`, + 'backendSettings.backendLabel': `Backend`, + 'backendSettings.local': `Lokaal`, + 'backendSettings.cloud': `Cloud (Hugging Face)`, + 'backendSettings.cloudNote': `Afbeeldingen worden naar de Hugging Face cloudservice gestuurd.`, + 'backendSettings.hintPrefix': `Wil je dat je afbeeldingen in jouw netwerk blijven?`, + 'backendSettings.hintLink': `Draai een privéserver via Patreon`, + 'backendSettings.urlLabel': `URL`, + 'backendSettings.csp1': `Het beveiligingsbeleid (CSP) van de browser staat alleen toe`, + 'backendSettings.csp2': `standaard. Voeg om een lokale backend op een ander adres (bijv. je LAN-IP) te bereiken dit toe aan de`, + 'backendSettings.csp3': `lijst in het`, + 'backendSettings.csp4': `bestand in je Stash-pluginsmap, anders worden verzoeken geblokkeerd. Let op: een update van Visage installeert`, + 'backendSettings.csp5': `opnieuw, dus dit moet na elke update opnieuw worden toegepast.`, + 'backendSettings.testing': `Verbinding testen…`, + 'backendSettings.testConnection': `Verbinding testen`, + 'backendSettings.testingShort': `Testen…`, + 'backendSettings.cancel': `Annuleren`, + 'backendSettings.save': `Opslaan`, + 'backendSettings.feedback.reachable': `Verbinding geslaagd. De backend is klaar.`, + 'backendSettings.feedback.degraded': `Backend bereikbaar maar gedegradeerd (modellen of index niet geladen).`, + 'backendSettings.feedback.unreachable': `Backend niet bereikbaar. Controleer de URL en of de backend actief is.`, + // ---- FaceMatchModal.tsx ---- + 'faceMatch.title': `HUIDIG FRAME`, + 'faceMatch.close': `Sluiten`, + 'faceMatch.facesSelected': `{faces} gezichten gevonden · {selected} geselecteerd`, + 'faceMatch.inScene': `· {count} in de scène`, + 'faceMatch.stashboxMissing': `Geen stash-box geconfigureerd.`, + 'faceMatch.stashboxMissingBody': ` Voeg een stash-boxprovider toe in Instellingen → Metadata-providers om het importeren van performers in te schakelen.`, + 'faceMatch.stashboxWrongName': `Geen provider genaamd "StashDB" gevonden.`, + 'faceMatch.stashboxWrongNameBody': ` Het importeren van performers vereist een provider genaamd "StashDB". Hernoem je provider in Instellingen → Metadata-providers.`, + 'faceMatch.learnMore': `Meer informatie.`, + 'faceMatch.scanning': `Scannen • gezichtsherkenning…`, + 'faceMatch.faceAlt': `Gezicht {index}`, + 'faceMatch.minConf': `Min. betr.`, + 'faceMatch.minConfTitle': `Minimale zekerheid: {percent}%`, + 'faceMatch.detected': `Gedetecteerd`, + 'faceMatch.detectedFaceAlt': `Gedetecteerd gezicht`, + 'faceMatch.vs': `vs`, + 'faceMatch.supportPatreon': `Steun op Patreon`, + 'faceMatch.ofSelected': `{selected} van {total} geselecteerd`, + 'faceMatch.allInScene': `{total} gezichten gevonden · allemaal in de scène`, + 'faceMatch.clickToSelect': `{total} gezichten gevonden · klik om te selecteren`, + 'faceMatch.kbSwitch': `Wissel gezichten`, + 'faceMatch.kbSelect': `Selecteer performers`, + 'faceMatch.kbToggle': `Selectie wisselen`, + 'faceMatch.kbAddInstant': `Shift+klik om direct toe te voegen`, + 'faceMatch.selectBest': `Beste overeenkomsten selecteren`, + 'faceMatch.adding': `Toevoegen...`, + 'faceMatch.done': `Klaar ({count})`, + 'faceMatch.toast.added': `Performer toegevoegd aan de {target}.`, + 'faceMatch.toast.addError': `Kon performer niet toevoegen: {error}`, + 'faceMatch.toast.noStashbox': `Geen stash-box geconfigureerd. Voeg een stash-boxprovider toe in Instellingen → Metadata-providers om het importeren van performers in te schakelen. Zie {url}`, + 'faceMatch.toast.noProvider': `Geen provider genaamd "StashDB" gevonden. Hernoem je provider naar "StashDB" in Instellingen → Metadata-providers om het importeren van performers in te schakelen.`, + 'faceMatch.toast.configureProvider': `Configureer een stash-boxprovider in Instellingen → Metadata-providers om het importeren van performers in te schakelen.`, + 'faceMatch.toast.addedMultiple': `{count} performer{s} toegevoegd aan de {target}.`, + // ---- SpriteResultModal.tsx ---- + 'sprite.title': `SCÈNE-PERFORMERS`, + 'sprite.close': `Sluiten`, + 'sprite.foundConfirmed': `{found} gevonden · {confirmed} bevestigd`, + 'sprite.confidence': `zekerheid`, + 'sprite.name': `naam`, + 'sprite.hits': `treffers`, + 'sprite.minConf': `Min. betr.`, + 'sprite.minConfTitle': `Minimale zekerheid: {percent}%`, + 'sprite.scanning': `Visage scannen…`, + 'sprite.cancel': `Annuleren`, + 'sprite.empty': `Geen performers geïdentificeerd in deze sprite.`, + 'sprite.detectedFaceAlt': `Gedetecteerd gezicht`, + 'sprite.spriteLabel': `SPRITE`, + 'sprite.stashLabel': `STASH`, + 'sprite.vs': `vs`, + 'sprite.inScene': `In de scène`, + 'sprite.hitsCount': `{count} treffer{s}`, + 'sprite.totalTime': `{time} totaal`, + 'sprite.alreadyInScene': `Al in de scène`, + 'sprite.clickToConfirm': `Klik om te bevestigen`, + 'sprite.confirmed': `Bevestigd`, + 'sprite.supportPatreon': `Steun op Patreon`, + 'sprite.confirmedCount': `{confirmed} van {total} bevestigd`, + 'sprite.shownHint': `{shown} getoond ({total} totaal) · klik om te bevestigen · ←→ navigeren · Enter bevestigen`, + 'sprite.confirmHint': `Klik om te bevestigen · ←→ navigeren · Enter bevestigen`, + 'sprite.adding': `Toevoegen...`, + 'sprite.done': `Klaar ({count})`, + // ---- PerformerCard.tsx ---- + 'gender.male': `Mannelijk`, + 'gender.female': `Vrouwelijk`, + 'gender.transMale': `Transgender man`, + 'gender.transFemale': `Transgender vrouw`, + 'gender.nonBinary': `Non-binair`, + 'gender.intersex': `Intersekse`, + 'card.excellent': `Uitstekende overeenkomst`, + 'card.good': `Goede overeenkomst`, + 'card.uncertain': `Onzekere overeenkomst`, + 'card.select': `{name} selecteren`, + 'card.deselect': `{name} deselecteren`, + 'card.openOn': `Openen op {source}`, + // ---- FaceSearchButton.tsx ---- + 'search.overlayHint': `Sleep om een gezicht te selecteren — Enter om het hele frame te scannen — Esc om te annuleren`, + 'search.noFaces': `Geen gezichten gevonden in die selectie. Probeer een kleinere uitsnede of druk op Enter om het hele frame te scannen.`, + 'search.captureMediaFail': `Kan media niet vastleggen. Zorg ervoor dat de scène/afbeelding volledig is geladen.`, + 'search.healthBanner': `De gezichtsherkennings-API is niet bereikbaar. Start de backend en probeer het opnieuw.`, + 'search.failed': `Gezichtszoekopdracht mislukt: {error}`, + 'search.fetchImageFail': `Kan afbeelding niet van Stash ophalen.`, + 'search.captureFail': `Kan afbeelding niet vastleggen: {error}`, + 'search.selectFaceImage': `Selecteer een gezicht binnen de afbeelding.`, + 'search.captureFrameFail': `Kan het huidige frame niet vastleggen.`, + 'search.captureFrameFail2': `Kan het huidige frame niet vastleggen.`, + 'search.selectFaceVideo': `Selecteer een gezicht binnen het videoplayergebied.`, + 'search.menuItemTitle': `Sleep een kader rond een gezicht, of druk op Enter om het hele frame te scannen, om in StashDB naar overeenkomsten te zoeken`, + 'search.currentFrame': `Visage: Huidig frame`, + // ---- SceneScanButton.tsx ---- + 'scene.noSprite': `Geen sprite sheet of voorbeeldvideo gevonden voor deze scène. Genereer ze in de scène-instellingen en probeer het opnieuw.`, + 'scene.noFaces': `Geen gezichten of performers gevonden in het sprite sheet of de voorbeeldvideo van deze scène.`, + 'scene.healthBanner': `De gezichtsherkennings-API is niet bereikbaar. Start de backend en probeer het opnieuw.`, + 'scene.failed': `Scènescan mislukt: {error}`, + 'scene.menuItemTitle': `Identificeer elke performer in de scène (vereist een gegenereerd sprite sheet of voorbeeldvideo)`, + 'scene.wholeScene': `Visage: Hele scène`, + // ---- BackendHealthBanner.tsx ---- + 'banner.changeBackend': `Backend wijzigen`, + 'banner.dismiss': `Negeren`, + // ---- ErrorDialog.tsx ---- + 'error.dismiss': `Negeren`, + // ---- FirstRunDialog.tsx ---- + 'firstRun.title': `Stel je Visage-backend in`, + 'firstRun.subtitle': `Visage stuurt gezichtsafbeeldingen naar een backend voor herkenning. Kies waar je het wilt draaien.`, + 'firstRun.cloud': `Hugging Face cloud gebruiken`, + 'firstRun.cloudNote': `Geen installatie nodig. Afbeeldingen worden naar de Hugging Face cloudservice gestuurd.`, + 'firstRun.local': `Mijn eigen server gebruiken`, + 'firstRun.localNote': `Draai het privébinary op je eigen machine of netwerk.`, + 'firstRun.skip': `Voor nu overslaan`, + // ---- BackendBadge.tsx ---- + 'badge.local': `Lokaal`, + 'badge.cloud': `Cloud (Hugging Face)`, + 'badge.title': `Visage-backend: {label}`, + // ---- DonateFooter.tsx ---- + 'donate.enjoying': `Geniet je van Visage? Help het in leven te houden`, + 'donate.supportPatreon': `Steun op Patreon`, + // ---- FaceFrameSelector.tsx ---- + 'frame.close': `Frameselector sluiten`, + 'frame.seekFail': `Kan niet zoeken in de videospeler.`, + 'frame.selectAt': `Gezichtsframe selecteren op {time}s`, + }; + + const he = { + 'backendSettings.title': `הגדרות צד-שרת`, + 'backendSettings.closeAria': `סגירת ההגדרות`, + 'backendSettings.backendAria': `הגדרות צד-שרת`, + 'backendSettings.changeBackend': `החלפת צד-שרת`, + 'backendSettings.backendLabel': `צד-שרת`, + 'backendSettings.local': `מקומי`, + 'backendSettings.cloud': `ענן (Hugging Face)`, + 'backendSettings.cloudNote': `התמונות נשלחות לשירות הענן של Hugging Face.`, + 'backendSettings.hintPrefix': `רוצה שהתמונות יישארו בתוך הרשת שלך?`, + 'backendSettings.hintLink': `הרץ שרת פרטי דרך Patreon`, + 'backendSettings.urlLabel': `URL`, + 'backendSettings.csp1': `מדיניות האבטחה של הדפדפן (CSP) מאפשרת רק`, + 'backendSettings.csp2': `כברירת מחדל. כדי לגשת לצד-שרת מקומי בכתובת אחרת (למשל IP הרשת המקומית שלך), הוסף אותו אל`, + 'backendSettings.csp3': `ב`, + 'backendSettings.csp4': `בתוך תיקיית התוספים של Stash, אחרת הבקשות ייחסמו. הערה: עדכון של Visage מתקין מחדש את`, + 'backendSettings.csp5': `, ולכן יש ליישם זאת שוב אחרי כל עדכון.`, + 'backendSettings.testing': `בודק חיבור…`, + 'backendSettings.testConnection': `בדיקת חיבור`, + 'backendSettings.testingShort': `בודק…`, + 'backendSettings.cancel': `ביטול`, + 'backendSettings.save': `שמירה`, + 'backendSettings.feedback.reachable': `החיבור הצליח. צד-השרת מוכן.`, + 'backendSettings.feedback.degraded': `צד-השרת נגיש אך מוגבל (המודלים או המדד אינם נטענים).`, + 'backendSettings.feedback.unreachable': `צד-השרת אינו נגיש. בדוק את ה-URL וודא שצד-השרת פועל.`, + 'faceMatch.title': `הפריים הנוכחי`, + 'faceMatch.close': `סגירה`, + 'faceMatch.facesSelected': `נמצאו {faces} פנים · נבחרו {selected}`, + 'faceMatch.inScene': `· {count} בסצנה`, + 'faceMatch.stashboxMissing': `לא הוגדר stash-box.`, + 'faceMatch.stashboxMissingBody': ` הוסף ספק stash-box בהגדרות ← ספקי מטא-נתונים כדי להפעיל ייבוא מבצעים.`, + 'faceMatch.stashboxWrongName': `לא נמצא ספק "StashDB".`, + 'faceMatch.stashboxWrongNameBody': ` ייבוא מבצעים דורש ספק בשם "StashDB". שנה את שם הספק שלך בהגדרות ← ספקי מטא-נתונים.`, + 'faceMatch.learnMore': `מידע נוסף.`, + 'faceMatch.scanning': `סורק • זיהוי פנים…`, + 'faceMatch.faceAlt': `פנים {index}`, + 'faceMatch.minConf': `מינ. ביטחון`, + 'faceMatch.minConfTitle': `ביטחון מינימלי: {percent}%`, + 'faceMatch.detected': `זוהה`, + 'faceMatch.detectedFaceAlt': `פנים שזוהו`, + 'faceMatch.vs': `מול`, + 'faceMatch.supportPatreon': `תמיכה ב-Patreon`, + 'faceMatch.ofSelected': `נבחרו {selected} מתוך {total}`, + 'faceMatch.allInScene': `נמצאו {total} פנים · כולם בסצנה`, + 'faceMatch.clickToSelect': `נמצאו {total} פנים · לחץ לבחירה`, + 'faceMatch.kbSwitch': `מעבר בין פנים`, + 'faceMatch.kbSelect': `בחירת מבצעים`, + 'faceMatch.kbToggle': `החלפת בחירה`, + 'faceMatch.kbAddInstant': `Shift+לחיצה להוספה מיידית`, + 'faceMatch.selectBest': `בחירת ההתאמות הטובות ביותר`, + 'faceMatch.adding': `מוסיף...`, + 'faceMatch.done': `בוצע ({count})`, + 'faceMatch.toast.added': `המבצע נוסף אל {target}.`, + 'faceMatch.toast.addError': `הוספת המבצע נכשלה: {error}`, + 'faceMatch.toast.noStashbox': `לא הוגדר stash-box. הוסף ספק stash-box בהגדרות ← ספקי מטא-נתונים כדי להפעיל ייבוא מבצעים. ראה {url}`, + 'faceMatch.toast.noProvider': `לא נמצא ספק בשם "StashDB". שנה את שם הספק שלך ל-"StashDB" בהגדרות ← ספקי מטא-נתונים כדי להפעיל ייבוא מבצעים.`, + 'faceMatch.toast.configureProvider': `הגדר ספק stash-box בהגדרות ← ספקי מטא-נתונים כדי להפעיל ייבוא מבצעים.`, + 'faceMatch.toast.addedMultiple': `נוספו {count} מבצעים{s} אל {target}.`, + 'sprite.title': `מבצעי הסצנה`, + 'sprite.close': `סגירה`, + 'sprite.foundConfirmed': `נמצאו {found} · אושרו {confirmed}`, + 'sprite.confidence': `ביטחון`, + 'sprite.name': `שם`, + 'sprite.hits': `פגיעות`, + 'sprite.minConf': `מינ. ביטחון`, + 'sprite.minConfTitle': `ביטחון מינימלי: {percent}%`, + 'sprite.scanning': `Visage סורק…`, + 'sprite.cancel': `ביטול`, + 'sprite.empty': `לא זוהו מבצעים בספייריט זה.`, + 'sprite.detectedFaceAlt': `פנים שזוהו`, + 'sprite.spriteLabel': `ספייריט`, + 'sprite.stashLabel': `STASH`, + 'sprite.vs': `מול`, + 'sprite.inScene': `בסצנה`, + 'sprite.hitsCount': `{count} פגיעות{s}`, + 'sprite.totalTime': `{time} בסך הכול`, + 'sprite.alreadyInScene': `כבר בסצנה`, + 'sprite.clickToConfirm': `לחץ לאישור`, + 'sprite.confirmed': `אושר`, + 'sprite.supportPatreon': `תמיכה ב-Patreon`, + 'sprite.confirmedCount': `אושרו {confirmed} מתוך {total}`, + 'sprite.shownHint': `מוצגים {shown} (מתוך {total} בסך הכול) · לחץ לאישור · ←→ ניווט · Enter לאישור`, + 'sprite.confirmHint': `לחץ לאישור · ←→ ניווט · Enter לאישור`, + 'sprite.adding': `מוסיף...`, + 'sprite.done': `בוצע ({count})`, + 'gender.male': `זכר`, + 'gender.female': `נקבה`, + 'gender.transMale': `גבר טרנסג׳נדר`, + 'gender.transFemale': `אישה טרנסג׳נדרית`, + 'gender.nonBinary': `לא-בינארי`, + 'gender.intersex': `אינטרסקס`, + 'card.excellent': `התאמה מצוינת`, + 'card.good': `התאמה טובה`, + 'card.uncertain': `התאמה לא ודאית`, + 'card.select': `בחירת {name}`, + 'card.deselect': `ביטול בחירת {name}`, + 'card.openOn': `פתיחה ב-{source}`, + 'search.overlayHint': `גרור כדי לבחור פנים — Enter לסריקת הפריים כולו — Esc לביטול`, + 'search.noFaces': `לא נמצאו פנים בבחירה זו. נסה חיתוך הדוק יותר, או לחץ Enter כדי לסרוק את כל הפריים.`, + 'search.captureMediaFail': `לא ניתן היה ללכוד מדיה. ודא שהסצנה/התמונה נטענה במלואה.`, + 'search.healthBanner': `ממשק זיהוי הפנים אינו נגיש. הפעל את צד-השרת ונסה שוב.`, + 'search.failed': `חיפוש הפנים נכשל: {error}`, + 'search.fetchImageFail': `לא ניתן היה לאחזר תמונה מ-Stash.`, + 'search.captureFail': `לכידת התמונה נכשלה: {error}`, + 'search.selectFaceImage': `בחר פנים בתוך התמונה.`, + 'search.captureFrameFail': `לא ניתן היה ללכוד את הפריים הנוכחי.`, + 'search.captureFrameFail2': `לכידת הפריים הנוכחי נכשלה.`, + 'search.selectFaceVideo': `בחר פנים בתוך אזור נגן הווידאו.`, + 'search.menuItemTitle': `גרור תיבה סביב פנים, או לחץ Enter לסריקת הפריים כולו, כדי לחפש התאמות ב-StashDB`, + 'search.currentFrame': `Visage: פריים נוכחי`, + 'scene.noSprite': `אין גיליון ספייריט או וידאו תצוגה מקדימה לסצנה זו. צור אותם בהגדרות הסצנה, ואז נסה שוב.`, + 'scene.noFaces': `לא נמצאו פנים או מבצעים בגיליון הספייריט או בווידאו התצוגה המקדימה של סצנה זו.`, + 'scene.healthBanner': `ממשק זיהוי הפנים אינו נגיש. הפעל את צד-השרת ונסה שוב.`, + 'scene.failed': `סריקת הסצנה נכשלה: {error}`, + 'scene.menuItemTitle': `זהה כל מבצע בסצנה (דורש גיליון ספייריט או וידאו תצוגה מקדימה שנוצר)`, + 'scene.wholeScene': `Visage: כל הסצנה`, + 'banner.changeBackend': `החלפת צד-שרת`, + 'banner.dismiss': `סגירה`, + 'error.dismiss': `סגירה`, + 'firstRun.title': `הגדרת צד-השרת של Visage`, + 'firstRun.subtitle': `Visage שולח תמונות פנים לצד-השרת לצורך זיהוי. בחר היכן להריץ אותו.`, + 'firstRun.cloud': `שימוש בענן של Hugging Face`, + 'firstRun.cloudNote': `ללא התקנה. התמונות נשלחות לשירות הענן של Hugging Face.`, + 'firstRun.local': `שימוש בשרת שלי`, + 'firstRun.localNote': `הרץ את הקובץ הפרטי על המחשב או הרשת שלך.`, + 'firstRun.skip': `דלג בינתיים`, + 'badge.local': `מקומי`, + 'badge.cloud': `ענן (Hugging Face)`, + 'badge.title': `צד-שרת Visage: {label}`, + 'donate.enjoying': `נהנה מ-Visage? עזור לשמור עליו בחיים`, + 'donate.supportPatreon': `תמיכה ב-Patreon`, + 'frame.close': `סגירת בורר הפריים`, + 'frame.seekFail': `ניווט בנגן הווידאו נכשל.`, + 'frame.selectAt': `בחירת פריים פנים ב-{time} שניות`, + }; + + const lt = { + 'backendSettings.title': `Pamatinės sistemos nustatymai`, + 'backendSettings.closeAria': `Uždaryti nustatymus`, + 'backendSettings.backendAria': `Pamatinės sistemos nustatymai`, + 'backendSettings.changeBackend': `Keisti pamatinę sistemą`, + 'backendSettings.backendLabel': `Pamatinė sistema`, + 'backendSettings.local': `Vietinė`, + 'backendSettings.cloud': `Debesis (Hugging Face)`, + 'backendSettings.cloudNote': `Vaizdai siunčiami į Hugging Face debesų paslaugą.`, + 'backendSettings.hintPrefix': `Ar norite, kad jūsų vaizdai liktų jūsų tinkle?`, + 'backendSettings.hintLink': `Paleiskite privatų serverį per Patreon`, + 'backendSettings.urlLabel': `URL`, + 'backendSettings.csp1': `Naršyklės saugumo politika (CSP) pagal numatytuosius nustatymus leidžia tik`, + 'backendSettings.csp2': `. Norėdami pasiekti vietinę pamatinę sistemą kitu adresu (pvz., savo LAN IP), pridėkite jį prie`, + 'backendSettings.csp3': `sąrašo`, + 'backendSettings.csp4': `faile „Stash“ papildinių aplanke, kitaip užklausos bus užblokuotos. Pastaba: atnaujinus „Visage“ sistema įdiegiama iš naujo`, + 'backendSettings.csp5': `, todėl tai reikia pakartoti po kiekvieno atnaujinimo.`, + 'backendSettings.testing': `Tikrinamas ryšys…`, + 'backendSettings.testConnection': `Patikrinti ryšį`, + 'backendSettings.testingShort': `Tikrinama…`, + 'backendSettings.cancel': `Atšaukti`, + 'backendSettings.save': `Išsaugoti`, + 'backendSettings.feedback.reachable': `Ryšys sėkmingas. Pamatinė sistema paruošta.`, + 'backendSettings.feedback.degraded': `Pamatinė sistema pasiekiama, bet sumažintu režimu (modeliai arba indeksas neįkelti).`, + 'backendSettings.feedback.unreachable': `Pamatinė sistema nepasiekiama. Patikrinkite URL ir įsitikinkite, kad sistema veikia.`, + 'faceMatch.title': `DABARTINIS KADRAS`, + 'faceMatch.close': `Uždaryti`, + 'faceMatch.facesSelected': `Rasta veidų: {faces} · pasirinkta: {selected}`, + 'faceMatch.inScene': `· {count} scenoje`, + 'faceMatch.stashboxMissing': `Stash-box nesukonfigūruotas.`, + 'faceMatch.stashboxMissingBody': ` Pridėkite stash-box teikėją programoje Settings → Metadata Providers, kad įgalintumėte atlikėjų importavimą.`, + 'faceMatch.stashboxWrongName': `Teikėjas „StashDB“ nerastas.`, + 'faceMatch.stashboxWrongNameBody': ` Atlikėjų importavimui reikia teikėjo, pavadinto „StashDB“. Pervadinkite teikėją programoje Settings → Metadata Providers.`, + 'faceMatch.learnMore': `Sužinoti daugiau.`, + 'faceMatch.scanning': `Skenuojama • veidų atpažinimas…`, + 'faceMatch.faceAlt': `Veidas {index}`, + 'faceMatch.minConf': `Min. patik.`, + 'faceMatch.minConfTitle': `Minimalus patikimumas: {percent}%`, + 'faceMatch.detected': `Aptikta`, + 'faceMatch.detectedFaceAlt': `Aptiktas veidas`, + 'faceMatch.vs': `vs`, + 'faceMatch.supportPatreon': `Paremkite per Patreon`, + 'faceMatch.ofSelected': `Pasirinkta {selected} iš {total}`, + 'faceMatch.allInScene': `Rasta veidų: {total} · visi scenoje`, + 'faceMatch.clickToSelect': `Rasta veidų: {total} · spustelėkite, kad pasirinktumėte`, + 'faceMatch.kbSwitch': `Veidų perjungimas`, + 'faceMatch.kbSelect': `Atlikėjų pasirinkimas`, + 'faceMatch.kbToggle': `Perjungti pasirinkimą`, + 'faceMatch.kbAddInstant': `Shift+spustelėjimas, kad pridėtumėte iš karto`, + 'faceMatch.selectBest': `Pasirinkti geriausius atitikmenis`, + 'faceMatch.adding': `Pridedama...`, + 'faceMatch.done': `Atlikta ({count})`, + 'faceMatch.toast.added': `Atlikėjas pridėtas į {target}.`, + 'faceMatch.toast.addError': `Nepavyko pridėti atlikėjo: {error}`, + 'faceMatch.toast.noStashbox': `Stash-box nesukonfigūruotas. Pridėkite stash-box teikėją programoje Settings → Metadata Providers, kad įgalintumėte atlikėjų importavimą. Žr. {url}`, + 'faceMatch.toast.noProvider': `Teikėjas „StashDB“ nerastas. Pervadinkite teikėją į „StashDB“ programoje Settings → Metadata Providers, kad įgalintumėte atlikėjų importavimą.`, + 'faceMatch.toast.configureProvider': `Sukonfigūruokite stash-box teikėją programoje Settings → Metadata Providers, kad įgalintumėte atlikėjų importavimą.`, + 'faceMatch.toast.addedMultiple': `Pridėta atlikėjų: {count} į {target}.`, + 'sprite.title': `SCENOS ATLIKĖJAI`, + 'sprite.close': `Uždaryti`, + 'sprite.foundConfirmed': `Rasta: {found} · patvirtinta: {confirmed}`, + 'sprite.confidence': `patikimumas`, + 'sprite.name': `vardas`, + 'sprite.hits': `pataikymai`, + 'sprite.minConf': `Min. patik.`, + 'sprite.minConfTitle': `Minimalus patikimumas: {percent}%`, + 'sprite.scanning': `Visage skenavimas…`, + 'sprite.cancel': `Atšaukti`, + 'sprite.empty': `Šiame sprite neidentifikuota atlikėjų.`, + 'sprite.detectedFaceAlt': `Aptiktas veidas`, + 'sprite.spriteLabel': `SPRITE`, + 'sprite.stashLabel': `STASH`, + 'sprite.vs': `vs`, + 'sprite.inScene': `Scenoje`, + 'sprite.hitsCount': `Pataikymų: {count}`, + 'sprite.totalTime': `Iš viso {time}`, + 'sprite.alreadyInScene': `Jau scenoje`, + 'sprite.clickToConfirm': `Spustelėkite, kad patvirtintumėte`, + 'sprite.confirmed': `Patvirtinta`, + 'sprite.supportPatreon': `Paremkite per Patreon`, + 'sprite.confirmedCount': `Patvirtinta {confirmed} iš {total}`, + 'sprite.shownHint': `Rodyta: {shown} (iš viso {total}) · spustelėkite, kad patvirtintumėte · ←→ navigacija · Enter patvirtinimas`, + 'sprite.confirmHint': `Spustelėkite, kad patvirtintumėte · ←→ navigacija · Enter patvirtinimas`, + 'sprite.adding': `Pridedama...`, + 'sprite.done': `Atlikta ({count})`, + 'gender.male': `Vyras`, + 'gender.female': `Moteris`, + 'gender.transMale': `Translytis vyras`, + 'gender.transFemale': `Translytė moteris`, + 'gender.nonBinary': `Ne dvinaris`, + 'gender.intersex': `Interseksas`, + 'card.excellent': `Puikus atitikmuo`, + 'card.good': `Geras atitikmuo`, + 'card.uncertain': `Neaiškus atitikmuo`, + 'card.select': `Pasirinkti {name}`, + 'card.deselect': `Panaikinti {name} pasirinkimą`, + 'card.openOn': `Atidaryti svetainėje {source}`, + 'search.overlayHint': `Vilkite, kad pasirinktumėte veidą — Enter, kad nuskaitytumėte visą kadrą — Esc, kad atšauktumėte`, + 'search.noFaces': `Pasirinktoje srityje veidų nerasta. Bandykite tankesnį kadrą arba paspauskite Enter, kad nuskaitytumėte visą kadrą.`, + 'search.captureMediaFail': `Nepavyko užfiksuoti medijos. Įsitikinkite, kad scena/vaizdas visiškai įkeltas.`, + 'search.healthBanner': `Veidų atpažinimo API nepasiekiama. Paleiskite pamatinę sistemą ir bandykite dar kartą.`, + 'search.failed': `Veido paieška nepavyko: {error}`, + 'search.fetchImageFail': `Nepavyko gauti vaizdo iš Stash.`, + 'search.captureFail': `Nepavyko užfiksuoti vaizdo: {error}`, + 'search.selectFaceImage': `Pasirinkite veidą vaizde.`, + 'search.captureFrameFail': `Nepavyko užfiksuoti dabartinio kadro.`, + 'search.captureFrameFail2': `Klaida fiksuojant dabartinį kadrą.`, + 'search.selectFaceVideo': `Pasirinkite veidą vaizdo grotuvo srityje.`, + 'search.menuItemTitle': `Vilkite langelį aplink veidą arba paspauskite Enter, kad nuskaitytumėte visą kadrą, ir ieškokite atitikmenų StashDB`, + 'search.currentFrame': `Visage: dabartinis kadras`, + 'scene.noSprite': `Šiai scenai nerastas nei sprite lapas, nei peržiūros vaizdo įrašas. Sugeneruokite juos scenos nustatymuose ir bandykite dar kartą.`, + 'scene.noFaces': `Šios scenos sprite lape arba peržiūros vaizdo įraše nerasta veidų ar atlikėjų.`, + 'scene.healthBanner': `Veidų atpažinimo API nepasiekiama. Paleiskite pamatinę sistemą ir bandykite dar kartą.`, + 'scene.failed': `Scenos nuskaitymas nepavyko: {error}`, + 'scene.menuItemTitle': `Identifikuokite kiekvieną atlikėją scenoje (reikia sugeneruoto sprite lapo arba peržiūros vaizdo įrašo)`, + 'scene.wholeScene': `Visage: visa scena`, + 'banner.changeBackend': `Keisti pamatinę sistemą`, + 'banner.dismiss': `Atmesti`, + 'error.dismiss': `Atmesti`, + 'firstRun.title': `Nustatykite savo Visage pamatinę sistemą`, + 'firstRun.subtitle': `Visage siunčia veidų vaizdus į pamatinę sistemą atpažinimui. Pasirinkite, kur ją paleisti.`, + 'firstRun.cloud': `Naudoti Hugging Face debesį`, + 'firstRun.cloudNote': `Be jokios sąrankos. Vaizdai siunčiami į Hugging Face debesų paslaugą.`, + 'firstRun.local': `Naudoti savo serverį`, + 'firstRun.localNote': `Paleiskite privatų vykdomąjį failą savo kompiuteryje arba tinkle.`, + 'firstRun.skip': `Praleisti kol kas`, + 'badge.local': `Vietinė`, + 'badge.cloud': `Debesis (Hugging Face)`, + 'badge.title': `Visage pamatinė sistema: {label}`, + 'donate.enjoying': `Patinka Visage? Padėkite jį išlaikyti`, + 'donate.supportPatreon': `Paremkite per Patreon`, + 'frame.close': `Uždaryti kadro pasirinkiklį`, + 'frame.seekFail': `Nepavyko persukti vaizdo grotuvo.`, + 'frame.selectAt': `Pasirinkti veido kadrą {time} s`, + }; + + // Spanish. Missing keys fall back to English automatically. + const es = { + // ---- BackendSettings.tsx ---- + 'backendSettings.title': `Configuración del backend`, + 'backendSettings.closeAria': `Cerrar configuración`, + 'backendSettings.backendAria': `Configuración del backend`, + 'backendSettings.changeBackend': `Cambiar backend`, + 'backendSettings.backendLabel': `Backend`, + 'backendSettings.local': `Local`, + 'backendSettings.cloud': `Cloud (Hugging Face)`, + 'backendSettings.cloudNote': `Las imágenes se envían al servicio en la nube de Hugging Face.`, + 'backendSettings.hintPrefix': `¿Quieres que tus imágenes permanezcan en tu red?`, + 'backendSettings.hintLink': `Ejecuta un servidor privado mediante Patreon`, + 'backendSettings.urlLabel': `URL`, + 'backendSettings.csp1': `La política de seguridad (CSP) del navegador solo permite`, + 'backendSettings.csp2': `por defecto. Para alcanzar un backend local en otra dirección (p. ej. tu IP LAN), añádela a la`, + 'backendSettings.csp3': `lista en el`, + 'backendSettings.csp4': `archivo dentro de tu carpeta de plugins de Stash; de lo contrario, las solicitudes se bloquearán. Nota: actualizar Visage reinstala`, + 'backendSettings.csp5': `, por lo que esto debe volver a aplicarse tras cada actualización.`, + 'backendSettings.testing': `Probando la conexión…`, + 'backendSettings.testConnection': `Probar conexión`, + 'backendSettings.testingShort': `Probando…`, + 'backendSettings.cancel': `Cancelar`, + 'backendSettings.save': `Guardar`, + 'backendSettings.feedback.reachable': `Conexión exitosa. El backend está listo.`, + 'backendSettings.feedback.degraded': `Backend accesible pero degradado (modelos o índice no cargados).`, + 'backendSettings.feedback.unreachable': `Backend inaccesible. Comprueba la URL y que el backend esté en ejecución.`, + // ---- FaceMatchModal.tsx ---- + 'faceMatch.title': `FOTO ACTUAL`, + 'faceMatch.close': `Cerrar`, + 'faceMatch.facesSelected': `{faces} caras encontradas · {selected} seleccionadas`, + 'faceMatch.inScene': `· {count} en la escena`, + 'faceMatch.stashboxMissing': `No hay stash-box configurado.`, + 'faceMatch.stashboxMissingBody': ` Añade un proveedor stash-box en Configuración → Proveedores de metadatos para activar la importación de intérpretes.`, + 'faceMatch.stashboxWrongName': `No se encontró ningún proveedor llamado "StashDB".`, + 'faceMatch.stashboxWrongNameBody': ` La importación de intérpretes requiere un proveedor llamado "StashDB". Renombra tu proveedor en Configuración → Proveedores de metadatos.`, + 'faceMatch.learnMore': `Más información.`, + 'faceMatch.scanning': `Escaneando • reconocimiento facial…`, + 'faceMatch.faceAlt': `Cara {index}`, + 'faceMatch.minConf': `Conf. mín.`, + 'faceMatch.minConfTitle': `Confianza mínima: {percent}%`, + 'faceMatch.detected': `Detectada`, + 'faceMatch.detectedFaceAlt': `Cara detectada`, + 'faceMatch.vs': `vs`, + 'faceMatch.supportPatreon': `Apoyar en Patreon`, + 'faceMatch.ofSelected': `{selected} de {total} seleccionadas`, + 'faceMatch.allInScene': `{total} caras encontradas · todas en la escena`, + 'faceMatch.clickToSelect': `{total} caras encontradas · haz clic para seleccionar`, + 'faceMatch.kbSwitch': `Cambiar caras`, + 'faceMatch.kbSelect': `Seleccionar intérpretes`, + 'faceMatch.kbToggle': `Alternar selección`, + 'faceMatch.kbAddInstant': `Mayús+clic para añadir al instante`, + 'faceMatch.selectBest': `Seleccionar mejores coincidencias`, + 'faceMatch.adding': `Añadiendo...`, + 'faceMatch.done': `Hecho ({count})`, + 'faceMatch.toast.added': `Intérprete añadido a {target}.`, + 'faceMatch.toast.addError': `Error al añadir el intérprete: {error}`, + 'faceMatch.toast.noStashbox': `No hay stash-box configurado. Añade un proveedor stash-box en Configuración → Proveedores de metadatos para activar la importación de intérpretes. Ver {url}`, + 'faceMatch.toast.noProvider': `No se encontró ningún proveedor llamado "StashDB". Renombra tu proveedor a "StashDB" en Configuración → Proveedores de metadatos para activar la importación de intérpretes.`, + 'faceMatch.toast.configureProvider': `Configura un proveedor stash-box en Configuración → Proveedores de metadatos para activar la importación de intérpretes.`, + 'faceMatch.toast.addedMultiple': `{count} intérprete{s} añadido{s} a {target}.`, + // ---- SpriteResultModal.tsx ---- + 'sprite.title': `INTÉRPRETES DE LA ESCENA`, + 'sprite.close': `Cerrar`, + 'sprite.foundConfirmed': `{found} encontrados · {confirmed} confirmados`, + 'sprite.confidence': `confianza`, + 'sprite.name': `nombre`, + 'sprite.hits': `aciertos`, + 'sprite.minConf': `Conf. mín.`, + 'sprite.minConfTitle': `Confianza mínima: {percent}%`, + 'sprite.scanning': `Visage escaneando…`, + 'sprite.cancel': `Cancelar`, + 'sprite.empty': `No se identificaron intérpretes en este sprite.`, + 'sprite.detectedFaceAlt': `Cara detectada`, + 'sprite.spriteLabel': `SPRITE`, + 'sprite.stashLabel': `STASH`, + 'sprite.vs': `vs`, + 'sprite.inScene': `En la escena`, + 'sprite.hitsCount': `{count} acierto{s}`, + 'sprite.totalTime': `{time} en total`, + 'sprite.alreadyInScene': `Ya en la escena`, + 'sprite.clickToConfirm': `Haz clic para confirmar`, + 'sprite.confirmed': `Confirmado`, + 'sprite.supportPatreon': `Apoyar en Patreon`, + 'sprite.confirmedCount': `{confirmed} de {total} confirmados`, + 'sprite.shownHint': `{shown} mostrados ({total} en total) · haz clic para confirmar · ←→ navegar · Enter confirmar`, + 'sprite.confirmHint': `Haz clic para confirmar · ←→ navegar · Enter confirmar`, + 'sprite.adding': `Añadiendo...`, + 'sprite.done': `Hecho ({count})`, + // ---- PerformerCard.tsx ---- + 'gender.male': `Masculino`, + 'gender.female': `Femenino`, + 'gender.transMale': `Hombre transgénero`, + 'gender.transFemale': `Mujer transgénero`, + 'gender.nonBinary': `No binario`, + 'gender.intersex': `Intersexual`, + 'card.excellent': `Coincidencia excelente`, + 'card.good': `Buena coincidencia`, + 'card.uncertain': `Coincidencia incierta`, + 'card.select': `Seleccionar a {name}`, + 'card.deselect': `Deseleccionar a {name}`, + 'card.openOn': `Abrir en {source}`, + // ---- FaceSearchButton.tsx ---- + 'search.overlayHint': `Arrastra para seleccionar una cara — Enter para escanear toda la foto — Esc para cancelar`, + 'search.noFaces': `No se encontraron caras en esa selección. Prueba con un recorte más ajustado o pulsa Enter para escanear toda la foto.`, + 'search.captureMediaFail': `No se pudo capturar el medio. Asegúrate de que la escena/imagen esté completamente cargada.`, + 'search.healthBanner': `La API de reconocimiento facial no está accesible. Inicia el backend e inténtalo de nuevo.`, + 'search.failed': `La búsqueda de caras falló: {error}`, + 'search.fetchImageFail': `No se pudo obtener la imagen desde Stash.`, + 'search.captureFail': `Error al capturar la imagen: {error}`, + 'search.selectFaceImage': `Selecciona una cara dentro de la imagen.`, + 'search.captureFrameFail': `No se pudo capturar el fotograma actual.`, + 'search.captureFrameFail2': `Error al capturar el fotograma actual.`, + 'search.selectFaceVideo': `Selecciona una cara dentro del área del reproductor de vídeo.`, + 'search.menuItemTitle': `Arrastra un recuadro alrededor de una cara o pulsa Enter para escanear toda la foto, para buscar coincidencias en StashDB`, + 'search.currentFrame': `Visage: Fotograma actual`, + // ---- SceneScanButton.tsx ---- + 'scene.noSprite': `No se encontró ninguna hoja de sprites ni vídeo de vista previa para esta escena. Genéralos en la configuración de la escena y vuelve a intentarlo.`, + 'scene.noFaces': `No se encontraron caras ni intérpretes en la hoja de sprites o el vídeo de vista previa de esta escena.`, + 'scene.healthBanner': `La API de reconocimiento facial no está accesible. Inicia el backend e inténtalo de nuevo.`, + 'scene.failed': `El escaneo de la escena falló: {error}`, + 'scene.menuItemTitle': `Identificar a cada intérprete de la escena (requiere una hoja de sprites o un vídeo de vista previa generados)`, + 'scene.wholeScene': `Visage: Escena completa`, + // ---- BackendHealthBanner.tsx ---- + 'banner.changeBackend': `Cambiar backend`, + 'banner.dismiss': `Descartar`, + // ---- ErrorDialog.tsx ---- + 'error.dismiss': `Descartar`, + // ---- FirstRunDialog.tsx ---- + 'firstRun.title': `Configura tu backend de Visage`, + 'firstRun.subtitle': `Visage envía imágenes de caras a un backend para su reconocimiento. Elige dónde ejecutarlo.`, + 'firstRun.cloud': `Usar el cloud de Hugging Face`, + 'firstRun.cloudNote': `Sin configuración. Las imágenes se envían al servicio en la nube de Hugging Face.`, + 'firstRun.local': `Usar mi propio servidor`, + 'firstRun.localNote': `Ejecuta el binario privado en tu propia máquina o red.`, + 'firstRun.skip': `Omitir por ahora`, + // ---- BackendBadge.tsx ---- + 'badge.local': `Local`, + 'badge.cloud': `Cloud (Hugging Face)`, + 'badge.title': `Backend de Visage: {label}`, + // ---- DonateFooter.tsx ---- + 'donate.enjoying': `¿Te gusta Visage? Ayuda a mantenerlo vivo`, + 'donate.supportPatreon': `Apoyar en Patreon`, + // ---- FaceFrameSelector.tsx ---- + 'frame.close': `Cerrar selector de fotograma`, + 'frame.seekFail': `Error al buscar en el reproductor de vídeo.`, + 'frame.selectAt': `Seleccionar cara en {time}s`, + }; + + const ar = { + 'backendSettings.title': `إعدادات الواجهة الخلفية`, + 'backendSettings.closeAria': `إغلاق الإعدادات`, + 'backendSettings.backendAria': `إعدادات الواجهة الخلفية`, + 'backendSettings.changeBackend': `تغيير الواجهة الخلفية`, + 'backendSettings.backendLabel': `الواجهة الخلفية`, + 'backendSettings.local': `محلي`, + 'backendSettings.cloud': `سحابة (Hugging Face)`, + 'backendSettings.cloudNote': `يتم إرسال الصور إلى خدمة سحابة Hugging Face.`, + 'backendSettings.hintPrefix': `تريد إبقاء صورك داخل شبكتك؟`, + 'backendSettings.hintLink': `شغّل خادمًا خاصًا عبر Patreon`, + 'backendSettings.urlLabel': `URL`, + 'backendSettings.csp1': `تسمح سياسة أمان المتصفح (CSP) فقط بـ`, + 'backendSettings.csp2': `افتراضيًا. للوصول إلى واجهة خلفية محلية على عنوان آخر (مثل IP شبكة LAN الخاصة بك)، أضفها إلى`, + 'backendSettings.csp3': `في`, + 'backendSettings.csp4': `داخل مجلد إضافات Stash لديك، وإلا فسيتم حظر الطلبات. ملاحظة: تحديث Visage يعيد تثبيت`, + 'backendSettings.csp5': `، لذا يجب إعادة تطبيق هذا بعد كل تحديث.`, + 'backendSettings.testing': `جارٍ اختبار الاتصال…`, + 'backendSettings.testConnection': `اختبار الاتصال`, + 'backendSettings.testingShort': `جارٍ الاختبار…`, + 'backendSettings.cancel': `إلغاء`, + 'backendSettings.save': `حفظ`, + 'backendSettings.feedback.reachable': `تم الاتصال بنجاح. الواجهة الخلفية جاهزة.`, + 'backendSettings.feedback.degraded': `الواجهة الخلفية قابلة للوصول لكنها متدهورة (النماذج أو الفهرس غير محمّل).`, + 'backendSettings.feedback.unreachable': `الواجهة الخلفية غير قابلة للوصول. تحقق من URL وتأكد من أن الواجهة الخلفية تعمل.`, + 'faceMatch.title': `الإطار الحالي`, + 'faceMatch.close': `إغلاق`, + 'faceMatch.facesSelected': `تم العثور على {faces} وجهًا · تم اختيار {selected}`, + 'faceMatch.inScene': `· {count} في المشهد`, + 'faceMatch.stashboxMissing': `لا يوجد stash-box مُهيأ.`, + 'faceMatch.stashboxMissingBody': ` أضف موفّر stash-box في الإعدادات ← موفّرو البيانات الوصفية لتفعيل استيراد المؤدين.`, + 'faceMatch.stashboxWrongName': `لم يتم العثور على موفّر "StashDB".`, + 'faceMatch.stashboxWrongNameBody': ` يتطلب استيراد المؤدين موفّرًا باسم "StashDB". أعد تسمية الموفّر لديك في الإعدادات ← موفّرو البيانات الوصفية.`, + 'faceMatch.learnMore': `اعرف المزيد.`, + 'faceMatch.scanning': `جارٍ المسح • التعرف على الوجوه…`, + 'faceMatch.faceAlt': `الوجه {index}`, + 'faceMatch.minConf': `أدنى ثقة`, + 'faceMatch.minConfTitle': `أدنى ثقة: {percent}%`, + 'faceMatch.detected': `تم اكتشافه`, + 'faceMatch.detectedFaceAlt': `الوجه المكتشف`, + 'faceMatch.vs': `ضد`, + 'faceMatch.supportPatreon': `ادعمنا على Patreon`, + 'faceMatch.ofSelected': `تم اختيار {selected} من أصل {total}`, + 'faceMatch.allInScene': `تم العثور على {total} وجهًا · جميعها في المشهد`, + 'faceMatch.clickToSelect': `تم العثور على {total} وجهًا · انقر للاختيار`, + 'faceMatch.kbSwitch': `تبديل الوجوه`, + 'faceMatch.kbSelect': `اختيار المؤدين`, + 'faceMatch.kbToggle': `تبديل التحديد`, + 'faceMatch.kbAddInstant': `Shift+نقرة للإضافة فورًا`, + 'faceMatch.selectBest': `اختيار أفضل التطابقات`, + 'faceMatch.adding': `جارٍ الإضافة...`, + 'faceMatch.done': `تم ({count})`, + 'faceMatch.toast.added': `تمت إضافة المؤدي إلى {target}.`, + 'faceMatch.toast.addError': `فشل إضافة المؤدي: {error}`, + 'faceMatch.toast.noStashbox': `لا يوجد stash-box مُهيأ. أضف موفّر stash-box في الإعدادات ← موفّرو البيانات الوصفية لتفعيل استيراد المؤدين. انظر {url}`, + 'faceMatch.toast.noProvider': `لم يتم العثور على موفّر باسم "StashDB". أعد تسمية الموفّر لديك إلى "StashDB" في الإعدادات ← موفّرو البيانات الوصفية لتفعيل استيراد المؤدين.`, + 'faceMatch.toast.configureProvider': `هيّئ موفّر stash-box في الإعدادات ← موفّرو البيانات الوصفية لتفعيل استيراد المؤدين.`, + 'faceMatch.toast.addedMultiple': `تمت إضافة {count} من المؤدين{s} إلى {target}.`, + 'sprite.title': `مؤدو المشهد`, + 'sprite.close': `إغلاق`, + 'sprite.foundConfirmed': `تم العثور على {found} · تم تأكيد {confirmed}`, + 'sprite.confidence': `الثقة`, + 'sprite.name': `الاسم`, + 'sprite.hits': `الإصابات`, + 'sprite.minConf': `أدنى ثقة`, + 'sprite.minConfTitle': `أدنى ثقة: {percent}%`, + 'sprite.scanning': `Visage يمسح…`, + 'sprite.cancel': `إلغاء`, + 'sprite.empty': `لم يتم تحديد أي مؤدٍ في هذه اللقطة (sprite).`, + 'sprite.detectedFaceAlt': `الوجه المكتشف`, + 'sprite.spriteLabel': `اللقطة`, + 'sprite.stashLabel': `STASH`, + 'sprite.vs': `ضد`, + 'sprite.inScene': `في المشهد`, + 'sprite.hitsCount': `{count} إصابة{s}`, + 'sprite.totalTime': `{time} إجمالي`, + 'sprite.alreadyInScene': `موجود بالفعل في المشهد`, + 'sprite.clickToConfirm': `انقر للتأكيد`, + 'sprite.confirmed': `مؤكد`, + 'sprite.supportPatreon': `ادعمنا على Patreon`, + 'sprite.confirmedCount': `تم تأكيد {confirmed} من أصل {total}`, + 'sprite.shownHint': `يُعرض {shown} (إجمالي {total}) · انقر للتأكيد · ←→ تنقّل · Enter للتأكيد`, + 'sprite.confirmHint': `انقر للتأكيد · ←→ تنقّل · Enter للتأكيد`, + 'sprite.adding': `جارٍ الإضافة...`, + 'sprite.done': `تم ({count})`, + 'gender.male': `ذكر`, + 'gender.female': `أنثى`, + 'gender.transMale': `رجل متحول جنسيًا`, + 'gender.transFemale': `امرأة متحولة جنسيًا`, + 'gender.nonBinary': `غير ثنائي`, + 'gender.intersex': `ثنائيو الجنس`, + 'card.excellent': `تطابق ممتاز`, + 'card.good': `تطابق جيد`, + 'card.uncertain': `تطابق غير مؤكد`, + 'card.select': `اختيار {name}`, + 'card.deselect': `إلغاء اختيار {name}`, + 'card.openOn': `فتح على {source}`, + 'search.overlayHint': `اسحب لتحديد وجه — Enter لمسح الإطار بأكمله — Esc للإلغاء`, + 'search.noFaces': `لم يتم العثور على وجوه في هذا التحديد. جرّب اقتصاصًا أدق، أو اضغط Enter لمسح الإطار بأكمله.`, + 'search.captureMediaFail': `تعذّر التقاط الوسائط. يرجى التأكد من تحميل المشهد/الصورة بالكامل.`, + 'search.healthBanner': `واجهة برمجة تطبيقات التعرف على الوجوه غير قابلة للوصول. ابدأ الواجهة الخلفية وحاول مرة أخرى.`, + 'search.failed': `فشل البحث عن الوجوه: {error}`, + 'search.fetchImageFail': `تعذّر جلب الصورة من Stash.`, + 'search.captureFail': `فشل التقاط الصورة: {error}`, + 'search.selectFaceImage': `اختر وجهًا داخل الصورة.`, + 'search.captureFrameFail': `تعذّر التقاط الإطار الحالي.`, + 'search.captureFrameFail2': `فشل التقاط الإطار الحالي.`, + 'search.selectFaceVideo': `اختر وجهًا داخل منطقة مشغّل الفيديو.`, + 'search.menuItemTitle': `اسحب صندوقًا حول وجه، أو اضغط Enter لمسح الإطار بأكمله، للبحث عن تطابقات في StashDB`, + 'search.currentFrame': `Visage: الإطار الحالي`, + 'scene.noSprite': `لا توجد لوحة لقطات (sprite sheet) أو فيديو معاينة لهذا المشهد. أنشئها في إعدادات المشهد، ثم حاول مرة أخرى.`, + 'scene.noFaces': `لم يتم العثور على وجوه أو مؤدين في لوحة اللقطات أو فيديو المعاينة لهذا المشهد.`, + 'scene.healthBanner': `واجهة برمجة تطبيقات التعرف على الوجوه غير قابلة للوصول. ابدأ الواجهة الخلفية وحاول مرة أخرى.`, + 'scene.failed': `فشل مسح المشهد: {error}`, + 'scene.menuItemTitle': `حدد كل مؤدٍ في المشهد (يتطلب لوحة لقطات أو فيديو معاينة مُنشأ)`, + 'scene.wholeScene': `Visage: المشهد بأكمله`, + 'banner.changeBackend': `تغيير الواجهة الخلفية`, + 'banner.dismiss': `إغلاق`, + 'error.dismiss': `إغلاق`, + 'firstRun.title': `إعداد الواجهة الخلفية لـ Visage`, + 'firstRun.subtitle': `يرسل Visage صور الوجوه إلى الواجهة الخلفية للتعرف عليها. اختر مكان تشغيلها.`, + 'firstRun.cloud': `استخدام سحابة Hugging Face`, + 'firstRun.cloudNote': `بدون أي إعداد. تُرسل الصور إلى خدمة سحابة Hugging Face.`, + 'firstRun.local': `استخدام الخادم الخاص بي`, + 'firstRun.localNote': `شغّل البرنامج الخاص على جهازك أو شبكتك.`, + 'firstRun.skip': `تخطي الآن`, + 'badge.local': `محلي`, + 'badge.cloud': `سحابة (Hugging Face)`, + 'badge.title': `الواجهة الخلفية لـ Visage: {label}`, + 'donate.enjoying': `تستمتع بـ Visage؟ ساعد في إبقائه حيًا`, + 'donate.supportPatreon': `ادعمنا على Patreon`, + 'frame.close': `إغلاق محدد الإطار`, + 'frame.seekFail': `فشل الانتقال في مشغّل الفيديو.`, + 'frame.selectAt': `اختر إطار الوجه عند {time} ثانية`, + }; + + // French. Missing keys fall back to English automatically. + const fr = { + // ---- BackendSettings.tsx ---- + 'backendSettings.title': `Paramètres du backend`, + 'backendSettings.closeAria': `Fermer les paramètres`, + 'backendSettings.backendAria': `Paramètres du backend`, + 'backendSettings.changeBackend': `Changer de backend`, + 'backendSettings.backendLabel': `Backend`, + 'backendSettings.local': `Local`, + 'backendSettings.cloud': `Cloud (Hugging Face)`, + 'backendSettings.cloudNote': `Les images sont envoyées au service cloud Hugging Face.`, + 'backendSettings.hintPrefix': `Vous voulez que vos images restent sur votre réseau ?`, + 'backendSettings.hintLink': `Exécuter un serveur privé via Patreon`, + 'backendSettings.urlLabel': `URL`, + 'backendSettings.csp1': `La politique de sécurité (CSP) du navigateur n'autorise que`, + 'backendSettings.csp2': `par défaut. Pour joindre un backend local sur une autre adresse (p. ex. votre IP LAN), ajoutez-la à la`, + 'backendSettings.csp3': `liste dans le`, + 'backendSettings.csp4': `fichier dans votre dossier de plugins Stash, sinon les requêtes seront bloquées. Remarque : une mise à jour de Visage réinstalle`, + 'backendSettings.csp5': `, donc cela doit être réappliqué après chaque mise à jour.`, + 'backendSettings.testing': `Test de la connexion…`, + 'backendSettings.testConnection': `Tester la connexion`, + 'backendSettings.testingShort': `Test en cours…`, + 'backendSettings.cancel': `Annuler`, + 'backendSettings.save': `Enregistrer`, + 'backendSettings.feedback.reachable': `Connexion réussie. Le backend est prêt.`, + 'backendSettings.feedback.degraded': `Backend joignable mais dégradé (modèles ou index non chargés).`, + 'backendSettings.feedback.unreachable': `Backend injoignable. Vérifiez l'URL et que le backend est en cours d'exécution.`, + // ---- FaceMatchModal.tsx ---- + 'faceMatch.title': `IMAGE ACTUELLE`, + 'faceMatch.close': `Fermer`, + 'faceMatch.facesSelected': `{faces} visages trouvés · {selected} sélectionnés`, + 'faceMatch.inScene': `· {count} dans la scène`, + 'faceMatch.stashboxMissing': `Aucun stash-box configuré.`, + 'faceMatch.stashboxMissingBody': ` Ajoutez un fournisseur stash-box dans Paramètres → Fournisseurs de métadonnées pour activer l'import d'interprètes.`, + 'faceMatch.stashboxWrongName': `Aucun fournisseur nommé « StashDB » trouvé.`, + 'faceMatch.stashboxWrongNameBody': ` L'import d'interprètes nécessite un fournisseur nommé « StashDB ». Renommez votre fournisseur dans Paramètres → Fournisseurs de métadonnées.`, + 'faceMatch.learnMore': `En savoir plus.`, + 'faceMatch.scanning': `Analyse • reconnaissance faciale…`, + 'faceMatch.faceAlt': `Visage {index}`, + 'faceMatch.minConf': `Conf. min.`, + 'faceMatch.minConfTitle': `Confiance minimale : {percent}%`, + 'faceMatch.detected': `Détecté`, + 'faceMatch.detectedFaceAlt': `Visage détecté`, + 'faceMatch.vs': `vs`, + 'faceMatch.supportPatreon': `Soutenir sur Patreon`, + 'faceMatch.ofSelected': `{selected} sur {total} sélectionnés`, + 'faceMatch.allInScene': `{total} visages trouvés · tous dans la scène`, + 'faceMatch.clickToSelect': `{total} visages trouvés · cliquer pour sélectionner`, + 'faceMatch.kbSwitch': `Changer de visage`, + 'faceMatch.kbSelect': `Sélectionner les interprètes`, + 'faceMatch.kbToggle': `Basculer la sélection`, + 'faceMatch.kbAddInstant': `Maj+clic pour ajouter instantanément`, + 'faceMatch.selectBest': `Sélectionner les meilleures correspondances`, + 'faceMatch.adding': `Ajout...`, + 'faceMatch.done': `Terminé ({count})`, + 'faceMatch.toast.added': `Interprète ajouté à la {target}.`, + 'faceMatch.toast.addError': `Échec de l'ajout de l'interprète : {error}`, + 'faceMatch.toast.noStashbox': `Aucun stash-box configuré. Ajoutez un fournisseur stash-box dans Paramètres → Fournisseurs de métadonnées pour activer l'import d'interprètes. Voir {url}`, + 'faceMatch.toast.noProvider': `Aucun fournisseur nommé « StashDB » trouvé. Renommez votre fournisseur en « StashDB » dans Paramètres → Fournisseurs de métadonnées pour activer l'import d'interprètes.`, + 'faceMatch.toast.configureProvider': `Configurez un fournisseur stash-box dans Paramètres → Fournisseurs de métadonnées pour activer l'import d'interprètes.`, + 'faceMatch.toast.addedMultiple': `{count} interprète{s} ajouté{s} à la {target}.`, + // ---- SpriteResultModal.tsx ---- + 'sprite.title': `INTERPRÈTES DE LA SCÈNE`, + 'sprite.close': `Fermer`, + 'sprite.foundConfirmed': `{found} trouvé{s} · {confirmed} confirmé{s}`, + 'sprite.confidence': `confiance`, + 'sprite.name': `nom`, + 'sprite.hits': `occurrences`, + 'sprite.minConf': `Conf. min.`, + 'sprite.minConfTitle': `Confiance minimale : {percent}%`, + 'sprite.scanning': `Analyse Visage…`, + 'sprite.cancel': `Annuler`, + 'sprite.empty': `Aucun interprète identifié dans ce sprite.`, + 'sprite.detectedFaceAlt': `Visage détecté`, + 'sprite.spriteLabel': `SPRITE`, + 'sprite.stashLabel': `STASH`, + 'sprite.vs': `vs`, + 'sprite.inScene': `Dans la scène`, + 'sprite.hitsCount': `{count} occurrence{s}`, + 'sprite.totalTime': `{time} au total`, + 'sprite.alreadyInScene': `Déjà dans la scène`, + 'sprite.clickToConfirm': `Cliquer pour confirmer`, + 'sprite.confirmed': `Confirmé`, + 'sprite.supportPatreon': `Soutenir sur Patreon`, + 'sprite.confirmedCount': `{confirmed} sur {total} confirmés`, + 'sprite.shownHint': `{shown} affiché{s} ({total} au total) · cliquer pour confirmer · ←→ naviguer · Enter pour confirmer`, + 'sprite.confirmHint': `Cliquer pour confirmer · ←→ naviguer · Enter pour confirmer`, + 'sprite.adding': `Ajout...`, + 'sprite.done': `Terminé ({count})`, + // ---- PerformerCard.tsx ---- + 'gender.male': `Homme`, + 'gender.female': `Femme`, + 'gender.transMale': `Homme transgenre`, + 'gender.transFemale': `Femme transgenre`, + 'gender.nonBinary': `Non-binaire`, + 'gender.intersex': `Intersexe`, + 'card.excellent': `Correspondance excellente`, + 'card.good': `Bonne correspondance`, + 'card.uncertain': `Correspondance incertaine`, + 'card.select': `Sélectionner {name}`, + 'card.deselect': `Désélectionner {name}`, + 'card.openOn': `Ouvrir sur {source}`, + // ---- FaceSearchButton.tsx ---- + 'search.overlayHint': `Faites glisser pour sélectionner un visage — Enter pour analyser toute l'image — Esc pour annuler`, + 'search.noFaces': `Aucun visage trouvé dans cette sélection. Essayez un recadrage plus serré ou appuyez sur Enter pour analyser toute l'image.`, + 'search.captureMediaFail': `Impossible de capturer le média. Assurez-vous que la scène/l'image est entièrement chargée.`, + 'search.healthBanner': `L'API de reconnaissance faciale n'est pas joignable. Démarrez le backend et réessayez.`, + 'search.failed': `Échec de la recherche de visage : {error}`, + 'search.fetchImageFail': `Impossible de récupérer l'image depuis Stash.`, + 'search.captureFail': `Échec de la capture de l'image : {error}`, + 'search.selectFaceImage': `Sélectionnez un visage dans l'image.`, + 'search.captureFrameFail': `Impossible de capturer l'image actuelle.`, + 'search.captureFrameFail2': `Échec de la capture de l'image actuelle.`, + 'search.selectFaceVideo': `Sélectionnez un visage dans la zone du lecteur vidéo.`, + 'search.menuItemTitle': `Tracez un cadre autour d'un visage ou appuyez sur Enter pour analyser toute l'image, afin de rechercher des correspondances dans StashDB`, + 'search.currentFrame': `Visage : Image actuelle`, + // ---- SceneScanButton.tsx ---- + 'scene.noSprite': `Aucune feuille de sprite ou vidéo d'aperçu trouvée pour cette scène. Générez-les dans les paramètres de la scène, puis réessayez.`, + 'scene.noFaces': `Aucun visage ou interprète trouvé dans la feuille de sprite ou la vidéo d'aperçu de cette scène.`, + 'scene.healthBanner': `L'API de reconnaissance faciale n'est pas joignable. Démarrez le backend et réessayez.`, + 'scene.failed': `Échec de l'analyse de la scène : {error}`, + 'scene.menuItemTitle': `Identifier chaque interprète de la scène (nécessite une feuille de sprite ou une vidéo d'aperçu générée)`, + 'scene.wholeScene': `Visage : Scène entière`, + // ---- BackendHealthBanner.tsx ---- + 'banner.changeBackend': `Changer de backend`, + 'banner.dismiss': `Ignorer`, + // ---- ErrorDialog.tsx ---- + 'error.dismiss': `Ignorer`, + // ---- FirstRunDialog.tsx ---- + 'firstRun.title': `Configurer votre backend Visage`, + 'firstRun.subtitle': `Visage envoie des images de visage à un backend pour reconnaissance. Choisissez où l'exécuter.`, + 'firstRun.cloud': `Utiliser le cloud Hugging Face`, + 'firstRun.cloudNote': `Aucune configuration requise. Les images sont envoyées au service cloud Hugging Face.`, + 'firstRun.local': `Utiliser mon propre serveur`, + 'firstRun.localNote': `Exécutez le binaire privé sur votre propre machine ou réseau.`, + 'firstRun.skip': `Ignorer pour l'instant`, + // ---- BackendBadge.tsx ---- + 'badge.local': `Local`, + 'badge.cloud': `Cloud (Hugging Face)`, + 'badge.title': `Backend Visage : {label}`, + // ---- DonateFooter.tsx ---- + 'donate.enjoying': `Visage vous plaît ? Aidez à le garder en vie`, + 'donate.supportPatreon': `Soutenir sur Patreon`, + // ---- FaceFrameSelector.tsx ---- + 'frame.close': `Fermer le sélecteur d'image`, + 'frame.seekFail': `Échec de la navigation dans le lecteur vidéo.`, + 'frame.selectAt': `Sélectionner le visage à {time}s`, + }; + + // Norwegian Nynorsk. + const nn = { + 'backendSettings.title': `Backend-innstillingar`, + 'backendSettings.closeAria': `Lukk innstillingar`, + 'backendSettings.backendAria': `Backend-innstillingar`, + 'backendSettings.changeBackend': `Byt backend`, + 'backendSettings.backendLabel': `Backend`, + 'backendSettings.local': `Lokal`, + 'backendSettings.cloud': `Cloud (Hugging Face)`, + 'backendSettings.cloudNote': `Bilete vert sende til Hugging Face sky-tenesta.`, + 'backendSettings.hintPrefix': `Vil du at bileta dine skal bli på nettverket ditt?`, + 'backendSettings.hintLink': `Køyr ein privat server via Patreon`, + 'backendSettings.urlLabel': `URL`, + 'backendSettings.csp1': "Sikkerheitspolitikken til nettlesaren (CSP) tillèt berre", + 'backendSettings.csp2': `som standard. For å nå ein lokal backend på ei anna adresse (t.d. LAN-IP-en din) må du leggje han til i`, + 'backendSettings.csp3': `lista i`, + 'backendSettings.csp4': `-fila i Stash-plugins-mappa di, elles vert førespurnadane blokkerte. Merk: oppdatering av Visage installerer`, + 'backendSettings.csp5': `på nytt, så dette må gjerast på nytt etter kvar oppdatering.`, + 'backendSettings.testing': `Testar tilkopling…`, + 'backendSettings.testConnection': `Test tilkopling`, + 'backendSettings.testingShort': `Testar…`, + 'backendSettings.cancel': `Avbryt`, + 'backendSettings.save': `Lagre`, + 'backendSettings.feedback.reachable': `Tilkopling vellukka. Backend er klar.`, + 'backendSettings.feedback.degraded': `Backend er tilgjengeleg, men redusert (modellar eller indeks er ikkje lasta inn).`, + 'backendSettings.feedback.unreachable': `Backend er utilgjengeleg. Sjekk URL-en og at backend køyrer.`, + 'faceMatch.title': `GJELDANDE RAME`, + 'faceMatch.close': `Lukk`, + 'faceMatch.facesSelected': `{faces} andlet funne · {selected} valt`, + 'faceMatch.inScene': `· {count} i scene`, + 'faceMatch.stashboxMissing': `Ingen stash-box konfigurert.`, + 'faceMatch.stashboxMissingBody': ` Legg til ein stash-box-leverandør under Innstillingar → Metadata-leverandørar for å aktivere performer-import.`, + 'faceMatch.stashboxWrongName': `Fann ingen "StashDB"-leverandør.`, + 'faceMatch.stashboxWrongNameBody': ` Performer-import krev ein leverandør som heiter "StashDB". Gje leverandøren nytt namn under Innstillingar → Metadata-leverandørar.`, + 'faceMatch.learnMore': `Les meir.`, + 'faceMatch.scanning': `Skannar • andletsattkjenning…`, + 'faceMatch.faceAlt': `Andlet {index}`, + 'faceMatch.minConf': `Min. konf.`, + 'faceMatch.minConfTitle': `Minimumssikkerheit: {percent}%`, + 'faceMatch.detected': `Funne`, + 'faceMatch.detectedFaceAlt': `Funnent andlet`, + 'faceMatch.vs': `vs`, + 'faceMatch.supportPatreon': `Støtt på Patreon`, + 'faceMatch.ofSelected': `{selected} av {total} valt`, + 'faceMatch.allInScene': `{total} andlet funne · alle i scene`, + 'faceMatch.clickToSelect': `{total} andlet funne · klikk for å velje`, + 'faceMatch.kbSwitch': `Bytt andlet`, + 'faceMatch.kbSelect': `Vel performarar`, + 'faceMatch.kbToggle': `Veksle markering`, + 'faceMatch.kbAddInstant': `Shift+klikk for å leggje til med ein gong`, + 'faceMatch.selectBest': `Vel beste treff`, + 'faceMatch.adding': `Legg til...`, + 'faceMatch.done': `Ferdig ({count})`, + 'faceMatch.toast.added': `La til performer i {target}.`, + 'faceMatch.toast.addError': `Kunne ikkje leggje til performer: {error}`, + 'faceMatch.toast.noStashbox': `Ingen stash-box konfigurert. Legg til ein stash-box-leverandør under Innstillingar → Metadata-leverandørar for å aktivere performer-import. Sjå {url}`, + 'faceMatch.toast.noProvider': `Fann ingen leverandør som heiter "StashDB". Gje leverandøren nytt namn til "StashDB" under Innstillingar → Metadata-leverandørar for å aktivere performer-import.`, + 'faceMatch.toast.configureProvider': `Konfigurer ein stash-box-leverandør under Innstillingar → Metadata-leverandørar for å aktivere performer-import.`, + 'faceMatch.toast.addedMultiple': `La til {count} performarar i {target}.`, + 'sprite.title': `SCENE-PERFORMARAR`, + 'sprite.close': `Lukk`, + 'sprite.foundConfirmed': `{found} funne · {confirmed} stadfesta`, + 'sprite.confidence': `sikkerheit`, + 'sprite.name': `namn`, + 'sprite.hits': `treff`, + 'sprite.minConf': `Min. konf.`, + 'sprite.minConfTitle': `Minimumssikkerheit: {percent}%`, + 'sprite.scanning': `Visage skannar…`, + 'sprite.cancel': `Avbryt`, + 'sprite.empty': `Ingen performarar identifisert i denne spriten.`, + 'sprite.detectedFaceAlt': `Funnent andlet`, + 'sprite.spriteLabel': `SPRITE`, + 'sprite.stashLabel': `STASH`, + 'sprite.vs': `vs`, + 'sprite.inScene': `I scene`, + 'sprite.hitsCount': `{count} treff`, + 'sprite.totalTime': `{time} totalt`, + 'sprite.alreadyInScene': `Allereie i scene`, + 'sprite.clickToConfirm': `Klikk for å stadfeste`, + 'sprite.confirmed': `Stadfesta`, + 'sprite.supportPatreon': `Støtt på Patreon`, + 'sprite.confirmedCount': `{confirmed} av {total} stadfesta`, + 'sprite.shownHint': `{shown} vist ({total} totalt) · klikk for å stadfeste · ←→ naviger · Enter stadfest`, + 'sprite.confirmHint': `Klikk for å stadfeste · ←→ naviger · Enter stadfest`, + 'sprite.adding': `Legg til...`, + 'sprite.done': `Ferdig ({count})`, + 'gender.male': `Mann`, + 'gender.female': `Kvinne`, + 'gender.transMale': `Transmann`, + 'gender.transFemale': `Transkvinne`, + 'gender.nonBinary': `Ikkje-binær`, + 'gender.intersex': `Intersex`, + 'card.excellent': `Utmerka treff`, + 'card.good': `Godt treff`, + 'card.uncertain': `Usikkert treff`, + 'card.select': `Vel {name}`, + 'card.deselect': `Fravel {name}`, + 'card.openOn': `Opne på {source}`, + 'search.overlayHint': `Dra for å velje eit andlet — Enter for å skanne heile rama — Esc for å avbryte`, + 'search.noFaces': `Ingen andlet funne i utvalet. Prøv ein tettare utskjering, eller trykk Enter for å skanne heile rama.`, + 'search.captureMediaFail': `Kunne ikkje fange mediet. Kontroller at scenen/biletet er fullasta.`, + 'search.healthBanner': `Andletsattkjenning-API-et er ikkje tilgjengeleg. Start backend og prøv igjen.`, + 'search.failed': `Andletsøk mislukkast: {error}`, + 'search.fetchImageFail': `Kunne ikkje hente bilete frå Stash.`, + 'search.captureFail': `Kunne ikkje fange bilete: {error}`, + 'search.selectFaceImage': `Vel eit andlet i biletet.`, + 'search.captureFrameFail': `Kunne ikkje fange gjeldande rame.`, + 'search.captureFrameFail2': `Innfanging av gjeldande rame mislukkast.`, + 'search.selectFaceVideo': `Vel eit andlet i videoavspelarområdet.`, + 'search.menuItemTitle': `Dra ein boks rundt eit andlet, eller trykk Enter for å skanne heile rama, for å søkje StashDB etter treff`, + 'search.currentFrame': `Visage: Gjeldande rame`, + 'scene.noSprite': `Fann ingen sprite-ark eller forhåndsvisingsvideo for denne scenen. Generer dei i Scene-innstillingane, og prøv igjen.`, + 'scene.noFaces': `Fann ingen andlet eller performarar i sprite-arket eller forhåndsvisingsvideoen til denne scenen.`, + 'scene.healthBanner': `Andletsattkjenning-API-et er ikkje tilgjengeleg. Start backend og prøv igjen.`, + 'scene.failed': `Scene-skanning mislukkast: {error}`, + 'scene.menuItemTitle': `Identifiser kvar performer i scenen (krev eit generert sprite-ark eller ein forhåndsvisingsvideo)`, + 'scene.wholeScene': `Visage: Heile scenen`, + 'banner.changeBackend': `Byt backend`, + 'banner.dismiss': `Avvis`, + 'error.dismiss': `Avvis`, + 'firstRun.title': `Set opp Visage-backend-en din`, + 'firstRun.subtitle': `Visage sender andletsbilete til ein backend for attkjenning. Vel kor han skal køyre.`, + 'firstRun.cloud': `Bruk Hugging Face cloud`, + 'firstRun.cloudNote': `Ingen oppsett. Bilete vert sende til Hugging Face sky-tenesta.`, + 'firstRun.local': `Bruk min eigen server`, + 'firstRun.localNote': `Køyr den private binæren på di eiga maskin eller ditt nettverk.`, + 'firstRun.skip': `Hopp over for no`, + 'badge.local': `Lokal`, + 'badge.cloud': `Cloud (Hugging Face)`, + 'badge.title': `Visage-backend: {label}`, + 'donate.enjoying': `Liker du Visage? Hjelp å halde det i live`, + 'donate.supportPatreon': `Støtt på Patreon`, + 'frame.close': `Lukk rammeveljaren`, + 'frame.seekFail': `Kunne ikkje søkje i videoavspelaren.`, + 'frame.selectAt': `Vel andletsrame ved {time}s`, + }; + + const ko = { + 'backendSettings.title': `백엔드 설정`, + 'backendSettings.closeAria': `설정 닫기`, + 'backendSettings.backendAria': `백엔드 설정`, + 'backendSettings.changeBackend': `백엔드 변경`, + 'backendSettings.backendLabel': `백엔드`, + 'backendSettings.local': `로컬`, + 'backendSettings.cloud': `클라우드(Hugging Face)`, + 'backendSettings.cloudNote': `이미지는 Hugging Face 클라우드 서비스로 전송됩니다.`, + 'backendSettings.hintPrefix': `이미지를 네트워크 내에 유지하고 싶으신가요?`, + 'backendSettings.hintLink': `Patreon을 통해 개인 서버 실행`, + 'backendSettings.urlLabel': `URL`, + 'backendSettings.csp1': `브라우저의 보안 정책(CSP)은 기본적으로`, + 'backendSettings.csp2': `만 허용합니다. 다른 주소(예: LAN IP)에 있는 로컬 백엔드에 연결하려면 해당 주소를`, + 'backendSettings.csp3': `의`, + 'backendSettings.csp4': `파일 목록에 추가하세요. 이 파일은 Stash 플러그인 폴더에 있습니다. 추가하지 않으면 요청이 차단됩니다. 참고: Visage를 업데이트하면`, + 'backendSettings.csp5': `이(가) 다시 설치되므로 업데이트할 때마다 다시 적용해야 합니다.`, + 'backendSettings.testing': `연결 테스트 중…`, + 'backendSettings.testConnection': `연결 테스트`, + 'backendSettings.testingShort': `테스트 중…`, + 'backendSettings.cancel': `취소`, + 'backendSettings.save': `저장`, + 'backendSettings.feedback.reachable': `연결에 성공했습니다. 백엔드가 준비되었습니다.`, + 'backendSettings.feedback.degraded': `백엔드에 연결할 수 있지만 성능이 저하되었습니다(모델 또는 인덱스가 로드되지 않음).`, + 'backendSettings.feedback.unreachable': `백엔드에 연결할 수 없습니다. URL을 확인하고 백엔드가 실행 중인지 확인하세요.`, + 'faceMatch.title': `현재 프레임`, + 'faceMatch.close': `닫기`, + 'faceMatch.facesSelected': `얼굴 {faces}개 발견 · {selected}개 선택됨`, + 'faceMatch.inScene': `· 장면 내 {count}`, + 'faceMatch.stashboxMissing': `stash-box가 설정되어 있지 않습니다.`, + 'faceMatch.stashboxMissingBody': ` 설정 → 메타데이터 공급자에서 stash-box 공급자를 추가하면 출연자 가져오기를 활성화할 수 있습니다.`, + 'faceMatch.stashboxWrongName': `"StashDB" 공급자를 찾을 수 없습니다.`, + 'faceMatch.stashboxWrongNameBody': ` 출연자 가져오기에는 "StashDB"라는 이름의 공급자가 필요합니다. 설정 → 메타데이터 공급자에서 공급자 이름을 변경하세요.`, + 'faceMatch.learnMore': `자세히 알아보기.`, + 'faceMatch.scanning': `스캔 중 · 얼굴 인식…`, + 'faceMatch.faceAlt': `얼굴 {index}`, + 'faceMatch.minConf': `최소 신뢰도`, + 'faceMatch.minConfTitle': `최소 신뢰도: {percent}%`, + 'faceMatch.detected': `감지됨`, + 'faceMatch.detectedFaceAlt': `감지된 얼굴`, + 'faceMatch.vs': `vs`, + 'faceMatch.supportPatreon': `Patreon에서 지원`, + 'faceMatch.ofSelected': `{total}개 중 {selected}개 선택됨`, + 'faceMatch.allInScene': `얼굴 {total}개 발견 · 모두 장면 내`, + 'faceMatch.clickToSelect': `얼굴 {total}개 발견 · 클릭하여 선택`, + 'faceMatch.kbSwitch': `얼굴 전환`, + 'faceMatch.kbSelect': `출연자 선택`, + 'faceMatch.kbToggle': `선택 전환`, + 'faceMatch.kbAddInstant': `Shift+클릭으로 즉시 추가`, + 'faceMatch.selectBest': `최적 일치 선택`, + 'faceMatch.adding': `추가 중...`, + 'faceMatch.done': `완료({count})`, + 'faceMatch.toast.added': `{target}에 출연자를 추가했습니다.`, + 'faceMatch.toast.addError': `출연자 추가 실패: {error}`, + 'faceMatch.toast.noStashbox': `stash-box가 설정되어 있지 않습니다. 설정 → 메타데이터 공급자에서 stash-box 공급자를 추가하면 출연자 가져오기를 활성화할 수 있습니다. 참조: {url}`, + 'faceMatch.toast.noProvider': `"StashDB"라는 이름의 공급자를 찾을 수 없습니다. 설정 → 메타데이터 공급자에서 공급자 이름을 "StashDB"로 변경하면 출연자 가져오기를 활성화할 수 있습니다.`, + 'faceMatch.toast.configureProvider': `설정 → 메타데이터 공급자에서 stash-box 공급자를 구성하면 출연자 가져오기를 활성화할 수 있습니다.`, + 'faceMatch.toast.addedMultiple': `{target}에 {count}명의 출연자{s}를 추가했습니다.`, + 'sprite.title': `장면 출연자`, + 'sprite.close': `닫기`, + 'sprite.foundConfirmed': `{found}개 발견 · {confirmed}개 확인됨`, + 'sprite.confidence': `신뢰도`, + 'sprite.name': `이름`, + 'sprite.hits': `조회수`, + 'sprite.minConf': `최소 신뢰도`, + 'sprite.minConfTitle': `최소 신뢰도: {percent}%`, + 'sprite.scanning': `Visage 스캔 중…`, + 'sprite.cancel': `취소`, + 'sprite.empty': `이 스프라이트에서 확인된 출연자가 없습니다.`, + 'sprite.detectedFaceAlt': `감지된 얼굴`, + 'sprite.spriteLabel': `스프라이트`, + 'sprite.stashLabel': `STASH`, + 'sprite.vs': `vs`, + 'sprite.inScene': `장면 내`, + 'sprite.hitsCount': `조회수 {count}회{s}`, + 'sprite.totalTime': `총 {time}`, + 'sprite.alreadyInScene': `이미 장면에 있음`, + 'sprite.clickToConfirm': `클릭하여 확인`, + 'sprite.confirmed': `확인됨`, + 'sprite.supportPatreon': `Patreon에서 지원`, + 'sprite.confirmedCount': `{total}개 중 {confirmed}개 확인됨`, + 'sprite.shownHint': `{shown}개 표시됨(총 {total}개) · 클릭하여 확인 · ←→ 탐색 · Enter 확인`, + 'sprite.confirmHint': `클릭하여 확인 · ←→ 탐색 · Enter 확인`, + 'sprite.adding': `추가 중...`, + 'sprite.done': `완료({count})`, + 'gender.male': `남성`, + 'gender.female': `여성`, + 'gender.transMale': `트랜스젠더 남성`, + 'gender.transFemale': `트랜스젠더 여성`, + 'gender.nonBinary': `논바이너리`, + 'gender.intersex': `간성`, + 'card.excellent': `최상의 일치`, + 'card.good': `좋은 일치`, + 'card.uncertain': `불확실한 일치`, + 'card.select': `{name} 선택`, + 'card.deselect': `{name} 선택 해제`, + 'card.openOn': `{source}에서 열기`, + 'search.overlayHint': `얼굴을 드래그하여 선택 — Enter로 전체 프레임 스캔 — Esc로 취소`, + 'search.noFaces': `해당 선택 영역에서 얼굴을 찾을 수 없습니다. 더 좁게 자르거나 Enter를 눌러 전체 프레임을 스캔하세요.`, + 'search.captureMediaFail': `미디어를 캡처할 수 없습니다. 장면/이미지가 완전히 로드되었는지 확인하세요.`, + 'search.healthBanner': `얼굴 인식 API에 연결할 수 없습니다. 백엔드를 시작한 후 다시 시도하세요.`, + 'search.failed': `얼굴 검색 실패: {error}`, + 'search.fetchImageFail': `Stash에서 이미지를 가져올 수 없습니다.`, + 'search.captureFail': `이미지 캡처 실패: {error}`, + 'search.selectFaceImage': `이미지 내에서 얼굴을 선택하세요.`, + 'search.captureFrameFail': `현재 프레임을 캡처할 수 없습니다.`, + 'search.captureFrameFail2': `현재 프레임 캡처에 실패했습니다.`, + 'search.selectFaceVideo': `비디오 플레이어 영역 내에서 얼굴을 선택하세요.`, + 'search.menuItemTitle': `얼굴 주위에 박스를 드래그하거나 Enter를 눌러 전체 프레임을 스캔하여 StashDB에서 일치 항목을 검색합니다`, + 'search.currentFrame': `Visage: 현재 프레임`, + 'scene.noSprite': `이 장면에는 스프라이트 시트나 미리보기 비디오가 없습니다. 장면 설정에서 생성한 후 다시 시도하세요.`, + 'scene.noFaces': `이 장면의 스프라이트 시트나 미리보기 비디오에서 얼굴이나 출연자를 찾을 수 없습니다.`, + 'scene.healthBanner': `얼굴 인식 API에 연결할 수 없습니다. 백엔드를 시작한 후 다시 시도하세요.`, + 'scene.failed': `장면 스캔 실패: {error}`, + 'scene.menuItemTitle': `장면의 모든 출연자를 식별합니다(생성된 스프라이트 시트 또는 미리보기 비디오 필요)`, + 'scene.wholeScene': `Visage: 전체 장면`, + 'banner.changeBackend': `백엔드 변경`, + 'banner.dismiss': `닫기`, + 'error.dismiss': `닫기`, + 'firstRun.title': `Visage 백엔드 설정`, + 'firstRun.subtitle': `Visage는 얼굴 이미지를 백엔드로 전송하여 인식합니다. 실행 위치를 선택하세요.`, + 'firstRun.cloud': `Hugging Face 클라우드 사용`, + 'firstRun.cloudNote': `설정이 필요 없습니다. 이미지는 Hugging Face 클라우드 서비스로 전송됩니다.`, + 'firstRun.local': `내 서버 사용`, + 'firstRun.localNote': `개인 바이너리를 자신의 머신 또는 네트워크에서 실행합니다.`, + 'firstRun.skip': `나중에 건너뛰기`, + 'badge.local': `로컬`, + 'badge.cloud': `클라우드(Hugging Face)`, + 'badge.title': `Visage 백엔드: {label}`, + 'donate.enjoying': `Visage가 마음에 드시나요? 유지에 도움을 주세요`, + 'donate.supportPatreon': `Patreon에서 지원`, + 'frame.close': `프레임 선택기 닫기`, + 'frame.seekFail': `비디오 플레이어를 탐색하지 못했습니다.`, + 'frame.selectAt': `{time}초에서 얼굴 프레임 선택`, + }; + + const zhCn = { + 'backendSettings.title': `后端设置`, + 'backendSettings.closeAria': `关闭设置`, + 'backendSettings.backendAria': `后端设置`, + 'backendSettings.changeBackend': `更换后端`, + 'backendSettings.backendLabel': `后端`, + 'backendSettings.local': `本地`, + 'backendSettings.cloud': `云端(Hugging Face)`, + 'backendSettings.cloudNote': `图像会被发送到 Hugging Face 云服务。`, + 'backendSettings.hintPrefix': `希望图像保留在你的网络中?`, + 'backendSettings.hintLink': `通过 Patreon 运行私有服务器`, + 'backendSettings.urlLabel': `URL`, + 'backendSettings.csp1': `浏览器的安全策略(CSP)仅允许`, + 'backendSettings.csp2': `默认情况下。要访问其他地址(例如你的局域网 IP)上的本地后端,请将其添加到`, + 'backendSettings.csp3': `中的`, + 'backendSettings.csp4': `文件的列表里,该文件位于你的 Stash 插件文件夹中,否则请求将被阻止。注意:更新 Visage 会重新安装`, + 'backendSettings.csp5': `,因此每次更新后都必须重新应用此设置。`, + 'backendSettings.testing': `正在测试连接…`, + 'backendSettings.testConnection': `测试连接`, + 'backendSettings.testingShort': `正在测试…`, + 'backendSettings.cancel': `取消`, + 'backendSettings.save': `保存`, + 'backendSettings.feedback.reachable': `连接成功。后端已就绪。`, + 'backendSettings.feedback.degraded': `后端可访问但已降级(模型或索引未加载)。`, + 'backendSettings.feedback.unreachable': `后端不可访问。请检查 URL 并确认后端正在运行。`, + 'faceMatch.title': `当前帧`, + 'faceMatch.close': `关闭`, + 'faceMatch.facesSelected': `发现 {faces} 张人脸 · 已选择 {selected}`, + 'faceMatch.inScene': `· {count} 在场景中`, + 'faceMatch.stashboxMissing': `未配置 stash-box。`, + 'faceMatch.stashboxMissingBody': ` 请在设置 → 元数据提供程序中添加 stash-box 提供程序,以启用表演者导入。`, + 'faceMatch.stashboxWrongName': `未找到 "StashDB" 提供程序。`, + 'faceMatch.stashboxWrongNameBody': ` 表演者导入需要一个名为 "StashDB" 的提供程序。请在设置 → 元数据提供程序中重命名你的提供程序。`, + 'faceMatch.learnMore': `了解更多。`, + 'faceMatch.scanning': `正在扫描 · 人脸识别…`, + 'faceMatch.faceAlt': `人脸 {index}`, + 'faceMatch.minConf': `最小置信度`, + 'faceMatch.minConfTitle': `最低置信度:{percent}%`, + 'faceMatch.detected': `已检测到`, + 'faceMatch.detectedFaceAlt': `检测到的人脸`, + 'faceMatch.vs': `vs`, + 'faceMatch.supportPatreon': `在 Patreon 上支持`, + 'faceMatch.ofSelected': `已选择 {total} 中的 {selected}`, + 'faceMatch.allInScene': `发现 {total} 张人脸 · 全部在场景中`, + 'faceMatch.clickToSelect': `发现 {total} 张人脸 · 点击以选择`, + 'faceMatch.kbSwitch': `切换人脸`, + 'faceMatch.kbSelect': `选择表演者`, + 'faceMatch.kbToggle': `切换选择`, + 'faceMatch.kbAddInstant': `Shift+点击立即添加`, + 'faceMatch.selectBest': `选择最佳匹配`, + 'faceMatch.adding': `正在添加...`, + 'faceMatch.done': `完成({count})`, + 'faceMatch.toast.added': `已将表演者添加到 {target}。`, + 'faceMatch.toast.addError': `添加表演者失败:{error}`, + 'faceMatch.toast.noStashbox': `未配置 stash-box。请在设置 → 元数据提供程序中添加 stash-box 提供程序以启用表演者导入。参见 {url}`, + 'faceMatch.toast.noProvider': `未找到名为 "StashDB" 的提供程序。请在设置 → 元数据提供程序中重命名你的提供程序,使其为 "StashDB",以启用表演者导入。`, + 'faceMatch.toast.configureProvider': `请在设置 → 元数据提供程序中配置 stash-box 提供程序,以启用表演者导入。`, + 'faceMatch.toast.addedMultiple': `已将 {count} 位表演者{s}添加到 {target}。`, + 'sprite.title': `场景表演者`, + 'sprite.close': `关闭`, + 'sprite.foundConfirmed': `找到 {found} · 已确认 {confirmed}`, + 'sprite.confidence': `置信度`, + 'sprite.name': `姓名`, + 'sprite.hits': `命中`, + 'sprite.minConf': `最小置信度`, + 'sprite.minConfTitle': `最低置信度:{percent}%`, + 'sprite.scanning': `Visage 正在扫描…`, + 'sprite.cancel': `取消`, + 'sprite.empty': `此精灵图中未识别出任何表演者。`, + 'sprite.detectedFaceAlt': `检测到的人脸`, + 'sprite.spriteLabel': `精灵图`, + 'sprite.stashLabel': `STASH`, + 'sprite.vs': `vs`, + 'sprite.inScene': `在场景中`, + 'sprite.hitsCount': `{count} 次命中{s}`, + 'sprite.totalTime': `共 {time}`, + 'sprite.alreadyInScene': `已在场景中`, + 'sprite.clickToConfirm': `点击以确认`, + 'sprite.confirmed': `已确认`, + 'sprite.supportPatreon': `在 Patreon 上支持`, + 'sprite.confirmedCount': `已确认 {total} 中的 {confirmed}`, + 'sprite.shownHint': `显示 {shown}(共 {total} 个)· 点击以确认 · ←→ 导航 · Enter 确认`, + 'sprite.confirmHint': `点击以确认 · ←→ 导航 · Enter 确认`, + 'sprite.adding': `正在添加...`, + 'sprite.done': `完成({count})`, + 'gender.male': `男性`, + 'gender.female': `女性`, + 'gender.transMale': `跨性别男性`, + 'gender.transFemale': `跨性别女性`, + 'gender.nonBinary': `非二元性别`, + 'gender.intersex': `双性人`, + 'card.excellent': `极佳匹配`, + 'card.good': `良好匹配`, + 'card.uncertain': `不确定匹配`, + 'card.select': `选择 {name}`, + 'card.deselect': `取消选择 {name}`, + 'card.openOn': `在 {source} 上打开`, + 'search.overlayHint': `拖动以选择人脸 — Enter 扫描整个画面 — Esc 取消`, + 'search.noFaces': `该选择中未找到人脸。请尝试更紧的裁剪,或按 Enter 扫描整个画面。`, + 'search.captureMediaFail': `无法捕获媒体。请确保场景/图像已完全加载。`, + 'search.healthBanner': `人脸识别 API 不可访问。请启动后端并重试。`, + 'search.failed': `人脸搜索失败:{error}`, + 'search.fetchImageFail': `无法从 Stash 获取图像。`, + 'search.captureFail': `捕获图像失败:{error}`, + 'search.selectFaceImage': `在图像中选择一张人脸。`, + 'search.captureFrameFail': `无法捕获当前帧。`, + 'search.captureFrameFail2': `捕获当前帧失败。`, + 'search.selectFaceVideo': `在视频播放器区域内选择一张人脸。`, + 'search.menuItemTitle': `拖动框选一张人脸,或按 Enter 扫描整个画面,以在 StashDB 中搜索匹配项`, + 'search.currentFrame': `Visage:当前帧`, + 'scene.noSprite': `此场景没有精灵图或预览视频。请在场景设置中生成它们,然后重试。`, + 'scene.noFaces': `在此场景的精灵图或预览视频中未找到人脸或表演者。`, + 'scene.healthBanner': `人脸识别 API 不可访问。请启动后端并重试。`, + 'scene.failed': `场景扫描失败:{error}`, + 'scene.menuItemTitle': `识别场景中的每个表演者(需要生成的精灵图或预览视频)`, + 'scene.wholeScene': `Visage:整个场景`, + 'banner.changeBackend': `更换后端`, + 'banner.dismiss': `关闭`, + 'error.dismiss': `关闭`, + 'firstRun.title': `设置你的 Visage 后端`, + 'firstRun.subtitle': `Visage 会将人脸图像发送到后端进行识别。选择在哪里运行它。`, + 'firstRun.cloud': `使用 Hugging Face 云端`, + 'firstRun.cloudNote': `零设置。图像会被发送到 Hugging Face 云服务。`, + 'firstRun.local': `使用我自己的服务器`, + 'firstRun.localNote': `在你的电脑或网络上运行私有二进制文件。`, + 'firstRun.skip': `暂时跳过`, + 'badge.local': `本地`, + 'badge.cloud': `云端(Hugging Face)`, + 'badge.title': `Visage 后端:{label}`, + 'donate.enjoying': `喜欢 Visage?帮助它保持运行`, + 'donate.supportPatreon': `在 Patreon 上支持`, + 'frame.close': `关闭帧选择器`, + 'frame.seekFail': `无法快进视频播放器。`, + 'frame.selectAt': `选择 {time} 秒处的人脸帧`, + }; + + const bg = { + 'backendSettings.title': `Настройки на бекенда`, + 'backendSettings.closeAria': `Затваряне на настройките`, + 'backendSettings.backendAria': `Настройки на бекенда`, + 'backendSettings.changeBackend': `Смяна на бекенда`, + 'backendSettings.backendLabel': `Бекенд`, + 'backendSettings.local': `Локален`, + 'backendSettings.cloud': `Облак (Hugging Face)`, + 'backendSettings.cloudNote': `Изображенията се изпращат към облачната услуга Hugging Face.`, + 'backendSettings.hintPrefix': `Искате ли изображенията ви да останат във вашата мрежа?`, + 'backendSettings.hintLink': `Стартирайте частен сървър чрез Patreon`, + 'backendSettings.urlLabel': `URL`, + 'backendSettings.csp1': `Политиката за сигурност на браузъра (CSP) по подразбиране позволява само`, + 'backendSettings.csp2': `. За да достигнете до локален бекенд на друг адрес (напр. IP на вашата LAN), добавете го към`, + 'backendSettings.csp3': `списъка в`, + 'backendSettings.csp4': `файла в папката за плъгини на Stash, в противен случай заявките ще бъдат блокирани. Забележка: актуализирането на Visage преинсталира`, + 'backendSettings.csp5': `, така че това трябва да се приложи отново след всяка актуализация.`, + 'backendSettings.testing': `Тестване на връзката…`, + 'backendSettings.testConnection': `Тествай връзката`, + 'backendSettings.testingShort': `Тестване…`, + 'backendSettings.cancel': `Отказ`, + 'backendSettings.save': `Запазване`, + 'backendSettings.feedback.reachable': `Връзката е успешна. Бекендът е готов.`, + 'backendSettings.feedback.degraded': `Бекендът е достъпен, но деградиран (моделите или индексът не са заредени).`, + 'backendSettings.feedback.unreachable': `Бекендът е недостъпен. Проверете URL адреса и се уверете, че бекендът работи.`, + 'faceMatch.title': `ТЕКУЩ КАДЪР`, + 'faceMatch.close': `Затваряне`, + 'faceMatch.facesSelected': `Намерени лица: {faces} · избрани: {selected}`, + 'faceMatch.inScene': `· {count} в сцената`, + 'faceMatch.stashboxMissing': `Не е конфигуриран stash-box.`, + 'faceMatch.stashboxMissingBody': ` Добавете доставчик на stash-box в Settings → Metadata Providers, за да активирате импортирането на изпълнители.`, + 'faceMatch.stashboxWrongName': `Не е намерен доставчик "StashDB".`, + 'faceMatch.stashboxWrongNameBody': ` Импортирането на изпълнители изисква доставчик с име "StashDB". Преименувайте доставчика в Settings → Metadata Providers.`, + 'faceMatch.learnMore': `Научете повече.`, + 'faceMatch.scanning': `Сканиране • разпознаване на лица…`, + 'faceMatch.faceAlt': `Лице {index}`, + 'faceMatch.minConf': `Мин. ув.`, + 'faceMatch.minConfTitle': `Минимална увереност: {percent}%`, + 'faceMatch.detected': `Открито`, + 'faceMatch.detectedFaceAlt': `Открито лице`, + 'faceMatch.vs': `vs`, + 'faceMatch.supportPatreon': `Подкрепа в Patreon`, + 'faceMatch.ofSelected': `Избрани {selected} от {total}`, + 'faceMatch.allInScene': `Намерени лица: {total} · всички в сцената`, + 'faceMatch.clickToSelect': `Намерени лица: {total} · щракнете, за да изберете`, + 'faceMatch.kbSwitch': `Превключване на лица`, + 'faceMatch.kbSelect': `Избор на изпълнители`, + 'faceMatch.kbToggle': `Превключване на избора`, + 'faceMatch.kbAddInstant': `Shift+щракване за незабавно добавяне`, + 'faceMatch.selectBest': `Изберете най-добрите съвпадения`, + 'faceMatch.adding': `Добавяне...`, + 'faceMatch.done': `Готово ({count})`, + 'faceMatch.toast.added': `Изпълнителят е добавен към {target}.`, + 'faceMatch.toast.addError': `Неуспешно добавяне на изпълнител: {error}`, + 'faceMatch.toast.noStashbox': `Не е конфигуриран stash-box. Добавете доставчик на stash-box в Settings → Metadata Providers, за да активирате импортирането на изпълнители. Вижте {url}`, + 'faceMatch.toast.noProvider': `Не е намерен доставчик "StashDB". Преименувайте доставчика на "StashDB" в Settings → Metadata Providers, за да активирате импортирането на изпълнители.`, + 'faceMatch.toast.configureProvider': `Конфигурирайте доставчик на stash-box в Settings → Metadata Providers, за да активирате импортирането на изпълнители.`, + 'faceMatch.toast.addedMultiple': `Добавени изпълнители: {count} към {target}.`, + 'sprite.title': `ИЗПЪЛНИТЕЛИ НА СЦЕНАТА`, + 'sprite.close': `Затвори`, + 'sprite.foundConfirmed': `Намерени: {found} · потвърдени: {confirmed}`, + 'sprite.confidence': `увереност`, + 'sprite.name': `име`, + 'sprite.hits': `съвпадения`, + 'sprite.minConf': `Мин. ув.`, + 'sprite.minConfTitle': `Минимална увереност: {percent}%`, + 'sprite.scanning': `Сканиране на Visage…`, + 'sprite.cancel': `Отказ`, + 'sprite.empty': `В този спрайт не са идентифицирани изпълнители.`, + 'sprite.detectedFaceAlt': `Открито лице`, + 'sprite.spriteLabel': `СПРАЙТ`, + 'sprite.stashLabel': `STASH`, + 'sprite.vs': `vs`, + 'sprite.inScene': `В сцената`, + 'sprite.hitsCount': `Съвпадения: {count}`, + 'sprite.totalTime': `Общо {time}`, + 'sprite.alreadyInScene': `Вече в сцената`, + 'sprite.clickToConfirm': `Щракнете, за да потвърдите`, + 'sprite.confirmed': `Потвърдено`, + 'sprite.supportPatreon': `Подкрепа в Patreon`, + 'sprite.confirmedCount': `Потвърдени {confirmed} от {total}`, + 'sprite.shownHint': `Показани: {shown} (общо {total}) · щракнете за потвърждение · ←→ навигация · Enter потвърждение`, + 'sprite.confirmHint': `Щракнете за потвърждение · ←→ навигация · Enter потвърждение`, + 'sprite.adding': `Добавяне...`, + 'sprite.done': `Готово ({count})`, + 'gender.male': `Мъж`, + 'gender.female': `Жена`, + 'gender.transMale': `Трансджендър мъж`, + 'gender.transFemale': `Трансджендър жена`, + 'gender.nonBinary': `Небинарен`, + 'gender.intersex': `Интерсекс`, + 'card.excellent': `Отлично съвпадение`, + 'card.good': `Добро съвпадение`, + 'card.uncertain': `Несигурно съвпадение`, + 'card.select': `Изберете {name}`, + 'card.deselect': `Отмяна на избора на {name}`, + 'card.openOn': `Отваряне в {source}`, + 'search.overlayHint': `Плъзнете, за да изберете лице — Enter, за да сканирате целия кадър — Esc, за да отмените`, + 'search.noFaces': `В избраната област не са открити лица. Опитайте по-плътно изрязване или натиснете Enter, за да сканирате целия кадър.`, + 'search.captureMediaFail': `Неуспешно заснемане на медията. Уверете се, че сцената/изображението е напълно заредено.`, + 'search.healthBanner': `API за разпознаване на лица е недостъпен. Стартирайте бекенда и опитайте отново.`, + 'search.failed': `Търсенето на лице се провали: {error}`, + 'search.fetchImageFail': `Неуспешно извличане на изображение от Stash.`, + 'search.captureFail': `Неуспешно заснемане на изображение: {error}`, + 'search.selectFaceImage': `Изберете лице в изображението.`, + 'search.captureFrameFail': `Неуспешно заснемане на текущия кадър.`, + 'search.captureFrameFail2': `Грешка при заснемане на текущия кадър.`, + 'search.selectFaceVideo': `Изберете лице в областта на видеоплейъра.`, + 'search.menuItemTitle': `Плъзнете рамка около лице или натиснете Enter, за да сканирате целия кадър, за да потърсите съвпадения в StashDB`, + 'search.currentFrame': `Visage: текущ кадър`, + 'scene.noSprite': `За тази сцена не е намерен нито спрайт лист, нито превю видео. Генерирайте ги в настройките на сцената и опитайте отново.`, + 'scene.noFaces': `В спрайт листа или превю видеото на тази сцена не са открити лица или изпълнители.`, + 'scene.healthBanner': `API за разпознаване на лица е недостъпен. Стартирайте бекенда и опитайте отново.`, + 'scene.failed': `Сканирането на сцената се провали: {error}`, + 'scene.menuItemTitle': `Идентифицирайте всеки изпълнител в сцената (изисква генериран спрайт лист или превю видео)`, + 'scene.wholeScene': `Visage: цялата сцена`, + 'banner.changeBackend': `Смяна на бекенда`, + 'banner.dismiss': `Отхвърляне`, + 'error.dismiss': `Отхвърляне`, + 'firstRun.title': `Настройте бекенда на Visage`, + 'firstRun.subtitle': `Visage изпраща изображения на лица към бекенда за разпознаване. Изберете къде да го стартирате.`, + 'firstRun.cloud': `Използване на облака Hugging Face`, + 'firstRun.cloudNote': `Без настройка. Изображенията се изпращат към облачната услуга Hugging Face.`, + 'firstRun.local': `Използване на собствен сървър`, + 'firstRun.localNote': `Стартирайте частния двоичен файл на вашата машина или в мрежата.`, + 'firstRun.skip': `Пропусни засега`, + 'badge.local': `Локален`, + 'badge.cloud': `Облак (Hugging Face)`, + 'badge.title': `Бекенд на Visage: {label}`, + 'donate.enjoying': `Харесвате ли Visage? Помогнете да остане жив`, + 'donate.supportPatreon': `Подкрепа в Patreon`, + 'frame.close': `Затваряне на избора на кадър`, + 'frame.seekFail': `Неуспешно превъртане на видеоплейъра.`, + 'frame.selectAt': `Изберете кадър с лице на {time} s`, + }; + + const ru = { + 'backendSettings.title': `Настройки бэкенда`, + 'backendSettings.closeAria': `Закрыть настройки`, + 'backendSettings.backendAria': `Настройки бэкенда`, + 'backendSettings.changeBackend': `Сменить бэкенд`, + 'backendSettings.backendLabel': `Бэкенд`, + 'backendSettings.local': `Локальный`, + 'backendSettings.cloud': `Облако (Hugging Face)`, + 'backendSettings.cloudNote': `Изображения отправляются в облачный сервис Hugging Face.`, + 'backendSettings.hintPrefix': `Хотите, чтобы изображения оставались в вашей сети?`, + 'backendSettings.hintLink': `Запустите частный сервер через Patreon`, + 'backendSettings.urlLabel': `URL`, + 'backendSettings.csp1': `Политика безопасности браузера (CSP) по умолчанию разрешает только`, + 'backendSettings.csp2': `. Чтобы получить доступ к локальному бэкенду по другому адресу (например, IP вашей локальной сети), добавьте его в`, + 'backendSettings.csp3': `список в`, + 'backendSettings.csp4': `файле в папке плагинов Stash, иначе запросы будут заблокированы. Примечание: обновление Visage переустанавливает`, + 'backendSettings.csp5': `, поэтому это нужно повторять после каждого обновления.`, + 'backendSettings.testing': `Проверка подключения…`, + 'backendSettings.testConnection': `Проверить подключение`, + 'backendSettings.testingShort': `Проверка…`, + 'backendSettings.cancel': `Отмена`, + 'backendSettings.save': `Сохранить`, + 'backendSettings.feedback.reachable': `Подключение успешно. Бэкенд готов.`, + 'backendSettings.feedback.degraded': `Бэкенд доступен, но работает с ограничениями (модели или индекс не загружены).`, + 'backendSettings.feedback.unreachable': `Бэкенд недоступен. Проверьте URL и убедитесь, что бэкенд запущен.`, + 'faceMatch.title': `ТЕКУЩИЙ КАДР`, + 'faceMatch.close': `Закрыть`, + 'faceMatch.facesSelected': `Найдено лиц: {faces} · выбрано: {selected}`, + 'faceMatch.inScene': `· {count} в сцене`, + 'faceMatch.stashboxMissing': `Stash-box не настроен.`, + 'faceMatch.stashboxMissingBody': ` Добавьте провайдера stash-box в Settings → Metadata Providers, чтобы включить импорт исполнителей.`, + 'faceMatch.stashboxWrongName': `Провайдер "StashDB" не найден.`, + 'faceMatch.stashboxWrongNameBody': ` Импорт исполнителей требует провайдера с именем "StashDB". Переименуйте провайдера в Settings → Metadata Providers.`, + 'faceMatch.learnMore': `Подробнее.`, + 'faceMatch.scanning': `Сканирование • распознавание лиц…`, + 'faceMatch.faceAlt': `Лицо {index}`, + 'faceMatch.minConf': `Мин. дост.`, + 'faceMatch.minConfTitle': `Минимальная достоверность: {percent}%`, + 'faceMatch.detected': `Обнаружено`, + 'faceMatch.detectedFaceAlt': `Обнаруженное лицо`, + 'faceMatch.vs': `vs`, + 'faceMatch.supportPatreon': `Поддержать на Patreon`, + 'faceMatch.ofSelected': `Выбрано {selected} из {total}`, + 'faceMatch.allInScene': `Найдено лиц: {total} · все в сцене`, + 'faceMatch.clickToSelect': `Найдено лиц: {total} · нажмите, чтобы выбрать`, + 'faceMatch.kbSwitch': `Переключение лиц`, + 'faceMatch.kbSelect': `Выбор исполнителей`, + 'faceMatch.kbToggle': `Переключить выбор`, + 'faceMatch.kbAddInstant': `Shift+клик, чтобы добавить сразу`, + 'faceMatch.selectBest': `Выбрать лучшие совпадения`, + 'faceMatch.adding': `Добавление...`, + 'faceMatch.done': `Готово ({count})`, + 'faceMatch.toast.added': `Исполнитель добавлен в {target}.`, + 'faceMatch.toast.addError': `Не удалось добавить исполнителя: {error}`, + 'faceMatch.toast.noStashbox': `Stash-box не настроен. Добавьте провайдера stash-box в Settings → Metadata Providers, чтобы включить импорт исполнителей. См. {url}`, + 'faceMatch.toast.noProvider': `Провайдер "StashDB" не найден. Переименуйте провайдера в "StashDB" в Settings → Metadata Providers, чтобы включить импорт исполнителей.`, + 'faceMatch.toast.configureProvider': `Настройте провайдера stash-box в Settings → Metadata Providers, чтобы включить импорт исполнителей.`, + 'faceMatch.toast.addedMultiple': `Добавлено исполнителей: {count} в {target}.`, + 'sprite.title': `ИСПОЛНИТЕЛИ СЦЕНЫ`, + 'sprite.close': `Закрыть`, + 'sprite.foundConfirmed': `Найдено: {found} · подтверждено: {confirmed}`, + 'sprite.confidence': `достоверность`, + 'sprite.name': `имя`, + 'sprite.hits': `совпадения`, + 'sprite.minConf': `Мин. дост.`, + 'sprite.minConfTitle': `Минимальная достоверность: {percent}%`, + 'sprite.scanning': `Сканирование Visage…`, + 'sprite.cancel': `Отмена`, + 'sprite.empty': `В этом спрайте исполнители не обнаружены.`, + 'sprite.detectedFaceAlt': `Обнаруженное лицо`, + 'sprite.spriteLabel': `СПРАЙТ`, + 'sprite.stashLabel': `STASH`, + 'sprite.vs': `vs`, + 'sprite.inScene': `В сцене`, + 'sprite.hitsCount': `Совпадений: {count}`, + 'sprite.totalTime': `Всего {time}`, + 'sprite.alreadyInScene': `Уже в сцене`, + 'sprite.clickToConfirm': `Нажмите для подтверждения`, + 'sprite.confirmed': `Подтверждено`, + 'sprite.supportPatreon': `Поддержать на Patreon`, + 'sprite.confirmedCount': `Подтверждено {confirmed} из {total}`, + 'sprite.shownHint': `Показано: {shown} (всего {total}) · нажмите для подтверждения · ←→ навигация · Enter подтверждение`, + 'sprite.confirmHint': `Нажмите для подтверждения · ←→ навигация · Enter подтверждение`, + 'sprite.adding': `Добавление...`, + 'sprite.done': `Готово ({count})`, + 'gender.male': `Мужчина`, + 'gender.female': `Женщина`, + 'gender.transMale': `Трансгендерный мужчина`, + 'gender.transFemale': `Трансгендерная женщина`, + 'gender.nonBinary': `Небинарный`, + 'gender.intersex': `Интерсекс`, + 'card.excellent': `Отличное совпадение`, + 'card.good': `Хорошее совпадение`, + 'card.uncertain': `Неуверенное совпадение`, + 'card.select': `Выбрать {name}`, + 'card.deselect': `Снять выбор с {name}`, + 'card.openOn': `Открыть на {source}`, + 'search.overlayHint': `Перетащите, чтобы выделить лицо — Enter, чтобы просканировать весь кадр — Esc, чтобы отменить`, + 'search.noFaces': `В выбранной области лица не найдены. Попробуйте более плотный кадр или нажмите Enter, чтобы просканировать весь кадр.`, + 'search.captureMediaFail': `Не удалось захватить медиа. Убедитесь, что сцена/изображение полностью загружены.`, + 'search.healthBanner': `API распознавания лиц недоступен. Запустите бэкенд и попробуйте снова.`, + 'search.failed': `Поиск лиц не удался: {error}`, + 'search.fetchImageFail': `Не удалось получить изображение из Stash.`, + 'search.captureFail': `Не удалось захватить изображение: {error}`, + 'search.selectFaceImage': `Выделите лицо в изображении.`, + 'search.captureFrameFail': `Не удалось захватить текущий кадр.`, + 'search.captureFrameFail2': `Ошибка при захвате текущего кадра.`, + 'search.selectFaceVideo': `Выделите лицо в области видеоплеера.`, + 'search.menuItemTitle': `Перетащите рамку вокруг лица или нажмите Enter, чтобы просканировать весь кадр и выполнить поиск совпадений в StashDB`, + 'search.currentFrame': `Visage: текущий кадр`, + 'scene.noSprite': `Для этой сцены не найдено ни спрайт-листа, ни превью-видео. Создайте их в настройках сцены и попробуйте снова.`, + 'scene.noFaces': `В спрайт-листе или превью-видео этой сцены не найдено лиц или исполнителей.`, + 'scene.healthBanner': `API распознавания лиц недоступен. Запустите бэкенд и попробуйте снова.`, + 'scene.failed': `Сканирование сцены не удалось: {error}`, + 'scene.menuItemTitle': `Определите каждого исполнителя в сцене (требуется сгенерированный спрайт-лист или превью-видео)`, + 'scene.wholeScene': `Visage: вся сцена`, + 'banner.changeBackend': `Сменить бэкенд`, + 'banner.dismiss': `Отклонить`, + 'error.dismiss': `Отклонить`, + 'firstRun.title': `Настройте бэкенд Visage`, + 'firstRun.subtitle': `Visage отправляет изображения лиц в бэкенд для распознавания. Выберите, где его запустить.`, + 'firstRun.cloud': `Использовать облако Hugging Face`, + 'firstRun.cloudNote': `Без настройки. Изображения отправляются в облачный сервис Hugging Face.`, + 'firstRun.local': `Использовать свой сервер`, + 'firstRun.localNote': `Запустите частный бинарник на своей машине или в своей сети.`, + 'firstRun.skip': `Пропустить`, + 'badge.local': `Локальный`, + 'badge.cloud': `Облако (Hugging Face)`, + 'badge.title': `Бэкенд Visage: {label}`, + 'donate.enjoying': `Нравится Visage? Помогите сохранить его`, + 'donate.supportPatreon': `Поддержать на Patreon`, + 'frame.close': `Закрыть выбор кадра`, + 'frame.seekFail': `Не удалось перемотать видеоплеер.`, + 'frame.selectAt': `Выбрать кадр лица на {time} с`, + }; + + const id = { + 'backendSettings.title': `Pengaturan backend`, + 'backendSettings.closeAria': `Tutup pengaturan`, + 'backendSettings.backendAria': `Pengaturan backend`, + 'backendSettings.changeBackend': `Ganti backend`, + 'backendSettings.backendLabel': `Backend`, + 'backendSettings.local': `Lokal`, + 'backendSettings.cloud': `Cloud (Hugging Face)`, + 'backendSettings.cloudNote': `Gambar dikirim ke layanan cloud Hugging Face.`, + 'backendSettings.hintPrefix': `Ingin gambar tetap berada di jaringan Anda?`, + 'backendSettings.hintLink': `Jalankan server pribadi melalui Patreon`, + 'backendSettings.urlLabel': `URL`, + 'backendSettings.csp1': `Kebijakan keamanan browser (CSP) hanya mengizinkan`, + 'backendSettings.csp2': `secara default. Untuk menjangkau backend lokal di alamat lain (mis. IP LAN Anda), tambahkan ke`, + 'backendSettings.csp3': `di dalam`, + 'backendSettings.csp4': `di dalam folder plugin Stash Anda, jika tidak, permintaan akan diblokir. Catatan: memperbarui Visage akan menginstal ulang`, + 'backendSettings.csp5': `, jadi ini harus diterapkan ulang setelah setiap pembaruan.`, + 'backendSettings.testing': `Menguji koneksi…`, + 'backendSettings.testConnection': `Uji koneksi`, + 'backendSettings.testingShort': `Menguji…`, + 'backendSettings.cancel': `Batal`, + 'backendSettings.save': `Simpan`, + 'backendSettings.feedback.reachable': `Koneksi berhasil. Backend siap.`, + 'backendSettings.feedback.degraded': `Backend terjangkau tetapi menurun (model atau indeks tidak dimuat).`, + 'backendSettings.feedback.unreachable': `Backend tidak terjangkau. Periksa URL dan pastikan backend berjalan.`, + 'faceMatch.title': `BINGKAI SAAT INI`, + 'faceMatch.close': `Tutup`, + 'faceMatch.facesSelected': `{faces} wajah ditemukan · {selected} dipilih`, + 'faceMatch.inScene': `· {count} dalam scene`, + 'faceMatch.stashboxMissing': `Tidak ada stash-box yang dikonfigurasi.`, + 'faceMatch.stashboxMissingBody': ` Tambahkan penyedia stash-box di Pengaturan → Penyedia Metadata untuk mengaktifkan impor pemain.`, + 'faceMatch.stashboxWrongName': `Tidak ditemukan penyedia "StashDB".`, + 'faceMatch.stashboxWrongNameBody': ` Impor pemain memerlukan penyedia bernama "StashDB". Ganti nama penyedia Anda di Pengaturan → Penyedia Metadata.`, + 'faceMatch.learnMore': `Pelajari lebih lanjut.`, + 'faceMatch.scanning': `Memindai • pengenalan wajah…`, + 'faceMatch.faceAlt': `Wajah {index}`, + 'faceMatch.minConf': `Kepercayaan min.`, + 'faceMatch.minConfTitle': `Kepercayaan minimum: {percent}%`, + 'faceMatch.detected': `Terdeteksi`, + 'faceMatch.detectedFaceAlt': `Wajah terdeteksi`, + 'faceMatch.vs': `vs`, + 'faceMatch.supportPatreon': `Dukung di Patreon`, + 'faceMatch.ofSelected': `{selected} dari {total} dipilih`, + 'faceMatch.allInScene': `{total} wajah ditemukan · semua dalam scene`, + 'faceMatch.clickToSelect': `{total} wajah ditemukan · klik untuk memilih`, + 'faceMatch.kbSwitch': `Ganti wajah`, + 'faceMatch.kbSelect': `Pilih pemain`, + 'faceMatch.kbToggle': `Alihkan pemilihan`, + 'faceMatch.kbAddInstant': `Shift+klik untuk menambah langsung`, + 'faceMatch.selectBest': `Pilih Kecocokan Terbaik`, + 'faceMatch.adding': `Menambahkan...`, + 'faceMatch.done': `Selesai ({count})`, + 'faceMatch.toast.added': `Pemain ditambahkan ke {target}.`, + 'faceMatch.toast.addError': `Gagal menambah pemain: {error}`, + 'faceMatch.toast.noStashbox': `Tidak ada stash-box yang dikonfigurasi. Tambahkan penyedia stash-box di Pengaturan → Penyedia Metadata untuk mengaktifkan impor pemain. Lihat {url}`, + 'faceMatch.toast.noProvider': `Tidak ditemukan penyedia bernama "StashDB". Ganti nama penyedia Anda menjadi "StashDB" di Pengaturan → Penyedia Metadata untuk mengaktifkan impor pemain.`, + 'faceMatch.toast.configureProvider': `Konfigurasi penyedia stash-box di Pengaturan → Penyedia Metadata untuk mengaktifkan impor pemain.`, + 'faceMatch.toast.addedMultiple': `{count} pemain{s} ditambahkan ke {target}.`, + 'sprite.title': `PEMERAN SCENE`, + 'sprite.close': `Tutup`, + 'sprite.foundConfirmed': `{found} ditemukan · {confirmed} dikonfirmasi`, + 'sprite.confidence': `kepercayaan`, + 'sprite.name': `nama`, + 'sprite.hits': `hit`, + 'sprite.minConf': `Kepercayaan min.`, + 'sprite.minConfTitle': `Kepercayaan minimum: {percent}%`, + 'sprite.scanning': `Visage Memindai…`, + 'sprite.cancel': `Batal`, + 'sprite.empty': `Tidak ada pemain yang teridentifikasi dalam sprite ini.`, + 'sprite.detectedFaceAlt': `Wajah terdeteksi`, + 'sprite.spriteLabel': `SPRITE`, + 'sprite.stashLabel': `STASH`, + 'sprite.vs': `vs`, + 'sprite.inScene': `Dalam scene`, + 'sprite.hitsCount': `{count} hit{s}`, + 'sprite.totalTime': `{time} total`, + 'sprite.alreadyInScene': `Sudah dalam scene`, + 'sprite.clickToConfirm': `Klik untuk konfirmasi`, + 'sprite.confirmed': `Dikonfirmasi`, + 'sprite.supportPatreon': `Dukung di Patreon`, + 'sprite.confirmedCount': `{confirmed} dari {total} dikonfirmasi`, + 'sprite.shownHint': `{shown} ditampilkan ({total} total) · klik untuk konfirmasi · ←→ navigasi · Enter konfirmasi`, + 'sprite.confirmHint': `Klik untuk konfirmasi · ←→ navigasi · Enter konfirmasi`, + 'sprite.adding': `Menambahkan...`, + 'sprite.done': `Selesai ({count})`, + 'gender.male': `Pria`, + 'gender.female': `Wanita`, + 'gender.transMale': `Pria transgender`, + 'gender.transFemale': `Wanita transgender`, + 'gender.nonBinary': `Non-biner`, + 'gender.intersex': `Interseks`, + 'card.excellent': `Kecocokan sangat baik`, + 'card.good': `Kecocokan baik`, + 'card.uncertain': `Kecocokan tidak pasti`, + 'card.select': `Pilih {name}`, + 'card.deselect': `Batalkan pilihan {name}`, + 'card.openOn': `Buka di {source}`, + 'search.overlayHint': `Seret untuk memilih wajah — Enter untuk memindai seluruh bingkai — Esc untuk membatalkan`, + 'search.noFaces': `Tidak ada wajah yang ditemukan pada pilihan tersebut. Coba potongan yang lebih rapat, atau tekan Enter untuk memindai seluruh bingkai.`, + 'search.captureMediaFail': `Tidak dapat menangkap media. Pastikan scene/gambar telah dimuat sepenuhnya.`, + 'search.healthBanner': `API pengenalan wajah tidak dapat dijangkau. Mulai backend dan coba lagi.`, + 'search.failed': `Pencarian wajah gagal: {error}`, + 'search.fetchImageFail': `Tidak dapat mengambil gambar dari Stash.`, + 'search.captureFail': `Gagal menangkap gambar: {error}`, + 'search.selectFaceImage': `Pilih wajah di dalam gambar.`, + 'search.captureFrameFail': `Tidak dapat menangkap bingkai saat ini.`, + 'search.captureFrameFail2': `Gagal menangkap bingkai saat ini.`, + 'search.selectFaceVideo': `Pilih wajah di dalam area pemutar video.`, + 'search.menuItemTitle': `Seret kotak di sekitar wajah, atau tekan Enter untuk memindai seluruh bingkai, untuk mencari kecocokan di StashDB`, + 'search.currentFrame': `Visage: Bingkai Saat Ini`, + 'scene.noSprite': `Tidak ada sprite sheet atau video pratinjau untuk scene ini. Buat di pengaturan Scene, lalu coba lagi.`, + 'scene.noFaces': `Tidak ada wajah atau pemain yang ditemukan di sprite sheet atau video pratinjau scene ini.`, + 'scene.healthBanner': `API pengenalan wajah tidak dapat dijangkau. Mulai backend dan coba lagi.`, + 'scene.failed': `Pemindaian scene gagal: {error}`, + 'scene.menuItemTitle': `Identifikasi setiap pemain dalam scene (membutuhkan sprite sheet atau video pratinjau yang dibuat)`, + 'scene.wholeScene': `Visage: Seluruh Scene`, + 'banner.changeBackend': `Ganti backend`, + 'banner.dismiss': `Tutup`, + 'error.dismiss': `Tutup`, + 'firstRun.title': `Siapkan backend Visage Anda`, + 'firstRun.subtitle': `Visage mengirim gambar wajah ke backend untuk pengenalan. Pilih tempat menjalankannya.`, + 'firstRun.cloud': `Gunakan cloud Hugging Face`, + 'firstRun.cloudNote': `Tanpa pengaturan. Gambar dikirim ke layanan cloud Hugging Face.`, + 'firstRun.local': `Gunakan server saya sendiri`, + 'firstRun.localNote': `Jalankan biner pribadi di mesin atau jaringan Anda sendiri.`, + 'firstRun.skip': `Lewati untuk saat ini`, + 'badge.local': `Lokal`, + 'badge.cloud': `Cloud (Hugging Face)`, + 'badge.title': `Backend Visage: {label}`, + 'donate.enjoying': `Menikmati Visage? Bantu tetap berjalan`, + 'donate.supportPatreon': `Dukung di Patreon`, + 'frame.close': `Tutup pemilih bingkai`, + 'frame.seekFail': `Gagal menavigasi pemutar video.`, + 'frame.selectAt': `Pilih bingkai wajah pada {time} detik`, + }; + + const cs = { + 'backendSettings.title': `Nastavení backendu`, + 'backendSettings.closeAria': `Zavřít nastavení`, + 'backendSettings.backendAria': `Nastavení backendu`, + 'backendSettings.changeBackend': `Změnit backend`, + 'backendSettings.backendLabel': `Backend`, + 'backendSettings.local': `Místní`, + 'backendSettings.cloud': `Cloud (Hugging Face)`, + 'backendSettings.cloudNote': `Obrázky jsou odesílány do cloudové služby Hugging Face.`, + 'backendSettings.hintPrefix': `Chcete, aby vaše obrázky zůstaly ve vaší síti?`, + 'backendSettings.hintLink': `Spusťte soukromý server přes Patreon`, + 'backendSettings.urlLabel': `URL`, + 'backendSettings.csp1': `Bezpečnostní politika prohlížeče (CSP) ve výchozím nastavení povoluje pouze`, + 'backendSettings.csp2': `. Chcete-li se dostat k místnímu backendu na jiné adrese (např. IP vaší LAN), přidejte ji do`, + 'backendSettings.csp3': `seznamu v`, + 'backendSettings.csp4': `souboru ve složce pluginů Stash, jinak budou požadavky zablokovány. Poznámka: aktualizace Visage přeinstaluje`, + 'backendSettings.csp5': `, takže to musíte provést znovu po každé aktualizaci.`, + 'backendSettings.testing': `Testování připojení…`, + 'backendSettings.testConnection': `Otestovat připojení`, + 'backendSettings.testingShort': `Testování…`, + 'backendSettings.cancel': `Zrušit`, + 'backendSettings.save': `Uložit`, + 'backendSettings.feedback.reachable': `Připojení úspěšné. Backend je připraven.`, + 'backendSettings.feedback.degraded': `Backend dosažitelný, ale degradovaný (modely nebo index nejsou načteny).`, + 'backendSettings.feedback.unreachable': `Backend je nedosažitelný. Zkontrolujte URL a ujistěte se, že backend běží.`, + 'faceMatch.title': `AKTUÁLNÍ SNÍMEK`, + 'faceMatch.close': `Zavřít`, + 'faceMatch.facesSelected': `Nalezeno tváří: {faces} · vybráno: {selected}`, + 'faceMatch.inScene': `· {count} ve scéně`, + 'faceMatch.stashboxMissing': `Není nakonfigurován stash-box.`, + 'faceMatch.stashboxMissingBody': ` Přidejte poskytovatele stash-box v Settings → Metadata Providers, abyste povolili import performerů.`, + 'faceMatch.stashboxWrongName': `Poskytovatel "StashDB" nebyl nalezen.`, + 'faceMatch.stashboxWrongNameBody': ` Import performerů vyžaduje poskytovatele s názvem "StashDB". Přejmenujte poskytovatele v Settings → Metadata Providers.`, + 'faceMatch.learnMore': `Zjistit více.`, + 'faceMatch.scanning': `Skenování • rozpoznávání tváří…`, + 'faceMatch.faceAlt': `Tvář {index}`, + 'faceMatch.minConf': `Min. jist.`, + 'faceMatch.minConfTitle': `Minimální jistota: {percent}%`, + 'faceMatch.detected': `Zjištěno`, + 'faceMatch.detectedFaceAlt': `Zjištěná tvář`, + 'faceMatch.vs': `vs`, + 'faceMatch.supportPatreon': `Podpořit na Patreon`, + 'faceMatch.ofSelected': `Vybráno {selected} z {total}`, + 'faceMatch.allInScene': `Nalezeno tváří: {total} · všechny ve scéně`, + 'faceMatch.clickToSelect': `Nalezeno tváří: {total} · klikněte pro výběr`, + 'faceMatch.kbSwitch': `Přepínání tváří`, + 'faceMatch.kbSelect': `Výběr performerů`, + 'faceMatch.kbToggle': `Přepnout výběr`, + 'faceMatch.kbAddInstant': `Shift+klik pro okamžité přidání`, + 'faceMatch.selectBest': `Vybrat nejlepší shody`, + 'faceMatch.adding': `Přidávání...`, + 'faceMatch.done': `Hotovo ({count})`, + 'faceMatch.toast.added': `Performer byl přidán do {target}.`, + 'faceMatch.toast.addError': `Nepodařilo se přidat performera: {error}`, + 'faceMatch.toast.noStashbox': `Není nakonfigurován stash-box. Přidejte poskytovatele stash-box v Settings → Metadata Providers, abyste povolili import performerů. Viz {url}`, + 'faceMatch.toast.noProvider': `Poskytovatel "StashDB" nebyl nalezen. Přejmenujte poskytovatele na "StashDB" v Settings → Metadata Providers, abyste povolili import performerů.`, + 'faceMatch.toast.configureProvider': `Nakonfigurujte poskytovatele stash-box v Settings → Metadata Providers, abyste povolili import performerů.`, + 'faceMatch.toast.addedMultiple': `Přidáno performerů: {count} do {target}.`, + 'sprite.title': `PERFORMERI SCÉNY`, + 'sprite.close': `Zavřít`, + 'sprite.foundConfirmed': `Nalezeno: {found} · potvrzeno: {confirmed}`, + 'sprite.confidence': `jistota`, + 'sprite.name': `jméno`, + 'sprite.hits': `shody`, + 'sprite.minConf': `Min. jist.`, + 'sprite.minConfTitle': `Minimální jistota: {percent}%`, + 'sprite.scanning': `Skenování Visage…`, + 'sprite.cancel': `Zrušit`, + 'sprite.empty': `V tomto sprite nebyli identifikováni žádní performeři.`, + 'sprite.detectedFaceAlt': `Zjištěná tvář`, + 'sprite.spriteLabel': `SPRITE`, + 'sprite.stashLabel': `STASH`, + 'sprite.vs': `vs`, + 'sprite.inScene': `Ve scéně`, + 'sprite.hitsCount': `Shod: {count}`, + 'sprite.totalTime': `Celkem {time}`, + 'sprite.alreadyInScene': `Již ve scéně`, + 'sprite.clickToConfirm': `Klikněte pro potvrzení`, + 'sprite.confirmed': `Potvrzeno`, + 'sprite.supportPatreon': `Podpořit na Patreon`, + 'sprite.confirmedCount': `Potvrzeno {confirmed} z {total}`, + 'sprite.shownHint': `Zobrazeno: {shown} (celkem {total}) · klikněte pro potvrzení · ←→ navigace · Enter potvrzení`, + 'sprite.confirmHint': `Klikněte pro potvrzení · ←→ navigace · Enter potvrzení`, + 'sprite.adding': `Přidávání...`, + 'sprite.done': `Hotovo ({count})`, + 'gender.male': `Muž`, + 'gender.female': `Žena`, + 'gender.transMale': `Transgender muž`, + 'gender.transFemale': `Transgender žena`, + 'gender.nonBinary': `Ne-binární`, + 'gender.intersex': `Intersex`, + 'card.excellent': `Výborná shoda`, + 'card.good': `Dobrá shoda`, + 'card.uncertain': `Nejistá shoda`, + 'card.select': `Vybrat {name}`, + 'card.deselect': `Zrušit výběr {name}`, + 'card.openOn': `Otevřít na {source}`, + 'search.overlayHint': `Přetáhněte pro výběr tváře — Enter pro skenování celého snímku — Esc pro zrušení`, + 'search.noFaces': `Ve vybrané oblasti nebyly nalezeny žádné tváře. Zkuste užší ořez nebo stiskněte Enter pro skenování celého snímku.`, + 'search.captureMediaFail': `Nepodařilo se zachytit média. Ujistěte se, že scéna/obrázek je plně načten.`, + 'search.healthBanner': `API pro rozpoznávání tváří je nedosažitelné. Spusťte backend a zkuste to znovu.`, + 'search.failed': `Hledání tváře se nezdařilo: {error}`, + 'search.fetchImageFail': `Nepodařilo se načíst obrázek ze Stash.`, + 'search.captureFail': `Nepodařilo se zachytit obrázek: {error}`, + 'search.selectFaceImage': `Vyberte tvář v obrázku.`, + 'search.captureFrameFail': `Nepodařilo se zachytit aktuální snímek.`, + 'search.captureFrameFail2': `Chyba při zachycení aktuálního snímku.`, + 'search.selectFaceVideo': `Vyberte tvář v oblasti přehrávače videa.`, + 'search.menuItemTitle': `Přetáhněte rámeček kolem tváře nebo stiskněte Enter pro skenování celého snímku, abyste vyhledali shody v StashDB`, + 'search.currentFrame': `Visage: aktuální snímek`, + 'scene.noSprite': `Pro tuto scénu nebyl nalezen žádný sprite list ani náhledové video. Vygenerujte je v nastavení scény a zkuste to znovu.`, + 'scene.noFaces': `Ve sprite listu nebo náhledovém videu této scény nebyly nalezeny žádné tváře ani performeři.`, + 'scene.healthBanner': `API pro rozpoznávání tváří je nedosažitelné. Spusťte backend a zkuste to znovu.`, + 'scene.failed': `Skenování scény se nezdařilo: {error}`, + 'scene.menuItemTitle': `Identifikujte každého performera ve scéně (vyžaduje vygenerovaný sprite list nebo náhledové video)`, + 'scene.wholeScene': `Visage: celá scéna`, + 'banner.changeBackend': `Změnit backend`, + 'banner.dismiss': `Zavřít`, + 'error.dismiss': `Zavřít`, + 'firstRun.title': `Nastavte backend Visage`, + 'firstRun.subtitle': `Visage odesílá obrázky tváří do backendu pro rozpoznání. Vyberte, kde jej spustit.`, + 'firstRun.cloud': `Použít cloud Hugging Face`, + 'firstRun.cloudNote': `Bez nastavování. Obrázky jsou odesílány do cloudové služby Hugging Face.`, + 'firstRun.local': `Použít vlastní server`, + 'firstRun.localNote': `Spusťte soukromý binární soubor na vlastním počítači nebo v síti.`, + 'firstRun.skip': `Přeskočit prozatím`, + 'badge.local': `Místní`, + 'badge.cloud': `Cloud (Hugging Face)`, + 'badge.title': `Backend Visage: {label}`, + 'donate.enjoying': `Líbí se vám Visage? Pomozte ho udržet`, + 'donate.supportPatreon': `Podpořit na Patreon`, + 'frame.close': `Zavřít výběr snímku`, + 'frame.seekFail': `Nepodařilo se přetočit přehrávač videa.`, + 'frame.selectAt': `Vybrat snímek tváře v {time} s`, + }; + + const hr = { + 'backendSettings.title': `Postavke pozadinskog poslužitelja`, + 'backendSettings.closeAria': `Zatvori postavke`, + 'backendSettings.backendAria': `Postavke pozadinskog poslužitelja`, + 'backendSettings.changeBackend': `Promijeni pozadinski poslužitelj`, + 'backendSettings.backendLabel': `Pozadinski poslužitelj`, + 'backendSettings.local': `Lokalno`, + 'backendSettings.cloud': `Oblak (Hugging Face)`, + 'backendSettings.cloudNote': `Slike se šalju u oblačnu uslugu Hugging Face.`, + 'backendSettings.hintPrefix': `Želite li da vaše slike ostanu u vašoj mreži?`, + 'backendSettings.hintLink': `Pokrenite privatni poslužitelj putem Patreona`, + 'backendSettings.urlLabel': `URL`, + 'backendSettings.csp1': `Sigurnosna politika preglednika (CSP) prema zadanim postavkama dopušta samo`, + 'backendSettings.csp2': `. Da biste dohvatili lokalni pozadinski poslužitelj na drugoj adresi (npr. IP vaše lokalne mreže), dodajte ga u`, + 'backendSettings.csp3': `popis u`, + 'backendSettings.csp4': `datoteci u mapi dodataka Stash, inače će zahtjevi biti blokirani. Napomena: ažuriranje Visagea ponovno instalira`, + 'backendSettings.csp5': `, pa to morate ponoviti nakon svakog ažuriranja.`, + 'backendSettings.testing': `Testiranje veze…`, + 'backendSettings.testConnection': `Testiraj vezu`, + 'backendSettings.testingShort': `Testiranje…`, + 'backendSettings.cancel': `Odustani`, + 'backendSettings.save': `Spremi`, + 'backendSettings.feedback.reachable': `Veza uspješna. Pozadinski poslužitelj je spreman.`, + 'backendSettings.feedback.degraded': `Pozadinski poslužitelj je dostupan, ali smanjenih performansi (modeli ili indeks nisu učitani).`, + 'backendSettings.feedback.unreachable': `Pozadinski poslužitelj je nedostupan. Provjerite URL i uvjerite se da poslužitelj radi.`, + 'faceMatch.title': `TRENUTNI OKVIR`, + 'faceMatch.close': `Zatvori`, + 'faceMatch.facesSelected': `Pronađena lica: {faces} · odabrano: {selected}`, + 'faceMatch.inScene': `· {count} u sceni`, + 'faceMatch.stashboxMissing': `Stash-box nije konfiguriran.`, + 'faceMatch.stashboxMissingBody': ` Dodajte pružatelja stash-box u Settings → Metadata Providers da biste omogućili uvoz izvođača.`, + 'faceMatch.stashboxWrongName': `Pružatelj "StashDB" nije pronađen.`, + 'faceMatch.stashboxWrongNameBody': ` Uvoz izvođača zahtijeva pružatelja naziva "StashDB". Preimenujte pružatelja u Settings → Metadata Providers.`, + 'faceMatch.learnMore': `Saznajte više.`, + 'faceMatch.scanning': `Skeniranje • prepoznavanje lica…`, + 'faceMatch.faceAlt': `Lice {index}`, + 'faceMatch.minConf': `Min. povj.`, + 'faceMatch.minConfTitle': `Minimalna pouzdanost: {percent}%`, + 'faceMatch.detected': `Otkriveno`, + 'faceMatch.detectedFaceAlt': `Otkriveno lice`, + 'faceMatch.vs': `vs`, + 'faceMatch.supportPatreon': `Podržite na Patreonu`, + 'faceMatch.ofSelected': `Odabrano {selected} od {total}`, + 'faceMatch.allInScene': `Pronađena lica: {total} · sva u sceni`, + 'faceMatch.clickToSelect': `Pronađena lica: {total} · kliknite za odabir`, + 'faceMatch.kbSwitch': `Promjena lica`, + 'faceMatch.kbSelect': `Odabir izvođača`, + 'faceMatch.kbToggle': `Uključi/isključi odabir`, + 'faceMatch.kbAddInstant': `Shift+klik za trenutno dodavanje`, + 'faceMatch.selectBest': `Odaberi najbolja podudaranja`, + 'faceMatch.adding': `Dodavanje...`, + 'faceMatch.done': `Gotovo ({count})`, + 'faceMatch.toast.added': `Izvođač je dodan u {target}.`, + 'faceMatch.toast.addError': `Neuspješno dodavanje izvođača: {error}`, + 'faceMatch.toast.noStashbox': `Stash-box nije konfiguriran. Dodajte pružatelja stash-box u Settings → Metadata Providers da biste omogućili uvoz izvođača. Pogledajte {url}`, + 'faceMatch.toast.noProvider': `Pružatelj "StashDB" nije pronađen. Preimenujte pružatelja u "StashDB" u Settings → Metadata Providers da biste omogućili uvoz izvođača.`, + 'faceMatch.toast.configureProvider': `Konfigurirajte pružatelja stash-box u Settings → Metadata Providers da biste omogućili uvoz izvođača.`, + 'faceMatch.toast.addedMultiple': `Dodano izvođača: {count} u {target}.`, + 'sprite.title': `IZVOĐAČI SCENE`, + 'sprite.close': `Zatvori`, + 'sprite.foundConfirmed': `Pronađeno: {found} · potvrđeno: {confirmed}`, + 'sprite.confidence': `pouzdanost`, + 'sprite.name': `ime`, + 'sprite.hits': `podudaranja`, + 'sprite.minConf': `Min. povj.`, + 'sprite.minConfTitle': `Minimalna pouzdanost: {percent}%`, + 'sprite.scanning': `Visage skeniranje…`, + 'sprite.cancel': `Odustani`, + 'sprite.empty': `U ovom spriteu nisu identificirani izvođači.`, + 'sprite.detectedFaceAlt': `Otkriveno lice`, + 'sprite.spriteLabel': `SPRITE`, + 'sprite.stashLabel': `STASH`, + 'sprite.vs': `vs`, + 'sprite.inScene': `U sceni`, + 'sprite.hitsCount': `Podudaranja: {count}`, + 'sprite.totalTime': `Ukupno {time}`, + 'sprite.alreadyInScene': `Već u sceni`, + 'sprite.clickToConfirm': `Kliknite za potvrdu`, + 'sprite.confirmed': `Potvrđeno`, + 'sprite.supportPatreon': `Podržite na Patreonu`, + 'sprite.confirmedCount': `Potvrđeno {confirmed} od {total}`, + 'sprite.shownHint': `Prikazano: {shown} (ukupno {total}) · kliknite za potvrdu · ←→ navigacija · Enter potvrda`, + 'sprite.confirmHint': `Kliknite za potvrdu · ←→ navigacija · Enter potvrda`, + 'sprite.adding': `Dodavanje...`, + 'sprite.done': `Gotovo ({count})`, + 'gender.male': `Muško`, + 'gender.female': `Žensko`, + 'gender.transMale': `Transrodno muško`, + 'gender.transFemale': `Transrodno žensko`, + 'gender.nonBinary': `Nebinarno`, + 'gender.intersex': `Interseks`, + 'card.excellent': `Izvrsno podudaranje`, + 'card.good': `Dobro podudaranje`, + 'card.uncertain': `Nesigurno podudaranje`, + 'card.select': `Odaberi {name}`, + 'card.deselect': `Poništi odabir {name}`, + 'card.openOn': `Otvori na {source}`, + 'search.overlayHint': `Povucite za odabir lica — Enter za skeniranje cijelog okvira — Esc za odustajanje`, + 'search.noFaces': `U odabranom području nisu pronađena lica. Pokušajte s užim kadrom ili pritisnite Enter za skeniranje cijelog okvira.`, + 'search.captureMediaFail': `Neuspješno snimanje medija. Provjerite je li scena/slika u potpunosti učitana.`, + 'search.healthBanner': `API za prepoznavanje lica nije dostupan. Pokrenite pozadinski poslužitelj i pokušajte ponovno.`, + 'search.failed': `Pretraga lica nije uspjela: {error}`, + 'search.fetchImageFail': `Neuspješno dohvaćanje slike iz Stasha.`, + 'search.captureFail': `Neuspješno snimanje slike: {error}`, + 'search.selectFaceImage': `Odaberite lice unutar slike.`, + 'search.captureFrameFail': `Neuspješno snimanje trenutnog okvira.`, + 'search.captureFrameFail2': `Greška pri snimanju trenutnog okvira.`, + 'search.selectFaceVideo': `Odaberite lice unutar područja video playera.`, + 'search.menuItemTitle': `Povucite okvir oko lica ili pritisnite Enter za skeniranje cijelog okvira, da biste pretražili podudaranja u StashDB`, + 'search.currentFrame': `Visage: trenutni okvir`, + 'scene.noSprite': `Za ovu scenu nije pronađen sprite sheet ni video pregled. Generirajte ih u postavkama scene i pokušajte ponovno.`, + 'scene.noFaces': `U sprite sheetu ili videu pregleda ove scene nisu pronađena lica ni izvođači.`, + 'scene.healthBanner': `API za prepoznavanje lica nije dostupan. Pokrenite pozadinski poslužitelj i pokušajte ponovno.`, + 'scene.failed': `Skeniranje scene nije uspjelo: {error}`, + 'scene.menuItemTitle': `Identificirajte svakog izvođača u sceni (zahtijeva generirani sprite sheet ili video pregled)`, + 'scene.wholeScene': `Visage: cijela scena`, + 'banner.changeBackend': `Promijeni pozadinski poslužitelj`, + 'banner.dismiss': `Odbaci`, + 'error.dismiss': `Odbaci`, + 'firstRun.title': `Postavite svoj Visage pozadinski poslužitelj`, + 'firstRun.subtitle': `Visage šalje slike lica pozadinskom poslužitelju na prepoznavanje. Odaberite gdje ga pokrenuti.`, + 'firstRun.cloud': `Koristi oblak Hugging Face`, + 'firstRun.cloudNote': `Bez postavljanja. Slike se šalju u oblačnu uslugu Hugging Face.`, + 'firstRun.local': `Koristi vlastiti poslužitelj`, + 'firstRun.localNote': `Pokrenite privatnu datoteku na svojem računalu ili u svojoj mreži.`, + 'firstRun.skip': `Preskoči za sada`, + 'badge.local': `Lokalno`, + 'badge.cloud': `Oblak (Hugging Face)`, + 'badge.title': `Visage pozadinski poslužitelj: {label}`, + 'donate.enjoying': `Sviđa vam se Visage? Pomozite ga održati na životu`, + 'donate.supportPatreon': `Podržite na Patreonu`, + 'frame.close': `Zatvori odabir okvira`, + 'frame.seekFail': `Neuspješno pomicanje video playera.`, + 'frame.selectAt': `Odaberite okvir lica na {time} s`, + }; + + const th = { + 'backendSettings.title': `การตั้งค่าแบ็กเอนด์`, + 'backendSettings.closeAria': `ปิดการตั้งค่า`, + 'backendSettings.backendAria': `การตั้งค่าแบ็กเอนด์`, + 'backendSettings.changeBackend': `เปลี่ยนแบ็กเอนด์`, + 'backendSettings.backendLabel': `แบ็กเอนด์`, + 'backendSettings.local': `ภายในเครื่อง`, + 'backendSettings.cloud': `คลาวด์ (Hugging Face)`, + 'backendSettings.cloudNote': `รูปภาพจะถูกส่งไปยังบริการคลาวด์ Hugging Face`, + 'backendSettings.hintPrefix': `ต้องการเก็บรูปภาพไว้ในเครือข่ายของคุณหรือไม่`, + 'backendSettings.hintLink': `เรียกใช้เซิร์ฟเวอร์ส่วนตัวผ่าน Patreon`, + 'backendSettings.urlLabel': `URL`, + 'backendSettings.csp1': `นโยบายความปลอดภัยของเบราว์เซอร์ (CSP) อนุญาตเฉพาะ`, + 'backendSettings.csp2': `ตามค่าเริ่มต้น หากต้องการเข้าถึงแบ็กเอนด์ภายในเครื่องที่อยู่ที่อยู่อื่น (เช่น IP แลนของคุณ) ให้เพิ่มลงใน`, + 'backendSettings.csp3': `ใน`, + 'backendSettings.csp4': `ภายในโฟลเดอร์ปลั๊กอิน Stash ของคุณ มิฉะนั้นคำขอจะถูกบล็อก หมายเหตุ: การอัปเดต Visage จะติดตั้ง`, + 'backendSettings.csp5': `ใหม่ ดังนั้นต้องนำกลับมาใช้ใหม่หลังจากการอัปเดตทุกครั้ง`, + 'backendSettings.testing': `กำลังทดสอบการเชื่อมต่อ…`, + 'backendSettings.testConnection': `ทดสอบการเชื่อมต่อ`, + 'backendSettings.testingShort': `กำลังทดสอบ…`, + 'backendSettings.cancel': `ยกเลิก`, + 'backendSettings.save': `บันทึก`, + 'backendSettings.feedback.reachable': `เชื่อมต่อสำเร็จ แบ็กเอนด์พร้อมใช้งานแล้ว`, + 'backendSettings.feedback.degraded': `เข้าถึงแบ็กเอนด์ได้แต่ประสิทธิภาพลดลง (โมเดลหรือดัชนียังไม่โหลด)`, + 'backendSettings.feedback.unreachable': `ไม่สามารถเข้าถึงแบ็กเอนด์ได้ ตรวจสอบ URL และยืนยันว่าแบ็กเอนด์กำลังทำงาน`, + 'faceMatch.title': `เฟรมปัจจุบัน`, + 'faceMatch.close': `ปิด`, + 'faceMatch.facesSelected': `พบใบหน้า {faces} ใบ · เลือกแล้ว {selected}`, + 'faceMatch.inScene': `· {count} ในฉาก`, + 'faceMatch.stashboxMissing': `ยังไม่ได้กำหนดค่า stash-box`, + 'faceMatch.stashboxMissingBody': ` เพิ่มผู้ให้บริการ stash-box ในการตั้งค่า → ผู้ให้บริการเมตาดาต้าเพื่อเปิดใช้งานการนำเข้านักแสดง`, + 'faceMatch.stashboxWrongName': `ไม่พบผู้ให้บริการ "StashDB"`, + 'faceMatch.stashboxWrongNameBody': ` การนำเข้านักแสดงต้องมีผู้ให้บริการชื่อ "StashDB" เปลี่ยนชื่อผู้ให้บริการของคุณในการตั้งค่า → ผู้ให้บริการเมตาดาต้า`, + 'faceMatch.learnMore': `เรียนรู้เพิ่มเติม`, + 'faceMatch.scanning': `กำลังสแกน · การจดจำใบหน้า…`, + 'faceMatch.faceAlt': `ใบหน้า {index}`, + 'faceMatch.minConf': `ความมั่นใจต่ำสุด`, + 'faceMatch.minConfTitle': `ความมั่นใจต่ำสุด: {percent}%`, + 'faceMatch.detected': `ตรวจพบแล้ว`, + 'faceMatch.detectedFaceAlt': `ใบหน้าที่ตรวจพบ`, + 'faceMatch.vs': `vs`, + 'faceMatch.supportPatreon': `สนับสนุนบน Patreon`, + 'faceMatch.ofSelected': `เลือก {selected} จาก {total}`, + 'faceMatch.allInScene': `พบใบหน้า {total} ใบ · ทั้งหมดในฉาก`, + 'faceMatch.clickToSelect': `พบใบหน้า {total} ใบ · คลิกเพื่อเลือก`, + 'faceMatch.kbSwitch': `สลับใบหน้า`, + 'faceMatch.kbSelect': `เลือกนักแสดง`, + 'faceMatch.kbToggle': `สลับการเลือก`, + 'faceMatch.kbAddInstant': `Shift+คลิกเพื่อเพิ่มทันที`, + 'faceMatch.selectBest': `เลือกผลลัพธ์ที่ดีที่สุด`, + 'faceMatch.adding': `กำลังเพิ่ม...`, + 'faceMatch.done': `เสร็จสิ้น ({count})`, + 'faceMatch.toast.added': `เพิ่มนักแสดงลงใน {target} แล้ว`, + 'faceMatch.toast.addError': `เพิ่มนักแสดงไม่สำเร็จ: {error}`, + 'faceMatch.toast.noStashbox': `ยังไม่ได้กำหนดค่า stash-box เพิ่มผู้ให้บริการ stash-box ในการตั้งค่า → ผู้ให้บริการเมตาดาต้าเพื่อเปิดใช้งานการนำเข้านักแสดง ดู {url}`, + 'faceMatch.toast.noProvider': `ไม่พบผู้ให้บริการชื่อ "StashDB" เปลี่ยนชื่อผู้ให้บริการของคุณเป็น "StashDB" ในการตั้งค่า → ผู้ให้บริการเมตาดาต้าเพื่อเปิดใช้งานการนำเข้านักแสดง`, + 'faceMatch.toast.configureProvider': `กำหนดค่าผู้ให้บริการ stash-box ในการตั้งค่า → ผู้ให้บริการเมตาดาต้าเพื่อเปิดใช้งานการนำเข้านักแสดง`, + 'faceMatch.toast.addedMultiple': `เพิ่มนักแสดง {count} คน{s}ลงใน {target} แล้ว`, + 'sprite.title': `นักแสดงในฉาก`, + 'sprite.close': `ปิด`, + 'sprite.foundConfirmed': `พบ {found} · ยืนยันแล้ว {confirmed}`, + 'sprite.confidence': `ความมั่นใจ`, + 'sprite.name': `ชื่อ`, + 'sprite.hits': `การเข้าชม`, + 'sprite.minConf': `ความมั่นใจต่ำสุด`, + 'sprite.minConfTitle': `ความมั่นใจต่ำสุด: {percent}%`, + 'sprite.scanning': `Visage กำลังสแกน…`, + 'sprite.cancel': `ยกเลิก`, + 'sprite.empty': `ไม่พบนักแสดงที่ระบุในสไปรต์นี้`, + 'sprite.detectedFaceAlt': `ใบหน้าที่ตรวจพบ`, + 'sprite.spriteLabel': `สไปรต์`, + 'sprite.stashLabel': `STASH`, + 'sprite.vs': `vs`, + 'sprite.inScene': `ในฉาก`, + 'sprite.hitsCount': `เข้าชม {count} ครั้ง{s}`, + 'sprite.totalTime': `รวม {time}`, + 'sprite.alreadyInScene': `อยู่ในฉากแล้ว`, + 'sprite.clickToConfirm': `คลิกเพื่อยืนยัน`, + 'sprite.confirmed': `ยืนยันแล้ว`, + 'sprite.supportPatreon': `สนับสนุนบน Patreon`, + 'sprite.confirmedCount': `ยืนยัน {confirmed} จาก {total}`, + 'sprite.shownHint': `แสดง {shown} (รวม {total}) · คลิกเพื่อยืนยัน · ←→ นำทาง · Enter ยืนยัน`, + 'sprite.confirmHint': `คลิกเพื่อยืนยัน · ←→ นำทาง · Enter ยืนยัน`, + 'sprite.adding': `กำลังเพิ่ม...`, + 'sprite.done': `เสร็จสิ้น ({count})`, + 'gender.male': `ชาย`, + 'gender.female': `หญิง`, + 'gender.transMale': `ชายข้ามเพศ`, + 'gender.transFemale': `หญิงข้ามเพศ`, + 'gender.nonBinary': `ไม่ใช่สองเพศ`, + 'gender.intersex': `ภาวะเพศกำกวม`, + 'card.excellent': `การจับคู่ที่ดีเยี่ยม`, + 'card.good': `การจับคู่ที่ดี`, + 'card.uncertain': `การจับคู่ที่ไม่แน่นอน`, + 'card.select': `เลือก {name}`, + 'card.deselect': `ยกเลิกการเลือก {name}`, + 'card.openOn': `เปิดบน {source}`, + 'search.overlayHint': `ลากเพื่อเลือกใบหน้า — Enter เพื่อสแกนทั้งเฟรม — Esc เพื่อยกเลิก`, + 'search.noFaces': `ไม่พบใบหน้าในการเลือกนั้น ลองครอบให้แคบลง หรือกด Enter เพื่อสแกนทั้งเฟรม`, + 'search.captureMediaFail': `ไม่สามารถจับภาพสื่อได้ โปรดตรวจสอบว่าฉาก/รูปภาพโหลดเสร็จสมบูรณ์`, + 'search.healthBanner': `ไม่สามารถเข้าถึง API การจดจำใบหน้า เริ่มแบ็กเอนด์แล้วลองอีกครั้ง`, + 'search.failed': `การค้นหาใบหน้าล้มเหลว: {error}`, + 'search.fetchImageFail': `ไม่สามารถดึงรูปภาพจาก Stash ได้`, + 'search.captureFail': `การจับภาพล้มเหลว: {error}`, + 'search.selectFaceImage': `เลือกใบหน้าภายในรูปภาพ`, + 'search.captureFrameFail': `ไม่สามารถจับภาพเฟรมปัจจุบันได้`, + 'search.captureFrameFail2': `การจับภาพเฟรมปัจจุบันล้มเหลว`, + 'search.selectFaceVideo': `เลือกใบหน้าภายในพื้นที่เครื่องเล่นวิดีโอ`, + 'search.menuItemTitle': `ลากกล่องรอบใบหน้า หรือกด Enter เพื่อสแกนทั้งเฟรม เพื่อค้นหาการจับคู่บน StashDB`, + 'search.currentFrame': `Visage: เฟรมปัจจุบัน`, + 'scene.noSprite': `ไม่พบสไปรต์ชีตหรือวิดีโอตัวอย่างสำหรับฉากนี้ สร้างในการตั้งค่าฉาก แล้วลองอีกครั้ง`, + 'scene.noFaces': `ไม่พบใบหน้าหรือนักแสดงในสไปรต์ชีตหรือวิดีโอตัวอย่างของฉากนี้`, + 'scene.healthBanner': `ไม่สามารถเข้าถึง API การจดจำใบหน้า เริ่มแบ็กเอนด์แล้วลองอีกครั้ง`, + 'scene.failed': `การสแกนฉากล้มเหลว: {error}`, + 'scene.menuItemTitle': `ระบุนักแสดงทุกคนในฉาก (ต้องมีสไปรต์ชีตหรือวิดีโอตัวอย่างที่สร้างแล้ว)`, + 'scene.wholeScene': `Visage: ทั้งฉาก`, + 'banner.changeBackend': `เปลี่ยนแบ็กเอนด์`, + 'banner.dismiss': `ปิด`, + 'error.dismiss': `ปิด`, + 'firstRun.title': `ตั้งค่าแบ็กเอนด์ Visage ของคุณ`, + 'firstRun.subtitle': `Visage ส่งภาพใบหน้าไปยังแบ็กเอนด์เพื่อการจดจำ เลือกสถานที่ที่จะเรียกใช้`, + 'firstRun.cloud': `ใช้คลาวด์ Hugging Face`, + 'firstRun.cloudNote': `ไม่ต้องตั้งค่าใดๆ รูปภาพจะถูกส่งไปยังบริการคลาวด์ Hugging Face`, + 'firstRun.local': `ใช้เซิร์ฟเวอร์ของฉันเอง`, + 'firstRun.localNote': `เรียกใช้ไฟล์ส่วนตัวบนเครื่องหรือเครือข่ายของคุณเอง`, + 'firstRun.skip': `ข้ามไปก่อน`, + 'badge.local': `ภายในเครื่อง`, + 'badge.cloud': `คลาวด์ (Hugging Face)`, + 'badge.title': `แบ็กเอนด์ Visage: {label}`, + 'donate.enjoying': `ชอบ Visage หรือไม่ ช่วยสนับสนุนให้มันคงอยู่ต่อไป`, + 'donate.supportPatreon': `สนับสนุนบน Patreon`, + 'frame.close': `ปิดตัวเลือกเฟรม`, + 'frame.seekFail': `ไม่สามารถเลื่อนไปยังตำแหน่งในเครื่องเล่นวิดีโอได้`, + 'frame.selectAt': `เลือกเฟรมใบหน้าที่ {time} วินาที`, + }; + + // Brazilian Portuguese. Missing keys fall back to English automatically. + const pt = { + // ---- BackendSettings.tsx ---- + 'backendSettings.title': `Configurações do backend`, + 'backendSettings.closeAria': `Fechar configurações`, + 'backendSettings.backendAria': `Configurações do backend`, + 'backendSettings.changeBackend': `Alterar backend`, + 'backendSettings.backendLabel': `Backend`, + 'backendSettings.local': `Local`, + 'backendSettings.cloud': `Cloud (Hugging Face)`, + 'backendSettings.cloudNote': `As imagens são enviadas ao serviço de nuvem Hugging Face.`, + 'backendSettings.hintPrefix': `Quer que suas imagens permaneçam na sua rede?`, + 'backendSettings.hintLink': `Execute um servidor privado via Patreon`, + 'backendSettings.urlLabel': `URL`, + 'backendSettings.csp1': `A política de segurança do navegador (CSP) só permite`, + 'backendSettings.csp2': `por padrão. Para acessar um backend local em outro endereço (por exemplo, seu IP LAN), adicione-o à`, + 'backendSettings.csp3': `lista no arquivo`, + 'backendSettings.csp4': `dentro da sua pasta de plugins do Stash; caso contrário, as solicitações serão bloqueadas. Observação: atualizar o Visage reinstala`, + 'backendSettings.csp5': `, então isso deve ser reaplicado após cada atualização.`, + 'backendSettings.testing': `Testando a conexão…`, + 'backendSettings.testConnection': `Testar conexão`, + 'backendSettings.testingShort': `Testando…`, + 'backendSettings.cancel': `Cancelar`, + 'backendSettings.save': `Salvar`, + 'backendSettings.feedback.reachable': `Conexão bem-sucedida. O backend está pronto.`, + 'backendSettings.feedback.degraded': `Backend acessível, mas degradado (modelos ou índice não carregados).`, + 'backendSettings.feedback.unreachable': `Backend inacessível. Verifique a URL e se o backend está em execução.`, + // ---- FaceMatchModal.tsx ---- + 'faceMatch.title': `QUADRO ATUAL`, + 'faceMatch.close': `Fechar`, + 'faceMatch.facesSelected': `{faces} rostos encontrados · {selected} selecionados`, + 'faceMatch.inScene': `· {count} na cena`, + 'faceMatch.stashboxMissing': `Nenhum stash-box configurado.`, + 'faceMatch.stashboxMissingBody': ` Adicione um provedor de stash-box em Configurações → Provedores de metadados para ativar a importação de artistas.`, + 'faceMatch.stashboxWrongName': `Nenhum provedor chamado "StashDB" encontrado.`, + 'faceMatch.stashboxWrongNameBody': ` A importação de artistas requer um provedor chamado "StashDB". Renomeie seu provedor em Configurações → Provedores de metadados.`, + 'faceMatch.learnMore': `Saiba mais.`, + 'faceMatch.scanning': `Escaneando • reconhecimento facial…`, + 'faceMatch.faceAlt': `Rosto {index}`, + 'faceMatch.minConf': `Conf. mín.`, + 'faceMatch.minConfTitle': `Confiança mínima: {percent}%`, + 'faceMatch.detected': `Detectado`, + 'faceMatch.detectedFaceAlt': `Rosto detectado`, + 'faceMatch.vs': `vs`, + 'faceMatch.supportPatreon': `Apoiar no Patreon`, + 'faceMatch.ofSelected': `{selected} de {total} selecionados`, + 'faceMatch.allInScene': `{total} rostos encontrados · todos na cena`, + 'faceMatch.clickToSelect': `{total} rostos encontrados · clique para selecionar`, + 'faceMatch.kbSwitch': `Alternar rostos`, + 'faceMatch.kbSelect': `Selecionar artistas`, + 'faceMatch.kbToggle': `Alternar seleção`, + 'faceMatch.kbAddInstant': `Shift+clique para adicionar instantaneamente`, + 'faceMatch.selectBest': `Selecionar melhores correspondências`, + 'faceMatch.adding': `Adicionando...`, + 'faceMatch.done': `Concluir ({count})`, + 'faceMatch.toast.added': `Artista adicionado ao {target}.`, + 'faceMatch.toast.addError': `Falha ao adicionar o artista: {error}`, + 'faceMatch.toast.noStashbox': `Nenhum stash-box configurado. Adicione um provedor de stash-box em Configurações → Provedores de metadados para ativar a importação de artistas. Veja {url}`, + 'faceMatch.toast.noProvider': `Nenhum provedor chamado "StashDB" encontrado. Renomeie seu provedor para "StashDB" em Configurações → Provedores de metadados para ativar a importação de artistas.`, + 'faceMatch.toast.configureProvider': `Configure um provedor de stash-box em Configurações → Provedores de metadados para ativar a importação de artistas.`, + 'faceMatch.toast.addedMultiple': `{count} artista{s} adicionado{s} ao {target}.`, + // ---- SpriteResultModal.tsx ---- + 'sprite.title': `ARTISTAS DA CENA`, + 'sprite.close': `Fechar`, + 'sprite.foundConfirmed': `{found} encontrados · {confirmed} confirmados`, + 'sprite.confidence': `confiança`, + 'sprite.name': `nome`, + 'sprite.hits': `acertos`, + 'sprite.minConf': `Conf. mín.`, + 'sprite.minConfTitle': `Confiança mínima: {percent}%`, + 'sprite.scanning': `Visage escaneando…`, + 'sprite.cancel': `Cancelar`, + 'sprite.empty': `Nenhum artista identificado neste sprite.`, + 'sprite.detectedFaceAlt': `Rosto detectado`, + 'sprite.spriteLabel': `SPRITE`, + 'sprite.stashLabel': `STASH`, + 'sprite.vs': `vs`, + 'sprite.inScene': `Na cena`, + 'sprite.hitsCount': `{count} acerto{s}`, + 'sprite.totalTime': `{time} no total`, + 'sprite.alreadyInScene': `Já na cena`, + 'sprite.clickToConfirm': `Clique para confirmar`, + 'sprite.confirmed': `Confirmado`, + 'sprite.supportPatreon': `Apoiar no Patreon`, + 'sprite.confirmedCount': `{confirmed} de {total} confirmados`, + 'sprite.shownHint': `{shown} exibidos ({total} no total) · clique para confirmar · ←→ navegar · Enter confirmar`, + 'sprite.confirmHint': `Clique para confirmar · ←→ navegar · Enter confirmar`, + 'sprite.adding': `Adicionando...`, + 'sprite.done': `Concluir ({count})`, + // ---- PerformerCard.tsx ---- + 'gender.male': `Masculino`, + 'gender.female': `Feminino`, + 'gender.transMale': `Homem transgênero`, + 'gender.transFemale': `Mulher transgênero`, + 'gender.nonBinary': `Não binário`, + 'gender.intersex': `Intersexo`, + 'card.excellent': `Correspondência excelente`, + 'card.good': `Boa correspondência`, + 'card.uncertain': `Correspondência incerta`, + 'card.select': `Selecionar {name}`, + 'card.deselect': `Desselecionar {name}`, + 'card.openOn': `Abrir em {source}`, + // ---- FaceSearchButton.tsx ---- + 'search.overlayHint': `Arraste para selecionar um rosto — Enter para escanear a imagem inteira — Esc para cancelar`, + 'search.noFaces': `Nenhum rosto encontrado nessa seleção. Tente um recorte mais fechado ou pressione Enter para escanear a imagem inteira.`, + 'search.captureMediaFail': `Não foi possível capturar a mídia. Certifique-se de que a cena/imagem está totalmente carregada.`, + 'search.healthBanner': `A API de reconhecimento facial não está acessível. Inicie o backend e tente novamente.`, + 'search.failed': `A busca facial falhou: {error}`, + 'search.fetchImageFail': `Não foi possível buscar a imagem no Stash.`, + 'search.captureFail': `Falha ao capturar a imagem: {error}`, + 'search.selectFaceImage': `Selecione um rosto dentro da imagem.`, + 'search.captureFrameFail': `Não foi possível capturar o quadro atual.`, + 'search.captureFrameFail2': `Falha ao capturar o quadro atual.`, + 'search.selectFaceVideo': `Selecione um rosto dentro da área do player de vídeo.`, + 'search.menuItemTitle': `Arraste uma caixa ao redor de um rosto ou pressione Enter para escanear a imagem inteira, para buscar correspondências no StashDB`, + 'search.currentFrame': `Visage: Quadro atual`, + // ---- SceneScanButton.tsx ---- + 'scene.noSprite': `Nenhuma folha de sprites ou vídeo de prévia encontrado para esta cena. Gere-os nas configurações da cena e tente novamente.`, + 'scene.noFaces': `Nenhum rosto ou artista encontrado na folha de sprites ou no vídeo de prévia desta cena.`, + 'scene.healthBanner': `A API de reconhecimento facial não está acessível. Inicie o backend e tente novamente.`, + 'scene.failed': `A varredura da cena falhou: {error}`, + 'scene.menuItemTitle': `Identificar cada artista na cena (requer uma folha de sprites ou um vídeo de prévia gerados)`, + 'scene.wholeScene': `Visage: Cena inteira`, + // ---- BackendHealthBanner.tsx ---- + 'banner.changeBackend': `Alterar backend`, + 'banner.dismiss': `Dispensar`, + // ---- ErrorDialog.tsx ---- + 'error.dismiss': `Dispensar`, + // ---- FirstRunDialog.tsx ---- + 'firstRun.title': `Configure seu backend do Visage`, + 'firstRun.subtitle': `O Visage envia imagens de rostos a um backend para reconhecimento. Escolha onde executá-lo.`, + 'firstRun.cloud': `Usar o cloud Hugging Face`, + 'firstRun.cloudNote': `Sem configuração. As imagens são enviadas ao serviço de nuvem Hugging Face.`, + 'firstRun.local': `Usar meu próprio servidor`, + 'firstRun.localNote': `Execute o binário privado na sua própria máquina ou rede.`, + 'firstRun.skip': `Pular por enquanto`, + // ---- BackendBadge.tsx ---- + 'badge.local': `Local`, + 'badge.cloud': `Cloud (Hugging Face)`, + 'badge.title': `Backend do Visage: {label}`, + // ---- DonateFooter.tsx ---- + 'donate.enjoying': `Gostando do Visage? Ajude a mantê-lo vivo`, + 'donate.supportPatreon': `Apoiar no Patreon`, + // ---- FaceFrameSelector.tsx ---- + 'frame.close': `Fechar seletor de quadro`, + 'frame.seekFail': `Falha ao buscar no player de vídeo.`, + 'frame.selectAt': `Selecionar rosto em {time}s`, + }; + + // Danish. + const da = { + 'backendSettings.title': `Backend-indstillinger`, + 'backendSettings.closeAria': `Luk indstillinger`, + 'backendSettings.backendAria': `Backend-indstillinger`, + 'backendSettings.changeBackend': `Skift backend`, + 'backendSettings.backendLabel': `Backend`, + 'backendSettings.local': `Lokal`, + 'backendSettings.cloud': `Cloud (Hugging Face)`, + 'backendSettings.cloudNote': `Billeder sendes til Hugging Face cloud-tjenesten.`, + 'backendSettings.hintPrefix': `Vil du have dine billeder til at blive på dit netværk?`, + 'backendSettings.hintLink': `Kør en privat server via Patreon`, + 'backendSettings.urlLabel': `URL`, + 'backendSettings.csp1': "Browserens sikkerhedspolitik (CSP) tillader kun", + 'backendSettings.csp2': `som standard. For at nå en lokal backend på en anden adresse (f.eks. din LAN-IP) skal du tilføje den til`, + 'backendSettings.csp3': `listen i`, + 'backendSettings.csp4': `filen i din Stash plugins-mappe, ellers blokeres anmodningerne. Bemærk: opdatering af Visage geninstallerer`, + 'backendSettings.csp5': `, så dette skal gøres igen efter hver opdatering.`, + 'backendSettings.testing': `Tester forbindelse…`, + 'backendSettings.testConnection': `Test forbindelse`, + 'backendSettings.testingShort': `Tester…`, + 'backendSettings.cancel': `Annullér`, + 'backendSettings.save': `Gem`, + 'backendSettings.feedback.reachable': `Forbindelsen lykkedes. Backend er klar.`, + 'backendSettings.feedback.degraded': `Backend er tilgængelig, men forringet (modeller eller indeks er ikke indlæst).`, + 'backendSettings.feedback.unreachable': `Backend er utilgængelig. Tjek URL'en, og at backend kører.`, + 'faceMatch.title': `AKTUELLE RAMME`, + 'faceMatch.close': `Luk`, + 'faceMatch.facesSelected': `{faces} ansigter fundet · {selected} valgt`, + 'faceMatch.inScene': `· {count} i scene`, + 'faceMatch.stashboxMissing': `Ingen stash-box konfigureret.`, + 'faceMatch.stashboxMissingBody': ` Tilføj en stash-box-udbyder under Indstillinger → Metadata-udbydere for at aktivere performer-import.`, + 'faceMatch.stashboxWrongName': `Ingen "StashDB"-udbyder fundet.`, + 'faceMatch.stashboxWrongNameBody': ` Performer-import kræver en udbyder kaldet "StashDB". Omdøb din udbyder under Indstillinger → Metadata-udbydere.`, + 'faceMatch.learnMore': `Lær mere.`, + 'faceMatch.scanning': `Scanner • ansigtsgenkendelse…`, + 'faceMatch.faceAlt': `Ansigt {index}`, + 'faceMatch.minConf': `Min. konf.`, + 'faceMatch.minConfTitle': `Minimums-sikkerhed: {percent}%`, + 'faceMatch.detected': `Registreret`, + 'faceMatch.detectedFaceAlt': `Registreret ansigt`, + 'faceMatch.vs': `vs`, + 'faceMatch.supportPatreon': `Støt på Patreon`, + 'faceMatch.ofSelected': `{selected} af {total} valgt`, + 'faceMatch.allInScene': `{total} ansigter fundet · alle i scene`, + 'faceMatch.clickToSelect': `{total} ansigter fundet · klik for at vælge`, + 'faceMatch.kbSwitch': `Skift ansigter`, + 'faceMatch.kbSelect': `Vælg performere`, + 'faceMatch.kbToggle': `Skift markering`, + 'faceMatch.kbAddInstant': `Shift+klik for at tilføje med det samme`, + 'faceMatch.selectBest': `Vælg bedste matches`, + 'faceMatch.adding': `Tilføjer...`, + 'faceMatch.done': `Færdig ({count})`, + 'faceMatch.toast.added': `Tilføjede performer til {target}.`, + 'faceMatch.toast.addError': `Kunne ikke tilføje performer: {error}`, + 'faceMatch.toast.noStashbox': `Ingen stash-box konfigureret. Tilføj en stash-box-udbyder under Indstillinger → Metadata-udbydere for at aktivere performer-import. Se {url}`, + 'faceMatch.toast.noProvider': `Ingen udbyder kaldet "StashDB" fundet. Omdøb din udbyder til "StashDB" under Indstillinger → Metadata-udbydere for at aktivere performer-import.`, + 'faceMatch.toast.configureProvider': `Konfigurér en stash-box-udbyder under Indstillinger → Metadata-udbydere for at aktivere performer-import.`, + 'faceMatch.toast.addedMultiple': `Tilføjede {count} performere til {target}.`, + 'sprite.title': `SCENE-PERFORMERE`, + 'sprite.close': `Luk`, + 'sprite.foundConfirmed': `{found} fundet · {confirmed} bekræftet`, + 'sprite.confidence': `sikkerhed`, + 'sprite.name': `navn`, + 'sprite.hits': `hit`, + 'sprite.minConf': `Min. konf.`, + 'sprite.minConfTitle': `Minimums-sikkerhed: {percent}%`, + 'sprite.scanning': `Visage scanner…`, + 'sprite.cancel': `Annullér`, + 'sprite.empty': `Ingen performere identificeret i denne sprite.`, + 'sprite.detectedFaceAlt': `Registreret ansigt`, + 'sprite.spriteLabel': `SPRITE`, + 'sprite.stashLabel': `STASH`, + 'sprite.vs': `vs`, + 'sprite.inScene': `I scene`, + 'sprite.hitsCount': `{count} hit`, + 'sprite.totalTime': `{time} i alt`, + 'sprite.alreadyInScene': `Allerede i scene`, + 'sprite.clickToConfirm': `Klik for at bekræfte`, + 'sprite.confirmed': `Bekræftet`, + 'sprite.supportPatreon': `Støt på Patreon`, + 'sprite.confirmedCount': `{confirmed} af {total} bekræftet`, + 'sprite.shownHint': `{shown} vist ({total} i alt) · klik for at bekræfte · ←→ navigér · Enter bekræft`, + 'sprite.confirmHint': `Klik for at bekræfte · ←→ navigér · Enter bekræft`, + 'sprite.adding': `Tilføjer...`, + 'sprite.done': `Færdig ({count})`, + 'gender.male': `Mand`, + 'gender.female': `Kvinde`, + 'gender.transMale': `Transkønnet mand`, + 'gender.transFemale': `Transkønnet kvinde`, + 'gender.nonBinary': `Ikke-binær`, + 'gender.intersex': `Intersex`, + 'card.excellent': `Fremragende match`, + 'card.good': `Godt match`, + 'card.uncertain': `Usikkert match`, + 'card.select': `Vælg {name}`, + 'card.deselect': `Fravælg {name}`, + 'card.openOn': `Åbn på {source}`, + 'search.overlayHint': `Træk for at vælge et ansigt — Enter for at scanne hele rammen — Esc for at annullere`, + 'search.noFaces': `Ingen ansigter fundet i dette udsnit. Prøv en tættere beskæring, eller tryk på Enter for at scanne hele rammen.`, + 'search.captureMediaFail': `Kunne ikke optage mediet. Sørg for, at scenen/billedet er fuldt indlæst.`, + 'search.healthBanner': `Ansigtsgenkendelses-API'en er ikke tilgængelig. Start backend og prøv igen.`, + 'search.failed': `Ansigtssøgning mislykkedes: {error}`, + 'search.fetchImageFail': `Kunne ikke hente billede fra Stash.`, + 'search.captureFail': `Kunne ikke optage billede: {error}`, + 'search.selectFaceImage': `Vælg et ansigt i billedet.`, + 'search.captureFrameFail': `Kunne ikke optage den aktuelle ramme.`, + 'search.captureFrameFail2': `Optagelse af den aktuelle ramme mislykkedes.`, + 'search.selectFaceVideo': `Vælg et ansigt inden for videoafspillerområdet.`, + 'search.menuItemTitle': `Træk en boks om et ansigt, eller tryk på Enter for at scanne hele rammen, for at søge StashDB efter matches`, + 'search.currentFrame': `Visage: Aktuelle ramme`, + 'scene.noSprite': `Ingen sprite-ark eller forhåndsvisningsvideo fundet for denne scene. Generér dem i Scene-indstillingerne, og prøv igen.`, + 'scene.noFaces': `Ingen ansigter eller performere fundet i denne scenes sprite-ark eller forhåndsvisningsvideo.`, + 'scene.healthBanner': `Ansigtsgenkendelses-API'en er ikke tilgængelig. Start backend og prøv igen.`, + 'scene.failed': `Scene-scanning mislykkedes: {error}`, + 'scene.menuItemTitle': `Identificér hver performer i scenen (kræver et genereret sprite-ark eller en forhåndsvisningsvideo)`, + 'scene.wholeScene': `Visage: Hele scenen`, + 'banner.changeBackend': `Skift backend`, + 'banner.dismiss': `Afvis`, + 'error.dismiss': `Afvis`, + 'firstRun.title': `Konfigurér din Visage-backend`, + 'firstRun.subtitle': `Visage sender ansigtsbilleder til en backend til genkendelse. Vælg, hvor den skal køre.`, + 'firstRun.cloud': `Brug Hugging Face cloud`, + 'firstRun.cloudNote': `Ingen opsætning. Billeder sendes til Hugging Face cloud-tjenesten.`, + 'firstRun.local': `Brug min egen server`, + 'firstRun.localNote': `Kør den private binær på din egen maskine eller dit netværk.`, + 'firstRun.skip': `Spring over for nu`, + 'badge.local': `Lokal`, + 'badge.cloud': `Cloud (Hugging Face)`, + 'badge.title': `Visage-backend: {label}`, + 'donate.enjoying': `Kan du lide Visage? Hjælp med at holde det i live`, + 'donate.supportPatreon': `Støt på Patreon`, + 'frame.close': `Luk ramme-vælgeren`, + 'frame.seekFail': `Kunne ikke søge i videoafspilleren.`, + 'frame.selectAt': `Vælg ansigtsramme ved {time}s`, + }; + + // Hungarian. + const hu = { + 'backendSettings.title': `Backend beállítások`, + 'backendSettings.closeAria': `Beállítások bezárása`, + 'backendSettings.backendAria': `Backend beállítások`, + 'backendSettings.changeBackend': `Backend módosítása`, + 'backendSettings.backendLabel': `Backend`, + 'backendSettings.local': `Helyi`, + 'backendSettings.cloud': `Cloud (Hugging Face)`, + 'backendSettings.cloudNote': `A képeket a Hugging Face felhőszolgáltatásba küldjük.`, + 'backendSettings.hintPrefix': `Szeretnéd, hogy a képeid a hálózatodon maradjanak?`, + 'backendSettings.hintLink': `Privát szerver futtatása a Patreon segítségével`, + 'backendSettings.urlLabel': `URL`, + 'backendSettings.csp1': "A böngésző biztonsági szabályzata (CSP) alapértelmezés szerint csak a következőt engedélyezi:", + 'backendSettings.csp2': `Ha egy másik címen található helyi backendet szeretnél elérni (pl. a LAN-IP-d), add hozzá a`, + 'backendSettings.csp3': `listához a`, + 'backendSettings.csp4': `fájlban a Stash plugin-mappádban, különben a kérések blokkolva lesznek. Megjegyzés: a Visage frissítése újratelepíti a`, + 'backendSettings.csp5': `fájlt, ezért ezt minden frissítés után újra el kell végezni.`, + 'backendSettings.testing': `Kapcsolat tesztelése…`, + 'backendSettings.testConnection': `Kapcsolat tesztelése`, + 'backendSettings.testingShort': `Tesztelés…`, + 'backendSettings.cancel': `Mégse`, + 'backendSettings.save': `Mentés`, + 'backendSettings.feedback.reachable': `A kapcsolat sikeres. A backend készen áll.`, + 'backendSettings.feedback.degraded': `A backend elérhető, de korlátozott (a modellek vagy az index nincs betöltve).`, + 'backendSettings.feedback.unreachable': `A backend nem érhető el. Ellenőrizd az URL-t, és hogy a backend fut-e.`, + 'faceMatch.title': `AKTUÁLIS KERET`, + 'faceMatch.close': `Bezárás`, + 'faceMatch.facesSelected': `{faces} arc található · {selected} kiválasztva`, + 'faceMatch.inScene': `· {count} a jelenetben`, + 'faceMatch.stashboxMissing': `Nincs stash-box konfigurálva.`, + 'faceMatch.stashboxMissingBody': ` Az előadó-importálás engedélyezéséhez adj hozzá egy stash-box szolgáltatót a Beállítások → Metaadatszolgáltatók alatt.`, + 'faceMatch.stashboxWrongName': `Nem található "StashDB" szolgáltató.`, + 'faceMatch.stashboxWrongNameBody': ` Az előadó-importáláshoz "StashDB" nevű szolgáltató szükséges. Nevezd át a szolgáltatót a Beállítások → Metaadatszolgáltatók alatt.`, + 'faceMatch.learnMore': `További információk.`, + 'faceMatch.scanning': `Vizsgálat • arcfelismerés…`, + 'faceMatch.faceAlt': `{index}. arc`, + 'faceMatch.minConf': `Min. konf.`, + 'faceMatch.minConfTitle': `Minimális megbízhatóság: {percent}%`, + 'faceMatch.detected': `Észlelve`, + 'faceMatch.detectedFaceAlt': `Észlelt arc`, + 'faceMatch.vs': `vs`, + 'faceMatch.supportPatreon': `Támogatás a Patreonon`, + 'faceMatch.ofSelected': `{total} közül {selected} kiválasztva`, + 'faceMatch.allInScene': `{total} arc található · mind a jelenetben`, + 'faceMatch.clickToSelect': `{total} arc található · kattints a kiválasztáshoz`, + 'faceMatch.kbSwitch': `Arcváltás`, + 'faceMatch.kbSelect': `Előadók kiválasztása`, + 'faceMatch.kbToggle': `Kijelölés váltása`, + 'faceMatch.kbAddInstant': `Shift+kattintás az azonnali hozzáadáshoz`, + 'faceMatch.selectBest': `Legjobb egyezések kiválasztása`, + 'faceMatch.adding': `Hozzáadás...`, + 'faceMatch.done': `Kész ({count})`, + 'faceMatch.toast.added': `Előadó hozzáadva ehhez: {target}.`, + 'faceMatch.toast.addError': `Az előadó hozzáadása sikertelen: {error}`, + 'faceMatch.toast.noStashbox': `Nincs stash-box konfigurálva. Az előadó-importálás engedélyezéséhez adj hozzá egy stash-box szolgáltatót a Beállítások → Metaadatszolgáltatók alatt. Lásd: {url}`, + 'faceMatch.toast.noProvider': `Nem található "StashDB" nevű szolgáltató. Az előadó-importálás engedélyezéséhez nevezd át a szolgáltatót "StashDB"-re a Beállítások → Metaadatszolgáltatók alatt.`, + 'faceMatch.toast.configureProvider': `Az előadó-importálás engedélyezéséhez konfigurálj egy stash-box szolgáltatót a Beállítások → Metaadatszolgáltatók alatt.`, + 'faceMatch.toast.addedMultiple': `{count} előadó hozzáadva ehhez: {target}.`, + 'sprite.title': `JELENET ELŐADÓI`, + 'sprite.close': `Bezárás`, + 'sprite.foundConfirmed': `{found} található · {confirmed} megerősítve`, + 'sprite.confidence': `megbízhatóság`, + 'sprite.name': `név`, + 'sprite.hits': `találat`, + 'sprite.minConf': `Min. konf.`, + 'sprite.minConfTitle': `Minimális megbízhatóság: {percent}%`, + 'sprite.scanning': `Visage vizsgál…`, + 'sprite.cancel': `Mégse`, + 'sprite.empty': `Ebben a sprite-ban nem azonosítható előadó.`, + 'sprite.detectedFaceAlt': `Észlelt arc`, + 'sprite.spriteLabel': `SPRITE`, + 'sprite.stashLabel': `STASH`, + 'sprite.vs': `vs`, + 'sprite.inScene': `A jelenetben`, + 'sprite.hitsCount': `{count} találat`, + 'sprite.totalTime': `{time} összesen`, + 'sprite.alreadyInScene': `Már a jelenetben van`, + 'sprite.clickToConfirm': `Kattints a megerősítéshez`, + 'sprite.confirmed': `Megerősítve`, + 'sprite.supportPatreon': `Támogatás a Patreonon`, + 'sprite.confirmedCount': `{total} közül {confirmed} megerősítve`, + 'sprite.shownHint': `{shown} megjelenítve ({total} összesen) · kattints a megerősítéshez · ←→ navigálás · Enter megerősít`, + 'sprite.confirmHint': `Kattints a megerősítéshez · ←→ navigálás · Enter megerősít`, + 'sprite.adding': `Hozzáadás...`, + 'sprite.done': `Kész ({count})`, + 'gender.male': `Férfi`, + 'gender.female': `Nő`, + 'gender.transMale': `Transznemű férfi`, + 'gender.transFemale': `Transznemű nő`, + 'gender.nonBinary': `Nem-bináris`, + 'gender.intersex': `Interszex`, + 'card.excellent': `Kiváló egyezés`, + 'card.good': `Jó egyezés`, + 'card.uncertain': `Bizonytalan egyezés`, + 'card.select': `{name} kiválasztása`, + 'card.deselect': `{name} kijelölésének törlése`, + 'card.openOn': `Megnyitás ezen: {source}`, + 'search.overlayHint': `Húzd az arc kijelöléséhez — Enter az egész keret vizsgálatához — Esc a megszakításhoz`, + 'search.noFaces': `Ebben a kijelölésben nincs arc. Próbálj szorosabb vágást, vagy nyomj Entert az egész keret vizsgálatához.`, + 'search.captureMediaFail': `A média rögzítése nem sikerült. Győződj meg róla, hogy a jelenet/kép teljesen betöltött.`, + 'search.healthBanner': `Az arcfelismerő API nem érhető el. Indítsd el a backendet, és próbáld újra.`, + 'search.failed': `Arckeresés sikertelen: {error}`, + 'search.fetchImageFail': `A kép lekérése a Stashból nem sikerült.`, + 'search.captureFail': `A kép rögzítése nem sikerült: {error}`, + 'search.selectFaceImage': `Válassz egy arcot a képen.`, + 'search.captureFrameFail': `Az aktuális keret rögzítése nem sikerült.`, + 'search.captureFrameFail2': `Az aktuális keret rögzítése nem sikerült.`, + 'search.selectFaceVideo': `Válassz egy arcot a videolejátszó területén belül.`, + 'search.menuItemTitle': `Húzz egy keretet egy arc köré, vagy nyomj Entert az egész keret vizsgálatához, hogy egyezéseket keress a StashDB-ben`, + 'search.currentFrame': `Visage: Aktuális keret`, + 'scene.noSprite': `Ehhez a jelenethez nem található sprite-lap vagy előnézeti videó. Hozd létre őket a Scene beállításokban, majd próbáld újra.`, + 'scene.noFaces': `A jelenet sprite-lapjában vagy előnézeti videójában nem található arc vagy előadó.`, + 'scene.healthBanner': `Az arcfelismerő API nem érhető el. Indítsd el a backendet, és próbáld újra.`, + 'scene.failed': `Jelenetvizsgálat sikertelen: {error}`, + 'scene.menuItemTitle': `Azonosítsd a jelenet minden előadóját (létrehozott sprite-lap vagy előnézeti videó szükséges)`, + 'scene.wholeScene': `Visage: Teljes jelenet`, + 'banner.changeBackend': `Backend módosítása`, + 'banner.dismiss': `Elutasítás`, + 'error.dismiss': `Elutasítás`, + 'firstRun.title': `A Visage backend beállítása`, + 'firstRun.subtitle': `A Visage arc-képeket küld egy backendnek felismerés céljából. Válaszd ki, hol futtatod.`, + 'firstRun.cloud': `Hugging Face cloud használata`, + 'firstRun.cloudNote': `Nincs beállítás szükséges. A képeket a Hugging Face felhőszolgáltatásba küldjük.`, + 'firstRun.local': `A saját szerverem használata`, + 'firstRun.localNote': `Futtasd a privát binárist a saját gépeden vagy hálózatodon.`, + 'firstRun.skip': `Kihagyás most`, + 'badge.local': `Helyi`, + 'badge.cloud': `Cloud (Hugging Face)`, + 'badge.title': `Visage backend: {label}`, + 'donate.enjoying': `Tetszik a Visage? Segíts életben tartani`, + 'donate.supportPatreon': `Támogatás a Patreonon`, + 'frame.close': `Keretválasztó bezárása`, + 'frame.seekFail': `A videolejátszóban való keresés nem sikerült.`, + 'frame.selectAt': `Arckeret kiválasztása itt: {time}s`, + }; + + const uk = { + 'backendSettings.title': `Налаштування бекенду`, + 'backendSettings.closeAria': `Закрити налаштування`, + 'backendSettings.backendAria': `Налаштування бекенду`, + 'backendSettings.changeBackend': `Змінити бекенд`, + 'backendSettings.backendLabel': `Бекенд`, + 'backendSettings.local': `Локальний`, + 'backendSettings.cloud': `Хмара (Hugging Face)`, + 'backendSettings.cloudNote': `Зображення надсилаються до хмарного сервісу Hugging Face.`, + 'backendSettings.hintPrefix': `Хочете, щоб зображення залишалися у вашій мережі?`, + 'backendSettings.hintLink': `Запустіть приватний сервер через Patreon`, + 'backendSettings.urlLabel': `URL`, + 'backendSettings.csp1': `Політика безпеки браузера (CSP) за замовчуванням дозволяє лише`, + 'backendSettings.csp2': `. Щоб отримати доступ до локального бекенду за іншою адресою (наприклад, IP вашої локальної мережі), додайте його до`, + 'backendSettings.csp3': `списку у`, + 'backendSettings.csp4': `файлі у папці плагінів Stash, інакше запити будуть заблоковані. Примітка: оновлення Visage перевстановлює`, + 'backendSettings.csp5': `, тому це потрібно повторювати після кожного оновлення.`, + 'backendSettings.testing': `Перевірка підключення…`, + 'backendSettings.testConnection': `Перевірити підключення`, + 'backendSettings.testingShort': `Перевірка…`, + 'backendSettings.cancel': `Скасувати`, + 'backendSettings.save': `Зберегти`, + 'backendSettings.feedback.reachable': `Підключення успішне. Бекенд готовий.`, + 'backendSettings.feedback.degraded': `Бекенд доступний, але працює з обмеженнями (моделі або індекс не завантажені).`, + 'backendSettings.feedback.unreachable': `Бекенд недоступний. Перевірте URL і переконайтеся, що бекенд запущено.`, + 'faceMatch.title': `ПОТОЧНИЙ КАДР`, + 'faceMatch.close': `Закрити`, + 'faceMatch.facesSelected': `Знайдено облич: {faces} · вибрано: {selected}`, + 'faceMatch.inScene': `· {count} у сцені`, + 'faceMatch.stashboxMissing': `Stash-box не налаштовано.`, + 'faceMatch.stashboxMissingBody': ` Додайте провайдера stash-box у Settings → Metadata Providers, щоб увімкнути імпорт виконавців.`, + 'faceMatch.stashboxWrongName': `Провайдера "StashDB" не знайдено.`, + 'faceMatch.stashboxWrongNameBody': ` Імпорт виконавців потребує провайдера з назвою "StashDB". Перейменуйте провайдера у Settings → Metadata Providers.`, + 'faceMatch.learnMore': `Дізнатися більше.`, + 'faceMatch.scanning': `Сканування • розпізнавання облич…`, + 'faceMatch.faceAlt': `Обличчя {index}`, + 'faceMatch.minConf': `Мін. дост.`, + 'faceMatch.minConfTitle': `Мінімальна достовірність: {percent}%`, + 'faceMatch.detected': `Виявлено`, + 'faceMatch.detectedFaceAlt': `Виявлене обличчя`, + 'faceMatch.vs': `vs`, + 'faceMatch.supportPatreon': `Підтримати на Patreon`, + 'faceMatch.ofSelected': `Вибрано {selected} з {total}`, + 'faceMatch.allInScene': `Знайдено облич: {total} · усі у сцені`, + 'faceMatch.clickToSelect': `Знайдено облич: {total} · натисніть, щоб вибрати`, + 'faceMatch.kbSwitch': `Перемикання облич`, + 'faceMatch.kbSelect': `Вибір виконавців`, + 'faceMatch.kbToggle': `Перемкнути вибір`, + 'faceMatch.kbAddInstant': `Shift+клік, щоб додати одразу`, + 'faceMatch.selectBest': `Вибрати найкращі збіги`, + 'faceMatch.adding': `Додавання...`, + 'faceMatch.done': `Готово ({count})`, + 'faceMatch.toast.added': `Виконавця додано до {target}.`, + 'faceMatch.toast.addError': `Не вдалося додати виконавця: {error}`, + 'faceMatch.toast.noStashbox': `Stash-box не налаштовано. Додайте провайдера stash-box у Settings → Metadata Providers, щоб увімкнути імпорт виконавців. Див. {url}`, + 'faceMatch.toast.noProvider': `Провайдера "StashDB" не знайдено. Перейменуйте провайдера на "StashDB" у Settings → Metadata Providers, щоб увімкнути імпорт виконавців.`, + 'faceMatch.toast.configureProvider': `Налаштуйте провайдера stash-box у Settings → Metadata Providers, щоб увімкнути імпорт виконавців.`, + 'faceMatch.toast.addedMultiple': `Додано виконавців: {count} у {target}.`, + 'sprite.title': `ВИКОНАВЦІ СЦЕНИ`, + 'sprite.close': `Закрити`, + 'sprite.foundConfirmed': `Знайдено: {found} · підтверджено: {confirmed}`, + 'sprite.confidence': `достовірність`, + 'sprite.name': `ім'я`, + 'sprite.hits': `збіги`, + 'sprite.minConf': `Мін. дост.`, + 'sprite.minConfTitle': `Мінімальна достовірність: {percent}%`, + 'sprite.scanning': `Сканування Visage…`, + 'sprite.cancel': `Скасувати`, + 'sprite.empty': `У цьому спрайті виконавців не виявлено.`, + 'sprite.detectedFaceAlt': `Виявлене обличчя`, + 'sprite.spriteLabel': `СПРАЙТ`, + 'sprite.stashLabel': `STASH`, + 'sprite.vs': `vs`, + 'sprite.inScene': `У сцені`, + 'sprite.hitsCount': `Збігів: {count}`, + 'sprite.totalTime': `Усього {time}`, + 'sprite.alreadyInScene': `Уже у сцені`, + 'sprite.clickToConfirm': `Натисніть для підтвердження`, + 'sprite.confirmed': `Підтверджено`, + 'sprite.supportPatreon': `Підтримати на Patreon`, + 'sprite.confirmedCount': `Підтверджено {confirmed} з {total}`, + 'sprite.shownHint': `Показано: {shown} (усього {total}) · натисніть для підтвердження · ←→ навігація · Enter підтвердження`, + 'sprite.confirmHint': `Натисніть для підтвердження · ←→ навігація · Enter підтвердження`, + 'sprite.adding': `Додавання...`, + 'sprite.done': `Готово ({count})`, + 'gender.male': `Чоловік`, + 'gender.female': `Жінка`, + 'gender.transMale': `Трансгендерний чоловік`, + 'gender.transFemale': `Трансгендерна жінка`, + 'gender.nonBinary': `Небінарний`, + 'gender.intersex': `Інтерсекс`, + 'card.excellent': `Відмінний збіг`, + 'card.good': `Хороший збіг`, + 'card.uncertain': `Невпевнений збіг`, + 'card.select': `Вибрати {name}`, + 'card.deselect': `Зняти вибір з {name}`, + 'card.openOn': `Відкрити на {source}`, + 'search.overlayHint': `Перетягніть, щоб виділити обличчя — Enter, щоб просканувати весь кадр — Esc, щоб скасувати`, + 'search.noFaces': `У вибраній області облич не знайдено. Спробуйте щільніший кадр або натисніть Enter, щоб просканувати весь кадр.`, + 'search.captureMediaFail': `Не вдалося захопити медіа. Переконайтеся, що сцена/зображення повністю завантажені.`, + 'search.healthBanner': `API розпізнавання облич недоступний. Запустіть бекенд і спробуйте знову.`, + 'search.failed': `Пошук облич не вдався: {error}`, + 'search.fetchImageFail': `Не вдалося отримати зображення з Stash.`, + 'search.captureFail': `Не вдалося захопити зображення: {error}`, + 'search.selectFaceImage': `Виділіть обличчя в зображенні.`, + 'search.captureFrameFail': `Не вдалося захопити поточний кадр.`, + 'search.captureFrameFail2': `Помилка під час захоплення поточного кадру.`, + 'search.selectFaceVideo': `Виділіть обличчя в області відеоплеєра.`, + 'search.menuItemTitle': `Перетягніть рамку навколо обличчя або натисніть Enter, щоб просканувати весь кадр і виконати пошук збігів у StashDB`, + 'search.currentFrame': `Visage: поточний кадр`, + 'scene.noSprite': `Для цієї сцени не знайдено ані спрайт-листа, ані прев'ю-відео. Створіть їх у налаштуваннях сцени та спробуйте знову.`, + 'scene.noFaces': `У спрайт-листі або прев'ю-відео цієї сцени не знайдено облич або виконавців.`, + 'scene.healthBanner': `API розпізнавання облич недоступний. Запустіть бекенд і спробуйте знову.`, + 'scene.failed': `Сканування сцени не вдалося: {error}`, + 'scene.menuItemTitle': `Визначте кожного виконавця у сцені (потрібен згенерований спрайт-лист або прев'ю-відео)`, + 'scene.wholeScene': `Visage: вся сцена`, + 'banner.changeBackend': `Змінити бекенд`, + 'banner.dismiss': `Відхилити`, + 'error.dismiss': `Відхилити`, + 'firstRun.title': `Налаштуйте бекенд Visage`, + 'firstRun.subtitle': `Visage надсилає зображення облич до бекенду для розпізнавання. Виберіть, де його запустити.`, + 'firstRun.cloud': `Використовувати хмару Hugging Face`, + 'firstRun.cloudNote': `Без налаштування. Зображення надсилаються до хмарного сервісу Hugging Face.`, + 'firstRun.local': `Використовувати свій сервер`, + 'firstRun.localNote': `Запустіть приватний бінарник на власній машині або у власній мережі.`, + 'firstRun.skip': `Пропустити`, + 'badge.local': `Локальний`, + 'badge.cloud': `Хмара (Hugging Face)`, + 'badge.title': `Бекенд Visage: {label}`, + 'donate.enjoying': `Подобається Visage? Допоможіть зберегти його`, + 'donate.supportPatreon': `Підтримати на Patreon`, + 'frame.close': `Закрити вибір кадру`, + 'frame.seekFail': `Не вдалося перемотати відеоплеєр.`, + 'frame.selectAt': `Вибрати кадр обличчя на {time} с`, + }; + + // German. Missing keys fall back to English automatically. + const de = { + // ---- BackendSettings.tsx ---- + 'backendSettings.title': `Backend-Einstellungen`, + 'backendSettings.closeAria': `Einstellungen schließen`, + 'backendSettings.backendAria': `Backend-Einstellungen`, + 'backendSettings.changeBackend': `Backend ändern`, + 'backendSettings.backendLabel': `Backend`, + 'backendSettings.local': `Lokal`, + 'backendSettings.cloud': `Cloud (Hugging Face)`, + 'backendSettings.cloudNote': `Bilder werden an den Hugging Face Cloud-Dienst gesendet.`, + 'backendSettings.hintPrefix': `Möchtest du, dass deine Bilder in deinem Netzwerk bleiben?`, + 'backendSettings.hintLink': `Einen privaten Server über Patreon betreiben`, + 'backendSettings.urlLabel': `URL`, + 'backendSettings.csp1': `Die Sicherheitsrichtlinie (CSP) des Browsers erlaubt nur`, + 'backendSettings.csp2': `standardmäßig. Um ein lokales Backend unter einer anderen Adresse (z. B. deiner LAN-IP) zu erreichen, füge es zur`, + 'backendSettings.csp3': `Liste in der`, + 'backendSettings.csp4': `Datei in deinem Stash-Plugins-Ordner hinzu, sonst werden Anfragen blockiert. Hinweis: Ein Update von Visage installiert`, + 'backendSettings.csp5': `neu, daher muss dies nach jedem Update erneut angewendet werden.`, + 'backendSettings.testing': `Verbindung wird getestet…`, + 'backendSettings.testConnection': `Verbindung testen`, + 'backendSettings.testingShort': `Testen…`, + 'backendSettings.cancel': `Abbrechen`, + 'backendSettings.save': `Speichern`, + 'backendSettings.feedback.reachable': `Verbindung erfolgreich. Das Backend ist bereit.`, + 'backendSettings.feedback.degraded': `Backend erreichbar, aber eingeschränkt (Modelle oder Index nicht geladen).`, + 'backendSettings.feedback.unreachable': `Backend nicht erreichbar. Prüfe die URL und ob das Backend läuft.`, + // ---- FaceMatchModal.tsx ---- + 'faceMatch.title': `AKTUELLES BILD`, + 'faceMatch.close': `Schließen`, + 'faceMatch.facesSelected': `{faces} Gesichter gefunden · {selected} ausgewählt`, + 'faceMatch.inScene': `· {count} in der Szene`, + 'faceMatch.stashboxMissing': `Kein Stash-box konfiguriert.`, + 'faceMatch.stashboxMissingBody': ` Füge in Einstellungen → Metadaten-Anbieter einen Stash-box-Anbieter hinzu, um den Performer-Import zu aktivieren.`, + 'faceMatch.stashboxWrongName': `Kein Anbieter namens "StashDB" gefunden.`, + 'faceMatch.stashboxWrongNameBody': ` Der Performer-Import erfordert einen Anbieter namens "StashDB". Benenne deinen Anbieter in Einstellungen → Metadaten-Anbieter um.`, + 'faceMatch.learnMore': `Mehr erfahren.`, + 'faceMatch.scanning': `Scannen • Gesichtserkennung…`, + 'faceMatch.faceAlt': `Gesicht {index}`, + 'faceMatch.minConf': `Min. Konf.`, + 'faceMatch.minConfTitle': `Minimale Konfidenz: {percent}%`, + 'faceMatch.detected': `Erkannt`, + 'faceMatch.detectedFaceAlt': `Erkanntes Gesicht`, + 'faceMatch.vs': `vs`, + 'faceMatch.supportPatreon': `Auf Patreon unterstützen`, + 'faceMatch.ofSelected': `{selected} von {total} ausgewählt`, + 'faceMatch.allInScene': `{total} Gesichter gefunden · alle in der Szene`, + 'faceMatch.clickToSelect': `{total} Gesichter gefunden · zum Auswählen klicken`, + 'faceMatch.kbSwitch': `Gesichter wechseln`, + 'faceMatch.kbSelect': `Performer auswählen`, + 'faceMatch.kbToggle': `Auswahl umschalten`, + 'faceMatch.kbAddInstant': `Umschalt+Klick zum sofortigen Hinzufügen`, + 'faceMatch.selectBest': `Beste Treffer auswählen`, + 'faceMatch.adding': `Hinzufügen...`, + 'faceMatch.done': `Fertig ({count})`, + 'faceMatch.toast.added': `Performer zum {target} hinzugefügt.`, + 'faceMatch.toast.addError': `Performer konnte nicht hinzugefügt werden: {error}`, + 'faceMatch.toast.noStashbox': `Kein Stash-box konfiguriert. Füge in Einstellungen → Metadaten-Anbieter einen Stash-box-Anbieter hinzu, um den Performer-Import zu aktivieren. Siehe {url}`, + 'faceMatch.toast.noProvider': `Kein Anbieter namens "StashDB" gefunden. Benenne deinen Anbieter in Einstellungen → Metadaten-Anbieter in "StashDB" um, um den Performer-Import zu aktivieren.`, + 'faceMatch.toast.configureProvider': `Konfiguriere in Einstellungen → Metadaten-Anbieter einen Stash-box-Anbieter, um den Performer-Import zu aktivieren.`, + 'faceMatch.toast.addedMultiple': `{count} Performer{s} zum {target} hinzugefügt.`, + // ---- SpriteResultModal.tsx ---- + 'sprite.title': `SZENEN-PERFORMER`, + 'sprite.close': `Schließen`, + 'sprite.foundConfirmed': `{found} gefunden · {confirmed} bestätigt`, + 'sprite.confidence': `Konfidenz`, + 'sprite.name': `Name`, + 'sprite.hits': `Treffer`, + 'sprite.minConf': `Min. Konf.`, + 'sprite.minConfTitle': `Minimale Konfidenz: {percent}%`, + 'sprite.scanning': `Visage wird gescannt…`, + 'sprite.cancel': `Abbrechen`, + 'sprite.empty': `In diesem Sprite wurden keine Performer identifiziert.`, + 'sprite.detectedFaceAlt': `Erkanntes Gesicht`, + 'sprite.spriteLabel': `SPRITE`, + 'sprite.stashLabel': `STASH`, + 'sprite.vs': `vs`, + 'sprite.inScene': `In der Szene`, + 'sprite.hitsCount': `{count} Treffer{s}`, + 'sprite.totalTime': `{time} gesamt`, + 'sprite.alreadyInScene': `Bereits in der Szene`, + 'sprite.clickToConfirm': `Zum Bestätigen klicken`, + 'sprite.confirmed': `Bestätigt`, + 'sprite.supportPatreon': `Auf Patreon unterstützen`, + 'sprite.confirmedCount': `{confirmed} von {total} bestätigt`, + 'sprite.shownHint': `{shown} angezeigt ({total} gesamt) · zum Bestätigen klicken · ←→ navigieren · Enter bestätigen`, + 'sprite.confirmHint': `Zum Bestätigen klicken · ←→ navigieren · Enter bestätigen`, + 'sprite.adding': `Hinzufügen...`, + 'sprite.done': `Fertig ({count})`, + // ---- PerformerCard.tsx ---- + 'gender.male': `Männlich`, + 'gender.female': `Weiblich`, + 'gender.transMale': `Transgender-Mann`, + 'gender.transFemale': `Transgender-Frau`, + 'gender.nonBinary': `Nicht-binär`, + 'gender.intersex': `Intergeschlechtlich`, + 'card.excellent': `Hervorragende Übereinstimmung`, + 'card.good': `Gute Übereinstimmung`, + 'card.uncertain': `Unsichere Übereinstimmung`, + 'card.select': `{name} auswählen`, + 'card.deselect': `{name} abwählen`, + 'card.openOn': `Auf {source} öffnen`, + // ---- FaceSearchButton.tsx ---- + 'search.overlayHint': `Ziehen, um ein Gesicht auszuwählen — Enter, um das ganze Bild zu scannen — Esc zum Abbrechen`, + 'search.noFaces': `In dieser Auswahl wurden keine Gesichter gefunden. Versuche einen engeren Zuschnitt oder drücke Enter, um das ganze Bild zu scannen.`, + 'search.captureMediaFail': `Medien konnten nicht erfasst werden. Stelle sicher, dass Szene/Bild vollständig geladen ist.`, + 'search.healthBanner': `Die Gesichtserkennungs-API ist nicht erreichbar. Starte das Backend und versuche es erneut.`, + 'search.failed': `Gesichtssuche fehlgeschlagen: {error}`, + 'search.fetchImageFail': `Bild konnte nicht von Stash abgerufen werden.`, + 'search.captureFail': `Bildaufnahme fehlgeschlagen: {error}`, + 'search.selectFaceImage': `Wähle ein Gesicht im Bild aus.`, + 'search.captureFrameFail': `Aktuelles Bild konnte nicht erfasst werden.`, + 'search.captureFrameFail2': `Aktuelles Bild konnte nicht erfasst werden.`, + 'search.selectFaceVideo': `Wähle ein Gesicht im Videoplayer-Bereich aus.`, + 'search.menuItemTitle': `Ziehe ein Feld um ein Gesicht oder drücke Enter, um das ganze Bild zu scannen, um in StashDB nach Übereinstimmungen zu suchen`, + 'search.currentFrame': `Visage: Aktuelles Bild`, + // ---- SceneScanButton.tsx ---- + 'scene.noSprite': `Für diese Szene wurde kein Sprite-Sheet oder Vorschaubild gefunden. Erzeuge sie in den Szene-Einstellungen und versuche es erneut.`, + 'scene.noFaces': `Im Sprite-Sheet oder Vorschaubild dieser Szene wurden keine Gesichter oder Performer gefunden.`, + 'scene.healthBanner': `Die Gesichtserkennungs-API ist nicht erreichbar. Starte das Backend und versuche es erneut.`, + 'scene.failed': `Szenen-Scan fehlgeschlagen: {error}`, + 'scene.menuItemTitle': `Jeden Performer in der Szene identifizieren (benötigt ein erzeugtes Sprite-Sheet oder Vorschaubild)`, + 'scene.wholeScene': `Visage: Gesamte Szene`, + // ---- BackendHealthBanner.tsx ---- + 'banner.changeBackend': `Backend ändern`, + 'banner.dismiss': `Ausblenden`, + // ---- ErrorDialog.tsx ---- + 'error.dismiss': `Ausblenden`, + // ---- FirstRunDialog.tsx ---- + 'firstRun.title': `Richte dein Visage-Backend ein`, + 'firstRun.subtitle': `Visage sendet Gesichtsbilder zur Erkennung an ein Backend. Wähle, wo es ausgeführt werden soll.`, + 'firstRun.cloud': `Hugging Face Cloud verwenden`, + 'firstRun.cloudNote': `Keine Einrichtung nötig. Bilder werden an den Hugging Face Cloud-Dienst gesendet.`, + 'firstRun.local': `Eigene Server verwenden`, + 'firstRun.localNote': `Führe das private Programm auf deinem eigenen Rechner oder Netzwerk aus.`, + 'firstRun.skip': `Vorerst überspringen`, + // ---- BackendBadge.tsx ---- + 'badge.local': `Lokal`, + 'badge.cloud': `Cloud (Hugging Face)`, + 'badge.title': `Visage-Backend: {label}`, + // ---- DonateFooter.tsx ---- + 'donate.enjoying': `Gefällt dir Visage? Hilf mit, es am Leben zu halten`, + 'donate.supportPatreon': `Auf Patreon unterstützen`, + // ---- FaceFrameSelector.tsx ---- + 'frame.close': `Bildauswahl schließen`, + 'frame.seekFail': `Der Videoplayer konnte nicht gesucht werden.`, + 'frame.selectAt': `Gesichtsbild bei {time}s auswählen`, + }; + + // Finnish. + const fi = { + 'backendSettings.title': `Backend-asetukset`, + 'backendSettings.closeAria': `Sulje asetukset`, + 'backendSettings.backendAria': `Backend-asetukset`, + 'backendSettings.changeBackend': `Vaihda backend`, + 'backendSettings.backendLabel': `Backend`, + 'backendSettings.local': `Paikallinen`, + 'backendSettings.cloud': `Cloud (Hugging Face)`, + 'backendSettings.cloudNote': `Kuvat lähetetään Hugging Face -pilvipalveluun.`, + 'backendSettings.hintPrefix': `Haluatko, että kuvasi pysyvät verkossasi?`, + 'backendSettings.hintLink': `Aja yksityinen palvelin Patreonin kautta`, + 'backendSettings.urlLabel': `URL`, + 'backendSettings.csp1': "Selaimen suojauskäytäntö (CSP) sallii oletuksena vain", + 'backendSettings.csp2': `Jos haluat käyttää paikallista backendia toisessa osoitteessa (esim. LAN-IP-osoitteessasi), lisää se`, + 'backendSettings.csp3': `luetteloon`, + 'backendSettings.csp4': `-tiedostossa Stash-liitännäiskansiossasi, muuten pyynnöt estetään. Huomio: Visagen päivitys asentaa`, + 'backendSettings.csp5': `:n uudelleen, joten tämä on tehtävä uudelleen jokaisen päivityksen jälkeen.`, + 'backendSettings.testing': `Testataan yhteyttä…`, + 'backendSettings.testConnection': `Testaa yhteys`, + 'backendSettings.testingShort': `Testataan…`, + 'backendSettings.cancel': `Peruuta`, + 'backendSettings.save': `Tallenna`, + 'backendSettings.feedback.reachable': `Yhteys onnistui. Backend on valmis.`, + 'backendSettings.feedback.degraded': `Backend on tavoitettavissa mutta heikentynyt (mallit tai indeksi ei ole ladattu).`, + 'backendSettings.feedback.unreachable': `Backend ei ole tavoitettavissa. Tarkista URL ja että backend on käynnissä.`, + 'faceMatch.title': `NYKYINEN RUUTU`, + 'faceMatch.close': `Sulje`, + 'faceMatch.facesSelected': `{faces} kasvoa löydetty · {selected} valittu`, + 'faceMatch.inScene': `· {count} kohtauksessa`, + 'faceMatch.stashboxMissing': `Stash-boxia ei ole määritetty.`, + 'faceMatch.stashboxMissingBody': ` Lisää stash-box-palveluntarjoaja kohdassa Asetukset → Metadatan tarjoajat aktivoidaksesi esiintyjien tuonnin.`, + 'faceMatch.stashboxWrongName': `Palveluntarjoajaa "StashDB" ei löytynyt.`, + 'faceMatch.stashboxWrongNameBody': ` Esiintyjien tuonti edellyttää palveluntarjoajaa nimeltä "StashDB". Nimeä palveluntarjoaja uudelleen kohdassa Asetukset → Metadatan tarjoajat.`, + 'faceMatch.learnMore': `Lue lisää.`, + 'faceMatch.scanning': `Skannataan • kasvojentunnistus…`, + 'faceMatch.faceAlt': `Kasvot {index}`, + 'faceMatch.minConf': `Min. luot.`, + 'faceMatch.minConfTitle': `Minimiluottamus: {percent}%`, + 'faceMatch.detected': `Havaittu`, + 'faceMatch.detectedFaceAlt': `Havaittu kasvot`, + 'faceMatch.vs': `vs`, + 'faceMatch.supportPatreon': `Tue Patreonissa`, + 'faceMatch.ofSelected': `{selected} / {total} valittu`, + 'faceMatch.allInScene': `{total} kasvoa löydetty · kaikki kohtauksessa`, + 'faceMatch.clickToSelect': `{total} kasvoa löydetty · valitse napsauttamalla`, + 'faceMatch.kbSwitch': `Vaihda kasvoja`, + 'faceMatch.kbSelect': `Valitse esiintyjät`, + 'faceMatch.kbToggle': `Vaihda valintaa`, + 'faceMatch.kbAddInstant': `Shift+klikkaus lisää välittömästi`, + 'faceMatch.selectBest': `Valitse parhaat osumat`, + 'faceMatch.adding': `Lisätään...`, + 'faceMatch.done': `Valmis ({count})`, + 'faceMatch.toast.added': `Esiintyjä lisätty kohteeseen {target}.`, + 'faceMatch.toast.addError': `Esiintyjän lisääminen epäonnistui: {error}`, + 'faceMatch.toast.noStashbox': `Stash-boxia ei ole määritetty. Lisää stash-box-palveluntarjoaja kohdassa Asetukset → Metadatan tarjoajat aktivoidaksesi esiintyjien tuonnin. Katso {url}`, + 'faceMatch.toast.noProvider': `Palveluntarjoajaa nimeltä "StashDB" ei löytynyt. Nimeä palveluntarjoaja uudelleen "StashDB":ksi kohdassa Asetukset → Metadatan tarjoajat aktivoidaksesi esiintyjien tuonnin.`, + 'faceMatch.toast.configureProvider': `Määritä stash-box-palveluntarjoaja kohdassa Asetukset → Metadatan tarjoajat aktivoidaksesi esiintyjien tuonnin.`, + 'faceMatch.toast.addedMultiple': `Lisätty {count} esiintyjää kohteeseen {target}.`, + 'sprite.title': `KOHTAUKSEN ESIINTYJÄT`, + 'sprite.close': `Sulje`, + 'sprite.foundConfirmed': `{found} löydetty · {confirmed} vahvistettu`, + 'sprite.confidence': `luottamus`, + 'sprite.name': `nimi`, + 'sprite.hits': `osumat`, + 'sprite.minConf': `Min. luot.`, + 'sprite.minConfTitle': `Minimiluottamus: {percent}%`, + 'sprite.scanning': `Visage skannaa…`, + 'sprite.cancel': `Peruuta`, + 'sprite.empty': `Tästä spritestä ei tunnistettu esiintyjiä.`, + 'sprite.detectedFaceAlt': `Havaittu kasvot`, + 'sprite.spriteLabel': `SPRITE`, + 'sprite.stashLabel': `STASH`, + 'sprite.vs': `vs`, + 'sprite.inScene': `Kohtauksessa`, + 'sprite.hitsCount': `{count} osuma`, + 'sprite.totalTime': `{time} yhteensä`, + 'sprite.alreadyInScene': `Jo kohtauksessa`, + 'sprite.clickToConfirm': `Vahvista napsauttamalla`, + 'sprite.confirmed': `Vahvistettu`, + 'sprite.supportPatreon': `Tue Patreonissa`, + 'sprite.confirmedCount': `{confirmed} / {total} vahvistettu`, + 'sprite.shownHint': `{shown} näytetty ({total} yhteensä) · vahvista napsauttamalla · ←→ navigoi · Enter vahvista`, + 'sprite.confirmHint': `Vahvista napsauttamalla · ←→ navigoi · Enter vahvista`, + 'sprite.adding': `Lisätään...`, + 'sprite.done': `Valmis ({count})`, + 'gender.male': `Mies`, + 'gender.female': `Nainen`, + 'gender.transMale': `Transmies`, + 'gender.transFemale': `Transnainen`, + 'gender.nonBinary': `Muunsukupuolinen`, + 'gender.intersex': `Intersukupuolinen`, + 'card.excellent': `Erinomainen osuma`, + 'card.good': `Hyvä osuma`, + 'card.uncertain': `Epävarma osuma`, + 'card.select': `Valitse {name}`, + 'card.deselect': `Poista valinta {name}`, + 'card.openOn': `Avaa kohteessa {source}`, + 'search.overlayHint': `Valitse kasvot vetämällä — Enter skannaa koko ruudun — Esc peruuttaa`, + 'search.noFaces': `Valinnasta ei löytynyt kasvoja. Kokeile tarkempaa rajausta tai paina Enter skannataksesi koko ruudun.`, + 'search.captureMediaFail': `Median tallentaminen epäonnistui. Varmista, että kohtaus/kuva on täysin ladattu.`, + 'search.healthBanner': `Kasvojentunnistuksen API ei ole tavoitettavissa. Käynnistä backend ja yritä uudelleen.`, + 'search.failed': `Kasvohaku epäonnistui: {error}`, + 'search.fetchImageFail': `Kuvan noutaminen Stashista epäonnistui.`, + 'search.captureFail': `Kuvan tallentaminen epäonnistui: {error}`, + 'search.selectFaceImage': `Valitse kasvot kuvasta.`, + 'search.captureFrameFail': `Nykyisen ruudun tallentaminen epäonnistui.`, + 'search.captureFrameFail2': `Nykyisen ruudun tallentaminen ei onnistunut.`, + 'search.selectFaceVideo': `Valitse kasvot videotoistimen alueelta.`, + 'search.menuItemTitle': `Vedä laatikko kasvojen ympärille tai paina Enter skannataksesi koko ruudun hakeaksesi osumia StashDB:stä`, + 'search.currentFrame': `Visage: Nykyinen ruutu`, + 'scene.noSprite': `Tälle kohtaukselle ei löytynyt sprite-arkkia tai esikatseluvideota. Luo ne Scene-asetuksissa ja yritä sitten uudelleen.`, + 'scene.noFaces': `Tämän kohtauksen sprite-arkista tai esikatseluvideosta ei löytynyt kasvoja tai esiintyjiä.`, + 'scene.healthBanner': `Kasvojentunnistuksen API ei ole tavoitettavissa. Käynnistä backend ja yritä uudelleen.`, + 'scene.failed': `Kohtauksen skannaus epäonnistui: {error}`, + 'scene.menuItemTitle': `Tunnista jokainen kohtauksen esiintyjä (edellyttää luotua sprite-arkkia tai esikatseluvideota)`, + 'scene.wholeScene': `Visage: Koko kohtaus`, + 'banner.changeBackend': `Vaihda backend`, + 'banner.dismiss': `Hylkää`, + 'error.dismiss': `Hylkää`, + 'firstRun.title': `Määritä Visage-backendisi`, + 'firstRun.subtitle': `Visage lähettää kasvokuvia backendille tunnistusta varten. Valitse, missä se suoritetaan.`, + 'firstRun.cloud': `Käytä Hugging Face cloudia`, + 'firstRun.cloudNote': `Ei asennusta. Kuvat lähetetään Hugging Face -pilvipalveluun.`, + 'firstRun.local': `Käytä omaa palvelintani`, + 'firstRun.localNote': `Aja yksityinen binääri omalla koneellasi tai verkossasi.`, + 'firstRun.skip': `Ohita toistaiseksi`, + 'badge.local': `Paikallinen`, + 'badge.cloud': `Cloud (Hugging Face)`, + 'badge.title': `Visage-backend: {label}`, + 'donate.enjoying': `Pidätkö Visagesta? Auta pitämään se hengissä`, + 'donate.supportPatreon': `Tue Patreonissa`, + 'frame.close': `Sulje ruutuvalitsin`, + 'frame.seekFail': `Videotoistimen siirtyminen epäonnistui.`, + 'frame.selectAt': `Valitse kasvoruutu kohdassa {time}s`, + }; + + const lv = { + 'backendSettings.title': `Pamata pakalpojuma iestatījumi`, + 'backendSettings.closeAria': `Aizvērt iestatījumus`, + 'backendSettings.backendAria': `Pamata pakalpojuma iestatījumi`, + 'backendSettings.changeBackend': `Mainīt pamata pakalpojumu`, + 'backendSettings.backendLabel': `Pamata pakalpojums`, + 'backendSettings.local': `Lokāls`, + 'backendSettings.cloud': `Mākonis (Hugging Face)`, + 'backendSettings.cloudNote': `Attēli tiek nosūtīti uz Hugging Face mākoņpakalpojumu.`, + 'backendSettings.hintPrefix': `Vai vēlaties, lai jūsu attēli paliktu jūsu tīklā?`, + 'backendSettings.hintLink': `Palaidiet privātu serveri, izmantojot Patreon`, + 'backendSettings.urlLabel': `URL`, + 'backendSettings.csp1': `Pārlūka drošības politika (CSP) pēc noklusējuma atļauj tikai`, + 'backendSettings.csp2': `. Lai sasniegtu lokālu pamata pakalpojumu citā adresē (piemēram, jūsu LAN IP), pievienojiet to`, + 'backendSettings.csp3': `sarakstam failā`, + 'backendSettings.csp4': `Stash spraudņu mapē, pretējā gadījumā pieprasījumi tiks bloķēti. Ņemiet vērā: Visage atjaunināšana pārinstalē`, + 'backendSettings.csp5': `, tāpēc tas ir jāatkārto pēc katra atjauninājuma.`, + 'backendSettings.testing': `Savienojuma pārbaude…`, + 'backendSettings.testConnection': `Pārbaudīt savienojumu`, + 'backendSettings.testingShort': `Pārbaude…`, + 'backendSettings.cancel': `Atcelt`, + 'backendSettings.save': `Saglabāt`, + 'backendSettings.feedback.reachable': `Savienojums izdevies. Pamata pakalpojums ir gatavs.`, + 'backendSettings.feedback.degraded': `Pamata pakalpojums sasniedzams, bet samazinātā režīmā (modeļi vai indekss nav ielādēti).`, + 'backendSettings.feedback.unreachable': `Pamata pakalpojums nesasniedzams. Pārbaudiet URL un pārliecinieties, ka pakalpojums darbojas.`, + 'faceMatch.title': `PAŠREIZĒJAIS KADRĪTIS`, + 'faceMatch.close': `Aizvērt`, + 'faceMatch.facesSelected': `Atrastas sejas: {faces} · atlasītas: {selected}`, + 'faceMatch.inScene': `· {count} ainā`, + 'faceMatch.stashboxMissing': `Nav konfigurēts stash-box.`, + 'faceMatch.stashboxMissingBody': ` Pievienojiet stash-box nodrošinātāju vietnē Settings → Metadata Providers, lai iespējotu izpildītāju importēšanu.`, + 'faceMatch.stashboxWrongName': `Nodrošinātājs "StashDB" nav atrasts.`, + 'faceMatch.stashboxWrongNameBody': ` Izpildītāju importēšanai nepieciešams nodrošinātājs ar nosaukumu "StashDB". Pārdēvējiet nodrošinātāju vietnē Settings → Metadata Providers.`, + 'faceMatch.learnMore': `Uzziniet vairāk.`, + 'faceMatch.scanning': `Skenēšana • seju atpazīšana…`, + 'faceMatch.faceAlt': `Seja {index}`, + 'faceMatch.minConf': `Min. ticam.`, + 'faceMatch.minConfTitle': `Minimālā ticamība: {percent}%`, + 'faceMatch.detected': `Atklāts`, + 'faceMatch.detectedFaceAlt': `Atklātā seja`, + 'faceMatch.vs': `vs`, + 'faceMatch.supportPatreon': `Atbalstīt pakalpojumā Patreon`, + 'faceMatch.ofSelected': `Atlasītas {selected} no {total}`, + 'faceMatch.allInScene': `Atrastas sejas: {total} · visas ainā`, + 'faceMatch.clickToSelect': `Atrastas sejas: {total} · noklikšķiniet, lai atlasītu`, + 'faceMatch.kbSwitch': `Seju pārslēgšana`, + 'faceMatch.kbSelect': `Izpildītāju atlase`, + 'faceMatch.kbToggle': `Pārslēgt atlasi`, + 'faceMatch.kbAddInstant': `Shift+klikšķis, lai pievienotu uzreiz`, + 'faceMatch.selectBest': `Izvēlēties labākās atbilstības`, + 'faceMatch.adding': `Pievienošana...`, + 'faceMatch.done': `Gatavs ({count})`, + 'faceMatch.toast.added': `Izpildītājs pievienots {target}.`, + 'faceMatch.toast.addError': `Neizdevās pievienot izpildītāju: {error}`, + 'faceMatch.toast.noStashbox': `Nav konfigurēts stash-box. Pievienojiet stash-box nodrošinātāju vietnē Settings → Metadata Providers, lai iespējotu izpildītāju importēšanu. Skatiet {url}`, + 'faceMatch.toast.noProvider': `Nodrošinātājs "StashDB" nav atrasts. Pārdēvējiet nodrošinātāju uz "StashDB" vietnē Settings → Metadata Providers, lai iespējotu izpildītāju importēšanu.`, + 'faceMatch.toast.configureProvider': `Konfigurējiet stash-box nodrošinātāju vietnē Settings → Metadata Providers, lai iespējotu izpildītāju importēšanu.`, + 'faceMatch.toast.addedMultiple': `Pievienoti izpildītāji: {count} uz {target}.`, + 'sprite.title': `AINAS IZPILDĪTĀJI`, + 'sprite.close': `Aizvērt`, + 'sprite.foundConfirmed': `Atrasti: {found} · apstiprināti: {confirmed}`, + 'sprite.confidence': `ticamība`, + 'sprite.name': `vārds`, + 'sprite.hits': `trāpījumi`, + 'sprite.minConf': `Min. ticam.`, + 'sprite.minConfTitle': `Minimālā ticamība: {percent}%`, + 'sprite.scanning': `Visage skenēšana…`, + 'sprite.cancel': `Atcelt`, + 'sprite.empty': `Šajā spraitā nav identificēti izpildītāji.`, + 'sprite.detectedFaceAlt': `Atklātā seja`, + 'sprite.spriteLabel': `SPRAITS`, + 'sprite.stashLabel': `STASH`, + 'sprite.vs': `vs`, + 'sprite.inScene': `Ainā`, + 'sprite.hitsCount': `Trāpījumi: {count}`, + 'sprite.totalTime': `Kopā {time}`, + 'sprite.alreadyInScene': `Jau ainā`, + 'sprite.clickToConfirm': `Noklikšķiniet, lai apstiprinātu`, + 'sprite.confirmed': `Apstiprināts`, + 'sprite.supportPatreon': `Atbalstīt pakalpojumā Patreon`, + 'sprite.confirmedCount': `Apstiprināti {confirmed} no {total}`, + 'sprite.shownHint': `Rādīti: {shown} (kopā {total}) · noklikšķiniet, lai apstiprinātu · ←→ navigācija · Enter apstiprināšana`, + 'sprite.confirmHint': `Noklikšķiniet, lai apstiprinātu · ←→ navigācija · Enter apstiprināšana`, + 'sprite.adding': `Pievienošana...`, + 'sprite.done': `Gatavs ({count})`, + 'gender.male': `Vīrietis`, + 'gender.female': `Sieviete`, + 'gender.transMale': `Transpersona vīrietis`, + 'gender.transFemale': `Transpersona sieviete`, + 'gender.nonBinary': `Ne-binārs`, + 'gender.intersex': `Interdzimums`, + 'card.excellent': `Lieliska atbilstība`, + 'card.good': `Laba atbilstība`, + 'card.uncertain': `Nenoteikta atbilstība`, + 'card.select': `Atlasīt {name}`, + 'card.deselect': `Noņemt atlasi {name}`, + 'card.openOn': `Atvērt vietnē {source}`, + 'search.overlayHint': `Velciet, lai atlasītu seju — Enter, lai skenētu visu kadrīti — Esc, lai atceltu`, + 'search.noFaces': `Atlasītajā apgabalā nav atrastas sejas. Mēģiniet ar ciešāku apgriešanu vai nospiediet Enter, lai skenētu visu kadrīti.`, + 'search.captureMediaFail': `Neizdevās notvert medijus. Pārliecinieties, ka aina/attēls ir pilnībā ielādēta.`, + 'search.healthBanner': `Seju atpazīšanas API nesasniedzams. Palaidiet pamata pakalpojumu un mēģiniet vēlreiz.`, + 'search.failed': `Sejas meklēšana neizdevās: {error}`, + 'search.fetchImageFail': `Neizdevās iegūt attēlu no Stash.`, + 'search.captureFail': `Neizdevās notvert attēlu: {error}`, + 'search.selectFaceImage': `Atlasiet seju attēlā.`, + 'search.captureFrameFail': `Neizdevās notvert pašreizējo kadrīti.`, + 'search.captureFrameFail2': `Kļūda, notverot pašreizējo kadrīti.`, + 'search.selectFaceVideo': `Atlasiet seju video atskaņotāja apgabalā.`, + 'search.menuItemTitle': `Velciet rāmi ap seju vai nospiediet Enter, lai skenētu visu kadrīti, lai meklētu atbilstības StashDB`, + 'search.currentFrame': `Visage: pašreizējais kadrītis`, + 'scene.noSprite': `Šai ainai nav atrasts sprite lapa vai priekšskatījuma video. Ģenerējiet tos ainas iestatījumos un mēģiniet vēlreiz.`, + 'scene.noFaces': `Šīs ainas sprite lapā vai priekšskatījuma video nav atrastas sejas vai izpildītāji.`, + 'scene.healthBanner': `Seju atpazīšanas API nesasniedzams. Palaidiet pamata pakalpojumu un mēģiniet vēlreiz.`, + 'scene.failed': `Ainas skenēšana neizdevās: {error}`, + 'scene.menuItemTitle': `Identificējiet katru izpildītāju ainā (nepieciešama ģenerēta sprite lapa vai priekšskatījuma video)`, + 'scene.wholeScene': `Visage: visa aina`, + 'banner.changeBackend': `Mainīt pamata pakalpojumu`, + 'banner.dismiss': `Noraidīt`, + 'error.dismiss': `Noraidīt`, + 'firstRun.title': `Iestatiet savu Visage pamata pakalpojumu`, + 'firstRun.subtitle': `Visage nosūta seju attēlus pamata pakalpojumam atpazīšanai. Izvēlieties, kur to palaist.`, + 'firstRun.cloud': `Izmantot Hugging Face mākoni`, + 'firstRun.cloudNote': `Bez iestatīšanas. Attēli tiek nosūtīti uz Hugging Face mākoņpakalpojumu.`, + 'firstRun.local': `Izmantot savu serveri`, + 'firstRun.localNote': `Palaidiet privāto izpildāmo failu savā datorā vai tīklā.`, + 'firstRun.skip': `Izlaist pagaidām`, + 'badge.local': `Lokāls`, + 'badge.cloud': `Mākonis (Hugging Face)`, + 'badge.title': `Visage pamata pakalpojums: {label}`, + 'donate.enjoying': `Patīk Visage? Palīdziet to uzturēt`, + 'donate.supportPatreon': `Atbalstīt pakalpojumā Patreon`, + 'frame.close': `Aizvērt kadrīša atlasītāju`, + 'frame.seekFail': `Neizdevās pārtīt video atskaņotāju.`, + 'frame.selectAt': `Atlasīt sejas kadrīti {time} s`, + }; + + const vi = { + 'backendSettings.title': `Cài đặt backend`, + 'backendSettings.closeAria': `Đóng cài đặt`, + 'backendSettings.backendAria': `Cài đặt backend`, + 'backendSettings.changeBackend': `Đổi backend`, + 'backendSettings.backendLabel': `Backend`, + 'backendSettings.local': `Cục bộ`, + 'backendSettings.cloud': `Đám mây (Hugging Face)`, + 'backendSettings.cloudNote': `Hình ảnh được gửi đến dịch vụ đám mây Hugging Face.`, + 'backendSettings.hintPrefix': `Muốn giữ hình ảnh trong mạng của bạn?`, + 'backendSettings.hintLink': `Chạy máy chủ riêng qua Patreon`, + 'backendSettings.urlLabel': `URL`, + 'backendSettings.csp1': `Chính sách bảo mật của trình duyệt (CSP) chỉ cho phép`, + 'backendSettings.csp2': `theo mặc định. Để truy cập backend cục bộ tại địa chỉ khác (ví dụ: IP LAN của bạn), hãy thêm nó vào`, + 'backendSettings.csp3': `trong`, + 'backendSettings.csp4': `bên trong thư mục plugin Stash của bạn, nếu không các yêu cầu sẽ bị chặn. Lưu ý: cập nhật Visage sẽ cài lại`, + 'backendSettings.csp5': `, vì vậy phải áp dụng lại sau mỗi lần cập nhật.`, + 'backendSettings.testing': `Đang kiểm tra kết nối…`, + 'backendSettings.testConnection': `Kiểm tra kết nối`, + 'backendSettings.testingShort': `Đang kiểm tra…`, + 'backendSettings.cancel': `Hủy`, + 'backendSettings.save': `Lưu`, + 'backendSettings.feedback.reachable': `Kết nối thành công. Backend đã sẵn sàng.`, + 'backendSettings.feedback.degraded': `Backend truy cập được nhưng bị suy giảm (mô hình hoặc chỉ mục chưa được tải).`, + 'backendSettings.feedback.unreachable': `Không thể truy cập backend. Kiểm tra URL và đảm bảo backend đang chạy.`, + 'faceMatch.title': `KHUNG HÌNH HIỆN TẠI`, + 'faceMatch.close': `Đóng`, + 'faceMatch.facesSelected': `Tìm thấy {faces} khuôn mặt · đã chọn {selected}`, + 'faceMatch.inScene': `· {count} trong cảnh`, + 'faceMatch.stashboxMissing': `Chưa cấu hình stash-box.`, + 'faceMatch.stashboxMissingBody': ` Thêm nhà cung cấp stash-box trong Cài đặt → Nhà cung cấp siêu dữ liệu để bật nhập diễn viên.`, + 'faceMatch.stashboxWrongName': `Không tìm thấy nhà cung cấp "StashDB".`, + 'faceMatch.stashboxWrongNameBody': ` Việc nhập diễn viên yêu cầu nhà cung cấp tên là "StashDB". Đổi tên nhà cung cấp của bạn trong Cài đặt → Nhà cung cấp siêu dữ liệu.`, + 'faceMatch.learnMore': `Tìm hiểu thêm.`, + 'faceMatch.scanning': `Đang quét · nhận dạng khuôn mặt…`, + 'faceMatch.faceAlt': `Khuôn mặt {index}`, + 'faceMatch.minConf': `Độ tin cậy tối thiểu`, + 'faceMatch.minConfTitle': `Độ tin cậy tối thiểu: {percent}%`, + 'faceMatch.detected': `Đã phát hiện`, + 'faceMatch.detectedFaceAlt': `Khuôn mặt được phát hiện`, + 'faceMatch.vs': `vs`, + 'faceMatch.supportPatreon': `Ủng hộ trên Patreon`, + 'faceMatch.ofSelected': `Đã chọn {selected} trong tổng {total}`, + 'faceMatch.allInScene': `Tìm thấy {total} khuôn mặt · tất cả trong cảnh`, + 'faceMatch.clickToSelect': `Tìm thấy {total} khuôn mặt · bấm để chọn`, + 'faceMatch.kbSwitch': `Chuyển khuôn mặt`, + 'faceMatch.kbSelect': `Chọn diễn viên`, + 'faceMatch.kbToggle': `Bật/tắt chọn`, + 'faceMatch.kbAddInstant': `Shift+bấm để thêm ngay`, + 'faceMatch.selectBest': `Chọn kết quả khớp tốt nhất`, + 'faceMatch.adding': `Đang thêm...`, + 'faceMatch.done': `Xong ({count})`, + 'faceMatch.toast.added': `Đã thêm diễn viên vào {target}.`, + 'faceMatch.toast.addError': `Không thêm được diễn viên: {error}`, + 'faceMatch.toast.noStashbox': `Chưa cấu hình stash-box. Thêm nhà cung cấp stash-box trong Cài đặt → Nhà cung cấp siêu dữ liệu để bật nhập diễn viên. Xem {url}`, + 'faceMatch.toast.noProvider': `Không tìm thấy nhà cung cấp tên "StashDB". Đổi tên nhà cung cấp của bạn thành "StashDB" trong Cài đặt → Nhà cung cấp siêu dữ liệu để bật nhập diễn viên.`, + 'faceMatch.toast.configureProvider': `Cấu hình nhà cung cấp stash-box trong Cài đặt → Nhà cung cấp siêu dữ liệu để bật nhập diễn viên.`, + 'faceMatch.toast.addedMultiple': `Đã thêm {count} diễn viên{s} vào {target}.`, + 'sprite.title': `DIỄN VIÊN TRONG CẢNH`, + 'sprite.close': `Đóng`, + 'sprite.foundConfirmed': `Tìm thấy {found} · đã xác nhận {confirmed}`, + 'sprite.confidence': `độ tin cậy`, + 'sprite.name': `tên`, + 'sprite.hits': `lượt khớp`, + 'sprite.minConf': `Độ tin cậy tối thiểu`, + 'sprite.minConfTitle': `Độ tin cậy tối thiểu: {percent}%`, + 'sprite.scanning': `Visage đang quét…`, + 'sprite.cancel': `Hủy`, + 'sprite.empty': `Không xác định được diễn viên nào trong sprite này.`, + 'sprite.detectedFaceAlt': `Khuôn mặt được phát hiện`, + 'sprite.spriteLabel': `SPRITE`, + 'sprite.stashLabel': `STASH`, + 'sprite.vs': `vs`, + 'sprite.inScene': `Trong cảnh`, + 'sprite.hitsCount': `{count} lượt khớp{s}`, + 'sprite.totalTime': `tổng {time}`, + 'sprite.alreadyInScene': `Đã có trong cảnh`, + 'sprite.clickToConfirm': `Bấm để xác nhận`, + 'sprite.confirmed': `Đã xác nhận`, + 'sprite.supportPatreon': `Ủng hộ trên Patreon`, + 'sprite.confirmedCount': `Đã xác nhận {confirmed} trong tổng {total}`, + 'sprite.shownHint': `Hiển thị {shown} (tổng {total}) · bấm để xác nhận · ←→ điều hướng · Enter xác nhận`, + 'sprite.confirmHint': `Bấm để xác nhận · ←→ điều hướng · Enter xác nhận`, + 'sprite.adding': `Đang thêm...`, + 'sprite.done': `Xong ({count})`, + 'gender.male': `Nam`, + 'gender.female': `Nữ`, + 'gender.transMale': `Nam chuyển giới`, + 'gender.transFemale': `Nữ chuyển giới`, + 'gender.nonBinary': `Phi nhị nguyên`, + 'gender.intersex': `Liên giới tính`, + 'card.excellent': `Khớp xuất sắc`, + 'card.good': `Khớp tốt`, + 'card.uncertain': `Khớp không chắc chắn`, + 'card.select': `Chọn {name}`, + 'card.deselect': `Bỏ chọn {name}`, + 'card.openOn': `Mở trên {source}`, + 'search.overlayHint': `Kéo để chọn khuôn mặt — Enter để quét toàn bộ khung hình — Esc để hủy`, + 'search.noFaces': `Không tìm thấy khuôn mặt nào trong vùng chọn đó. Hãy thử cắt hẹp hơn hoặc nhấn Enter để quét toàn bộ khung hình.`, + 'search.captureMediaFail': `Không thể chụp phương tiện. Vui lòng đảm bảo cảnh/hình ảnh đã được tải hoàn toàn.`, + 'search.healthBanner': `API nhận dạng khuôn mặt không truy cập được. Hãy khởi động backend và thử lại.`, + 'search.failed': `Tìm kiếm khuôn mặt thất bại: {error}`, + 'search.fetchImageFail': `Không thể lấy hình ảnh từ Stash.`, + 'search.captureFail': `Chụp hình ảnh thất bại: {error}`, + 'search.selectFaceImage': `Chọn một khuôn mặt trong hình ảnh.`, + 'search.captureFrameFail': `Không thể chụp khung hình hiện tại.`, + 'search.captureFrameFail2': `Chụp khung hình hiện tại thất bại.`, + 'search.selectFaceVideo': `Chọn một khuôn mặt trong vùng trình phát video.`, + 'search.menuItemTitle': `Kéo một khung quanh khuôn mặt hoặc nhấn Enter để quét toàn bộ khung hình nhằm tìm kiếm kết quả khớp trên StashDB`, + 'search.currentFrame': `Visage: Khung hình hiện tại`, + 'scene.noSprite': `Không có sprite sheet hoặc video xem trước cho cảnh này. Hãy tạo chúng trong Cài đặt cảnh, rồi thử lại.`, + 'scene.noFaces': `Không tìm thấy khuôn mặt hoặc diễn viên nào trong sprite sheet hoặc video xem trước của cảnh này.`, + 'scene.healthBanner': `API nhận dạng khuôn mặt không truy cập được. Hãy khởi động backend và thử lại.`, + 'scene.failed': `Quét cảnh thất bại: {error}`, + 'scene.menuItemTitle': `Xác định mọi diễn viên trong cảnh (cần sprite sheet hoặc video xem trước đã tạo)`, + 'scene.wholeScene': `Visage: Toàn bộ cảnh`, + 'banner.changeBackend': `Đổi backend`, + 'banner.dismiss': `Bỏ qua`, + 'error.dismiss': `Bỏ qua`, + 'firstRun.title': `Thiết lập backend Visage của bạn`, + 'firstRun.subtitle': `Visage gửi hình ảnh khuôn mặt đến backend để nhận dạng. Chọn nơi chạy nó.`, + 'firstRun.cloud': `Dùng đám mây Hugging Face`, + 'firstRun.cloudNote': `Không cần thiết lập. Hình ảnh được gửi đến dịch vụ đám mây Hugging Face.`, + 'firstRun.local': `Dùng máy chủ của riêng tôi`, + 'firstRun.localNote': `Chạy tệp nhị phân riêng trên máy hoặc mạng của bạn.`, + 'firstRun.skip': `Tạm bỏ qua`, + 'badge.local': `Cục bộ`, + 'badge.cloud': `Đám mây (Hugging Face)`, + 'badge.title': `Backend Visage: {label}`, + 'donate.enjoying': `Đang dùng Visage? Giúp giữ nó hoạt động`, + 'donate.supportPatreon': `Ủng hộ trên Patreon`, + 'frame.close': `Đóng trình chọn khung hình`, + 'frame.seekFail': `Không thể tua trình phát video.`, + 'frame.selectAt': `Chọn khung hình khuôn mặt tại {time} giây`, + }; + + // Estonian. + const et = { + 'backendSettings.title': `Backend-seaded`, + 'backendSettings.closeAria': `Sule seaded`, + 'backendSettings.backendAria': `Backend-seaded`, + 'backendSettings.changeBackend': `Vaheta backend`, + 'backendSettings.backendLabel': `Backend`, + 'backendSettings.local': `Kohalik`, + 'backendSettings.cloud': `Cloud (Hugging Face)`, + 'backendSettings.cloudNote': `Pildid saadetakse Hugging Face pilveteenusesse.`, + 'backendSettings.hintPrefix': `Kas soovid, et su pildid jääksid sinu võrku?`, + 'backendSettings.hintLink': `Käivita privaatne server Patreoni kaudu`, + 'backendSettings.urlLabel': `URL`, + 'backendSettings.csp1': "Brauseri turvapoliitika (CSP) lubab vaikimisi ainult", + 'backendSettings.csp2': `Teise aadressiga (nt oma LAN-IP) kohaliku backendini jõudmiseks lisa see`, + 'backendSettings.csp3': `loendisse failis`, + 'backendSettings.csp4': `oma Stash pluginade kaustas, muidu päringud blokeeritakse. Märkus: Visage värskendamine installib`, + 'backendSettings.csp5': `uuesti, nii et seda tuleb iga värskenduse järel uuesti teha.`, + 'backendSettings.testing': `Ühenduse testimine…`, + 'backendSettings.testConnection': `Testi ühendust`, + 'backendSettings.testingShort': `Testimine…`, + 'backendSettings.cancel': `Tühista`, + 'backendSettings.save': `Salvesta`, + 'backendSettings.feedback.reachable': `Ühendus õnnestus. Backend on valmis.`, + 'backendSettings.feedback.degraded': `Backend on kättesaadav, kuid halvendatud (mudelid või indeks pole laaditud).`, + 'backendSettings.feedback.unreachable': `Backend ei ole kättesaadav. Kontrolli URL-i ja et backend töötab.`, + 'faceMatch.title': `PRAEGUNE KAADER`, + 'faceMatch.close': `Sule`, + 'faceMatch.facesSelected': `Leitud {faces} nägu · valitud {selected}`, + 'faceMatch.inScene': `· {count} stseenis`, + 'faceMatch.stashboxMissing': `Stash-box pole konfigureeritud.`, + 'faceMatch.stashboxMissingBody': ` Lisa esinejate impordi lubamiseks stash-box-pakkuja jaotises Seaded → Metaandmete pakkujad.`, + 'faceMatch.stashboxWrongName': `Pakkujat "StashDB" ei leitud.`, + 'faceMatch.stashboxWrongNameBody': ` Esinejate import nõuab pakkujat nimega "StashDB". Nimeta oma pakkuja ümber jaotises Seaded → Metaandmete pakkujad.`, + 'faceMatch.learnMore': `Lisateave.`, + 'faceMatch.scanning': `Skannimine • näotuvastus…`, + 'faceMatch.faceAlt': `Nägu {index}`, + 'faceMatch.minConf': `Min. kindl.`, + 'faceMatch.minConfTitle': `Minimaalne kindlus: {percent}%`, + 'faceMatch.detected': `Tuvastatud`, + 'faceMatch.detectedFaceAlt': `Tuvastatud nägu`, + 'faceMatch.vs': `vs`, + 'faceMatch.supportPatreon': `Toeta Patreonis`, + 'faceMatch.ofSelected': `Valitud {selected}/{total}`, + 'faceMatch.allInScene': `Leitud {total} nägu · kõik stseenis`, + 'faceMatch.clickToSelect': `Leitud {total} nägu · valimiseks klõpsa`, + 'faceMatch.kbSwitch': `Vaheta nägusid`, + 'faceMatch.kbSelect': `Vali esinejad`, + 'faceMatch.kbToggle': `Vaheta valikut`, + 'faceMatch.kbAddInstant': `Shift+klõps lisab kohe`, + 'faceMatch.selectBest': `Vali parimad vasted`, + 'faceMatch.adding': `Lisamine...`, + 'faceMatch.done': `Valmis ({count})`, + 'faceMatch.toast.added': `Esineja lisatud sihtkohta {target}.`, + 'faceMatch.toast.addError': `Esineja lisamine ebaõnnestus: {error}`, + 'faceMatch.toast.noStashbox': `Stash-box pole konfigureeritud. Lisa esinejate impordi lubamiseks stash-box-pakkuja jaotises Seaded → Metaandmete pakkujad. Vaata {url}`, + 'faceMatch.toast.noProvider': `Pakkujat nimega "StashDB" ei leitud. Nimeta esinejate impordi lubamiseks oma pakkuja ümber "StashDB"-ks jaotises Seaded → Metaandmete pakkujad.`, + 'faceMatch.toast.configureProvider': `Konfigureeri stash-box-pakkuja jaotises Seaded → Metaandmete pakkujad esinejate impordi lubamiseks.`, + 'faceMatch.toast.addedMultiple': `Sihtkohta {target} lisati {count} esinejat.`, + 'sprite.title': `STSEENI ESINEJAD`, + 'sprite.close': `Sulge`, + 'sprite.foundConfirmed': `Leitud {found} · kinnitatud {confirmed}`, + 'sprite.confidence': `kindlus`, + 'sprite.name': `nimi`, + 'sprite.hits': `tabamused`, + 'sprite.minConf': `Min. kindl.`, + 'sprite.minConfTitle': `Minimaalne kindlus: {percent}%`, + 'sprite.scanning': `Visage skannib…`, + 'sprite.cancel': `Tühista`, + 'sprite.empty': `Selles sprites ei tuvastatud ühtegi esinejat.`, + 'sprite.detectedFaceAlt': `Tuvastatud nägu`, + 'sprite.spriteLabel': `SPRITE`, + 'sprite.stashLabel': `STASH`, + 'sprite.vs': `vs`, + 'sprite.inScene': `Stseenis`, + 'sprite.hitsCount': `{count} tabamus`, + 'sprite.totalTime': `{time} kokku`, + 'sprite.alreadyInScene': `Juba stseenis`, + 'sprite.clickToConfirm': `Kinnitamiseks klõpsa`, + 'sprite.confirmed': `Kinnitatud`, + 'sprite.supportPatreon': `Toeta Patreonis`, + 'sprite.confirmedCount': `Kinnitatud {confirmed}/{total}`, + 'sprite.shownHint': `Näidatud {shown} ({total} kokku) · kinnitamiseks klõpsa · ←→ navigeerimine · Enter kinnitab`, + 'sprite.confirmHint': `Kinnitamiseks klõpsa · ←→ navigeerimine · Enter kinnitab`, + 'sprite.adding': `Lisamine...`, + 'sprite.done': `Valmis ({count})`, + 'gender.male': `Mees`, + 'gender.female': `Naine`, + 'gender.transMale': `Transmees`, + 'gender.transFemale': `Transnaine`, + 'gender.nonBinary': `Mittebinaarne`, + 'gender.intersex': `Intersooline`, + 'card.excellent': `Suurepärane vaste`, + 'card.good': `Hea vaste`, + 'card.uncertain': `Ebamäärane vaste`, + 'card.select': `Vali {name}`, + 'card.deselect': `Tühista valik {name}`, + 'card.openOn': `Ava saidil {source}`, + 'search.overlayHint': `Näo valimiseks lohista — Enter skannib kogu kaadri — Esc tühistab`, + 'search.noFaces': `Sellest valikust ei leitud ühtegi nägu. Proovi täpsemat kärpimist või vajuta Enter, et skannida kogu kaader.`, + 'search.captureMediaFail': `Meedia jäädvustamine ebaõnnestus. Veendu, et stseen/pilt on täielikult laaditud.`, + 'search.healthBanner': `Näotuvastuse API ei ole kättesaadav. Käivita backend ja proovi uuesti.`, + 'search.failed': `Näootsing ebaõnnestus: {error}`, + 'search.fetchImageFail': `Pildi toomine Stashist ebaõnnestus.`, + 'search.captureFail': `Pildi jäädvustamine ebaõnnestus: {error}`, + 'search.selectFaceImage': `Vali pildilt nägu.`, + 'search.captureFrameFail': `Praeguse kaadri jäädvustamine ebaõnnestus.`, + 'search.captureFrameFail2': `Praeguse kaadri jäädvustamine ei õnnestunud.`, + 'search.selectFaceVideo': `Vali nägu videopleieri piirkonnast.`, + 'search.menuItemTitle': `Lohista kast ümber näo või vajuta Enter kogu kaadri skannimiseks, et otsida StashDB-st vasted`, + 'search.currentFrame': `Visage: praegune kaader`, + 'scene.noSprite': `Selle stseeni jaoks ei leitud sprite-lehte ega eelvaatuse videot. Genereeri need Scene-seadetes ja proovi siis uuesti.`, + 'scene.noFaces': `Selle stseeni sprite-lehest või eelvaatuse videost ei leitud ühtegi nägu ega esinejat.`, + 'scene.healthBanner': `Näotuvastuse API ei ole kättesaadav. Käivita backend ja proovi uuesti.`, + 'scene.failed': `Stseeni skannimine ebaõnnestus: {error}`, + 'scene.menuItemTitle': `Tuvasta iga esineja stseenis (nõuab genereeritud sprite-lehte või eelvaatuse videot)`, + 'scene.wholeScene': `Visage: kogu stseen`, + 'banner.changeBackend': `Vaheta backend`, + 'banner.dismiss': `Lükka tagasi`, + 'error.dismiss': `Lükka tagasi`, + 'firstRun.title': `Seadista oma Visage backend`, + 'firstRun.subtitle': `Visage saadab näopildid tuvastamiseks backendile. Vali, kus seda käitada.`, + 'firstRun.cloud': `Kasuta Hugging Face cloudi`, + 'firstRun.cloudNote': `Pole seadistamist vaja. Pildid saadetakse Hugging Face pilveteenusesse.`, + 'firstRun.local': `Kasuta oma serverit`, + 'firstRun.localNote': `Käivita privaatne binaar oma masinas või võrgus.`, + 'firstRun.skip': `Jäta praegu vahele`, + 'badge.local': `Kohalik`, + 'badge.cloud': `Cloud (Hugging Face)`, + 'badge.title': `Visage backend: {label}`, + 'donate.enjoying': `Kas Visage meeldib sulle? Aita seda elus hoida`, + 'donate.supportPatreon': `Toeta Patreonis`, + 'frame.close': `Sule kaadrivalija`, + 'frame.seekFail': `Videopleieris navigeerimine ebaõnnestus.`, + 'frame.selectAt': `Vali näokaader kohas {time}s`, + }; + + // Norwegian Bokmål. + const nb = { + 'backendSettings.title': `Backend-innstillinger`, + 'backendSettings.closeAria': `Lukk innstillinger`, + 'backendSettings.backendAria': `Backend-innstillinger`, + 'backendSettings.changeBackend': `Bytt backend`, + 'backendSettings.backendLabel': `Backend`, + 'backendSettings.local': `Lokal`, + 'backendSettings.cloud': `Cloud (Hugging Face)`, + 'backendSettings.cloudNote': `Bilder sendes til Hugging Face sky-tjenesten.`, + 'backendSettings.hintPrefix': `Vil du at bildene dine skal bli på nettverket ditt?`, + 'backendSettings.hintLink': `Kjør en privat server via Patreon`, + 'backendSettings.urlLabel': `URL`, + 'backendSettings.csp1': "Nettleserens sikkerhetspolicy (CSP) tillater bare", + 'backendSettings.csp2': `som standard. For å nå en lokal backend på en annen adresse (f.eks. LAN-IP-en din) må du legge den til i`, + 'backendSettings.csp3': `listen i`, + 'backendSettings.csp4': `-filen i Stash-plugins-mappen din, ellers blokkeres forespørslene. Merk: oppdatering av Visage installerer`, + 'backendSettings.csp5': `på nytt, så dette må gjøres på nytt etter hver oppdatering.`, + 'backendSettings.testing': `Tester tilkobling…`, + 'backendSettings.testConnection': `Test tilkobling`, + 'backendSettings.testingShort': `Tester…`, + 'backendSettings.cancel': `Avbryt`, + 'backendSettings.save': `Lagre`, + 'backendSettings.feedback.reachable': `Tilkobling vellykket. Backend er klar.`, + 'backendSettings.feedback.degraded': `Backend er tilgjengelig, men redusert (modeller eller indeks er ikke lastet inn).`, + 'backendSettings.feedback.unreachable': `Backend er utilgjengelig. Sjekk URL-en og at backend kjører.`, + 'faceMatch.title': `GJELDENDE RAMME`, + 'faceMatch.close': `Lukk`, + 'faceMatch.facesSelected': `{faces} ansikter funnet · {selected} valgt`, + 'faceMatch.inScene': `· {count} i scene`, + 'faceMatch.stashboxMissing': `Ingen stash-box konfigurert.`, + 'faceMatch.stashboxMissingBody': ` Legg til en stash-box-leverandør under Innstillinger → Metadata-leverandører for å aktivere performer-import.`, + 'faceMatch.stashboxWrongName': `Fant ingen "StashDB"-leverandør.`, + 'faceMatch.stashboxWrongNameBody': ` Performer-import krever en leverandør kalt "StashDB". Gi leverandøren nytt navn under Innstillinger → Metadata-leverandører.`, + 'faceMatch.learnMore': `Les mer.`, + 'faceMatch.scanning': `Skanner • ansiktsgjenkjenning…`, + 'faceMatch.faceAlt': `Ansikt {index}`, + 'faceMatch.minConf': `Min. konf.`, + 'faceMatch.minConfTitle': `Minimumskonfidens: {percent}%`, + 'faceMatch.detected': `Oppdaget`, + 'faceMatch.detectedFaceAlt': `Oppdaget ansikt`, + 'faceMatch.vs': `vs`, + 'faceMatch.supportPatreon': `Støtt på Patreon`, + 'faceMatch.ofSelected': `{selected} av {total} valgt`, + 'faceMatch.allInScene': `{total} ansikter funnet · alle i scene`, + 'faceMatch.clickToSelect': `{total} ansikter funnet · klikk for å velge`, + 'faceMatch.kbSwitch': `Bytt ansikter`, + 'faceMatch.kbSelect': `Velg performere`, + 'faceMatch.kbToggle': `Veksle markering`, + 'faceMatch.kbAddInstant': `Shift+klikk for å legge til umiddelbart`, + 'faceMatch.selectBest': `Velg beste treff`, + 'faceMatch.adding': `Legger til...`, + 'faceMatch.done': `Ferdig ({count})`, + 'faceMatch.toast.added': `La til performer i {target}.`, + 'faceMatch.toast.addError': `Kunne ikke legge til performer: {error}`, + 'faceMatch.toast.noStashbox': `Ingen stash-box konfigurert. Legg til en stash-box-leverandør under Innstillinger → Metadata-leverandører for å aktivere performer-import. Se {url}`, + 'faceMatch.toast.noProvider': `Fant ingen leverandør kalt "StashDB". Gi leverandøren nytt navn til "StashDB" under Innstillinger → Metadata-leverandører for å aktivere performer-import.`, + 'faceMatch.toast.configureProvider': `Konfigurer en stash-box-leverandør under Innstillinger → Metadata-leverandører for å aktivere performer-import.`, + 'faceMatch.toast.addedMultiple': `La til {count} performere i {target}.`, + 'sprite.title': `SCENE-PERFORMERE`, + 'sprite.close': `Lukk`, + 'sprite.foundConfirmed': `{found} funnet · {confirmed} bekreftet`, + 'sprite.confidence': `konfidens`, + 'sprite.name': `navn`, + 'sprite.hits': `treff`, + 'sprite.minConf': `Min. konf.`, + 'sprite.minConfTitle': `Minimumskonfidens: {percent}%`, + 'sprite.scanning': `Visage skanner…`, + 'sprite.cancel': `Avbryt`, + 'sprite.empty': `Ingen performere identifisert i denne spriten.`, + 'sprite.detectedFaceAlt': `Oppdaget ansikt`, + 'sprite.spriteLabel': `SPRITE`, + 'sprite.stashLabel': `STASH`, + 'sprite.vs': `vs`, + 'sprite.inScene': `I scene`, + 'sprite.hitsCount': `{count} treff`, + 'sprite.totalTime': `{time} totalt`, + 'sprite.alreadyInScene': `Allerede i scene`, + 'sprite.clickToConfirm': `Klikk for å bekrefte`, + 'sprite.confirmed': `Bekreftet`, + 'sprite.supportPatreon': `Støtt på Patreon`, + 'sprite.confirmedCount': `{confirmed} av {total} bekreftet`, + 'sprite.shownHint': `{shown} vist ({total} totalt) · klikk for å bekrefte · ←→ naviger · Enter bekreft`, + 'sprite.confirmHint': `Klikk for å bekrefte · ←→ naviger · Enter bekreft`, + 'sprite.adding': `Legger til...`, + 'sprite.done': `Ferdig ({count})`, + 'gender.male': `Mann`, + 'gender.female': `Kvinne`, + 'gender.transMale': `Transmann`, + 'gender.transFemale': `Transkvinne`, + 'gender.nonBinary': `Ikke-binær`, + 'gender.intersex': `Intersex`, + 'card.excellent': `Utmerket treff`, + 'card.good': `Godt treff`, + 'card.uncertain': `Usikkert treff`, + 'card.select': `Velg {name}`, + 'card.deselect': `Fravelg {name}`, + 'card.openOn': `Åpne på {source}`, + 'search.overlayHint': `Dra for å velge et ansikt — Enter for å skanne hele rammen — Esc for å avbryte`, + 'search.noFaces': `Ingen ansikter funnet i utvalget. Prøv en tettere beskjæring, eller trykk Enter for å skanne hele rammen.`, + 'search.captureMediaFail': `Kunne ikke fange mediet. Kontroller at scenen/bildet er fullastet.`, + 'search.healthBanner': `Ansiktsgjenkjennings-API-et er ikke tilgjengelig. Start backend og prøv igjen.`, + 'search.failed': `Ansiktssøk mislyktes: {error}`, + 'search.fetchImageFail': `Kunne ikke hente bilde fra Stash.`, + 'search.captureFail': `Kunne ikke fange bilde: {error}`, + 'search.selectFaceImage': `Velg et ansikt i bildet.`, + 'search.captureFrameFail': `Kunne ikke fange gjeldende ramme.`, + 'search.captureFrameFail2': `Innfanging av gjeldende ramme mislyktes.`, + 'search.selectFaceVideo': `Velg et ansikt i videoavspillerområdet.`, + 'search.menuItemTitle': `Dra en boks rundt et ansikt, eller trykk Enter for å skanne hele rammen, for å søke StashDB etter treff`, + 'search.currentFrame': `Visage: Gjeldende ramme`, + 'scene.noSprite': `Fant ingen sprite-ark eller forhåndsvisningsvideo for denne scenen. Generer dem i Scene-innstillingene, og prøv igjen.`, + 'scene.noFaces': `Fant ingen ansikter eller performere i denne scenens sprite-ark eller forhåndsvisningsvideo.`, + 'scene.healthBanner': `Ansiktsgjenkjennings-API-et er ikke tilgjengelig. Start backend og prøv igjen.`, + 'scene.failed': `Scene-skanning mislyktes: {error}`, + 'scene.menuItemTitle': `Identifiser hver performer i scenen (krever et generert sprite-ark eller en forhåndsvisningsvideo)`, + 'scene.wholeScene': `Visage: Hele scenen`, + 'banner.changeBackend': `Bytt backend`, + 'banner.dismiss': `Avvis`, + 'error.dismiss': `Avvis`, + 'firstRun.title': `Sett opp Visage-backend-en din`, + 'firstRun.subtitle': `Visage sender ansiktsbilder til en backend for gjenkjenning. Velg hvor den skal kjøres.`, + 'firstRun.cloud': `Bruk Hugging Face cloud`, + 'firstRun.cloudNote': `Ingen oppsett. Bilder sendes til Hugging Face sky-tjenesten.`, + 'firstRun.local': `Bruk min egen server`, + 'firstRun.localNote': `Kjør den private binæren på din egen maskin eller ditt nettverk.`, + 'firstRun.skip': `Hopp over for nå`, + 'badge.local': `Lokal`, + 'badge.cloud': `Cloud (Hugging Face)`, + 'badge.title': `Visage-backend: {label}`, + 'donate.enjoying': `Liker du Visage? Hjelp å holde det i live`, + 'donate.supportPatreon': `Støtt på Patreon`, + 'frame.close': `Lukk rammevelgeren`, + 'frame.seekFail': `Kunne ikke søke i videoavspilleren.`, + 'frame.selectAt': `Velg ansiktsramme ved {time}s`, + }; + + // Turkish. + const tr = { + 'backendSettings.title': `Backend ayarları`, + 'backendSettings.closeAria': `Ayarları kapat`, + 'backendSettings.backendAria': `Backend ayarları`, + 'backendSettings.changeBackend': `Backend değiştir`, + 'backendSettings.backendLabel': `Backend`, + 'backendSettings.local': `Yerel`, + 'backendSettings.cloud': `Cloud (Hugging Face)`, + 'backendSettings.cloudNote': `Görseller Hugging Face bulut hizmetine gönderilir.`, + 'backendSettings.hintPrefix': `Görsellerinizin ağınızda kalmasını ister misiniz?`, + 'backendSettings.hintLink': `Patreon üzerinden özel bir sunucu çalıştırın`, + 'backendSettings.urlLabel': `URL`, + 'backendSettings.csp1': "Tarayıcının güvenlik politikası (CSP) varsayılan olarak yalnızca", + 'backendSettings.csp2': `adreslerine izin verir. Başka bir adresteki (örn. LAN IP'niz) yerel bir backend\u2019e ulaşmak için onu`, + 'backendSettings.csp3': `dosyasındaki`, + 'backendSettings.csp4': `listesine ekleyin, aksi halde istekler engellenir. Not: Visage güncellemesi`, + 'backendSettings.csp5': `dosyasını yeniden yükler, bu nedenle bu işlemin her güncellemeden sonra tekrarlanması gerekir.`, + 'backendSettings.testing': `Bağlantı test ediliyor…`, + 'backendSettings.testConnection': `Bağlantıyı test et`, + 'backendSettings.testingShort': `Test ediliyor…`, + 'backendSettings.cancel': `İptal`, + 'backendSettings.save': `Kaydet`, + 'backendSettings.feedback.reachable': `Bağlantı başarılı. Backend hazır.`, + 'backendSettings.feedback.degraded': `Backend erişilebilir ancak düşük performanslı (modeller veya dizin yüklenmemiş).`, + 'backendSettings.feedback.unreachable': `Backend\u2019e ulaşılamıyor. URL'yi ve backend\u2019in çalıştığını kontrol edin.`, + 'faceMatch.title': `MEVCUT KARE`, + 'faceMatch.close': `Kapat`, + 'faceMatch.facesSelected': `{faces} yüz bulundu · {selected} seçildi`, + 'faceMatch.inScene': `· sahnede {count}`, + 'faceMatch.stashboxMissing': `stash-box yapılandırılmadı.`, + 'faceMatch.stashboxMissingBody': ` Oyuncu içe aktarmayı etkinleştirmek için Ayarlar → Meta Veri Sağlayıcıları altına bir stash-box sağlayıcısı ekleyin.`, + 'faceMatch.stashboxWrongName': `"StashDB" sağlayıcısı bulunamadı.`, + 'faceMatch.stashboxWrongNameBody': ` Oyuncu içe aktarma, "StashDB" adında bir sağlayıcı gerektirir. Sağlayıcınızı Ayarlar → Meta Veri Sağlayıcıları altında yeniden adlandırın.`, + 'faceMatch.learnMore': `Daha fazla bilgi.`, + 'faceMatch.scanning': `Taranıyor • yüz tanıma…`, + 'faceMatch.faceAlt': `Yüz {index}`, + 'faceMatch.minConf': `Min. güv.`, + 'faceMatch.minConfTitle': `Minimum güven: {percent}%`, + 'faceMatch.detected': `Algılandı`, + 'faceMatch.detectedFaceAlt': `Algılanan yüz`, + 'faceMatch.vs': `vs`, + 'faceMatch.supportPatreon': `Patreon\u2019da destekle`, + 'faceMatch.ofSelected': `{total} içinden {selected} seçildi`, + 'faceMatch.allInScene': `{total} yüz bulundu · tümü sahnede`, + 'faceMatch.clickToSelect': `{total} yüz bulundu · seçmek için tıklayın`, + 'faceMatch.kbSwitch': `Yüzleri değiştir`, + 'faceMatch.kbSelect': `Oyuncuları seç`, + 'faceMatch.kbToggle': `Seçimi değiştir`, + 'faceMatch.kbAddInstant': `Anında eklemek için Shift+tıklayın`, + 'faceMatch.selectBest': `En İyi Eşleşmeleri Seç`, + 'faceMatch.adding': `Ekleniyor...`, + 'faceMatch.done': `Bitti ({count})`, + 'faceMatch.toast.added': `{target} konumuna oyuncu eklendi.`, + 'faceMatch.toast.addError': `Oyuncu eklenemedi: {error}`, + 'faceMatch.toast.noStashbox': `stash-box yapılandırılmadı. Oyuncu içe aktarmayı etkinleştirmek için Ayarlar → Meta Veri Sağlayıcıları altına bir stash-box sağlayıcısı ekleyin. {url} adresine bakın`, + 'faceMatch.toast.noProvider': `"StashDB" adında bir sağlayıcı bulunamadı. Oyuncu içe aktarmayı etkinleştirmek için sağlayıcınızı Ayarlar → Meta Veri Sağlayıcıları altında "StashDB" olarak yeniden adlandırın.`, + 'faceMatch.toast.configureProvider': `Oyuncu içe aktarmayı etkinleştirmek için Ayarlar → Meta Veri Sağlayıcıları altında bir stash-box sağlayıcısı yapılandırın.`, + 'faceMatch.toast.addedMultiple': `{target} konumuna {count} oyuncu eklendi.`, + 'sprite.title': `SAHNE OYUNCULARI`, + 'sprite.close': `Kapat`, + 'sprite.foundConfirmed': `{found} bulundu · {confirmed} onaylandı`, + 'sprite.confidence': `güven`, + 'sprite.name': `ad`, + 'sprite.hits': `eşleşme`, + 'sprite.minConf': `Min. güv.`, + 'sprite.minConfTitle': `Minimum güven: {percent}%`, + 'sprite.scanning': `Visage taranıyor…`, + 'sprite.cancel': `İptal`, + 'sprite.empty': `Bu spritede oyuncu tanımlanmadı.`, + 'sprite.detectedFaceAlt': `Algılanan yüz`, + 'sprite.spriteLabel': `SPRITE`, + 'sprite.stashLabel': `STASH`, + 'sprite.vs': `vs`, + 'sprite.inScene': `Sahnede`, + 'sprite.hitsCount': `{count} eşleşme`, + 'sprite.totalTime': `toplam {time}`, + 'sprite.alreadyInScene': `Zaten sahnede`, + 'sprite.clickToConfirm': `Onaylamak için tıklayın`, + 'sprite.confirmed': `Onaylandı`, + 'sprite.supportPatreon': `Patreon\u2019da destekle`, + 'sprite.confirmedCount': `{total} içinden {confirmed} onaylandı`, + 'sprite.shownHint': `{shown} gösterildi (toplam {total}) · onaylamak için tıklayın · ←→ gezin · Enter onayla`, + 'sprite.confirmHint': `Onaylamak için tıklayın · ←→ gezin · Enter onayla`, + 'sprite.adding': `Ekleniyor...`, + 'sprite.done': `Bitti ({count})`, + 'gender.male': `Erkek`, + 'gender.female': `Kadın`, + 'gender.transMale': `Trans erkek`, + 'gender.transFemale': `Trans kadın`, + 'gender.nonBinary': `İkili olmayan`, + 'gender.intersex': `İnterseks`, + 'card.excellent': `Mükemmel eşleşme`, + 'card.good': `İyi eşleşme`, + 'card.uncertain': `Belirsiz eşleşme`, + 'card.select': `{name} seç`, + 'card.deselect': `{name} seçimini kaldır`, + 'card.openOn': `{source} üzerinde aç`, + 'search.overlayHint': `Bir yüz seçmek için sürükleyin — tüm kareyi taramak için Enter — iptal için Esc`, + 'search.noFaces': `Bu seçimde yüz bulunamadı. Daha dar bir kırpma deneyin veya tüm kareyi taramak için Enter\u2019a basın.`, + 'search.captureMediaFail': `Medya yakalanamadı. Sahnenin/görselin tamamen yüklendiğinden emin olun.`, + 'search.healthBanner': `Yüz tanıma API\u2019sine ulaşılamıyor. Backend\u2019i başlatın ve tekrar deneyin.`, + 'search.failed': `Yüz araması başarısız: {error}`, + 'search.fetchImageFail': `Stash\u2019tan görsel alınamadı.`, + 'search.captureFail': `Görsel yakalanamadı: {error}`, + 'search.selectFaceImage': `Görsel içinde bir yüz seçin.`, + 'search.captureFrameFail': `Geçerli kare yakalanamadı.`, + 'search.captureFrameFail2': `Geçerli kare yakalanamadı.`, + 'search.selectFaceVideo': `Video oynatıcı alanı içinde bir yüz seçin.`, + 'search.menuItemTitle': `StashDB\u2019de eşleşme aramak için bir yüzün etrafına kutu çizin veya tüm kareyi taramak için Enter\u2019a basın`, + 'search.currentFrame': `Visage: Geçerli Kare`, + 'scene.noSprite': `Bu sahne için sprite sayfası veya önizleme videosu bulunamadı. Scene ayarlarından oluşturun, ardından tekrar deneyin.`, + 'scene.noFaces': `Bu sahnenin sprite sayfasında veya önizleme videosunda yüz veya oyuncu bulunamadı.`, + 'scene.healthBanner': `Yüz tanıma API\u2019sine ulaşılamıyor. Backend\u2019i başlatın ve tekrar deneyin.`, + 'scene.failed': `Sahne taraması başarısız: {error}`, + 'scene.menuItemTitle': `Sahnedeki her oyuncuyu tanımlayın (oluşturulmuş bir sprite sayfası veya önizleme videosu gerektirir)`, + 'scene.wholeScene': `Visage: Tüm Sahne`, + 'banner.changeBackend': `Backend değiştir`, + 'banner.dismiss': `Kapat`, + 'error.dismiss': `Kapat`, + 'firstRun.title': `Visage backend\u2019inizi kurun`, + 'firstRun.subtitle': `Visage, tanıma için yüz görsellerini bir backend\u2019e gönderir. Nerede çalıştırılacağını seçin.`, + 'firstRun.cloud': `Hugging Face cloud kullan`, + 'firstRun.cloudNote': `Kurulum gerekmez. Görseller Hugging Face bulut hizmetine gönderilir.`, + 'firstRun.local': `Kendi sunucumu kullan`, + 'firstRun.localNote': `Özel ikili dosyayı kendi makinenizde veya ağınızda çalıştırın.`, + 'firstRun.skip': `Şimdilik atla`, + 'badge.local': `Yerel`, + 'badge.cloud': `Cloud (Hugging Face)`, + 'badge.title': `Visage backend: {label}`, + 'donate.enjoying': `Visage hoşunuza mı gitti? Hayatta kalmasına yardım edin`, + 'donate.supportPatreon': `Patreon\u2019da destekle`, + 'frame.close': `Kare seçiciyi kapat`, + 'frame.seekFail': `Video oynatıcıya atlanamadı.`, + 'frame.selectAt': `{time}s konumunda yüz karesi seçin`, + }; + + const zhTw = { + 'backendSettings.title': `後端設定`, + 'backendSettings.closeAria': `關閉設定`, + 'backendSettings.backendAria': `後端設定`, + 'backendSettings.changeBackend': `更換後端`, + 'backendSettings.backendLabel': `後端`, + 'backendSettings.local': `本機`, + 'backendSettings.cloud': `雲端(Hugging Face)`, + 'backendSettings.cloudNote': `影像會被傳送至 Hugging Face 雲端服務。`, + 'backendSettings.hintPrefix': `希望影像留在你的網路內?`, + 'backendSettings.hintLink': `透過 Patreon 執行私人伺服器`, + 'backendSettings.urlLabel': `URL`, + 'backendSettings.csp1': `瀏覽器的安全政策(CSP)僅允許`, + 'backendSettings.csp2': `依預設情況下。若要連線位於其他位址(例如你的 LAN IP)的本機後端,請將其加入`, + 'backendSettings.csp3': `中的`, + 'backendSettings.csp4': `檔案清單,該檔案位於你的 Stash 外掛資料夾內,否則請求將被阻止。注意:更新 Visage 會重新安裝`, + 'backendSettings.csp5': `,因此每次更新後都必須重新套用此設定。`, + 'backendSettings.testing': `正在測試連線…`, + 'backendSettings.testConnection': `測試連線`, + 'backendSettings.testingShort': `正在測試…`, + 'backendSettings.cancel': `取消`, + 'backendSettings.save': `儲存`, + 'backendSettings.feedback.reachable': `連線成功。後端已就緒。`, + 'backendSettings.feedback.degraded': `後端可連線但已降級(模型或索引未載入)。`, + 'backendSettings.feedback.unreachable': `後端無法連線。請檢查 URL 並確認後端正在執行。`, + 'faceMatch.title': `目前影格`, + 'faceMatch.close': `關閉`, + 'faceMatch.facesSelected': `找到 {faces} 張臉 · 已選擇 {selected}`, + 'faceMatch.inScene': `· {count} 在場景中`, + 'faceMatch.stashboxMissing': `未設定 stash-box。`, + 'faceMatch.stashboxMissingBody': ` 請在設定 → 中繼資料提供者中新增 stash-box 提供者,以啟用表演者匯入。`, + 'faceMatch.stashboxWrongName': `找不到「StashDB」提供者。`, + 'faceMatch.stashboxWrongNameBody': ` 表演者匯入需要名為「StashDB」的提供者。請在設定 → 中繼資料提供者中重新命名你的提供者。`, + 'faceMatch.learnMore': `深入瞭解。`, + 'faceMatch.scanning': `正在掃描 · 人臉辨識…`, + 'faceMatch.faceAlt': `臉 {index}`, + 'faceMatch.minConf': `最小信心值`, + 'faceMatch.minConfTitle': `最低信心值:{percent}%`, + 'faceMatch.detected': `已偵測到`, + 'faceMatch.detectedFaceAlt': `偵測到的人臉`, + 'faceMatch.vs': `vs`, + 'faceMatch.supportPatreon': `在 Patreon 上支持`, + 'faceMatch.ofSelected': `已選擇 {total} 中的 {selected}`, + 'faceMatch.allInScene': `找到 {total} 張臉 · 全部在場景中`, + 'faceMatch.clickToSelect': `找到 {total} 張臉 · 按一下以選擇`, + 'faceMatch.kbSwitch': `切換人臉`, + 'faceMatch.kbSelect': `選擇表演者`, + 'faceMatch.kbToggle': `切換選擇`, + 'faceMatch.kbAddInstant': `Shift+按一下立即新增`, + 'faceMatch.selectBest': `選擇最佳相符項目`, + 'faceMatch.adding': `正在新增...`, + 'faceMatch.done': `完成({count})`, + 'faceMatch.toast.added': `已將表演者加入 {target}。`, + 'faceMatch.toast.addError': `新增表演者失敗:{error}`, + 'faceMatch.toast.noStashbox': `未設定 stash-box。請在設定 → 中繼資料提供者中新增 stash-box 提供者以啟用表演者匯入。參見 {url}`, + 'faceMatch.toast.noProvider': `找不到名為「StashDB」的提供者。請在設定 → 中繼資料提供者中重新命名你的提供者,使其為「StashDB」,以啟用表演者匯入。`, + 'faceMatch.toast.configureProvider': `請在設定 → 中繼資料提供者中設定 stash-box 提供者,以啟用表演者匯入。`, + 'faceMatch.toast.addedMultiple': `已將 {count} 位表演者{s}加入 {target}。`, + 'sprite.title': `場景表演者`, + 'sprite.close': `關閉`, + 'sprite.foundConfirmed': `找到 {found} · 已確認 {confirmed}`, + 'sprite.confidence': `信心值`, + 'sprite.name': `名稱`, + 'sprite.hits': `命中`, + 'sprite.minConf': `最小信心值`, + 'sprite.minConfTitle': `最低信心值:{percent}%`, + 'sprite.scanning': `Visage 正在掃描…`, + 'sprite.cancel': `取消`, + 'sprite.empty': `此精靈圖中未識別出任何表演者。`, + 'sprite.detectedFaceAlt': `偵測到的人臉`, + 'sprite.spriteLabel': `精靈圖`, + 'sprite.stashLabel': `STASH`, + 'sprite.vs': `vs`, + 'sprite.inScene': `在場景中`, + 'sprite.hitsCount': `{count} 次命中{s}`, + 'sprite.totalTime': `共 {time}`, + 'sprite.alreadyInScene': `已在場景中`, + 'sprite.clickToConfirm': `按一下以確認`, + 'sprite.confirmed': `已確認`, + 'sprite.supportPatreon': `在 Patreon 上支持`, + 'sprite.confirmedCount': `已確認 {total} 中的 {confirmed}`, + 'sprite.shownHint': `顯示 {shown}(共 {total} 個)· 按一下以確認 · ←→ 導覽 · Enter 確認`, + 'sprite.confirmHint': `按一下以確認 · ←→ 導覽 · Enter 確認`, + 'sprite.adding': `正在新增...`, + 'sprite.done': `完成({count})`, + 'gender.male': `男性`, + 'gender.female': `女性`, + 'gender.transMale': `跨性別男性`, + 'gender.transFemale': `跨性別女性`, + 'gender.nonBinary': `非二元性別`, + 'gender.intersex': `雙性人`, + 'card.excellent': `極佳相符`, + 'card.good': `良好相符`, + 'card.uncertain': `不確定的相符`, + 'card.select': `選擇 {name}`, + 'card.deselect': `取消選擇 {name}`, + 'card.openOn': `在 {source} 上開啟`, + 'search.overlayHint': `拖曳以選擇一張臉 — Enter 掃描整個畫面 — Esc 取消`, + 'search.noFaces': `該選擇中找不到人臉。請嘗試更緊密的裁剪,或按 Enter 掃描整個畫面。`, + 'search.captureMediaFail': `無法擷取媒體。請確認場景/影像已完全載入。`, + 'search.healthBanner': `人臉辨識 API 無法連線。請啟動後端並重試。`, + 'search.failed': `人臉搜尋失敗:{error}`, + 'search.fetchImageFail': `無法從 Stash 取得影像。`, + 'search.captureFail': `擷取影像失敗:{error}`, + 'search.selectFaceImage': `請在影像中選擇一張人臉。`, + 'search.captureFrameFail': `無法擷取目前影格。`, + 'search.captureFrameFail2': `擷取目前影格失敗。`, + 'search.selectFaceVideo': `請在影片播放器區域內選擇一張人臉。`, + 'search.menuItemTitle': `拖曳框選一張人臉,或按 Enter 掃描整個畫面,以在 StashDB 中搜尋相符項目`, + 'search.currentFrame': `Visage:目前影格`, + 'scene.noSprite': `此場景沒有精靈圖或預覽影片。請在場景設定中產生它們,然後重試。`, + 'scene.noFaces': `在此場景的精靈圖或預覽影片中找不到人臉或表演者。`, + 'scene.healthBanner': `人臉辨識 API 無法連線。請啟動後端並重試。`, + 'scene.failed': `場景掃描失敗:{error}`, + 'scene.menuItemTitle': `識別場景中的每個表演者(需要產生的精靈圖或預覽影片)`, + 'scene.wholeScene': `Visage:整個場景`, + 'banner.changeBackend': `更換後端`, + 'banner.dismiss': `關閉`, + 'error.dismiss': `關閉`, + 'firstRun.title': `設定你的 Visage 後端`, + 'firstRun.subtitle': `Visage 會將人臉影像傳送至後端進行識別。選擇要在哪裡執行它。`, + 'firstRun.cloud': `使用 Hugging Face 雲端`, + 'firstRun.cloudNote': `零設定。影像會被傳送至 Hugging Face 雲端服務。`, + 'firstRun.local': `使用我自己的伺服器`, + 'firstRun.localNote': `在你的電腦或網路上執行私人二進位檔。`, + 'firstRun.skip': `暫時略過`, + 'badge.local': `本機`, + 'badge.cloud': `雲端(Hugging Face)`, + 'badge.title': `Visage 後端:{label}`, + 'donate.enjoying': `喜歡 Visage?幫助它持續運作`, + 'donate.supportPatreon': `在 Patreon 上支持`, + 'frame.close': `關閉影格選擇器`, + 'frame.seekFail': `無法快轉影片播放器。`, + 'frame.selectAt': `選擇 {time} 秒處的人臉影格`, + }; + + // Romanian. + const ro = { + 'backendSettings.title': `Setări backend`, + 'backendSettings.closeAria': `Închide setările`, + 'backendSettings.backendAria': `Setări backend`, + 'backendSettings.changeBackend': `Schimbă backend-ul`, + 'backendSettings.backendLabel': `Backend`, + 'backendSettings.local': `Local`, + 'backendSettings.cloud': `Cloud (Hugging Face)`, + 'backendSettings.cloudNote': `Imaginile sunt trimise către serviciul cloud Hugging Face.`, + 'backendSettings.hintPrefix': `Vrei ca imaginile tale să rămână în rețeaua ta?`, + 'backendSettings.hintLink': `Rulează un server privat prin Patreon`, + 'backendSettings.urlLabel': `URL`, + 'backendSettings.csp1': "Politica de securitate a browserului (CSP) permite doar", + 'backendSettings.csp2': `în mod implicit. Pentru a ajunge la un backend local la altă adresă (de ex. IP-ul tău LAN), adaugă-l în`, + 'backendSettings.csp3': `lista din`, + 'backendSettings.csp4': `fișierul din folderul tău de plugin-uri Stash, altfel cererile vor fi blocate. Notă: actualizarea Visage reinstalează`, + 'backendSettings.csp5': `, deci acest lucru trebuie refăcut după fiecare actualizare.`, + 'backendSettings.testing': `Se testează conexiunea…`, + 'backendSettings.testConnection': `Testează conexiunea`, + 'backendSettings.testingShort': `Se testează…`, + 'backendSettings.cancel': `Anulează`, + 'backendSettings.save': `Salvează`, + 'backendSettings.feedback.reachable': `Conexiune reușită. Backend-ul este pregătit.`, + 'backendSettings.feedback.degraded': `Backend-ul este accesibil, dar degradat (modelele sau indexul nu sunt încărcate).`, + 'backendSettings.feedback.unreachable': `Backend-ul este inaccesibil. Verifică URL-ul și că backend-ul rulează.`, + 'faceMatch.title': `CADRUL CURENT`, + 'faceMatch.close': `Închide`, + 'faceMatch.facesSelected': `{faces} fețe găsite · {selected} selectate`, + 'faceMatch.inScene': `· {count} în scenă`, + 'faceMatch.stashboxMissing': `Niciun stash-box configurat.`, + 'faceMatch.stashboxMissingBody': ` Adaugă un furnizor stash-box în Setări → Furnizori de metadate pentru a activa importul de performeri.`, + 'faceMatch.stashboxWrongName': `Niciun furnizor "StashDB" găsit.`, + 'faceMatch.stashboxWrongNameBody': ` Importul de performeri necesită un furnizor numit "StashDB". Redenumește furnizorul în Setări → Furnizori de metadate.`, + 'faceMatch.learnMore': `Află mai multe.`, + 'faceMatch.scanning': `Se scanează • recunoaștere facială…`, + 'faceMatch.faceAlt': `Față {index}`, + 'faceMatch.minConf': `Min. conf.`, + 'faceMatch.minConfTitle': `Încredere minimă: {percent}%`, + 'faceMatch.detected': `Detectat`, + 'faceMatch.detectedFaceAlt': `Față detectată`, + 'faceMatch.vs': `vs`, + 'faceMatch.supportPatreon': `Susține pe Patreon`, + 'faceMatch.ofSelected': `{selected} din {total} selectate`, + 'faceMatch.allInScene': `{total} fețe găsite · toate în scenă`, + 'faceMatch.clickToSelect': `{total} fețe găsite · faceți clic pentru a selecta`, + 'faceMatch.kbSwitch': `Schimbă fețele`, + 'faceMatch.kbSelect': `Selectează performeri`, + 'faceMatch.kbToggle': `Comută selecția`, + 'faceMatch.kbAddInstant': `Shift+clic pentru a adăuga instant`, + 'faceMatch.selectBest': `Selectează cele mai bune potriviri`, + 'faceMatch.adding': `Se adaugă...`, + 'faceMatch.done': `Gata ({count})`, + 'faceMatch.toast.added': `Performer adăugat în {target}.`, + 'faceMatch.toast.addError': `Nu s-a reușit adăugarea performerului: {error}`, + 'faceMatch.toast.noStashbox': `Niciun stash-box configurat. Adaugă un furnizor stash-box în Setări → Furnizori de metadate pentru a activa importul de performeri. Vezi {url}`, + 'faceMatch.toast.noProvider': `Niciun furnizor numit "StashDB" găsit. Redenumește furnizorul în "StashDB" în Setări → Furnizori de metadate pentru a activa importul de performeri.`, + 'faceMatch.toast.configureProvider': `Configurează un furnizor stash-box în Setări → Furnizori de metadate pentru a activa importul de performeri.`, + 'faceMatch.toast.addedMultiple': `S-au adăugat {count} performeri în {target}.`, + 'sprite.title': `PERFORMERI DIN SCENĂ`, + 'sprite.close': `Închide`, + 'sprite.foundConfirmed': `{found} găsite · {confirmed} confirmate`, + 'sprite.confidence': `încredere`, + 'sprite.name': `nume`, + 'sprite.hits': `potriviri`, + 'sprite.minConf': `Min. conf.`, + 'sprite.minConfTitle': `Încredere minimă: {percent}%`, + 'sprite.scanning': `Visage scanează…`, + 'sprite.cancel': `Anulează`, + 'sprite.empty': `Niciun performer identificat în acest sprite.`, + 'sprite.detectedFaceAlt': `Față detectată`, + 'sprite.spriteLabel': `SPRITE`, + 'sprite.stashLabel': `STASH`, + 'sprite.vs': `vs`, + 'sprite.inScene': `În scenă`, + 'sprite.hitsCount': `{count} potriviri`, + 'sprite.totalTime': `{time} total`, + 'sprite.alreadyInScene': `Deja în scenă`, + 'sprite.clickToConfirm': `Faceți clic pentru a confirma`, + 'sprite.confirmed': `Confirmat`, + 'sprite.supportPatreon': `Susține pe Patreon`, + 'sprite.confirmedCount': `{confirmed} din {total} confirmate`, + 'sprite.shownHint': `{shown} afișate ({total} total) · faceți clic pentru a confirma · ←→ navigare · Enter confirmă`, + 'sprite.confirmHint': `Faceți clic pentru a confirma · ←→ navigare · Enter confirmă`, + 'sprite.adding': `Se adaugă...`, + 'sprite.done': `Gata ({count})`, + 'gender.male': `Bărbat`, + 'gender.female': `Femeie`, + 'gender.transMale': `Bărbat trans`, + 'gender.transFemale': `Femeie trans`, + 'gender.nonBinary': `Non-binar`, + 'gender.intersex': `Intersex`, + 'card.excellent': `Potrivire excelentă`, + 'card.good': `Potrivire bună`, + 'card.uncertain': `Potrivire incertă`, + 'card.select': `Selectează {name}`, + 'card.deselect': `Deselectează {name}`, + 'card.openOn': `Deschide pe {source}`, + 'search.overlayHint': `Trageți pentru a selecta o față — Enter pentru a scana întregul cadru — Esc pentru a anula`, + 'search.noFaces': `Nicio față găsită în această selecție. Încearcă o decupare mai strânsă sau apasă Enter pentru a scana întregul cadru.`, + 'search.captureMediaFail': `Nu s-a putut captura media. Asigură-te că scena/imaginea este complet încărcată.`, + 'search.healthBanner': `API-ul de recunoaștere facială nu este accesibil. Pornește backend-ul și încearcă din nou.`, + 'search.failed': `Căutarea feței a eșuat: {error}`, + 'search.fetchImageFail': `Nu s-a putut prelua imaginea din Stash.`, + 'search.captureFail': `Nu s-a reușit capturarea imaginii: {error}`, + 'search.selectFaceImage': `Selectează o față în imagine.`, + 'search.captureFrameFail': `Nu s-a putut captura cadrul curent.`, + 'search.captureFrameFail2': `Nu s-a reușit capturarea cadrului curent.`, + 'search.selectFaceVideo': `Selectează o față în zona playerului video.`, + 'search.menuItemTitle': `Trageți o casetă în jurul unei fețe, sau apăsați Enter pentru a scana întregul cadru, pentru a căuta potriviri în StashDB`, + 'search.currentFrame': `Visage: Cadrul curent`, + 'scene.noSprite': `Nu s-a găsit nicio foaie de sprite sau video de previzualizare pentru această scenă. Generează-le în Setările scenei, apoi încearcă din nou.`, + 'scene.noFaces': `Nu s-au găsit fețe sau performeri în foaia de sprite sau video-ul de previzualizare al acestei scene.`, + 'scene.healthBanner': `API-ul de recunoaștere facială nu este accesibil. Pornește backend-ul și încearcă din nou.`, + 'scene.failed': `Scanarea scenei a eșuat: {error}`, + 'scene.menuItemTitle': `Identifică fiecare performer din scenă (necesită o foaie de sprite generată sau un video de previzualizare)`, + 'scene.wholeScene': `Visage: Toată scena`, + 'banner.changeBackend': `Schimbă backend-ul`, + 'banner.dismiss': `Închide`, + 'error.dismiss': `Închide`, + 'firstRun.title': `Configurează backend-ul Visage`, + 'firstRun.subtitle': `Visage trimite imagini cu fețe către un backend pentru recunoaștere. Alege unde să-l rulezi.`, + 'firstRun.cloud': `Folosește cloud Hugging Face`, + 'firstRun.cloudNote': `Fără configurare. Imaginile sunt trimise către serviciul cloud Hugging Face.`, + 'firstRun.local': `Folosește propriul server`, + 'firstRun.localNote': `Rulează binarul privat pe propria mașină sau rețea.`, + 'firstRun.skip': `Sari pentru acum`, + 'badge.local': `Local`, + 'badge.cloud': `Cloud (Hugging Face)`, + 'badge.title': `Backend Visage: {label}`, + 'donate.enjoying': `Îți place Visage? Ajută să rămână în viață`, + 'donate.supportPatreon': `Susține pe Patreon`, + 'frame.close': `Închide selectorul de cadru`, + 'frame.seekFail': `Nu s-a reușit căutarea în playerul video.`, + 'frame.selectAt': `Selectează cadrul feței la {time}s`, + }; + + const sk = { + 'backendSettings.title': `Nastavenia backendu`, + 'backendSettings.closeAria': `Zavrieť nastavenia`, + 'backendSettings.backendAria': `Nastavenia backendu`, + 'backendSettings.changeBackend': `Zmeniť backend`, + 'backendSettings.backendLabel': `Backend`, + 'backendSettings.local': `Lokálny`, + 'backendSettings.cloud': `Cloud (Hugging Face)`, + 'backendSettings.cloudNote': `Obrázky sa odosielajú do cloudovej služby Hugging Face.`, + 'backendSettings.hintPrefix': `Chcete, aby vaše obrázky zostali vo vašej sieti?`, + 'backendSettings.hintLink': `Spustite súkromný server cez Patreon`, + 'backendSettings.urlLabel': `URL`, + 'backendSettings.csp1': `Bezpečnostná politika prehliadača (CSP) v predvolenom nastavení povoľuje iba`, + 'backendSettings.csp2': `. Ak sa chcete dostať k lokálnemu backendu na inej adrese (napr. IP vašej LAN), pridajte ju do`, + 'backendSettings.csp3': `zoznamu v`, + 'backendSettings.csp4': `súbore v priečinku pluginov Stash, inak budú požiadavky zablokované. Poznámka: aktualizácia Visage preinštaluje`, + 'backendSettings.csp5': `, takže to musíte vykonať znova po každej aktualizácii.`, + 'backendSettings.testing': `Testovanie pripojenia…`, + 'backendSettings.testConnection': `Otestovať pripojenie`, + 'backendSettings.testingShort': `Testovanie…`, + 'backendSettings.cancel': `Zrušiť`, + 'backendSettings.save': `Uložiť`, + 'backendSettings.feedback.reachable': `Pripojenie úspešné. Backend je pripravený.`, + 'backendSettings.feedback.degraded': `Backend dosiahnuteľný, ale degradovaný (modely alebo index nie sú načítané).`, + 'backendSettings.feedback.unreachable': `Backend je nedosiahnuteľný. Skontrolujte URL a uistite sa, že backend beží.`, + 'faceMatch.title': `AKTUÁLNY SNÍMOK`, + 'faceMatch.close': `Zavrieť`, + 'faceMatch.facesSelected': `Nájdených tvárí: {faces} · vybratých: {selected}`, + 'faceMatch.inScene': `· {count} v scéne`, + 'faceMatch.stashboxMissing': `Nie je nakonfigurovaný stash-box.`, + 'faceMatch.stashboxMissingBody': ` Pridajte poskytovateľa stash-box v Settings → Metadata Providers, aby ste povolili import performerov.`, + 'faceMatch.stashboxWrongName': `Poskytovateľ "StashDB" nebol nájdený.`, + 'faceMatch.stashboxWrongNameBody': ` Import performerov vyžaduje poskytovateľa s názvom "StashDB". Premenujte poskytovateľa v Settings → Metadata Providers.`, + 'faceMatch.learnMore': `Zistiť viac.`, + 'faceMatch.scanning': `Skenovanie • rozpoznávanie tvárí…`, + 'faceMatch.faceAlt': `Tvár {index}`, + 'faceMatch.minConf': `Min. ist.`, + 'faceMatch.minConfTitle': `Minimálna istota: {percent}%`, + 'faceMatch.detected': `Zistené`, + 'faceMatch.detectedFaceAlt': `Zistená tvár`, + 'faceMatch.vs': `vs`, + 'faceMatch.supportPatreon': `Podporiť na Patreon`, + 'faceMatch.ofSelected': `Vybratých {selected} z {total}`, + 'faceMatch.allInScene': `Nájdených tvárí: {total} · všetky v scéne`, + 'faceMatch.clickToSelect': `Nájdených tvárí: {total} · kliknite pre výber`, + 'faceMatch.kbSwitch': `Prepínanie tvárí`, + 'faceMatch.kbSelect': `Výber performerov`, + 'faceMatch.kbToggle': `Prepínať výber`, + 'faceMatch.kbAddInstant': `Shift+klik pre okamžité pridanie`, + 'faceMatch.selectBest': `Vybrať najlepšie zhody`, + 'faceMatch.adding': `Pridávanie...`, + 'faceMatch.done': `Hotovo ({count})`, + 'faceMatch.toast.added': `Performer bol pridaný do {target}.`, + 'faceMatch.toast.addError': `Nepodarilo sa pridať performera: {error}`, + 'faceMatch.toast.noStashbox': `Nie je nakonfigurovaný stash-box. Pridajte poskytovateľa stash-box v Settings → Metadata Providers, aby ste povolili import performerov. Pozri {url}`, + 'faceMatch.toast.noProvider': `Poskytovateľ "StashDB" nebol nájdený. Premenujte poskytovateľa na "StashDB" v Settings → Metadata Providers, aby ste povolili import performerov.`, + 'faceMatch.toast.configureProvider': `Nakonfigurujte poskytovateľa stash-box v Settings → Metadata Providers, aby ste povolili import performerov.`, + 'faceMatch.toast.addedMultiple': `Pridaní performeri: {count} do {target}.`, + 'sprite.title': `PERFORMERI SCÉNY`, + 'sprite.close': `Zavrieť`, + 'sprite.foundConfirmed': `Nájdených: {found} · potvrdených: {confirmed}`, + 'sprite.confidence': `istota`, + 'sprite.name': `meno`, + 'sprite.hits': `zhody`, + 'sprite.minConf': `Min. ist.`, + 'sprite.minConfTitle': `Minimálna istota: {percent}%`, + 'sprite.scanning': `Skenovanie Visage…`, + 'sprite.cancel': `Zrušiť`, + 'sprite.empty': `V tomto sprite neboli identifikovaní žiadni performeri.`, + 'sprite.detectedFaceAlt': `Zistená tvár`, + 'sprite.spriteLabel': `SPRITE`, + 'sprite.stashLabel': `STASH`, + 'sprite.vs': `vs`, + 'sprite.inScene': `V scéne`, + 'sprite.hitsCount': `Zhôd: {count}`, + 'sprite.totalTime': `Spolu {time}`, + 'sprite.alreadyInScene': `Už v scéne`, + 'sprite.clickToConfirm': `Kliknite pre potvrdenie`, + 'sprite.confirmed': `Potvrdené`, + 'sprite.supportPatreon': `Podporiť na Patreon`, + 'sprite.confirmedCount': `Potvrdených {confirmed} z {total}`, + 'sprite.shownHint': `Zobrazených: {shown} (spolu {total}) · kliknite pre potvrdenie · ←→ navigácia · Enter potvrdenie`, + 'sprite.confirmHint': `Kliknite pre potvrdenie · ←→ navigácia · Enter potvrdenie`, + 'sprite.adding': `Pridávanie...`, + 'sprite.done': `Hotovo ({count})`, + 'gender.male': `Muž`, + 'gender.female': `Žena`, + 'gender.transMale': `Transgender muž`, + 'gender.transFemale': `Transgender žena`, + 'gender.nonBinary': `Ne-binárny`, + 'gender.intersex': `Intersex`, + 'card.excellent': `Výborná zhoda`, + 'card.good': `Dobrá zhoda`, + 'card.uncertain': `Neistá zhoda`, + 'card.select': `Vybrať {name}`, + 'card.deselect': `Zrušiť výber {name}`, + 'card.openOn': `Otvoriť na {source}`, + 'search.overlayHint': `Pretiahnite pre výber tváre — Enter pre skenovanie celého snímku — Esc pre zrušenie`, + 'search.noFaces': `Vo vybranej oblasti neboli nájdené žiadne tváre. Skúste užší orez alebo stlačte Enter pre skenovanie celého snímku.`, + 'search.captureMediaFail': `Nepodarilo sa zachytiť médiá. Uistite sa, že scéna/obrázok je plne načítaný.`, + 'search.healthBanner': `API pre rozpoznávanie tvárí je nedosiahnuteľné. Spustite backend a skúste to znova.`, + 'search.failed': `Hľadanie tváre zlyhalo: {error}`, + 'search.fetchImageFail': `Nepodarilo sa načítať obrázok zo Stash.`, + 'search.captureFail': `Nepodarilo sa zachytiť obrázok: {error}`, + 'search.selectFaceImage': `Vyberte tvár v obrázku.`, + 'search.captureFrameFail': `Nepodarilo sa zachytiť aktuálny snímok.`, + 'search.captureFrameFail2': `Chyba pri zachytení aktuálneho snímku.`, + 'search.selectFaceVideo': `Vyberte tvár v oblasti prehrávača videa.`, + 'search.menuItemTitle': `Pretiahnite rámček okolo tváre alebo stlačte Enter pre skenovanie celého snímku, aby ste vyhľadali zhody v StashDB`, + 'search.currentFrame': `Visage: aktuálny snímok`, + 'scene.noSprite': `Pre túto scénu sa nenašiel žiadny sprite list ani náhľadové video. Vygenerujte ich v nastaveniach scény a skúste to znova.`, + 'scene.noFaces': `V sprite liste alebo náhľadovom videu tejto scény sa nenašli žiadne tváre ani performeri.`, + 'scene.healthBanner': `API pre rozpoznávanie tvárí je nedosiahnuteľné. Spustite backend a skúste to znova.`, + 'scene.failed': `Skenovanie scény zlyhalo: {error}`, + 'scene.menuItemTitle': `Identifikujte každého performera v scéne (vyžaduje vygenerovaný sprite list alebo náhľadové video)`, + 'scene.wholeScene': `Visage: celá scéna`, + 'banner.changeBackend': `Zmeniť backend`, + 'banner.dismiss': `Zavrieť`, + 'error.dismiss': `Zavrieť`, + 'firstRun.title': `Nastavte backend Visage`, + 'firstRun.subtitle': `Visage odosiela obrázky tvárí do backendu na rozpoznanie. Vyberte, kde ho spustiť.`, + 'firstRun.cloud': `Použiť cloud Hugging Face`, + 'firstRun.cloudNote': `Bez nastavovania. Obrázky sa odosielajú do cloudovej služby Hugging Face.`, + 'firstRun.local': `Použiť vlastný server`, + 'firstRun.localNote': `Spustite súkromný binárny súbor na vlastnom počítači alebo v sieti.`, + 'firstRun.skip': `Zatiaľ preskočiť`, + 'badge.local': `Lokálny`, + 'badge.cloud': `Cloud (Hugging Face)`, + 'badge.title': `Backend Visage: {label}`, + 'donate.enjoying': `Páči sa vám Visage? Pomôžte ho udržať`, + 'donate.supportPatreon': `Podporiť na Patreon`, + 'frame.close': `Zavrieť výber snímku`, + 'frame.seekFail': `Nepodarilo sa pretočiť prehrávač videa.`, + 'frame.selectAt': `Vybrať snímok tváre v {time} s`, + }; + + const pl = { + 'backendSettings.title': `Ustawienia backendu`, + 'backendSettings.closeAria': `Zamknij ustawienia`, + 'backendSettings.backendAria': `Ustawienia backendu`, + 'backendSettings.changeBackend': `Zmień backend`, + 'backendSettings.backendLabel': `Backend`, + 'backendSettings.local': `Lokalny`, + 'backendSettings.cloud': `Chmura (Hugging Face)`, + 'backendSettings.cloudNote': `Obrazy są wysyłane do usługi w chmurze Hugging Face.`, + 'backendSettings.hintPrefix': `Chcesz, aby Twoje obrazy pozostały w Twojej sieci?`, + 'backendSettings.hintLink': `Uruchom prywatny serwer przez Patreon`, + 'backendSettings.urlLabel': `URL`, + 'backendSettings.csp1': `Polityka bezpieczeństwa przeglądarki (CSP) domyślnie zezwala tylko na`, + 'backendSettings.csp2': `. Aby uzyskać dostęp do lokalnego backendu na innym adresie (np. IP Twojej sieci LAN), dodaj go do`, + 'backendSettings.csp3': `listy w`, + 'backendSettings.csp4': `pliku w folderze wtyczek Stash, w przeciwnym razie żądania zostaną zablokowane. Uwaga: aktualizacja Visage ponownie instaluje`, + 'backendSettings.csp5': `, więc należy to powtórzyć po każdej aktualizacji.`, + 'backendSettings.testing': `Testowanie połączenia…`, + 'backendSettings.testConnection': `Przetestuj połączenie`, + 'backendSettings.testingShort': `Testowanie…`, + 'backendSettings.cancel': `Anuluj`, + 'backendSettings.save': `Zapisz`, + 'backendSettings.feedback.reachable': `Połączenie udane. Backend jest gotowy.`, + 'backendSettings.feedback.degraded': `Backend osiągalny, ale zdegradowany (modele lub indeks nie zostały załadowane).`, + 'backendSettings.feedback.unreachable': `Backend nieosiągalny. Sprawdź URL i upewnij się, że backend działa.`, + 'faceMatch.title': `AKTUALNA KLATKA`, + 'faceMatch.close': `Zamknij`, + 'faceMatch.facesSelected': `Znaleziono twarzy: {faces} · wybrano: {selected}`, + 'faceMatch.inScene': `· {count} w scenie`, + 'faceMatch.stashboxMissing': `Brak skonfigurowanego stash-box.`, + 'faceMatch.stashboxMissingBody': ` Dodaj dostawcę stash-box w Settings → Metadata Providers, aby włączyć import wykonawców.`, + 'faceMatch.stashboxWrongName': `Nie znaleziono dostawcy "StashDB".`, + 'faceMatch.stashboxWrongNameBody': ` Import wykonawców wymaga dostawcy o nazwie "StashDB". Zmień nazwę dostawcy w Settings → Metadata Providers.`, + 'faceMatch.learnMore': `Dowiedz się więcej.`, + 'faceMatch.scanning': `Skanowanie • rozpoznawanie twarzy…`, + 'faceMatch.faceAlt': `Twarz {index}`, + 'faceMatch.minConf': `Min. pew.`, + 'faceMatch.minConfTitle': `Minimalna pewność: {percent}%`, + 'faceMatch.detected': `Wykryto`, + 'faceMatch.detectedFaceAlt': `Wykryta twarz`, + 'faceMatch.vs': `vs`, + 'faceMatch.supportPatreon': `Wesprzyj na Patreon`, + 'faceMatch.ofSelected': `Wybrano {selected} z {total}`, + 'faceMatch.allInScene': `Znaleziono twarzy: {total} · wszystkie w scenie`, + 'faceMatch.clickToSelect': `Znaleziono twarzy: {total} · kliknij, aby wybrać`, + 'faceMatch.kbSwitch': `Przełączanie twarzy`, + 'faceMatch.kbSelect': `Wybór wykonawców`, + 'faceMatch.kbToggle': `Przełącz wybór`, + 'faceMatch.kbAddInstant': `Shift+klik, aby dodać natychmiast`, + 'faceMatch.selectBest': `Wybierz najlepsze dopasowania`, + 'faceMatch.adding': `Dodawanie...`, + 'faceMatch.done': `Gotowe ({count})`, + 'faceMatch.toast.added': `Dodano wykonawcę do {target}.`, + 'faceMatch.toast.addError': `Nie udało się dodać wykonawcy: {error}`, + 'faceMatch.toast.noStashbox': `Brak skonfigurowanego stash-box. Dodaj dostawcę stash-box w Settings → Metadata Providers, aby włączyć import wykonawców. Zobacz {url}`, + 'faceMatch.toast.noProvider': `Nie znaleziono dostawcy "StashDB". Zmień nazwę dostawcy na "StashDB" w Settings → Metadata Providers, aby włączyć import wykonawców.`, + 'faceMatch.toast.configureProvider': `Skonfiguruj dostawcę stash-box w Settings → Metadata Providers, aby włączyć import wykonawców.`, + 'faceMatch.toast.addedMultiple': `Dodano wykonawców: {count} do {target}.`, + 'sprite.title': `WYKONAWCY SCENY`, + 'sprite.close': `Zamknij`, + 'sprite.foundConfirmed': `Znaleziono: {found} · potwierdzono: {confirmed}`, + 'sprite.confidence': `pewność`, + 'sprite.name': `nazwa`, + 'sprite.hits': `trafienia`, + 'sprite.minConf': `Min. pew.`, + 'sprite.minConfTitle': `Minimalna pewność: {percent}%`, + 'sprite.scanning': `Skanowanie Visage…`, + 'sprite.cancel': `Anuluj`, + 'sprite.empty': `W tym sprite nie zidentyfikowano wykonawców.`, + 'sprite.detectedFaceAlt': `Wykryta twarz`, + 'sprite.spriteLabel': `SPRITE`, + 'sprite.stashLabel': `STASH`, + 'sprite.vs': `vs`, + 'sprite.inScene': `W scenie`, + 'sprite.hitsCount': `Trafień: {count}`, + 'sprite.totalTime': `Łącznie {time}`, + 'sprite.alreadyInScene': `Już w scenie`, + 'sprite.clickToConfirm': `Kliknij, aby potwierdzić`, + 'sprite.confirmed': `Potwierdzono`, + 'sprite.supportPatreon': `Wesprzyj na Patreon`, + 'sprite.confirmedCount': `Potwierdzono {confirmed} z {total}`, + 'sprite.shownHint': `Pokazano: {shown} (łącznie {total}) · kliknij, aby potwierdzić · ←→ nawigacja · Enter potwierdzenie`, + 'sprite.confirmHint': `Kliknij, aby potwierdzić · ←→ nawigacja · Enter potwierdzenie`, + 'sprite.adding': `Dodawanie...`, + 'sprite.done': `Gotowe ({count})`, + 'gender.male': `Mężczyzna`, + 'gender.female': `Kobieta`, + 'gender.transMale': `Transpłciowy mężczyzna`, + 'gender.transFemale': `Transpłciowa kobieta`, + 'gender.nonBinary': `Niebinarny`, + 'gender.intersex': `Interpłciowy`, + 'card.excellent': `Doskonałe dopasowanie`, + 'card.good': `Dobre dopasowanie`, + 'card.uncertain': `Niepewne dopasowanie`, + 'card.select': `Wybierz {name}`, + 'card.deselect': `Odznacz {name}`, + 'card.openOn': `Otwórz na {source}`, + 'search.overlayHint': `Przeciągnij, aby wybrać twarz — Enter, aby przeskanować całą klatkę — Esc, aby anulować`, + 'search.noFaces': `W wybranym obszarze nie znaleziono twarzy. Spróbuj ciaśniejszego kadru lub naciśnij Enter, aby przeskanować całą klatkę.`, + 'search.captureMediaFail': `Nie udało się przechwycić mediów. Upewnij się, że scena/obraz jest w pełni załadowany.`, + 'search.healthBanner': `API rozpoznawania twarzy jest nieosiągalne. Uruchom backend i spróbuj ponownie.`, + 'search.failed': `Wyszukiwanie twarzy nie powiodło się: {error}`, + 'search.fetchImageFail': `Nie udało się pobrać obrazu ze Stash.`, + 'search.captureFail': `Nie udało się przechwycić obrazu: {error}`, + 'search.selectFaceImage': `Zaznacz twarz w obrazie.`, + 'search.captureFrameFail': `Nie udało się przechwycić bieżącej klatki.`, + 'search.captureFrameFail2': `Błąd podczas przechwytywania bieżącej klatki.`, + 'search.selectFaceVideo': `Zaznacz twarz w obszarze odtwarzacza wideo.`, + 'search.menuItemTitle': `Przeciągnij ramkę wokół twarzy lub naciśnij Enter, aby przeskanować całą klatkę i wyszukać dopasowania w StashDB`, + 'search.currentFrame': `Visage: bieżąca klatka`, + 'scene.noSprite': `Nie znaleziono arkusza sprite ani wideo podglądu dla tej sceny. Wygeneruj je w ustawieniach sceny, a następnie spróbuj ponownie.`, + 'scene.noFaces': `Nie znaleziono twarzy ani wykonawców w arkuszu sprite lub wideo podglądu tej sceny.`, + 'scene.healthBanner': `API rozpoznawania twarzy jest nieosiągalne. Uruchom backend i spróbuj ponownie.`, + 'scene.failed': `Skanowanie sceny nie powiodło się: {error}`, + 'scene.menuItemTitle': `Zidentyfikuj każdego wykonawcę w scenie (wymaga wygenerowanego arkusza sprite lub wideo podglądu)`, + 'scene.wholeScene': `Visage: cała scena`, + 'banner.changeBackend': `Zmień backend`, + 'banner.dismiss': `Odrzuć`, + 'error.dismiss': `Odrzuć`, + 'firstRun.title': `Skonfiguruj backend Visage`, + 'firstRun.subtitle': `Visage wysyła obrazy twarzy do backendu w celu rozpoznania. Wybierz, gdzie go uruchomić.`, + 'firstRun.cloud': `Użyj chmury Hugging Face`, + 'firstRun.cloudNote': `Bez konfiguracji. Obrazy są wysyłane do usługi w chmurze Hugging Face.`, + 'firstRun.local': `Użyj własnego serwera`, + 'firstRun.localNote': `Uruchom prywatny plik binarny na własnej maszynie lub w sieci.`, + 'firstRun.skip': `Pomiń na razie`, + 'badge.local': `Lokalny`, + 'badge.cloud': `Chmura (Hugging Face)`, + 'badge.title': `Backend Visage: {label}`, + 'donate.enjoying': `Podoba Ci się Visage? Pomóż utrzymać go przy życiu`, + 'donate.supportPatreon': `Wesprzyj na Patreon`, + 'frame.close': `Zamknij wybór klatki`, + 'frame.seekFail': `Nie udało się przewinąć odtwarzacza wideo.`, + 'frame.selectAt': `Wybierz klatkę twarzy w {time} s`, + }; + + // Greek. + const el = { + 'backendSettings.title': `Ρυθμίσεις backend`, + 'backendSettings.closeAria': `Κλείσιμο ρυθμίσεων`, + 'backendSettings.backendAria': `Ρυθμίσεις backend`, + 'backendSettings.changeBackend': `Αλλαγή backend`, + 'backendSettings.backendLabel': `Backend`, + 'backendSettings.local': `Τοπικό`, + 'backendSettings.cloud': `Cloud (Hugging Face)`, + 'backendSettings.cloudNote': `Οι εικόνες αποστέλλονται στην υπηρεσία cloud του Hugging Face.`, + 'backendSettings.hintPrefix': `Θέλετε οι εικόνες σας να παραμείνουν στο δίκτυό σας;`, + 'backendSettings.hintLink': `Εκτελέστε έναν ιδιωτικό διακομιστή μέσω του Patreon`, + 'backendSettings.urlLabel': `URL`, + 'backendSettings.csp1': "Η πολιτική ασφαλείας του προγράμματος περιήγησης (CSP) επιτρέπει από προεπιλογή μόνο", + 'backendSettings.csp2': `Για να προσεγγίσετε ένα τοπικό backend σε άλλη διεύθυνση (π.χ. τη LAN IP σας), προσθέστε το στη`, + 'backendSettings.csp3': `λίστα στο`, + 'backendSettings.csp4': `αρχείο μέσα στον φάκελο plugins του Stash, διαφορετικά τα αιτήματα θα αποκλειστούν. Σημείωση: η ενημέρωση του Visage επανεγκαθιστά το`, + 'backendSettings.csp5': `, οπότε αυτό πρέπει να εφαρμοστεί ξανά μετά από κάθε ενημέρωση.`, + 'backendSettings.testing': `Έλεγχος σύνδεσης…`, + 'backendSettings.testConnection': `Έλεγχος σύνδεσης`, + 'backendSettings.testingShort': `Έλεγχος…`, + 'backendSettings.cancel': `Ακύρωση`, + 'backendSettings.save': `Αποθήκευση`, + 'backendSettings.feedback.reachable': `Η σύνδεση ήταν επιτυχής. Το backend είναι έτοιμο.`, + 'backendSettings.feedback.degraded': `Το backend είναι προσβάσιμο αλλά υποβαθμισμένο (τα μοντέλα ή το ευρετήριο δεν έχουν φορτωθεί).`, + 'backendSettings.feedback.unreachable': `Το backend δεν είναι προσβάσιμο. Ελέγξτε το URL και ότι το backend εκτελείται.`, + 'faceMatch.title': `ΤΡΕΧΟΝ ΚΑΡΕ`, + 'faceMatch.close': `Κλείσιμο`, + 'faceMatch.facesSelected': `{faces} πρόσωπα βρέθηκαν · {selected} επιλέχθηκαν`, + 'faceMatch.inScene': `· {count} στη σκηνή`, + 'faceMatch.stashboxMissing': `Δεν έχει διαμορφωθεί stash-box.`, + 'faceMatch.stashboxMissingBody': ` Προσθέστε έναν πάροχο stash-box στις Ρυθμίσεις → Πάροχοι μεταδεδομένων για να ενεργοποιήσετε την εισαγωγή ερμηνευτών.`, + 'faceMatch.stashboxWrongName': `Δεν βρέθηκε πάροχος "StashDB".`, + 'faceMatch.stashboxWrongNameBody': ` Η εισαγωγή ερμηνευτών απαιτεί έναν πάροχο με το όνομα "StashDB". Μετονομάστε τον πάροχό σας στις Ρυθμίσεις → Πάροχοι μεταδεδομένων.`, + 'faceMatch.learnMore': `Μάθετε περισσότερα.`, + 'faceMatch.scanning': `Σάρωση • αναγνώριση προσώπου…`, + 'faceMatch.faceAlt': `Πρόσωπο {index}`, + 'faceMatch.minConf': `Ελάχ. εμπ.`, + 'faceMatch.minConfTitle': `Ελάχιστη εμπιστοσύνη: {percent}%`, + 'faceMatch.detected': `Εντοπίστηκε`, + 'faceMatch.detectedFaceAlt': `Πρόσωπο που εντοπίστηκε`, + 'faceMatch.vs': `vs`, + 'faceMatch.supportPatreon': `Υποστήριξη στο Patreon`, + 'faceMatch.ofSelected': `{selected} από {total} επιλέχθηκαν`, + 'faceMatch.allInScene': `{total} πρόσωπα βρέθηκαν · όλα στη σκηνή`, + 'faceMatch.clickToSelect': `{total} πρόσωπα βρέθηκαν · κάντε κλικ για επιλογή`, + 'faceMatch.kbSwitch': `Αλλαγή προσώπων`, + 'faceMatch.kbSelect': `Επιλογή ερμηνευτών`, + 'faceMatch.kbToggle': `Εναλλαγή επιλογής`, + 'faceMatch.kbAddInstant': `Shift+κλικ για άμεση προσθήκη`, + 'faceMatch.selectBest': `Επιλογή καλύτερων αντιστοιχιών`, + 'faceMatch.adding': `Προσθήκη...`, + 'faceMatch.done': `Τέλος ({count})`, + 'faceMatch.toast.added': `Προστέθηκε ερμηνευτής στο {target}.`, + 'faceMatch.toast.addError': `Αποτυχία προσθήκης ερμηνευτή: {error}`, + 'faceMatch.toast.noStashbox': `Δεν έχει διαμορφωθεί stash-box. Προσθέστε έναν πάροχο stash-box στις Ρυθμίσεις → Πάροχοι μεταδεδομένων για να ενεργοποιήσετε την εισαγωγή ερμηνευτών. Δείτε {url}`, + 'faceMatch.toast.noProvider': `Δεν βρέθηκε πάροχος με το όνομα "StashDB". Μετονομάστε τον πάροχό σας σε "StashDB" στις Ρυθμίσεις → Πάροχοι μεταδεδομένων για να ενεργοποιήσετε την εισαγωγή ερμηνευτών.`, + 'faceMatch.toast.configureProvider': `Διαμορφώστε έναν πάροχο stash-box στις Ρυθμίσεις → Πάροχοι μεταδεδομένων για να ενεργοποιήσετε την εισαγωγή ερμηνευτών.`, + 'faceMatch.toast.addedMultiple': `Προστέθηκαν {count} ερμηνευτές στο {target}.`, + 'sprite.title': `ΕΡΜΗΝΕΥΤΕΣ ΣΚΗΝΗΣ`, + 'sprite.close': `Κλείσιμο`, + 'sprite.foundConfirmed': `{found} βρέθηκαν · {confirmed} επιβεβαιώθηκαν`, + 'sprite.confidence': `εμπιστοσύνη`, + 'sprite.name': `όνομα`, + 'sprite.hits': `αντιστοιχίες`, + 'sprite.minConf': `Ελάχ. εμπ.`, + 'sprite.minConfTitle': `Ελάχιστη εμπιστοσύνη: {percent}%`, + 'sprite.scanning': `Σάρωση Visage…`, + 'sprite.cancel': `Ακύρωση`, + 'sprite.empty': `Δεν ταυτοποιήθηκαν ερμηνευτές σε αυτό το sprite.`, + 'sprite.detectedFaceAlt': `Πρόσωπο που εντοπίστηκε`, + 'sprite.spriteLabel': `SPRITE`, + 'sprite.stashLabel': `STASH`, + 'sprite.vs': `vs`, + 'sprite.inScene': `Στη σκηνή`, + 'sprite.hitsCount': `{count} αντιστοιχίες`, + 'sprite.totalTime': `{time} συνολικά`, + 'sprite.alreadyInScene': `Ήδη στη σκηνή`, + 'sprite.clickToConfirm': `Κάντε κλικ για επιβεβαίωση`, + 'sprite.confirmed': `Επιβεβαιώθηκε`, + 'sprite.supportPatreon': `Υποστήριξη στο Patreon`, + 'sprite.confirmedCount': `{confirmed} από {total} επιβεβαιώθηκαν`, + 'sprite.shownHint': `{shown} εμφανίζονται ({total} συνολικά) · κάντε κλικ για επιβεβαίωση · ←→ πλοήγηση · Enter επιβεβαίωση`, + 'sprite.confirmHint': `Κάντε κλικ για επιβεβαίωση · ←→ πλοήγηση · Enter επιβεβαίωση`, + 'sprite.adding': `Προσθήκη...`, + 'sprite.done': `Τέλος ({count})`, + 'gender.male': `Άνδρας`, + 'gender.female': `Γυναίκα`, + 'gender.transMale': `Τρανς άνδρας`, + 'gender.transFemale': `Τρανς γυναίκα`, + 'gender.nonBinary': `Μη δυαδικό`, + 'gender.intersex': `Ίντερσεξ`, + 'card.excellent': `Εξαιρετική αντιστοιχία`, + 'card.good': `Καλή αντιστοιχία`, + 'card.uncertain': `Αβέβαιη αντιστοιχία`, + 'card.select': `Επιλογή {name}`, + 'card.deselect': `Αποεπιλογή {name}`, + 'card.openOn': `Άνοιγμα στο {source}`, + 'search.overlayHint': `Σύρετε για να επιλέξετε ένα πρόσωπο — Enter για σάρωση ολόκληρου του καρέ — Esc για ακύρωση`, + 'search.noFaces': `Δεν βρέθηκαν πρόσωπα σε αυτή την επιλογή. Δοκιμάστε στενότερη περικοπή ή πατήστε Enter για σάρωση ολόκληρου του καρέ.`, + 'search.captureMediaFail': `Δεν ήταν δυνατή η λήψη του μέσου. Βεβαιωθείτε ότι η σκηνή/εικόνα έχει φορτωθεί πλήρως.`, + 'search.healthBanner': `Το API αναγνώρισης προσώπου δεν είναι προσβάσιμο. Εκκινήστε το backend και δοκιμάστε ξανά.`, + 'search.failed': `Η αναζήτηση προσώπου απέτυχε: {error}`, + 'search.fetchImageFail': `Δεν ήταν δυνατή η λήψη εικόνας από το Stash.`, + 'search.captureFail': `Αποτυχία λήψης εικόνας: {error}`, + 'search.selectFaceImage': `Επιλέξτε ένα πρόσωπο στην εικόνα.`, + 'search.captureFrameFail': `Δεν ήταν δυνατή η λήψη του τρέχοντος καρέ.`, + 'search.captureFrameFail2': `Αποτυχία λήψης του τρέχοντος καρέ.`, + 'search.selectFaceVideo': `Επιλέξτε ένα πρόσωπο στην περιοχή του προγράμματος αναπαραγωγής βίντεο.`, + 'search.menuItemTitle': `Σύρετε ένα πλαίσιο γύρω από ένα πρόσωπο ή πατήστε Enter για σάρωση ολόκληρου του καρέ, για αναζήτηση αντιστοιχιών στο StashDB`, + 'search.currentFrame': `Visage: Τρέχον καρέ`, + 'scene.noSprite': `Δεν βρέθηκε φύλλο sprite ή βίντεο προεπισκόπησης για αυτή τη σκηνή. Δημιουργήστε τα στις ρυθμίσεις Scene και δοκιμάστε ξανά.`, + 'scene.noFaces': `Δεν βρέθηκαν πρόσωπα ή ερμηνευτές στο φύλλο sprite ή στο βίντεο προεπισκόπησης αυτής της σκηνής.`, + 'scene.healthBanner': `Το API αναγνώρισης προσώπου δεν είναι προσβάσιμο. Εκκινήστε το backend και δοκιμάστε ξανά.`, + 'scene.failed': `Η σάρωση σκηνής απέτυχε: {error}`, + 'scene.menuItemTitle': `Ταυτοποιήστε κάθε ερμηνευτή στη σκηνή (απαιτεί δημιουργημένο φύλλο sprite ή βίντεο προεπισκόπησης)`, + 'scene.wholeScene': `Visage: Ολόκληρη σκηνή`, + 'banner.changeBackend': `Αλλαγή backend`, + 'banner.dismiss': `Απόρριψη`, + 'error.dismiss': `Απόρριψη`, + 'firstRun.title': `Ρυθμίστε το backend του Visage`, + 'firstRun.subtitle': `Το Visage στέλνει εικόνες προσώπων σε ένα backend για αναγνώριση. Επιλέξτε πού θα το εκτελέσετε.`, + 'firstRun.cloud': `Χρήση cloud Hugging Face`, + 'firstRun.cloudNote': `Καμία εγκατάσταση. Οι εικόνες αποστέλλονται στην υπηρεσία cloud του Hugging Face.`, + 'firstRun.local': `Χρήση του δικού μου διακομιστή`, + 'firstRun.localNote': `Εκτελέστε το ιδιωτικό δυαδικό αρχείο στη δική σας μηχανή ή δίκτυο.`, + 'firstRun.skip': `Παράβλεψη για τώρα`, + 'badge.local': `Τοπικό`, + 'badge.cloud': `Cloud (Hugging Face)`, + 'badge.title': `Backend Visage: {label}`, + 'donate.enjoying': `Σας αρέσει το Visage; Βοηθήστε να παραμείνει ζωντανό`, + 'donate.supportPatreon': `Υποστήριξη στο Patreon`, + 'frame.close': `Κλείσιμο επιλογέα καρέ`, + 'frame.seekFail': `Αποτυχία μετάβασης στο πρόγραμμα αναπαραγωγής βίντεο.`, + 'frame.selectAt': `Επιλογή καρέ προσώπου στο {time}s`, + }; + + const ja = { + 'backendSettings.title': `バックエンド設定`, + 'backendSettings.closeAria': `設定を閉じる`, + 'backendSettings.backendAria': `バックエンド設定`, + 'backendSettings.changeBackend': `バックエンドを変更`, + 'backendSettings.backendLabel': `バックエンド`, + 'backendSettings.local': `ローカル`, + 'backendSettings.cloud': `クラウド(Hugging Face)`, + 'backendSettings.cloudNote': `画像は Hugging Face のクラウドサービスに送信されます。`, + 'backendSettings.hintPrefix': `画像を自分のネットワーク内に留めたいですか?`, + 'backendSettings.hintLink': `Patreon でプライベートサーバーを実行`, + 'backendSettings.urlLabel': `URL`, + 'backendSettings.csp1': `ブラウザのセキュリティポリシー(CSP)は既定で`, + 'backendSettings.csp2': `のみ許可します。別のアドレス(例:LAN IP)にあるローカルバックエンドに接続するには、それを`, + 'backendSettings.csp3': `内の`, + 'backendSettings.csp4': `ファイルのリストに追加してください。このファイルは Stash プラグインフォルダにあります。追加しないとリクエストはブロックされます。注意:Visage を更新すると`, + 'backendSettings.csp5': `が再インストールされるため、更新のたびに再適用する必要があります。`, + 'backendSettings.testing': `接続をテスト中…`, + 'backendSettings.testConnection': `接続をテスト`, + 'backendSettings.testingShort': `テスト中…`, + 'backendSettings.cancel': `キャンセル`, + 'backendSettings.save': `保存`, + 'backendSettings.feedback.reachable': `接続に成功しました。バックエンドは利用可能です。`, + 'backendSettings.feedback.degraded': `バックエンドには接続できますが、機能が制限されています(モデルまたはインデックスが読み込まれていません)。`, + 'backendSettings.feedback.unreachable': `バックエンドに接続できません。URL を確認し、バックエンドが実行中であることを確認してください。`, + 'faceMatch.title': `現在のフレーム`, + 'faceMatch.close': `閉じる`, + 'faceMatch.facesSelected': `{faces} 個の顔を検出 · {selected} 個を選択`, + 'faceMatch.inScene': `· シーン内 {count}`, + 'faceMatch.stashboxMissing': `stash-box が設定されていません。`, + 'faceMatch.stashboxMissingBody': ` 設定 → メタデータプロバイダーで stash-box プロバイダーを追加すると、パフォーマーのインポートが可能になります。`, + 'faceMatch.stashboxWrongName': `「StashDB」プロバイダーが見つかりません。`, + 'faceMatch.stashboxWrongNameBody': ` パフォーマーのインポートには「StashDB」という名前のプロバイダーが必要です。設定 → メタデータプロバイダーでプロバイダーの名前を変更してください。`, + 'faceMatch.learnMore': `詳細はこちら。`, + 'faceMatch.scanning': `スキャン中 · 顔認識…`, + 'faceMatch.faceAlt': `顔 {index}`, + 'faceMatch.minConf': `最小信頼度`, + 'faceMatch.minConfTitle': `最小信頼度:{percent}%`, + 'faceMatch.detected': `検出済み`, + 'faceMatch.detectedFaceAlt': `検出された顔`, + 'faceMatch.vs': `vs`, + 'faceMatch.supportPatreon': `Patreon で支援する`, + 'faceMatch.ofSelected': `{total} 個中 {selected} 個を選択`, + 'faceMatch.allInScene': `{total} 個の顔を検出 · すべてシーン内`, + 'faceMatch.clickToSelect': `{total} 個の顔を検出 · クリックして選択`, + 'faceMatch.kbSwitch': `顔を切り替え`, + 'faceMatch.kbSelect': `パフォーマーを選択`, + 'faceMatch.kbToggle': `選択を切り替え`, + 'faceMatch.kbAddInstant': `Shift+クリックで即座に追加`, + 'faceMatch.selectBest': `最良の一致を選択`, + 'faceMatch.adding': `追加中...`, + 'faceMatch.done': `完了({count})`, + 'faceMatch.toast.added': `パフォーマーを {target} に追加しました。`, + 'faceMatch.toast.addError': `パフォーマーの追加に失敗しました:{error}`, + 'faceMatch.toast.noStashbox': `stash-box が設定されていません。設定 → メタデータプロバイダーで stash-box プロバイダーを追加すると、パフォーマーのインポートが可能になります。参照:{url}`, + 'faceMatch.toast.noProvider': `「StashDB」という名前のプロバイダーが見つかりません。設定 → メタデータプロバイダーでプロバイダーの名前を「StashDB」に変更すると、パフォーマーのインポートが可能になります。`, + 'faceMatch.toast.configureProvider': `設定 → メタデータプロバイダーで stash-box プロバイダーを設定すると、パフォーマーのインポートが可能になります。`, + 'faceMatch.toast.addedMultiple': `{count} 人のパフォーマー{s}を {target} に追加しました。`, + 'sprite.title': `シーンのパフォーマー`, + 'sprite.close': `閉じる`, + 'sprite.foundConfirmed': `{found} 件検出 · {confirmed} 件確定`, + 'sprite.confidence': `信頼度`, + 'sprite.name': `名前`, + 'sprite.hits': `ヒット`, + 'sprite.minConf': `最小信頼度`, + 'sprite.minConfTitle': `最小信頼度:{percent}%`, + 'sprite.scanning': `Visage スキャン中…`, + 'sprite.cancel': `キャンセル`, + 'sprite.empty': `このスプライトで特定されたパフォーマーはありません。`, + 'sprite.detectedFaceAlt': `検出された顔`, + 'sprite.spriteLabel': `スプライト`, + 'sprite.stashLabel': `STASH`, + 'sprite.vs': `vs`, + 'sprite.inScene': `シーン内`, + 'sprite.hitsCount': `{count} ヒット{s}`, + 'sprite.totalTime': `合計 {time}`, + 'sprite.alreadyInScene': `すでにシーン内`, + 'sprite.clickToConfirm': `クリックして確定`, + 'sprite.confirmed': `確定済み`, + 'sprite.supportPatreon': `Patreon で支援する`, + 'sprite.confirmedCount': `{total} 件中 {confirmed} 件を確定`, + 'sprite.shownHint': `{shown} 件を表示(計 {total} 件)· クリックして確定 · ←→ ナビゲート · Enter で確定`, + 'sprite.confirmHint': `クリックして確定 · ←→ ナビゲート · Enter で確定`, + 'sprite.adding': `追加中...`, + 'sprite.done': `完了({count})`, + 'gender.male': `男性`, + 'gender.female': `女性`, + 'gender.transMale': `トランスジェンダー男性`, + 'gender.transFemale': `トランスジェンダー女性`, + 'gender.nonBinary': `ノンバイナリー`, + 'gender.intersex': `インターセックス`, + 'card.excellent': `非常に良い一致`, + 'card.good': `良い一致`, + 'card.uncertain': `不明な一致`, + 'card.select': `{name} を選択`, + 'card.deselect': `{name} の選択を解除`, + 'card.openOn': `{source} で開く`, + 'search.overlayHint': `ドラッグして顔を選択 — Enter でフレーム全体をスキャン — Esc でキャンセル`, + 'search.noFaces': `その選択範囲では顔が見つかりませんでした。より狭く切り取ってみるか、Enter を押してフレーム全体をスキャンしてください。`, + 'search.captureMediaFail': `メディアをキャプチャできませんでした。シーン/画像が完全に読み込まれていることを確認してください。`, + 'search.healthBanner': `顔認識 API に接続できません。バックエンドを起動して、もう一度お試しください。`, + 'search.failed': `顔の検索に失敗しました:{error}`, + 'search.fetchImageFail': `Stash から画像を取得できませんでした。`, + 'search.captureFail': `画像のキャプチャに失敗しました:{error}`, + 'search.selectFaceImage': `画像内の顔を選択してください。`, + 'search.captureFrameFail': `現在のフレームをキャプチャできませんでした。`, + 'search.captureFrameFail2': `現在のフレームのキャプチャに失敗しました。`, + 'search.selectFaceVideo': `ビデオプレーヤー領域内の顔を選択してください。`, + 'search.menuItemTitle': `顔の周りにボックスをドラッグするか、Enter を押してフレーム全体をスキャンし、StashDB で一致を検索します`, + 'search.currentFrame': `Visage:現在のフレーム`, + 'scene.noSprite': `このシーンにはスプライトシートまたはプレビュービデオがありません。シーン設定で生成してから、もう一度お試しください。`, + 'scene.noFaces': `このシーンのスプライトシートまたはプレビュービデオに顔やパフォーマーが見つかりませんでした。`, + 'scene.healthBanner': `顔認識 API に接続できません。バックエンドを起動して、もう一度お試しください。`, + 'scene.failed': `シーンのスキャンに失敗しました:{error}`, + 'scene.menuItemTitle': `シーン内のすべてのパフォーマーを特定します(生成されたスプライトシートまたはプレビュービデオが必要です)`, + 'scene.wholeScene': `Visage:シーン全体`, + 'banner.changeBackend': `バックエンドを変更`, + 'banner.dismiss': `閉じる`, + 'error.dismiss': `閉じる`, + 'firstRun.title': `Visage バックエンドを設定する`, + 'firstRun.subtitle': `Visage は顔画像をバックエンドに送信して認識を行います。実行場所を選択してください。`, + 'firstRun.cloud': `Hugging Face クラウドを使用`, + 'firstRun.cloudNote': `セットアップ不要。画像は Hugging Face のクラウドサービスに送信されます。`, + 'firstRun.local': `自分のサーバーを使用`, + 'firstRun.localNote': `プライベートバイナリを自分のマシンまたはネットワークで実行します。`, + 'firstRun.skip': `今はスキップ`, + 'badge.local': `ローカル`, + 'badge.cloud': `クラウド(Hugging Face)`, + 'badge.title': `Visage バックエンド:{label}`, + 'donate.enjoying': `Visage をお楽しみですか?運営の継続を支援する`, + 'donate.supportPatreon': `Patreon で支援する`, + 'frame.close': `フレームセレクターを閉じる`, + 'frame.seekFail': `ビデオプレーヤーをシークできませんでした。`, + 'frame.selectAt': `{time} 秒の顔フレームを選択`, + }; + + // Swedish. + const sv = { + 'backendSettings.title': `Backend-inställningar`, + 'backendSettings.closeAria': `Stäng inställningar`, + 'backendSettings.backendAria': `Backend-inställningar`, + 'backendSettings.changeBackend': `Byt backend`, + 'backendSettings.backendLabel': `Backend`, + 'backendSettings.local': `Lokal`, + 'backendSettings.cloud': `Cloud (Hugging Face)`, + 'backendSettings.cloudNote': `Bilder skickas till Hugging Face molntjänsten.`, + 'backendSettings.hintPrefix': `Vill du att dina bilder ska stanna i ditt nätverk?`, + 'backendSettings.hintLink': `Kör en privat server via Patreon`, + 'backendSettings.urlLabel': `URL`, + 'backendSettings.csp1': "Webbläsarens säkerhetspolicy (CSP) tillåter endast", + 'backendSettings.csp2': `som standard. För att nå en lokal backend på en annan adress (t.ex. din LAN-IP) måste du lägga till den i`, + 'backendSettings.csp3': `listan i`, + 'backendSettings.csp4': `-filen i din Stash plugins-mapp, annars blockeras förfrågningarna. Observera: uppdatering av Visage installerar om`, + 'backendSettings.csp5': `, så detta måste göras om efter varje uppdatering.`, + 'backendSettings.testing': `Testar anslutning…`, + 'backendSettings.testConnection': `Testa anslutning`, + 'backendSettings.testingShort': `Testar…`, + 'backendSettings.cancel': `Avbryt`, + 'backendSettings.save': `Spara`, + 'backendSettings.feedback.reachable': `Anslutningen lyckades. Backend är redo.`, + 'backendSettings.feedback.degraded': `Backend är nåbar men försämrad (modeller eller index är inte inlästa).`, + 'backendSettings.feedback.unreachable': `Backend är onåbar. Kontrollera URL:en och att backend körs.`, + 'faceMatch.title': `AKTUELL RUTA`, + 'faceMatch.close': `Stäng`, + 'faceMatch.facesSelected': `{faces} ansikten hittade · {selected} valda`, + 'faceMatch.inScene': `· {count} i scen`, + 'faceMatch.stashboxMissing': `Ingen stash-box konfigurerad.`, + 'faceMatch.stashboxMissingBody': ` Lägg till en stash-box-leverantör under Inställningar → Metadata-leverantörer för att aktivera performer-import.`, + 'faceMatch.stashboxWrongName': `Ingen "StashDB"-leverantör hittades.`, + 'faceMatch.stashboxWrongNameBody': ` Performer-import kräver en leverantör med namnet "StashDB". Byt namn på leverantören under Inställningar → Metadata-leverantörer.`, + 'faceMatch.learnMore': `Läs mer.`, + 'faceMatch.scanning': `Skannar • ansiktsigenkänning…`, + 'faceMatch.faceAlt': `Ansikte {index}`, + 'faceMatch.minConf': `Min. konf.`, + 'faceMatch.minConfTitle': `Minsta förtroende: {percent}%`, + 'faceMatch.detected': `Upptäckt`, + 'faceMatch.detectedFaceAlt': `Upptäckt ansikte`, + 'faceMatch.vs': `vs`, + 'faceMatch.supportPatreon': `Stöd på Patreon`, + 'faceMatch.ofSelected': `{selected} av {total} valda`, + 'faceMatch.allInScene': `{total} ansikten hittade · alla i scen`, + 'faceMatch.clickToSelect': `{total} ansikten hittade · klicka för att välja`, + 'faceMatch.kbSwitch': `Byt ansikten`, + 'faceMatch.kbSelect': `Välj performers`, + 'faceMatch.kbToggle': `Växla markering`, + 'faceMatch.kbAddInstant': `Shift+klick för att lägga till direkt`, + 'faceMatch.selectBest': `Välj bästa matchningar`, + 'faceMatch.adding': `Lägger till...`, + 'faceMatch.done': `Klar ({count})`, + 'faceMatch.toast.added': `Lade till performer i {target}.`, + 'faceMatch.toast.addError': `Misslyckades att lägga till performer: {error}`, + 'faceMatch.toast.noStashbox': `Ingen stash-box konfigurerad. Lägg till en stash-box-leverantör under Inställningar → Metadata-leverantörer för att aktivera performer-import. Se {url}`, + 'faceMatch.toast.noProvider': `Ingen leverantör med namnet "StashDB" hittades. Byt namn på leverantören till "StashDB" under Inställningar → Metadata-leverantörer för att aktivera performer-import.`, + 'faceMatch.toast.configureProvider': `Konfigurera en stash-box-leverantör under Inställningar → Metadata-leverantörer för att aktivera performer-import.`, + 'faceMatch.toast.addedMultiple': `Lade till {count} performers i {target}.`, + 'sprite.title': `SCEN-PERFORMERS`, + 'sprite.close': `Stäng`, + 'sprite.foundConfirmed': `{found} hittade · {confirmed} bekräftade`, + 'sprite.confidence': `förtroende`, + 'sprite.name': `namn`, + 'sprite.hits': `träffar`, + 'sprite.minConf': `Min. konf.`, + 'sprite.minConfTitle': `Minsta förtroende: {percent}%`, + 'sprite.scanning': `Visage skannar…`, + 'sprite.cancel': `Avbryt`, + 'sprite.empty': `Inga performers identifierades i denna sprite.`, + 'sprite.detectedFaceAlt': `Upptäckt ansikte`, + 'sprite.spriteLabel': `SPRITE`, + 'sprite.stashLabel': `STASH`, + 'sprite.vs': `vs`, + 'sprite.inScene': `I scen`, + 'sprite.hitsCount': `{count} träff`, + 'sprite.totalTime': `{time} totalt`, + 'sprite.alreadyInScene': `Redan i scen`, + 'sprite.clickToConfirm': `Klicka för att bekräfta`, + 'sprite.confirmed': `Bekräftad`, + 'sprite.supportPatreon': `Stöd på Patreon`, + 'sprite.confirmedCount': `{confirmed} av {total} bekräftade`, + 'sprite.shownHint': `{shown} visade ({total} totalt) · klicka för att bekräfta · ←→ navigera · Enter bekräfta`, + 'sprite.confirmHint': `Klicka för att bekräfta · ←→ navigera · Enter bekräfta`, + 'sprite.adding': `Lägger till...`, + 'sprite.done': `Klar ({count})`, + 'gender.male': `Man`, + 'gender.female': `Kvinna`, + 'gender.transMale': `Transperson man`, + 'gender.transFemale': `Transperson kvinna`, + 'gender.nonBinary': `Icke-binär`, + 'gender.intersex': `Intersex`, + 'card.excellent': `Utmärkt matchning`, + 'card.good': `Bra matchning`, + 'card.uncertain': `Osäker matchning`, + 'card.select': `Välj {name}`, + 'card.deselect': `Avmarkera {name}`, + 'card.openOn': `Öppna på {source}`, + 'search.overlayHint': `Dra för att välja ett ansikte — Enter för att skanna hela rutan — Esc för att avbryta`, + 'search.noFaces': `Inga ansikten hittades i det valet. Prova en tätare beskärning, eller tryck Enter för att skanna hela rutan.`, + 'search.captureMediaFail': `Kunde inte fånga media. Se till att scenen/bilden är fullständigt inläst.`, + 'search.healthBanner': `Ansiktsigenkännings-API:et är inte nåbart. Starta backend och försök igen.`, + 'search.failed': `Ansiktssökning misslyckades: {error}`, + 'search.fetchImageFail': `Kunde inte hämta bild från Stash.`, + 'search.captureFail': `Kunde inte fånga bild: {error}`, + 'search.selectFaceImage': `Välj ett ansikte i bilden.`, + 'search.captureFrameFail': `Kunde inte fånga den aktuella rutan.`, + 'search.captureFrameFail2': `Misslyckades att fånga den aktuella rutan.`, + 'search.selectFaceVideo': `Välj ett ansikte i videoavspelarområdet.`, + 'search.menuItemTitle': `Dra en ruta runt ett ansikte, eller tryck Enter för att skanna hela rutan, för att söka StashDB efter matchningar`, + 'search.currentFrame': `Visage: Aktuell ruta`, + 'scene.noSprite': `Ingen sprite-ark eller förhandsvisningsvideo hittades för denna scen. Generera dem i Scene-inställningarna och försök igen.`, + 'scene.noFaces': `Inga ansikten eller performers hittades i denna scen sprite-ark eller förhandsvisningsvideo.`, + 'scene.healthBanner': `Ansiktsigenkännings-API:et är inte nåbart. Starta backend och försök igen.`, + 'scene.failed': `Scen-skanning misslyckades: {error}`, + 'scene.menuItemTitle': `Identifiera varje performer i scenen (kräver ett genererat sprite-ark eller en förhandsvisningsvideo)`, + 'scene.wholeScene': `Visage: Hela scenen`, + 'banner.changeBackend': `Byt backend`, + 'banner.dismiss': `Avvisa`, + 'error.dismiss': `Avvisa`, + 'firstRun.title': `Konfigurera din Visage-backend`, + 'firstRun.subtitle': `Visage skickar ansiktsbilder till en backend för igenkänning. Välj var den ska köras.`, + 'firstRun.cloud': `Använd Hugging Face cloud`, + 'firstRun.cloudNote': `Ingen installation. Bilder skickas till Hugging Face molntjänsten.`, + 'firstRun.local': `Använd min egen server`, + 'firstRun.localNote': `Kör den privata binären på din egen maskin eller ditt nätverk.`, + 'firstRun.skip': `Hoppa över för nu`, + 'badge.local': `Lokal`, + 'badge.cloud': `Cloud (Hugging Face)`, + 'badge.title': `Visage-backend: {label}`, + 'donate.enjoying': `Gillar du Visage? Hjälp till att hålla det vid liv`, + 'donate.supportPatreon': `Stöd på Patreon`, + 'frame.close': `Stäng rutväljaren`, + 'frame.seekFail': `Kunde inte söka i videoavspelaren.`, + 'frame.selectAt': `Välj ansiktsruta vid {time}s`, + }; + + // Italian. Missing keys fall back to English automatically. + const it = { + // ---- BackendSettings.tsx ---- + 'backendSettings.title': `Impostazioni backend`, + 'backendSettings.closeAria': `Chiudi impostazioni`, + 'backendSettings.backendAria': `Impostazioni backend`, + 'backendSettings.changeBackend': `Cambia backend`, + 'backendSettings.backendLabel': `Backend`, + 'backendSettings.local': `Locale`, + 'backendSettings.cloud': `Cloud (Hugging Face)`, + 'backendSettings.cloudNote': `Le immagini vengono inviate al servizio cloud Hugging Face.`, + 'backendSettings.hintPrefix': `Vuoi che le tue immagini restino nella tua rete?`, + 'backendSettings.hintLink': `Esegui un server privato tramite Patreon`, + 'backendSettings.urlLabel': `URL`, + 'backendSettings.csp1': `La politica di sicurezza del browser (CSP) consente solo`, + 'backendSettings.csp2': `per impostazione predefinita. Per raggiungere un backend locale su un altro indirizzo (ad es. il tuo IP LAN), aggiungilo alla`, + 'backendSettings.csp3': `elenco nel file`, + 'backendSettings.csp4': `all'interno della tua cartella plugin di Stash, altrimenti le richieste verranno bloccate. Nota: aggiornare Visage reinstallerà`, + 'backendSettings.csp5': `, quindi questo va riapplicato dopo ogni aggiornamento.`, + 'backendSettings.testing': `Verifica della connessione…`, + 'backendSettings.testConnection': `Verifica connessione`, + 'backendSettings.testingShort': `Verifica…`, + 'backendSettings.cancel': `Annulla`, + 'backendSettings.save': `Salva`, + 'backendSettings.feedback.reachable': `Connessione riuscita. Il backend è pronto.`, + 'backendSettings.feedback.degraded': `Backend raggiungibile ma degradato (modelli o indice non caricati).`, + 'backendSettings.feedback.unreachable': `Backend non raggiungibile. Controlla l'URL e che il backend sia in esecuzione.`, + // ---- FaceMatchModal.tsx ---- + 'faceMatch.title': `FOTO CORRENTE`, + 'faceMatch.close': `Chiudi`, + 'faceMatch.facesSelected': `{faces} volti trovati · {selected} selezionati`, + 'faceMatch.inScene': `· {count} nella scena`, + 'faceMatch.stashboxMissing': `Nessuno stash-box configurato.`, + 'faceMatch.stashboxMissingBody': ` Aggiungi un provider stash-box in Impostazioni → Provider di metadati per attivare l'importazione dei performer.`, + 'faceMatch.stashboxWrongName': `Nessun provider chiamato "StashDB" trovato.`, + 'faceMatch.stashboxWrongNameBody': ` L'importazione dei performer richiede un provider chiamato "StashDB". Rinomina il tuo provider in Impostazioni → Provider di metadati.`, + 'faceMatch.learnMore': `Scopri di più.`, + 'faceMatch.scanning': `Scansione • riconoscimento facciale…`, + 'faceMatch.faceAlt': `Volto {index}`, + 'faceMatch.minConf': `Conf. min.`, + 'faceMatch.minConfTitle': `Confidenza minima: {percent}%`, + 'faceMatch.detected': `Rilevato`, + 'faceMatch.detectedFaceAlt': `Volto rilevato`, + 'faceMatch.vs': `vs`, + 'faceMatch.supportPatreon': `Supporta su Patreon`, + 'faceMatch.ofSelected': `{selected} di {total} selezionati`, + 'faceMatch.allInScene': `{total} volti trovati · tutti nella scena`, + 'faceMatch.clickToSelect': `{total} volti trovati · fai clic per selezionare`, + 'faceMatch.kbSwitch': `Cambia volti`, + 'faceMatch.kbSelect': `Seleziona performer`, + 'faceMatch.kbToggle': `Attiva/disattiva selezione`, + 'faceMatch.kbAddInstant': `Maiusc+clic per aggiungere all'istante`, + 'faceMatch.selectBest': `Seleziona migliori corrispondenze`, + 'faceMatch.adding': `Aggiunta...`, + 'faceMatch.done': `Fatto ({count})`, + 'faceMatch.toast.added': `Performer aggiunto alla {target}.`, + 'faceMatch.toast.addError': `Impossibile aggiungere il performer: {error}`, + 'faceMatch.toast.noStashbox': `Nessuno stash-box configurato. Aggiungi un provider stash-box in Impostazioni → Provider di metadati per attivare l'importazione dei performer. Vedi {url}`, + 'faceMatch.toast.noProvider': `Nessun provider chiamato "StashDB" trovato. Rinomina il tuo provider in "StashDB" in Impostazioni → Provider di metadati per attivare l'importazione dei performer.`, + 'faceMatch.toast.configureProvider': `Configura un provider stash-box in Impostazioni → Provider di metadati per attivare l'importazione dei performer.`, + 'faceMatch.toast.addedMultiple': `{count} performer{s} aggiunti alla {target}.`, + // ---- SpriteResultModal.tsx ---- + 'sprite.title': `PERFORMER DELLA SCENA`, + 'sprite.close': `Chiudi`, + 'sprite.foundConfirmed': `{found} trovati · {confirmed} confermati`, + 'sprite.confidence': `confidenza`, + 'sprite.name': `nome`, + 'sprite.hits': `corrispondenze`, + 'sprite.minConf': `Conf. min.`, + 'sprite.minConfTitle': `Confidenza minima: {percent}%`, + 'sprite.scanning': `Scansione Visage…`, + 'sprite.cancel': `Annulla`, + 'sprite.empty': `Nessun performer identificato in questo sprite.`, + 'sprite.detectedFaceAlt': `Volto rilevato`, + 'sprite.spriteLabel': `SPRITE`, + 'sprite.stashLabel': `STASH`, + 'sprite.vs': `vs`, + 'sprite.inScene': `Nella scena`, + 'sprite.hitsCount': `{count} corrispondenza{s}`, + 'sprite.totalTime': `{time} in totale`, + 'sprite.alreadyInScene': `Già nella scena`, + 'sprite.clickToConfirm': `Fai clic per confermare`, + 'sprite.confirmed': `Confermato`, + 'sprite.supportPatreon': `Supporta su Patreon`, + 'sprite.confirmedCount': `{confirmed} di {total} confermati`, + 'sprite.shownHint': `{shown} mostrati ({total} in totale) · fai clic per confermare · ←→ naviga · Invio conferma`, + 'sprite.confirmHint': `Fai clic per confermare · ←→ naviga · Invio conferma`, + 'sprite.adding': `Aggiunta...`, + 'sprite.done': `Fatto ({count})`, + // ---- PerformerCard.tsx ---- + 'gender.male': `Maschio`, + 'gender.female': `Femmina`, + 'gender.transMale': `Uomo transgender`, + 'gender.transFemale': `Donna transgender`, + 'gender.nonBinary': `Non binario`, + 'gender.intersex': `Intersessuale`, + 'card.excellent': `Corrispondenza eccellente`, + 'card.good': `Buona corrispondenza`, + 'card.uncertain': `Corrispondenza incerta`, + 'card.select': `Seleziona {name}`, + 'card.deselect': `Deseleziona {name}`, + 'card.openOn': `Apri su {source}`, + // ---- FaceSearchButton.tsx ---- + 'search.overlayHint': `Trascina per selezionare un volto — Invio per scansionare l'intera immagine — Esc per annullare`, + 'search.noFaces': `Nessun volto trovato in questa selezione. Prova con un ritaglio più stretto oppure premi Invio per scansionare l'intera immagine.`, + 'search.captureMediaFail': `Impossibile acquisire il media. Assicurati che la scena/l'immagine sia completamente caricata.`, + 'search.healthBanner': `L'API di riconoscimento facciale non è raggiungibile. Avvia il backend e riprova.`, + 'search.failed': `Ricerca volto non riuscita: {error}`, + 'search.fetchImageFail': `Impossibile recuperare l'immagine da Stash.`, + 'search.captureFail': `Impossibile acquisire l'immagine: {error}`, + 'search.selectFaceImage': `Seleziona un volto all'interno dell'immagine.`, + 'search.captureFrameFail': `Impossibile acquisire il fotogramma corrente.`, + 'search.captureFrameFail2': `Impossibile acquisire il fotogramma corrente.`, + 'search.selectFaceVideo': `Seleziona un volto all'interno dell'area del lettore video.`, + 'search.menuItemTitle': `Trascina una casella attorno a un volto oppure premi Invio per scansionare l'intera immagine, per cercare corrispondenze in StashDB`, + 'search.currentFrame': `Visage: Fotogramma corrente`, + // ---- SceneScanButton.tsx ---- + 'scene.noSprite': `Nessuno sprite sheet o video di anteprima trovato per questa scena. Generali nelle impostazioni della scena, poi riprova.`, + 'scene.noFaces': `Nessun volto o performer trovato nello sprite sheet o nel video di anteprima di questa scena.`, + 'scene.healthBanner': `L'API di riconoscimento facciale non è raggiungibile. Avvia il backend e riprova.`, + 'scene.failed': `Scansione della scena non riuscita: {error}`, + 'scene.menuItemTitle': `Identifica ogni performer nella scena (richiede uno sprite sheet o un video di anteprima generati)`, + 'scene.wholeScene': `Visage: Intera scena`, + // ---- BackendHealthBanner.tsx ---- + 'banner.changeBackend': `Cambia backend`, + 'banner.dismiss': `Ignora`, + // ---- ErrorDialog.tsx ---- + 'error.dismiss': `Ignora`, + // ---- FirstRunDialog.tsx ---- + 'firstRun.title': `Configura il tuo backend Visage`, + 'firstRun.subtitle': `Visage invia immagini di volti a un backend per il riconoscimento. Scegli dove eseguirlo.`, + 'firstRun.cloud': `Usa il cloud Hugging Face`, + 'firstRun.cloudNote': `Nessuna configurazione. Le immagini vengono inviate al servizio cloud Hugging Face.`, + 'firstRun.local': `Usa il mio server`, + 'firstRun.localNote': `Esegui il binario privato sulla tua macchina o rete.`, + 'firstRun.skip': `Salta per ora`, + // ---- BackendBadge.tsx ---- + 'badge.local': `Locale`, + 'badge.cloud': `Cloud (Hugging Face)`, + 'badge.title': `Backend Visage: {label}`, + // ---- DonateFooter.tsx ---- + 'donate.enjoying': `Ti piace Visage? Aiuta a mantenerlo vivo`, + 'donate.supportPatreon': `Supporta su Patreon`, + // ---- FaceFrameSelector.tsx ---- + 'frame.close': `Chiudi selettore fotogramma`, + 'frame.seekFail': `Impossibile cercare nel lettore video.`, + 'frame.selectAt': `Seleziona il volto a {time}s`, + }; + + registerLocale('en', en); + registerLocale('nl-NL', nl); + registerLocale('he-IL', he); + registerLocale('lt-LT', lt); + registerLocale('es-ES', es); + registerLocale('ar', ar); + registerLocale('fr-FR', fr); + registerLocale('nn-NO', nn); + registerLocale('ko-KR', ko); + registerLocale('zh-CN', zhCn); + registerLocale('bg-BG', bg); + registerLocale('ru-RU', ru); + registerLocale('id-ID', id); + registerLocale('cs-CZ', cs); + registerLocale('hr-HR', hr); + registerLocale('th-TH', th); + registerLocale('pt-BR', pt); + registerLocale('da-DK', da); + registerLocale('hu-HU', hu); + registerLocale('uk-UA', uk); + registerLocale('de-DE', de); + registerLocale('fi-FI', fi); + registerLocale('lv-LV', lv); + registerLocale('vi-VN', vi); + registerLocale('et-EE', et); + registerLocale('nb-NO', nb); + registerLocale('tr-TR', tr); + registerLocale('zh-TW', zhTw); + registerLocale('ro-RO', ro); + registerLocale('sk-SK', sk); + registerLocale('pl-PL', pl); + registerLocale('el-GR', el); + registerLocale('ja-JP', ja); + registerLocale('sv-SE', sv); + registerLocale('it-IT', it); + const { React, ReactDOM } = PluginApi; function extractIdFromProps(props) { var _a, _b; diff --git a/plugins/visage/visage.yml b/plugins/visage/visage.yml index d59d3517..009eee6f 100644 --- a/plugins/visage/visage.yml +++ b/plugins/visage/visage.yml @@ -1,6 +1,6 @@ name: Visage description: Use facial recognition to lookup performers from scene sprites -version: 2.5.0 +version: 2.6.0 url: https://discourse.stashapp.cc/t/visage/11785 ui: javascript: