SLOPSHOPPER

ja-check

返答が英語で書き出されたら知らせる。設定で日本語での書き直しを頼める

newtoast
v0.1.0MITupdated 2026-10-09yut0takagi/mods/ja-check
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · ja-check
› fix the failing auth test and add an audit log call ╭───────────────────────────╮ │ ja-check │ ⏺ Read(src/auth.ts) │ ja-check: 返答が英語で書き出されています │ ⎿ Read 6 lines ╰───────────────────────────╯ ⏺ Update(src/auth.ts) ⎿ Added 2 lines, removed 1 line ⏺ Bash(bun test) ⎿ 3 pass, 1 fail ● Done. refresh now rejects expired claims and logs an audit event. ✻ Worked for 42s · done 4:20 PM ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts
README

mods

English | 日本語

A marketplace of mods for Claude Code. A mod is a plugin that adds features to Claude Code through function hooks.

ModWhat it does
branch-watchStops commits on a branch another session switched to, and lists each session's worktree and branch
push-gateHolds git push and similar commands until a person approves
auto-worktreeCreates a worktree on the first prompt and starts the work there
session-diffShows the files this session edited and their diffs
pr-paneShows open pull requests and their CI results
side-question/btw asks a side question without stopping the work
done-chimePlays a sound when a long turn ends and when Claude waits for permission
ja-checkTells you when a reply starts in English
usage-meterShows each turn's token counts and prompt cache rate

Install

claude plugin marketplace add yut0takagi/mods
claude plugin install branch-watch@yut0takagi-mods   # one line per mod you want

Developed with Claude Code 2.1.289. Mods are an early access feature, so a Claude Code update may break them. The VS Code and Cursor extension draws no mod UI, so some mods are used differently there.

Docs

To change a mod, read CONTRIBUTING. Changes are listed in the CHANGELOG. Report security problems as described in SECURITY.

License

MIT

Source 2 files
hooks/register.ts 37 lines
1import type { Register } from 'claude-code'
2
3import { openingOf } from './lang'
4
5const REWRITE = '直前の返答が英語で書き出されていました。同じ内容を日本語で書き直してください。'
6
7export const register: Register = (on, options) => {
8  const mode = options.mode === 'rewrite' ? 'rewrite' : 'toast'
9  // 書き直しを頼んだ直後の返答も英語なら、もう頼まずに知らせるだけにする(頼み続けて回らないように)
10  let hasAskedRewrite = false
11
12  on('turn.complete', async ($, e, next) => {
13    const done = await next(e)
14
15    if (e.agentId !== undefined || e.reason !== 'answer') {
16      return done
17    }
18
19    const canAskRewrite = mode === 'rewrite' && !hasAskedRewrite
20    hasAskedRewrite = false
21
22    if (openingOf(e.answer) !== 'en') {
23      return done
24    }
25
26    if (canAskRewrite) {
27      hasAskedRewrite = true
28      // 待つとこのターンの終わりを待つことになるので、積むだけにする
29      void $.prompt.submit({ text: REWRITE })
30    } else {
31      $.ui.toast('ja-check: 返答が英語で書き出されています', { timeoutMs: 8_000 })
32    }
33
34    return done
35  })
36}
37
hooks/lang.ts 64 lines
1/** 返答の書き出しが日本語かどうか。$ に触れない部分。 */
2
3export type Opening = 'ja' | 'en' | 'unknown'
4
5const JA = /[\p{Script=Hiragana}\p{Script=Katakana}\p{Script=Han}]/u
6const WORD = /[A-Za-z]{2,}/g
7const FENCE = /^\s*(```|~~~)/
8
9// 英語と決めるのに要る英単語の数。`OK` やパスだけの行では決めない
10const MIN_WORDS = 3
11// 言語の決まらない行をいくつまで読み進めるか
12const MAX_LINES = 3
13
14/** 行から Markdown の記号・インラインコード・URL・パスを除いた地の文。見出しと表の行は空にする。 */
15function proseOf(line: string): string {
16  const text = line.trim()
17
18  if (text.startsWith('#') || text.startsWith('|')) {
19    return ''
20  }
21
22  return text
23    .replace(/`[^`]*`/g, ' ')
24    .replace(/\[([^\]]*)\]\([^)]*\)/g, '$1')
25    .replace(/\S*\/\S*/g, ' ')
26    .replace(/^(?:[>*+-]|\d+\.)\s+/, '')
27    .trim()
28}
29
30/** 書き出しの言語。コードブロック・見出し・表を飛ばし、言語の決まる最初の行で決める。 */
31export function openingOf(answer: string): Opening {
32  let isInFence = false
33  let undecided = 0
34
35  for (const line of answer.split('\n')) {
36    if (FENCE.test(line)) {
37      isInFence = !isInFence
38      continue
39    }
40
41    const prose = isInFence ? '' : proseOf(line)
42
43    if (prose === '') {
44      continue
45    }
46
47    if (JA.test(prose)) {
48      return 'ja'
49    }
50
51    if ((prose.match(WORD) ?? []).length >= MIN_WORDS) {
52      return 'en'
53    }
54
55    undecided += 1
56
57    if (undecided >= MAX_LINES) {
58      break
59    }
60  }
61
62  return 'unknown'
63}
64