Forráskód Böngészése

feat(i18n): 实现国际化功能并添加多语言支持

- 在preload bridge中暴露语言获取方法
- 为相机控制器添加国际化错误消息
- 为生成控制器添加国际化错误消息
- 为设置控制器添加国际化错误消息
- 为拍照控制器添加国际化错误消息
- 更新前端英语语言包添加新翻译项
- 更新前端中文语言包添加新翻译项
- 在i18n索引中添加Electron端语言同步功能
- 为货号选择对话框组件添加国际化支持
- 为硬件检测组件添加国际化前缀文本
- 为模特生成组件添加国际化支持
- 为场景提示对话框添加国际化支持
- 在IPC工具中添加语言设置功能
- 在应用主入口添加语言同步逻辑
- 为在线状态组件添加国际化支持
- 为用户检查组件添加国际化支持
- 创建Electron端国际化配置文件
- 创建Electron端国际化翻译文件(中英两种)
panqiuyao 1 napja
szülő
commit
5b08d4b883

+ 115 - 0
electron/config/i18n.js

@@ -0,0 +1,115 @@
+'use strict';
+
+const path = require('path');
+const fs = require('fs');
+
+// 获取当前语言设置
+const getCurrentLanguage = () => {
+  try {
+    // 1. 优先从用户数据目录的缓存文件读取(由前端设置)
+    const userDataPath = getElectronUserDataPath();
+    const langCachePath = path.join(userDataPath, 'language.json');
+
+    if (fs.existsSync(langCachePath)) {
+      const data = JSON.parse(fs.readFileSync(langCachePath, 'utf-8'));
+      if (data.language && ['zh', 'en', 'zh-CN'].includes(data.language)) {
+        return data.language;
+      }
+    }
+
+    // 2. 尝试从配置文件读取
+    const configPath = path.join(__dirname, 'config.default.js');
+    if (fs.existsSync(configPath)) {
+      const config = require(configPath);
+      return config.language || 'zh';
+    }
+  } catch (e) {
+    console.log('Failed to read language config:', e.message);
+  }
+  return 'zh';
+};
+
+// 获取 Electron 用户数据目录
+const getElectronUserDataPath = () => {
+  try {
+    const { app } = require('electron');
+    return app.getPath('userData');
+  } catch (e) {
+    // 在非 Electron 环境(如测试)下返回默认路径
+    return path.join(__dirname, '..', '..');
+  }
+};
+
+// 加载翻译文件
+const loadTranslations = () => {
+  const language = getCurrentLanguage();
+
+  // 标准化语言代码:en -> en, zh-CN/zh -> zh
+  let i18nFile = 'en';
+  if (language.startsWith('zh')) {
+    i18nFile = 'zh';
+  }
+
+  const i18nPath = path.join(__dirname, 'i18n', `${i18nFile}.json`);
+
+  try {
+    if (fs.existsSync(i18nPath)) {
+      return JSON.parse(fs.readFileSync(i18nPath, 'utf-8'));
+    }
+  } catch (e) {
+    console.log('Failed to load i18n file:', e.message);
+  }
+
+  // 默认加载中文
+  const defaultPath = path.join(__dirname, 'i18n', 'zh.json');
+  try {
+    return JSON.parse(fs.readFileSync(defaultPath, 'utf-8'));
+  } catch (e) {
+    console.error('Failed to load default i18n file:', e.message);
+    return {};
+  }
+};
+
+// 缓存翻译数据
+let translations = null;
+
+// 获取翻译
+const t = (key, params = {}) => {
+  if (!translations) {
+    translations = loadTranslations();
+  }
+
+  // 支持嵌套 key 如 "camera.cameraNotConnected"
+  const keys = key.split('.');
+  let value = translations;
+
+  for (const k of keys) {
+    if (value && typeof value === 'object' && k in value) {
+      value = value[k];
+    } else {
+      return key; // 未找到翻译,返回原始 key
+    }
+  }
+
+  // 如果找到翻译,用参数替换占位符
+  if (typeof value === 'string' && Object.keys(params).length > 0) {
+    return value.replace(/\{(\w+)\}/g, (match, paramKey) => {
+      return params[paramKey] !== undefined ? params[paramKey] : match;
+    });
+  }
+
+  return value;
+};
+
+// 重新加载翻译(当语言改变时调用)
+const reloadTranslations = () => {
+  translations = loadTranslations();
+  console.log('i18n translations reloaded');
+};
+
+module.exports = {
+  t,
+  getCurrentLanguage,
+  loadTranslations,
+  reloadTranslations,
+};

+ 24 - 0
electron/config/i18n/en.json

