-
Notifications
You must be signed in to change notification settings - Fork 58
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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
Showing
5 changed files
with
291 additions
and
7 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
} | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"} | ||
] | ||
}] | ||
} | ||
}] | ||
}] | ||
} | ||
}] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
} | ||
|