镜像自地址
https://github.com/lhly/cloud-drive-renamer.git
已同步 2026-10-20 13:47:47 +08:00
feat(i18n): 实现多语言支持并优化脚本注入
为工具栏、配置面板、预览面板等组件添加多语言支持 优化页面脚本注入逻辑,添加超时处理和清理机制 添加语言变更时强制重新渲染Lit组件的功能 更新中英文翻译文件
这个提交包含在:
父节点
104407fe6b
当前提交
4c27a3e18c
@ -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
|
||||
*/
|
||||
|
||||
/**
|
||||
|
||||
正在加载...
在新工单中引用
屏蔽一个用户