国际化

This commit is contained in:
Yeban8090 2025-04-14 13:22:02 +08:00
parent 49b91bbb29
commit 452a16b58b
37 changed files with 1839 additions and 478 deletions

View file

@ -3,6 +3,7 @@ import { Book, ChapterNode } from '../types/book';
import { BookManager } from '../services/BookManager';
import { NamePromptModal } from '../modals/NamePromptModal';
import { ConfirmModal } from '../modals/ConfirmModal';
import { i18n } from '../i18n/i18n';
export class ChapterTree {
// === 属性 ===
@ -28,13 +29,13 @@ export class ChapterTree {
const menu = new Menu();
menu.addItem((item) => {
item.setTitle("新建文件");
item.setTitle(i18n.t('NEW_FILE'));
item.setIcon("file-plus");
item.onClick(() => this.createNode('file'));
});
menu.addItem((item) => {
item.setTitle("新建文件夹");
item.setTitle(i18n.t('NEW_FOLDER'));
item.setIcon("folder-plus");
item.onClick(() => this.createNode('group'));
});
@ -215,7 +216,7 @@ export class ChapterTree {
// 为文件夹类型添加创建选项
if (node.type === 'group') {
menu.addItem((item) => {
item.setTitle("新建文件");
item.setTitle(i18n.t('NEW_FILE'));
item.setIcon("file-plus");
item.onClick(() => {
this.createNode('file', node.path, node);
@ -223,7 +224,7 @@ export class ChapterTree {
});
menu.addItem((item) => {
item.setTitle("新建文件夹");
item.setTitle(i18n.t('NEW_FOLDER'));
item.setIcon("folder-plus");
item.onClick(() => {
this.createNode('group', node.path, node);
@ -235,7 +236,7 @@ export class ChapterTree {
if (node.type === 'file') {
menu.addItem((item) => {
item.setTitle("在新标签页中打开");
item.setTitle(i18n.t('OPEN_IN_NEW_TAB'));
item.setIcon("file-plus");
item.onClick(() => {
if (file instanceof TFile) {
@ -245,7 +246,7 @@ export class ChapterTree {
});
menu.addItem((item) => {
item.setTitle("在新标签组中打开");
item.setTitle(i18n.t('OPEN_IN_NEW_PANE'));
item.setIcon("files");
item.onClick(() => {
if (file instanceof TFile) {
@ -259,7 +260,9 @@ export class ChapterTree {
// 只有当文件不排除之外时,才显示"标记完成章节"选项
if (!node.exclude) {
menu.addItem((item) => {
item.setTitle(node.default_status === 'done' ? "标记重新创作" : "标记完成章节");
item.setTitle(node.default_status === 'done' ?
i18n.t('MARK_AS_DRAFT') :
i18n.t('MARK_AS_COMPLETE'));
item.setIcon(node.default_status === 'done' ? "x-circle" : "check-circle");
item.onClick(async () => {
node.default_status = node.default_status === 'done' ? 'draft' : 'done';
@ -281,7 +284,9 @@ export class ChapterTree {
// 添加排除选项
menu.addItem((item) => {
item.setTitle(node.exclude ? "包含在统计与导出中" : "排除在统计与导出外");
item.setTitle(node.exclude ?
i18n.t('INCLUDE_IN_STATS') :
i18n.t('EXCLUDE_FROM_STATS'));
item.setIcon(node.exclude ? "plus-circle" : "minus-circle");
item.onClick(async () => {
node.exclude = !node.exclude;
@ -301,8 +306,8 @@ export class ChapterTree {
});
await this.onDragComplete?.();
new Notice(node.exclude ?
`已将"${node.title}"排除` :
`已将"${node.title}"包含`);
i18n.t('EXCLUDED_NOTICE', { title: node.title }) :
i18n.t('INCLUDED_NOTICE', { title: node.title }));
});
});
@ -310,13 +315,13 @@ export class ChapterTree {
}
menu.addItem((item) => {
item.setTitle("创建副本");
item.setTitle(i18n.t('CREATE_COPY'));
item.setIcon("copy");
item.onClick(async () => {
try {
const parentPath = node.path.substring(0, node.path.lastIndexOf('/'));
const baseName = node.title;
const newName = `${baseName} 副本`;
const newName = i18n.t('COPY_NAME', { name: baseName });
const newPath = parentPath
? `${this.bookPath}/${parentPath}/${newName}${node.type === 'file' ? '.md' : ''}`
: `${this.bookPath}/${newName}${node.type === 'file' ? '.md' : ''}`;
@ -354,22 +359,21 @@ export class ChapterTree {
structure: this.book.structure
});
await this.onDragComplete?.();
new Notice('创建副本成功');
new Notice(i18n.t('COPY_SUCCESS'));
} catch (error) {
new Notice(`创建副本失败: ${error.message}`);
new Notice(i18n.t('COPY_FAILED', { error: error.message }));
}
});
});
menu.addItem((item) => {
item.setTitle("重命名");
item.setTitle(i18n.t('RENAME'));
item.setIcon("pencil");
item.onClick(async () => {
// 获取当前文件名(不含扩展名)
const currentName = node.type === 'file'
? node.title.replace(/\.md$/, '')
: node.title;
const newName = await this.promptForName("请输入新名称", currentName);
const newName = await this.promptForName(i18n.t('ENTER_NEW_NAME'), currentName);
if (!newName) return;
try {
@ -394,21 +398,23 @@ export class ChapterTree {
});
await this.onDragComplete?.();
new Notice('重命名成功');
new Notice(i18n.t('RENAME_SUCCESS'));
} catch (error) {
new Notice(`重命名失败: ${error.message}`);
new Notice(i18n.t('RENAME_FAILED', { error: error.message }));
}
});
});
menu.addItem((item) => {
item.setTitle("删除");
item.setTitle(i18n.t('DELETE'));
item.setIcon("trash");
item.onClick(() => {
const title = node.type === 'file' ? "删除文件" : "删除文件夹";
const title = node.type === 'file' ?
i18n.t('DELETE_FILE_TITLE') :
i18n.t('DELETE_FOLDER_TITLE');
const message = node.type === 'file'
? `确定要删除文件 "${node.title}" 吗?\n它将被移动到系统回收站。`
: `确定要删除文件夹 "${node.title}" 及其所有内容吗?\n它们将被移动到系统回收站。`;
? i18n.t('DELETE_FILE_DESC', { title: node.title })
: i18n.t('DELETE_FOLDER_DESC', { title: node.title });
new ConfirmModal(this.app, title, message, async () => {
try {
@ -418,9 +424,9 @@ export class ChapterTree {
structure: this.book.structure
});
await this.onDragComplete?.();
new Notice('删除成功');
new Notice(i18n.t('DELETE_SUCCESS'));
} catch (error) {
new Notice(`删除失败: ${error.message}`);
new Notice(i18n.t('DELETE_FAILED', { error: error.message }));
}
}).open();
});
@ -541,18 +547,18 @@ export class ChapterTree {
const sourceFile = this.app.vault.getAbstractFileByPath(sourcePath);
if (!sourceFile) {
throw new Error('源文件不存在');
throw new Error(i18n.t('SOURCE_NOT_FOUND'));
}
const existingFile = this.app.vault.getAbstractFileByPath(targetPath);
if (existingFile && existingFile !== sourceFile) {
throw new Error('目标位置已存在同名文件');
throw new Error(i18n.t('TARGET_EXISTS'));
}
if (position === 'inside') {
const targetFolder = this.app.vault.getAbstractFileByPath(`${this.bookPath}/${targetNode.path}`);
if (!targetFolder) {
throw new Error('目标文件夹不存在');
throw new Error(i18n.t('TARGET_FOLDER_NOT_FOUND'));
}
}
@ -564,7 +570,7 @@ export class ChapterTree {
await this.onDragComplete?.();
} catch (error) {
console.error('移动失败:', error);
new Notice(`移动失败`);
new Notice(i18n.t('MOVE_FAILED'));
} finally {
this.draggedNode = null;
}
@ -632,7 +638,9 @@ export class ChapterTree {
parentNode?: ChapterNode
) {
const isFile = type === 'file';
const name = await this.promptForName(`请输入${isFile ? '文件' : '文件夹'}`);
const name = await this.promptForName(
isFile ? i18n.t('ENTER_FILE_NAME') : i18n.t('ENTER_FOLDER_NAME')
);
if (!name) return;
const newPath = parentPath

View file

@ -1,13 +1,8 @@
import { setIcon, App, Notice } from 'obsidian';
import { setIcon, App } from 'obsidian';
import { ConfirmModal } from '../modals/ConfirmModal';
import { FocusManager, FocusState } from '../services/FocusManager';
import BookSmithPlugin from '../main';
interface FocusStats {
completedSessions: number;
interruptions: number;
totalWords: number;
}
import { i18n } from '../i18n/i18n';
export class FocusToolView {
// ================ 属性定义 ================
@ -51,7 +46,7 @@ export class FocusToolView {
private createHeader() {
const header = this.container.createDiv({ cls: 'focus-tool-header' });
setIcon(header.createSpan({ cls: 'focus-tool-header-icon' }), 'target');
header.createSpan({ text: '专注模式', cls: 'focus-tool-title' });
header.createSpan({ text: i18n.t('FOCUS_MODE'), cls: 'focus-tool-title' });
}
private createTimerSection() {
@ -107,7 +102,7 @@ export class FocusToolView {
private createStatusSection() {
this.statusEl = this.container.createDiv({ cls: 'focus-tool-status' });
this.statusEl.setText('准备开始');
this.statusEl.setText(i18n.t('READY_TO_START'));
}
private createControlSection() {
@ -116,23 +111,23 @@ export class FocusToolView {
}
private createInitialControls(controls: HTMLElement) {
this.createButton(controls, '开始专注', true, () => {
this.createButton(controls, i18n.t('START_FOCUS'), true, () => {
this.focusManager.startFocus();
controls.empty();
this.createControlButtons(controls);
});
this.createButton(controls, '退出', false, () => this.handleExit());
this.createButton(controls, i18n.t('EXIT'), false, () => this.handleExit());
}
private createStatsSection() {
const statsContainer = this.container.createDiv({ cls: 'focus-tool-stats-container' });
const stats = this.focusManager.getStats();
this.completedEl = this.createStatItem(statsContainer, '专注次数', stats.completedSessions.toString());
this.interruptedEl = this.createStatItem(statsContainer, '中断次数', stats.interruptions.toString());
this.wordCountEl = this.createStatItem(statsContainer, '当前字数', '0');
this.wordGoalEl = this.createStatItem(statsContainer, '目标字数', this.plugin.settings.focus.wordGoal.toString());
this.totalWordsEl = this.createStatItem(statsContainer, '专注总字数', stats.totalWords.toString());
this.completedEl = this.createStatItem(statsContainer, i18n.t('FOCUS_SESSIONS'), stats.completedSessions.toString());
this.interruptedEl = this.createStatItem(statsContainer, i18n.t('INTERRUPTIONS'), stats.interruptions.toString());
this.wordCountEl = this.createStatItem(statsContainer, i18n.t('CURRENT_WORDS'), '0');
this.wordGoalEl = this.createStatItem(statsContainer, i18n.t('WORD_GOAL'), this.plugin.settings.focus.wordGoal.toString());
this.totalWordsEl = this.createStatItem(statsContainer, i18n.t('TOTAL_FOCUS_WORDS'), stats.totalWords.toString());
}
private createControlButtons(controls: HTMLElement) {
@ -140,13 +135,13 @@ export class FocusToolView {
const state = this.focusManager.getState();
if (state === FocusState.WORKING) {
this.createButton(controls, '暂停', false, () => this.focusManager.pauseFocus());
this.createButton(controls, i18n.t('PAUSE'), false, () => this.focusManager.pauseFocus());
} else if (state === FocusState.PAUSED) {
this.createButton(controls, '继续', true, () => this.focusManager.resumeFocus());
this.createButton(controls, '结束', false, () => this.showEndConfirmation());
this.createButton(controls, i18n.t('RESUME'), true, () => this.focusManager.resumeFocus());
this.createButton(controls, i18n.t('END'), false, () => this.showEndConfirmation());
}
this.createButton(controls, '退出', false, () => this.handleExit());
this.createButton(controls, i18n.t('EXIT'), false, () => this.handleExit());
}
private createButton(container: HTMLElement, text: string, solid: boolean, onClick: () => void) {
@ -311,8 +306,8 @@ export class FocusToolView {
if (state === FocusState.WORKING || state === FocusState.PAUSED) {
new ConfirmModal(
this.app,
'退出专注',
'确定要退出专注吗?当前专注进度将会丢失。',
i18n.t('EXIT_FOCUS'),
i18n.t('EXIT_FOCUS_DESC'),
() => {
this.focusManager.endFocus();
this.onExit();
@ -329,8 +324,8 @@ export class FocusToolView {
private showEndConfirmation() {
new ConfirmModal(
this.app,
'结束专注',
'确定要结束专注吗?这将计入中断次数。',
i18n.t('END_FOCUS'),
i18n.t('END_FOCUS_DESC'),
() => {
this.focusManager.endFocus();
this.onExit();
@ -341,10 +336,10 @@ export class FocusToolView {
// ================ 工具方法 ================
private getStatusText(state: FocusState): string {
switch (state) {
case FocusState.WORKING: return '专注中';
case FocusState.PAUSED: return '已暂停';
case FocusState.BREAK: return '休息时间';
default: return '准备开始';
case FocusState.WORKING: return i18n.t('FOCUSING');
case FocusState.PAUSED: return i18n.t('PAUSED');
case FocusState.BREAK: return i18n.t('BREAK_TIME');
default: return i18n.t('READY_TO_START');
}
}

95
src/i18n/i18n.ts Normal file
View file

@ -0,0 +1,95 @@
import { moment } from 'obsidian';
import { Translation } from './interfaces';
// 支持的语言
export type Locale = 'zh-CN' | 'en' | 'ja' | 'ko' | 'fr' | 'de' | 'es' | 'pt' | 'ru';
// 语言管理器
export class I18n {
private locale: Locale;
private translations: Record<Locale, Translation>;
constructor() {
// 初始化为系统语言或默认为中文
this.locale = this.getSystemLocale();
this.translations = {} as Record<Locale, Translation>;
this.loadTranslations();
}
// 获取系统语言
private getSystemLocale(): Locale {
const systemLocale = moment.locale();
// 映射 moment 语言代码到我们支持的语言
if (systemLocale.startsWith('zh')) return 'zh-CN';
if (systemLocale.startsWith('en')) return 'en';
if (systemLocale.startsWith('ja')) return 'ja';
if (systemLocale.startsWith('ko')) return 'ko';
if (systemLocale.startsWith('fr')) return 'fr';
if (systemLocale.startsWith('de')) return 'de';
if (systemLocale.startsWith('es')) return 'es';
if (systemLocale.startsWith('pt')) return 'pt';
if (systemLocale.startsWith('ru')) return 'ru';
// 默认英文
return 'en';
}
// 设置当前语言
public setLocale(locale: Locale) {
this.locale = locale;
}
// 获取当前语言
public getLocale(): Locale {
return this.locale;
}
// 加载翻译文件
private loadTranslations() {
// 导入所有语言文件
import('./locales/zh-CN').then(module => {
this.translations['zh-CN'] = module.default;
});
import('./locales/en').then(module => {
this.translations['en'] = module.default;
});
// 其他语言可以在这里添加
// import('./locales/ja').then(module => {
// this.translations['ja'] = module.default;
// });
// ...
}
// 获取翻译文本
public t(key: keyof Translation, params?: Record<string, any>): string {
let text: string;
// 如果当前语言的翻译不存在,回退到英文
if (!this.translations[this.locale]) {
text = this.translations['en'][key] || key as string;
} else {
// 如果当前语言的特定翻译不存在,回退到英文
text = this.translations[this.locale][key] ||
this.translations['en'][key] ||
key as string;
}
// 如果有参数,替换文本中的占位符
if (params) {
Object.keys(params).forEach(paramKey => {
text = text.replace(new RegExp(`{${paramKey}}`, 'g'), params[paramKey]);
});
}
return text;
}
}
// 导出单例实例
export const i18n = new I18n();
// 导出接口
export * from './interfaces';

View file

@ -0,0 +1,10 @@
// 通用翻译接口
export interface CommonTranslation {
// 通用
PLUGIN_NAME: string;
SETTINGS: string;
SAVE: string;
CANCEL: string;
HIDE: string;
SHOW: string;
}

View file

@ -0,0 +1,60 @@
export interface ComponentTranslation {
// ChapterTree
NEW_FILE: string;
NEW_FOLDER: string;
ENTER_FILE_NAME: string;
ENTER_FOLDER_NAME: string;
OPEN_IN_NEW_TAB: string;
OPEN_IN_NEW_PANE: string;
MARK_AS_COMPLETE: string;
MARK_AS_DRAFT: string;
EXCLUDE_FROM_STATS: string;
INCLUDE_IN_STATS: string;
CREATE_COPY: string;
COPY_NAME: string; // 添加这一行
RENAME: string;
DELETE: string;
DELETE_FILE_TITLE: string;
DELETE_FILE_DESC: string;
DELETE_FOLDER_TITLE: string;
DELETE_FOLDER_DESC: string;
COPY_SUCCESS: string;
COPY_FAILED: string;
RENAME_SUCCESS: string;
RENAME_FAILED: string;
DELETE_SUCCESS: string;
DELETE_FAILED: string;
MOVE_FAILED: string;
SOURCE_NOT_FOUND: string;
TARGET_EXISTS: string;
TARGET_FOLDER_NOT_FOUND: string;
EXCLUDED_NOTICE: string;
INCLUDED_NOTICE: string;
ENTER_NEW_NAME: string;
// FocusToolView
FOCUS_MODE: string;
START_FOCUS: string;
EXIT: string;
FOCUS_SESSIONS: string;
INTERRUPTIONS: string;
CURRENT_WORDS: string;
WORD_GOAL: string;
TOTAL_FOCUS_WORDS: string;
PAUSE: string;
RESUME: string;
END: string;
ENCOURAGEMENT_1: string;
ENCOURAGEMENT_2: string;
ENCOURAGEMENT_3: string;
ENCOURAGEMENT_4: string;
ENCOURAGEMENT_5: string;
EXIT_FOCUS: string;
EXIT_FOCUS_DESC: string;
END_FOCUS: string;
END_FOCUS_DESC: string;
FOCUSING: string;
PAUSED: string;
BREAK_TIME: string;
READY_TO_START: string;
}

View file

@ -0,0 +1,28 @@
// 导出所有翻译接口
import { CommonTranslation } from './common';
import { BookSmithViewTranslation } from './views/booksmith-view';
import { ToolViewTranslation } from './views/tool-view';
import { SettingsTranslation } from './settings/settings';
import { ModalTranslation } from './modals/modals';
import { ToolbarModalTranslation } from './modals/toolbarModals';
import { ManagerTranslation } from './manager/managers';
import { ComponentTranslation } from './components/components';
// 合并所有翻译接口
export interface Translation extends
CommonTranslation,
BookSmithViewTranslation,
ToolViewTranslation,
SettingsTranslation,
ModalTranslation,
ToolbarModalTranslation,
ManagerTranslation,
ComponentTranslation{}
export * from './common';
export * from './views/booksmith-view';
export * from './views/tool-view';
export * from './settings/settings';
export * from './modals/modals';
export * from './modals/toolbarModals';
export * from './manager/managers';
export * from './components/components';

View file

@ -0,0 +1,36 @@
export interface ManagerTranslation {
// BookManager 相关
BOOK_EXISTS: string;
BOOK_NOT_FOUND: string;
BOOK_FOLDER_NOT_FOUND: string;
SAVE_CONFIG_FAILED: string;
IMPORT_BOOK_FAILED: string;
UNKNOWN_AUTHOR: string;
// TemplateManager 相关
TEMPLATE_TYPE_NOT_FOUND: string;
TEMPLATE_EXISTS: string;
TEMPLATE_SAVE_FAILED: string;
// FileManager 相关
CREATE_FOLDER_FAILED: string;
CREATE_FILE_FAILED: string;
READ_FILE_FAILED: string;
WRITE_FILE_FAILED: string;
// FocusManager 相关
BREAK_TIME_START: string;
FOCUS_SUMMARY: string;
// ReferenceManager 相关
REFERENCE_FILE_NAME: string;
REFERENCE_FILE_NOT_FOUND: string;
REFERENCE_FILE_ERROR: string;
SELECT_TEXT_TO_REFERENCE: string;
CHAPTER_INFO_ERROR: string;
// ReferenceManager 菜单项
EDIT_REFERENCE: string;
DELETE_REFERENCE: string;
INSERT_REFERENCE: string;
}

View file

@ -0,0 +1,111 @@
// 模态框翻译接口
export interface ModalTranslation {
// 通用
COVER: string;
COVER_DESC: string;
BOOK_TITLE: string;
BOOK_TITLE_DESC: string;
BOOK_TITLE_PLACEHOLDER: string;
SUBTITLE: string;
SUBTITLE_DESC: string;
SUBTITLE_PLACEHOLDER: string;
TARGET_WORDS: string;
TARGET_WORDS_DESC: string;
TARGET_WORDS_PLACEHOLDER: string;
AUTHOR: string;
AUTHOR_DESC: string;
AUTHOR_PLACEHOLDER: string;
DESCRIPTION: string;
DESCRIPTION_DESC: string;
DESCRIPTION_PLACEHOLDER: string;
REQUIRED_FIELDS: string;
// CreateBookModal
CREATE_BOOK_TITLE: string;
BOOK_TEMPLATE: string;
TEMPLATE_CHECK_DESC: string;
SELECT_IMAGE: string;
COVER_UPLOAD_SUCCESS: string;
COVER_UPLOAD_FAILED: string;
CREATE: string;
CREATE_SUCCESS: string;
CREATE_FAILED: string;
// EditBookModal
EDIT_BOOK_TITLE: string;
CHANGE_COVER: string;
SELECT_COVER: string;
COVER_UPDATE_SUCCESS: string;
COVER_UPDATE_FAILED: string;
SAVE: string;
SAVE_SUCCESS: string;
SAVE_FAILED: string;
// ManageBooksModal
MANAGE_BOOKS_TITLE: string;
SEARCH_BOOKS_PLACEHOLDER: string;
IMPORT_BOOK: string;
BOOK_AUTHOR_PREFIX: string;
BOOK_DESC_PREFIX: string;
BOOK_PROGRESS_PREFIX: string;
DELETE_BOOK: string;
EDIT_BOOK: string;
DELETE_BOOK_TITLE: string;
DELETE_BOOK_DESC: string;
DELETE_SUCCESS: string;
DELETE_FAILED: string;
BOOKS_ROOT_NOT_FOUND: string;
NO_UNIMPORTED_BOOKS: string;
DETECT_UNIMPORTED_FAILED: string;
IMPORT_SUCCESS: string;
IMPORT_FAILED: string;
// SwitchBookModal
SWITCH_BOOK_TITLE: string;
SEARCH_BOOK_PLACEHOLDER: string;
BOOK_AUTHOR_LABEL: string;
BOOK_PROGRESS_LABEL: string;
BOOK_WORDCOUNT_LABEL: string;
BOOK_LASTMOD_LABEL: string;
SELECT_BOOK: string;
// UnimportedBooksModal
UNIMPORTED_BOOKS_TITLE: string;
NO_UNIMPORTED_FOLDERS: string;
CLOSE: string;
SEARCH_FOLDERS_PLACEHOLDER: string;
NO_MATCHING_FOLDERS: string;
IMPORT: string;
SELECT_FOLDER_FIRST: string;
// ReferenceModal
REFERENCE_MODAL_TITLE: string;
REFERENCE_CONTENT: string;
REFERENCE_CONTENT_DESC: string;
// ConfirmModal
// NamePromptModal
NAME_LABEL: string;
CONFIRM: string;
CANCEL: string;
// TemplateEditModal
TEMPLATE_EDIT_TITLE: string;
TEMPLATE_CREATE_TITLE: string;
TEMPLATE_NAME: string;
TEMPLATE_NAME_PLACEHOLDER: string;
TEMPLATE_DESC: string;
TEMPLATE_DESC_PLACEHOLDER: string;
TEMPLATE_STRUCTURE: string;
ADD_FILE: string;
ADD_FOLDER: string;
DELETE_FOLDER_CONFIRM: string;
NEW_CHAPTER: string;
NEW_DIRECTORY: string;
ENTER_NAME_PLACEHOLDER: string;
TEMPLATE_NAME_REQUIRED: string;
TEMPLATE_NODE_REQUIRED: string;
TEMPLATE_NAME_EXISTS: string;
TEMPLATE_SAVE_SUCCESS: string;
TEMPLATE_SAVE_FAILED: string;
}

View file

@ -0,0 +1,55 @@
export interface ToolbarModalTranslation {
// CommunityModal
COMMUNITY_TITLE: string;
COMMUNITY_INTRO: string;
FEATURE_SHARE_TITLE: string;
FEATURE_SHARE_DESC: string;
FEATURE_DISCUSS_TITLE: string;
FEATURE_DISCUSS_DESC: string;
FEATURE_CHALLENGE_TITLE: string;
FEATURE_CHALLENGE_DESC: string;
JOIN_SECTION_TITLE: string;
JOIN_SECTION_DESC: string;
OFFICIAL_ACCOUNT: string;
COPY_ACCOUNT: string;
// ContactModal
ABOUT_AUTHOR: string;
AUTHOR_INTRO_1: string;
AUTHOR_INTRO_2_1: string;
AUTHOR_INTRO_2_2: string;
AUTHOR_INTRO_2_3: string;
AUTHOR_INTRO_2_4: string;
AUTHOR_INTRO_2_5: string;
AUTHOR_INTRO_3: string;
AUTHOR_INTRO_4_1: string;
AUTHOR_INTRO_4_2: string;
DONATE_TEXT: string;
DONATE_BUTTON: string;
MORE_INFO_TEXT: string;
CONTACT_TITLE: string;
CONTACT_WECHAT_OFFICIAL: string;
CONTACT_XIAOHONGSHU: string;
CONTACT_WECHAT: string;
CONTACT_GITHUB: string;
COPY_SUCCESS: string;
COPY_FAILED: string;
// DonateModal
DONATE_MODAL_TITLE: string;
COMMUNITY_STATS_TITLE: string;
COMMUNITY_STATS_USERS: string;
COMMUNITY_STATS_WORDS: string;
DONATE_AMOUNT_COFFEE: string;
DONATE_AMOUNT_CHAPTER: string;
DONATE_AMOUNT_FEATURE: string;
DONATE_FEEDBACK_COFFEE: string;
DONATE_FEEDBACK_CHAPTER: string;
DONATE_FEEDBACK_FEATURE: string;
PAYMENT_WECHAT: string;
PAYMENT_ALIPAY: string;
PAYMENT_KOFI: string;
CURRENCY_UNIT: string;
}

View file

@ -0,0 +1,36 @@
export interface SettingsTranslation {
// 设置页面
SETTINGS_TITLE: string;
BASIC_SETTINGS: string;
TEMPLATE_SETTINGS: string;
WRITING_TOOLS_SETTINGS: string;
// 基本设置
LANGUAGE_SETTING: string;
LANGUAGE_DESC: string;
DEFAULT_AUTHOR: string;
DEFAULT_AUTHOR_DESC: string;
DEFAULT_AUTHOR_PLACEHOLDER: string;
BOOK_STORAGE_PATH: string;
BOOK_STORAGE_DESC: string;
STORAGE_PATH_CHANGED: string;
// 模板设置
DEFAULT_TEMPLATE: string;
DEFAULT_TEMPLATE_DESC: string;
BOOK_TEMPLATES: string;
ADD_NEW_TEMPLATE: string;
EDIT_TEMPLATE: string;
DELETE_TEMPLATE: string;
DELETE_TEMPLATE_TITLE: string;
DELETE_TEMPLATE_DESC: string;
// 写作工具箱设置
FOCUS_MODE_SETTINGS: string;
FOCUS_DURATION: string;
FOCUS_DURATION_DESC: string;
BREAK_DURATION: string;
BREAK_DURATION_DESC: string;
WORD_GOAL: string;
WORD_GOAL_DESC: string;
}

View file

@ -0,0 +1,49 @@
// 书籍管理视图翻译接口
export interface BookSmithViewTranslation {
// 主界面
BOOK_MANAGER: string;
NEW_BOOK: string;
SWITCH_BOOK: string;
MANAGE_BOOK: string;
// 书籍相关
BOOK_TITLE: string;
BOOK_AUTHOR: string;
BOOK_DESCRIPTION: string;
BOOK_TAGS: string;
BOOK_COVER: string;
// 章节相关
CHAPTER: string;
ADD_CHAPTER: string;
DELETE_CHAPTER: string;
RENAME_CHAPTER: string;
// 统计相关
STATS: string;
WORD_COUNT: string;
CHAPTER_COUNT: string;
// 帮助提示
HELP_TOOLTIP: string;
// 通知消息
SWITCHED_TO_BOOK: string;
IMPORTED_AND_SWITCHED: string;
CURRENT_BOOK_DELETED: string;
NO_BOOKS_TO_SWITCH: string;
// 统计文本
TODAY_WORDS: string;
TOTAL_WORDS: string;
CHAPTER_COMPLETION: string;
WRITING_DAYS: string;
AVERAGE_DAILY_WORDS: string;
WORD_UNIT: string;
DAY_UNIT: string;
TEN_THOUSAND: string;
// 空状态提示
WELCOME_MESSAGE: string;
EMPTY_STATE_HINT: string;
}

View file

@ -0,0 +1,28 @@
// 工具箱视图翻译接口
export interface ToolViewTranslation {
WRITING_TOOLBOX: string;
// 写作助手
WRITING_ASSISTANT: string;
FOCUS_MODE: string;
CREATIVE_INSPIRATION: string;
CHARACTER_PROFILES: string;
WORLD_BUILDING: string;
// 导出发布
EXPORT_PUBLISH: string;
DESIGN_TYPOGRAPHY: string;
GENERATE_EBOOK: string;
MORE_FEATURES: string;
MORE_FEATURES_MESSAGE: string;
// 写作圈子
WRITING_COMMUNITY: string;
CREATIVE_COMMUNITY: string;
CONTACT_AUTHOR: string;
DONATE_SUPPORT: string;
// 面板设置
PANEL_SETTINGS: string;
FEATURE_COMING_SOON: string;
}

445
src/i18n/locales/en.ts Normal file
View file

@ -0,0 +1,445 @@
import {
Translation,
CommonTranslation,
BookSmithViewTranslation,
ToolViewTranslation,
SettingsTranslation,
ModalTranslation,
ManagerTranslation,
ToolbarModalTranslation,
ComponentTranslation
} from '../interfaces';
// 通用翻译
const commonTranslation: CommonTranslation = {
PLUGIN_NAME: 'BookSmith',
SETTINGS: 'Settings',
SAVE: 'Save',
CANCEL: 'Cancel',
HIDE: 'Hide',
SHOW: 'Show'
};
// 书籍管理视图翻译
const bookSmithViewTranslation: BookSmithViewTranslation = {
// 主界面
BOOK_MANAGER: 'Book Manager',
NEW_BOOK: 'New Book',
SWITCH_BOOK: 'Switch Book',
MANAGE_BOOK: 'Manage Book',
// 书籍相关
BOOK_TITLE: 'Title',
BOOK_AUTHOR: 'Author',
BOOK_DESCRIPTION: 'Description',
BOOK_TAGS: 'Tags',
BOOK_COVER: 'Cover',
// 章节相关
CHAPTER: 'Chapter',
ADD_CHAPTER: 'Add Chapter',
DELETE_CHAPTER: 'Delete Chapter',
RENAME_CHAPTER: 'Rename Chapter',
// 统计相关
STATS: 'Statistics',
WORD_COUNT: 'Word Count',
CHAPTER_COUNT: 'Chapter Count',
// 帮助提示
HELP_TOOLTIP: `👋 Welcome to BookSmith
🚀 Getting Started
Open the Writing Toolbox on the right to activate writing assistance
Access Focus Mode, Statistics, Reference Management with one click
📚 Book Management
Create: Choose templates to create book projects
Switch: Freely switch between different works
Manage: Import and edit your collection
Templates: Customize your writing framework
📑 Chapter Organization
Tree Structure: Visualize your book hierarchy
Drag & Drop: Flexibly adjust chapter order
Status Markers: Track writing progress
Context Menu: Convenient chapter operations
Writing Assistant
Real-time Stats: Word count and progress updates
Data Analysis: In-depth writing habit statistics
Focus Mode: Improve writing efficiency
💡 Tips
Support for custom writing templates
Quickly adjust chapters via drag & drop
Right-click for more operations
May BookSmith bring joy to your creative journey.
💝 Support
If BookSmith helps you, please consider supporting
the developer via the "Donate" button in the Writing Toolbox.`,
// 通知消息
SWITCHED_TO_BOOK: 'Switched to "{title}"',
IMPORTED_AND_SWITCHED: 'Imported and switched to new book',
CURRENT_BOOK_DELETED: 'Current book has been deleted',
NO_BOOKS_TO_SWITCH: 'No books available to switch',
// 统计文本
TODAY_WORDS: 'Today',
TOTAL_WORDS: 'Total Words',
CHAPTER_COMPLETION: 'Completion',
WRITING_DAYS: 'Writing Days',
AVERAGE_DAILY_WORDS: 'Daily Average',
WORD_UNIT: ' words',
DAY_UNIT: ' days',
TEN_THOUSAND: 'K',
// 空状态提示
WELCOME_MESSAGE: '👋 Welcome to BookSmith',
EMPTY_STATE_HINT: 'Click "New Book" above to create a work, or use the "Switch" button to select an existing book'
};
// 工具箱视图翻译
const toolViewTranslation: ToolViewTranslation = {
WRITING_TOOLBOX: 'Writing Toolbox',
// 写作助手
WRITING_ASSISTANT: 'Writing Assistant',
FOCUS_MODE: 'Focus Mode',
CREATIVE_INSPIRATION: 'Creative Inspiration',
CHARACTER_PROFILES: 'Character Profiles',
WORLD_BUILDING: 'World Building',
// 导出发布
EXPORT_PUBLISH: 'Export & Publish',
DESIGN_TYPOGRAPHY: 'Design & Typography',
GENERATE_EBOOK: 'Generate E-book',
MORE_FEATURES: 'More Features...',
MORE_FEATURES_MESSAGE: 'More features coming with your participation',
// 写作圈子
WRITING_COMMUNITY: 'Writing Community',
CREATIVE_COMMUNITY: 'Creative Community',
CONTACT_AUTHOR: 'Contact Author',
DONATE_SUPPORT: 'Donate & Support',
// 面板设置
PANEL_SETTINGS: 'Panel Settings',
FEATURE_COMING_SOON: '{feature} feature coming soon'
};
// 设置面板翻译
const settingsTranslation: SettingsTranslation = {
SETTINGS_TITLE: 'Book Smith Settings',
BASIC_SETTINGS: 'Basic Settings',
TEMPLATE_SETTINGS: 'Template Settings',
WRITING_TOOLS_SETTINGS: 'Writing Tools Settings',
// Basic Settings
LANGUAGE_SETTING: 'Language',
LANGUAGE_DESC: 'Choose plugin language / 选择插件界面语言',
DEFAULT_AUTHOR: 'Default Author',
DEFAULT_AUTHOR_DESC: 'Default author name for new books',
DEFAULT_AUTHOR_PLACEHOLDER: 'Enter default author name',
BOOK_STORAGE_PATH: 'Book Storage Path',
BOOK_STORAGE_DESC: 'Default storage path for new books',
STORAGE_PATH_CHANGED: 'Storage path changed, please restart Obsidian or reload for changes to take effect',
// Template Settings
DEFAULT_TEMPLATE: 'Default Template',
DEFAULT_TEMPLATE_DESC: 'Default template used when creating new books',
BOOK_TEMPLATES: 'Book Templates',
ADD_NEW_TEMPLATE: 'Add New Template',
EDIT_TEMPLATE: 'Edit Template',
DELETE_TEMPLATE: 'Delete Template',
DELETE_TEMPLATE_TITLE: 'Delete Template',
DELETE_TEMPLATE_DESC: 'Are you sure you want to delete this template? This action cannot be undone.',
// Writing Tools Settings
FOCUS_MODE_SETTINGS: 'Focus Mode Settings',
FOCUS_DURATION: 'Focus Duration',
FOCUS_DURATION_DESC: 'Work duration for each focus session (minutes)',
BREAK_DURATION: 'Break Duration',
BREAK_DURATION_DESC: 'Break duration after each focus session (minutes)',
WORD_GOAL: 'Word Goal',
WORD_GOAL_DESC: 'Target word count for each focus session'
};
// 模态框翻译
const modalTranslation: ModalTranslation = {
// Common
COVER: 'Cover',
COVER_DESC: 'Select cover image (optional)',
BOOK_TITLE: 'Title',
BOOK_TITLE_DESC: 'Please enter book title',
BOOK_TITLE_PLACEHOLDER: 'Book title',
SUBTITLE: 'Subtitle',
SUBTITLE_DESC: 'Optional',
SUBTITLE_PLACEHOLDER: 'Subtitle',
TARGET_WORDS: 'Target Word Count',
TARGET_WORDS_DESC: 'Set estimated total word count (in 10k)',
TARGET_WORDS_PLACEHOLDER: 'e.g., 20 or 20.0',
AUTHOR: 'Author',
AUTHOR_DESC: 'Enter author names, separate multiple authors with commas',
AUTHOR_PLACEHOLDER: 'Author',
DESCRIPTION: 'Description',
DESCRIPTION_DESC: 'Please enter book description',
DESCRIPTION_PLACEHOLDER: 'Book description',
REQUIRED_FIELDS: 'Please fill in required fields',
// CreateBookModal
CREATE_BOOK_TITLE: 'Create New Book',
BOOK_TEMPLATE: 'Template',
TEMPLATE_CHECK_DESC: 'Please select a book template',
SELECT_IMAGE: 'Select Image',
COVER_UPLOAD_SUCCESS: 'Cover uploaded successfully',
COVER_UPLOAD_FAILED: 'Cover upload failed: ',
CREATE: 'Create',
CREATE_SUCCESS: 'Book created successfully',
CREATE_FAILED: 'Creation failed: ',
// EditBookModal
EDIT_BOOK_TITLE: 'Edit Book',
CHANGE_COVER: 'Change Cover',
SELECT_COVER: 'Select Cover',
COVER_UPDATE_SUCCESS: 'Cover updated successfully',
COVER_UPDATE_FAILED: 'Failed to update cover: ',
SAVE: 'Save',
SAVE_SUCCESS: 'Saved successfully',
SAVE_FAILED: 'Save failed: ',
// ManageBooksModal
MANAGE_BOOKS_TITLE: 'Manage Books',
SEARCH_BOOKS_PLACEHOLDER: 'Search books...',
IMPORT_BOOK: 'Import Book',
BOOK_AUTHOR_PREFIX: 'Author: ',
BOOK_DESC_PREFIX: '\nDescription: ',
BOOK_PROGRESS_PREFIX: '\nProgress: ',
DELETE_BOOK: 'Delete',
EDIT_BOOK: 'Edit',
DELETE_BOOK_TITLE: 'Delete Book',
DELETE_BOOK_DESC: 'Are you sure you want to delete "{title}"?\nThis action cannot be undone.',
DELETE_SUCCESS: 'Deleted successfully',
DELETE_FAILED: 'Delete failed: ',
BOOKS_ROOT_NOT_FOUND: 'Books root directory does not exist or is inaccessible',
NO_UNIMPORTED_BOOKS: 'No unimported book directories found',
DETECT_UNIMPORTED_FAILED: 'Failed to detect unimported books: ',
IMPORT_SUCCESS: 'Successfully imported book "{title}"',
IMPORT_FAILED: 'Failed to create book configuration: ',
// SwitchBookModal
SWITCH_BOOK_TITLE: 'Switch Book',
SEARCH_BOOK_PLACEHOLDER: 'Search books...',
BOOK_AUTHOR_LABEL: 'Author',
BOOK_PROGRESS_LABEL: 'Progress',
BOOK_WORDCOUNT_LABEL: 'Words',
BOOK_LASTMOD_LABEL: 'Last Modified',
SELECT_BOOK: 'Select',
// UnimportedBooksModal
UNIMPORTED_BOOKS_TITLE: 'Select Book Directory to Import',
NO_UNIMPORTED_FOLDERS: 'No unimported book directories found',
CLOSE: 'Close',
SEARCH_FOLDERS_PLACEHOLDER: 'Search directories...',
NO_MATCHING_FOLDERS: 'No matching directories',
IMPORT: 'Import',
SELECT_FOLDER_FIRST: 'Please select a directory first',
// ReferenceModal
REFERENCE_MODAL_TITLE: 'Add Reference',
REFERENCE_CONTENT: 'Reference Content',
REFERENCE_CONTENT_DESC: 'Please enter the details of the reference',
// NamePromptModal
NAME_LABEL: 'Name',
CONFIRM: 'Confirm',
CANCEL: 'Cancel',
// TemplateEditModal
TEMPLATE_EDIT_TITLE: 'Edit Template',
TEMPLATE_CREATE_TITLE: 'Create Template',
TEMPLATE_NAME: 'Template Name',
TEMPLATE_NAME_PLACEHOLDER: 'Enter template name',
TEMPLATE_DESC: 'Template Description',
TEMPLATE_DESC_PLACEHOLDER: 'Describe the template structure',
TEMPLATE_STRUCTURE: 'Chapter Tree',
ADD_FILE: 'Add File',
ADD_FOLDER: 'Add Folder',
DELETE_FOLDER_CONFIRM: 'Are you sure you want to delete this folder and all its contents?',
NEW_CHAPTER: 'New Chapter',
NEW_DIRECTORY: 'New Directory',
ENTER_NAME_PLACEHOLDER: 'Enter name',
TEMPLATE_NAME_REQUIRED: 'Please enter template name',
TEMPLATE_NODE_REQUIRED: 'Please add at least one node',
TEMPLATE_NAME_EXISTS: 'Template with this name already exists',
TEMPLATE_SAVE_SUCCESS: 'Template saved successfully',
TEMPLATE_SAVE_FAILED: 'Failed to save template, check console for details'
};
const managerTranslation: ManagerTranslation = {
// BookManager related
BOOK_EXISTS: 'Book already exists',
BOOK_NOT_FOUND: 'Book not found',
BOOK_FOLDER_NOT_FOUND: 'Book folder not found',
SAVE_CONFIG_FAILED: 'Failed to save configuration file',
IMPORT_BOOK_FAILED: 'Failed to import book',
UNKNOWN_AUTHOR: 'Unknown Author',
// TemplateManager related
TEMPLATE_TYPE_NOT_FOUND: 'Template "{type}" does not exist',
TEMPLATE_EXISTS: 'Template already exists',
TEMPLATE_SAVE_FAILED: 'Failed to save template',
// FileManager related
CREATE_FOLDER_FAILED: 'Failed to create folder',
CREATE_FILE_FAILED: 'Failed to create file',
READ_FILE_FAILED: 'Failed to read file',
WRITE_FILE_FAILED: 'Failed to write file',
// FocusManager related
BREAK_TIME_START: 'Break time started',
FOCUS_SUMMARY: 'Focus completed!\nDuration: {duration} minutes\nInterruptions: {interruptions}\nWords written: {words}',
// ReferenceManager 相关
REFERENCE_FILE_NAME: 'references.md',
REFERENCE_FILE_NOT_FOUND: 'Please create "references.md" file in the book directory first',
REFERENCE_FILE_ERROR: 'Reference file does not exist or type error',
SELECT_TEXT_TO_REFERENCE: 'Please select text to reference',
CHAPTER_INFO_ERROR: 'Unable to get current chapter information',
// ReferenceManager 菜单项
EDIT_REFERENCE: 'Edit Reference',
DELETE_REFERENCE: 'Delete Reference',
INSERT_REFERENCE: 'Insert Reference'
};
const toolbarModalTranslation: ToolbarModalTranslation = {
// CommunityModal
COMMUNITY_TITLE: 'Writing Community',
COMMUNITY_INTRO: 'Join the Billion Writes community to connect with other writers, get inspiration and feedback.',
FEATURE_SHARE_TITLE: 'Share Works',
FEATURE_SHARE_DESC: 'Share your creations and get reader feedback',
FEATURE_DISCUSS_TITLE: 'Writing Exchange',
FEATURE_DISCUSS_DESC: 'Discuss writing techniques with other creators',
FEATURE_CHALLENGE_TITLE: 'Writing Challenges',
FEATURE_CHALLENGE_DESC: 'Participate in community writing challenges to improve your skills',
JOIN_SECTION_TITLE: 'How to Join',
JOIN_SECTION_DESC: 'Copy and search the official account below',
OFFICIAL_ACCOUNT: 'Official Account: BilionWrites',
COPY_ACCOUNT: 'Copy Account',
// ContactModal
ABOUT_AUTHOR: 'About Author',
AUTHOR_INTRO_1: 'Hello, I\'m Yeban, a full-time writer and independent developer.',
AUTHOR_INTRO_2_1: 'This plugin is ',
AUTHOR_INTRO_2_2: 'a tool I developed to help authors write long-form content in Obsidian',
AUTHOR_INTRO_2_3: ', hoping to ',
AUTHOR_INTRO_2_4: 'make your writing process smoother and more enjoyable',
AUTHOR_INTRO_2_5: '.',
AUTHOR_INTRO_3: 'If this plugin helps with your writing, or if you\'d like to support my independent development and creation, feel free to buy me a coffee ☕.',
AUTHOR_INTRO_4_1: 'Your support means a lot',
AUTHOR_INTRO_4_2: ', it allows me to focus on developing more useful tools to aid your creative journey.',
DONATE_TEXT: 'Support the author:',
DONATE_BUTTON: 'Support',
MORE_INFO_TEXT: 'If you want to learn more about writing, creative techniques, or follow my future work updates, welcome to follow my social media.',
CONTACT_TITLE: 'Contact:',
CONTACT_WECHAT_OFFICIAL: 'WeChat Official',
CONTACT_XIAOHONGSHU: 'Xiaohongshu',
CONTACT_WECHAT: 'WeChat',
CONTACT_GITHUB: 'GitHub',
COPY_SUCCESS: '{type} copied to clipboard',
COPY_FAILED: 'Copy failed, please copy manually: {value}',
// DonateModal
DONATE_MODAL_TITLE: 'Support Me',
COMMUNITY_STATS_TITLE: 'Community Stats',
COMMUNITY_STATS_USERS: '1200+ users, 32 supporters',
COMMUNITY_STATS_WORDS: '5000+ words written daily',
DONATE_AMOUNT_COFFEE: 'Buy me a coffee',
DONATE_AMOUNT_CHAPTER: 'Chapter sponsor',
DONATE_AMOUNT_FEATURE: 'Feature builder',
DONATE_FEEDBACK_COFFEE: 'Thanks for your coffee support!',
DONATE_FEEDBACK_CHAPTER: 'You get one vote for new features',
DONATE_FEEDBACK_FEATURE: 'Join our beta testing group, contact via WeChat Official Account',
PAYMENT_WECHAT: 'WeChat Pay',
PAYMENT_ALIPAY: 'Alipay',
PAYMENT_KOFI: 'Ko-fi',
CURRENCY_UNIT: 'CNY'
};
const componentTranslation: ComponentTranslation = {
// ChapterTree
NEW_FILE: 'New File',
NEW_FOLDER: 'New Folder',
ENTER_FILE_NAME: 'Enter file name',
ENTER_FOLDER_NAME: 'Enter folder name',
OPEN_IN_NEW_TAB: 'Open in New Tab',
OPEN_IN_NEW_PANE: 'Open in New Pane',
MARK_AS_COMPLETE: 'Mark as Complete',
MARK_AS_DRAFT: 'Mark as Draft',
EXCLUDE_FROM_STATS: 'Exclude from Stats',
INCLUDE_IN_STATS: 'Include in Stats',
CREATE_COPY: 'Create Copy',
COPY_NAME: '{name} Copy',
RENAME: 'Rename',
DELETE: 'Delete',
DELETE_FILE_TITLE: 'Delete File',
DELETE_FILE_DESC: 'Are you sure you want to delete "{title}"?\nIt will be moved to system trash.',
DELETE_FOLDER_TITLE: 'Delete Folder',
DELETE_FOLDER_DESC: 'Are you sure you want to delete folder "{title}" and all its contents?\nThey will be moved to system trash.',
COPY_SUCCESS: 'Copy created successfully',
COPY_FAILED: 'Failed to create copy: {error}',
RENAME_SUCCESS: 'Renamed successfully',
RENAME_FAILED: 'Failed to rename: {error}',
DELETE_SUCCESS: 'Deleted successfully',
DELETE_FAILED: 'Failed to delete: {error}',
MOVE_FAILED: 'Failed to move',
SOURCE_NOT_FOUND: 'Source file not found',
TARGET_EXISTS: 'File with same name exists at target location',
TARGET_FOLDER_NOT_FOUND: 'Target folder not found',
EXCLUDED_NOTICE: '"{title}" has been excluded',
INCLUDED_NOTICE: '"{title}" has been included',
ENTER_NEW_NAME: 'Enter new name',
// FocusToolView
FOCUS_MODE: 'Focus Mode',
START_FOCUS: 'Start Focus',
EXIT: 'Exit',
FOCUS_SESSIONS: 'Focus Sessions',
INTERRUPTIONS: 'Interruptions',
CURRENT_WORDS: 'Current Words',
WORD_GOAL: 'Word Goal',
TOTAL_FOCUS_WORDS: 'Total Focus Words',
PAUSE: 'Pause',
RESUME: 'Resume',
END: 'End',
ENCOURAGEMENT_1: '🎉 Amazing! You\'ve reached your word goal! Keep it up~',
ENCOURAGEMENT_2: '✨ Excellent! Goal achieved! Let\'s keep moving forward!',
ENCOURAGEMENT_3: '🌟 Perfect! Goal reached! Maintain this enthusiasm!',
ENCOURAGEMENT_4: '🎯 Goal achieved! Your persistence is admirable!',
ENCOURAGEMENT_5: '💪 Outstanding performance! Goal completed! Keep going!',
EXIT_FOCUS: 'Exit Focus',
EXIT_FOCUS_DESC: 'Are you sure you want to exit? Current focus progress will be lost.',
END_FOCUS: 'End Focus',
END_FOCUS_DESC: 'Are you sure you want to end focus? This will count as an interruption.',
FOCUSING: 'Focusing',
PAUSED: 'Paused',
BREAK_TIME: 'Break Time',
READY_TO_START: 'Ready to Start'
};
// 合并所有翻译
const translation: Translation = {
...commonTranslation,
...bookSmithViewTranslation,
...toolViewTranslation,
...settingsTranslation,
...modalTranslation,
...managerTranslation,
...toolbarModalTranslation,
...componentTranslation
};
export default translation;

449
src/i18n/locales/zh-CN.ts Normal file
View file

@ -0,0 +1,449 @@
import {
Translation,
CommonTranslation,
BookSmithViewTranslation,
ToolViewTranslation,
SettingsTranslation,
ModalTranslation,
ManagerTranslation,
ToolbarModalTranslation,
ComponentTranslation
} from '../interfaces';
// 通用翻译
const commonTranslation: CommonTranslation = {
PLUGIN_NAME: 'BookSmith 书籍创作',
SETTINGS: '设置',
SAVE: '保存',
CANCEL: '取消',
HIDE: '隐藏',
SHOW: '显示'
};
// 书籍管理视图翻译
const bookSmithViewTranslation: BookSmithViewTranslation = {
// 主界面
BOOK_MANAGER: '书籍管理',
NEW_BOOK: '新建书籍',
SWITCH_BOOK: '切换书籍',
MANAGE_BOOK: '管理书籍',
// 书籍相关
BOOK_TITLE: '书名',
BOOK_AUTHOR: '作者',
BOOK_DESCRIPTION: '简介',
BOOK_TAGS: '标签',
BOOK_COVER: '封面',
// 章节相关
CHAPTER: '章节',
ADD_CHAPTER: '添加章节',
DELETE_CHAPTER: '删除章节',
RENAME_CHAPTER: '重命名章节',
// 统计相关
STATS: '统计',
WORD_COUNT: '字数',
CHAPTER_COUNT: '章节数',
// 帮助提示
HELP_TOOLTIP: `👋 欢迎使用 BookSmith
🚀 使
📚
📑
便
💡
BookSmith
💝
BookSmith
`,
// 通知消息
SWITCHED_TO_BOOK: '已切换到《{title}》',
IMPORTED_AND_SWITCHED: '已导入并切换到新书籍',
CURRENT_BOOK_DELETED: '当前书籍已被删除',
NO_BOOKS_TO_SWITCH: '暂无可切换的书籍',
// 统计文本
TODAY_WORDS: '今日字数',
TOTAL_WORDS: '字数统计',
CHAPTER_COMPLETION: '章节完成',
WRITING_DAYS: '写作天数',
AVERAGE_DAILY_WORDS: '日均字数',
WORD_UNIT: '字',
DAY_UNIT: '天',
TEN_THOUSAND: '万',
// 空状态提示
WELCOME_MESSAGE: '👋 欢迎使用 BookSmith',
EMPTY_STATE_HINT: '点击上方的"新建书籍"创建作品,或使用"切换"按钮选择已有书籍'
};
// 工具箱视图翻译
const toolViewTranslation: ToolViewTranslation = {
WRITING_TOOLBOX: '写作工具箱',
// 写作助手
WRITING_ASSISTANT: '写作助手',
FOCUS_MODE: '专注模式',
CREATIVE_INSPIRATION: '创作灵感',
CHARACTER_PROFILES: '人物档案',
WORLD_BUILDING: '世界构建',
// 导出发布
EXPORT_PUBLISH: '导出发布',
DESIGN_TYPOGRAPHY: '设计排版',
GENERATE_EBOOK: '生成电子书',
MORE_FEATURES: '更多功能...',
MORE_FEATURES_MESSAGE: '更多功能等你一起共创',
// 写作圈子
WRITING_COMMUNITY: '写作圈子',
CREATIVE_COMMUNITY: '创作社区',
CONTACT_AUTHOR: '联系作者',
DONATE_SUPPORT: '赞助捐赠',
// 面板设置
PANEL_SETTINGS: '面板设置',
FEATURE_COMING_SOON: '{feature}功能即将上线'
};
// 设置面板翻译
const settingsTranslation: SettingsTranslation = {
SETTINGS_TITLE: 'Book Smith 设置',
BASIC_SETTINGS: '基本设置',
TEMPLATE_SETTINGS: '模板设置',
WRITING_TOOLS_SETTINGS: '写作工具箱设置',
// 基本设置
LANGUAGE_SETTING: '语言',
LANGUAGE_DESC: '选择插件界面语言 / Choose plugin language',
DEFAULT_AUTHOR: '默认作者',
DEFAULT_AUTHOR_DESC: '创建新书籍时的默认作者名',
DEFAULT_AUTHOR_PLACEHOLDER: '输入默认作者名',
BOOK_STORAGE_PATH: '书籍存储路径',
BOOK_STORAGE_DESC: '新建书籍的默认存储路径',
STORAGE_PATH_CHANGED: '存储路径已更改,请重启 Obsidian 或重新加载以使更改生效',
// 模板设置
DEFAULT_TEMPLATE: '默认模板',
DEFAULT_TEMPLATE_DESC: '创建新书籍时使用的默认模板',
BOOK_TEMPLATES: '书籍模板',
ADD_NEW_TEMPLATE: '添加新模板',
EDIT_TEMPLATE: '编辑模板',
DELETE_TEMPLATE: '删除模板',
DELETE_TEMPLATE_TITLE: '删除模板',
DELETE_TEMPLATE_DESC: '确定要删除此模板吗?删除后无法恢复。',
// 写作工具箱设置
FOCUS_MODE_SETTINGS: '专注模式设置',
FOCUS_DURATION: '专注时长',
FOCUS_DURATION_DESC: '每个专注周期的工作时长(分钟)',
BREAK_DURATION: '间隔时长',
BREAK_DURATION_DESC: '每个专注周期后的休息时长(分钟)',
WORD_GOAL: '字数目标',
WORD_GOAL_DESC: '每个专注周期的目标字数'
};
// 模态框翻译
const modalTranslation: ModalTranslation = {
// 通用
COVER: '封面',
COVER_DESC: '选择封面图片(可选)',
BOOK_TITLE: '书名',
BOOK_TITLE_DESC: '请输入书籍标题',
BOOK_TITLE_PLACEHOLDER: '书名',
SUBTITLE: '副标题',
SUBTITLE_DESC: '可选',
SUBTITLE_PLACEHOLDER: '副标题',
TARGET_WORDS: '目标字数',
TARGET_WORDS_DESC: '设置本书预计总字数:万字',
TARGET_WORDS_PLACEHOLDER: '例如20或20.0万',
AUTHOR: '作者',
AUTHOR_DESC: '请输入作者名称,多个作者用逗号分隔',
AUTHOR_PLACEHOLDER: '作者',
DESCRIPTION: '简介',
DESCRIPTION_DESC: '请输入书籍简介',
DESCRIPTION_PLACEHOLDER: '书籍简介',
REQUIRED_FIELDS: '请填写必要信息',
// CreateBookModal
CREATE_BOOK_TITLE: '创建新书籍',
BOOK_TEMPLATE: '模板',
TEMPLATE_CHECK_DESC: '请选择书籍模板',
SELECT_IMAGE: '选择图片',
COVER_UPLOAD_SUCCESS: '封面上传成功',
COVER_UPLOAD_FAILED: '封面上传失败:',
CREATE: '创建',
CREATE_SUCCESS: '书籍创建成功',
CREATE_FAILED: '创建失败:',
// EditBookModal
EDIT_BOOK_TITLE: '编辑书籍',
CHANGE_COVER: '更换封面',
SELECT_COVER: '选择封面',
COVER_UPDATE_SUCCESS: '封面更新成功',
COVER_UPDATE_FAILED: '封面更新失败:',
SAVE: '保存',
SAVE_SUCCESS: '保存成功',
SAVE_FAILED: '保存失败:',
// ManageBooksModal
MANAGE_BOOKS_TITLE: '管理书籍',
SEARCH_BOOKS_PLACEHOLDER: '搜索书籍...',
IMPORT_BOOK: '导入书籍',
BOOK_AUTHOR_PREFIX: '作者:',
BOOK_DESC_PREFIX: '\n简介',
BOOK_PROGRESS_PREFIX: '\n创作轨迹',
DELETE_BOOK: '删除',
EDIT_BOOK: '编辑',
DELETE_BOOK_TITLE: '删除书籍',
DELETE_BOOK_DESC: '确定要删除《{title}》吗?\n此操作不可恢复。',
DELETE_SUCCESS: '删除成功',
DELETE_FAILED: '删除失败:',
BOOKS_ROOT_NOT_FOUND: '书籍根目录不存在或无法访问',
NO_UNIMPORTED_BOOKS: '没有找到未导入的书籍目录',
DETECT_UNIMPORTED_FAILED: '检测未导入书籍失败:',
IMPORT_SUCCESS: '成功导入书籍《{title}》',
IMPORT_FAILED: '创建书籍配置失败:',
// SwitchBookModal
SWITCH_BOOK_TITLE: '切换书籍',
SEARCH_BOOK_PLACEHOLDER: '搜索书籍...',
BOOK_AUTHOR_LABEL: '作者',
BOOK_PROGRESS_LABEL: '进度',
BOOK_WORDCOUNT_LABEL: '字数',
BOOK_LASTMOD_LABEL: '最后修改',
SELECT_BOOK: '选择',
// UnimportedBooksModal
UNIMPORTED_BOOKS_TITLE: '选择要导入的书籍目录',
NO_UNIMPORTED_FOLDERS: '没有找到未导入的书籍目录',
CLOSE: '关闭',
SEARCH_FOLDERS_PLACEHOLDER: '搜索目录...',
NO_MATCHING_FOLDERS: '没有匹配的目录',
IMPORT: '导入',
SELECT_FOLDER_FIRST: '请先选择一个目录',
// ReferenceModal
REFERENCE_MODAL_TITLE: '添加引用内容',
REFERENCE_CONTENT: '引用内容',
REFERENCE_CONTENT_DESC: '请输入引用内容的详细信息',
// ConfirmModal
// NamePromptModal
NAME_LABEL: '名称',
CONFIRM: '确定',
CANCEL: '取消',
// TemplateEditModal
TEMPLATE_EDIT_TITLE: '编辑模板',
TEMPLATE_CREATE_TITLE: '新建模板',
TEMPLATE_NAME: '模板名称',
TEMPLATE_NAME_PLACEHOLDER: '请输入模板名称',
TEMPLATE_DESC: '模板描述',
TEMPLATE_DESC_PLACEHOLDER: '可描述模板的简要结构',
TEMPLATE_STRUCTURE: '章节树',
ADD_FILE: '添加文件',
ADD_FOLDER: '添加文件夹',
DELETE_FOLDER_CONFIRM: '确定要删除此文件夹及其所有内容吗?',
NEW_CHAPTER: '新章节',
NEW_DIRECTORY: '新目录',
ENTER_NAME_PLACEHOLDER: '输入名称',
TEMPLATE_NAME_REQUIRED: '请输入模板名称',
TEMPLATE_NODE_REQUIRED: '请至少添加一个节点',
TEMPLATE_NAME_EXISTS: '已存在同名模板,请修改模板名称',
TEMPLATE_SAVE_SUCCESS: '模板保存成功',
TEMPLATE_SAVE_FAILED: '保存模板失败,请查看控制台了解详情'
};
const managerTranslation: ManagerTranslation = {
// BookManager 相关
BOOK_EXISTS: '书籍已存在',
BOOK_NOT_FOUND: '书籍不存在',
BOOK_FOLDER_NOT_FOUND: '书籍文件夹不存在',
SAVE_CONFIG_FAILED: '保存配置文件失败',
IMPORT_BOOK_FAILED: '导入书籍失败',
UNKNOWN_AUTHOR: '未知作者',
// TemplateManager 相关
TEMPLATE_TYPE_NOT_FOUND: '模板 "{type}" 不存在',
TEMPLATE_EXISTS: '模板已存在',
TEMPLATE_SAVE_FAILED: '保存模板失败',
// FileManager 相关
CREATE_FOLDER_FAILED: '创建文件夹失败',
CREATE_FILE_FAILED: '创建文件失败',
READ_FILE_FAILED: '读取文件失败',
WRITE_FILE_FAILED: '写入文件失败',
// FocusManager 相关
BREAK_TIME_START: '休息时间开始',
FOCUS_SUMMARY: '专注结束!\n完成时间{duration}分钟\n中断次数{interruptions}次\n本次字数{words}',
// ReferenceManager 相关
REFERENCE_FILE_NAME: '引用书目.md',
REFERENCE_FILE_NOT_FOUND: '请先在书籍目录下创建"引用书目.md"文件',
REFERENCE_FILE_ERROR: '引用书目文件不存在或类型错误',
SELECT_TEXT_TO_REFERENCE: '请选择要引用的文本',
CHAPTER_INFO_ERROR: '无法获取当前章节信息',
// ReferenceManager 菜单项
EDIT_REFERENCE: '编辑当前引用',
DELETE_REFERENCE: '删除当前引用',
INSERT_REFERENCE: '插入新引用'
};
const toolbarModalTranslation: ToolbarModalTranslation = {
// CommunityModal
COMMUNITY_TITLE: '创作社区',
COMMUNITY_INTRO: '加入亿万写作创作社区,与其他创作者交流,获取写作灵感和反馈。',
FEATURE_SHARE_TITLE: '作品分享',
FEATURE_SHARE_DESC: '分享你的创作,获取读者反馈',
FEATURE_DISCUSS_TITLE: '创作交流',
FEATURE_DISCUSS_DESC: '与其他创作者讨论写作技巧',
FEATURE_CHALLENGE_TITLE: '写作挑战',
FEATURE_CHALLENGE_DESC: '参与社区写作挑战,提升创作能力',
JOIN_SECTION_TITLE: '加入方式',
JOIN_SECTION_DESC: '复制下方公众号,搜索关注',
OFFICIAL_ACCOUNT: '公众号BilionWrites',
COPY_ACCOUNT: '复制公众号',
// ContactModal
ABOUT_AUTHOR: '关于作者',
AUTHOR_INTRO_1: '你好,我是【夜半】,一名全职写作与独立开发者。',
AUTHOR_INTRO_2_1: '这款插件是',
AUTHOR_INTRO_2_2: '我为了帮助在Obsidian中进行长篇创作的作者而开发的工具',
AUTHOR_INTRO_2_3: ',希望能',
AUTHOR_INTRO_2_4: '让你的写作过程更流畅,创作体验更愉悦',
AUTHOR_INTRO_2_5: '。',
AUTHOR_INTRO_3: '如果这款插件对你的写作有所帮助,或者你愿意支持我的独立开发与创作,欢迎请我喝咖啡☕。',
AUTHOR_INTRO_4_1: '你的支持意义重大',
AUTHOR_INTRO_4_2: ',它能让我更专注地开发更多实用工具,助力你的创作之旅。',
DONATE_TEXT: '如需支持作者:',
DONATE_BUTTON: '赞赏支持',
MORE_INFO_TEXT: '如果你想了解更多关于写作、创作技巧的内容,或者关注我未来的作品动态,欢迎关注我的社交媒体。',
CONTACT_TITLE: '联系方式:',
CONTACT_WECHAT_OFFICIAL: '公众号',
CONTACT_XIAOHONGSHU: '小红书',
CONTACT_WECHAT: '微信',
CONTACT_GITHUB: 'GitHub',
COPY_SUCCESS: '{type}已复制到剪贴板',
COPY_FAILED: '复制失败,请手动复制:{value}',
// DonateModal
DONATE_MODAL_TITLE: '笔墨有情',
COMMUNITY_STATS_TITLE: '社区数据',
COMMUNITY_STATS_USERS: '已有 1200+ 用户32位支持者',
COMMUNITY_STATS_WORDS: '平均每天创作 5000+ 字',
DONATE_AMOUNT_COFFEE: '暖心咖啡',
DONATE_AMOUNT_CHAPTER: '章节赞助',
DONATE_AMOUNT_FEATURE: '功能共建',
DONATE_FEEDBACK_COFFEE: '感谢您的咖啡赞助!',
DONATE_FEEDBACK_CHAPTER: '给您一次新功能投票权',
DONATE_FEEDBACK_FEATURE: '邀请您加入内测社群,公众号链接我',
PAYMENT_WECHAT: '微信赞赏',
PAYMENT_ALIPAY: '支付宝赞赏',
PAYMENT_KOFI: 'Ko-fi 赞赏',
CURRENCY_UNIT: '元'
};
const componentTranslation: ComponentTranslation = {
// ChapterTree
NEW_FILE: '新建文件',
NEW_FOLDER: '新建文件夹',
ENTER_FILE_NAME: '请输入文件名',
ENTER_FOLDER_NAME: '请输入文件夹名',
OPEN_IN_NEW_TAB: '在新标签页中打开',
OPEN_IN_NEW_PANE: '在新标签组中打开',
MARK_AS_COMPLETE: '标记完成章节',
MARK_AS_DRAFT: '标记重新创作',
EXCLUDE_FROM_STATS: '排除在统计与导出外',
INCLUDE_IN_STATS: '包含在统计与导出中',
CREATE_COPY: '创建副本',
COPY_NAME: '{name} 副本',
RENAME: '重命名',
DELETE: '删除',
DELETE_FILE_TITLE: '删除文件',
DELETE_FILE_DESC: '确定要删除文件 "{title}" 吗?\n它将被移动到系统回收站。',
DELETE_FOLDER_TITLE: '删除文件夹',
DELETE_FOLDER_DESC: '确定要删除文件夹 "{title}" 及其所有内容吗?\n它们将被移动到系统回收站。',
COPY_SUCCESS: '创建副本成功',
COPY_FAILED: '创建副本失败: {error}',
RENAME_SUCCESS: '重命名成功',
RENAME_FAILED: '重命名失败: {error}',
DELETE_SUCCESS: '删除成功',
DELETE_FAILED: '删除失败: {error}',
MOVE_FAILED: '移动失败',
SOURCE_NOT_FOUND: '源文件不存在',
TARGET_EXISTS: '目标位置已存在同名文件',
TARGET_FOLDER_NOT_FOUND: '目标文件夹不存在',
EXCLUDED_NOTICE: '已将"{title}"排除',
INCLUDED_NOTICE: '已将"{title}"包含',
ENTER_NEW_NAME: '请输入新名称',
// FocusToolView
FOCUS_MODE: '专注模式',
START_FOCUS: '开始专注',
EXIT: '退出',
FOCUS_SESSIONS: '专注次数',
INTERRUPTIONS: '中断次数',
CURRENT_WORDS: '当前字数',
WORD_GOAL: '目标字数',
TOTAL_FOCUS_WORDS: '专注总字数',
PAUSE: '暂停',
RESUME: '继续',
END: '结束',
ENCOURAGEMENT_1: '🎉 太棒了!已达到目标字数!继续保持~',
ENCOURAGEMENT_2: '✨ 厉害!目标达成!让我们继续前进!',
ENCOURAGEMENT_3: '🌟 完美!达到目标了!保持这份热情!',
ENCOURAGEMENT_4: '🎯 目标达成!你的坚持值得表扬!',
ENCOURAGEMENT_5: '💪 出色的表现!目标完成!再接再厉!',
EXIT_FOCUS: '退出专注',
EXIT_FOCUS_DESC: '确定要退出专注吗?当前专注进度将会丢失。',
END_FOCUS: '结束专注',
END_FOCUS_DESC: '确定要结束专注吗?这将计入中断次数。',
FOCUSING: '专注中',
PAUSED: '已暂停',
BREAK_TIME: '休息时间',
READY_TO_START: '准备开始'
};
// 合并所有翻译
const translation: Translation = {
...commonTranslation,
...bookSmithViewTranslation,
...toolViewTranslation,
...settingsTranslation,
...modalTranslation,
...managerTranslation,
...toolbarModalTranslation,
...componentTranslation
};
export default translation;

View file

@ -7,6 +7,7 @@ import { activateView } from './utils/viewUtils';
import { BookManager } from './services/BookManager';
import { TemplateManager } from './services/TemplateManager';
import { BookStatsManager } from './services/BookStatsManager';
import { i18n, Locale } from './i18n/i18n';
export default class BookSmithPlugin extends Plugin {
settings: BookSmithSettings;
@ -16,6 +17,7 @@ export default class BookSmithPlugin extends Plugin {
async onload() {
await this.loadSettings();
i18n.setLocale(this.settings.language);
// 初始化所有管理器
this.bookManager = new BookManager(this.app, this.settings);

View file

@ -1,8 +1,9 @@
import { BaseModal } from "./BaseModal";
import { i18n } from '../i18n/i18n';
export class CommunityModal extends BaseModal {
constructor(container: HTMLElement) {
super(container, '创作社区');
super(container, i18n.t('COMMUNITY_TITLE'));
}
protected createContent() {
@ -10,34 +11,30 @@ export class CommunityModal extends BaseModal {
// 社区介绍
const intro = content.createDiv({ cls: 'community-intro' });
intro.createEl('p', {
text: '加入亿万写作创作社区,与其他创作者交流,获取写作灵感和反馈。'
});
intro.createEl('p', { text: i18n.t('COMMUNITY_INTRO') });
// 社区功能卡片
const features = content.createDiv({ cls: 'community-features' });
this.createFeatureCard(features, '📝', '作品分享', '分享你的创作,获取读者反馈');
this.createFeatureCard(features, '👥', '创作交流', '与其他创作者讨论写作技巧');
this.createFeatureCard(features, '🏆', '写作挑战', '参与社区写作挑战,提升创作能力');
this.createFeatureCard(features, '📝', i18n.t('FEATURE_SHARE_TITLE'), i18n.t('FEATURE_SHARE_DESC'));
this.createFeatureCard(features, '👥', i18n.t('FEATURE_DISCUSS_TITLE'), i18n.t('FEATURE_DISCUSS_DESC'));
this.createFeatureCard(features, '🏆', i18n.t('FEATURE_CHALLENGE_TITLE'), i18n.t('FEATURE_CHALLENGE_DESC'));
// 加入方式
const joinSection = content.createDiv({ cls: 'join-section' });
joinSection.createEl('h3', { text: '加入方式' });
joinSection.createEl('h3', { text: i18n.t('JOIN_SECTION_TITLE') });
const joinInfo = joinSection.createDiv({ cls: 'join-info' });
joinInfo.createEl('p', { text: '复制下方公众号,搜索关注' });
joinInfo.createEl('p', { text: '公众号BilionWrites', cls: 'account-name' });
joinInfo.createEl('p', { text: i18n.t('JOIN_SECTION_DESC') });
joinInfo.createEl('p', { text: i18n.t('OFFICIAL_ACCOUNT'), cls: 'account-name' });
// 加入按钮
const joinBtn = content.createDiv({ cls: 'join-btn', text: '复制公众号' });
const joinBtn = content.createDiv({ cls: 'join-btn', text: i18n.t('COPY_ACCOUNT') });
joinBtn.addEventListener('click', () => {
navigator.clipboard.writeText('BilionWrites').then(() => {
this.showNotice('公众号已复制到剪贴板');
this.showNotice(i18n.t('COPY_SUCCESS', { type: i18n.t('OFFICIAL_ACCOUNT') }));
}).catch(() => {
this.showNotice('复制失败请手动复制BilionWrites');
this.showNotice(i18n.t('COPY_FAILED', { value: 'BilionWrites' }));
});
});
}

View file

@ -1,4 +1,5 @@
import { App, Modal } from 'obsidian';
import { i18n } from '../i18n/i18n';
export class ConfirmModal extends Modal {
private confirmed = false;
@ -25,13 +26,13 @@ export class ConfirmModal extends Modal {
const buttonContainer = contentEl.createDiv({ cls: 'modal-button-container' });
// 取消按钮
buttonContainer.createEl('button', { text: '取消' })
buttonContainer.createEl('button', { text: i18n.t('CANCEL') })
.addEventListener('click', () => this.close());
// 确认按钮
const confirmButton = buttonContainer.createEl('button', {
cls: 'mod-cta',
text: '确认'
text: i18n.t('CONFIRM')
});
confirmButton.addEventListener('click', () => {
this.confirmed = true;

View file

@ -1,83 +1,69 @@
import { BaseModal } from "./BaseModal";
import { setIcon } from "obsidian";
import { i18n } from '../i18n/i18n';
export class ContactModal extends BaseModal {
constructor(container: HTMLElement) {
super(container, '关于作者');
super(container, i18n.t('ABOUT_AUTHOR'));
}
protected createContent() {
const content = this.element.createDiv({ cls: 'contact-content' });
// 作者介绍
const intro = content.createDiv({ cls: 'author-intro' });
intro.createEl('p', { text: i18n.t('AUTHOR_INTRO_1') });
intro.createEl('p', {
text: '你好,我是【夜半】,一名全职写作与独立开发者。'
});
// 使用HTML元素添加带有强调样式的文本
const p1 = intro.createEl('p');
p1.createSpan({ text: '这款插件是' });
p1.createSpan({ text: '我为了帮助在Obsidian中进行长篇创作的作者而开发的工具', cls: 'text-accent' });
p1.createSpan({ text: ',希望能' });
p1.createSpan({ text: '让你的写作过程更流畅,创作体验更愉悦', cls: 'text-accent' });
p1.createSpan({ text: '。' });
p1.createSpan({ text: i18n.t('AUTHOR_INTRO_2_1') });
p1.createSpan({ text: i18n.t('AUTHOR_INTRO_2_2'), cls: 'text-accent' });
p1.createSpan({ text: i18n.t('AUTHOR_INTRO_2_3') });
p1.createSpan({ text: i18n.t('AUTHOR_INTRO_2_4'), cls: 'text-accent' });
p1.createSpan({ text: i18n.t('AUTHOR_INTRO_2_5') });
const p2 = intro.createEl('p');
p2.createSpan({ text: '如果这款插件对你的写作有所帮助,或者你愿意支持我的独立开发与创作,欢迎请我喝咖啡☕。', cls: 'text-accent' });
p2.createSpan({ text: i18n.t('AUTHOR_INTRO_3'), cls: 'text-accent' });
const p3 = intro.createEl('p');
p3.createSpan({ text: '你的支持意义重大', cls: 'text-accent' });
p3.createSpan({ text: ',它能让我更专注地开发更多实用工具,助力你的创作之旅。' });
p3.createSpan({ text: i18n.t('AUTHOR_INTRO_4_1'), cls: 'text-accent' });
p3.createSpan({ text: i18n.t('AUTHOR_INTRO_4_2') });
// 赞赏支持区域 - 改为卡片式设计
const donateSection = content.createDiv({ cls: 'info-card donate-section' });
donateSection.createSpan({ cls: 'donate-text', text: '如需支持作者:' });
const donateBtn = donateSection.createDiv({ cls: 'donate-button', text: '赞赏支持' });
donateSection.createSpan({ cls: 'donate-text', text: i18n.t('DONATE_TEXT') });
const donateBtn = donateSection.createDiv({ cls: 'donate-button', text: i18n.t('DONATE_BUTTON') });
donateBtn.addEventListener('click', () => {
this.close();
const event = new CustomEvent('open-donate-modal');
document.dispatchEvent(event);
});
// 了解更多区域 - 改为卡片式设计
const moreInfo = content.createDiv({ cls: 'info-card more-info-section' });
moreInfo.createEl('p', {
text: '如果你想了解更多关于写作、创作技巧的内容,或者关注我未来的作品动态,欢迎关注我的社交媒体。'
});
moreInfo.createEl('p', { text: i18n.t('MORE_INFO_TEXT') });
// 联系方式标题
content.createEl('h3', { text: '联系方式:', cls: 'contact-title' });
content.createEl('h3', { text: i18n.t('CONTACT_TITLE'), cls: 'contact-title' });
// 联系方式卡片
const contactMethods = content.createDiv({ cls: 'contact-methods' });
this.createContactCard(contactMethods, 'file-text', '公众号', '夜半');
this.createContactCard(contactMethods, 'book-open', '小红书', '夜半Yeban');
this.createContactCard(contactMethods, 'message-circle', '微信', 'Bruce169229添加注明来意');
this.createContactCard(contactMethods, 'globe', 'GitHub', 'https://github.com/Yeban8090');
this.createContactCard(contactMethods, 'file-text', i18n.t('CONTACT_WECHAT_OFFICIAL'), '夜半');
this.createContactCard(contactMethods, 'book-open', i18n.t('CONTACT_XIAOHONGSHU'), '夜半Yeban');
this.createContactCard(contactMethods, 'message-circle', i18n.t('CONTACT_WECHAT'), 'Bruce169229添加注明来意');
this.createContactCard(contactMethods, 'globe', i18n.t('CONTACT_GITHUB'), 'https://github.com/Yeban8090');
}
private createContactCard(container: HTMLElement, icon: string, title: string, value: string) {
const card = container.createDiv({ cls: 'contact-card' });
// 图标容器
const iconContainer = card.createDiv({ cls: 'contact-icon-container' });
setIcon(iconContainer, icon);
// 内容区域
const contentContainer = card.createDiv({ cls: 'contact-info' });
contentContainer.createDiv({ cls: 'contact-label', text: title });
contentContainer.createDiv({ cls: 'contact-value', text: value });
card.addEventListener('click', () => {
navigator.clipboard.writeText(value).then(() => {
this.showNotice(`${title}已复制到剪贴板`);
this.showNotice(i18n.t('COPY_SUCCESS', { type: title }));
}).catch(() => {
this.showNotice(`复制失败,请手动复制:${value}`);
this.showNotice(i18n.t('COPY_FAILED', { value }));
});
});
}

View file

@ -2,6 +2,7 @@ import { App, Modal, Setting, Notice } from 'obsidian';
import { BookBasicInfo, Book } from '../types/book';
import BookSmithPlugin from '../main';
import { TemplateManager } from '../services/TemplateManager';
import { i18n } from '../i18n/i18n';
export class CreateBookModal extends Modal {
private bookInfo: Partial<BookBasicInfo> = {
@ -23,12 +24,12 @@ export class CreateBookModal extends Modal {
onOpen() {
const { contentEl } = this;
contentEl.addClass('book-smith-create-book-modal');
contentEl.createEl('h2', { text: '创建新书籍' });
contentEl.createEl('h2', { text: i18n.t('CREATE_BOOK_TITLE') });
// 添加模板选择
new Setting(contentEl)
.setName('模板')
.setDesc('请选择书籍模板')
.setName(i18n.t('BOOK_TEMPLATE'))
.setDesc(i18n.t('TEMPLATE_CHECK_DESC'))
.addDropdown(dropdown => {
const templates = this.templateManager.getAllTemplateTypes();
templates.forEach(template => {
@ -43,10 +44,10 @@ export class CreateBookModal extends Modal {
});
// 添加封面上传
new Setting(contentEl)
.setName('封面')
.setDesc('选择封面图片(可选)')
.setName(i18n.t('COVER'))
.setDesc(i18n.t('COVER_DESC'))
.addButton(button => button
.setButtonText('选择图片')
.setButtonText(i18n.t('SELECT_IMAGE'))
.onClick(async () => {
const input = document.createElement('input');
input.type = 'file';
@ -66,33 +67,34 @@ export class CreateBookModal extends Modal {
const coverPath = `${coversPath}/${fileName}`;
await this.plugin.app.vault.adapter.writeBinary(coverPath, await file.arrayBuffer());
this.bookInfo.cover = coverPath;
new Notice('封面上传成功');
new Notice(i18n.t('COVER_UPLOAD_SUCCESS'));
} catch (error) {
new Notice('封面上传失败:' + error.message);
new Notice(i18n.t('COVER_UPLOAD_FAILED') + error.message);
}
}
};
input.click();
}));
new Setting(contentEl)
.setName('书名')
.setDesc('请输入书籍标题')
.setName(i18n.t('BOOK_TITLE'))
.setDesc(i18n.t('BOOK_TITLE_DESC'))
.addText(text => text
.setPlaceholder('书名')
.setPlaceholder(i18n.t('BOOK_TITLE_PLACEHOLDER'))
.onChange(value => this.bookInfo.title = value));
new Setting(contentEl)
.setName('副标题')
.setDesc('可选')
.setName(i18n.t('SUBTITLE'))
.setDesc(i18n.t('SUBTITLE_DESC'))
.addText(text => text
.setPlaceholder('副标题')
.setPlaceholder(i18n.t('SUBTITLE_PLACEHOLDER'))
.onChange(value => this.bookInfo.subtitle = value));
// 添加目标字数设置
new Setting(contentEl)
.setName('目标字数')
.setDesc('设置本书预计总字数:万字')
.setName(i18n.t('TARGET_WORDS'))
.setDesc(i18n.t('TARGET_WORDS_DESC'))
.addText(text => text
.setPlaceholder('例如20或20.0万')
.setPlaceholder(i18n.t('TARGET_WORDS_PLACEHOLDER'))
.onChange(value => {
// 处理输入的字数
const match = value.match(/^(\d+(?:\.\d+)?)万?$/);
@ -103,35 +105,29 @@ export class CreateBookModal extends Modal {
this.targetTotalWords = 10000;
}
}));
new Setting(contentEl)
.setName('作者')
.setDesc('请输入作者名称,多个作者用逗号分隔')
.setName(i18n.t('AUTHOR'))
.setDesc(i18n.t('AUTHOR_DESC'))
.addText(text => text
.setPlaceholder('作者')
.setPlaceholder(i18n.t('AUTHOR_PLACEHOLDER'))
.setValue(this.plugin.settings.defaultAuthor || '')
.onChange(value => this.bookInfo.author = value ? value.split(',') : []));
new Setting(contentEl)
.setName('简介')
.setDesc('请输入书籍简介')
.setName(i18n.t('DESCRIPTION'))
.setDesc(i18n.t('DESCRIPTION_DESC'))
.addTextArea(text => text
.setPlaceholder('书籍简介')
.setPlaceholder(i18n.t('DESCRIPTION_PLACEHOLDER'))
.onChange(value => this.bookInfo.desc = value));
// 修改创建按钮的处理
new Setting(contentEl)
.addButton(btn => btn
.setButtonText('创建')
.setButtonText(i18n.t('CREATE'))
.setCta()
.onClick(async () => {
if (!this.validateBookInfo()) {
new Notice('请填写必要信息');
new Notice(i18n.t('REQUIRED_FIELDS'));
return;
}
try {
@ -140,13 +136,13 @@ export class CreateBookModal extends Modal {
this.selectedTemplate,
this.targetTotalWords
);
new Notice('书籍创建成功');
new Notice(i18n.t('CREATE_SUCCESS'));
if (this.onBookCreated) {
this.onBookCreated(newBook);
}
this.close();
} catch (error) {
new Notice(`创建失败: ${error.message}`);
new Notice(i18n.t('CREATE_FAILED') + error.message);
}
}));
}

View file

@ -2,17 +2,18 @@ import { Notice } from "obsidian";
import { BaseModal } from "./BaseModal";
import { WechatQRCode } from '../assets/wechat-qrcode';
import { AlipayQRCode } from '../assets/alipay-qrcode';
import { i18n } from '../i18n/i18n';
export class DonateModal extends BaseModal {
private selectedAmount: number = 6;
private amounts = [
{ value: 6, label: '暖心咖啡', icon: '☕️', feedback: '感谢您的咖啡赞助!' },
{ value: 18, label: '章节赞助', icon: '📖', feedback: '给您一次新功能投票权' },
{ value: 66, label: '功能共建', icon: '🎨', feedback: '邀请您加入内测社群,公众号链接我' }
{ value: 6, label: i18n.t('DONATE_AMOUNT_COFFEE'), icon: '☕️', feedback: i18n.t('DONATE_FEEDBACK_COFFEE') },
{ value: 18, label: i18n.t('DONATE_AMOUNT_CHAPTER'), icon: '📖', feedback: i18n.t('DONATE_FEEDBACK_CHAPTER') },
{ value: 66, label: i18n.t('DONATE_AMOUNT_FEATURE'), icon: '🎨', feedback: i18n.t('DONATE_FEEDBACK_FEATURE') }
];
constructor(container: HTMLElement) {
super(container, '笔墨有情');
super(container, i18n.t('DONATE_MODAL_TITLE'));
}
protected createContent() {
@ -23,38 +24,33 @@ export class DonateModal extends BaseModal {
private createCommunityStats() {
const communityStats = this.element.createDiv({ cls: 'book-smith-community-stats' });
// 社区数据卡片
const statsCard = communityStats.createDiv({ cls: 'stats-card' });
// 图标和标题
const header = statsCard.createDiv({ cls: 'stats-header' });
header.createSpan({ text: '📊', cls: 'stats-icon' });
header.createSpan({ text: '社区数据', cls: 'stats-title' });
header.createSpan({ text: i18n.t('COMMUNITY_STATS_TITLE'), cls: 'stats-title' });
// 数据列表
const statsList = statsCard.createDiv({ cls: 'stats-list' });
statsList.createEl('p', {
text: '已有 1200+ 用户32位支持者',
text: i18n.t('COMMUNITY_STATS_USERS'),
cls: 'stats-item'
});
statsList.createEl('p', {
text: '平均每天创作 5000+ 字',
text: i18n.t('COMMUNITY_STATS_WORDS'),
cls: 'stats-item'
});
}
private createAmountPanel() {
const panel = this.element.createDiv({ cls: 'book-smith-amount-panel' });
// 预设金额
const presets = panel.createDiv({ cls: 'amount-presets' });
this.amounts.forEach(amount => {
const btn = presets.createDiv({ cls: 'amount-btn' });
const content = btn.createDiv({ cls: 'amount-content' });
content.createSpan({ cls: 'amount-icon', text: amount.icon });
content.createSpan({ cls: 'amount-label', text: amount.label });
content.createSpan({ cls: 'amount-value', text: `${amount.value}` });
content.createSpan({ cls: 'amount-value', text: `${amount.value}${i18n.t('CURRENCY_UNIT')}` });
if (amount.value === this.selectedAmount) {
btn.addClass('selected');
@ -71,15 +67,18 @@ export class DonateModal extends BaseModal {
private createPaymentChannels() {
const channels = this.element.createDiv({ cls: 'payment-channels' });
// 支付方式选项卡
const tabs = channels.createDiv({ cls: 'payment-tabs' });
const wechatTab = tabs.createDiv({
cls: 'payment-tab active',
text: '微信赞赏'
text: i18n.t('PAYMENT_WECHAT')
});
const alipayTab = tabs.createDiv({
cls: 'payment-tab',
text: '支付宝赞赏'
text: i18n.t('PAYMENT_ALIPAY')
});
const kofiTab = tabs.createDiv({
cls: 'payment-tab',
text: i18n.t('PAYMENT_KOFI')
});
// 二维码展示区
@ -88,7 +87,7 @@ export class DonateModal extends BaseModal {
wechatQR.createEl('img', {
attr: {
src: WechatQRCode,
alt: '微信支付'
alt: i18n.t('PAYMENT_WECHAT')
}
});
@ -96,24 +95,43 @@ export class DonateModal extends BaseModal {
alipayQR.createEl('img', {
attr: {
src: AlipayQRCode,
alt: '支付宝'
alt: i18n.t('PAYMENT_ALIPAY')
}
});
const kofiQR = qrcodeContainer.createDiv({ cls: 'qrcode-item' });
const kofiLink = kofiQR.createEl('a', {
cls: 'kofi-link',
href: 'https://ko-fi.com/bruceyeban',
attr: { target: '_blank' }
});
kofiLink.createEl('img', {
attr: {
src: 'https://storage.ko-fi.com/cdn/kofi3.png?v=3',
alt: i18n.t('PAYMENT_KOFI'),
style: 'height: 50px;'
}
});
// 切换逻辑
wechatTab.addEventListener('click', () => {
wechatTab.addClass('active');
alipayTab.removeClass('active');
wechatQR.addClass('active');
alipayQR.removeClass('active');
this.activateTab(wechatTab, wechatQR, [alipayTab, kofiTab], [alipayQR, kofiQR]);
});
alipayTab.addEventListener('click', () => {
alipayTab.addClass('active');
wechatTab.removeClass('active');
alipayQR.addClass('active');
wechatQR.removeClass('active');
this.activateTab(alipayTab, alipayQR, [wechatTab, kofiTab], [wechatQR, kofiQR]);
});
kofiTab.addEventListener('click', () => {
this.activateTab(kofiTab, kofiQR, [wechatTab, alipayTab], [wechatQR, alipayQR]);
});
}
private activateTab(activeTab: HTMLElement, activeContent: HTMLElement, inactiveTabs: HTMLElement[], inactiveContents: HTMLElement[]) {
activeTab.addClass('active');
activeContent.addClass('active');
inactiveTabs.forEach(tab => tab.removeClass('active'));
inactiveContents.forEach(content => content.removeClass('active'));
}
private selectAmount(amount: number) {

View file

@ -2,6 +2,8 @@ import { App, Modal, Setting, Notice } from 'obsidian';
import { Book, BookBasicInfo } from '../types/book';
import { BookManager } from '../services/BookManager';
import BookSmithPlugin from '../main';
import { i18n } from '../i18n/i18n';
export class EditBookModal extends Modal {
private bookInfo: Partial<BookBasicInfo>;
private targetTotalWords: number;
@ -21,13 +23,13 @@ export class EditBookModal extends Modal {
onOpen() {
const { contentEl } = this;
contentEl.addClass('book-smith-edit-book-modal');
contentEl.createEl('h2', { text: '编辑书籍' });
contentEl.createEl('h2', { text: i18n.t('EDIT_BOOK_TITLE') });
new Setting(contentEl)
.setName('封面')
.setDesc('选择封面图片(可选)')
.setName(i18n.t('COVER'))
.setDesc(i18n.t('COVER_DESC'))
.addButton(button => button
.setButtonText(this.bookInfo.cover ? '更换封面' : '选择封面')
.setButtonText(this.bookInfo.cover ? i18n.t('CHANGE_COVER') : i18n.t('SELECT_COVER'))
.onClick(async () => {
const input = document.createElement('input');
input.type = 'file';
@ -36,49 +38,45 @@ export class EditBookModal extends Modal {
const file = input.files?.[0];
if (file) {
try {
// 确保 covers 目录存在
const coversPath = `${this.plugin.settings.defaultBookPath}/covers`;
if (!await this.app.vault.adapter.exists(coversPath)) {
await this.app.vault.adapter.mkdir(coversPath);
}
// 复制图片到插件目录
const fileName = `cover-${Date.now()}.${file.name.split('.').pop()}`;
const coverPath = `${coversPath}/${fileName}`;
await this.app.vault.adapter.writeBinary(coverPath, await file.arrayBuffer());
this.bookInfo.cover = coverPath;
new Notice('封面更新成功');
new Notice(i18n.t('COVER_UPDATE_SUCCESS'));
} catch (error) {
new Notice('封面更新失败:' + error.message);
new Notice(i18n.t('COVER_UPDATE_FAILED') + error.message);
}
}
};
input.click();
}));
new Setting(contentEl)
.setName('书名')
.setDesc('请输入书籍标题')
.setName(i18n.t('BOOK_TITLE'))
.setDesc(i18n.t('BOOK_TITLE_DESC'))
.addText(text => text
.setValue(this.bookInfo.title || '')
.onChange(value => this.bookInfo.title = value));
new Setting(contentEl)
.setName('副标题')
.setDesc('可选')
.setName(i18n.t('SUBTITLE'))
.setDesc(i18n.t('SUBTITLE_DESC'))
.addText(text => text
.setValue(this.bookInfo.subtitle || '')
.onChange(value => this.bookInfo.subtitle = value));
new Setting(contentEl)
.setName('目标字数')
.setDesc('设置本书预计总字数:万字')
.setName(i18n.t('TARGET_WORDS'))
.setDesc(i18n.t('TARGET_WORDS_DESC'))
.addText(text => text
.setPlaceholder('例如20或20.0万')
.setPlaceholder(i18n.t('TARGET_WORDS_PLACEHOLDER'))
.setValue(`${Math.floor(this.targetTotalWords / 10000)}`)
.onChange(value => {
// 处理输入的字数
const match = value.match(/^(\d+(?:\.\d+)?)万?$/);
if (match) {
const num = parseFloat(match[1]) * 10000;
@ -87,28 +85,28 @@ export class EditBookModal extends Modal {
this.targetTotalWords = 10000;
}
}));
new Setting(contentEl)
.setName('作者')
.setDesc('请输入作者名称,多个作者用逗号分隔')
.setName(i18n.t('AUTHOR'))
.setDesc(i18n.t('AUTHOR_DESC'))
.addText(text => text
.setValue(this.bookInfo.author?.join(',') || '')
.onChange(value => this.bookInfo.author = value ? value.split(',') : []));
new Setting(contentEl)
.setName('简介')
.setDesc('请输入书籍简介')
.setName(i18n.t('DESCRIPTION'))
.setDesc(i18n.t('DESCRIPTION_DESC'))
.addTextArea(text => text
.setValue(this.bookInfo.desc || '')
.onChange(value => this.bookInfo.desc = value));
// 修改保存按钮的处理
new Setting(contentEl)
.addButton(btn => btn
.setButtonText('保存')
.setButtonText(i18n.t('SAVE'))
.setCta()
.onClick(async () => {
if (!this.validateBookInfo()) {
new Notice('请填写必要信息');
new Notice(i18n.t('REQUIRED_FIELDS'));
return;
}
try {
@ -119,11 +117,11 @@ export class EditBookModal extends Modal {
target_total_words: this.targetTotalWords
}
});
new Notice('保存成功');
new Notice(i18n.t('SAVE_SUCCESS'));
this.close();
this.onSaved?.({ type: 'edited', bookId: this.book.basic.uuid });
} catch (error) {
new Notice(`保存失败: ${error.message}`);
new Notice(i18n.t('SAVE_FAILED') + error.message);
}
}));
}

View file

@ -4,6 +4,7 @@ import { ConfirmModal } from './ConfirmModal';
import { UnimportedBooksModal } from './UnimportedBooksModal'; // 添加导入
import BookSmithPlugin from '../main';
import { Book } from '../types/book';
import { i18n } from '../i18n/i18n';
export class ManageBooksModal extends Modal {
constructor(
@ -22,7 +23,7 @@ export class ManageBooksModal extends Modal {
const { contentEl } = this;
contentEl.empty();
contentEl.addClass('book-smith-manage-books-modal');
contentEl.createEl('h2', { text: '管理书籍' });
contentEl.createEl('h2', { text: i18n.t('MANAGE_BOOKS_TITLE') });
// 添加搜索框和导入按钮的容器
const topContainer = contentEl.createDiv({ cls: 'book-smith-manage-top-container' });
@ -31,17 +32,13 @@ export class ManageBooksModal extends Modal {
const searchContainer = topContainer.createDiv({ cls: 'book-smith-manage-search-container' });
this.searchInput = searchContainer.createEl('input', {
type: 'text',
placeholder: '搜索书籍...',
placeholder: i18n.t('SEARCH_BOOKS_PLACEHOLDER'),
cls: 'book-smith-manage-search-input'
});
this.searchInput.addEventListener('input', () => {
this.renderBooks(this.filterBooks());
});
// 添加导入按钮
const importButton = topContainer.createEl('button', {
text: '导入书籍',
text: i18n.t('IMPORT_BOOK'),
cls: 'book-smith-import-button'
});
@ -60,18 +57,6 @@ export class ManageBooksModal extends Modal {
this.renderBooks(this.books);
}
private filterBooks(): Book[] {
const searchTerm = this.searchInput.value.toLowerCase();
if (!searchTerm) return this.books;
return this.books.filter(book =>
book.basic.title.toLowerCase().includes(searchTerm) ||
(book.basic.subtitle?.toLowerCase().includes(searchTerm)) ||
book.basic.author.some(author => author.toLowerCase().includes(searchTerm)) ||
(book.basic.desc?.toLowerCase().includes(searchTerm))
);
}
private renderBooks(books: Book[]) {
this.bookList.empty();
@ -81,7 +66,7 @@ export class ManageBooksModal extends Modal {
// 添加封面
const coverContainer = bookContainer.createDiv({ cls: 'book-smith-book-cover' });
if (book.basic.cover) {
const coverImg = coverContainer.createEl('img', {
coverContainer.createEl('img', {
attr: {
src: this.app.vault.adapter.getResourcePath(book.basic.cover),
alt: book.basic.title
@ -100,35 +85,35 @@ export class ManageBooksModal extends Modal {
}
}))
.setDesc(
`作者:${book.basic.author.join('、')}
${book.basic.desc ? `\n简介${book.basic.desc}` : ''}
\n创作轨迹${book.stats.total_words}${book.stats.target_total_words
`${i18n.t('BOOK_AUTHOR_PREFIX')}${book.basic.author.join('、')}
${book.basic.desc ? `${i18n.t('BOOK_DESC_PREFIX')}${book.basic.desc}` : ''}
${i18n.t('BOOK_PROGRESS_PREFIX')}${book.stats.total_words}${book.stats.target_total_words
? ` / ${(book.stats.target_total_words / 10000).toFixed(1)}`
: ' / 0万'
}`
);
setting.addButton(btn => btn
.setButtonText('删除')
.setButtonText(i18n.t('DELETE_BOOK'))
.setWarning()
.onClick(() => {
new ConfirmModal(
this.app,
"删除书籍",
`确定要删除《${book.basic.title}》吗?\n此操作不可恢复。`,
i18n.t('DELETE_BOOK_TITLE'),
i18n.t('DELETE_BOOK_DESC', { title: book.basic.title }),
async () => {
try {
await this.plugin.bookManager.deleteBook(book.basic.uuid);
new Notice('删除成功');
new Notice(i18n.t('DELETE_SUCCESS'));
this.onBookChange?.({ type: 'deleted', bookId: book.basic.uuid });
this.onOpen();
} catch (error) {
new Notice(`删除失败: ${error.message}`);
new Notice(i18n.t('DELETE_FAILED') + error.message);
}
}
).open();
})).addButton(btn => btn
.setButtonText('编辑')
.setButtonText(i18n.t('EDIT_BOOK'))
.onClick(() => {
new EditBookModal(
this.app,
@ -152,7 +137,7 @@ export class ManageBooksModal extends Modal {
const rootFolder = this.app.vault.getAbstractFileByPath(booksPath);
if (!(rootFolder instanceof TFolder)) {
new Notice('书籍根目录不存在或无法访问');
new Notice(i18n.t('BOOKS_ROOT_NOT_FOUND'));
return;
}
@ -171,7 +156,7 @@ export class ManageBooksModal extends Modal {
}
}
if (unimportedBooks.length === 0) {
new Notice('没有找到未导入的书籍目录');
new Notice(i18n.t('NO_UNIMPORTED_BOOKS'));
return;
}
@ -188,7 +173,7 @@ export class ManageBooksModal extends Modal {
).open();
} catch (error) {
new Notice(`检测未导入书籍失败: ${error.message}`);
new Notice(i18n.t('DETECT_UNIMPORTED_FAILED') + error.message);
}
}
@ -209,10 +194,10 @@ export class ManageBooksModal extends Modal {
bookId: newBook.basic.uuid
});
new Notice(`成功导入书籍《${newBook.basic.title}`);
new Notice(i18n.t('IMPORT_SUCCESS', { title: newBook.basic.title }));
} catch (error) {
new Notice(`创建书籍配置失败: ${error.message}`);
new Notice(i18n.t('IMPORT_FAILED') + error.message);
}
}
}

View file

@ -1,4 +1,5 @@
import { App, Modal, Setting } from 'obsidian';
import { i18n } from '../i18n/i18n';
export class NamePromptModal extends Modal {
private result: string;
@ -21,7 +22,7 @@ export class NamePromptModal extends Modal {
contentEl.createEl('h2', { text: this.placeholder });
new Setting(contentEl)
.setName('名称')
.setName(i18n.t('NAME_LABEL'))
.addText((text) => {
text.setValue(this.defaultValue || '')
.onChange((value) => {
@ -39,7 +40,7 @@ export class NamePromptModal extends Modal {
new Setting(contentEl)
.addButton((btn) =>
btn
.setButtonText('确定')
.setButtonText(i18n.t('CONFIRM'))
.setCta()
.onClick(() => {
this.close();
@ -47,7 +48,7 @@ export class NamePromptModal extends Modal {
}))
.addButton((btn) =>
btn
.setButtonText('取消')
.setButtonText(i18n.t('CANCEL'))
.onClick(() => {
this.close();
this.onSubmit(null);

View file

@ -1,4 +1,5 @@
import { App, Modal, Setting } from 'obsidian';
import { i18n } from '../i18n/i18n';
export class ReferenceModal extends Modal {
private result: string;
@ -16,11 +17,11 @@ export class ReferenceModal extends Modal {
onOpen() {
const { contentEl } = this;
contentEl.createEl('h3', { text: '添加引用内容' });
contentEl.createEl('h3', { text: i18n.t('REFERENCE_MODAL_TITLE') });
new Setting(contentEl)
.setName('引用内容')
.setDesc('请输入引用内容的详细信息')
.setName(i18n.t('REFERENCE_CONTENT'))
.setDesc(i18n.t('REFERENCE_CONTENT_DESC'))
.addTextArea((text) => {
text
.setValue(this.result)
@ -32,7 +33,7 @@ export class ReferenceModal extends Modal {
new Setting(contentEl)
.addButton((btn) =>
btn
.setButtonText('确定')
.setButtonText(i18n.t('CONFIRM'))
.setCta()
.onClick(() => {
this.onSubmit(this.result);

View file

@ -1,5 +1,6 @@
import { App, Modal, Setting } from 'obsidian';
import { Book } from '../types/book';
import { i18n } from '../i18n/i18n';
export class SwitchBookModal extends Modal {
constructor(
@ -16,13 +17,13 @@ export class SwitchBookModal extends Modal {
onOpen() {
const { contentEl } = this;
contentEl.addClass('book-smith-switch-book-modal');
contentEl.createEl('h2', { text: '切换书籍' });
contentEl.createEl('h2', { text: i18n.t('SWITCH_BOOK_TITLE') });
// 添加搜索框
const searchContainer = contentEl.createDiv({ cls: 'book-smith-search-container' });
this.searchInput = searchContainer.createEl('input', {
type: 'text',
placeholder: '搜索书籍...',
placeholder: i18n.t('SEARCH_BOOK_PLACEHOLDER'),
cls: 'book-smith-search-input'
});
@ -57,12 +58,12 @@ export class SwitchBookModal extends Modal {
new Setting(this.bookList)
.setName(`${book.basic.title}${book.basic.subtitle ? ` - ${book.basic.subtitle}` : ''}`)
.setDesc(
`作者${book.basic.author.join('、')}
\n | ${Math.round(book.stats.progress_by_chapter * 100)}% | ${(book.stats.target_total_words / 10000).toFixed(1)}
\n | ${new Date(book.stats.last_modified).toLocaleString()}`
`${i18n.t('BOOK_AUTHOR_LABEL')}${book.basic.author.join('、')}
\n | ${i18n.t('BOOK_PROGRESS_LABEL')}${Math.round(book.stats.progress_by_chapter * 100)}% | ${i18n.t('BOOK_WORDCOUNT_LABEL')}${(book.stats.target_total_words / 10000).toFixed(1)}
\n | ${i18n.t('BOOK_LASTMOD_LABEL')}${new Date(book.stats.last_modified).toLocaleString()}`
)
.addButton(btn => btn
.setButtonText('选择')
.setButtonText(i18n.t('SELECT_BOOK'))
.setCta()
.onClick(() => {
this.onSelect(book);

View file

@ -2,6 +2,7 @@ import { App, Modal, Setting, setIcon, Menu, Notice } from 'obsidian';
import { ChapterTree, ChapterNode } from '../types/book';
import BookSmithPlugin from '../main';
import { v4 as uuidv4 } from 'uuid';
import { i18n } from '../i18n/i18n';
export class TemplateEditModal extends Modal {
private templateName: string = '';
@ -37,41 +38,41 @@ export class TemplateEditModal extends Modal {
contentEl.empty();
contentEl.addClass('book-smith-template-modal');
contentEl.createEl('h2', { text: this.isEdit ? '编辑模板' : '新建模板' });
contentEl.createEl('h2', { text: this.isEdit ? i18n.t('TEMPLATE_EDIT_TITLE') : i18n.t('TEMPLATE_CREATE_TITLE') });
// 基本信息设置
const formContainer = contentEl.createDiv('book-smith-template-form');
new Setting(formContainer)
.setName('模板名称')
.setName(i18n.t('TEMPLATE_NAME'))
.addText(text => text
.setPlaceholder('请输入模板名称')
.setPlaceholder(i18n.t('TEMPLATE_NAME_PLACEHOLDER'))
.setValue(this.templateName)
.onChange(value => this.templateName = value));
new Setting(formContainer)
.setName('模板描述')
.setName(i18n.t('TEMPLATE_DESC'))
.addTextArea(text => text
.setPlaceholder('可描述模板的简要结构')
.setPlaceholder(i18n.t('TEMPLATE_DESC_PLACEHOLDER'))
.setValue(this.templateDesc)
.onChange(value => this.templateDesc = value));
// 模板结构编辑区域
const structureContainer = contentEl.createDiv('book-smith-template-structure');
structureContainer.createEl('h3', { text: '章节树' });
structureContainer.createEl('h3', { text: i18n.t('TEMPLATE_STRUCTURE') });
// 添加新节点按钮
new Setting(structureContainer)
.addButton(btn => btn
.setButtonText('添加文件')
.setButtonText(i18n.t('ADD_FILE'))
.onClick(() => {
const newNode = this.createNewNode('file', '新章节');
const newNode = this.createNewNode('file', i18n.t('NEW_CHAPTER'));
this.templateStructure.tree.push(newNode);
this.createNodeSetting(nodeListContainer, newNode, this.templateStructure.tree.length - 1);
}))
.addButton(btn => btn
.setButtonText('添加文件夹')
.setButtonText(i18n.t('ADD_FOLDER'))
.onClick(() => {
const newNode = this.createNewNode('group', '新目录');
const newNode = this.createNewNode('group', i18n.t('NEW_DIRECTORY'));
this.templateStructure.tree.push(newNode);
this.createNodeSetting(nodeListContainer, newNode, this.templateStructure.tree.length - 1);
}));
@ -112,20 +113,20 @@ export class TemplateEditModal extends Modal {
nodeContent.addEventListener('contextmenu', (e) => {
const menu = new Menu();
menu.addItem((item) => {
item.setTitle('添加文件')
item.setTitle(i18n.t('ADD_FILE'))
.setIcon('file-plus')
.onClick(() => {
const newNode = this.createNewNode('file', '新章节', node);
const newNode = this.createNewNode('file', i18n.t('NEW_CHAPTER'), node);
node.children = node.children || [];
node.children.push(newNode);
this.refreshStructure(container);
});
});
menu.addItem((item) => {
item.setTitle('添加文件夹')
item.setTitle(i18n.t('ADD_FOLDER'))
.setIcon('folder-plus')
.onClick(() => {
const newNode = this.createNewNode('group', '新目录', node);
const newNode = this.createNewNode('group', i18n.t('NEW_DIRECTORY'), node);
node.children = node.children || [];
node.children.push(newNode);
this.refreshStructure(container);
@ -275,27 +276,25 @@ export class TemplateEditModal extends Modal {
}
}
// 保存验证和提示
async saveTemplate(): Promise<boolean> {
// 验证模板名称
if (!this.templateName.trim()) {
new Notice('请输入模板名称');
new Notice(i18n.t('TEMPLATE_NAME_REQUIRED'));
return false;
}
// 验证是否有节点
if (this.templateStructure.tree.length === 0) {
new Notice('请至少添加一个节点');
new Notice(i18n.t('TEMPLATE_NODE_REQUIRED'));
return false;
}
try {
const key = this.isEdit ?
this.originalKey :
`${uuidv4().slice(0, 8)}`;
// 如果是新建模板,检查是否已存在同名模板
if (!this.isEdit && Object.values(this.plugin.settings.templates.custom).some(t => t.name === this.templateName)) {
new Notice('已存在同名模板,请修改模板名称');
new Notice(i18n.t('TEMPLATE_NAME_EXISTS'));
return false;
}
@ -305,15 +304,15 @@ export class TemplateEditModal extends Modal {
structure: this.templateStructure,
isBuiltin: false
};
await this.plugin.saveSettings();
new Notice('模板保存成功');
new Notice(i18n.t('TEMPLATE_SAVE_SUCCESS'));
this.onSaved?.();
return true;
} catch (error) {
console.error('保存模板失败:', error);
new Notice('保存模板失败,请查看控制台了解详情');
new Notice(i18n.t('TEMPLATE_SAVE_FAILED'));
return false;
}
}

View file

@ -1,4 +1,5 @@
import { App, Modal, setIcon } from 'obsidian';
import { i18n } from '../i18n/i18n';
export class UnimportedBooksModal extends Modal {
private result: string | null = null;
@ -17,18 +18,17 @@ export class UnimportedBooksModal extends Modal {
contentEl.empty();
contentEl.addClass('book-smith-unimported-books-modal');
contentEl.createEl('h2', { text: '选择要导入的书籍目录' });
contentEl.createEl('h2', { text: i18n.t('UNIMPORTED_BOOKS_TITLE') });
if (this.folders.length === 0) {
contentEl.createEl('p', {
cls: 'book-smith-unimported-empty-message',
text: '没有找到未导入的书籍目录'
text: i18n.t('NO_UNIMPORTED_FOLDERS')
});
// 添加关闭按钮
const buttonContainer = contentEl.createEl('div', { cls: 'book-smith-unimported-buttons' });
const closeButton = buttonContainer.createEl('button', {
text: '关闭',
text: i18n.t('CLOSE'),
cls: 'book-smith-unimported-button-cancel'
});
@ -40,24 +40,22 @@ export class UnimportedBooksModal extends Modal {
return;
}
// 添加搜索框
const searchContainer = contentEl.createEl('div', { cls: 'book-smith-unimported-search-container' });
const searchInput = searchContainer.createEl('input', {
type: 'text',
placeholder: '搜索目录...',
placeholder: i18n.t('SEARCH_FOLDERS_PLACEHOLDER'),
cls: 'book-smith-unimported-search-input'
});
const listEl = contentEl.createEl('div', { cls: 'book-smith-unimported-list' });
// 渲染文件夹列表
const renderFolders = (folders: string[]) => {
listEl.empty();
if (folders.length === 0) {
listEl.createEl('div', {
cls: 'book-smith-unimported-empty-result',
text: '没有匹配的目录'
text: i18n.t('NO_MATCHING_FOLDERS')
});
return;
}
@ -87,10 +85,8 @@ export class UnimportedBooksModal extends Modal {
}
};
// 初始渲染
renderFolders(this.folders);
// 搜索功能
searchInput.addEventListener('input', () => {
const searchTerm = searchInput.value.toLowerCase();
if (!searchTerm) {
@ -108,12 +104,12 @@ export class UnimportedBooksModal extends Modal {
const buttonContainer = contentEl.createEl('div', { cls: 'book-smith-unimported-buttons' });
const cancelButton = buttonContainer.createEl('button', {
text: '取消',
text: i18n.t('CANCEL'),
cls: 'book-smith-unimported-button-cancel'
});
const importButton = buttonContainer.createEl('button', {
text: '导入',
text: i18n.t('IMPORT'),
cls: 'book-smith-unimported-button-import'
});
@ -124,13 +120,11 @@ export class UnimportedBooksModal extends Modal {
importButton.addEventListener('click', () => {
if (!this.result) {
// 如果没有选择,显示提示
const notice = contentEl.createEl('div', {
cls: 'book-smith-unimported-notice',
text: '请先选择一个目录'
text: i18n.t('SELECT_FOLDER_FIRST')
});
// 2秒后自动消失
setTimeout(() => {
notice.remove();
}, 2000);

View file

@ -3,6 +3,7 @@ import { Book, BookBasicInfo, ChapterTree, ChapterNode } from '../types/book';
import { v4 as uuidv4 } from 'uuid';
import { TemplateManager } from './TemplateManager';
import { BookSmithSettings } from '../settings/settings';
import { i18n } from '../i18n/i18n';
export class BookManager {
private templateManager: TemplateManager;
@ -72,12 +73,11 @@ export class BookManager {
// 确保目录结构存在
private async ensureDirectoryStructure(basicInfo: Omit<BookBasicInfo, 'uuid' | 'created_at'>): Promise<void> {
// 检查书籍是否已存在
const existingFolder = this.app.vault.getAbstractFileByPath(
`${this.rootPath}/${basicInfo.title}`
);
if (existingFolder) {
throw new Error('书籍已存在');
throw new Error(i18n.t('BOOK_EXISTS'));
}
// 确保根目录存在
@ -126,14 +126,14 @@ export class BookManager {
async updateBook(uuid: string, updates: Partial<Book>): Promise<Book> {
const book = await this.getBookById(uuid);
if (!book) {
throw new Error('书籍不存在');
throw new Error(i18n.t('BOOK_NOT_FOUND'));
}
const oldBookPath = `${this.settings.defaultBookPath}/${book.basic.title}`;
const folder = this.app.vault.getAbstractFileByPath(oldBookPath);
if (!(folder instanceof TFolder)) {
throw new Error('书籍文件夹不存在');
throw new Error(i18n.t('BOOK_FOLDER_NOT_FOUND'));
}
const updatedBook = {
@ -160,9 +160,8 @@ export class BookManager {
// 如果书名改变了,需要重命名文件夹
if (updates.basic?.title && updates.basic.title !== book.basic.title) {
const newBookPath = `${this.settings.defaultBookPath}/${updates.basic.title}`;
// 检查新路径是否已存在
if (this.app.vault.getAbstractFileByPath(newBookPath)) {
throw new Error('书籍已存在');
throw new Error(i18n.t('BOOK_EXISTS'));
}
await this.app.vault.rename(folder, newBookPath);
}
@ -174,7 +173,7 @@ export class BookManager {
async deleteBook(uuid: string): Promise<void> {
const book = await this.getBookById(uuid);
if (!book) {
throw new Error('书籍不存在');
throw new Error(i18n.t('BOOK_NOT_FOUND'));
}
const bookPath = `${this.settings.defaultBookPath}/${book.basic.title}`;
@ -183,7 +182,7 @@ export class BookManager {
if (folder) {
await this.app.vault.trash(folder, true);
} else {
throw new Error('书籍文件夹不存在');
throw new Error(i18n.t('BOOK_FOLDER_NOT_FOUND'));
}
}
@ -217,7 +216,7 @@ export class BookManager {
}
} catch (error) {
console.error('保存配置文件时发生错误:', error);
throw new Error('保存配置文件失败');
throw new Error(i18n.t('SAVE_CONFIG_FAILED'));
}
}
@ -265,7 +264,7 @@ export class BookManager {
basic: {
uuid: uuidv4(),
title: folderName,
author: this.settings.defaultAuthor ? [this.settings.defaultAuthor] : ['未知作者'],
author: this.settings.defaultAuthor ? [this.settings.defaultAuthor] : [i18n.t('UNKNOWN_AUTHOR')],
created_at: new Date().toISOString()
},
structure: {
@ -292,14 +291,14 @@ export class BookManager {
// 保存书籍配置
const bookFolder = this.app.vault.getAbstractFileByPath(folderPath);
if (!(bookFolder instanceof TFolder)) {
throw new Error('书籍文件夹不存在');
throw new Error(i18n.t('BOOK_FOLDER_NOT_FOUND'));
}
await this.saveBookConfig(bookFolder, newBook);
return newBook;
} catch (error) {
console.error('导入书籍失败:', error);
throw error;
throw new Error(i18n.t('IMPORT_BOOK_FAILED'));
}
}

View file

@ -1,5 +1,6 @@
import { MarkdownView, Notice } from 'obsidian';
import BookSmithPlugin from '../main';
import { i18n } from '../i18n/i18n';
export interface FocusStats {
interruptions: number; // 中断次数
@ -235,10 +236,18 @@ export class FocusManager {
this.state = FocusState.BREAK;
this.remainingTime = this.plugin.settings.focus.breakDuration * 60;
this.startTimer();
new Notice('休息时间开始');
new Notice(i18n.t('BREAK_TIME_START'));
this.notifyUpdate();
}
private showSummary(): void {
new Notice(i18n.t('FOCUS_SUMMARY', {
duration: this.plugin.settings.focus.workDuration,
interruptions: this.stats.interruptions,
words: this.currentWords
}));
}
// =============== 事件监听方法 ===============
onUpdate(callback: () => void): void {
this.updateListeners.push(callback);
@ -269,9 +278,4 @@ export class FocusManager {
}, wait);
};
}
private showSummary(): void {
const summary = `专注结束!\n完成时间${this.plugin.settings.focus.workDuration}分钟\n中断次数${this.stats.interruptions}\n本次字数${this.currentWords}`;
new Notice(summary);
}
}

View file

@ -3,6 +3,7 @@ import BookSmithPlugin from '../main';
import { Book, ChapterNode } from '../types/book';
import { Reference, ReferenceData, ChapterReferences } from '../types/reference';
import { ReferenceModal } from '../modals/ReferenceModal';
import { i18n } from '../i18n/i18n';
export class ReferenceManager {
// === 1. 初始化 ===
@ -39,10 +40,10 @@ export class ReferenceManager {
}
private checkReferenceFile(bookPath: string): boolean {
const referencePath = `${bookPath}/引用书目.md`;
const referencePath = `${bookPath}/${i18n.t('REFERENCE_FILE_NAME')}`;
const file = this.app.vault.getAbstractFileByPath(referencePath);
if (!file) {
new Notice('请先在书籍目录下创建"引用书目.md"文件');
new Notice(i18n.t('REFERENCE_FILE_NOT_FOUND'));
return false;
}
return true;
@ -191,10 +192,10 @@ export class ReferenceManager {
if (!this.checkReferenceFile(bookPath)) return;
const referencePath = `${bookPath}/引用书目.md`;
const referencePath = `${bookPath}/${i18n.t('REFERENCE_FILE_NAME')}`;
const file = this.app.vault.getAbstractFileByPath(referencePath);
if (!(file instanceof TFile)) {
throw new Error('引用书目文件不存在或类型错误');
throw new Error(i18n.t('REFERENCE_FILE_ERROR'));
}
references.chapters.sort((a, b) => {
@ -253,7 +254,7 @@ export class ReferenceManager {
private addEditReferenceMenuItem(menu: Menu, editor: Editor, bookPath: string, refId: string) {
menu.addItem((item) => {
item
.setTitle("编辑当前引用")
.setTitle(i18n.t('EDIT_REFERENCE'))
.setIcon("edit")
.onClick(async () => {
const references = await this.getReferenceData(bookPath);
@ -265,17 +266,10 @@ export class ReferenceManager {
});
}
private addDeleteReferenceMenuItem(
menu: Menu,
editor: Editor,
bookPath: string,
refId: string,
linkPart: string,
orderPart: string
) {
private addDeleteReferenceMenuItem(menu: Menu, editor: Editor, bookPath: string, refId: string, linkPart: string, orderPart: string) {
menu.addItem((item) => {
item
.setTitle("删除当前引用")
.setTitle(i18n.t('DELETE_REFERENCE'))
.setIcon("trash")
.onClick(async () => {
const references = await this.getReferenceData(bookPath);
@ -306,7 +300,7 @@ export class ReferenceManager {
private addNewReferenceMenuItem(menu: Menu, editor: Editor, file: TFile | null) {
menu.addItem((item) => {
item
.setTitle("插入新引用")
.setTitle(i18n.t('INSERT_REFERENCE'))
.setIcon("quote-glyph")
.onClick(async () => {
await this.handleReferenceInsertion(editor, file);
@ -318,7 +312,7 @@ export class ReferenceManager {
private async handleReferenceInsertion(editor: Editor, file: TFile | null) {
const selectedText = editor.getSelection().trim();
if (!selectedText || selectedText.length === 0) {
new Notice("请选择要引用的文本");
new Notice(i18n.t('SELECT_TEXT_TO_REFERENCE'));
return;
}
@ -354,7 +348,7 @@ export class ReferenceManager {
if (editor && selectedText) {
const found = this.findReferenceById(references, ref.id);
if (found) {
const referenceLink = `[[${bookPath}/引用书目#^${ref.id}|${selectedText}]]${this.toSuperscript(found.order)}`;
const referenceLink = `[[${bookPath}/${i18n.t('REFERENCE_FILE_NAME')}#^${ref.id}|${selectedText}]]${this.toSuperscript(found.order)}`;
editor.replaceSelection(referenceLink);
}
}
@ -373,7 +367,7 @@ export class ReferenceManager {
const currentNode = this.findCurrentNode(file);
if (!currentNode) {
new Notice("无法获取当前章节信息");
new Notice(i18n.t('CHAPTER_INFO_ERROR'));
return;
}
@ -389,7 +383,7 @@ export class ReferenceManager {
chapter.references.push(newRef);
await this.updateReferenceFiles(bookPath, references);
const referenceLink = `[[${bookPath}/引用书目#^${newRef.id}|${selectedText}]]${this.toSuperscript(newRef.order)}`;
const referenceLink = `[[${bookPath}/${i18n.t('REFERENCE_FILE_NAME')}#^${newRef.id}|${selectedText}]]${this.toSuperscript(newRef.order)}`;
editor.replaceSelection(referenceLink);
}).open();
}

View file

@ -1,5 +1,6 @@
import { ChapterTree } from '../types/book';
import { BookSmithSettings } from '../settings/settings';
import { i18n } from '../i18n/i18n';
export class TemplateManager {
constructor(private settings: BookSmithSettings) {}
@ -7,7 +8,7 @@ export class TemplateManager {
getTemplate(templateType: string): ChapterTree {
const template = this.settings.templates.custom[templateType];
if (!template) {
throw new Error(`模板 "${templateType}" 不存在`);
throw new Error(i18n.t('TEMPLATE_TYPE_NOT_FOUND', { type: templateType }));
}
return JSON.parse(JSON.stringify(template.structure));
}

View file

@ -1,5 +1,6 @@
import { App, PluginSettingTab, Setting, setIcon, Notice } from 'obsidian';
import BookSmithPlugin from '../main';
import { i18n, Locale } from '../i18n/i18n';
import { TemplateEditModal } from '../modals/TemplateEditModal';
import { ConfirmModal } from '../modals/ConfirmModal';
@ -15,15 +16,15 @@ export class BookSmithSettingTab extends PluginSettingTab {
private createSection(containerEl: HTMLElement, title: string, renderContent: (contentEl: HTMLElement) => void) {
const section = containerEl.createDiv('settings-section');
const header = section.createDiv('settings-section-header');
const toggle = header.createSpan('settings-section-toggle');
setIcon(toggle, 'chevron-right');
header.createEl('h4', { text: title });
const content = section.createDiv('settings-section-content');
renderContent(content);
header.addEventListener('click', () => {
const isExpanded = !section.hasClass('is-expanded');
section.toggleClass('is-expanded', isExpanded);
@ -34,14 +35,14 @@ export class BookSmithSettingTab extends PluginSettingTab {
this.expandedSections.delete(title);
}
});
// 根据保存的状态或默认第一个展开
if (this.expandedSections.has(title) || (!containerEl.querySelector('.settings-section'))) {
section.addClass('is-expanded');
setIcon(toggle, 'chevron-down');
this.expandedSections.add(title);
}
return section;
}
@ -50,25 +51,65 @@ export class BookSmithSettingTab extends PluginSettingTab {
containerEl.empty();
containerEl.addClass('book-smith-settings');
containerEl.createEl('h2', { text: 'Book Smith设置' });
containerEl.createEl('h2', { text: i18n.t('SETTINGS_TITLE') });
// 基本设置
this.createSection(containerEl, '基本设置', el => this.renderBasicSettings(el));
this.createSection(containerEl, i18n.t('BASIC_SETTINGS'), el => this.renderBasicSettings(el));
// 模板设置
this.createSection(containerEl, '模板设置', el => this.renderTemplateSettings(el));
this.createSection(containerEl, i18n.t('TEMPLATE_SETTINGS'), el => this.renderTemplateSettings(el));
// 写作工具箱设置
this.createSection(containerEl, '写作工具箱设置', el => this.renderWritingToolsSettings(el));
this.createSection(containerEl, i18n.t('WRITING_TOOLS_SETTINGS'), el => this.renderWritingToolsSettings(el));
}
// 添加新的模板设置渲染方法
private renderBasicSettings(containerEl: HTMLElement): void {
// 添加语言选择
new Setting(containerEl)
.setName(i18n.t('LANGUAGE_SETTING'))
.setDesc(i18n.t('LANGUAGE_DESC'))
.addDropdown(dropdown => {
dropdown
.addOption('zh-CN', '简体中文')
.addOption('en', 'English')
.setValue(this.plugin.settings.language)
.onChange(async (value: Locale) => {
this.plugin.settings.language = value;
i18n.setLocale(value);
await this.plugin.saveSettings();
this.display();
});
});
new Setting(containerEl)
.setName(i18n.t('DEFAULT_AUTHOR'))
.setDesc(i18n.t('DEFAULT_AUTHOR_DESC'))
.addText(text => text
.setPlaceholder(i18n.t('DEFAULT_AUTHOR_PLACEHOLDER'))
.setValue(this.plugin.settings.defaultAuthor)
.onChange(async (value) => {
this.plugin.settings.defaultAuthor = value;
await this.plugin.saveSettings();
}));
new Setting(containerEl)
.setName(i18n.t('BOOK_STORAGE_PATH'))
.setDesc(i18n.t('BOOK_STORAGE_DESC'))
.addText(text => text
.setPlaceholder('books')
.setValue(this.plugin.settings.defaultBookPath)
.onChange(async (value) => {
this.plugin.settings.defaultBookPath = value;
await this.plugin.saveSettings();
new Notice(i18n.t('STORAGE_PATH_CHANGED'));
}));
}
private renderTemplateSettings(containerEl: HTMLElement): void {
// 默认模板选择
new Setting(containerEl)
.setName('默认模板')
.setDesc('创建新书籍时使用的默认模板')
.setName(i18n.t('DEFAULT_TEMPLATE'))
.setDesc(i18n.t('DEFAULT_TEMPLATE_DESC'))
.addDropdown(dropdown => {
const templates = this.plugin.settings.templates.custom;
Object.keys(templates).forEach(key => {
@ -80,11 +121,11 @@ export class BookSmithSettingTab extends PluginSettingTab {
await this.plugin.saveSettings();
});
});
// 模板列表
const templateList = containerEl.createDiv('template-list');
templateList.createEl('h4', { text: '书籍模板' });
templateList.createEl('h4', { text: i18n.t('BOOK_TEMPLATES') });
// 先渲染默认模板(内置模板)
const defaultTemplate = this.plugin.settings.templates.custom['default'];
if (defaultTemplate) {
@ -93,7 +134,7 @@ export class BookSmithSettingTab extends PluginSettingTab {
.setName(defaultTemplate.name)
.setDesc(defaultTemplate.description);
}
// 渲染其他自定义模板
Object.entries(this.plugin.settings.templates.custom)
.filter(([key]) => key !== 'default')
@ -128,57 +169,30 @@ export class BookSmithSettingTab extends PluginSettingTab {
).open();
}));
});
// 添加新模板按钮
new Setting(containerEl)
.addButton(btn => btn
.setButtonText('添加新模板')
.setButtonText(i18n.t('ADD_NEW_TEMPLATE'))
.setCta()
.onClick(() => {
new TemplateEditModal(
this.app,
this.app,
this.plugin,
undefined,
() => this.display() // 添加回调函数
() => this.display()
).open();
}));
}
private renderBasicSettings(containerEl: HTMLElement): void {
new Setting(containerEl)
.setName('默认作者')
.setDesc('创建新书籍时的默认作者名')
.addText(text => text
.setPlaceholder('输入默认作者名')
.setValue(this.plugin.settings.defaultAuthor)
.onChange(async (value) => {
this.plugin.settings.defaultAuthor = value;
await this.plugin.saveSettings();
}));
new Setting(containerEl)
.setName('书籍存储路径')
.setDesc('新建书籍的默认存储路径')
.addText(text => text
.setPlaceholder('books')
.setValue(this.plugin.settings.defaultBookPath)
.onChange(async (value) => {
this.plugin.settings.defaultBookPath = value;
await this.plugin.saveSettings();
new Notice('存储路径已更改,请重启 Obsidian 或重新加载以使更改生效');
}));
}
private renderWritingToolsSettings(containerEl: HTMLElement): void {
// 专注模式设置
const focusSection = containerEl.createDiv();
focusSection.createEl('h4', { text: '专注模式设置' });
focusSection.createEl('h4', { text: i18n.t('FOCUS_MODE_SETTINGS') });
new Setting(focusSection)
.setName('专注时长')
.setDesc('每个专注周期的工作时长(分钟)')
.setName(i18n.t('FOCUS_DURATION'))
.setDesc(i18n.t('FOCUS_DURATION_DESC'))
.addText(text => text
.setPlaceholder('25')
.setValue(this.plugin.settings.focus.workDuration.toString())
@ -186,10 +200,10 @@ export class BookSmithSettingTab extends PluginSettingTab {
this.plugin.settings.focus.workDuration = Number(value) || 25;
await this.plugin.saveSettings();
}));
new Setting(focusSection)
.setName('间隔时长')
.setDesc('每个专注周期后的休息时长(分钟)')
.setName(i18n.t('BREAK_DURATION'))
.setDesc(i18n.t('BREAK_DURATION_DESC'))
.addText(text => text
.setPlaceholder('5')
.setValue(this.plugin.settings.focus.breakDuration.toString())
@ -197,10 +211,10 @@ export class BookSmithSettingTab extends PluginSettingTab {
this.plugin.settings.focus.breakDuration = Number(value) || 5;
await this.plugin.saveSettings();
}));
new Setting(focusSection)
.setName('字数目标')
.setDesc('每个专注周期的目标字数')
.setName(i18n.t('WORD_GOAL'))
.setDesc(i18n.t('WORD_GOAL_DESC'))
.addText(text => text
.setPlaceholder('500')
.setValue(this.plugin.settings.focus.wordGoal.toString())

View file

@ -1,7 +1,9 @@
import { defaultTemplate } from '../templates/default';
import { ChapterTree } from '../types/book';
import { Locale } from '../i18n/i18n';
export interface BookSmithSettings {
// 基础配置
language: Locale;
defaultAuthor: string;
defaultBookPath: string;
lastBookId?: string;
@ -44,6 +46,7 @@ export interface BookSmithSettings {
}
export const DEFAULT_SETTINGS: BookSmithSettings = {
language: 'zh-CN', // 默认中文
defaultAuthor: '夜半',
defaultBookPath: 'books',
lastBookId: '',

View file

@ -110,7 +110,7 @@
.payment-tabs {
display: flex;
justify-content: center;
gap: 10px;
gap: 1px;
margin-bottom: 15px;
}

View file

@ -55,22 +55,12 @@ export const defaultTemplate: ChapterTree = {
}
]
},
{
id: 'reference',
title: '引用书目',
type: 'file',
path: '引用书目.md',
order: 3,
default_status: 'draft',
created_at: new Date().toISOString(),
last_modified: new Date().toISOString()
},
{
id: 'afterword',
title: '后记',
type: 'file',
path: '后记.md',
order: 4,
order: 3,
default_status: 'draft',
created_at: new Date().toISOString(),
last_modified: new Date().toISOString()

View file

@ -8,6 +8,7 @@ import { SwitchBookModal } from '../modals/SwitchBookModal';
import { ChapterTree } from '../components/ChapterTree';
import { FileEventManager } from '../services/FileEventManager';
import { ReferenceManager } from '../services/ReferenceManager';
import { i18n } from '../i18n/i18n';
// === 视图类定义 ===
export class BookSmithView extends ItemView {
@ -103,7 +104,7 @@ export class BookSmithView extends ItemView {
}
getDisplayText() {
return '书籍管理';
return i18n.t('BOOK_MANAGER');
}
getIcon() {
@ -126,28 +127,28 @@ export class BookSmithView extends ItemView {
const newBookBtn = toolbar.createEl('button', { cls: 'book-smith-toolbar-btn' });
setIcon(newBookBtn, 'create-new');
newBookBtn.appendChild(createSpan({ text: ' 新建' }));
newBookBtn.appendChild(createSpan({ text: ` ${i18n.t('NEW_BOOK')}` }));
newBookBtn.addEventListener('click', () => {
new CreateBookModal(this.app, this.plugin, async (newBook) => {
if (newBook) {
this.plugin.settings.lastBookId = newBook.basic.uuid;
await this.plugin.saveSettings();
await this.refreshView();
new Notice(`已切换到《${newBook.basic.title}`);
new Notice(i18n.t('SWITCHED_TO_BOOK', { title: newBook.basic.title }));
}
}).open();
});
const switchBookBtn = toolbar.createEl('button', { cls: 'book-smith-toolbar-btn' });
setIcon(switchBookBtn, 'switch');
switchBookBtn.appendChild(createSpan({ text: ' 切换' }));
switchBookBtn.appendChild(createSpan({ text: ` ${i18n.t('SWITCH_BOOK')}` }));
switchBookBtn.addEventListener('click', () => {
this.switchBook();
});
const manageBookBtn = toolbar.createEl('button', { cls: 'book-smith-toolbar-btn' });
setIcon(manageBookBtn, 'library');
manageBookBtn.appendChild(createSpan({ text: ' 管理' }));
manageBookBtn.appendChild(createSpan({ text: ` ${i18n.t('MANAGE_BOOK')}` }));
manageBookBtn.addEventListener('click', async () => {
new ManageBooksModal(this.app, this.plugin, async (result) => {
if (result.type === 'imported' && result.bookId) {
@ -155,14 +156,14 @@ export class BookSmithView extends ItemView {
this.plugin.settings.lastBookId = result.bookId;
await this.plugin.saveSettings();
await this.refreshView();
new Notice(`已导入并切换到新书籍`);
new Notice(i18n.t('IMPORTED_AND_SWITCHED'));
} else if (result.bookId === this.currentBook?.basic.uuid) {
if (result.type === 'deleted') {
this.plugin.settings.lastBookId = undefined;
await this.plugin.saveSettings();
this.currentBook = null;
await this.refreshView();
new Notice('当前书籍已被删除');
new Notice(i18n.t('CURRENT_BOOK_DELETED'));
} else if (result.type === 'edited') {
await this.refreshView();
}
@ -177,38 +178,7 @@ export class BookSmithView extends ItemView {
helpBtnContainer.createEl('div', {
cls: 'book-smith-help-tooltip',
text: `👋 欢迎使用 BookSmith
使
便
BookSmith
💝
BookSmith `
text: i18n.t('HELP_TOOLTIP')
});
}
@ -252,14 +222,14 @@ export class BookSmithView extends ItemView {
// 渲染书籍信息
const infoSection = bookContent.createDiv({ cls: 'book-smith-book-info' });
const authorRow = infoSection.createDiv({ cls: 'book-smith-info-row' });
authorRow.createSpan({ text: '作者', cls: 'book-smith-info-label' });
authorRow.createSpan({ text: i18n.t('BOOK_AUTHOR'), cls: 'book-smith-info-label' });
authorRow.createSpan({
text: this.currentBook.basic.author.join(', '),
cls: 'book-smith-info-value'
});
const descRow = infoSection.createDiv({ cls: 'book-smith-info-row' });
descRow.createSpan({ text: '简介', cls: 'book-smith-info-label' });
descRow.createSpan({ text: i18n.t('BOOK_DESCRIPTION'), cls: 'book-smith-info-label' });
descRow.createSpan({
text: this.currentBook.basic.desc || '',
cls: 'book-smith-info-value description'
@ -291,11 +261,11 @@ export class BookSmithView extends ItemView {
private renderEmptyState(container: HTMLElement) {
const emptyState = container.createDiv({ cls: 'book-smith-empty-state' });
emptyState.createEl('p', {
text: '👋 欢迎使用 BookSmith',
text: i18n.t('WELCOME_MESSAGE'),
cls: 'book-smith-empty-title'
});
emptyState.createEl('p', {
text: '点击上方的"新建书籍"创建作品,或使用"切换"按钮选择已有书籍',
text: i18n.t('EMPTY_STATE_HINT'),
cls: 'book-smith-empty-desc'
});
}
@ -309,21 +279,21 @@ export class BookSmithView extends ItemView {
const todayWords = statsContainer.createDiv({ cls: 'book-smith-stat-item' });
const todayWordsLabel = todayWords.createSpan();
setIcon(todayWordsLabel, 'pencil');
todayWordsLabel.appendChild(createSpan({ text: ' 今日字数' }));
todayWordsLabel.appendChild(createSpan({ text: ` ${i18n.t('TODAY_WORDS')}` }));
todayWords.createEl('span', {
cls: 'book-smith-stat-value',
text: `${this.currentBook.stats.daily_words[today] || 0}`
text: `${this.currentBook.stats.daily_words[today] || 0}${i18n.t('WORD_UNIT')}`
});
// 总字数统计
const wordCount = statsContainer.createDiv({ cls: 'book-smith-stat-item' });
const wordCountLabel = wordCount.createSpan();
setIcon(wordCountLabel, 'document');
wordCountLabel.appendChild(createSpan({ text: ' 字数统计' }));
wordCountLabel.appendChild(createSpan({ text: ` ${i18n.t('TOTAL_WORDS')}` }));
wordCount.createEl('span', {
cls: 'book-smith-stat-value',
text: `${this.currentBook.stats.total_words}${this.currentBook.stats.target_total_words
? ` / ${(this.currentBook.stats.target_total_words / 10000).toFixed(1)}`
? ` / ${(this.currentBook.stats.target_total_words / 10000).toFixed(1)}${i18n.t('TEN_THOUSAND')}`
: ''
}`
});
@ -332,7 +302,7 @@ export class BookSmithView extends ItemView {
const progress = statsContainer.createDiv({ cls: 'book-smith-stat-item' });
const progressLabel = progress.createSpan();
setIcon(progressLabel, 'target');
progressLabel.appendChild(createSpan({ text: ' 章节完成' }));
progressLabel.appendChild(createSpan({ text: ` ${i18n.t('CHAPTER_COMPLETION')}` }));
progress.createEl('span', {
cls: 'book-smith-stat-value',
text: `${Math.round(this.currentBook.stats.progress_by_chapter * 100)}%`
@ -342,20 +312,20 @@ export class BookSmithView extends ItemView {
const duration = statsContainer.createDiv({ cls: 'book-smith-stat-item' });
const durationLabel = duration.createSpan();
setIcon(durationLabel, 'clock');
durationLabel.appendChild(createSpan({ text: ' 写作天数' }));
durationLabel.appendChild(createSpan({ text: ` ${i18n.t('WRITING_DAYS')}` }));
duration.createEl('span', {
cls: 'book-smith-stat-value',
text: `${this.currentBook.stats.writing_days}`
text: `${this.currentBook.stats.writing_days}${i18n.t('DAY_UNIT')}`
});
// 日均字数
const average = statsContainer.createDiv({ cls: 'book-smith-stat-item' });
const averageLabel = average.createSpan();
setIcon(averageLabel, 'calendar-clock');
averageLabel.appendChild(createSpan({ text: ' 日均字数' }));
averageLabel.appendChild(createSpan({ text: ` ${i18n.t('AVERAGE_DAILY_WORDS')}` }));
average.createEl('span', {
cls: 'book-smith-stat-value',
text: `${Math.round(this.currentBook.stats.average_daily_words)}`
text: `${Math.round(this.currentBook.stats.average_daily_words)}${i18n.t('WORD_UNIT')}`
});
}
@ -363,7 +333,7 @@ export class BookSmithView extends ItemView {
private async switchBook() {
const books = await this.plugin.bookManager.getAllBooks();
if (books.length === 0) {
new Notice('暂无可切换的书籍');
new Notice(i18n.t('NO_BOOKS_TO_SWITCH'));
return;
}
@ -371,7 +341,7 @@ export class BookSmithView extends ItemView {
this.plugin.settings.lastBookId = selectedBook.basic.uuid;
await this.plugin.saveSettings();
await this.refreshView();
new Notice(`已切换到《${selectedBook.basic.title}`);
new Notice(i18n.t('SWITCHED_TO_BOOK', { title: selectedBook.basic.title }));
}).open();
}
}

View file

@ -6,6 +6,8 @@ import { EbookModal } from '../modals/EbookModal';
import { CommunityModal } from '../modals/CommunityModal';
import { ContactModal } from '../modals/ContactModal';
import BookSmithPlugin from '../main';
import { i18n } from '../i18n/i18n';
interface ToolItem {
icon: string;
text: string;
@ -32,7 +34,7 @@ export class ToolView extends ItemView {
// 视图基础配置
getViewType() { return 'book-smith-tool'; }
getDisplayText() { return '写作工具箱'; }
getDisplayText() { return i18n.t('WRITING_TOOLBOX'); }
getIcon() { return 'wrench'; }
// 生命周期方法
@ -74,37 +76,37 @@ export class ToolView extends ItemView {
// 保持原有的图标和标题
const mainIconSpan = titleContainer.createSpan({ cls: 'book-smith-panel-icon' });
setIcon(mainIconSpan, 'archive');
titleContainer.createSpan({ text: '写作工具箱' });
titleContainer.createSpan({ text: i18n.t('WRITING_TOOLBOX') });
}
// 工具组创建
private createToolGroups(container: HTMLElement) {
// 写作助手工具组
if (this.plugin.settings.tools.assistant) {
this.createToolGroup(container, '写作助手', [
this.createToolGroup(container, i18n.t('WRITING_ASSISTANT'), [
{
icon: 'target',
text: '专注模式',
text: i18n.t('FOCUS_MODE'),
hasProgress: true,
onClick: () => this.enterFocusMode()
},
{
icon: 'brain',
text: '创作灵感',
text: i18n.t('CREATIVE_INSPIRATION'),
onClick: () => new InspirationModal(this.containerEl).open()
},
{
icon: 'file-text',
text: '人物档案',
text: i18n.t('CHARACTER_PROFILES'),
onClick: () => {
new Notice('人物档案功能即将上线');
new Notice(i18n.t('FEATURE_COMING_SOON', { feature: i18n.t('CHARACTER_PROFILES') }));
}
},
{
icon: 'map',
text: '世界构建',
text: i18n.t('WORLD_BUILDING'),
onClick: () => {
new Notice('世界构建功能即将上线');
new Notice(i18n.t('FEATURE_COMING_SOON', { feature: i18n.t('WORLD_BUILDING') }));
}
}
]);
@ -112,24 +114,24 @@ export class ToolView extends ItemView {
// 导出发布工具组
if (this.plugin.settings.tools.export) {
this.createToolGroup(container, '导出发布', [
this.createToolGroup(container, i18n.t('EXPORT_PUBLISH'), [
{
icon: 'edit-3',
text: '设计排版',
text: i18n.t('DESIGN_TYPOGRAPHY'),
onClick: () => {
new Notice('书籍设计排版功能即将上线');
new Notice(i18n.t('FEATURE_COMING_SOON', { feature: i18n.t('DESIGN_TYPOGRAPHY') }));
}
},
{
icon: 'book',
text: '生成电子书',
text: i18n.t('GENERATE_EBOOK'),
onClick: () => new EbookModal(this.containerEl).open()
},
{
icon: 'clock',
text: '更多功能...',
text: i18n.t('MORE_FEATURES'),
onClick: () => {
new Notice('更多功能等你一起共创');
new Notice(i18n.t('MORE_FEATURES_MESSAGE'));
}
}
]);
@ -137,21 +139,21 @@ export class ToolView extends ItemView {
// 写作圈子工具组
if (this.plugin.settings.tools.community) {
this.createToolGroup(container, '写作圈子', [
this.createToolGroup(container, i18n.t('WRITING_COMMUNITY'), [
{
icon: 'users',
text: '创作社区',
text: i18n.t('CREATIVE_COMMUNITY'),
extra: '',
onClick: () => new CommunityModal(this.containerEl).open()
},
{
icon: 'message-square',
text: '联系作者',
text: i18n.t('CONTACT_AUTHOR'),
onClick: () => new ContactModal(this.containerEl).open()
},
{
icon: 'heart',
text: '赞助捐赠',
text: i18n.t('DONATE_SUPPORT'),
onClick: () => new DonateModal(this.containerEl).open()
}
]);
@ -192,15 +194,15 @@ export class ToolView extends ItemView {
// 设置面板创建
private createSettings(container: HTMLElement) {
const settingsItem = this.createToolItem(container, 'settings', '面板设置');
const settingsItem = this.createToolItem(container, 'settings', i18n.t('PANEL_SETTINGS'));
settingsItem.addClass('settings');
settingsItem.addEventListener('contextmenu', (event) => {
const menu = new Menu();
// 添加工具显隐选项
this.addToolVisibilityMenuItem(menu, 'assistant', 'target', '写作助手');
this.addToolVisibilityMenuItem(menu, 'export', 'download', '导出发布');
this.addToolVisibilityMenuItem(menu, 'assistant', 'target', i18n.t('WRITING_ASSISTANT'));
this.addToolVisibilityMenuItem(menu, 'export', 'download', i18n.t('EXPORT_PUBLISH'));
menu.showAtMouseEvent(event);
});
@ -209,7 +211,7 @@ export class ToolView extends ItemView {
// 添加工具显隐菜单项
private addToolVisibilityMenuItem(menu: Menu, key: keyof typeof this.plugin.settings.tools, icon: string, text: string) {
menu.addItem(item => item
.setTitle(`${this.plugin.settings.tools[key] ? '隐藏' : '显示'}${text}`)
.setTitle(`${this.plugin.settings.tools[key] ? i18n.t('HIDE') : i18n.t('SHOW')}${text}`)
.setIcon(icon)
.setChecked(this.plugin.settings.tools[key])
.onClick(async () => {