Junjie
1 天以前 66a9fc7a0065c4b1f0d488018659da98ee8594e7
#国际化i18n
11个文件已添加
15个文件已修改
3476 ■■■■■ 已修改文件
docs/i18n-language-pack.md 134 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/common/config/AdminInterceptor.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/common/config/CoolExceptionHandler.java 9 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/common/config/WebConfig.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/common/i18n/I18nController.java 40 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/common/i18n/I18nLocaleUtils.java 114 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/common/i18n/I18nMessageService.java 406 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/common/i18n/I18nProperties.java 52 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/common/i18n/I18nResponseBodyAdvice.java 37 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/common/i18n/RequestLocaleInterceptor.java 53 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/common/web/AuthController.java 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/system/controller/ResourceController.java 16 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/application.yml 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/i18n/en-US/legacy.properties 707 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/i18n/en-US/messages.properties 176 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/i18n/zh-CN/legacy.properties 88 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/i18n/zh-CN/messages.properties 176 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/webapp/static/js/common.js 932 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/webapp/static/js/deviceLogs/deviceLogs.js 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/webapp/views/ai/llm_config.html 43 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/webapp/views/deviceLogs/deviceLogs.html 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/webapp/views/index.html 348 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/webapp/views/login.html 63 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/webapp/views/role/role.html 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/webapp/views/role/role_detail.html 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/webapp/views/role/role_power_detail.html 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/i18n-language-pack.md
New file
@@ -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. 刷新页面,或重启服务。
系统会按配置周期检查外置目录,因此简单文本调整理论上不必重启;但正式环境建议仍按发布流程重启或重载。
## 推荐维护方式
1. 新增页面和新增接口文案,优先写入 `messages.properties`。
2. 老页面先通过 `legacy.properties` 兼容,不要一开始就全量重构。
3. 某个老页面重构时,再把它的纯文本逐步迁移到显式 key。
## 说明
- 默认语言是 `zh-CN`
- 前端请求会自动携带 `X-Lang`
- 登录页和首页都已经支持语言切换
- 非默认语言如果缺少菜单 key,后端会先尝试根据 `resource.code` 自动生成可读名称,再回退到原始中文名
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", "*");
    }
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)
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("/**")
                ;
