SLOPSHOPPER

ja-questions

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

newguardtoaststatuspromptmodel
v0.1.0no licenseupdated 2026-10-23kokotosu/claude-mods/ja-questions
A shopper browsing a rack in a slop shop
README

claude-mods

kokot が作った Claude Code 用の mod 集です。

modできること
usage-band入力欄の上に、残りの使用量(5時間枠・週枠・コンテキスト)とリセットまでの時間を帯で表示する。7日間のトークンを何に使ったかの円グラフも出す。チャット名の色分けもする
chat-timeLINE のように、各メッセージの下に送った日時を表示する
ja-questionsClaude からの質問が英語なら、日本語に訳して表示する
push-to-talk決めたキーを押している間だけ録音して、話した内容を入力欄に入れる
shield危ない操作(フォルダごと削除・履歴の上書き・秘密ファイルの書き換えなど)は止めて確認し、Claude が書いたファイルは自動で構文チェックする。ほかの作業中のチャットも表示し、名前を押すとそのチャットへ飛べる。守るもの・守れないものは shield/SAFETY.md

別のパソコンに入れる方法

ログインはいりません。ほしい mod だけ選んで入れられます。

  1. ターミナルで、まず棚の登録を打つ(1回だけ)
  2. そのあと、ほしい 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
  1. Claude を開き直す

Claude に頼むなら「GitHub の kokotosu/claude-mods から chat-time だけ入れて」のように言えば入れてくれる。

更新したとき

claude plugin marketplace update kokot-claude-mods

そのあと claude plugin update <mod名>@kokot-claude-mods を打つ。

注意

  • chat-time と usage-band の円グラフは Node.js が要る
  • push-to-talk は Windows 専用
  • shield のチャットへ飛ぶボタンは、パソコンの Claude アプリで押したときだけ効く
  • チャット名の色分けを前のパソコンと同じにしたいときは、~/.claude/mods/chat-colors.json もコピーする
Source 1 files
hooks/register.ts 123 lines
1import 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