From 66a9fc7a0065c4b1f0d488018659da98ee8594e7 Mon Sep 17 00:00:00 2001
From: Junjie <fallin.jie@qq.com>
Date: 星期一, 09 三月 2026 13:57:11 +0800
Subject: [PATCH] #国际化i18n

---
 src/main/java/com/zy/common/i18n/I18nProperties.java           |   52 
 src/main/java/com/zy/common/i18n/I18nMessageService.java       |  406 +++++++
 src/main/java/com/zy/common/config/AdminInterceptor.java       |    2 
 src/main/java/com/zy/common/config/WebConfig.java              |    6 
 src/main/java/com/zy/system/controller/ResourceController.java |   16 
 docs/i18n-language-pack.md                                     |  134 ++
 src/main/webapp/views/ai/llm_config.html                       |   43 
 src/main/resources/i18n/zh-CN/messages.properties              |  176 +++
 src/main/java/com/zy/common/web/AuthController.java            |   28 
 src/main/java/com/zy/common/i18n/I18nResponseBodyAdvice.java   |   37 
 src/main/resources/i18n/en-US/messages.properties              |  176 +++
 src/main/resources/i18n/zh-CN/legacy.properties                |   88 +
 src/main/webapp/views/index.html                               |  348 ++++-
 src/main/webapp/views/role/role_detail.html                    |    2 
 src/main/java/com/zy/common/i18n/I18nLocaleUtils.java          |  114 +
 src/main/webapp/static/js/common.js                            |  932 ++++++++++++++++
 src/main/webapp/views/deviceLogs/deviceLogs.html               |    4 
 src/main/webapp/views/role/role_power_detail.html              |    2 
 src/main/java/com/zy/common/config/CoolExceptionHandler.java   |    9 
 src/main/resources/i18n/en-US/legacy.properties                |  707 ++++++++++++
 src/main/webapp/static/js/deviceLogs/deviceLogs.js             |   24 
 src/main/java/com/zy/common/i18n/I18nController.java           |   40 
 src/main/java/com/zy/common/i18n/RequestLocaleInterceptor.java |   53 
 src/main/webapp/views/role/role.html                           |    2 
 src/main/webapp/views/login.html                               |   63 
 src/main/resources/application.yml                             |   12 
 26 files changed, 3,378 insertions(+), 98 deletions(-)

diff --git a/docs/i18n-language-pack.md b/docs/i18n-language-pack.md
new file mode 100644
index 0000000..8b9985a
--- /dev/null
+++ b/docs/i18n-language-pack.md
@@ -0,0 +1,134 @@
+# WCS 鍥介檯鍖栬瑷�鍖呰鏄�
+
+## 璇诲彇浣嶇疆
+
+褰撳墠瀹炵幇鏈変袱灞傝瑷�鍖呮潵婧愶細
+
+1. 鍐呯疆璇█鍖�
+
+```text
+classpath:/i18n/<locale>/
+```
+
+涔熷氨鏄粨搴撻噷鐨勶細
+
+```text
+src/main/resources/i18n/<locale>/
+```
+
+杩欎竴灞傛槸椤圭洰鑷甫鐨勯粯璁よ瑷�鍖咃紝淇濊瘉绯荤粺寮�绠卞彲鐢ㄣ��
+
+2. 澶栫疆璇█鍖�
+
+```text
+./stock/out/wcs/i18n/<locale>/
+```
+
+杩欎竴灞傛槸杩愯鏃跺彲瀹夎銆佸彲鏇挎崲銆佸彲瑕嗙洊鐨勮瑷�鍖呯洰褰曪紝瀵瑰簲 `application.yml` 閲岀殑 `app.i18n.pack-path`銆�
+
+瀹為檯鍔犺浇椤哄簭锛�
+
+1. 鍏堣鍐呯疆璇█鍖�
+2. 鍐嶈澶栫疆璇█鍖�
+3. 澶栫疆璇█鍖呭悓鍚� key 浼氳鐩栧唴缃��
+
+褰撳墠宸查厤缃瑷�锛�
+
+- `zh-CN`
+- `en-US`
+
+澶栫疆鐩綍绀轰緥锛�
+
+```text
+stock/out/wcs/i18n/
+  en-US/
+    messages.properties
+    legacy.properties
+  zh-CN/
+    messages.properties
+    legacy.properties
+```
+
+## 鏂囦欢鐢ㄩ��
+
+### `messages.properties`
+
+鐢ㄤ簬绋冲畾鐨� key 褰㈠紡鍥介檯鍖栥��
+
+閫傚悎浠ヤ笅鍐呭锛�
+
+- 鑿滃崟鍚嶇О
+- 椤甸潰鏍囬
+- 瀵硅瘽妗嗘爣棰�
+- 鎸夐挳鏂囨
+- 鐘舵�佸悕绉�
+- 鏂板鍔熻兘鏂囨
+
+### `legacy.properties`
+
+鐢ㄤ簬鏃ч〉闈㈢殑绾枃鏈吋瀹规浛鎹€��
+
+杩欎釜椤圭洰閲岃�佺殑 Layui/jQuery 椤甸潰寰堝锛屽瓨鍦ㄥぇ閲忕洿鎺ュ啓姝荤殑涓枃锛涜繖灞傛槸杩囨浮鏂规锛屼究浜庡厛璁╄嫳鏂囧彲鐢紝鍐嶉�愭鎶婃棫椤甸潰鏀规垚 key 鍖栥��
+
+## key 瑙勫垯
+
+### 鑿滃崟 / 璧勬簮 key
+
+鍚庣鑿滃崟缈昏瘧 key 鐢� `sys_resource.code` 鎺ㄥ銆�
+
+渚嬪锛�
+
+```properties
+resource.develop=Development
+resource.ai.llm_config=AI Configuration
+resource.notifyReport.notifyReport=Notification Report
+resource.ai.llm_config.view=View
+```
+
+### 鏉冮檺 key
+
+鏉冮檺缈昏瘧 key 鐢� `action` 鎺ㄥ銆�
+
+渚嬪锛�
+
+```properties
+permission.function=Specified Functions
+permission.user.resetPassword=Reset Password
+```
+
+### 閫氱敤 UI key
+
+渚嬪锛�
+
+```properties
+common.profile=Profile
+common.logout=Log Out
+index.homeTab=Control Center
+login.title=WCS System V3.0
+```
+
+## 瀹夎璇█鍖�
+
+濡傛灉鍙槸浣跨敤椤圭洰鑷甫涓枃鍜岃嫳鏂囷紝涓嶉渶瑕侀澶栨搷浣溿��
+
+濡傛灉瑕佸畨瑁呭缃瑷�鍖咃紝鎸変笅闈㈠仛锛�
+
+1. 鍦� `stock/out/wcs/i18n/` 涓嬪垱寤烘柊璇█鐩綍銆�
+2. 浠庣幇鏈夎瑷�鍖呭鍒� `messages.properties` 鍜� `legacy.properties` 浣滀负妯℃澘銆�
+3. 鍙炕璇� value锛屼笉瑕佹敼 key銆�
+4. 鍒锋柊椤甸潰锛屾垨閲嶅惎鏈嶅姟銆�
+
+绯荤粺浼氭寜閰嶇疆鍛ㄦ湡妫�鏌ュ缃洰褰曪紝鍥犳绠�鍗曟枃鏈皟鏁寸悊璁轰笂涓嶅繀閲嶅惎锛涗絾姝e紡鐜寤鸿浠嶆寜鍙戝竷娴佺▼閲嶅惎鎴栭噸杞姐��
+
+## 鎺ㄨ崘缁存姢鏂瑰紡
+
+1. 鏂板椤甸潰鍜屾柊澧炴帴鍙f枃妗堬紝浼樺厛鍐欏叆 `messages.properties`銆�
+2. 鑰侀〉闈㈠厛閫氳繃 `legacy.properties` 鍏煎锛屼笉瑕佷竴寮�濮嬪氨鍏ㄩ噺閲嶆瀯銆�
+3. 鏌愪釜鑰侀〉闈㈤噸鏋勬椂锛屽啀鎶婂畠鐨勭函鏂囨湰閫愭杩佺Щ鍒版樉寮� key銆�
+
+## 璇存槑
+
+- 榛樿璇█鏄� `zh-CN`
+- 鍓嶇璇锋眰浼氳嚜鍔ㄦ惡甯� `X-Lang`
+- 鐧诲綍椤靛拰棣栭〉閮藉凡缁忔敮鎸佽瑷�鍒囨崲
+- 闈為粯璁よ瑷�濡傛灉缂哄皯鑿滃崟 key锛屽悗绔細鍏堝皾璇曟牴鎹� `resource.code` 鑷姩鐢熸垚鍙鍚嶇О锛屽啀鍥為��鍒板師濮嬩腑鏂囧悕
diff --git a/src/main/java/com/zy/common/config/AdminInterceptor.java b/src/main/java/com/zy/common/config/AdminInterceptor.java
index f758e4d..184f627 100644
--- a/src/main/java/com/zy/common/config/AdminInterceptor.java
+++ b/src/main/java/com/zy/common/config/AdminInterceptor.java
@@ -149,7 +149,7 @@
         response.setHeader("Access-Control-Allow-Origin", "*");
         response.setHeader("Access-Control-Allow-Credentials", "true");
         response.setHeader("Access-Control-Allow-Methods", "*");
-        response.setHeader("Access-Control-Allow-Headers", "Content-Type,Access-Token");
+        response.setHeader("Access-Control-Allow-Headers", "Content-Type,Access-Token,token,X-Lang,Accept-Language");
         response.setHeader("Access-Control-Expose-Headers", "*");
 
     }
diff --git a/src/main/java/com/zy/common/config/CoolExceptionHandler.java b/src/main/java/com/zy/common/config/CoolExceptionHandler.java
index aacc45a..79cfc08 100644
--- a/src/main/java/com/zy/common/config/CoolExceptionHandler.java
+++ b/src/main/java/com/zy/common/config/CoolExceptionHandler.java
@@ -2,6 +2,8 @@
 
 import com.core.common.R;
 import com.core.exception.CoolException;
+import com.zy.common.i18n.I18nMessageService;
+import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.web.HttpRequestMethodNotSupportedException;
 import org.springframework.web.bind.annotation.ExceptionHandler;
 import org.springframework.web.bind.annotation.RestControllerAdvice;
