001package org.hl7.fhir.r5.elementmodel;
002
003import java.util.ArrayList;
004import java.util.Arrays;
005import java.util.HashSet;
006import java.util.List;
007import java.util.Set;
008
009import org.checkerframework.checker.units.qual.cd;
010import org.hl7.fhir.r5.context.ContextUtilities;
011import org.hl7.fhir.r5.context.IWorkerContext;
012import org.hl7.fhir.r5.model.Base;
013import org.hl7.fhir.r5.model.CodeSystem;
014import org.hl7.fhir.r5.model.CodeSystem.ConceptDefinitionComponent;
015import org.hl7.fhir.r5.model.CodeSystem.ConceptDefinitionDesignationComponent;
016import org.hl7.fhir.r5.model.CodeSystem.ConceptPropertyComponent;
017import org.hl7.fhir.r5.model.ContactDetail;
018import org.hl7.fhir.r5.model.DataType;
019import org.hl7.fhir.r5.model.ElementDefinition;
020import org.hl7.fhir.r5.model.ElementDefinition.ElementDefinitionBindingAdditionalComponent;
021import org.hl7.fhir.r5.model.ElementDefinition.ElementDefinitionConstraintComponent;
022import org.hl7.fhir.r5.model.Extension;
023import org.hl7.fhir.r5.model.MarkdownType;
024import org.hl7.fhir.r5.model.PrimitiveType;
025import org.hl7.fhir.r5.model.Property;
026import org.hl7.fhir.r5.model.Resource;
027import org.hl7.fhir.r5.model.StringType;
028import org.hl7.fhir.r5.model.StructureDefinition;
029import org.hl7.fhir.r5.terminologies.CodeSystemUtilities;
030import org.hl7.fhir.r5.utils.ToolingExtensions;
031import org.hl7.fhir.utilities.TextFile;
032import org.hl7.fhir.utilities.Utilities;
033import org.hl7.fhir.utilities.i18n.AcceptLanguageHeader;
034import org.hl7.fhir.utilities.i18n.AcceptLanguageHeader.LanguagePreference;
035import org.hl7.fhir.utilities.i18n.LanguageFileProducer;
036import org.hl7.fhir.utilities.i18n.LanguageFileProducer.LanguageProducerLanguageSession;
037import org.hl7.fhir.utilities.i18n.LanguageFileProducer.TextUnit;
038import org.hl7.fhir.utilities.i18n.LanguageFileProducer.TranslationUnit;
039import org.hl7.fhir.utilities.json.model.JsonElement;
040import org.hl7.fhir.utilities.validation.ValidationMessage;
041import org.hl7.fhir.utilities.validation.ValidationMessage.IssueSeverity;
042import org.hl7.fhir.utilities.validation.ValidationMessage.IssueType;
043import org.hl7.fhir.utilities.validation.ValidationMessage.Source;
044
045/**
046 * in here:
047 *   * generateTranslations
048 *   * importFromTranslations
049 *   * stripTranslations
050 *   * switchLanguage
051 *   
052 *  in the validator
053 *  
054 * @author grahamegrieve
055 *  generateTranslations = -langTransform export -src {src} -tgt {tgt} -dest {dest}
056 *  importFromTranslations =  -langTransform import -src {src} -tgt {tgt} -dest {dest}
057 */
058public class LanguageUtils {
059
060  public static final List<String> TRANSLATION_SUPPLEMENT_RESOURCE_TYPES = Arrays.asList("CodeSystem", "StructureDefinition", "Questionnaire");
061
062  private static final String ORPHAN_TRANSLATIONS_NAME = "translations.orphans";
063
064  private static final String SUPPLEMENT_SOURCE_RESOURCE = "translations.supplemented";
065  private static final String SUPPLEMENT_SOURCE_TRANSLATIONS = "translations.source-list";
066  
067  IWorkerContext context;
068  private List<String> crlist;
069  
070  
071  public LanguageUtils(IWorkerContext context) {
072    super();
073    this.context = context;
074  }
075
076  public void generateTranslations(Element resource, LanguageProducerLanguageSession session) {
077    translate(null, resource, session);
078  }
079  
080  
081  private void translate(Element parent, Element element, LanguageProducerLanguageSession langSession) {
082    if (element.isPrimitive() && isTranslatable(element)) {
083      String base = element.primitiveValue();
084      if (base != null) {
085        String translation = getSpecialTranslation(parent, element, langSession.getTargetLang());
086        if (translation == null) {
087          translation = element.getTranslation(langSession.getTargetLang());
088        }
089        langSession.entry(new TextUnit(pathForElement(element), contextForElement(element), base, translation));
090      }
091    }
092    for (Element c: element.getChildren()) {
093      if (!c.getName().equals("designation")) {
094        translate(element, c, langSession);
095      }
096    }
097  }
098
099  private String contextForElement(Element element) {
100    throw new Error("Not done yet");
101  }
102
103  private String getSpecialTranslation(Element parent, Element element, String targetLang) {
104    if (parent == null) {
105      return null;
106    }
107    if (Utilities.existsInList(pathForElement(parent), "CodeSystem.concept", "CodeSystem.concept.concept") && "CodeSystem.concept.display".equals(pathForElement(element))) {
108      return getDesignationTranslation(parent, targetLang);
109    }
110    if (Utilities.existsInList(pathForElement(parent), "ValueSet.compose.include.concept") && "ValueSet.compose.include.concept.display".equals(pathForElement(element))) {
111      return getDesignationTranslation(parent, targetLang);
112    }
113    if (Utilities.existsInList(pathForElement(parent), "ValueSet.expansion.contains", "ValueSet.expansion.contains.contains") && "ValueSet.expansion.contains.display".equals(pathForElement(element))) {
114      return getDesignationTranslation(parent, targetLang);
115    }
116    return null;
117  }
118
119  private String getDesignationTranslation(Element parent, String targetLang) {
120    for (Element e : parent.getChildren("designation")) {
121      String lang = e.getNamedChildValue("language");
122      if (langsMatch(targetLang, lang)) {
123        return e.getNamedChildValue("value");
124      }
125    }
126    return null;
127  }
128
129  private boolean isTranslatable(Element element) {    
130    return element.getProperty().isTranslatable();
131  }
132
133  private String pathForElement(Element element) {
134    String bp = element.getBasePath();
135    return pathForElement(bp, element.getProperty().getStructure().getType());
136  }
137  
138  private String pathForElement(String path, String type) {
139    // special case support for metadata elements prior to R5:
140    if (crlist == null) {
141      crlist = new ContextUtilities(context).getCanonicalResourceNames();
142    }
143    if (crlist.contains(type)) {
144      String fp = path.replace(type+".", "CanonicalResource.");
145      if (Utilities.existsInList(fp,
146         "CanonicalResource.url", "CanonicalResource.identifier", "CanonicalResource.version", "CanonicalResource.name", 
147         "CanonicalResource.title", "CanonicalResource.status", "CanonicalResource.experimental", "CanonicalResource.date",
148         "CanonicalResource.publisher", "CanonicalResource.contact", "CanonicalResource.description", "CanonicalResource.useContext", 
149         "CanonicalResource.jurisdiction"))  {
150        return fp;
151      }
152    }
153    return path; 
154  }
155  
156  
157  public int importFromTranslations(Element resource, List<TranslationUnit> translations) {
158    return importFromTranslations(null, resource, translations, new HashSet<>());
159  }
160  
161  public int importFromTranslations(Element resource, List<TranslationUnit> translations, List<ValidationMessage> messages) {
162    Set<TranslationUnit> usedUnits = new HashSet<>();
163    int r = 0;
164    if (resource.fhirType().equals("StructureDefinition")) {
165      r = importFromTranslationsForSD(null, resource, translations, usedUnits);
166    } else {
167     r = importFromTranslations(null, resource, translations, usedUnits);
168    }
169    for (TranslationUnit t : translations) {
170      if (!usedUnits.contains(t)) {
171        if (messages != null) {
172          messages.add(new ValidationMessage(Source.Publisher, IssueType.INFORMATIONAL, t.getId(), "Unused '"+t.getLanguage()+"' translation '"+t.getSrcText()+"' -> '"+t.getTgtText()+"'", IssueSeverity.INFORMATION));
173        }
174      }
175    }
176    return r;
177  }
178  
179  public int importFromTranslations(Resource resource, List<TranslationUnit> translations, List<ValidationMessage> messages) {
180    Set<TranslationUnit> usedUnits = new HashSet<>();
181    int r = 0;
182    if (resource.fhirType().equals("StructureDefinition")) {
183      // todo... r = importFromTranslationsForSD(null, resource, translations, usedUnits);
184    } else {
185     r = importResourceFromTranslations(null, resource, translations, usedUnits, resource.fhirType());
186    }
187    for (TranslationUnit t : translations) {
188      if (!usedUnits.contains(t)) {
189        if (messages != null) {
190          messages.add(new ValidationMessage(Source.Publisher, IssueType.INFORMATIONAL, t.getId(), "Unused '"+t.getLanguage()+"' translation '"+t.getSrcText()+"' -> '"+t.getTgtText()+"'", IssueSeverity.INFORMATION));
191        }
192      }
193    }
194    return r;
195  }
196  
197
198  /*
199   * */
200  private int importFromTranslationsForSD(Object object, Element resource, List<TranslationUnit> translations, Set<TranslationUnit> usedUnits) {
201    int r = 0;
202    r = r + checkForTranslations(translations, usedUnits, resource, "name", "name");
203    r = r + checkForTranslations(translations, usedUnits, resource, "title", "title");
204    r = r + checkForTranslations(translations, usedUnits, resource, "publisher", "publisher");
205    for (Element cd : resource.getChildrenByName("contact")) {
206      r = r + checkForTranslations(translations, usedUnits, cd, "contact.name", "name");
207    }
208    r = r + checkForTranslations(translations, usedUnits, resource, "purpose", "purpose");
209    r = r + checkForTranslations(translations, usedUnits, resource, "copyright", "copyright");
210    Element diff = resource.getNamedChild("differential");
211    if (diff != null) {
212      for (Element ed : diff.getChildrenByName("element")) {
213        String id = ed.getNamedChildValue("id");
214        r = r + checkForTranslations(translations, usedUnits, ed, id+"/label", "label");
215        r = r + checkForTranslations(translations, usedUnits, ed, id+"/short", "short");
216        r = r + checkForTranslations(translations, usedUnits, ed, id+"/definition", "definition");
217        r = r + checkForTranslations(translations, usedUnits, ed, id+"/comment", "comment");
218        r = r + checkForTranslations(translations, usedUnits, ed, id+"/requirements", "requirements");
219        r = r + checkForTranslations(translations, usedUnits, ed, id+"/meaningWhenMissing", "meaningWhenMissing");
220        r = r + checkForTranslations(translations, usedUnits, ed, id+"/orderMeaning", "orderMeaning");
221        //      for (ElementDefinitionConstraintComponent con : ed.getConstraint()) {
222        //        addToList(list, lang, con, ed.getId()+"/constraint", "human", con.getHumanElement());
223        //      }
224        //      if (ed.hasBinding()) {
225        //        addToList(list, lang, ed.getBinding(), ed.getId()+"/b/desc", "description", ed.getBinding().getDescriptionElement());
226        //        for (ElementDefinitionBindingAdditionalComponent ab : ed.getBinding().getAdditional()) {
227        //          addToList(list, lang, ab, ed.getId()+"/ab/doco", "documentation", ab.getDocumentationElement());
228        //          addToList(list, lang, ab, ed.getId()+"/ab/short", "shortDoco", ab.getShortDocoElement());
229        //        }
230        //      }
231      }
232    }
233    return r;
234  }
235
236  private int checkForTranslations(List<TranslationUnit> translations, Set<TranslationUnit> usedUnits, Element context, String tname, String pname) {
237    int r = 0;
238    Element child = context.getNamedChild(pname);
239    if (child != null) {
240      String v = child.primitiveValue();
241      if (v != null) {
242        for (TranslationUnit tu : translations) {
243          if (tname.equals(tu.getId()) && v.equals(tu.getSrcText())) {
244            usedUnits.add(tu);
245            child.setTranslation(tu.getLanguage(), tu.getTgtText());
246            r++;
247          }
248        }
249      }
250    }
251    return r;
252  }
253
254  private int importResourceFromTranslations(Base parent, Base element, List<TranslationUnit> translations, Set<TranslationUnit> usedUnits, String path) {
255    int t = 0;
256    if (element.isPrimitive() && isTranslatable(element, path) && element instanceof org.hl7.fhir.r5.model.Element) {
257      org.hl7.fhir.r5.model.Element e = (org.hl7.fhir.r5.model.Element) element;
258      String base = element.primitiveValue();
259      if (base != null) {
260        String epath = pathForElement(path, element.fhirType());
261        Set<TranslationUnit> tlist = findTranslations(epath, base, translations);
262        for (TranslationUnit translation : tlist) {
263          t++;
264          if (!handleAsSpecial(parent, element, translation)) {
265            ToolingExtensions.setLanguageTranslation(e, translation.getLanguage(), translation.getTgtText());
266            usedUnits.add(translation);
267          }
268        }
269      }
270    }
271    for (Property c : element.children()) {
272      for (Base v : c.getValues()) {
273        if (!c.getName().equals("designation")) {
274          t = t + importResourceFromTranslations(element, v, translations, usedUnits, path+"."+c.getName());
275        }
276      }
277    }
278    return t;
279  }
280
281  private boolean handleAsSpecial(Base parent, Base element, TranslationUnit translation) {
282    return false;
283  }
284
285  private boolean isTranslatable(Base element, String path) {
286    return Utilities.existsInList(element.fhirType(), "string", "markdown");
287  }
288
289  private int importFromTranslations(Element parent, Element element, List<TranslationUnit> translations, Set<TranslationUnit> usedUnits) {
290    int t = 0;
291    if (element.isPrimitive() && isTranslatable(element)) {
292      String base = element.primitiveValue();
293      if (base != null) {
294        String path = pathForElement(element);
295        Set<TranslationUnit> tlist = findTranslations(path, base, translations);
296        for (TranslationUnit translation : tlist) {
297          t++;
298          if (!handleAsSpecial(parent, element, translation)) {
299            element.setTranslation(translation.getLanguage(), translation.getTgtText());
300            usedUnits.add(translation);
301          }
302        }
303      }
304    }
305    // Create a copy of the children collection before iterating
306    List<Element> childrenCopy = List.copyOf(element.getChildren());
307    for (Element c : childrenCopy) {
308      if (!c.getName().equals("designation")) {
309        t = t + importFromTranslations(element, c, translations, usedUnits);
310      }
311    }
312    return t;
313  }
314
315  private boolean handleAsSpecial(Element parent, Element element, TranslationUnit translation) {
316    if (parent == null) {
317      return false;
318    }
319    if (Utilities.existsInList(pathForElement(parent), "CodeSystem.concept", "CodeSystem.concept.concept") && "CodeSystem.concept.display".equals(pathForElement(element))) {
320      return setDesignationTranslation(parent, translation.getLanguage(), translation.getTgtText());
321    }
322    if (Utilities.existsInList(pathForElement(parent), "ValueSet.compose.include.concept") && "ValueSet.compose.include.concept.display".equals(pathForElement(element))) {
323      return setDesignationTranslation(parent, translation.getLanguage(), translation.getTgtText());
324    }
325    if (Utilities.existsInList(pathForElement(parent), "ValueSet.expansion.contains", "ValueSet.expansion.contains.contains") && "ValueSet.expansion.contains.display".equals(pathForElement(element))) {
326      return setDesignationTranslation(parent, translation.getLanguage(), translation.getTgtText());
327    }
328    return false;
329  }
330
331  private boolean setDesignationTranslation(Element parent, String targetLang, String translation) {
332    for (Element e : parent.getChildren("designation")) {
333      String lang = e.getNamedChildValue("language");
334      if (langsMatch(targetLang, lang)) {
335        Element value = e.getNamedChild("value");
336        if (value != null) {
337          value.setValue(translation);
338        } else {
339          e.addElement("value").setValue(translation);
340        }
341        return true;
342      }
343    }
344    Element d = parent.addElement("designation");
345    d.addElement("language").setValue(targetLang);
346    d.addElement("value").setValue(translation);
347    return true;
348  }
349
350  private Set<TranslationUnit> findTranslations(String path, String src, List<TranslationUnit> translations) {
351    Set<TranslationUnit> res = new HashSet<>();
352    for (TranslationUnit translation : translations) {
353      if (path.equals(translation.getId()) && src.equals(translation.getSrcText())) {
354        res.add(translation);
355      }
356    }
357    return res;
358  }
359
360  public static boolean langsMatchExact(AcceptLanguageHeader langs, String srcLang) {
361    if (langs == null) {
362      return false;
363    }
364    for (LanguagePreference lang : langs.getLangs()) {
365      if (lang.getValue() > 0) {
366        if ("*".equals(lang.getLang())) {
367          return true;
368        } else {
369          return langsMatch(lang.getLang(), srcLang);
370        }
371      }
372    }
373    return false;
374  }
375
376  public static boolean langsMatch(AcceptLanguageHeader langs, String srcLang) {
377    if (langs == null) {
378      return false;
379    }
380    for (LanguagePreference lang : langs.getLangs()) {
381      if (lang.getValue() > 0) {
382        if ("*".equals(lang.getLang())) {
383          return true;
384        } else {
385          boolean ok = langsMatch(lang.getLang(), srcLang);
386          if (ok) {
387            return true;
388          }
389        }
390      }
391    }
392    return false;
393  }
394
395  public static boolean langsMatchExact(String dstLang, String srcLang) {
396    return dstLang == null ? false : dstLang.equals(srcLang);
397  }
398
399  public static boolean langsMatch(String dstLang, String srcLang) {
400    return dstLang == null || srcLang == null ? false : dstLang.startsWith(srcLang) || "*".equals(srcLang);
401  }
402
403  public void fillSupplement(CodeSystem csSrc, CodeSystem csDst, List<TranslationUnit> list) {
404    csDst.setUserData(SUPPLEMENT_SOURCE_RESOURCE, csSrc);
405    csDst.setUserData(SUPPLEMENT_SOURCE_TRANSLATIONS, list);
406    for (TranslationUnit tu : list) {
407      String code = tu.getId();
408      String subCode = null;
409      if (code.contains("@")) {
410        subCode = code.substring(code.indexOf("@")+1);
411        code = code.substring(0, code.indexOf("@"));
412      }
413      ConceptDefinitionComponent cdSrc = CodeSystemUtilities.getCode(csSrc, tu.getId());
414      if (cdSrc == null) {
415        addOrphanTranslation(csSrc, tu);
416      } else {
417        ConceptDefinitionComponent cdDst = CodeSystemUtilities.getCode(csDst, cdSrc.getCode());
418        if (cdDst == null) {
419          cdDst = csDst.addConcept().setCode(cdSrc.getCode());
420        }
421        String tt = tu.getTgtText();
422        if (tt.startsWith("!!")) {
423          tt = tt.substring(3);
424        }
425        if (subCode == null) {
426          cdDst.setDisplay(tt);
427        } else if ("definition".equals(subCode)) {
428          cdDst.setDefinition(tt);
429        } else {
430          boolean found = false;
431          for (ConceptDefinitionDesignationComponent d : cdSrc.getDesignation()) {
432            if (d.hasUse() && subCode.equals(d.getUse().getCode())) {
433              found = true;
434              cdDst.addDesignation().setUse(d.getUse()).setLanguage(tu.getLanguage()).setValue(tt); //.setUserData(SUPPLEMENT_SOURCE, tu);       
435              break;
436            }
437          }
438          if (!found) {
439            for (Extension e : cdSrc.getExtension()) {
440              if (subCode.equals(tail(e.getUrl()))) {
441                found = true;
442                cdDst.addExtension().setUrl(e.getUrl()).setValue(
443                    e.getValue().fhirType().equals("markdown") ? new MarkdownType(tt) : new StringType(tt)); //.setUserData(SUPPLEMENT_SOURCE, tu);          
444                break;
445              }
446            }
447          }
448          if (!found) {
449            addOrphanTranslation(csSrc, tu);            
450          }
451        }
452      }      
453    }    
454  }
455
456  private String tail(String url) {
457    return url.contains("/") ? url.substring(url.lastIndexOf("/")+1) : url;
458  }
459
460  private void addOrphanTranslation(CodeSystem cs, TranslationUnit tu) {
461    List<TranslationUnit> list = (List<TranslationUnit>) cs.getUserData(ORPHAN_TRANSLATIONS_NAME);
462    if (list == null) {
463      list = new ArrayList<>();
464      cs.setUserData(ORPHAN_TRANSLATIONS_NAME, list);
465    }
466    list.add(tu);
467  }
468
469  public String nameForLang(String lang) {
470    // todo: replace with structures from loading languages properly
471    switch (lang) {
472    case "en" : return "English";
473    case "de" : return "German";
474    case "es" : return "Spanish";
475    case "nl" : return "Dutch";
476    }
477    return Utilities.capitalize(lang);
478  }
479
480  public String titleForLang(String lang) {
481    // todo: replace with structures from loading languages properly
482    switch (lang) {
483    case "en" : return "English";
484    case "de" : return "German";
485    case "es" : return "Spanish";
486    case "nl" : return "Dutch";
487    }
488    return Utilities.capitalize(lang);
489  }
490
491  public boolean handlesAsResource(Resource resource) {
492    return (resource instanceof CodeSystem && resource.hasUserData(SUPPLEMENT_SOURCE_RESOURCE)) || (resource instanceof StructureDefinition);
493  }
494
495  public boolean handlesAsElement(Element element) {
496    return true; // for now...
497  }
498
499  public List<TranslationUnit> generateTranslations(Resource res, String lang) {
500    List<TranslationUnit> list = new ArrayList<>();
501    if (res instanceof StructureDefinition) {
502      StructureDefinition sd = (StructureDefinition) res;
503      generateTranslations(list, sd, lang);
504      if (res.hasUserData(ORPHAN_TRANSLATIONS_NAME)) {
505        List<TranslationUnit> orphans = (List<TranslationUnit>) res.getUserData(ORPHAN_TRANSLATIONS_NAME);
506        for (TranslationUnit t : orphans) {
507          list.add(new TranslationUnit(lang, "!!"+t.getId(), t.getContext1(), t.getSrcText(), t.getTgtText()));
508        }
509      }
510    } else {
511      CodeSystem cs = (CodeSystem) res.getUserData(SUPPLEMENT_SOURCE_RESOURCE);
512      List<TranslationUnit> inputs = res.hasUserData(SUPPLEMENT_SOURCE_TRANSLATIONS) ? (List<TranslationUnit>) res.getUserData(SUPPLEMENT_SOURCE_TRANSLATIONS) : new ArrayList<>();
513      for (ConceptDefinitionComponent cd : cs.getConcept()) {
514        generateTranslations(list, cd, lang, inputs);
515      }
516      if (cs.hasUserData(ORPHAN_TRANSLATIONS_NAME)) {
517        List<TranslationUnit> orphans = (List<TranslationUnit>) cs.getUserData(ORPHAN_TRANSLATIONS_NAME);
518        for (TranslationUnit t : orphans) {
519          list.add(new TranslationUnit(lang, "!!"+t.getId(), t.getContext1(), t.getSrcText(), t.getTgtText()));
520        }
521      }
522    }
523    return list;
524  }
525
526  private void generateTranslations(List<TranslationUnit> list, StructureDefinition sd, String lang) {
527    addToList(list, lang, sd, "name", "name", sd.getNameElement());
528    addToList(list, lang, sd, "title", "title", sd.getTitleElement());
529    addToList(list, lang, sd, "publisher", "publisher", sd.getPublisherElement());
530    for (ContactDetail cd : sd.getContact()) {
531      addToList(list, lang, cd, "contact.name", "name", cd.getNameElement());
532    }
533    addToList(list, lang, sd, "purpose", "purpose", sd.getPurposeElement());
534    addToList(list, lang, sd, "copyright", "copyright", sd.getCopyrightElement());
535    for (ElementDefinition ed : sd.getDifferential().getElement()) {
536      addToList(list, lang, ed, ed.getId()+"/label", "label", ed.getLabelElement());
537      addToList(list, lang, ed, ed.getId()+"/short", "short", ed.getShortElement());
538      addToList(list, lang, ed, ed.getId()+"/definition", "definition", ed.getDefinitionElement());
539      addToList(list, lang, ed, ed.getId()+"/comment", "comment", ed.getCommentElement());
540      addToList(list, lang, ed, ed.getId()+"/requirements", "requirements", ed.getRequirementsElement());
541      addToList(list, lang, ed, ed.getId()+"/meaningWhenMissing", "meaningWhenMissing", ed.getMeaningWhenMissingElement());
542      addToList(list, lang, ed, ed.getId()+"/orderMeaning", "orderMeaning", ed.getOrderMeaningElement());
543      for (ElementDefinitionConstraintComponent con : ed.getConstraint()) {
544        addToList(list, lang, con, ed.getId()+"/constraint", "human", con.getHumanElement());
545      }
546      if (ed.hasBinding()) {
547        addToList(list, lang, ed.getBinding(), ed.getId()+"/b/desc", "description", ed.getBinding().getDescriptionElement());
548        for (ElementDefinitionBindingAdditionalComponent ab : ed.getBinding().getAdditional()) {
549          addToList(list, lang, ab, ed.getId()+"/ab/doco", "documentation", ab.getDocumentationElement());
550          addToList(list, lang, ab, ed.getId()+"/ab/short", "shortDoco", ab.getShortDocoElement());
551        }
552      }
553    }
554  }
555
556  private void addToList(List<TranslationUnit> list, String lang, Base ctxt, String name, String propName, DataType value) {
557    if (value != null && value.hasPrimitiveValue()) {
558      list.add(new TranslationUnit(lang, name, ctxt.getNamedProperty(propName).getDefinition(), value.primitiveValue(), value.getTranslation(lang)));
559    }
560    
561  }
562
563  private void generateTranslations(List<TranslationUnit> list, ConceptDefinitionComponent cd, String lang, List<TranslationUnit> inputs) {
564    // we generate translation units for the display, the definition, and any designations and extensions that we find
565    // the id of the designation is the use.code (there will be a use) and for the extension, the tail of the extension URL 
566    // todo: do we need to worry about name clashes? why would we, and more importantly, how would we solve that?
567
568    addTranslationUnit(list, cd.getCode(), cd.getDisplay(), lang, inputs);
569    if (cd.hasDefinition()) {
570      addTranslationUnit(list, cd.getCode()+"@definition", cd.getDefinition(), lang, inputs);        
571    }
572    for (ConceptDefinitionDesignationComponent d : cd.getDesignation()) {
573      addTranslationUnit(list, cd.getCode()+"@"+d.getUse().getCode(), d.getValue(), lang, inputs);              
574    }
575    for (Extension e : cd.getExtension()) {
576      addTranslationUnit(list, cd.getCode()+"@"+tail(e.getUrl()), e.getValue().primitiveValue(), lang, inputs);                    
577    }
578  }
579
580  private void addTranslationUnit(List<TranslationUnit> list, String id, String srcText, String lang, List<TranslationUnit> inputs) {
581    TranslationUnit existing = null;
582    for (TranslationUnit t : inputs) {
583      if (id.equals(t.getId())) {
584        existing = t;
585        break;
586      }
587    }
588    // not sure what to do with context?
589    if (existing == null) {
590      list.add(new TranslationUnit(lang, id, null, srcText, null));
591    } else if (srcText.equals(existing.getSrcText())) {
592      list.add(new TranslationUnit(lang, id, null, srcText, existing.getTgtText()));
593    } else {
594      list.add(new TranslationUnit(lang, id, null, srcText, "!!"+existing.getTgtText()).setOriginal(existing.getSrcText()));
595    }    
596  }
597  
598  private String getDefinition(ConceptDefinitionComponent cd) {
599    ConceptPropertyComponent v = CodeSystemUtilities.getProperty(cd, "translation-context");
600    if (v != null && v.hasValue()) {
601      return v.getValue().primitiveValue();
602    } else {
603      return cd.getDefinition();
604    }
605  }
606
607  public List<TranslationUnit> generateTranslations(Element e, String lang) {
608    List<TranslationUnit> list = new ArrayList<>();
609    generateTranslations(e, lang, list);
610    return list;
611  }
612
613  private void generateTranslations(Element e, String lang, List<TranslationUnit> list) {
614    if (e.getProperty().isTranslatable()) {
615      String id = pathForElement(e); // .getProperty().getDefinition().getPath();
616      String context = e.getProperty().getDefinition().getDefinition();
617      String src = e.primitiveValue();
618      String tgt = getTranslation(e, lang);
619      list.add(new TranslationUnit(lang, id, context, src, tgt));
620    }
621    if (e.hasChildren()) {
622      for (Element c : e.getChildren()) {
623        generateTranslations(c, lang, list);
624      }
625    }
626    
627  }
628
629  private String getTranslation(Element e, String lang) {
630    if (!e.hasChildren()) {
631      return null;
632    }
633    for (Element ext : e.getChildren()) {
634      if ("Extension".equals(ext.fhirType()) && "http://hl7.org/fhir/StructureDefinition/translation".equals(ext.getNamedChildValue("url"))) {
635        String l = null;
636        String v = null;
637        for (Element subExt : ext.getChildren()) {
638          if ("Extension".equals(subExt.fhirType()) && "lang".equals(subExt.getNamedChildValue("url"))) {
639            l = subExt.getNamedChildValue("value");
640          }
641          if ("Extension".equals(subExt.fhirType()) && "content".equals(subExt.getNamedChildValue("url"))) {
642            v = subExt.getNamedChildValue("value");
643          }
644        }
645        if (lang.equals(l)) {
646          return v;
647        }
648      }
649    }
650    return null;
651  }
652 
653  public boolean switchLanguage(Element e, String lang) {
654    if (e.getProperty().isTranslatable()) {
655      String cnt = getTranslation(e, lang);
656      e.removeExtension(ToolingExtensions.EXT_TRANSLATION);
657      if (cnt != null) {
658        e.setValue(cnt);
659      }
660    }
661    if (e.hasChildren()) {
662      for (Element c : e.getChildren()) {
663        if (!switchLanguage(c, lang)) {
664          return false;
665        }
666      }
667    }
668    return true;
669  }
670
671  public boolean hasTranslation(org.hl7.fhir.r5.model.Element e, String lang) {
672    return getTranslation(e, lang) != null;
673  }
674
675  public String getTranslation(org.hl7.fhir.r5.model.Element e, String lang) {
676    for (Extension ext : e.getExtensionsByUrl(ToolingExtensions.EXT_TRANSLATION)) {
677      String l = ext.getExtensionString("lang");
678      String v =  ext.getExtensionString("content");
679      if (langsMatch(l, lang) && v != null) {
680        return v;
681      }
682    }
683    return null;
684  }
685
686  public String getTranslationOrBase(PrimitiveType<?> e, String lang) {
687    for (Extension ext : e.getExtensionsByUrl(ToolingExtensions.EXT_TRANSLATION)) {
688      String l = ext.getExtensionString("lang");
689      String v =  ext.getExtensionString("content");
690      if (langsMatch(l, lang) && v != null) {
691        return v;
692      }
693    }
694    return e.primitiveValue();
695  }
696}