Skip to content

Commit e734c2f

Browse files
author
gitlab
committed
Merge branch 'ZSTAC-81675' into '5.5.12'
<feature>[errorcode]: global error code i18n See merge request zstackio/zstack!9246
2 parents 96f7326 + 8455f36 commit e734c2f

12 files changed

Lines changed: 927 additions & 3 deletions

File tree

conf/springConfigXml/Error.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,9 @@
1313
default-init-method="init" default-destroy-method="destroy">
1414

1515
<bean id="ErrorFacade" class="org.zstack.core.errorcode.ErrorFacadeImpl" />
16+
<bean id="GlobalErrorCodeI18nService" class="org.zstack.core.errorcode.GlobalErrorCodeI18nServiceImpl">
17+
<zstack:plugin>
18+
<zstack:extension interface="org.zstack.header.Component"/>
19+
</zstack:plugin>
20+
</bean>
1621
</beans>

core/src/main/java/org/zstack/core/Platform.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -979,6 +979,11 @@ public static ErrorCode err(String globalErrorCode, Enum errCode, ErrorCode caus
979979
handleErrorElaboration(errCode, fmt, result, cause, args);
980980
addErrorCounter(result);
981981
result.setGlobalErrorCode(globalErrorCode);
982+
if (args != null && args.length > 0) {
983+
result.setFormatArgs(java.util.Arrays.stream(args)
984+
.map(a -> a == null ? "null" : a.toString())
985+
.toArray(String[]::new));
986+
}
982987

983988
return result;
984989
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package org.zstack.core.errorcode;
2+
3+
import org.zstack.header.errorcode.ErrorCode;
4+
5+
public interface GlobalErrorCodeI18nService {
6+
/**
7+
* Get localized message for a globalErrorCode.
8+
*
9+
* @param globalErrorCode the global error code key
10+
* @param locale the locale key (e.g. "zh_CN", "ja-JP")
11+
* @param formatArgs optional format arguments for %s placeholders
12+
* @return the localized message, or null if not found
13+
*/
14+
String getLocalizedMessage(String globalErrorCode, String locale, String[] formatArgs);
15+
16+
/**
17+
* Recursively localize an ErrorCode and its cause chain,
18+
* setting the message field on each ErrorCode.
19+
*
20+
* @param error the ErrorCode to localize
21+
* @param locale the locale key
22+
*/
23+
void localizeErrorCode(ErrorCode error, String locale);
24+
25+
/**
26+
* Get the set of available locale keys loaded from JSON files.
27+
*/
28+
java.util.Set<String> getAvailableLocales();
29+
}
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
package org.zstack.core.errorcode;
2+
3+
import org.zstack.header.Component;
4+
import org.zstack.header.errorcode.ErrorCode;
5+
import org.zstack.header.errorcode.ErrorCodeList;
6+
import org.zstack.utils.Utils;
7+
import org.zstack.utils.gson.JSONObjectUtil;
8+
import org.zstack.utils.logging.CLogger;
9+
import org.zstack.utils.path.PathUtil;
10+
11+
import java.io.File;
12+
import java.nio.charset.StandardCharsets;
13+
import java.nio.file.Files;
14+
import java.util.*;
15+
import java.util.concurrent.ConcurrentHashMap;
16+
17+
public class GlobalErrorCodeI18nServiceImpl implements GlobalErrorCodeI18nService, Component {
18+
private static final CLogger logger = Utils.getLogger(GlobalErrorCodeI18nServiceImpl.class);
19+
20+
private static final String I18N_FOLDER = "i18n" + File.separator + "globalErrorCodeMapping";
21+
private static final String FILE_PREFIX = "global-error-";
22+
private static final String FILE_SUFFIX = ".json";
23+
24+
// locale -> (globalErrorCode -> template)
25+
private final Map<String, Map<String, String>> localeMessages = new ConcurrentHashMap<>();
26+
27+
@Override
28+
public boolean start() {
29+
loadAllJsonFiles();
30+
return true;
31+
}
32+
33+
@Override
34+
public boolean stop() {
35+
return true;
36+
}
37+
38+
private void loadAllJsonFiles() {
39+
try {
40+
List<String> paths = PathUtil.scanFolderOnClassPath(I18N_FOLDER);
41+
for (String path : paths) {
42+
if (!path.endsWith(FILE_SUFFIX)) {
43+
continue;
44+
}
45+
46+
File file = new File(path);
47+
String fileName = file.getName();
48+
if (!fileName.startsWith(FILE_PREFIX)) {
49+
continue;
50+
}
51+
52+
String locale = fileName.substring(FILE_PREFIX.length(),
53+
fileName.length() - FILE_SUFFIX.length());
54+
55+
try {
56+
String content = new String(Files.readAllBytes(file.toPath()), StandardCharsets.UTF_8);
57+
@SuppressWarnings("unchecked")
58+
Map<String, String> messages = JSONObjectUtil.toObject(content, LinkedHashMap.class);
59+
localeMessages.put(locale, messages);
60+
logger.info(String.format("loaded %d i18n error messages for locale [%s]",
61+
messages.size(), locale));
62+
} catch (Exception e) {
63+
logger.warn(String.format("failed to load i18n file [%s]: %s", path, e.getMessage()), e);
64+
}
65+
}
66+
} catch (Exception e) {
67+
logger.warn(String.format("failed to scan i18n folder: %s", e.getMessage()));
68+
}
69+
70+
logger.info(String.format("GlobalErrorCodeI18nService loaded %d locales: %s",
71+
localeMessages.size(), localeMessages.keySet()));
72+
}
73+
74+
@Override
75+
public Set<String> getAvailableLocales() {
76+
return Collections.unmodifiableSet(localeMessages.keySet());
77+
}
78+
79+
@Override
80+
public String getLocalizedMessage(String globalErrorCode, String locale, String[] formatArgs) {
81+
if (globalErrorCode == null || locale == null) {
82+
return null;
83+
}
84+
85+
String template = getTemplate(globalErrorCode, locale);
86+
if (template == null) {
87+
return null;
88+
}
89+
90+
return formatTemplate(template, formatArgs);
91+
}
92+
93+
private String getTemplate(String globalErrorCode, String locale) {
94+
Map<String, String> messages = localeMessages.get(locale);
95+
if (messages != null) {
96+
String template = messages.get(globalErrorCode);
97+
if (template != null) {
98+
return template;
99+
}
100+
}
101+
102+
// fallback to en_US
103+
if (!"en_US".equals(locale)) {
104+
Map<String, String> enMessages = localeMessages.get("en_US");
105+
if (enMessages != null) {
106+
return enMessages.get(globalErrorCode);
107+
}
108+
}
109+
110+
return null;
111+
}
112+
113+
private String formatTemplate(String template, String[] formatArgs) {
114+
if (formatArgs == null || formatArgs.length == 0) {
115+
return template;
116+
}
117+
118+
try {
119+
return String.format(template, (Object[]) formatArgs);
120+
} catch (Exception e) {
121+
logger.debug(String.format("failed to format i18n template [%s]: %s", template, e.getMessage()));
122+
return template;
123+
}
124+
}
125+
126+
@Override
127+
public void localizeErrorCode(ErrorCode error, String locale) {
128+
if (error == null || locale == null) {
129+
return;
130+
}
131+
132+
if (error.getGlobalErrorCode() != null) {
133+
String message = getLocalizedMessage(error.getGlobalErrorCode(), locale, error.getFormatArgs());
134+
if (message != null) {
135+
error.setMessage(message);
136+
}
137+
}
138+
139+
if (error.getCause() != null) {
140+
localizeErrorCode(error.getCause(), locale);
141+
}
142+
143+
if (error instanceof ErrorCodeList) {
144+
List<ErrorCode> causes = ((ErrorCodeList) error).getCauses();
145+
if (causes != null) {
146+
for (ErrorCode cause : causes) {
147+
localizeErrorCode(cause, locale);
148+
}
149+
}
150+
}
151+
}
152+
}
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
package org.zstack.core.errorcode;
2+
3+
import org.zstack.utils.Utils;
4+
import org.zstack.utils.logging.CLogger;
5+
6+
import java.util.*;
7+
8+
public class LocaleUtils {
9+
private static final CLogger logger = Utils.getLogger(LocaleUtils.class);
10+
public static final String DEFAULT_LOCALE = "en_US";
11+
12+
private static final Map<String, String> LANGUAGE_TO_LOCALE = new HashMap<>();
13+
14+
// Languages that use underscore format in locale file names (e.g. zh_CN, en_US)
15+
private static final Set<String> UNDERSCORE_LANGS = new HashSet<>(Arrays.asList("zh", "en"));
16+
17+
static {
18+
LANGUAGE_TO_LOCALE.put("zh", "zh_CN");
19+
LANGUAGE_TO_LOCALE.put("en", "en_US");
20+
LANGUAGE_TO_LOCALE.put("ja", "ja-JP");
21+
LANGUAGE_TO_LOCALE.put("ko", "ko-KR");
22+
LANGUAGE_TO_LOCALE.put("de", "de-DE");
23+
LANGUAGE_TO_LOCALE.put("fr", "fr-FR");
24+
LANGUAGE_TO_LOCALE.put("ru", "ru-RU");
25+
LANGUAGE_TO_LOCALE.put("th", "th-TH");
26+
LANGUAGE_TO_LOCALE.put("id", "id-ID");
27+
}
28+
29+
/**
30+
* Parse Accept-Language header and return the best matching locale key
31+
* from the set of available locales.
32+
*
33+
* @param acceptLanguage the Accept-Language header value
34+
* @param availableLocales the set of locale keys loaded from JSON files
35+
* @return the best matching locale key, or en_US as fallback
36+
*/
37+
public static String resolveLocale(String acceptLanguage, Set<String> availableLocales) {
38+
if (acceptLanguage == null || acceptLanguage.trim().isEmpty()) {
39+
return DEFAULT_LOCALE;
40+
}
41+
42+
List<LocaleEntry> entries = parseAcceptLanguage(acceptLanguage);
43+
for (LocaleEntry entry : entries) {
44+
if (entry.quality <= 0) {
45+
continue;
46+
}
47+
48+
String normalized = normalizeTag(entry.tag);
49+
if (availableLocales.contains(normalized)) {
50+
return normalized;
51+
}
52+
53+
String lang = entry.tag.split("[-_]")[0].toLowerCase();
54+
String mapped = LANGUAGE_TO_LOCALE.get(lang);
55+
if (mapped != null && availableLocales.contains(mapped)) {
56+
return mapped;
57+
}
58+
}
59+
60+
return DEFAULT_LOCALE;
61+
}
62+
63+
/**
64+
* Normalize an HTTP language tag to match file locale keys.
65+
* e.g. "zh-CN" -> "zh_CN", "en-US" -> "en_US", "ja-JP" -> "ja-JP"
66+
* See UNDERSCORE_LANGS for languages that use underscore format.
67+
*/
68+
static String normalizeTag(String tag) {
69+
tag = tag.trim();
70+
String[] parts = tag.split("[-_]");
71+
if (parts.length == 2) {
72+
String lang = parts[0].toLowerCase();
73+
String region = parts[1].toUpperCase();
74+
if (UNDERSCORE_LANGS.contains(lang)) {
75+
return lang + "_" + region;
76+
}
77+
return lang + "-" + region;
78+
}
79+
return tag;
80+
}
81+
82+
private static List<LocaleEntry> parseAcceptLanguage(String header) {
83+
List<LocaleEntry> entries = new ArrayList<>();
84+
String[] parts = header.split(",");
85+
for (String part : parts) {
86+
part = part.trim();
87+
if (part.isEmpty()) {
88+
continue;
89+
}
90+
String[] tagAndParams = part.split(";");
91+
String tag = tagAndParams[0].trim();
92+
double quality = 1.0;
93+
for (int i = 1; i < tagAndParams.length; i++) {
94+
String param = tagAndParams[i].trim();
95+
if (param.startsWith("q=")) {
96+
try {
97+
quality = Double.parseDouble(param.substring(2).trim());
98+
} catch (NumberFormatException e) {
99+
logger.debug(String.format("failed to parse quality value [%s]: %s", param, e.getMessage()));
100+
quality = 0;
101+
}
102+
}
103+
}
104+
entries.add(new LocaleEntry(tag, quality));
105+
}
106+
entries.sort((a, b) -> Double.compare(b.quality, a.quality));
107+
return entries;
108+
}
109+
110+
private static class LocaleEntry {
111+
final String tag;
112+
final double quality;
113+
114+
LocaleEntry(String tag, double quality) {
115+
this.tag = tag;
116+
this.quality = quality;
117+
}
118+
}
119+
}

0 commit comments

Comments
 (0)