i18n.ts 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295
  1. import type { ChatDepth } from "./chat-depth.js";
  2. export type TuiLocale = "zh-CN" | "en";
  3. export interface TuiCopy {
  4. readonly locale: TuiLocale;
  5. readonly labels: {
  6. readonly project: string;
  7. readonly book: string;
  8. readonly depth: string;
  9. readonly session: string;
  10. readonly messageCount: (count: number) => string;
  11. readonly stage: string;
  12. readonly mode: string;
  13. readonly model: string;
  14. readonly error: string;
  15. readonly recent: string;
  16. readonly pending: string;
  17. readonly ready: string;
  18. readonly none: string;
  19. readonly notConfigured: string;
  20. readonly unknown: string;
  21. };
  22. readonly modeLabels: Record<string, string>;
  23. readonly composer: {
  24. readonly placeholder: string;
  25. readonly emptyConversation: string;
  26. readonly helper: string;
  27. readonly submitting: string;
  28. readonly failed: string;
  29. readonly ready: string;
  30. };
  31. readonly notes: {
  32. readonly help: string;
  33. readonly status: (stage: string, mode: string) => string;
  34. readonly config: string;
  35. readonly depthSet: (depthLabel: string) => string;
  36. readonly noLlmConfig: string;
  37. readonly setupProvider: string;
  38. readonly toolInitFailed: (message: string) => string;
  39. readonly toolInitHint: string;
  40. };
  41. readonly roles: {
  42. readonly user: string;
  43. readonly assistant: string;
  44. readonly system: string;
  45. };
  46. readonly activity: Record<"thinking" | "checking" | "writing" | "reviewing" | "updating", string>;
  47. readonly depthLabels: Record<ChatDepth, string>;
  48. readonly results: {
  49. readonly modeSwitched: (mode: string) => string;
  50. readonly booksListed: string;
  51. readonly activeBook: (bookId: string) => string;
  52. readonly completed: (intent: string) => string;
  53. readonly intentLabels: Partial<Record<string, string>>;
  54. };
  55. }
  56. const ZH_CN: TuiCopy = {
  57. locale: "zh-CN",
  58. labels: {
  59. project: "项目",
  60. book: "作品",
  61. depth: "深度",
  62. session: "会话",
  63. messageCount: (count) => `${count} 条消息`,
  64. stage: "阶段",
  65. mode: "模式",
  66. model: "模型",
  67. error: "错误",
  68. recent: "最近",
  69. pending: "待确认",
  70. ready: "就绪",
  71. none: "无",
  72. notConfigured: "未配置",
  73. unknown: "未知",
  74. },
  75. modeLabels: {
  76. auto: "自动",
  77. semi: "半自动",
  78. manual: "手动",
  79. },
  80. composer: {
  81. placeholder: "告诉 InkOS 要写什么、修改什么,或解释什么…",
  82. emptyConversation: "先告诉 InkOS 你要做什么。",
  83. helper: "回车发送 • /help • /status • /clear • /config • /depth • /quit",
  84. submitting: "处理中…",
  85. failed: "上次请求失败",
  86. ready: "就绪",
  87. },
  88. notes: {
  89. help: "可用命令:/help、/status、/clear、/config、/depth、/quit。也支持直接输入自然语言。",
  90. status: (stage, mode) => `当前状态:${stage}(${mode})。`,
  91. config: "当前 Ink 仪表盘里还不支持交互式 /config。请使用 inkos config set-global。",
  92. depthSet: (depthLabel) => `思考深度已切换为 ${depthLabel}。`,
  93. noLlmConfig: "未发现 LLM 配置。",
  94. setupProvider: "先配置 API 提供方。",
  95. toolInitFailed: (message) => `初始化 TUI 工具失败:${message}`,
  96. toolInitHint: "请检查 .env,或运行:inkos config set-global",
  97. },
  98. roles: {
  99. user: "你",
  100. assistant: "InkOS",
  101. system: "系统",
  102. },
  103. activity: {
  104. thinking: "思考中",
  105. checking: "检查中",
  106. writing: "写作中",
  107. reviewing: "审阅中",
  108. updating: "更新中",
  109. },
  110. depthLabels: {
  111. light: "轻量",
  112. normal: "标准",
  113. deep: "深入",
  114. },
  115. results: {
  116. modeSwitched: (mode) => `已切换到 ${mode} 模式。`,
  117. booksListed: "已列出作品。",
  118. activeBook: (bookId) => `当前作品:${bookId}`,
  119. completed: (intent) => `已完成 ${intent}`,
  120. intentLabels: {
  121. write_next: "已写完下一章",
  122. revise_chapter: "已修订章节",
  123. rewrite_chapter: "已重写章节",
  124. update_focus: "已更新焦点",
  125. explain_status: "状态说明",
  126. explain_failure: "失败说明",
  127. pause_book: "已暂停作品",
  128. rename_entity: "已重命名实体",
  129. patch_chapter_text: "已修补正文",
  130. edit_truth: "已更新真相文件",
  131. },
  132. },
  133. };
  134. const EN: TuiCopy = {
  135. locale: "en",
  136. labels: {
  137. project: "Project",
  138. book: "Book",
  139. depth: "Depth",
  140. session: "Session",
  141. messageCount: (count) => `${count} msgs`,
  142. stage: "Stage",
  143. mode: "Mode",
  144. model: "Model",
  145. error: "Error",
  146. recent: "Recent",
  147. pending: "Pending",
  148. ready: "Ready",
  149. none: "none",
  150. notConfigured: "not configured",
  151. unknown: "unknown",
  152. },
  153. modeLabels: {
  154. auto: "auto",
  155. semi: "semi",
  156. manual: "manual",
  157. },
  158. composer: {
  159. placeholder: "Ask InkOS to write, revise, or explain…",
  160. emptyConversation: "Start by asking InkOS what to do.",
  161. helper: "Enter to send • /help • /status • /clear • /config • /depth • /quit",
  162. submitting: "Submitting…",
  163. failed: "Last request failed",
  164. ready: "Ready",
  165. },
  166. notes: {
  167. help: "Commands: /help, /status, /clear, /config, /depth, /quit. Natural language still works.",
  168. status: (stage, mode) => `Status: ${stage} (${mode}).`,
  169. config: "Interactive /config is not available inside the Ink dashboard yet. Use inkos config set-global.",
  170. depthSet: (depthLabel) => `Thinking depth set to ${depthLabel}.`,
  171. noLlmConfig: "No LLM configuration found.",
  172. setupProvider: "Let's set up your API provider first.",
  173. toolInitFailed: (message) => `Failed to initialize TUI tools: ${message}`,
  174. toolInitHint: "Check your .env or run: inkos config set-global",
  175. },
  176. roles: {
  177. user: "You",
  178. assistant: "InkOS",
  179. system: "System",
  180. },
  181. activity: {
  182. thinking: "thinking",
  183. checking: "checking",
  184. writing: "writing",
  185. reviewing: "reviewing",
  186. updating: "updating",
  187. },
  188. depthLabels: {
  189. light: "light",
  190. normal: "normal",
  191. deep: "deep",
  192. },
  193. results: {
  194. modeSwitched: (mode) => `Mode switched to ${mode}.`,
  195. booksListed: "Books listed.",
  196. activeBook: (bookId) => `Active book: ${bookId}`,
  197. completed: (intent) => `Completed ${intent}`,
  198. intentLabels: {
  199. write_next: "Chapter written",
  200. revise_chapter: "Chapter revised",
  201. rewrite_chapter: "Chapter rewritten",
  202. update_focus: "Focus updated",
  203. explain_status: "Status",
  204. explain_failure: "Explanation",
  205. pause_book: "Book paused",
  206. rename_entity: "Entity renamed",
  207. patch_chapter_text: "Text patched",
  208. edit_truth: "Truth file updated",
  209. },
  210. },
  211. };
  212. export function resolveTuiLocale(
  213. env: NodeJS.ProcessEnv = process.env,
  214. preferredLanguage?: string,
  215. ): TuiLocale {
  216. const requested = normalizeLocale(env.INKOS_TUI_LOCALE ?? env.INKOS_LOCALE);
  217. if (requested) {
  218. return requested;
  219. }
  220. const preferred = normalizeLocale(preferredLanguage);
  221. if (preferred) {
  222. return preferred;
  223. }
  224. const detected = normalizeLocale(env.LC_ALL ?? env.LC_MESSAGES ?? env.LANG);
  225. return detected ?? "zh-CN";
  226. }
  227. export function getTuiCopy(locale: TuiLocale): TuiCopy {
  228. return locale === "en" ? EN : ZH_CN;
  229. }
  230. export function normalizeStageLabel(label: string, copy: TuiCopy): string {
  231. const normalized = label.trim().toLowerCase();
  232. if (!normalized) {
  233. return label;
  234. }
  235. const replacements: Array<[RegExp, string]> = [
  236. [/^thinking\b/i, copy.activity.thinking],
  237. [/^checking\b/i, copy.activity.checking],
  238. [/^writing\b/i, copy.activity.writing],
  239. [/^reviewing\b/i, copy.activity.reviewing],
  240. [/^updating\b/i, copy.activity.updating],
  241. [/^completed\b/i, copy.locale === "en" ? "completed" : "已完成"],
  242. [/^failed\b/i, copy.locale === "en" ? "failed" : "失败"],
  243. [/^blocked\b/i, copy.locale === "en" ? "blocked" : "已阻塞"],
  244. [/^waiting_human\b/i, copy.locale === "en" ? "waiting for your decision" : "等待你的决定"],
  245. [/^paused by user\b/i, copy.locale === "en" ? "paused by user" : "已由用户暂停"],
  246. [/^ready to continue\b/i, copy.locale === "en" ? "ready to continue" : "可继续执行"],
  247. ];
  248. for (const [pattern, value] of replacements) {
  249. if (pattern.test(label)) {
  250. return copy.locale === "en" ? label : value;
  251. }
  252. }
  253. if (normalized === "idle") {
  254. return copy.labels.ready;
  255. }
  256. return label;
  257. }
  258. export function formatModeLabel(mode: string, copy: TuiCopy): string {
  259. return copy.modeLabels[mode] ?? mode;
  260. }
  261. function normalizeLocale(value: string | undefined): TuiLocale | undefined {
  262. if (!value) {
  263. return undefined;
  264. }
  265. const normalized = value.trim().toLowerCase();
  266. if (!normalized || normalized === "auto") {
  267. return undefined;
  268. }
  269. if (normalized.startsWith("zh")) {
  270. return "zh-CN";
  271. }
  272. if (normalized.startsWith("en")) {
  273. return "en";
  274. }
  275. return undefined;
  276. }