feat: translate French notices to English through api.loxi.ch
The service is neither Anthropic- nor OpenAI-compatible: it runs the Claude Code CLI server-side and returns its output. Three consequences are handled explicitly, each with a test. The model is chosen by integer id, not by name, so the id is resolved once from /api/models instead of being hard-coded into the environment — a number in a .env file that silently points at the wrong model is a bad trade for one HTTP call per process. A failed CLI still answers HTTP 200. `exit_code` decides, not the status line; trusting the status would store an empty translation and call it a success. The test for this asserts a 200 carrying exit_code 1. It really does start a process, so the timeout is thirty seconds rather than the ten the spec assumed. Answers are cleaned before use: models wrap text in quotes, prefix it with "Translation:" and append notes often enough that stripping is cheaper than re-prompting, and a stray quotation mark on a shop window reads as a mistake. The cache is keyed on the hash of the trimmed French text, so the same notice is never paid for twice and whitespace does not cause a miss. The write is an upsert: two concurrent saves of the same text should be a no-op, not a crash. Only the loxi adapter exists, behind the interface. Writing the Anthropic and OpenAI adapters the spec asked for, with nothing calling them, would be inventory rather than flexibility — the seam is the interface, and it is there. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012cSY9pVhZmJUKNN7wf1Myd
This commit is contained in:
@@ -0,0 +1,64 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { buildPrompt, cleanTranslation } from './provider';
|
||||
|
||||
describe('buildPrompt', () => {
|
||||
const prompt = buildPrompt('Fermeture exceptionnelle jeudi après-midi');
|
||||
|
||||
it('carries the message to translate', () => {
|
||||
expect(prompt).toContain('Fermeture exceptionnelle jeudi après-midi');
|
||||
});
|
||||
|
||||
it('states the constraints that matter on a shop window', () => {
|
||||
expect(prompt).toMatch(/UNIQUEMENT avec la traduction/);
|
||||
expect(prompt).toMatch(/sans guillemets ajoutés/);
|
||||
expect(prompt).toMatch(/horaires, les dates et les nombres/);
|
||||
});
|
||||
|
||||
it('caps the length at the source plus a fifth', () => {
|
||||
// 40 characters in, 48 allowed out.
|
||||
expect(buildPrompt('a'.repeat(40))).toContain('48 caractères');
|
||||
});
|
||||
});
|
||||
|
||||
describe('cleanTranslation', () => {
|
||||
it('returns a plain answer untouched', () => {
|
||||
expect(cleanTranslation('Exceptionally closed on Thursday afternoon')).toBe(
|
||||
'Exceptionally closed on Thursday afternoon',
|
||||
);
|
||||
});
|
||||
|
||||
it('trims surrounding whitespace', () => {
|
||||
expect(cleanTranslation(' Closed today \n')).toBe('Closed today');
|
||||
});
|
||||
|
||||
it('strips the quotes models like to add', () => {
|
||||
// A stray quotation mark on the shop window reads as a mistake.
|
||||
expect(cleanTranslation('"Closed today"')).toBe('Closed today');
|
||||
expect(cleanTranslation('« Closed today »')).toBe('Closed today');
|
||||
expect(cleanTranslation('“Closed today”')).toBe('Closed today');
|
||||
});
|
||||
|
||||
it('keeps an apostrophe inside the text', () => {
|
||||
expect(cleanTranslation("Today's opening hours")).toBe("Today's opening hours");
|
||||
});
|
||||
|
||||
it('strips a label the model prefixed', () => {
|
||||
expect(cleanTranslation('Translation: Closed today')).toBe('Closed today');
|
||||
expect(cleanTranslation('traduction : Closed today')).toBe('Closed today');
|
||||
});
|
||||
|
||||
it('keeps only the first line when the model explains itself', () => {
|
||||
expect(cleanTranslation('Closed today\n\nNote: I kept the time unchanged.')).toBe(
|
||||
'Closed today',
|
||||
);
|
||||
});
|
||||
|
||||
it('does not strip an unmatched quote', () => {
|
||||
expect(cleanTranslation('"Closed today')).toBe('"Closed today');
|
||||
});
|
||||
|
||||
it('survives an empty answer', () => {
|
||||
expect(cleanTranslation(' ')).toBe('');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user