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。
zh_TW。th_TH,并兼容 th-TH 和 th。messages_th_TH.properties 必须与现有四个本地化语言包拥有完全一致且非空的消息 key。{0}、{1} 不得遗漏。toLangCode()、getUserLanguageCode()、数据库字段或业务数据语言映射。Files:
ruoyi-admin/src/test/java/com/ruoyi/common/utils/LocaleUtilsTest.javaruoyi-common/src/main/java/com/ruoyi/common/utils/LocaleUtils.javaInterfaces:
LocaleUtils.parseLocale(String) 的代码。Produces: public static Locale parseLocale(String language);支持白名单语言代码并对空值、未知值回退 zh_TW。
[ ] Step 1: 编写 Locale 白名单解析测试源码
创建参数化测试,锁定所有受支持别名与默认回退:
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"));
}
}
保留 DEFAULT_LOCALE,新增明确的 Locale 常量并只修改 parseLocale。先 trim(),再把 - 转为 _ 并使用 Locale.ROOT 转小写,最后使用 switch 返回白名单 Locale:
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()。
运行:
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 解析改动
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,并为现有受支持语言建立白名单解析;空值和未知语言继续回退台湾繁体。"
Files:
ruoyi-admin/src/main/resources/i18n/messages_th_TH.propertiesruoyi-admin/src/test/java/com/ruoyi/common/utils/BackendI18nBundleContractTest.javaruoyi-admin/src/test/java/com/ruoyi/app/flashdelivery/service/FlashDeliveryI18nContractTest.javaInterfaces:
ResourceBundleMessageSource 的 basename i18n/messages 和现有 319 个本地化消息 key。Produces: 完整的泰语资源包 messages_th_TH.properties,由 Locale("th", "TH") 自动加载。
[ ] Step 1: 编写语言包完整性和占位符契约测试源码
创建 BackendI18nBundleContractTest,使用 UTF-8 Reader 加载资源;以繁体中文包作为 key 与占位符参考,逐一比较英文、简中、越南语和泰语:
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<String> 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<String> placeholders(String value) {
List<String> result = new ArrayList<>();
Matcher matcher = PLACEHOLDER.matcher(value);
while (matcher.find()) result.add(matcher.group());
return result;
}
}
在 FlashDeliveryI18nContractTest.FILES 末尾增加:
"messages_th_TH.properties"
保留现有全部闪送与地址消息 key。
以 messages_zh_TW.properties 的 key 和顺序为基准创建 messages_th_TH.properties:
{0}、{1}、{min}、{max} 等占位符原样。\uXXXX 转义泰文。关键值必须精确包含:
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
运行:
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: 提交泰语消息资源
$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 "补齐全部后端消息的泰语翻译,并通过键集合、占位符和典型消息契约防止语言包缺失或格式错误。"
Files:
ruoyi-admin/src/test/java/com/ruoyi/framework/config/BackendI18nConfigTest.javaruoyi-framework/src/main/java/com/ruoyi/framework/config/I18nConfig.javaInterfaces:
I18nConfig.localeResolver() 和 I18nConfig.localeChangeInterceptor()。Produces: 统一使用 LocaleUtils.parseLocale(String) 的 HTTP 语言切换,确保无 lang 时为 zh_TW,三种泰语代码都切换为 th_TH。
[ ] Step 1: 编写默认语言与 HTTP 切换测试源码
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:
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
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 切换契约
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 参数复用受支持语言白名单解析,兼容三种泰语代码,并增加台湾繁体默认语言回归契约。"
Files:
ruoyi-common/src/main/java/com/ruoyi/common/utils/LocaleUtils.javaruoyi-framework/src/main/java/com/ruoyi/framework/config/I18nConfig.javaruoyi-admin/src/main/resources/i18n/messages_th_TH.propertiesruoyi-admin/src/test/java/com/ruoyi/common/utils/LocaleUtilsTest.javaruoyi-admin/src/test/java/com/ruoyi/common/utils/BackendI18nBundleContractTest.javaruoyi-admin/src/test/java/com/ruoyi/framework/config/BackendI18nConfigTest.javaruoyi-admin/src/test/java/com/ruoyi/app/flashdelivery/service/FlashDeliveryI18nContractTest.javaInterfaces:
Produces: JDK 21 下可验证的完整后端泰语提示支持。
[ ] Step 1: 使用 JDK 21 运行全部相关定向测试
$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 执行后端模块构建
$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: 检查最终差异和未混入文件
git diff --check
git status --short
git log --oneline -4
确认泰语功能提交没有包含 .claude/homunculus/observations.jsonl,也没有包含此前未提交的闪送请求 DTO/Controller 注释修改。
交付内容必须列出:
zh_TW。