mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-24 22:45:19 +00:00
i18n: fill all locale gaps so no string falls back to English (#1393)
Follow-up to #1391 — the Codex review flagged that the new feature-string batch stopped at vi, omitting fil, pt-BR, zh-Hans, and zh-Hant. - Translate the 137 feature strings into fil, pt-BR, zh-Hans, zh-Hant (548 entries) - Translate fingerprint.message.vouched_by (plural) into the 16 locales it was missing, with CLDR-correct plural categories per language - Add the 13 locales missing from the share extension catalog (78 entries), bringing it to the same 29 locales as the main app - Mark the '#%@' channel-hashtag format as shouldTranslate=false like '%@' - Add LocalizationCoverageTests: fails if any translatable key in either catalog is missing any supported locale, or if the share extension supports fewer locales than the main app Machine translations marked needs_review, matching #1391. Verified no existing translation was altered; full suite (1348 tests) green. Co-authored-by: jack <jackjackbits@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
jack
Claude Fable 5
parent
8415c52913
commit
28ffc53f3f
+3787
-1
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,74 @@
|
||||
import Testing
|
||||
import Foundation
|
||||
|
||||
/// Guards against locale gaps in the string catalogs: every translatable key
|
||||
/// must have a localization for every supported locale, so no user ever sees
|
||||
/// an English fallback (see PR #1391 review).
|
||||
struct LocalizationCoverageTests {
|
||||
private static let repoRoot = URL(fileURLWithPath: #filePath)
|
||||
.deletingLastPathComponent() // bitchatTests
|
||||
.deletingLastPathComponent() // repo root
|
||||
|
||||
private struct Catalog {
|
||||
let sourceLanguage: String
|
||||
/// key -> set of locales with a localization entry
|
||||
let coverage: [String: Set<String>]
|
||||
/// all locales appearing anywhere in the catalog
|
||||
var allLocales: Set<String> { coverage.values.reduce(into: []) { $0.formUnion($1) } }
|
||||
}
|
||||
|
||||
private static func loadCatalog(_ relativePath: String) throws -> Catalog {
|
||||
let url = repoRoot.appendingPathComponent(relativePath)
|
||||
let data = try Data(contentsOf: url)
|
||||
let root = try #require(try JSONSerialization.jsonObject(with: data) as? [String: Any])
|
||||
let strings = try #require(root["strings"] as? [String: Any])
|
||||
let sourceLanguage = try #require(root["sourceLanguage"] as? String)
|
||||
|
||||
var coverage: [String: Set<String>] = [:]
|
||||
for (key, value) in strings {
|
||||
guard let entry = value as? [String: Any] else { continue }
|
||||
if entry["shouldTranslate"] as? Bool == false { continue }
|
||||
let localizations = entry["localizations"] as? [String: Any] ?? [:]
|
||||
var locales: Set<String> = []
|
||||
for (locale, loc) in localizations {
|
||||
guard let loc = loc as? [String: Any] else { continue }
|
||||
// A localization counts if it has a non-empty stringUnit value
|
||||
// or uses variations/substitutions (plural forms).
|
||||
if let unit = loc["stringUnit"] as? [String: Any],
|
||||
let unitValue = unit["value"] as? String, !unitValue.isEmpty {
|
||||
locales.insert(locale)
|
||||
} else if loc["variations"] != nil || loc["substitutions"] != nil {
|
||||
locales.insert(locale)
|
||||
}
|
||||
}
|
||||
coverage[key] = locales
|
||||
}
|
||||
return Catalog(sourceLanguage: sourceLanguage, coverage: coverage)
|
||||
}
|
||||
|
||||
@Test func mainCatalogCoversAllLocalesForEveryKey() throws {
|
||||
let catalog = try Self.loadCatalog("bitchat/Localizable.xcstrings")
|
||||
let expected = catalog.allLocales
|
||||
#expect(expected.count > 1, "catalog should declare more locales than the source language")
|
||||
for (key, locales) in catalog.coverage.sorted(by: { $0.key < $1.key }) {
|
||||
let missing = expected.subtracting(locales).sorted()
|
||||
#expect(missing.isEmpty, "\(key) is missing locales: \(missing.joined(separator: ", "))")
|
||||
}
|
||||
}
|
||||
|
||||
@Test func shareExtensionCatalogCoversAllLocalesForEveryKey() throws {
|
||||
let catalog = try Self.loadCatalog("bitchatShareExtension/Localization/Localizable.xcstrings")
|
||||
let expected = catalog.allLocales
|
||||
for (key, locales) in catalog.coverage.sorted(by: { $0.key < $1.key }) {
|
||||
let missing = expected.subtracting(locales).sorted()
|
||||
#expect(missing.isEmpty, "\(key) is missing locales: \(missing.joined(separator: ", "))")
|
||||
}
|
||||
}
|
||||
|
||||
@Test func shareExtensionSupportsSameLocalesAsMainApp() throws {
|
||||
let main = try Self.loadCatalog("bitchat/Localizable.xcstrings")
|
||||
let shareExt = try Self.loadCatalog("bitchatShareExtension/Localization/Localizable.xcstrings")
|
||||
let missing = main.allLocales.subtracting(shareExt.allLocales).sorted()
|
||||
#expect(missing.isEmpty, "share extension is missing locales: \(missing.joined(separator: ", "))")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user