Skip to content

Commit 99fcede

Browse files
committed
allow i18n of displayName for dataset types #11747
1 parent e6447b7 commit 99fcede

7 files changed

Lines changed: 211 additions & 14 deletions

File tree

src/main/java/edu/harvard/iq/dataverse/api/Datasets.java

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
import edu.harvard.iq.dataverse.externaltools.ExternalToolHandler;
3535
import edu.harvard.iq.dataverse.globus.GlobusServiceBean;
3636
import edu.harvard.iq.dataverse.globus.GlobusUtil;
37+
import edu.harvard.iq.dataverse.i18n.i18nUtil;
3738
import edu.harvard.iq.dataverse.ingest.IngestServiceBean;
3839
import edu.harvard.iq.dataverse.ingest.IngestUtil;
3940
import edu.harvard.iq.dataverse.makedatacount.*;
@@ -99,13 +100,12 @@
99100
import java.util.stream.Collectors;
100101
import static edu.harvard.iq.dataverse.api.ApiConstants.*;
101102

102-
import edu.harvard.iq.dataverse.dataset.DatasetType;
103-
import edu.harvard.iq.dataverse.dataset.DatasetTypeServiceBean;
104103
import edu.harvard.iq.dataverse.license.License;
105104

106105
import static edu.harvard.iq.dataverse.util.json.JsonPrinter.*;
107106
import static edu.harvard.iq.dataverse.util.json.NullSafeJsonBuilder.jsonObjectBuilder;
108107

