feat(i18n): 实现多语言支持并优化脚本注入

为工具栏、配置面板、预览面板等组件添加多语言支持
优化页面脚本注入逻辑,添加超时处理和清理机制
添加语言变更时强制重新渲染Lit组件的功能
更新中英文翻译文件
这个提交包含在:
Zain 2025-12-30 12:32:16 +08:00
父节点 104407fe6b
当前提交 4c27a3e18c
共有 11 个文件被更改,包括 106 次插入 和 26 次删除

查看文件

@ -208,7 +208,7 @@ export class ConfigPanel extends LitElement {
${!showExecutionView && hasConflicts
? html`
<div class="warning-message">
⚠️ 检测到 ${this.conflictCount} 个冲突
⚠️ ${I18nService.t('conflicts_detected', [String(this.conflictCount)])}
</div>
`
: ''}

查看文件

@ -446,7 +446,7 @@ export class FileSelectorPanel extends LitElement {
// Confirm if there are conflicts
if (this.conflictIds.size > 0) {
const conflictMessage = `检测到 ${this.conflictIds.size} 个冲突,是否继续?`;
const conflictMessage = I18nService.t('conflicts_detected_confirm', [String(this.conflictIds.size)]);
const confirmed = confirm(conflictMessage);
if (!confirmed) {

查看文件

@ -60,9 +60,7 @@ export class PreviewPanel extends LitElement {
<line x1="12" y1="9" x2="12" y2="13"></line>
<line x1="12" y1="17" x2="12.01" y2="17"></line>
</svg>
<span>
检测到 ${this.conflictCount} 个冲突
</span>
<span>${I18nService.t('conflicts_detected', [String(this.conflictCount)])}</span>
</div>
`
: ''}
@ -70,7 +68,7 @@ export class PreviewPanel extends LitElement {
<div class="panel-stats">
<div class="stat-item">
<span class="stat-label">预览项:</span>
<span class="stat-label">${I18nService.t('preview_items')}</span>
<span class="stat-value">${this.items.length}</span>
</div>
${this.showStatus
@ -92,7 +90,7 @@ export class PreviewPanel extends LitElement {
${hasConflicts
? html`
<div class="stat-item conflict">
<span class="stat-label">冲突:</span>
<span class="stat-label">${I18nService.t('preview_summary_conflict')}</span>
<span class="stat-value">${this.conflictCount}</span>
</div>
`
@ -104,7 +102,7 @@ export class PreviewPanel extends LitElement {
? html`
<div class="loading-state">
<div class="spinner"></div>
<p>正在生成预览...</p>
<p>${I18nService.t('preview_loading')}</p>
</div>
`
: html`

查看文件

@ -152,7 +152,7 @@ export class Toolbar extends LitElement {
<div class="toolbar-right">
<span class="selected-count">
已选择 ${this.selectedCount} / ${this.totalCount} 个文件
${I18nService.t('selected_files_count', [String(this.selectedCount), String(this.totalCount)])}
</span>
</div>
</div>

查看文件

@ -2,6 +2,7 @@ import { LitElement, html, css, type PropertyValues } from 'lit';
import { customElement, property } from 'lit/decorators.js';
import { virtualize, virtualizerRef } from '@lit-labs/virtualizer/virtualize.js';
import { FileItem } from '../../types/platform';
import { I18nService } from '../../utils/i18n';
/**
* Virtual File List Component
@ -201,7 +202,7 @@ export class VirtualFileList extends LitElement {
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
<path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"></path>
</svg>
<p>没有找到文件</p>
<p>${I18nService.t('no_files_found')}</p>
</div>
`;
}

查看文件

@ -153,7 +153,7 @@ export class VirtualPreviewList extends LitElement {
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
<polyline points="14 2 14 8 20 8"></polyline>
</svg>
<p>选择文件并配置规则以查看预览</p>
<p>${I18nService.t('preview_empty_hint')}</p>
</div>
`;
}

查看文件

@ -169,36 +169,60 @@ function cleanupOldInstances(): void {
* @param platform - 平台名称 (aliyun/baidu/quark)
*/
async function injectPageScriptToMainWorld(platform: 'aliyun' | 'baidu' | 'quark'): Promise<void> {
// ✅ 引用编译后的 .js 文件(Vite 会将 TypeScript 编译为 JavaScript)
const scriptPath = `src/adapters/${platform}/page-script.js`;
const scriptURL = chrome.runtime.getURL(scriptPath);
let script: HTMLScriptElement | null = null;
try {
// ✅ 引用编译后的 .js 文件(Vite 会将 TypeScript 编译为 JavaScript)
const scriptPath = `src/adapters/${platform}/page-script.js`;
const scriptURL = chrome.runtime.getURL(scriptPath);
// 创建 script 标签并注入到页面 (MAIN world)
const script = document.createElement('script');
script.src = scriptURL;
script.type = 'module'; // 支持 ES6 模块
const scriptEl = document.createElement('script');
script = scriptEl;
scriptEl.src = scriptURL;
scriptEl.type = 'module'; // 支持 ES6 模块
// 等待脚本加载完成
// 等待脚本加载完成(加超时保护,避免 CSP/站点脚本拦截导致 onload/onerror 永不触发,从而阻塞整个悬浮按钮注入)
const timeoutMs = 1500;
await new Promise<void>((resolve, reject) => {
script.onload = () => {
const cleanup = () => {
scriptEl.onload = null;
scriptEl.onerror = null;
};
const timer = window.setTimeout(() => {
cleanup();
reject(new Error(`Timeout loading page-script for ${platform}`));
}, timeoutMs);
scriptEl.onload = () => {
window.clearTimeout(timer);
cleanup();
resolve();
};
script.onerror = () => {
scriptEl.onerror = () => {
window.clearTimeout(timer);
cleanup();
reject(new Error(`Failed to load page-script for ${platform}`));
};
// 注入到页面
(document.head || document.documentElement).appendChild(script);
(document.head || document.documentElement).appendChild(scriptEl);
});
// 脚本加载后可以移除标签 (代码已执行)
script.remove();
scriptEl.remove();
logger.info(`${platform} page-script injected to MAIN world successfully`);
} catch (error) {
logger.error(`Failed to inject ${platform} page-script:`, error instanceof Error ? error : new Error(String(error)));
throw error;
// 不要阻塞悬浮按钮注入:即使 page-script 注入失败,也应该让 UI 先显示出来
// 相关平台的 API 调用可能会退化/失败,但用户至少能看到按钮和面板,且能在控制台看到诊断信息
logger.warn(
`[DIAG] Failed to inject ${platform} page-script (continue without it):`,
error instanceof Error ? error : new Error(String(error))
);
} finally {
// Best-effort cleanup: 避免遗留无用的 <script>
script?.remove();
}
}
@ -395,6 +419,40 @@ function setupStorageListener(platform: PlatformName): void {
});
}
/**
* Force Lit-based components to re-render when language changes.
*
* Reason: I18nService stores language in a static field; updating it won't
* automatically trigger LitElement re-rendering unless some reactive state changes.
* We traverse the panel subtree (including nested shadow roots) and call
* requestUpdate() for any element that exposes it.
*/
function requestLitRenderDeep(root: Element): void {
const queue: Array<Element | ShadowRoot> = [root];
const visited = new Set<Node>();
while (queue.length) {
const node = queue.shift();
if (!node || visited.has(node)) continue;
visited.add(node);
if (node instanceof Element) {
const maybeLit = node as unknown as { requestUpdate?: () => void; shadowRoot?: ShadowRoot | null };
maybeLit.requestUpdate?.();
if (maybeLit.shadowRoot) {
queue.push(maybeLit.shadowRoot);
}
queue.push(...Array.from(node.children));
continue;
}
// ShadowRoot (DocumentFragment)
queue.push(...Array.from(node.children));
}
}
/**
* 设置语言变更监听器
* 同步 popup 的语言切换到 content script 上下文
@ -411,6 +469,11 @@ function setupLanguageChangeListener(): void {
// 通知 FloatingButton 更新 UI
floatingButton?.updateLanguage();
// ✅ 同步更新已打开的面板(以及其内部的所有 Lit 组件)
if (fileSelectorPanel?.open) {
requestLitRenderDeep(fileSelectorPanel);
}
logger.info(`Language synced successfully to: ${newLanguage}`);
}
});

查看文件

@ -6,6 +6,7 @@
"config_panel_title": "Configuration",
"file_list_panel_title": "File List",
"preview_panel_title": "Preview",
"preview_items": "Preview items:",
"execute_rename": "Execute Rename",
"executing": "Executing...",
"loading_files": "Loading files...",
@ -24,6 +25,8 @@
"type_image": "Image",
"type_document": "Document",
"type_other": "Other",
"selected_files_count": "Selected $1 / $2 files",
"no_files_found": "No files found",
"rename_dialog_file_count": "files",
"rename_dialog_close": "×",
"rule_selector_title": "Select Rename Rule",
@ -90,7 +93,10 @@
"sync_failed": "Sync failed, please refresh the page",
"sync_retry": "Resync",
"sync_not_supported": "Auto sync is not supported on this platform. Please refresh the page.",
"conflicts_detected": "Conflicts detected: $1",
"conflicts_detected_confirm": "Conflicts detected: $1. Continue?",
"preview_empty": "No files to preview",
"preview_empty_hint": "Select files and configure rules to see the preview",
"preview_loading": "Generating preview...",
"preview_summary_total": "Total:",
"preview_summary_changed": "To Change:",

查看文件

@ -6,6 +6,7 @@
"config_panel_title": "配置",
"file_list_panel_title": "文件列表",
"preview_panel_title": "预览",
"preview_items": "预览项:",
"execute_rename": "执行重命名",
"executing": "执行中...",
"loading_files": "正在加载文件...",
@ -24,6 +25,8 @@
"type_image": "图片",
"type_document": "文档",
"type_other": "其他",
"selected_files_count": "已选择 $1 / $2 个文件",
"no_files_found": "没有找到文件",
"rename_dialog_file_count": "个文件",
"rename_dialog_close": "×",
"rule_selector_title": "选择重命名规则",
@ -90,7 +93,10 @@
"sync_failed": "同步失败,可能需要手动刷新页面",
"sync_retry": "重新同步",
"sync_not_supported": "当前平台暂不支持自动同步,请手动刷新页面",
"conflicts_detected": "检测到 $1 个冲突",
"conflicts_detected_confirm": "检测到 $1 个冲突,是否继续?",
"preview_empty": "暂无文件可预览",
"preview_empty_hint": "选择文件并配置规则以查看预览",
"preview_loading": "生成预览中...",
"preview_summary_total": "总计:",
"preview_summary_changed": "将改变:",

查看文件

@ -6,6 +6,7 @@
"config_panel_title": "設定",
"file_list_panel_title": "檔案列表",
"preview_panel_title": "預覽",
"preview_items": "預覽項:",
"execute_rename": "執行重新命名",
"executing": "執行中...",
"loading_files": "正在載入檔案...",
@ -24,6 +25,8 @@
"type_image": "圖片",
"type_document": "文件",
"type_other": "其他",
"selected_files_count": "已選擇 $1 / $2 個檔案",
"no_files_found": "沒有找到檔案",
"rename_dialog_file_count": "個檔案",
"rename_dialog_close": "×",
"rule_selector_title": "選擇重新命名規則",
@ -90,7 +93,10 @@
"sync_failed": "同步失敗,可能需要手動重新整理頁面",
"sync_retry": "重新同步",
"sync_not_supported": "目前平台暫不支援自動同步,請手動重新整理頁面",
"conflicts_detected": "偵測到 $1 個衝突",
"conflicts_detected_confirm": "偵測到 $1 個衝突,是否繼續?",
"preview_empty": "暫無檔案可預覽",
"preview_empty_hint": "選擇檔案並設定規則以查看預覽",
"preview_loading": "產生預覽中...",
"preview_summary_total": "總計:",
"preview_summary_changed": "將變更:",

查看文件

@ -10,7 +10,7 @@
* 1. Edit version in package.json
* 2. Run: npm run sync:version (or automatically via prebuild/predev)
*
* Generated at: 2025-12-29T11:20:31.860Z
* Generated at: 2025-12-30T04:26:21.804Z
*/
/**