Skip to content
Draft
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
15 changes: 15 additions & 0 deletions Sources/HTMLKit/Framework/Localization/Locale.swift
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,11 @@ public struct Locale: Sendable {
return timeFormats[tag]
}

/// The plural rules of the language.
public var pluralRules: [String: Int]? {
return Grammar.pluralRules[tag]
}

/// The locale identifier.
internal let tag: String

Expand All @@ -113,6 +118,16 @@ public struct Locale: Sendable {

extension Locale {

internal enum Grammar {

internal static var pluralRules: [String: [String: Int]] {
return [
"en-GB": ["zero": 0, "one": 1, "two": 2, "few": 3, "many": 4],
"de-DE": ["zero": 0, "one": 1, "two": 2, "few": 3, "many": 4]
]
}
}

internal var currencyCodes: [String: String] {
return [
"ar-AE": "AED‏",
Expand Down
66 changes: 60 additions & 6 deletions Sources/HTMLKit/Framework/Localization/Localization.swift
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ public struct Localization: Sendable {
var entries = [String: Translation]()

for (key, value) in translations {
entries[key] = Translation(value: value)
entries[key] = Translation(default: value)
}

tables.append(TranslationTable(name: path.deletingPathExtension().lastPathComponent, translations: entries))
Expand All @@ -174,7 +174,7 @@ public struct Localization: Sendable {
var entries = [String: Translation]()

for (key, value) in translations {
entries[key] = Translation(value: value)
entries[key] = Translation(default: value)
}

catalogs[locale] = [TranslationTable(name: path.deletingPathExtension().lastPathComponent, translations: entries)]
Expand All @@ -195,9 +195,35 @@ public struct Localization: Sendable {

let locale = Locale(tag: tag)

if let unit = localization.unit {
if let singular = localization.singular {

let translation = Translation(value: unit.value, comment: entry.comment)
let translation = Translation(default: singular.value, comment: entry.comment)

if var tables = catalogs[locale] {

if let index = tables.firstIndex(where: { $0.name == path.deletingPathExtension().lastPathComponent }) {
tables[index].upsert(translation, for: key)

} else {
tables.append(TranslationTable(name: path.deletingPathExtension().lastPathComponent, translations: [key: translation]))
}

catalogs[locale] = tables

} else {

catalogs[locale] = [TranslationTable(name: path.deletingPathExtension().lastPathComponent, translations: [key: translation])]
}
}

if let plural = localization.plural {

var translation = Translation(default: plural.other.value, comment: entry.comment)
translation.append(plural.zero?.value, for: .zero)
translation.append(plural.one?.value, for: .one)
translation.append(plural.two?.value, for: .two)
translation.append(plural.few?.value, for: .few)
translation.append(plural.many?.value, for: .many)

if var tables = catalogs[locale] {

Expand Down Expand Up @@ -311,7 +337,7 @@ public struct Localization: Sendable {
throw Error.missingKey(identifier: string.key.value, locale: candidate)
}

var value = translation.value
var value = translation.cases[.other]!

interpolate(arguments: string.key.arguments, to: &value, for: candidate)

Expand All @@ -323,7 +349,35 @@ public struct Localization: Sendable {

if let translation = table.retrieve(for: string.key.value) {

var value = translation.value
var value = ""

if !string.key.arguments.isEmpty {

for argument in string.key.arguments {

switch argument {
case .int(let int):

for `case` in translation.cases {

if int == candidate.pluralRules?[`case`.key.rawValue] {

value = `case`.value

break

} else {
value = translation.cases[.other]!
}
}

default:
value = translation.cases[.other]!
}
}
} else {
value = translation.cases[.other]!
}

interpolate(arguments: string.key.arguments, to: &value, for: candidate)

Expand Down
21 changes: 21 additions & 0 deletions Sources/HTMLKit/Framework/Localization/PluralCategory.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/// A type that represents a set of plural categories.
enum PluralCategory: String {

/// Indicates the empty form.
case zero

/// Indicates the singular form.
case one

/// Indicates the dual form.
case two

/// Indicates the paucal form.
case few

/// Indicates the plural form.
case many

/// Indicates the default form.
case other
}
69 changes: 64 additions & 5 deletions Sources/HTMLKit/Framework/Localization/StringCatalog.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ internal struct StringCatalog: Codable {
}

/// The entries within the catalog.
internal let entries: [String: StringCatalog.Entry]
internal let entries: [String: Entry]
}

extension StringCatalog {
Expand All @@ -16,27 +16,86 @@ extension StringCatalog {
internal struct Entry: Codable {

/// The associated values.
internal let localizations: [String: StringCatalog.Localization]
internal let localizations: [String: Localization]

/// The associated comment.
internal let comment: String?
}

/// A type that represents a entry value.
/// A type that represents an entry value.
internal struct Localization: Codable {

internal enum CodingKeys: String, CodingKey {

case singular = "stringUnit"
case plural = "variations"
}

/// Whether there is a singular value.
internal let singular: Unit?

/// Whether there is a plural value.
internal let plural: Plural?
}

/// A type that represents a plural value.
@dynamicMemberLookup
internal struct Plural: Codable {

internal enum CodingKeys: String, CodingKey {

case forms = "plural"
}

/// The associated rules.
private let forms: Forms

internal subscript<T>(dynamicMember keyPath: KeyPath<Forms, T>) -> T {
return forms[keyPath: keyPath]
}
}

/// A type that represents the forms for the plural value.
internal struct Forms: Codable {

/// The associated general form.
let other: Form

/// The associated empty form.
let zero: Form?

/// The associated singular form.
let one: Form?

/// The associated dual form.
let two: Form?

/// The associated paucal form.
let few: Form?

/// The associated plural form.
let many: Form?
}

@dynamicMemberLookup
internal struct Form: Codable {

internal enum CodingKeys: String, CodingKey {

case unit = "stringUnit"
}

internal let unit: StringCatalog.Unit?
private let unit: StringCatalog.Unit

internal subscript<T>(dynamicMember keyPath: KeyPath<Unit, T>) -> T {
return unit[keyPath: keyPath]
}
}

/// A type that represents a value representation.
internal struct Unit: Codable {

/// The associated representation.
internal let value: String
}
}

15 changes: 11 additions & 4 deletions Sources/HTMLKit/Framework/Localization/Translation.swift
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
/// A type that represents the translation within the translation table.
internal struct Translation: Sendable {

/// The value of the translation.
internal let value: String
/// The plural cases of the translation.
internal var cases: [PluralCategory: String]

/// The comment describing the context.
internal let comment: String?
Expand All @@ -12,9 +12,16 @@ internal struct Translation: Sendable {
/// - Parameters:
/// - value: The value for the translation.
/// - comment: The comment for some context.
internal init(value: String, comment: String? = nil) {
internal init(default value: String, comment: String? = nil) {

self.value = value
self.cases = [.other: value]
self.comment = comment
}

internal mutating func append(_ value: String?, for category: PluralCategory) {

if let value = value {
self.cases[category] = value
}
}
}
53 changes: 53 additions & 0 deletions Tests/HTMLKitTests/Localization/Localizable.xcstrings
Original file line number Diff line number Diff line change
@@ -1,6 +1,59 @@
{
"sourceLanguage" : "en",
"strings" : {
"%lld iPhone" : {
"extractionState" : "manual",
"localizations" : {
"en" : {
"variations" : {
"plural" : {
"one" : {
"stringUnit" : {
"state" : "translated",
"value" : "%lld iPhone"
}
},
"other" : {
"stringUnit" : {
"state" : "translated",
"value" : "%lld iPhones"
}
},
"zero" : {
"stringUnit" : {
"state" : "translated",
"value" : "%lld iPhones"
}
}
}
}
},
"en-GB" : {
"variations" : {
"plural" : {
"one" : {
"stringUnit" : {
"state" : "translated",
"value" : "%lld iPhone"
}
},
"other" : {
"stringUnit" : {
"state" : "translated",
"value" : "%lld iPhones"
}
},
"zero" : {
"stringUnit" : {
"state" : "translated",
"value" : "%lld iPhones"
}
}
}
}
}
}
},
"hello.xcstrings" : {
"comment" : "One more thing",
"extractionState" : "manual",
Expand Down
9 changes: 9 additions & 0 deletions Tests/HTMLKitTests/LocalizationTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,15 @@ struct LocalizationTests {

#expect(translation.comment == "One more thing")
}

/// Tests the correct use of pluralization.
@Test
func testPluralization() throws {

#expect(try localization!.localize(string: .init(key: "\(0) iPhone")) == "0 iPhones")
#expect(try localization!.localize(string: .init(key: "\(1) iPhone")) == "1 iPhone")
#expect(try localization!.localize(string: .init(key: "\(4) iPhone")) == "4 iPhones")
}
}

extension LocalizationTests {
Expand Down
Loading