diff --git a/cmd/opensave-app/frontend/src/components/ConflictModal.svelte b/cmd/opensave-app/frontend/src/components/ConflictModal.svelte
index b16e4151..1a856f64 100644
--- a/cmd/opensave-app/frontend/src/components/ConflictModal.svelte
+++ b/cmd/opensave-app/frontend/src/components/ConflictModal.svelte
@@ -5,6 +5,8 @@
import { visited } from '../lib/later.js';
import { api } from '../lib/api.js';
import { demandAttention } from '../lib/notify.js';
+ // `fmtTime` below has a parameter named `t`, so the translator comes in aliased.
+ import { t as i18n, segments } from '../lib/i18n.js';
import Chevron from './ui/Chevron.svelte';
import Gamepad2 from 'lucide-svelte/icons/gamepad-2';
import Monitor from 'lucide-svelte/icons/monitor';
@@ -58,7 +60,7 @@
}
$: gameName = current ? ($games[current[0]]?.name ?? current[0]) : '';
$: conflict = current ? current[1] : null;
- $: peerName = conflict ? (conflict.peer.Name ?? conflict.peer.name ?? 'the other device') : '';
+ $: peerName = conflict ? (conflict.peer.Name ?? conflict.peer.name ?? $i18n('conflict.otherDevice')) : '';
$: peerType = conflict ? $peers[conflict.peer.ID ?? conflict.peer.id]?.deviceType : '';
$: localIcon = isHandheld($settings?.deviceType) ? Gamepad2 : Monitor;
$: peerIcon = isHandheld(peerType) ? Gamepad2 : Monitor;
@@ -71,9 +73,9 @@
const fresh = list.filter(([gid]) => !seen.has(gid));
if (fresh.length > 0) {
const game = $games[fresh[0][0]];
- const name = game?.name ?? 'a game';
- demandAttention('conflicts', { title: name, body: 'Its save changed here and on another device at once — choose which to keep.', game });
- toast(`Save conflict for “${name}” — choose which version to keep`, 'error');
+ const name = game?.name ?? $i18n('conflict.aGame');
+ demandAttention('conflicts', { title: name, body: $i18n('conflict.attentionBody'), game });
+ toast($i18n('conflict.attentionToast', { name }), 'error');
}
seen = new Set(list.map(([gid]) => gid));
// Housekeeping: forget "applying" markers for conflicts that no longer
@@ -136,9 +138,9 @@
// different measurements.
function sideSummary(stats) {
const files = stats?.files;
- if (files === 0) return 'save folder is empty';
+ if (files === 0) return $i18n('conflict.saveFolderEmpty');
if (typeof files !== 'number') return '';
- return `whole save: ${files} file${files === 1 ? '' : 's'} · ${fmtSize(stats?.totalBytes ?? -1)}`;
+ return $i18n('conflict.wholeSave', { n: files, size: fmtSize(stats?.totalBytes ?? -1) });
}
async function resolve(resolution) {
@@ -156,7 +158,7 @@
});
applying = new Set(applying).add(gameId);
if (resolution !== 'keep-local') {
- toast(`Applying your choice for “${name}” — transferring ${peerName}'s files…`, 'info');
+ toast($i18n('conflict.applyingToast', { name, peer: peerName }), 'info');
}
showDiff = false;
} catch (e) {
@@ -167,12 +169,30 @@
}
const fmtTime = (t) => (t ? new Date(t).toLocaleString() : '—');
- const fmtMs = (ms) => (ms ? new Date(ms).toLocaleString() : 'unknown');
+ const fmtMs = (ms) => (ms ? new Date(ms).toLocaleString() : $i18n('conflict.unknown'));
const fmtSize = (n) =>
n < 0 ? '—' : n >= 1048576 ? (n / 1048576).toFixed(1) + ' MB' : n >= 1024 ? (n / 1024).toFixed(1) + ' KB' : n + ' B';
const diffIcon = (s) => (s === 'changed' ? '✱' : s === 'only-remote' ? '+' : '−');
const diffLabel = (s) =>
- s === 'changed' ? 'differs' : s === 'only-remote' ? `only on ${peerName}` : 'only on this device';
+ s === 'changed'
+ ? $i18n('conflict.differs')
+ : s === 'only-remote'
+ ? $i18n('conflict.onlyOnPeer', { peer: peerName })
+ : $i18n('conflict.onlyOnDevice');
+
+ // The hint paragraph reads as one sentence stream in every language; the
+ // three option names arrive already quoted and are bolded in place.
+ const hintParams = (peerName) => ({
+ both: $i18n('conflict.keepBothQuoted'),
+ mine: $i18n('conflict.keepMineQuoted'),
+ theirs: $i18n('conflict.keepTheirsQuoted'),
+ peer: peerName
+ });
+ $: hintParts = segments($i18n('conflict.hintAll', hintParams(peerName)), hintParams(peerName), [
+ 'both',
+ 'mine',
+ 'theirs'
+ ]);