feat: localize official plugins in Russian

This commit is contained in:
2026-07-11 12:42:37 +08:00
parent 077de94e61
commit 3dd0ec9f1f
57 changed files with 1478 additions and 330 deletions
+51 -34
View File
@@ -1,4 +1,6 @@
<script>
import { onDestroy, onMount } from 'svelte'
export let api = null
let settings = null
@@ -15,19 +17,26 @@
let password = ''
let syncInterval = 5
let autoSync = false
let locale = api?.i18n?.getLocale?.() || 'en'
let unsubscribeLocale
function tr(key, params, fallback) {
locale
return api?.i18n?.t?.(key, params, fallback) || fallback || key
}
const INPUT_STYLE = 'width:100%;background:#0f3460;border:1px solid #1a3a5c;color:#e0e0f0;padding:8px 10px;border-radius:4px;font-size:0.85rem;box-sizing:border-box;height:36px;'
const INPUT_FOCUS_STYLE = INPUT_STYLE + 'outline:none;border-color:#4ecca3;'
function sanitizeError(msg) {
if (!msg) return 'Unknown error'
if (!msg) return tr('ui.unknownError', null, 'Unknown error')
let s = String(msg).replace(/<[^>]+>/g, '')
if (s.length > 200) s = s.substring(0, 200) + '...'
return s
}
function syncAPI() {
if (!api?.sync) throw new Error('Plugin API sync namespace not available')
if (!api?.sync) throw new Error(tr('ui.apiUnavailable', null, 'Plugin API sync namespace not available'))
return api.sync
}
@@ -57,7 +66,7 @@
async function saveSettings() {
if (syncInterval < 1 || syncInterval > 1440) {
errorMsg = 'Sync interval must be between 1 and 1440 minutes.'
errorMsg = tr('ui.intervalError', null, 'Sync interval must be between 1 and 1440 minutes.')
return
}
loading = true
@@ -68,7 +77,7 @@
await api.settings.writeAll({ serverUrl, username, autoSync, syncInterval })
}
await syncAPI().setInterval(autoSync ? syncInterval : 0)
resultMsg = 'Settings saved.'
resultMsg = tr('ui.settingsSaved', null, 'Settings saved.')
resultKind = ''
} catch (e) {
errorMsg = sanitizeError(e.message || e)
@@ -77,7 +86,7 @@
}
async function testConnection() {
if (!serverUrl) { errorMsg = 'Server URL is required.'; return }
if (!serverUrl) { errorMsg = tr('ui.serverRequired', null, 'Server URL is required.'); return }
loading = true
connectionResult = ''
connectionOk = null
@@ -85,22 +94,22 @@
try {
await syncAPI().testConnection(serverUrl, username, password)
connectionOk = true
connectionResult = 'Connection successful.'
connectionResult = tr('ui.connectionSuccessful', null, 'Connection successful.')
} catch (e) {
connectionOk = false
connectionResult = 'Connection failed: ' + sanitizeError(e.message || e)
connectionResult = tr('ui.connectionFailed', { error: sanitizeError(e.message || e) }, 'Connection failed: {error}')
}
loading = false
}
async function configureSync() {
if (!serverUrl) { errorMsg = 'Server URL is required.'; return }
if (!serverUrl) { errorMsg = tr('ui.serverRequired', null, 'Server URL is required.'); return }
loading = true
errorMsg = ''
connectionResult = ''
try {
await syncAPI().configure(serverUrl, username, password)
connectionResult = 'Connected successfully.'
connectionResult = tr('ui.connectedSuccessfully', null, 'Connected successfully.')
connectionOk = true
username = ''
password = ''
@@ -115,8 +124,8 @@
const conflicts = Array.isArray(result?.conflicts) ? result.conflicts : []
const applyErrors = Array.isArray(result?.applyErrors) ? result.applyErrors : []
const parts = []
if (conflicts.length > 0) parts.push(conflicts.length + ' conflict(s)')
if (applyErrors.length > 0) parts.push(applyErrors.length + ' error(s)')
if (conflicts.length > 0) parts.push(tr('ui.conflictsCount', { count: conflicts.length }, '{count} conflict(s)'))
if (applyErrors.length > 0) parts.push(tr('ui.errorsCount', { count: applyErrors.length }, '{count} error(s)'))
return parts.join(' · ')
}
@@ -146,7 +155,7 @@
conflictDetails = []
try {
const r = await syncAPI().now()
const summary = 'Pushed ' + (r?.pushed || 0) + ', pulled ' + (r?.pulled || 0)
const summary = tr('ui.syncSummary', { pushed: r?.pushed || 0, pulled: r?.pulled || 0 }, 'Pushed {pushed}, pulled {pulled}')
const warning = syncResultWarning(r)
const conflicts = Array.isArray(r?.conflicts) ? r.conflicts : []
conflictDetails = conflicts.slice(0, 5).map(formatSyncConflict)
@@ -167,7 +176,7 @@
function saveInterval() {
if (syncInterval < 1 || syncInterval > 1440) {
errorMsg = 'Sync interval must be between 1 and 1440 minutes.'
errorMsg = tr('ui.intervalError', null, 'Sync interval must be between 1 and 1440 minutes.')
return
}
autoSync = syncInterval > 0
@@ -180,7 +189,7 @@
resultMsg = ''
try {
await syncAPI().disconnect()
resultMsg = 'Disconnected from server.'
resultMsg = tr('ui.disconnected', null, 'Disconnected from server.')
resultKind = ''
settings = null
await load()
@@ -196,7 +205,7 @@
resultMsg = ''
try {
await syncAPI().resetKey()
resultMsg = 'Sync key reset. Connect again to pair this device.'
resultMsg = tr('ui.keyReset', null, 'Sync key reset. Connect again to pair this device.')
resultKind = ''
await load()
} catch (e) {
@@ -204,11 +213,19 @@
}
loading = false
}
onMount(() => {
unsubscribeLocale = api?.i18n?.onDidChangeLocale?.((nextLocale) => {
locale = nextLocale
})
})
onDestroy(() => unsubscribeLocale?.())
</script>
<div style="padding:1.5rem;max-width:500px;">
<h2 style="margin:0 0 0.25rem;color:#e0e0f0;font-size:1.2rem;">Sync</h2>
<p style="color:#a0a0b8;font-size:0.85rem;margin-bottom:1.25rem;">Synchronize your vault across devices.</p>
<h2 style="margin:0 0 0.25rem;color:#e0e0f0;font-size:1.2rem;">{tr('ui.title', null, 'Sync')}</h2>
<p style="color:#a0a0b8;font-size:0.85rem;margin-bottom:1.25rem;">{tr('ui.description', null, 'Synchronize your vault across devices.')}</p>
{#if errorMsg}
<div style="padding:0.5rem 0.75rem;margin-bottom:0.75rem;background:rgba(255,107,107,0.1);border:1px solid rgba(255,107,107,0.3);border-radius:6px;color:#ff6b6b;font-size:0.85rem;">{errorMsg}</div>
@@ -218,7 +235,7 @@
{/if}
{#if conflictDetails.length > 0 && !errorMsg}
<div style="padding:0.5rem 0.75rem;margin-bottom:0.75rem;background:rgba(245,158,11,0.1);border:1px solid rgba(245,158,11,0.3);border-radius:6px;color:#f59e0b;font-size:0.85rem;">
<div style="font-weight:600;margin-bottom:0.35rem;">Sync conflicts</div>
<div style="font-weight:600;margin-bottom:0.35rem;">{tr('ui.syncConflicts', null, 'Sync conflicts')}</div>
{#each conflictDetails as detail}
<div>{detail}</div>
{/each}
@@ -226,31 +243,31 @@
{/if}
{#if settings && settings.lastError && !errorMsg}
<div style="padding:0.5rem 0.75rem;margin-bottom:0.75rem;background:rgba(255,107,107,0.1);border:1px solid rgba(255,107,107,0.3);border-radius:6px;color:#ff6b6b;font-size:0.85rem;">
Last sync error: {sanitizeError(settings.lastError)}
{tr('ui.lastSyncError', { error: sanitizeError(settings.lastError) }, 'Last sync error: {error}')}
</div>
{/if}
<div style="background:#16213e;border:1px solid #0f3460;border-radius:8px;padding:1rem 1.25rem;margin-bottom:1rem;">
<h3 style="margin:0 0 0.75rem;color:#e0e0f0;font-size:0.95rem;">Server</h3>
<h3 style="margin:0 0 0.75rem;color:#e0e0f0;font-size:0.95rem;">{tr('ui.server', null, 'Server')}</h3>
<div style="margin-bottom:0.75rem;">
<label for="sync-server-url" style="display:block;color:#a0a0b8;font-size:0.85rem;margin-bottom:0.35rem;">Server URL</label>
<label for="sync-server-url" style="display:block;color:#a0a0b8;font-size:0.85rem;margin-bottom:0.35rem;">{tr('ui.serverUrl', null, 'Server URL')}</label>
<input id="sync-server-url" type="text" style={INPUT_STYLE} on:focus={e => e.target.style.cssText = INPUT_FOCUS_STYLE} on:blur={e => e.target.style.cssText = INPUT_STYLE} bind:value={serverUrl} placeholder="https://example.com" />
</div>
<div style="margin-bottom:0.75rem;">
<label for="sync-username" style="display:block;color:#a0a0b8;font-size:0.85rem;margin-bottom:0.35rem;">Username</label>
<label for="sync-username" style="display:block;color:#a0a0b8;font-size:0.85rem;margin-bottom:0.35rem;">{tr('ui.username', null, 'Username')}</label>
<input id="sync-username" type="text" style={INPUT_STYLE} on:focus={e => e.target.style.cssText = INPUT_FOCUS_STYLE} on:blur={e => e.target.style.cssText = INPUT_STYLE} bind:value={username} />
</div>
<div style="margin-bottom:0.75rem;">
<label for="sync-password" style="display:block;color:#a0a0b8;font-size:0.85rem;margin-bottom:0.35rem;">Password</label>
<label for="sync-password" style="display:block;color:#a0a0b8;font-size:0.85rem;margin-bottom:0.35rem;">{tr('ui.password', null, 'Password')}</label>
<input id="sync-password" type="password" style={INPUT_STYLE} on:focus={e => e.target.style.cssText = INPUT_FOCUS_STYLE} on:blur={e => e.target.style.cssText = INPUT_STYLE} bind:value={password} />
</div>
<div style="display:flex;gap:0.5rem;margin-top:1rem;">
<button style="background:#1a1a2e;color:#e0e0f0;border:1px solid #1a3a5c;padding:0.4rem 0.75rem;border-radius:4px;cursor:pointer;font-size:0.85rem;" on:click={testConnection} disabled={loading || !serverUrl}>Test Connection</button>
<button style="background:#4ecca3;color:#1a1a2e;border:1px solid #4ecca3;padding:0.4rem 0.75rem;border-radius:4px;cursor:pointer;font-size:0.85rem;font-weight:600;" on:click={configureSync} disabled={loading || !serverUrl}>Connect</button>
<button style="background:#1a1a2e;color:#e0e0f0;border:1px solid #1a3a5c;padding:0.4rem 0.75rem;border-radius:4px;cursor:pointer;font-size:0.85rem;" on:click={testConnection} disabled={loading || !serverUrl}>{tr('ui.testConnection', null, 'Test Connection')}</button>
<button style="background:#4ecca3;color:#1a1a2e;border:1px solid #4ecca3;padding:0.4rem 0.75rem;border-radius:4px;cursor:pointer;font-size:0.85rem;font-weight:600;" on:click={configureSync} disabled={loading || !serverUrl}>{tr('ui.connect', null, 'Connect')}</button>
</div>
{#if connectionResult}
@@ -259,34 +276,34 @@
</div>
<div style="background:#16213e;border:1px solid #0f3460;border-radius:8px;padding:1rem 1.25rem;margin-bottom:1rem;">
<h3 style="margin:0 0 0.75rem;color:#e0e0f0;font-size:0.95rem;">Sync Behavior</h3>
<h3 style="margin:0 0 0.75rem;color:#e0e0f0;font-size:0.95rem;">{tr('ui.behavior', null, 'Sync Behavior')}</h3>
<div style="margin-bottom:0.75rem;display:flex;align-items:center;gap:0.5rem;">
<input type="checkbox" id="auto-sync" bind:checked={autoSync} on:change={toggleAutoSync} style="width:16px;height:16px;accent-color:#4ecca3;" />
<label for="auto-sync" style="color:#e0e0f0;font-size:0.9rem;cursor:pointer;">Enable auto-sync</label>
<label for="auto-sync" style="color:#e0e0f0;font-size:0.9rem;cursor:pointer;">{tr('ui.enableAutoSync', null, 'Enable auto-sync')}</label>
</div>
<div style="margin-bottom:0.75rem;">
<label for="sync-interval" style="display:block;color:#a0a0b8;font-size:0.85rem;margin-bottom:0.35rem;">Sync interval</label>
<label for="sync-interval" style="display:block;color:#a0a0b8;font-size:0.85rem;margin-bottom:0.35rem;">{tr('ui.interval', null, 'Sync interval')}</label>
<div style="display:flex;align-items:center;gap:0.5rem;">
<input id="sync-interval" type="number" min="1" max="1440" bind:value={syncInterval} on:change={saveInterval} style="width:100px;background:#0f3460;border:1px solid #1a3a5c;color:#e0e0f0;padding:8px 10px;border-radius:4px;font-size:0.85rem;height:36px;" />
<span style="color:#a0a0b8;font-size:0.85rem;">minutes</span>
<span style="color:#a0a0b8;font-size:0.85rem;">{tr('ui.minutes', null, 'minutes')}</span>
</div>
</div>
{#if settings && settings.lastSyncAt}
<div style="color:#a0a0b8;font-size:0.85rem;">
Last sync: {settings.lastSyncAt}
{tr('ui.lastSync', { date: settings.lastSyncAt }, 'Last sync: {date}')}
</div>
{/if}
</div>
<div style="display:flex;gap:0.5rem;flex-wrap:wrap;">
<button style="background:#4ecca3;color:#1a1a2e;border:1px solid #4ecca3;padding:0.4rem 0.75rem;border-radius:4px;cursor:pointer;font-size:0.85rem;font-weight:600;" on:click={saveSettings} disabled={loading}>Save</button>
<button style="background:#4ecca3;color:#1a1a2e;border:1px solid #4ecca3;padding:0.4rem 0.75rem;border-radius:4px;cursor:pointer;font-size:0.85rem;font-weight:600;" on:click={saveSettings} disabled={loading}>{tr('ui.save', null, 'Save')}</button>
{#if settings && settings.configured}
<button style="background:#1a1a2e;color:#e0e0f0;border:1px solid #1a3a5c;padding:0.4rem 0.75rem;border-radius:4px;cursor:pointer;font-size:0.85rem;" on:click={runSyncNow} disabled={loading}>Sync Now</button>
<button style="background:#1a1a2e;color:#e0e0f0;border:1px solid #1a3a5c;padding:0.4rem 0.75rem;border-radius:4px;cursor:pointer;font-size:0.85rem;" on:click={resetKey} disabled={loading}>Reset Key</button>
<button style="background:#e94560;color:#fff;border:1px solid #e94560;padding:0.4rem 0.75rem;border-radius:4px;cursor:pointer;font-size:0.85rem;" on:click={doDisconnect} disabled={loading}>Disconnect</button>
<button style="background:#1a1a2e;color:#e0e0f0;border:1px solid #1a3a5c;padding:0.4rem 0.75rem;border-radius:4px;cursor:pointer;font-size:0.85rem;" on:click={runSyncNow} disabled={loading}>{tr('ui.syncNow', null, 'Sync Now')}</button>
<button style="background:#1a1a2e;color:#e0e0f0;border:1px solid #1a3a5c;padding:0.4rem 0.75rem;border-radius:4px;cursor:pointer;font-size:0.85rem;" on:click={resetKey} disabled={loading}>{tr('ui.resetKey', null, 'Reset Key')}</button>
<button style="background:#e94560;color:#fff;border:1px solid #e94560;padding:0.4rem 0.75rem;border-radius:4px;cursor:pointer;font-size:0.85rem;" on:click={doDisconnect} disabled={loading}>{tr('ui.disconnect', null, 'Disconnect')}</button>
{/if}
</div>
</div>
+17 -6
View File
@@ -5,6 +5,13 @@
let status = 'disabled'
let interval
let locale = api?.i18n?.getLocale?.() || 'en'
let unsubscribeLocale
function tr(key, params, fallback) {
locale
return api?.i18n?.t?.(key, params, fallback) || fallback || key
}
async function loadStatus() {
try {
@@ -32,11 +39,11 @@
function statusText() {
const labels = {
connected: 'Connected',
disconnected: 'Disconnected',
disabled: 'Not configured',
error: 'Error',
revoked: 'Revoked',
connected: tr('ui.status.connected', null, 'Connected'),
disconnected: tr('ui.status.disconnected', null, 'Disconnected'),
disabled: tr('ui.status.disabled', null, 'Not configured'),
error: tr('ui.status.error', null, 'Error'),
revoked: tr('ui.status.revoked', null, 'Revoked'),
}
return labels[status] || status
}
@@ -50,14 +57,18 @@
onMount(() => {
loadStatus()
interval = setInterval(loadStatus, 15000)
unsubscribeLocale = api?.i18n?.onDidChangeLocale?.((nextLocale) => {
locale = nextLocale
})
})
onDestroy(() => {
if (interval) clearInterval(interval)
unsubscribeLocale?.()
})
</script>
<button class="sync-status-bar" on:click={openSettings} title="Sync: {statusText()}">
<button class="sync-status-bar" on:click={openSettings} title="{tr('ui.title', null, 'Sync')}: {statusText()}">
<span class="status-dot" style="background: {statusColor()}"></span>
<span class="status-label">{statusText()}</span>
</button>
+42
View File
@@ -0,0 +1,42 @@
{
"manifest.name": "Sync",
"contributions.settingsPanels.verstak.sync.settings.title": "Sync",
"contributions.statusBarItems.verstak.sync.status.label": "Sync",
"ui.title": "Sync",
"ui.description": "Synchronize your vault across devices.",
"ui.unknownError": "Unknown error",
"ui.apiUnavailable": "Plugin API sync namespace not available",
"ui.intervalError": "Sync interval must be between 1 and 1440 minutes.",
"ui.settingsSaved": "Settings saved.",
"ui.serverRequired": "Server URL is required.",
"ui.connectionSuccessful": "Connection successful.",
"ui.connectionFailed": "Connection failed: {error}",
"ui.connectedSuccessfully": "Connected successfully.",
"ui.conflictsCount": "{count} conflict(s)",
"ui.errorsCount": "{count} error(s)",
"ui.syncSummary": "Pushed {pushed}, pulled {pulled}",
"ui.disconnected": "Disconnected from server.",
"ui.keyReset": "Sync key reset. Connect again to pair this device.",
"ui.syncConflicts": "Sync conflicts",
"ui.lastSyncError": "Last sync error: {error}",
"ui.server": "Server",
"ui.serverUrl": "Server URL",
"ui.username": "Username",
"ui.password": "Password",
"ui.testConnection": "Test Connection",
"ui.connect": "Connect",
"ui.behavior": "Sync Behavior",
"ui.enableAutoSync": "Enable auto-sync",
"ui.interval": "Sync interval",
"ui.minutes": "minutes",
"ui.lastSync": "Last sync: {date}",
"ui.save": "Save",
"ui.syncNow": "Sync Now",
"ui.resetKey": "Reset Key",
"ui.disconnect": "Disconnect",
"ui.status.connected": "Connected",
"ui.status.disconnected": "Disconnected",
"ui.status.disabled": "Not configured",
"ui.status.error": "Error",
"ui.status.revoked": "Revoked"
}
+42
View File
@@ -0,0 +1,42 @@
{
"manifest.name": "Синхронизация",
"contributions.settingsPanels.verstak.sync.settings.title": "Синхронизация",
"contributions.statusBarItems.verstak.sync.status.label": "Синхронизация",
"ui.title": "Синхронизация",
"ui.description": "Синхронизируйте хранилище между устройствами.",
"ui.unknownError": "Неизвестная ошибка",
"ui.apiUnavailable": "API синхронизации плагина недоступен",
"ui.intervalError": "Интервал синхронизации должен быть от 1 до 1440 минут.",
"ui.settingsSaved": "Настройки сохранены.",
"ui.serverRequired": "Укажите URL сервера.",
"ui.connectionSuccessful": "Подключение успешно.",
"ui.connectionFailed": "Не удалось подключиться: {error}",
"ui.connectedSuccessfully": "Подключение установлено.",
"ui.conflictsCount": "Конфликтов: {count}",
"ui.errorsCount": "Ошибок: {count}",
"ui.syncSummary": "Отправлено: {pushed}, получено: {pulled}",
"ui.disconnected": "Сервер отключён.",
"ui.keyReset": "Ключ синхронизации сброшен. Подключитесь снова, чтобы связать устройство.",
"ui.syncConflicts": "Конфликты синхронизации",
"ui.lastSyncError": "Ошибка последней синхронизации: {error}",
"ui.server": "Сервер",
"ui.serverUrl": "URL сервера",
"ui.username": "Имя пользователя",
"ui.password": "Пароль",
"ui.testConnection": "Проверить подключение",
"ui.connect": "Подключить",
"ui.behavior": "Параметры синхронизации",
"ui.enableAutoSync": "Включить автосинхронизацию",
"ui.interval": "Интервал синхронизации",
"ui.minutes": "минут",
"ui.lastSync": "Последняя синхронизация: {date}",
"ui.save": "Сохранить",
"ui.syncNow": "Синхронизировать",
"ui.resetKey": "Сбросить ключ",
"ui.disconnect": "Отключить",
"ui.status.connected": "Подключено",
"ui.status.disconnected": "Отключено",
"ui.status.disabled": "Не настроено",
"ui.status.error": "Ошибка",
"ui.status.revoked": "Доступ отозван"
}
+1
View File
@@ -4,6 +4,7 @@
"name": "Sync",
"version": "0.1.0",
"apiVersion": "0.1.0",
"localization": { "defaultLocale": "en", "locales": { "en": "locales/en.json", "ru": "locales/ru.json" } },
"source": "official",
"provides": ["verstak/sync/v1", "verstak/sync.status/v1"],
"requires": ["verstak/core/files/v1"],