i18n work in rendering context (WIP) (#1592)

This commit is contained in:
Grahame Grieve 2024-04-09 05:19:25 +10:00
parent 2ce5bbd0d8
commit a111989bb4
8 changed files with 212 additions and 68 deletions

View File

@ -42,6 +42,7 @@ import javax.annotation.Nullable;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.Locale;
import java.util.Map;
import java.util.TimeZone;
import java.util.concurrent.ConcurrentHashMap;
@ -50,7 +51,7 @@ import static org.apache.commons.lang3.StringUtils.isBlank;
public abstract class BaseDateTimeType extends PrimitiveType<Date> {
static final long NANOS_PER_MILLIS = 1000000L;
static final long NANOS_PER_MILLIS = 1000000L;
static final long NANOS_PER_SECOND = 1000000000L;
private static final Map<String, TimeZone> timezoneCache = new ConcurrentHashMap<>();
@ -816,9 +817,14 @@ public abstract class BaseDateTimeType extends PrimitiveType<Date> {
* {@link #toHumanDisplayLocalTimezone()} instead.
* </p>
*/
public String toHumanDisplay() {
return DateTimeUtil.toHumanDisplay(getTimeZone(), getPrecision(), getValue(), getValueAsString());
}
public String toHumanDisplay() {
return DateTimeUtil.toHumanDisplay(getTimeZone(), getPrecision(), getValue(), getValueAsString());
}
public String toHumanDisplay(Locale locale) {
return DateTimeUtil.toHumanDisplay(locale, getTimeZone(), getPrecision(), getValue());
}
/**
* Returns a human readable version of this date/time using the system local format, converted to the local timezone

View File

@ -24,8 +24,11 @@ import org.hl7.fhir.r5.model.Coding;
import org.hl7.fhir.r5.model.Enumeration;
import org.hl7.fhir.r5.model.Extension;
import org.hl7.fhir.r5.model.Resource;
import org.hl7.fhir.r5.model.StringType;
import org.hl7.fhir.r5.renderers.CodeSystemRenderer.Translateable;
import org.hl7.fhir.r5.renderers.utils.RenderingContext;
import org.hl7.fhir.r5.renderers.utils.RenderingContext.KnownLinkType;
import org.hl7.fhir.r5.renderers.utils.RenderingContext.MultiLanguagePolicy;
import org.hl7.fhir.r5.renderers.utils.Resolver.ResourceContext;
import org.hl7.fhir.r5.terminologies.CodeSystemUtilities;
import org.hl7.fhir.r5.terminologies.CodeSystemUtilities.CodeSystemNavigator;
@ -38,6 +41,27 @@ import org.hl7.fhir.utilities.xhtml.XhtmlNode;
public class CodeSystemRenderer extends TerminologyRenderer {
public class Translateable {
private String lang;
private StringType value;
public Translateable(String lang, StringType value) {
this.lang = lang;
this.value = value;
}
public String getLang() {
return lang;
}
public StringType getValue() {
return value;
}
}
private Boolean doMarkdown = null;
public CodeSystemRenderer(RenderingContext context) {
@ -426,34 +450,47 @@ public class CodeSystemRenderer extends TerminologyRenderer {
if (hasDefinitions) {
td = tr.td();
if (c != null &&c.hasDefinitionElement()) {
if (getContext().getLang() == null) {
if (hasMarkdownInDefinitions(cs)) {
addMarkdown(renderStatusDiv(c.getDefinitionElement(), td), c.getDefinition());
} else {
renderStatus(c.getDefinitionElement(), td).addText(c.getDefinition());
// translations of the definition might come from either the translation extension, or from the designations
StringType defn = context.getTranslatedElement(c.getDefinitionElement());
boolean sl = false;
for (ConceptDefinitionDesignationComponent cd : c.getDesignation()) {
if (cd.getUse().is("http://terminology.hl7.org/CodeSystem/designation-usage", "definition") && cd.hasLanguage() && !c.getDefinition().equalsIgnoreCase(cd.getValue())) {
sl = true;
}
} else if (getContext().getLang().equals("*")) {
boolean sl = false;
for (ConceptDefinitionDesignationComponent cd : c.getDesignation())
if (cd.getUse().is("http://terminology.hl7.org/CodeSystem/designation-usage", "definition") && cd.hasLanguage() && !c.getDefinition().equalsIgnoreCase(cd.getValue()))
sl = true;
td.addText((sl ? cs.getLanguage("en")+": " : ""));
if (hasMarkdownInDefinitions(cs))
addMarkdown(renderStatusDiv(c.getDefinitionElement(), td), c.getDefinition());
else
renderStatus(c.getDefinitionElement(), td).addText(c.getDefinition());
for (ConceptDefinitionDesignationComponent cd : c.getDesignation()) {
}
if (getContext().getMultiLanguagePolicy() == MultiLanguagePolicy.NONE && (sl || ToolingExtensions.hasLanguageTranslations(defn))) {
if (hasMarkdownInDefinitions(cs)) {
addMarkdown(renderStatusDiv(defn, td), defn.asStringValue());
} else {
renderStatus(defn, td).addText(defn.asStringValue());
}
} else {
List<Translateable> list = new ArrayList<>();
list.add(new Translateable(cs.getLanguage(), defn));
for (Extension ext : defn.getExtensionsByUrl(ToolingExtensions.EXT_TRANSLATION)) {
list.add(new Translateable(ext.getExtensionString("lang"), ext.getExtensionByUrl("content").getValueStringType()));
}
for (ConceptDefinitionDesignationComponent cd : c.getDesignation()) {
if (cd.getUse().is("http://terminology.hl7.org/CodeSystem/designation-usage", "definition") && cd.hasLanguage() && !c.getDefinition().equalsIgnoreCase(cd.getValue())) {
td.br();
td.addText(cd.getLanguage()+": "+cd.getValue());
list.add(new Translateable(cd.getLanguage(), cd.getValueElement()));
}
}
} else if (getContext().getLang().equals(cs.getLanguage()) || (getContext().getLang().equals("en") && !cs.hasLanguage())) {
renderStatus(c.getDefinitionElement(), td).addText(c.getDefinition());
} else {
for (ConceptDefinitionDesignationComponent cd : c.getDesignation()) {
if (cd.getUse().is("http://terminology.hl7.org/CodeSystem/designation-usage", "definition") && cd.hasLanguage() && cd.getLanguage().equals(getContext().getLang())) {
td.addText(cd.getValue());
boolean first = true;
for (Translateable ti : list) {
if (first) {
first = false;
} else {
td.br();
}
if (ti.lang != null) {
td.addText(ti.lang + ": ");
}
if (hasMarkdownInDefinitions(cs)) {
addMarkdown(renderStatusDiv(ti.getValue(), td), ti.getValue().asStringValue());
} else {
renderStatus(ti.getValue(), td).addText(ti.getValue().asStringValue());
}
}
}

View File

@ -346,7 +346,7 @@ public class DataRenderer extends Renderer implements CodeResolver {
if (JurisdictionUtilities.isJurisdiction(system)) {
return JurisdictionUtilities.displayJurisdiction(system+"#"+code);
}
ValidationResult t = getContext().getWorker().validateCode(getContext().getTerminologyServiceOptions().withLanguage(context.getLang()).withVersionFlexible(true), system, version, code, null);
ValidationResult t = getContext().getWorker().validateCode(getContext().getTerminologyServiceOptions().withLanguage(context.getLocale().toString()).withVersionFlexible(true), system, version, code, null);
if (t != null && t.getDisplay() != null)
return t.getDisplay();

View File

@ -843,8 +843,8 @@ public abstract class ResourceRenderer extends DataRenderer {
}
public void markLanguage(XhtmlNode x) {
x.setAttribute("lang", context.getLang());
x.setAttribute("xml:lang", context.getLang());
x.setAttribute("lang", context.getLocale().toString());
x.setAttribute("xml:lang", context.getLocale().toString());
x.addTag(0, "hr");
x.addTag(0, "p").b().tx(context.getLocale().getDisplayName());
x.addTag(0, "hr");

View File

@ -6,9 +6,11 @@ import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import org.hl7.fhir.exceptions.FHIRException;
import org.hl7.fhir.exceptions.FHIRFormatError;
@ -18,11 +20,8 @@ import org.hl7.fhir.r5.context.IWorkerContext;
import org.hl7.fhir.r5.elementmodel.Element;
import org.hl7.fhir.r5.fhirpath.FHIRPathEngine.IEvaluationContext;
import org.hl7.fhir.r5.model.Base;
import org.hl7.fhir.r5.model.BaseDateTimeType;
import org.hl7.fhir.r5.model.DateTimeType;
import org.hl7.fhir.r5.model.DomainResource;
import org.hl7.fhir.r5.model.Enumeration;
import org.hl7.fhir.r5.model.Extension;
import org.hl7.fhir.r5.model.PrimitiveType;
import org.hl7.fhir.r5.model.StringType;
import org.hl7.fhir.r5.renderers.utils.Resolver.IReferenceResolver;
@ -36,6 +35,40 @@ import org.hl7.fhir.utilities.Utilities;
import org.hl7.fhir.utilities.i18n.RenderingI18nContext;
import org.hl7.fhir.utilities.validation.ValidationOptions;
/**
* Managing Language when rendering
*
* You can specify a language to use when rendering resources by setting the setLocale() on
* the super class. The locale drives the following:
* - choice of java supplied rendering phrase, if translations are provided for the locale
* - integer and date formats used (but see below for date formats)
* - automatic translation of coded values, if language supplements are available
* - choosing text representation considering the FHIR translation extension
*
* By default, the locale is null, and the default locale for the underlying system is used.
* If you set locale to a specific value, then that value will be used instead of the default locale.
*
* By default, only a single language is rendered, based on the locale. Where resources contain
* multiple language content (designations in CodeSystem and ValueSet, or using the translation
* extension), you can control what languages are presented using the properties multiLanguagePolicy
* and languages
* - multiLanguagePolicy: NONE (default), DESIGNATIONS, ALL
* - languages: a list of allowed languages. Default is empty which means all languages in scope via multiLanguagePolicy
*
* Managing Date/Time Formatting
*
* This class has multiple parameters that influence date/time formatting when rendering resources
*
* - The default rendering is using the default java locale as above
* - If you setLocale() to something, then the defaults for the locale will be used
* - Else you can set the values of dateTimeFormat, dateFormat, dateYearFormat and dateYearMonthFormat
*
* If you set the value of locale, the values of dateTimeFormat, dateFormat, dateYearFormat and dateYearMonthFormat are
* reset to the system defaults
*
* Timezones: by default, date/times are rendered in their source timezone
*
*/
public class RenderingContext extends RenderingI18nContext {
// provides liquid templates, if they are available for the content
@ -176,6 +209,12 @@ public class RenderingContext extends RenderingI18nContext {
}
}
public enum MultiLanguagePolicy {
NONE, // ONLY render the language in the locale
DESIGNATIONS, // in addition to the locale language, render designations from other languages (eg. as found in code systems and value sets
ALL // in addition to translations in designations, look for an render translations (WIP)
}
private IWorkerContext worker;
private MarkDownProcessor markdown;
private ResourceRendererMode mode;
@ -185,7 +224,15 @@ public class RenderingContext extends RenderingI18nContext {
private IEvaluationContext services;
private ITypeParser parser;
private String lang;
// i18n related fields
private MultiLanguagePolicy multiLanguagePolicy = MultiLanguagePolicy.NONE;
private Set<String> allowedLanguages = new HashSet<>();
private ZoneId timeZoneId;
private DateTimeFormatter dateTimeFormat;
private DateTimeFormatter dateFormat;
private DateTimeFormatter dateYearFormat;
private DateTimeFormatter dateYearMonthFormat;
private String localPrefix; // relative link within local context
private int headerLevelContext;
private boolean canonicalUrlsAsLinks;
@ -212,11 +259,6 @@ public class RenderingContext extends RenderingI18nContext {
private boolean showComments = false;
private FhirPublication targetVersion;
private ZoneId timeZoneId;
private DateTimeFormatter dateTimeFormat;
private DateTimeFormatter dateFormat;
private DateTimeFormatter dateYearFormat;
private DateTimeFormatter dateYearMonthFormat;
private boolean copyButton;
private ProfileKnowledgeProvider pkp;
private String changeVersion;
@ -233,13 +275,13 @@ public class RenderingContext extends RenderingI18nContext {
* @param markdown - appropriate markdown processing engine
* @param terminologyServiceOptions - options to use when looking up codes
* @param specLink - path to FHIR specification
* @param lang - langauage to render in
* @param locale - i18n for rendering
*/
public RenderingContext(IWorkerContext worker, MarkDownProcessor markdown, ValidationOptions terminologyServiceOptions, String specLink, String localPrefix, String lang, ResourceRendererMode mode, GenerationRules rules) {
public RenderingContext(IWorkerContext worker, MarkDownProcessor markdown, ValidationOptions terminologyServiceOptions, String specLink, String localPrefix, Locale locale, ResourceRendererMode mode, GenerationRules rules) {
super();
this.worker = worker;
this.markdown = markdown;
this.lang = lang;
this.locale = locale;
this.links.put(KnownLinkType.SPEC, specLink);
this.localPrefix = localPrefix;
this.mode = mode;
@ -247,12 +289,10 @@ public class RenderingContext extends RenderingI18nContext {
if (terminologyServiceOptions != null) {
this.terminologyServiceOptions = terminologyServiceOptions;
}
// default to US locale - discussion here: https://github.com/hapifhir/org.hl7.fhir.core/issues/666
this.locale = new Locale.Builder().setLanguageTag("en-US").build();
}
public RenderingContext copy() {
RenderingContext res = new RenderingContext(worker, markdown, terminologyServiceOptions, getLink(KnownLinkType.SPEC), localPrefix, lang, mode, rules);
RenderingContext res = new RenderingContext(worker, markdown, terminologyServiceOptions, getLink(KnownLinkType.SPEC), localPrefix, locale, mode, rules);
res.resolver = resolver;
res.templateProvider = templateProvider;
@ -291,6 +331,8 @@ public class RenderingContext extends RenderingI18nContext {
res.terminologyServiceOptions = terminologyServiceOptions.copy();
res.typeMap.putAll(typeMap);
res.multiLanguagePolicy = multiLanguagePolicy;
res.allowedLanguages.addAll(allowedLanguages);
return res;
}
@ -330,8 +372,16 @@ public class RenderingContext extends RenderingI18nContext {
return markdown;
}
public String getLang() {
return lang;
public MultiLanguagePolicy getMultiLanguagePolicy() {
return multiLanguagePolicy;
}
public void setMultiLanguagePolicy(MultiLanguagePolicy multiLanguagePolicy) {
this.multiLanguagePolicy = multiLanguagePolicy;
}
public Set<String> getAllowedLanguages() {
return allowedLanguages;
}
public String getLocalPrefix() {
@ -502,14 +552,6 @@ public class RenderingContext extends RenderingI18nContext {
return ref;
}
public RenderingContext setLang(String lang) {
this.lang = lang;
if (lang != null) {
setLocale(new Locale(lang));
}
return this;
}
public RenderingContext setLocalPrefix(String localPrefix) {
this.localPrefix = localPrefix;
return this;
@ -746,8 +788,8 @@ public class RenderingContext extends RenderingI18nContext {
public String getTranslated(PrimitiveType<?> t) {
if (lang != null) {
String v = ToolingExtensions.getLanguageTranslation(t, lang);
if (locale != null) {
String v = ToolingExtensions.getLanguageTranslation(t, locale.toString());
if (v != null) {
return v;
}
@ -755,18 +797,32 @@ public class RenderingContext extends RenderingI18nContext {
return t.asStringValue();
}
public StringType getTranslatedElement(PrimitiveType<?> t) {
if (locale != null) {
StringType v = ToolingExtensions.getLanguageTranslationElement(t, locale.toString());
if (v != null) {
return v;
}
}
if (t instanceof StringType) {
return (StringType) t;
} else {
return new StringType(t.asStringValue());
}
}
public String getTranslatedCode(Base b, String codeSystem) {
if (b instanceof org.hl7.fhir.r5.model.Element) {
org.hl7.fhir.r5.model.Element e = (org.hl7.fhir.r5.model.Element) b;
if (lang != null) {
String v = ToolingExtensions.getLanguageTranslation(e, lang);
if (locale != null) {
String v = ToolingExtensions.getLanguageTranslation(e, locale.toString());
if (v != null) {
return v;
}
// no? then see if the tx service can translate it for us
try {
ValidationResult t = getContext().validateCode(getTerminologyServiceOptions().withLanguage(lang).withVersionFlexible(true),
ValidationResult t = getContext().validateCode(getTerminologyServiceOptions().withLanguage(locale.toString()).withVersionFlexible(true),
codeSystem, null, e.primitiveValue(), null);
if (t.isOk() && t.getDisplay() != null) {
return t.getDisplay();
@ -788,14 +844,14 @@ public class RenderingContext extends RenderingI18nContext {
}
public String getTranslatedCode(Enumeration<?> e, String codeSystem) {
if (lang != null) {
String v = ToolingExtensions.getLanguageTranslation(e, lang);
if (locale != null) {
String v = ToolingExtensions.getLanguageTranslation(e, locale.toString());
if (v != null) {
return v;
}
// no? then see if the tx service can translate it for us
try {
ValidationResult t = getContext().validateCode(getTerminologyServiceOptions().withLanguage(lang).withVersionFlexible(true),
ValidationResult t = getContext().validateCode(getTerminologyServiceOptions().withLanguage(locale.toString()).withVersionFlexible(true),
codeSystem, null, e.getCode(), null);
if (t.isOk() && t.getDisplay() != null) {
return t.getDisplay();
@ -818,14 +874,14 @@ public class RenderingContext extends RenderingI18nContext {
}
public String getTranslatedCode(Element e, String codeSystem) {
if (lang != null) {
if (locale != null) {
// first we look through the translation extensions
for (Element ext : e.getChildrenByName("extension")) {
String url = ext.getNamedChildValue("url");
if (url.equals(ToolingExtensions.EXT_TRANSLATION)) {
Base e1 = ext.getExtensionValue("lang");
if (e1 != null && e1.primitiveValue() != null && e1.primitiveValue().equals(lang)) {
if (e1 != null && e1.primitiveValue() != null && e1.primitiveValue().equals(locale.toString())) {
e1 = ext.getExtensionValue("content");
if (e1 != null && e1.isPrimitive()) {
return e1.primitiveValue();
@ -835,7 +891,7 @@ public class RenderingContext extends RenderingI18nContext {
}
// no? then see if the tx service can translate it for us
try {
ValidationResult t = getContext().validateCode(getTerminologyServiceOptions().withLanguage(lang).withVersionFlexible(true),
ValidationResult t = getContext().validateCode(getTerminologyServiceOptions().withLanguage(locale.toString()).withVersionFlexible(true),
codeSystem, null, e.primitiveValue(), null);
if (t.isOk() && t.getDisplay() != null) {
return t.getDisplay();

View File

@ -776,6 +776,15 @@ public class ToolingExtensions {
// throw new Error("Attempt to assign multiple OIDs to value set "+vs.getName()+" ("+vs.getUrl()+"). Has "+readStringExtension(vs, EXT_OID)+", trying to add "+oid);
// }
public static boolean hasLanguageTranslations(Element element) {
for (Extension e : element.getExtension()) {
if (e.getUrl().equals(EXT_TRANSLATION)) {
return true;
}
}
return false;
}
public static boolean hasLanguageTranslation(Element element, String lang) {
for (Extension e : element.getExtension()) {
if (e.getUrl().equals(EXT_TRANSLATION)) {
@ -802,6 +811,20 @@ public class ToolingExtensions {
return null;
}
public static StringType getLanguageTranslationElement(Element element, String lang) {
for (Extension e : element.getExtension()) {
if (e.getUrl().equals(EXT_TRANSLATION)) {
Extension e1 = ExtensionHelper.getExtension(e, "lang");
if (e1 != null && e1.getValue() != null && e1.getValue() instanceof CodeType && ((CodeType) e1.getValue()).getValue().equals(lang)) {
e1 = ExtensionHelper.getExtension(e, "content");
return ((StringType) e1.getValue());
}
}
}
return null;
}
public static void addLanguageTranslation(Element element, String lang, String value) {
if (Utilities.noString(lang) || Utilities.noString(value))
return;

View File

@ -2,6 +2,7 @@ package org.hl7.fhir.utilities;
import java.util.Calendar;
import java.util.Date;
import java.util.Locale;
import java.util.TimeZone;
import org.apache.commons.lang3.time.FastDateFormat;
@ -42,4 +43,25 @@ public class DateTimeUtil {
return ourHumanDateTimeFormat.format(theValue);
}
}
public static String toHumanDisplay(Locale locale, TimeZone theTimeZone, TemporalPrecisionEnum thePrecision, Date theValue) {
Calendar value = theTimeZone != null ? Calendar.getInstance(theTimeZone) : Calendar.getInstance();
value.setTime(theValue);
FastDateFormat dateFormat = FastDateFormat.getDateInstance(FastDateFormat.MEDIUM, locale);
FastDateFormat dateTimeFormat = FastDateFormat.getDateTimeInstance(FastDateFormat.MEDIUM, FastDateFormat.MEDIUM, locale);
switch (thePrecision) {
case YEAR:
case MONTH:
case DAY:
return dateFormat.format(value);
case MILLI:
case SECOND:
default:
return dateTimeFormat.format(value);
}
}
}

View File

@ -29,7 +29,7 @@ public abstract class I18nBase {
if (Objects.nonNull(locale)) {
return locale;
} else {
return Locale.US;
return Locale.getDefault();
}
}