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">鐧�     褰�</button>
+ <button id="login-button" data-i18n-key="login.submit" class="layui-btn layui-btn-fluid layui-btn-normal" lay-submit="" lay-filter="login">鐧�     褰�</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