@@ -13,15 +15,18 @@
 @RestControllerAdvice
 public class CoolExceptionHandler {
 
+    @Autowired
+    private I18nMessageService i18nMessageService;
+
     @ExceptionHandler(Exception.class)
     public R handlerException(HandlerMethod handler, Exception e) {
         e.printStackTrace();
-        return R.error();
+        return R.error(i18nMessageService.getMessage("response.common.systemError"));
     }
 
     @ExceptionHandler(HttpRequestMethodNotSupportedException.class)
     public R handleHttpRequestMethodNotSupportedException(HttpRequestMethodNotSupportedException e) {
-        return R.error();
+        return R.error(i18nMessageService.getMessage("response.common.methodNotAllowed"));
     }
 
     @ExceptionHandler(CoolException.class)
diff --git a/src/main/java/com/zy/common/config/WebConfig.java b/src/main/java/com/zy/common/config/WebConfig.java
index 11bb6b2..460d733 100644
--- a/src/main/java/com/zy/common/config/WebConfig.java
+++ b/src/main/java/com/zy/common/config/WebConfig.java
@@ -1,5 +1,6 @@
 package com.zy.common.config;
 
+import com.zy.common.i18n.RequestLocaleInterceptor;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.context.annotation.Configuration;
 import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
@@ -12,10 +13,15 @@
 public class WebConfig implements WebMvcConfigurer {
 
     @Autowired
+    private RequestLocaleInterceptor requestLocaleInterceptor;
+
+    @Autowired
     private AdminInterceptor adminInterceptor;
 
     @Override
     public void addInterceptors(InterceptorRegistry registry) {
+        registry.addInterceptor(requestLocaleInterceptor)
+                .addPathPatterns("/**");
         registry.addInterceptor(adminInterceptor)
                 .addPathPatterns("/**")
                 ;
diff --git a/src/main/java/com/zy/common/i18n/I18nController.java b/src/main/java/com/zy/common/i18n/I18nController.java
new file mode 100644
index 0000000..2bdfd96
--- /dev/null
+++ b/src/main/java/com/zy/common/i18n/I18nController.java
@@ -0,0 +1,40 @@
+package com.zy.common.i18n;
+
+import com.core.common.R;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.*;
+
+@RestController
+@RequestMapping("/i18n")
+public class I18nController {
+
+    @Autowired
+    private I18nMessageService i18nMessageService;
+
+    @RequestMapping("/messages")
+    public R messages(@RequestParam(required = false) String lang) {
+        Locale locale = i18nMessageService.resolveLocale(lang);
+        Map<String, Object> result = new LinkedHashMap<>();
+        result.put("locale", I18nLocaleUtils.toTag(locale));
+        result.put("defaultLocale", i18nMessageService.getDefaultLocaleTag());
+        result.put("supportedLocales", localeOptions(locale));
+        result.put("messages", i18nMessageService.getMessages(locale));
+        result.put("legacy", i18nMessageService.getLegacyMessages(locale));
+        return R.ok(result);
+    }
+
+    private List<Map<String, String>> localeOptions(Locale locale) {
+        List<Map<String, String>> options = new ArrayList<>();
+        for (String supportedLocale : i18nMessageService.getSupportedLocaleTags()) {
+            LinkedHashMap<String, String> option = new LinkedHashMap<>();
+            option.put("tag", supportedLocale);
+            option.put("label", i18nMessageService.getMessage("lang." + supportedLocale, locale));
+            options.add(option);
+        }
+        return options;
+    }
+}
diff --git a/src/main/java/com/zy/common/i18n/I18nLocaleUtils.java b/src/main/java/com/zy/common/i18n/I18nLocaleUtils.java
new file mode 100644
index 0000000..23029f2
--- /dev/null
+++ b/src/main/java/com/zy/common/i18n/I18nLocaleUtils.java
@@ -0,0 +1,114 @@
+package com.zy.common.i18n;
+
+import com.core.common.Cools;
+
+import java.util.*;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class I18nLocaleUtils {
+
+    private static final Set<String> UPPERCASE_WORDS = new HashSet<>(
+            Arrays.asList("ai", "api", "wcs", "wms", "llm", "rgv", "crn", "plc", "http", "mcp", "erp", "io")
+    );
+
+    private static final Pattern WORD_BOUNDARY = Pattern.compile("([a-z0-9])([A-Z])");
+
+    private I18nLocaleUtils() {
+    }
+
+    public static Locale defaultLocale(I18nProperties properties) {
+        return toLocale(properties.getDefaultLocale());
+    }
+
+    public static Locale toLocale(String value) {
+        if (Cools.isEmpty(value)) {
+            return Locale.SIMPLIFIED_CHINESE;
+        }
+        String normalized = value.replace('_', '-').trim();
+        return Locale.forLanguageTag(normalized);
+    }
+
+    public static String toTag(Locale locale) {
+        if (locale == null) {
+            return "zh-CN";
+        }
+        String tag = locale.toLanguageTag();
+        return Cools.isEmpty(tag) || "und".equalsIgnoreCase(tag) ? "zh-CN" : tag;
+    }
+
+    public static Locale resolveLocale(String requested, I18nProperties properties) {
+        Locale fallback = defaultLocale(properties);
+        if (Cools.isEmpty(requested)) {
+            return fallback;
+        }
+        String requestedTag = requested.replace('_', '-').trim();
+        if (requestedTag.contains(",")) {
+            requestedTag = requestedTag.substring(0, requestedTag.indexOf(',')).trim();
+        }
+        if (requestedTag.contains(";")) {
+            requestedTag = requestedTag.substring(0, requestedTag.indexOf(';')).trim();
+        }
+        for (String supported : properties.getSupportedLocales()) {
+            String supportedTag = supported.replace('_', '-').trim();
+            if (supportedTag.equalsIgnoreCase(requestedTag)) {
+                return toLocale(supportedTag);
+            }
+            Locale supportedLocale = toLocale(supportedTag);
+            if (supportedLocale.getLanguage().equalsIgnoreCase(toLocale(requestedTag).getLanguage())) {
+                return supportedLocale;
+            }
+        }
+        return fallback;
+    }
+
+    public static boolean isDefaultLocale(Locale locale, I18nProperties properties) {
+        return toTag(defaultLocale(properties)).equalsIgnoreCase(toTag(locale));
+    }
+
+    public static String humanizeCode(String code) {
+        if (Cools.isEmpty(code)) {
+            return null;
+        }
+        String normalized = code;
+        int hashIndex = normalized.indexOf('#');
+        if (hashIndex > -1 && hashIndex < normalized.length() - 1) {
+            normalized = normalized.substring(hashIndex + 1);
+        } else {
+            int slashIndex = normalized.lastIndexOf('/');
+            if (slashIndex > -1 && slashIndex < normalized.length() - 1) {
+                normalized = normalized.substring(slashIndex + 1);
+            }
+            if (normalized.endsWith(".html")) {
+                normalized = normalized.substring(0, normalized.length() - 5);
+            }
+        }
+        normalized = normalized.replaceAll("[^A-Za-z0-9_\\-]+", " ");
+        Matcher matcher = WORD_BOUNDARY.matcher(normalized);
+        normalized = matcher.replaceAll("$1 $2");
+        normalized = normalized.replace('_', ' ').replace('-', ' ');
+        String[] parts = normalized.trim().split("\\s+");
+        if (parts.length == 0) {
+            return code;
+        }
+        StringBuilder builder = new StringBuilder();
+        for (String part : parts) {
+            if (Cools.isEmpty(part)) {
+                continue;
+            }
+            if (builder.length() > 0) {
+                builder.append(' ');
+            }
+            String lower = part.toLowerCase(Locale.ENGLISH);
+            if (UPPERCASE_WORDS.contains(lower)) {
+                builder.append(lower.toUpperCase(Locale.ENGLISH));
+            } else {
+                builder.append(Character.toUpperCase(part.charAt(0)));
+                if (part.length() > 1) {
+                    builder.append(part.substring(1));
+                }
+            }
+        }
+        return builder.length() == 0 ? code : builder.toString();
+    }
+}
diff --git a/src/main/java/com/zy/common/i18n/I18nMessageService.java b/src/main/java/com/zy/common/i18n/I18nMessageService.java
new file mode 100644
index 0000000..c125264
--- /dev/null
+++ b/src/main/java/com/zy/common/i18n/I18nMessageService.java
@@ -0,0 +1,406 @@
+package com.zy.common.i18n;
+
+import com.core.common.Cools;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.core.io.ClassPathResource;
+import org.springframework.stereotype.Service;
+import org.springframework.context.i18n.LocaleContextHolder;
+
+import java.io.File;
+import java.io.BufferedReader;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.nio.charset.StandardCharsets;
+import java.text.MessageFormat;
+import java.util.*;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.regex.PatternSyntaxException;
+
+@Service
+public class I18nMessageService {
+
+    private static final String MESSAGE_BUNDLE = "messages";
+    private static final String LEGACY_BUNDLE = "legacy";
+
+    private final Map<String, CacheEntry> cache = new ConcurrentHashMap<>();
+
+    @Autowired
+    private I18nProperties properties;
+
+    public Locale getCurrentLocale() {
+        Locale locale = LocaleContextHolder.getLocale();
+        return locale == null ? I18nLocaleUtils.defaultLocale(properties) : locale;
+    }
+
+    public Locale resolveLocale(String requested) {
+        return I18nLocaleUtils.resolveLocale(requested, properties);
+    }
+
+    public String getDefaultLocaleTag() {
+        return properties.getDefaultLocale();
+    }
+
+    public List<String> getSupportedLocaleTags() {
+        return properties.getSupportedLocales();
+    }
+
+    public String getMessage(String key, Object... args) {
+        return getMessage(key, getCurrentLocale(), args);
+    }
+
+    public String getMessage(String key, Locale locale, Object... args) {
+        if (Cools.isEmpty(key)) {
+            return "";
+        }
+        Locale resolvedLocale = locale == null ? I18nLocaleUtils.defaultLocale(properties) : locale;
+        String value = mergedBundle(resolvedLocale, MESSAGE_BUNDLE).get(key);
+        if (value == null && !I18nLocaleUtils.isDefaultLocale(resolvedLocale, properties)) {
+            value = mergedBundle(I18nLocaleUtils.defaultLocale(properties), MESSAGE_BUNDLE).get(key);
+        }
+        if (value == null) {
+            return key;
+        }
+        return args == null || args.length == 0 ? value : MessageFormat.format(value, args);
+    }
+
+    public boolean hasMessage(String key, Locale locale) {
+        return mergedBundle(locale == null ? I18nLocaleUtils.defaultLocale(properties) : locale, MESSAGE_BUNDLE).containsKey(key);
+    }
+
+    public Map<String, String> getMessages(Locale locale) {
+        return new LinkedHashMap<>(mergedBundle(locale, MESSAGE_BUNDLE));
+    }
+
+    public Map<String, String> getLegacyMessages(Locale locale) {
+        return new LinkedHashMap<>(mergedBundle(locale, LEGACY_BUNDLE));
+    }
+
+    public String translateLegacy(String text) {
+        return translateLegacy(text, getCurrentLocale());
+    }
+
+    public String translateLegacy(String text, Locale locale) {
+        if (Cools.isEmpty(text)) {
+            return text;
+        }
+        Locale resolvedLocale = locale == null ? I18nLocaleUtils.defaultLocale(properties) : locale;
+        if (I18nLocaleUtils.isDefaultLocale(resolvedLocale, properties)) {
+            return text;
+        }
+        Map<String, String> bundle = mergedBundle(resolvedLocale, LEGACY_BUNDLE);
+        String direct = directTranslate(text, bundle);
+        if (!text.equals(direct)) {
+            return direct;
+        }
+        String regex = regexTranslate(text, bundle);
+        if (!text.equals(regex)) {
+            return regex;
+        }
+        return fragmentTranslate(text, bundle);
+    }
+
+    public String resolveResourceText(String fallbackName, String code, Long id) {
+        Locale locale = getCurrentLocale();
+        String key = resourceKey(code, id);
+        if (hasMessage(key, locale)) {
+            return getMessage(key, locale);
+        }
+        if (!I18nLocaleUtils.isDefaultLocale(locale, properties)) {
+            String humanized = I18nLocaleUtils.humanizeCode(code);
+            if (!Cools.isEmpty(humanized)) {
+                return humanized;
+            }
+        }
+        return fallbackName;
+    }
+
+    public String resolvePermissionText(String fallbackName, String action, Long id) {
+        Locale locale = getCurrentLocale();
+        String key = permissionKey(action, id);
+        if (hasMessage(key, locale)) {
+            return getMessage(key, locale);
+        }
+        if (!I18nLocaleUtils.isDefaultLocale(locale, properties)) {
+            String humanized = I18nLocaleUtils.humanizeCode(action);
+            if (!Cools.isEmpty(humanized)) {
+                return humanized;
+            }
+        }
+        return fallbackName;
+    }
+
+    public String resourceKey(String code, Long id) {
+        return "resource." + normalizeKey(code, id, "resource");
+    }
+
+    public String permissionKey(String action, Long id) {
+        return "permission." + normalizeKey(action, id, "permission");
+    }
+
+    private Map<String, String> mergedBundle(Locale locale, String bundleName) {
+        Locale resolvedLocale = locale == null ? I18nLocaleUtils.defaultLocale(properties) : locale;
+        LinkedHashMap<String, String> merged = new LinkedHashMap<>();
+        Locale defaultLocale = I18nLocaleUtils.defaultLocale(properties);
+        merged.putAll(loadBundle(defaultLocale, bundleName));
+        if (!I18nLocaleUtils.toTag(defaultLocale).equalsIgnoreCase(I18nLocaleUtils.toTag(resolvedLocale))) {
+            merged.putAll(loadBundle(resolvedLocale, bundleName));
+        }
+        return merged;
+    }
+
+    private Map<String, String> loadBundle(Locale locale, String bundleName) {
+        String localeTag = I18nLocaleUtils.toTag(locale);
+        String cacheKey = localeTag + ":" + bundleName;
+        CacheEntry cacheEntry = cache.get(cacheKey);
+        File externalFile = externalBundle(localeTag, bundleName);
+        long refreshMillis = Math.max(1, properties.getRefreshSeconds()) * 1000L;
+        long externalLastModified = externalFile.exists() ? externalFile.lastModified() : -1L;
+        if (cacheEntry != null
+                && (System.currentTimeMillis() - cacheEntry.loadedAt) < refreshMillis
+                && cacheEntry.externalLastModified == externalLastModified) {
+            return cacheEntry.values;
+        }
+        synchronized (cache.computeIfAbsent(cacheKey, key -> new CacheEntry())) {
+            CacheEntry latest = cache.get(cacheKey);
+            if (latest != null
+                    && (System.currentTimeMillis() - latest.loadedAt) < refreshMillis
+                    && latest.externalLastModified == externalLastModified) {
+                return latest.values;
+            }
+            LinkedHashMap<String, String> values = new LinkedHashMap<>();
+            readClasspathBundle(values, localeTag, bundleName);
+            readExternalBundle(values, externalFile);
+            CacheEntry updated = new CacheEntry();
+            updated.values = values;
+            updated.loadedAt = System.currentTimeMillis();
+            updated.externalLastModified = externalLastModified;
+            cache.put(cacheKey, updated);
+            return updated.values;
+        }
+    }
+
+    private void readClasspathBundle(Map<String, String> values, String localeTag, String bundleName) {
+        ClassPathResource resource = new ClassPathResource("i18n/" + localeTag + "/" + bundleName + ".properties");
+        if (!resource.exists()) {
+            return;
+        }
+        try (InputStream inputStream = resource.getInputStream()) {
+            loadProperties(values, inputStream);
+        } catch (IOException ex) {
+            throw new IllegalStateException("Failed to load classpath i18n bundle: " + resource.getPath(), ex);
+        }
+    }
+
+    private void readExternalBundle(Map<String, String> values, File file) {
+        if (!file.exists() || !file.isFile()) {
+            return;
+        }
+        try (InputStream inputStream = new FileInputStream(file)) {
+            loadProperties(values, inputStream);
+        } catch (IOException ex) {
+            throw new IllegalStateException("Failed to load external i18n bundle: " + file.getAbsolutePath(), ex);
+        }
+    }
+
+    private void loadProperties(Map<String, String> values, InputStream inputStream) throws IOException {
+        try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8))) {
+            String line;
+            while ((line = reader.readLine()) != null) {
+                parsePropertyLine(values, line);
+            }
+        }
+    }
+
+    private void parsePropertyLine(Map<String, String> values, String line) {
+        if (line == null) {
+            return;
+        }
+        String trimmed = line.trim();
+        if (trimmed.isEmpty() || trimmed.startsWith("#") || trimmed.startsWith("!")) {
+            return;
+        }
+        int separatorIndex = findSeparator(line);
+        String rawKey = separatorIndex >= 0 ? line.substring(0, separatorIndex) : line;
+        String rawValue = separatorIndex >= 0 ? line.substring(separatorIndex + 1) : "";
+        while (!rawValue.isEmpty() && Character.isWhitespace(rawValue.charAt(0))) {
+            rawValue = rawValue.substring(1);
+        }
+        String key = unescapePropertyToken(rawKey);
+        String value = unescapePropertyToken(rawValue);
+        if (!key.isEmpty()) {
+            values.put(key, value);
+        }
+    }
+
+    private int findSeparator(String line) {
+        boolean escaping = false;
+        for (int i = 0; i < line.length(); i++) {
+            char ch = line.charAt(i);
+            if (escaping) {
+                escaping = false;
+                continue;
+            }
+            if (ch == '\\') {
+                escaping = true;
+                continue;
+            }
+            if (ch == '=' || ch == ':') {
+                return i;
+            }
+        }
+        return -1;
+    }
+
+    private String unescapePropertyToken(String text) {
+        StringBuilder builder = new StringBuilder(text.length());
+        boolean escaping = false;
+        for (int i = 0; i < text.length(); i++) {
+            char ch = text.charAt(i);
+            if (!escaping) {
+                if (ch == '\\') {
+                    escaping = true;
+                } else {
+                    builder.append(ch);
+                }
+                continue;
+            }
+            switch (ch) {
+                case 't':
+                    builder.append('\t');
+                    break;
+                case 'r':
+                    builder.append('\r');
+                    break;
+                case 'n':
+                    builder.append('\n');
+                    break;
+                case 'f':
+                    builder.append('\f');
+                    break;
+                case 'u':
+                    if (i + 4 < text.length()) {
+                        String hex = text.substring(i + 1, i + 5);
+                        try {
+                            builder.append((char) Integer.parseInt(hex, 16));
+                            i += 4;
+                            break;
+                        } catch (NumberFormatException ex) {
+                            builder.append('u');
+                            break;
+                        }
+                    }
+                    builder.append('u');
+                    break;
+                default:
+                    builder.append(ch);
+                    break;
+            }
+            escaping = false;
+        }
+        if (escaping) {
+            builder.append('\\');
+        }
+        return builder.toString();
+    }
+
+    private File externalBundle(String localeTag, String bundleName) {
+        return new File(properties.getPackPath(), localeTag + File.separator + bundleName + ".properties");
+    }
+
+    private String directTranslate(String text, Map<String, String> bundle) {
+        String trimmed = text.trim();
+        String translated = bundle.get(trimmed);
+        if (translated == null) {
+            return text;
+        }
+        return preserveOuterWhitespace(text, translated);
+    }
+
+    private String regexTranslate(String text, Map<String, String> bundle) {
+        String trimmed = text == null ? "" : text.trim();
+        for (Map.Entry<String, String> entry : bundle.entrySet()) {
+            String key = entry.getKey();
+            if (Cools.isEmpty(key) || !key.startsWith("regex:")) {
+                continue;
+            }
+            String patternText = key.substring("regex:".length());
+            if (Cools.isEmpty(patternText)) {
+                continue;
+            }
+            try {
+                Pattern pattern = Pattern.compile(patternText);
+                if (!trimmed.isEmpty()) {
+                    Matcher trimmedMatcher = pattern.matcher(trimmed);
+                    if (trimmedMatcher.matches()) {
+                        return preserveOuterWhitespace(text, trimmedMatcher.replaceAll(entry.getValue()));
+                    }
+                }
+                Matcher matcher = pattern.matcher(text);
+                if (matcher.find()) {
+                    return matcher.replaceAll(entry.getValue());
+                }
+            } catch (PatternSyntaxException ex) {
+                // Ignore invalid regex entries so a bad pack does not break all translations.
+            }
+        }
+        return text;
+    }
+
+    private String fragmentTranslate(String text, Map<String, String> bundle) {
+        List<Map.Entry<String, String>> entries = new ArrayList<>(bundle.entrySet());
+        entries.sort((left, right) -> Integer.compare(right.getKey().length(), left.getKey().length()));
+        String result = text;
+        for (Map.Entry<String, String> entry : entries) {
+            if (Cools.isEmpty(entry.getKey())
+                    || entry.getKey().length() < 2
+                    || entry.getKey().startsWith("regex:")
+                    || entry.getKey().equals(entry.getValue())) {
+                continue;
+            }
+            result = result.replace(entry.getKey(), entry.getValue());
+        }
+        return result;
+    }
+
+    private String preserveOuterWhitespace(String original, String translated) {
+        String trimmed = original == null ? "" : original.trim();
+        if (trimmed.isEmpty()) {
+            return translated;
+        }
+        int leading = original.indexOf(trimmed);
+        int trailing = original.length() - leading - trimmed.length();
+        StringBuilder builder = new StringBuilder();
+        if (leading > 0) {
+            builder.append(original, 0, leading);
+        }
+        builder.append(translated);
+        if (trailing > 0) {
+            builder.append(original.substring(original.length() - trailing));
+        }
+        return builder.toString();
+    }
+
+    private String normalizeKey(String raw, Long id, String prefix) {
+        if (Cools.isEmpty(raw)) {
+            return prefix + "." + (id == null ? "unknown" : id);
+        }
+        String normalized = raw.replaceAll("\\.html", "");
+        normalized = normalized.replaceAll("[^A-Za-z0-9]+", ".");
+        normalized = normalized.replaceAll("\\.+", ".");
+        normalized = normalized.replaceAll("^\\.|\\.$", "");
+        if (Cools.isEmpty(normalized)) {
+            return prefix + "." + (id == null ? "unknown" : id);
+        }
+        return normalized;
+    }
+
+    private static class CacheEntry {
+        private Map<String, String> values = new LinkedHashMap<>();
+        private long loadedAt;
+        private long externalLastModified = -1L;
+    }
+}
diff --git a/src/main/java/com/zy/common/i18n/I18nProperties.java b/src/main/java/com/zy/common/i18n/I18nProperties.java
new file mode 100644
index 0000000..c452b13
--- /dev/null
+++ b/src/main/java/com/zy/common/i18n/I18nProperties.java
@@ -0,0 +1,52 @@
+package com.zy.common.i18n;
+
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.stereotype.Component;
+
+import java.util.Arrays;
+import java.util.List;
+
+@Component
+@ConfigurationProperties(prefix = "app.i18n")
+public class I18nProperties {
+
+    private String defaultLocale = "zh-CN";
+
+    private List<String> supportedLocales = Arrays.asList("zh-CN", "en-US");
+
+    private String packPath = "./stock/out/wcs/i18n";
+
+    private long refreshSeconds = 10;
+
+    public String getDefaultLocale() {
+        return defaultLocale;
+    }
+
+    public void setDefaultLocale(String defaultLocale) {
+        this.defaultLocale = defaultLocale;
+    }
+
+    public List<String> getSupportedLocales() {
+        return supportedLocales;
+    }
+
+    public void setSupportedLocales(List<String> supportedLocales) {
+        this.supportedLocales = supportedLocales;
+    }
+
+    public String getPackPath() {
+        return packPath;
+    }
+
+    public void setPackPath(String packPath) {
+        this.packPath = packPath;
+    }
+
+    public long getRefreshSeconds() {
+        return refreshSeconds;
+    }
+
+    public void setRefreshSeconds(long refreshSeconds) {
+        this.refreshSeconds = refreshSeconds;
+    }
+}
diff --git a/src/main/java/com/zy/common/i18n/I18nResponseBodyAdvice.java b/src/main/java/com/zy/common/i18n/I18nResponseBodyAdvice.java
new file mode 100644
index 0000000..350f08a
--- /dev/null
+++ b/src/main/java/com/zy/common/i18n/I18nResponseBodyAdvice.java
@@ -0,0 +1,37 @@
+package com.zy.common.i18n;
+
+import com.core.common.R;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.core.MethodParameter;
+import org.springframework.http.MediaType;
+import org.springframework.http.converter.HttpMessageConverter;
+import org.springframework.http.server.ServerHttpRequest;
+import org.springframework.http.server.ServerHttpResponse;
+import org.springframework.web.bind.annotation.RestControllerAdvice;
+import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
+
+@RestControllerAdvice
+public class I18nResponseBodyAdvice implements ResponseBodyAdvice<Object> {
+
+    @Autowired
+    private I18nMessageService i18nMessageService;
+
+    @Override
+    public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
+        return true;
+    }
+
+    @Override
+    public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType,
+                                  Class<? extends HttpMessageConverter<?>> selectedConverterType,
+                                  ServerHttpRequest request, ServerHttpResponse response) {
+        if (body instanceof R) {
+            R result = (R) body;
+            Object msg = result.get("msg");
+            if (msg instanceof String) {
+                result.put("msg", i18nMessageService.translateLegacy((String) msg));
+            }
+        }
+        return body;
+    }
+}
diff --git a/src/main/java/com/zy/common/i18n/RequestLocaleInterceptor.java b/src/main/java/com/zy/common/i18n/RequestLocaleInterceptor.java
new file mode 100644
index 0000000..3cf87b0
--- /dev/null
+++ b/src/main/java/com/zy/common/i18n/RequestLocaleInterceptor.java
@@ -0,0 +1,53 @@
+package com.zy.common.i18n;
+
+import com.core.common.Cools;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
+
+import javax.servlet.http.Cookie;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.util.Locale;
+
+@Component
+public class RequestLocaleInterceptor extends HandlerInterceptorAdapter {
+
+    public static final String LANG_COOKIE_NAME = "wcs_lang";
+
+    @Autowired
+    private I18nMessageService i18nMessageService;
+
+    @Override
+    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
+        Locale locale = i18nMessageService.resolveLocale(extractLocale(request));
+        org.springframework.context.i18n.LocaleContextHolder.setLocale(locale);
+        request.setAttribute("wcsLocale", locale);
+        return true;
+    }
+
+    @Override
+    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
+        org.springframework.context.i18n.LocaleContextHolder.resetLocaleContext();
+    }
+
+    private String extractLocale(HttpServletRequest request) {
+        String locale = request.getParameter("lang");
+        if (!Cools.isEmpty(locale)) {
+            return locale;
+        }
+        locale = request.getHeader("X-Lang");
+        if (!Cools.isEmpty(locale)) {
+            return locale;
+        }
+        Cookie[] cookies = request.getCookies();
+        if (cookies != null) {
+            for (Cookie cookie : cookies) {
+                if (LANG_COOKIE_NAME.equals(cookie.getName()) && !Cools.isEmpty(cookie.getValue())) {
+                    return cookie.getValue();
+                }
+            }
+        }
+        return request.getHeader("Accept-Language");
+    }
+}
diff --git a/src/main/java/com/zy/common/web/AuthController.java b/src/main/java/com/zy/common/web/AuthController.java
index a86b971..5410b76 100644
--- a/src/main/java/com/zy/common/web/AuthController.java
+++ b/src/main/java/com/zy/common/web/AuthController.java
@@ -8,6 +8,7 @@
 import com.core.common.R;
 import com.core.exception.CoolException;
 import com.zy.common.CodeRes;
+import com.zy.common.i18n.I18nMessageService;
 import com.zy.common.entity.Parameter;
 import com.zy.common.model.PowerDto;
 import com.zy.common.model.enums.HtmlNavIconType;
@@ -50,13 +51,15 @@
     private RolePermissionService rolePermissionService;
     @Autowired
     private LicenseTimer licenseTimer;
+    @Autowired
+    private I18nMessageService i18nMessageService;
 
     @RequestMapping("/login.action")
     @ManagerAuth(value = ManagerAuth.Auth.NONE, memo = "鐧诲綍")
     public R loginAction(String mobile, String password){
         //楠岃瘉璁稿彲璇佹槸鍚︽湁鏁�
         if (!licenseTimer.getSystemSupport()){
-            return R.parse(CodeRes.SYSTEM_20001);
+            return new R(20001, i18nMessageService.getMessage("response.system.licenseExpired"));
         }
         if (mobile.equals("super") && password.equals(Cools.md5(superPwd))) {
             Map<String, Object> res = new HashMap<>();
@@ -68,13 +71,13 @@
         userWrapper.eq("mobile", mobile);
         User user = userService.selectOne(userWrapper);
         if (Cools.isEmpty(user)){
-            return R.parse(CodeRes.USER_10001);
+            return new R(10001, i18nMessageService.getMessage("response.user.notFound"));
         }
         if (user.getStatus()!=1){
-            return R.parse(CodeRes.USER_10002);
+            return new R(10002, i18nMessageService.getMessage("response.user.disabled"));
         }
         if (!user.getPassword().equals(password)){
-            return R.parse(CodeRes.USER_10003);
+            return new R(10003, i18nMessageService.getMessage("response.user.passwordMismatch"));
         }
         String token = Cools.enToken(System.currentTimeMillis() + mobile, user.getPassword());
         userLoginService.delete(new EntityWrapper<UserLogin>().eq("user_id", user.getId()).eq("system_type", "WCS"));
@@ -165,6 +168,7 @@
                         }
                     }
 
+                    resource.setName(localizeResourceName(resource));
                     subMenu.add(resource);
                     iterator.remove();
                 }
@@ -175,7 +179,7 @@
             map.put("menuId", menu.getId());
             map.put("menuCode", menu.getCode());
             map.put("menuIcon", HtmlNavIconType.get(menu.getCode()));
-            map.put("menu", menu.getName());
+            map.put("menu", localizeResourceName(menu));
             map.put("subMenu", subMenu);
             result.add(map);
         }
