Skip to content

Commit

Permalink
Add localized computed var and func
Browse files Browse the repository at this point in the history
These are convenience methods for FHIRString (and ValueSet.compose.include.concept) to retrieve localized strings. They search for an “http://hl7.org/fhir/StructureDefinition/translation” extension on the string and compare the desired locale to the available locales.
  • Loading branch information
p2 committed Jan 3, 2017
1 parent a7347fd commit 30c0a0f
Show file tree
Hide file tree
Showing 5 changed files with 291 additions and 7 deletions.
13 changes: 13 additions & 0 deletions Sources/Client/Element+Extensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,19 @@ import Models
#endif


public extension FHIRPrimitive {

/**
Returns an array of `Extension` elements for the given extension URL, if any.

- parameter forURI: The URI defining the extension on the receiver
*/
public final func extensions(forURI uri: String) -> [Extension]? {
return extension_fhir?.filter() { return $0.url?.absoluteString == uri }
}
}


public extension Element {

/**
Expand Down
114 changes: 114 additions & 0 deletions Sources/Client/FHIRString+Localization.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
//
// FHIRString+Localization.swift
// SwiftFHIR
//
// Created by Pascal Pfiffner on 1/3/17.
// 2017, SMART Health IT.
//

import Foundation


extension FHIRString {

/// The string, localized in the device language; uses `Locale.current` with `localized(in: Locale)`
public var localized: String {
return localized(in: Locale.current).string
}

/**
Returns the string localized in the given locale, if available, self.string otherwise.

The method will fall back to language code only if you provide a language and region code (e.g. "en-US") but a localization is only
available for the language code (e.g. "en") or a different region (e.g. "en-AU").

- parameter locale: The name of the locale for which to retrieve the localization, e.g. "fr" or "de-CH"
- returns: A String in the given locale, untranslated otherwise
*/
public func localized(in locale: String) -> String {
return localized(in: Locale(identifier: locale)).string
}

/**
Returns the string localized in the given locale, if available, self otherwise.

The method will fall back to language code only if you provide a language and region code (e.g. "en-US") but a localization is only
available for the language code (e.g. "en") or a different region (e.g. "en-AU").

- parameter locale: The locale for which to retrieve the localization
- returns: The FHIRString in the given locale, untranslated otherwise
*/
public func localized(in locale: Locale) -> FHIRString {
if let translations = extensions(forURI: "http://hl7.org/fhir/StructureDefinition/translation") {

// check for exact locale matches
for translation in translations {
if let langCode = translation.extensions(forURI: "lang")?.first?.valueCode?.string, Locale(identifier: langCode) == locale {
if let localized = translation.extensions(forURI: "content")?.first?.valueString {
return localized
}
}
}

// no exact match, only test for languageCode this time
for translation in translations {
if let langCode = translation.extensions(forURI: "lang")?.first?.valueCode?.string, Locale(identifier: langCode).languageCode == locale.languageCode {
if let localized = translation.extensions(forURI: "content")?.first?.valueString {
return localized
}
}
}
}
return self
}
}


extension ValueSetComposeIncludeConcept {

/// The `display` string, localized in the device language; uses `Locale.current` with `localized(in: Locale)`
public var display_localized: String? {
return display_localized(in: Locale.current)?.string
}

/**
Returns the `display.string` value, localized in the given locale if available, `self.display.string` otherwise.

The method will fall back to language code only if you provide a language and region code (e.g. "en-US") but a localization is only
available for the language code (e.g. "en") or a different region (e.g. "en-AU").

- parameter locale: The locale for which to retrieve the localization
- returns: A String in the given locale, untranslated otherwise
*/
public func display_localized(in locale: String) -> String? {
return display_localized(in: Locale(identifier: locale))?.string
}

/**
Returns the `display` value, localized in the given locale if available, `self.display` otherwise.

The method will fall back to language code only if you provide a language and region code (e.g. "en-US") but a localization is only
available for the language code (e.g. "en") or a different region (e.g. "en-AU").

- parameter locale: The locale for which to retrieve the localization
- returns: The FHIRString in the given locale, untranslated otherwise
*/
public func display_localized(in locale: Locale) -> FHIRString? {
if let translations = designation {
for translation in translations {
if let lang = translation.language?.string, Locale(identifier: lang) == locale, let localized = translation.value {
return localized
}
}

// no exact match; test for languageCode only
for translation in translations {
if let lang = translation.language?.string, Locale(identifier: lang).languageCode == locale.languageCode, let localized = translation.value {
return localized
}
}
}
return display?.localized(in: locale)
}
}

