Add internationalization (i18n) support

- Introduced i18n framework with translation support
- Updated config, main, and styles files to use translation keys
- Implemented dynamic locale setting based on Obsidian's language
- Replaced hardcoded Chinese strings with translatable keys
- Added locale change event listener to update translations dynamically
This commit is contained in:
wood chen 2025-02-03 23:07:04 +08:00
parent 9d64d31947
commit f27ada6a4e
6 changed files with 212 additions and 96 deletions

View File

@ -1,5 +1,6 @@
import { PluginSettingTab, Setting, App } from 'obsidian';
import DiscourseSyncPlugin from './main';
import { t } from './i18n';
export interface DiscourseSyncSettings {
baseUrl: string;
@ -28,8 +29,8 @@ export class DiscourseSyncSettingsTab extends PluginSettingTab {
containerEl.empty();
new Setting(containerEl)
.setName("论坛地址")
.setDesc("Discourse 论坛的网址")
.setName(t('FORUM_URL'))
.setDesc(t('FORUM_URL_DESC'))
.addText((text) =>
text
.setPlaceholder("https://forum.example.com")
@ -41,8 +42,8 @@ export class DiscourseSyncSettingsTab extends PluginSettingTab {
);
new Setting(containerEl)
.setName("API 密钥")
.setDesc("在'/admin/api/keys'中创建的 API 密钥")
.setName(t('API_KEY'))
.setDesc(t('API_KEY_DESC'))
.addText((text) =>
text
.setPlaceholder("api_key")
@ -54,8 +55,8 @@ export class DiscourseSyncSettingsTab extends PluginSettingTab {
);
new Setting(containerEl)
.setName("用户名")
.setDesc("Discourse 用户名")
.setName(t('USERNAME'))
.setDesc(t('USERNAME_DESC'))
.addText((text) =>
text
.setPlaceholder("username")

37
src/i18n/en.ts Normal file
View File

@ -0,0 +1,37 @@
export default {
// Settings page
'FORUM_URL': 'Forum URL',
'FORUM_URL_DESC': 'The URL of your Discourse forum',
'API_KEY': 'API Key',
'API_KEY_DESC': "API key created in '/admin/api/keys'",
'USERNAME': 'Username',
'USERNAME_DESC': 'Your Discourse username',
// Publish page
'PUBLISH_TO_DISCOURSE': 'Publish to Discourse',
'UPDATE_POST': 'Update Post',
'CATEGORY': 'Category',
'TAGS': 'Tags',
'ENTER_TAG': 'Enter tag name (press Enter to add)',
'ENTER_TAG_WITH_CREATE': 'Enter tag name (can create new tags)',
'PUBLISHING': 'Publishing...',
'UPDATING': 'Updating...',
'PUBLISH': 'Publish',
'UPDATE': 'Update',
'RETRY': 'Retry',
// Success messages
'PUBLISH_SUCCESS': '✓ Published successfully!',
'UPDATE_SUCCESS': '✓ Updated successfully!',
// Error messages
'PUBLISH_FAILED': 'Publish failed',
'UPDATE_FAILED': 'Update failed',
'PUBLISH_ERROR': 'Publish error',
'UPDATE_ERROR': 'Update error',
'PERMISSION_ERROR': 'Insufficient permissions, can only use existing tags',
'UNKNOWN_ERROR': 'Unknown error',
'TRY_AGAIN': 'Please try again',
'POST_ID_ERROR': 'Published successfully but failed to get post ID',
'SAVE_POST_ID_ERROR': 'Published successfully but failed to save post ID',
}

30
src/i18n/index.ts Normal file
View File

@ -0,0 +1,30 @@
import { moment } from 'obsidian';
import zhCN from './zh-CN';
import en from './en';
const localeMap: { [key: string]: any } = {
'zh': zhCN,
'zh-cn': zhCN,
'en': en,
'en-us': en,
'en-gb': en,
};
let currentLocale = 'en';
export function setLocale(locale: string) {
const normalizedLocale = locale.toLowerCase();
if (localeMap[normalizedLocale]) {
currentLocale = normalizedLocale;
moment.locale(normalizedLocale);
}
}
export function getCurrentLocale(): string {
return currentLocale;
}
export function t(key: string): string {
const translations = localeMap[currentLocale] || localeMap['en'];
return translations[key] || localeMap['en'][key] || key;
}

37
src/i18n/zh-CN.ts Normal file
View File

@ -0,0 +1,37 @@
export default {
// 设置页面
'FORUM_URL': '论坛地址',
'FORUM_URL_DESC': 'Discourse 论坛的网址',
'API_KEY': 'API 密钥',
'API_KEY_DESC': "在'/admin/api/keys'中创建的 API 密钥",
'USERNAME': '用户名',
'USERNAME_DESC': 'Discourse 用户名',
// 发布页面
'PUBLISH_TO_DISCOURSE': '发布到 Discourse',
'UPDATE_POST': '更新帖子',
'CATEGORY': '分类',
'TAGS': '标签',
'ENTER_TAG': '输入标签名称(回车添加)',
'ENTER_TAG_WITH_CREATE': '输入标签名称(可创建新标签)',
'PUBLISHING': '发布中...',
'UPDATING': '更新中...',
'PUBLISH': '发布',
'UPDATE': '更新',
'RETRY': '重试',
// 成功提示
'PUBLISH_SUCCESS': '✓ 发布成功!',
'UPDATE_SUCCESS': '✓ 更新成功!',
// 错误提示
'PUBLISH_FAILED': '发布失败',
'UPDATE_FAILED': '更新失败',
'PUBLISH_ERROR': '发布出错',
'UPDATE_ERROR': '更新出错',
'PERMISSION_ERROR': '权限不足,只能使用已有的标签',
'UNKNOWN_ERROR': '未知错误',
'TRY_AGAIN': '请重试',
'POST_ID_ERROR': '发布成功但无法获取帖子ID',
'SAVE_POST_ID_ERROR': '发布成功但无法保存帖子ID',
}

View File

@ -1,15 +1,26 @@
import { App, Menu, MenuItem, Plugin, Modal, requestUrl, TFile } from 'obsidian';
import { App, Menu, MenuItem, Plugin, Modal, requestUrl, TFile, moment } from 'obsidian';
import { DEFAULT_SETTINGS, DiscourseSyncSettings, DiscourseSyncSettingsTab } from './config';
import * as yaml from 'yaml';
import { t, setLocale } from './i18n';
export default class DiscourseSyncPlugin extends Plugin {
settings: DiscourseSyncSettings;
activeFile: {
name: string;
content: string;
postId?: number; // 添加帖子ID字段
postId?: number; // Post ID field
};
async onload() {
// Set locale based on Obsidian's language setting
setLocale(moment.locale());
// Update locale when Obsidian's language changes
this.registerEvent(
this.app.workspace.on('window-open', () => {
setLocale(moment.locale());
})
);
await this.loadSettings();
this.addSettingTab(new DiscourseSyncSettingsTab(this.app, this));
this.registerEvent(
@ -20,7 +31,7 @@ export default class DiscourseSyncPlugin extends Plugin {
this.addCommand({
id: "category-modal",
name: "发布到 Discourse",
name: t('PUBLISH_TO_DISCOURSE'),
callback: () => {
this.openCategoryModal();
},
@ -118,11 +129,11 @@ export default class DiscourseSyncPlugin extends Plugin {
}
let content = this.activeFile.content;
// 检查是否包含帖子ID的frontmatter
// Check if frontmatter contains post ID
const frontMatter = this.getFrontMatter(content);
const postId = frontMatter?.discourse_post_id;
// 过滤掉笔记属性部分
// Filter out note properties section
content = content.replace(/^---[\s\S]*?---\n/, '');
const imageReferences = this.extractImageReferences(content);
@ -162,20 +173,20 @@ export default class DiscourseSyncPlugin extends Plugin {
if (response.status === 200) {
if (!isUpdate) {
try {
// 获取新帖子的ID
// Get new post ID
const responseData = response.json;
if (responseData && responseData.id) {
await this.updateFrontMatter(responseData.id);
} else {
return {
message: "Error",
error: "发布成功但无法获取帖子ID"
error: t('POST_ID_ERROR')
};
}
} catch (error) {
return {
message: "Error",
error: "发布成功但无法保存帖子ID"
error: t('SAVE_POST_ID_ERROR')
};
}
}
@ -198,20 +209,20 @@ export default class DiscourseSyncPlugin extends Plugin {
} catch (parseError) {
return {
message: "Error",
error: `${isUpdate ? '更新' : '发布'}失败 (${response.status})`
error: `${isUpdate ? t('UPDATE_FAILED') : t('PUBLISH_FAILED')} (${response.status})`
};
}
}
} catch (error) {
return {
message: "Error",
error: `${isUpdate ? '更新' : '发布'}失败: ${error.message || '未知错误'}`
error: `${isUpdate ? t('UPDATE_FAILED') : t('PUBLISH_FAILED')}: ${error.message || t('UNKNOWN_ERROR')}`
};
}
return { message: "Error", error: `${isUpdate ? '更新' : '发布'}失败,请重试` };
return { message: "Error", error: `${isUpdate ? t('UPDATE_FAILED') : t('PUBLISH_FAILED')}, ${t('TRY_AGAIN')}` };
}
// 获取frontmatter数据
// Get frontmatter data
private getFrontMatter(content: string): any {
const fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
if (fmMatch) {
@ -224,10 +235,10 @@ export default class DiscourseSyncPlugin extends Plugin {
return null;
}
// 更新frontmatter添加帖子ID
// Update frontmatter, add post ID
private async updateFrontMatter(postId: number) {
try {
// 获取当前活动文件
// Get current active file
const activeFile = this.app.workspace.getActiveFile();
if (!activeFile) {
return;
@ -238,11 +249,11 @@ export default class DiscourseSyncPlugin extends Plugin {
let newContent: string;
if (fm) {
// 更新现有的frontmatter
// Update existing frontmatter
const updatedFm = { ...fm, discourse_post_id: postId };
newContent = content.replace(/^---\n[\s\S]*?\n---\n/, `---\n${yaml.stringify(updatedFm)}---\n`);
} else {
// 添加新的frontmatter
// Add new frontmatter
const newFm = { discourse_post_id: postId };
newContent = `---\n${yaml.stringify(newFm)}---\n${content}`;
}
@ -251,7 +262,7 @@ export default class DiscourseSyncPlugin extends Plugin {
} catch (error) {
return {
message: "Error",
error: "更新失败"
error: t('UPDATE_FAILED')
};
}
}
@ -309,9 +320,9 @@ export default class DiscourseSyncPlugin extends Plugin {
if (response.status === 200) {
const data = response.json;
// 获取用户权限信息
// Get user permissions
const canCreateTags = await this.checkCanCreateTags();
// Discourse 返回的 tags 列表在 tags 数组中
// Tags list returned by Discourse is in the tags array
return data.tags.map((tag: any) => ({
name: tag.name,
canCreate: canCreateTags
@ -323,7 +334,7 @@ export default class DiscourseSyncPlugin extends Plugin {
}
}
// 检查用户是否有创建标签的权限
// Check if user has permission to create tags
private async checkCanCreateTags(): Promise<boolean> {
try {
const url = `${this.settings.baseUrl}/u/${this.settings.disUser}.json`;
@ -342,7 +353,7 @@ export default class DiscourseSyncPlugin extends Plugin {
if (response.status === 200) {
const data = response.json;
// 检查用户的 trust_level
// Check user's trust_level
return data.user.trust_level >= 3;
}
return false;
@ -353,7 +364,7 @@ export default class DiscourseSyncPlugin extends Plugin {
registerDirMenu(menu: Menu, file: TFile) {
const syncDiscourse = (item: MenuItem) => {
item.setTitle("发布到 Discourse");
item.setTitle(t('PUBLISH_TO_DISCOURSE'));
item.onClick(async () => {
const content = await this.app.vault.read(file);
const fm = this.getFrontMatter(content);
@ -441,23 +452,23 @@ export class SelectCategoryModal extends Modal {
}
onOpen() {
// 添加模态框基础样式
// Add modal base style
this.modalEl.addClass('mod-discourse-sync');
const { contentEl } = this;
contentEl.empty();
contentEl.addClass('discourse-sync-modal');
const isUpdate = this.plugin.activeFile.postId !== undefined;
contentEl.createEl("h1", { text: isUpdate ? '更新帖子' : '发布到 Discourse' });
contentEl.createEl("h1", { text: isUpdate ? t('UPDATE_POST') : t('PUBLISH_TO_DISCOURSE') });
// 创建表单区域容器
// Create form area container
const formArea = contentEl.createEl('div', { cls: 'form-area' });
// 创建选择器容器
// Create selector container
const selectContainer = formArea.createEl('div', { cls: 'select-container' });
if (!isUpdate) {
// 只在新建帖子时显示分类选择
selectContainer.createEl('label', { text: '分类' });
// Only show category selector when creating a new post
selectContainer.createEl('label', { text: t('CATEGORY') });
const selectEl = selectContainer.createEl('select');
this.categories.forEach(category => {
const option = selectEl.createEl('option', { text: category.name });
@ -465,18 +476,18 @@ export class SelectCategoryModal extends Modal {
});
}
// 创建标签选择容器
// Create tag selector container
const tagContainer = formArea.createEl('div', { cls: 'tag-container' });
tagContainer.createEl('label', { text: '标签' });
tagContainer.createEl('label', { text: t('TAGS') });
// 创建标签选择区域
// Create tag selector area
const tagSelectArea = tagContainer.createEl('div', { cls: 'tag-select-area' });
// 已选标签显示区域
// Selected tags display area
const selectedTagsContainer = tagSelectArea.createEl('div', { cls: 'selected-tags' });
const selectedTags = new Set<string>();
// 更新已选标签显示
// Update selected tags display
const updateSelectedTags = () => {
selectedTagsContainer.empty();
selectedTags.forEach(tag => {
@ -495,19 +506,19 @@ export class SelectCategoryModal extends Modal {
});
};
// 创建标签输入容器
// Create tag input container
const tagInputContainer = tagSelectArea.createEl('div', { cls: 'tag-input-container' });
// 创建标签输入和建议
// Create tag input and suggestions
const tagInput = tagInputContainer.createEl('input', {
type: 'text',
placeholder: this.canCreateTags ? '输入标签名称(回车添加)' : '输入标签名称(回车添加)'
placeholder: this.canCreateTags ? t('ENTER_TAG_WITH_CREATE') : t('ENTER_TAG')
});
// 创建标签建议容器
// Create tag suggestions container
const tagSuggestions = tagInputContainer.createEl('div', { cls: 'tag-suggestions' });
// 处理输入事件,显示匹配的标签
// Handle input event, show matching tags
tagInput.oninput = () => {
const value = tagInput.value.toLowerCase();
tagSuggestions.empty();
@ -521,14 +532,14 @@ export class SelectCategoryModal extends Modal {
.slice(0, 10);
if (matches.length > 0) {
// 获取输入框的位置和宽度
// Get input box position and width
const inputRect = tagInput.getBoundingClientRect();
const modalRect = this.modalEl.getBoundingClientRect();
// 确保建议列表不会超出模态框宽度
const maxWidth = modalRect.right - inputRect.left - 24; // 24px 是右侧padding
// Ensure suggestions list doesn't exceed modal width
const maxWidth = modalRect.right - inputRect.left - 24; // 24px is right padding
// 设置建议列表的位置和宽度
// Set suggestions list position and width
tagSuggestions.style.top = `${inputRect.bottom + 4}px`;
tagSuggestions.style.left = `${inputRect.left}px`;
tagSuggestions.style.width = `${Math.min(inputRect.width, maxWidth)}px`;
@ -549,7 +560,7 @@ export class SelectCategoryModal extends Modal {
}
};
// 处理回车事件
// Handle enter event
tagInput.onkeydown = (e) => {
if (e.key === 'Enter' && tagInput.value) {
e.preventDefault();
@ -563,10 +574,10 @@ export class SelectCategoryModal extends Modal {
selectedTags.add(value);
updateSelectedTags();
} else {
// 显示权限提示
// Show permission notice
const notice = contentEl.createEl('div', {
cls: 'tag-notice',
text: '权限不足,只能使用已有的标签'
text: t('PERMISSION_ERROR')
});
setTimeout(() => {
notice.remove();
@ -578,15 +589,15 @@ export class SelectCategoryModal extends Modal {
}
};
// 处理失焦事件,隐藏建议
// Handle blur event, hide suggestions
tagInput.onblur = () => {
// 延迟隐藏,以便可以点击建议
// Delay hide, so suggestions can be clicked
setTimeout(() => {
tagSuggestions.empty();
}, 200);
};
// 处理窗口滚动时更新建议列表位置
// Handle window scroll, update suggestions list position
const updateSuggestionsPosition = () => {
if (tagSuggestions.childNodes.length > 0) {
const inputRect = tagInput.getBoundingClientRect();
@ -596,22 +607,22 @@ export class SelectCategoryModal extends Modal {
}
};
// 监听滚动事件
// Listen for scroll event
this.modalEl.addEventListener('scroll', updateSuggestionsPosition);
// 在模态框关闭时移除事件监听
// Remove event listeners when modal closes
this.modalEl.onclose = () => {
this.modalEl.removeEventListener('scroll', updateSuggestionsPosition);
};
// 创建按钮区域
// Create button area
const buttonArea = contentEl.createEl('div', { cls: 'button-area' });
const submitButton = buttonArea.createEl('button', {
text: isUpdate ? '更新' : '发布',
text: isUpdate ? t('UPDATE') : t('PUBLISH'),
cls: 'submit-button'
});
// 创建提示信息容器
// Create notice container
const noticeContainer = buttonArea.createEl('div');
submitButton.onclick = async () => {
@ -625,25 +636,25 @@ export class SelectCategoryModal extends Modal {
await this.plugin.saveSettings();
}
// 保存选中的标签
// Save selected tags
this.plugin.settings.selectedTags = Array.from(selectedTags);
await this.plugin.saveSettings();
// 禁用提交按钮,显示加载状态
// Disable submit button, show loading state
submitButton.disabled = true;
submitButton.textContent = isUpdate ? '更新中...' : '发布中...';
submitButton.textContent = isUpdate ? t('UPDATING') : t('PUBLISHING');
try {
const reply = await this.plugin.postTopic();
// 显示提示信息
// Show notice
noticeContainer.empty();
if (reply.message === 'Success') {
noticeContainer.createEl('div', {
cls: 'notice success',
text: isUpdate ? '✓ 更新成功!' : '✓ 发布成功!'
text: isUpdate ? t('UPDATE_SUCCESS') : t('PUBLISH_SUCCESS')
});
// 成功后延迟关闭
// Close after success
setTimeout(() => {
this.close();
}, 1500);
@ -651,24 +662,24 @@ export class SelectCategoryModal extends Modal {
const errorContainer = noticeContainer.createEl('div', { cls: 'notice error' });
errorContainer.createEl('div', {
cls: 'error-title',
text: isUpdate ? '更新失败' : '发布失败'
text: isUpdate ? t('UPDATE_ERROR') : t('PUBLISH_ERROR')
});
// 显示 Discourse 返回的具体错误信息
// Show Discourse-specific error information
errorContainer.createEl('div', {
cls: 'error-message',
text: reply.error || (isUpdate ? '更新失败,请重试' : '发布失败,请重试')
text: reply.error || (isUpdate ? t('UPDATE_FAILED') + ', ' + t('TRY_AGAIN') : t('PUBLISH_FAILED') + ', ' + t('TRY_AGAIN'))
});
// 添加重试按钮
// Add retry button
const retryButton = errorContainer.createEl('button', {
cls: 'retry-button',
text: '重试'
text: t('RETRY')
});
retryButton.onclick = () => {
noticeContainer.empty();
submitButton.disabled = false;
submitButton.textContent = isUpdate ? '更新' : '发布';
submitButton.textContent = isUpdate ? t('UPDATE') : t('PUBLISH');
};
}
} catch (error) {
@ -676,29 +687,29 @@ export class SelectCategoryModal extends Modal {
const errorContainer = noticeContainer.createEl('div', { cls: 'notice error' });
errorContainer.createEl('div', {
cls: 'error-title',
text: isUpdate ? '更新出错' : '发布出错'
text: isUpdate ? t('UPDATE_ERROR') : t('PUBLISH_ERROR')
});
errorContainer.createEl('div', {
cls: 'error-message',
text: error.message || '未知错误'
text: error.message || t('UNKNOWN_ERROR')
});
// 添加重试按钮
// Add retry button
const retryButton = errorContainer.createEl('button', {
cls: 'retry-button',
text: '重试'
text: t('RETRY')
});
retryButton.onclick = () => {
noticeContainer.empty();
submitButton.disabled = false;
submitButton.textContent = isUpdate ? '更新' : '发布';
submitButton.textContent = isUpdate ? t('UPDATE') : t('PUBLISH');
};
}
// 如果发生错误,重置按钮状态
// If error occurs, reset button state
if (submitButton.disabled) {
submitButton.disabled = false;
submitButton.textContent = isUpdate ? '更新' : '发布';
submitButton.textContent = isUpdate ? t('UPDATE') : t('PUBLISH');
}
};
}

View File

@ -7,7 +7,7 @@ If your plugin does not need CSS, delete this file.
*/
/* 基础模态框样式覆盖 */
/* Basic Modal Style Override */
.modal.mod-discourse-sync {
max-width: 500px;
max-height: 90vh;
@ -18,7 +18,7 @@ If your plugin does not need CSS, delete this file.
flex-direction: column;
}
/* 内容区域样式 */
/* Content Area Style */
.discourse-sync-modal {
padding: 24px;
width: 100%;
@ -28,14 +28,14 @@ If your plugin does not need CSS, delete this file.
overflow-y: auto;
}
/* 表单区域 */
/* Form Area */
.discourse-sync-modal .form-area {
flex-grow: 1;
min-height: 0;
margin-bottom: 16px;
}
/* 按钮区域固定在底部 */
/* Button Area Fixed at Bottom */
.discourse-sync-modal .button-area {
flex-shrink: 0;
margin-top: auto;
@ -48,11 +48,11 @@ If your plugin does not need CSS, delete this file.
color: var(--text-normal);
}
/* 表单容器通用样式 */
/* Common Form Container Style */
.discourse-sync-modal .select-container,
.discourse-sync-modal .tag-container {
margin-bottom: 24px;
padding: 0; /* 移除内边距 */
padding: 0; /* Remove padding */
}
.discourse-sync-modal .select-container label,
@ -63,7 +63,7 @@ If your plugin does not need CSS, delete this file.
color: var(--text-normal);
}
/* 输入框和选择器样式 */
/* Input and Selector Style */
.discourse-sync-modal select,
.discourse-sync-modal .tag-select-area {
width: 100%;
@ -108,7 +108,7 @@ If your plugin does not need CSS, delete this file.
cursor: not-allowed;
}
/* 提示信息样式 */
/* Notice Style */
.discourse-sync-modal .notice {
margin-top: 16px;
padding: 16px;
@ -171,7 +171,7 @@ If your plugin does not need CSS, delete this file.
color: white;
}
/* 标签选择区域样式 */
/* Tag Select Area Style */
.discourse-sync-modal .tag-select-area {
padding: 8px;
display: flex;
@ -181,7 +181,7 @@ If your plugin does not need CSS, delete this file.
}
.discourse-sync-modal .selected-tags {
display: none; /* 默认隐藏 */
display: none; /* Hidden by default */
flex-wrap: wrap;
gap: 6px;
margin-bottom: 8px;
@ -189,7 +189,7 @@ If your plugin does not need CSS, delete this file.
}
.discourse-sync-modal .selected-tags:not(:empty) {
display: flex; /* 当有内容时显示 */
display: flex; /* Show when has content */
}
.discourse-sync-modal .tag {
@ -237,13 +237,13 @@ If your plugin does not need CSS, delete this file.
border-bottom: 1px solid var(--background-modifier-border);
}
/* 标签输入容器 */
/* Tag Input Container */
.discourse-sync-modal .tag-input-container {
position: relative;
width: 100%;
}
/* 标签建议下拉框 */
/* Tag Suggestion Dropdown */
.discourse-sync-modal .tag-suggestions {
position: fixed;
background-color: var(--background-primary);
@ -254,14 +254,14 @@ If your plugin does not need CSS, delete this file.
max-height: 180px;
overflow-y: auto;
margin-top: 4px;
display: none; /* 默认隐藏 */
display: none; /* Hidden by default */
}
.discourse-sync-modal .tag-suggestions:not(:empty) {
display: block; /* 当有内容时显示 */
display: block; /* Show when has content */
}
/* 标签建议项 */
/* Tag Suggestion Item */
.discourse-sync-modal .tag-suggestion {
padding: 8px 12px;
cursor: pointer;
@ -279,7 +279,7 @@ If your plugin does not need CSS, delete this file.
background-color: var(--background-modifier-hover);
}
/* 美化滚动条 */
/* Tag Suggestion Scrollbar Style */
.discourse-sync-modal .tag-suggestions::-webkit-scrollbar {
width: 4px;
}
@ -297,7 +297,7 @@ If your plugin does not need CSS, delete this file.
background-color: var(--background-modifier-border-hover);
}
/* 移除 tag-container 的 z-index避免干扰 */
/* Remove tag-container z-index to avoid interference */
.discourse-sync-modal .tag-container {
position: relative;
}