@@ -191,7 +195,7 @@
         for (Resource oneLevel : oneLevels){
             List<Map> twoLevelsList = new ArrayList<>();
             Map<String, Object> oneLevelMap = new HashMap<>();
-            oneLevelMap.put("title", oneLevel.getName());
+            oneLevelMap.put("title", localizeResourceName(oneLevel));
             oneLevelMap.put("id", oneLevel.getId());
             oneLevelMap.put("spread", true);
             oneLevelMap.put("children", twoLevelsList);
@@ -199,7 +203,7 @@
             // 浜岀骇
             for (Resource twoLevel : twoLevels){
                 Map<String, Object> twoLevelMap = new HashMap<>();
-                twoLevelMap.put("title", twoLevel.getName());
+                twoLevelMap.put("title", localizeResourceName(twoLevel));
                 twoLevelMap.put("id", twoLevel.getId());
                 twoLevelMap.put("spread", false);
 
@@ -209,7 +213,7 @@
                 List<Resource> threeLevels = resourceService.selectList(new EntityWrapper<Resource>().eq("resource_id", twoLevel.getId()).eq("level", 3).eq("status", 1).orderBy("sort"));
                 for (Resource threeLevel : threeLevels){
                     Map<String, Object> threeLevelMap = new HashMap<>();
-                    threeLevelMap.put("title", threeLevel.getName());
+                    threeLevelMap.put("title", localizeResourceName(threeLevel));
                     threeLevelMap.put("id", threeLevel.getId());
                     threeLevelMap.put("checked", false);
                     threeLevelsList.add(threeLevelMap);
@@ -222,7 +226,7 @@
 
         // 鍔熻兘妯″潡
         Map<String, Object> functions = new HashMap<>();
-        functions.put("title", "鎸囧畾鍔熻兘");
+        functions.put("title", i18nMessageService.getMessage("permission.function"));
         functions.put("id", "function");
         functions.put("spread", true);
         List<Map> funcs = new ArrayList<>();
@@ -230,7 +234,7 @@
         List<Permission> permissions = permissionService.selectList(new EntityWrapper<Permission>().eq("status", 1));
         for (Permission permission : permissions) {
             Map<String, Object> func = new HashMap<>();
-            func.put("title", permission.getName());
+            func.put("title", i18nMessageService.resolvePermissionText(permission.getName(), permission.getAction(), permission.getId()));
             func.put("id", permission.getAction());
             func.put("spread", true);
             funcs.add(func);
@@ -240,6 +244,10 @@
         return R.ok(result);
     }
 
+    private String localizeResourceName(Resource resource) {
+        return i18nMessageService.resolveResourceText(resource.getName(), resource.getCode(), resource.getId());
+    }
+
     @RequestMapping(value = "/power/{roleId}/auth")
     @ManagerAuth
     public R get(@PathVariable("roleId") Long roleId) {
diff --git a/src/main/java/com/zy/system/controller/ResourceController.java b/src/main/java/com/zy/system/controller/ResourceController.java
index 4cc94dc..9eac7ac 100644
--- a/src/main/java/com/zy/system/controller/ResourceController.java
+++ b/src/main/java/com/zy/system/controller/ResourceController.java
@@ -8,6 +8,7 @@
 import com.core.common.DateUtils;
 import com.core.common.R;
 import com.core.controller.AbstractBaseController;
+import com.zy.common.i18n.I18nMessageService;
 import com.zy.system.entity.Resource;
 import com.zy.system.service.ResourceService;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -20,6 +21,8 @@
 
     @Autowired
     private ResourceService resourceService;
+    @Autowired
+    private I18nMessageService i18nMessageService;
 
     @RequestMapping(value = "/resource/{id}/auth")
     @ManagerAuth
@@ -117,7 +120,12 @@
         for (Resource resource : page.getRecords()){
             Map<String, Object> map = new HashMap<>();
             map.put("id", resource.getId());
-            map.put("value", resource.getName().concat("(").concat(resource.getLevel$().substring(0, 2).concat(")")));
+            String levelText = i18nMessageService.translateLegacy(Cools.isEmpty(resource.getLevel$()) ? "" : resource.getLevel$());
+            String localizedName = i18nMessageService.resolveResourceText(resource.getName(), resource.getCode(), resource.getId());
+            String shortLevelText = levelText.matches(".*[\\u4E00-\\u9FA5].*")
+                    ? levelText.substring(0, Math.min(2, levelText.length()))
+                    : levelText;
+            map.put("value", localizedName.concat("(").concat(shortLevelText).concat(")"));
             result.add(map);
         }
         return R.ok(result);
@@ -137,7 +145,11 @@
         else {
             wrapper.orderBy("sort");
         }
-        return R.parse("0-鎿嶄綔鎴愬姛").add(resourceService.selectList(wrapper));
+        List<Resource> resources = resourceService.selectList(wrapper);
+        for (Resource resource : resources) {
+            resource.setName(i18nMessageService.resolveResourceText(resource.getName(), resource.getCode(), resource.getId()));
+        }
+        return R.parse("0-鎿嶄綔鎴愬姛").add(resources);
     }
 
 }
diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml
index a521e25..056f16e 100644
--- a/src/main/resources/application.yml
+++ b/src/main/resources/application.yml
@@ -1,7 +1,17 @@
 # 绯荤粺鐗堟湰淇℃伅
 app:
-  version: 1.0.5.2
+  version: 1.0.5.3
   version-type: dev  # prd 鎴� dev
+  i18n:
+    default-locale: zh-CN
+    supported-locales:
+      - zh-CN
+      - en-US
+    # 鍐呯疆璇█鍖呰鍙栦綅缃細classpath:/i18n/<locale>/*.properties
+    # 澶栫疆鍙畨瑁呰瑷�鍖呰鐩栫洰褰曪細./stock/out/@pom.build.finalName@/i18n/<locale>/*.properties
+    pack-path: ./stock/out/@pom.build.finalName@/i18n
+    # 澶栫疆璇█鍖呯儹鍔犺浇妫�鏌ュ懆鏈燂紙绉掞級
+    refresh-seconds: 10
 
 server:
   port: 9090
diff --git a/src/main/resources/i18n/en-US/legacy.properties b/src/main/resources/i18n/en-US/legacy.properties
new file mode 100644
index 0000000..63e8a9e
--- /dev/null
+++ b/src/main/resources/i18n/en-US/legacy.properties
@@ -0,0 +1,707 @@
+璐﹀彿=Account
+瀵嗙爜=Password
+鐧诲綍=Sign In
+绯荤粺宸ュ叿=System Tools
+鎺ㄨ崘鎿嶄綔=Recommended Actions
+鍏朵粬宸ュ叿=Other Tools
+鑾峰彇璇锋眰鐮�=Get Request Code
+涓�閿縺娲�=Activate
+鑾峰彇椤圭洰鍚嶇О=Get Project Name
+鑾峰彇绯荤粺閰嶇疆=Get System Config
+褰曞叆璁稿彲璇�=Import License
+宸插鍒跺埌鍓创鏉�=Copied to clipboard
+澶嶅埗澶辫触=Copy failed
+鑾峰彇璇锋眰鐮佸け璐�=Failed to get request code
+鑾峰彇绯荤粺閰嶇疆淇℃伅澶辫触=Failed to get system configuration
+璁稿彲璇佸唴瀹逛笉鑳戒负绌�=License content cannot be empty
+璁稿彲璇佹洿鏂版垚鍔�=License updated successfully
+璁稿彲璇佹洿鏂板け璐�=Failed to update license
+璁稿彲璇佸綍鍏ュけ璐�=Failed to import license
+婵�娲绘垚鍔�=Activation successful
+婵�娲诲け璐�=Activation failed
+鑾峰彇椤圭洰鍚嶇О澶辫触=Failed to get project name
+璇疯緭鍏ヨ处鍙�=Please enter account
+璇疯緭鍏ュ瘑鐮�=Please enter password
+鎼滅储鑿滃崟=Search menu
+娌℃湁鍖归厤鑿滃崟=No matching menus
+褰撳墠璐﹀彿娌℃湁鍙敤鑿滃崟=No available menus
+涓存椂璁稿彲璇佹湁鏁堟湡锛�=Temporary license valid:
+浠跨湡杩愯涓�=Simulation Running
+浠跨湡鏈繍琛�=Simulation Stopped
+鍩烘湰璧勬枡=Profile
+閫�鍑虹櫥褰�=Log Out
+鍏抽棴鍏朵粬椤电=Close Other Tabs
+杩斿洖鎺у埗涓績=Back to Dashboard
+璁稿彲璇佸嵆灏嗚繃鏈�=License Expiring Soon
+鐭ラ亾浜�=OK
+鎺у埗涓績=Control Center
+瀹炴椂鐩戞帶=Real-time Monitoring
+璐︽埛涓績=Account Center
+绠$悊鍛�=Admin
+姝e湪鍔犺浇椤甸潰...=Loading page...
+AI鍔╂墜=AI Assistant
+纭畾瑕佸仠姝豢鐪熸ā鎷熷悧锛�=Are you sure you want to stop the simulation?
+纭畾瑕佸惎鍔ㄤ豢鐪熸ā鎷熷悧锛�=Are you sure you want to start the simulation?
+浠跨湡妯℃嫙宸插仠姝�=Simulation stopped
+浠跨湡妯℃嫙宸插惎鍔�=Simulation started
+鎿嶄綔澶辫触=Operation failed
+鑿滃崟鍔犺浇澶辫触=Failed to load menu
+鑿滃崟鍔犺浇澶辫触锛岃妫�鏌ユ帴鍙g姸鎬�=Failed to load menu. Please check the API status.
+宸ヤ綔椤甸潰=Work Page
+涓氬姟椤甸潰=Business Page
+缂栧彿=ID
+璧峰鏃堕棿 - 缁堟鏃堕棿=Start time - End time
+璇疯緭鍏�=Please enter
+璇疯緭鍏�...=Please enter...
+璇烽�夋嫨鏁版嵁=Please select data
+璇烽�夋嫨瑕佸垹闄ょ殑鏁版嵁=Please select data to delete
+鏃犳暟鎹�=No data
+宸插瓨鍦�=Already exists
+涓嶅彲鐢�=Unavailable
+鍙栨秷閫夋嫨=Clear selection
+姝e父=Active
+绂佺敤=Disabled
+鍚敤=Enabled
+鍐荤粨=Frozen
+鍒犻櫎=Delete
+涓�绾ц彍鍗�=Level 1 Menu
+浜岀骇鑿滃崟=Level 2 Menu
+涓夌骇鑿滃崟=Level 3 Menu
+鏌ヨ=Search
+閲嶇疆=Reset
+鏂板=Add
+缂栬緫=Edit
+淇敼=Edit
+瀵煎嚭=Export
+淇濆瓨=Save
+鍙栨秷=Cancel
+杩斿洖=Back
+璇︽儏=Details
+宸ヤ綔鍙�=Work No.
+WMS宸ヤ綔鍙�=WMS Work No.
+婧愬簱浣�=Source Location
+鐩爣搴撲綅=Target Location
+鍫嗗灈鏈�=Crane
+鍙屽伐浣嶅爢鍨涙満=Dual-station Crane
+鍖哄煙缂栫爜=Area Code
+鏈懡鍚嶉〉闈�=Untitled Page
+鏈懡鍚嶅垎缁�=Untitled Group
+
+# Generic controls
+鎿嶄綔=Actions
+鎵撳嵃=Print
+鎼滅储=Search
+閲嶇疆=Reset
+绛涢�夊垪=Columns
+鍒扮=Go to
+纭畾=OK
+缂栧彿=ID
+瀵煎嚭=Export
+璧峰鏃堕棿 - 缁堟鏃堕棿=Start Time - End Time
+淇敼浜哄憳=Updated By
+淇敼鏃堕棿=Updated At
+澶囨敞=Remarks
+鏂板=Add
+宸ヤ綔鍙�=Work No.
+闄嶅簭=Descending
+鍗囧簭=Ascending
+娣诲姞鏃堕棿=Created At
+淇敼=Edit
+鐘舵��=Status
+鐩爣搴撲綅=Target Location
+璇烽�夋嫨=Please select
+鏃犳暟鎹�=No data
+灏鹃〉=Last Page
+姝e父=Active
+娣诲姞浜哄憳=Created By
+寮傚父=Exception
+寮傚父鐮�=Error Code
+宸ヤ綔鐘舵��=Work Status
+娌℃湁閫夐」=No options
+鐩爣绔�=Target Station
+婧愮珯=Source Station
+鍫嗗灈鏈哄彿=Crane No.
+鍛戒护=Command
+璇锋眰鍝嶅簲=Request/Response
+鍏ュ嚭搴撶被鍨�=IO Type
+璁惧缂栧彿=Device No.
+鏉$爜=Barcode
+绯荤粺鐘舵��=System Status
+涓嬪彂鏃堕棿=Issued At
+涓嬪彂鐘舵��=Issue Status
+浣滀笟=Operation
+鍒涘缓鏃堕棿=Created At
+鍙戠敓鏃堕棿=Occurred At
+缁撴灉=Result
+缁撴潫鏃堕棿=Ended At
+绫诲瀷=Type
+鍚嶇О=Name
+璧风偣搴撲綅=Source Location
+鍒锋柊=Refresh
+宸蹭笅鍙�=Issued
+淇濆瓨=Save
+缂栫爜=Code
+鎴愬姛=Success
+瑙掕壊=Role
+鎺ュ彛鍦板潃=API Endpoint
+鎵规=Batch
+鎵规搴忓垪=Batch Sequence
+鍓嶅線=Go
+璁惧绫诲瀷=Device Type
+澶辫触=Failed
+璇︽儏=Details
+鐢ㄦ埛=User
+浼樺厛绾�=Priority
+鏆傛棤鏁版嵁=No Data
+娉ㄥ唽鏃堕棿=Registered At
+鑷姩=Auto
+RGV鍙�=RGV No.
+鍏抽棴=Close
+宸ヤ綔鏃堕棿=Work Time
+鎵樼洏鐮�=Pallet Code
+婧愮珯鐐�=Source Station
+鐩爣绔欑偣=Target Station
+璁惧ip=Device IP
+璁惧绔彛=Device Port
+缃戝叧缂栧彿=Gateway No.
+瀹炵幇绫�=Implementation Class
+铏氭嫙璁惧=Virtual Device
+鍒涘缓浜哄憳=Created By
+璇锋眰鏃堕棿=Request Time
+鍝嶅簲鍙傛暟=Response Payload
+璇锋眰鍙傛暟=Request Payload
+璇锋眰鍐呭=Request Content
+鍝嶅簲鍐呭=Response Content
+鍚嶇О绌洪棿=Namespace
+鐧诲綍璐︽埛=Login Account
+鎵嬫満鍙�=Phone
+閭=Email
+纭淇敼=Confirm Update
+璁剧疆鎴戠殑璧勬枡=Edit My Profile
+涓嶅彲淇敼=Not Editable
+閲嶈锛佷竴鑸敤浜庡悗鍙扮櫥鍏�=Important: usually used for admin login
+褰撳墠瑙掕壊涓嶅彲鏇存敼涓哄叾瀹冭鑹�=Current role cannot be changed
+鎵嬫満鍙�:=Phone:
+鐢ㄦ埛鍚�:=Username:
+杈撳叆鎵嬫満鍙�=Enter phone number
+杈撳叆鐢ㄦ埛鍚�=Enter username
+閲嶇疆瀵嗙爜=Reset Password
+棣栭〉鑿滃崟=Home Menu
+瀹㈡埛绔疘P=Client IP
+璇锋眰鏁版嵁=Request Data
+鍝嶅簲鏁版嵁=Response Data
+鎿嶄綔鍐呭=Operation Content
+鍑瘉鍊�=Credential Value
+鍛樺伐=Employee
+鑿滃崟=Menu
+鎸夐挳=Button
+娣诲姞=Add
+鑿滃崟缂栫爜=Menu Code
+鑿滃崟鍚嶇О=Menu Name
+鏉冮檺鍚嶇О=Permission Name
+鎵�灞炶彍鍗�=Menu
+鏉冮檺=Permissions
+涓婄骇=Parent
+璐﹀彿=Account
+瀵嗙爜=Password
+
+# Common dynamic patterns
+regex\:^(\\d+)\\s*鏉�/椤�$=$1 / page
+regex\:^(\\d+)鏉�/椤�$=$1 / page
+regex\:^鍏盶\s*(\\d+)\\s*鏉�$=Total $1 items
+regex\:^鍏盶\s*(\\d+)\\s*涓澶�$=Total $1 devices
+regex\:^(\\d+)鍙峰爢鍨涙満$=Crane $1
+regex\:^(\\d+)鍙峰弻宸ヤ綅鍫嗗灈鏈�$=Dual-station Crane $1
+regex\:^鍫嗗灈鏈篭\s*(\\d+)$=Crane $1
+regex\:^鍙屽伐浣嶅爢鍨涙満\\s*(\\d+)$=Dual-station Crane $1
+regex\:^绔欑偣鍒楄〃\\s*\\((\\d+)\\)$=Station List ($1)
+regex\:^鍫嗗灈鏈鸿澶嘰\s*\\((\\d+)\\)$=Crane Devices ($1)
+regex\:^鍑哄簱鍖哄煙\\s*\\((\\d+)\\)$=Outbound Areas ($1)
+regex\:^鍑哄簱绔欑偣\\s*\\((\\d+)\\)$=Outbound Stations ($1)
+regex\:^宸ヤ綔鍙穃\s*(\\d+)\\s*\\|\\s*鐩爣\\s*(.+)$=Work No. $1 | Target $2
+regex\:^A(\\d+)\\s*-\\s*鏈堝彴(\\d+)$=A$1 - Dock $2
+
+# Watch and map pages
+鍦板浘鎿嶄綔=Map Controls
+閲嶇疆瑙嗗浘=Reset View
+绔欑偣棰滆壊=Station Color
+鏀惰捣闈㈡澘=Collapse Panel
+鐩戞帶宸ヤ綔鍙�=Monitoring Workbench
+鍫嗗灈鏈虹洃鎺�=Crane Monitor
+鍙屽伐浣�=Dual Station
+杈撻�佺珯=Stations
+涓婁竴椤�=Previous
+涓嬩竴椤�=Next
+鎵撳紑鎺у埗涓績=Open Control Center
+妤煎眰=Floor
+鏃嬭浆=Rotate
+闀滃儚=Mirror
+鏄剧ず绔欑偣鏂瑰悜=Show Station Direction
+搴撲綅鍦板浘=Location Map
+鐐瑰嚮搴撲綅鍚庡湪鍙充晶鏌ョ湅璇︽儏銆�=Click a location to view details on the right.
+鐐瑰嚮搴撲綅鍙煡鐪嬪簱浣嶇姸鎬侊紝鐐瑰嚮绔欑偣鍙煡鐪嬬珯鐐逛綔涓氳鎯呫�傚湴鍥炬搷浣滃湪鍙充笂瑙掑伐鍏烽潰鏉夸腑銆�=Click a location to view its status, or a station to view job details. Use the top-right tool panel for map controls.
+璐ф灦=Shelf
+瀹炴椂鏁版嵁=Live Data
+鍘熷鍦板浘=Raw Map
+鏈�杩戞暟鎹�=Latest Data
+鍒濆鍖栧簱浣�=Initialize Locations
+瀵煎叆鍦板浘=Import Map
+
+# Work pages
+浠诲姟绠$悊=Task Management
+浠诲姟绫诲瀷=Task Type
+绯荤粺娑堟伅=System Message
+婧愬簱浣�=Source Location
+鏃堕棿=Time
+
+# Baseline data and enums
+1.鍏ュ簱=1. Inbound
+101.鍑哄簱=101. Outbound
+201.绉诲簱浠诲姟=201. Transfer
+1.鐢熸垚鍏ュ簱浠诲姟=1. Create Inbound Task
+2.璁惧涓婅蛋=2. Device Moving Up
+3.璁惧鎼繍涓�=3. Device Handling
+9.鍏ュ簱瀹屾垚=9. Inbound Completed
+109.鍑哄簱瀹屾垚=109. Outbound Completed
+F.鍦ㄥ簱=F. In Stock
+X.绂佺敤=X. Disabled
+O.绌哄簱浣�=O. Empty Location
+C.鍏呯數鍗犵敤=C. Charging Occupied
+R.鍑哄簱棰勭害=R. Outbound Reserved
+S.鍏ュ簱棰勭害=S. Inbound Reserved
+鍑哄簱绔欏垪琛�=Outbound Stations
+鍏ュ簱绔欏垪琛�=Inbound Stations
+娣卞簱浣嶆帓鍙�=Deep Location Row No.
+鎺у埗搴撲綅鎺掑彿=Control Location Row No.
+绔欑偣鏁版嵁=Station Data
+椤跺崌绉绘牻鐐�=Lift Transfer Point
+鍑哄簱绔欑偣鏁版嵁=Outbound Station Data
+鍏ュ簱绔欑偣鏁版嵁=Inbound Station Data
+绔欑偣鍒悕=Station Alias
+绔欑偣妤煎眰=Station Floor
+搴撲綅鐘舵�佷唬鍙�=Location Status Code
+搴撲綅鐘舵�佹弿杩�=Location Status Description
+鐘舵�佹弿杩�=Status Description
+鍏ュ嚭绫诲瀷浠e彿=IO Type Code
+鍏ュ嚭绫诲瀷鎻忚堪=IO Type Description
+褰撳墠ID=Current ID
+鍚姩鍏ュ簱=Start Inbound
+璧峰ID=Start ID
+缁堟ID=End ID
+绔欑偣鍥為��=Station Rollback
+绉诲簱浠诲姟=Transfer Task
+浠跨湡闅忔満宸ヤ綔鍙�=Simulated Random Work No.
+浣庡簱浣�=Low Location
+楂樺簱浣�=High Location
+搴撲綅鍙�=Location Code
+瀹藉簱浣�=Wide Location
+璧锋灞�=Start/End Level
+璧锋鍒�=Start/End Column
+璧锋鎺�=Start/End Row
+杞诲簱浣�=Light Location
+绐勫簱浣�=Narrow Location
+閲嶅簱浣�=Heavy Location
+楂樹綆绫诲瀷=High/Low Type
+搴撲綅绫诲瀷=Location Type
+
+# Station and area configuration
+绔欑偣璁惧鍏崇郴閰嶇疆=Station-Device Mapping
+绔欑偣涓庡爢鍨涙満浠诲姟鍏宠仈閰嶇疆=Station-Crane Task Mapping
+淇濆瓨閰嶇疆=Save Configuration
+鎿嶄綔璇存槑锛氭嫋鎷藉乏渚х殑銆愮珯鐐广�戝埌鍙充晶鐨勩�愯澶囥�戜笂鍗冲彲寤虹珛鍏宠仈銆傜偣鍑昏繛绾垮彲鍒犻櫎鍏宠仈銆�=Drag a station from the left to a device on the right to create a mapping. Click a link to remove it.
+鍑哄簱绔欎笌鍑哄簱鍖哄煙缁戝畾閰嶇疆=Outbound Station-Area Binding
+鍖哄煙鍚嶇О=Area Name
+鏂板鍖哄煙=Add Area
+鎿嶄綔璇存槑锛氭嫋鎷藉乏渚с�愬嚭搴撶珯鐐广�戝埌鍙充晶銆愬嚭搴撳尯鍩熴�戜笂寤虹珛缁戝畾銆傜偣鍑昏繛绾垮彲鍒犻櫎缁戝畾銆�=Drag an outbound station from the left to an outbound area on the right to create a binding. Click a link to remove the binding.
+
+# Notification report
+閫氱煡涓婃姤=Notification Report
+琛ュ彂=Retry
+浠诲姟=Task
+浠诲姟鍙�=Task No.
+璁惧鍙�=Device No.
+杈撻�佺嚎=Conveyor
+宸插紑鍚�=Enabled
+鏌ョ湅鎶ユ枃=View Request
+鏌ョ湅鍝嶅簲=View Response
+闃熷垪鐘舵��=Queue Status
+鍙戦�佹椂闂�=Sent At
+鎵归噺琛ュ彂=Retry Selected
+浠诲姟瀹屾垚=Task Completed
+鏃ュ織缁撴灉=Log Result
+绛涢�夋潯浠�=Filters
+涓婃姤寮�鍏�=Reporting Switch
+鍒锋柊鍏ㄥ眬=Refresh Global
+
+# Device logs
+璁惧鏃ュ織=Device Logs
+鍏ㄩ儴=All
+涓嬭浇=Download
+鏃ユ湡閫夋嫨=Date Selection
+璁惧鍒楄〃=Device List
+閫変腑鏃ユ湡=Selected Date
+璧峰搴忓彿=Start Index
+鏈�澶ф枃浠�=Max Files
+璇疯緭鍏ョ紪鍙�=Enter ID
+鏂囦欢涓嬭浇涓�=Downloading file
+鏃ュ織鍙鍖� - ()=Device Logs - ()
+鏆傛棤鏁版嵁锛岃鍏堥�夋嫨鏃ユ湡=No data. Please select a date first.
+
+# Debug and system config
+璋冭瘯鍙傛暟=Debug Parameters
+鍏呯數鍙傛暟=Charging Settings
+璋冨害鍙傛暟=Dispatch Settings
+閬块殰鍐呭湀鍗婂緞=Inner Avoidance Radius
+閬块殰澶栧湀鍗婂緞=Outer Avoidance Radius
+瀹氭椂鍏呯數寮�鍏�=Scheduled Charging
+鍏ュ簱棰勭暀灏忚溅=Reserved Inbound AGV
+灏忚溅婊$數鏍″噯=Full Battery Calibration
+婕旂ず妯″紡鍙傛暟=Demo Mode Settings
+瀹氭椂鍏呯數鏃堕棿娈�=Scheduled Charging Window
+灏忚溅瀹氭椂鍏呯數绾�=Scheduled Charging Line
+灏忚溅榛樿鍏呯數绾�=Default Charging Line
+婕旂ず妯″紡-璺戝簱=Demo Mode - Cycle Storage
+灏忚溅鍏呯數鏈�澶ч槇鍊�=Max Charge Threshold
+灏忚溅鐢甸噺棰勮闃堝��=Low Battery Threshold
+杈撳嚭RCS璋冭瘯鏃ュ織=Output RCS Debug Logs
+婕旂ず妯″紡-璐х墿鎼繍=Demo Mode - Cargo Handling
+绉诲姩婕旂ず妯″紡-妤煎眰=Moving Demo Mode - Floor
+璋冨害灏忚溅鍚屽眰鏈�澶ф暟閲�=Max AGVs Per Floor
+灏忚溅鍑烘彁鍗囨満杩戠偣璺濈=AGV Lift Exit Distance
+灏忚溅绉诲姩杩炵画涓嬪彂鎸囦护=Continuous AGV Move Commands
+鍏佽浜ょ閲嶆柊瑙勫垝璺緞=Allow Traffic Replan
+鍦板浘姣嶈建鏂瑰悜(x,y)=Map Main Rail Direction (x,y)
+绉诲姩婕旂ず妯″紡-鏄惁鎹㈠眰=Moving Demo Mode - Change Floor
+灏忚溅(x,y)鍛戒护杩愯鏂瑰悜棰犲��=Invert AGV (x,y) Command Direction
+瀵瑰簲鍊�=Value
+绛涢�夌被鍨�=Filter Type
+鍒锋柊缂撳瓨=Refresh Cache
+鍑哄簱杩熷埌鎯╃綒=Outbound Delay Penalty
+鐩戞帶鍦板浘闀滃儚=Monitor Map Mirror
+鐩戞帶鍦板浘鏃嬭浆=Monitor Map Rotation
+
+# AI page
+AI閰嶇疆=AI Configuration
+蹇呭~=Required
+娴嬭瘯=Test
+澶嶅埗=Copy
+鏇村=More
+妯″瀷=Model
+鍚敤=Enabled
+鎬濊��=Thinking
+鍐峰嵈涓�=Cooling
+娓呭喎鍗�=Clear Cooldown
+鎬昏矾鐢�=Default Route
+璋冪敤鏃ュ織=Call Logs
+棰濆害鍒囨崲=Quota Switch
+澶嶅埗鍏ㄦ枃=Copy All
+鏁呴殰鍒囨崲=Failover
+鍐峰嵈绉掓暟=Cooldown Seconds
+璺敱鍚嶇О=Route Name
+鏃ュ織璇︽儏=Log Details
+鏂板璺敱=Add Route
+瀵煎嚭JSON=Export JSON
+瀵煎叆JSON=Import JSON
+
+# Second-pass audit fixes
+鏌ヨ=Query
+鍑哄簱=Outbound
+鍏ュ簱=Inbound
+浠e彿=Code
+鎺掑簭=Sort
+绛涢��=Filter
+褰撳墠娌℃湁鍙睍绀虹殑鍫嗗灈鏈烘暟鎹�=No crane data available
+褰撳墠娌℃湁寰呭彂閫侀�氱煡=No pending notifications
+鍙屼几浣嶅爢鍨涙満=Dual-reach Crane
+鍙屼几浣岰rane=Dual-reach Crane
+閫氱煡鍦板潃=Notify URL
+閫氱煡ID=Notify ID
+娑堟伅鎻忚堪=Message Description
+閲嶈瘯娆℃暟=Retry Count
+褰撳墠闃熷垪鏁�=Current Queue Size
+闂撮殧(s)=Interval (s)
+閫氱煡鏃ュ織鏁�=Notification Log Count
+褰撳墠閫氱煡闃熷垪=Current Notification Queue
+閫氱煡鍙戦�佹棩蹇�=Notification Send Logs
+閫氱煡绫诲瀷=Notification Type
+閫氱煡Type=Notification Type
+涓婃閲嶈瘯鏃堕棿=Last Retry Time
+涓婃閲嶈瘯Time=Last Retry Time
+娑堟伅绫诲瀷/鎻忚堪=Message Type / Description
+娑堟伅Type/鎻忚堪=Message Type / Description
+閫氱煡鏌ョ湅涓庤ˉ鍙�=Notification View & Retry
+閫氱煡鏌ョ湅涓嶳etry=Notification View & Retry
+閫氱煡鎺ュ彛鍘嗗彶璋冪敤璁板綍=Notification API History
+灞曠ず寰呭彂閫佸拰寰呴噸璇曢�氱煡=Show pending and retry notifications
+鏀寔浠庡巻鍙叉棩蹇楅噸鏂板彂閫侀�氱煡=Support resending from history
+宸查�� 0 鏉�=0 selected
+绔欑偣ID=Station ID
+閫氱煡涓婃姤=Notification Report
+褰撳墠椤电锛氶�氱煡闃熷垪=Current Tab: Notification Queue
+閫氱煡闃熷垪=Notification Queue
+閫氱煡鏃ュ織=Notification Logs
+浠诲姟绫诲瀷/鎻忚堪=Task Type / Description
+娑堟伅绫诲瀷=Message Type
+娑堟伅Type=Message Type
+閫氱煡绫诲瀷/鎻忚堪=Notification Type / Description
+璁惧鏃ュ織=Device Logs
+鏃ュ織鍙鍖� - ()=Device Logs - ()
+鏃ュ織鍙鍖�=Device Logs
+鏃ユ湡閫夋嫨=Date Selection
+閫変腑鏃ユ湡=Selected Date
+璧峰搴忓彿=Start Index
+鏈�澶ф枃浠�=Max Files
+鏂囦欢涓嬭浇涓�=Downloading file
+璁惧鍒楄〃=Device List
+璁惧鍙�=Device No.
+褰撳墠娌℃湁寰呭彂閫侀�氱煡=No notifications to send
+03鏈�=03
+03鏃�=03
+04鏃�=04
+05鏃�=05
+06鏃�=06
+07鏃�=07
+08鏃�=08
+09鏃�=09
+鏈煡=Unknown
+瀹界獎绫诲瀷=Width Type
+瀹界獎Type=Width Type
+杞婚噸绫诲瀷=Weight Type
+杞婚噸Type=Weight Type
+鍫嗗灈鏈烘暟閲�=Crane Count
+Crane鏁伴噺=Crane Count
+搴撲綅鐘舵��=Location Status
+搴撲綅Status=Location Status
+灞傛暟=Levels
+鏇存柊鏃堕棿=Updated At
+鏇存柊Time=Updated At
+绯荤粺杩愯鐘舵��=System Runtime Status
+绯荤粺杩愯Status=System Runtime Status
+鍚姩浠跨湡妯℃嫙=Start Simulation
+鍋滄浠跨湡妯℃嫙=Stop Simulation
+寮傚父鎯呭喌=Exception Details
+Exception鎯呭喌=Exception Details
+绯荤粺鐘舵�佹暟鎹�=System Status Payload
+System Status鏁版嵁=System Status Payload
+鏈�澶у嚭搴撲换鍔℃暟=Max Outbound Tasks
+鏈�澶у嚭搴揟ask鏁�=Max Outbound Tasks
+鏈�澶у叆搴撲换鍔℃暟=Max Inbound Tasks
+鏈�澶у叆搴揟ask鏁�=Max Inbound Tasks
+鍙嚭(checkBox)=Outbound Enabled (Checkbox)
+鍙叆(checkBox)=Inbound Enabled (Checkbox)
+宸ヤ綅1绂佹鎵ц鍒�=Station 1 Disabled Columns
+宸ヤ綅2绂佹鎵ц鍒�=Station 2 Disabled Columns
+鍑哄簱绔欑偣=Outbound Stations
+鍏ュ簱绔欑偣=Inbound Stations
+鍑哄簱鎺掑簭浜や簰鐐�=Outbound Sort Interaction Point
+鍒濆鍖栫珯鐐规暟鎹�=Initialize Station Data
+鍒濆鍖朣tation Data=Initialize Station Data
+杩愯鍫靛閲嶆柊鍒嗛厤搴撲綅绔欑偣鏁版嵁=Reassign blocked location station data
+杩愯鍫靛閲嶆柊鍒嗛厤搴撲綅Station Data=Reassign blocked location station data
+铏氭嫙璁惧鍒濆鍖栬澶囩姸鎬�=Virtual Device Initializes Device Status
+Virtual Device鍒濆鍖栬澶嘢tatus=Virtual Device Initializes Device Status
+Initialize Locations鍚庡皢Delete搴撳瓨鏄庣粏锛岃璋ㄦ厧Actions锛�=Initializing locations will delete inventory details. Proceed carefully.
+缁曞湀鏈�澶ф壙杞介噺=Max Loop Load
+瀹屽伐鏃堕棿鏉冮噸=Finish Time Weight
+瀹屽伐Time鏉冮噸=Finish Time Weight
+骞惰鎼滅储绾跨▼鏁�=Parallel Search Threads
+骞惰Search绾跨▼鏁�=Parallel Search Threads
+鏈�澶ф眰瑙f椂闂�(s)=Max Solve Time (s)
+鏈�澶ф眰瑙ime(s)=Max Solve Time (s)
+浠诲姟绛夊緟鏃堕棿鏉冮噸=Task Wait Time Weight
+Task绛夊緟Time鏉冮噸=Task Wait Time Weight
+鏄惁鍚敤鍑哄簱鑺傛媿=Enable Outbound Rhythm
+鏄惁Enabled鍑哄簱鑺傛媿=Enable Outbound Rhythm
+浼樺厛绾ф棭瀹屾垚鏉冮噸=Priority Early Completion Weight
+Priority鏃╁畬鎴愭潈閲�=Priority Early Completion Weight
+绔欑偣鐐圭粫鍦堟ā寮�=Station Loop Mode
+Stations鐐圭粫鍦堟ā寮�=Station Loop Mode
+鍚屼竴鍑哄簱缁勫簭鍒椾箣闂寸殑鏈�灏忛棿闅�=Min Interval Between Same Outbound Group Sequences
+鍫嗗灈鏈鸿揣鍙夋斁璐ц�楁椂(s)=Crane Fork Putaway Time (s)
+Crane璐у弶鏀捐揣鑰楁椂(s)=Crane Fork Putaway Time (s)
+鍫嗗灈鏈鸿揣鍙夊彇璐ц�楁椂(s)=Crane Fork Pickup Time (s)
+Crane璐у弶鍙栬揣鑰楁椂(s)=Crane Fork Pickup Time (s)
+杈撻�佺嚎鍛戒护鍒嗘闀垮害=Conveyor Command Segment Length
+ConveyorCommand鍒嗘闀垮害=Conveyor Command Segment Length
+绔欑偣鐐规渶澶т换鍔℃暟閲忎笂闄�=Max Tasks Per Station
+Stations鐐规渶澶ask鏁伴噺涓婇檺=Max Tasks Per Station
+鍐峰嵈鍒�: -=Cooldown Until: -
+鏈�杩戦敊璇�: -=Latest Error: -
+鏁呴殰鍒囨崲寮�鍚�=Failover Enabled
+Failover寮�鍚�=Failover Enabled
+棰濆害鍒囨崲寮�鍚�=Quota Switch Enabled
+Quota Switch寮�鍚�=Quota Switch Enabled
+浼樺厛绾э紙瓒婂皬瓒婁紭鍏堬級=Priority (smaller is higher)
+AI閰嶇疆 - LLM璺敱=AI Configuration - LLM Routes
+鏀寔澶欰PI銆佸妯″瀷銆佸Key锛岄搴﹁�楀敖鎴栨晠闅滆嚜鍔ㄥ垏鎹�=Supports multiple APIs, models, and keys with automatic failover on quota exhaustion or errors
+鏀寔澶欰PI銆佸Model銆佸Key锛岄搴﹁�楀敖鎴栨晠闅淎uto鍒囨崲=Supports multiple APIs, models, and keys with automatic failover on quota exhaustion or errors
+鎴愬姛 0 / 澶辫触 0 / 杩炵画澶辫触 0=Success 0 / Failed 0 / Consecutive Failures 0
+Success 0 / Failed 0 / 杩炵画Failed 0=Success 0 / Failed 0 / Consecutive Failures 0
+Success 7 / Failed 2 / 杩炵画Failed 0=Success 7 / Failed 2 / Consecutive Failures 0
+Success 8 / Failed 0 / 杩炵画Failed 0=Success 8 / Failed 0 / Consecutive Failures 0
+蹇呭~锛屼緥濡�: https://dashscope.aliyuncs.com/compatible-mode/v1=Required, for example: https://dashscope.aliyuncs.com/compatible-mode/v1
+Required锛屼緥濡�: https://dashscope.aliyuncs.com/compatible-mode/v1=Required, for example: https://dashscope.aliyuncs.com/compatible-mode/v1
+鏈�杩戦敊璇�:=Latest Error:
+鍐峰嵈鍒�:=Cooldown Until:
+D.绌烘《/绌烘爤鏉�=D. Empty Tote/Pallet
+E.鍑哄叆涓撶敤杞ㄩ亾=E. Dedicated IO Rail
+W.绌挎杞︽瘝杞ㄩ亾=W. Shuttle Main Rail
+P.鎷f枡/鐩樼偣/骞舵澘鍑哄簱涓�=P. Picking/Counting/Merging Outbound
+Q.鎷f枡/鐩樼偣/骞舵澘鍐嶅叆搴�=Q. Picking/Counting/Merging Re-Inbound
+4.璁惧鎼繍瀹屾垚=4. Device Handling Completed
+10.搴撳瓨鏇存柊瀹屾垚=10. Inventory Update Completed
+102.璁惧鎼繍涓�=102. Device Handling In Progress
+104.绔欑偣杩愯涓�=104. Station Running
+502.璁惧鎼繍涓�=502. Device Handling In Progress
+103.璁惧鎼繍瀹屾垚=103. Device Handling Completed
+110.搴撳瓨鏇存柊瀹屾垚=110. Inventory Update Completed
+503.璁惧鎼繍瀹屾垚=503. Device Handling Completed
+509.绉诲簱瀹屾垚=509. Transfer Completed
+101.鐢熸垚鍑哄簱浠诲姟=101. Create Outbound Task
+101.鐢熸垚鍑哄簱Task=101. Create Outbound Task
+501.鐢熸垚绉诲簱浠诲姟=501. Create Transfer Task
+501.鐢熸垚Transfer Task=501. Create Transfer Task
+
+椤�=page
+鎺�=Row
+鍒�=Column
+灞�=Level
+鏄�=Yes
+鍚�=No
+寮�=On
+鍏�=Off
+宸ヤ綔鏁版嵁涓嶅瓨鍦�=Work data does not exist
+宸插彈鐞嗭紙寮傛鎵ц锛�=Accepted (async)
+Command宸插彈鐞嗭紙寮傛鎵ц锛�=Command accepted (async)
+閫氱煡涓婃姤涓績=Notification Report Center
+鏌ョ湅褰撳墠寰呴�氱煡闃熷垪銆佹帴鍙e彂閫佹棩蹇楋紝鏀寔鎸変换鍔�/璁惧蹇�熺瓫閫夛紝骞跺澶辫触鎴栧緟鍙戦�侀�氱煡鎵ц鎵嬪姩琛ュ彂銆�=View pending notification queues and send logs, filter quickly by task or device, and manually resend failed or pending notifications.
+Redis 涓緟涓婃姤鎴栧緟閲嶈瘯閫氱煡=Notifications pending report or retry in Redis
+鏉ユ簮锛歯otifyUri + notifyUriPath=Source: notifyUri + notifyUriPath
+闃熷垪涓庢棩蹇楀叡鐢ㄥ悓涓�缁勬煡璇㈡潯浠讹紝鍒囨崲椤电鏃朵繚鎸佷竴鑷淬��=Queue and log tabs share the same query conditions and stay in sync when switching tabs.
+閫氱敤鎼滅储锛氫换鍔″彿銆佹秷鎭弿杩般�佹姤鏂囧叧閿瓧銆丷edis Key=General search: Task No., message description, payload keyword, Redis key
+褰撳墠闃熷垪鏄剧ず Redis 瀹炴椂鏁版嵁锛屽彂閫佹棩蹇楁樉绀哄巻鍙叉帴鍙h皟鐢ㄧ粨鏋溿��=The current queue shows live Redis data, and send logs show historical API call results.
+CraneInboundTask鎵ц涓�=CraneInboundTask in progress
+LLM璋冪敤鏃ュ織=LLM Call Logs
+鏃ュ織璇︽儏 - =Log Details - 
+鏃ュ織鍙鍖� - =Device Logs - 
+淇℃伅=Info
+纭畾瀵煎嚭Excel鍚�=Export to Excel?
+纭畾淇敼璧勬枡鍚楋紵=Confirm profile update?
+淇敼瀵嗙爜=Change Password
+褰撳墠瀵嗙爜=Current Password
+鏂板瘑鐮�=New Password
+纭鏂板瘑鐮�=Confirm New Password
+瀵嗙爜涓嶅尮閰�=Password does not match
+鏂板瘑鐮佷笉鑳戒负绌�=New password cannot be empty
+涓嶈兘灏戜簬4涓瓧绗�=Must be at least 4 characters
+涓庢棫瀵嗙爜涓嶈兘鐩稿悓=Must differ from the current password
+瀵嗙爜涓嶄竴鑷�=Passwords do not match
+瀵嗙爜淇敼鎴愬姛锛岃閲嶆柊鐧诲綍=Password updated successfully. Please sign in again.
+鏀惰捣鎿嶄綔=Hide Controls
+闅愯棌绔欑偣鏂瑰悜=Hide Station Direction
+鍙栨秷闀滃儚=Disable Mirror
+灞曞紑闈㈡澘=Expand Panel
+鏀惰捣鎺у埗涓績=Hide Control Center
+鍙屽伐浣嶅爢鍨涙満鐩戞帶=Dual-station Crane Monitor
+杈撻�佺洃鎺�=Station Monitor
+RGV鐩戞帶=RGV Monitor
+璇疯緭鍏ュ爢鍨涙満鍙�=Enter crane number
+璇疯緭鍏GV鍙�=Enter RGV number
+璇疯緭鍏ョ珯鍙�=Enter station number
+鍫嗗灈鏈哄彿=Crane No.
+RGV鍙�=RGV No.
+绔欏彿=Station No.
+婧愮偣=Source Point
+鐩爣鐐�=Target Point
+杈撳叆婧愮偣=Enter source point
+杈撳叆鐩爣鐐�=Enter target point
+杈撳叆宸ヤ綔鍙�=Enter work number
+杈撳叆鐩爣绔欏彿=Enter target station no.
+鍙栨斁璐�=Pick/Put
+绉诲姩=Move
+浠诲姟瀹屾垚=Task Complete
+涓嬪彂=Send
+澶嶄綅=Reset
+缂栧彿=ID
+妯″紡=Mode
+鐘舵��=Status
+鏄惁鏈夌墿=Loaded
+浠诲姟鎺ユ敹=Task Receive
+璐у弶瀹氫綅=Fork Position
+杞借揣鍙板畾浣�=Lift Position
+璧拌瀹氫綅=Travel Position
+璧拌閫熷害=Travel Speed
+鍗囬檷閫熷害=Lift Speed
+鍙夌墮閫熷害=Fork Speed
+绉伴噸鏁版嵁=Weight
+鏉$爜鏁版嵁=Barcode
+鏁呴殰浠g爜=Error Code
+鏁呴殰鎻忚堪=Alarm Description
+鎵╁睍鏁版嵁=Extended Data
+褰撳墠娌℃湁鍙睍绀虹殑鍫嗗灈鏈烘暟鎹�=No crane data available
+宸ヤ綅=Station
+宸ヤ綅1=Station 1
+宸ヤ綅2=Station 2
+缂栬緫宸ヤ綅1浠诲姟鍙�=Edit Station 1 Task No.
+缂栬緫宸ヤ綅2浠诲姟鍙�=Edit Station 2 Task No.
+褰撳墠娌℃湁鍙睍绀虹殑鍙屽伐浣嶅爢鍨涙満鏁版嵁=No dual-station crane data available
+寮傚父鐮�=Error Code
+宸ヤ綅1浠诲姟鍙�=Station 1 Task No.
+宸ヤ綅2浠诲姟鍙�=Station 2 Task No.
+璁惧宸ヤ綅1浠诲姟鍙�=Device Station 1 Task No.
+璁惧宸ヤ綅2浠诲姟鍙�=Device Station 2 Task No.
+宸ヤ綅1鐘舵��=Station 1 Status
+宸ヤ綅2鐘舵��=Station 2 Status
+宸ヤ綅1鏄惁鏈夌墿=Station 1 Loaded
+宸ヤ綅2鏄惁鏈夌墿=Station 2 Loaded
+宸ヤ綅1璐у弶瀹氫綅=Station 1 Fork Position
+宸ヤ綅2璐у弶瀹氫綅=Station 2 Fork Position
+宸ヤ綅1浠诲姟鎺ユ敹=Station 1 Task Receive
+宸ヤ綅2浠诲姟鎺ユ敹=Station 2 Task Receive
+宸ヤ綅1涓嬪彂鏁版嵁=Station 1 Sent Data
+宸ヤ綅2涓嬪彂鏁版嵁=Station 2 Sent Data
+绔�=Station
+浠诲姟=Task
+鎵嬪姩=Manual
+鏈夌墿=Loaded
+鍙叆=Inbound Enabled
+鍙嚭=Outbound Enabled
+绌烘澘淇″彿=Empty Pallet Signal
+婊℃澘淇″彿=Full Pallet Signal
+杩愯闃诲=Run Block
+鎵樼洏楂樺害=Pallet Height
+鏉$爜=Barcode
+閲嶉噺=Weight
+浠诲姟鍙啓鍖�=Task Writable Area
+鏁呴殰淇℃伅=Error Message
+褰撳墠娌℃湁鍙睍绀虹殑绔欑偣鏁版嵁=No station data available
+渚嬪 1=For example: 1
+渚嬪 2=For example: 2
+渚嬪 101=For example: 101
+褰撳墠娌℃湁鍙睍绀虹殑RGV鏁版嵁=No RGV data available
+杞ㄩ亾浣�=Track Position
+搴撲綅璇︽儏=Location Details
+绔欑偣淇℃伅=Station Info
+绔欑偣浣滀笟鐘舵�併�佹潵婧愮洰鏍囧拰浣滀笟鍙傛暟=Station job status, source/target, and job parameters
+搴撲綅褰撳墠鐘舵�佸拰鎵�鍦ㄦ帓鍒楀眰淇℃伅=Current location status and row/bay/level details
+绔欑偣棰滆壊閰嶇疆=Station Color Configuration
+鏆傛棤绔欑偣棰滆壊閰嶇疆椤�=No station color configuration items
+regex\:^(\\d+)绔�$=Station $1
+regex\:^浠诲姟\\s*(.+)\\s*\\|\\s*鐩爣绔橽\s*(.+)$=Task $1 | Target Station $2
+regex\:^杞ㄩ亾浣峔\s*(.+)\\s*\\|\\s*浠诲姟\\s*(.+)$=Track $1 | Task $2
+regex\:^宸ヤ綅1\\s*(.+)\\s*\\|\\s*宸ヤ綅2\\s*(.+)$=Station 1 $1 | Station 2 $2
+regex\:^(\\d+)鍙稲GV$=RGV $1
+15鏉�/椤�=15 / page
+30鏉�/椤�=30 / page
+50鏉�/椤�=50 / page
+100鏉�/椤�=100 / page
+200鏉�/椤�=200 / page
+500鏉�/椤�=500 / page
+Stations鐐圭粫鍦圡ode=Station Loop Mode
+闃块噷鐧剧偧-MiniMax-M2.1=Alibaba Bailian - MiniMax-M2.1
+闃块噷鐧剧偧-MiniMax-M2.5=Alibaba Bailian - MiniMax-M2.5
+闃块噷鐧剧偧-qwen3.5-flash=Alibaba Bailian - qwen3.5-flash
+闃块噷鐧剧偧-qwen3.5-plus=Alibaba Bailian - qwen3.5-plus
+闃块噷鐧剧偧-kimi-k2.5=Alibaba Bailian - kimi-k2.5
+闃块噷鐧剧偧-glm-5=Alibaba Bailian - glm-5
+纭呭熀娴佸姩=SiliconFlow
diff --git a/src/main/resources/i18n/en-US/messages.properties b/src/main/resources/i18n/en-US/messages.properties
new file mode 100644
index 0000000..f08806f
--- /dev/null
+++ b/src/main/resources/i18n/en-US/messages.properties
@@ -0,0 +1,176 @@
+lang.zh-CN=Chinese (Simplified)
+lang.en-US=English
+app.title=Zhejiang Zhongyang - Automated AS/RS - WCS
+app.company=Zhejiang Zhongyang Warehouse Technology Co., Ltd.
+common.loadingPage=Loading page...
+common.loadingTab=Loading "{0}" ...
+common.refreshingTab=Refreshing "{0}" ...
+common.ok=OK
+common.prompt=Prompt
+common.language=Language
+common.profile=Profile
+common.logout=Log Out
+common.closeOtherTabs=Close Other Tabs
+common.backHome=Back to Dashboard
+common.aiAssistant=AI Assistant
+common.workPage=Work Page
+common.businessPage=Business Page
+login.title=WCS System V3.0
+login.username=Account
+login.password=Password
+login.submit=Sign In
+login.tools.title=System Tools
+login.tools.recommended=Recommended Actions
+login.tools.recommendedDesc=Use "Get Request Code" and "Activate" first to complete license application and activation.
+login.tools.others=Other Tools
+login.tools.requestCode=Get Request Code
+login.tools.activate=Activate
+login.tools.projectName=Get Project Name
+login.tools.serverInfo=Get System Config
+login.tools.uploadLicense=Import License
+login.dialog.copy=Copy
+login.dialog.copied=Copied to clipboard
+login.dialog.copyFailed=Copy failed
+login.requestCode.title=Request Code
+login.requestCode.label=Request Code
+login.requestCode.tip=The request code already contains the project name and can be sent directly to the license service.
+login.serverInfo.title=System Configuration
+login.serverInfo.label=System Configuration
+login.serverInfo.tip=Legacy projects can still use this hardware JSON to apply for a license.
+index.searchMenu=Search menu
+index.noMatchedMenu=No matching menus
+index.noAvailableMenu=No available menus for current account
+index.licenseDays=Temporary license valid: {0} day(s)
+index.fakeRunning=Simulation Running
+index.fakeStopped=Simulation Stopped
+index.licenseExpiring=License Expiring Soon
+index.homeTab=Control Center
+index.homeGroup=Real-time Monitoring
+index.profileGroup=Account Center
+index.versionLoading=Version loading...
+index.licenseExpireAt=The license will expire on {0}. Remaining validity: {1} day(s).
+index.confirmStopFake=Are you sure you want to stop the simulation?
+index.confirmStartFake=Are you sure you want to start the simulation?
+index.fakeStoppedSuccess=Simulation stopped
+index.fakeStartedSuccess=Simulation started
+index.operationFailed=Operation failed
+index.menuLoadFailed=Failed to load menu
+index.menuLoadFailedDetail=Failed to load menu. Please check the API status.
+response.user.notFound=Account does not exist
+response.user.disabled=Account is disabled
+response.user.passwordMismatch=Incorrect password
+response.system.licenseExpired=License has expired
+response.common.systemError=System error. Please try again later.
+response.common.methodNotAllowed=Request method not supported
+resource.index=Control Center
+resource.system=System Management
+resource.set=System Settings
+resource.merchant=Customer Management
+resource.develop=Development
+resource.stock=Inventory
+resource.logReport=Logs & Reports
+resource.ioWork=Inbound/Outbound Jobs
+resource.workFlow=Workflow
+resource.base=Basic Data
+resource.erp=ERP Integration
+resource.sensor=Sensor Devices
+resource.ai.llm_config=AI Configuration
+resource.notifyReport.notifyReport=Notification Report
+resource.view=View
+permission.function=Specified Functions
+el.colorpicker.confirm=OK
+el.colorpicker.clear=Clear
+el.datepicker.now=Now
+el.datepicker.today=Today
+el.datepicker.cancel=Cancel
+el.datepicker.clear=Clear
+el.datepicker.confirm=OK
+el.datepicker.selectDate=Select date
+el.datepicker.selectTime=Select time
+el.datepicker.startDate=Start date
+el.datepicker.startTime=Start time
+el.datepicker.endDate=End date
+el.datepicker.endTime=End time
+el.datepicker.prevYear=Previous year
+el.datepicker.nextYear=Next year
+el.datepicker.prevMonth=Previous month
+el.datepicker.nextMonth=Next month
+el.datepicker.year=
+el.datepicker.month1=January
+el.datepicker.month2=February
+el.datepicker.month3=March
+el.datepicker.month4=April
+el.datepicker.month5=May
+el.datepicker.month6=June
+el.datepicker.month7=July
+el.datepicker.month8=August
+el.datepicker.month9=September
+el.datepicker.month10=October
+el.datepicker.month11=November
+el.datepicker.month12=December
+el.datepicker.weeks.sun=Sun
+el.datepicker.weeks.mon=Mon
+el.datepicker.weeks.tue=Tue
+el.datepicker.weeks.wed=Wed
+el.datepicker.weeks.thu=Thu
+el.datepicker.weeks.fri=Fri
+el.datepicker.weeks.sat=Sat
+el.datepicker.months.jan=Jan
+el.datepicker.months.feb=Feb
+el.datepicker.months.mar=Mar
+el.datepicker.months.apr=Apr
+el.datepicker.months.may=May
+el.datepicker.months.jun=Jun
+el.datepicker.months.jul=Jul
+el.datepicker.months.aug=Aug
+el.datepicker.months.sep=Sep
+el.datepicker.months.oct=Oct
+el.datepicker.months.nov=Nov
+el.datepicker.months.dec=Dec
+el.select.loading=Loading
+el.select.noMatch=No matching data
+el.select.noData=No data
+el.select.placeholder=Please select
+el.cascader.noMatch=No matching data
+el.cascader.loading=Loading
+el.cascader.placeholder=Please select
+el.cascader.noData=No data
+el.pagination.goto=Go to
+el.pagination.pagesize=/page
+el.pagination.total=Total {total}
+el.pagination.pageClassifier=
+el.messagebox.title=Prompt
+el.messagebox.confirm=OK
+el.messagebox.cancel=Cancel
+el.messagebox.error=Invalid input
+el.upload.deleteTip=Press delete to remove
+el.upload.delete=Delete
+el.upload.preview=Preview
+el.upload.continue=Continue
+legacy.regex.loopStatus=Zone {0} | Stations: {1} | Tasks: {2} | Load: {3}
+legacy.regex.stationDeviceLink=Click to remove mapping: Station {0} -> Device {1}
+deviceLogs.visualizationPrefix=Device Logs - 
+deviceLogs.downloadDialogTitle=Downloading file
+llm.logsTitle=LLM Call Logs
+llm.logDetailTitle=Log Details
+llm.logDetailPrefix=Log Details - 
+el.table.emptyText=No data
+el.table.confirmFilter=Columns
+el.table.resetFilter=Reset
+el.table.clearFilter=All
+el.table.sumText=Sum
+el.table.sort.ascending=Ascending
+el.table.sort.descending=Descending
+el.tree.emptyText=No data
+el.transfer.noMatch=No matching data
+el.transfer.noData=No data
+el.transfer.titles.0=List 1
+el.transfer.titles.1=List 2
+el.transfer.filterPlaceholder=Enter keyword
+el.transfer.noCheckedFormat=0 items
+el.transfer.hasCheckedFormat={checked}/{total} checked
+el.image.error=Load failed
+el.pageHeader.title=Back
+el.popconfirm.confirmButtonText=OK
+el.popconfirm.cancelButtonText=Cancel
+el.empty.description=No data
diff --git a/src/main/resources/i18n/zh-CN/legacy.properties b/src/main/resources/i18n/zh-CN/legacy.properties
new file mode 100644
index 0000000..558d01a
--- /dev/null
+++ b/src/main/resources/i18n/zh-CN/legacy.properties
@@ -0,0 +1,88 @@
+璐﹀彿=璐﹀彿
+瀵嗙爜=瀵嗙爜
+鐧诲綍=鐧诲綍
+绯荤粺宸ュ叿=绯荤粺宸ュ叿
+鎺ㄨ崘鎿嶄綔=鎺ㄨ崘鎿嶄綔
+鍏朵粬宸ュ叿=鍏朵粬宸ュ叿
+鑾峰彇璇锋眰鐮�=鑾峰彇璇锋眰鐮�
+涓�閿縺娲�=涓�閿縺娲�
+鑾峰彇椤圭洰鍚嶇О=鑾峰彇椤圭洰鍚嶇О
+鑾峰彇绯荤粺閰嶇疆=鑾峰彇绯荤粺閰嶇疆
+褰曞叆璁稿彲璇�=褰曞叆璁稿彲璇�
+宸插鍒跺埌鍓创鏉�=宸插鍒跺埌鍓创鏉�
+澶嶅埗澶辫触=澶嶅埗澶辫触
+鑾峰彇璇锋眰鐮佸け璐�=鑾峰彇璇锋眰鐮佸け璐�
+鑾峰彇绯荤粺閰嶇疆淇℃伅澶辫触=鑾峰彇绯荤粺閰嶇疆淇℃伅澶辫触
+璁稿彲璇佸唴瀹逛笉鑳戒负绌�=璁稿彲璇佸唴瀹逛笉鑳戒负绌�
+璁稿彲璇佹洿鏂版垚鍔�=璁稿彲璇佹洿鏂版垚鍔�
+璁稿彲璇佹洿鏂板け璐�=璁稿彲璇佹洿鏂板け璐�
+璁稿彲璇佸綍鍏ュけ璐�=璁稿彲璇佸綍鍏ュけ璐�
+婵�娲绘垚鍔�=婵�娲绘垚鍔�
+婵�娲诲け璐�=婵�娲诲け璐�
+鑾峰彇椤圭洰鍚嶇О澶辫触=鑾峰彇椤圭洰鍚嶇О澶辫触
+璇疯緭鍏ヨ处鍙�=璇疯緭鍏ヨ处鍙�
+璇疯緭鍏ュ瘑鐮�=璇疯緭鍏ュ瘑鐮�
+鎼滅储鑿滃崟=鎼滅储鑿滃崟
+娌℃湁鍖归厤鑿滃崟=娌℃湁鍖归厤鑿滃崟
+褰撳墠璐﹀彿娌℃湁鍙敤鑿滃崟=褰撳墠璐﹀彿娌℃湁鍙敤鑿滃崟
+涓存椂璁稿彲璇佹湁鏁堟湡锛�=涓存椂璁稿彲璇佹湁鏁堟湡锛�
+浠跨湡杩愯涓�=浠跨湡杩愯涓�
+浠跨湡鏈繍琛�=浠跨湡鏈繍琛�
+鍩烘湰璧勬枡=鍩烘湰璧勬枡
+閫�鍑虹櫥褰�=閫�鍑虹櫥褰�
+鍏抽棴鍏朵粬椤电=鍏抽棴鍏朵粬椤电
+杩斿洖鎺у埗涓績=杩斿洖鎺у埗涓績
+璁稿彲璇佸嵆灏嗚繃鏈�=璁稿彲璇佸嵆灏嗚繃鏈�
+鐭ラ亾浜�=鐭ラ亾浜�
+鎺у埗涓績=鎺у埗涓績
+瀹炴椂鐩戞帶=瀹炴椂鐩戞帶
+璐︽埛涓績=璐︽埛涓績
+绠$悊鍛�=绠$悊鍛�
+姝e湪鍔犺浇椤甸潰...=姝e湪鍔犺浇椤甸潰...
+AI鍔╂墜=AI鍔╂墜
+纭畾瑕佸仠姝豢鐪熸ā鎷熷悧锛�=纭畾瑕佸仠姝豢鐪熸ā鎷熷悧锛�
+纭畾瑕佸惎鍔ㄤ豢鐪熸ā鎷熷悧锛�=纭畾瑕佸惎鍔ㄤ豢鐪熸ā鎷熷悧锛�
+浠跨湡妯℃嫙宸插仠姝�=浠跨湡妯℃嫙宸插仠姝�
+浠跨湡妯℃嫙宸插惎鍔�=浠跨湡妯℃嫙宸插惎鍔�
+鎿嶄綔澶辫触=鎿嶄綔澶辫触
+鑿滃崟鍔犺浇澶辫触=鑿滃崟鍔犺浇澶辫触
+鑿滃崟鍔犺浇澶辫触锛岃妫�鏌ユ帴鍙g姸鎬�=鑿滃崟鍔犺浇澶辫触锛岃妫�鏌ユ帴鍙g姸鎬�
+宸ヤ綔椤甸潰=宸ヤ綔椤甸潰
+涓氬姟椤甸潰=涓氬姟椤甸潰
+缂栧彿=缂栧彿
+璧峰鏃堕棿 - 缁堟鏃堕棿=璧峰鏃堕棿 - 缁堟鏃堕棿
+璇疯緭鍏�=璇疯緭鍏�
+璇疯緭鍏�...=璇疯緭鍏�...
+璇烽�夋嫨鏁版嵁=璇烽�夋嫨鏁版嵁
+璇烽�夋嫨瑕佸垹闄ょ殑鏁版嵁=璇烽�夋嫨瑕佸垹闄ょ殑鏁版嵁
+鏃犳暟鎹�=鏃犳暟鎹�
+宸插瓨鍦�=宸插瓨鍦�
+涓嶅彲鐢�=涓嶅彲鐢�
+鍙栨秷閫夋嫨=鍙栨秷閫夋嫨
+姝e父=姝e父
+绂佺敤=绂佺敤
+鍚敤=鍚敤
+鍐荤粨=鍐荤粨
+鍒犻櫎=鍒犻櫎
+涓�绾ц彍鍗�=涓�绾ц彍鍗�
+浜岀骇鑿滃崟=浜岀骇鑿滃崟
+涓夌骇鑿滃崟=涓夌骇鑿滃崟
+鏌ヨ=鏌ヨ
+閲嶇疆=閲嶇疆
+鏂板=鏂板
+缂栬緫=缂栬緫
+淇敼=淇敼
+瀵煎嚭=瀵煎嚭
+淇濆瓨=淇濆瓨
+鍙栨秷=鍙栨秷
+杩斿洖=杩斿洖
+璇︽儏=璇︽儏
+宸ヤ綔鍙�=宸ヤ綔鍙�
+WMS宸ヤ綔鍙�=WMS宸ヤ綔鍙�
+婧愬簱浣�=婧愬簱浣�
+鐩爣搴撲綅=鐩爣搴撲綅
+鍫嗗灈鏈�=鍫嗗灈鏈�
+鍙屽伐浣嶅爢鍨涙満=鍙屽伐浣嶅爢鍨涙満
+鍖哄煙缂栫爜=鍖哄煙缂栫爜
+鏈懡鍚嶉〉闈�=鏈懡鍚嶉〉闈�
+鏈懡鍚嶅垎缁�=鏈懡鍚嶅垎缁�
diff --git a/src/main/resources/i18n/zh-CN/messages.properties b/src/main/resources/i18n/zh-CN/messages.properties
new file mode 100644
index 0000000..7fa647a
--- /dev/null
+++ b/src/main/resources/i18n/zh-CN/messages.properties
@@ -0,0 +1,176 @@
+lang.zh-CN=绠�浣撲腑鏂�
+lang.en-US=鑻辨枃
+app.title=娴欐睙涓壃 - 鑷姩鍖栫珛浣撲粨搴� - WCS
+app.company=娴欐睙涓壃绔嬪簱鎶�鏈湁闄愬叕鍙�
+common.loadingPage=姝e湪鍔犺浇椤甸潰...
+common.loadingTab=姝e湪鍔犺浇 鈥渰0}鈥� ...
+common.refreshingTab=姝e湪鍒锋柊 鈥渰0}鈥� ...
+common.ok=濂界殑
+common.prompt=鎻愮ず
+common.language=璇█
+common.profile=鍩烘湰璧勬枡
+common.logout=閫�鍑虹櫥褰�
+common.closeOtherTabs=鍏抽棴鍏朵粬椤电
+common.backHome=杩斿洖鎺у埗涓績
+common.aiAssistant=AI鍔╂墜
+common.workPage=宸ヤ綔椤甸潰
+common.businessPage=涓氬姟椤甸潰
+login.title=WCS绯荤粺V3.0
+login.username=璐﹀彿
+login.password=瀵嗙爜
+login.submit=鐧诲綍
+login.tools.title=绯荤粺宸ュ叿
+login.tools.recommended=鎺ㄨ崘鎿嶄綔
+login.tools.recommendedDesc=浼樺厛浣跨敤鈥滆幏鍙栬姹傜爜鈥濆拰鈥滀竴閿縺娲烩�濆畬鎴愯鍙瘉鐢宠涓庢縺娲汇��
+login.tools.others=鍏朵粬宸ュ叿
+login.tools.requestCode=鑾峰彇璇锋眰鐮�
+login.tools.activate=涓�閿縺娲�
+login.tools.projectName=鑾峰彇椤圭洰鍚嶇О
+login.tools.serverInfo=鑾峰彇绯荤粺閰嶇疆
+login.tools.uploadLicense=褰曞叆璁稿彲璇�
+login.dialog.copy=澶嶅埗
+login.dialog.copied=宸插鍒跺埌鍓创鏉�
+login.dialog.copyFailed=澶嶅埗澶辫触
+login.requestCode.title=鑾峰彇璇锋眰鐮�
+login.requestCode.label=璇锋眰鐮�
+login.requestCode.tip=璇锋眰鐮佷腑宸插寘鍚」鐩悕绉帮紝鐩存帴鍙戠粰璁稿彲璇佹湇鍔$鍗冲彲銆�
+login.serverInfo.title=鑾峰彇绯荤粺閰嶇疆
+login.serverInfo.label=绯荤粺閰嶇疆淇℃伅
+login.serverInfo.tip=鑰侀」鐩粛鍙户缁娇鐢ㄨ繖浠界‖浠朵俊鎭� JSON 鐢宠璁稿彲璇併��
+index.searchMenu=鎼滅储鑿滃崟
+index.noMatchedMenu=娌℃湁鍖归厤鑿滃崟
+index.noAvailableMenu=褰撳墠璐﹀彿娌℃湁鍙敤鑿滃崟
+index.licenseDays=涓存椂璁稿彲璇佹湁鏁堟湡锛歿0}澶�
+index.fakeRunning=浠跨湡杩愯涓�
+index.fakeStopped=浠跨湡鏈繍琛�
+index.licenseExpiring=璁稿彲璇佸嵆灏嗚繃鏈�
+index.homeTab=鎺у埗涓績
+index.homeGroup=瀹炴椂鐩戞帶
+index.profileGroup=璐︽埛涓績
+index.versionLoading=鐗堟湰鍔犺浇涓�...
+index.licenseExpireAt=璁稿彲璇佸皢浜� {0} 杩囨湡锛屽墿浣欐湁鏁堟湡锛歿1} 澶┿��
+index.confirmStopFake=纭畾瑕佸仠姝豢鐪熸ā鎷熷悧锛�
+index.confirmStartFake=纭畾瑕佸惎鍔ㄤ豢鐪熸ā鎷熷悧锛�
+index.fakeStoppedSuccess=浠跨湡妯℃嫙宸插仠姝�
+index.fakeStartedSuccess=浠跨湡妯℃嫙宸插惎鍔�
+index.operationFailed=鎿嶄綔澶辫触
+index.menuLoadFailed=鑿滃崟鍔犺浇澶辫触
+index.menuLoadFailedDetail=鑿滃崟鍔犺浇澶辫触锛岃妫�鏌ユ帴鍙g姸鎬�
+response.user.notFound=璐﹀彿涓嶅瓨鍦�
+response.user.disabled=璐﹀彿宸茶绂佺敤
+response.user.passwordMismatch=瀵嗙爜閿欒
+response.system.licenseExpired=璁稿彲璇佸凡澶辨晥
+response.common.systemError=绯荤粺寮傚父锛岃绋嶅悗閲嶈瘯
+response.common.methodNotAllowed=璇锋眰鏂瑰紡涓嶆敮鎸�
+resource.index=鎺у埗涓績
+resource.system=绯荤粺绠$悊
+resource.set=绯荤粺閰嶇疆
+resource.merchant=瀹㈡埛绠$悊
+resource.develop=寮�鍙戜笓鐢�
+resource.stock=搴撳瓨绠$悊
+resource.logReport=鏃ュ織鎶ヨ〃
+resource.ioWork=鍏ュ嚭搴撲綔涓�
+resource.workFlow=浣滀笟娴佺▼
+resource.base=鍩虹璧勬枡
+resource.erp=ERP瀵规帴
+resource.sensor=鎰熺煡璁惧
+resource.ai.llm_config=AI閰嶇疆
+resource.notifyReport.notifyReport=閫氱煡涓婃姤
+resource.view=鏌ョ湅
+permission.function=鎸囧畾鍔熻兘
+el.colorpicker.confirm=纭畾
+el.colorpicker.clear=娓呯┖
+el.datepicker.now=姝ゅ埢
+el.datepicker.today=浠婂ぉ
+el.datepicker.cancel=鍙栨秷
+el.datepicker.clear=娓呯┖
+el.datepicker.confirm=纭畾
+el.datepicker.selectDate=閫夋嫨鏃ユ湡
+el.datepicker.selectTime=閫夋嫨鏃堕棿
+el.datepicker.startDate=寮�濮嬫棩鏈�
+el.datepicker.startTime=寮�濮嬫椂闂�
+el.datepicker.endDate=缁撴潫鏃ユ湡
+el.datepicker.endTime=缁撴潫鏃堕棿
+el.datepicker.prevYear=鍓嶄竴骞�
+el.datepicker.nextYear=鍚庝竴骞�
+el.datepicker.prevMonth=涓婁釜鏈�
+el.datepicker.nextMonth=涓嬩釜鏈�
+el.datepicker.year=骞�
+el.datepicker.month1=1 鏈�
+el.datepicker.month2=2 鏈�
+el.datepicker.month3=3 鏈�
+el.datepicker.month4=4 鏈�
+el.datepicker.month5=5 鏈�
+el.datepicker.month6=6 鏈�
+el.datepicker.month7=7 鏈�
+el.datepicker.month8=8 鏈�
+el.datepicker.month9=9 鏈�
+el.datepicker.month10=10 鏈�
+el.datepicker.month11=11 鏈�
+el.datepicker.month12=12 鏈�
+el.datepicker.weeks.sun=鏃�
+el.datepicker.weeks.mon=涓�
+el.datepicker.weeks.tue=浜�
+el.datepicker.weeks.wed=涓�
+el.datepicker.weeks.thu=鍥�
+el.datepicker.weeks.fri=浜�
+el.datepicker.weeks.sat=鍏�
+el.datepicker.months.jan=涓�鏈�
+el.datepicker.months.feb=浜屾湀
+el.datepicker.months.mar=涓夋湀
+el.datepicker.months.apr=鍥涙湀
+el.datepicker.months.may=浜旀湀
+el.datepicker.months.jun=鍏湀
+el.datepicker.months.jul=涓冩湀
+el.datepicker.months.aug=鍏湀
+el.datepicker.months.sep=涔濇湀
+el.datepicker.months.oct=鍗佹湀
+el.datepicker.months.nov=鍗佷竴鏈�
+el.datepicker.months.dec=鍗佷簩鏈�
+el.select.loading=鍔犺浇涓�
+el.select.noMatch=鏃犲尮閰嶆暟鎹�
+el.select.noData=鏃犳暟鎹�
+el.select.placeholder=璇烽�夋嫨
+el.cascader.noMatch=鏃犲尮閰嶆暟鎹�
+el.cascader.loading=鍔犺浇涓�
+el.cascader.placeholder=璇烽�夋嫨
+el.cascader.noData=鏆傛棤鏁版嵁
+el.pagination.goto=鍓嶅線
+el.pagination.pagesize=鏉�/椤�
+el.pagination.total=鍏� {total} 鏉�
+el.pagination.pageClassifier=椤�
+el.messagebox.title=鎻愮ず
+el.messagebox.confirm=纭畾
+el.messagebox.cancel=鍙栨秷
+el.messagebox.error=杈撳叆鐨勬暟鎹笉鍚堟硶
+el.upload.deleteTip=鎸� delete 閿彲鍒犻櫎
+el.upload.delete=鍒犻櫎
+el.upload.preview=鏌ョ湅鍥剧墖
+el.upload.continue=缁х画涓婁紶
+legacy.regex.loopStatus=鍦坽0} | 绔欑偣: {1} | 浠诲姟: {2} | 鎵胯浇: {3}
+legacy.regex.stationDeviceLink=鐐瑰嚮鍒犻櫎鍏宠仈: 绔欑偣 {0} -> 璁惧 {1}
+deviceLogs.visualizationPrefix=鏃ュ織鍙鍖� - 
+deviceLogs.downloadDialogTitle=鏂囦欢涓嬭浇涓�
+llm.logsTitle=LLM璋冪敤鏃ュ織
+llm.logDetailTitle=鏃ュ織璇︽儏
+llm.logDetailPrefix=鏃ュ織璇︽儏 - 
+el.table.emptyText=鏆傛棤鏁版嵁
+el.table.confirmFilter=绛涢�夊垪
+el.table.resetFilter=閲嶇疆
+el.table.clearFilter=鍏ㄩ儴
+el.table.sumText=鍚堣
+el.table.sort.ascending=鍗囧簭
+el.table.sort.descending=闄嶅簭
+el.tree.emptyText=鏆傛棤鏁版嵁
+el.transfer.noMatch=鏃犲尮閰嶆暟鎹�
+el.transfer.noData=鏃犳暟鎹�
+el.transfer.titles.0=鍒楄〃 1
+el.transfer.titles.1=鍒楄〃 2
+el.transfer.filterPlaceholder=璇疯緭鍏ユ悳绱㈠唴瀹�
+el.transfer.noCheckedFormat=鍏� 0 椤�
+el.transfer.hasCheckedFormat=宸查�� {checked}/{total} 椤�
+el.image.error=鍔犺浇澶辫触
+el.pageHeader.title=杩斿洖
+el.popconfirm.confirmButtonText=纭畾
+el.popconfirm.cancelButtonText=鍙栨秷
+el.empty.description=鏆傛棤鏁版嵁
diff --git a/src/main/webapp/static/js/common.js b/src/main/webapp/static/js/common.js
index e361ccd..277ebc8 100644
--- a/src/main/webapp/static/js/common.js
+++ b/src/main/webapp/static/js/common.js
@@ -1,5 +1,937 @@
 var baseUrl = "/wcs";
 
