为网站增加全站即时翻译功能

前2天在群里,石塘义工队兄弟搞了个全站翻译的功能,核心是引用了translate.js来实现即时翻译,官网是这里

我感觉这个功能很有用,就查看了他的源代码,以及参考官网的说明,给我的网站也做了js配置,但是始终页面显示不了语言选择框。后来,在群里我看到其它博友也反馈部署以后显示不了,但是我创建一个DEMO文件,只把官方的DEMO代码放进去的时候可以显示,说明官网的方法和部分主题CSS不兼容,可能是主题的CSS优先级较高,覆盖了这个功能。

把问题交给元宝AI,官方的文件肯定是修改不了的,只有修改调用方式了,同时为了好看便利,我仿照了一下360安全卫士等桌面软件悬浮球及随意拖动吸附模式,经过几个小时的调试,居然成功了。原则上支持wordpress\typecho所有PHP程序,支持苹果安卓浏览器和电脑手机平板样式。

一共2个文件,floating-translator.js和translate.js,其中floating-translator.js是引用文件,它代码中引用translate.js,我们只需要把两个上传,引用floating-translator.js就行。floating-translator.js里面的代码,把translate.js路径改为实际地址。

floating-translator.js全部代码如下:

(function() {
  const CONFIG = {
    ballSize: 50,
    snappedSize: 25,
    edgeThreshold: 20,
    panelOffset: 60,
    translateDelay: 500,  // 延长初始化延迟确保DOM加载
    touchHoldDelay: 150,  // 优化触摸延迟
    colors: {
      primary: '#4a6cf7',
      error: '#ff4444'
    }
  };

  class FloatingTranslator {
    constructor() {
      this.floatBall = null;
      this.ball = null;
      this.panel = null;
      this.isDragging = false;
      this.touchTimer = null;
      this.initElements();
    }

    initElements() {
      this.floatBall = document.createElement('div');
      this.floatBall.id = 'translate-float-ball';
      this.floatBall.className = 'translate-ball';
      this.floatBall.innerHTML = `
        <div class="ball">⏳</div>
        <div class="language-panel" style="display:none"></div>
      `;
      
      this.ball = this.floatBall.querySelector('.ball');
      this.panel = this.floatBall.querySelector('.language-panel');
      document.body.appendChild(this.floatBall);
    }

    initTranslation() {
      try {
        // 隐藏默认工具栏但保留DOM结构
        const container = document.querySelector('.translate-container');
        if (container) {
          container.style.cssText = `
            display: none !important;
            position: absolute !important;
            width: 0 !important;
            height: 0 !important;
            overflow: hidden !important;
          `;
        }

        translate.setAutoDiscriminateLocalLanguage('chinese_simplified');
        translate.service.use('client.edge');
        translate.listener.start();
        translate.execute();
        this.ball.textContent = '🌐';

        // 强化语言选择器移植
        setTimeout(() => {
          const translateDiv = document.getElementById('translate');
          if (translateDiv) {
            translateDiv.style.cssText = `
              display: block !important;
              position: static !important;
              width: 100% !important;
              opacity: 1 !important;
              border: none !important;
              background: transparent !important;
            `;
            this.panel.appendChild(translateDiv);
            this.panel.style.display = 'none'; // 保持初始隐藏
          }
        }, CONFIG.translateDelay);

      } catch (e) {
        this.showError('翻译初始化失败');
      }
    }

    setupInteractions() {
      // 桌面端事件
      this.ball.addEventListener('mousedown', this.handleStart.bind(this));
      document.addEventListener('mousemove', this.handleMove.bind(this));
      document.addEventListener('mouseup', this.handleEnd.bind(this));

      // 移动端事件 (重点修复触摸问题)
      this.ball.addEventListener('touchstart', (e) => {
        this.touchTimer = setTimeout(() => {
          this.handleStart(e);
        }, CONFIG.touchHoldDelay);
      }, { passive: false });

      this.ball.addEventListener('touchend', (e) => {
        clearTimeout(this.touchTimer);
        if (!this.isDragging) {
          this.togglePanel();
          e.preventDefault();
        }
        this.handleEnd();
      });

      document.addEventListener('touchmove', (e) => {
        if (this.isDragging) {
          this.handleMove(e);
          e.preventDefault();
        }
      }, { passive: false });

      // 点击事件统一处理
      this.ball.addEventListener('click', (e) => {
        if (!this.isDragging) {
          this.togglePanel();
          e.stopPropagation();
        }
      });

      // 点击外部关闭
      document.addEventListener('click', (e) => {
        if (!this.panel.contains(e.target) && !this.ball.contains(e.target)) {
          this.hidePanel();
        }
      });

      window.addEventListener('resize', () => this.checkEdgeSnap());
    }

    togglePanel() {
      this.panel.style.display = this.panel.style.display === 'none' ? 'block' : 'none';
    }

    hidePanel() {
      this.panel.style.display = 'none';
    }

    handleStart(e) {
      this.isDragging = true;
      this.hidePanel();
      this.floatBall.style.transition = 'none';
      
      const clientX = e.type.includes('touch') ? e.touches[0].clientX : e.clientX;
      const clientY = e.type.includes('touch') ? e.touches[0].clientY : e.clientY;
      
      this.startPos = {
        x: clientX - this.floatBall.offsetLeft,
        y: clientY - this.floatBall.offsetTop
      };
      
      this.floatBall.classList.remove('edge-left', 'edge-right');
      if (e.cancelable) e.preventDefault();
    }

    handleMove(e) {
      if (!this.isDragging) return;
      
      const clientX = e.type.includes('touch') ? e.touches[0].clientX : e.clientX;
      const clientY = e.type.includes('touch') ? e.touches[0].clientY : e.clientY;
      
      requestAnimationFrame(() => {
        this.floatBall.style.left = (clientX - this.startPos.x) + 'px';
        this.floatBall.style.top = (clientY - this.startPos.y) + 'px';
        this.floatBall.style.right = 'auto';
      });
      
      if (e.cancelable) e.preventDefault();
    }

    handleEnd() {
      if (this.isDragging) {
        this.isDragging = false;
        this.floatBall.style.transition = 'all 0.3s';
        this.checkEdgeSnap();
      }
    }

    checkEdgeSnap() {
      const rect = this.floatBall.getBoundingClientRect();
      const viewportWidth = window.innerWidth;
      
      this.floatBall.classList.remove('edge-left', 'edge-right');
      
      if (rect.left <= CONFIG.edgeThreshold) {
        this.floatBall.classList.add('edge-left');
        this.floatBall.style.left = '0';
      } 
      else if (rect.right >= viewportWidth - CONFIG.edgeThreshold) {
        this.floatBall.classList.add('edge-right');
        this.floatBall.style.right = '0';
        this.floatBall.style.left = 'auto';
      }
    }

    showError(message) {
      this.ball.textContent = '❌';
      const errorBall = document.createElement('div');
      errorBall.className = 'error-balloon';
      errorBall.textContent = message;
      document.body.appendChild(errorBall);
      setTimeout(() => errorBall.remove(), 3000);
    }
  }

  function init() {
    const script = document.createElement('script');
    script.src = '修改这里填入translate.js的实际网址';
    const translator = new FloatingTranslator();
    
    script.onload = () => {
      translator.initTranslation();
      translator.setupInteractions();
    };
    
    script.onerror = () => translator.showError('翻译加载失败');
    document.head.appendChild(script);
    injectStyles();
  }

  function injectStyles() {
    const style = document.createElement('style');
    style.textContent = `
      :root {
        --ball-size: ${CONFIG.ballSize}px;
        --snapped-size: ${CONFIG.snappedSize}px;
        --edge-threshold: ${CONFIG.edgeThreshold}px;
        --panel-offset: ${CONFIG.panelOffset}px;
        --primary-color: ${CONFIG.colors.primary};
        --error-color: ${CONFIG.colors.error};
      }
      
      .translate-ball {
        position: fixed;
        z-index: 99999;
        right: 20px;
        top: 50%;
        transform: translateY(-50%);
        width: var(--ball-size);
        height: var(--ball-size);
        cursor: move;
        touch-action: none;
        -webkit-tap-highlight-color: transparent;
      }
      
      .translate-ball .ball {
        width: 100%;
        height: 100%;
        background: var(--primary-color);
        border-radius: 50%;
        display: flex;
        align-items: center;
        justify-content: center;
        color: white;
        font-size: 24px;
        transition: all 0.3s;
        box-shadow: 0 2px 10px rgba(0,0,0,0.2);
        user-select: none;
      }
      
      /* 边缘吸附效果 */
      .translate-ball.edge-left {
        left: 0 !important;
        right: auto !important;
      }
      .translate-ball.edge-left .ball {
        border-radius: 0 50% 50% 0;
        width: var(--snapped-size);
      }
      .translate-ball.edge-right {
        right: 0 !important;
        left: auto !important;
      }
      .translate-ball.edge-right .ball {
        border-radius: 50% 0 0 50%;
        width: var(--snapped-size);
      }
      
      /* 语言面板修复 */
      .translate-ball .language-panel {
        position: absolute;
        top: 50%;
        transform: translateY(-50%);
        background: white;
        border-radius: 8px;
        box-shadow: 0 4px 12px rgba(0,0,0,0.15);
        padding: 12px;
        z-index: 99998;
        min-width: 160px;
      }
      .translate-ball:not(.edge-left) .language-panel {
        right: var(--panel-offset);
      }
      .translate-ball.edge-left .language-panel {
        left: var(--panel-offset);
      }
      
      /* 语言选择器强化样式 */
      #translate {
        display: block !important;
        position: static !important;
        width: 100% !important;
        opacity: 1 !important;
        border: none !important;
        background: transparent !important;
      }
      .translateSelectLanguage {
        display: block !important;
        width: 100% !important;
        min-width: 140px !important;
        padding: 10px !important;
        font-size: 15px !important;
        border: 1px solid #eee !important;
        border-radius: 4px !important;
      }
      
      /* 移动端专属优化 */
      @media (hover: none) {
        .translate-ball .ball {
          font-size: 28px;
        }
        .translateSelectLanguage {
          font-size: 16px !important;
          padding: 12px !important;
        }
      }

      .error-balloon {
        position: fixed;
        right: 20px;
        top: 50%;
        transform: translateY(-50%);
        background: var(--error-color);
        color: white;
        padding: 12px 18px;
        border-radius: 25px;
        z-index: 99999;
        box-shadow: 0 2px 10px rgba(0,0,0,0.2);
        animation: fadeIn 0.3s;
      }
      
      @keyframes fadeIn {
        from { opacity: 0; transform: translateY(-50%) scale(0.9); }
        to { opacity: 1; transform: translateY(-50%) scale(1); }
      }
    `;
    document.head.appendChild(style);
  }

  // 确保DOM完全加载后初始化
  if (document.readyState === 'complete') {
    init();
  } else {
    document.addEventListener('DOMContentLoaded', init);
  }
})();

