/* * #%L * Wisdom-Framework * %% * Copyright (C) 2013 - 2014 Wisdom Framework * %% * Licensed 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. * #L% */ package org.wisdom.i18n; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ObjectNode; import com.google.common.base.Strings; import org.apache.felix.ipojo.annotations.Requires; import org.wisdom.api.DefaultController; import org.wisdom.api.annotations.*; import org.wisdom.api.content.Json; import org.wisdom.api.http.*; import org.wisdom.api.i18n.InternationalizationService; import java.util.*; /** * A controller allowing clients to retrieve the internationalized messages. */ @Controller public class I18nController extends DefaultController { @Requires InternationalizationService service; @Requires Json json; /** * Gets the internationalized message as a Java resource bundle (property file). * * @param file the name of the file indicating the requested locale * @param ifNoneMatch the received ETAG if any * @return the result containing the messages as properties. */ @Route(method = HttpMethod.GET, uri = "i18n/bundles/{file<.+>}.properties") public Result getBundleResource(@PathParameter("file") String file, @HttpParameter(HeaderNames.IF_NONE_MATCH) String ifNoneMatch) { // Extract the locale from file if (Strings.isNullOrEmpty(file)) { return notFound().as(MimeTypes.TEXT); } Locale locale = InternationalizationService.DEFAULT_LOCALE; if (file.contains("_")) { // We got a locale locale = Locale.forLanguageTag(file.substring(file.indexOf('_') + 1).replace("_", "-")); } // Check if we have received a etag String etag = service.etag(locale); if (ifNoneMatch != null && ifNoneMatch.equals(etag)) { return new Result(Status.NOT_MODIFIED); } Collection<ResourceBundle> bundles = service.bundles(locale); // Do we have this locale if (bundles.isEmpty()) { // No, return not found return notFound().as(MimeTypes.TEXT); } else { StringBuilder builder = new StringBuilder(); for (ResourceBundle bundle : bundles) { for (String key : bundle.keySet()) { builder.append(key).append("=").append(bundle.getString(key)).append("\n"); } } return ok(builder.toString()).as(MimeTypes.TEXT).with(HeaderNames.ETAG, etag); } } /** * Gets the internationalized messages as a JSON document compliant with the I18Next format. * * @param listOfLocales the list of locales * @param ifNoneMatch the received ETAG if any * @return the JSON document containing the messages */ @Route(method = HttpMethod.GET, uri = "i18n/bundles/{file<.+>}.json") public Result getBundleResourceForI18Next(@QueryParameter("locales") String listOfLocales, @HttpParameter(HeaderNames.IF_NONE_MATCH) String ifNoneMatch) { // Parse the list of locale List<Locale> locales = new ArrayList<>(); if (!Strings.isNullOrEmpty(listOfLocales)) { String[] items = listOfLocales.split(" "); for (String item : items) { // Manage the 'dev' value (it's the default locale used by i18next if ("dev".equalsIgnoreCase(item)) { locales.add(InternationalizationService.DEFAULT_LOCALE); } else { locales.add(Locale.forLanguageTag(item)); } } } String etag; StringBuilder builder = new StringBuilder(); for (Locale locale : locales) { builder.append(service.etag(locale)); } etag = builder.toString(); if (ifNoneMatch != null && ifNoneMatch.equals(etag)) { return new Result(Status.NOT_MODIFIED); } // i18next use a specific Json Format ObjectNode result = json.newObject(); for (Locale locale : locales) { ObjectNode lang = json.newObject(); ObjectNode translation = json.newObject(); lang.set("translation", translation); Collection<ResourceBundle> bundles = service.bundles(locale); for (ResourceBundle bundle : bundles) { for (String key : bundle.keySet()) { populateJsonResourceBundle(translation, key, bundle.getString(key)); } } String langName = locale.toLanguageTag(); if (locale.equals(InternationalizationService.DEFAULT_LOCALE)) { langName = "dev"; } result.set(langName, lang); } return ok(result).with(HeaderNames.ETAG, etag); } private void populateJsonResourceBundle(ObjectNode node, String key, String value) { final int indexOfDot = key.indexOf('.'); if (indexOfDot != -1) { String prefix = key.substring(0, indexOfDot); String remainder = key.substring(indexOfDot + 1); JsonNode subNode = node.get(prefix); if (subNode == null) { subNode = json.newObject(); node.set(prefix, subNode); } else if (!subNode.isObject()) { throw new IllegalStateException("Invalid JSON Resource Bundle format, the key " + prefix + " is " + "already present and is not an Object Node"); } populateJsonResourceBundle((ObjectNode) subNode, remainder, value); } else { node.put(key, value); } } /** * Gets a specific internationalized message. * * @param key the key * @param locale the locale. If {@code null} if uses the first locale from the request. * @return the message as text, or {@code NOT FOUND} if the key cannot be found. */ @Route(method = HttpMethod.GET, uri = "i18n/{key}") public Result getMessage(@Parameter("key") String key, @QueryParameter("locale") Locale locale) { String message; if (locale != null && !locale.equals(InternationalizationService.DEFAULT_LOCALE)) { message = service.get(locale, key); } else { message = service.get(context().request().languages(), key); } if (message != null) { return ok(message).as(MimeTypes.TEXT); } else { return notFound("No message for " + key).as(MimeTypes.TEXT); } } /** * Gets all messages of the given locales as JSON. The format is just a set of key:value. * * @param locales the locales to include, if {@code null} it uses the languages from the request * @param ifNoneMatch the received ETAG if any * @return the set of messages */ @Route(method = HttpMethod.GET, uri = "i18n") public Result getMessages(@QueryParameter("locales") List<Locale> locales, @HttpParameter(HeaderNames.IF_NONE_MATCH) String ifNoneMatch) { // We have to deal with several format here. // First, if `locales` is set, use it // Finally use the Accept-Language header Map<String, String> messages; String etag; StringBuilder builder = new StringBuilder(); if (locales != null && !locales.isEmpty()) { for (Locale locale : locales) { builder.append(service.etag(locale)); } } else { for (Locale locale : context().request().languages()) { builder.append(service.etag(locale)); } } etag = builder.toString(); if (ifNoneMatch != null && ifNoneMatch.equals(etag)) { return new Result(Status.NOT_MODIFIED); } if (locales != null && !locales.isEmpty()) { messages = service.getAllMessages(locales.toArray(new Locale[locales.size()])); } else { messages = service.getAllMessages(context().request().languages()); } if (messages != null) { return ok(messages).with(HeaderNames.ETAG, etag).json(); } else { return notFound().json(); } } }