Add server-side language fallback.

This allows to keep the same Javascript calls to load languages, so it
does not require any change for extensions to benefit from this.

Closes #1350. Fixes #2209.
This commit is contained in:
Antonin Delpeuch 2019-11-07 17:23:02 +01:00
parent 11c7788239
commit efbfce29bb
2 changed files with 102 additions and 33 deletions

View File

@ -35,11 +35,14 @@ import java.io.InputStreamReader;
import java.io.Reader;
import java.io.UnsupportedEncodingException;
import java.util.Arrays;
import java.util.Iterator;
import java.util.Map.Entry;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.fasterxml.jackson.databind.node.TextNode;
import com.google.refine.ProjectManager;
@ -86,32 +89,34 @@ public class LoadLanguageCommand extends Command {
}
// Default language is English
langs = Arrays.copyOf(langs, langs.length+1);
langs[langs.length-1] = "en";
if (langs.length == 0 || langs[langs.length-1] != "en" ) {
langs = Arrays.copyOf(langs, langs.length+1);
langs[langs.length-1] = "en";
}
ObjectNode json = null;
boolean loaded = false;
for (String lang : langs) {
if (lang == null) continue;
json = loadLanguage(this.servlet, modname, lang);
ObjectNode translations = null;
for (int i = langs.length-1; i >= 0; i--) {
if (langs[i] == null) continue;
ObjectNode json = loadLanguage(this.servlet, modname, langs[i]);
if (json != null) {
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json");
try {
ObjectNode node = ParsingUtilities.mapper.createObjectNode();
node.put("dictionary", json);
node.put("lang", new TextNode(lang));
ParsingUtilities.mapper.writeValue(response.getWriter(), node);
} catch (IOException e) {
logger.error("Error writing language labels to response stream");
if (translations == null) {
translations = json;
} else {
translations = mergeLanguages(json, translations);
}
response.getWriter().flush();
response.getWriter().close();
loaded = true;
break;
}
}
if (!loaded) {
if (translations != null) {
try {
ObjectNode node = ParsingUtilities.mapper.createObjectNode();
node.put("dictionary", translations);
node.put("lang", new TextNode(langs[0]));
respondJSON(response, node);
} catch (IOException e) {
logger.error("Error writing language labels to response stream");
}
} else {
logger.error("Failed to load any language files");
}
}
@ -130,4 +135,29 @@ public class LoadLanguageCommand extends Command {
}
return null;
}
/**
* Perform a language fallback, server-side
* @param preferred
* the JSON translation for the preferred language
* @param fallback
* the JSON translation for the fallback language
* @return
* a JSON object where values are from the preferred
* language if available, and the fallback language otherwise
*/
static ObjectNode mergeLanguages(ObjectNode preferred, ObjectNode fallback) {
ObjectNode results = ParsingUtilities.mapper.createObjectNode();
Iterator<Entry<String, JsonNode>> iterator = fallback.fields();
while(iterator.hasNext()) {
Entry<String,JsonNode> entry = iterator.next();
String code = entry.getKey();
JsonNode value = preferred.get(code);
if (value == null) {;
value = entry.getValue();
}
results.put(code, value);
}
return results;
}
}

View File

@ -2,15 +2,9 @@ package com.google.refine.commands.lang;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertTrue;
import com.fasterxml.jackson.databind.JsonNode;
import com.google.refine.RefineServlet;
import com.google.refine.commands.CommandTestBase;
import com.google.refine.util.ParsingUtilities;
import edu.mit.simile.butterfly.ButterflyModule;
import java.io.File;
import java.io.IOException;
@ -19,6 +13,16 @@ import javax.servlet.ServletException;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;
import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.refine.RefineServlet;
import com.google.refine.commands.CommandTestBase;
import com.google.refine.util.ParsingUtilities;
import edu.mit.simile.butterfly.ButterflyModule;
public class LoadLanguageCommandTests extends CommandTestBase {
@BeforeMethod
@ -26,11 +30,11 @@ public class LoadLanguageCommandTests extends CommandTestBase {
command = new LoadLanguageCommand();
ButterflyModule coreModule = mock(ButterflyModule.class);
when(coreModule.getName()).thenReturn("core");
when(coreModule.getPath()).thenReturn(new File("webapp/modules/core"));
RefineServlet servlet = mock(RefineServlet.class);
when(servlet.getModule("core")).thenReturn(coreModule);
command.init(servlet);
when(coreModule.getName()).thenReturn("core");
when(coreModule.getPath()).thenReturn(new File("webapp/modules/core"));
RefineServlet servlet = mock(RefineServlet.class);
when(servlet.getModule("core")).thenReturn(coreModule);
command.init(servlet);
}
@Test
@ -44,5 +48,40 @@ public class LoadLanguageCommandTests extends CommandTestBase {
assertTrue(response.has("dictionary"));
assertTrue(response.has("lang"));
}
@Test
public void testLoadUnknownLanguage() throws ServletException, IOException {
when(request.getParameter("module")).thenReturn("core");
when(request.getParameterValues("lang")).thenReturn(new String[] {"foobar"});
command.doPost(request, response);
JsonNode response = ParsingUtilities.mapper.readValue(writer.toString(), JsonNode.class);
assertTrue(response.has("dictionary"));
ssertEquals(response.get("lang").asText(), "foobar");
}
@Test
public void testLanguageFallback() throws JsonParseException, JsonMappingException, IOException {
String fallbackJson = "{"
+ "\"foo\":\"hello\","
+ "\"bar\":\"world\""
+ "}";
String preferredJson = "{"
+ "\"foo\":\"hallo\""
+ "}";
String expectedJson = "{"
+ "\"foo\":\"hallo\","
+ "\"bar\":\"world\""
+ "}";
ObjectNode fallback = ParsingUtilities.mapper.readValue(fallbackJson, ObjectNode.class);
ObjectNode preferred = ParsingUtilities.mapper.readValue(preferredJson, ObjectNode.class);
ObjectNode expected = ParsingUtilities.mapper.readValue(expectedJson, ObjectNode.class);
ObjectNode merged = LoadLanguageCommand.mergeLanguages(preferred, fallback);
assertEquals(merged, expected);
}
}