Files
aiturk-hermes-ide/apps/desktop/src/plugins/hermes-bots/i18n.ts
T

1194 lines
53 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Plugin-scoped i18n for Bot Mode — bundles registered under the plugin id via
* `ctx.i18n.register`, never touching core `en.ts`. Mirrors the kanban plugin:
* `usePluginI18n` returns a stringly-typed `t(key, …)`, and `useBots()` binds it
* to the message SHAPE so components keep typed `b.roster.search` access.
*
* Only strings Bot Mode OWNS live here. Generic verbs (Cancel, Delete, Remove,
* Retry, Close, Loading…) and shared vocabulary core already ships in every
* locale — weekday names, Daily/Hourly, Scheduled jobs — resolve against core
* via `useI18n()` / `translateNow()`. Duplicating those here would be a
* second, worse translation that drifts.
*
* Three kinds of literal deliberately stay hardcoded, and none of them is a
* missed key:
*
* - **Prompts sent to a model**, not shown as chrome: the room-picture image
* prompt and the scheduled-routine instruction. They are addressed to the
* model, which reads English best.
* - **Syntax and identifiers**: cron expressions and their examples, React
* keys, workspace ids.
* - **`'You'`**, the author marker on room-log entries. It is persisted into
* the log and compared as a sentinel (`group-activity.ts`), so translating
* it in place would break both. Localizing it needs the marker and its
* rendering split apart — worth doing, not doable as a rename.
*
* Locales follow kanban: `en` / `ja` / `zh` / `zh-hant`. Arabic falls through
* the resolution chain (active locale → this plugin's `en` → the key) the
* same way a missing string in any locale does. Nouns match core: ボット /
* 机器人 / 機器人, プロファイル / 配置档案 / 設定檔, ゲートウェイ / 网关 / 閘道.
*/
import { type PluginLocaleBundles, type PluginTranslate, usePluginI18n } from '@hermes/plugin-sdk'
import { useMemo } from 'react'
import { getPluginCtx } from './shared'
type BotsMessages = {
/** Left rail: the bot + group-chat roster. */
roster: {
search: string
searchPlaceholder: string
newBotOrGroup: string
groupChats: string
emptyTitle: string
emptyDesc: string
noMatchQuery: (query: string) => string
noMatchQueryOn: (query: string, gateway: string) => string
noMatchFiltersOn: (gateway: string) => string
noMatchFilters: string
clearFilters: string
allHidden: string
allHiddenDesc: string
showHidden: string
noHiddenMatch: string
hiddenFromRoster: string
pinned: string
needsAttention: string
needsInput: string
/** The kind filter's three options, in menu order. */
botsAndGroups: string
botsOnly: string
groupsOnly: string
/** The activity filter's four options, in menu order. */
anyActivity: string
activeNow: string
recentlyActive: string
older: string
/** How a row's owning gateway is doing — see `botSourceStatus`. */
gatewayRemoved: string
onDemand: string
ready: string
statusUnknown: string
unavailable: string
retryNow: string
rosterUnavailable: (reason: string) => string
waitingForGateway: string
}
/** User-made roster sections (folders the user files bots into). */
sections: {
newSection: string
newTitle: string
renameTitle: string
nameLabel: string
namePlaceholder: string
create: string
rename: string
moveUp: string
moveDown: string
unassigned: string
options: (name: string) => string
headingTip: string
emptyHint: string
moveTo: string
newSectionEllipsis: string
removeFromSection: string
deleted: (name: string, count: number) => string
undo: string
}
/** Creating, editing and removing a bot. */
bot: {
newTitle: string
editTitle: string
editMenu: string
helpPromptPlaceholder: string
descriptionHint: string
newChatWith: string
/** Re-opens the forever-chat on purpose. A plain row click only returns to
* the tabs already open, so a closed Bot Chat needs an explicit ask. */
openBotChat: string
duplicate: string
duplicateFailed: string
deleteTitle: string
removeFromAllGroups: string
createFirstHint: string
createFailed: string
advanced: string
advancedHint: string
advancedFailed: string
openAnotherChatUnsupported: string
remoteConnectionsUnsupported: string
/** Stands under the bot's name in a chat it has not spoken in yet. */
chatEmpty: string
/** First line of a brand-new bot's forever-chat — see `kickoffText`. */
kickoff: string
}
/** Avatar picker: shapes, blobs, pets, uploads, generation. */
avatar: {
classicShapes: string
blobFromName: string
unlockFollowsName: string
randomize: string
/** The picker's four tabs, in order. */
tabBot: string
tabGenerate: string
upload: string
tabPet: string
removeImage: string
removeBackToShape: string
describePlaceholder: string
describeHint: string
matchTheName: string
pickPet: string
petLoadFailed: string
imageTooLarge: string
generationFailed: string
savedLocally: string
savedLocallyDescriptionFailed: string
generate: string
generating: string
}
/** Group chats: the room, its composer, threads and activity feed. */
group: {
newTitle: string
manageDesc: string
manageTitle: string
settingsTitle: string
settingsDesc: string
nameLabel: string
searchToAdd: string
searchToAddPlaceholder: string
removeFromSelection: string
disbandTitle: string
deleteTitle: string
deleteAction: string
composerPlaceholder: string
attachHint: string
newThread: string
reply: string
replyInThread: string
replyInThreadPlaceholder: string
openThread: string
collapseThread: string
collapseThreadLabel: string
activity: string
noActivityYet: string
showActivity: string
hideActivity: string
stop: string
stopHint: string
allHeldStatus: (count: number) => string
heldMembersStatus: (members: string) => string
holdReleaseHint: string
needsYourInput: string
pictureGenerationFailed: string
nameTaken: (name: string) => string
memberCount: (count: number) => string
settingsHint: (group: string) => string
settingsLabel: (group: string) => string
disbandHint: (group: string) => string
disbandLabel: (group: string) => string
disbandAction: string
disbanding: string
disbandDone: string
disbanded: (group: string) => string
/** Wraps the bolded group name, so the name can lead the sentence in
* languages that put it there — see core's cron.deleteDesc* pair. */
disbandDescPrefix: string
disbandDescSuffix: (count: number) => string
stopped: (group: string) => string
removeAttachment: string
threadFallback: string
replyCount: (replies: number) => string
dropToThread: string
dropToRoom: string
waitingForAnswer: string
memberThinking: (name: string) => string
roomWorking: string
messageRoom: (group: string) => string
newThreadPlaceholder: (group: string) => string
everyoneMeta: string
commandApproval: string
answerFailed: (handle: string, error: string) => string
wantsToRunCommand: (handle: string) => string
asks: (handle: string) => string
answerTo: (member: string) => string
}
/** Skills hub + MCP setup surfaces embedded in the bot editor. */
tools: {
skillsHub: string
filterSkills: string
searchHub: string
noMcpServers: string
}
/** Bot-scoped scheduled jobs. Generic scheduling chrome (weekday names,
* Daily/Hourly, the job verbs) resolves against core's `cron` section. */
cron: {
filterHint: string
needsRosterFirst: string
staleNotice: string
readFailure: string
createDesc: (bot: string) => string
instruction: string
whenToRun: string
dayOfMonth: string
sendResultsTo: string
runHistoryOnly: string
botChatTarget: (bot: string) => string
continuity: string
onceIn: (when: string) => string
everyNDays: (days: number) => string
everyNHours: (hours: number) => string
everyNMinutes: (minutes: number) => string
/** The frequency picker's eight options, in menu order. */
freqOnce: string
freqHourly: string
freqDaily: string
freqWeekdays: string
freqWeekly: string
freqMonthly: string
freqInterval: string
freqAdvanced: string
unitMinutes: string
unitHours: string
unitDays: string
/** One-line plain-language read-back of the picker's current state. */
runsOnce: (count: number, unit: string) => string
runsHourly: string
runsDaily: (time: string) => string
runsWeekdays: (time: string) => string
runsWeekly: (day: string, time: string) => string
runsMonthly: (day: string, time: string) => string
runsInterval: (count: number, unit: string) => string
runsRaw: string
timesTotal: (count: number) => string
}
}
const en: BotsMessages = {
roster: {
search: 'Search bots and group chats',
searchPlaceholder: 'Search bots and group chats…',
newBotOrGroup: 'New bot or group chat',
groupChats: 'Group chats',
emptyTitle: 'No bots yet',
emptyDesc: 'Create your first bot.',
noMatchQuery: query => `No bots or group chats match “${query}”`,
noMatchQueryOn: (query, gateway) => `No bots or group chats match “${query}” on ${gateway}`,
noMatchFiltersOn: gateway => `No bots or group chats match these filters on ${gateway}`,
noMatchFilters: 'No bots or group chats match these filters.',
clearFilters: 'Clear filters',
allHidden: 'All bots are hidden',
allHiddenDesc: 'They keep working and retain their history.',
showHidden: 'Show hidden bots',
noHiddenMatch: 'No hidden bots match these filters.',
hiddenFromRoster: 'Hidden from the roster',
pinned: 'Pinned',
needsAttention: 'needs attention',
needsInput: 'Needs your input',
botsAndGroups: 'Bots and group chats',
botsOnly: 'Bots only',
groupsOnly: 'Group chats only',
anyActivity: 'Any activity',
activeNow: 'Active now',
recentlyActive: 'Recently active',
older: 'Older',
gatewayRemoved: 'Gateway removed',
onDemand: 'On demand',
ready: 'Ready',
statusUnknown: 'Status unknown',
unavailable: 'Unavailable',
retryNow: 'Retry now',
rosterUnavailable: reason =>
`Roster unavailable: ${reason}. If your gateway predates profiles.list, update Hermes and restart the gateway.`,
waitingForGateway:
'Waiting for the gateway connection… (remote gateways can take a few seconds; retries automatically)'
},
sections: {
newSection: 'New section',
newTitle: 'New section',
renameTitle: 'Rename section',
nameLabel: 'Section name',
namePlaceholder: 'e.g. Clients',
create: 'Create',
rename: 'Rename…',
moveUp: 'Move up',
moveDown: 'Move down',
unassigned: 'Unassigned',
options: name => `${name} section options`,
headingTip: 'Drop bots here · double-click to rename',
emptyHint: 'Drag bots here',
moveTo: 'Move to section',
newSectionEllipsis: 'New section…',
removeFromSection: 'Remove from section',
deleted: (name, count) =>
count === 0
? `Deleted “${name}”`
: `Deleted “${name}” — ${count} ${count === 1 ? 'bot' : 'bots'} moved to Unassigned`,
undo: 'Undo'
},
bot: {
newTitle: 'New bot',
editTitle: 'Edit profile',
editMenu: 'Edit…',
helpPromptPlaceholder: 'What should this bot help with?',
descriptionHint: 'Leave blank to generate from the bot’s name and description.',
newChatWith: 'New chat with this bot',
openBotChat: 'Open Bot Chat',
duplicate: 'Duplicate',
duplicateFailed: 'Duplicate failed',
deleteTitle: 'Delete bot and profile?',
removeFromAllGroups: 'Remove from all groups',
createFirstHint: 'Open the Bots pane and hit “New Bot”.',
createFailed: 'Could not create the profile yet',
advanced: 'Advanced',
advancedHint: 'Advanced — model, skills, toolsets, SOUL.md',
advancedFailed: 'Advanced configuration failed',
openAnotherChatUnsupported: 'Update Hermes Desktop to open another Bot chat.',
remoteConnectionsUnsupported: 'Update Hermes Desktop to chat with bots on other connections.',
chatEmpty: 'Say something to get started.',
kickoff: 'Hey, tell me about yourself!'
},
avatar: {
classicShapes: 'Classic shapes',
blobFromName: 'Blob face — drawn from the bot’s name',
unlockFollowsName: 'Unlock — the face follows the bot’s name again',
randomize: 'Randomize',
tabBot: 'Bot',
tabGenerate: 'Generate',
upload: 'Upload',
tabPet: 'Pet',
removeImage: 'Remove image — use shape',
removeBackToShape: 'Remove — back to shape avatar',
describePlaceholder: 'Describe your avatar…',
describeHint: 'Leave blank to auto-generate from name/title/description + agent-messaging roster.',
matchTheName: 'Match the name',
pickPet: 'Pick a pet as this bot’s profile picture.',
petLoadFailed: 'Could not load that pet — try another.',
imageTooLarge: 'Image too large (max 15MB).',
generationFailed: 'Avatar generation failed',
savedLocally: 'Saved look locally; remote persistence failed',
savedLocallyDescriptionFailed: 'Saved look locally; description update failed',
generate: 'Generate',
generating: 'Generating…'
},
group: {
newTitle: 'New group chat',
manageDesc: 'A bot can join multiple group chats. Memberships sync to every machine.',
manageTitle: 'Manage groups',
settingsTitle: 'Group settings',
settingsDesc: 'Rename the group or set a room picture. Members and history are kept.',
nameLabel: 'Group name',
searchToAdd: 'Search bots to add',
searchToAddPlaceholder: 'Search bots to add…',
removeFromSelection: 'Remove from selection',
disbandTitle: 'Disband group chat?',
deleteTitle: 'Delete group chat?',
deleteAction: 'Delete',
composerPlaceholder: 'Say something — every bot in this group hears the room.',
attachHint: 'Attach files — every responding bot sees them',
newThread: 'New Thread',
reply: 'Reply',
replyInThread: 'Reply in thread',
replyInThreadPlaceholder: 'Reply in thread…',
openThread: 'Open this thread',
collapseThread: 'Collapse thread',
collapseThreadLabel: 'Collapse this thread',
activity: 'Activity',
noActivityYet: 'No activity in this turn yet.',
showActivity: 'Show room activity',
hideActivity: 'Hide room activity',
stop: 'Stop',
stopHint: 'Stop this run — interrupts the member on turn and holds the rest',
allHeldStatus: count => `All ${count} bots are paused`,
heldMembersStatus: members => `Paused: ${members}`,
holdReleaseHint: 'Mention a paused bot or send @all resume to release them.',
needsYourInput: 'A bot in this group chat needs your input',
pictureGenerationFailed: 'Group picture generation failed',
nameTaken: name => `A group named “${name}” already exists.`,
memberCount: count => `${count} bots`,
settingsHint: group => `Group settings — rename ${group} or set a room picture`,
settingsLabel: group => `Group settings for ${group}`,
disbandHint: group => `Disband the ${group} group chat`,
disbandLabel: group => `Disband ${group}`,
disbandAction: 'Disband',
disbanding: 'Disbanding…',
disbandDone: 'Disbanded',
disbanded: group => `Disbanded “${group}”`,
disbandDescPrefix: 'This removes the ',
disbandDescSuffix: count =>
` grouping from its ${count} bots and clears the shared room log. The bots themselves and their per-group sessions are kept.`,
stopped: group => `Stopped ${group} — remaining turns are held until you resume`,
removeAttachment: 'Remove attachment',
threadFallback: 'Thread',
replyCount: replies => `${replies} ${replies === 1 ? 'reply' : 'replies'}`,
dropToThread: 'Drop to attach to this thread reply',
dropToRoom: 'Drop to attach — every responding bot sees it',
waitingForAnswer: 'Waiting for your answer…',
memberThinking: name => `${name} is thinking…`,
roomWorking: 'The room is working…',
messageRoom: group => `Message ${group}`,
newThreadPlaceholder: group => `New thread in ${group}… (@name to direct, @everyone for all)`,
everyoneMeta: 'Every bot in the room',
commandApproval: 'command approval',
answerFailed: (handle, error) => `Could not send the answer to @${handle}: ${error}`,
wantsToRunCommand: handle => `@${handle} wants to run a command:`,
asks: handle => `@${handle} asks:`,
answerTo: member => `Answer @${member}`
},
tools: {
skillsHub: 'Hermes Skills Hub',
filterSkills: 'Filter skills…',
searchHub: 'Search the hub (community + well-known sources)…',
noMcpServers: 'No MCP servers configured or in the catalog.'
},
cron: {
filterHint:
'Scheduled jobs exist in this profile but none are tagged for this bot. Name a job "[bot:<name>] …" to show it here, or see them in Cron below.',
needsRosterFirst: 'This bot has to appear in the roster first.',
staleNotice: 'Could not refresh scheduled jobs. Showing the last list we had.',
readFailure: 'The list may still be there — this was a read failure, not a delete.',
createDesc: bot => `A recurring task ${bot} runs on a schedule. Runs land in its own chat history.`,
instruction: 'Instruction',
whenToRun: 'When to run',
dayOfMonth: 'Day of month',
sendResultsTo: 'Send results to',
runHistoryOnly: 'Run history only',
botChatTarget: bot => `${bot}’s chat (bot responds)`,
continuity: 'Continuity: each run sees the previous run’s output (dedupe, continue where it left off)',
onceIn: when => `Once (${when})`,
everyNDays: days => `Every ${days} days`,
everyNHours: hours => `Every ${hours}h`,
everyNMinutes: minutes => `Every ${minutes}m`,
freqOnce: 'Once, in…',
freqHourly: 'Every hour',
freqDaily: 'Every day',
freqWeekdays: 'Weekdays',
freqWeekly: 'Every week',
freqMonthly: 'Every month',
freqInterval: 'Interval',
freqAdvanced: 'Advanced…',
unitMinutes: 'minute(s)',
unitHours: 'hour(s)',
unitDays: 'day(s)',
runsOnce: (count, unit) => `Runs once, ${count} ${unit} from now`,
runsHourly: 'Runs at the top of every hour',
runsDaily: time => `Runs every day at ${time}`,
runsWeekdays: time => `Runs Monday–Friday at ${time}`,
runsWeekly: (day, time) => `Runs every ${day} at ${time}`,
runsMonthly: (day, time) => `Runs on day ${day} of each month at ${time}`,
runsInterval: (count, unit) => `Runs every ${count} ${unit}`,
runsRaw: 'Raw schedule — every Nm/Nh/Nd or 5-field cron',
timesTotal: count => `, ${count} time(s) total`
}
}
const ja: BotsMessages = {
roster: {
search: 'ボットとグループチャットを検索',
searchPlaceholder: 'ボットとグループチャットを検索…',
newBotOrGroup: '新しいボットまたはグループチャット',
groupChats: 'グループチャット',
emptyTitle: 'ボットはまだありません',
emptyDesc: '最初のボットを作成しましょう。',
noMatchQuery: query => `「${query}」に一致するボットやグループチャットはありません`,
noMatchQueryOn: (query, gateway) => `${gateway} に「${query}」に一致するボットやグループチャットはありません`,
noMatchFiltersOn: gateway => `${gateway} にこれらのフィルタに一致するボットやグループチャットはありません`,
noMatchFilters: 'これらのフィルタに一致するボットやグループチャットはありません。',
clearFilters: 'フィルタをクリア',
allHidden: 'すべてのボットが非表示です',
allHiddenDesc: '非表示でも動作を続け、履歴も残ります。',
showHidden: '非表示のボットを表示',
noHiddenMatch: 'これらのフィルタに一致する非表示ボットはありません。',
hiddenFromRoster: '名簿から非表示',
pinned: 'ピン留め',
needsAttention: '要対応',
needsInput: '入力が必要です',
botsAndGroups: 'ボットとグループチャット',
botsOnly: 'ボットのみ',
groupsOnly: 'グループチャットのみ',
anyActivity: 'すべてのアクティビティ',
activeNow: '現在アクティブ',
recentlyActive: '最近アクティブ',
older: '以前',
gatewayRemoved: 'ゲートウェイが削除されました',
onDemand: 'オンデマンド',
ready: '準備完了',
statusUnknown: '状態不明',
unavailable: '利用できません',
retryNow: '今すぐ再試行',
rosterUnavailable: reason =>
`名簿を取得できません: ${reason}。ゲートウェイが profiles.list より前の場合は、Hermes を更新してゲートウェイを再起動してください。`,
waitingForGateway: 'ゲートウェイ接続を待っています…(リモートは数秒かかることがあります。自動で再試行します)'
},
sections: {
newSection: '新しいセクション',
newTitle: '新しいセクション',
renameTitle: 'セクション名を変更',
nameLabel: 'セクション名',
namePlaceholder: '例: クライアント',
create: '作成',
rename: '名前を変更…',
moveUp: '上へ移動',
moveDown: '下へ移動',
unassigned: '未分類',
options: name => `${name} セクションのオプション`,
headingTip: 'ここにボットをドロップ · ダブルクリックで名前を変更',
emptyHint: 'ここにボットをドラッグ',
moveTo: 'セクションへ移動',
newSectionEllipsis: '新しいセクション…',
removeFromSection: 'セクションから外す',
deleted: (name, count) =>
count === 0
? `「${name}」を削除しました`
: `「${name}」を削除しました — ${count} 件のボットを未分類に移動しました`,
undo: '元に戻す'
},
bot: {
newTitle: '新しいボット',
editTitle: 'プロファイルを編集',
editMenu: '編集…',
helpPromptPlaceholder: 'このボットは何を手伝いますか?',
descriptionHint: '空欄のままにすると、ボットの名前と説明から生成します。',
newChatWith: 'このボットと新しいチャット',
openBotChat: 'ボットチャットを開く',
duplicate: '複製',
duplicateFailed: '複製に失敗しました',
deleteTitle: 'ボットとプロファイルを削除しますか?',
removeFromAllGroups: 'すべてのグループから外す',
createFirstHint: 'ボットパネルを開いて「新しいボット」を押してください。',
createFailed: 'プロファイルをまだ作成できませんでした',
advanced: '詳細設定',
advancedHint: '詳細設定 — モデル、スキル、ツールセット、SOUL.md',
advancedFailed: '詳細設定に失敗しました',
openAnotherChatUnsupported: '別のボットチャットを開くには Hermes Desktop を更新してください。',
remoteConnectionsUnsupported: '他の接続上のボットとチャットするには Hermes Desktop を更新してください。',
chatEmpty: '何か書いて始めましょう。',
kickoff: 'こんにちは、自己紹介をしてください!'
},
avatar: {
classicShapes: 'クラシックシェイプ',
blobFromName: 'ブロブ顔 — ボットの名前から描画',
unlockFollowsName: 'ロック解除 — 顔がボットの名前に再び追従します',
randomize: 'ランダム',
tabBot: 'ボット',
tabGenerate: '生成',
upload: 'アップロード',
tabPet: 'ペット',
removeImage: '画像を削除してシェイプを使う',
removeBackToShape: '削除 — シェイプアバターに戻す',
describePlaceholder: 'アバターを説明…',
describeHint: '空欄のままにすると、名前・タイトル・説明と agent-messaging の名簿から自動生成します。',
matchTheName: '名前に合わせる',
pickPet: 'このボットのプロフィール画像としてペットを選びます。',
petLoadFailed: 'そのペットを読み込めませんでした。別のペットを試してください。',
imageTooLarge: '画像が大きすぎます(最大 15MB)。',
generationFailed: 'アバターの生成に失敗しました',
savedLocally: '見た目はローカルに保存されましたが、リモートへの保存に失敗しました',
savedLocallyDescriptionFailed: '見た目はローカルに保存されましたが、説明の更新に失敗しました',
generate: '生成',
generating: '生成中…'
},
group: {
newTitle: '新しいグループチャット',
manageDesc: 'ボットは複数のグループチャットに参加できます。メンバーシップはすべてのマシンに同期されます。',
manageTitle: 'グループを管理',
settingsTitle: 'グループ設定',
settingsDesc: 'グループ名の変更や部屋の画像の設定ができます。メンバーと履歴は保持されます。',
nameLabel: 'グループ名',
searchToAdd: '追加するボットを検索',
searchToAddPlaceholder: '追加するボットを検索…',
removeFromSelection: '選択から外す',
disbandTitle: 'グループチャットを解散しますか?',
deleteTitle: 'グループチャットを削除しますか?',
deleteAction: '削除',
composerPlaceholder: '何か書いてください — このグループのすべてのボットが部屋の内容を受け取ります。',
attachHint: 'ファイルを添付 — 応答するすべてのボットが見ます',
newThread: '新しいスレッド',
reply: '返信',
replyInThread: 'スレッドで返信',
replyInThreadPlaceholder: 'スレッドで返信…',
openThread: 'このスレッドを開く',
collapseThread: 'スレッドを折りたたむ',
collapseThreadLabel: 'このスレッドを折りたたむ',
activity: 'アクティビティ',
noActivityYet: 'このターンのアクティビティはまだありません。',
showActivity: '部屋のアクティビティを表示',
hideActivity: '部屋のアクティビティを隠す',
stop: '停止',
stopHint: 'この実行を停止 — ターン中のメンバーを中断し、残りを保留します',
allHeldStatus: count => `すべてのボット(${count}体)が一時停止中`,
heldMembersStatus: members => `一時停止中: ${members}`,
holdReleaseHint: '一時停止中のボットにメンションするか、@all resume を送信して再開します。',
needsYourInput: 'このグループチャットのボットが入力を待っています',
pictureGenerationFailed: 'グループ画像の生成に失敗しました',
nameTaken: name => `「${name}」という名前のグループはすでに存在します。`,
memberCount: count => `ボット${count}体`,
settingsHint: group => `グループ設定 — ${group}の名前変更やルーム画像の設定`,
settingsLabel: group => `${group}のグループ設定`,
disbandHint: group => `${group}グループチャットを解散`,
disbandLabel: group => `${group}を解散`,
disbandAction: '解散',
disbanding: '解散中…',
disbandDone: '解散しました',
disbanded: group => `「${group}」を解散しました`,
disbandDescPrefix: '',
disbandDescSuffix: count =>
`のグループ分けをボット${count}体から解除し、共有ルームログを消去します。ボット自体と各グループのセッションは保持されます。`,
stopped: group => `${group}を停止しました — 残りのターンは再開するまで保留されます`,
removeAttachment: '添付を削除',
threadFallback: 'スレッド',
replyCount: replies => `返信${replies}件`,
dropToThread: 'ドロップしてこのスレッド返信に添付',
dropToRoom: 'ドロップして添付 — 応答するすべてのボットが見られます',
waitingForAnswer: 'あなたの回答を待っています…',
memberThinking: name => `${name}が考えています…`,
roomWorking: 'ルームが作業中です…',
messageRoom: group => `${group}にメッセージ`,
newThreadPlaceholder: group => `${group}で新しいスレッド…(@名前で個別、@everyoneで全員)`,
everyoneMeta: 'ルーム内のすべてのボット',
commandApproval: 'コマンドの承認',
answerFailed: (handle, error) => `@${handle}に回答を送信できませんでした: ${error}`,
wantsToRunCommand: handle => `@${handle}がコマンドを実行しようとしています:`,
asks: handle => `@${handle}からの質問:`,
answerTo: member => `@${member}に回答`
},
tools: {
skillsHub: 'Hermes スキルハブ',
filterSkills: 'スキルを絞り込み…',
searchHub: 'ハブを検索(コミュニティと既知のソース)…',
noMcpServers: '設定済みまたはカタログ内の MCP サーバーはありません。'
},
cron: {
filterHint:
'このプロファイルには定期実行ジョブがありますが、このボット向けのタグが付いたものはありません。ジョブ名を「[bot:<名前>] …」にするとここに表示されます。下のCronでも確認できます。',
needsRosterFirst: 'このボットは先に名簿に表示される必要があります。',
staleNotice: '定期実行ジョブを更新できませんでした。最後に取得したリストを表示しています。',
readFailure: 'リストはまだ存在している可能性があります — これは読み取りの失敗で、削除ではありません。',
createDesc: bot => `${bot}がスケジュールに沿って実行する定期タスクです。実行結果は専用のチャット履歴に残ります。`,
instruction: '指示',
whenToRun: '実行するタイミング',
dayOfMonth: '日付',
sendResultsTo: '結果の送信先',
runHistoryOnly: '実行履歴のみ',
botChatTarget: bot => `${bot}のチャット(ボットが応答)`,
continuity: '継続: 各実行が前回の出力を参照します(重複を避け、続きから実行)',
onceIn: when => `1回のみ(${when})`,
everyNDays: days => `${days}日ごと`,
everyNHours: hours => `${hours}時間ごと`,
everyNMinutes: minutes => `${minutes}分ごと`,
freqOnce: '1回のみ、…後',
freqHourly: '毎時',
freqDaily: '毎日',
freqWeekdays: '平日',
freqWeekly: '毎週',
freqMonthly: '毎月',
freqInterval: '間隔',
freqAdvanced: '詳細…',
unitMinutes: '分',
unitHours: '時間',
unitDays: '日',
runsOnce: (count, unit) => `今から${count}${unit}後に1回実行します`,
runsHourly: '毎時0分に実行します',
runsDaily: time => `毎日${time}に実行します`,
runsWeekdays: time => `月曜〜金曜の${time}に実行します`,
runsWeekly: (day, time) => `毎週${day}の${time}に実行します`,
runsMonthly: (day, time) => `毎月${day}日の${time}に実行します`,
runsInterval: (count, unit) => `${count}${unit}ごとに実行します`,
runsRaw: '生のスケジュール — Nm/Nh/Nd または5フィールドのcron',
timesTotal: count => `、合計${count}回`
}
}
const zh: BotsMessages = {
roster: {
search: '搜索机器人和群聊',
searchPlaceholder: '搜索机器人和群聊…',
newBotOrGroup: '新建机器人或群聊',
groupChats: '群聊',
emptyTitle: '还没有机器人',
emptyDesc: '创建你的第一个机器人。',
noMatchQuery: query => `没有机器人或群聊匹配“${query}”`,
noMatchQueryOn: (query, gateway) => `${gateway} 上没有机器人或群聊匹配“${query}”`,
noMatchFiltersOn: gateway => `${gateway} 上没有机器人或群聊匹配这些筛选条件`,
noMatchFilters: '没有机器人或群聊匹配这些筛选条件。',
clearFilters: '清除筛选',
allHidden: '所有机器人都已隐藏',
allHiddenDesc: '它们会继续运行,并保留各自的历史。',
showHidden: '显示已隐藏的机器人',
noHiddenMatch: '没有已隐藏的机器人匹配这些筛选条件。',
hiddenFromRoster: '已从名单中隐藏',
pinned: '已置顶',
needsAttention: '需要处理',
needsInput: '需要你输入',
botsAndGroups: '机器人和群聊',
botsOnly: '仅机器人',
groupsOnly: '仅群聊',
anyActivity: '任何活动',
activeNow: '正在活动',
recentlyActive: '最近活跃',
older: '更早',
gatewayRemoved: '网关已移除',
onDemand: '按需',
ready: '就绪',
statusUnknown: '状态未知',
unavailable: '不可用',
retryNow: '立即重试',
rosterUnavailable: reason => `无法获取名单:${reason}。如果网关早于 profiles.list,请更新 Hermes 并重启网关。`,
waitingForGateway: '正在等待网关连接…(远程网关可能需要几秒;会自动重试)'
},
sections: {
newSection: '新建分区',
newTitle: '新建分区',
renameTitle: '重命名分区',
nameLabel: '分区名称',
namePlaceholder: '例如:客户',
create: '创建',
rename: '重命名…',
moveUp: '上移',
moveDown: '下移',
unassigned: '未分类',
options: name => `${name} 分区选项`,
headingTip: '将机器人拖放到此处 · 双击重命名',
emptyHint: '将机器人拖到此处',
moveTo: '移动到分区',
newSectionEllipsis: '新建分区…',
removeFromSection: '移出分区',
deleted: (name, count) => (count === 0 ? `已删除“${name}”` : `已删除“${name}” — ${count} 个机器人已移至未分类`),
undo: '撤销'
},
bot: {
newTitle: '新建机器人',
editTitle: '编辑配置档案',
editMenu: '编辑…',
helpPromptPlaceholder: '这个机器人应该帮你做什么?',
descriptionHint: '留空则根据机器人的名称和描述生成。',
newChatWith: '与此机器人开新聊天',
openBotChat: '打开机器人聊天',
duplicate: '复制',
duplicateFailed: '复制失败',
deleteTitle: '删除机器人和配置档案?',
removeFromAllGroups: '从所有群组中移除',
createFirstHint: '打开机器人面板,点击“新建机器人”。',
createFailed: '暂时无法创建配置档案',
advanced: '高级',
advancedHint: '高级 — 模型、技能、工具集、SOUL.md',
advancedFailed: '高级配置失败',
openAnotherChatUnsupported: '请更新 Hermes Desktop 以打开另一个机器人聊天。',
remoteConnectionsUnsupported: '请更新 Hermes Desktop 以与其他连接上的机器人聊天。',
chatEmpty: '说点什么开始吧。',
kickoff: '你好,介绍一下你自己吧!'
},
avatar: {
classicShapes: '经典形状',
blobFromName: '斑点脸 — 根据机器人名称绘制',
unlockFollowsName: '解锁 — 面孔再次跟随机器人名称',
randomize: '随机',
tabBot: '机器人',
tabGenerate: '生成',
upload: '上传',
tabPet: '宠物',
removeImage: '移除图片,改用形状',
removeBackToShape: '移除 — 回到形状头像',
describePlaceholder: '描述你的头像…',
describeHint: '留空则根据名称/标题/描述和 agent-messaging 名册自动生成。',
matchTheName: '匹配名称',
pickPet: '选择一只宠物作为此机器人的头像。',
petLoadFailed: '无法加载该宠物 — 请换一只试试。',
imageTooLarge: '图片过大(最大 15MB)。',
generationFailed: '头像生成失败',
savedLocally: '外观已保存在本地;远程持久化失败',
savedLocallyDescriptionFailed: '外观已保存在本地;描述更新失败',
generate: '生成',
generating: '生成中…'
},
group: {
newTitle: '新建群聊',
manageDesc: '一个机器人可以加入多个群聊。成员关系会同步到每台设备。',
manageTitle: '管理群组',
settingsTitle: '群组设置',
settingsDesc: '重命名群组或设置房间图片。成员和历史都会保留。',
nameLabel: '群组名称',
searchToAdd: '搜索要添加的机器人',
searchToAddPlaceholder: '搜索要添加的机器人…',
removeFromSelection: '从选择中移除',
disbandTitle: '解散群聊?',
deleteTitle: '删除群聊?',
deleteAction: '删除',
composerPlaceholder: '说点什么 — 这个群里的每个机器人都会听到。',
attachHint: '附加文件 — 每个回应的机器人都能看到',
newThread: '新帖子',
reply: '回复',
replyInThread: '在帖子中回复',
replyInThreadPlaceholder: '在帖子中回复…',
openThread: '打开此帖子',
collapseThread: '收起帖子',
collapseThreadLabel: '收起此帖子',
activity: '活动',
noActivityYet: '本回合还没有活动。',
showActivity: '显示房间活动',
hideActivity: '隐藏房间活动',
stop: '停止',
stopHint: '停止本次运行 — 中断当前回合的成员,并暂停其余成员',
allHeldStatus: count => `全部 ${count} 个机器人已暂停`,
heldMembersStatus: members => `已暂停:${members}`,
holdReleaseHint: '提及已暂停的机器人,或发送 @all resume 以恢复它们。',
needsYourInput: '此群聊中有机器人需要你输入',
pictureGenerationFailed: '群组图片生成失败',
nameTaken: name => `已存在名为“${name}”的群聊。`,
memberCount: count => `${count} 个机器人`,
settingsHint: group => `群聊设置 — 重命名 ${group} 或设置房间图片`,
settingsLabel: group => `${group} 的群聊设置`,
disbandHint: group => `解散 ${group} 群聊`,
disbandLabel: group => `解散 ${group}`,
disbandAction: '解散',
disbanding: '正在解散…',
disbandDone: '已解散',
disbanded: group => `已解散“${group}”`,
disbandDescPrefix: '',
disbandDescSuffix: count =>
` 的分组将从 ${count} 个机器人中移除,并清空共享房间日志。机器人本身及其各群聊会话都会保留。`,
stopped: group => `已停止 ${group} — 其余轮次将保留到你恢复为止`,
removeAttachment: '移除附件',
threadFallback: '讨论串',
replyCount: replies => `${replies} 条回复`,
dropToThread: '拖放以附加到此讨论串回复',
dropToRoom: '拖放以附加 — 每个回应的机器人都能看到',
waitingForAnswer: '等待你的回答…',
memberThinking: name => `${name} 正在思考…`,
roomWorking: '房间正在处理…',
messageRoom: group => `发消息给 ${group}`,
newThreadPlaceholder: group => `在 ${group} 中开启新讨论串…(@名称指定,@everyone 全体)`,
everyoneMeta: '房间里的所有机器人',
commandApproval: '命令批准',
answerFailed: (handle, error) => `无法将回答发送给 @${handle}:${error}`,
wantsToRunCommand: handle => `@${handle} 想执行一个命令:`,
asks: handle => `@${handle} 的提问:`,
answerTo: member => `回答 @${member}`
},
tools: {
skillsHub: 'Hermes 技能中心',
filterSkills: '筛选技能…',
searchHub: '搜索技能中心(社区和常见来源)…',
noMcpServers: '未配置 MCP 服务器,目录中也没有。'
},
cron: {
filterHint:
'此配置档案中有定时任务,但没有一个标记给这个机器人。将任务命名为“[bot:<名称>] …”即可显示在这里,也可以在下方的 Cron 中查看。',
needsRosterFirst: '这个机器人需要先出现在名册中。',
staleNotice: '无法刷新定时任务。显示的是上一次获取的列表。',
readFailure: '列表可能仍然存在 — 这是一次读取失败,不是删除。',
createDesc: bot => `由 ${bot} 按计划运行的重复任务。运行结果会保存在它自己的聊天记录中。`,
instruction: '指令',
whenToRun: '运行时间',
dayOfMonth: '每月日期',
sendResultsTo: '结果发送到',
runHistoryOnly: '仅运行历史',
botChatTarget: bot => `${bot} 的聊天(机器人会回应)`,
continuity: '连续性:每次运行都能看到上次的输出(去重,从上次的地方继续)',
onceIn: when => `一次(${when})`,
everyNDays: days => `每 ${days} 天`,
everyNHours: hours => `每 ${hours} 小时`,
everyNMinutes: minutes => `每 ${minutes} 分钟`,
freqOnce: '一次,在…之后',
freqHourly: '每小时',
freqDaily: '每天',
freqWeekdays: '工作日',
freqWeekly: '每周',
freqMonthly: '每月',
freqInterval: '间隔',
freqAdvanced: '高级…',
unitMinutes: '分钟',
unitHours: '小时',
unitDays: '天',
runsOnce: (count, unit) => `从现在起 ${count} ${unit}后运行一次`,
runsHourly: '每小时整点运行',
runsDaily: time => `每天 ${time} 运行`,
runsWeekdays: time => `周一至周五 ${time} 运行`,
runsWeekly: (day, time) => `每${day} ${time} 运行`,
runsMonthly: (day, time) => `每月 ${day} 日 ${time} 运行`,
runsInterval: (count, unit) => `每 ${count} ${unit}运行`,
runsRaw: '原始计划 — every Nm/Nh/Nd 或 5 段 cron',
timesTotal: count => `,共 ${count} 次`
}
}
const zhHant: BotsMessages = {
roster: {
search: '搜尋機器人和群組聊天',
searchPlaceholder: '搜尋機器人和群組聊天…',
newBotOrGroup: '新增機器人或群組聊天',
groupChats: '群組聊天',
emptyTitle: '還沒有機器人',
emptyDesc: '建立你的第一個機器人。',
noMatchQuery: query => `沒有機器人或群組聊天符合「${query}」`,
noMatchQueryOn: (query, gateway) => `${gateway} 上沒有機器人或群組聊天符合「${query}」`,
noMatchFiltersOn: gateway => `${gateway} 上沒有機器人或群組聊天符合這些篩選條件`,
noMatchFilters: '沒有機器人或群組聊天符合這些篩選條件。',
clearFilters: '清除篩選',
allHidden: '所有機器人都已隱藏',
allHiddenDesc: '它們會繼續運作,並保留各自的歷史。',
showHidden: '顯示已隱藏的機器人',
noHiddenMatch: '沒有已隱藏的機器人符合這些篩選條件。',
hiddenFromRoster: '已從名單中隱藏',
pinned: '已釘選',
needsAttention: '需要處理',
needsInput: '需要您的輸入',
botsAndGroups: '機器人和群組聊天',
botsOnly: '僅機器人',
groupsOnly: '僅群組聊天',
anyActivity: '任何活動',
activeNow: '目前活躍',
recentlyActive: '最近活躍',
older: '更早',
gatewayRemoved: '閘道已移除',
onDemand: '隨需',
ready: '就緒',
statusUnknown: '狀態未知',
unavailable: '不可用',
retryNow: '立即重試',
rosterUnavailable: reason => `無法取得名單:${reason}。如果閘道早於 profiles.list,請更新 Hermes 並重新啟動閘道。`,
waitingForGateway: '正在等待閘道連線…(遠端閘道可能需要幾秒;會自動重試)'
},
sections: {
newSection: '新增分區',
newTitle: '新增分區',
renameTitle: '重新命名分區',
nameLabel: '分區名稱',
namePlaceholder: '例如:客戶',
create: '建立',
rename: '重新命名…',
moveUp: '上移',
moveDown: '下移',
unassigned: '未分類',
options: name => `${name} 分區選項`,
headingTip: '將機器人拖放到此處 · 雙擊重新命名',
emptyHint: '將機器人拖到此處',
moveTo: '移動到分區',
newSectionEllipsis: '新增分區…',
removeFromSection: '移出分區',
deleted: (name, count) => (count === 0 ? `已刪除「${name}」` : `已刪除「${name}」— ${count} 個機器人已移至未分類`),
undo: '復原'
},
bot: {
newTitle: '新增機器人',
editTitle: '編輯設定檔',
editMenu: '編輯…',
helpPromptPlaceholder: '這個機器人應該幫你做什麼?',
descriptionHint: '留空則依機器人的名稱和描述產生。',
newChatWith: '與此機器人開新聊天',
openBotChat: '開啟機器人聊天',
duplicate: '複製',
duplicateFailed: '複製失敗',
deleteTitle: '刪除機器人和設定檔?',
removeFromAllGroups: '從所有群組中移除',
createFirstHint: '開啟機器人面板,點「新增機器人」。',
createFailed: '暫時無法建立設定檔',
advanced: '進階',
advancedHint: '進階 — 模型、技能、工具集、SOUL.md',
advancedFailed: '進階設定失敗',
openAnotherChatUnsupported: '請更新 Hermes Desktop 以開啟另一個機器人聊天。',
remoteConnectionsUnsupported: '請更新 Hermes Desktop 以與其他連線上的機器人聊天。',
chatEmpty: '說點什麼開始吧。',
kickoff: '你好,介紹一下你自己吧!'
},
avatar: {
classicShapes: '經典形狀',
blobFromName: '斑點臉 — 依機器人名稱繪製',
unlockFollowsName: '解鎖 — 面孔再次跟隨機器人名稱',
randomize: '隨機',
tabBot: '機器人',
tabGenerate: '生成',
upload: '上傳',
tabPet: '寵物',
removeImage: '移除圖片,改用形狀',
removeBackToShape: '移除 — 回到形狀頭像',
describePlaceholder: '描述你的頭像…',
describeHint: '留空則依名稱/標題/描述與 agent-messaging 名冊自動產生。',
matchTheName: '符合名稱',
pickPet: '選擇一隻寵物作為此機器人的頭像。',
petLoadFailed: '無法載入該寵物 — 請換一隻試試。',
imageTooLarge: '圖片過大(最大 15MB)。',
generationFailed: '頭像產生失敗',
savedLocally: '外觀已儲存在本機;遠端持久化失敗',
savedLocallyDescriptionFailed: '外觀已儲存在本機;描述更新失敗',
generate: '生成',
generating: '生成中…'
},
group: {
newTitle: '新增群組聊天',
manageDesc: '一個機器人可以加入多個群組聊天。成員關係會同步到每台裝置。',
manageTitle: '管理群組',
settingsTitle: '群組設定',
settingsDesc: '重新命名群組或設定房間圖片。成員和歷史都會保留。',
nameLabel: '群組名稱',
searchToAdd: '搜尋要加入的機器人',
searchToAddPlaceholder: '搜尋要加入的機器人…',
removeFromSelection: '從選取中移除',
disbandTitle: '解散群組聊天?',
deleteTitle: '刪除群組聊天?',
deleteAction: '刪除',
composerPlaceholder: '說點什麼 — 這個群組裡的每個機器人都會聽到。',
attachHint: '附加檔案 — 每個回應的機器人都能看到',
newThread: '新討論串',
reply: '回覆',
replyInThread: '在討論串中回覆',
replyInThreadPlaceholder: '在討論串中回覆…',
openThread: '開啟此討論串',
collapseThread: '收合討論串',
collapseThreadLabel: '收合此討論串',
activity: '活動',
noActivityYet: '本回合還沒有活動。',
showActivity: '顯示房間活動',
hideActivity: '隱藏房間活動',
stop: '停止',
stopHint: '停止本次執行 — 中斷目前回合的成員,並暫停其餘成員',
allHeldStatus: count => `全部 ${count} 個機器人已暫停`,
heldMembersStatus: members => `已暫停:${members}`,
holdReleaseHint: '提及已暫停的機器人,或傳送 @all resume 以恢復它們。',
needsYourInput: '此群組聊天中有機器人需要您的輸入',
pictureGenerationFailed: '群組圖片產生失敗',
nameTaken: name => `已存在名為「${name}」的群組聊天。`,
memberCount: count => `${count} 個機器人`,
settingsHint: group => `群組設定 — 重新命名 ${group} 或設定房間圖片`,
settingsLabel: group => `${group} 的群組設定`,
disbandHint: group => `解散 ${group} 群組聊天`,
disbandLabel: group => `解散 ${group}`,
disbandAction: '解散',
disbanding: '正在解散…',
disbandDone: '已解散',
disbanded: group => `已解散「${group}」`,
disbandDescPrefix: '',
disbandDescSuffix: count =>
` 的分組將從 ${count} 個機器人中移除,並清空共享房間日誌。機器人本身及其各群組工作階段都會保留。`,
stopped: group => `已停止 ${group} — 其餘回合將保留到你恢復為止`,
removeAttachment: '移除附件',
threadFallback: '討論串',
replyCount: replies => `${replies} 則回覆`,
dropToThread: '拖放以附加到此討論串回覆',
dropToRoom: '拖放以附加 — 每個回應的機器人都能看到',
waitingForAnswer: '等待你的回答…',
memberThinking: name => `${name} 正在思考…`,
roomWorking: '房間正在處理…',
messageRoom: group => `傳訊息給 ${group}`,
newThreadPlaceholder: group => `在 ${group} 中開啟新討論串…(@名稱指定,@everyone 全體)`,
everyoneMeta: '房間裡的所有機器人',
commandApproval: '命令核准',
answerFailed: (handle, error) => `無法將回答傳送給 @${handle}:${error}`,
wantsToRunCommand: handle => `@${handle} 想執行一個命令:`,
asks: handle => `@${handle} 的提問:`,
answerTo: member => `回覆 @${member}`
},
tools: {
skillsHub: 'Hermes 技能中心',
filterSkills: '篩選技能…',
searchHub: '搜尋技能中心(社群和常見來源)…',
noMcpServers: '未設定 MCP 伺服器,目錄中也沒有。'
},
cron: {
filterHint:
'此設定檔中有排程工作,但沒有任何一個標記給這個機器人。將工作命名為「[bot:<名稱>] …」即可顯示在這裡,也可以在下方的 Cron 中查看。',
needsRosterFirst: '這個機器人需要先出現在名冊中。',
staleNotice: '無法重新整理排程工作。顯示的是上一次取得的清單。',
readFailure: '清單可能仍然存在 — 這是一次讀取失敗,不是刪除。',
createDesc: bot => `由 ${bot} 按排程執行的重複工作。執行結果會保存在它自己的聊天紀錄中。`,
instruction: '指示',
whenToRun: '執行時間',
dayOfMonth: '每月日期',
sendResultsTo: '結果傳送到',
runHistoryOnly: '僅執行紀錄',
botChatTarget: bot => `${bot} 的聊天(機器人會回應)`,
continuity: '連續性:每次執行都能看到上次的輸出(去重,從上次的地方繼續)',
onceIn: when => `一次(${when})`,
everyNDays: days => `每 ${days} 天`,
everyNHours: hours => `每 ${hours} 小時`,
everyNMinutes: minutes => `每 ${minutes} 分鐘`,
freqOnce: '一次,在…之後',
freqHourly: '每小時',
freqDaily: '每天',
freqWeekdays: '工作日',
freqWeekly: '每週',
freqMonthly: '每月',
freqInterval: '間隔',
freqAdvanced: '進階…',
unitMinutes: '分鐘',
unitHours: '小時',
unitDays: '天',
runsOnce: (count, unit) => `從現在起 ${count} ${unit}後執行一次`,
runsHourly: '每小時整點執行',
runsDaily: time => `每天 ${time} 執行`,
runsWeekdays: time => `週一至週五 ${time} 執行`,
runsWeekly: (day, time) => `每${day} ${time} 執行`,
runsMonthly: (day, time) => `每月 ${day} 日 ${time} 執行`,
runsInterval: (count, unit) => `每 ${count} ${unit}執行`,
runsRaw: '原始排程 — every Nm/Nh/Nd 或 5 段 cron',
timesTotal: count => `,共 ${count} 次`
}
}
/** Registered via `ctx.i18n.register` at plugin load (disposer tracked). */
export const BOTS_LOCALES: PluginLocaleBundles = { en, ja, zh, 'zh-hant': zhHant }
// Bind the message SHAPE to a plugin translator: string leaves resolve now,
// function leaves forward their args through t(path, …).
type Bound<T> = {
[K in keyof T]: T[K] extends (...args: infer A) => string
? (...args: A) => string
: T[K] extends object
? Bound<T[K]>
: string
}
function bind<T extends object>(t: PluginTranslate, template: T, prefix = ''): Bound<T> {
const out = {} as Record<string, unknown>
for (const [key, value] of Object.entries(template)) {
const path = prefix ? `${prefix}.${key}` : key
out[key] =
typeof value === 'function'
? (...args: unknown[]) => t(path, ...args)
: value && typeof value === 'object'
? bind(t, value as object, path)
: t(path)
}
return out as Bound<T>
}
export type BotsText = Bound<BotsMessages>
/** The Bot Mode strings for the active locale — one hook every component reads. */
export function useBots(): BotsText {
const t = usePluginI18n('hermes-bots')
return useMemo(() => bind(t, en), [t])
}
/** Resolve a dotted path against the English bundle — the floor for a read
* that beats `ctx.i18n` into existence, so an unresolved key never ships as
* the literal `cron.runsHourly`. */
function english(key: string, ...args: unknown[]): string {
const leaf = key.split('.').reduce<unknown>((node, part) => (node as Record<string, unknown>)?.[part], en)
return typeof leaf === 'function' ? (leaf as (...a: unknown[]) => string)(...args) : String(leaf ?? key)
}
let bound: { text: BotsText; translate: PluginTranslate } | null = null
/** `useBots` for the module-level functions a hook can't reach — the schedule
* summarizers and label helpers that render inside components but aren't
* components. Non-reactive on its own; every caller is invoked during a
* render that a core `useI18n()` already subscribes to, so a locale switch
* still repaints. Cached on translator identity: `bind` walks the whole tree,
* and these run per row. */
export function botsText(): BotsText {
const translate = getPluginCtx()?.i18n?.t ?? english
if (bound?.translate !== translate) {
bound = { text: bind(translate, en), translate }
}
return bound.text
}