Preface
The company has transferred the project from Struts2 to Springmvc. Since the company's business is overseas services, the demand for international functions is very high. The internationalization function of Struts2 is more perfect than Springmvc, but the big feature of spring is that it is customizable and customized, so its internationalization function is added when the company's project is transplanted to Springmvc. I have compiled the records and improved them here.
The main functions implemented in this article:
Directly load multiple international files from a folder. The background setting front-end page displays international information. The file that displays international information is automatically set using interceptors and annotations. The file that displays international information on the front-end page displays international information.
Note: This article does not introduce in detail how to configure internationalization, regional parser, etc.
accomplish
International project initialization
First create a basic Spring-Boot+thymeleaf+internationalization information (message.properties) project. If you need it, you can download it from my Github.
A brief look at the directory and files of the project
Among them, I18nApplication.java sets a CookieLocaleResolver , which uses cookies to control international languages. Also set up a LocaleChangeInterceptor interceptor to intercept changes in international languages.
@SpringBootApplication@Configurationpublic class I18nApplication { public static void main(String[] args) { SpringApplication.run(I18nApplication.class, args); } @Bean public LocaleResolver localeResolver() { CookieLocaleResolver slr = new CookieLocaleResolver(); slr.setCookieMaxAge(3600); slr.setCookieName("Language");//Set the name of the stored cookie to Language return slr; } @Bean public WebMvcConfigurer webMvcConfigurer() { return new WebMvcConfigurer() { //Interceptor@Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new LocaleChangeInterceptor()).addPathPatterns("/**"); } }; }}Let's take a look at what is written in hello.html:
<!DOCTYPE html><html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"><head> <title>Hello World!</title></head><body><h1 th:text="#{i18n_page}"></h1><h3 th:text="#{hello}"></h3></body></html> Now start the project and visit http://localhost:9090/hello (I set the port to 9090 in application.properties).
Since the default language of the browser is Chinese, it will default to search in messages_zh_CN.properties, and if not, it will search in messages.properties for internationalized words.
Then we enter http://localhost:9090/hello?locale=en_US in the browser and the language will be cut to English. Similarly, if the parameter after url is set to locale=zh_CH , the language will be cut to Chinese.
Load multiple international files directly from a folder
In our hello.html page, there are only two international information 'i18n_page' and 'hello'. However, in actual projects, there will definitely not be as small as a few international information, usually hundreds of them. Then we must not put so much international information in a file with messages.properties , and usually the international information is classified and stored in several files. However, when the project becomes larger, these international files will become more and more. At this time, it is inconvenient to configure this file one by one in the application.properties file. So now we implement a function to automatically load all international files in the formulation directory.
Inherit ResourceBundleMessageSource
Create a class under the project to inherit ResourceBundleMessageSource or ReloadableResourceBundleMessageSource and name it MessageResourceExtension . And inject it into the bean and named messageSource , where we inherit ResourceBundleMessageSource.
@Component("messageSource")public class MessageResourceExtension extends ResourceBundleMessageSource {} Note that our Component name must be 'messageSource', because when initializing ApplicationContext , the bean with the bean named 'messageSource' will be looked up. This process is in AbstractApplicationContext.java . Let's take a look at the source code
/*** Initialize the MessageSource.* Use parent's if none defined in this context.*/protected void initMessageSource() { ConfigurableListableBeanFactory beanFactory = getBeanFactory(); if (beanFactory.containsLocalBean(MESSAGE_SOURCE_BEAN_NAME)) { this.messageSource = beanFactory.getBean(MESSAGE_SOURCE_BEAN_NAME, MessageSource.class); ... }}... In this method of initializing MessageSource, beanFactory looks for beans injected with the name MESSAGE_SOURCE_BEAN_NAME(messageSource) . If not found, it will look for whether there is a bean with the name in its parent class.
Implement file loading
Now we can start MessageResourceExtension we just created
The method to load the file is written.
@Component("messageSource")public class MessageResourceExtension extends ResourceBundleMessageSource { private final static Logger logger = LoggerFactory.getLogger(MessageResourceExtension.class); /** * Specified internationalization file directory*/ @Value(value = "${spring.messages.baseFolder:i18n}") private String baseFolder; /** * Internationalization file specified by parent MessageSource*/ @Value(value = "${spring.messages.basename:message}") private String basename; @PostConstruct public void init() { logger.info("init MessageResourceExtension..."); if (!StringUtils.isEmpty(baseFolder)) { try { this.setBasenames(getAllBaseNames(baseFolder)); } catch (IOException e) { logger.error(e.getMessage()); } } //Set parent MessageSource ResourceBundleMessageSource parent = new ResourceBundleMessageSource(); parent.setBasename(basename); this.setParentMessageSource(parent); } /** * Get all international file names in the folder* * @param folderName File name* @return * @throws IOException */ private String[] getAllBaseNames(String folderName) throws IOException { Resource resource = new ClassPathResource(folderName); File file = resource.getFile(); List<String> baseNames = new ArrayList<>(); if (file.exists() && file.isDirectory()) { this.getAllFile(baseNames, file, ""); } else { logger.error("The specified baseFile does not exist or is not a folder"); } return baseNames.toArray(new String[baseNames.size()]); } /** * traverse all files* * @param basenames * @param folder * @param path */ private void getAllFile(List<String> basenames, File folder, String path) { if (folder.isDirectory()) { for (File file : folder.listFiles()) { this.getAllFile(basenames, file, path + folder.getName() + File.separator); } } else { String i18Name = this.getI18FileName(path + folder.getName()); if (!basenames.contains(i18Name)) { basenames.add(i18Name); } } } /** * Convert normal file names to international file names* * @param filename * @return */ private String getI18FileName(String filename) { filename = filename.replace(".properties", ""); for (int i = 0; i < 2; i++) { int index = filename.lastIndexOf("_"); if (index != -1) { filename = filename.substring(0, index); } } return filename; }}Explain several methods in turn.
@PostConstruct annotation on init() method, which will automatically call init() method after the MessageResourceExtension class is instantiated. This method gets all the internationalization files in baseFolder directory and sets them to basenameSet . And set a ParentMessageSource , which will call the parent MessageSource to find the international information when the international information cannot be found.getAllBaseNames() method gets the path to baseFolder , and then calls getAllFile() method to get the file names of all international files in the directory.getAllFile() traverses the directory, if it is a folder, continues to traverse, if it is a file, call getI18FileName() to convert the file name to an international resource name in the format 'i18n/basename/'. So simply put it simply, after MessageResourceExtension is instantiated, the name of the resource file under the 'i18n' folder is loaded into Basenames . Now let’s see the effect.
First, we add a spring.messages.baseFolder=i18n to the application.properties file, which will assign the value 'i18n' to baseFolder in MessageResourceExtension .
After starting, I saw that the init information was printed in the console, indicating that the init() method annotated by @PostConstruct has been executed.
Then we create two sets of international information files: 'dashboard' and 'merchant', which only has one international information: 'dashboard.hello' and 'merchant.hello' respectively.
Then modify the hello.html file and then visit the hello page.
...<body><h1>Internationalization page!</h1><p th:text="#{hello}"></p><p th:text="#{merchant.hello}"></p><p th:text="#{dashboard.hello}"></p></body>...You can see that the internationalization information in 'message', 'dashboard' and 'merchant' is loaded in the web page, indicating that we have successfully loaded the file under the 'i18n' folder at one time.
Files that display international information on the front-end page of the background settings
In the previous section, we successfully loaded multiple internationalization files and displayed their internationalization information. But the internationalization information in 'dashboard.properties' is 'dashboard.hello' and 'merchant.properties' is 'merchant.hello'. So it would be very troublesome to write a prefix for each. Now I want to only write 'hello' in the internationalization files of 'dashboard' and 'merchant', but the internationalization information in 'dashboard' or 'merchant' is displayed.
Rewrite resolveCodeWithoutArguments method in MessageResourceExtension (rewrite resolveCode if there is a need for character formatting).
@Component("messageSource")public class MessageResourceExtension extends ResourceBundleMessageSource { ... public static String I18N_ATTRIBUTE = "i18n_attribute"; @Override protected String resolveCodeWithoutArguments(String code, Locale locale) { // Get the specified international file name set in the request ServletRequestAttributes attr = (ServletRequestAttributes) RequestContextHolder.currentRequestAttributes(); final String i18File = (String) attr.getAttribute(I18N_ATTRIBUTE, RequestAttributes.SCOPE_REQUEST); if (!StringUtils.isEmpty(i18File)) { //Get the international file name matched in the basenameSet String basename = getBasenameSet().stream() .filter(name -> StringUtils.endsWithIgnoreCase(name, i18File)) .findFirst().orElse(null); if (!StringUtils.isEmpty(basename)) { //Get the specified international file resource ResourceBundle bundle = getResourceBundle(basename, locale); if (bundle != null) { return getStringOrNull(bundle, code); } } } //If there is no internationalization field in the specified i18 folder, returning null will look for return null in ParentMessageSource; } ...} In the resolveCodeWithoutArguments method we rewritten, get 'I18N_ATTRIBUTE' from HttpServletRequest (will talk about where to set this) corresponding to the international file name we want to display. Then we look up the file in BasenameSet , then get the resource through getResourceBundle , and finally getStringOrNull to get the corresponding international information.
Now let's add two methods to our HelloController .
@Controllerpublic class HelloController { @GetMapping("/hello") public String index(HttpServletRequest request) { request.setAttribute(MessageResourceExtension.I18N_ATTRIBUTE, "hello"); return "system/hello"; } @GetMapping("/dashboard") public String dashboard(HttpServletRequest request) { request.setAttribute(MessageResourceExtension.I18N_ATTRIBUTE, "dashboard"); return "dashboard"; } @GetMapping("/merchant") public String merchant(HttpServletRequest request) { request.setAttribute(MessageResourceExtension.I18N_ATTRIBUTE, "merchant"); return "merchant"; }} See that we set a corresponding 'I18N_ATTRIBUTE' in each method, which will set the corresponding internationalization file in each request and then get it in MessageResourceExtension .
At this time, we look at our internationalization file and we can see that all keywords are 'hello', but the information is different.
At the same time, two new html files are 'dashboard.html' and 'merchant.html', which only has an international information for 'hello' and a title for distinguishing.
<!-- This is hello.html --><body><h1>Internationalization page!</h1><p th:text="#{hello}"></p></body> <!-- This is dashboard.html --><body><h1>Internationalization page (dashboard)!</h1><p th:text="#{hello}"></p></body> <!-- This is merchant.html --><body><h1>Internationalization page (merchant)!</h1><p th:text="#{hello}"></p></body>At this time, let’s start the project and take a look.
You can see that although the internationalization word on each page is 'hello', we display the information we want to display on the corresponding page.
Use interceptors and annotations to automatically set up files that display international information on the front-end page
Although the corresponding internationalization information can be specified, it is too troublesome to set the internationalization file in the HttpServletRequest in each controller, so now we implement automatic judgment to display the corresponding file.
First we create an annotation, which can be placed on a class or a method.
@Target({ElementType.TYPE, ElementType.METHOD})@Retention(RetentionPolicy.RUNTIME)public @interface I18n { /** * Internationalized file name*/ String value();} Then we put the I18n annotation created in the Controller method just now. In order to display its effect, we create a ShopController and UserController , and also create the corresponding 'shop' and 'user' international files, and the content is also a 'hello'.
@Controllerpublic class HelloController { @GetMapping("/hello") public String index() { return "system/hello"; } @I18n("dashboard") @GetMapping("/dashboard") public String dashboard() { return "dashboard"; } @I18n("merchant") @GetMapping("/merchant") public String merchant() { return "merchant"; }} @I18n("shop")@Controllerpublic class ShopController { @GetMapping("shop") public String shop() { return "shop"; }} @Controllerpublic class UserController { @GetMapping("user") public String user() { return "user"; }} We place I18n annotation under dashboard and merchant methods under HelloController , and on ShopController class respectively. And the statement that sets 'I18N_ATTRIBUTE' under the original dashboard and merchant methods is removed.
The preparations are all done, now see how to automatically specify internationalization files based on these annotations.
public class MessageResourceInterceptor implements HandlerInterceptor { @Override public void postHandle(HttpServletRequest req, HttpServletResponse rep, Object handler, ModelAndView modelAndView) { // Set the i18 path in the method if (null != req.getAttribute(MessageResourceExtension.I18N_ATTRIBUTE)) { return; } HandlerMethod method = (HandlerMethod) handler; // annotation on the method I18 I18n i18nMethod = method.getMethodAnnotation(I18n.class); if (null != i18nMethod) { req.setAttribute(MessageResourceExtension.I18N_ATTRIBUTE, i18nMethod.value()); return; } // annotation on the Controller I18n i18nController = method.getBeanType().getAnnotation(I18n.class); if (null != i18nController) { req.setAttribute(MessageResourceExtension.I18N_ATTRIBUTE, i18nController.value()); return; } // Set i18 String controller = method.getBeanType().getName(); int index = controller.lastIndexOf("."); if (index != -1) { controller = controller.substring(index + 1, controller.length()); } index = controller.toUpperCase().indexOf("CONTROLLER"); if (index != -1) { controller = controller.substring(index + 1, controller.length()); } index = controller.toUpperCase().indexOf("CONTROLLER"); if (index != -1) { controller = controller.substring(0, index); } req.setAttribute(MessageResourceExtension.I18N_ATTRIBUTE, controller); } @Override public boolean preHandle(HttpServletRequest req, HttpServletResponse rep, Object handler) { // When jumping to this method, first clear the internationalization information in the request req.removeAttribute(MessageResourceExtension.I18N_ATTRIBUTE); return true; }}Let me briefly explain this interceptor.
First, if there is already 'I18N_ATTRIBUTE' in the request, it means that the settings are specified in the Controller method, and no longer judgement is made.
Then determine whether there is an I18n annotation on the method to enter the interceptor. If there is, set 'I18N_ATTRIBUTE' into the request and exit the interceptor. If there is no, continue.
Then determine whether there is an I18n annotation on the class that entered the intercept. If there is, set 'I18N_ATTRIBUTE' into the request and exit the interceptor. If there is no, continue.
Finally, if there is no I18n annotation on the method and class, we can automatically set the specified internationalization file according to the Controller name. For example, 'UserController', then we will look for the 'user' internationalization file.
Now let’s run it again to see the effect and see the content in their corresponding international information displayed on each link.
at last
I just completed the basic functions of our entire internationalization enhancement. Finally, I sorted out all the code and integrated bootstrap4 to show the implementation effect of the function.
For detailed code, please see the code of Spring-Boot-I18n-Pro on Github
The above is all the content of this article. I hope it will be helpful to everyone's learning and I hope everyone will support Wulin.com more.