@@ -0,0 +1,24 @@
+{
+  "camera": {
+    "cameraNotConnected": "Camera not connected, please check camera connection.",
+    "cameraNotConnected2": "Camera not connected, please connect the camera.",
+    "installSmartShooter": "Please install SmartShooter5 software",
+    "installDigiCamControl": "Please install digiCamControl software and enable the web service, port is 5513",
+    "getCameraListSuccess": "Get camera list successfully",
+    "noCameraList": "No camera list available"
+  },
+  "utils": {
+    "missingExeName": "Missing executable file name",
+    "toolNotFound": "Tool not found: {name}",
+    "runExternalToolFailed": "Failed to run external tool",
+    "selectFolder": "Select Folder",
+    "selectImage": "Select Image",
+    "selectFile": "Select File",
+    "supportImageFormats": "Support JPG, PNG, GIF",
+    "noImageData": "No image data to save",
+    "saveImageFailed": "Failed to save generated image"
+  },
+  "common": {
+    "requestFailed": "Request failed, please contact administrator"
+  }
+}

+ 24 - 0
electron/config/i18n/zh.json

@@ -0,0 +1,24 @@
+{
+  "camera": {
+    "cameraNotConnected": "相机硬件未连接,请检查相机连接。",
+    "cameraNotConnected2": "相机未连接,请链接相机。",
+    "installSmartShooter": "请安装SmartShooter5软件",
+    "installDigiCamControl": "请安装digiCamControl软件,并打开digiCamControl软件的web服务,端口为5513",
+    "getCameraListSuccess": "获取成功",
+    "noCameraList": "暂无相机列表"
+  },
+  "utils": {
+    "missingExeName": "缺少可执行文件名称",
+    "toolNotFound": "未找到工具:{name}",
+    "runExternalToolFailed": "运行外部工具失败",
+    "selectFolder": "选择文件夹",
+    "selectImage": "选择图片",
+    "selectFile": "选择文件",
+    "supportImageFormats": "支持JPG,png,gif",
+    "noImageData": "无可保存的图片数据",
+    "saveImageFailed": "保存生成图片失败"
+  },
+  "common": {
+    "requestFailed": "请求失败,请联系管理员"
+  }
+}

+ 7 - 6
electron/controller/camera.js

@@ -7,6 +7,7 @@ const {
   liveShow: liveShowApi, liveHide: liveHideApi, setParams, capture, getParams,CMD,captureLive,closeOtherWindow } = require('../api/camera');
 
 const { readConfigFile } = require('../utils/config');
+const { t } = require('../config/i18n');
 
 let  isOPen = true
 let isSoftwareStarted = false; // 标记相机控制软件是否已经启动
@@ -65,7 +66,7 @@ class CameraController extends Controller {
         if(res === '未将对象引用设置到对象的实例。' || !res){
           return {
             status:-1,
-            msg: "相机硬件未连接,请检查相机连接。",
+            msg: t('camera.cameraNotConnected'),
           }
         }
 
@@ -100,7 +101,7 @@ class CameraController extends Controller {
         if (res?.device_status === -1) {
           return {
             status: -1,
-            msg: "相机硬件未连接,请检查相机连接。",
+            msg: t('camera.cameraNotConnected'),
           };
         }
 
@@ -118,15 +119,15 @@ class CameraController extends Controller {
 
         return {
           status:-1,
-          msg:"相机未连接,请链接相机。",
+          msg: t('camera.cameraNotConnected2'),
         }
       }
 
 
     } catch (error) {
 
-      let msg = '请安装SmartShooter5软件'
-      if(readConfigFile().controlType === 'digiCamControl') msg = '请安装digiCamControl软件,并打开digiCamControl软件的web服务,端口为5513'
+      let msg = t('camera.installSmartShooter')
+      if(readConfigFile().controlType === 'digiCamControl') msg = t('camera.installDigiCamControl')
       return {
         status:-1,
         msg,
@@ -202,7 +203,7 @@ class CameraController extends Controller {
     return {
       CameraLists: cachedCameraList,
       status: cachedCameraList.length > 0 ? 2 : 0,
-      msg: cachedCameraList.length > 0 ? '获取成功' : '暂无相机列表'
+      msg: cachedCameraList.length > 0 ? t('camera.getCameraListSuccess') : t('camera.noCameraList')
     };
   }
 

+ 3 - 2
electron/controller/generate.js

@@ -6,10 +6,11 @@ const Services = require('ee-core/services');
 const path = require('path');
 const fs = require('fs');
 const { generatePhotoDetail, getLogoList, addLogo, deleteLogo, getGoodsImageJson } = require('../api/generate');
+const { t } = require('../config/i18n');
 
 const errData = {
-  msg :'请求失败,请联系管理员',
-  code:999
+  msg : t('common.requestFailed'),
+  code: 999
 }
 
 /**

+ 4 - 2
electron/controller/setting.js

@@ -22,9 +22,11 @@ const {
   syncActions
 } = require('../api/setting');
 
+const { t } = require('../config/i18n');
+
 const errData = {
-  msg :'请求失败,请联系管理员',
-  code:999
+  msg : t('common.requestFailed'),
+  code: 999
 }
 
 /**

+ 4 - 2
electron/controller/takephoto.js

@@ -3,9 +3,11 @@ const fs = require('fs');
 const Log = require('ee-core/log');
 const { Controller } = require('ee-core');
 const  { getPhotoRecords,delectGoodsArts,createMainImage,getLastPhotoRecord,import_dirs,delete_all_goods_arts } =  require('../api/takephoto');
+const { t } = require('../config/i18n');
+
 const errData = {
-  msg :'请求失败,请联系管理员',
-  code:999
+  msg : t('common.requestFailed'),
+  code: 999
 }
 let id = 1;
 

+ 40 - 19
electron/controller/utils.js

@@ -13,10 +13,7 @@ const Log = require('ee-core/log');
 
 const { readConfigFile } = require('../utils/config');
 const configDeault = readConfigFile();
-const errData = {
-  msg :'请求失败,请联系管理员',
-  code:999
-}
+const { t } = require('../config/i18n');
 const sharp = require('sharp'); // 确保安装:npm install sharp
 
 
@@ -40,7 +37,7 @@ class UtilsController extends Controller {
       const targetName = exePath || exeName;
 
       if (!targetName) {
-        throw new Error('缺少可执行文件名称');
+        throw new Error(t('utils.missingExeName'));
       }
 
       const isPackaged = app.isPackaged;
@@ -82,7 +79,7 @@ class UtilsController extends Controller {
       const resolvedPath = candidates.find((filePath) => filePath && fs.existsSync(filePath));
 
       if (!resolvedPath) {
-        throw new Error(`未找到工具:${targetName}`);
+        throw new Error(t('utils.toolNotFound', { name: targetName }));
       }
 
       // 如果已经有正在运行的子进程,则先关闭旧进程,再重新启动新的(保证始终只有一个进程,同时刷新页面参数)
@@ -121,7 +118,7 @@ class UtilsController extends Controller {
       console.error('runExternalTool error:', error);
       return {
         code: 1,
-        msg: error.message || '运行外部工具失败'
+        msg: error.message || t('utils.runExternalToolFailed')
       };
     }
   }
@@ -196,10 +193,10 @@ class UtilsController extends Controller {
   }
 
   async openDirectory(optiops={
-    title:"选择文件夹"
+    title: t('utils.selectFolder')
     }){
     const filePaths = dialog.showOpenDialogSync({
-      title:optiops.title || '选择文件夹',
+      title:optiops.title || t('utils.selectFolder'),
       properties: ['openDirectory']
     })
     if(filePaths[0]) return filePaths[0];
@@ -237,18 +234,18 @@ class UtilsController extends Controller {
 
   async openImage(
       optiops= {
-        title:"选择图片",
+        title: t('utils.selectImage'),
         filters:[
-          { name: '支持JPG,png,gif', extensions: ['jpg','jpeg','png'] },
+          { name: t('utils.supportImageFormats'), extensions: ['jpg','jpeg','png'] },
         ],
       }
    ){
 
         const filePaths = dialog.showOpenDialogSync({
-          title:optiops.title || '选择图片',
+          title:optiops.title || t('utils.selectImage'),
           properties:['openFile'],
           filters:optiops.filters || [
-            { name: '支持JPG,png,gif', extensions: ['jpg','jpeg','png'] },
+            { name: t('utils.supportImageFormats'), extensions: ['jpg','jpeg','png'] },
           ]
         })
         const filePath = filePaths[0];
@@ -292,7 +289,7 @@ class UtilsController extends Controller {
       const { app } = require('electron');
       const bundles = payload.bundles || [];
       if (!Array.isArray(bundles) || !bundles.length) {
-        return { code: 1, msg: '无可保存的图片数据' };
+        return { code: 1, msg: t('utils.noImageData') };
       }
 
       // 运行目录:打包后为 EXE 所在目录,开发环境为项目根目录
@@ -355,22 +352,22 @@ class UtilsController extends Controller {
       console.error('saveGeneratedImages error:', error);
       return {
         code: 1,
-        msg: error.message || '保存生成图片失败',
+        msg: error.message || t('utils.saveImageFailed'),
       };
     }
   }
   async openFile(optiops= {
-    title:"选择文件",
+    title: t('utils.selectFile'),
     filters:[
-      { name: '支持JPG', extensions: ['jpg','jpeg'] },
+      { name: t('utils.supportImageFormats'), extensions: ['jpg','jpeg'] },
     ],
   }){
 
     const filePaths = dialog.showOpenDialogSync({
-      title:optiops.title || '选择文件',
+      title:optiops.title || t('utils.selectFile'),
       properties: ['openFile'],
       filters: optiops.filters || [
-        { name: '选择文件' },
+        { name: t('utils.selectFile') },
       ]
     })
     if(filePaths[0]) return filePaths[0];
@@ -510,6 +507,30 @@ class UtilsController extends Controller {
     return { success: true, message: 'IPC is working', args }
   }
 
+  // 设置语言(由前端调用,保持 Electron 和前端语言同步)
+  async setLanguage(args, event) {
+    try {
+      const { language } = args;
+      const fs = require('fs');
+
+      // 将语言设置写入缓存文件
+      const userDataPath = app.getPath('userData');
+      const langCachePath = path.join(userDataPath, 'language.json');
+
+      fs.writeFileSync(langCachePath, JSON.stringify({ language }), 'utf-8');
+
+      // 重新加载 i18n
+      const i18n = require('../config/i18n');
+      i18n.reloadTranslations();
+
+      console.log('Language set to:', language);
+      return { success: true, language };
+    } catch (error) {
+      console.error('Failed to set language:', error);
+      return { success: false, error: error.message };
+    }
+  }
+
   async readFileImageForPath(filePath,maxWidth=1500){
 
     const getMimeType = (fileName)=>{

+ 9 - 1
electron/preload/bridge.js

@@ -1,4 +1,4 @@
-/* 
+/*
  * 如果启用了上下文隔离,渲染进程无法使用electron的api,
  * 可通过contextBridge 导出api给渲染进程使用
  */
@@ -7,4 +7,12 @@ const { contextBridge, ipcRenderer } = require('electron')
 
 contextBridge.exposeInMainWorld('electron', {
   ipcRenderer: ipcRenderer,
+  // 暴露语言获取方法,供主进程使用
+  getLanguage: () => {
+    try {
+      return localStorage.getItem('camera_machine_language') || 'zh-CN'
+    } catch (e) {
+      return 'zh-CN'
+    }
+  }
 })

+ 11 - 10
frontend/src/components/ModelGeneration/index.vue

@@ -1,5 +1,5 @@
 <template>
-  <el-dialog v-model="dialogVisible" title="选择模特" width="1000px" :close-on-click-modal="false"
+  <el-dialog v-model="dialogVisible" :title="$t('modelGeneration.title')" width="1000px" :close-on-click-modal="false"
     :close-on-press-escape="false"  custom-class="model-generation-dialog" @close="handleClose">
     <div class="model-generation-container">
       <!-- 主要内容区域 -->
@@ -7,24 +7,24 @@
 
         <!-- 左侧:女模特选择 -->
         <div class="model-section">
-          <h2>女模特</h2>
+          <h2>{{ $t('modelGeneration.femaleModel') }}</h2>
           <div class="model-display">
             <el-image v-if="selectedFemaleModel" :src="selectedFemaleModel.image_url" :alt="selectedFemaleModel.name"
               class="selected-model-image" lazy :preview-src-list="[selectedFemaleModel.image_url]" hide-on-click-modal fit="cover" />
             <div v-else class="placeholder-image">
-              <span>请在下方列表选择</span>
+              <span>{{ $t('modelGeneration.pleaseSelect') }}</span>
             </div>
           </div>
         </div>
 
         <!-- 右侧:男模特选择 -->
         <div class="model-section">
-          <h2>男模特</h2>
+          <h2>{{ $t('modelGeneration.maleModel') }}</h2>
           <div class="model-display">
             <el-image v-if="selectedMaleModel" :src="selectedMaleModel.image_url" :alt="selectedMaleModel.name"
               class="selected-model-image" lazy :preview-src-list="[selectedMaleModel.image_url]"  hide-on-click-modal fit="cover" />
             <div v-else class="placeholder-image">
-              <span>请在下方列表选择</span>
+              <span>{{ $t('modelGeneration.pleaseSelect') }}</span>
             </div>
           </div>
         </div>
@@ -35,10 +35,10 @@
         <!-- 标签页切换 -->
         <div class="tabs-container">
           <div class="tab-item" :class="{ active: activeTab === 'female' }" @click="switchTab('female')">
-            女模特
+            {{ $t('modelGeneration.femaleTab') }}
           </div>
           <div class="tab-item" :class="{ active: activeTab === 'male' }" @click="switchTab('male')">
-            男模特
+            {{ $t('modelGeneration.maleTab') }}
           </div>
         </div>
 
@@ -54,9 +54,9 @@
 
     <template #footer>
       <div class="dialog-footer">
-        <el-button @click="handleCancel">取消</el-button>
+        <el-button @click="handleCancel">{{ $t('modelGeneration.cancel') }}</el-button>
         <el-button type="primary" @click="handleConfirm" :disabled="!canConfirm">
-          确认
+          {{ $t('modelGeneration.confirm') }}
         </el-button>
       </div>
     </template>
@@ -65,6 +65,7 @@
 
 <script setup lang="ts">
 import { ref, reactive, computed, watch, nextTick } from 'vue'
+import i18n from '@/locales'
 import { ElMessage } from 'element-plus'
 import { getShoesModelTemplateApi } from '@/apis/other'
 
@@ -258,7 +259,7 @@ const fetchModelList = async () => {
       }, 100)
     }
   } catch (error) {
-    ElMessage.error('获取模特列表失败')
+    ElMessage.error(i18n.global.t('modelGeneration.fetchFailed'))
   }
 }
 

