Spanish for the 79 strings the 0.32 features added

`Scripts/check-spanish-catalog.py` reported 1466 keys in code against 1391 in
the catalog. The 79 without Spanish were all from the release branch: greeting
cards, events, games, the Today tab, the small-web directory, story
take-downs, and the two scanner refusal lines added by this merge. It now
reports 0 missing.

Terms follow docs/i18n/glossary-es.md and, where Android already shipped the
same label, Android's wording: Today is "Hoy", Card "Tarjeta", Event "Evento",
Game "Juego", story "historia", take down "retirar", directory "directorio",
bridge line "línea del puente". Every entry is `needs_review`, like the rest
of the es column: nothing ships until the owner or a Localization Lab
volunteer has read it.

The old parental-controls footer is deleted. The merge replaced it with the
longer text that also covers Today and the directory, so its key was dead.
"Attach photo" and "Search contacts" are still reported unused, as on main:
both reach the catalog through a `String` parameter the compiler does not
record, and "Search contacts" has three live call sites.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Paul Hitt
2026-10-14 11:02:23 -04:00
co-authored by Claude Opus 5
parent 21c23852c8
commit b3c47c9926
12 changed files with 9476 additions and 8613 deletions
+1 -1
View File
@@ -1,3 +1,3 @@
[submodule "Packages/Obfs4"]
path = Packages/Obfs4
url = https://gitlab.com/extant2000/flamenet-elligator2.git
url = https://git.extant2000.com/extant2000/flamenet-elligator2.git
File diff suppressed because it is too large Load Diff
+13 -1
View File
@@ -46,6 +46,16 @@ enum LinkPreviewFetcher {
/// `fetchUnlessBridged`, which refuses while a bridge is saved (§24.6).
/// This file is also compiled into flamenet-e2e's interop driver, which
/// has no bridge code, so the check lives beside it, not in it.
/// Stand-in protocol handlers, for a test that watches whether this fetch
/// happens at all.
///
/// The session below is `.ephemeral` on purpose, so a preview fetch
/// carries no cookies, cache or credentials to the linked site. A session
/// built from a configuration does not see `URLProtocol.registerClass`,
/// which registers only for the shared session, so without a seam here a
/// test cannot observe the fetch either way. Nil in the app.
nonisolated(unsafe) static var protocolClasses: [AnyClass]?
static func fetch(_ url: URL, timeout: TimeInterval = 5) async -> LinkPreview? {
var request = URLRequest(url: url)
request.timeoutInterval = timeout
@@ -54,7 +64,9 @@ enum LinkPreviewFetcher {
// site that wants to refuse these should be able to.
request.setValue("FlamenetLinkPreview/1.0", forHTTPHeaderField: "User-Agent")
guard let (data, response) = try? await URLSession(configuration: .ephemeral).data(for: request),
let configuration = URLSessionConfiguration.ephemeral
if let protocolClasses { configuration.protocolClasses = protocolClasses }
guard let (data, response) = try? await URLSession(configuration: configuration).data(for: request),
let http = response as? HTTPURLResponse, (200..<300).contains(http.statusCode)
else { return nil }
+4 -1
View File
@@ -200,7 +200,10 @@ extension ScannerViewController: AVCaptureMetadataOutputObjectsDelegate {
extension ScannerViewController {
struct Representable: UIViewControllerRepresentable {
let onFound: (String) -> Void
let onRefused: () -> Void
/// Called for a code `accepts` turns down, so a screen can say so.
/// Defaults to nothing: a sheet that has no notice to show need not
/// say so at every call site.
var onRefused: () -> Void = {}
let onDenied: () -> Void
var accepts: (String) -> Bool = { ScannedCode.card(from: $0) != nil }
@@ -458,6 +458,7 @@ struct BridgeLineScanner: View {
let onFound: (String) -> Void
@Environment(\.dismiss) private var dismiss
@State private var denied = false
@State private var refused = false
var body: some View {
NavigationStack {
@@ -476,10 +477,29 @@ struct BridgeLineScanner: View {
dismiss()
onFound(text.trimmingCharacters(in: .whitespacesAndNewlines))
},
onRefused: { refused = true },
onDenied: { denied = true },
accepts: { Obfs4Settings.parse($0) != nil }
)
.ignoresSafeArea()
.overlay(alignment: .bottom) {
// A code that is not a bridge line gets a plain answer
// and scanning carries on, rather than the camera
// seeming not to see it.
Text(
refused
? "That code isn't a bridge line."
: "Point at the code holding the bridge line."
)
.font(.caption)
.padding(.horizontal, 12).padding(.vertical, 8)
.background(
.ultraThinMaterial,
in: RoundedRectangle(cornerRadius: Theme.Radius.small)
)
.padding(.bottom, 28)
.accessibilityIdentifier("obfs4.scan.hint")
}
}
}
.navigationTitle("Scan a bridge")
+13 -6
View File
@@ -87,9 +87,16 @@ final class ASnoozedChatStopsAskingTests: XCTestCase {
}
/// Listen to both ends of the notification path, with the conversations
/// a test uses unmuted, so a mute left behind cannot pass for a snooze.
@MainActor private func listen(unmuting buddies: [Int]) -> Notices {
for id in buddies { NotificationManager.shared.setMuted(false, buddyID: id) }
/// a test uses unmuted and accepted, so neither a mute left behind nor
/// section 14.5's request gate can pass for a snooze. A sender this
/// account has not accepted is a Request, which is a quiet badge and never
/// a notification, so without `accept` every check here would pass on an
/// app that posts nothing at all.
@MainActor private func listen(on inbox: E2EInbox, unmuting buddies: [Int]) -> Notices {
for id in buddies {
NotificationManager.shared.setMuted(false, buddyID: id)
inbox.accept(id)
}
let notices = Notices()
NotificationManager.testObserver = { key, body, _ in notices.asked.append((key, body)) }
NotificationManager.testPostedObserver = { key, body, _ in
@@ -109,7 +116,7 @@ final class ASnoozedChatStopsAskingTests: XCTestCase {
reset(aliceStore, userID: 3711)
reset(bobStore, userID: 3712)
}
let notices = await listen(unmuting: [3711])
let notices = await listen(on: bob, unmuting: [3711])
// The control: before the snooze a message is posted, or every check
// below could pass on a host that posts nothing at all.
@@ -174,7 +181,7 @@ final class ASnoozedChatStopsAskingTests: XCTestCase {
reset(aliceStore, userID: 3713)
reset(bobStore, userID: 3714)
}
let notices = await listen(unmuting: [3713])
let notices = await listen(on: bob, unmuting: [3713])
await MainActor.run { bob.snooze(3713, seconds: 86_400) }
try await alice.send("first", to: 3714)
await bob.pollOnce()
@@ -196,7 +203,7 @@ final class ASnoozedChatStopsAskingTests: XCTestCase {
reset(bobStore, userID: 3716)
reset(carolStore, userID: 3717)
}
let notices = await listen(unmuting: [3715, 3717])
let notices = await listen(on: bob, unmuting: [3715, 3717])
await MainActor.run { bob.snooze(3715, seconds: 3_600) }
let group = try await alice.createGroup(name: "Book club", memberIDs: [3716])
+12
View File
@@ -100,9 +100,21 @@ final class DirectoryShareTests: XCTestCase {
NoFetching.seen = []
URLProtocol.registerClass(NoFetching.self)
defer { URLProtocol.unregisterClass(NoFetching.self) }
// The preview fetch runs on its own ephemeral session, which does not
// see a globally registered protocol, so it is handed the class.
LinkPreviewFetcher.protocolClasses = [NoFetching.self]
defer { LinkPreviewFetcher.protocolClasses = nil }
let saved = UserDefaults.standard.object(forKey: SettingsKeys.linkPreviews)
UserDefaults.standard.set(true, forKey: SettingsKeys.linkPreviews)
defer { UserDefaults.standard.set(saved, forKey: SettingsKeys.linkPreviews) }
// Previews are off while a bridge is on (§24.6), and the bridge list is
// process-wide UserDefaults that another suite can leave set. Without
// this the control at the end of the test fetches nothing and reads as
// a preview bug.
let bridges = Obfs4Settings.lines.map(\.line)
for line in bridges { Obfs4Settings.remove(line) }
defer { for line in bridges { _ = Obfs4Settings.add(line) } }
XCTAssertFalse(AroundTheBridge.active, "a bridge is on, so nothing will be fetched")
let (alice, aliceStore) = try await inbox(userID: 9301)
let (bob, bobStore) = try await inbox(userID: 9302)
+12 -4
View File
@@ -33,10 +33,18 @@ final class EveryScannerBindsByFnidTests: XCTestCase {
while let path = walk?.nextObject() as? String {
guard path.hasSuffix(".swift") else { continue }
let body = try String(contentsOf: sources.appendingPathComponent(path), encoding: .utf8)
var from = body.startIndex
while let found = body.range(of: "CardScanner {", range: from..<body.endIndex) {
sites.append(((path as NSString).lastPathComponent, String(body[found.lowerBound...].prefix(240))))
from = found.upperBound
// Both call shapes: `CardScanner { }` and, where a site also
// takes a contact link, `CardScanner(onFound:onLink:)`.
for opener in ["CardScanner {", "CardScanner("] {
var from = body.startIndex
while let found = body.range(of: opener, range: from..<body.endIndex) {
let site = String(body[found.lowerBound...].prefix(240))
// The declaration itself is not a call site.
if !site.hasPrefix("CardScanner: View") {
sites.append(((path as NSString).lastPathComponent, site))
}
from = found.upperBound
}
}
}
XCTAssertGreaterThanOrEqual(sites.count, 2, "the scan found too few scanners to prove anything")
+6 -1
View File
@@ -288,6 +288,11 @@ final class HelloLinkTests: XCTestCase {
let types = try XCTUnwrap(
Bundle.main.object(forInfoDictionaryKey: "CFBundleURLTypes") as? [[String: Any]])
let schemes = types.flatMap { $0["CFBundleURLSchemes"] as? [String] ?? [] }
XCTAssertEqual(schemes, [HelloLink.scheme])
// Two schemes, two URL types: `flamenet` for contact links (§8.14) and
// `flamenetmessenger` for hello links (§6.34). Both are registered, so
// asserting on one alone would have to delete the other.
XCTAssertTrue(schemes.contains(HelloLink.scheme), "the hello scheme is not registered")
XCTAssertTrue(schemes.contains("flamenet"), "the contact-link scheme is not registered")
XCTAssertEqual(Set(schemes).count, schemes.count, "a scheme is registered twice")
}
}
+4 -1
View File
@@ -21,7 +21,10 @@ final class SupervisionHidesTodayTests: XCTestCase {
let tabs = try source("Sources/Views/MainTabView.swift")
XCTAssertTrue(tabs.contains("case 7 where !ParentalControls.todayHidden:"), "no guard on the screen")
XCTAssertTrue(tabs.contains("todayHidden: Bool = ParentalControls.todayHidden"))
XCTAssertTrue(tabs.contains("todayHidden ? nil : .init(id: 7"))
// Whitespace-squeezed: swift-format decides where this ternary wraps,
// and the guard is what matters, not the line breaks.
let squeezed = tabs.split(whereSeparator: \.isWhitespace).joined(separator: " ")
XCTAssertTrue(squeezed.contains("todayHidden ? nil : .init( id: 7"), "no guard on the tab")
}
/// The directory is reached from Today and from nowhere else, so hiding
+4 -1
View File
@@ -52,7 +52,10 @@ final class SpanishScreensUITests: XCTestCase {
private func es(_ english: String) -> String { spanish[english] ?? english }
private func launch(spanish: Bool, reset: Bool) -> XCUIApplication {
let app = XCUIApplication()
// `underTest()`, not a bare XCUIApplication: it sets
// FNMSG_TEST_LAUNCH, which is what keeps a test launch off a
// production relay. UITestSafetyTests gates this.
let app = XCUIApplication.underTest()
app.launchArguments += ["-FNMSGServer", relay]
if reset { app.launchArguments += ["-FNMSGResetState"] }
app.launchArguments += spanish
+10
View File
@@ -172,12 +172,22 @@ targets:
# `cards/<design>.png` and `directory/directory-v1.json`. A group copies
# every file to the bundle root, where `love.png` the card and a sticker
# of the same name would collide.
#
# `group:` names a group OTHER than Resources on purpose. Put in a group
# whose own path is `Resources`, xcodegen 2.45.2 writes the reference as
# `path = stickers; sourceTree = SOURCE_ROOT`, which resolves against the
# project root and not the group, so the build fails with "The file
# 'stickers' couldn't be opened". Outside that group it writes
# `path = Resources/stickers`, which is correct.
- path: Resources/stickers
type: folder
group: Bundle folders
- path: Resources/cards
type: folder
group: Bundle folders
- path: Resources/directory
type: folder
group: Bundle folders
dependencies:
- package: WebRTCBinary
product: WebRTC