+var WCS_I18N = (function (window, document) {
+    "use strict";
+
+    var STORAGE_KEY = "wcs_lang";
+    var COOKIE_KEY = "wcs_lang";
+    var I18N_PATH = "/i18n/messages";
+    var TRANSLATABLE_RE = /[\u3400-\u9fff]/;
+    var state = {
+        ready: false,
+        loading: false,
+        locale: null,
+        defaultLocale: "zh-CN",
+        supportedLocales: ["zh-CN", "en-US"],
+        localeOptions: [],
+        messages: {},
+        legacy: {},
+        legacyEntries: [],
+        legacyRegexEntries: [],
+        builtinRegexEntries: [],
+        translateCache: {},
+        translateCacheSize: 0,
+        callbacks: [],
+        observer: null,
+        bridgeAttempts: 0,
+        applying: false,
+        pendingNodes: [],
+        flushTimer: 0
+    };
+
+    patchXmlHttpRequest();
+    patchFetch();
+    scheduleBridgeWrap();
+
+    if (document && document.documentElement) {
+        document.documentElement.lang = getLocale();
+    }
+
+    if (document.readyState === "loading") {
+        document.addEventListener("DOMContentLoaded", function () {
+            load();
+        });
+    } else {
+        load();
+    }
+
+    function normalizeLocale(locale) {
+        var value = (locale || "").replace(/_/g, "-").trim();
+        if (!value) {
+            return state.defaultLocale;
+        }
+        if (/^en/i.test(value)) {
+            return "en-US";
+        }
+        if (/^zh/i.test(value)) {
+            return "zh-CN";
+        }
+        return value;
+    }
+
+    function getBasePath() {
+        return typeof baseUrl === "string" && baseUrl ? baseUrl : "";
+    }
+
+    function isExternalUrl(url) {
+        return /^(?:[a-z]+:)?\/\//i.test(url || "");
+    }
+
+    function appendNonceUrl(url) {
+        var value = (url || "").trim();
+        if (!value || isExternalUrl(value) || value.charAt(0) === "<") {
+            return url;
+        }
+        if (/[?&]_wcsI18nNonce=/.test(value)) {
+            return value;
+        }
+        return value + (value.indexOf("?") === -1 ? "?" : "&") + "_wcsI18nNonce=" + new Date().getTime();
+    }
+
+    function getCookie(name) {
+        var pattern = new RegExp("(?:^|; )" + name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + "=([^;]*)");
+        var matches = document.cookie.match(pattern);
+        return matches ? decodeURIComponent(matches[1]) : "";
+    }
+
+    function setCookie(name, value) {
+        var path = getBasePath() || "/";
+        document.cookie = name + "=" + encodeURIComponent(value) + "; path=" + path + "; max-age=31536000";
+    }
+
+    function getStoredLocale() {
+        var locale = "";
+        try {
+            locale = window.localStorage ? window.localStorage.getItem(STORAGE_KEY) : "";
+        } catch (e) {
+            locale = "";
+        }
+        if (!locale) {
+            locale = getCookie(COOKIE_KEY);
+        }
+        if (!locale && window.navigator) {
+            locale = window.navigator.language || (window.navigator.languages && window.navigator.languages[0]) || "";
+        }
+        return normalizeLocale(locale || state.defaultLocale);
+    }
+
+    function setStoredLocale(locale) {
+        var normalized = normalizeLocale(locale);
+        try {
+            if (window.localStorage) {
+                window.localStorage.setItem(STORAGE_KEY, normalized);
+            }
+        } catch (e) {
+        }
+        setCookie(COOKIE_KEY, normalized);
+    }
+
+    function getLocale() {
+        if (!state.locale) {
+            state.locale = getStoredLocale();
+        }
+        return state.locale;
+    }
+
+    function setLocale(locale, reload) {
+        var normalized = normalizeLocale(locale);
+        state.locale = normalized;
+        setStoredLocale(normalized);
+        if (document && document.documentElement) {
+            document.documentElement.lang = normalized;
+        }
+        if (reload === false) {
+            state.ready = false;
+            load();
+            return;
+        }
+        window.location.reload();
+    }
+
+    function onReady(callback) {
+        if (typeof callback !== "function") {
+            return;
+        }
+        if (state.ready) {
+            callback(api);
+            return;
+        }
+        state.callbacks.push(callback);
+    }
+
+    function flushCallbacks() {
+        var queue = state.callbacks.slice();
+        var i;
+        state.callbacks = [];
+        for (i = 0; i < queue.length; i++) {
+            try {
+                queue[i](api);
+            } catch (e) {
+            }
+        }
+    }
+
+    function parseJson(text) {
+        try {
+            return JSON.parse(text);
+        } catch (e) {
+            return null;
+        }
+    }
+
+    function request(url, success, failure) {
+        var xhr = new XMLHttpRequest();
+        xhr.open("GET", url, true);
+        xhr.setRequestHeader("X-Lang", getLocale());
+        xhr.onreadystatechange = function () {
+            if (xhr.readyState !== 4) {
+                return;
+            }
+            if (xhr.status >= 200 && xhr.status < 300) {
+                success(parseJson(xhr.responseText) || {});
+            } else if (typeof failure === "function") {
+                failure(xhr);
+            }
+        };
+        xhr.send(null);
+    }
+
+    function load(callback) {
+        if (typeof callback === "function") {
+            onReady(callback);
+        }
+        if (state.loading || state.ready) {
+            return;
+        }
+        state.loading = true;
+        request(getBasePath() + I18N_PATH + "?lang=" + encodeURIComponent(getLocale()) + "&_t=" + new Date().getTime(), function (res) {
+            var data = res && res.code === 200 ? (res.data || {}) : {};
+            state.loading = false;
+            state.ready = true;
+            state.locale = normalizeLocale(data.locale || getLocale());
+            state.defaultLocale = normalizeLocale(data.defaultLocale || state.defaultLocale);
+            state.supportedLocales = readLocaleTags(data.supportedLocales);
+            state.localeOptions = readLocaleOptions(data.supportedLocales);
+            state.messages = data.messages || {};
+            state.legacy = data.legacy || {};
+            state.legacyRegexEntries = readLegacyRegexEntries(state.legacy);
+            state.legacyEntries = sortedLegacyEntries(state.legacy);
+            state.builtinRegexEntries = buildBuiltinRegexEntries();
+            resetTranslateCache();
+            if (document && document.documentElement) {
+                document.documentElement.lang = state.locale;
+            }
+            wrapLibraries();
+            observeDom();
+            apply(document.body || document.documentElement);
+            if (document.title) {
+                document.title = translate(document.title);
+            }
+            flushCallbacks();
+        }, function () {
+            state.loading = false;
+            state.ready = true;
+            state.locale = getLocale();
+            state.builtinRegexEntries = buildBuiltinRegexEntries();
+            resetTranslateCache();
+            wrapLibraries();
+            flushCallbacks();
+        });
+    }
+
+    function readLocaleTags(options) {
+        var tags = [];
+        var i;
+        if (!options || !options.length) {
+            return state.supportedLocales.slice();
+        }
+        for (i = 0; i < options.length; i++) {
+            if (typeof options[i] === "string") {
+                tags.push(normalizeLocale(options[i]));
+            } else if (options[i] && options[i].tag) {
+                tags.push(normalizeLocale(options[i].tag));
+            }
+        }
+        return tags.length ? tags : state.supportedLocales.slice();
+    }
+
+    function readLocaleOptions(options) {
+        var list = [];
+        var i;
+        if (!options || !options.length) {
+            options = state.supportedLocales.slice();
+        }
+        for (i = 0; i < options.length; i++) {
+            if (typeof options[i] === "string") {
+                list.push({ tag: normalizeLocale(options[i]), label: options[i] });
+            } else if (options[i] && options[i].tag) {
+                list.push({
+                    tag: normalizeLocale(options[i].tag),
+                    label: options[i].label || options[i].tag
+                });
+            }
+        }
+        return list;
+    }
+
+    function sortedLegacyEntries(map) {
+        var entries = [];
+        var key;
+        for (key in map) {
+            if (map.hasOwnProperty(key) && key.indexOf("regex:") !== 0) {
+                entries.push([key, map[key]]);
+            }
+        }
+        entries.sort(function (left, right) {
+            return right[0].length - left[0].length;
+        });
+        return entries;
+    }
+
+    function readLegacyRegexEntries(map) {
+        var entries = [];
+        var key;
+        var source;
+        for (key in map) {
+            if (!map.hasOwnProperty(key) || key.indexOf("regex:") !== 0) {
+                continue;
+            }
+            source = key.substring(6);
+            if (!source) {
+                continue;
+            }
+            if (isBrokenRegexEntry(source, map[key])) {
+                continue;
+            }
+            try {
+                entries.push({
+                    source: source,
+                    regex: new RegExp(source),
+                    value: map[key]
+                });
+            } catch (e) {
+            }
+        }
+        entries.sort(function (left, right) {
+            return right.source.length - left.source.length;
+        });
+        return entries;
+    }
+
+    function isBrokenRegexEntry(source, value) {
+        if (!source || !value) {
+            return false;
+        }
+        if (value.indexOf("$=") >= 0) {
+            return true;
+        }
+        return /\\s\*|\\d\+|\\\||\\:|\(\?:/.test(value);
+    }
+
+    function format(template, params) {
+        var result = template || "";
+        var index;
+        if (!params) {
+            return result;
+        }
+        if (Object.prototype.toString.call(params) === "[object Array]") {
+            for (index = 0; index < params.length; index++) {
+                result = result.replace(new RegExp("\\{" + index + "\\}", "g"), params[index]);
+            }
+            return result;
+        }
+        for (index in params) {
+            if (params.hasOwnProperty(index)) {
+                result = result.replace(new RegExp("\\{" + index + "\\}", "g"), params[index]);
+            }
+        }
+        return result;
+    }
+
+    function resetTranslateCache() {
+        state.translateCache = {};
+        state.translateCacheSize = 0;
+    }
+
+    function rememberTranslateCache(text, translated) {
+        if (state.translateCacheSize > 4000) {
+            resetTranslateCache();
+        }
+        if (!Object.prototype.hasOwnProperty.call(state.translateCache, text)) {
+            state.translateCacheSize += 1;
+        }
+        state.translateCache[text] = translated;
+        return translated;
+    }
+
+    function canTranslateText(text) {
+        return !!(text && TRANSLATABLE_RE.test(text));
+    }
+
+    function buildBuiltinRegexEntries() {
+        return [
+            {
+                regex: /^鍦�(\d+)\s*\|\s*绔欑偣:\s*(\d+)\s*\|\s*(?:浠诲姟|Task):\s*(\d+)\s*\|\s*鎵胯浇:\s*([\d.]+%)$/,
+                key: "legacy.regex.loopStatus",
+                fallback: "Zone {0} | Stations: {1} | Tasks: {2} | Load: {3}"
+            },
+            {
+                regex: /^鐐瑰嚮(?:鍒犻櫎|Delete)鍏宠仈:\s*绔欑偣\s*(\d+)\s*->\s*璁惧\s*(\d+)$/,
+                key: "legacy.regex.stationDeviceLink",
+                fallback: "Click to remove mapping: Station {0} -> Device {1}"
+            }
+        ];
+    }
+
+    function formatBuiltinRegex(entry, args) {
+        var value = t(entry.key, args);
+        if (value === entry.key && entry.fallback) {
+            return format(entry.fallback, args);
+        }
+        return value;
+    }
+
+    function t(key, params) {
+        var value = state.messages[key];
+        if (!value) {
+            return key;
+        }
+        return format(value, params);
+    }
+
+    function preserveSpaces(original, translated) {
+        var match = original.match(/^(\s*)([\s\S]*?)(\s*)$/);
+        if (!match) {
+            return translated;
+        }
+        return match[1] + translated + match[3];
+    }
+
+    function exactLegacy(text) {
+        var trimmed = (text || "").trim();
+        var translated = state.legacy[trimmed];
+        if (!translated) {
+            return text;
+        }
+        return preserveSpaces(text, translated);
+    }
+
+    function fragmentLegacy(text) {
+        var result = text;
+        var i;
+        var entry;
+        for (i = 0; i < state.legacyEntries.length; i++) {
+            entry = state.legacyEntries[i];
+            if (!entry[0] || entry[0].length < 2 || entry[0] === entry[1]) {
+                continue;
+            }
+            result = result.split(entry[0]).join(entry[1]);
+        }
+        return result;
+    }
+
+    function regexLegacy(text) {
+        var trimmed = (text || "").trim();
+        var i;
+        var entry;
+        var translated;
+        for (i = 0; i < state.legacyRegexEntries.length; i++) {
+            entry = state.legacyRegexEntries[i];
+            if (!entry || !entry.regex) {
+                continue;
+            }
+            if (trimmed) {
+                translated = trimmed.replace(entry.regex, entry.value);
+                if (translated !== trimmed) {
+                    return preserveSpaces(text, translated);
+                }
+            }
+            translated = text.replace(entry.regex, entry.value);
+            if (translated !== text) {
+                return translated;
+            }
+        }
+        for (i = 0; i < state.builtinRegexEntries.length; i++) {
+            entry = state.builtinRegexEntries[i];
+            if (!entry || !entry.regex) {
+                continue;
+            }
+            if (trimmed) {
+                translated = trimmed.replace(entry.regex, function () {
+                    return formatBuiltinRegex(entry, Array.prototype.slice.call(arguments, 1, -2));
+                });
+                if (translated !== trimmed) {
+                    return preserveSpaces(text, translated);
+                }
+            }
+            translated = text.replace(entry.regex, function () {
+                return formatBuiltinRegex(entry, Array.prototype.slice.call(arguments, 1, -2));
+            });
+            if (translated !== text) {
+                return translated;
+            }
+        }
+        return text;
+    }
+
+    function translate(text) {
+        if (!text || !state.ready || state.locale === state.defaultLocale) {
+            return text;
+        }
+        if (Object.prototype.hasOwnProperty.call(state.translateCache, text)) {
+            return state.translateCache[text];
+        }
+        if (!canTranslateText(text)) {
+            return rememberTranslateCache(text, text);
+        }
+        var exact = exactLegacy(text);
+        if (exact !== text) {
+            return rememberTranslateCache(text, exact);
+        }
+        var regex = regexLegacy(text);
+        if (regex !== text) {
+            return rememberTranslateCache(text, regex);
+        }
+        return rememberTranslateCache(text, fragmentLegacy(text));
+    }
+
+    function translateAttribute(element, attrName) {
+        var value = element.getAttribute(attrName);
+        var translated = translate(value);
+        if (translated && translated !== value) {
+            element.setAttribute(attrName, translated);
+        }
+    }
+
+    function applyI18nKeys(element) {
+        var textKey = element.getAttribute("data-i18n-key");
+        var placeholderKey = element.getAttribute("data-i18n-placeholder-key");
+        var titleKey = element.getAttribute("data-i18n-title-key");
+        var params = element.getAttribute("data-i18n-params");
+        var parsedParams = params ? parseJson(params) : null;
+        if (textKey) {
+            if (/^(INPUT|TEXTAREA)$/i.test(element.tagName)) {
+                if (element.type === "button" || element.type === "submit" || element.type === "reset") {
+                    var buttonText = t(textKey, parsedParams);
+                    if (element.value !== buttonText) {
+                        element.value = buttonText;
+                    }
+                }
+            } else {
+                var textValue = t(textKey, parsedParams);
+                if (element.textContent !== textValue) {
+                    element.textContent = textValue;
+                }
+            }
+        }
+        if (placeholderKey) {
+            var placeholderValue = t(placeholderKey, parsedParams);
+            if (element.getAttribute("placeholder") !== placeholderValue) {
+                element.setAttribute("placeholder", placeholderValue);
+            }
+        }
+        if (titleKey) {
+            var titleValue = t(titleKey, parsedParams);
+            if (element.getAttribute("title") !== titleValue) {
+                element.setAttribute("title", titleValue);
+            }
+        }
+    }
+
+    function shouldSkipElement(element) {
+        if (!element || element.nodeType !== 1) {
+            return true;
+        }
+        return /^(SCRIPT|STYLE|TEXTAREA|CODE|PRE)$/i.test(element.tagName);
+    }
+
+    function traverse(node) {
+        var child;
+        if (!node) {
+            return;
+        }
+        if (node.nodeType === 3) {
+            var translatedText = translate(node.nodeValue);
+            if (translatedText !== node.nodeValue) {
+                node.nodeValue = translatedText;
+            }
+            return;
+        }
+        if (node.nodeType !== 1 || shouldSkipElement(node)) {
+            return;
+        }
+        applyI18nKeys(node);
+        translateAttribute(node, "placeholder");
+        translateAttribute(node, "title");
+        translateAttribute(node, "aria-label");
+        translateAttribute(node, "alt");
+        if (/^(INPUT)$/i.test(node.tagName)) {
+            if (/^(button|submit|reset)$/i.test(node.type || "")) {
+                if (node.value) {
+                    var translatedValue = translate(node.value);
+                    if (translatedValue !== node.value) {
+                        node.value = translatedValue;
+                    }
+                }
+            } else if ((node.readOnly || node.disabled || node.getAttribute("readonly") !== null) && node.value) {
+                var translatedReadonlyValue = translate(node.value);
+                if (translatedReadonlyValue !== node.value) {
+                    node.value = translatedReadonlyValue;
+                }
+            }
+        }
+        child = node.firstChild;
+        while (child) {
+            traverse(child);
+            child = child.nextSibling;
+        }
+    }
+
+    function apply(root) {
+        if (!root || !state.ready) {
+            return;
+        }
+        state.applying = true;
+        try {
+            traverse(root);
+        } finally {
+            state.applying = false;
+        }
+    }
+
+    function scheduleFlush() {
+        if (state.flushTimer) {
+            return;
+        }
+        var scheduler = window.requestAnimationFrame || function (callback) {
+            return window.setTimeout(callback, 16);
+        };
+        state.flushTimer = scheduler(flushPendingNodes);
+    }
+
+    function enqueueNode(node) {
+        if (!node || node.__wcsI18nQueued) {
+            return;
+        }
+        node.__wcsI18nQueued = true;
+        state.pendingNodes.push(node);
+        scheduleFlush();
+    }
+
+    function flushPendingNodes() {
+        var queue;
+        var i;
+        state.flushTimer = 0;
+        if (!state.pendingNodes.length) {
+            return;
+        }
+        if (state.applying) {
+            scheduleFlush();
+            return;
+        }
+        queue = state.pendingNodes.slice();
+        state.pendingNodes = [];
+        state.applying = true;
+        try {
+            for (i = 0; i < queue.length; i++) {
+                if (!queue[i]) {
+                    continue;
+                }
+                queue[i].__wcsI18nQueued = false;
+                if (queue[i].nodeType === 3) {
+                    var translatedText = translate(queue[i].nodeValue);
+                    if (translatedText !== queue[i].nodeValue) {
+                        queue[i].nodeValue = translatedText;
+                    }
+                } else if (queue[i].nodeType === 1) {
+                    traverse(queue[i]);
+                }
+            }
+        } finally {
+            state.applying = false;
+        }
+        if (state.pendingNodes.length) {
+            scheduleFlush();
+        }
+    }
+
+    function observeDom() {
+        if (state.observer || !window.MutationObserver || !document || !document.documentElement) {
+            return;
+        }
+        state.observer = new MutationObserver(function (mutations) {
+            var i;
+            if (state.applying) {
+                return;
+            }
+            for (i = 0; i < mutations.length; i++) {
+                if (mutations[i].type === "childList") {
+                    for (var j = 0; j < mutations[i].addedNodes.length; j++) {
+                        enqueueNode(mutations[i].addedNodes[j]);
+                    }
+                } else if (mutations[i].type === "characterData" && mutations[i].target) {
+                    enqueueNode(mutations[i].target);
+                } else if (mutations[i].type === "attributes" && mutations[i].target) {
+                    enqueueNode(mutations[i].target);
+                }
+            }
+        });
+        state.observer.observe(document.documentElement, {
+            childList: true,
+            subtree: true,
+            characterData: true,
+            attributes: true,
+            attributeFilter: ["placeholder", "title", "aria-label", "alt"]
+        });
+    }
+
+    function wrapLayer() {
+        if (!window.layer || window.layer.__wcsI18nWrapped) {
+            return;
+        }
+        var layer = window.layer;
+        var rawMsg = layer.msg;
+        var rawAlert = layer.alert;
+        var rawConfirm = layer.confirm;
+        var rawTips = layer.tips;
+        var rawOpen = layer.open;
+
+        if (typeof rawMsg === "function") {
+            layer.msg = function (content, options, end) {
+                return rawMsg.call(layer, translate(content), options, end);
+            };
+        }
+        if (typeof rawAlert === "function") {
+            layer.alert = function (content, options, yes) {
+                if (options && typeof options === "object" && typeof options.title === "string") {
+                    options.title = translate(options.title);
+                }
+                return rawAlert.call(layer, translate(content), options, yes);
+            };
+        }
+        if (typeof rawConfirm === "function") {
+            layer.confirm = function (content, options, yes, cancel) {
+                if (typeof options === "string") {
+                    options = translate(options);
+                } else if (options && typeof options === "object") {
+                    if (typeof options.title === "string") {
+                        options.title = translate(options.title);
+                    } else if (!options.title) {
+                        options.title = translate("淇℃伅");
+                    }
+                    if (Object.prototype.toString.call(options.btn) === "[object Array]") {
+                        for (var i = 0; i < options.btn.length; i++) {
+                            options.btn[i] = translate(options.btn[i]);
+                        }
+                    }
+                }
+                return rawConfirm.call(layer, translate(content), options, yes, cancel);
+            };
+        }
+        if (typeof rawTips === "function") {
+            layer.tips = function (content, follow, options) {
+                return rawTips.call(layer, translate(content), follow, options);
+            };
+        }
+        if (typeof rawOpen === "function") {
+            layer.open = function (options) {
+                if (options && typeof options === "object") {
+                    if (typeof options.title === "string") {
+                        options.title = translate(options.title);
+                    } else if (Object.prototype.toString.call(options.title) === "[object Array]" && typeof options.title[0] === "string") {
+                        options.title[0] = translate(options.title[0]);
+                    }
+                    if (typeof options.content === "string") {
+                        if (options.type === 2) {
+                            options.content = appendNonceUrl(options.content);
+                        } else {
+                            options.content = translate(options.content);
+                        }
+                    }
+                    if (Object.prototype.toString.call(options.btn) === "[object Array]") {
+                        for (var i = 0; i < options.btn.length; i++) {
+                            options.btn[i] = translate(options.btn[i]);
+                        }
+                    }
+                }
+                return rawOpen.call(layer, options);
+            };
+        }
+        window.layer.__wcsI18nWrapped = true;
+    }
+
+    function wrapElement() {
+        if (!window.ELEMENT || window.ELEMENT.__wcsI18nWrapped) {
+            return;
+        }
+        var element = window.ELEMENT;
+        var methods = ["success", "warning", "info", "error"];
+        var i;
+        if (typeof element.i18n === "function") {
+            element.i18n(function (key, value) {
+                return t(key, value);
+            });
+        }
+        if (typeof element.Message === "function") {
+            var rawMessage = element.Message;
+            element.Message = function (options) {
+                if (typeof options === "string") {
+                    return rawMessage.call(element, translate(options));
+                }
+                if (options && typeof options === "object" && typeof options.message === "string") {
+                    options.message = translate(options.message);
+                }
+                return rawMessage.call(element, options);
+            };
+            for (i = 0; i < methods.length; i++) {
+                if (typeof rawMessage[methods[i]] === "function") {
+                    (function (methodName) {
+                        element.Message[methodName] = function (options) {
+                            if (typeof options === "string") {
+                                return rawMessage[methodName].call(rawMessage, translate(options));
+                            }
+                            if (options && typeof options === "object" && typeof options.message === "string") {
+                                options.message = translate(options.message);
+                            }
+                            return rawMessage[methodName].call(rawMessage, options);
+                        };
+                    })(methods[i]);
+                }
+            }
+        }
+        if (element.MessageBox) {
+            wrapMessageBoxMethod(element.MessageBox, "alert");
+            wrapMessageBoxMethod(element.MessageBox, "confirm");
+            wrapMessageBoxMethod(element.MessageBox, "prompt");
+        }
+        window.ELEMENT.__wcsI18nWrapped = true;
+    }
+
+    function wrapMessageBoxMethod(box, methodName) {
+        if (typeof box[methodName] !== "function") {
+            return;
+        }
+        var raw = box[methodName];
+        box[methodName] = function (message, title, options) {
+            if (typeof message === "string") {
+                message = translate(message);
+            }
+            if (typeof title === "string") {
+                title = translate(title);
+            } else if (title && typeof title === "object" && typeof title.message === "string") {
+                title.message = translate(title.message);
+            }
+            if (options && typeof options === "object") {
+                if (typeof options.confirmButtonText === "string") {
+                    options.confirmButtonText = translate(options.confirmButtonText);
+                }
+                if (typeof options.cancelButtonText === "string") {
+                    options.cancelButtonText = translate(options.cancelButtonText);
+                }
+                if (typeof options.title === "string") {
+                    options.title = translate(options.title);
+                }
+            }
+            return raw.call(box, message, title, options);
+        };
+    }
+
+    function wrapLibraries() {
+        wrapLayer();
+        wrapElement();
+    }
+
+    function scheduleBridgeWrap() {
+        var timer = window.setInterval(function () {
+            state.bridgeAttempts += 1;
+            wrapLibraries();
+            if ((window.layer || !window.ELEMENT) && state.bridgeAttempts > 20) {
+                window.clearInterval(timer);
+            }
+        }, 300);
+    }
+
+    function patchXmlHttpRequest() {
+        if (!window.XMLHttpRequest || window.XMLHttpRequest.__wcsI18nPatched) {
+            return;
+        }
+        var rawOpen = window.XMLHttpRequest.prototype.open;
+        var rawSend = window.XMLHttpRequest.prototype.send;
+        var rawSetHeader = window.XMLHttpRequest.prototype.setRequestHeader;
+
+        window.XMLHttpRequest.prototype.open = function (method, url) {
+            this.__wcsI18nUrl = url;
+            this.__wcsI18nHeaderSet = false;
+            return rawOpen.apply(this, arguments);
+        };
+        window.XMLHttpRequest.prototype.setRequestHeader = function (name, value) {
+            if (String(name).toLowerCase() === "x-lang") {
+                this.__wcsI18nHeaderSet = true;
+            }
+            return rawSetHeader.apply(this, arguments);
+        };
+        window.XMLHttpRequest.prototype.send = function () {
+            try {
+                if (!this.__wcsI18nHeaderSet && shouldAttachLangHeader(this.__wcsI18nUrl)) {
+                    rawSetHeader.call(this, "X-Lang", getLocale());
+                    this.__wcsI18nHeaderSet = true;
+                }
+            } catch (e) {
+            }
+            return rawSend.apply(this, arguments);
+        };
+        window.XMLHttpRequest.__wcsI18nPatched = true;
+    }
+
+    function patchFetch() {
+        if (!window.fetch || window.fetch.__wcsI18nPatched) {
+            return;
+        }
+        var rawFetch = window.fetch;
+        window.fetch = function (input, init) {
+            var requestUrl = typeof input === "string" ? input : (input && input.url);
+            init = init || {};
+            if (shouldAttachLangHeader(requestUrl)) {
+                init.headers = init.headers || {};
+                if (typeof Headers !== "undefined" && init.headers instanceof Headers) {
+                    if (!init.headers.has("X-Lang")) {
+                        init.headers.set("X-Lang", getLocale());
+                    }
+                } else if (!init.headers["X-Lang"] && !init.headers["x-lang"]) {
+                    init.headers["X-Lang"] = getLocale();
+                }
+            }
+            return rawFetch.call(window, input, init);
+        };
+        window.fetch.__wcsI18nPatched = true;
+    }
+
+    function shouldAttachLangHeader(url) {
+        if (!url) {
+            return true;
+        }
+        if (typeof url !== "string") {
+            return true;
+        }
+        return url.indexOf("http://") !== 0 && url.indexOf("https://") !== 0 || url.indexOf(window.location.origin) === 0;
+    }
+
+    var api = {
+        load: load,
+        onReady: onReady,
+        t: t,
+        tl: translate,
+        apply: apply,
+        getLocale: getLocale,
+        setLocale: setLocale,
+        getLocaleOptions: function () {
+            return state.localeOptions.slice();
+        },
+        getSupportedLocales: function () {
+            return state.supportedLocales.slice();
+        },
+        getDefaultLocale: function () {
+            return state.defaultLocale;
+        },
+        addNonceUrl: appendNonceUrl,
+        isReady: function () {
+            return state.ready;
+        }
+    };
+
+    return api;
+})(window, document);
+
 // 璧嬪��
 function setVal(el, val) {
     if (el.text() !== val){
diff --git a/src/main/webapp/static/js/deviceLogs/deviceLogs.js b/src/main/webapp/static/js/deviceLogs/deviceLogs.js
index 9d44d8f..f28a6d7 100644
--- a/src/main/webapp/static/js/deviceLogs/deviceLogs.js
+++ b/src/main/webapp/static/js/deviceLogs/deviceLogs.js
@@ -56,7 +56,10 @@
             return this.deviceList;
         },
         visualizationTitle() {
-            return `鏃ュ織鍙鍖� - ${this.visDeviceType} ${this.visDeviceNo} (${this.searchForm.day})`;
+            return this.i18n('deviceLogs.visualizationPrefix', '鏃ュ織鍙鍖� - ') + this.visDeviceType + ' ' + this.visDeviceNo + ' (' + this.searchForm.day + ')';
+        },
+        downloadDialogTitle() {
+            return this.i18n('deviceLogs.downloadDialogTitle', '鏂囦欢涓嬭浇涓�');
         },
         maxSliderValue() {
             return Math.max(0, this.endTime - this.startTime);
@@ -81,7 +84,24 @@
         this.loadDeviceEnums();
         this.loadDateTree();
     },
+    mounted() {
+        if (window.WCS_I18N && typeof window.WCS_I18N.onReady === 'function') {
+            let that = this;
+            window.WCS_I18N.onReady(function () {
+                that.$forceUpdate();
+            });
+        }
+    },
     methods: {
+        i18n(key, fallback, params) {
+            if (window.WCS_I18N && typeof window.WCS_I18N.t === 'function') {
+                var translated = window.WCS_I18N.t(key, params);
+                if (translated && translated !== key) {
+                    return translated;
+                }
+            }
+            return fallback || key;
+        },
         // --- Initialization ---
         loadDeviceEnums() {
             let that = this;
@@ -842,4 +862,4 @@
             }
         }
     }
-});
\ No newline at end of file
+});
diff --git a/src/main/webapp/views/ai/llm_config.html b/src/main/webapp/views/ai/llm_config.html
index c80314c..8d91b18 100644
--- a/src/main/webapp/views/ai/llm_config.html
+++ b/src/main/webapp/views/ai/llm_config.html
@@ -309,7 +309,11 @@
       <div class="route-card" :class="routeCardClass(route)" v-for="(route, idx) in routes" :key="route.id ? ('route_' + route.id) : ('new_' + idx)">
         <div class="route-head">
           <div class="route-title">