+ 8 - 7
frontend/src/components/ScenePromptDialog/index.vue

@@ -1,16 +1,16 @@
 <template>
-  <el-dialog v-model="dialogVisible" title="场景图生成" width="600px" :close-on-click-modal="false"
+  <el-dialog v-model="dialogVisible" :title="$t('scenePrompt.title')" width="600px" :close-on-click-modal="false"
     :close-on-press-escape="false" custom-class="scene-prompt-dialog" @close="handleClose">
     <div class="scene-prompt-container">
       <!-- 场景提示词输入区域 -->
       <div class="input-section">
         <div class="input-wrapper">
-          <el-input v-model="scenePrompt" type="textarea" :rows="8" placeholder="请输入场景提示词" class="scene-input"
+          <el-input v-model="scenePrompt" type="textarea" :rows="8" :placeholder="$t('scenePrompt.placeholder')" class="scene-input"
             resize="none" maxlength="500"  />
           <!-- AI帮我写按钮 -->
           <div class="ai-help-button-container">
             <el-button type="primary"  @click="handleAIHelp" :loading="aiLoading" class="ai-help-button">
-              AI帮我写
+              {{ $t('scenePrompt.aiHelp') }}
             </el-button>
           </div>
         </div>
@@ -20,7 +20,7 @@
     <template #footer>
       <div class="dialog-footer">
         <el-button type="primary" @click="handleConfirm" :disabled="!canConfirm" class="confirm-button">
