Commit 9f415510 authored by Evan W. Patton's avatar Evan W. Patton Committed by Jeffrey Schiller

Save and restore user preferred locale

For non-English users of App Inventor, if they go to the main page or
click a link without a locale specified, for example, a repo link,
then they will be presented App Inventor in English. This is bad from
a UX perspective as the user then has to change the language and wait
for the site to reload. It also interrupts whatever workflow they were
currently doing (e.g., when opening a template project).

This change stores the last locale from the query string as the user's
preferred locale. When a locale isn't specified in the URL, we will
check the locale and if it is set, redirect to that page
automatically. To also save on performing actions that would be
canceled by the redirect, we also reorder some initialization of Ode
so that it only occurs if the redirect won't happen.

Change-Id: I1b9ffa756aa08f05495832768b242341e4a30c38
parent 10dd3723
......@@ -296,6 +296,16 @@
<pathelement location="${lib.dir}/powermock/powermock-easymock-1.4.10-full.jar" />
<pathelement location="${lib.dir}/responder-iq/responderiq-test.jar" />
<pathelement location="${lib.dir}/gcs/appengine-gcs-client-0.6.jar" />
<pathelement location="${lib.dir}/gcs/jackson-core-2.1.3.jar" />
<pathelement location="${lib.dir}/gcs/google-api-client-1.22.0.jar" />
<pathelement location="${lib.dir}/gcs/google-api-client-appengine-1.22.0.jar" />
<pathelement location="${lib.dir}/gcs/google-http-client-1.22.0.jar" />
<pathelement location="${lib.dir}/gcs/google-http-client-appengine-1.22.0.jar" />
<pathelement location="${lib.dir}/gcs/google-http-client-jackson-1.22.0.jar" />
<pathelement location="${lib.dir}/gcs/google-http-client-jackson2-1.22.0.jar" />
<pathelement location="${lib.dir}/gcs/google-api-services-storage-v1-rev91-1.22.0.jar" />
<pathelement location="${lib.dir}/gcs/httpclient-4.0.1.jar" />
<pathelement location="${lib.dir}/gcs/joda-time-2.6.jar" />
<pathelement location="${lib.dir}/protobuf/protobuf-2.6.1.jar" />
<pathelement location="${gwt.sdk}/gwt-servlet.jar" />
<pathelement location="${gwt.sdk}/gwt-user.jar"/>
......@@ -304,6 +314,7 @@
<pathelement location="${appengine.sdk}/lib/impl/appengine-api-stubs.jar"/>
<pathelement location="${appengine.sdk}/lib/testing/appengine-testing.jar" />
<pathelement location="${appengine.sdk}/lib/user/orm/geronimo-jpa_3.0_spec-1.1.1.jar" />
<fileset dir="${build.war.dir}/WEB-INF/lib" includes="*.jar" excludes="AiServerLib.jar"/>
</path>
<path id="AiServerLibTests.path">
......@@ -507,19 +518,36 @@
<path id="libsForAiClientLibTests.path">
<pathelement location="${build.war.dir}/WEB-INF/classes"/>
<pathelement location="${local.build.dir}/AiServerLib.jar" />
<pathelement location="${local.build.dir}/AiRebindLib.jar" />
<pathelement location="${local.build.dir}/AiSharedLib.jar" />
<pathelement location="${build.dir}/common/CommonTestUtils.jar" />
<pathelement location="${build.dir}/components/CommonConstants.jar"/>
<pathelement location="${build.dir}/components/CommonConstants-gwt.jar"/>
<pathelement location="${build.dir}/common/CommonUtils.jar" />
<pathelement location="${build.dir}/common/CommonUtils-gwt.jar" />
<pathelement location="${build.dir}/common/CommonVersion-gwt.jar" />
<pathelement location="${lib.dir}/guava/guava-20.0.jar" />
<pathelement location="${lib.dir}/guava/error_prone_annotations-2.0.12.jar" />
<pathelement location="${lib.dir}/guava/j2objc-annotations-1.1.jar" />
<pathelement location="${lib.dir}/gwt_query/gwtquery-1.5-beta1.jar" />
<pathelement location="${lib.dir}/gwt_dragdrop/gwt-dnd-3.2.3.jar" />
<pathelement location="${lib.dir}/gwt_incubator/gwt-incubator-20101117-r1766.jar" />
<pathelement location="${lib.dir}/guava/guava-gwt-20.0.jar" />
<pathelement location="${lib.dir}/json/json.jar" />
<pathelement location="${lib.dir}/junit/junit-4.8.2.jar" />
<pathelement location="${gwt.sdk}/gwt-user.jar"/>
<pathelement location="${gwt.sdk}/gwt-dev.jar"/>
<pathelement location="${gwt.sdk}/validation-api-1.0.0.GA-sources.jar"/>
<pathelement location="${gwt.sdk}/validation-api-1.0.0.GA.jar"/>
<!-- Paths to sources (needed by GWTTestCase) -->
<pathelement location="${build.dir}/components/ComponentTranslation/src" />
<pathelement location="src"/>
<pathelement location="tests"/>
</path>
<path id="AiClientLibTests.path">
<path refid="libsForAiClientLibTests.path"/>
<path refid="libsForAiServerLibTests.path"/>
<pathelement location="${local.build.dir}/AiSharedLib.jar" />
<pathelement location="${local.build.dir}/AiClientLibTests.jar" />
</path>
......@@ -528,6 +556,7 @@
depends="AiClientLib,AiServerLib,common_CommonTestUtils,components_CommonConstants,common_CommonUtils"
description="build and run the test suite" >
<ai.dojunit aij-testingtarget="AiClientLibTests"
aij-prod="true"
aij-dir="${appinventor.pkg}/client" >
</ai.dojunit>
</target>
......
......@@ -84,6 +84,7 @@ import com.google.appinventor.shared.rpc.user.User;
import com.google.appinventor.shared.rpc.user.UserInfoService;
import com.google.appinventor.shared.rpc.user.UserInfoServiceAsync;
import com.google.appinventor.shared.settings.SettingsConstants;
import com.google.common.annotations.VisibleForTesting;
import com.google.gwt.core.client.Callback;
import com.google.gwt.core.client.EntryPoint;
import com.google.gwt.core.client.GWT;
......@@ -812,48 +813,48 @@ public class Ode implements EntryPoint {
}
userSettings = new UserSettings(user);
userSettings.loadSettings(new Command() {
@Override
public void execute() {
// Gallery settings
gallerySettings = new GallerySettings();
//gallerySettings.loadGallerySettings();
loadGallerySettings();
// Gallery settings
gallerySettings = new GallerySettings();
//gallerySettings.loadGallerySettings();
loadGallerySettings();
// Initialize project and editor managers
// The project manager loads the user's projects asynchronously
projectManager = new ProjectManager();
projectManager.addProjectManagerEventListener(new ProjectManagerEventAdapter() {
@Override
public void onProjectsLoaded() {
projectManager.removeProjectManagerEventListener(this);
// Initialize project and editor managers
// The project manager loads the user's projects asynchronously
projectManager = new ProjectManager();
projectManager.addProjectManagerEventListener(new ProjectManagerEventAdapter() {
@Override
public void onProjectsLoaded() {
projectManager.removeProjectManagerEventListener(this);
openPreviousProject();
// This handles any built-in templates stored in /war
// Retrieve template data stored in war/templates folder and
// and save it for later use in TemplateUploadWizard
OdeAsyncCallback<String> templateCallback =
new OdeAsyncCallback<String>(
// failure message
MESSAGES.createProjectError()) {
@Override
public void onSuccess(String json) {
// Save the templateData
TemplateUploadWizard.initializeBuiltInTemplates(json);
}
};
Ode.getInstance().getProjectService().retrieveTemplateData(TemplateUploadWizard.TEMPLATES_ROOT_DIRECTORY, templateCallback);
}
});
editorManager = new EditorManager();
// Initialize UI
initializeUi();
// This handles any built-in templates stored in /war
// Retrieve template data stored in war/templates folder and
// and save it for later use in TemplateUploadWizard
OdeAsyncCallback<String> templateCallback =
new OdeAsyncCallback<String>(
// failure message
MESSAGES.createProjectError()) {
@Override
public void onSuccess(String json) {
// Save the templateData
TemplateUploadWizard.initializeBuiltInTemplates(json);
// Here we call userSettings.loadSettings, but the settings are actually loaded
// asynchronously, so this loadSettings call will return before they are loaded.
// After the user settings have been loaded, openPreviousProject will be called.
// We have to call this after the builtin templates have been loaded otherwise
// we will get a NPF.
userSettings.loadSettings();
}
};
Ode.getInstance().getProjectService().retrieveTemplateData(TemplateUploadWizard.TEMPLATES_ROOT_DIRECTORY, templateCallback);
topPanel.showUserEmail(user.getUserEmail());
}
});
editorManager = new EditorManager();
// Initialize UI
initializeUi();
topPanel.showUserEmail(user.getUserEmail());
}
private boolean isSet(String str) {
......@@ -1500,6 +1501,49 @@ public class Ode implements EntryPoint {
return pb;
}
/**
* Compares two locales and determines if they are equal. We consider oldLocale value
* of null to be equal to the empty string to handle default values.
* @param oldLocale one locale
* @param newLocale another locale
* @param defaultValue the default locale
* @return true if the locale ISO strings are equal modulo case or if both
* are empty, otherwise false
*/
@VisibleForTesting
static boolean compareLocales(String oldLocale, String newLocale, String defaultValue) {
if ((oldLocale == null || oldLocale.isEmpty()) && (newLocale == null || newLocale.isEmpty())) {
return true;
} else if (oldLocale == null || oldLocale.isEmpty()) {
return defaultValue.equalsIgnoreCase(newLocale);
} else {
return oldLocale.equalsIgnoreCase(newLocale);
}
}
/**
* Check the user's locale against the currently requested locale. No locale
* is specified in the query string, then we redirect to the user's previous
* locale. English, the default locale, won't redirect in this scenario to
* prevent double requests for most of our users. If locale parameter is
* specified and the locales don't match, then we set the user's last locale
* to the current locale.
*/
public static boolean handleUserLocale() {
String locale = Window.Location.getParameter("locale");
String lastUserLocale = userSettings.getSettings(SettingsConstants.USER_GENERAL_SETTINGS).getPropertyValue(SettingsConstants.USER_LAST_LOCALE);
if (!compareLocales(locale, lastUserLocale, "en")) {
if (locale == null) {
Window.Location.assign(Window.Location.createUrlBuilder().setParameter("locale", lastUserLocale).buildString());
return false;
} else {
userSettings.getSettings(SettingsConstants.USER_GENERAL_SETTINGS).changePropertyValue(SettingsConstants.USER_LAST_LOCALE, locale);
userSettings.saveSettings(null);
}
}
return true;
}
private void resizeWorkArea(WorkAreaPanel workArea) {
// Subtract 16px from width to account for vertical scrollbar FF3 likes to add
workArea.onResize(Window.getClientWidth() - 16, Window.getClientHeight());
......
......@@ -31,6 +31,8 @@ public final class GeneralSettings extends Settings {
EditableProperty.TYPE_INVISIBLE));
addProperty(new EditableProperty(this, SettingsConstants.DISABLED_USER_URL, "",
EditableProperty.TYPE_INVISIBLE));
addProperty(new EditableProperty(this, SettingsConstants.USER_LAST_LOCALE, "en",
EditableProperty.TYPE_INVISIBLE));
}
@Override
......@@ -40,8 +42,6 @@ public final class GeneralSettings extends Settings {
// Account is disabled, show dialog box and stop further processing
// i.e., do not open previous project.
Ode.getInstance().disabledAccountDialog(disabledUrl);
} else {
Ode.getInstance().openPreviousProject();
}
}
}
......@@ -39,6 +39,10 @@ public final class UserSettings extends CommonSettings implements SettingsAccess
@Override
public void loadSettings() {
loadSettings(null);
}
public void loadSettings(final Command next) {
loading = true;
Ode.getInstance().getUserInfoService().loadUserSettings(
new OdeAsyncCallback<String>(MESSAGES.settingsLoadError()) {
......@@ -50,6 +54,10 @@ public final class UserSettings extends CommonSettings implements SettingsAccess
changed = false;
loaded = true;
loading = false;
if (Ode.handleUserLocale() && next != null) {
next.execute();
}
}
@Override
......
......@@ -96,9 +96,6 @@ public class LoginServlet extends HttpServlet {
// These params are passed around so they can take effect even if we
// were not logged in.
String locale = params.get("locale");
if (locale == null) {
locale = "en";
}
String repo = params.get("repo");
String galleryId = params.get("galleryId");
String redirect = params.get("redirect");
......@@ -106,7 +103,12 @@ public class LoginServlet extends HttpServlet {
if (DEBUG) {
LOG.info("locale = " + locale + " bundle: " + new Locale(locale));
}
ResourceBundle bundle = ResourceBundle.getBundle("com/google/appinventor/server/loginmessages", new Locale(locale));
ResourceBundle bundle;
if (locale == null) {
bundle = ResourceBundle.getBundle("com/google/appinventor/server/loginmessages", new Locale("en"));
} else {
bundle = ResourceBundle.getBundle("com/google/appinventor/server/loginmessages", new Locale(locale));
}
if (page.equals("google")) {
// We get here after we have gone through the Google Login page
......@@ -164,7 +166,7 @@ public class LoginServlet extends HttpServlet {
return;
}
String uri = new UriBuilder("/login/google")
.add("locale", locale)
.add("locale", locale.equals("en") ? null : locale)
.add("repo", repo)
.add("galleryId", galleryId)
.add("redirect", redirect).build();
......
......@@ -26,6 +26,7 @@ public class SettingsConstants {
// disable someone's account. The URL can be user specific in order to deliver
// a particular message to a particular user.
public static final String DISABLED_USER_URL = "DisabledUserUrl";
public static final String USER_LAST_LOCALE = "LastLocale";
public static final String SPLASH_SETTINGS = "SplashSettings";
......
package com.google.appinventor.client;
import com.google.gwt.junit.client.GWTTestCase;
public class OdeTest extends GWTTestCase {
public void testCompareLocales() {
assertTrue("Handles default case one is null",
Ode.compareLocales(null, "en", "en"));
assertTrue("Handles both cases being null",
Ode.compareLocales(null, null, "en"));
assertTrue("Handles when both cases are the same",
Ode.compareLocales("en", "en", "en"));
assertFalse("Handles when the cases are different",
Ode.compareLocales("en", "fr_FR", "en"));
assertFalse("Handles when the default is different",
Ode.compareLocales(null, "fr_FR", "en"));
}
@Override
public String getModuleName() {
return "com.google.appinventor.YaClient";
}
}
......@@ -13,7 +13,7 @@
Contains definitions common to multiple App Inventor build.xml files.
====================================================================== -->
<project name="CommonDefinitions">
<project name="CommonDefinitions" xmlns:if="ant:if" xmlns:unless="ant:unless">
<!-- Always build with debug set... (jis) -->
<property name="debug" value="true"/>
<description>
......@@ -170,6 +170,7 @@
<macrodef name="ai.dojunit">
<attribute name="aij-testingtarget" />
<attribute name="aij-dir" />
<attribute name="aij-prod" default="false" />
<!-- The following element is used to workaround a bug in older JDKs that prevents
Robolectric's annotations from being read by javac -->
<element name="aij-supplemental-includes" optional="true" />
......@@ -203,6 +204,9 @@
maxmemory="925m"
showoutput="no">
<jvmarg value="-XX:MaxPermSize=128m"/>
<sysproperty key="gwt.args" value="-prod -gen ${local.build.dir}/gen -war ${local.build.dir}/build/war" if:true="@{aij-prod}"/>
<sysproperty key="gwt.args" value="-devMode -logLevel WARN -war ${local.build.dir}/build/war" unless:true="@{aij-prod}"/>
<sysproperty key="java.awt.headless" value="true"/>
<classpath refid="@{aij-testingtarget}.path"/>
<formatter type="xml"/>
<!-- If the ant command line sets the test_name property (see above)
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment