SpringBoot i18n Basic

Keywords: Java

1. Description

Recent demand is for project internationalization, and then preliminary research on this internationalization, the following is to collate the content of the research.

An internationalization scheme with front-end and back-end separation, where each interface adds parameters and the code becomes redundant, it is best to add language variables to the request header. In addition to the request header, cookies can also be implemented through cookies, but some clients do not support cookies and cookies are at risk of being tampered with, so a standard request header is more recommended.

The specific process is as follows:

1, the client adds the request header Accept-Language.

2. Server configures international resource files.

3. The server uses the current requested Acceptt-Language value to decide which region's language to use, finds the corresponding resource file, formats it, and returns it to the client.

2. Basic Use

Related Dependencies

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-web</artifactId>
</dependency>

Configure Resouce bundle messages

Testing code

@RestController
public class LanguageController {

    @Autowired
    private MessageSource messageSource;

    @GetMapping("/ping")
    public String ping(){
        return "pong";
    }

    @GetMapping("/currentLanguage")
    public String currentLanguage(){
        return LanguageUtil.getCurrentLang();
    }

    @GetMapping("/i18n")
    public String i18n(){
        return messageSource.getMessage("title", new Object[]{"A","B"}, LocaleContextHolder.getLocale());
    }
}

3. Analysis of Basic Understanding

SpringBoot's support for internationalization is done by default through the AcceptHeaderLocaleResolver parser, which defaults to the Accept-Language field in the request header to determine the current request's context and then to provide an appropriate response. SpringMVC also provides a variety of internationalization implementations.

MessageSource

Spring defines the MessageSource interface for accessing internationalized information.

MessageSourceAutoConfiguration

First, the MessageSource configuration takes effect by a ResourceBundleCondition condition, reading the value of spring.messages.basename from the environment variable, default value is messages, this value is the name of the resource file corresponding to MessageSource, the extension of the resource file is.properties, and then using PathMatchingResourcePatternResolver from "classpath*:"The corresponding resource file is read in the directory, and the configuration class is loaded if the resource file can be read normally.

@Configuration
@ConditionalOnMissingBean(value = MessageSource.class, search = SearchStrategy.CURRENT)
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE)
@Conditional(ResourceBundleCondition.class)
@EnableConfigurationProperties
public class MessageSourceAutoConfiguration {

	private static final Resource[] NO_RESOURCES = {};

	@Bean
	@ConfigurationProperties(prefix = "spring.messages")
	public MessageSourceProperties messageSourceProperties() {
		return new MessageSourceProperties();
	}

	@Bean
	public MessageSource messageSource(MessageSourceProperties properties) {
		ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
		if (StringUtils.hasText(properties.getBasename())) {
			messageSource.setBasenames(StringUtils.commaDelimitedListToStringArray(
					StringUtils.trimAllWhitespace(properties.getBasename())));
		}
		if (properties.getEncoding() != null) {
			messageSource.setDefaultEncoding(properties.getEncoding().name());
		}
		messageSource.setFallbackToSystemLocale(properties.isFallbackToSystemLocale());
		Duration cacheDuration = properties.getCacheDuration();
		if (cacheDuration != null) {
			messageSource.setCacheMillis(cacheDuration.toMillis());
		}
		messageSource.setAlwaysUseMessageFormat(properties.isAlwaysUseMessageFormat());
		messageSource.setUseCodeAsDefaultMessage(properties.isUseCodeAsDefaultMessage());
		return messageSource;
	}

	protected static class ResourceBundleCondition extends SpringBootCondition {

		private static ConcurrentReferenceHashMap<String, ConditionOutcome> cache = new ConcurrentReferenceHashMap<>();

		@Override
		public ConditionOutcome getMatchOutcome(ConditionContext context,
				AnnotatedTypeMetadata metadata) {
			String basename = context.getEnvironment()
					.getProperty("spring.messages.basename", "messages");
			ConditionOutcome outcome = cache.get(basename);
			if (outcome == null) {
				outcome = getMatchOutcomeForBasename(context, basename);
				cache.put(basename, outcome);
			}
			return outcome;
		}

		private ConditionOutcome getMatchOutcomeForBasename(ConditionContext context,
				String basename) {
			ConditionMessage.Builder message = ConditionMessage
					.forCondition("ResourceBundle");
			for (String name : StringUtils.commaDelimitedListToStringArray(
					StringUtils.trimAllWhitespace(basename))) {
				for (Resource resource : getResources(context.getClassLoader(), name)) {
					if (resource.exists()) {
						return ConditionOutcome
								.match(message.found("bundle").items(resource));
					}
				}
			}
			return ConditionOutcome.noMatch(
					message.didNotFind("bundle with basename " + basename).atAll());
		}

		private Resource[] getResources(ClassLoader classLoader, String name) {
			String target = name.replace('.', '/');
			try {
				return new PathMatchingResourcePatternResolver(classLoader)
						.getResources("classpath*:" + target + ".properties");
			}
			catch (Exception ex) {
				return NO_RESOURCES;
			}
		}

	}

}

The springmvc auto-assembly configuration class registers a RequestContextFilter filter that saves localization information for the current request for each request in the filter.

@Configuration(
    proxyBeanMethods = false
)
@ConditionalOnWebApplication(
    type = Type.SERVLET
)
@ConditionalOnClass({Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class})
@ConditionalOnMissingBean({WebMvcConfigurationSupport.class})
@AutoConfigureOrder(-2147483638)
@AutoConfigureAfter({DispatcherServletAutoConfiguration.class, TaskExecutionAutoConfiguration.class, ValidationAutoConfiguration.class})
public class WebMvcAutoConfiguration {
   
    @Bean
    @ConditionalOnMissingBean({RequestContextListener.class, RequestContextFilter.class})
    @ConditionalOnMissingFilterBean({RequestContextFilter.class})
    public static RequestContextFilter requestContextFilter() {
        return new OrderedRequestContextFilter();
    }
  

}

public class RequestContextFilter extends OncePerRequestFilter {

    private void initContextHolders(HttpServletRequest request, ServletRequestAttributes requestAttributes) {
        LocaleContextHolder.setLocale(request.getLocale(), this.threadContextInheritable);
        RequestContextHolder.setRequestAttributes(requestAttributes, this.threadContextInheritable);
        if (this.logger.isTraceEnabled()) {
            this.logger.trace("Bound request context to thread: " + request);
        }

    }

}

When using MessageSourceAccessor to get specific information based on code, if the localized object configured by default is empty, it is obtained through LocaleContextHolder.

public class MessageSourceAccessor {

    private final MessageSource messageSource;

    @Nullable
    public String getMessage(String code, @Nullable Object[] args) throws NoSuchMessageException {
        return this.messageSource.getMessage(code, args, this.getDefaultLocale());
    }

    protected Locale getDefaultLocale() {
        return this.defaultLocale != null ? this.defaultLocale : LocaleContextHolder.getLocale();
    }

}

Posted by guarriman on Thu, 23 Sep 2021 09:12:02 -0700