-          确认
+          {{ $t('scenePrompt.confirm') }}
         </el-button>
       </div>
     </template>
@@ -29,6 +29,7 @@
 
 <script setup lang="ts">
 import { ref, computed, watch } from 'vue'
+import i18n from '@/locales'
 import { ElMessage } from 'element-plus'
 import { expandCameraWordsApi } from '@/apis/other'
 
@@ -99,12 +100,12 @@ const handleAIHelp = async () => {
     const result = res?.data?.result || ''
     if (result) {
       scenePrompt.value = result
-      ElMessage.success('AI 已为您生成场景提示词')
+      ElMessage.success(i18n.global.t('scenePrompt.aiGenerated'))
     } else {
-      ElMessage.error('AI 未返回结果,请稍后重试')
+      ElMessage.error(i18n.global.t('scenePrompt.aiNoResult'))
     }
   } catch (error) {
-    ElMessage.error('AI 生成失败,请重试')
+    ElMessage.error(i18n.global.t('scenePrompt.aiFailed'))
   } finally {
     aiLoading.value = false
   }

+ 1 - 1
frontend/src/components/check/index.vue

@@ -28,7 +28,7 @@
               :stroke-width="8"
               v-if="checkLoading"
             ></el-progress>
-            <div class="check-error-result" v-else-if="!checkSuccess">失败的原因,{{ checkInfoStore.getErrorMsg }}</div>
+            <div class="check-error-result" v-else-if="!checkSuccess">{{ $t('hardwareCheck.checkFailedPrefix') }}{{ checkInfoStore.getErrorMsg }}</div>
           </div>
         </el-timeline-item>
 

+ 6 - 4
frontend/src/composables/online.ts

@@ -1,5 +1,7 @@
 
 import { ElMessageBox  } from 'element-plus';
