Claude からの質問が英語だったら、表示する前に日本語へ翻訳する

kokot が作った Claude Code 用の mod 集です。
| mod | できること |
|---|---|
| usage-band | 入力欄の上に、残りの使用量(5時間枠・週枠・コンテキスト)とリセットまでの時間を帯で表示する。7日間のトークンを何に使ったかの円グラフも出す。チャット名の色分けもする |
| chat-time | LINE のように、各メッセージの下に送った日時を表示する |
| ja-questions | Claude からの質問が英語なら、日本語に訳して表示する |
| push-to-talk | 決めたキーを押している間だけ録音して、話した内容を入力欄に入れる |
| shield | 危ない操作(フォルダごと削除・履歴の上書き・秘密ファイルの書き換えなど)は止めて確認し、Claude が書いたファイルは自動で構文チェックする。ほかの作業中のチャットも表示し、名前を押すとそのチャットへ飛べる。守るもの・守れないものは shield/SAFETY.md |
ログインはいりません。ほしい mod だけ選んで入れられます。
claude plugin marketplace add kokotosu/claude-mods
claude plugin install usage-band@kokot-claude-mods
claude plugin install chat-time@kokot-claude-mods
claude plugin install ja-questions@kokot-claude-mods
claude plugin install push-to-talk@kokot-claude-mods
claude plugin install shield@kokot-claude-mods
Claude に頼むなら「GitHub の kokotosu/claude-mods から chat-time だけ入れて」のように言えば入れてくれる。
claude plugin marketplace update kokot-claude-mods
そのあと claude plugin update <mod名>@kokot-claude-mods を打つ。
~/.claude/mods/chat-colors.json もコピーするhooks/register.ts 123 lines1import type { Register } from 'claude-code'
2
3const JAPANESE = /[-ヿ一-鿿]/
4const HEADER_MAX = 12
5
6const LANGUAGE = {
7 id: 'ja-questions:language',
8 text: 'ユーザーへの質問は必ず日本語で書くこと。AskUserQuestion の question・header・選択肢の label と description も日本語にする(header は12文字以内)。コード、コマンド、ファイル名、固有名詞はそのままでよい。',
9 scope: 'session',
10} as const
11
12const TRANSLATOR =
13 'あなたは翻訳者です。入力は文字列の JSON 配列です。各要素を自然な日本語に訳し、同じ長さ・同じ順序の JSON 配列だけを返してください。コード、コマンド、ファイル名、製品名は訳さずそのまま残します。説明や前置きは書かないでください。'
14
15const NOTE =
16 'ja-questions: この質問は日本語に翻訳して表示した。回答の質問文と選択肢は翻訳後の文言になっている。'
17
18const needsTranslation = (text: string) =>
19 text.trim() !== '' && !JAPANESE.test(text)
20
21const parseArray = (reply: string, length: number): string[] | undefined => {
22 const start = reply.indexOf('[')
23 const end = reply.lastIndexOf(']')
24
25 if (start === -1 || end <= start) {
26 return undefined
27 }
28
29 try {
30 const parsed: unknown = JSON.parse(reply.slice(start, end + 1))
31 const isValid =
32 Array.isArray(parsed) &&
33 parsed.length === length &&
34 parsed.every(item => typeof item === 'string' && item.trim() !== '')
35
36 return isValid ? (parsed as string[]) : undefined
37 } catch {
38 return undefined
39 }
40}
41
42export const register: Register = on => {
43 on('prompt.compose', async ($, e, next) => {
44 const composed = await next(e)
45
46 return { sections: [...composed.sections, LANGUAGE] }
47 })
48
49 on('tool.call', { tool: 'AskUserQuestion' }, async ($, e, next) => {
50 const sources: string[] = []
51 const collect = (text: string) => {
52 if (needsTranslation(text) && !sources.includes(text)) {
53 sources.push(text)
54 }
55 }
56
57 for (const q of e.questions) {
58 collect(q.question)
59 collect(q.header)
60
61 for (const option of q.options) {
62 collect(option.label)
63 collect(option.description)
64 }
65 }
66
67 if (sources.length === 0) {
68 return next(e)
69 }
70
71 $.ui.status('質問を日本語に翻訳中…')
72 const reply = await $.model.complete({
73 model: 'haiku',
74 system: TRANSLATOR,
75 prompt: JSON.stringify(sources),
76 maxTokens: 4096,
77 effort: 'low',
78 timeoutMs: 20000,
79 })
80 $.ui.status(undefined)
81
82 const translated = reply.isAnswered
83 ? parseArray(reply.text, sources.length)
84 : undefined
85
86 if (translated === undefined) {
87 $.ui.toast('ja-questions: 翻訳できなかったので原文のまま表示します')
88
89 return next(e)
90 }
91
92 const ja = (text: string) => {
93 const at = sources.indexOf(text)
94
95 return at === -1 ? text : (translated[at] ?? text)
96 }
97
98 const questions = e.questions.map(q => {
99 const labels = q.options.map(option => ja(option.label))
100 const isDistinct = new Set(labels).size === labels.length
101
102 return {
103 ...q,
104 question: ja(q.question),
105 header: [...ja(q.header)].slice(0, HEADER_MAX).join(''),
106 options: q.options.map((option, i) => ({
107 ...option,
108 label: isDistinct ? (labels[i] ?? option.label) : option.label,
109 description: ja(option.description),
110 })),
111 }
112 })
113
114 const answered = await next({ ...e, questions })
115
116 if (answered.deny !== undefined) {
117 return answered
118 }
119
120 return { ...answered, context: [...(answered.context ?? []), NOTE] }
121 })
122}
123