下面是translate.js的压缩包,解压上传取得具体网址后,修改上面代码中的js网址。

这样,在网站右侧就会出现一个地球的图标,可以随意拖动,左右吸附,以及即时翻译全站。

为网站增加全站即时翻译功能-似水流年
Comments | 8 条评论
  • 彬红茶

    Google Chrome 116 Google Chrome 116 Windows 10 Windows 10 cn中国–广东–广州–天河区 电信 ip address 116.23.*.*

    这个有个缺点,JavaScript实时更新时没法翻译,内容多的话有一点点延迟,不过也是目前挺好的方案

    • 似水流年

      IBrowse r IBrowse r Android 12 Android 12 cn中国 中国联通 ip address 2408:8220:5f11:3dd0:*:*

      它毕竟是机器翻译,需要翻译的时间。

      • 彬红茶

        Google Chrome 116 Google Chrome 116 Windows 10 Windows 10 cn中国–广东–广州–天河区 电信 ip address 116.23.*.*

        这个还是开源的,可以自建API,拿个100核100g的服务器自建翻译系统,直接秒出

        但是JavaScript实时更新内容还是没法翻译的😀

        • 似水流年

          Microsoft Edge 139 Microsoft Edge 139 Windows 10 Windows 10 cn中国 中国联通 ip address 2408:8220:5f11:3dd0:*:*

          这太夸张了。😂

  • obaby

    Google Chrome 134 Google Chrome 134 Mac OS X 10.15 Mac OS X 10.15 cn中国 中国联通 ip address 2408:8418:d00:5b08:*:*

    这功能高级,问题是,我直接屏蔽老外,哈哈哈

    • 似水流年

      IBrowse r IBrowse r Android 12 Android 12 cn中国 中国移动 ip address 2409:894a:5f1b:973:*:*

      这不行啊,得走国际化。🤭

  • 李的日志

    Unknown Unknown Unknown Unknown cn中国–广东 广电网 ip address 240a:42cd:a201:8af4:*:*

    大家都还没有来,那首评就是我的了哈哈哈。支持一下好功能

    • 似水流年

      IBrowse r IBrowse r Android 12 Android 12 cn中国 中国联通 ip address 2408:8220:5f11:3dd0:*:*

      首评必须顶!🤭

消息盒子
# 您需要首次评论以获取消息 #
# 您需要首次评论以获取消息 #

只显示最新10条未读和已读信息