+import i18n from '@/locales';
+
 export function listenerOnline() {
 
     if (!navigator.onLine) {
@@ -10,8 +12,8 @@ export function listenerOnline() {
         console.log('online')
 
         ElMessageBox({
-            title:"网络变化",
-            message:'网络已连接,5S后将自动刷新网页!',
+            title: i18n.global.t('online.networkChange'),
+            message: i18n.global.t('online.networkConnected'),
             showCancelButton:false,
             showConfirmButton:false,
             closeOnClickModal:false,
@@ -32,8 +34,8 @@ export function listenerOnline() {
     function ShowError(){
 
         ElMessageBox({
-            title:"网络掉线!",
-            message:'网络已断开,请检查网络连接!',
+            title: i18n.global.t('online.networkOffline'),
+            message: i18n.global.t('online.networkDisconnected'),
             showCancelButton:false,
             showConfirmButton:false,
             closeOnClickModal:false,

+ 3 - 2
frontend/src/composables/userCheck.ts

@@ -1,5 +1,6 @@
 
 import { ElMessageBox  } from 'element-plus';
+import i18n from '@/locales';
 
 import  configInfo  from '@/stores/modules/config';
 const configInfoStore = configInfo();
@@ -16,8 +17,8 @@ export function useCheckInfo() {
         function ShowError(){
             if(configInfoStore.appModel === 2)  return;
             ElMessageBox({
-                title:"连接出错!",
-                message:'设备连接出错,请在主窗口中重新连接设备后,在重新打开此窗口后进行操作',
+                title: i18n.global.t('userCheck.connectionError'),
+                message: i18n.global.t('userCheck.connectionErrorMsg'),
                 showCancelButton:false,
                 showConfirmButton:false,
                 closeOnClickModal:false,

+ 55 - 0
frontend/src/locales/en.ts

@@ -628,6 +628,30 @@ export default {
     saveSuccess: 'Saved successfully',
   },
 
+  // Model selection
+  modelGeneration: {
+    title: 'Select Model',
+    femaleModel: 'Female Model',
+    maleModel: 'Male Model',
+    pleaseSelect: 'Please select from the list below',
+    femaleTab: 'Female',
+    maleTab: 'Male',
+    cancel: 'Cancel',
+    confirm: 'Confirm',
+    fetchFailed: 'Failed to get model list',
+  },
+
+  // Scene prompt
+  scenePrompt: {
+    title: 'Scene Generation',
+    placeholder: 'Please enter scene prompt',
+    aiHelp: 'AI Help',
+    confirm: 'Confirm',
+    aiGenerated: 'AI has generated scene prompt for you',
+    aiNoResult: 'AI returned no result, please try again later',
+    aiFailed: 'AI generation failed, please try again',
+  },
+
   // 硬件检测
   hardwareCheck: {
     title: 'Hardware Check',
@@ -645,6 +669,7 @@ export default {
     refresh: 'Refresh',
     getStatusFailed: 'Failed to get device status',
     getStatusTimeout: 'Device status request timeout',
+    checkFailedPrefix: 'Failure reason: ',
     statusNotInit: 'Not Initialized',
     statusMoving: 'Moving',
     statusStopped: 'Stopped',
@@ -665,6 +690,20 @@ export default {
     progress: 'Checking up to',
   },
 
+  // User check connection error
+  userCheck: {
+    connectionError: 'Connection Error!',
+    connectionErrorMsg: 'Device connection error. Please reconnect the device in the main window and reopen this window.',
+  },
+
+  // Online status
+  online: {
+    networkChange: 'Network Change',
+    networkConnected: 'Network connected. Page will refresh in 5 seconds!',
+    networkOffline: 'Network Offline!',
+    networkDisconnected: 'Network disconnected. Please check your network connection!',
+  },
+
   // checkStore 设备检查
   checkStore: {
     notConnected: 'Not Connected',
@@ -674,4 +713,20 @@ export default {
     photoMachineConnectFailedDetail: 'Photo machine connection failed. Please reconnect the photo machine and try again. You may try restarting the photo machine, unplugging and replugging the USB cable, or performing a forced initialization. Please check if the camera and turntable are in standby or sleep mode. If confirmed not in sleep, check if all connection cables are properly connected. Then try again. If the problem persists, please call: 15957854217, 18805712182.',
   },
 
+  // 货号选择对话框
+  goodsSelectDialog: {
+    title: 'Please select a goods number as the custom product template',
+    tip: 'Note: After selecting the goods number, this template will automatically use the related configuration of this goods!',
+    selectedGoods: 'Selected goods {goods} contains {count} images ({order})',
+    imageCount: 'images',
+    cannotSelectTip: 'Cannot read shooting config for this product, cannot select',
+    generating: 'Generating custom product template',
+    confirm: 'Confirm',
+    cancel: 'Cancel',
+    templateGenerateFailed: 'Product template generation failed',
+    templateGenerateError: 'Product template generation error',
+    configIncomplete: 'This goods config is incomplete, cannot select',
+    pleaseSelectGoods: 'Please select a goods number',
+  },
+
 }

+ 11 - 2
frontend/src/locales/index.ts

@@ -2,6 +2,7 @@ import { createI18n } from 'vue-i18n'
 import zhCN from './zh-CN'
 import en from './en'
 import { getLanguage } from '@/utils/language'
+import client from '@/stores/modules/client'
 
 const i18n = createI18n({
   legacy: false,
@@ -18,12 +19,20 @@ const i18n = createI18n({
 
 export default i18n
 
-// 切换语言
-export function switchLanguage(lang: 'zh-CN' | 'en') {
+// 切换语言(同时同步到 Electron 端)
+export async function switchLanguage(lang: 'zh-CN' | 'en') {
   ;(i18n.global.locale as any).value = lang
   import('@/utils/language').then(({ setLanguage }) => {
     setLanguage(lang)
   })
+
+  // 同步语言到 Electron 端
+  try {
+    const clientStore = client()
+    clientStore.ipc.send('controller.utils.setLanguage', { language: lang })
+  } catch (e) {
+    console.log('Failed to sync language to electron:', e)
+  }
 }
 
 export type { SupportedLocale } from '@/utils/language'

+ 55 - 0
frontend/src/locales/zh-CN.ts

@@ -641,6 +641,30 @@ export default {
     saveSuccess: '保存成功',
   },
 
+  // 模特选择
+  modelGeneration: {
+    title: '选择模特',
+    femaleModel: '女模特',
+    maleModel: '男模特',
+    pleaseSelect: '请在下方列表选择',
+    femaleTab: '女模特',
+    maleTab: '男模特',
+    cancel: '取消',
+    confirm: '确认',
+    fetchFailed: '获取模特列表失败',
+  },
+
+  // 场景提示词
+  scenePrompt: {
+    title: '场景图生成',
+    placeholder: '请输入场景提示词',
+    aiHelp: 'AI帮我写',
+    confirm: '确认',
+    aiGenerated: 'AI 已为您生成场景提示词',
+    aiNoResult: 'AI 未返回结果,请稍后重试',
+    aiFailed: 'AI 生成失败,请重试',
+  },
+
   // 硬件检测
   hardwareCheck: {
     title: '检测硬件',
@@ -658,6 +682,7 @@ export default {
     refresh: '刷新',
     getStatusFailed: '获取设备状态失败',
     getStatusTimeout: '获取设备状态超时',
+    checkFailedPrefix: '失败的原因:',
     statusNotInit: '未初始化',
     statusMoving: '运动中',
     statusStopped: '已停止',
@@ -678,6 +703,20 @@ export default {
     progress: '共检查至',
   },
 
+  // 用户检查连接错误
+  userCheck: {
+    connectionError: '连接出错!',
+    connectionErrorMsg: '设备连接出错,请在主窗口中重新连接设备后,在重新打开此窗口后进行操作',
+  },
+
+  // 网络状态
+  online: {
+    networkChange: '网络变化',
+    networkConnected: '网络已连接,5S后将自动刷新网页!',
+    networkOffline: '网络掉线!',
+    networkDisconnected: '网络已断开,请检查网络连接!',
+  },
+
   // checkStore 设备检查
   checkStore: {
     notConnected: '未连接',
@@ -687,4 +726,20 @@ export default {
     photoMachineConnectFailedDetail: '拍照机连接失败,请重新检查,可以尝试重新启动拍照机,插拔USB口,强制初始化等,请检查相机和支撑转盘是否待机休眠。如果确认未休眠,请检查各种连接线是否,正常连上。然后再重试一次,一直无法解决问题,请致电:15957854217,18805712182。',
   },
 
+  // 货号选择对话框
+  goodsSelectDialog: {
+    title: '请选择一个货号作为自定义商品的模板',
+    tip: '请注意,选择货号后,该模板会自动使用此货号的相关配置!',
+    selectedGoods: '选中货号 {goods} 包含 {count} 张图片({order})',
+    imageCount: '张图片',
+    cannotSelectTip: '无法读取到该商品拍摄配置,无法选择',
+    generating: '正在生成自定义商品模版',
+    confirm: '确定',
+    cancel: '取消',
+    templateGenerateFailed: '商品模板生成失败',
+    templateGenerateError: '商品模板生成异常',
+    configIncomplete: '该货号配置不完整,无法选择',
+    pleaseSelectGoods: '请选择一个货号',
+  },
+
 }

+ 15 - 1
frontend/src/main.ts

@@ -13,6 +13,19 @@ import { updateHttpConfig } from './utils/http'
 import client from './stores/modules/client'
 import icpList from './utils/ipc'
 import i18n from './locales'
+import { getLanguage } from './utils/language'
+
+// 同步语言到 Electron(前端初始化时执行,确保 Electron 与前端语言一致)
+function syncLanguageToElectron() {
+    try {
+        const clientStore = client()
+        const currentLang = getLanguage() // 获取当前语言(读取 localStorage 或浏览器语言)
+        clientStore.ipc.send(icpList.utils.setLanguage, { language: currentLang })
+        console.log('[i18n] Language synced to Electron:', currentLang)
+    } catch (e) {
+        console.warn('[i18n] Failed to sync language to electron:', e)
+    }
+}
 
 // 初始化应用配置
 function initializeAppConfig() {
@@ -53,8 +66,9 @@ log(router)
 
 app.mount('#app')
 
-// 应用挂载后初始化应用配置
+// 应用挂载后初始化应用配置和语言同步
 initializeAppConfig()
+syncLanguageToElectron()
 
 // 刷新后根据会话标记显示登录弹窗
 try {

+ 2 - 1
frontend/src/utils/ipc.ts

@@ -28,7 +28,8 @@ const icpList = {
         downloadFont: 'controller.utils.downloadFont',
         loadFontFromCache: 'controller.utils.loadFontFromCache',
         getFontCachePath: 'controller.utils.getFontCachePath',
-        testIPC: 'controller.utils.testIPC'
+        testIPC: 'controller.utils.testIPC',
+        setLanguage: 'controller.utils.setLanguage'
     },
     setting:{
         getDeviceConfigDetail: 'controller.setting.getDeviceConfigDetail',

+ 14 - 11
frontend/src/views/Photography/components/GoodsSelectDialog.vue

@@ -1,7 +1,7 @@
 <template>
   <el-dialog
     :model-value="visible"
-    title="请选择一个货号作为自定义商品的模板"
+    :title="$t('goodsSelectDialog.title')"
     width="60%"
     :close-on-click-modal="false"
     :close-on-press-escape="false"
@@ -10,9 +10,9 @@
   >
 
     <template #title>
-      <span>请选择一个货号作为自定义商品的模板</span>
+      <span>{{ $t('goodsSelectDialog.title') }}</span>
       <div class="fs-12 c-666">
-        *请注意,选择货号后,该模板会自动使用此货号的相关配置!<span v-if="selectedGoodsArtNo">选中货号 {{ selectedGoodsArtNo }} 包含 {{ getSelectedGoodsImageCount }} 张图片({{ getSelectedGoodsImageName.join(',') }})</span>
+        *{{ $t('goodsSelectDialog.tip') }}<span v-if="selectedGoodsArtNo">{{ $t('goodsSelectDialog.selectedGoods', { goods: selectedGoodsArtNo, count: getSelectedGoodsImageCount, order: getSelectedGoodsImageName.join(',') }) }}</span>
       </div>
     </template>
     <div class="goods-select-content" v-if="visible" ref="containerRef">
@@ -42,9 +42,9 @@
                 </span>
                 <span class="image-count mar-left-10 flex left">
                   <img src="@/assets/images/processImage.vue/tup.png" />
-                  {{ item.items?.length || 0 }}张图片
+                  {{ item.items?.length || 0 }}{{ $t('goodsSelectDialog.imageCount') }}
                 </span>
-                <span v-if="!item.syncConfig" class="mar-left-10 c-FF4C00">无法读取到该商品拍摄配置,无法选择</span>
+                <span v-if="!item.syncConfig" class="mar-left-10 c-FF4C00">{{ $t('goodsSelectDialog.cannotSelectTip') }}</span>
               </div>
             </div>
           </div>
@@ -99,14 +99,14 @@
                 @current-change="onCurrentChange"
               />
             </div>
-            <el-button @click="handleCancel">取消</el-button>
+            <el-button @click="handleCancel">{{ $t('goodsSelectDialog.cancel') }}</el-button>
             <el-button
               type="primary"
               :loading="goodsGenerateLoading"
               :disabled="goodsGenerateLoading"
               @click="handleConfirm"
             >
-              {{ goodsGenerateLoading ? '正在生成自定义商品模版' : '确定' }}
+              {{ goodsGenerateLoading ? $t('goodsSelectDialog.generating') : $t('goodsSelectDialog.confirm') }}
             </el-button>
           </div>
         </div>
@@ -123,6 +123,9 @@ import icpList from '@/utils/ipc'
 import usePhotography from '../mixin/usePhotography'
 import { ElMessage } from 'element-plus'
 
+// 导入 i18n
+import i18n from '@/locales'
+
 interface Props {
   visible: boolean
 }
@@ -196,13 +199,13 @@ const generateGoodsTemplate = (goodsArtNo: string) => {
         if (result && result.code === 0) {
           resolve(result.data)
         } else {
-          const msg = result?.msg || '商品模板生成失败'
+          const msg = result?.msg || i18n.global.t('goodsSelectDialog.templateGenerateFailed')
           ElMessage.error(msg)
           reject(new Error(msg))
         }
       })
     } catch (error: any) {
-      ElMessage.error(error?.message || '商品模板生成异常')
+      ElMessage.error(error?.message || i18n.global.t('goodsSelectDialog.templateGenerateError'))
       reject(error)
     }
   })
@@ -277,7 +280,7 @@ const handleGoodsSelection = (goodsArtNo: string) => {
   const item = goodsList.value.find((g: any) => g.goods_art_no === goodsArtNo)
   if (!item) return
   if (!item.syncConfig) {
-    ElMessage.warning('该货号配置不完整,无法选择')
+    ElMessage.warning(i18n.global.t('goodsSelectDialog.configIncomplete'))
     return
   }
   selectedGoodsArtNo.value = goodsArtNo
@@ -285,7 +288,7 @@ const handleGoodsSelection = (goodsArtNo: string) => {
 
 const handleConfirm = async () => {
   if (!selectedGoodsArtNo.value) {
-    ElMessage.warning('请选择一个货号')
+    ElMessage.warning(i18n.global.t('goodsSelectDialog.pleaseSelectGoods'))
     return
   }