-            <el-input v-model="route.name" size="mini" placeholder="璺敱鍚嶇О"></el-input>
+            <el-input
+              :value="displayRouteName(route)"
+              size="mini"
+              placeholder="璺敱鍚嶇О"
+              @input="handleRouteNameInput(route, $event)"></el-input>
             <div class="route-id-line">#{{ route.id || 'new' }} 路 浼樺厛绾� {{ route.priority || 0 }}</div>
           </div>
           <div class="route-state">
@@ -393,7 +397,7 @@
     </div>
   </div>
 
-  <el-dialog title="LLM璋冪敤鏃ュ織" :visible.sync="logDialogVisible" width="88%" :close-on-click-modal="false">
+  <el-dialog :title="i18n('llm.logsTitle', 'LLM璋冪敤鏃ュ織')" :visible.sync="logDialogVisible" width="88%" :close-on-click-modal="false">
     <div class="log-toolbar">
       <el-select v-model="logQuery.scene" size="mini" clearable placeholder="鍦烘櫙" style="width:180px;">
         <el-option label="chat" value="chat"></el-option>
@@ -457,7 +461,7 @@
     </div>
   </el-dialog>
 
-  <el-dialog :title="logDetailTitle || '鏃ュ織璇︽儏'" :visible.sync="logDetailVisible" width="82%" :close-on-click-modal="false" append-to-body>
+  <el-dialog :title="logDetailTitle || i18n('llm.logDetailTitle', '鏃ュ織璇︽儏')" :visible.sync="logDetailVisible" width="82%" :close-on-click-modal="false" append-to-body>
     <div class="log-detail-body">{{ logDetailText || '-' }}</div>
     <span slot="footer" class="dialog-footer">
       <el-button size="mini" @click="copyText(logDetailText)">澶嶅埗鍏ㄦ枃</el-button>
