diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/exception/NoSuchMessageException.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/exception/NoSuchMessageException.java new file mode 100644 index 000000000..ef818f12f --- /dev/null +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/exception/NoSuchMessageException.java @@ -0,0 +1,36 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.fesod.sheet.exception; + +import java.util.Locale; + +/** + * Exception thrown when a message cannot be resolved. + */ +public class NoSuchMessageException extends ExcelRuntimeException { + + public NoSuchMessageException(String code, Locale locale) { + super("No message found under code '" + code + "' for locale '" + locale + "'."); + } + + public NoSuchMessageException(String code) { + super("No message found under code '" + code + "' for locale '" + Locale.getDefault() + "'."); + } +} diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/i18n/AbstractMessageSource.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/i18n/AbstractMessageSource.java new file mode 100644 index 000000000..54cdb63fc --- /dev/null +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/i18n/AbstractMessageSource.java @@ -0,0 +1,162 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.fesod.sheet.i18n; + +import java.util.LinkedHashSet; +import java.util.Locale; +import java.util.Set; +import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import org.apache.commons.lang3.StringUtils; +import org.apache.fesod.sheet.exception.NoSuchMessageException; + +/** + * Abstract implementation of the {@link MessageSource} interface, + * providing common functionality for message resolution, lookup, and interpolation. + */ +public abstract class AbstractMessageSource implements MessageSource { + + private static final char PREFIX = '{'; + private static final char SUFFIX = '}'; + private static final char ESCAPE = '\\'; + + /** + * whether to use the message code as default message. + */ + @Getter + @Setter + private boolean useCodeAsDefaultMessage = false; + + /** + * Resolve a message for the given code and locale. + * + * @param code the message code + * @param locale the target locale to use + * @return the resolved message + * @throws NoSuchMessageException if the message is not found + */ + @Override + public String getMessage(String code, Locale locale) throws NoSuchMessageException { + String msg = getMessageInternal(code, locale, new LinkedHashSet<>(4)); + if (msg != null) { + return msg; + } + throw new NoSuchMessageException(code, locale); + } + + /** + * Interpolate the given message template. + * + * @param messageTemplate the message template + * @param locale the target locale to use + * @return the interpolated message + */ + @Override + public String interpolate(String messageTemplate, Locale locale) { + if (StringUtils.isEmpty(messageTemplate)) { + return messageTemplate; + } + return replaceParameters(messageTemplate, locale, new LinkedHashSet<>(4)); + } + + /** + * Resolve the raw message template for the given code. + * + * @param code the message code + * @param locale the locale to use + * @return the raw message + */ + protected abstract String doResolveCode(String code, Locale locale); + + private String getRawMessage(String code, Locale locale) { + if (code == null) { + return null; + } + return doResolveCode(code, locale == null ? Locale.getDefault() : locale); + } + + private String getMessageInternal(String code, Locale locale, Set visitedParameters) { + String msg = getRawMessage(code, locale); + if (msg != null) { + return replaceParameters(msg, locale, visitedParameters); + } + return isUseCodeAsDefaultMessage() ? code : null; + } + + private String replaceParameters(String message, Locale locale, Set visitedParameters) { + if (StringUtils.isEmpty(message)) { + return message; + } + + StringBuilder buf = new StringBuilder(message); + int startIndex = -1; + + for (int i = 0; i < buf.length(); i++) { + char c = buf.charAt(i); + + // Handle redundant escape characters + if (c == ESCAPE && i + 1 < buf.length()) { + char next = buf.charAt(i + 1); + if (next == PREFIX || next == SUFFIX || next == ESCAPE) { + buf.deleteCharAt(i); + continue; + } + } + if (c == PREFIX) { + if (startIndex != -1) { + throw new IllegalArgumentException( + String.format("The message template '%s' contains nested parameters", message)); + } + startIndex = i; + } else if (c == SUFFIX && startIndex != -1) { + int endIndex = i; + String parameter = buf.substring(startIndex + 1, endIndex); + + if (!visitedParameters.add(parameter)) { + throw new IllegalArgumentException("Circular reference detected: '{" + + String.join(" -> ", visitedParameters) + " -> " + parameter + "}'"); + } + + try { + String value = getMessageInternal(parameter, locale, visitedParameters); + + if (value != null) { + buf.replace(startIndex, endIndex + 1, value); + i = startIndex + value.length() - 1; + } + } finally { + visitedParameters.remove(parameter); + } + + startIndex = -1; + } + } + return buf.toString(); + } + + @EqualsAndHashCode + @AllArgsConstructor + protected static final class CachedMessageKey { + final Locale locale; + final String code; + } +} diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/i18n/LocalizationContext.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/i18n/LocalizationContext.java new file mode 100644 index 000000000..9296b03bd --- /dev/null +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/i18n/LocalizationContext.java @@ -0,0 +1,44 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.fesod.sheet.i18n; + +import java.util.Locale; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +/** + * Localization context for i18n. + */ +@Getter +@RequiredArgsConstructor +public class LocalizationContext { + + public static final LocalizationContext DEFAULT = new LocalizationContext(MessageSource.PASS, Locale.getDefault()); + + /** + * The message source providing message resolution. + */ + private final MessageSource messageSource; + + /** + * The locale to use to resolve messages. + */ + private final Locale locale; +} diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/i18n/MessageInterpolator.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/i18n/MessageInterpolator.java new file mode 100644 index 000000000..0bbd51f7a --- /dev/null +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/i18n/MessageInterpolator.java @@ -0,0 +1,37 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.fesod.sheet.i18n; + +import java.util.Locale; + +/** + * Strategy interface for interpolating message templates. + */ +public interface MessageInterpolator { + + /** + * Interpolate the given message template. + * + * @param messageTemplate the message template + * @param locale the target locale to use + * @return the interpolated message + */ + String interpolate(String messageTemplate, Locale locale); +} diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/i18n/MessageSource.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/i18n/MessageSource.java new file mode 100644 index 000000000..9d9ec1a88 --- /dev/null +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/i18n/MessageSource.java @@ -0,0 +1,51 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.fesod.sheet.i18n; + +import java.util.Locale; +import org.apache.fesod.sheet.exception.NoSuchMessageException; + +/** + * Strategy interface for resolving messages. + */ +public interface MessageSource extends MessageInterpolator { + + MessageSource PASS = new MessageSource() { + @Override + public String getMessage(String code, Locale locale) throws NoSuchMessageException { + return code; + } + + @Override + public String interpolate(String messageTemplate, Locale locale) { + return messageTemplate; + } + }; + + /** + * Resolve a message for the given code and locale. + * + * @param code the message code + * @param locale the target locale to use + * @return the resolved message + * @throws NoSuchMessageException if the message is not found + */ + String getMessage(String code, Locale locale) throws NoSuchMessageException; +} diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/i18n/ResourceBundleMessageSource.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/i18n/ResourceBundleMessageSource.java new file mode 100644 index 000000000..b1aa3b8d2 --- /dev/null +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/i18n/ResourceBundleMessageSource.java @@ -0,0 +1,159 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.fesod.sheet.i18n; + +import java.util.Locale; +import java.util.Map; +import java.util.MissingResourceException; +import java.util.ResourceBundle; +import java.util.concurrent.ConcurrentHashMap; +import lombok.RequiredArgsConstructor; +import org.apache.commons.lang3.Validate; + +/** + * A {@link MessageSource} implementation backed by a resource bundle. + */ +public class ResourceBundleMessageSource extends AbstractMessageSource { + + /** + * The default base name. + */ + private static final String DEFAULT_BASE_NAME = "messages"; + + /** + * The base name of the resource bundle. + */ + private final String baseName; + + /** + * The control which gives information for the resource bundle loading process. + */ + private final ResourceBundle.Control control; + + /** + * Cache to hold loaded ResourceBundles. + */ + private final Map cachedBundles = new ConcurrentHashMap<>(); + + /** + * Cache to hold resolved messages. + */ + private final Map cachedMessages = new ConcurrentHashMap<>(); + + public ResourceBundleMessageSource() { + this(DEFAULT_BASE_NAME, false, null); + } + + public ResourceBundleMessageSource(String baseName) { + this(baseName, false, null); + } + + public ResourceBundleMessageSource(String baseName, boolean useCodeAsDefault) { + this(baseName, useCodeAsDefault, null); + } + + public ResourceBundleMessageSource(String baseName, ResourceBundle.Control control) { + this(baseName, false, control); + } + + public ResourceBundleMessageSource(String baseName, boolean useCodeAsDefault, ResourceBundle.Control control) { + Validate.notBlank(baseName, "Base name must not be empty"); + + this.baseName = baseName; + this.control = control; + setUseCodeAsDefaultMessage(useCodeAsDefault); + } + + @Override + protected String doResolveCode(String code, Locale locale) { + CachedMessageKey key = new CachedMessageKey(locale, code); + MessageHolder holder = cachedMessages.get(key); + if (holder != null) { + return holder.message; + } + + ResourceBundle bundle = getResourceBundle(locale); + if (bundle == null) { + cachedMessages.put(key, MessageHolder.NONE); + return null; + } + + holder = getMessageHolder(bundle, code); + cachedMessages.put(key, holder); + return holder.message; + } + + /** + * Returns a ResourceBundle for the given locale and baseName. + */ + private ResourceBundle getResourceBundle(Locale locale) { + ResourceBundleHolder holder = this.cachedBundles.get(locale); + if (holder != null) { + return holder.bundle; + } + try { + holder = new ResourceBundleHolder(doGetBundle(locale)); + } catch (MissingResourceException ex) { + holder = ResourceBundleHolder.NONE; + } + + this.cachedBundles.put(locale, holder); + return holder.bundle; + } + + private ResourceBundle doGetBundle(Locale locale) throws MissingResourceException { + ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); + if (classLoader == null) { + classLoader = ResourceBundleMessageSource.class.getClassLoader(); + } + if (control == null) { + return ResourceBundle.getBundle(baseName, locale, classLoader); + } + return ResourceBundle.getBundle(baseName, locale, classLoader, control); + } + + /** + * Returns the message value for the specified key. + */ + private MessageHolder getMessageHolder(ResourceBundle bundle, String key) { + if (!bundle.containsKey(key)) { + return MessageHolder.NONE; + } + try { + return new MessageHolder(bundle.getString(key)); + } catch (MissingResourceException ex) { + return MessageHolder.NONE; + } + } + + @RequiredArgsConstructor + private static class ResourceBundleHolder { + static final ResourceBundleHolder NONE = new ResourceBundleHolder(null); + + final ResourceBundle bundle; + } + + @RequiredArgsConstructor + private static class MessageHolder { + static final MessageHolder NONE = new MessageHolder(null); + + final String message; + } +} diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/i18n/StaticMessageSource.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/i18n/StaticMessageSource.java new file mode 100644 index 000000000..00ac88acd --- /dev/null +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/i18n/StaticMessageSource.java @@ -0,0 +1,74 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.fesod.sheet.i18n; + +import java.util.Locale; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import org.apache.fesod.sheet.util.Validate; + +/** + * Simple {@link MessageSource} implementation backed by an in-memory map. + */ +public class StaticMessageSource extends AbstractMessageSource { + + private final Map cachedMessages = new ConcurrentHashMap<>(); + + /** + * Associate the given message with the given code. + * + * @param code the code + * @param message the message + */ + public void addMessage(String code, String message) { + addMessage(code, message, Locale.getDefault()); + } + + /** + * Associate the given message with the given code. + * + * @param code the code + * @param message the message + * @param locale the target locale to use + */ + public void addMessage(String code, String message, Locale locale) { + Validate.notNull(code, "Code must not be null"); + Validate.notNull(message, "Message must not be null"); + Validate.notNull(locale, "Locale must not be null"); + + cachedMessages.put(new CachedMessageKey(locale, code), message); + } + + /** + * Associate the given message values with the given keys as codes. + * + * @param messages the messages + * @param locale the target locale to use + */ + public void addMessages(Map messages, Locale locale) { + Validate.notNull(messages, "Messages map must not be null"); + messages.forEach((code, message) -> addMessage(code, message, locale)); + } + + @Override + protected String doResolveCode(String code, Locale locale) { + return cachedMessages.get(new CachedMessageKey(locale, code)); + } +} diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/metadata/AbstractHolder.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/metadata/AbstractHolder.java index 35b63aff4..05298df67 100644 --- a/fesod-sheet/src/main/java/org/apache/fesod/sheet/metadata/AbstractHolder.java +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/metadata/AbstractHolder.java @@ -117,6 +117,15 @@ public AbstractHolder(BasicParameter basicParameter, AbstractHolder prentAbstrac globalConfiguration.setLocale(basicParameter.getLocale()); } + if (basicParameter.getLocalizationContext() == null) { + if (prentAbstractHolder != null) { + globalConfiguration.setLocalizationContext( + prentAbstractHolder.getGlobalConfiguration().getLocalizationContext()); + } + } else { + globalConfiguration.setLocalizationContext(basicParameter.getLocalizationContext()); + } + if (basicParameter.getFiledCacheLocation() == null) { if (prentAbstractHolder != null) { globalConfiguration.setFiledCacheLocation( diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/metadata/AbstractParameterBuilder.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/metadata/AbstractParameterBuilder.java index de25c3c08..89749898c 100644 --- a/fesod-sheet/src/main/java/org/apache/fesod/sheet/metadata/AbstractParameterBuilder.java +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/metadata/AbstractParameterBuilder.java @@ -33,6 +33,8 @@ import org.apache.fesod.common.util.ListUtils; import org.apache.fesod.sheet.converters.Converter; import org.apache.fesod.sheet.enums.CacheLocationEnum; +import org.apache.fesod.sheet.i18n.LocalizationContext; +import org.apache.fesod.sheet.i18n.MessageSource; /** * ExcelBuilder @@ -152,6 +154,28 @@ public T locale(Locale locale) { return self(); } + /** + * Configure localization for spreadsheet headers. + * + * @param messageSource the {@link MessageSource} to use + * @return this builder + */ + public T localizeHead(MessageSource messageSource) { + return localizeHead(messageSource, Locale.getDefault()); + } + + /** + * Configure localization for spreadsheet headers. + * + * @param messageSource the {@link MessageSource} to use + * @param locale the target locale to use + * @return this builder + */ + public T localizeHead(MessageSource messageSource, Locale locale) { + parameter().setLocalizationContext(new LocalizationContext(messageSource, locale)); + return self(); + } + /** * The cache used when parsing fields such as head. *

diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/metadata/BasicParameter.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/metadata/BasicParameter.java index 19a2bece0..279c5b51b 100644 --- a/fesod-sheet/src/main/java/org/apache/fesod/sheet/metadata/BasicParameter.java +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/metadata/BasicParameter.java @@ -32,6 +32,7 @@ import lombok.Setter; import org.apache.fesod.sheet.converters.Converter; import org.apache.fesod.sheet.enums.CacheLocationEnum; +import org.apache.fesod.sheet.i18n.LocalizationContext; /** * Basic parameter @@ -72,6 +73,10 @@ public class BasicParameter { */ private Locale locale; + /** + * Context for i18n and header localization. + */ + private LocalizationContext localizationContext; /** * Whether to use scientific Format. * diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/metadata/GlobalConfiguration.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/metadata/GlobalConfiguration.java index 6a894dc57..36c17d8bc 100644 --- a/fesod-sheet/src/main/java/org/apache/fesod/sheet/metadata/GlobalConfiguration.java +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/metadata/GlobalConfiguration.java @@ -30,6 +30,7 @@ import lombok.Getter; import lombok.Setter; import org.apache.fesod.sheet.enums.CacheLocationEnum; +import org.apache.fesod.sheet.i18n.LocalizationContext; /** * Global configuration @@ -77,6 +78,13 @@ public class GlobalConfiguration { */ private Boolean autoStrip; + /** + * Context for i18n and header localization. + * + * Defaults to {@link LocalizationContext#DEFAULT} + */ + private LocalizationContext localizationContext; + public GlobalConfiguration() { this.autoTrim = Boolean.TRUE; this.autoStrip = Boolean.FALSE; @@ -84,5 +92,6 @@ public GlobalConfiguration() { this.locale = Locale.getDefault(); this.useScientificFormat = Boolean.FALSE; this.filedCacheLocation = CacheLocationEnum.THREAD_LOCAL; + this.localizationContext = LocalizationContext.DEFAULT; } } diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/metadata/property/ExcelHeadProperty.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/metadata/property/ExcelHeadProperty.java index 7c4903b62..68063d48b 100644 --- a/fesod-sheet/src/main/java/org/apache/fesod/sheet/metadata/property/ExcelHeadProperty.java +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/metadata/property/ExcelHeadProperty.java @@ -36,6 +36,8 @@ import lombok.extern.slf4j.Slf4j; import org.apache.fesod.common.util.StringUtils; import org.apache.fesod.sheet.enums.HeadKindEnum; +import org.apache.fesod.sheet.i18n.LocalizationContext; +import org.apache.fesod.sheet.i18n.MessageSource; import org.apache.fesod.sheet.metadata.ConfigurationHolder; import org.apache.fesod.sheet.metadata.FieldCache; import org.apache.fesod.sheet.metadata.FieldWrapper; @@ -90,7 +92,7 @@ public ExcelHeadProperty(ConfigurationHolder configurationHolder, Class headC } // convert headClazz to head initColumnProperties(configurationHolder); - + localizeHead(configurationHolder); initHeadRowNumber(); if (log.isDebugEnabled()) { log.debug("The initialization sheet/table 'ExcelHeadProperty' is complete , head kind is {}", headKind); @@ -159,6 +161,30 @@ private void initOneColumnProperty(int index, FieldWrapper field, Boolean forceI headMap.put(index, head); } + /** + * Perform i18n and localization on spreadsheet header names. + */ + private void localizeHead(ConfigurationHolder configurationHolder) { + LocalizationContext context = configurationHolder.globalConfiguration().getLocalizationContext(); + if (context == null || context == LocalizationContext.DEFAULT) { + return; + } + + MessageSource messageSource = context.getMessageSource(); + for (Head head : headMap.values()) { + List headNames = head.getHeadNameList(); + if (headNames == null || headNames.isEmpty()) { + continue; + } + + List localized = new ArrayList<>(headNames.size()); + for (String headName : headNames) { + localized.add(messageSource.interpolate(headName, context.getLocale())); + } + head.setHeadNameList(localized); + } + } + public boolean hasHead() { return headKind != HeadKindEnum.NONE; } diff --git a/fesod-sheet/src/test/java/org/apache/fesod/sheet/i18n/LocalizedHeadDataTest.java b/fesod-sheet/src/test/java/org/apache/fesod/sheet/i18n/LocalizedHeadDataTest.java new file mode 100644 index 000000000..685bc1f99 --- /dev/null +++ b/fesod-sheet/src/test/java/org/apache/fesod/sheet/i18n/LocalizedHeadDataTest.java @@ -0,0 +1,270 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.fesod.sheet.i18n; + +import java.io.File; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import org.apache.fesod.sheet.ExcelWriter; +import org.apache.fesod.sheet.FesodSheet; +import org.apache.fesod.sheet.annotation.ExcelProperty; +import org.apache.fesod.sheet.testkit.Tags; +import org.apache.fesod.sheet.testkit.base.AbstractExcelTest; +import org.apache.fesod.sheet.testkit.enums.ExcelFormat; +import org.apache.fesod.sheet.testkit.params.ExcelFormatSource; +import org.apache.fesod.sheet.testkit.params.FormatScope; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.params.ParameterizedTest; + +/** + * Integration tests for head-name localization. + */ +@Tag(Tags.ROUND_TRIP) +class LocalizedHeadDataTest extends AbstractExcelTest { + + private static final String NAME_CODE = "head.name"; + private static final String CODE_CODE = "head.code"; + private static final String ADDRESS_CODE = "head.address"; + private static final String CITY_CODE = "head.city"; + private static final String STREET_CODE = "head.street"; + + private static StaticMessageSource headMessageSource() { + StaticMessageSource source = new StaticMessageSource(); + source.addMessage(NAME_CODE, "Name", Locale.ENGLISH); + source.addMessage(CODE_CODE, "Code", Locale.ENGLISH); + source.addMessage(NAME_CODE, "姓名", Locale.SIMPLIFIED_CHINESE); + source.addMessage(CODE_CODE, "编号", Locale.SIMPLIFIED_CHINESE); + source.addMessage(ADDRESS_CODE, "Address", Locale.ENGLISH); + source.addMessage(CITY_CODE, "City", Locale.ENGLISH); + source.addMessage(STREET_CODE, "Street", Locale.ENGLISH); + source.addMessage(ADDRESS_CODE, "地址", Locale.SIMPLIFIED_CHINESE); + source.addMessage(CITY_CODE, "城市", Locale.SIMPLIFIED_CHINESE); + source.addMessage(STREET_CODE, "街道", Locale.SIMPLIFIED_CHINESE); + return source; + } + + private static List> readRawRows(File file) { + return FesodSheet.read(file).headRowNumber(0).sheet().doReadSync(); + } + + private static List> readRawRows(File file, int sheetNo) { + return FesodSheet.read(file).headRowNumber(0).sheet(sheetNo).doReadSync(); + } + + @ParameterizedTest + @ExcelFormatSource + void shouldLocalizeHeadNamesForConfiguredLocale(ExcelFormat format) throws Exception { + File englishFile = createTempFile("localizedHeadEn", format); + FesodSheet.write(englishFile, LocalizedHeadData.class) + .localizeHead(headMessageSource(), Locale.ENGLISH) + .sheet() + .doWrite(Collections.singletonList(new LocalizedHeadData("Alice", "A1"))); + Map englishHeader = readRawRows(englishFile).get(0); + Assertions.assertEquals("Name", englishHeader.get(0)); + Assertions.assertEquals("Code", englishHeader.get(1)); + + File chineseFile = createTempFile("localizedHeadZh", format); + FesodSheet.write(chineseFile, LocalizedHeadData.class) + .localizeHead(headMessageSource(), Locale.SIMPLIFIED_CHINESE) + .sheet() + .doWrite(Collections.singletonList(new LocalizedHeadData("Alice", "A1"))); + Map chineseHeader = readRawRows(chineseFile).get(0); + Assertions.assertEquals("姓名", chineseHeader.get(0)); + Assertions.assertEquals("编号", chineseHeader.get(1)); + } + + @ParameterizedTest + @ExcelFormatSource + void shouldKeepHeadTemplatesLiteralWithoutLocalization(ExcelFormat format) throws Exception { + File file = createTempFile("literalHead", format); + FesodSheet.write(file, LocalizedHeadData.class) + .sheet() + .doWrite(Collections.singletonList(new LocalizedHeadData("Alice", "A1"))); + + Map header = readRawRows(file).get(0); + Assertions.assertEquals("{head.name}", header.get(0)); + Assertions.assertEquals("{head.code}", header.get(1)); + } + + @ParameterizedTest + @ExcelFormatSource(FormatScope.BINARY) + void shouldPropagateWorkbookLocalizationToSheetWrites(ExcelFormat format) throws Exception { + File file = createTempFile("localizedHeadSheets", format); + try (ExcelWriter excelWriter = FesodSheet.write(file, LocalizedHeadData.class) + .localizeHead(headMessageSource(), Locale.ENGLISH) + .build()) { + excelWriter.write( + Collections.singletonList(new LocalizedHeadData("Alice", "A1")), + FesodSheet.writerSheet(0, "sheet0").build()); + excelWriter.write( + Collections.singletonList(new LocalizedHeadData("Bob", "B1")), + FesodSheet.writerSheet(1, "sheet1").build()); + } + + Assertions.assertEquals("Name", readRawRows(file, 0).get(0).get(0)); + Assertions.assertEquals("Name", readRawRows(file, 1).get(0).get(0)); + } + + @ParameterizedTest + @ExcelFormatSource + void shouldRoundTripLocalizedHeadersWithMatchingLocale(ExcelFormat format) throws Exception { + StaticMessageSource source = headMessageSource(); + File file = createTempFile("localizedHeadRoundTrip", format); + FesodSheet.write(file, LocalizedHeadData.class) + .localizeHead(source, Locale.SIMPLIFIED_CHINESE) + .sheet() + .doWrite(Collections.singletonList(new LocalizedHeadData("Alice", "A1"))); + + List result = FesodSheet.read(file) + .head(LocalizedHeadData.class) + .localizeHead(source, Locale.SIMPLIFIED_CHINESE) + .sheet() + .doReadSync(); + + Assertions.assertEquals(1, result.size()); + Assertions.assertEquals("Alice", result.get(0).getName()); + Assertions.assertEquals("A1", result.get(0).getCode()); + } + + @ParameterizedTest + @ExcelFormatSource + void shouldRoundTripLocalizedDynamicHeadersWithMatchingLocale(ExcelFormat format) throws Exception { + StaticMessageSource source = headMessageSource(); + File file = createTempFile("localizedDynamicHeadRoundTrip", format); + FesodSheet.write(file) + .head(head -> head.column("{" + NAME_CODE + "}").column("{" + CODE_CODE + "}")) + .localizeHead(source, Locale.SIMPLIFIED_CHINESE) + .sheet() + .doWrite(Collections.singletonList(Arrays.asList("Alice", "A1"))); + + List> result = FesodSheet.read(file) + .head(head -> head.column("{" + NAME_CODE + "}").column("{" + CODE_CODE + "}")) + .localizeHead(source, Locale.SIMPLIFIED_CHINESE) + .sheet() + .doReadSync(); + + Assertions.assertEquals(1, result.size()); + Assertions.assertEquals("Alice", result.get(0).get(0)); + Assertions.assertEquals("A1", result.get(0).get(1)); + } + + @ParameterizedTest + @ExcelFormatSource + void shouldLocalizeEveryLevelOfMultiLevelHeadNames(ExcelFormat format) throws Exception { + File englishFile = createTempFile("localizedMultiHeadEn", format); + FesodSheet.write(englishFile, MultiLevelHeadData.class) + .localizeHead(headMessageSource(), Locale.ENGLISH) + .sheet() + .doWrite(Collections.singletonList(new MultiLevelHeadData("Berlin", "Main St"))); + List> englishRows = readRawRows(englishFile); + Assertions.assertEquals("Address", englishRows.get(0).get(0)); + Assertions.assertEquals("City", englishRows.get(1).get(0)); + Assertions.assertEquals("Street", englishRows.get(1).get(1)); + + File chineseFile = createTempFile("localizedMultiHeadZh", format); + FesodSheet.write(chineseFile, MultiLevelHeadData.class) + .localizeHead(headMessageSource(), Locale.SIMPLIFIED_CHINESE) + .sheet() + .doWrite(Collections.singletonList(new MultiLevelHeadData("Berlin", "Main St"))); + List> chineseRows = readRawRows(chineseFile); + Assertions.assertEquals("地址", chineseRows.get(0).get(0)); + Assertions.assertEquals("城市", chineseRows.get(1).get(0)); + Assertions.assertEquals("街道", chineseRows.get(1).get(1)); + } + + @ParameterizedTest + @ExcelFormatSource + void shouldRoundTripMultiLevelLocalizedHeadersWithMatchingLocale(ExcelFormat format) throws Exception { + StaticMessageSource source = headMessageSource(); + File file = createTempFile("localizedMultiHeadRoundTrip", format); + FesodSheet.write(file, MultiLevelHeadData.class) + .localizeHead(source, Locale.SIMPLIFIED_CHINESE) + .sheet() + .doWrite(Collections.singletonList(new MultiLevelHeadData("Berlin", "Main St"))); + + List result = FesodSheet.read(file) + .head(MultiLevelHeadData.class) + .localizeHead(source, Locale.SIMPLIFIED_CHINESE) + .sheet() + .doReadSync(); + + Assertions.assertEquals(1, result.size()); + Assertions.assertEquals("Berlin", result.get(0).getCity()); + Assertions.assertEquals("Main St", result.get(0).getStreet()); + } + + @ParameterizedTest + @ExcelFormatSource + void shouldRoundTripMultiLevelLocalizedDynamicHeadersWithMatchingLocale(ExcelFormat format) throws Exception { + StaticMessageSource source = headMessageSource(); + File file = createTempFile("localizedMultiDynamicHeadRoundTrip", format); + FesodSheet.write(file) + .head(head -> head.column("{" + ADDRESS_CODE + "}", "{" + CITY_CODE + "}") + .column("{" + STREET_CODE + "}", "{" + STREET_CODE + "}")) + .localizeHead(source, Locale.SIMPLIFIED_CHINESE) + .sheet() + .doWrite(Collections.singletonList(Arrays.asList("Berlin", "Main St"))); + + List> result = FesodSheet.read(file) + .head(head -> head.column("{" + ADDRESS_CODE + "}", "{" + CITY_CODE + "}") + .column("{" + STREET_CODE + "}", "{" + STREET_CODE + "}")) + .localizeHead(source, Locale.SIMPLIFIED_CHINESE) + .sheet() + .doReadSync(); + + Assertions.assertEquals(1, result.size()); + Assertions.assertEquals("Berlin", result.get(0).get(0)); + Assertions.assertEquals("Main St", result.get(0).get(1)); + } + + @Getter + @Setter + @NoArgsConstructor + @AllArgsConstructor + public static class LocalizedHeadData { + + @ExcelProperty("{" + NAME_CODE + "}") + private String name; + + @ExcelProperty("{" + CODE_CODE + "}") + private String code; + } + + @Getter + @Setter + @NoArgsConstructor + @AllArgsConstructor + public static class MultiLevelHeadData { + + @ExcelProperty({"{" + ADDRESS_CODE + "}", "{" + CITY_CODE + "}"}) + private String city; + + @ExcelProperty({"{" + ADDRESS_CODE + "}", "{" + STREET_CODE + "}"}) + private String street; + } +} diff --git a/fesod-sheet/src/test/java/org/apache/fesod/sheet/i18n/ResourceBundleMessageSourceTest.java b/fesod-sheet/src/test/java/org/apache/fesod/sheet/i18n/ResourceBundleMessageSourceTest.java new file mode 100644 index 000000000..c7be0649e --- /dev/null +++ b/fesod-sheet/src/test/java/org/apache/fesod/sheet/i18n/ResourceBundleMessageSourceTest.java @@ -0,0 +1,147 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.fesod.sheet.i18n; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.net.URL; +import java.net.URLConnection; +import java.nio.charset.StandardCharsets; +import java.util.Locale; +import java.util.PropertyResourceBundle; +import java.util.ResourceBundle; +import org.apache.fesod.sheet.exception.NoSuchMessageException; +import org.apache.fesod.sheet.testkit.Tags; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +/** + * Tests {@link ResourceBundleMessageSource}. + */ +@Tag(Tags.UNIT) +class ResourceBundleMessageSourceTest { + + private static final String BASENAME = "i18n.messages"; + private static final String EXTRA_BASENAME = "i18n.extra"; + private static final String MISSING_BASENAME = "i18n.does-not-exist"; + + private static final String GREETING = "fesod.msg.greeting"; + private static final String TARGET = "fesod.msg.target"; + private static final String PLACEHOLDER_GREETING = "fesod.msg.placeholder.greeting"; + private static final String CIRCULAR_A = "fesod.msg.circular.a"; + private static final String ESCAPED = "fesod.msg.escaped"; + private static final String EXTRA_ONLY = "fesod.msg.extra.only"; + private static final String UNKNOWN_CODE = "fesod.msg.unknown"; + + private ResourceBundleMessageSource source; + + @BeforeEach + void setUp() { + Locale.setDefault(Locale.ENGLISH); + // Ensure UTF-8 encoding when loading properties files on JDK 8 (which defaults to ISO-8859-1) + source = new ResourceBundleMessageSource(BASENAME, new UTF8ResourceBundleControl()); + } + + @Test + void shouldResolveMessageFromBundleMatchingRequestedLocale() { + Assertions.assertEquals("Hi", source.getMessage(GREETING, Locale.US)); + Assertions.assertEquals("你好", source.getMessage(GREETING, Locale.SIMPLIFIED_CHINESE)); + } + + @Test + void shouldFallBackToRootBundleWhenNoVariantMatchesLocale() { + Assertions.assertEquals("Hello", source.getMessage(GREETING, Locale.GERMAN)); + } + + @Test + void shouldInterpolateReferencedMessagesFromBundle() { + Assertions.assertEquals("Hello World", source.getMessage(PLACEHOLDER_GREETING, Locale.ENGLISH)); + } + + @Test + void shouldKeepEscapedPlaceholderLiteral() { + Assertions.assertEquals("Hello {" + TARGET + "}", source.getMessage(ESCAPED, Locale.ENGLISH)); + } + + @Test + void shouldFailOnCircularMessageReferences() { + Assertions.assertThrows(IllegalArgumentException.class, () -> source.getMessage(CIRCULAR_A, Locale.ENGLISH)); + } + + @Test + void shouldResolveMessagesFromConfiguredBaseName() { + ResourceBundleMessageSource extraSource = new ResourceBundleMessageSource(EXTRA_BASENAME); + + Assertions.assertEquals("Extra", extraSource.getMessage(GREETING, Locale.ENGLISH)); + Assertions.assertEquals("OnlyInExtra", extraSource.getMessage(EXTRA_ONLY, Locale.ENGLISH)); + } + + @Test + void shouldThrowNoSuchMessageExceptionWhenBundleIsMissing() { + ResourceBundleMessageSource missingSource = new ResourceBundleMessageSource(MISSING_BASENAME); + + Assertions.assertThrows(NoSuchMessageException.class, () -> missingSource.getMessage(GREETING, Locale.ENGLISH)); + } + + @Test + void shouldReturnCodeItselfWhenMissingAndCodeAsDefaultEnabled() { + ResourceBundleMessageSource defaultingSource = new ResourceBundleMessageSource(BASENAME, true); + + Assertions.assertEquals(UNKNOWN_CODE, defaultingSource.getMessage(UNKNOWN_CODE, Locale.ENGLISH)); + } + + public static class UTF8ResourceBundleControl extends ResourceBundle.Control { + + @Override + public ResourceBundle newBundle( + String baseName, Locale locale, String format, ClassLoader loader, boolean reload) + throws IllegalAccessException, InstantiationException, IOException { + if (format.equals("java.properties")) { + String bundleName = toBundleName(baseName, locale); + final String resourceName = toResourceName(bundleName, "properties"); + InputStream is = null; + if (reload) { + URL url = loader.getResource(resourceName); + if (url != null) { + URLConnection connection = url.openConnection(); + if (connection != null) { + connection.setUseCaches(false); + is = connection.getInputStream(); + } + } + } else { + is = loader.getResourceAsStream(resourceName); + } + + if (is == null) { + return null; + } + try (Reader reader = new InputStreamReader(is, StandardCharsets.UTF_8)) { + return new PropertyResourceBundle(reader); + } + } + return super.newBundle(baseName, locale, format, loader, reload); + } + } +} diff --git a/fesod-sheet/src/test/java/org/apache/fesod/sheet/i18n/StaticMessageSourceTest.java b/fesod-sheet/src/test/java/org/apache/fesod/sheet/i18n/StaticMessageSourceTest.java new file mode 100644 index 000000000..46ecca6e7 --- /dev/null +++ b/fesod-sheet/src/test/java/org/apache/fesod/sheet/i18n/StaticMessageSourceTest.java @@ -0,0 +1,115 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.fesod.sheet.i18n; + +import java.util.Locale; +import org.apache.fesod.sheet.exception.NoSuchMessageException; +import org.apache.fesod.sheet.testkit.Tags; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +/** + * Tests {@link StaticMessageSource}. + */ +@Tag(Tags.UNIT) +class StaticMessageSourceTest { + + private static final String GREETING = "msg.greeting"; + private static final String NAME = "msg.name"; + private static final String PRODUCT = "msg.product"; + private static final String CIRCULAR_A = "msg.circular.a"; + private static final String CIRCULAR_B = "msg.circular.b"; + + private StaticMessageSource source; + + @BeforeEach + void setUp() { + Locale.setDefault(Locale.ENGLISH); + + source = new StaticMessageSource(); + } + + @Test + void shouldResolveMessageRegisteredForRequestedLocale() { + source.addMessage(GREETING, "Hello!", Locale.ENGLISH); + + Assertions.assertEquals("Hello!", source.getMessage(GREETING, Locale.ENGLISH)); + } + + @Test + void shouldResolveDefaultLocaleMessageWhenLocaleOmitted() { + source.addMessage(GREETING, "Hello!"); + + Assertions.assertEquals("Hello!", source.getMessage(GREETING, null)); + } + + @Test + void shouldRejectCodeRegisteredOnlyForAnotherLocale() { + source.addMessage(GREETING, "Ni hao!", Locale.SIMPLIFIED_CHINESE); + + Assertions.assertThrows(NoSuchMessageException.class, () -> source.getMessage(GREETING, Locale.ENGLISH)); + } + + @Test + void shouldReturnCodeItselfWhenMissingAndCodeAsDefaultEnabled() { + source.setUseCodeAsDefaultMessage(true); + + Assertions.assertEquals(GREETING, source.getMessage(GREETING, Locale.ENGLISH)); + } + + @Test + void shouldResolveNestedMessageValueReferences() { + source.addMessage(GREETING, "Hello {msg.name}!", Locale.ENGLISH); + source.addMessage(NAME, "{msg.product} user", Locale.ENGLISH); + source.addMessage(PRODUCT, "fesod", Locale.ENGLISH); + + Assertions.assertEquals("Hello fesod user!", source.getMessage(GREETING, Locale.ENGLISH)); + } + + @Test + void shouldInterpolateTemplateWithMessageReferences() { + source.addMessage(NAME, "fesod", Locale.ENGLISH); + + Assertions.assertEquals("Hello fesod!", source.interpolate("Hello {" + NAME + "}!", Locale.ENGLISH)); + } + + @Test + void shouldFailFastOnNestedParametersInTemplate() { + Assertions.assertThrows( + IllegalArgumentException.class, () -> source.interpolate("Hello {msg.{msg.kind}}!", Locale.ENGLISH)); + } + + @Test + void shouldFailFastOnNestedParametersInResolvedMessage() { + source.addMessage(GREETING, "Hi {user.{name}}!", Locale.ENGLISH); + + Assertions.assertThrows(IllegalArgumentException.class, () -> source.getMessage(GREETING, Locale.ENGLISH)); + } + + @Test + void shouldFailOnCircularMessageReferences() { + source.addMessage(CIRCULAR_A, "{" + CIRCULAR_B + "}", Locale.ENGLISH); + source.addMessage(CIRCULAR_B, "{" + CIRCULAR_A + "}", Locale.ENGLISH); + + Assertions.assertThrows(IllegalArgumentException.class, () -> source.getMessage(CIRCULAR_A, Locale.ENGLISH)); + } +} diff --git a/fesod-sheet/src/test/resources/i18n/extra.properties b/fesod-sheet/src/test/resources/i18n/extra.properties new file mode 100644 index 000000000..ac80ac273 --- /dev/null +++ b/fesod-sheet/src/test/resources/i18n/extra.properties @@ -0,0 +1,19 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +fesod.msg.greeting=Extra +fesod.msg.extra.only=OnlyInExtra diff --git a/fesod-sheet/src/test/resources/i18n/messages.properties b/fesod-sheet/src/test/resources/i18n/messages.properties new file mode 100644 index 000000000..bb00e4f27 --- /dev/null +++ b/fesod-sheet/src/test/resources/i18n/messages.properties @@ -0,0 +1,23 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +fesod.msg.greeting=Hello +fesod.msg.target=World +fesod.msg.placeholder.greeting=Hello {fesod.msg.target} +fesod.msg.circular.a={fesod.msg.circular.b} +fesod.msg.circular.b={fesod.msg.circular.a} +fesod.msg.escaped=Hello \\{fesod.msg.target} diff --git a/fesod-sheet/src/test/resources/i18n/messages_en_US.properties b/fesod-sheet/src/test/resources/i18n/messages_en_US.properties new file mode 100644 index 000000000..313a63fc0 --- /dev/null +++ b/fesod-sheet/src/test/resources/i18n/messages_en_US.properties @@ -0,0 +1,18 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +fesod.msg.greeting=Hi diff --git a/fesod-sheet/src/test/resources/i18n/messages_zh_CN.properties b/fesod-sheet/src/test/resources/i18n/messages_zh_CN.properties new file mode 100644 index 000000000..99252442b --- /dev/null +++ b/fesod-sheet/src/test/resources/i18n/messages_zh_CN.properties @@ -0,0 +1,18 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +fesod.msg.greeting=你好