1194 lines
53 KiB
TypeScript
1194 lines
53 KiB
TypeScript
/**
|
||
* 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
|
||
}
|