From e84a64903861633e828671d56b065904213599a9 Mon Sep 17 00:00:00 2001 From: Tomasz Szulc Date: Sat, 1 Aug 2015 14:36:22 +0200 Subject: [PATCH] Document the code --- Swifternalization/Expression.swift | 66 +++++--- Swifternalization/ExpressionMatcher.swift | 4 +- Swifternalization/LengthVariation.swift | 11 +- .../LoadedTranslationsProcessor.swift | 10 +- Swifternalization/Regex.swift | 5 - Swifternalization/Swifternalization.swift | 146 +++++++++++++++--- Swifternalization/Translation.swift | 33 +++- .../SharedBaseExpressionTests.swift | 8 +- .../SharedPolishExpressionTests.swift | 4 +- 9 files changed, 215 insertions(+), 72 deletions(-) diff --git a/Swifternalization/Expression.swift b/Swifternalization/Expression.swift index a82af1d..66dc65c 100644 --- a/Swifternalization/Expression.swift +++ b/Swifternalization/Expression.swift @@ -8,44 +8,62 @@ import Foundation -/// String that contains expression pattern, e.g. `ie:x<5`, `exp:^1$`. +/** +String that contains expression pattern, e.g. `ie:x<5`, `exp:^1$`. +*/ typealias ExpressionPattern = String /** -Represents simple epxressions. +This class contains pattern of expression and localized value as well as +length variations if any are associated. During instance initialization pattern +is analyzed and correct expression matcher is created. If no matcher matches +the expression pattern then when validating there is only check if passed value +is the same like pattern (equality). If there is matcher then its internal logic +validates passed value. */ struct Expression { - /// Pattern of expression. - let pattern: String + /** + Pattern of an expression. + */ + let pattern: ExpressionPattern - /// A localized value. If `lengthVariations` array is empty or you want to - /// get full localized value use this property. - let localizedValue: String + /** + A localized value. If length vartiations array is empty or you want to + get full localized value use this property. + */ + let value: String - /// Array of length variations + /** + Array of length variations. + */ let lengthVariations: [LengthVariation] - /// Expression matcher that is used in validation + /** + Expression matcher that is used in validation. + */ private var expressionMatcher: ExpressionMatcher? = nil - // Returns expression object - init(pattern: String, localizedValue: String, lengthVariations: [LengthVariation] = [LengthVariation]()) { + /** + Returns expression object. + */ + init(pattern: String, value: String, lengthVariations: [LengthVariation] = [LengthVariation]()) { self.pattern = pattern - self.localizedValue = localizedValue + self.value = value self.lengthVariations = lengthVariations - // Create expression matcher + /* + Create expression matcher if pattern matches some expression type. + If not matching any expression type then the pattern equality test + will be perfomed when during validation. + */ if let type = getExpressionType(pattern) { - switch (type as ExpressionPatternType) { - case .Inequality: - expressionMatcher = InequalityExpressionParser(pattern).parse() - - case .InequalityExtended: - expressionMatcher = InequalityExtendedExpressionParser(pattern).parse() - - case .Regex: - expressionMatcher = RegexExpressionParser(pattern).parse() - } + expressionMatcher = { + switch (type as ExpressionPatternType) { + case .Inequality: return InequalityExpressionParser(pattern).parse() + case .InequalityExtended: return InequalityExtendedExpressionParser(pattern).parse() + case .Regex: return RegexExpressionParser(pattern).parse() + } + }() } } @@ -64,7 +82,7 @@ struct Expression { } /** - Method used to get `ExpressionPatternType` of passed `ExpressionPattern`. + Method used to get `ExpressionPatternType` of passed expression pattern. :param: pattern expression pattern that will be checked. :returns: `ExpressionPatternType` if pattern is supported, otherwise nil. diff --git a/Swifternalization/ExpressionMatcher.swift b/Swifternalization/ExpressionMatcher.swift index d4e9e4e..eab29bb 100644 --- a/Swifternalization/ExpressionMatcher.swift +++ b/Swifternalization/ExpressionMatcher.swift @@ -7,8 +7,8 @@ // /** - Protocol that is the base protocol to conform for expression matchers like - `InequalityExpressionMatcher` or `RegexExpressionMatcher`. +Protocol that is the base protocol to conform for expression matchers like +`InequalityExpressionMatcher` or `RegexExpressionMatcher`. */ protocol ExpressionMatcher { /** diff --git a/Swifternalization/LengthVariation.swift b/Swifternalization/LengthVariation.swift index 9fa797e..b154d02 100644 --- a/Swifternalization/LengthVariation.swift +++ b/Swifternalization/LengthVariation.swift @@ -1,12 +1,17 @@ import Foundation /** -Length variation representation. +Length variation representation. It contains a width property which specifies +up to which width of a screen the text in `value` property should be presented. */ struct LengthVariation { - /// width of a screen. + /** + Max width of a screen on which the `value` should be presented. + */ let width: Int - /// localized string. + /** + String with localized content in some language. + */ let value: String } diff --git a/Swifternalization/LoadedTranslationsProcessor.swift b/Swifternalization/LoadedTranslationsProcessor.swift index bf423cc..f1e78ed 100644 --- a/Swifternalization/LoadedTranslationsProcessor.swift +++ b/Swifternalization/LoadedTranslationsProcessor.swift @@ -49,7 +49,7 @@ class LoadedTranslationsProcessor { case .Simple: // Simple translation with key and value. let value = $0.content[$0.key] as! String - return Translation(key: $0.key, expressions: [Expression(pattern: $0.key, localizedValue: value)]) + return Translation(key: $0.key, expressions: [Expression(pattern: $0.key, value: value)]) case .WithExpressions: // Translation that contains expression. @@ -60,7 +60,7 @@ class LoadedTranslationsProcessor { var expressions = [Expression]() for (key, value) in $0.content as! Dictionary { let pattern = sharedExpressions.filter({$0.identifier == key}).first?.pattern ?? key - expressions.append(Expression(pattern: pattern, localizedValue: value)) + expressions.append(Expression(pattern: pattern, value: value)) } return Translation(key: $0.key, expressions: expressions) @@ -70,7 +70,7 @@ class LoadedTranslationsProcessor { for (key, value) in $0.content as! Dictionary { lengthVariations.append(LengthVariation(width: self.parseNumberFromLengthVariation(key), value: value)) } - return Translation(key: $0.key, expressions: [Expression(pattern: $0.key, localizedValue: lengthVariations.last!.value, lengthVariations: lengthVariations)]) + return Translation(key: $0.key, expressions: [Expression(pattern: $0.key, value: lengthVariations.last!.value, lengthVariations: lengthVariations)]) case .WithExpressionsAndLengthVariations: // The most advanced translation type. It contains expressions @@ -87,9 +87,9 @@ class LoadedTranslationsProcessor { for (lvKey, lvValue) in value as! Dictionary { lengthVariations.append(LengthVariation(width: self.parseNumberFromLengthVariation(lvKey), value: lvValue)) } - expressions.append(Expression(pattern: pattern, localizedValue: lengthVariations.last!.value, lengthVariations: lengthVariations)) + expressions.append(Expression(pattern: pattern, value: lengthVariations.last!.value, lengthVariations: lengthVariations)) } else if value is String { - expressions.append(Expression(pattern:pattern, localizedValue: value as! String)) + expressions.append(Expression(pattern:pattern, value: value as! String)) } } return Translation(key: $0.key, expressions: expressions) diff --git a/Swifternalization/Regex.swift b/Swifternalization/Regex.swift index aa27f58..a9d0def 100644 --- a/Swifternalization/Regex.swift +++ b/Swifternalization/Regex.swift @@ -18,7 +18,6 @@ class Regex { :param: str A string that will be matched. :param: pattern A regex pattern. - :returns: `String` that matches pattern or nil. */ class func matchInString(str: String, pattern: String, capturingGroupIdx: Int?) -> String? { @@ -44,7 +43,6 @@ class Regex { :param: str A string that will be matched. :param: pattern A regexp pattern. - :returns: `String` that matches pattern or nil. */ class func firstMatchInString(str: String, pattern: String) -> String? { @@ -59,7 +57,6 @@ class Regex { :param: str A string that will be matched. :param: pattern A regexp pattern. - :returns: Array of `Strings`s. If nothing found empty array is returned. */ class func matchesInString(str: String, pattern: String) -> [String] { @@ -77,7 +74,6 @@ class Regex { Returns new `NSRegularExpression` object. :param: pattern A regexp pattern. - :returns: `NSRegularExpression` object or nil if it cannot be created. */ private class func regexp(pattern: String) -> NSRegularExpression? { @@ -94,7 +90,6 @@ class Regex { :param: str A string that is source of substraction. :param: range A range that tells which part of `str` will be substracted. - :returns: A string contained in `range`. */ private class func substring(str: String, range: NSRange) -> String { diff --git a/Swifternalization/Swifternalization.swift b/Swifternalization/Swifternalization.swift index 53886a2..99c097c 100644 --- a/Swifternalization/Swifternalization.swift +++ b/Swifternalization/Swifternalization.swift @@ -8,73 +8,169 @@ import Foundation -/// Handy typealias that can be used instead of long `Swifternalization` +/// Handy typealias that can be used instead of longer `Swifternalization` public typealias I18n = Swifternalization +/** +This is the main class of Swifternalization library. It exposes methods +that can be used to get localized strings. + +It uses expressions.json and base.json, en.json, pl.json and so on to work. +expressions.json file contains shared expressions that are used by other +files with translations. + +Internal classes of the Swifternalization contains logic that is responsible +for detecting which value should be used for the given key and value. + +Before you can get any localized value you have to configure the Swifternalization +first. Call `configure:` method first and then you can use other methods. +*/ public class Swifternalization { - static let sharedInstance = Swifternalization() + /** + Shared instance of Swifternalization used internally. + */ + private static let sharedInstance = Swifternalization() + /** + Array of translations that contain expressions and localized values. + */ private var translations = [Translation]() // MARK: Public Methods + /** + Call the method to configure Swifternalization. + + :param: bundle A bundle when expressions.json and other files are located. + */ public class func configure(bundle: NSBundle = NSBundle.mainBundle()) { sharedInstance.load(bundle: bundle) } + /** + Get localized value for a key. + + :param: key A key. + :param: fittingWidth A max width that value should fit to. If there is no + value specified the full-length localized string is returned. If a + passed fitting width is greater than highest available then a value for + highest available width is returned. + :param: defaultValue A default value that is returned when there is no + localized value for passed `key`. + :param: comment A comment about the key and localized value. Just for + developer use for describing key-value pair. + :returns: localized string if found, otherwise `defaultValue` is returned if + specified or `key` if `defaultValue` is not specified. + */ + public class func localizedString(key: String, fittingWidth: Int? = nil, defaultValue: String? = nil, comment: String? = nil) -> String { + return localizedString(key, stringValue: key, fittingWidth: fittingWidth, defaultValue: defaultValue, comment: comment) + } + + + /** + Get localized value for a key and string value. + + :param: key A key. + :param: stringValue A value that is matched by expressions. + :param: fittingWidth A max width that value should fit to. If there is no + value specified the full-length localized string is returned. If a + passed fitting width is greater than highest available then a value for + highest available width is returned. + :param: defaultValue A default value that is returned when there is no + localized value for passed `key`. + :param: comment A comment about the key and localized value. Just for + developer use for describing key-value pair. + :returns: localized string if found, otherwise `defaultValue` is returned if + specified or `key` if `defaultValue` is not specified. + */ public class func localizedString(key: String, stringValue: String, fittingWidth: Int? = nil, defaultValue: String? = nil, comment: String? = nil) -> String { + /** + Filter translations and get only these that match passed `key`. + In ideal case when all is correctly filled by a developer it should be + at most only one key so we're getting just first found key. + */ let filteredTranslations = sharedInstance.translations.filter({$0.key == key}) if let translation = filteredTranslations.first { - if let matchingExpression = translation.validate(stringValue ?? key, length: fittingWidth) { - if fittingWidth == nil { - return matchingExpression.localizedValue - } else if matchingExpression.lengthVariations.count == 0 { - return matchingExpression.localizedValue - } else if matchingExpression.lengthVariations.count > 0 { - let sortedVariations = matchingExpression.lengthVariations.sorted({$0.width < $1.width}) - - var selectedValue = matchingExpression.localizedValue - for variation in sortedVariations { - if variation.width <= fittingWidth { - selectedValue = variation.value - } else { - break - } - } - - return selectedValue - } + /** + If there is translation for the `key` it should validate passed + `stringValue`and `fittingWidth`. Translation returns localized + string (that matches `fittingWidth` if specified or nil. + If `fittingWidth` is not specified then full length localized string + is returned if translation matches the validated `stringValue`. + */ + if let localizedValue = translation.validate(stringValue, length: fittingWidth) { + return localizedValue } } + /** + If there is not translation that validated `stringValue` successfully + then return `defaultValue` if not nil or the `key`. + */ return (defaultValue != nil) ? defaultValue! : key } + /** + Get localized value for a key and string value. + + :param: key A key. + :param: intValue A value that is matched by expressions. + :param: fittingWidth A max width that value should fit to. If there is no + value specified the full-length localized string is returned. If a + passed fitting width is greater than highest available then a value for + highest available width is returned. + :param: defaultValue A default value that is returned when there is no + localized value for passed `key`. + :param: comment A comment about the key and localized value. Just for + developer use for describing key-value pair. + :returns: localized string if found, otherwise `defaultValue` is returned if + specified or `key` if `defaultValue` is not specified. + */ public class func localizedString(key: String, intValue: Int, fittingWidth: Int? = nil, defaultValue: String? = nil, comment: String? = nil) -> String { return localizedString(key, stringValue: "\(intValue)", fittingWidth: fittingWidth, defaultValue: defaultValue, comment: comment) } - public class func localizedString(key: String, fittingWidth: Int? = nil, defaultValue: String? = nil, comment: String? = nil) -> String { - return localizedString(key, stringValue: key, fittingWidth: fittingWidth, defaultValue: defaultValue, comment: comment) - } // MARK: Private Methods + /** + Loads expressions and translations from expression.json and translation + json files. + + :param: bundle A bundle when files are located. + */ private func load(bundle: NSBundle = NSBundle.mainBundle()) { + // Set base and prefered languages. let base = "base" let language = getPreferredLanguage(bundle) + /* + Load base and prefered language expressions from expressions.json, + convert them into SharedExpression objects and process them and return + array only of unique expressions. `SharedExpressionsProcessor` do its + own things inside like check if expression is unique or overriding base + expressions by prefered language ones if there is such expression. + */ let baseExpressions = SharedExpressionsLoader.loadExpressions(JSONFileLoader.loadExpressions(base, bundle: bundle)) let languageExpressions = SharedExpressionsLoader.loadExpressions(JSONFileLoader.loadExpressions(language, bundle: bundle)) let expressions = SharedExpressionsProcessor.processSharedExpression(language, preferedLanguageExpressions: languageExpressions, baseLanguageExpressions: baseExpressions) + /* + Load base and prefered language translations from proper language files + specified by `language` constant. Convert them into arrays of + `LoadedTranslation`s and then process and convert into `Translation` + objects using `LoadedTranslationsProcessor`. + */ let baseTranslations = TranslationsLoader.loadTranslations(JSONFileLoader.loadTranslations(base, bundle: bundle)) let languageTranslations = TranslationsLoader.loadTranslations(JSONFileLoader.loadTranslations(language, bundle: bundle)) + // Store processed translations in `translations` variable for future use. translations = LoadedTranslationsProcessor.processTranslations(baseTranslations, preferedLanguageTranslations: languageTranslations, sharedExpressions: expressions) } - /// Gets preferred language of user's device + /** + Gets preferred language of user's device + */ private func getPreferredLanguage(bundle: NSBundle) -> CountryCode { // Get preferred language, the one which is set on user's device return bundle.preferredLocalizations.first as! CountryCode diff --git a/Swifternalization/Translation.swift b/Swifternalization/Translation.swift index 7420e6a..89840f0 100644 --- a/Swifternalization/Translation.swift +++ b/Swifternalization/Translation.swift @@ -18,10 +18,39 @@ struct Translation { /// Expressions that are related to a translation. let expressions: [Expression] - func validate(text: String, length: Int?) -> Expression? { + /** + Validates passed `text` and uses `fittingWidth` for getting proper + localized string. + + :param: text A text that is matched. + :param: fittingWidth A max width of a screen that text should match. + :returns: A localized string if any expression validates the `text`, + otherwise nil. + */ + func validate(text: String, fittingWidth: Int?) -> String? { + // Find first expression that validates the `text`. for expression in expressions { if expression.validate(text) { - return expression + /* + Get the localized value of expression if it match the `text`. + Check if there is `fittingValue` defined as method argument + and if there are some variations in the expression get proper + variant for passed length. + */ + var localizedValue = expression.value + if fittingWidth != nil && expression.lengthVariations.count > 0 { + /* + Sort variations in ascending order. + If variation width is shorter or equal `fittingWidth` + take associated value. + */ + for variation in expression.lengthVariations.sorted({$0.width < $1.width}) { + if variation.width <= fittingWidth! { + localizedValue = variation.value + } + } + } + return localizedValue } } return nil diff --git a/SwifternalizationTests/SharedBaseExpressionTests.swift b/SwifternalizationTests/SharedBaseExpressionTests.swift index 31d1888..ab11f61 100644 --- a/SwifternalizationTests/SharedBaseExpressionTests.swift +++ b/SwifternalizationTests/SharedBaseExpressionTests.swift @@ -14,7 +14,7 @@ class SharedBaseExpressionTests: XCTestCase { func testOne() { let sharedExp = SharedBaseExpression.allExpressions().filter({$0.identifier == "one"}).first! - let expression = Expression(pattern: sharedExp.pattern, localizedValue: "") + let expression = Expression(pattern: sharedExp.pattern, value: "") XCTAssertTrue(expression.validate("1"), "Should match 1") XCTAssertFalse(expression.validate("2"), "Should not match 2") @@ -22,7 +22,7 @@ class SharedBaseExpressionTests: XCTestCase { func testMoreThanOne() { let sharedExp = SharedBaseExpression.allExpressions().filter({$0.identifier == ">one"}).first! - let expression = Expression(pattern: sharedExp.pattern, localizedValue: "") + let expression = Expression(pattern: sharedExp.pattern, value: "") XCTAssertTrue(expression.validate("2"), "Should match 2") XCTAssertTrue(expression.validate("3"), "Should match 3") @@ -31,7 +31,7 @@ class SharedBaseExpressionTests: XCTestCase { func testTwo() { let sharedExp = SharedBaseExpression.allExpressions().filter({$0.identifier == "two"}).first! - let expression = Expression(pattern: sharedExp.pattern, localizedValue: "") + let expression = Expression(pattern: sharedExp.pattern, value: "") XCTAssertTrue(expression.validate("2"), "Should match 2") XCTAssertFalse(expression.validate("1"), "Should not match 1") @@ -39,7 +39,7 @@ class SharedBaseExpressionTests: XCTestCase { func testOther() { let sharedExp = SharedBaseExpression.allExpressions().filter({$0.identifier == "other"}).first! - let expression = Expression(pattern: sharedExp.pattern, localizedValue: "") + let expression = Expression(pattern: sharedExp.pattern, value: "") XCTAssertTrue(expression.validate("0"), "Should match 0") XCTAssertTrue(expression.validate("2"), "Should match 2") diff --git a/SwifternalizationTests/SharedPolishExpressionTests.swift b/SwifternalizationTests/SharedPolishExpressionTests.swift index d2ed5fb..7cd8bcf 100644 --- a/SwifternalizationTests/SharedPolishExpressionTests.swift +++ b/SwifternalizationTests/SharedPolishExpressionTests.swift @@ -13,7 +13,7 @@ class SharedPolishExpressionTests: XCTestCase { func testFew() { let sharedExp = SharedPolishExpression.allExpressions().filter({$0.identifier == "few"}).first! - let expression = Expression(pattern: sharedExp.pattern, localizedValue: "") + let expression = Expression(pattern: sharedExp.pattern, value: "") XCTAssertTrue(expression.validate("2"), "Should match 2") XCTAssertTrue(expression.validate("24"), "Should match 24") @@ -28,7 +28,7 @@ class SharedPolishExpressionTests: XCTestCase { func testMany() { let sharedExp = SharedPolishExpression.allExpressions().filter({$0.identifier == "many"}).first! - let expression = Expression(pattern: sharedExp.pattern, localizedValue: "") + let expression = Expression(pattern: sharedExp.pattern, value: "") XCTAssertTrue(expression.validate("10"), "Should match 10") XCTAssertTrue(expression.validate("18"), "Should match 18")