How to serve static content based on locale in springboot

Viewed 90

is it possible to use someone's locale to serve different static content with springboot?

this is what my code currently does

@Configuration
@EnableWebMvc
public class MyConfig implements WebMvcConfigurer {
    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry
          .addResourceHandler("/en-ca/**")
          .addResourceLocations("/angular-output/en-CA");   
        registry
          .addResourceHandler("/fr-ca/**")
          .addResourceLocations("/angular-output/fr-CA");
    }
}

but this method only allows me to setup mappings from a url-path to a file-path with no intermediate step to consider someone's locale to pick the file-path I want to use.

I wish I could do something like

@Configuration
@EnableWebMvc
public class MyConfig implements WebMvcConfigurer {
    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry
          .addResourceHandler("/**").addResourceLocationSelector(
              new ResourceLocationSelector() {
                  @Override
                  public string select() {
                      if (LocaleContextHolder.getLocale() == Locale.English) {
                          return "/angular-output/en-CA";
                      } else { 
                          return "/angular-output/fr-CA";
                      }
                  }
              }
          )
    }
}

I've had little luck trying to do such a seemingly basic thing with Spring Boot.

1 Answers

You should be able to customize static endpoints at request time by using the ResourceResolver API.

How it works:

  1. On the ResourceHandlerRegistry, add a resource handler, and optionally abase resource location.
  2. Use resourceChain function to specify that from this point, you want to add intermediate path resolvers, that will customize static content location depending on user query.

Here is a little example that will serve content from static directory from the classpath, but that use a resource resolver to append a lang directory (complete working example here)

@Configuration
@EnableWebMvc
public class StaticContentConfiguration implements WebMvcConfigurer {

    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry
                .addResourceHandler("/**")
                .addResourceLocations("classpath:/static/")
                // Maybe you will want to set it to true to activate cache
                .resourceChain(false) 
                // This will add a suffix to the above resource location by analyzin
                .addResolver(new LocaleResolver());
    }

    /**
     * Modify static content path initially resolved by spring to include a locale dependent piece.
     */
    private class LocaleResolver implements ResourceResolver {

        @Override
        public Resource resolveResource(HttpServletRequest request, String requestPath, List<? extends Resource> locations, ResourceResolverChain chain) {
            final var lang = solveLang(request, "en").toLowerCase(Locale.ROOT);
            for (Resource loc: locations) {
                try {
                    // IMPORTANT: it looks like the createRelative method needs that appended directories end with a slash 
                    final Resource langSpecializedResource = loc.createRelative(lang+"/");
                    if (langSpecializedResource.exists()) {
                        // Let spring build final path from modified prefix.
                        return chain.resolveResource(request, requestPath, List.of(langSpecializedResource));
                    }
                } catch (IOException e) {
                    throw new UncheckedIOException("Failure while resolving paths", e);
                }
            }

            // TODO: that does NOT propagate properly
            throw new HttpClientErrorException(HttpStatus.BAD_REQUEST, "Language not supported");
        }

        @Override
        public String resolveUrlPath(String resourcePath, List<? extends Resource> locations, ResourceResolverChain chain) {
            throw new UnsupportedOperationException("Not supported yet");
        }
    }

    /**
     * Try to extract user language from request. Search in order:
     * <ol>
     *     <li>for a <pre>lang</pre> query parameter</li>
     *     <li>for a valid <pre>Accept-Language</pre> header</li>
     * </ol>
     *
     * @param request User request to analyze.
     * @param fallback The default language value to use.
     * @return found language, or given fallback if none can be extracted from input request.
     */
    private String solveLang(final HttpServletRequest request, final String fallback) {
        if (request == null) return fallback;

        final var langs = request.getParameterValues("lang");
        if (langs != null && langs.length > 0) return langs[0];

        final var header = request.getHeader(HttpHeaders.ACCEPT_LANGUAGE);
        if (header != null) {
            return Locale.forLanguageTag(header).getLanguage();
        }

        return fallback;
    }
}
Related