Reputation: 73
I have set up i18n in Spring MVC 3, and it is working correctly. There are several files, each with its own language: messages_en.properties, messages_de.properties, etc.
In one of my JSPs, I need to show the users a combo with all available languages, and I would like this list to be dynamic i.e. generated on the fly from the existing language files in the server.
Is there any built-in method to generate this list? Or do I have to resort to check the folder where the language files reside and parse them?
Thanks!
Nacho
Upvotes: 6
Views: 7658
Reputation: 211
Idea is following: create test
variable in your .properties
file and content of this variable must be ISO country name (2 symbols e.g: Russia=RU). For default locale you can set test=DEFAUL
.
Folder structure and resource bundle content:
{
public static void main(String[] args) {
Locale[] availableLocales = Locale.getAvailableLocales();
List<Locale> existingLocales = new ArrayList<>();
existingLocales.add(Locale.getDefault());
for (Locale value : availableLocales) {
if (isLocaleExist(value)) {
existingLocales.add(value);
}
}
for (Locale value : existingLocales) {
System.out.println(value);
}
}
public static boolean isLocaleExist(Locale locale) {
ResourceBundle bundle = ResourceBundle.getBundle("languages.but", locale);
final String value = bundle.getString("test");
return value.equals(locale.getCountry());
}
}
Upvotes: 1
Reputation: 1554
Hope this helps, if anyone is still looking for a concise answer:
import org.springframework.core.io.Resource;
@Configuration
class LanguageConfig {
private final Set<Locale> availableLocals;
public LanguageConfig(@Value("classpath:messages_*.properties") final Resource[] localesResources) {
availableLocals = getAvailableLocalesFromResources(localesResources);
}
private Set<Locale> getAvailableLocalesFromResources(Resource[] localesResources) {
return Arrays.stream(localesResources).map(resource -> {
final String localeCode = resource.getFilename().split("messages_")[1].split(".properties")[0];
return Locale.forLanguageTag(localeCode);
}).collect(Collectors.toSet());
}
}
The idea is to Autowire
all available message sources messages_*.properties
and derive the available locales depending on the filenames.
The default locale could be marked separately as supported like so:
availableLocals.add(Locale.getDefault()); // for default messages.properties
Upvotes: 0
Reputation: 307
I wish to share with you my solution.
The validated response (with the two solutions) of the current question is really interresting. The only problem on the first solution is to use a hard coded message key ("currentLanguage") that can disappear from the corresponding properties file. The second one needs to hard code the basename ("fr-messages_") of the properties file. But the file name can be changed...
So, I followed the example of the validated response to extend my custom ResourceBundleMessageSource to do that.
Initialy, I needed to get the content of the Spring message properties files (messages_en.properties, messages_fr.properties, ...) because I have a full Javascript front end (using ExtJs). So, I needed to load all the (internationalized) labels of the application on a JS object. But it doesn't exist... For this reason, I have developed a custom ReloadableResourceBundleMessageSource class. The corresponding methods are "getAllProperties()", "getAllPropertiesAsMap()" and "getAllPropertiesAsMessages()".
Later, I needed to get the available Locales on the application. And reading this stackoverflow page, I had the idea to extend my ReloadableResourceBundleMessageSource class to do that. You can see the "getAvailableLocales()" and "isAvailableLocale()" (to test just one Locale) methods.
package fr.ina.archibald.web.support;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Properties;
import java.util.Set;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.lang3.LocaleUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import org.springframework.util.ReflectionUtils;
import fr.ina.archibald.commons.util.StringUtils;
import fr.ina.archibald.entity.MessageEntity;
/**
* Custom {@link org.springframework.context.support.ReloadableResourceBundleMessageSource}.
*
* @author srambeau
*/
public class ReloadableResourceBundleMessageSource extends org.springframework.context.support.ReloadableResourceBundleMessageSource {
private static final Logger LOGGER = LoggerFactory.getLogger(ReloadableResourceBundleMessageSource.class);
private static final String PROPERTIES_SUFFIX = ".properties";
private static final String XML_SUFFIX = ".xml";
private Set<Locale> cacheAvailableLocales;
private Set<Resource> cacheResources;
/**
* Returns all messages for the specified {@code Locale}.
*
* @param locale the {@code Locale}.
*
* @return a {@code Properties} containing all the expected messages or {@code null} if the {@code locale} argument is null or if the properties are empty.
*/
public Properties getAllProperties(final Locale locale) {
if(locale == null) {
LOGGER.debug("Cannot get all properties. 'locale' argument is null.");
return null;
}
return getMergedProperties(locale).getProperties();
}
/**
* Returns all messages for the specified {@code Locale}.
*
* @param locale the {@code Locale}.
*
* @return a {@code Map} containing all the expected messages or {@code null} if the {@code locale} argument is null or if the properties are empty.
*/
@SuppressWarnings({ "unchecked", "rawtypes" })
public Map<String, String> getAllPropertiesAsMap(final Locale locale) {
if(locale == null) {
LOGGER.debug("Cannot get all properties as Map. 'locale' argument is null.");
return null;
}
Properties props = getAllProperties(locale);
if(props == null) {
LOGGER.debug("Cannot get all properties as Map. The properties are missing.");
return null;
}
return new HashMap<String, String>((Map) props);
}
/**
* Returns all messages for the specified {@code Locale}.
*
* @param locale the {@code Locale}.
*
* @return a {@code List<MessageEntity>} containing all the expected messages or {@code null} if the {@code locale} argument is null or if the properties are empty.
*/
public List<MessageEntity> getAllPropertiesAsMessages(final Locale locale) {
if(locale == null) {
LOGGER.debug("Cannot get all properties as MessageEntity. 'locale' argument is null.");
return null;
}
Properties props = getAllProperties(locale);
if(props == null) {
LOGGER.debug("Cannot get all properties as MessageEntity. The properties are missing.");
return null;
}
Set<Entry<Object, Object>> propsSet = props.entrySet();
List<MessageEntity> messages = new ArrayList<MessageEntity>();
for(Entry<Object, Object> prop : propsSet) {
messages.add(new MessageEntity((String) prop.getKey(), (String) prop.getValue()));
}
return messages;
}
/**
* Returns the available {@code Locales} on the specified application context. Calculated from the Spring message files of the application context.
* <p>
* Example of Locales returned corresponding with the messages files defines on the application:
*
* <pre>
* messages_en.properties --> en
* messages_fr.properties --> fr
* messages_en.properties, messages_fr.properties --> en, fr
* </pre>
* </p>
*
* @return the set of {@code Locales} or null if an error occurs.
*/
public Set<Locale> getAvailableLocales() {
if(cacheAvailableLocales != null) {
return cacheAvailableLocales;
}
cacheAvailableLocales = getLocales(getAllFileNames(), getMessageFilePrefixes());
return cacheAvailableLocales;
}
/**
* Indicates if the specified {@code Locale} is available on the application.
* <p>
* Examples of results returned if the application contains the files "messages_en.properties" and "messages_fr.properties":
*
* <pre>
* en --> true
* fr --> true
* de --> false
* es --> false
* </pre>
*
* @param locale the {@code Locale}.
*
* @return {@code true} if the locale is available, {@code false} otherwise.
*/
public boolean isAvailableLocale(final Locale locale) {
Set<Locale> locales = getAvailableLocales();
if(locales == null) {
return false;
}
return locales.contains(locale);
}
// ********************** PRIVATE METHODES **********************
/**
* Returns the {@code Locales} specified on the file names.
*
* @param fileNames the file names.
* @param filePrefixes the basenames' prefixes of the resources bundles.
*
* @return the set of the {@code Locales}.
*/
private Set<Locale> getLocales(final List<String> fileNames, List<String> filePrefixes) {
if(fileNames == null || fileNames.isEmpty() || filePrefixes == null || filePrefixes.isEmpty()) {
LOGGER.debug("Cannot get available Locales. fileNames=[" + StringUtils.toString(fileNames) + "], filePrefixes=[" + StringUtils.toString(filePrefixes) + "]");
return null;
}
Set<Locale> locales = new HashSet<Locale>();
for(String fileName : fileNames) {
String fileNameWithoutExtension = FilenameUtils.getBaseName(fileName);
for(String filePrefixe : filePrefixes) {
String localeStr = fileNameWithoutExtension.substring(filePrefixe.length() + 1);
try {
locales.add(LocaleUtils.toLocale(localeStr));
} catch(IllegalArgumentException ex) {
continue;
}
}
}
return locales;
}
/**
* Returns all the file names of the resources bundles.
*
* @return the list of file names or {@code null} if the resources are missing.
*/
private List<String> getAllFileNames() {
Set<Resource> resources = getAllResources();
if(resources == null) {
LOGGER.debug("Missing resources bundles.");
return null;
}
List<String> filenames = new ArrayList<String>(resources.size());
for(Resource resource : resources) {
filenames.add(resource.getFilename());
}
return filenames;
}
/**
* Gets the array of the prefixes for messages files.
*
* <pre>
* "WEB-INF/messages" --> "messages"
* "classpath:config/i18n/messages" --> "messages"
* "messages" --> "messages"
* </pre>
*
* @return the array of the prefixes or null if an error occurs.
*/
private List<String> getMessageFilePrefixes() {
String[] basenames = getBasenames();
if(basenames == null) {
LOGGER.debug("Missing basenames of the resources bundles.");
return null;
}
List<String> prefixes = new ArrayList<String>(basenames.length);
for(int i = 0; i < basenames.length; ++i) {
prefixes.add(FilenameUtils.getName(basenames[i]));
}
return prefixes;
}
/**
* Returns all the resources bundles.
*
* @return the set of resources or null if {@code basenames} or the {@link ResourceLoader} is missing.
*/
private Set<Resource> getAllResources() {
if(cacheResources != null) {
return cacheResources;
}
String[] basenames = getBasenames();
if(basenames == null) {
LOGGER.debug("Missing basenames of the resources bundles.");
return null;
}
ResourceLoader resourceLoader = getResourceLoader();
if(resourceLoader == null) {
LOGGER.debug("Missing ResourceLoader.");
return null;
}
Set<Resource> resources = new HashSet<Resource>();
for(String basename : basenames) {
for(Locale locale : Locale.getAvailableLocales()) {
List<String> filenames = calculateFilenamesForLocale(basename, locale);
for(String filename : filenames) {
Resource resource = resourceLoader.getResource(filename + PROPERTIES_SUFFIX);
if( ! resource.exists()) {
resource = resourceLoader.getResource(filename + XML_SUFFIX);
}
if(resource.exists()) {
resources.add(resource);
}
}
}
}
cacheResources = resources;
return resources;
}
/**
* Gets the array of basenames, each following the basic ResourceBundle convention of not specifying file extension or language codes.
*
* @return the array of basenames or null if an error occurs.
*
* @see org.springframework.context.support.ReloadableResourceBundleMessageSource#setBasenames
*/
private String[] getBasenames() {
Field field = ReflectionUtils.findField(org.springframework.context.support.ReloadableResourceBundleMessageSource.class, "basenames");
if(field == null) {
LOGGER.debug("Missing field 'basenames' from 'org.springframework.context.support.ReloadableResourceBundleMessageSource' class.");
return null;
}
ReflectionUtils.makeAccessible(field);
try {
return (String[]) field.get(this);
} catch(Exception ex) {
LOGGER.debug("Unable to get the 'basenames' field value from the 'org.springframework.context.support.ReloadableResourceBundleMessageSource' class.");
return null;
}
}
/**
* Gets the resource loader.
*
* @return the resource loader.
*
* @see org.springframework.context.support.ReloadableResourceBundleMessageSource#setResourceLoader
*/
private ResourceLoader getResourceLoader() {
Field field = ReflectionUtils.findField(org.springframework.context.support.ReloadableResourceBundleMessageSource.class, "resourceLoader");
if(field == null) {
LOGGER.debug("Missing field 'resourceLoader' from 'org.springframework.context.support.ReloadableResourceBundleMessageSource' class.");
return null;
}
ReflectionUtils.makeAccessible(field);
try {
return (ResourceLoader) field.get(this);
} catch(Exception ex) {
LOGGER.debug("Unable to get the 'resourceLoader' field value from the 'org.springframework.context.support.ReloadableResourceBundleMessageSource' class.");
return null;
}
}
}
If you want to use the two functionnalities (get the available Locales and get all Spring messages from the properties files), so you need to get this complete class.
To use this ReloadableResourceBundleMessageSource, it is really simple. You need to declare the resource bundle :
<!-- Custom message source. -->
<bean id="messageSource" class="fr.ina.archibald.web.support.ReloadableResourceBundleMessageSource">
<property name="basename" value="classpath:config/i18n/messages" />
<property name="defaultEncoding" value="UTF-8" />
</bean>
Then, you just need to inject the resource bundle into the class where you want to get the available Locales:
@Inject
private ReloadableResourceBundleMessageSource resourceBundleMessageSource;
Here is a usage example to check if the Locale is available before automatically update the browsing Locale of the User on database when the Spring LocaleChangeInterceptor detect a change (via URL for example => 'http://your.domain?lang=en'):
package fr.ina.archibald.web.resolver;
import java.util.Locale;
import javax.inject.Inject;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import fr.ina.archibald.commons.annotation.Log;
import fr.ina.archibald.dao.entity.UserEntity;
import fr.ina.archibald.security.entity.CustomUserDetails;
import fr.ina.archibald.security.util.SecurityUtils;
import fr.ina.archibald.service.UserService;
import fr.ina.archibald.web.support.ReloadableResourceBundleMessageSource;
/**
* Custom SessionLocaleResolver.
*
* @author srambeau
*
* @see org.springframework.web.servlet.i18n.SessionLocaleResolver
*/
public class SessionLocaleResolver extends org.springframework.web.servlet.i18n.SessionLocaleResolver {
@Log
private Logger logger;
@Inject
private UserService userService;
@Inject
private ReloadableResourceBundleMessageSource resourceBundleMessageSource;
@Override
public void setLocale(HttpServletRequest req, HttpServletResponse res, Locale newLocale) {
super.setLocale(req, res, newLocale);
updateUserLocale(newLocale);
}
// /**
// * Returns the default Locale that this resolver is supposed to fall back to, if any.
// */
// @Override
// public Locale getDefaultLocale() {
// return super.getDefaultLocale();
// }
// ********************** PRIVATE METHODES **********************
/**
* Updates the locale of the currently logged in user with the new Locale.
* <p>
* The locale is not updated if the specified locale is {@code null} or the same as the previous, if the user is missing or if an error occurs.
* </p>
*
* @param newLocale the new locale.
*/
private void updateUserLocale(final Locale newLocale) {
if(newLocale == null) {
logger.debug("Cannot update the user's browsing locale. The new locale is null.");
return;
}
CustomUserDetails userDetails = SecurityUtils.getCurrentUser();
if(userDetails == null || userDetails.getUser() == null) {
logger.debug("Cannot update the user's browsing locale. The user is missing.");
return;
}
UserEntity user = userDetails.getUser();
// Updates the user locale if and only if the locale has changed and is available on the application.
if(newLocale.equals(user.getBrowsingLocale()) || ! resourceBundleMessageSource.isAvailableLocale(newLocale)) {
return;
}
user.setBrowsingLocale(newLocale);
try {
userService.update(user);
} catch(Exception ex) {
logger.error("The browsing locale of the user with identifier " + user.getUserId() + " cannot be updated.", ex);
}
}
}
The corresponding SessionLocaleResolver declaration:
<!-- This custom SessionLocaleResolver allows to update the user Locale when it change. -->
<bean id="localeResolver" class="fr.ina.archibald.web.resolver.SessionLocaleResolver">
<property name="defaultLocale" value="fr" />
</bean>
I hope this will be useful to you...
Enjoy! :-)
Upvotes: 0
Reputation: 73
Ok, two solutions found. For both, assume they are being executed inside a Spring MVC @Controller
-annotated class. Each will produce a HashMap (languages
) in which the key is the 2-letter ISO language code, and the value the language name (in the current Locale, which in these examples is a static variable called HSConstants.currentLocale
)
1.- The one submitted by @millhouse (see above/below), which works after a bit of tweaking:
HashMap languages = new HashMap();
final String defaultMessage = "NOT FOUND";
HashMap availableLocales = new HashMap();
for (Locale locale : Locale.getAvailableLocales()) {
String msg = rrbms.getMessage("currentLanguage", null, defaultMessage, locale);
if (!defaultMessage.equals(msg) && !availableLocales.containsKey(locale.getLanguage())){
availableLocales.put(locale.getLanguage(), locale);
}
}
for (String c : availableLocales.keySet()){
languages.put(c, availableLocales.get(c).getDisplayLanguage(HSConstants.currentLocale));
}
model.addAttribute("languages", languages);
This solution requires that, in each of your language .properties files, you set an entry with the language (in the example above, it would be 'currentLanguage'). For ecample, in messages_it.properties, there must be an entry like this: currentLanguage=Italiano
2.- Raw method, i.e. accesing the folder/files directly: assuming the files languages are in /WEB-INF/languages, and have a basename of fr-messages:
HashMap languages = new HashMap();
String languagesFolderPath = request.getSession().getServletContext().getRealPath("/WEB-INF/languages");
File folder = new File(languagesFolderPath);
File[] listOfFiles = folder.listFiles();
for (int i = 0; i < listOfFiles.length; i++){
String fileName = listOfFiles[i].getName();
if (fileName.startsWith("fr-messages_") && fileName.endsWith(".properties")){
// Extract the language code, which is between the underscore and the .properties extension
String language = fileName.substring(12, fileName.indexOf(".properties"));
Locale l = new Locale(language);
languages.put(language, l.getDisplayLanguage(HSConstants.currentLocale));
}
}
model.addAttribute("languages", languages);
And then, in your JSP, render the select box using the languages
map:
<select name="language">
<c:forEach items="${languages}" var="language">
<c:choose>
<c:when test="${platform.language == language.key}">
<option value="${language.key}" selected="SELECTED">${language.value}</option>
</c:when>
<c:otherwise>
<option value="${language.key}">${language.value}</option>
</c:otherwise>
</c:choose>
</c:forEach>
</select>
Upvotes: 0
Reputation: 10007
How about putting this into something that has access to the ReloadableResourceBundleMessageSource
?
ReloadableResourceBundleMessageSource rrbms = getMessageSource();
final String defaultMessage = "NOT FOUND";
List<Locale> availableLocales = new ArrayList<Locale>();
for (Locale locale : Locale.getAvailableLocales()) {
String msg = rrbms.getMessage("test.code", null, defaultMessage, locale);
if (!defaultMessage.equals(msg)) {
availableLocales.add(locale);
}
}
Just make sure each supported language supplies a test.code
value and you're done.
Upvotes: 1
Reputation: 10007
It would be a nice feature, but I don't think you'll find a built-in method because the "fall-through" mechanism of properties files means that having a messages_de.properties doesn't necessarily mean every message is available in German. So Spring can't build up a nice Map<Locale, ResourceBundle>
from which you could obtain the keys.
You should be able to use Spring to make your job easier though, and not have to hit the filesystem yourself:
ResourceBundle
ClassLoader
from Spring's ResourceBundleMessageSourceClassLoader
to list all the "messages" resources:Enumeration<URL> allMsgs = bundleClassLoader.findResources("messages");
Enumeration
, getting the locale (en
, de
etc) part from each URL
Upvotes: 0