Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve localization usage #113

Merged
merged 11 commits into from
Mar 1, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -193,9 +193,11 @@ class CrowdinContentDeliveryAPI: BaseAPI {
let response = try JSONDecoder().decode(ManifestResponse.self, from: data)
return response
} catch {
LocalizationUpdateObserver.shared.notifyError(with: [error])
return nil
}
} else {
LocalizationUpdateObserver.shared.notifyError(with: [NSError(domain: "Unable to download manifest for hash - \(hash)", code: defaultCrowdinErrorCode, userInfo: nil)])
return nil
}
}
Expand Down
9 changes: 6 additions & 3 deletions CrowdinSDK/Classes/CrowdinAPI/SocketAPI/SocketAPI.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ class SocketAPI: NSObject {
var ws: WebSocket
var onConnect: (() -> Void)? = nil
var onError: ((Error) -> Void)? = nil
var onDisconnect: (() -> Void)? = nil
var didReceiveUpdateDraft: ((UpdateDraftResponse) -> Void)? = nil
var didReceiveUpdateTopSuggestion: ((TopSuggestionResponse) -> Void)? = nil

Expand All @@ -44,7 +45,7 @@ class SocketAPI: NSObject {
}

func disconect() {
self.ws.disconnect()
self.ws.disconnect(forceTimeout: 1, closeCode: CloseCode.normal.rawValue)
}

func subscribeOnUpdateDraft(localization: String, stringId: Int) {
Expand All @@ -70,10 +71,12 @@ extension SocketAPI: WebSocketDelegate {
}

func websocketDidDisconnect(socket: WebSocketClient, error: Error?) {
if let error = error {
if let wsError = error as? WSError, wsError.code == CloseCode.normal.rawValue {
self.onDisconnect?()
} else if let error = error {
self.onError?(error)
} else {
self.onError?(NSError(domain: Errors.didDisconect.rawValue, code: defaultCrowdinErrorCode, userInfo: nil))
self.onDisconnect?()
}
}

Expand Down
71 changes: 15 additions & 56 deletions CrowdinSDK/Classes/CrowdinSDK/CrowdinSDK.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,44 +16,10 @@ public typealias CrowdinSDKLocalizationUpdateError = ([Error]) -> Void

/// Main interface for working with CrowdinSDK library.
@objcMembers public class CrowdinSDK: NSObject {
/// Enum representing available SDK modes.
///
/// autoSDK - Automaticly detect current localization and change localized strings to crowdin strings.
///
/// customSDK - Enable user defined localization from crowdin supported languages.
///
/// autoBundle - Does not enable crowdin localization. In this mode will be used bundle localization detected by system.
///
/// customBundle - Set user defined localization from bundle supported languages.
public enum Mode: Int {
case autoSDK
case customSDK
case autoBundle
case customBundle

var isAutoMode: Bool {
return self == .autoSDK || self == .autoBundle
}

var isSDKMode: Bool {
return self == .autoSDK || self == .customSDK
}
}

/// Current SDK mode.
public class var mode: Mode {
get {
return Localization.mode
}
set {
Localization.mode = newValue
}
}

/// Current localization language code.
public class var currentLocalization: String? {
get {
return Localization.currentLocalization
return Localization.currentLocalization ?? Localization.current?.provider.localization
}
set {
Localization.currentLocalization = newValue
Expand All @@ -64,7 +30,16 @@ public typealias CrowdinSDKLocalizationUpdateError = ([Error]) -> Void
public class var inSDKLocalizations: [String] { return Localization.current?.inProvider ?? [] }

/// List of supported in app localizations.
public class var inBundleLocalizations: [String] { return Localization.current?.inBundle ?? Bundle.main.localizations }
public class var inBundleLocalizations: [String] { Bundle.main.inBundleLocalizations }

/// List of all available localizations in bundle and on crowdin.
public class var allAvalaibleLocalizations: [String] {
var localizations = Array(Set<String>(inSDKLocalizations + inBundleLocalizations))
if let index = localizations.firstIndex(where: { $0 == "Base" }) {
localizations.remove(at: index)
}
return localizations
}

// swiftlint:disable implicitly_unwrapped_optional
static var config: CrowdinSDKConfig!
Expand All @@ -89,36 +64,24 @@ public typealias CrowdinSDKLocalizationUpdateError = ([Error]) -> Void

/// Removes all stored information by SDK from application Documents folder. Use to clean up all files used by SDK.
public class func deintegrate() {
Localization.current.provider.deintegrate()
Localization.current?.provider.deintegrate()
}

/// Method for changing SDK lcoalization and mode. There are 4 avalaible modes in SDK. For more information please look on Mode enum description.
///
/// - Parameters:
/// - sdkLocalization: Bool value which indicate whether to use SDK localization or native in bundle localization.
/// - localization: Localization code to use.
@available(*, deprecated, message: "Please use currentLocalization instead.")
public class func enableSDKLocalization(_ sdkLocalization: Bool, localization: String?) {
if sdkLocalization {
if localization != nil {
self.mode = .customSDK
} else {
self.mode = .autoSDK
}
} else {
if localization != nil {
self.mode = .customBundle
} else {
self.mode = .autoBundle
}
}
self.currentLocalization = localization
}

/// Sets localization provider to SDK. If you want to use your own localization implementation you can set it by using this method. Note: your object should be inherited from @BaseLocalizationProvider class.
///
/// - Parameter remoteStorage: Localization remote storage which contains all strings, plurals and avalaible localizations values.
class func setRemoteStorage(_ remoteStorage: RemoteLocalizationStorageProtocol) {
let localizations = remoteStorage.localizations;
let localizations = remoteStorage.localizations + self.inBundleLocalizations;
let localization = self.currentLocalization ?? Bundle.main.preferredLanguage(with: localizations)
let localizationProvider = LocalizationProvider(localization: localization, localizations: localizations, remoteStorage: remoteStorage)
Localization.current = Localization(provider: localizationProvider)
Expand Down Expand Up @@ -211,11 +174,7 @@ extension CrowdinSDK {

/// Method for library initialization.
class func initializeLib() {
if self.mode == .customSDK || self.mode == .autoSDK {
CrowdinSDK.swizzle()
} else {
CrowdinSDK.unswizzle()
}
self.swizzle()

self.setupLoginIfNeeded()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,14 @@ extension Bundle {
return self.preferredLanguages.first ?? defaultLocalization
}

var inBundleLocalizations: [String] {
var localizations = self.localizations
while let index = localizations.firstIndex(where: { $0 == "Base" }) {
localizations.remove(at: index)
}
return localizations
}

/// Return ordered list of language codes according to device settings, and bundle localizations.
// TODO: Add handling case when intersection of preffered languages from settings and localizations in bundle is empty.
var preferredLanguages: [String] {
Expand All @@ -35,7 +43,7 @@ extension Bundle {

/// Returns detected preffered language from device settings and passed localizations. If bundle localizations is empty then return default locazation - "en".
func preferredLanguage(with availableLanguages: [String]) -> String {
return self.preferredLanguages(with: availableLanguages).first ?? defaultLocalization
return Bundle.preferredLocalizations(from: availableLanguages, forPreferences: nil).first ?? defaultLocalization
}

/// Return ordered list of language codes according to device settings, and passed localizations.
Expand Down
15 changes: 15 additions & 0 deletions CrowdinSDK/Classes/CrowdinSDK/Extensions/Locale.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ extension Locale {
// TODO: find a better way of getting language identifiers without replacing "_" to "-".
return Locale.identifier(fromComponents: components).replacingOccurrences(of: "_", with: "-")
})

// Also add language code from localization with regions: "en-US" -> "en", "uk-UA" -> "uk".
localizations.forEach {
if let language = $0.split(separator: "-").map({ String($0) }).first, language != $0, !localizations.contains(language) {
Expand All @@ -33,6 +34,20 @@ extension Locale {
}
}
}

// Add region code to localizations without region: "en" -> "en-US".
if let regionCode = Locale.current.regionCode {
localizations.forEach({
if !$0.hasLocaleId {
localizations.append("\($0)-\(regionCode)")
}
})
}
return localizations
}

}

private extension String {
var hasLocaleId: Bool { split(separator: "-").count > 1 }
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,27 +7,31 @@

import Foundation

class LocalLocalizationExtractor {
static var allLocalizations: [String] {
return Bundle.main.localizations
final class LocalLocalizationExtractor {
enum Strings: String {
case LocalPlurals
case LocalizableStringsdict
}

var allKeys: [String] {
return self.localizationDict.keys.map({ String($0) })
}
var allValues: [String] {
return self.localizationDict.values.map({ String($0) })
}
static var allLocalizations: [String] { Bundle.main.inBundleLocalizations }

var allKeys: [String] { localizationDict.keys.map({ String($0) }) }
var allValues: [String] { localizationDict.values.map({ String($0) }) }

var localizationDict: [String: String] = [:]
var localizationPluralsDict: [AnyHashable: Any] = [:]

var localization: String
var pluralsFolder: FolderProtocol
var pluralsBundle: DictionaryBundleProtocol?

var isEmpty: Bool {
return self.localizationDict.isEmpty && self.localizationPluralsDict.isEmpty
var localization: String {
didSet {
extract()
}
}

var isEmpty: Bool { localizationDict.isEmpty && self.localizationPluralsDict.isEmpty }

var stringsFiles: [String] {
guard let filePath = Bundle.main.path(forResource: localization, ofType: FileType.lproj.rawValue) else { return [] }
guard var files = try? FileManager.default.contentsOfDirectory(atPath: filePath) else { return [] }
Expand All @@ -44,30 +48,29 @@ class LocalLocalizationExtractor {

init(localization: String) {
self.localization = localization
self.extract()
// If we're unable to extract localization passed/detected language then try to extract Base localization.
if self.isEmpty, let developmentRegion = Bundle.main.developmentRegion {
self.localization = developmentRegion
self.extract()
}
pluralsFolder = Folder(path: CrowdinFolder.shared.path + String.pathDelimiter + Strings.LocalPlurals.rawValue)
extract()
}

func setLocalization(_ localization: String) {
self.localization = localization
self.extract()
extract()
}

func extract() {
self.stringsFiles.forEach { (file) in
localizationDict = [:]
stringsFiles.forEach { (file) in
guard let dict = NSDictionary(contentsOfFile: file) else { return }
self.localizationDict.merge(with: dict as? [String: String] ?? [:])
}

self.stringsdictFiles.forEach { (file) in
localizationPluralsDict = [:]
stringsdictFiles.forEach { (file) in
guard let dict = NSMutableDictionary (contentsOfFile: file) else { return }
guard let strings = dict as? [AnyHashable: Any] else { return }
self.localizationPluralsDict = self.localizationPluralsDict + strings
}
setupPluralsBundle()
}

static func extractLocalizationJSONFile(to path: String) {
Expand Down Expand Up @@ -119,4 +122,24 @@ class LocalLocalizationExtractor {
_ = ectractor.extractLocalizationPlurals(to: path)
}
}

func setupPluralsBundle() {
pluralsBundle?.remove()
pluralsFolder.directories.forEach{ try? $0.remove() }
let localizationFolderName = localization + String.minus + UUID().uuidString
pluralsBundle = DictionaryBundle(path: pluralsFolder.path + String.pathDelimiter + localizationFolderName, fileName: Strings.LocalizableStringsdict.rawValue, dictionary: self.localizationPluralsDict)
}

// Localization methods
func localizedString(for key: String) -> String? {
var string = self.localizationDict[key]
if string == nil {
string = self.pluralsBundle?.bundle.swizzled_LocalizedString(forKey: key, value: nil, table: nil)
// Plurals localization works as default bundle localization. In case localized string for key is missing the key string will be returned. To prevent issues with localization where key equals value(for example for english language) we need to set nil here.
if string == key {
string = nil
}
}
return string
}
}
Loading