mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-08-29 07:27:16 +00:00
Localize the last hardcoded UI strings; wire up two dead translations (#1656)
* Add 26 code-referenced keys to the catalog + a test that closes the gap
LocalizationCoverageTests validated the catalog but never the code: a
String(localized:) whose key is missing from every catalog compiles
fine and silently ships its English defaultValue to all 29 non-source
locales. That blind spot let 26 keys go untranslated while CI stayed
green: the entire notices/board composer (10), the private-media
encryption warnings and delivery-failure reasons (9), both
courier-header strings, two delivery states, two media failure
reasons, and the [? people] channel row.
- All 26 keys added with full 30-locale coverage.
- New everyCodeReferencedKeyExistsInACatalog scans the source for
literal String(localized:) keys and fails on any key absent from
both catalogs. (The audit's original count of 17 was itself an
undercount — its scan missed multi-line String( localized: calls;
the test's regex does not.)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Localize CommandProcessor: all 48 command strings, 30 locales
CommandProcessor had zero String(localized:) calls — every command
error, usage hint, and the entire /help reference was hardcoded
English. The autocomplete panel (CommandInfo) is fully localized, so a
non-English speaker got localized suggestions while typing and English
the moment anything went wrong or they asked for help.
- 47 literal sites converted to String(localized:)/String(format:)
with positional specifiers where languages reorder (%1$@, %2$lld).
- /trace's "hop"/"hops" pluralization split into path_one/path_many;
the "you" chain label localized too.
- helpText and groupUsage become computed so they resolve per-locale.
- 49 command.* keys added with full 30-locale coverage (command
syntax stays verbatim; only prose translates).
- Copy fix from the audit: "blocked X. you will no longer receive
messages from them" → "…no longer see their messages" (blocking
filters at display time; packets still arrive and relay).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Localize the last hardcoded UI strings; wire up two dead translations
The stragglers from the unlocalized-strings audit:
- Nearby push notification: "bitchatters nearby!" title and the
hand-rolled English pluralization ("1 person around" / "N people
around") now localize via body_one/body_many.
- Voice recording errors (mic permission, start/too-short/save
failures) and the "Recording Error" alert title — also brought into
the lowercase house voice.
- The recording HUD's Cancel label now uses common.cancel.
- "No image selected" in both image pickers.
- The DM screenshot echo "you took a screenshot" (the wire-format
sibling stays English for Android compat).
- "Voice/Images are only available in mesh chats." had fully
translated catalog entries in all 30 locales that were never looked
up — the call sites passed the raw string instead of resolving the
key. Wired up via String(localized:).
10 new keys x 30 locales.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: jack <jackjackbits@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
5c4b814c56
commit
9b84b36122
File diff suppressed because it is too large
Load Diff
@ -260,8 +260,10 @@ final class NotificationService {
|
||||
}
|
||||
|
||||
func sendNetworkAvailableNotification(peerCount: Int) {
|
||||
let title = "👥 bitchatters nearby!"
|
||||
let body = peerCount == 1 ? "1 person around" : "\(peerCount) people around"
|
||||
let title = String(localized: "notification.nearby.title", defaultValue: "👥 bitchatters nearby!", comment: "Title of the local notification when mesh peers come into range")
|
||||
let body = peerCount == 1
|
||||
? String(localized: "notification.nearby.body_one", defaultValue: "1 person around", comment: "Body of the nearby notification for exactly one peer")
|
||||
: String(format: String(localized: "notification.nearby.body_many", defaultValue: "%lld people around", comment: "Body of the nearby notification; placeholder is the peer count"), locale: .current, peerCount)
|
||||
// Fixed identifier so iOS updates the existing notification instead of creating new ones
|
||||
let identifier = "network-available"
|
||||
|
||||
|
||||
@ -304,7 +304,7 @@ private extension ChatLifecycleCoordinator {
|
||||
func appendPrivateScreenshotNotice(for peerID: PeerID) {
|
||||
let notice = BitchatMessage(
|
||||
sender: "system",
|
||||
content: "you took a screenshot",
|
||||
content: String(localized: "system.screenshot.you", defaultValue: "you took a screenshot", comment: "Local system line after your screenshot notice was sent to the DM peer"),
|
||||
timestamp: Date(),
|
||||
isRelay: false,
|
||||
originalSender: nil,
|
||||
|
||||
@ -551,7 +551,7 @@ final class ChatMediaTransferCoordinator {
|
||||
guard context.canSendMediaInCurrentContext else {
|
||||
SecureLogger.info("Voice note blocked outside mesh/private context", category: .session)
|
||||
try? FileManager.default.removeItem(at: url)
|
||||
context.addSystemMessage("Voice notes are only available in mesh chats.")
|
||||
context.addSystemMessage(String(localized: "Voice notes are only available in mesh chats.", comment: "System message when a voice note is attempted outside mesh chats"))
|
||||
return
|
||||
}
|
||||
|
||||
@ -694,7 +694,7 @@ final class ChatMediaTransferCoordinator {
|
||||
guard context.canSendMediaInCurrentContext else {
|
||||
SecureLogger.info("Image send blocked outside mesh/private context", category: .session)
|
||||
cleanup?()
|
||||
context.addSystemMessage("Images are only available in mesh chats.")
|
||||
context.addSystemMessage(String(localized: "Images are only available in mesh chats.", comment: "System message when an image send is attempted outside mesh chats"))
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@ -29,7 +29,7 @@ final class VoiceRecordingViewModel: ObservableObject {
|
||||
var alertMessage: String {
|
||||
switch self {
|
||||
case .error(let message): message
|
||||
case .permissionDenied: "Microphone access is required to record voice notes."
|
||||
case .permissionDenied: String(localized: "voice.error.mic_permission", defaultValue: "microphone access is required to record voice notes.", comment: "Alert message when the microphone permission is denied")
|
||||
case .idle, .requestingPermission, .preparing, .recording: ""
|
||||
}
|
||||
}
|
||||
@ -156,7 +156,7 @@ final class VoiceRecordingViewModel: ObservableObject {
|
||||
}
|
||||
}
|
||||
activeSession = nil
|
||||
state = .error(message: "Could not start recording.")
|
||||
state = .error(message: String(localized: "voice.error.start_failed", defaultValue: "could not start recording.", comment: "Alert message when the recorder fails to start"))
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -202,8 +202,8 @@ final class VoiceRecordingViewModel: ObservableObject {
|
||||
guard state == .idle else { return }
|
||||
state = .error(
|
||||
message: finalDuration < VoiceRecorder.minRecordingDuration
|
||||
? "Recording is too short."
|
||||
: "Recording failed to save."
|
||||
? String(localized: "voice.error.too_short", defaultValue: "recording is too short.", comment: "Alert message when a voice note is released too quickly to save")
|
||||
: String(localized: "voice.error.save_failed", defaultValue: "recording failed to save.", comment: "Alert message when a finished voice note cannot be saved")
|
||||
)
|
||||
return
|
||||
}
|
||||
@ -212,8 +212,8 @@ final class VoiceRecordingViewModel: ObservableObject {
|
||||
guard generation == holdGeneration, state == .idle else { return }
|
||||
state = .error(
|
||||
message: finalDuration < VoiceRecorder.minRecordingDuration
|
||||
? "Recording is too short."
|
||||
: "Recording failed to save."
|
||||
? String(localized: "voice.error.too_short", defaultValue: "recording is too short.", comment: "Alert message when a voice note is released too quickly to save")
|
||||
: String(localized: "voice.error.save_failed", defaultValue: "recording failed to save.", comment: "Alert message when a finished voice note cannot be saved")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -238,7 +238,7 @@ private extension ContentComposerView {
|
||||
}
|
||||
Spacer()
|
||||
Button(action: voiceRecordingVM.cancel) {
|
||||
Label("Cancel", systemImage: "xmark.circle")
|
||||
Label(String(localized: "common.cancel", comment: "Cancel action in the voice recording HUD"), systemImage: "xmark.circle")
|
||||
.labelStyle(.iconOnly)
|
||||
.font(.bitchatSystem(size: 18))
|
||||
.foregroundColor(.red)
|
||||
|
||||
@ -282,7 +282,7 @@ struct ContentPeopleSheetView: View {
|
||||
}
|
||||
}
|
||||
#endif
|
||||
.alert("Recording Error", isPresented: voiceAlertBinding, actions: {
|
||||
.alert(Text(String(localized: "voice.error.title", defaultValue: "recording error", comment: "Title of the voice recording error alert")), isPresented: voiceAlertBinding, actions: {
|
||||
Button("common.ok", role: .cancel) {}
|
||||
if voiceRecordingVM.state == .permissionDenied {
|
||||
Button("location_channels.action.open_settings") {
|
||||
|
||||
@ -397,7 +397,7 @@ struct ContentView: View {
|
||||
ImagePreviewView(url: url)
|
||||
}
|
||||
}
|
||||
.alert("Recording Error", isPresented: rootVoiceAlertBinding, actions: {
|
||||
.alert(Text(String(localized: "voice.error.title", defaultValue: "recording error", comment: "Title of the voice recording error alert")), isPresented: rootVoiceAlertBinding, actions: {
|
||||
Button("common.ok", role: .cancel) {}
|
||||
if voiceRecordingVM.state == .permissionDenied {
|
||||
Button("location_channels.action.open_settings") {
|
||||
|
||||
@ -64,7 +64,7 @@ struct ImagePickerView: UIViewControllerRepresentable {
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
} else {
|
||||
Text("No image selected")
|
||||
Text(String(localized: "image_picker.none_selected", defaultValue: "no image selected", comment: "Placeholder shown in the image picker before a selection"))
|
||||
}
|
||||
Button("Show") { isPresented = true }
|
||||
}
|
||||
|
||||
@ -63,7 +63,7 @@ struct MacImagePickerView: View {
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
} else {
|
||||
Text("No image selected")
|
||||
Text(String(localized: "image_picker.none_selected", defaultValue: "no image selected", comment: "Placeholder shown in the image picker before a selection"))
|
||||
}
|
||||
Button("Show") { isPresented = true }
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user