Skip to content
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
1 change: 1 addition & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,6 @@ let package = Package(
.target(name: "W3WSwiftCore", dependencies: []),
.testTarget(name: "w3w-swift-typesTests", dependencies: ["W3WSwiftCore"]),
.testTarget(name: "w3w-swift-Tests", dependencies: ["W3WSwiftCore"]),
.testTarget(name: "languages-Tests", dependencies: ["W3WSwiftCore"])
]
)
18 changes: 17 additions & 1 deletion Sources/W3WSwiftCore/Localization/W3WTranslationsProtocol.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
//


public protocol W3WTranslationsProtocol {
public protocol W3WTranslationsProtocol: W3WAvailableLanguageProtocol {

/// given a translation id return the translation for the given device locale
/// - Parameters:
Expand All @@ -21,6 +21,15 @@ public protocol W3WTranslationsProtocol {

}

public protocol W3WLanguageSelectionProtocol {
/// to set RFCLanguage
func set(language: W3WRfcLanguage)
}

/// list out all available RFCLanguages
public protocol W3WAvailableLanguageProtocol {
func availableLanguages() -> [W3WRfcLanguage]
}

public extension W3WTranslationsProtocol {

Expand All @@ -42,3 +51,10 @@ public extension W3WTranslationsProtocol {
return String(format: localized, arguments)
}
}

public extension W3WAvailableLanguageProtocol {
/// default convenience func for available languages, should override when in need
func availableLanguages() -> [W3WRfcLanguage] {
return []
}
}
29 changes: 29 additions & 0 deletions Sources/W3WSwiftCore/RfcLanguage/W3WRfcLanguage+W3WLanguage.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
//
// File.swift
// w3w-swift-core
//
// Created by Kaley Nguyen on 16/9/25.
//

import Foundation

public extension W3WRfcLanguageProtocol {
/// convert W3WRfcLanguage to W3WLanguage
func toW3wLanguage() -> W3WLanguage? {
guard let code, !code.isEmpty else {
return nil
}
return W3WBaseLanguage(locale: self.shortIdentifier)
}
}

@available(iOS 13.0.0, *)
@available(watchOS 6.0.0, *)
extension W3WBaseLanguage: W3WRfcLanguageConvertable {
/// convert W3WBaseLanguage to any W3WRfcLanguageProtocol
public func toRfcLanguage() -> some W3WRfcLanguageProtocol {
let parsedLocale = Locale(identifier: locale)
return W3WRfcLanguage(locale: parsedLocale)
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
//
// File.swift
// w3w-swift-core
//
// Created by Kaley Nguyen on 21/11/25.
//

import Foundation
#if canImport(w3w)
import w3w

public extension W3WRfcLanguageProtocol {
/// convert W3WRfcLanguage to W3WSdkLanguage
func toW3wSdkLanguage() throws -> W3WSdkLanguage? {
guard let code, !code.isEmpty else {
return nil
}
return try W3WSdkLanguage(shortIdentifier)
}
}

@available(iOS 13.0.0, *)
@available(watchOS 6.0.0, *)
extension W3WSdkLanguage: W3WRfcLanguageConvertable {
/// convert W3WSdkLanguage to any W3WRfcLanguageProtocol
public func toRfcLanguage() -> some W3WRfcLanguageProtocol {
return W3WRfcLanguage(locale: Locale(identifier: locale))
}
}
#endif

29 changes: 29 additions & 0 deletions Sources/W3WSwiftCore/RfcLanguage/W3WRfcLanguage.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# W3WRfcLanguage

A lightweight representation of an RFC 5646 language tag used in w3w-swift-core. It models:

- language code (ISO 639; prefer 2-letter when available)
- optional script code (4 letters, Titlecase)
- optional region code (2 letters or 3 digits)

It provides:
- A protocol `W3WRfcLanguageProtocol` that any type can conform to
- A concrete struct `W3WRfcLanguage`
- An extension that makes `Locale.Language` conform to the protocol on iOS 16+/watchOS 9+
- Utilities to build identifiers and parse from strings

## Availability

- `Locale.Language` conformance requires iOS 16+ or watchOS 9+.
- `W3WRfcLanguage` itself is available on all supported platforms; when running on iOS < 16/watchOS < 9, script extraction from `Locale` may be unavailable and remain `nil`.

## Types

### W3WRfcLanguageProtocol
```swift
public protocol W3WRfcLanguageProtocol: Equatable {
var code: String? { get } // ISO 639 language code
var scriptCode: String? { get } // 4-letter Titlecase script code (e.g. "Latn", "Hans")
var regionCode: String? { get } // 2-letter or 3-digit region (e.g. "US", "419")
}

150 changes: 150 additions & 0 deletions Sources/W3WSwiftCore/RfcLanguage/W3WRfcLanguage.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
//
// File.swift
// w3w-swift-core
//
// Created by Kaley Nguyen on 15/8/25.
//

import Foundation

/// language[-script][-region]
/// - language = ISO 639
/// Prefer ISO 639-1 (2-letter) whenever it exists
/// Only fall back to ISO 639-2 (3-letter) if a language has no 2-letter code
/// - script (optional, 4 letters, titlecased)
/// - region (optional, 2 letters or 3-digit number)
public struct W3WRfcLanguage: W3WRfcLanguageProtocol {
/// default language is "en". name's default can be changed externally to use a different language by an app if nessesary
public static var `default` = W3WRfcLanguage(code: "en")

public var code: String?
public var scriptCode: String?
public var regionCode: String?

public init(locale: Locale) {
if #available(iOS 16, *), #available(watchOS 9, *) {
// from iOS 16 supports getting script
self.init(from: locale.language)
} else {
// no script for iOS < 16
self.init(code: locale.languageCode,
scriptCode: locale.scriptCode,
regionCode: locale.regionCode)
}
}

public init(
code: String? = nil,
scriptCode: String? = nil,
regionCode: String? = nil
) {
self.code = code
self.scriptCode = scriptCode
self.regionCode = regionCode
}

@available(iOS 16, *)
@available(watchOS 9, *)
public init(from language: Locale.Language) {
self.code = language.code
self.scriptCode = language.scriptCode
self.regionCode = language.regionCode
}
}

public extension W3WRfcLanguage {
/// Initialize from a string
init(from string: String) {
// Normalize separators
let normalized = string
.trimmingCharacters(in: .whitespacesAndNewlines)
.replacingOccurrences(of: "_", with: "-")

// Handle “Base” or empty strings, check if string is a valid locale string
guard !normalized.isEmpty, string.isValidLocale else {
self.init(code: nil, scriptCode: nil, regionCode: nil)
return
}

if #available(iOS 16, *), #available(watchOS 9, *) {
self.init(from: Locale.Language(identifier: normalized))
} else {
// Split into parts (e.g. zh-Hans-CN → ["zh", "Hans", "CN"])
let parts = normalized.split(separator: "-").map(String.init)

var langCode: String?
var script: String?
var region: String?

if parts.count > 0 {
langCode = parts[0]
}

if parts.count > 1 {
let part = parts[1]
// Detect if this is a script code (Titlecase and 4 letters)
if part.count == 4, part.first?.isUppercase == true {
script = part
if parts.count > 2 {
region = parts[2]
}
} else {
region = part
}
}

self.init(code: langCode, scriptCode: script, regionCode: region)
}
}
}

public extension W3WRfcLanguageProtocol {
/// full version: code - script - region
var identifier: String {
return [code, scriptCode, regionCode]
.compactMap { $0 }
.joined(separator: "-")
}
/// short : code - region
var shortIdentifier: String {
return [code, regionCode]
.compactMap { $0 }
.joined(separator: "-")
}
}

@available(watchOS 9, *)
@available(iOS 16, *)
extension Locale.Language : W3WRfcLanguageProtocol {
public var code: String? {
return languageCode?.identifier
}

public var scriptCode: String? {
return self.script?.identifier
}

public var regionCode: String? {
return region?.identifier
}
}

extension W3WRfcLanguage {
public var nativeName: String? {
return LanguageUtils.getLanguageName(forLocale: identifier, inLocale: identifier)
}

public var name: String? {
return LanguageUtils.getLanguageName(forLocale: identifier, inLocale: Self.default.identifier)
}

/// get name of the language in any particular locale
func name(in locale: String) -> String? {
return LanguageUtils.getLanguageName(forLocale: identifier, inLocale: locale)
}

/// get name of the language in any other language
func name(in language: W3WRfcLanguage) -> String? {
return name(in: language.identifier)
}
}
20 changes: 20 additions & 0 deletions Sources/W3WSwiftCore/RfcLanguage/W3WRfcLanguageProtocol.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
//
// File.swift
// w3w-swift-core
//
// Created by Kaley Nguyen on 21/11/25.
//

import Foundation

public protocol W3WRfcLanguageProtocol: Equatable {
var code: String? { get }
var scriptCode: String? { get }
var regionCode: String? { get }
}

/// protocol to convert any language to W3WRfcLanguage
public protocol W3WRfcLanguageConvertable<Language> {
associatedtype Language: W3WRfcLanguageProtocol
func toRfcLanguage() -> Language
}
35 changes: 35 additions & 0 deletions Sources/W3WSwiftCore/Types/Util/LanguageUtils.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
//
// LanguageUtils.swift
// w3w-swift-core
//
// Created by Kaley Nguyen on 14/11/25.
//
import Foundation

public class LanguageUtils {
/// gets the langauge name for a locale in the language specified by 'inLocale'
/// Parameters
/// - forLocale: locale to find the language of
/// - inLocale: locale to translate the langauge name into
public static func getLanguageName(forLocale: String, inLocale: String) -> String? {
let inLocaleObj = NSLocale(localeIdentifier: inLocale)
let forLocaleObj = NSLocale(localeIdentifier: forLocale)

var languageName = inLocaleObj.localizedString(forLanguageCode: forLocale) ?? ""

if forLocale.count > 2 {
if let countryCode = forLocaleObj.countryCode {
languageName += " (" + (inLocaleObj.localizedString(forCountryCode: countryCode) ?? "") + ")"
}
}

return languageName
}
}

extension String {
var isValidLocale: Bool {
let normalized = self.replacingOccurrences(of: "-", with: "_")
return Locale.availableIdentifiers.contains(normalized)
}
}
Loading