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
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,14 @@ Continuous integration:

- Software requirements specification: [docs/SRS.md](docs/SRS.md)
- Sample catalog fixture: [docs/example.xcstrings](docs/example.xcstrings)
- Module guide index: [docs/modules/README.md](docs/modules/README.md)
- `XCSCore` API guide: [docs/modules/XCSCore.md](docs/modules/XCSCore.md)
- `XCSParser` API guide: [docs/modules/XCSParser.md](docs/modules/XCSParser.md)
- `XCSValidator` API guide: [docs/modules/XCSValidator.md](docs/modules/XCSValidator.md)
- `XCSTranslator` API guide: [docs/modules/XCSTranslator.md](docs/modules/XCSTranslator.md)
- `XCSWriter` API guide: [docs/modules/XCSWriter.md](docs/modules/XCSWriter.md)
- `XCSAnalytics` API guide: [docs/modules/XCSAnalytics.md](docs/modules/XCSAnalytics.md)
- `XCSKit` umbrella guide: [docs/modules/XCSKit.md](docs/modules/XCSKit.md)

## Status

Expand Down
221 changes: 150 additions & 71 deletions Sources/XCSAnalytics/XCSAnalytics.swift
Original file line number Diff line number Diff line change
Expand Up @@ -41,13 +41,15 @@ public struct LanguageProgress: Equatable, Sendable {
public let missingLeafCount: Int
public let untranslatedLeafCount: Int
public let needsReviewLeafCount: Int
private let sourceKeyCount: Int
private let translatedKeyCount: Int

public var progress: Double {
guard sourceLeafCount > 0 else {
guard sourceKeyCount > 0 else {
return 0
}

return Double(translatedLeafCount) / Double(sourceLeafCount)
return Double(translatedKeyCount) / Double(sourceKeyCount)
}

public init(
Expand All @@ -56,14 +58,18 @@ public struct LanguageProgress: Equatable, Sendable {
translatedLeafCount: Int,
missingLeafCount: Int,
untranslatedLeafCount: Int,
needsReviewLeafCount: Int
needsReviewLeafCount: Int,
sourceKeyCount: Int? = nil,
translatedKeyCount: Int? = nil
) {
self.locale = locale
self.sourceLeafCount = sourceLeafCount
self.translatedLeafCount = translatedLeafCount
self.missingLeafCount = missingLeafCount
self.untranslatedLeafCount = untranslatedLeafCount
self.needsReviewLeafCount = needsReviewLeafCount
self.sourceKeyCount = sourceKeyCount ?? sourceLeafCount
self.translatedKeyCount = translatedKeyCount ?? translatedLeafCount
}
}

Expand All @@ -77,14 +83,16 @@ public struct CatalogStatistics: Equatable, Sendable {
public let missingLeafCount: Int
public let untranslatedLeafCount: Int
public let languageProgress: [LanguageProgress]
private let progressKeyCount: Int
private let translatedProgressCount: Int

public var overallCoverage: Double {
let possibleLeafCount = totalSourceLeafCount * targetLanguages.count
guard possibleLeafCount > 0 else {
let possibleTranslationCount = progressKeyCount * targetLanguages.count
guard possibleTranslationCount > 0 else {
return 0
}

return Double(translatedLeafCount) / Double(possibleLeafCount)
return Double(translatedProgressCount) / Double(possibleTranslationCount)
}

public init(
Expand All @@ -96,7 +104,9 @@ public struct CatalogStatistics: Equatable, Sendable {
translatedLeafCount: Int,
missingLeafCount: Int,
untranslatedLeafCount: Int,
languageProgress: [LanguageProgress]
languageProgress: [LanguageProgress],
progressKeyCount: Int? = nil,
translatedProgressCount: Int? = nil
) {
self.sourceLanguage = sourceLanguage
self.keyCount = keyCount
Expand All @@ -107,6 +117,8 @@ public struct CatalogStatistics: Equatable, Sendable {
self.missingLeafCount = missingLeafCount
self.untranslatedLeafCount = untranslatedLeafCount
self.languageProgress = languageProgress
self.progressKeyCount = progressKeyCount ?? totalSourceLeafCount
self.translatedProgressCount = translatedProgressCount ?? translatedLeafCount
}
}

Expand Down Expand Up @@ -194,11 +206,7 @@ public enum XCSAnalytics {
}

for entry in catalog.strings.values {
guard let sourceLocalization = catalog.sourceLocalization(for: entry) else {
continue
}

let sourceLeaves = leafMap(for: sourceLocalization)
let sourceLeaves = resolvedSourceLeafMap(for: entry, sourceLanguage: catalog.sourceLanguage)
guard !sourceLeaves.isEmpty else {
continue
}
Expand All @@ -208,24 +216,15 @@ public enum XCSAnalytics {
continue
}

let targetLeaves = leafMap(for: entry.localizations?[locale])
accumulator.sourceLeafCount += sourceLeaves.count

for (path, _) in sourceLeaves {
guard let targetLeaf = targetLeaves[path] else {
accumulator.missingLeafCount += 1
accumulator.untranslatedLeafCount += 1
continue
}

if targetLeaf.state == "translated" {
accumulator.translatedLeafCount += 1
} else {
accumulator.untranslatedLeafCount += 1
if targetLeaf.state == "needs_review" {
accumulator.needsReviewLeafCount += 1
}
}
let coverage = localeCoverage(for: entry, locale: locale, sourceLeaves: sourceLeaves)
accumulator.sourceLeafCount += coverage.sourceLeafCount
accumulator.translatedLeafCount += coverage.translatedLeafCount
accumulator.missingLeafCount += coverage.missingLeafCount
accumulator.untranslatedLeafCount += coverage.untranslatedLeafCount
accumulator.needsReviewLeafCount += coverage.needsReviewLeafCount
accumulator.sourceKeyCount += 1
if coverage.isComplete {
accumulator.translatedKeyCount += 1
}
}
}
Expand All @@ -235,35 +234,29 @@ public enum XCSAnalytics {

public static func statistics(in catalog: Catalog) -> CatalogStatistics {
let locales = targetLanguages(in: catalog)
let totalSourceLeafCount = sourceLeafMap(in: catalog).values.reduce(0) { $0 + $1.count }
var totalSourceLeafCount = 0
var translatedLeafCount = 0
var missingLeafCount = 0
var untranslatedLeafCount = 0
var progressKeyCount = 0
var translatedProgressCount = 0

for entry in catalog.strings.values {
guard let sourceLocalization = catalog.sourceLocalization(for: entry) else {
continue
}

let sourceLeaves = leafMap(for: sourceLocalization)
let sourceLeaves = resolvedSourceLeafMap(for: entry, sourceLanguage: catalog.sourceLanguage)
guard !sourceLeaves.isEmpty else {
continue
}

for locale in locales {
let targetLeaves = leafMap(for: entry.localizations?[locale])
for (path, _) in sourceLeaves {
guard let targetLeaf = targetLeaves[path] else {
missingLeafCount += 1
untranslatedLeafCount += 1
continue
}
totalSourceLeafCount += sourceLeaves.count
progressKeyCount += 1

if targetLeaf.state == "translated" {
translatedLeafCount += 1
} else {
untranslatedLeafCount += 1
}
for locale in locales {
let coverage = localeCoverage(for: entry, locale: locale, sourceLeaves: sourceLeaves)
translatedLeafCount += coverage.translatedLeafCount
missingLeafCount += coverage.missingLeafCount
untranslatedLeafCount += coverage.untranslatedLeafCount
if coverage.isComplete {
translatedProgressCount += 1
}
}
}
Expand All @@ -277,7 +270,9 @@ public enum XCSAnalytics {
translatedLeafCount: translatedLeafCount,
missingLeafCount: missingLeafCount,
untranslatedLeafCount: untranslatedLeafCount,
languageProgress: languageProgress(in: catalog)
languageProgress: languageProgress(in: catalog),
progressKeyCount: progressKeyCount,
translatedProgressCount: translatedProgressCount
)
}

Expand All @@ -286,13 +281,24 @@ public enum XCSAnalytics {
let stringUnit: StringUnit
}

private struct LeafCoverage {
let sourceLeafCount: Int
let translatedLeafCount: Int
let missingLeafCount: Int
let untranslatedLeafCount: Int
let needsReviewLeafCount: Int
let isComplete: Bool
}

private final class LanguageProgressAccumulator {
let locale: String
var sourceLeafCount: Int = 0
var translatedLeafCount: Int = 0
var missingLeafCount: Int = 0
var untranslatedLeafCount: Int = 0
var needsReviewLeafCount: Int = 0
var sourceKeyCount: Int = 0
var translatedKeyCount: Int = 0

init(locale: String) {
self.locale = locale
Expand All @@ -305,19 +311,17 @@ public enum XCSAnalytics {
translatedLeafCount: translatedLeafCount,
missingLeafCount: missingLeafCount,
untranslatedLeafCount: untranslatedLeafCount,
needsReviewLeafCount: needsReviewLeafCount
needsReviewLeafCount: needsReviewLeafCount,
sourceKeyCount: sourceKeyCount,
translatedKeyCount: translatedKeyCount
)
}
}

private static func sourceLeafMap(in catalog: Catalog) -> [String: [[VariationSelection]: StringUnit]] {
var map: [String: [[VariationSelection]: StringUnit]] = [:]
for entry in catalog.strings.values {
guard let sourceLocalization = catalog.sourceLocalization(for: entry) else {
continue
}

let leaves = leafMap(for: sourceLocalization)
let leaves = resolvedSourceLeafMap(for: entry, sourceLanguage: catalog.sourceLanguage)
guard !leaves.isEmpty else {
continue
}
Expand All @@ -339,6 +343,46 @@ public enum XCSAnalytics {
return map
}

private static func leafMap(for leaves: [Leaf]) -> [[VariationSelection]: StringUnit] {
var map: [[VariationSelection]: StringUnit] = [:]
for leaf in leaves {
map[leaf.path] = leaf.stringUnit
}
return map
}

private static func resolvedSourceLeafMap(
for entry: Entry,
sourceLanguage: String
) -> [[VariationSelection]: StringUnit] {
leafMap(for: resolvedSourceLeaves(for: entry, sourceLanguage: sourceLanguage))
}

private static func resolvedSourceLeaves(for entry: Entry, sourceLanguage: String) -> [Leaf] {
guard entry.shouldTranslate != false else {
return []
}

guard let localizations = entry.localizations else {
return fallbackSourceLeaves(for: entry)
}

guard let sourceLocalization = localizations[sourceLanguage] else {
return fallbackSourceLeaves(for: entry)
}

return collectLeaves(from: sourceLocalization)
}

private static func fallbackSourceLeaves(for entry: Entry) -> [Leaf] {
[
Leaf(
path: [],
stringUnit: StringUnit(state: "translated", value: entry.key)
)
]
}

private static func collectLeaves(from localization: Localization) -> [Leaf] {
var leaves: [Leaf] = []
if let stringUnit = localization.stringUnit {
Expand Down Expand Up @@ -377,17 +421,55 @@ public enum XCSAnalytics {
}
}

private static func localeCoverage(
for entry: Entry,
locale: String,
sourceLeaves: [[VariationSelection]: StringUnit]
) -> LeafCoverage {
let targetLeaves = leafMap(for: entry.localizations?[locale])
let entryIsStale = entry.extractionState == .stale
var translatedLeafCount = 0
var missingLeafCount = 0
var untranslatedLeafCount = 0
var needsReviewLeafCount = 0
var isComplete = !entryIsStale

for (path, _) in sourceLeaves {
guard let targetLeaf = targetLeaves[path] else {
missingLeafCount += 1
untranslatedLeafCount += 1
isComplete = false
continue
}

if !entryIsStale, targetLeaf.state == "translated" {
translatedLeafCount += 1
} else {
untranslatedLeafCount += 1
isComplete = false
if !entryIsStale, targetLeaf.state == "needs_review" {
needsReviewLeafCount += 1
}
}
}

return LeafCoverage(
sourceLeafCount: sourceLeaves.count,
translatedLeafCount: translatedLeafCount,
missingLeafCount: missingLeafCount,
untranslatedLeafCount: untranslatedLeafCount,
needsReviewLeafCount: needsReviewLeafCount,
isComplete: isComplete
)
}

private static func keyCoverage(
for entry: Entry,
key: String,
locale: String?,
catalog: Catalog
) -> KeyCoverageSummary? {
guard let sourceLocalization = catalog.sourceLocalization(for: entry) else {
return nil
}

let sourceLeaves = leafMap(for: sourceLocalization)
let sourceLeaves = resolvedSourceLeafMap(for: entry, sourceLanguage: catalog.sourceLanguage)
guard !sourceLeaves.isEmpty else {
return nil
}
Expand All @@ -396,21 +478,18 @@ public enum XCSAnalytics {
var missingLeafCount = 0

if let locale {
let targetLeaves = leafMap(for: entry.localizations?[locale])
let coverage = localeCoverage(for: entry, locale: locale, sourceLeaves: sourceLeaves)
translatedLeafCount = coverage.translatedLeafCount
missingLeafCount = coverage.missingLeafCount
} else {
let locales = targetLanguages(in: catalog)
for (path, _) in sourceLeaves {
guard let targetLeaf = targetLeaves[path] else {
var translated = false
guard entry.extractionState != .stale else {
missingLeafCount += 1
continue
}

if targetLeaf.state == "translated" {
translatedLeafCount += 1
}
}
} else {
let locales = targetLanguages(in: catalog)
for (path, _) in sourceLeaves {
var translated = false
for targetLocale in locales {
guard let targetLeaf = leafMap(for: entry.localizations?[targetLocale])[path] else {
continue
Expand Down
2 changes: 1 addition & 1 deletion Tests/XCSKitTests/ANL_010.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ struct ANL_010 {
#expect(fr.missingLeafCount == 1)
#expect(fr.untranslatedLeafCount == 2)
#expect(fr.needsReviewLeafCount == 0)
#expect(fr.progress == 0.5)
#expect(fr.progress == 1.0 / 3.0)

let ja = try #require(progress.first { $0.locale == "ja" })
#expect(ja.sourceLeafCount == 4)
Expand Down
Loading
Loading