/* * Copyright 2010 Google Inc. * * 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. */ package com.google.gwt.i18n.linker; import com.google.gwt.core.ext.TreeLogger; import com.google.gwt.core.ext.UnableToCompleteException; import com.google.gwt.core.ext.linker.ConfigurationProperty; import com.google.gwt.core.ext.linker.PropertyProviderGenerator; import com.google.gwt.user.rebind.SourceWriter; import com.google.gwt.user.rebind.StringSourceWriter; import java.util.SortedSet; import java.util.regex.Pattern; /** * Generates a property provider implementation for the "locale" property. */ public class LocalePropertyProviderGenerator implements PropertyProviderGenerator { public static final String LOCALE_QUERYPARAM = "locale.queryparam"; public static final String LOCALE_COOKIE = "locale.cookie"; public static final String LOCALE_SEARCHORDER = "locale.searchorder"; public static final String LOCALE_USEMETA = "locale.usemeta"; public static final String LOCALE_USERAGENT = "locale.useragent"; protected static final Pattern COOKIE_PATTERN = Pattern.compile("^[A-Za-z][A-Za-z0-9_]*$"); protected static final Pattern QUERYPARAM_PATTERN = Pattern.compile("^[A-Za-z][A-Za-z0-9_]*$"); /** * Return true when the supplied value represents a true/yes/on value. * * @param value * @return true if the string represents true/yes/on */ protected static boolean isTrue(String value) { return value != null && ("yes".equalsIgnoreCase(value) || "y".equalsIgnoreCase(value) || "true".equalsIgnoreCase(value) || "on".equalsIgnoreCase(value)); } public String generate(TreeLogger logger, SortedSet<String> possibleValues, String fallback, SortedSet<ConfigurationProperty> configProperties) throws UnableToCompleteException { // get relevant config property values String localeQueryParam = null; String localeCookie = null; boolean localeUserAgent = false; boolean localeUseMeta = false; String localeSearchOrder = "queryparam,cookie,meta,useragent"; for (ConfigurationProperty configProp : configProperties) { String name = configProp.getName(); if (LOCALE_QUERYPARAM.equals(name)) { localeQueryParam = configProp.getValues().get(0); if (localeQueryParam != null && localeQueryParam.length() != 0 && !validateQueryParam(localeQueryParam)) { logger.log(TreeLogger.WARN, "Ignoring invalid value of '" + localeQueryParam + "' from '" + LOCALE_QUERYPARAM + "', not a valid query parameter name"); localeQueryParam = null; } } else if (LOCALE_COOKIE.equals(name)) { localeCookie = configProp.getValues().get(0); if (localeCookie != null && localeCookie.length() != 0 && !validateCookieName(localeCookie)) { logger.log(TreeLogger.WARN, "Ignoring invalid value of '" + localeCookie + "' from '" + LOCALE_COOKIE + "', not a valid cookie name"); localeCookie = null; } } else if (LOCALE_USEMETA.equals(name)) { localeUseMeta = isTrue(configProp.getValues().get(0)); } else if (LOCALE_USERAGENT.equals(name)) { localeUserAgent = isTrue(configProp.getValues().get(0)); } else if (LOCALE_SEARCHORDER.equals(name)) { localeSearchOrder = configProp.getValues().get(0); } } // provide a default for the search order localeSearchOrder = localeSearchOrder.trim(); if (localeSearchOrder == null || localeSearchOrder.length() == 0) { localeSearchOrder = "queryparam,cookie,meta,useragent"; } if (fallback == null) { // TODO(jat): define this in a common place fallback = "default"; } // build property provider body StringSourceWriter body = new StringSourceWriter(); body.println("{"); body.indent(); body.println("var locale = null;"); body.println("var rtlocale = '" + fallback + "';"); body.println("try {"); for (String method : localeSearchOrder.split(",")) { if ("queryparam".equals(method)) { if (localeQueryParam != null && localeQueryParam.length() > 0) { body.println("if (!locale) {"); body.indent(); generateQueryParamLookup(logger, body, localeQueryParam); body.outdent(); body.println("}"); } } else if ("cookie".equals(method)) { if (localeCookie != null && localeCookie.length() > 0) { body.println("if (!locale) {"); body.indent(); generateCookieLookup(logger, body, localeCookie); body.outdent(); body.println("}"); } } else if ("meta".equals(method)) { if (localeUseMeta) { body.println("if (!locale) {"); body.indent(); generateMetaLookup(logger, body); body.outdent(); body.println("}"); } } else if ("useragent".equals(method)) { if (localeUserAgent) { body.println("if (!locale) {"); body.indent(); generateUserAgentLookup(logger, body); body.outdent(); body.println("}"); } } else { logger.log(TreeLogger.WARN, "Ignoring unknown locale lookup method \"" + method + "\""); body.println("// ignoring invalid lookup method '" + method + "'"); } } body.println("if (!locale) {"); body.indent(); body.println("locale = $wnd['__gwt_Locale'];"); body.outdent(); body.println("}"); body.println("if (locale) {"); body.indent(); body.println("rtlocale = locale;"); body.outdent(); body.println("}"); generateInheritanceLookup(logger, body); body.outdent(); body.println("} catch (e) {"); body.indent(); body.println("alert(\"Unexpected exception in locale detection, using " + "default: \" + e);\n"); body.outdent(); body.println("}"); body.println("$wnd['__gwt_Locale'] = rtlocale;"); body.println("return locale || \"" + fallback + "\";"); body.outdent(); body.println("}"); return body.toString(); } /** * Generate JS code that looks up the locale value from a cookie. * * @param logger logger to use * @param body * @param cookieName * @throws UnableToCompleteException */ protected void generateCookieLookup(TreeLogger logger, SourceWriter body, String cookieName) throws UnableToCompleteException { body.println("var cookies = $doc.cookie;"); body.println("var idx = cookies.indexOf(\"" + cookieName + "=\");"); body.println("if (idx >= 0) {"); body.indent(); body.println("var end = cookies.indexOf(';', idx);"); body.println("if (end < 0) {"); body.indent(); body.println("end = cookies.length;"); body.outdent(); body.println("}"); body.println("locale = cookies.substring(idx + " + (cookieName.length() + 1) + ", end);"); body.outdent(); body.println("}"); } /** * Generate JS code that takes the value of the "locale" variable and finds * parent locales until the value is a supported locale or the default locale. * * @param logger logger to use * @param body * @throws UnableToCompleteException */ protected void generateInheritanceLookup(TreeLogger logger, SourceWriter body) throws UnableToCompleteException { body.println("while (locale && !__gwt_isKnownPropertyValue(\"locale\", locale)) {"); body.indent(); body.println("var lastIndex = locale.lastIndexOf(\"_\");"); body.println("if (lastIndex < 0) {"); body.indent(); body.println("locale = null;"); body.println("break;"); body.outdent(); body.println("}"); body.println("locale = locale.substring(0, lastIndex);"); body.outdent(); body.println("}"); } /** * Generate JS code to fetch the locale from a meta property. * * @param logger logger to use * @param body * @throws UnableToCompleteException */ protected void generateMetaLookup(TreeLogger logger, SourceWriter body) throws UnableToCompleteException { // TODO(jat): do we want to allow customizing the meta property name? body.println("locale = __gwt_getMetaProperty(\"locale\");"); } /** * Generate JS code to get the locale from a query parameter. * * @param logger logger to use * @param body where to append JS output * @param queryParam the query parameter to use * @throws UnableToCompleteException */ protected void generateQueryParamLookup(TreeLogger logger, SourceWriter body, String queryParam) throws UnableToCompleteException { body.println("var queryParam = location.search;"); body.println("var qpStart = queryParam.indexOf(\"" + queryParam + "=\");"); body.println("if (qpStart >= 0) {"); body.indent(); body.println("var value = queryParam.substring(qpStart + " + (queryParam.length() + 1) + ");"); body.println("var end = queryParam.indexOf(\"&\", qpStart);"); body.println("if (end < 0) {"); body.indent(); body.println("end = queryParam.length;"); body.outdent(); body.println("}"); body.println("locale = queryParam.substring(qpStart + " + (queryParam.length() + 1) + ", end);"); body.outdent(); body.println("}"); } /** * Generate JS code to fetch the locale from the user agent's compile-time * locale. * * @param logger logger to use * @param body * @throws UnableToCompleteException */ protected void generateUserAgentLookup(TreeLogger logger, SourceWriter body) throws UnableToCompleteException { body.println("var language = navigator.browserLanguage ? " + "navigator.browserLanguage : navigator.language;"); body.println("if (language) {"); body.indent(); body.println("var parts = language.split(/[-_]/);"); body.println("if (parts.length > 1) {"); body.indent(); body.println("parts[1] = parts[1].toUpperCase();"); body.outdent(); body.println("}"); body.println("locale = parts.join(\"_\");"); body.outdent(); body.println("}"); } /** * Validate that a name is a valid cookie name. * * @param cookieName * @return true if cookieName is an acceptable cookie name */ protected boolean validateCookieName(String cookieName) { return COOKIE_PATTERN.matcher(cookieName).matches(); } /** * Validate that a value is a valid query parameter name. * * @param queryParam * @return true if queryParam is a valid query parameter name. */ protected boolean validateQueryParam(String queryParam) { return QUERYPARAM_PATTERN.matcher(queryParam).matches(); } }