Toasts when the last answer looks English or is French written without accents; /french-guard off|on|status

Catches answers that drift out of properly accented French, for users who expect French replies.
deja, probleme, resultat…; the toast shows up to 4 examples)./french-guard status reports the outcome of the last check.| Command | Effect |
|---|---|
/french-guard off / on | Disable / enable (default on) |
/french-guard status | Shows on/off and the last check result |
claude --plugin-dir /path/to/ModsTools/mods/french-guard
claude plugin validate mods/french-guard
claude plugin test mods/french-guard # 10 testshooks/register.ts 42 lines1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import { messages } from './detect'
5
6const guard = atom({ plugin: 'french-guard', key: 'guard' } as const, { isEnabled: true, last: 'no answer checked yet' })
7
8async function answer($: EngineInterface, arg: string): Promise<string> {
9 if (arg === 'off' || arg === 'on') {
10 await update($, guard, g => ({ ...g, isEnabled: arg === 'on' }))
11
12 return `french-guard ${arg}.`
13 }
14 if (arg === 'status' || arg === '') {
15 const g = await read($, guard)
16
17 return `french-guard ${g.isEnabled ? 'on' : 'off'}; last check: ${g.last}`
18 }
19
20 return 'Usage: /french-guard off|on|status'
21}
22
23export const register: Register = on => {
24 on('session.start', async ($, e, next) => {
25 await $.command.register({ name: 'french-guard', description: 'french-guard: off|on|status' })
26
27 return next(e)
28 })
29
30 on('turn.complete', async ($, e, next) => {
31 if (e.agentId === undefined && (await read($, guard)).isEnabled) {
32 const found = messages(e.answer)
33 for (const text of found) $.ui.toast(text)
34 await update($, guard, g => ({ ...g, last: found.length === 0 ? 'ok' : found.join('; ') }))
35 }
36
37 return next(e)
38 })
39
40 on('command.run', { command: 'french-guard' }, async ($, e) => ({ text: await answer($, e.args.trim()) }))
41}
42hooks/detect.ts 62 lines1export const MIN_WORDS = 40
2export const ENGLISH_RATIO = 0.65
3export const MIN_UNACCENTED = 3
4
5const ENGLISH = new Set(
6 'the and is are was were with that this for to of it you have has not be will can which from but if then when what there their would should could been your we they i by an'.split(' '),
7)
8const FRENCH = new Set(
9 'le la les un une des du de et est sont avec que qui pour dans pas ce cette ces il elle nous vous je ne se au aux sur par mais où ou en ça été être très je tu ils elles'.split(' '),
10)
11// Frequent French words written without their accent; none is a valid French or common English word.
12const UNACCENTED = new Set(
13 `deja etre tres voila apres probleme problemes cree crees verifie verifies verifier reponse reponses numero numeros
14 electricite etape etapes ecran fenetre fenetres francais resultat resultats systeme systemes evenement evenements
15 creer genere generer generes memoire regle regles premiere derniere ecrire ecrit developpeur deploye specifique
16 specifiques theorie ca`.split(/\s+/),
17)
18
19export function stripNonProse(text: string): string {
20 return text
21 .replace(/```[\s\S]*?(```|$)/g, ' ')
22 .replace(/~~~[\s\S]*?(~~~|$)/g, ' ')
23 .replace(/`[^`\n]*`/g, ' ')
24 .replace(/\bhttps?:\/\/\S+/g, ' ')
25 .replace(/\S*\/\S*/g, ' ')
26 .replace(/\b[\w-]+\.(?:ts|tsx|js|json|md|swift|py|sh|yml|yaml|toml|html|css|txt|plist)\b/g, ' ')
27}
28
29export function words(prose: string): string[] {
30 return prose.toLowerCase().match(/[a-zàâäçéèêëîïôöùûüÿœ]+(?:'[a-zàâäçéèêëîïôöùûüÿœ]+)?/g) ?? []
31}
32
33export type Verdict = { isEnglish: boolean; unaccented: string[]; wordCount: number }
34
35export function analyze(answer: string): Verdict {
36 const all = words(stripNonProse(answer))
37 let en = 0
38 let fr = 0
39 const unaccented: string[] = []
40 for (const w of all) {
41 const bare = w.includes("'") ? (w.split("'")[1] ?? w) : w
42 if (ENGLISH.has(w)) en += 1
43 else if (FRENCH.has(w) || FRENCH.has(bare)) fr += 1
44 if (UNACCENTED.has(bare)) unaccented.push(bare)
45 }
46 const isEnglish = all.length >= MIN_WORDS && en + fr > 0 && en / (en + fr) >= ENGLISH_RATIO
47
48 return { isEnglish, unaccented, wordCount: all.length }
49}
50
51export function messages(answer: string): string[] {
52 const v = analyze(answer)
53 if (v.isEnglish) return ['french-guard: last answer looks English']
54 if (v.unaccented.length >= MIN_UNACCENTED) {
55 const sample = [...new Set(v.unaccented)].slice(0, 4).join(', ')
56
57 return [`french-guard: ${v.unaccented.length} unaccented words (${sample})`]
58 }
59
60 return []
61}
62types/index.d.ts 8 lines1export type GuardState = { isEnabled: boolean; last: string }
2
3declare module 'claude-code' {
4 interface PluginState {
5 'french-guard': { guard: GuardState }
6 }
7}
8