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