Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions __tests__/lib/runtime-fallback.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,27 @@ describe('runtimePlanMessageKey', () => {
expect(runtimePlanMessageKey({ action: 'none' })).toBe('');
});

it('a gioco chiuso non dichiara impossibile ciò che è solo fallito', () => {
// "Questo gioco non si lascia tradurre nei file" è vero se il motore non
// espone testo, ed è una bugia se è appena caduto Ollama.
const plan = { action: 'await-launch', processName: 'Game.exe' } as const;
expect(runtimePlanMessageKey(plan, 'engine-unsupported')).toBe(
'gameDetail.runtimeFallbackAwaitLaunch',
);
expect(runtimePlanMessageKey(plan, 'run-failed')).toBe(
'gameDetail.runtimeFallbackAfterFailure',
);
});

it('la causa non cambia il messaggio quando si inietta subito', () => {
// Il gioco è aperto: quello che succede è identico, e il messaggio descrive
// l'azione, non la diagnosi.
const plan = { action: 'inject', processName: 'Game.exe' } as const;
expect(runtimePlanMessageKey(plan, 'engine-unsupported')).toBe(
runtimePlanMessageKey(plan, 'run-failed'),
);
});

it('distingue il blocco per motivo', () => {
expect(runtimePlanMessageKey({ action: 'unavailable', blocker: 'anti-cheat' })).toBe(
'gameDetail.runtimeFallbackBlocked.anti-cheat',
Expand Down Expand Up @@ -146,6 +167,26 @@ describe('buildRunReport', () => {
});
expect(r.nextStepKey).toBe('gameDetail.runtimeFallbackAwaitLaunch');
});

it('propaga la causa fino alla chiave del messaggio', () => {
const r = buildRunReport({
gameTitle: 'Gioco',
staticOutcome: 'failure',
plan: { action: 'await-launch', processName: 'Game.exe' },
cause: 'run-failed',
});
expect(r.cause).toBe('run-failed');
expect(r.nextStepKey).toBe('gameDetail.runtimeFallbackAfterFailure');
});

it('senza causa esplicita assume motore non supportato', () => {
const r = buildRunReport({
gameTitle: 'Gioco',
staticOutcome: 'failure',
plan: { action: 'await-launch', processName: 'Game.exe' },
});
expect(r.cause).toBe('engine-unsupported');
});
});

