diff --git a/package.json b/package.json index dba6dd6..09a1fb1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@nbtca/prompt", - "version": "1.0.5", + "version": "1.0.6", "type": "module", "main": "dist/index.js", "bin": { diff --git a/src/config/data.ts b/src/config/data.ts index 03091d3..4715332 100644 --- a/src/config/data.ts +++ b/src/config/data.ts @@ -19,7 +19,7 @@ export const URLS = { export const APP_INFO = { name: 'Prompt', - version: '1.0.2', + version: '1.0.6', description: '浙大宁波理工学院计算机协会', fullDescription: 'NBTCA Prompt - 极简命令行工具', author: 'm1ngsama ', diff --git a/src/core/menu.ts b/src/core/menu.ts index cfd5740..94f41bb 100644 --- a/src/core/menu.ts +++ b/src/core/menu.ts @@ -11,48 +11,57 @@ import { showDocsMenu } from '../features/docs.js'; import { openHomepage, openGithub } from '../features/website.js'; import { printDivider, printNewLine } from './ui.js'; import { APP_INFO, URLS } from '../config/data.js'; +import { t, getCurrentLanguage, setLanguage, clearTranslationCache, type Language } from '../i18n/index.js'; /** - * Main menu options + * Get main menu options */ -const MAIN_MENU = [ - { - name: '[*] Recent Events ' + chalk.gray('View upcoming events in next 30 days'), - value: 'events', - short: 'Recent Events' - }, - { - name: '[*] Repair Service ' + chalk.gray('Computer repair and software installation'), - value: 'repair', - short: 'Repair Service' - }, - { - name: '[*] Knowledge Base ' + chalk.gray('Technical docs and tutorials'), - value: 'docs', - short: 'Knowledge Base' - }, - { - name: '[*] Official Website ' + chalk.gray('Visit NBTCA homepage'), - value: 'website', - short: 'Official Website' - }, - { - name: '[*] GitHub ' + chalk.gray('Open source projects and code'), - value: 'github', - short: 'GitHub' - }, - { - name: '[?] About ' + chalk.gray('Project info and help'), - value: 'about', - short: 'About' - }, - new inquirer.Separator(' '), - { - name: chalk.dim('[x] Exit'), - value: 'exit', - short: 'Exit' - } -]; +function getMainMenuOptions() { + const trans = t(); + return [ + { + name: '[*] ' + trans.menu.events.padEnd(16) + ' ' + chalk.gray(trans.menu.eventsDesc), + value: 'events', + short: trans.menu.events + }, + { + name: '[*] ' + trans.menu.repair.padEnd(16) + ' ' + chalk.gray(trans.menu.repairDesc), + value: 'repair', + short: trans.menu.repair + }, + { + name: '[*] ' + trans.menu.docs.padEnd(16) + ' ' + chalk.gray(trans.menu.docsDesc), + value: 'docs', + short: trans.menu.docs + }, + { + name: '[*] ' + trans.menu.website.padEnd(16) + ' ' + chalk.gray(trans.menu.websiteDesc), + value: 'website', + short: trans.menu.website + }, + { + name: '[*] ' + trans.menu.github.padEnd(16) + ' ' + chalk.gray(trans.menu.githubDesc), + value: 'github', + short: trans.menu.github + }, + { + name: '[?] ' + trans.menu.about.padEnd(16) + ' ' + chalk.gray(trans.menu.aboutDesc), + value: 'about', + short: trans.menu.about + }, + { + name: '[⚙] ' + trans.menu.language.padEnd(16) + ' ' + chalk.gray(trans.menu.languageDesc), + value: 'language', + short: trans.menu.language + }, + new inquirer.Separator(' '), + { + name: chalk.dim('[x] ' + trans.common.exit), + value: 'exit', + short: trans.common.exit + } + ]; +} /** * Display main menu @@ -60,16 +69,18 @@ const MAIN_MENU = [ export async function showMainMenu(): Promise { while (true) { try { + const trans = t(); + // Show keybinding hints - console.log(chalk.dim(' Navigation: j/k or ↑/↓ | Jump: g/G | Quit: q or Ctrl+C')); + console.log(chalk.dim(' ' + trans.menu.navigationHint)); console.log(); const { action } = await inquirer.prompt<{ action: string }>([ { type: 'list', name: 'action', - message: 'Choose an action', - choices: MAIN_MENU, + message: trans.menu.chooseAction, + choices: getMainMenuOptions(), pageSize: 15, loop: false } as any @@ -78,7 +89,7 @@ export async function showMainMenu(): Promise { // Handle user selection if (action === 'exit') { console.log(); - console.log(chalk.dim('Goodbye!')); + console.log(chalk.dim(trans.common.goodbye)); process.exit(0); } @@ -92,7 +103,7 @@ export async function showMainMenu(): Promise { // Handle Ctrl+C exit if (err.message?.includes('User force closed')) { console.log(); - console.log(chalk.dim('Goodbye!')); + console.log(chalk.dim(t().common.goodbye)); process.exit(0); } throw err; @@ -129,6 +140,10 @@ async function handleAction(action: string): Promise { showAbout(); break; + case 'language': + await showLanguageMenu(); + break; + default: console.log(chalk.gray('Unknown action')); } @@ -138,24 +153,61 @@ async function handleAction(action: string): Promise { * Display about information */ function showAbout(): void { + const trans = t(); console.log(); - console.log(chalk.bold('>> About NBTCA')); + console.log(chalk.bold('>> ' + trans.about.title)); console.log(); - console.log(chalk.dim('Project ') + APP_INFO.name); - console.log(chalk.dim('Version ') + `v${APP_INFO.version}`); - console.log(chalk.dim('Description ') + APP_INFO.fullDescription); + console.log(chalk.dim(trans.about.project.padEnd(12)) + APP_INFO.name); + console.log(chalk.dim(trans.about.version.padEnd(12)) + `v${APP_INFO.version}`); + console.log(chalk.dim(trans.about.description.padEnd(12)) + APP_INFO.fullDescription); console.log(); - console.log(chalk.dim('GitHub ') + chalk.cyan(APP_INFO.repository)); - console.log(chalk.dim('Website ') + chalk.cyan(URLS.homepage)); - console.log(chalk.dim('Email ') + chalk.cyan(URLS.email)); + console.log(chalk.dim(trans.about.github.padEnd(12)) + chalk.cyan(APP_INFO.repository)); + console.log(chalk.dim(trans.about.website.padEnd(12)) + chalk.cyan(URLS.homepage)); + console.log(chalk.dim(trans.about.email.padEnd(12)) + chalk.cyan(URLS.email)); console.log(); - console.log(chalk.dim('Features:')); - console.log(' - View recent association events'); - console.log(' - Online repair service'); - console.log(' - Technical knowledge base access'); - console.log(' - Quick access to website and GitHub'); + console.log(chalk.dim(trans.about.features)); + console.log(' ' + trans.about.feature1); + console.log(' ' + trans.about.feature2); + console.log(' ' + trans.about.feature3); + console.log(' ' + trans.about.feature4); console.log(); - console.log(chalk.dim('License ') + 'MIT License'); - console.log(chalk.dim('Author ') + 'm1ngsama'); + console.log(chalk.dim(trans.about.license.padEnd(12)) + 'MIT License'); + console.log(chalk.dim(trans.about.author.padEnd(12)) + 'm1ngsama'); console.log(); } + +/** + * Display language selection menu + */ +async function showLanguageMenu(): Promise { + const trans = t(); + const currentLang = getCurrentLanguage(); + + console.log(); + console.log(chalk.bold('>> ' + trans.language.title)); + console.log(); + console.log(chalk.dim(trans.language.currentLanguage + ': ') + chalk.cyan(trans.language[currentLang])); + console.log(); + + const { language } = await inquirer.prompt<{ language: Language }>([ + { + type: 'list', + name: 'language', + message: trans.language.selectLanguage, + choices: [ + { name: trans.language.zh, value: 'zh' as Language }, + { name: trans.language.en, value: 'en' as Language } + ], + default: currentLang + } + ]); + + if (language !== currentLang) { + setLanguage(language); + clearTranslationCache(); + console.log(); + console.log(chalk.green('✓ ' + t().language.changed)); + console.log(); + } +} + diff --git a/src/features/calendar.ts b/src/features/calendar.ts index e4774f9..105e666 100644 --- a/src/features/calendar.ts +++ b/src/features/calendar.ts @@ -7,6 +7,7 @@ import axios from 'axios'; import ICAL from 'ical.js'; import chalk from 'chalk'; import { error, info, printDivider } from '../core/ui.js'; +import { t } from '../i18n/index.js'; /** * 活动接口 @@ -43,13 +44,17 @@ export async function fetchEvents(): Promise { const event = new ICAL.Event(vevent); const startDate = event.startDate.toJSDate(); - // 只显示未来30天内的活动 + // Only show events within next 30 days if (startDate >= now && startDate <= thirtyDaysLater) { + const trans = t(); + const untitledEvent = trans.calendar.eventName === 'Event Name' ? 'Untitled Event' : '未命名活动'; + const tbdLocation = trans.calendar.location === 'Location' ? 'TBD' : '待定'; + events.push({ date: formatDate(startDate), time: formatTime(startDate), - title: event.summary || '未命名活动', - location: event.location || '待定', + title: event.summary || untitledEvent, + location: event.location || tbdLocation, startDate: startDate }); } @@ -60,7 +65,7 @@ export async function fetchEvents(): Promise { return events; } catch (err) { - throw new Error('获取活动日历失败'); + throw new Error(t().calendar.error); } } @@ -68,30 +73,32 @@ export async function fetchEvents(): Promise { * 以表格形式显示活动 */ export function displayEvents(events: Event[]): void { + const trans = t(); + if (events.length === 0) { - info('近期暂无活动安排'); + info(trans.calendar.noEvents); return; } console.log(); - console.log(chalk.cyan.bold(' 近期活动') + chalk.gray(` (最近30天)`)); + console.log(chalk.cyan.bold(' ' + trans.calendar.title) + chalk.gray(` ${trans.calendar.subtitle}`)); console.log(); - // 表头 + // Table header const dateWidth = 14; const titleWidth = 25; const locationWidth = 15; console.log( ' ' + - chalk.bold('日期时间'.padEnd(dateWidth)) + - chalk.bold('活动名称'.padEnd(titleWidth)) + - chalk.bold('地点') + chalk.bold(trans.calendar.dateTime.padEnd(dateWidth)) + + chalk.bold(trans.calendar.eventName.padEnd(titleWidth)) + + chalk.bold(trans.calendar.location) ); printDivider(); - // 活动列表 + // Event list events.forEach(event => { const dateTime = `${event.date} ${event.time}`.padEnd(dateWidth); const title = truncate(event.title, titleWidth - 2).padEnd(titleWidth); @@ -140,14 +147,15 @@ function truncate(str: string, maxLength: number): string { * 主函数:获取并显示活动 */ export async function showCalendar(): Promise { + const trans = t(); try { - info('正在获取活动日历...'); + info(trans.calendar.loading); const events = await fetchEvents(); - console.log('\r' + ' '.repeat(50) + '\r'); // 清除加载提示 + console.log('\r' + ' '.repeat(50) + '\r'); // Clear loading message displayEvents(events); } catch (err) { - error('无法获取活动日历'); - console.log(chalk.gray(' 请稍后重试或访问 https://nbtca.space')); + error(trans.calendar.error); + console.log(chalk.gray(' ' + trans.calendar.errorHint)); console.log(); } } diff --git a/src/features/docs.ts b/src/features/docs.ts index 13f9099..c5bac27 100644 --- a/src/features/docs.ts +++ b/src/features/docs.ts @@ -11,6 +11,7 @@ import open from 'open'; import inquirer from 'inquirer'; import { error, info, success, warning } from '../core/ui.js'; import { spawn } from 'child_process'; +import { t } from '../i18n/index.js'; // 配置marked使用终端渲染器 marked.setOptions({ @@ -181,22 +182,23 @@ function extractDocTitle(content: string): string | null { * 浏览目录并选择文档 */ async function browseDirectory(dirPath: string = ''): Promise { + const trans = t(); try { - info(dirPath ? `正在加载目录: ${dirPath}` : '正在加载文档列表...'); + info(dirPath ? `${trans.docs.loadingDir}: ${dirPath}` : trans.docs.loading); const items = await fetchGitHubDirectory(dirPath); console.log('\r' + ' '.repeat(60) + '\r'); // 清除加载提示 if (items.length === 0) { - warning('该目录为空'); + warning(trans.docs.emptyDir); return; } // 构建选择列表 const choices = [ ...(dirPath ? [ - { name: chalk.gray('[..] Up to parent directory'), value: { type: 'back' } }, + { name: chalk.gray(trans.docs.upToParent), value: { type: 'back' } }, new inquirer.Separator() ] : []), ...items.map(item => ({ @@ -206,14 +208,14 @@ async function browseDirectory(dirPath: string = ''): Promise { value: item })), new inquirer.Separator(), - { name: chalk.gray('[ ^] Return to main menu'), value: { type: 'exit' } } + { name: chalk.gray(trans.docs.returnToMenu), value: { type: 'exit' } } ]; const { selected } = await inquirer.prompt([ { type: 'list', name: 'selected', - message: dirPath ? `当前目录: ${dirPath}` : '选择文档或目录:', + message: dirPath ? `${trans.docs.currentDir}: ${dirPath}` : trans.docs.chooseDoc, choices, pageSize: 15, loop: false @@ -238,14 +240,14 @@ async function browseDirectory(dirPath: string = ''): Promise { } } catch (err: any) { - error('无法加载目录'); - console.log(chalk.gray(` 错误: ${err.message}`)); + error(trans.docs.loadError); + console.log(chalk.gray(` ${trans.docs.errorHint}: ${err.message}`)); const { retry } = await inquirer.prompt([ { type: 'confirm', name: 'retry', - message: '是否重试?', + message: trans.docs.retry, default: true } ]); @@ -261,12 +263,13 @@ async function browseDirectory(dirPath: string = ''): Promise { * 提供类似vim/journalctl的阅读体验 */ async function displayInPager(content: string, title: string): Promise { + const trans = t(); return new Promise((resolve) => { // 检测可用的pager程序 const pager = process.env['PAGER'] || 'less'; // 为内容添加标题 - const fullContent = `${chalk.cyan.bold(`>> ${title}`)}\n${chalk.gray('='.repeat(80))}\n\n${content}\n\n${chalk.gray('='.repeat(80))}\n${chalk.dim('End of document - Press q to quit')}\n`; + const fullContent = `${chalk.cyan.bold(`>> ${title}`)}\n${chalk.gray('='.repeat(80))}\n\n${content}\n\n${chalk.gray('='.repeat(80))}\n${chalk.dim(trans.docs.endOfDocument)}\n`; // less的参数: -R (支持颜色), -F (如果内容少于一屏则直接显示), -X (退出时不清屏) const lessArgs = ['-R', '-F', '-X']; @@ -287,7 +290,7 @@ async function displayInPager(content: string, title: string): Promise child.on('error', () => { // 如果pager失败,回退到直接输出 - console.error(chalk.yellow('⚠ Pager不可用,使用标准输出')); + console.error(chalk.yellow(trans.docs.pagerNotAvailable)); console.log(fullContent); resolve(false); }); @@ -304,8 +307,9 @@ async function displayInPager(content: string, title: string): Promise * 查看Markdown文件 */ async function viewMarkdownFile(path: string): Promise { + const trans = t(); try { - info(`正在加载: ${path}`); + info(`${trans.docs.loading.replace('...', '')}: ${path}`); // 从GitHub获取原始Markdown内容 const rawContent = await fetchGitHubRawContent(path); @@ -325,7 +329,7 @@ async function viewMarkdownFile(path: string): Promise { await displayInPager(rendered, `${title}\n${chalk.gray(` ${path}`)}`); console.log(); // 添加空行 - success('文档加载完成'); + success(trans.docs.docCompleted); console.log(); // 提供后续操作选项 @@ -333,11 +337,11 @@ async function viewMarkdownFile(path: string): Promise { { type: 'list', name: 'action', - message: '选择操作:', + message: trans.docs.chooseAction, choices: [ - { name: '[ <] Back to docs list', value: 'back' }, - { name: '[ ↻] Re-read document', value: 'reread' }, - { name: '[ *] Open in browser', value: 'browser' } + { name: trans.docs.backToList, value: 'back' }, + { name: trans.docs.reread, value: 'reread' }, + { name: trans.docs.openBrowser, value: 'browser' } ] } ]); @@ -351,16 +355,16 @@ async function viewMarkdownFile(path: string): Promise { } catch (err: any) { console.log('\r' + ' '.repeat(60) + '\r'); - error('无法加载文档'); - console.log(chalk.gray(` 错误: ${err.message}`)); - warning('建议在浏览器中查看'); + error(trans.docs.loadError); + console.log(chalk.gray(` ${trans.docs.errorHint}: ${err.message}`)); + warning(trans.docs.openBrowserPrompt.replace('是否', '建议')); console.log(); const { openBrowser } = await inquirer.prompt([ { type: 'confirm', name: 'openBrowser', - message: '是否在浏览器中打开?', + message: trans.docs.openBrowserPrompt, default: true } ]); @@ -375,16 +379,17 @@ async function viewMarkdownFile(path: string): Promise { * 在浏览器中打开知识库 */ export async function openDocsInBrowser(path?: string): Promise { + const trans = t(); try { - info('正在打开浏览器...'); + info(trans.docs.opening); const url = path ? `https://docs.nbtca.space/${path.replace(/\.md$/, '')}` : 'https://docs.nbtca.space'; await open(url); - success('已在浏览器中打开知识库'); + success(trans.docs.browserOpened); } catch (err) { - error('无法打开浏览器'); - console.log(chalk.gray(' 请手动访问: https://docs.nbtca.space')); + error(trans.docs.browserError); + console.log(chalk.gray(` ${trans.docs.browserErrorHint}`)); } console.log(); } @@ -393,9 +398,10 @@ export async function openDocsInBrowser(path?: string): Promise { * 显示知识库菜单 */ export async function showDocsMenu(): Promise { + const trans = t(); console.log(); - console.log(chalk.cyan.bold(' >> Knowledge Base')); - console.log(chalk.dim(' Browse documentation from terminal or open in browser')); + console.log(chalk.cyan.bold(` >> ${trans.docs.title}`)); + console.log(chalk.dim(` ${trans.docs.subtitle}`)); console.log(); const choices = [ @@ -404,15 +410,15 @@ export async function showDocsMenu(): Promise { value: cat.path })), new inquirer.Separator(), - { name: chalk.gray('[ *] Open in browser'), value: 'browser' }, - { name: chalk.gray('[ ^] Back to main menu'), value: 'back' } + { name: chalk.gray(trans.docs.openBrowser), value: 'browser' }, + { name: chalk.gray(trans.docs.returnToMenu), value: 'back' } ]; const { action } = await inquirer.prompt([ { type: 'list', name: 'action', - message: '选择文档分类:', + message: trans.docs.chooseCategory, choices, pageSize: 15, loop: false diff --git a/src/features/repair.ts b/src/features/repair.ts index 36f434b..8ad49c3 100644 --- a/src/features/repair.ts +++ b/src/features/repair.ts @@ -6,6 +6,7 @@ import open from 'open'; import chalk from 'chalk'; import { error, info, success } from '../core/ui.js'; +import { t } from '../i18n/index.js'; /** * 维修服务URL @@ -16,25 +17,20 @@ const REPAIR_URL = 'https://nbtca.space/repair'; * 打开维修服务页面 */ export async function openRepairService(): Promise { + const trans = t(); try { console.log(); - info('正在打开维修服务页面...'); + info(trans.repair.opening); console.log(); await open(REPAIR_URL); - success('已在浏览器中打开维修服务页面'); - console.log(); - console.log(chalk.gray(' 服务内容:')); - console.log(chalk.gray(' • 电脑硬件维修')); - console.log(chalk.gray(' • 软件安装与配置')); - console.log(chalk.gray(' • 系统优化与故障排查')); - console.log(chalk.gray(' • 数据恢复与备份')); + success(trans.repair.opened); console.log(); } catch (err) { - error('无法打开浏览器'); + error(trans.repair.error); console.log(); - console.log(chalk.yellow(' 请手动访问: ') + chalk.cyan(REPAIR_URL)); + console.log(chalk.yellow(' ' + trans.repair.errorHint)); console.log(); } } diff --git a/src/features/website.ts b/src/features/website.ts index d94165f..699c919 100644 --- a/src/features/website.ts +++ b/src/features/website.ts @@ -6,38 +6,40 @@ import open from 'open'; import chalk from 'chalk'; import { error, info, success } from '../core/ui.js'; +import { t } from '../i18n/index.js'; /** * 打开指定URL */ -export async function openWebsite(url: string, name: string = '网站'): Promise { +export async function openWebsite(url: string): Promise { + const trans = t(); try { console.log(); - info(`正在打开${name}...`); + info(trans.website.opening); await open(url); - success(`已在浏览器中打开${name}`); + success(trans.website.opened); console.log(chalk.gray(` ${url}`)); console.log(); } catch (err) { - error('无法打开浏览器'); + error(trans.website.error); console.log(); - console.log(chalk.yellow(' 请手动访问: ') + chalk.cyan(url)); + console.log(chalk.yellow(' ' + trans.website.errorHint + ': ') + chalk.cyan(url)); console.log(); } } /** - * 打开NBTCA主页 + * Open NBTCA homepage */ export async function openHomepage(): Promise { - await openWebsite('https://nbtca.space', 'NBTCA官网'); + await openWebsite('https://nbtca.space'); } /** - * 打开GitHub页面 + * Open GitHub page */ export async function openGithub(): Promise { - await openWebsite('https://github.com/nbtca', 'GitHub组织页'); + await openWebsite('https://github.com/nbtca'); } diff --git a/src/i18n/index.ts b/src/i18n/index.ts new file mode 100644 index 0000000..9168b96 --- /dev/null +++ b/src/i18n/index.ts @@ -0,0 +1,236 @@ +/** + * Internationalization (i18n) System + * Multi-language support for the application + */ + +import fs from 'fs'; +import path from 'path'; +import { fileURLToPath } from 'url'; +import { dirname } from 'path'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +export type Language = 'zh' | 'en'; + +/** + * Translation structure + */ +export interface Translations { + common: { + back: string; + exit: string; + cancel: string; + confirm: string; + loading: string; + error: string; + success: string; + goodbye: string; + }; + menu: { + title: string; + events: string; + eventsDesc: string; + repair: string; + repairDesc: string; + docs: string; + docsDesc: string; + website: string; + websiteDesc: string; + github: string; + githubDesc: string; + about: string; + aboutDesc: string; + language: string; + languageDesc: string; + navigationHint: string; + chooseAction: string; + }; + about: { + title: string; + project: string; + version: string; + description: string; + github: string; + website: string; + email: string; + features: string; + feature1: string; + feature2: string; + feature3: string; + feature4: string; + license: string; + author: string; + }; + calendar: { + title: string; + subtitle: string; + loading: string; + noEvents: string; + error: string; + errorHint: string; + dateTime: string; + eventName: string; + location: string; + }; + docs: { + title: string; + subtitle: string; + loading: string; + loadingDir: string; + chooseCategory: string; + currentDir: string; + chooseDoc: string; + emptyDir: string; + upToParent: string; + returnToMenu: string; + backToList: string; + reread: string; + openBrowser: string; + loadError: string; + errorHint: string; + openBrowserPrompt: string; + docCompleted: string; + chooseAction: string; + opening: string; + browserOpened: string; + browserError: string; + browserErrorHint: string; + retry: string; + pagerNotAvailable: string; + endOfDocument: string; + }; + repair: { + title: string; + subtitle: string; + opening: string; + opened: string; + error: string; + errorHint: string; + }; + website: { + opening: string; + opened: string; + error: string; + errorHint: string; + }; + language: { + title: string; + currentLanguage: string; + selectLanguage: string; + zh: string; + en: string; + changed: string; + }; +} + +/** + * Language configuration + */ +let currentLanguage: Language = 'zh'; // Default to Chinese + +/** + * Get configuration directory path + */ +function getConfigDir(): string { + const homeDir = process.env['HOME'] || process.env['USERPROFILE'] || ''; + return path.join(homeDir, '.nbtca'); +} + +/** + * Get language configuration file path + */ +function getLanguageConfigPath(): string { + return path.join(getConfigDir(), 'language.json'); +} + +/** + * Load language preference from config file + */ +export function loadLanguagePreference(): Language { + try { + const configPath = getLanguageConfigPath(); + if (fs.existsSync(configPath)) { + const config = JSON.parse(fs.readFileSync(configPath, 'utf-8')); + if (config.language === 'zh' || config.language === 'en') { + currentLanguage = config.language; + } + } + } catch (err) { + // If loading fails, use default (Chinese) + } + return currentLanguage; +} + +/** + * Save language preference to config file + */ +export function saveLanguagePreference(language: Language): void { + try { + const configDir = getConfigDir(); + if (!fs.existsSync(configDir)) { + fs.mkdirSync(configDir, { recursive: true }); + } + const configPath = getLanguageConfigPath(); + fs.writeFileSync(configPath, JSON.stringify({ language }, null, 2)); + currentLanguage = language; + } catch (err) { + // Silently fail if we can't save preference + } +} + +/** + * Get current language + */ +export function getCurrentLanguage(): Language { + return currentLanguage; +} + +/** + * Set current language + */ +export function setLanguage(language: Language): void { + currentLanguage = language; + saveLanguagePreference(language); +} + +/** + * Load translation file + */ +function loadTranslations(language: Language): Translations { + try { + const translationPath = path.join(__dirname, 'locales', `${language}.json`); + const content = fs.readFileSync(translationPath, 'utf-8'); + return JSON.parse(content); + } catch (err) { + // Fallback to Chinese if loading fails + const fallbackPath = path.join(__dirname, 'locales', 'zh.json'); + const content = fs.readFileSync(fallbackPath, 'utf-8'); + return JSON.parse(content); + } +} + +/** + * Translation cache + */ +let translationsCache: Map = new Map(); + +/** + * Get translations for current language + */ +export function t(): Translations { + if (!translationsCache.has(currentLanguage)) { + translationsCache.set(currentLanguage, loadTranslations(currentLanguage)); + } + return translationsCache.get(currentLanguage)!; +} + +/** + * Clear translation cache (useful when switching languages) + */ +export function clearTranslationCache(): void { + translationsCache.clear(); +} + +// Initialize language preference on module load +loadLanguagePreference(); diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json new file mode 100644 index 0000000..0de4aca --- /dev/null +++ b/src/i18n/locales/en.json @@ -0,0 +1,107 @@ +{ + "common": { + "back": "Back", + "exit": "Exit", + "cancel": "Cancel", + "confirm": "Confirm", + "loading": "Loading...", + "error": "Error", + "success": "Success", + "goodbye": "Goodbye!" + }, + "menu": { + "title": "Choose an action", + "events": "Recent Events", + "eventsDesc": "View upcoming events in next 30 days", + "repair": "Repair Service", + "repairDesc": "Computer repair and software installation", + "docs": "Knowledge Base", + "docsDesc": "Technical docs and tutorials", + "website": "Official Website", + "websiteDesc": "Visit NBTCA homepage", + "github": "GitHub", + "githubDesc": "Open source projects and code", + "about": "About", + "aboutDesc": "Project info and help", + "language": "Language", + "languageDesc": "Change display language", + "navigationHint": "Navigation: j/k or ↑/↓ | Jump: g/G | Quit: q or Ctrl+C", + "chooseAction": "Choose an action" + }, + "about": { + "title": "About NBTCA", + "project": "Project", + "version": "Version", + "description": "Description", + "github": "GitHub", + "website": "Website", + "email": "Email", + "features": "Features:", + "feature1": "- View recent association events", + "feature2": "- Online repair service", + "feature3": "- Technical knowledge base access", + "feature4": "- Quick access to website and GitHub", + "license": "License", + "author": "Author" + }, + "calendar": { + "title": "Recent Events", + "subtitle": "(Next 30 days)", + "loading": "Loading event calendar...", + "noEvents": "No upcoming events", + "error": "Failed to load event calendar", + "errorHint": "Please try again later or visit https://nbtca.space", + "dateTime": "Date & Time", + "eventName": "Event Name", + "location": "Location" + }, + "docs": { + "title": "Knowledge Base", + "subtitle": "Browse documentation from terminal or open in browser", + "loading": "Loading documentation list...", + "loadingDir": "Loading directory", + "chooseCategory": "Choose a category:", + "currentDir": "Current directory", + "chooseDoc": "Choose a document or directory:", + "emptyDir": "This directory is empty", + "upToParent": "[..] Up to parent directory", + "returnToMenu": "[ ^] Return to main menu", + "backToList": "[ <] Back to docs list", + "reread": "[ ↻] Re-read document", + "openBrowser": "[ *] Open in browser", + "loadError": "Failed to load directory", + "errorHint": "Error", + "openBrowserPrompt": "Open in browser?", + "docCompleted": "Document loaded successfully", + "chooseAction": "Choose an action:", + "opening": "Opening browser...", + "browserOpened": "Knowledge base opened in browser", + "browserError": "Failed to open browser", + "browserErrorHint": "Please visit manually: https://docs.nbtca.space", + "retry": "Retry?", + "pagerNotAvailable": "⚠ Pager not available, using standard output", + "endOfDocument": "End of document - Press q to quit" + }, + "repair": { + "title": "Repair Service", + "subtitle": "Computer repair and software installation support", + "opening": "Opening repair service page...", + "opened": "Repair service page opened in browser", + "error": "Failed to open browser", + "errorHint": "Please visit manually: https://nbtca.space/repair" + }, + "website": { + "opening": "Opening website...", + "opened": "Opened in browser", + "error": "Failed to open browser", + "errorHint": "Please visit manually" + }, + "language": { + "title": "Language Settings", + "currentLanguage": "Current Language", + "selectLanguage": "Select a language:", + "zh": "简体中文 (Simplified Chinese)", + "en": "English", + "changed": "Language changed successfully" + } +} diff --git a/src/i18n/locales/zh.json b/src/i18n/locales/zh.json new file mode 100644 index 0000000..af272e8 --- /dev/null +++ b/src/i18n/locales/zh.json @@ -0,0 +1,107 @@ +{ + "common": { + "back": "返回", + "exit": "退出", + "cancel": "取消", + "confirm": "确认", + "loading": "加载中...", + "error": "错误", + "success": "成功", + "goodbye": "再见!" + }, + "menu": { + "title": "选择操作", + "events": "近期活动", + "eventsDesc": "查看未来30天的活动", + "repair": "维修服务", + "repairDesc": "电脑维修和软件安装", + "docs": "知识库", + "docsDesc": "技术文档和教程", + "website": "官方网站", + "websiteDesc": "访问 NBTCA 主页", + "github": "GitHub", + "githubDesc": "开源项目和代码", + "about": "关于", + "aboutDesc": "项目信息和帮助", + "language": "切换语言", + "languageDesc": "更改显示语言", + "navigationHint": "导航: j/k 或 ↑/↓ | 跳转: g/G | 退出: q 或 Ctrl+C", + "chooseAction": "选择一个操作" + }, + "about": { + "title": "关于 NBTCA", + "project": "项目", + "version": "版本", + "description": "描述", + "github": "GitHub", + "website": "网站", + "email": "邮箱", + "features": "功能:", + "feature1": "- 查看协会近期活动", + "feature2": "- 在线维修服务", + "feature3": "- 技术知识库访问", + "feature4": "- 快速访问网站和 GitHub", + "license": "许可证", + "author": "作者" + }, + "calendar": { + "title": "近期活动", + "subtitle": "(最近30天)", + "loading": "正在获取活动日历...", + "noEvents": "近期暂无活动安排", + "error": "无法获取活动日历", + "errorHint": "请稍后重试或访问 https://nbtca.space", + "dateTime": "日期时间", + "eventName": "活动名称", + "location": "地点" + }, + "docs": { + "title": "知识库", + "subtitle": "从终端浏览文档或在浏览器中打开", + "loading": "正在加载文档列表...", + "loadingDir": "正在加载目录", + "chooseCategory": "选择文档分类:", + "currentDir": "当前目录", + "chooseDoc": "选择文档或目录:", + "emptyDir": "该目录为空", + "upToParent": "[..] 返回上级目录", + "returnToMenu": "[ ^] 返回主菜单", + "backToList": "[ <] 返回文档列表", + "reread": "[ ↻] 重新阅读文档", + "openBrowser": "[ *] 在浏览器中打开", + "loadError": "无法加载目录", + "errorHint": "错误", + "openBrowserPrompt": "是否在浏览器中打开?", + "docCompleted": "文档加载完成", + "chooseAction": "选择操作:", + "opening": "正在打开浏览器...", + "browserOpened": "已在浏览器中打开知识库", + "browserError": "无法打开浏览器", + "browserErrorHint": "请手动访问: https://docs.nbtca.space", + "retry": "是否重试?", + "pagerNotAvailable": "⚠ Pager不可用,使用标准输出", + "endOfDocument": "文档结束 - 按 q 退出" + }, + "repair": { + "title": "维修服务", + "subtitle": "电脑维修和软件安装支持", + "opening": "正在打开维修服务页面...", + "opened": "已在浏览器中打开维修服务页面", + "error": "无法打开浏览器", + "errorHint": "请手动访问: https://nbtca.space/repair" + }, + "website": { + "opening": "正在打开网站...", + "opened": "已在浏览器中打开", + "error": "无法打开浏览器", + "errorHint": "请手动访问" + }, + "language": { + "title": "语言设置", + "currentLanguage": "当前语言", + "selectLanguage": "选择语言:", + "zh": "简体中文", + "en": "English", + "changed": "语言已切换" + } +} diff --git a/src/main.ts b/src/main.ts index 5d183ef..fb8107f 100644 --- a/src/main.ts +++ b/src/main.ts @@ -9,6 +9,7 @@ import { printHeader, clearScreen } from './core/ui.js'; import { showMainMenu } from './core/menu.js'; import { APP_INFO } from './config/data.js'; import { enableVimKeys } from './core/vim-keys.js'; +import { t } from './i18n/index.js'; /** * Main program entry point @@ -34,7 +35,7 @@ export async function main(): Promise { // Handle Ctrl+C exit if (err.message?.includes('SIGINT') || err.message?.includes('User force closed')) { console.log(); - console.log(chalk.dim('Goodbye!')); + console.log(chalk.dim(t().common.goodbye)); process.exit(0); } else { console.error('Error occurred:', err);