108+
import static jakarta.ws.rs.core.HttpHeaders.ACCEPT_LANGUAGE;
109109
import static jakarta.ws.rs.core.Response.Status.BAD_REQUEST;
110110
import static jakarta.ws.rs.core.Response.Status.NOT_FOUND;
111111
import static jakarta.ws.rs.core.Response.Status.FORBIDDEN;
@@ -5684,17 +5684,19 @@ public Response resetPidGenerator(@Context ContainerRequestContext crc, @PathPar
56845684

56855685
@GET
56865686
@Path("datasetTypes")
5687-
public Response getDatasetTypes() {
5687+
public Response getDatasetTypes(@HeaderParam(ACCEPT_LANGUAGE) String acceptLanguage) {
5688+
Locale locale = i18nUtil.parseAcceptLanguageHeader(acceptLanguage);
56885689
JsonArrayBuilder jab = Json.createArrayBuilder();
56895690
for (DatasetType datasetType : datasetTypeSvc.listAll()) {
5690-
jab.add(datasetType.toJson());
5691+
jab.add(datasetType.toJson(locale));
56915692
}
56925693
return ok(jab);
56935694
}
56945695

56955696
@GET
56965697
@Path("datasetTypes/{idOrName}")
5697-
public Response getDatasetTypes(@PathParam("idOrName") String idOrName) {
5698+
public Response getDatasetTypes(@PathParam("idOrName") String idOrName, @HeaderParam(ACCEPT_LANGUAGE) String acceptLanguage) {
5699+
Locale locale = i18nUtil.parseAcceptLanguageHeader(acceptLanguage);
56985700
DatasetType datasetType = null;
56995701
if (StringUtils.isNumeric(idOrName)) {
57005702
try {
@@ -5707,7 +5709,7 @@ public Response getDatasetTypes(@PathParam("idOrName") String idOrName) {
57075709
datasetType = datasetTypeSvc.getByName(idOrName);
57085710
}
57095711
if (datasetType != null) {
5710-
return ok(datasetType.toJson());
5712+
return ok(datasetType.toJson(locale));
57115713
} else {
57125714
return error(NOT_FOUND, "Could not find a dataset type with name " + idOrName);
57135715
}
@@ -5796,7 +5798,10 @@ public Response addDatasetType(@Context ContainerRequestContext crc, String json
57965798
DatasetType saved = datasetTypeSvc.save(datasetType);
57975799
Long typeId = saved.getId();
57985800
String name = saved.getName();
5799-
return ok(saved.toJson());
5801+
// Locale is null because when creating the dataset type we are relying entirely
5802+
// on the database. The new dataset type has not yet been localized in a
5803+
// properties file.
5804+
return ok(saved.toJson(null));
58005805
} catch (WrappedResponse ex) {
58015806
return error(BAD_REQUEST, ex.getMessage());
58025807
}

src/main/java/edu/harvard/iq/dataverse/dataset/DatasetType.java

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import edu.harvard.iq.dataverse.MetadataBlock;
44
import edu.harvard.iq.dataverse.license.License;
5+
import edu.harvard.iq.dataverse.util.BundleUtil;
56
import jakarta.json.Json;
67
import jakarta.json.JsonArrayBuilder;
78
import jakarta.json.JsonObjectBuilder;
@@ -19,6 +20,9 @@
1920
import java.io.Serializable;
2021
import java.util.ArrayList;
2122
import java.util.List;
23+
import java.util.Locale;
24+
import java.util.MissingResourceException;
25+
import java.util.logging.Logger;
2226

2327
@NamedQueries({
2428
@NamedQuery(name = "DatasetType.findAll",
@@ -36,6 +40,8 @@
3640

3741
public class DatasetType implements Serializable {
3842

43+
private static final Logger logger = Logger.getLogger(DatasetType.class.getCanonicalName());
44+
3945
public static final String DATASET_TYPE_DATASET = "dataset";
4046
public static final String DATASET_TYPE_SOFTWARE = "software";
4147
public static final String DATASET_TYPE_WORKFLOW = "workflow";
@@ -90,6 +96,10 @@ public void setName(String name) {
9096
this.name = name;
9197
}
9298

99+
/**
100+
* In most cases, you should call the getDisplayName(locale) version. This is
101+
* here in case you really want the value from the database.
102+
*/
93103
public String getDisplayName() {
94104
return displayName;
95105
}
@@ -114,7 +124,7 @@ public void setLicenses(List<License> licenses) {
114124
this.licenses = licenses;
115125
}
116126

117-
public JsonObjectBuilder toJson() {
127+
public JsonObjectBuilder toJson(Locale locale) {
118128
JsonArrayBuilder linkedMetadataBlocks = Json.createArrayBuilder();
119129
for (MetadataBlock metadataBlock : this.getMetadataBlocks()) {
120130
linkedMetadataBlocks.add(metadataBlock.getName());
@@ -126,9 +136,34 @@ public JsonObjectBuilder toJson() {
126136
return Json.createObjectBuilder()
127137
.add("id", getId())
128138
.add("name", getName())
129-
.add("displayName", getDisplayName())
139+
.add("displayName", getDisplayName(locale))
130140
.add("linkedMetadataBlocks", linkedMetadataBlocks)
131141
.add("availableLicenses", availableLicenses);
132142
}
133143

144+
public String getDisplayName(Locale locale) {
145+
logger.fine("Getting display name for dataset type " + name + " and locale " + locale);
146+
if (locale == null) {
147+
logger.fine("Locale is null, returning default display name: " + displayName);
148+
return displayName;
149+
}
150+
if (locale.getLanguage().isBlank()) {
151+
logger.fine("Locale couldn't be parsed, returning default display name: " + displayName);
152+
return displayName;
153+
}
154+
if (locale.getLanguage().equals(Locale.ENGLISH.getLanguage())) {
155+
// This is here to prevent looking up datasetTypes_en.properties, which doesn't exist.
156+
// The English strings are in datasetTypes.properties (no _en).
157+
logger.fine("Locale is English, returning default display name: " + displayName);
158+
return displayName;
159+
}
160+
String propertiesFile = "datasetTypes_" + locale.toLanguageTag() + ".properties";
161+
try {
162+
logger.fine("Looking up " + name + ".displayName in " + propertiesFile);
163+
return BundleUtil.getStringFromPropertyFile(name + ".displayName", "datasetTypes", locale);
164+
} catch (MissingResourceException e) {
165+
logger.warning(name + ".displayName missing from " + propertiesFile + " (or file does not exist). Returning English version.");
166+
return displayName;
167+
}
168+
}
134169
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package edu.harvard.iq.dataverse.i18n;
2+
3+
import java.util.List;
4+
import java.util.Locale;
5+
6+
public class i18nUtil {
7+
8+
/**
9+
* @param acceptLanguageHeader The Accept-Language header value such as
10+
* "Accept-Language: en-US,en;q=0.5"
11+
* @return The first Locale or null.
12+
*/
13+
public static Locale parseAcceptLanguageHeader(String acceptLanguageHeader) {
14+
if (acceptLanguageHeader == null || acceptLanguageHeader.isEmpty()) {
15+
return null;
16+
}
17+
List<Locale.LanguageRange> list = Locale.LanguageRange.parse(acceptLanguageHeader);
18+
if (list.isEmpty()) {
19+
return null;
20+
}
21+
Locale.LanguageRange languageRange = list.get(0);
22+
return Locale.forLanguageTag(languageRange.getRange());
23+
}
24+
25+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# This file contains the strings for dataset types that can be
2+
# translated into other languages (French, Spanish, etc.).
3+
# Only the default dataset type (dataset) is included, as an example.
4+
# If you add additional dataset types (e.g. software), you don't
5+
# need to add them to this file if you are only running in English.
6+
# However, if you are running in additional languages, you should
7+
# add the additional dataset types to this file.
8+
dataset.displayName=Dataset

src/test/java/edu/harvard/iq/dataverse/api/DatasetTypesIT.java

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import static edu.harvard.iq.dataverse.api.ApiConstants.DS_VERSION_LATEST_PUBLISHED;
44
import edu.harvard.iq.dataverse.dataset.DatasetType;
55
import edu.harvard.iq.dataverse.util.StringUtil;
6+
import static io.restassured.path.json.JsonPath.with;
67
import io.restassured.RestAssured;
78
import io.restassured.path.json.JsonPath;
89
import io.restassured.response.Response;
@@ -12,6 +13,8 @@
1213
import static jakarta.ws.rs.core.Response.Status.CREATED;
1314
import static jakarta.ws.rs.core.Response.Status.FORBIDDEN;
1415
import static jakarta.ws.rs.core.Response.Status.OK;
16+
import java.util.List;
17+
import java.util.Map;
1518
import java.util.UUID;
1619
import org.hamcrest.CoreMatchers;
1720
import static org.hamcrest.CoreMatchers.containsString;
@@ -902,4 +905,66 @@ public void testCreateReview() {
902905
UtilIT.publishDatasetViaNativeApi(reviewPid, "major", apiTokenReviewer).then().assertThat().statusCode(OK.getStatusCode());
903906
}
904907

908+
@Test
909+
public void testInternationalization() {
910+
Response getDatasetType = UtilIT.getDatasetType("software");
911+
getDatasetType.prettyPrint();
912+
getDatasetType.then().assertThat()
913+
.statusCode(OK.getStatusCode())
914+
.body("data.name", is("software"))
915+
.body("data.displayName", is("Software"));
916+
917+
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Accept-Language
918+
getDatasetType = UtilIT.getDatasetType("software", "en-US,en;q=0.5");
919+
getDatasetType.prettyPrint();
920+
getDatasetType.then().assertThat()
921+
.statusCode(OK.getStatusCode())
922+
.body("data.name", is("software"))
923+
.body("data.displayName", is("Software"));
924+
925+
getDatasetType = UtilIT.getDatasetType("software", "en-US");
926+
getDatasetType.prettyPrint();
927+
getDatasetType.then().assertThat()
928+
.statusCode(OK.getStatusCode())
929+
.body("data.name", is("software"))
930+
.body("data.displayName", is("Software"));
931+
932+
getDatasetType = UtilIT.getDatasetType("software", "");
933+
getDatasetType.prettyPrint();
934+
getDatasetType.then().assertThat()
935+
.statusCode(OK.getStatusCode())
936+
.body("data.name", is("software"))
937+
.body("data.displayName", is("Software"));
938+
939+
boolean i18nIsConfigured = false;
940+
if (!i18nIsConfigured) {
941+
System.out.println("i18n is not configured; skipping test of non-English languages");
942+
return;
943+
}
944+
945+
getDatasetType = UtilIT.getDatasetType("software", "fr-CA,fr;q=0.8,en-US;q=0.6,en;q=0.4");
946+
getDatasetType.prettyPrint();
947+
getDatasetType.then().assertThat()
948+
.statusCode(OK.getStatusCode())
949+
.body("data.name", is("software"))
950+
.body("data.displayName", is("Logiciel"));
951+
952+
getDatasetType = UtilIT.getDatasetTypes("fr-CA,fr;q=0.8,en-US;q=0.6,en;q=0.4");
953+
getDatasetType.prettyPrint();
954+
getDatasetType.then().assertThat()
955+
.statusCode(OK.getStatusCode());
956+
957+
// Messy but the only way we've figured out ¯\_(ツ)_/¯
958+
List<Map<String, Object>> dataset = with(getDatasetType.body().asString()).param("dataset", "dataset")
959+
.getList("data.findAll { data -> data.name == dataset }");
960+
Map<String, Object> firstDataset = dataset.get(0);
961+
assertEquals("Ensemble de données", firstDataset.get("displayName"));
962+
963+
List<Map<String, Object>> instrument = with(getDatasetType.body().asString()).param("instrument", "instrument")
964+
.getList("data.findAll { data -> data.name == instrument }");
965+
Map<String, Object> firstInstrument = instrument.get(0);
966+
// Instrument isn't translated in the French properties file; should fall back to English
967+
assertEquals("Instrument", firstInstrument.get("displayName"));
968+
}
969+
905970
}

src/test/java/edu/harvard/iq/dataverse/api/UtilIT.java

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
import jakarta.json.JsonObject;
1717

1818
import static edu.harvard.iq.dataverse.util.json.NullSafeJsonBuilder.jsonObjectBuilder;
19+
import static jakarta.ws.rs.core.HttpHeaders.ACCEPT_LANGUAGE;
1920
import static jakarta.ws.rs.core.Response.Status.CREATED;
2021

2122
import java.nio.charset.StandardCharsets;
@@ -4622,14 +4623,28 @@ static Response listDataverseInputLevels(String dataverseAlias, String apiToken)
46224623
}
46234624

46244625
public static Response getDatasetTypes() {
4625-
Response response = given()
4626-
.get("/api/datasets/datasetTypes");
4627-
return response;
4626+
return getDatasetTypes(null);
4627+
}
4628+
4629+
public static Response getDatasetTypes(String acceptLanguage) {
4630+
RequestSpecification requestSpecification = given();
4631+
if (acceptLanguage != null) {
4632+
https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Accept-Language
4633+
requestSpecification.header(ACCEPT_LANGUAGE, acceptLanguage);
4634+
}
4635+
return requestSpecification.get("/api/datasets/datasetTypes");
46284636
}
46294637

46304638
static Response getDatasetType(String idOrName) {
4631-
return given()
4632-
.get("/api/datasets/datasetTypes/" + idOrName);
4639+
return getDatasetType(idOrName, null);
4640+
}
4641+
4642+
static Response getDatasetType(String idOrName, String acceptLanguage) {
4643+
RequestSpecification requestSpecification = given();
4644+
if (acceptLanguage != null) {
4645+
requestSpecification.header(ACCEPT_LANGUAGE, acceptLanguage);
4646+
}
4647+
return requestSpecification.get("/api/datasets/datasetTypes/" + idOrName);
46334648
}
46344649

46354650
static Response addDatasetType(String jsonIn, String apiToken) {
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package edu.harvard.iq.dataverse.i18n;
2+
3+
import org.junit.jupiter.api.Test;
4+
import static org.junit.jupiter.api.Assertions.*;
5+
import java.util.Locale;
6+
7+
public class i18nUtilTest {
8+
9+
@Test
10+
void testParseAcceptLanguageHeader_singleLanguage() {
11+
Locale locale = i18nUtil.parseAcceptLanguageHeader("en-US");
12+
assertEquals(Locale.forLanguageTag("en-US"), locale);
13+
}
14+
15+
@Test
16+
void testParseAcceptLanguageHeader_singleLanguageWithQ() {
17+
Locale locale = i18nUtil.parseAcceptLanguageHeader("en-US,en;q=0.5");
18+
assertEquals(Locale.forLanguageTag("en-US"), locale);
19+
}
20+
21+
@Test
22+
void testParseAcceptLanguageHeader_multipleLanguages() {
23+
Locale locale = i18nUtil.parseAcceptLanguageHeader("fr-CA,fr;q=0.8,en-US;q=0.6,en;q=0.4");
24+
assertEquals(Locale.forLanguageTag("fr-CA"), locale);
25+
}
26+
27+
@Test
28+
void testParseAcceptLanguageHeader_emptyHeader() {
29+
Locale locale = i18nUtil.parseAcceptLanguageHeader("");
30+
assertNull(locale);
31+
}
32+
33+
@Test
34+
void testParseAcceptLanguageHeader_nullHeader() {
35+
Locale locale = i18nUtil.parseAcceptLanguageHeader(null);
36+
assertNull(locale);
37+
}
38+
39+
@Test
40+
void testParseAcceptLanguageHeader_invalidHeader() {
41+
Locale locale = i18nUtil.parseAcceptLanguageHeader("invalid-header");
42+
assertEquals(Locale.forLanguageTag("invalid-header"), locale);
43+
}
44+
}

0 commit comments

Comments
 (0)