describe('summarizeRunReport', () => {
Expand Down
10 changes: 10 additions & 0 deletions components/game-detail-client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ import {
buildRunReport,
summarizeRunReport,
type RunReport,
type FallbackCause,
} from '@/lib/translation/runtime-fallback';
import { TARGET_LANGUAGES as CANONICAL_TARGET_LANGUAGES } from '@/lib/translation/target-languages';
import { LANG_TO_CODE } from '@/lib/translation/language-mappings';
Expand Down Expand Up @@ -2061,6 +2062,7 @@ export default function GameDetailPage() {
const tryRuntimeFallback = async (
staticOutcome: PatchOutcome,
counts: { injected?: number | null; total?: number | null },
cause: FallbackCause = 'engine-unsupported',
): Promise<RunReport | null> => {
if (!game?.installPath) return null;

Expand Down Expand Up @@ -2105,6 +2107,7 @@ export default function GameDetailPage() {
stringsInjected: counts.injected ?? null,
stringsTotal: counts.total ?? null,
plan,
cause,
});

// Il report per gioco finisce nella cronologia: dice cosa è entrato nel
Expand Down Expand Up @@ -2216,6 +2219,7 @@ export default function GameDetailPage() {
} catch (e) {
await dTracker.fail(e);
toast.error(t('gameDetail.dr1Error'), { id: toastId, description: String(e).slice(0, 180) });
void tryRuntimeFallback('failure', {}, 'run-failed');
} finally {
setAutoTranslateBusy(false);
setAutoTranslateProgress('');
Expand Down Expand Up @@ -2265,6 +2269,7 @@ export default function GameDetailPage() {
} catch (e) {
await hTracker.fail(e);
toast.error(t('gameDetail.errHendrix'), { id: toastId, description: String(e) });
void tryRuntimeFallback('failure', {}, 'run-failed');
} finally {
setAutoTranslateBusy(false);
setAutoTranslateProgress('');
Expand Down Expand Up @@ -2397,6 +2402,7 @@ export default function GameDetailPage() {
);
await rpTracker.fail(e);
toast.error(t('gameDetail.errRenpy'), { description: String(e) });
void tryRuntimeFallback('failure', {}, 'run-failed');
} finally {
setAutoTranslateBusy(false);
setAutoTranslateProgress('');
Expand Down Expand Up @@ -2540,6 +2546,7 @@ export default function GameDetailPage() {
await tray.notifyTranslationFailed(game.name || game.title || 'Gioco', String(e));
} catch { /* tray non disponibile */ }
toast.error(t('heroJob.visError').replace('{hint}', hint), { description: String(e) });
void tryRuntimeFallback('failure', {}, 'run-failed');
} finally {
VIS_RUNNING.delete(game.installPath);
setAutoTranslateBusy(false);
Expand Down Expand Up @@ -2640,6 +2647,7 @@ export default function GameDetailPage() {
setAutoTranslateError(friendly);
await tyTracker.fail(new Error(friendly));
toast.error(friendly, { description: msg.startsWith('TYRANO_') ? undefined : msg });
void tryRuntimeFallback('failure', {}, 'run-failed');
} finally {
setAutoTranslateBusy(false);
setAutoTranslateProgress('');
Expand Down Expand Up @@ -2767,6 +2775,7 @@ export default function GameDetailPage() {
},
duration: 10000,
});
void tryRuntimeFallback('failure', { injected: 0 });
}
} finally {
setAutoTranslateBusy(false);
Expand Down Expand Up @@ -2833,6 +2842,7 @@ export default function GameDetailPage() {
description: 'Niente stringhe estraibili dai file per questo motore.',
action: { label: 'Apri OCR', onClick: () => router.push(`/ocr-translator?${params.toString()}`) },
});
void tryRuntimeFallback('failure', { injected: 0 });
setAutoTranslateBusy(false);
setAutoTranslateProgress('');
autoTranslateRunningRef.current = false;
Expand Down
3 changes: 2 additions & 1 deletion lib/i18n/locales/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -962,7 +962,8 @@
"unknown-process": "Spiel-Programmdatei nicht erkannt: Es gibt keinen Prozess, auf den zugegriffen werden kann."
},
"runtimeFallbackReady": "Laufzeitübersetzung aktiv: Die Zeilen erscheinen während des Spielens übersetzt.",
"runtimeFallbackFailed": "Laufzeitübersetzung konnte nicht gestartet werden."
"runtimeFallbackFailed": "Laufzeitübersetzung konnte nicht gestartet werden.",
"runtimeFallbackAfterFailure": "Die dateibasierte Übersetzung ist nicht gelungen. Wenn du möchtest, starte das Spiel und versuche die Übersetzung auf dem Bildschirm."
},
"danganronpaPatcher": {
"errLoadDrat": "Fehler beim Laden der DRAT-Informationen:",
Expand Down
3 changes: 2 additions & 1 deletion lib/i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -962,7 +962,8 @@
"unknown-process": "Game executable not identified: no process to act on."
},
"runtimeFallbackReady": "Runtime translation active: lines will appear translated as you play.",
"runtimeFallbackFailed": "Could not start runtime translation."
"runtimeFallbackFailed": "Could not start runtime translation.",
"runtimeFallbackAfterFailure": "File-based translation did not succeed. If you like, launch the game and try translating on screen."
},
"danganronpaPatcher": {
"errLoadDrat": "Error loading DRAT info:",
Expand Down
3 changes: 2 additions & 1 deletion lib/i18n/locales/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -962,7 +962,8 @@
"unknown-process": "Ejecutable del juego no identificado: no hay ningún proceso sobre el que actuar."
},
"runtimeFallbackReady": "Traducción en tiempo de ejecución activa: las líneas aparecerán traducidas mientras juegas.",
"runtimeFallbackFailed": "No se pudo iniciar la traducción en tiempo de ejecución."
"runtimeFallbackFailed": "No se pudo iniciar la traducción en tiempo de ejecución.",
"runtimeFallbackAfterFailure": "La traducción en los archivos no se completó. Si quieres, inicia el juego y prueba la traducción en pantalla."
},
"danganronpaPatcher": {
"errLoadDrat": "Error al cargar la información de DRAT:",
Expand Down
3 changes: 2 additions & 1 deletion lib/i18n/locales/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -962,7 +962,8 @@
"unknown-process": "Exécutable du jeu non identifié : aucun processus sur lequel agir."
},
"runtimeFallbackReady": "Traduction à l'exécution active : les lignes apparaîtront traduites pendant que vous jouez.",
"runtimeFallbackFailed": "Impossible de démarrer la traduction à l'exécution."
"runtimeFallbackFailed": "Impossible de démarrer la traduction à l'exécution.",
"runtimeFallbackAfterFailure": "La traduction par fichiers n'a pas abouti. Si vous le souhaitez, lancez le jeu et essayez la traduction à l'écran."
},
"danganronpaPatcher": {
"errLoadDrat": "Erreur lors du chargement des infos DRAT :",
Expand Down
3 changes: 2 additions & 1 deletion lib/i18n/locales/it.json
Original file line number Diff line number Diff line change
Expand Up @@ -962,7 +962,8 @@
"unknown-process": "Eseguibile del gioco non identificato: non so in quale processo agire."
},
"runtimeFallbackReady": "Traduzione a runtime attiva: le righe appariranno tradotte mentre giochi.",
"runtimeFallbackFailed": "Attivazione della traduzione a runtime fallita."
"runtimeFallbackFailed": "Attivazione della traduzione a runtime fallita.",
"runtimeFallbackAfterFailure": "La traduzione sui file non è riuscita. Se vuoi, avvia il gioco e prova la traduzione a schermo."
},
"danganronpaPatcher": {
"errLoadDrat": "Errore caricamento info DRAT:",
Expand Down
3 changes: 2 additions & 1 deletion lib/i18n/locales/ja.json
Original file line number Diff line number Diff line change
Expand Up @@ -962,7 +962,8 @@
"unknown-process": "ゲームの実行ファイルを特定できません。操作対象のプロセスがありません。"
},
"runtimeFallbackReady": "実行時翻訳が有効です。プレイ中に各行が翻訳されて表示されます。",
"runtimeFallbackFailed": "実行時翻訳を開始できませんでした。"
"runtimeFallbackFailed": "実行時翻訳を開始できませんでした。",
"runtimeFallbackAfterFailure": "ファイルによる翻訳は成功しませんでした。よければゲームを起動して、画面上の翻訳を試してください。"
},
"danganronpaPatcher": {
"errLoadDrat": "DRAT情報の読み込みエラー:",
Expand Down
3 changes: 2 additions & 1 deletion lib/i18n/locales/ko.json
Original file line number Diff line number Diff line change
Expand Up @@ -962,7 +962,8 @@
"unknown-process": "게임 실행 파일을 식별하지 못했습니다. 작업할 프로세스가 없습니다."
},
"runtimeFallbackReady": "런타임 번역이 활성화되었습니다. 플레이 중 각 줄이 번역되어 표시됩니다.",
"runtimeFallbackFailed": "런타임 번역을 시작하지 못했습니다."
"runtimeFallbackFailed": "런타임 번역을 시작하지 못했습니다.",
"runtimeFallbackAfterFailure": "파일 기반 번역에 실패했습니다. 원하시면 게임을 실행하고 화면 번역을 사용해 보세요."
},
"danganronpaPatcher": {
"errLoadDrat": "DRAT 정보 로드 오류:",
Expand Down
3 changes: 2 additions & 1 deletion lib/i18n/locales/pl.json
Original file line number Diff line number Diff line change
Expand Up @@ -962,7 +962,8 @@
"unknown-process": "Nie rozpoznano pliku wykonywalnego gry: brak procesu, na którym można działać."
},
"runtimeFallbackReady": "Tłumaczenie w czasie działania aktywne: wiersze pojawią się przetłumaczone podczas gry.",
"runtimeFallbackFailed": "Nie udało się uruchomić tłumaczenia w czasie działania."
"runtimeFallbackFailed": "Nie udało się uruchomić tłumaczenia w czasie działania.",
"runtimeFallbackAfterFailure": "Tłumaczenie przez pliki się nie powiodło. Jeśli chcesz, uruchom grę i wypróbuj tłumaczenie na ekranie."
},
"danganronpaPatcher": {
"errLoadDrat": "Błąd wczytywania informacji DRAT:",
Expand Down
3 changes: 2 additions & 1 deletion lib/i18n/locales/pt.json
Original file line number Diff line number Diff line change
Expand Up @@ -962,7 +962,8 @@
"unknown-process": "Executável do jogo não identificado: não há processo sobre o qual agir."
},
"runtimeFallbackReady": "Tradução em tempo de execução ativa: as linhas aparecerão traduzidas enquanto joga.",
"runtimeFallbackFailed": "Não foi possível iniciar a tradução em tempo de execução."
"runtimeFallbackFailed": "Não foi possível iniciar a tradução em tempo de execução.",
"runtimeFallbackAfterFailure": "A tradução pelos ficheiros não foi bem-sucedida. Se quiser, inicie o jogo e experimente a tradução no ecrã."
},
"danganronpaPatcher": {
"errLoadDrat": "Erro ao carregar informações do DRAT:",
Expand Down
3 changes: 2 additions & 1 deletion lib/i18n/locales/ru.json
Original file line number Diff line number Diff line change
Expand Up @@ -962,7 +962,8 @@
"unknown-process": "Исполняемый файл игры не определён: нет процесса, с которым можно работать."
},
"runtimeFallbackReady": "Перевод во время игры включён: строки будут появляться переведёнными по ходу игры.",
"runtimeFallbackFailed": "Не удалось запустить перевод во время игры."
"runtimeFallbackFailed": "Не удалось запустить перевод во время игры.",
"runtimeFallbackAfterFailure": "Перевод через файлы не удался. При желании запустите игру и попробуйте перевод на экране."
},
"danganronpaPatcher": {
"errLoadDrat": "Ошибка загрузки информации DRAT:",
Expand Down
3 changes: 2 additions & 1 deletion lib/i18n/locales/zh.json
Original file line number Diff line number Diff line change
Expand Up @@ -962,7 +962,8 @@
"unknown-process": "未能识别游戏可执行文件:没有可操作的进程。"
},
"runtimeFallbackReady": "运行时翻译已启用:游玩过程中各行文本将显示为译文。",
"runtimeFallbackFailed": "无法启动运行时翻译。"
"runtimeFallbackFailed": "无法启动运行时翻译。",
"runtimeFallbackAfterFailure": "通过文件的翻译未能成功。如果需要,可以启动游戏并尝试屏幕翻译。"
},
"danganronpaPatcher": {
"errLoadDrat": "加载 DRAT 信息时出错:",
Expand Down
37 changes: 33 additions & 4 deletions lib/translation/runtime-fallback.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,15 +86,38 @@ export function planRuntimeFallback(ctx: RuntimeContext): RuntimePlan {
: { action: 'await-launch', processName: ctx.processName };
}

/** Chiave i18n del messaggio da mostrare per un piano. */
export function runtimePlanMessageKey(plan: RuntimePlan): string {
/**
* Perché la strada statica non ha inciso. Cambia cosa possiamo onestamente
* promettere, non cosa facciamo.
*/
export type FallbackCause =
/** Il motore non espone testo nei file: la strada statica non c'è, punto. */
| 'engine-unsupported'
/** Questa run è fallita — magari per configurazione, magari no. */
| 'run-failed';

/**
* Chiave i18n del messaggio da mostrare per un piano.
*
* A gioco chiuso il messaggio dipende dalla causa: «questo gioco non si lascia
* tradurre nei file» è vero quando il motore non espone testo, ed è una bugia
* quando è appena caduto Ollama. In quel caso si offre il runtime come
* alternativa, senza dichiarare impossibile una strada che potrebbe funzionare
* benissimo alla prossima prova.
*/
export function runtimePlanMessageKey(
plan: RuntimePlan,
cause: FallbackCause = 'engine-unsupported',
): string {
switch (plan.action) {
case 'none':
return '';
case 'inject':
return 'gameDetail.runtimeFallbackInjecting';
case 'await-launch':
return 'gameDetail.runtimeFallbackAwaitLaunch';
return cause === 'engine-unsupported'
? 'gameDetail.runtimeFallbackAwaitLaunch'
: 'gameDetail.runtimeFallbackAfterFailure';
case 'unavailable':
return `gameDetail.runtimeFallbackBlocked.${plan.blocker}`;
}
Expand All @@ -117,6 +140,8 @@ export interface RunReport {
staticOutcome: PatchOutcome;
/** Piano scelto dopo la strada statica. */
plan: RuntimePlan;
/** Perché la strada statica non ha inciso. */
cause: FallbackCause;
/** Chiave i18n del passo successivo suggerito. */
nextStepKey: string;
}
Expand All @@ -128,12 +153,15 @@ export function buildRunReport(args: {
stringsInjected?: number | null;
stringsTotal?: number | null;
plan: RuntimePlan;
cause?: FallbackCause;
}): RunReport {
const attempted: AttemptedPath[] = ['static'];
if (args.plan.action === 'inject') {
attempted.push('runtime');
}

const cause = args.cause ?? 'engine-unsupported';

return {
gameTitle: args.gameTitle,
engine: args.engine ?? null,
Expand All @@ -142,7 +170,8 @@ export function buildRunReport(args: {
stringsTotal: args.stringsTotal ?? null,
staticOutcome: args.staticOutcome,
plan: args.plan,
nextStepKey: runtimePlanMessageKey(args.plan),
cause,
nextStepKey: runtimePlanMessageKey(args.plan, cause),
};
}

Expand Down