✨ Add translation features
This commit is contained in:
364
frontend/src/views/editor/extensions/translator/index.ts
Normal file
364
frontend/src/views/editor/extensions/translator/index.ts
Normal file
@@ -0,0 +1,364 @@
|
||||
import { Extension, StateField, StateEffect } from '@codemirror/state';
|
||||
import { EditorView, showTooltip, Tooltip } from '@codemirror/view';
|
||||
import { createTranslationTooltip } from './tooltip';
|
||||
|
||||
/**
|
||||
* 翻译器扩展配置
|
||||
*/
|
||||
export interface TranslatorConfig {
|
||||
/** 默认翻译服务提供商 */
|
||||
defaultTranslator: string;
|
||||
/** 最小选择字符数才显示翻译按钮 */
|
||||
minSelectionLength: number;
|
||||
/** 最大翻译字符数 */
|
||||
maxTranslationLength: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 默认翻译器配置
|
||||
*/
|
||||
export const defaultConfig: TranslatorConfig = {
|
||||
defaultTranslator: 'bing',
|
||||
minSelectionLength: 2,
|
||||
maxTranslationLength: 5000,
|
||||
};
|
||||
|
||||
// 全局配置存储
|
||||
let currentConfig: TranslatorConfig = {...defaultConfig};
|
||||
// 存储选择的文本用于翻译
|
||||
let selectedTextForTranslation = "";
|
||||
|
||||
|
||||
/**
|
||||
* 翻译图标SVG
|
||||
*/
|
||||
const translationIconSvg = `
|
||||
<svg class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" width="24" height="24">
|
||||
<path d="M599.68 485.056h-8l30.592 164.672c20.352-7.04 38.72-17.344 54.912-31.104a271.36 271.36 0 0 1-40.704-64.64l32.256-4.032c8.896 17.664 19.072 33.28 30.592 46.72 23.872-27.968 42.24-65.152 55.04-111.744l-154.688 0.128z m121.92 133.76c18.368 15.36 39.36 26.56 62.848 33.472l14.784 4.416-8.64 30.336-14.72-4.352a205.696 205.696 0 0 1-76.48-41.728c-20.672 17.92-44.928 31.552-71.232 40.064l20.736 110.912H519.424l-9.984 72.512h385.152c18.112 0 32.704-14.144 32.704-31.616V295.424a32.128 32.128 0 0 0-32.704-31.552H550.528l35.2 189.696h79.424v-31.552h61.44v31.552h102.4v31.616h-42.688c-14.272 55.488-35.712 100.096-64.64 133.568zM479.36 791.68H193.472c-36.224 0-65.472-28.288-65.472-63.168V191.168C128 156.16 157.312 128 193.472 128h327.68l20.544 104.32h352.832c36.224 0 65.472 28.224 65.472 63.104v537.408c0 34.944-29.312 63.168-65.472 63.168H468.608l10.688-104.32zM337.472 548.352v-33.28H272.768v-48.896h60.16V433.28h-60.16v-41.728h64.704v-32.896h-102.4v189.632h102.4z m158.272 0V453.76c0-17.216-4.032-30.272-12.16-39.488-8.192-9.152-20.288-13.696-36.032-13.696a55.04 55.04 0 0 0-24.768 5.376 39.04 39.04 0 0 0-17.088 15.936h-1.984l-5.056-18.56h-28.352V548.48h37.12V480c0-17.088 2.304-29.376 6.912-36.736 4.608-7.424 12.16-11.072 22.528-11.072 7.616 0 13.248 2.56 16.64 7.872 3.52 5.248 5.312 13.056 5.312 23.488v84.736h36.928z" fill="currentColor"></path>
|
||||
</svg>`;
|
||||
|
||||
// 用于设置翻译气泡的状态效果
|
||||
const setTranslationTooltip = StateEffect.define<Tooltip | null>();
|
||||
|
||||
/**
|
||||
* 翻译气泡的状态字段
|
||||
*/
|
||||
const translationTooltipField = StateField.define<readonly Tooltip[]>({
|
||||
create() {
|
||||
return [];
|
||||
},
|
||||
update(tooltips, tr) {
|
||||
// 如果文档或选择变化,隐藏气泡
|
||||
if (tr.docChanged || tr.selection) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// 检查是否有特定的状态效果来更新tooltips
|
||||
for (const effect of tr.effects) {
|
||||
if (effect.is(setTranslationTooltip)) {
|
||||
return effect.value ? [effect.value] : [];
|
||||
}
|
||||
}
|
||||
|
||||
return tooltips;
|
||||
},
|
||||
provide: field => showTooltip.computeN([field], state => state.field(field))
|
||||
});
|
||||
|
||||
|
||||
/**
|
||||
* 根据当前选择获取翻译按钮tooltip
|
||||
*/
|
||||
function getTranslationButtonTooltips(state: any): readonly Tooltip[] {
|
||||
|
||||
// 如果气泡已显示,则不显示按钮
|
||||
if (state.field(translationTooltipField).length > 0) return [];
|
||||
|
||||
const selection = state.selection.main;
|
||||
|
||||
// 如果没有选中文本,不显示按钮
|
||||
if (selection.empty) return [];
|
||||
|
||||
// 获取选中的文本
|
||||
const selectedText = state.sliceDoc(selection.from, selection.to);
|
||||
|
||||
// 检查文本是否只包含空格
|
||||
if (!selectedText.trim()) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// 检查文本长度条件
|
||||
if (selectedText.length < currentConfig.minSelectionLength ||
|
||||
selectedText.length > currentConfig.maxTranslationLength) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// 保存选中的文本用于翻译
|
||||
selectedTextForTranslation = selectedText;
|
||||
|
||||
// 返回翻译按钮tooltip配置
|
||||
return [{
|
||||
pos: selection.to,
|
||||
above: false,
|
||||
strictSide: true,
|
||||
arrow: false,
|
||||
create: (view) => {
|
||||
// 创建按钮DOM
|
||||
const dom = document.createElement('div');
|
||||
dom.className = 'cm-translator-button';
|
||||
dom.innerHTML = translationIconSvg;
|
||||
|
||||
// 点击事件
|
||||
dom.addEventListener('mousedown', (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
// 显示翻译气泡
|
||||
showTranslationTooltip(view);
|
||||
});
|
||||
|
||||
return { dom };
|
||||
}
|
||||
}];
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示翻译气泡
|
||||
*/
|
||||
function showTranslationTooltip(view: EditorView) {
|
||||
if (!selectedTextForTranslation) return;
|
||||
|
||||
// 创建翻译气泡
|
||||
const tooltip = createTranslationTooltip(view, selectedTextForTranslation);
|
||||
|
||||
// 更新状态以显示气泡
|
||||
view.dispatch({
|
||||
effects: setTranslationTooltip.of(tooltip)
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 翻译按钮的状态字段
|
||||
*/
|
||||
const translationButtonField = StateField.define<readonly Tooltip[]>({
|
||||
create(state) {
|
||||
return getTranslationButtonTooltips(state);
|
||||
},
|
||||
|
||||
update(tooltips, tr) {
|
||||
// 如果文档或选择变化,重新计算tooltip
|
||||
if (tr.docChanged || tr.selection) {
|
||||
return getTranslationButtonTooltips(tr.state);
|
||||
}
|
||||
|
||||
// 检查是否有翻译气泡显示,如果有则不显示按钮
|
||||
if (tr.state.field(translationTooltipField).length > 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return tooltips;
|
||||
},
|
||||
|
||||
provide: field => showTooltip.computeN([field], state => state.field(field))
|
||||
});
|
||||
|
||||
/**
|
||||
* 创建翻译扩展
|
||||
*/
|
||||
export function createTranslatorExtension(config?: Partial<TranslatorConfig>): Extension {
|
||||
// 更新配置
|
||||
currentConfig = { ...defaultConfig, ...config };
|
||||
|
||||
return [
|
||||
// 翻译按钮tooltip
|
||||
translationButtonField,
|
||||
// 翻译气泡tooltip
|
||||
translationTooltipField,
|
||||
|
||||
// 添加基础样式
|
||||
EditorView.baseTheme({
|
||||
".cm-translator-button": {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
cursor: "pointer",
|
||||
background: "var(--bg-secondary, transparent)",
|
||||
color: "var(--text-muted, #4285f4)",
|
||||
border: "1px solid var(--border-color, #dadce0)",
|
||||
borderRadius: "3px",
|
||||
padding: "2px",
|
||||
width: "24px",
|
||||
height: "24px",
|
||||
boxShadow: "0 1px 2px rgba(0, 0, 0, 0.08)",
|
||||
userSelect: "none",
|
||||
"&:hover": {
|
||||
background: "var(--bg-hover, rgba(66, 133, 244, 0.1))"
|
||||
}
|
||||
},
|
||||
|
||||
// 翻译气泡样式
|
||||
".cm-translation-tooltip": {
|
||||
background: "var(--bg-secondary, #fff)",
|
||||
color: "var(--text-primary, #333)",
|
||||
border: "1px solid var(--border-color, #dadce0)",
|
||||
borderRadius: "3px",
|
||||
boxShadow: "0 2px 8px rgba(0, 0, 0, 0.1)",
|
||||
padding: "8px",
|
||||
maxWidth: "300px",
|
||||
maxHeight: "200px",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
overflow: "hidden",
|
||||
fontFamily: "var(--font-family, system-ui, -apple-system, sans-serif)",
|
||||
fontSize: "11px"
|
||||
},
|
||||
|
||||
".cm-translation-header": {
|
||||
marginBottom: "8px",
|
||||
flexShrink: "0"
|
||||
},
|
||||
|
||||
".cm-translation-controls": {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "4px",
|
||||
flexWrap: "nowrap"
|
||||
},
|
||||
|
||||
".cm-translation-select": {
|
||||
padding: "2px 4px",
|
||||
borderRadius: "3px",
|
||||
border: "1px solid var(--border-color, #dadce0)",
|
||||
background: "var(--bg-primary, #f5f5f5)",
|
||||
fontSize: "11px",
|
||||
color: "var(--text-primary, #333)",
|
||||
flex: "1",
|
||||
minWidth: "0",
|
||||
maxWidth: "80px"
|
||||
},
|
||||
|
||||
".cm-translation-swap": {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
width: "16px",
|
||||
height: "16px",
|
||||
borderRadius: "3px",
|
||||
border: "1px solid var(--border-color, #dadce0)",
|
||||
background: "var(--bg-primary, transparent)",
|
||||
color: "var(--text-muted, #666)",
|
||||
cursor: "pointer",
|
||||
padding: "0",
|
||||
flexShrink: "0",
|
||||
"&:hover": {
|
||||
background: "var(--bg-hover, rgba(66, 133, 244, 0.1))"
|
||||
}
|
||||
},
|
||||
|
||||
// 滚动容器
|
||||
".cm-translation-scroll-container": {
|
||||
overflowY: "auto",
|
||||
flex: "1",
|
||||
minHeight: "0"
|
||||
},
|
||||
|
||||
".cm-translation-result": {
|
||||
display: "flex",
|
||||
flexDirection: "column"
|
||||
},
|
||||
|
||||
".cm-translation-result-header": {
|
||||
display: "flex",
|
||||
justifyContent: "flex-end",
|
||||
marginBottom: "4px"
|
||||
},
|
||||
|
||||
".cm-translation-result-wrapper": {
|
||||
position: "relative",
|
||||
width: "100%"
|
||||
},
|
||||
|
||||
".cm-translation-copy-btn": {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
width: "20px",
|
||||
height: "20px",
|
||||
borderRadius: "3px",
|
||||
border: "1px solid var(--border-color, #dadce0)",
|
||||
background: "var(--bg-primary, transparent)",
|
||||
color: "var(--text-muted, #666)",
|
||||
cursor: "pointer",
|
||||
padding: "0",
|
||||
position: "absolute",
|
||||
top: "4px",
|
||||
right: "4px",
|
||||
zIndex: "2",
|
||||
opacity: "0.7",
|
||||
"&:hover": {
|
||||
background: "var(--bg-hover, rgba(66, 133, 244, 0.1))",
|
||||
opacity: "1"
|
||||
},
|
||||
"&.copied": {
|
||||
background: "var(--bg-success, #4caf50)",
|
||||
color: "white",
|
||||
border: "1px solid var(--bg-success, #4caf50)",
|
||||
opacity: "1"
|
||||
}
|
||||
},
|
||||
|
||||
".cm-translation-target": {
|
||||
padding: "6px",
|
||||
paddingRight: "28px", // 为复制按钮留出空间
|
||||
background: "var(--bg-primary, rgba(66, 133, 244, 0.05))",
|
||||
color: "var(--text-primary, #333)",
|
||||
borderRadius: "3px",
|
||||
whiteSpace: "pre-wrap",
|
||||
wordBreak: "break-word"
|
||||
},
|
||||
|
||||
".cm-translation-notice": {
|
||||
fontSize: "10px",
|
||||
color: "var(--text-muted, #888)",
|
||||
padding: "2px 0",
|
||||
fontStyle: "italic",
|
||||
textAlign: "center",
|
||||
marginBottom: "2px"
|
||||
},
|
||||
|
||||
".cm-translation-error": {
|
||||
color: "var(--text-danger, #d32f2f)",
|
||||
fontStyle: "italic"
|
||||
},
|
||||
|
||||
".cm-translation-loading": {
|
||||
padding: "8px",
|
||||
textAlign: "center",
|
||||
color: "var(--text-muted, #666)",
|
||||
fontSize: "11px",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
gap: "6px"
|
||||
},
|
||||
|
||||
".cm-translation-loading::before": {
|
||||
content: "''",
|
||||
display: "inline-block",
|
||||
width: "12px",
|
||||
height: "12px",
|
||||
borderRadius: "50%",
|
||||
border: "2px solid var(--text-muted, #666)",
|
||||
borderTopColor: "transparent",
|
||||
animation: "cm-translation-spin 1s linear infinite"
|
||||
},
|
||||
|
||||
"@keyframes cm-translation-spin": {
|
||||
"0%": { transform: "rotate(0deg)" },
|
||||
"100%": { transform: "rotate(360deg)" }
|
||||
}
|
||||
})
|
||||
];
|
||||
}
|
||||
|
||||
export default createTranslatorExtension;
|
507
frontend/src/views/editor/extensions/translator/tooltip.ts
Normal file
507
frontend/src/views/editor/extensions/translator/tooltip.ts
Normal file
@@ -0,0 +1,507 @@
|
||||
import { EditorView, Tooltip, TooltipView } from '@codemirror/view';
|
||||
import { useTranslationStore } from '@/stores/translationStore';
|
||||
|
||||
// 创建翻译气泡弹窗
|
||||
export class TranslationTooltip implements TooltipView {
|
||||
dom: HTMLElement;
|
||||
sourceText: string;
|
||||
translationStore: ReturnType<typeof useTranslationStore>;
|
||||
|
||||
// UI元素
|
||||
private translatorSelector: HTMLSelectElement;
|
||||
private sourceLangSelector: HTMLSelectElement;
|
||||
private targetLangSelector: HTMLSelectElement;
|
||||
private resultContainer: HTMLDivElement;
|
||||
private loadingIndicator: HTMLDivElement;
|
||||
private translatedText: string = '';
|
||||
private detectedSourceLang: string = ''; // 保存检测到的语言代码
|
||||
|
||||
constructor(_view: EditorView, text: string) {
|
||||
this.sourceText = text;
|
||||
this.translationStore = useTranslationStore();
|
||||
|
||||
// 创建气泡弹窗容器
|
||||
this.dom = document.createElement('div');
|
||||
this.dom.className = 'cm-translation-tooltip';
|
||||
|
||||
// 创建头部控制区域 - 固定在顶部
|
||||
const header = document.createElement('div');
|
||||
header.className = 'cm-translation-header';
|
||||
|
||||
// 控制选项容器 - 所有选择器在一行
|
||||
const controlsContainer = document.createElement('div');
|
||||
controlsContainer.className = 'cm-translation-controls';
|
||||
|
||||
// 创建选择器(初始为空,稍后填充)
|
||||
this.sourceLangSelector = document.createElement('select');
|
||||
this.sourceLangSelector.className = 'cm-translation-select';
|
||||
|
||||
// 交换语言按钮
|
||||
const swapButton = document.createElement('button');
|
||||
swapButton.className = 'cm-translation-swap';
|
||||
swapButton.innerHTML = `<svg viewBox="0 0 24 24" width="12" height="12"><path fill="currentColor" d="M7.5 21L3 16.5L7.5 12L9 13.5L7 15.5H15V13H17V17.5H7L9 19.5L7.5 21M16.5 3L21 7.5L16.5 12L15 10.5L17 8.5H9V11H7V6.5H17L15 4.5L16.5 3Z"/></svg>`;
|
||||
|
||||
// 目标语言选择
|
||||
this.targetLangSelector = document.createElement('select');
|
||||
this.targetLangSelector.className = 'cm-translation-select';
|
||||
|
||||
// 创建一个临时的翻译器选择器,稍后会被替换
|
||||
this.translatorSelector = document.createElement('select');
|
||||
this.translatorSelector.className = 'cm-translation-select';
|
||||
const tempOption = document.createElement('option');
|
||||
tempOption.textContent = 'Loading...';
|
||||
this.translatorSelector.appendChild(tempOption);
|
||||
|
||||
// 添加所有控制元素到一行
|
||||
controlsContainer.appendChild(this.sourceLangSelector);
|
||||
controlsContainer.appendChild(swapButton);
|
||||
controlsContainer.appendChild(this.targetLangSelector);
|
||||
controlsContainer.appendChild(this.translatorSelector);
|
||||
|
||||
// 添加到头部
|
||||
header.appendChild(controlsContainer);
|
||||
|
||||
// 创建内容滚动区域
|
||||
const scrollContainer = document.createElement('div');
|
||||
scrollContainer.className = 'cm-translation-scroll-container';
|
||||
|
||||
// 创建结果区域
|
||||
this.resultContainer = document.createElement('div');
|
||||
this.resultContainer.className = 'cm-translation-result';
|
||||
|
||||
// 加载指示器
|
||||
this.loadingIndicator = document.createElement('div');
|
||||
this.loadingIndicator.className = 'cm-translation-loading';
|
||||
this.loadingIndicator.textContent = 'Translation...';
|
||||
this.loadingIndicator.style.display = 'none';
|
||||
|
||||
// 将结果和加载指示器添加到滚动区域
|
||||
scrollContainer.appendChild(this.loadingIndicator);
|
||||
scrollContainer.appendChild(this.resultContainer);
|
||||
|
||||
// 将所有元素添加到主容器
|
||||
this.dom.appendChild(header);
|
||||
this.dom.appendChild(scrollContainer);
|
||||
|
||||
// 添加事件监听
|
||||
this.sourceLangSelector.addEventListener('change', () => {
|
||||
// 检查源语言和目标语言是否相同
|
||||
this.handleLanguageChange();
|
||||
this.translate();
|
||||
});
|
||||
|
||||
this.targetLangSelector.addEventListener('change', () => {
|
||||
// 检查源语言和目标语言是否相同
|
||||
this.handleLanguageChange();
|
||||
|
||||
// 增加选中语言的使用频率
|
||||
const targetLang = this.targetLangSelector.value;
|
||||
if (targetLang) {
|
||||
this.translationStore.incrementLanguageUsage(targetLang);
|
||||
}
|
||||
|
||||
this.translate();
|
||||
});
|
||||
|
||||
swapButton.addEventListener('click', () => {
|
||||
// 交换语言
|
||||
|
||||
const temp = this.sourceLangSelector.value;
|
||||
this.sourceLangSelector.value = this.targetLangSelector.value;
|
||||
this.targetLangSelector.value = temp;
|
||||
this.translate();
|
||||
});
|
||||
|
||||
// 显示加载中
|
||||
this.loadingIndicator.style.display = 'block';
|
||||
this.resultContainer.innerHTML = '<div class="cm-translation-loading">Loading...</div>';
|
||||
|
||||
// 加载翻译器选项
|
||||
this.loadTranslators().then(() => {
|
||||
// 尝试自动检测语言
|
||||
if (this.sourceText.length >= 10) {
|
||||
this.detectedSourceLang = this.translationStore.detectLanguage(this.sourceText);
|
||||
if (this.detectedSourceLang) {
|
||||
// 如果检测到语言,更新选择器
|
||||
this.updateLanguageSelectorsForDetectedLanguage(this.detectedSourceLang);
|
||||
}
|
||||
}
|
||||
|
||||
// 初始翻译
|
||||
this.translate();
|
||||
});
|
||||
}
|
||||
|
||||
// 处理语言变更,防止源和目标语言相同
|
||||
private handleLanguageChange() {
|
||||
// 防止源语言和目标语言相同
|
||||
if (this.sourceLangSelector.value === this.targetLangSelector.value) {
|
||||
// 寻找一个不同的目标语言
|
||||
const options = Array.from(this.targetLangSelector.options);
|
||||
for (const option of options) {
|
||||
if (option.value !== this.sourceLangSelector.value) {
|
||||
this.targetLangSelector.value = option.value;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 加载翻译器选项
|
||||
private async loadTranslators() {
|
||||
try {
|
||||
// 确保翻译器列表已加载
|
||||
if (!this.translationStore.hasTranslators) {
|
||||
await this.translationStore.loadAvailableTranslators();
|
||||
}
|
||||
|
||||
// 清空现有选项
|
||||
while (this.translatorSelector.firstChild) {
|
||||
this.translatorSelector.removeChild(this.translatorSelector.firstChild);
|
||||
}
|
||||
|
||||
// 添加翻译器选项
|
||||
const translators = this.translationStore.availableTranslators;
|
||||
|
||||
if (translators.length === 0) {
|
||||
// 如果没有可用翻译器,添加一个默认选项
|
||||
const option = document.createElement('option');
|
||||
option.value = 'bing';
|
||||
option.textContent = 'Bing';
|
||||
this.translatorSelector.appendChild(option);
|
||||
} else {
|
||||
translators.forEach(translator => {
|
||||
const option = document.createElement('option');
|
||||
option.value = translator;
|
||||
option.textContent = this.getTranslatorDisplayName(translator);
|
||||
option.selected = translator === this.translationStore.defaultTranslator;
|
||||
this.translatorSelector.appendChild(option);
|
||||
});
|
||||
}
|
||||
|
||||
// 添加事件监听
|
||||
this.translatorSelector.addEventListener('change', () => {
|
||||
// 更新当前翻译器
|
||||
this.translationStore.setDefaultConfig({
|
||||
translatorType: this.translatorSelector.value
|
||||
});
|
||||
|
||||
// 重置检测到的语言
|
||||
this.detectedSourceLang = '';
|
||||
|
||||
// 当切换翻译器时,可能需要重新排序语言列表
|
||||
|
||||
// 加载该翻译器的语言列表
|
||||
this.updateLanguageSelectors();
|
||||
|
||||
// 执行翻译
|
||||
this.translate();
|
||||
});
|
||||
|
||||
// 加载默认翻译器的语言列表
|
||||
await this.updateLanguageSelectors();
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Failed to load translators:', error);
|
||||
|
||||
// 清空现有选项
|
||||
while (this.translatorSelector.firstChild) {
|
||||
this.translatorSelector.removeChild(this.translatorSelector.firstChild);
|
||||
}
|
||||
|
||||
// 添加默认翻译器选项
|
||||
const defaultTranslators = ['bing', 'google', 'youdao', 'deepl'];
|
||||
defaultTranslators.forEach(translator => {
|
||||
const option = document.createElement('option');
|
||||
option.value = translator;
|
||||
option.textContent = this.getTranslatorDisplayName(translator);
|
||||
option.selected = translator === 'bing';
|
||||
this.translatorSelector.appendChild(option);
|
||||
});
|
||||
|
||||
// 添加事件监听
|
||||
this.translatorSelector.addEventListener('change', () => {
|
||||
// 更新选择器并重新翻译
|
||||
this.updateLanguageSelectors();
|
||||
this.translate();
|
||||
});
|
||||
|
||||
// 加载默认翻译器的语言列表
|
||||
await this.updateLanguageSelectors();
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 更新语言选择器
|
||||
private async updateLanguageSelectors() {
|
||||
const currentTranslator = this.translatorSelector.value;
|
||||
|
||||
// 保存当前选中的语言
|
||||
const currentSourceLang = this.sourceLangSelector.value || '';
|
||||
const currentTargetLang = this.targetLangSelector.value || 'zh';
|
||||
|
||||
// 清空源语言选择器
|
||||
while (this.sourceLangSelector.firstChild) {
|
||||
this.sourceLangSelector.removeChild(this.sourceLangSelector.firstChild);
|
||||
}
|
||||
|
||||
// 清空目标语言选择器
|
||||
while (this.targetLangSelector.firstChild) {
|
||||
this.targetLangSelector.removeChild(this.targetLangSelector.firstChild);
|
||||
}
|
||||
|
||||
// 获取当前翻译器的语言列表
|
||||
const languageMap = this.translationStore.currentLanguageMap;
|
||||
|
||||
// 如果语言列表为空,直接返回
|
||||
if (!languageMap || Object.keys(languageMap).length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取按使用频率排序的语言列表
|
||||
const sortedLanguages = this.translationStore.getSortedLanguages(currentTranslator);
|
||||
|
||||
// 添加所有语言选项
|
||||
if (Array.isArray(sortedLanguages)) {
|
||||
// 处理非分组返回值
|
||||
sortedLanguages.forEach(([code, langInfo]) => {
|
||||
this.addLanguageOption(code, langInfo);
|
||||
});
|
||||
} else {
|
||||
// 处理分组返回值
|
||||
// 先添加常用语言
|
||||
sortedLanguages.frequent.forEach(([code, langInfo]) => {
|
||||
this.addLanguageOption(code, langInfo);
|
||||
});
|
||||
|
||||
// 再添加其他语言
|
||||
sortedLanguages.others.forEach(([code, langInfo]) => {
|
||||
this.addLanguageOption(code, langInfo);
|
||||
});
|
||||
}
|
||||
|
||||
// 匹配之前的语言选项或使用默认值
|
||||
this.updateSelectedLanguages(currentSourceLang, currentTargetLang, currentTranslator);
|
||||
}
|
||||
|
||||
// 添加语言选项到选择器
|
||||
private addLanguageOption(code: string, langInfo: any) {
|
||||
// 使用后端提供的名称,而不是代码
|
||||
const displayName = langInfo.Name || langInfo.name || code;
|
||||
|
||||
// 源语言选项
|
||||
const sourceOption = document.createElement('option');
|
||||
sourceOption.value = code;
|
||||
sourceOption.textContent = displayName;
|
||||
this.sourceLangSelector.appendChild(sourceOption);
|
||||
|
||||
// 目标语言选项
|
||||
const targetOption = document.createElement('option');
|
||||
targetOption.value = code;
|
||||
|
||||
// 不再显示使用次数,直接使用语言名称
|
||||
targetOption.textContent = displayName;
|
||||
|
||||
this.targetLangSelector.appendChild(targetOption);
|
||||
}
|
||||
|
||||
// 更新选中的语言选项,确保语言代码在当前翻译器中有效
|
||||
private updateSelectedLanguages(sourceLang: string, targetLang: string, translatorType: string) {
|
||||
// 尝试在当前翻译器中找到匹配的语言代码
|
||||
const validSourceLang = this.translationStore.validateLanguage(sourceLang, translatorType);
|
||||
|
||||
// 如果找到有效的语言代码,且该代码在选择器中存在,则选中它
|
||||
if (validSourceLang && this.hasLanguageOption(this.sourceLangSelector, validSourceLang)) {
|
||||
this.sourceLangSelector.value = validSourceLang;
|
||||
} else if (this.detectedSourceLang) {
|
||||
// 如果没有找到匹配但有检测到的语言,尝试使用它
|
||||
const validDetectedLang = this.translationStore.validateLanguage(this.detectedSourceLang, translatorType);
|
||||
if (this.hasLanguageOption(this.sourceLangSelector, validDetectedLang)) {
|
||||
this.sourceLangSelector.value = validDetectedLang;
|
||||
} else if (this.sourceLangSelector.options.length > 0) {
|
||||
// 如果没有检测到的语言,使用第一个可用选项
|
||||
this.sourceLangSelector.selectedIndex = 0;
|
||||
}
|
||||
} else if (this.sourceLangSelector.options.length > 0) {
|
||||
// 如果没有检测到的语言,使用第一个可用选项
|
||||
this.sourceLangSelector.selectedIndex = 0;
|
||||
}
|
||||
|
||||
// 对于目标语言,尝试找到匹配的语言代码
|
||||
const validTargetLang = this.translationStore.validateLanguage(targetLang, translatorType);
|
||||
if (this.hasLanguageOption(this.targetLangSelector, validTargetLang)) {
|
||||
this.targetLangSelector.value = validTargetLang;
|
||||
} else {
|
||||
// 如果没有找到匹配,使用默认目标语言或第一个可用选项
|
||||
const defaultTarget = this.translationStore.defaultTargetLang;
|
||||
if (this.hasLanguageOption(this.targetLangSelector, defaultTarget)) {
|
||||
this.targetLangSelector.value = defaultTarget;
|
||||
} else if (this.targetLangSelector.options.length > 0) {
|
||||
this.targetLangSelector.selectedIndex = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// 确保源语言和目标语言不同
|
||||
this.handleLanguageChange();
|
||||
}
|
||||
|
||||
// 检查选择器是否有指定语言选项
|
||||
private hasLanguageOption(selector: HTMLSelectElement, langCode: string): boolean {
|
||||
return Array.from(selector.options).some(option => option.value === langCode);
|
||||
}
|
||||
|
||||
|
||||
// 为检测到的语言更新语言选择器
|
||||
private updateLanguageSelectorsForDetectedLanguage(detectedLang: string) {
|
||||
if (!detectedLang) return;
|
||||
|
||||
// 根据当前翻译器验证检测到的语言
|
||||
const currentTranslator = this.translatorSelector.value;
|
||||
const validLang = this.translationStore.validateLanguage(detectedLang, currentTranslator);
|
||||
|
||||
// 检查验证后的语言是否在选择器中
|
||||
const hasDetectedOption = this.hasLanguageOption(this.sourceLangSelector, validLang);
|
||||
|
||||
// 设置检测到的语言为源语言
|
||||
if (hasDetectedOption) {
|
||||
this.sourceLangSelector.value = validLang;
|
||||
}
|
||||
|
||||
// 存储检测到的语言代码,以便后续使用
|
||||
this.detectedSourceLang = validLang;
|
||||
}
|
||||
|
||||
// 获取翻译器显示名称
|
||||
private getTranslatorDisplayName(translatorType: string): string {
|
||||
switch (translatorType) {
|
||||
case 'google': return 'Google';
|
||||
case 'bing': return 'Bing';
|
||||
case 'youdao': return 'YouDao';
|
||||
case 'deepl': return 'DeepL';
|
||||
default: return translatorType;
|
||||
}
|
||||
}
|
||||
|
||||
// 执行翻译
|
||||
private async translate() {
|
||||
const targetLang = this.targetLangSelector.value;
|
||||
const translatorType = this.translatorSelector.value;
|
||||
|
||||
// 显示加载状态
|
||||
this.loadingIndicator.style.display = 'block';
|
||||
this.resultContainer.innerHTML = '';
|
||||
|
||||
try {
|
||||
// 执行翻译 - 源语言将在store中自动检测
|
||||
const result = await this.translationStore.translateText(
|
||||
this.sourceText,
|
||||
targetLang,
|
||||
translatorType
|
||||
);
|
||||
|
||||
// 如果检测到了语言,更新源语言选择器
|
||||
if (result.sourceLang) {
|
||||
this.detectedSourceLang = result.sourceLang;
|
||||
this.updateLanguageSelectorsForDetectedLanguage(result.sourceLang);
|
||||
}
|
||||
|
||||
// 显示翻译结果
|
||||
this.displayTranslationResult(result);
|
||||
|
||||
} catch (err) {
|
||||
console.error('Translation failed:', err);
|
||||
this.resultContainer.innerHTML = '';
|
||||
this.translatedText = '';
|
||||
} finally {
|
||||
// 隐藏加载状态
|
||||
this.loadingIndicator.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
// 显示翻译结果
|
||||
private displayTranslationResult(result: any) {
|
||||
// 更新结果显示
|
||||
this.resultContainer.innerHTML = '';
|
||||
|
||||
// 创建结果容器
|
||||
const resultWrapper = document.createElement('div');
|
||||
resultWrapper.className = 'cm-translation-result-wrapper';
|
||||
|
||||
// 只显示翻译结果区域
|
||||
const translatedTextElem = document.createElement('div');
|
||||
translatedTextElem.className = 'cm-translation-target';
|
||||
|
||||
if (result.error) {
|
||||
translatedTextElem.classList.add('cm-translation-error');
|
||||
translatedTextElem.textContent = result.error;
|
||||
this.translatedText = '';
|
||||
} else {
|
||||
this.translatedText = result.translatedText || '';
|
||||
translatedTextElem.textContent = this.translatedText || '';
|
||||
}
|
||||
|
||||
// 添加复制按钮
|
||||
if (this.translatedText) {
|
||||
const copyButton = document.createElement('button');
|
||||
copyButton.className = 'cm-translation-copy-btn';
|
||||
copyButton.innerHTML = `<svg viewBox="0 0 24 24" width="14" height="14"><path fill="currentColor" d="M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z"/></svg>`;
|
||||
|
||||
copyButton.addEventListener('click', () => {
|
||||
navigator.clipboard.writeText(this.translatedText).then(() => {
|
||||
// 显示复制成功提示
|
||||
const originalText = copyButton.innerHTML;
|
||||
copyButton.innerHTML = `<svg viewBox="0 0 24 24" width="14" height="14"><path fill="currentColor" d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"/></svg>`;
|
||||
copyButton.classList.add('copied');
|
||||
|
||||
setTimeout(() => {
|
||||
copyButton.innerHTML = originalText;
|
||||
copyButton.classList.remove('copied');
|
||||
}, 1500);
|
||||
});
|
||||
});
|
||||
|
||||
// 将复制按钮添加到结果包装器
|
||||
resultWrapper.appendChild(copyButton);
|
||||
}
|
||||
|
||||
// 添加翻译结果到包装器
|
||||
resultWrapper.appendChild(translatedTextElem);
|
||||
|
||||
// 添加到结果容器
|
||||
this.resultContainer.appendChild(resultWrapper);
|
||||
}
|
||||
|
||||
// 更新默认配置
|
||||
private updateDefaultConfig() {
|
||||
const targetLang = this.targetLangSelector.value;
|
||||
|
||||
// 增加目标语言的使用频率
|
||||
if (targetLang) {
|
||||
this.translationStore.incrementLanguageUsage(targetLang);
|
||||
}
|
||||
|
||||
this.translationStore.setDefaultConfig({
|
||||
targetLang: targetLang,
|
||||
translatorType: this.translatorSelector.value
|
||||
});
|
||||
}
|
||||
|
||||
// 当气泡弹窗被销毁时
|
||||
destroy() {
|
||||
// 保存当前配置作为默认值
|
||||
this.updateDefaultConfig();
|
||||
}
|
||||
}
|
||||
|
||||
// 创建翻译气泡
|
||||
export function createTranslationTooltip(view: EditorView, text: string): Tooltip {
|
||||
return {
|
||||
pos: view.state.selection.main.to, // 紧贴文本末尾
|
||||
above: false,
|
||||
strictSide: false,
|
||||
arrow: true,
|
||||
create: () => new TranslationTooltip(view, text)
|
||||
};
|
||||
}
|
@@ -11,6 +11,7 @@ import {hyperLink} from '../extensions/hyperlink'
|
||||
import {minimap} from '../extensions/minimap'
|
||||
import {vscodeSearch} from '../extensions/vscodeSearch'
|
||||
import {createCheckboxExtension} from '../extensions/checkbox'
|
||||
import {createTranslatorExtension} from '../extensions/translator'
|
||||
|
||||
import {foldingOnIndent} from '../extensions/fold/foldExtension'
|
||||
|
||||
@@ -79,8 +80,6 @@ export const minimapFactory: ExtensionFactory = {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 超链接扩展工厂
|
||||
*/
|
||||
@@ -126,8 +125,6 @@ export const searchFactory: ExtensionFactory = {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
export const foldFactory: ExtensionFactory = {
|
||||
create(config: any) {
|
||||
return foldingOnIndent;
|
||||
@@ -155,6 +152,29 @@ export const checkboxFactory: ExtensionFactory = {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 翻译扩展工厂
|
||||
*/
|
||||
export const translatorFactory: ExtensionFactory = {
|
||||
create(config: any) {
|
||||
return createTranslatorExtension({
|
||||
defaultTranslator: config.defaultTranslator || 'bing',
|
||||
minSelectionLength: config.minSelectionLength || 2,
|
||||
maxTranslationLength: config.maxTranslationLength || 5000,
|
||||
})
|
||||
},
|
||||
getDefaultConfig() {
|
||||
return {
|
||||
defaultTranslator: 'bing',
|
||||
minSelectionLength: 2,
|
||||
maxTranslationLength: 5000,
|
||||
}
|
||||
},
|
||||
validateConfig(config: any) {
|
||||
return typeof config === 'object'
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 所有扩展的统一配置
|
||||
* 排除$zero值以避免TypeScript类型错误
|
||||
@@ -177,6 +197,11 @@ const EXTENSION_CONFIGS = {
|
||||
displayNameKey: 'extensions.colorSelector.name',
|
||||
descriptionKey: 'extensions.colorSelector.description'
|
||||
},
|
||||
[ExtensionID.ExtensionTranslator]: {
|
||||
factory: translatorFactory,
|
||||
displayNameKey: 'extensions.translator.name',
|
||||
descriptionKey: 'extensions.translator.description'
|
||||
},
|
||||
|
||||
// UI增强扩展
|
||||
[ExtensionID.ExtensionMinimap]: {
|
||||
@@ -185,7 +210,6 @@ const EXTENSION_CONFIGS = {
|
||||
descriptionKey: 'extensions.minimap.description'
|
||||
},
|
||||
|
||||
|
||||
// 工具扩展
|
||||
[ExtensionID.ExtensionSearch]: {
|
||||
factory: searchFactory,
|
||||
|
Reference in New Issue
Block a user