@@ -511,6 +515,31 @@
       }
     },
     methods: {
+      i18n: function(key, fallback, params) {
+        if (window.WCS_I18N && typeof window.WCS_I18N.t === 'function') {
+          var translated = window.WCS_I18N.t(key, params);
+          if (translated && translated !== key) {
+            return translated;
+          }
+        }
+        return fallback || key;
+      },
+      translateLegacyText: function(text) {
+        if (window.WCS_I18N && typeof window.WCS_I18N.tl === 'function') {
+          return window.WCS_I18N.tl(text);
+        }
+        return text;
+      },
+      displayRouteName: function(route) {
+        var value = route && route.name ? String(route.name) : '';
+        return this.translateLegacyText(value);
+      },
+      handleRouteNameInput: function(route, value) {
+        if (!route) {
+          return;
+        }
+        route.name = value;
+      },
       formatDateTime: function(input) {
         if (!input) return '-';
         var d = input instanceof Date ? input : new Date(input);
@@ -801,7 +830,7 @@
           + '閿欒: ' + (row.errorMessage || '-') + '\n\n'
           + '璇锋眰:\n' + (row.requestContent || '-') + '\n\n'
           + '鍝嶅簲:\n' + (row.responseContent || '-');
-        this.logDetailTitle = '鏃ュ織璇︽儏 - ' + (row.traceId || row.id || '');
+        this.logDetailTitle = this.i18n('llm.logDetailPrefix', '鏃ュ織璇︽儏 - ') + (row.traceId || row.id || '');
         this.logDetailText = text;
         this.logDetailVisible = true;
       },
@@ -1006,6 +1035,12 @@
       }
     },
     mounted: function() {
+      var self = this;
+      if (window.WCS_I18N && typeof window.WCS_I18N.onReady === 'function') {
+        window.WCS_I18N.onReady(function() {
+          self.$forceUpdate();
+        });
+      }
       this.loadRoutes();
     }
   });
