# 后端泰语提示支持实施计划 > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** 为所有后端系统提示增加完整泰语资源和安全的泰语 Locale 解析,同时保持台湾繁体中文为默认语言。 **Architecture:** Spring MessageSource 继续从 `i18n/messages` 读取资源,新增 `messages_th_TH.properties` 提供完整泰语值;同步请求继续由 `lang` 参数切换 Locale,异步消息通过修复后的 `LocaleUtils.parseLocale(String)` 解析用户语言。业务数据的数字语言映射保持不变,避免把尚不存在的泰语商品或门店数据纳入本阶段。 **Tech Stack:** Java 21、Spring Boot MessageSource、Spring MVC LocaleResolver、JUnit 5、Maven、UTF-8 properties。 ## Global Constraints - 默认 Locale 必须继续是台湾繁体中文 `zh_TW`。 - 泰语标准代码为 `th_TH`,并兼容 `th-TH` 和 `th`。 - `messages_th_TH.properties` 必须与现有四个本地化语言包拥有完全一致且非空的消息 key。 - 占位符集合必须逐 key 保持一致,例如 `{0}`、`{1}` 不得遗漏。 - 不修改 `toLangCode()`、`getUserLanguageCode()`、数据库字段或业务数据语言映射。 - 不修改平台管理端、商家管理端和 App 前端语言资源。 - 现有工作区中的闪送请求 DTO/Controller 注释修改属于另一项工作,不得在泰语功能提交中丢失或误暂存。 - 按用户要求,先完成全部生产代码和测试源码,再统一运行测试与构建;中途不执行 Maven。 - Git 提交标题和正文使用中文,不添加英文 Conventional Commits 前缀。 --- ### Task 1: 定义 Locale 解析契约并修复解析器 **Files:** - Create: `ruoyi-admin/src/test/java/com/ruoyi/common/utils/LocaleUtilsTest.java` - Modify: `ruoyi-common/src/main/java/com/ruoyi/common/utils/LocaleUtils.java` **Interfaces:** - Consumes: Redis 中已保存的字符串语言代码,以及直接调用 `LocaleUtils.parseLocale(String)` 的代码。 - Produces: `public static Locale parseLocale(String language)`;支持白名单语言代码并对空值、未知值回退 `zh_TW`。 - [ ] **Step 1: 编写 Locale 白名单解析测试源码** 创建参数化测试,锁定所有受支持别名与默认回退: ```java package com.ruoyi.common.utils; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; import java.util.Locale; import static org.junit.jupiter.api.Assertions.assertEquals; class LocaleUtilsTest { @ParameterizedTest @CsvSource({ "zh_CN,zh,CN", "zh-CN,zh,CN", "zh,zh,CN", "zh_TW,zh,TW", "zh-TW,zh,TW", "tw,zh,TW", "en_US,en,US", "en-US,en,US", "en,en,US", "vi_VN,vi,VN", "vi-VN,vi,VN", "vi,vi,VN", "th_TH,th,TH", "th-TH,th,TH", "th,th,TH" }) void parsesSupportedLanguageAliases(String input, String language, String country) { assertEquals(new Locale(language, country), LocaleUtils.parseLocale(input)); } @Test void fallsBackToTraditionalChineseForMissingOrUnknownLanguage() { Locale expected = new Locale("zh", "TW"); assertEquals(expected, LocaleUtils.parseLocale(null)); assertEquals(expected, LocaleUtils.parseLocale(" ")); assertEquals(expected, LocaleUtils.parseLocale("fr-FR")); } } ``` - [ ] **Step 2: 实现白名单 Locale 解析** 保留 `DEFAULT_LOCALE`,新增明确的 Locale 常量并只修改 `parseLocale`。先 `trim()`,再把 `-` 转为 `_` 并使用 `Locale.ROOT` 转小写,最后使用 switch 返回白名单 Locale: ```java private static final Locale SIMPLIFIED_CHINESE = new Locale("zh", "CN"); private static final Locale ENGLISH = new Locale("en", "US"); private static final Locale VIETNAMESE = new Locale("vi", "VN"); private static final Locale THAI = new Locale("th", "TH"); public static Locale parseLocale(String language) { if (StringUtils.isEmpty(language) || language.trim().isEmpty()) { return DEFAULT_LOCALE; } String normalized = language.trim().replace('-', '_').toLowerCase(Locale.ROOT); return switch (normalized) { case "zh", "zh_cn" -> SIMPLIFIED_CHINESE; case "zh_tw", "tw" -> DEFAULT_LOCALE; case "en", "en_us" -> ENGLISH; case "vi", "vi_vn" -> VIETNAMESE; case "th", "th_th" -> THAI; default -> DEFAULT_LOCALE; }; } ``` 不得修改 `toLangCode()` 和 `getUserLanguageCode()`。 - [ ] **Step 3: 静态检查本任务差异,暂不运行 Maven** 运行: ```powershell git diff --check -- ruoyi-common/src/main/java/com/ruoyi/common/utils/LocaleUtils.java ruoyi-admin/src/test/java/com/ruoyi/common/utils/LocaleUtilsTest.java ``` 预期:无错误输出。按全局约束,将测试执行延后到 Task 4。 - [ ] **Step 4: 提交 Locale 解析改动** ```powershell git add -- ruoyi-common/src/main/java/com/ruoyi/common/utils/LocaleUtils.java ruoyi-admin/src/test/java/com/ruoyi/common/utils/LocaleUtilsTest.java git diff --cached --name-only git diff --cached --check git commit -m "支持后端泰语语言解析" -m "兼容 th_TH、th-TH 和 th,并为现有受支持语言建立白名单解析;空值和未知语言继续回退台湾繁体。" ``` ### Task 2: 新增完整泰语消息资源和语言包契约 **Files:** - Create: `ruoyi-admin/src/main/resources/i18n/messages_th_TH.properties` - Create: `ruoyi-admin/src/test/java/com/ruoyi/common/utils/BackendI18nBundleContractTest.java` - Modify: `ruoyi-admin/src/test/java/com/ruoyi/app/flashdelivery/service/FlashDeliveryI18nContractTest.java` **Interfaces:** - Consumes: `ResourceBundleMessageSource` 的 basename `i18n/messages` 和现有 319 个本地化消息 key。 - Produces: 完整的泰语资源包 `messages_th_TH.properties`,由 `Locale("th", "TH")` 自动加载。 - [ ] **Step 1: 编写语言包完整性和占位符契约测试源码** 创建 `BackendI18nBundleContractTest`,使用 UTF-8 Reader 加载资源;以繁体中文包作为 key 与占位符参考,逐一比较英文、简中、越南语和泰语: ```java package com.ruoyi.common.utils; import org.junit.jupiter.api.Test; import org.springframework.context.support.ResourceBundleMessageSource; import java.io.InputStream; import java.io.InputStreamReader; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.List; import java.util.Locale; import java.util.Properties; import java.util.regex.Matcher; import java.util.regex.Pattern; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; class BackendI18nBundleContractTest { private static final List LOCALIZED_BUNDLES = List.of( "messages_zh_CN.properties", "messages_zh_TW.properties", "messages_en_US.properties", "messages_vi.properties", "messages_th_TH.properties"); private static final Pattern PLACEHOLDER = Pattern.compile("\\{\\d+}"); @Test void everyLocalizedBundleHasTheSameNonBlankKeysAndPlaceholders() throws Exception { Properties reference = load("messages_zh_TW.properties"); for (String bundle : LOCALIZED_BUNDLES) { Properties candidate = load(bundle); assertEquals(reference.stringPropertyNames(), candidate.stringPropertyNames(), bundle); for (String key : reference.stringPropertyNames()) { assertFalse(candidate.getProperty(key).isBlank(), bundle + " blank " + key); assertEquals(placeholders(reference.getProperty(key)), placeholders(candidate.getProperty(key)), bundle + " placeholders " + key); } } } @Test void messageSourceLoadsThaiMessagesAndFormatsArguments() { ResourceBundleMessageSource source = new ResourceBundleMessageSource(); source.setBasename("i18n/messages"); source.setDefaultEncoding(StandardCharsets.UTF_8.name()); Locale thai = new Locale("th", "TH"); assertEquals("ราคาเสนอมีการเปลี่ยนแปลง โปรดยืนยันค่าบริการล่าสุดอีกครั้ง", source.getMessage("flash.delivery.quote.changed", null, thai)); assertEquals("ป้อนรหัสผ่านผิด 3 ครั้ง บัญชีถูกล็อก 10 นาที", source.getMessage("user.password.retry.limit.exceed", new Object[]{3, 10}, thai)); } private Properties load(String file) throws Exception { Properties properties = new Properties(); try (InputStream input = getClass().getClassLoader().getResourceAsStream("i18n/" + file)) { assertNotNull(input, file); properties.load(new InputStreamReader(input, StandardCharsets.UTF_8)); } return properties; } private List placeholders(String value) { List result = new ArrayList<>(); Matcher matcher = PLACEHOLDER.matcher(value); while (matcher.find()) result.add(matcher.group()); return result; } } ``` - [ ] **Step 2: 将泰语文件纳入闪送消息契约** 在 `FlashDeliveryI18nContractTest.FILES` 末尾增加: ```java "messages_th_TH.properties" ``` 保留现有全部闪送与地址消息 key。 - [ ] **Step 3: 创建完整泰语语言包** 以 `messages_zh_TW.properties` 的 key 和顺序为基准创建 `messages_th_TH.properties`: - 复制全部 319 个 key,不复制繁体中文值。 - 将每个值翻译为自然、简洁的泰语系统提示。 - 保持 `{0}`、`{1}`、`{min}`、`{max}` 等占位符原样。 - 保持 URL、HTTP(S)、PIN、TWD、LINE、Google、Apple、OMG、ATM、CVS 等技术或品牌标识原样。 - 文件使用 UTF-8,不使用 `\uXXXX` 转义泰文。 - 关键值必须精确包含: ```properties not.null=* จำเป็นต้องกรอก user.login.success=เข้าสู่ระบบสำเร็จ no.action.success=ดำเนินการสำเร็จ flash.delivery.item.invalid=ข้อมูลจำนวน น้ำหนักรวม หรือรายละเอียดสิ่งของไม่ถูกต้อง flash.delivery.quote.changed=ราคาเสนอมีการเปลี่ยนแปลง โปรดยืนยันค่าบริการล่าสุดอีกครั้ง user.password.retry.limit.exceed=ป้อนรหัสผ่านผิด {0} ครั้ง บัญชีถูกล็อก {1} นาที ``` - [ ] **Step 4: 静态检查资源和测试差异,暂不运行 Maven** 运行: ```powershell git diff --check -- ruoyi-admin/src/main/resources/i18n/messages_th_TH.properties ruoyi-admin/src/test/java/com/ruoyi/common/utils/BackendI18nBundleContractTest.java ruoyi-admin/src/test/java/com/ruoyi/app/flashdelivery/service/FlashDeliveryI18nContractTest.java ``` 预期:无错误输出。按全局约束,将测试执行延后到 Task 4。 - [ ] **Step 5: 提交泰语消息资源** ```powershell $paths=@( 'ruoyi-admin/src/main/resources/i18n/messages_th_TH.properties', 'ruoyi-admin/src/test/java/com/ruoyi/common/utils/BackendI18nBundleContractTest.java', 'ruoyi-admin/src/test/java/com/ruoyi/app/flashdelivery/service/FlashDeliveryI18nContractTest.java' ) git add -- $paths git diff --cached --name-only git diff --cached --check git commit -m "新增完整后端泰语提示" -m "补齐全部后端消息的泰语翻译,并通过键集合、占位符和典型消息契约防止语言包缺失或格式错误。" ``` ### Task 3: 锁定台湾繁体默认语言和 HTTP 泰语切换 **Files:** - Create: `ruoyi-admin/src/test/java/com/ruoyi/framework/config/BackendI18nConfigTest.java` - Modify: `ruoyi-framework/src/main/java/com/ruoyi/framework/config/I18nConfig.java` **Interfaces:** - Consumes: `I18nConfig.localeResolver()` 和 `I18nConfig.localeChangeInterceptor()`。 - Produces: 统一使用 `LocaleUtils.parseLocale(String)` 的 HTTP 语言切换,确保无 `lang` 时为 `zh_TW`,三种泰语代码都切换为 `th_TH`。 - [ ] **Step 1: 编写默认语言与 HTTP 切换测试源码** ```java package com.ruoyi.framework.config; import org.junit.jupiter.api.Test; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletResponse; import org.springframework.web.servlet.DispatcherServlet; import org.springframework.web.servlet.LocaleResolver; import org.springframework.web.servlet.i18n.LocaleChangeInterceptor; import java.util.Locale; import static org.junit.jupiter.api.Assertions.assertEquals; class BackendI18nConfigTest { @Test void defaultsToTraditionalChinese() { LocaleResolver resolver = new I18nConfig().localeResolver(); assertEquals(new Locale("zh", "TW"), resolver.resolveLocale(new MockHttpServletRequest())); } @Test void switchesAllThaiAliasesThroughExistingLangParameter() throws Exception { for (String language : new String[]{"th_TH", "th-TH", "th"}) { I18nConfig config = new I18nConfig(); LocaleResolver resolver = config.localeResolver(); LocaleChangeInterceptor interceptor = config.localeChangeInterceptor(); MockHttpServletRequest request = new MockHttpServletRequest(); request.setAttribute(DispatcherServlet.LOCALE_RESOLVER_ATTRIBUTE, resolver); request.setParameter("lang", language); interceptor.preHandle(request, new MockHttpServletResponse(), new Object()); assertEquals(new Locale("th", "TH"), resolver.resolveLocale(request), language); } } } ``` - [ ] **Step 2: 让 HTTP 拦截器复用白名单解析器** 在 `I18nConfig` 导入 `LocaleUtils`,并只覆盖拦截器的 Locale 值解析;参数名继续使用 `lang`: ```java import com.ruoyi.common.utils.LocaleUtils; @Bean public LocaleChangeInterceptor localeChangeInterceptor() { LocaleChangeInterceptor lci = new LocaleChangeInterceptor() { @Override protected Locale parseLocaleValue(String localeValue) { return LocaleUtils.parseLocale(localeValue); } }; lci.setParamName("lang"); return lci; } ``` `localeResolver()` 保持 `slr.setDefaultLocale(Locale.TRADITIONAL_CHINESE)`,不得修改默认语言。 - [ ] **Step 3: 静态检查配置和测试,暂不运行 Maven** ```powershell git diff --check -- ruoyi-framework/src/main/java/com/ruoyi/framework/config/I18nConfig.java ruoyi-admin/src/test/java/com/ruoyi/framework/config/BackendI18nConfigTest.java ``` 预期:无错误输出。 - [ ] **Step 4: 提交默认语言和 HTTP 切换契约** ```powershell git add -- ruoyi-framework/src/main/java/com/ruoyi/framework/config/I18nConfig.java ruoyi-admin/src/test/java/com/ruoyi/framework/config/BackendI18nConfigTest.java git diff --cached --name-only git diff --cached --check git commit -m "锁定后端默认语言与泰语切换" -m "让 lang 参数复用受支持语言白名单解析,兼容三种泰语代码,并增加台湾繁体默认语言回归契约。" ``` ### Task 4: 统一验证和交付检查 **Files:** - Verify: `ruoyi-common/src/main/java/com/ruoyi/common/utils/LocaleUtils.java` - Verify: `ruoyi-framework/src/main/java/com/ruoyi/framework/config/I18nConfig.java` - Verify: `ruoyi-admin/src/main/resources/i18n/messages_th_TH.properties` - Verify: `ruoyi-admin/src/test/java/com/ruoyi/common/utils/LocaleUtilsTest.java` - Verify: `ruoyi-admin/src/test/java/com/ruoyi/common/utils/BackendI18nBundleContractTest.java` - Verify: `ruoyi-admin/src/test/java/com/ruoyi/framework/config/BackendI18nConfigTest.java` - Verify: `ruoyi-admin/src/test/java/com/ruoyi/app/flashdelivery/service/FlashDeliveryI18nContractTest.java` **Interfaces:** - Consumes: Tasks 1 至 3 的全部生产代码、资源和测试源码。 - Produces: JDK 21 下可验证的完整后端泰语提示支持。 - [ ] **Step 1: 使用 JDK 21 运行全部相关定向测试** ```powershell $env:JAVA_HOME='C:\Users\qmj\.jdks\graalvm-jdk-21.0.7' $env:PATH="$env:JAVA_HOME\bin;$env:PATH" mvn -pl ruoyi-admin -am "-Dtest=LocaleUtilsTest,BackendI18nBundleContractTest,BackendI18nConfigTest,FlashDeliveryI18nContractTest" "-Dsurefire.failIfNoSpecifiedTests=false" test ``` 预期:四个测试类全部通过,Maven 输出 `BUILD SUCCESS`。 - [ ] **Step 2: 使用 JDK 21 执行后端模块构建** ```powershell $env:JAVA_HOME='C:\Users\qmj\.jdks\graalvm-jdk-21.0.7' $env:PATH="$env:JAVA_HOME\bin;$env:PATH" mvn -pl ruoyi-admin -am -DskipTests package ``` 预期:`ruoyi-common`、`ruoyi-system`、`ruoyi-framework`、`ruoyi-quartz`、`ruoyi-generator` 和 `ruoyi-admin` 均为 `SUCCESS`。 - [ ] **Step 3: 检查最终差异和未混入文件** ```powershell git diff --check git status --short git log --oneline -4 ``` 确认泰语功能提交没有包含 `.claude/homunculus/observations.jsonl`,也没有包含此前未提交的闪送请求 DTO/Controller 注释修改。 - [ ] **Step 4: 报告验证结果和提交 SHA** 交付内容必须列出: - Locale 解析提交 SHA。 - 泰语语言包提交 SHA。 - 默认语言回归测试提交 SHA。 - 实际执行的测试数量和 Maven 构建结果。 - 明确说明默认语言仍是 `zh_TW`。 - 明确说明未修改前端和业务数据多语言映射。