26 changes: 19 additions & 7 deletions SwiftFHIR.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -567,6 +567,10 @@
EEB430311DF973010040415A /* FHIRDecimal.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEB4302E1DF973010040415A /* FHIRDecimal.swift */; };
EEB430321DF973010040415A /* FHIRInteger.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEB4302F1DF973010040415A /* FHIRInteger.swift */; };
EEB430331DF973010040415A /* FHIRInteger.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEB4302F1DF973010040415A /* FHIRInteger.swift */; };
EEDB87361E1BE44400E53FD4 /* FHIRString+Localization.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEDB87351E1BE44400E53FD4 /* FHIRString+Localization.swift */; };
EEDB87371E1BE44400E53FD4 /* FHIRString+Localization.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEDB87351E1BE44400E53FD4 /* FHIRString+Localization.swift */; };
EEDB87391E1BE6CE00E53FD4 /* LocalizationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEDB87381E1BE6CE00E53FD4 /* LocalizationTests.swift */; };
EEDB873A1E1BE6CE00E53FD4 /* LocalizationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEDB87381E1BE6CE00E53FD4 /* LocalizationTests.swift */; };
EEE5DF371A5D862B002AFF53 /* FHIRSearchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEE5DF361A5D862B002AFF53 /* FHIRSearchTests.swift */; };
EEE5DF381A5D862B002AFF53 /* FHIRSearchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEE5DF361A5D862B002AFF53 /* FHIRSearchTests.swift */; };
EEED291B1DF94619007ADD02 /* FHIRBool.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEED291A1DF94619007ADD02 /* FHIRBool.swift */; };
Expand Down Expand Up @@ -911,6 +915,8 @@
EEA97F991DCA1C6F00C3F016 /* RequestGroup.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RequestGroup.swift; sourceTree = "<group>"; };
EEB4302E1DF973010040415A /* FHIRDecimal.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FHIRDecimal.swift; sourceTree = "<group>"; };
EEB4302F1DF973010040415A /* FHIRInteger.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FHIRInteger.swift; sourceTree = "<group>"; };
EEDB87351E1BE44400E53FD4 /* FHIRString+Localization.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "FHIRString+Localization.swift"; sourceTree = "<group>"; };
EEDB87381E1BE6CE00E53FD4 /* LocalizationTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LocalizationTests.swift; sourceTree = "<group>"; };
EEE5DF361A5D862B002AFF53 /* FHIRSearchTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FHIRSearchTests.swift; sourceTree = "<group>"; };
EEED291A1DF94619007ADD02 /* FHIRBool.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FHIRBool.swift; sourceTree = "<group>"; };
EEEE30E01BB1FD94008866E2 /* Account.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Account.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -977,7 +983,7 @@
children = (
EEF20E151A5D4FFE009A6EDA /* README.md */,
EE1310DC1C1B7E3700D9DAE7 /* Package.swift */,
EE684C2019A789BA00B5A2C0 /* SwiftFHIR */,
EE684C2019A789BA00B5A2C0 /* Sources */,
EE684C2A19A789BA00B5A2C0 /* Tests */,
EEEE31291BB2829A008866E2 /* Metadata */,
EE684C1F19A789BA00B5A2C0 /* Products */,
Expand All @@ -995,13 +1001,13 @@
name = Products;
sourceTree = "<group>";
};
EE684C2019A789BA00B5A2C0 /* SwiftFHIR */ = {
EE684C2019A789BA00B5A2C0 /* Sources */ = {
isa = PBXGroup;
children = (
EE9B31F61ACAD94800980AA9 /* Client */,
EE684C4219A78AEE00B5A2C0 /* Models */,
);
name = SwiftFHIR;
name = Sources;
sourceTree = "<group>";
};
EE684C2A19A789BA00B5A2C0 /* Tests */ = {
Expand Down Expand Up @@ -1203,22 +1209,24 @@
EE01F9721C58FC51003AEA7E /* ResourceTests.swift */,
EE39069E1CD3E4F6008FECEA /* RequestTests.swift */,
EEA97F921DCA19C300C3F016 /* ValidationTests.swift */,
EEDB87381E1BE6CE00E53FD4 /* LocalizationTests.swift */,
);
path = ClientTests;
sourceTree = "<group>";
};
EE9B31F61ACAD94800980AA9 /* Client */ = {
isa = PBXGroup;
children = (
EE1F49CE1C0D06360095BF0F /* FHIROpenServer.swift */,
EE1F49D11C0D11060095BF0F /* FHIRBaseServer.swift */,
EE1310D61C1B608E00D9DAE7 /* FHIRServerRequestHandler.swift */,
EE1310D91C1B681500D9DAE7 /* FHIRServerDataResponse.swift */,
EE8901221B7E07D700F1EDBF /* Element+Extensions.swift */,
EEDB87351E1BE44400E53FD4 /* FHIRString+Localization.swift */,
EE9B31F31ACAD93400980AA9 /* Resource+REST.swift */,
EE1310CE1C1B5EE500D9DAE7 /* Resource+Instantiation.swift */,
EE9B31FF1ACADE8000980AA9 /* Reference+Resolving.swift */,
EE01F96F1C58F6AB003AEA7E /* DomainResource+Containment.swift */,
EE1F49CE1C0D06360095BF0F /* FHIROpenServer.swift */,
EE1F49D11C0D11060095BF0F /* FHIRBaseServer.swift */,
EE1310D61C1B608E00D9DAE7 /* FHIRServerRequestHandler.swift */,
EE1310D91C1B681500D9DAE7 /* FHIRServerDataResponse.swift */,
EE02F6ED1ACF257000179969 /* FHIRSearch.swift */,
EE9EE2601ACB43D1004DBCBB /* FHIROperation.swift */,
EE9ABA2A1D803D8400BA8B54 /* Patient+SMART.swift */,
Expand Down Expand Up @@ -1626,6 +1634,7 @@
EE109CD11DF6AE3800DB1774 /* MessageDefinition.swift in Sources */,
EEEE30EA1BB1FD94008866E2 /* Account.swift in Sources */,
EE02F6511ACF252000179969 /* DocumentReference.swift in Sources */,
EEDB87361E1BE44400E53FD4 /* FHIRString+Localization.swift in Sources */,
EE02F6851ACF252000179969 /* Medication.swift in Sources */,
EE01F9701C58F6AB003AEA7E /* DomainResource+Containment.swift in Sources */,
EE02F6711ACF252000179969 /* HealthcareService.swift in Sources */,
Expand Down Expand Up @@ -1770,6 +1779,7 @@
EE02F7C21ACF259B00179969 /* OperationOutcomeTests.swift in Sources */,
EE31DC591D64AC3600B04BEA /* ImagingManifestTests.swift in Sources */,
EE02F74E1ACF259B00179969 /* AppointmentTests.swift in Sources */,
EEDB87391E1BE6CE00E53FD4 /* LocalizationTests.swift in Sources */,
EE02F7661ACF259B00179969 /* CompositionTests.swift in Sources */,
EE02F7DA1ACF259B00179969 /* ProcessRequestTests.swift in Sources */,
EE109CDA1DF6AE4F00DB1774 /* MessageDefinitionTests.swift in Sources */,
Expand Down Expand Up @@ -1937,6 +1947,7 @@
EE02F6521ACF252000179969 /* DocumentReference.swift in Sources */,
EE1310C11C1B542100D9DAE7 /* Element.swift in Sources */,
EE01F9711C58F6AB003AEA7E /* DomainResource+Containment.swift in Sources */,
EEDB87371E1BE44400E53FD4 /* FHIRString+Localization.swift in Sources */,
EE02F6861ACF252000179969 /* Medication.swift in Sources */,
EE65DB7B1CB39FFD00E25C72 /* ParameterDefinition.swift in Sources */,
EE109CD61DF6AE3800DB1774 /* ResearchSubject.swift in Sources */,
Expand Down Expand Up @@ -2081,6 +2092,7 @@
EE02F7DB1ACF259B00179969 /* ProcessRequestTests.swift in Sources */,
EE02F7E71ACF259B00179969 /* RelatedPersonTests.swift in Sources */,
EE31DC5E1D64AC3600B04BEA /* PlanDefinitionTests.swift in Sources */,
EEDB873A1E1BE6CE00E53FD4 /* LocalizationTests.swift in Sources */,
EEEE31171BB1FE32008866E2 /* SupplyRequestTests.swift in Sources */,
EE02F7D71ACF259B00179969 /* ProcedureRequestTests.swift in Sources */,
EE02F7651ACF259B00179969 /* CommunicationTests.swift in Sources */,
Expand Down
62 changes: 62 additions & 0 deletions TestResources/Localization.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
{
"resourceType": "Questionnaire",
"id": "Swift-FHIR.testcase.localization",
"status": "draft",
"item": [{
"linkId": "withdrawalReason",
"type": "choice",
"text": "I wish to withdraw because…",
"_text": {
"extension": [{
"url": "http://hl7.org/fhir/StructureDefinition/translation", "extension": [
{"url": "lang", "valueCode": "de-CH"}, {"url": "content", "valueString": "Ich möchte aus der Studie austreten weil…"}
]},{
"url": "http://hl7.org/fhir/StructureDefinition/translation", "extension": [
{"url": "lang", "valueCode": "de-DE"}, {"url": "content", "valueString": "Ich möchte gerne aus der Studie austreten weil ich…"}
]},{
"url": "http://hl7.org/fhir/StructureDefinition/translation", "extension": [
{"url": "lang", "valueCode": "fr"}, {"url": "content", "valueString": "Je veux retirer de l'étude parce que …"}
]}
]
},
"options": {
"reference": "#withdrawalReasons"
}
}],
"contained": [{
"id": "withdrawalReasons",
"resourceType": "ValueSet",
"status": "active",
"compose": {
"include": [{
"concept": [{
"code": "time-commitment",
"display": "Surveys take too much time",
"designation": [{
"language": "de-CH", "value": "Die Fragebögen sind zu lang"
},{
"language": "de-DE", "value": "Diese Fragebögen sind viel zu lange"
},{
"language": "fr", "value": "Les enquêtes prennent trop de temps"
}]
},{
"code": "too-difficult",
"display": "App is too difficult to use",
"_display": {
"extension": [{
"url": "http://hl7.org/fhir/StructureDefinition/translation", "extension": [
{"url": "lang", "valueCode": "de-CH"}, {"url": "content", "valueString": "Die Bedienung der App ist umständlich"}
]},{
"url": "http://hl7.org/fhir/StructureDefinition/translation", "extension": [
{"url": "lang", "valueCode": "de-DE"}, {"url": "content", "valueString": "Die Bedienung dieser App ist zu umständlich"}
]},{
"url": "http://hl7.org/fhir/StructureDefinition/translation", "extension": [
{"url": "lang", "valueCode": "fr"}, {"url": "content", "valueString": "Le fonctionnement de l'application est maladroit"}
]
}]
}
}]
}]
}
}]
}
83 changes: 83 additions & 0 deletions Tests/ClientTests/LocalizationTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
//
// LocalizationTests.swift
// SwiftFHIR
//
// Created by Pascal Pfiffner on 1/3/17.
// 2017, SMART Health IT.
//

import XCTest
import SwiftFHIR


/**
Test the shortcuts for the general translation extensions and for ValueSet concepts.
*/
class LocalizationTests: XCTestCase {

var item: QuestionnaireItem?

var concept1: ValueSetComposeIncludeConcept?

var concept2: ValueSetComposeIncludeConcept?

override func setUp() {
super.setUp()
do {
let json = try Bundle(for: type(of: self)).fhir_json(from: "Localization", subdirectory: "TestResources")
let questionnaire = try SwiftFHIR.Questionnaire(json: json)
item = questionnaire.item?.first
let valueSet = item?.options?.resolved(ValueSet.self)
concept1 = valueSet?.compose?.include?.first?.concept?.first
concept2 = valueSet?.compose?.include?.first?.concept?.last
}
catch let error {
XCTAssertTrue(false, "Failed to read bundled resource “Localization.json”: \(error)")
}
}

func testManual() {
XCTAssertNotNil(item)
XCTAssertEqual("I wish to withdraw because…", item?.text)
XCTAssertEqual("Ich möchte aus der Studie austreten weil…", item?.text?.localized(in: "de-CH"))
XCTAssertEqual("Ich möchte aus der Studie austreten weil…", item?.text?.localized(in: "de")) // will pick the first "de-XX" extension it finds
XCTAssertEqual("Ich möchte aus der Studie austreten weil…", item?.text?.localized(in: "de-AT")) // ditto
XCTAssertEqual("Ich möchte gerne aus der Studie austreten weil ich…", item?.text?.localized(in: "de-DE"))
XCTAssertEqual("Je veux retirer de l'étude parce que …", item?.text?.localized(in: "fr"))
XCTAssertEqual("Je veux retirer de l'étude parce que …", item?.text?.localized(in: "fr-CH"))
XCTAssertEqual("I wish to withdraw because…", item?.text?.localized(in: "es"), "Translation unavailable, must fall back to `text`")
XCTAssertEqual("I wish to withdraw because…", item?.text?.localized(in: "xy"), "Invalid locale, must fall back to `text`")
}

func testManualValueSet() {
XCTAssertNotNil(concept1)
XCTAssertEqual("Surveys take too much time", concept1?.display)
XCTAssertEqual("Die Fragebögen sind zu lang", concept1?.display_localized(in: "de-CH"))
XCTAssertEqual("Die Fragebögen sind zu lang", concept1?.display_localized(in: "de"))
XCTAssertEqual("Die Fragebögen sind zu lang", concept1?.display_localized(in: "de-AT"))
XCTAssertEqual("Diese Fragebögen sind viel zu lange", concept1?.display_localized(in: "de-DE"))
XCTAssertEqual("Les enquêtes prennent trop de temps", concept1?.display_localized(in: "fr"))
XCTAssertEqual("Les enquêtes prennent trop de temps", concept1?.display_localized(in: "fr-CH"))
XCTAssertEqual("Surveys take too much time", concept1?.display_localized(in: "es"), "Translation unavailable, must fall back to `display`")
XCTAssertEqual("Surveys take too much time", concept1?.display_localized(in: "xy"), "Invalid locale, must fall back to `display`")

XCTAssertNotNil(concept2)
XCTAssertEqual("App is too difficult to use", concept2?.display)
XCTAssertEqual("Die Bedienung der App ist umständlich", concept2?.display_localized(in: "de-CH"))
XCTAssertEqual("Die Bedienung der App ist umständlich", concept2?.display_localized(in: "de"))
XCTAssertEqual("Die Bedienung der App ist umständlich", concept2?.display_localized(in: "de-AT"))
XCTAssertEqual("Die Bedienung dieser App ist zu umständlich", concept2?.display_localized(in: "de-DE"))
XCTAssertEqual("Le fonctionnement de l'application est maladroit", concept2?.display_localized(in: "fr"))
XCTAssertEqual("Le fonctionnement de l'application est maladroit", concept2?.display_localized(in: "fr-CH"))
XCTAssertEqual("App is too difficult to use", concept2?.display_localized(in: "es"), "Translation unavailable, must fall back to `display`")
XCTAssertEqual("App is too difficult to use", concept2?.display_localized(in: "xy"), "Invalid locale, must fall back to `display`")
}

func testAutomatic() {
XCTAssertNotNil(item)
XCTAssertEqual("I wish to withdraw because…", item?.text)
// TODO: mock `Locale` to see if the following works correctly
// XCTAssertEqual("I wish to withdraw because…", item?.text?.localized)
}
}

0 comments on commit 30c0a0f

Please sign in to comment.