diff --git a/src/main/webapp/views/deviceLogs/deviceLogs.html b/src/main/webapp/views/deviceLogs/deviceLogs.html
index ed5c392..2af2915 100644
--- a/src/main/webapp/views/deviceLogs/deviceLogs.html
+++ b/src/main/webapp/views/deviceLogs/deviceLogs.html
@@ -192,7 +192,7 @@
     </el-dialog>
 
     <!-- Download Progress Dialog -->
-    <el-dialog title="鏂囦欢涓嬭浇涓�" :visible.sync="downloadDialogVisible" width="400px" :close-on-click-modal="false" :show-close="false">
+    <el-dialog :title="downloadDialogTitle" :visible.sync="downloadDialogVisible" width="400px" :close-on-click-modal="false" :show-close="false">
         <div style="padding: 10px;">
             <div style="margin-bottom: 5px; font-size: 14px;">鍘嬬缉鐢熸垚杩涘害</div>
             <el-progress :percentage="buildProgress" :text-inside="true" :stroke-width="18"></el-progress>
@@ -211,6 +211,6 @@
 <script src="../../components/WatchRgvCard.js"></script>
 <script src="../../components/WatchDualCrnCard.js"></script>
 <script src="../../components/DevpCard.js"></script>
-<script type="text/javascript" src="../../static/js/deviceLogs/deviceLogs.js" charset="utf-8"></script>
+<script type="text/javascript" src="../../static/js/deviceLogs/deviceLogs.js?v=20260309_i18n_pagefix1" charset="utf-8"></script>
 </body>
 </html>
diff --git a/src/main/webapp/views/index.html b/src/main/webapp/views/index.html
index 37d2110..fad5958 100644
--- a/src/main/webapp/views/index.html
+++ b/src/main/webapp/views/index.html
@@ -340,6 +340,10 @@
       flex-shrink: 0;
     }
 
+    .lang-select {
+      width: 160px;
+    }
+
     .header-right .el-tag {
       border-radius: 999px;
     }
@@ -540,7 +544,7 @@
             size="small"
             clearable
             prefix-icon="el-icon-search"
-            placeholder="鎼滅储鑿滃崟">
+            :placeholder="t('index.searchMenu')">
         </el-input>
       </div>
 
@@ -586,14 +590,14 @@
           <div class="aside-empty" v-if="filteredMenus.length === 0">
             <el-empty
                 :image-size="80"
-                :description="menuKeyword ? '娌℃湁鍖归厤鑿滃崟' : '褰撳墠璐﹀彿娌℃湁鍙敤鑿滃崟'">
+                :description="menuKeyword ? t('index.noMatchedMenu') : t('index.noAvailableMenu')">
             </el-empty>
           </div>
         </template>
       </el-scrollbar>
 
       <div class="aside-footer" v-show="!isCollapse">
-        <div class="aside-footer-copy">漏 2026 娴欐睙涓壃绔嬪簱鎶�鏈湁闄愬叕鍙�</div>
+        <div class="aside-footer-copy">漏 2026 {{ t('app.company') }}</div>
         <div class="aside-footer-version">
           <span class="aside-footer-version-text">{{ versionText }}</span>
           <el-tag
@@ -627,7 +631,7 @@
               size="mini"
               :type="licenseTagType"
               effect="dark">
-            涓存椂璁稿彲璇佹湁鏁堟湡锛歿{ licenseDays }}澶�
+            {{ t('index.licenseDays', [licenseDays]) }}
           </el-tag>
           <el-tag
               v-if="fakeVisible"
@@ -636,8 +640,20 @@
               :type="fakeRunning ? 'danger' : 'info'"
               effect="dark"
               @click.native="toggleFakeSystem">
-            {{ fakeRunning ? '浠跨湡杩愯涓�' : '浠跨湡鏈繍琛�' }}
+            {{ fakeRunning ? t('index.fakeRunning') : t('index.fakeStopped') }}
           </el-tag>
+          <el-select
+              v-model="currentLocale"
+              size="mini"
+              class="lang-select"
+              @change="handleLocaleChange">
+            <el-option
+                v-for="item in localeOptions"
+                :key="item.tag"
+                :label="item.label"
+                :value="item.tag">
+            </el-option>
+          </el-select>
           <el-button circle size="mini" icon="el-icon-refresh" @click="refreshActiveTab"></el-button>
           <el-button circle size="mini" icon="el-icon-full-screen" @click="toggleFullScreen"></el-button>
 
@@ -648,8 +664,8 @@
               <i class="el-icon-arrow-down"></i>
             </span>
             <el-dropdown-menu slot="dropdown">
-              <el-dropdown-item command="profile">鍩烘湰璧勬枡</el-dropdown-item>
-              <el-dropdown-item command="logout" divided>閫�鍑虹櫥褰�</el-dropdown-item>
+              <el-dropdown-item command="profile">{{ t('common.profile') }}</el-dropdown-item>
+              <el-dropdown-item command="logout" divided>{{ t('common.logout') }}</el-dropdown-item>
             </el-dropdown-menu>
           </el-dropdown>
         </div>
@@ -671,10 +687,10 @@
         </el-tabs>
 
         <div class="tabs-tools">
-          <el-tooltip content="鍏抽棴鍏朵粬椤电" placement="top">
+          <el-tooltip :content="t('common.closeOtherTabs')" placement="top">
             <el-button size="mini" icon="el-icon-close" @click="closeOtherTabs"></el-button>
           </el-tooltip>
-          <el-tooltip content="杩斿洖鎺у埗涓績" placement="top">
+          <el-tooltip :content="t('common.backHome')" placement="top">
             <el-button size="mini" type="primary" icon="el-icon-house" @click="openHomeTab"></el-button>
           </el-tooltip>
         </div>
@@ -691,6 +707,7 @@
               v-for="tab in tabs"
               :key="'frame-' + tab.name"
               class="page-frame"
+              :data-tab-name="tab.name"
               v-show="activeTab === tab.name"
               :src="tab.currentSrc"
               @load="handleFrameLoad(tab.name)">
@@ -701,13 +718,13 @@
   </el-container>
 
   <el-dialog
-      title="璁稿彲璇佸嵆灏嗚繃鏈�"
+      :title="t('index.licenseExpiring')"
       :visible.sync="licenseDialogVisible"
       width="420px"
       :close-on-click-modal="false">
     <div class="license-dialog-text">{{ licenseDialogText }}</div>
     <span slot="footer">
-      <el-button type="primary" @click="licenseDialogVisible = false">鐭ラ亾浜�</el-button>
+      <el-button type="primary" @click="licenseDialogVisible = false">{{ t('common.ok') }}</el-button>
     </span>
   </el-dialog>
 
@@ -742,6 +759,41 @@
   };
   var TAB_STORAGE_KEY = "wcs-element-home-tabs";
   var USER_STORAGE_KEY = "username";
+  var INDEX_I18N_FALLBACKS = {
+    "app.title": "娴欐睙涓壃 - 鑷姩鍖栫珛浣撲粨搴� - WCS",
+    "app.company": "娴欐睙涓壃绔嬪簱鎶�鏈湁闄愬叕鍙�",
+    "common.loadingPage": "姝e湪鍔犺浇椤甸潰...",
+    "common.loadingTab": "姝e湪鍔犺浇 鈥渰0}鈥� ...",
+    "common.refreshingTab": "姝e湪鍒锋柊 鈥渰0}鈥� ...",
+    "common.ok": "鐭ラ亾浜�",
+    "common.prompt": "鎻愮ず",
+    "common.profile": "鍩烘湰璧勬枡",
+    "common.logout": "閫�鍑虹櫥褰�",
+    "common.closeOtherTabs": "鍏抽棴鍏朵粬椤电",
+    "common.backHome": "杩斿洖鎺у埗涓績",
+    "common.aiAssistant": "AI鍔╂墜",
+    "common.workPage": "宸ヤ綔椤甸潰",
+    "common.businessPage": "涓氬姟椤甸潰",
+    "index.searchMenu": "鎼滅储鑿滃崟",
+    "index.noMatchedMenu": "娌℃湁鍖归厤鑿滃崟",
+    "index.noAvailableMenu": "褰撳墠璐﹀彿娌℃湁鍙敤鑿滃崟",
+    "index.licenseDays": "涓存椂璁稿彲璇佹湁鏁堟湡锛歿0}澶�",
+    "index.fakeRunning": "浠跨湡杩愯涓�",
+    "index.fakeStopped": "浠跨湡鏈繍琛�",
+    "index.licenseExpiring": "璁稿彲璇佸嵆灏嗚繃鏈�",
+    "index.homeTab": "鎺у埗涓績",
+    "index.homeGroup": "瀹炴椂鐩戞帶",
+    "index.profileGroup": "璐︽埛涓績",
+    "index.versionLoading": "Version loading...",
+    "index.licenseExpireAt": "璁稿彲璇佸皢浜� {0} 杩囨湡锛屽墿浣欐湁鏁堟湡锛歿1} 澶┿��",
+    "index.confirmStopFake": "纭畾瑕佸仠姝豢鐪熸ā鎷熷悧锛�",
+    "index.confirmStartFake": "纭畾瑕佸惎鍔ㄤ豢鐪熸ā鎷熷悧锛�",
+    "index.fakeStoppedSuccess": "浠跨湡妯℃嫙宸插仠姝�",
+    "index.fakeStartedSuccess": "浠跨湡妯℃嫙宸插惎鍔�",
+    "index.operationFailed": "鎿嶄綔澶辫触",
+    "index.menuLoadFailed": "鑿滃崟鍔犺浇澶辫触",
+    "index.menuLoadFailedDetail": "鑿滃崟鍔犺浇澶辫触锛岃妫�鏌ユ帴鍙g姸鎬�"
+  };
 
   new Vue({
     el: "#app",
@@ -750,7 +802,7 @@
         isCollapse: false,
         menuLoading: true,
         pageLoading: true,
-        loadingText: "姝e湪鍔犺浇椤甸潰...",
+        loadingText: window.WCS_I18N ? window.WCS_I18N.tl("姝e湪鍔犺浇椤甸潰...") : "姝e湪鍔犺浇椤甸潰...",
         menuKeyword: "",
         menus: [],
         defaultOpeneds: [],
@@ -762,12 +814,14 @@
         licenseDays: null,
         licenseDialogVisible: false,
         licenseDialogText: "",
+        currentLocale: window.WCS_I18N ? window.WCS_I18N.getLocale() : "zh-CN",
+        localeOptions: [],
         fakeVisible: false,
         fakeRunning: false,
         fakeStatusInterval: null,
         menuSyncVersion: 0,
         menuSyncTimer: null,
-        userName: localStorage.getItem(USER_STORAGE_KEY) || "绠$悊鍛�",
+        userName: localStorage.getItem(USER_STORAGE_KEY) || (window.WCS_I18N ? window.WCS_I18N.tl("绠$悊鍛�") : "绠$悊鍛�"),
         aiLayerIndex: null,
         aiTipIndex: null
       };
@@ -819,7 +873,7 @@
         return result;
       },
       activeTabMeta: function () {
-        return this.getTabByName(this.activeTab) || this.createTab(HOME_TAB_CONFIG);
+        return this.getTabByName(this.activeTab) || this.createTab(this.resolveHomeConfig());
       },
       activeTabTitle: function () {
         return this.activeTabMeta.title;
@@ -829,7 +883,7 @@
       },
       versionText: function () {
         if (!this.version) {
-          return "Version loading...";
+          return this.t("index.versionLoading");
         }
         return "Version " + this.version;
       },
@@ -861,18 +915,18 @@
         return "info";
       },
       userShortName: function () {
-        return (this.userName || "绠$悊鍛�").substring(0, 1);
+        return (this.userName || this.tl("绠$悊鍛�")).substring(0, 1);
       }
     },
     watch: {
       activeTab: function () {
         var tab = this.getTabByName(this.activeTab);
-        this.syncMenuStateByUrl(tab ? tab.url : HOME_TAB_CONFIG.url);
+        this.syncMenuStateByUrl(tab ? tab.url : this.resolveHomeConfig().url);
         this.pageLoading = !!(tab && !tab.loaded);
         if (this.pageLoading) {
-          this.loadingText = "姝e湪鍔犺浇 鈥�" + tab.title + "鈥� ...";
+          this.loadingText = this.t("common.loadingTab", [tab.title]);
         }
-        document.title = (tab ? tab.title : HOME_TAB_CONFIG.title) + " - 娴欐睙涓壃 - 鑷姩鍖栫珛浣撲粨搴� - WCS";
+        this.updateDocumentTitle(tab ? tab.title : this.resolveHomeConfig().title);
         this.persistTabs();
       }
     },
@@ -883,6 +937,7 @@
       }
 
       this.restoreTabs();
+      this.bindI18n();
       this.installCompatBridge();
       this.loadSystemVersion();
       this.loadMenu();