src/main/java/com/zy/common/i18n/I18nController.java
New file
@@ -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;
    }
}
src/main/java/com/zy/common/i18n/I18nLocaleUtils.java
New file
@@ -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();
    }
}
src/main/java/com/zy/common/i18n/I18nMessageService.java
New file
@@ -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;
    }
}
src/main/java/com/zy/common/i18n/I18nProperties.java
New file
@@ -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;
    }
}
src/main/java/com/zy/common/i18n/I18nResponseBodyAdvice.java
New file
@@ -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;
    }
}
src/main/java/com/zy/common/i18n/RequestLocaleInterceptor.java
New file
@@ -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");
    }
}
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) {
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);
    }
}
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
src/main/resources/i18n/en-US/legacy.properties
New file
@@ -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
正在加载页面...=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
菜单加载失败,请检查接口状态=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
正常=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
正常=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
客户端IP=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
入出类型代号=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
代号=Code
排序=Sort
筛选=Filter
当前没有可展示的堆垛机数据=No crane data available
当前没有待发送通知=No pending notifications
双伸位堆垛机=Dual-reach Crane
双伸位Crane=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
通知查看与Retry=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
最大出库Task数=Max Outbound Tasks
最大入库任务数=Max Inbound Tasks
最大入库Task数=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
初始化Station Data=Initialize Station Data
运行堵塞重新分配库位站点数据=Reassign blocked location station data
运行堵塞重新分配库位Station Data=Reassign blocked location station data
虚拟设备初始化设备状态=Virtual Device Initializes Device Status
Virtual Device初始化设备Status=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
最大求解时间(s)=Max Solve Time (s)
最大求解Time(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点最大Task数量上限=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
支持多API、多模型、多Key,额度耗尽或故障自动切换=Supports multiple APIs, models, and keys with automatic failover on quota exhaustion or errors
支持多API、多Model、多Key,额度耗尽或故障Auto切换=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.拣料/盘点/并板出库中=P. Picking/Counting/Merging Outbound
Q.拣料/盘点/并板再入库=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
查看当前待通知队列、接口发送日志,支持按任务/设备快速筛选,并对失败或待发送通知执行手动补发。=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
来源:notifyUri + notifyUriPath=Source: notifyUri + notifyUriPath
队列与日志共用同一组查询条件,切换页签时保持一致。=Queue and log tabs share the same query conditions and stay in sync when switching tabs.
通用搜索:任务号、消息描述、报文关键字、Redis Key=General search: Task No., message description, payload keyword, Redis key
当前队列显示 Redis 实时数据,发送日志显示历史接口调用结果。=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
请输入RGV号=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
故障代码=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+)号RGV$=RGV $1
15条/页=15 / page
30条/页=30 / page
50条/页=50 / page
100条/页=100 / page
200条/页=200 / page
500条/页=500 / page
Stations点绕圈Mode=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
src/main/resources/i18n/en-US/messages.properties
New file
@@ -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
src/main/resources/i18n/zh-CN/legacy.properties
New file
@@ -0,0 +1,88 @@
账号=账号
密码=密码
登录=登录
系统工具=系统工具
推荐操作=推荐操作
其他工具=其他工具
获取请求码=获取请求码
一键激活=一键激活
获取项目名称=获取项目名称
获取系统配置=获取系统配置
录入许可证=录入许可证
已复制到剪贴板=已复制到剪贴板
复制失败=复制失败
获取请求码失败=获取请求码失败
获取系统配置信息失败=获取系统配置信息失败
许可证内容不能为空=许可证内容不能为空
许可证更新成功=许可证更新成功
许可证更新失败=许可证更新失败
许可证录入失败=许可证录入失败
激活成功=激活成功
激活失败=激活失败
获取项目名称失败=获取项目名称失败
请输入账号=请输入账号
请输入密码=请输入密码
搜索菜单=搜索菜单
没有匹配菜单=没有匹配菜单
当前账号没有可用菜单=当前账号没有可用菜单
临时许可证有效期:=临时许可证有效期:
仿真运行中=仿真运行中
仿真未运行=仿真未运行
基本资料=基本资料
退出登录=退出登录
关闭其他页签=关闭其他页签
返回控制中心=返回控制中心
许可证即将过期=许可证即将过期
知道了=知道了
控制中心=控制中心
实时监控=实时监控
账户中心=账户中心
管理员=管理员
正在加载页面...=正在加载页面...
AI助手=AI助手
确定要停止仿真模拟吗?=确定要停止仿真模拟吗?
确定要启动仿真模拟吗?=确定要启动仿真模拟吗?
仿真模拟已停止=仿真模拟已停止
仿真模拟已启动=仿真模拟已启动
操作失败=操作失败
菜单加载失败=菜单加载失败
菜单加载失败,请检查接口状态=菜单加载失败,请检查接口状态
工作页面=工作页面
业务页面=业务页面
编号=编号
起始时间 - 终止时间=起始时间 - 终止时间
请输入=请输入
请输入...=请输入...
请选择数据=请选择数据
请选择要删除的数据=请选择要删除的数据
无数据=无数据
已存在=已存在
不可用=不可用
取消选择=取消选择
正常=正常
禁用=禁用
启用=启用
冻结=冻结
删除=删除
一级菜单=一级菜单
二级菜单=二级菜单
三级菜单=三级菜单
查询=查询
重置=重置
新增=新增
编辑=编辑
修改=修改
导出=导出
保存=保存
取消=取消
返回=返回
详情=详情
工作号=工作号
WMS工作号=WMS工作号
源库位=源库位
目标库位=目标库位
堆垛机=堆垛机
双工位堆垛机=双工位堆垛机
区域编码=区域编码
未命名页面=未命名页面
未命名分组=未命名分组
src/main/resources/i18n/zh-CN/messages.properties
New file
@@ -0,0 +1,176 @@
lang.zh-CN=简体中文
lang.en-US=英文
app.title=浙江中扬 - 自动化立体仓库 - WCS
app.company=浙江中扬立库技术有限公司
common.loadingPage=正在加载页面...
common.loadingTab=正在加载 “{0}” ...
common.refreshingTab=正在刷新 “{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=菜单加载失败,请检查接口状态
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=暂无数据
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){
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 @@
            }
        }
    }
});
});
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();
    }
  });
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>
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": "正在加载页面...",
    "common.loadingTab": "正在加载 “{0}” ...",
    "common.refreshingTab": "正在刷新 “{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": "菜单加载失败,请检查接口状态"
  };
  new Vue({
    el: "#app",
@@ -750,7 +802,7 @@
        isCollapse: false,
        menuLoading: true,
        pageLoading: true,
        loadingText: "正在加载页面...",
        loadingText: window.WCS_I18N ? window.WCS_I18N.tl("正在加载页面...") : "正在加载页面...",
        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 = "正在加载 “" + 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 = "正在加载 “" + 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 = "正在加载 “" + 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 = "正在加载 “控制中心” ...";
        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 = "正在刷新 “" + 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("菜单加载失败,请检查接口状态");
            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)
          });
        };
src/main/webapp/views/login.html
@@ -81,43 +81,62 @@
            border-radius: 4px;
            height: 52px;
        }
        .login-lang {
            position: fixed;
            top: 20px;
            right: 24px;
            z-index: 2;
        }
        .login-lang select {
            min-width: 140px;
            height: 34px;
            padding: 0 10px;
            border: 1px solid #d6dbe6;
            border-radius: 17px;
            background: rgba(255, 255, 255, 0.92);
            color: #3b4a5a;
            outline: none;
        }
    </style>
</head>
<body class="login-bg animsition">
<div class="login-lang">
    <select id="lang-switch" aria-label="Language"></select>