@@ -892,7 +947,7 @@
     },
     mounted: function () {
       $("#ai-assistant-btn").html(getAiIconHtml(60, 60));
-      document.title = this.activeTabTitle + " - 娴欐睙涓壃 - 鑷姩鍖栫珛浣撲粨搴� - WCS";
+      this.updateDocumentTitle(this.activeTabTitle);
     },
     beforeDestroy: function () {
       if (this.fakeStatusInterval) {
@@ -913,6 +968,98 @@
       }
     },
     methods: {
+      t: function (key, params) {
+        var value = window.WCS_I18N ? window.WCS_I18N.t(key, params) : key;
+        if (value !== key) {
+          return value;
+        }
+        value = INDEX_I18N_FALLBACKS[key] || key;
+        if (!params) {
+          return value;
+        }
+        if (Object.prototype.toString.call(params) === "[object Array]") {
+          for (var i = 0; i < params.length; i++) {
+            value = value.replace(new RegExp("\\{" + i + "\\}", "g"), params[i]);
+          }
+          return value;
+        }
+        return value;
+      },
+      tl: function (text) {
+        return window.WCS_I18N ? window.WCS_I18N.tl(text) : text;
+      },
+      bindI18n: function () {
+        var that = this;
+        if (!window.WCS_I18N) {
+          return;
+        }
+        window.WCS_I18N.onReady(function (i18n) {
+          that.currentLocale = i18n.getLocale();
+          that.localeOptions = i18n.getLocaleOptions();
+          that.refreshI18nState();
+        });
+      },
+      refreshI18nState: function () {
+        var that = this;
+        var homeConfig = this.resolveHomeConfig();
+        var profileConfig = this.resolveProfileConfig();
+        var i;
+        HOME_TAB_CONFIG.title = homeConfig.title;
+        HOME_TAB_CONFIG.group = homeConfig.group;
+        PROFILE_TAB_CONFIG.title = profileConfig.title;
+        PROFILE_TAB_CONFIG.group = profileConfig.group;
+        for (i = 0; i < this.tabs.length; i++) {
+          if (this.isHomeTabUrl(this.tabs[i].url)) {
+            this.tabs[i].title = homeConfig.title;
+            this.tabs[i].group = homeConfig.group;
+            this.tabs[i].home = true;
+          } else if (this.resolveViewSrc(this.tabs[i].url) === this.resolveViewSrc(profileConfig.url)) {
+            this.tabs[i].title = profileConfig.title;
+            this.tabs[i].group = profileConfig.group;
+          } else {
+            this.tabs[i].title = this.translateTabTitle(this.tabs[i].title);
+            this.tabs[i].group = this.tl(this.tabs[i].group);
+          }
+        }
+        this.updateDocumentTitle(this.activeTabTitle);
+        this.persistTabs();
+        this.updateLicenseDialogText();
+        this.$nextTick(function () {
+          if (window.WCS_I18N) {
+            window.WCS_I18N.apply(document.body);
+          }
+          that.syncAllFramesI18n();
+        });
+      },
+      handleLocaleChange: function (locale) {
+        if (window.WCS_I18N) {
+          window.WCS_I18N.setLocale(locale);
+        }
+      },
+      resolveHomeConfig: function () {
+        return {
+          title: this.t("index.homeTab"),
+          url: HOME_TAB_CONFIG.url,
+          home: true,
+          group: this.t("index.homeGroup"),
+          menuKey: HOME_TAB_CONFIG.menuKey || ""
+        };
+      },
+      resolveProfileConfig: function () {
+        return {
+          title: this.t("common.profile"),
+          url: PROFILE_TAB_CONFIG.url,
+          home: false,
+          group: this.t("index.profileGroup"),
+          menuKey: PROFILE_TAB_CONFIG.menuKey || ""
+        };
+      },
+      translateTabTitle: function (title) {
+        return this.tl(title);
+      },
+      updateDocumentTitle: function (title) {
+        document.title = title + " - " + this.t("app.title");
+      },
       resolveMenuIcon: function (code) {
         var iconMap = {
           index: "el-icon-s-home",
@@ -935,7 +1082,7 @@
           title: config.title,
           name: config.url,
           url: config.url,
-          currentSrc: config.url,
+          currentSrc: this.addNonce(config.url),
           home: !!config.home,
           group: config.group || "",
           menuKey: config.menuKey || "",
@@ -944,17 +1091,17 @@
       },
       normalizeStoredTab: function (tab) {
         var created = this.createTab({
-          title: tab.title,
+          title: this.translateTabTitle(tab.title),
           url: this.resolveViewSrc(tab.url),
           home: !!tab.home,
-          group: tab.group || "",
+          group: this.tl(tab.group || ""),
           menuKey: tab.menuKey || ""
         });
         created.loaded = false;
         return created;
       },
       restoreTabs: function () {
-        var homeTab = this.createTab(HOME_TAB_CONFIG);
+        var homeTab = this.createTab(this.resolveHomeConfig());
         var raw = localStorage.getItem(TAB_STORAGE_KEY);
         var parsed;
         var tabs = [];
@@ -996,9 +1143,9 @@
 
         this.tabs = tabs;
         this.activeTab = this.hasTab(active) ? active : homeTab.name;
-        this.loadingText = "姝e湪鍔犺浇 鈥�" + this.activeTabTitle + "鈥� ...";
+        this.loadingText = this.t("common.loadingTab", [this.activeTabTitle]);
         this.pageLoading = true;
-        document.title = this.activeTabTitle + " - 娴欐睙涓壃 - 鑷姩鍖栫珛浣撲粨搴� - WCS";
+        this.updateDocumentTitle(this.activeTabTitle);
       },
       persistTabs: function () {
         var tabs = [];
@@ -1021,7 +1168,8 @@
         return !!this.getTabByName(name);
       },
       isHomeTabUrl: function (url) {
-        return this.resolveViewSrc(url || HOME_TAB_CONFIG.url) === this.resolveViewSrc(HOME_TAB_CONFIG.url);
+        var homeUrl = this.resolveHomeConfig().url;
+        return this.resolveViewSrc(url || homeUrl) === this.resolveViewSrc(homeUrl);
       },
       getTabByName: function (name) {
         var i;
@@ -1054,28 +1202,29 @@
           }
         }
 
-        this.loadingText = "姝e湪鍔犺浇 鈥�" + tab.title + "鈥� ...";
+        this.loadingText = this.t("common.loadingTab", [tab.title]);
         this.pageLoading = !tab.loaded;
         this.activeTab = tab.name;
         this.syncMenuStateByUrl(tab.url);
       },
       openHomeTab: function () {
-        this.addOrActivateTab(HOME_TAB_CONFIG);
+        this.addOrActivateTab(this.resolveHomeConfig());
       },
       openProfileTab: function () {
-        this.addOrActivateTab(PROFILE_TAB_CONFIG);
+        this.addOrActivateTab(this.resolveProfileConfig());
       },
       closeAllTabs: function () {
-        this.tabs = [this.createTab(HOME_TAB_CONFIG)];
-        this.activeTab = HOME_TAB_CONFIG.url;
-        this.syncMenuStateByUrl(HOME_TAB_CONFIG.url);
+        var homeConfig = this.resolveHomeConfig();
+        this.tabs = [this.createTab(homeConfig)];
+        this.activeTab = homeConfig.url;
+        this.syncMenuStateByUrl(homeConfig.url);
         this.pageLoading = true;
-        this.loadingText = "姝e湪鍔犺浇 鈥滄帶鍒朵腑蹇冣�� ...";
+        this.loadingText = this.t("common.loadingTab", [homeConfig.title]);
         this.persistTabs();
       },
       closeOtherTabs: function () {
         var active = this.getTabByName(this.activeTab);
-        var homeTab = this.createTab(HOME_TAB_CONFIG);
+        var homeTab = this.createTab(this.resolveHomeConfig());
         var result = [homeTab];
 
         if (active && active.name !== homeTab.name) {
@@ -1087,7 +1236,7 @@
       },
       removeTab: function (name) {
         var i;
-        var nextTabName = HOME_TAB_CONFIG.url;
+        var nextTabName = this.resolveHomeConfig().url;
 
         for (i = 0; i < this.tabs.length; i++) {
           if (this.tabs[i].name === name) {
@@ -1104,8 +1253,8 @@
         }
 
         if (this.tabs.length === 0) {
-          this.tabs.push(this.createTab(HOME_TAB_CONFIG));
-          nextTabName = HOME_TAB_CONFIG.url;
+          this.tabs.push(this.createTab(this.resolveHomeConfig()));
+          nextTabName = this.resolveHomeConfig().url;
         }
 
         this.activeTab = nextTabName;
@@ -1116,6 +1265,7 @@
         if (tab) {
           tab.loaded = true;
         }
+        this.syncFrameI18n(name);
         if (this.activeTab === name) {
           this.pageLoading = false;
         }
@@ -1127,11 +1277,84 @@
         }
         tab.loaded = false;
         tab.currentSrc = this.addNonce(tab.url);
-        this.loadingText = "姝e湪鍒锋柊 鈥�" + tab.title + "鈥� ...";
+        this.loadingText = this.t("common.refreshingTab", [tab.title]);
         this.pageLoading = true;
       },
       addNonce: function (url) {
         return url + (url.indexOf("?") === -1 ? "?" : "&") + "_t=" + new Date().getTime();
+      },
+      findFrameElement: function (name) {
+        var frames;
+        var i;
+        if (!this.$el) {
+          return null;
+        }
+        frames = this.$el.querySelectorAll("iframe[data-tab-name]");
+        for (i = 0; i < frames.length; i++) {
+          if (frames[i].getAttribute("data-tab-name") === name) {
+            return frames[i];
+          }
+        }
+        return null;
+      },
+      syncFrameI18n: function (name) {
+        var that = this;
+        var frame = this.findFrameElement(name);
+        var frameWindow;
+        var frameDocument;
+        var script;
+
+        function applyFrameI18n() {
+          try {
+            if (!frameWindow.WCS_I18N || typeof frameWindow.WCS_I18N.onReady !== "function") {
+              return;
+            }
+            frameWindow.WCS_I18N.setLocale(that.currentLocale || "zh-CN", false);
+            frameWindow.WCS_I18N.onReady(function (i18n) {
+              i18n.apply(frameDocument.body || frameDocument.documentElement);
+              if (frameDocument.title) {
+                frameDocument.title = i18n.tl(frameDocument.title);
+              }
+            });
+          } catch (e) {
+          }
+        }
+
+        if (!frame) {
+          return;
+        }
+        try {
+          frameWindow = frame.contentWindow;
+          frameDocument = frameWindow.document;
+        } catch (e) {
+          return;
+        }
+        try {
+          frameWindow.localStorage.setItem("wcs_lang", this.currentLocale || "zh-CN");
+        } catch (e) {
+        }
+        if (frameDocument && frameDocument.documentElement) {
+          frameDocument.documentElement.lang = this.currentLocale || "zh-CN";
+        }
+        if (frameWindow.WCS_I18N && typeof frameWindow.WCS_I18N.onReady === "function") {
+          applyFrameI18n();
+          return;
+        }
+        if (!frameDocument || !frameDocument.head || frameDocument.getElementById("wcs-i18n-bridge-script")) {
+          return;
+        }
+        script = frameDocument.createElement("script");
+        script.id = "wcs-i18n-bridge-script";
+        script.type = "text/javascript";
+        script.src = baseUrl + "/static/js/common.js";
+        script.onload = applyFrameI18n;
+        frameDocument.head.appendChild(script);
+      },
+      syncAllFramesI18n: function () {
+        var i;
+        for (i = 0; i < this.tabs.length; i++) {
+          this.syncFrameI18n(this.tabs[i].name);
+        }
       },
       toggleCollapse: function () {
         this.isCollapse = !this.isCollapse;
@@ -1197,7 +1420,7 @@
             item = group.subMenu[j];
             subMenu.push({
               id: item.id,
-              name: item.name || item.code || "鏈懡鍚嶉〉闈�",
+              name: item.name || item.code || this.tl("鏈懡鍚嶉〉闈�"),
               code: item.code || "",
               url: this.buildMenuSrc(item.code, item.id),
               tabKey: this.buildMenuSrc(item.code, item.id)
@@ -1206,7 +1429,7 @@
 
           result.push({
             menuId: group.menuId,
-            menu: group.menu || "鏈懡鍚嶅垎缁�",
+            menu: group.menu || this.tl("鏈懡鍚嶅垎缁�"),
             menuCode: group.menuCode || "",
             subMenu: subMenu
           });
@@ -1233,7 +1456,7 @@
       },
       resolveViewSrc: function (path) {
         if (!path) {
-          return HOME_TAB_CONFIG.url;
+          return this.resolveHomeConfig().url;
         }
         if (/^https?:\/\//.test(path) || path.indexOf(baseUrl) === 0) {
           return path;
@@ -1266,7 +1489,7 @@
         }
       },
       syncMenuStateByUrl: function (url) {
-        var targetUrl = this.resolveViewSrc(url || HOME_TAB_CONFIG.url);
+        var targetUrl = this.resolveViewSrc(url || this.resolveHomeConfig().url);
         var activeMenuKey = "";
         var groupIndex = "";
         var that = this;
@@ -1336,12 +1559,12 @@
             } else if (res.code === 403) {
               top.location.href = baseUrl + "/login";
             } else {
-              that.$message.error(res.msg || "鑿滃崟鍔犺浇澶辫触");
+              that.$message.error(res.msg || that.t("index.menuLoadFailed"));
             }
           },
           error: function () {
             that.menuLoading = false;
-            that.$message.error("鑿滃崟鍔犺浇澶辫触锛岃妫�鏌ユ帴鍙g姸鎬�");
+            that.$message.error(that.t("index.menuLoadFailedDetail"));
           }
         });
       },
@@ -1388,6 +1611,7 @@
       showPopup: function (days) {
         var currentDate;
         var expiryDate;
+        var formattedDate;
 
         if (days === "" || days === null || typeof days === "undefined") {
           this.hidePopup();
@@ -1397,9 +1621,15 @@
         currentDate = new Date();
         expiryDate = new Date();
         expiryDate.setDate(currentDate.getDate() + Number(days) + 1);
-        this.licenseDialogText = "璁稿彲璇佸皢浜� " + new Intl.DateTimeFormat("zh-CN").format(expiryDate) +
-          " 杩囨湡锛屽墿浣欐湁鏁堟湡锛�" + days + " 澶┿��";
+        formattedDate = new Intl.DateTimeFormat(this.currentLocale || "zh-CN").format(expiryDate);
+        this.licenseDialogText = this.t("index.licenseExpireAt", [formattedDate, days]);
         this.licenseDialogVisible = true;
+      },
+      updateLicenseDialogText: function () {
+        if (this.licenseDays === "" || this.licenseDays === null || typeof this.licenseDays === "undefined" || this.licenseDays > 15) {
+          return;
+        }
+        this.showPopup(this.licenseDays);
       },
       hidePopup: function () {
         this.licenseDialogVisible = false;
@@ -1447,17 +1677,17 @@
 
         if (this.fakeRunning) {
           url = baseUrl + "/openapi/stopFakeSystem";
-          text = "纭畾瑕佸仠姝豢鐪熸ā鎷熷悧锛�";
-          successMsg = "浠跨湡妯℃嫙宸插仠姝�";
+          text = this.t("index.confirmStopFake");
+          successMsg = this.t("index.fakeStoppedSuccess");
           running = false;
         } else {
           url = baseUrl + "/openapi/startFakeSystem";
-          text = "纭畾瑕佸惎鍔ㄤ豢鐪熸ā鎷熷悧锛�";
-          successMsg = "浠跨湡妯℃嫙宸插惎鍔�";
+          text = this.t("index.confirmStartFake");
+          successMsg = this.t("index.fakeStartedSuccess");
           running = true;
         }
 
-        this.$confirm(text, "鎻愮ず", {
+        this.$confirm(text, this.t("common.prompt"), {
           type: "warning"
         }).then(function () {
           $.ajax({
@@ -1469,7 +1699,7 @@
                 that.fakeRunning = running;
                 that.$message.success(successMsg);
               } else {
-                that.$message.error(res.msg || "鎿嶄綔澶辫触");
+                that.$message.error(res.msg || that.t("index.operationFailed"));
               }
             }
           });
@@ -1484,7 +1714,7 @@
       },
       showAiTip: function () {
         this.hideAiTip();
-        this.aiTipIndex = layer.tips("AI鍔╂墜", "#ai-assistant-btn", {
+        this.aiTipIndex = layer.tips(this.t("common.aiAssistant"), "#ai-assistant-btn", {
           tips: [1, "#333"],
           time: -1
         });
@@ -1565,7 +1795,7 @@
       startUserSync: function () {
         var that = this;
         this.userSyncTimer = setInterval(function () {
-          that.userName = localStorage.getItem(USER_STORAGE_KEY) || "绠$悊鍛�";
+          that.userName = localStorage.getItem(USER_STORAGE_KEY) || that.tl("绠$悊鍛�");
         }, 1000);
       },
       installCompatBridge: function () {
@@ -1588,10 +1818,10 @@
           }
           url = that.resolveViewSrc(param.menuPath);
           that.addOrActivateTab({
-            title: that.stripTags(param.menuName) || "宸ヤ綔椤甸潰",
+            title: that.stripTags(param.menuName) || that.t("common.workPage"),
             url: url,
             home: false,
-            group: "涓氬姟椤甸潰",
+            group: that.t("common.businessPage"),
             menuKey: that.findMenuKeyByUrl(url)
           });
         };
@@ -1603,10 +1833,10 @@
           }
           url = that.resolveViewSrc(param.menuPath);
           that.addOrActivateTab({
-            title: that.stripTags(param.menuName) || HOME_TAB_CONFIG.title,
+            title: that.stripTags(param.menuName) || that.resolveHomeConfig().title,
             url: url,
             home: true,
-            group: HOME_TAB_CONFIG.group,
+            group: that.resolveHomeConfig().group,
             menuKey: that.findMenuKeyByUrl(url)
           });
         };
diff --git a/src/main/webapp/views/login.html b/src/main/webapp/views/login.html
index 5094c39..6523edb 100644
--- a/src/main/webapp/views/login.html
+++ b/src/main/webapp/views/login.html
@@ -81,43 +81,62 @@
             border-radius: 4px;
             height: 52px;
         }
+        .login-lang {
+            position: fixed;
+            top: 20px;
+            right: 24px;
+            z-index: 2;
+        }
+        .login-lang select {
+            min-width: 140px;
+            height: 34px;
+            padding: 0 10px;
+            border: 1px solid #d6dbe6;
+            border-radius: 17px;
+            background: rgba(255, 255, 255, 0.92);
+            color: #3b4a5a;
+            outline: none;
+        }
     </style>
 </head>
 <body class="login-bg animsition">
 
+<div class="login-lang">
+    <select id="lang-switch" aria-label="Language"></select>
+</div>
 <div id="login-wrapper" class="animate__animated animate__bounceInDown">
     <header>
-        <h2 id="login-title" style="cursor: pointer; user-select: none;">WCS绯荤粺V3.0</h2>
+        <h2 id="login-title" data-i18n-key="login.title" style="cursor: pointer; user-select: none;">WCS绯荤粺V3.0</h2>
     </header>
     <div class="layui-form layadmin-user-login-body">
         <div class="layui-form-item">
             <label class="layui-icon layui-icon-cellphone layadmin-user-login-icon"></label>
-            <input id="mobile" class="layui-input" type="text" name="mobile" lay-verify="mobile" placeholder="璐﹀彿">
+            <input id="mobile" class="layui-input" type="text" name="mobile" lay-verify="mobile" data-i18n-placeholder-key="login.username" placeholder="璐﹀彿">
         </div>
         <div class="layui-form-item">
             <label class="layui-icon layui-icon-password layadmin-user-login-icon"></label>
-            <input id="password" class="layui-input" type="password" name="password" lay-verify="password" placeholder="瀵嗙爜">
+            <input id="password" class="layui-input" type="password" name="password" lay-verify="password" data-i18n-placeholder-key="login.password" placeholder="瀵嗙爜">
         </div>
     </div>
     <div class="layui-form-item login-submit">
-        <button id="login-button" class="layui-btn layui-btn-fluid layui-btn-normal" lay-submit="" lay-filter="login">鐧� &nbsp  &nbsp 褰�</button>
+        <button id="login-button" data-i18n-key="login.submit" class="layui-btn layui-btn-fluid layui-btn-normal" lay-submit="" lay-filter="login">鐧� &nbsp  &nbsp 褰�</button>
     </div>
 </div>
 <div id="system-tools-panel" style="display: none; padding: 20px;">
     <div style="margin-bottom: 18px;">
-        <div style="margin-bottom: 10px; color: #666; font-weight: 600;">鎺ㄨ崘鎿嶄綔</div>
+        <div data-i18n-key="login.tools.recommended" style="margin-bottom: 10px; color: #666; font-weight: 600;">鎺ㄨ崘鎿嶄綔</div>
         <div style="display: flex; flex-wrap: wrap; gap: 12px;">
-            <button class="layui-btn layui-btn-normal layui-btn-sm" id="btn-request-code">鑾峰彇璇锋眰鐮�</button>
-            <button class="layui-btn layui-btn-normal layui-btn-sm" id="btn-activate">涓�閿縺娲�</button>
+            <button data-i18n-key="login.tools.requestCode" class="layui-btn layui-btn-normal layui-btn-sm" id="btn-request-code">鑾峰彇璇锋眰鐮�</button>
+            <button data-i18n-key="login.tools.activate" class="layui-btn layui-btn-normal layui-btn-sm" id="btn-activate">涓�閿縺娲�</button>
         </div>
-        <div style="margin-top: 8px; color: #999; font-size: 12px;">浼樺厛浣跨敤鈥滆幏鍙栬姹傜爜鈥濆拰鈥滀竴閿縺娲烩�濆畬鎴愯鍙瘉鐢宠涓庢縺娲汇��</div>
+        <div data-i18n-key="login.tools.recommendedDesc" style="margin-top: 8px; color: #999; font-size: 12px;">浼樺厛浣跨敤鈥滆幏鍙栬姹傜爜鈥濆拰鈥滀竴閿縺娲烩�濆畬鎴愯鍙瘉鐢宠涓庢縺娲汇��</div>
     </div>
     <div>
-        <div style="margin-bottom: 10px; color: #666; font-weight: 600;">鍏朵粬宸ュ叿</div>
+        <div data-i18n-key="login.tools.others" style="margin-bottom: 10px; color: #666; font-weight: 600;">鍏朵粬宸ュ叿</div>
         <div style="display: flex; flex-wrap: wrap; gap: 12px;">
-            <button class="layui-btn layui-btn-primary layui-btn-sm" id="btn-project-name">鑾峰彇椤圭洰鍚嶇О</button>
-            <button class="layui-btn layui-btn-primary layui-btn-sm" id="btn-server-info">鑾峰彇绯荤粺閰嶇疆</button>
-            <button class="layui-btn layui-btn-primary layui-btn-sm" id="btn-upload-license">褰曞叆璁稿彲璇�</button>
+            <button data-i18n-key="login.tools.projectName" class="layui-btn layui-btn-primary layui-btn-sm" id="btn-project-name">鑾峰彇椤圭洰鍚嶇О</button>
+            <button data-i18n-key="login.tools.serverInfo" class="layui-btn layui-btn-primary layui-btn-sm" id="btn-server-info">鑾峰彇绯荤粺閰嶇疆</button>
+            <button data-i18n-key="login.tools.uploadLicense" class="layui-btn layui-btn-primary layui-btn-sm" id="btn-upload-license">褰曞叆璁稿彲璇�</button>
         </div>
     </div>
 </div>
@@ -132,6 +151,26 @@
             layer = layui.layer,
             $ = layui.jquery;
 
+        function initLanguageSwitch() {
+            WCS_I18N.onReady(function (i18n) {
+                var select = document.getElementById('lang-switch');
+                var options = i18n.getLocaleOptions();
+                var current = i18n.getLocale();
+                var html = '';
+                var i;
+                for (i = 0; i < options.length; i++) {
+                    html += '<option value="' + options[i].tag + '"' + (options[i].tag === current ? ' selected' : '') + '>' + options[i].label + '</option>';
+                }
+                select.innerHTML = html;
+                select.onchange = function () {
+                    i18n.setLocale(this.value);
+                };
+                document.title = i18n.t('login.title');
+            });
+        }
+
+        initLanguageSwitch();
+
         // 杩炵画鐐瑰嚮涓夋鏍囬鏄剧ず闅愯棌鍔熻兘
         var titleClickCount = 0;
         var titleClickTimer = null;
diff --git a/src/main/webapp/views/role/role.html b/src/main/webapp/views/role/role.html
index cf51367..f5cacb1 100644
--- a/src/main/webapp/views/role/role.html
+++ b/src/main/webapp/views/role/role.html
@@ -49,7 +49,7 @@
 
 <script type="text/javascript" src="../../static/js/jquery/jquery-3.3.1.min.js"></script>
 <script type="text/javascript" src="../../static/layui/layui.js" charset="utf-8"></script>
-<script type="text/javascript" src="../../static/js/common.js?v=20260307" charset="utf-8"></script>
+<script type="text/javascript" src="../../static/js/common.js" charset="utf-8"></script>
 <script type="text/javascript" src="../../static/js/cool.js" charset="utf-8"></script>
 <script type="text/javascript" src="../../static/js/role/role.js" charset="utf-8"></script>
 
diff --git a/src/main/webapp/views/role/role_detail.html b/src/main/webapp/views/role/role_detail.html
index 564c0bf..1bac401 100644
--- a/src/main/webapp/views/role/role_detail.html
+++ b/src/main/webapp/views/role/role_detail.html
@@ -75,7 +75,7 @@
 </body>
 <script type="text/javascript" src="../../static/js/jquery/jquery-3.3.1.min.js"></script>
 <script type="text/javascript" src="../../static/layui/layui.js" charset="utf-8"></script>
-<script type="text/javascript" src="../../static/js/common.js?v=20260307" charset="utf-8"></script>
+<script type="text/javascript" src="../../static/js/common.js" charset="utf-8"></script>
 <script type="text/javascript" src="../../static/js/cool.js" charset="utf-8"></script>
 <script type="text/javascript" src="../../static/js/role/role.js" charset="utf-8"></script>
 </html>
diff --git a/src/main/webapp/views/role/role_power_detail.html b/src/main/webapp/views/role/role_power_detail.html
index afecc61..2395a2a 100644
--- a/src/main/webapp/views/role/role_power_detail.html
+++ b/src/main/webapp/views/role/role_power_detail.html
@@ -31,6 +31,6 @@
 </body>
 <script type="text/javascript" src="../../static/js/jquery/jquery-3.3.1.min.js"></script>
 <script type="text/javascript" src="../../static/layui/layui.js" charset="utf-8"></script>
-<script type="text/javascript" src="../../static/js/common.js?v=20260307"></script>
+<script type="text/javascript" src="../../static/js/common.js"></script>
 <script type="text/javascript" src="../../static/js/role/rolePower.js" charset="utf-8"></script>
 </html>

--
Gitblit v1.9.1