</div>
<div id="login-wrapper" class="animate__animated animate__bounceInDown">
    <header>
        <h2 id="login-title" style="cursor: pointer; user-select: none;">WCS系统V3.0</h2>
        <h2 id="login-title" data-i18n-key="login.title" style="cursor: pointer; user-select: none;">WCS系统V3.0</h2>
    </header>
    <div class="layui-form layadmin-user-login-body">
        <div class="layui-form-item">
            <label class="layui-icon layui-icon-cellphone layadmin-user-login-icon"></label>
            <input id="mobile" class="layui-input" type="text" name="mobile" lay-verify="mobile" placeholder="账号">
            <input id="mobile" class="layui-input" type="text" name="mobile" lay-verify="mobile" data-i18n-placeholder-key="login.username" placeholder="账号">
        </div>
        <div class="layui-form-item">
            <label class="layui-icon layui-icon-password layadmin-user-login-icon"></label>
            <input id="password" class="layui-input" type="password" name="password" lay-verify="password" placeholder="密码">
            <input id="password" class="layui-input" type="password" name="password" lay-verify="password" data-i18n-placeholder-key="login.password" placeholder="密码">
        </div>
    </div>
    <div class="layui-form-item login-submit">
        <button id="login-button" class="layui-btn layui-btn-fluid layui-btn-normal" lay-submit="" lay-filter="login">登 &nbsp  &nbsp 录</button>
        <button id="login-button" data-i18n-key="login.submit" class="layui-btn layui-btn-fluid layui-btn-normal" lay-submit="" lay-filter="login">登 &nbsp  &nbsp 录</button>
    </div>
</div>
<div id="system-tools-panel" style="display: none; padding: 20px;">
    <div style="margin-bottom: 18px;">
        <div style="margin-bottom: 10px; color: #666; font-weight: 600;">推荐操作</div>
        <div data-i18n-key="login.tools.recommended" style="margin-bottom: 10px; color: #666; font-weight: 600;">推荐操作</div>
        <div style="display: flex; flex-wrap: wrap; gap: 12px;">
            <button class="layui-btn layui-btn-normal layui-btn-sm" id="btn-request-code">获取请求码</button>
            <button class="layui-btn layui-btn-normal layui-btn-sm" id="btn-activate">一键激活</button>
            <button data-i18n-key="login.tools.requestCode" class="layui-btn layui-btn-normal layui-btn-sm" id="btn-request-code">获取请求码</button>
            <button data-i18n-key="login.tools.activate" class="layui-btn layui-btn-normal layui-btn-sm" id="btn-activate">一键激活</button>
        </div>
        <div style="margin-top: 8px; color: #999; font-size: 12px;">优先使用“获取请求码”和“一键激活”完成许可证申请与激活。</div>
        <div data-i18n-key="login.tools.recommendedDesc" style="margin-top: 8px; color: #999; font-size: 12px;">优先使用“获取请求码”和“一键激活”完成许可证申请与激活。</div>
    </div>
    <div>
        <div style="margin-bottom: 10px; color: #666; font-weight: 600;">其他工具</div>
        <div data-i18n-key="login.tools.others" style="margin-bottom: 10px; color: #666; font-weight: 600;">其他工具</div>
        <div style="display: flex; flex-wrap: wrap; gap: 12px;">
            <button class="layui-btn layui-btn-primary layui-btn-sm" id="btn-project-name">获取项目名称</button>
            <button class="layui-btn layui-btn-primary layui-btn-sm" id="btn-server-info">获取系统配置</button>
            <button class="layui-btn layui-btn-primary layui-btn-sm" id="btn-upload-license">录入许可证</button>
            <button data-i18n-key="login.tools.projectName" class="layui-btn layui-btn-primary layui-btn-sm" id="btn-project-name">获取项目名称</button>
            <button data-i18n-key="login.tools.serverInfo" class="layui-btn layui-btn-primary layui-btn-sm" id="btn-server-info">获取系统配置</button>
            <button data-i18n-key="login.tools.uploadLicense" class="layui-btn layui-btn-primary layui-btn-sm" id="btn-upload-license">录入许可证</button>
        </div>
    </div>
</div>
@@ -132,6 +151,26 @@
            layer = layui.layer,
            $ = layui.jquery;
        function initLanguageSwitch() {
            WCS_I18N.onReady(function (i18n) {
                var select = document.getElementById('lang-switch');
                var options = i18n.getLocaleOptions();
                var current = i18n.getLocale();
                var html = '';
                var i;
                for (i = 0; i < options.length; i++) {
                    html += '<option value="' + options[i].tag + '"' + (options[i].tag === current ? ' selected' : '') + '>' + options[i].label + '</option>';
                }
                select.innerHTML = html;
                select.onchange = function () {
                    i18n.setLocale(this.value);
                };
                document.title = i18n.t('login.title');
            });
        }
        initLanguageSwitch();
        // 连续点击三次标题显示隐藏功能
        var titleClickCount = 0;
        var titleClickTimer = null;
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>
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>
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>