diff --git a/apple/VniDrop/Resources/Localizable.xcstrings b/apple/VniDrop/Resources/Localizable.xcstrings index b34e4e2..8a0a4ba 100644 --- a/apple/VniDrop/Resources/Localizable.xcstrings +++ b/apple/VniDrop/Resources/Localizable.xcstrings @@ -1,2578 +1,3062 @@ { - "sourceLanguage" : "en", - "strings" : { - "%@" : { - + "sourceLanguage": "en", + "strings": { + "%@": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual" }, - "%@ · %@" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "%1$@ · %2$@" + "%@ %@ · %@": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "%1$@ %2$@ · %3$@" } } } }, - "%@ %@ · %@" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "%1$@ %2$@ · %3$@" + "%@ · %@": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "%1$@ · %2$@" } } } }, - "%@%%" : { - + "%@%%": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual" }, - "about_bug_report" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Report a bug" + "about_bug_report": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Report a bug" } } } }, - "about_description" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "VniDrop moves files and folders straight from one device to another over the network, without uploading them to any file-hosting service. There’s no account to create, and no copy of your transfer is left in the cloud once you’re done." + "about_description": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "VniDrop moves files and folders straight from one device to another over the network, without uploading them to any file-hosting service. There’s no account to create, and no copy of your transfer is left in the cloud once you’re done." } } } }, - "about_is_title" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "What VniDrop is" + "about_is_direct": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "A direct device-to-device transfer — your files go straight to the receiver." } } } }, - "about_is_direct" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "A direct device-to-device transfer — your files go straight to the receiver." + "about_is_encrypted": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Connections are authenticated and end-to-end encrypted (via Iroh), and incoming files are verified by their content hash." } } } }, - "about_is_no_account" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Account-free — there’s nothing to sign up for." + "about_is_in_control": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "You decide who receives: approve each request, or open a transfer to anyone holding the invitation." } } } }, - "about_is_in_control" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "You decide who receives: approve each request, or open a transfer to anyone holding the invitation." + "about_is_no_account": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Account-free — there’s nothing to sign up for." } } } }, - "about_is_encrypted" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Connections are authenticated and end-to-end encrypted (via Iroh), and incoming files are verified by their content hash." + "about_is_open": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Open source, released under the Apache 2.0 license." } } } }, - "about_is_open" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Open source, released under the Apache 2.0 license." + "about_is_title": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "What VniDrop is" } } } }, - "about_isnt_title" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "What VniDrop isn’t" + "about_isnt_cloud": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Not cloud storage — no server holds your files, and nothing waits in the cloud after a transfer." } } } }, - "about_isnt_cloud" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Not cloud storage — no server holds your files, and nothing waits in the cloud after a transfer." + "about_isnt_public": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Not public broadcasting — an invitation is a private access link, not an announcement to everyone nearby." } } } }, - "about_isnt_sync" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Not a sync or backup service." + "about_isnt_sync": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Not a sync or backup service." } } } }, - "about_isnt_public" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Not public broadcasting — an invitation is a private access link, not an announcement to everyone nearby." + "about_isnt_title": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "What VniDrop isn’t" } } } }, - "about_privacy_title" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Privacy & security" + "about_license_label": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "License" } } } }, - "about_privacy_capability" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Invitations are capabilities. Treat a QR code, NFC tag, or .vnd file like a private access link and share it only with the people you intend — especially with “Anyone with this transfer.”" + "about_privacy": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Privacy policy" } } } }, - "about_privacy_deny" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Deny by default — VniDrop serves only the content of an active share and rejects unknown requests." + "about_privacy_capability": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Invitations are capabilities. Treat a QR code, NFC tag, or .vnd file like a private access link and share it only with the people you intend — especially with “Anyone with this transfer.”" } } } }, - "about_privacy_relay" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "If two devices can’t connect directly, the encrypted connection may be relayed. Relays forward encrypted packets only; they never store your files." + "about_privacy_deny": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Deny by default — VniDrop serves only the content of an active share and rejects unknown requests." } } } }, - "about_privacy_local" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Received files are saved on your device and never silently overwrite an existing file." + "about_privacy_local": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Received files are saved on your device and never silently overwrite an existing file." } } } }, - "about_license_label" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "License" + "about_privacy_policy_label": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Privacy policy" } } } }, - "about_privacy_policy_label" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Privacy policy" + "about_privacy_relay": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "If two devices can’t connect directly, the encrypted connection may be relayed. Relays forward encrypted packets only; they never store your files." } } } }, - "about_tagline" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Send files directly. Stay in control of who receives them." + "about_privacy_title": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Privacy & security" } } } }, - "about_privacy" : { - "extractionState" : "stale", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Privacy policy" + "about_tagline": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Send files directly. Stay in control of who receives them." } } } }, - "about_title" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "About" + "about_title": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "About" } } } }, - "appearance_auto_description" : { - "extractionState" : "stale", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Match this device’s light or dark appearance." + "appearance_auto_description": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Match this device’s light or dark appearance." } } } }, - "appearance_dark_mode" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Dark mode" + "appearance_dark_mode": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Dark mode" } } } }, - "appearance_light_mode" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Light mode" + "appearance_light_mode": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Light mode" } } } }, - "appearance_system_mode" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "System" + "appearance_system_mode": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "System" } } } }, - "appearance_title" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Appearance" + "appearance_title": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Appearance" } } } }, - "approval_connection_request" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Receive request" + "approval_connection_request": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Receive request" } } } }, - "approval_endpoint_id" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Device ID: %1$@" + "approval_endpoint_id": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Device ID: %1$@" } } } }, - "approval_nearby_device" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "A nearby device" + "approval_nearby_device": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "A nearby device" } } } }, - "approval_pending_count" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$d requests waiting" + "approval_pending_count": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "%1$d requests waiting" } } } }, - "approval_request_body" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$@ wants to receive “%2$@”." + "approval_request_body": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "%1$@ wants to receive “%2$@”." } } } }, - "battery_level_title" : { - "extractionState" : "stale", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Battery level" + "battery_level_title": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Battery level" } } } }, - "bug_report_contact_hint" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "name@example.com" + "bug_report_contact_hint": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "name@example.com" } } } }, - "bug_report_contact_label" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Contact email (optional)" + "bug_report_contact_label": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Contact email (optional)" } } } }, - "bug_report_expected_hint" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Describe what you expected to happen" + "bug_report_description": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Tell us what went wrong. We attach device info and optional recent logs (with sensitive values redacted)." } } } }, - "bug_report_steps_hint" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "List the steps to reproduce the issue" + "bug_report_device_section": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Device information" } } } }, - "bug_report_what_hint" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Describe what happened" + "bug_report_expected_hint": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Describe what you expected to happen" } } } }, - "bug_report_description" : { - "extractionState" : "stale", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Tell us what went wrong. We attach device info and optional recent logs (with sensitive values redacted)." + "bug_report_expected_label": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "What did you expect?" } } } }, - "bug_report_device_section" : { - "extractionState" : "stale", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Device information" + "bug_report_include_logs": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Include recent logs" } } } }, - "bug_report_expected_label" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "What did you expect?" + "bug_report_include_logs_description": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Helps us diagnose the issue. Sensitive values are redacted before sending." } } } }, - "bug_report_include_logs" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Include recent logs" + "bug_report_logs_size": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Log attachment size" } } } }, - "bug_report_include_logs_description" : { - "extractionState" : "stale", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Helps us diagnose the issue. Sensitive values are redacted before sending." + "bug_report_missing_expected": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Please describe what you expected." } } } }, - "bug_report_logs_size" : { - "extractionState" : "stale", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Log attachment size" + "bug_report_missing_what": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Please describe what happened." } } } }, - "bug_report_missing_expected" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Please describe what you expected." + "bug_report_steps_hint": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "List the steps to reproduce the issue" } } } }, - "bug_report_missing_what" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Please describe what happened." + "bug_report_steps_label": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Steps to reproduce (optional)" } } } }, - "bug_report_steps_label" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Steps to reproduce (optional)" + "bug_report_submit": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Submit report" } } } }, - "bug_report_submit" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Submit report" + "bug_report_submit_failed": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Could not submit the bug report. Try again later." } } } }, - "bug_report_submit_failed" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Could not submit the bug report. Try again later." + "bug_report_submitted": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Thanks — your bug report was recorded." } } } }, - "bug_report_submitted" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Thanks — your bug report was recorded." + "bug_report_submitting": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Submitting…" } } } }, - "bug_report_submitting" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Submitting…" + "bug_report_what_hint": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Describe what happened" } } } }, - "bug_report_what_label" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "What happened?" + "bug_report_what_label": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "What happened?" } } } }, - "button_approve" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Approve" + "button_approve": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Approve" } } } }, - "button_back" : { - "extractionState" : "stale", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Back" + "button_back": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Back" } } } }, - "button_cancel" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Cancel" + "button_cancel": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Cancel" } } } }, - "button_cancel_receive" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Cancel" + "button_cancel_receive": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Cancel" } } } }, - "button_change_files" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Change files" + "button_change_files": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Change files" } } } }, - "button_choose_files" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Choose files" + "button_choose_files": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Choose files" } } } }, - "button_choose_folder" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Choose folder" + "button_choose_folder": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Choose folder" } } } }, - "button_clear" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Clear" + "button_clear": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Clear" } } } }, - "button_close" : { - "extractionState" : "stale", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Close" + "button_close": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Close" } } } }, - "button_create_new_transfer" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "New transfer" + "button_create_new_transfer": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "New transfer" } } } }, - "button_delete_transfer" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Delete transfer" + "button_delete_transfer": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Delete transfer" } } } }, - "button_download_invitation" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Save .vnd file" + "button_download_invitation": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Save .vnd file" } } } }, - "button_native_share" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Share invitation" + "button_native_share": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Share invitation" } } } }, - "button_open_settings" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Open Settings" + "button_open_settings": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Open Settings" } } } }, - "button_receive" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Receive" + "button_receive": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Receive" } } } }, - "button_receive_files" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Start receiving" + "button_receive_files": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Start receiving" } } } }, - "button_refuse" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Refuse" + "button_refuse": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Refuse" } } } }, - "button_remove_file" : { - "extractionState" : "stale", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Remove file" + "button_remove_file": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Remove file" } } } }, - "button_reset_default" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Use default" + "button_reset_default": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Use default" } } } }, - "button_retry" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Retry" + "button_retry": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Retry" } } } }, - "button_share_file" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Start sharing" + "button_share_file": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Start sharing" } } } }, - "button_sharing_file" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Preparing transfer…" + "button_sharing_file": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Preparing transfer…" } } } }, - "button_show_in_files" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Show in Files" + "button_show_in_files": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Show in Files" } } } }, - "button_write_nfc" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Write to NFC tag" + "button_write_nfc": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Write to NFC tag" } } } }, - "device_model_title" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Device model" + "device_model_title": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Device model" } } } }, - "device_name_title" : { - "extractionState" : "stale", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Device name" + "device_name_title": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Device name" } } } }, - "diagnostics_description" : { - "extractionState" : "stale", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Send anonymous crash reports and usage events so we can improve VniDrop. You can turn this off anytime. Invitations, file paths, and transfer contents are never included." + "diagnostics_description": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Send anonymous crash reports and usage events so we can improve VniDrop. You can turn this off anytime. Invitations, file paths, and transfer contents are never included." } } } }, - "diagnostics_disabled_message" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Diagnostics sharing is off." + "diagnostics_disabled_message": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Diagnostics sharing is off." } } } }, - "diagnostics_enabled_message" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Diagnostics sharing is on." + "diagnostics_enabled_message": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Diagnostics sharing is on." } } } }, - "diagnostics_title" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Share diagnostics" + "diagnostics_title": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Share diagnostics" } } } }, - "error_camera" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Camera access is required to scan a QR code." + "error_camera": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Camera access is required to scan a QR code." } } } }, - "error_device_info" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Could not load device information." + "error_device_info": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Could not load device information." } } } }, - "error_filesystem" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "VniDrop could not access the selected files or folder. Check permissions and try again." + "error_filesystem": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "VniDrop could not access the selected files or folder. Check permissions and try again." } } } }, - "error_generic" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Something went wrong. Try again." + "error_generic": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Something went wrong. Try again." } } } }, - "error_initialization" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "VniDrop could not finish starting up. Close the app and try again." + "error_initialization": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "VniDrop could not finish starting up. Close the app and try again." } } } }, - "error_invalid_ticket" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "This invitation could not be read. Ask the sender for a new one." + "error_invalid_ticket": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "This invitation could not be read. Ask the sender for a new one." } } } }, - "error_invitation_empty" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "That invitation is empty. Try opening it again." + "error_invitation_empty": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "That invitation is empty. Try opening it again." } } } }, - "error_missing_native_library" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "The native VniDrop library is missing from this build." + "error_missing_native_library": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "The native VniDrop library is missing from this build." } } } }, - "error_nfc" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "This NFC tag could not be used. Try another tag." + "error_nfc": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "This NFC tag could not be used. Try another tag." } } } }, - "error_permission" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "The sender has not approved this transfer, or it was refused." + "error_permission": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "The sender has not approved this transfer, or it was refused." } } } }, - "error_repository" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "VniDrop could not save transfer data on this device." + "error_repository": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "VniDrop could not save transfer data on this device." } } } }, - "error_selection_failed" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Could not open the selected item. Try choosing it again." + "error_selection_failed": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Could not open the selected item. Try choosing it again." } } } }, - "error_share_empty" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Select at least one item to share." + "error_share_empty": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Select at least one item to share." } } } }, - "error_socket_bind" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "VniDrop could not open its network sockets on this device." + "error_socket_bind": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "VniDrop could not open its network sockets on this device." } } } }, - "error_starting_up" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "VniDrop is still starting. Open the invitation again in a moment." + "error_starting_up": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "VniDrop is still starting. Open the invitation again in a moment." } } } }, - "error_transfer" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "The transfer could not be completed. Check your connection and try again." + "error_transfer": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "The transfer could not be completed. Check your connection and try again." } } } }, - "field_receiver_name" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Receiver name" + "field_receiver_name": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Receiver name" } } } }, - "field_sender_name" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Sender name" + "field_sender_name": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Sender name" } } } }, - "field_transfer_name" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Transfer name" + "field_transfer_name": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Transfer name" } } } }, - "field_username" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Display name" + "field_username": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Display name" } } } }, - "folder_status_permission_required" : { - "extractionState" : "stale", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Permission needed" + "folder_status_permission_required": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Permission needed" } } } }, - "folder_status_unavailable" : { - "extractionState" : "stale", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Unavailable" + "folder_status_unavailable": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Unavailable" } } } }, - "folder_status_validating" : { - "extractionState" : "stale", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Checking folder…" + "folder_status_validating": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Checking folder…" } } } }, - "folder_status_writable" : { - "extractionState" : "stale", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ready" + "folder_status_writable": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Ready" } } } }, - "metadata_files" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Files" + "metadata_files": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Files" } } } }, - "metadata_size" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Size" + "metadata_size": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Size" } } } }, - "metadata_status" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Status" + "metadata_status": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Status" } } } }, - "nav_receive" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Receive" + "nav_receive": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Receive" } } } }, - "nav_send" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Send" + "nav_send": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Send" } } } }, - "nav_settings" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Settings" + "nav_settings": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Settings" } } } }, - "network_title" : { - "extractionState" : "stale", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Network" + "network_title": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Network" } } } }, - "notifications_description" : { - "extractionState" : "stale", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Get notified about new receive requests while VniDrop is in the background." + "notifications_description": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Get notified about new receive requests while VniDrop is in the background." } } } }, - "notifications_enabled_message" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Notifications enabled." + "notifications_enabled_message": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Notifications enabled." } } } }, - "notifications_local_title" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Allow notifications" + "notifications_local_title": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Allow notifications" } } } }, - "notifications_permission_denied" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Notifications are turned off for VniDrop. You can enable them in Settings." + "notifications_permission_denied": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Notifications are turned off for VniDrop. You can enable them in Settings." } } } }, - "notifications_settings_open_failed" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Could not open notification settings." + "notifications_settings_open_failed": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Could not open notification settings." } } } }, - "notifications_title" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Notifications" + "notifications_title": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Notifications" } } } }, - "notifications_unsupported" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Notifications are not available on this device." + "notifications_unsupported": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Notifications are not available on this device." } } } }, - "os_version_title" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Operating system" + "os_version_title": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Operating system" } } } }, - "preferences_receive_folder_title" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Save received transfers to" + "preferences_receive_folder_title": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Save received transfers to" } } } }, - "preferences_title" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Preferences" + "preferences_title": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Preferences" } } } }, - "progress_cancelled" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Cancelled" + "progress_cancelled": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Cancelled" } } } }, - "progress_completed" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Completed" + "progress_completed": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Completed" } } } }, - "progress_connected" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Connected" + "progress_connected": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Connected" } } } }, - "progress_connecting" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Connecting" + "progress_connecting": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Connecting" } } } }, - "progress_downloading" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Downloading" + "progress_downloading": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Downloading" } } } }, - "progress_failed" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Failed" + "progress_failed": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Failed" } } } }, - "progress_getting_ready" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Getting ready" + "progress_getting_ready": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Getting ready" } } } }, - "progress_interrupted" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Transfer interrupted" + "progress_interrupted": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Transfer interrupted" } } } }, - "progress_preparing" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Preparing" + "progress_preparing": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Preparing" } } } }, - "progress_ready" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ready" + "progress_ready": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Ready" } } } }, - "progress_receiving" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Receiving" + "progress_receiving": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Receiving" } } } }, - "progress_requesting_access" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Requesting access" + "progress_requesting_access": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Requesting access" } } } }, - "progress_saving" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Saving" + "progress_saving": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Saving" } } } }, - "progress_sending" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Sending" + "progress_sending": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Sending" } } } }, - "progress_sending_to_count" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Sending to %lld" + "progress_sending_to_count": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Sending to %lld" } } } }, - "progress_share_ready" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ready to share" + "progress_share_ready": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Ready to share" } } } }, - "progress_working" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Working…" + "progress_working": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Working…" } } } }, - "receive_choose_method_body" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Choose the invitation method available to you." + "receive_choose_method_body": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Choose the invitation method available to you." } } } }, - "receive_choose_method_title" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "How would you like to connect?" + "receive_choose_method_title": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "How would you like to connect?" } } } }, - "receive_clear_history" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Clear history" + "receive_clear_history": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Clear history" } } } }, - "receive_clear_history_description" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "All completed, failed, and cancelled receives will be removed from VniDrop’s history. Downloaded files will remain on this device." + "receive_clear_history_description": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "All completed, failed, and cancelled receives will be removed from VniDrop’s history. Downloaded files will remain on this device." } } } }, - "receive_clear_history_title" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Clear receive history?" + "receive_clear_history_title": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Clear receive history?" } } } }, - "receive_completed" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Transfer received." + "receive_completed": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Transfer received." } } } }, - "receive_delete_history_description" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "“%1$@” will be removed from VniDrop’s history. The downloaded file will remain on this device." + "receive_delete_history_description": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "“%1$@” will be removed from VniDrop’s history. The downloaded file will remain on this device." } } } }, - "receive_delete_history_item" : { - "extractionState" : "stale", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Delete from receive history" + "receive_delete_history_item": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Delete from receive history" } } } }, - "receive_delete_history_title" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Remove from history?" + "receive_delete_history_title": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Remove from history?" } } } }, - "receive_empty_body" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Open a VniDrop invitation, scan a QR code, or hold near an NFC tag." + "receive_empty_body": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Open a VniDrop invitation, scan a QR code, or hold near an NFC tag." } } } }, - "receive_empty_title" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Nothing received yet" + "receive_empty_title": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Nothing received yet" } } } }, - "receive_history_cleared" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Receive history cleared." + "receive_history_cleared": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Receive history cleared." } } } }, - "receive_history_title" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "History" + "receive_history_title": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "History" } } } }, - "receive_method_file" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Open a .vnd invitation" + "receive_method_file": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Open a .vnd invitation" } } } }, - "receive_method_file_description" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Choose an invitation saved or shared to this device." + "receive_method_file_description": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Choose an invitation saved or shared to this device." } } } }, - "receive_method_nfc" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Read NFC tag" + "receive_method_nfc": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Read NFC tag" } } } }, - "receive_method_nfc_description" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Hold this device near the sender’s invitation tag." + "receive_method_nfc_description": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Hold this device near the sender’s invitation tag." } } } }, - "receive_method_scan" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Scan QR code" + "receive_method_scan": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Scan QR code" } } } }, - "receive_method_scan_description" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Use the camera to scan the sender’s VniDrop code." + "receive_method_scan_description": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Use the camera to scan the sender’s VniDrop code." } } } }, - "receive_new_subtitle" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Transfers you’ve received on this device." + "receive_new_subtitle": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Transfers you’ve received on this device." } } } }, - "receive_nfc_waiting" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Hold near the NFC tag…" + "receive_nfc_waiting": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Hold near the NFC tag…" } } } }, - "receive_open_files_failed" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Couldn’t open VniDrop in Files." + "receive_open_files_failed": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Couldn’t open VniDrop in Files." } } } }, - "receive_review_title" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Review transfer" + "receive_review_title": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Review transfer" } } } }, - "receive_title" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Receive" + "receive_title": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Receive" } } } }, - "receive_unknown_transfer" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "VniDrop transfer" + "receive_unknown_transfer": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "VniDrop transfer" } } } }, - "send_access_anyone" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Anyone with this transfer" + "send_access_anyone": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Anyone with this transfer" } } } }, - "send_access_anyone_description" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "No approval is required. Only use this for items you are comfortable sharing." + "send_access_anyone_description": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "No approval is required. Only use this for items you are comfortable sharing." } } } }, - "send_access_anyone_warning" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Anyone with the invitation can download until you stop sharing. Do not use this for private or sensitive items." + "send_access_anyone_warning": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Anyone with the invitation can download until you stop sharing. Do not use this for private or sensitive items." } } } }, - "send_access_approval" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ask before each download" + "send_access_approval": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Ask before each download" } } } }, - "send_access_approval_description" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "You approve or refuse every new receiver." + "send_access_approval_description": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "You approve or refuse every new receiver." } } } }, - "send_access_title" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Who can receive it?" + "send_access_title": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Who can receive it?" } } } }, - "send_choose_file_body" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Select files or a folder from this device. You can review the selection before creating the transfer." + "send_choose_file_body": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Select files or a folder from this device. You can review the selection before creating the transfer." } } } }, - "send_choose_file_title" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Choose what to share" + "send_choose_file_title": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Choose what to share" } } } }, - "send_empty_body" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Create a transfer, decide who can receive it, then invite them with a QR code, NFC tag, or invitation file." + "send_empty_body": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Create a transfer, decide who can receive it, then invite them with a QR code, NFC tag, or invitation file." } } } }, - "send_empty_title" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Nothing shared yet" + "send_empty_title": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Nothing shared yet" } } } }, - "send_file_size_unknown" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Size unavailable" + "send_file_size_unknown": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Size unavailable" } } } }, - "send_folder_label" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Folder" + "send_folder_label": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Folder" } } } }, - "send_new_transfer_description" : { - "extractionState" : "stale", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Create a new transfer" + "send_new_transfer_description": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Create a new transfer" } } } }, - "send_new_transfer_title" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "New transfer" + "send_new_transfer_title": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "New transfer" } } } }, - "send_review_title" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Review transfer" + "send_review_title": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Review transfer" } } } }, - "send_selected_files_count" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$d files selected" + "send_selected_files_count": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "%1$d files selected" } } } }, - "send_subtitle" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Transfers you’re sharing from this device." + "send_stop_sharing": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Stop sharing" } } } }, - "send_stop_sharing" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Stop sharing" + "send_stop_sharing_description": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "This stops the transfer and interrupts anyone currently downloading it. It stays in your history as Stopped." } } } }, - "send_stop_sharing_description" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "This stops the transfer and interrupts anyone currently downloading it. It stays in your history as Stopped." + "send_subtitle": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Transfers you’re sharing from this device." } } } }, - "send_title" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Send" + "send_title": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Send" } } } }, - "send_transfer_created" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Transfer created." + "send_transfer_created": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Transfer created." } } } }, - "send_transfer_details_title" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Transfer details" + "send_transfer_details_title": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Transfer details" } } } }, - "send_transfers_title" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Your transfers" + "send_transfers_title": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Your transfers" } } } }, - "settings_subtitle" : { - "extractionState" : "stale", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Your name, where transfers are saved, appearance, and notifications." + "settings_subtitle": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Your name, where transfers are saved, appearance, and notifications." } } } }, - "settings_title" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Settings" + "settings_title": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Settings" } } } }, - "snackbar_dismiss" : { - "extractionState" : "stale", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Dismiss" + "snackbar_dismiss": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Dismiss" } } } }, - "storage_calculating" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Calculating…" + "status_available": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Available" } } } }, - "storage_delete_transfers" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Delete all transfers" + "status_cancelled": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Cancelled" } } } }, - "storage_delete_transfers_description" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "This clears all sent and received transfer records from your history. Your received files are not deleted. Note: the transfer engine keeps its stored content, so Transfer data may not decrease. This can’t be undone." + "status_completed": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Completed" } } } }, - "storage_deleting" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Deleting…" + "status_failed": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Failed" } } } }, - "storage_footer" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Transfer data is managed by the transfer engine — your history plus the content of files you’ve shared. The button below clears your transfer records; the engine keeps its stored content, so this value may not drop. Received files are your downloaded files and are never deleted here." + "status_preparing": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Preparing" } } } }, - "storage_received_files" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Received files" + "status_receiving": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Receiving" } } } }, - "storage_temporary" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Temporary files" + "status_stopped": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Stopped" } } } }, - "storage_title" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Storage" + "storage_calculating": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Calculating…" } } } }, - "storage_total" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Total" + "storage_delete_transfers": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Delete all transfers" } } } }, - "storage_transfer_data" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Transfer data" + "storage_delete_transfers_description": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "This clears all sent and received transfer records from your history. Your received files are not deleted. Note: the transfer engine keeps its stored content, so Transfer data may not decrease. This can’t be undone." } } } }, - "storage_transfers_deleted" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "All transfers deleted" + "storage_deleting": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Deleting…" } } } }, - "status_available" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Available" + "storage_footer": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Transfer data is managed by the transfer engine — your history plus the content of files you’ve shared. The button below clears your transfer records; the engine keeps its stored content, so this value may not drop. Received files are your downloaded files and are never deleted here." } } } }, - "status_cancelled" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Cancelled" + "storage_received_files": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Received files" } } } }, - "status_completed" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Completed" + "storage_temporary": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Temporary files" } } } }, - "status_failed" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Failed" + "storage_title": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Storage" } } } }, - "status_preparing" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Preparing" + "storage_total": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Total" } } } }, - "status_receiving" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Receiving" + "storage_transfer_data": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Transfer data" } } } }, - "status_stopped" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Stopped" + "storage_transfers_deleted": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "All transfers deleted" } } } }, - "transfer_activity_description" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "See important updates for this transfer" + "transfer_activity_description": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "See important updates for this transfer" } } } }, - "transfer_activity_title" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Activity" + "transfer_activity_title": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Activity" } } } }, - "transfer_delete_description" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "“%1$@” will stop being shared and its transfer history will be removed from this device." + "transfer_delete_description": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "“%1$@” will stop being shared and its transfer history will be removed from this device." } } } }, - "transfer_delete_title" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Delete transfer?" + "transfer_delete_title": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Delete transfer?" } } } }, - "transfer_deleted" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Transfer deleted." + "transfer_deleted": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Transfer deleted." } } } }, - "transfer_deleting" : { - "extractionState" : "stale", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Deleting…" + "transfer_deleting": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Deleting…" } } } }, - "transfer_details_title" : { - "extractionState" : "stale", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Transfer details" + "transfer_details_title": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Transfer details" } } } }, - "transfer_event_approved" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Receiver access approved" + "transfer_event_approved": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Receiver access approved" } } } }, - "transfer_event_completed" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "A receiver completed the transfer" + "transfer_event_completed": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "A receiver completed the transfer" } } } }, - "transfer_event_connecting" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Connecting to sender" + "transfer_event_connecting": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Connecting to sender" } } } }, - "transfer_event_downloading" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Downloading" + "transfer_event_downloading": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Downloading" } } } }, - "transfer_event_failed" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "The transfer encountered a problem" + "transfer_event_failed": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "The transfer encountered a problem" } } } }, - "transfer_event_preparing" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Preparing your transfer" + "transfer_event_preparing": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Preparing your transfer" } } } }, - "transfer_event_ready" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ready to share" + "transfer_event_ready": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Ready to share" } } } }, - "transfer_event_refused" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Receiver access refused" + "transfer_event_refused": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Receiver access refused" } } } }, - "transfer_event_requested" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "A receiver requested access" + "transfer_event_requested": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "A receiver requested access" } } } }, - "transfer_event_saving" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Saving" + "transfer_event_saving": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Saving" } } } }, - "transfer_event_stopped" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Sharing stopped" + "transfer_event_stopped": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Sharing stopped" } } } }, - "transfer_event_updated" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Transfer updated" + "transfer_event_updated": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Transfer updated" } } } }, - "transfer_file_count_one" : { - "extractionState" : "stale", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$d file" + "transfer_file_count": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "variations": { + "plural": { + "one": { + "stringUnit": { + "state": "translated", + "value": "%1$d file" + } + }, + "other": { + "stringUnit": { + "state": "translated", + "value": "%1$d files" + } + } + } } } } }, - "transfer_file_count_other" : { - "extractionState" : "stale", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$d files" + "transfer_invitation_saved": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Invitation saved." } } } }, - "transfer_invitation_saved" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Invitation saved." + "transfer_nearby_device": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Nearby device" } } } }, - "transfer_nearby_device" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Nearby device" + "transfer_nfc_unavailable": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "NFC tag writing is not available on this device." } } } }, - "transfer_nfc_unavailable" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "NFC tag writing is not available on this device." + "transfer_nfc_waiting": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Hold your device near a writable NFC tag." } } } }, - "transfer_nfc_waiting" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Hold your device near a writable NFC tag." + "transfer_nfc_written": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Invitation written to the NFC tag." } } } }, - "transfer_nfc_written" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Invitation written to the NFC tag." + "transfer_no_activity": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "There is no activity to show yet." } } } }, - "transfer_no_activity" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "There is no activity to show yet." + "transfer_no_receivers": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Nobody has requested this transfer yet." } } } }, - "transfer_no_receivers" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Nobody has requested this transfer yet." + "transfer_receiver_accepted": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Approved — waiting for completion" } } } }, - "transfer_receiver_accepted" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Approved — waiting for completion" + "transfer_receiver_completed": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Received successfully" } } } }, - "transfer_receiver_completed" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Received successfully" + "transfer_receiver_expired": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Request expired" } } } }, - "transfer_receiver_expired" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Request expired" + "transfer_receiver_refused": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Request refused" } } } }, - "transfer_receiver_refused" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Request refused" + "transfer_receiver_requested": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Waiting for your approval" } } } }, - "transfer_receiver_requested" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Waiting for your approval" + "transfer_receiver_unknown": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Status unavailable" } } } }, - "transfer_receiver_unknown" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Status unavailable" + "transfer_receivers_completed_count": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "%1$d completed" } } } }, - "transfer_receivers_completed_count" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$d completed" + "transfer_receivers_description": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Requests, approvals, and completed deliveries" } } } }, - "transfer_receivers_description" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Requests, approvals, and completed deliveries" + "transfer_receivers_pending": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "%1$d waiting" } } } }, - "transfer_receivers_pending" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$d waiting" + "transfer_receivers_title": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Receivers" } } } }, - "transfer_receivers_title" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Receivers" + "transfer_scan_qr": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Scan with VniDrop to receive this transfer" } } } }, - "transfer_scan_qr" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Scan with VniDrop to receive this transfer" + "transfer_share_description": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "QR code, invitation file, and nearby options" } } } }, - "transfer_share_description" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "QR code, invitation file, and nearby options" + "transfer_share_title": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Share" } } } }, - "transfer_share_title" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Share" + "value_unavailable": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Not available" } } } }, - "value_unavailable" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Not available" - } - } - } - }, - "version_title" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "App version" + "version_title": { + "comment": "TODO: describe where this appears.", + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "App version" } } } } }, - "version" : "1.0" -} \ No newline at end of file + "version": "1.0" +} diff --git a/localization/README.md b/localization/README.md new file mode 100644 index 0000000..d277173 --- /dev/null +++ b/localization/README.md @@ -0,0 +1,87 @@ +# Localization + +Single source of truth for every user-facing string, in [`strings.json`](strings.json). +A Bun CLI generates the platform-native files from it: + +| Target | Output | Notes | +| --- | --- | --- | +| `apple` | `apple/VniDrop/Resources/Localizable.xcstrings` | one catalog, all languages nested | +| `kmp` | `shared/src/commonMain/composeResources/values[-lang]/strings.xml` | one file per language | + +## Workflow + +```bash +cd localization +bun run src/cli.ts validate # structural checks (run before committing) +bun run src/cli.ts generate # regenerate .xcstrings + strings.xml from strings.json +bun run src/cli.ts migrate # one-time: rebuild strings.json from existing platform files +``` + +**Never edit the generated `.xcstrings` / `strings.xml` by hand** — edit `strings.json` and +regenerate. Regenerated output is deterministic (sorted keys), so diffs stay small. + +## `strings.json` format + +```jsonc +{ + "sourceLanguage": "en", + "supportedLanguages": ["en", "fr"], + "strings": { + "send_title": { + "context": "Send tab — screen title.", + "translations": { "en": "Send", "fr": "Envoyer" } + }, + + "send_selected_files_count": { + "context": "Send flow — number of files chosen before creating a transfer.", + "targets": ["kmp", "apple"], + "args": [{ "name": "count", "type": "int" }], + "plural": { + "en": { "one": "{count} file selected", "other": "{count} files selected" }, + "fr": { "one": "{count} fichier sélectionné", "other": "{count} fichiers sélectionnés" } + } + } + } +} +``` + +### Fields + +- **`context`** *(required)* — where the string appears and its purpose. Emitted as the + `.xcstrings` comment and an XML comment; also the note translators see. +- **`targets`** *(optional)* — `["kmp", "apple"]`. Omit to mean **all** targets. +- **`args`** *(optional)* — ordered list of `{ name, type }`, `type` ∈ `string | int | double`. + Referenced in text as `{name}`. +- **`translations`** — flat text per language. **Mutually exclusive with `plural`.** +- **`plural`** — per language, per [CLDR category](https://cldr.unicode.org/index/cldr-spec/plural-rules) + (`zero`, `one`, `two`, `few`, `many`, `other`). `other` is always required. + +### Placeholders + +Write named tokens `{count}`, `{name}` in text. The generator converts them to the right +positional token per platform, using the declared `type`: + +| type | Apple | Android/KMP | +| --- | --- | --- | +| `string` | `%N$@` | `%N$s` | +| `int` | `%N$d` | `%N$d` | +| `double` | `%N$f` | `%N$f` | + +A literal `%` in text is emitted as `%%` whenever the string has args. + +## Adding a language + +Add its code to `supportedLanguages`, fill in `translations` / `plural` for each key, then +`generate`. KMP gets a new `values-/strings.xml`; Apple gets the language inside the +single catalog. `validate` warns about any key still missing that language. + +## Migration notes (from the initial import) + +- Apple keys that were literal English strings (`"%@ · %@"`) were imported verbatim — rename + them to semantic keys and update the Swift call sites. +- Arg names default to `arg1`, `arg2`… (a lone int arg becomes `count`). Rename for clarity; + keep the `{token}` in text in sync. +- Folding `transfer_file_count_one` / `_other` into the plural key `transfer_file_count` + requires switching the KMP call site from `Res.string.transfer_file_count_one` to the + Compose plural API (`pluralStringResource(Res.plurals.transfer_file_count, count, count)`), + and the Apple side to automatic plural inflection. diff --git a/localization/bun.lock b/localization/bun.lock new file mode 100644 index 0000000..6d5f035 --- /dev/null +++ b/localization/bun.lock @@ -0,0 +1,19 @@ +{ + "lockfileVersion": 1, + "configVersion": 1, + "workspaces": { + "": { + "name": "vnidrop-localization", + "devDependencies": { + "bun-types": "^1.3.14", + }, + }, + }, + "packages": { + "@types/node": ["@types/node@26.1.1", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-nxAkRSVkN1Y0JC1W8ky/fTfkGsMmcrRsbx+3XoZE+rMOX71kLYTV7fLXpqud1GpbpP5TuffXFqfX7fH2GgZREw=="], + + "bun-types": ["bun-types@1.3.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ=="], + + "undici-types": ["undici-types@8.3.0", "", {}, "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ=="], + } +} diff --git a/localization/package.json b/localization/package.json new file mode 100644 index 0000000..e78c781 --- /dev/null +++ b/localization/package.json @@ -0,0 +1,18 @@ +{ + "name": "vnidrop-localization", + "version": "0.1.0", + "private": true, + "type": "module", + "description": "Single-source localization for VniDrop: strings.json -> .xcstrings (Apple) + per-language strings.xml (KMP/Compose).", + "bin": { + "loc": "./src/cli.ts" + }, + "scripts": { + "migrate": "bun run src/cli.ts migrate", + "generate": "bun run src/cli.ts generate", + "validate": "bun run src/cli.ts validate" + }, + "devDependencies": { + "bun-types": "^1.3.14" + } +} diff --git a/localization/schema.json b/localization/schema.json new file mode 100644 index 0000000..4217690 --- /dev/null +++ b/localization/schema.json @@ -0,0 +1,72 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "VniDrop localization source", + "type": "object", + "required": ["sourceLanguage", "supportedLanguages", "strings"], + "additionalProperties": false, + "properties": { + "sourceLanguage": { "type": "string", "description": "Default/base language code, e.g. \"en\"." }, + "supportedLanguages": { + "type": "array", + "items": { "type": "string" }, + "minItems": 1, + "uniqueItems": true + }, + "strings": { + "type": "object", + "additionalProperties": { "$ref": "#/definitions/entry" } + } + }, + "definitions": { + "entry": { + "type": "object", + "required": ["context"], + "additionalProperties": false, + "oneOf": [ + { "required": ["translations"] }, + { "required": ["plural"] }, + { "not": { "anyOf": [{ "required": ["translations"] }, { "required": ["plural"] }] } } + ], + "properties": { + "context": { "type": "string" }, + "targets": { + "type": "array", + "items": { "enum": ["kmp", "apple"] }, + "uniqueItems": true + }, + "args": { + "type": "array", + "items": { + "type": "object", + "required": ["name", "type"], + "additionalProperties": false, + "properties": { + "name": { "type": "string", "pattern": "^[A-Za-z_][A-Za-z0-9_]*$" }, + "type": { "enum": ["string", "int", "double"] } + } + } + }, + "translations": { + "type": "object", + "additionalProperties": { "type": "string" } + }, + "plural": { + "type": "object", + "additionalProperties": { + "type": "object", + "required": ["other"], + "additionalProperties": false, + "properties": { + "zero": { "type": "string" }, + "one": { "type": "string" }, + "two": { "type": "string" }, + "few": { "type": "string" }, + "many": { "type": "string" }, + "other": { "type": "string" } + } + } + } + } + } + } +} diff --git a/localization/src/cli.ts b/localization/src/cli.ts new file mode 100644 index 0000000..bded606 --- /dev/null +++ b/localization/src/cli.ts @@ -0,0 +1,21 @@ +#!/usr/bin/env bun +import { migrate } from "./commands/migrate"; +import { generate } from "./commands/generate"; +import { validate } from "./commands/validate"; + +const [cmd] = process.argv.slice(2); + +switch (cmd) { + case "migrate": + await migrate(); + break; + case "generate": + await generate(); + break; + case "validate": + await validate(); + break; + default: + console.error(`Unknown command: ${cmd ?? "(none)"}\nUsage: loc `); + process.exit(1); +} diff --git a/localization/src/commands/generate.ts b/localization/src/commands/generate.ts new file mode 100644 index 0000000..6cff0bd --- /dev/null +++ b/localization/src/commands/generate.ts @@ -0,0 +1,97 @@ +/** + * Generate platform files from strings.json: + * - Apple: one Localizable.xcstrings (all languages nested). + * - KMP: one values[-lang]/strings.xml per supported language. + */ +import { mkdirSync } from "node:fs"; +import { join } from "node:path"; +import { + APPLE_XCSTRINGS, + kmpValuesDir, + KMP_RESOURCES, + STRINGS_JSON, +} from "../config"; +import { renderAndroid, type ParsedAndroid } from "../lib/android-xml"; +import { fromCanonical, type Flavor } from "../lib/placeholders"; +import { renderXcstrings, type XcCatalog, type XcEntry } from "../lib/xcstrings"; +import { targetsOf, type StringEntry, type StringsFile } from "../types"; + +function render(entry: StringEntry, text: string, flavor: Flavor): string { + return fromCanonical(text, entry.args ?? [], flavor); +} + +// ---- Apple ------------------------------------------------------------------ + +function buildXcstrings(doc: StringsFile): XcCatalog { + const strings: Record = {}; + + for (const [key, entry] of Object.entries(doc.strings)) { + if (!targetsOf(entry).includes("apple")) continue; + const xc: XcEntry = { extractionState: "manual" }; + if (entry.context) xc.comment = entry.context; + const localizations: XcEntry["localizations"] = {}; + + for (const lang of doc.supportedLanguages) { + const state = lang === doc.sourceLanguage ? "translated" : "needs_review"; + if (entry.plural) { + const forms = entry.plural[lang]; + if (!forms) continue; + const plural: Record = {}; + for (const [cat, value] of Object.entries(forms)) { + plural[cat] = { stringUnit: { state, value: render(entry, value, "apple") } }; + } + localizations[lang] = { variations: { plural } }; + } else { + const value = entry.translations?.[lang]; + if (value === undefined) continue; + localizations[lang] = { stringUnit: { state, value: render(entry, value, "apple") } }; + } + } + + if (Object.keys(localizations).length) xc.localizations = localizations; + strings[key] = xc; + } + + return { sourceLanguage: doc.sourceLanguage, strings, version: "1.0" }; +} + +// ---- KMP -------------------------------------------------------------------- + +function buildAndroid(doc: StringsFile, lang: string): ParsedAndroid { + const out: ParsedAndroid = { strings: [], plurals: [] }; + + for (const [key, entry] of Object.entries(doc.strings)) { + if (!targetsOf(entry).includes("kmp")) continue; + if (entry.plural) { + const forms = entry.plural[lang]; + if (!forms) continue; + const items: Record = {}; + for (const [cat, value] of Object.entries(forms)) { + items[cat] = render(entry, value, "android"); + } + out.plurals.push({ name: key, items }); + } else { + const value = entry.translations?.[lang]; + if (value === undefined) continue; + out.strings.push({ name: key, value: render(entry, value, "android") }); + } + } + return out; +} + +// ---- entry ------------------------------------------------------------------ + +export async function generate() { + const doc = JSON.parse(await Bun.file(STRINGS_JSON).text()) as StringsFile; + + await Bun.write(APPLE_XCSTRINGS, renderXcstrings(buildXcstrings(doc))); + console.log(`Wrote ${APPLE_XCSTRINGS}`); + + for (const lang of doc.supportedLanguages) { + const dir = join(KMP_RESOURCES, kmpValuesDir(lang, doc.sourceLanguage)); + mkdirSync(dir, { recursive: true }); + const file = join(dir, "strings.xml"); + await Bun.write(file, renderAndroid(buildAndroid(doc, lang))); + console.log(`Wrote ${file}`); + } +} diff --git a/localization/src/commands/migrate.ts b/localization/src/commands/migrate.ts new file mode 100644 index 0000000..a36818d --- /dev/null +++ b/localization/src/commands/migrate.ts @@ -0,0 +1,195 @@ +/** + * One-time migration: fold the existing .xcstrings (Apple) and Compose + * strings.xml (KMP) files into a single strings.json. + * + * - Placeholders are normalized to canonical `{argN}` tokens. + * - KMP `_one` / `_other` key pairs are folded into one plural entry. + * - `targets` is set from which platform(s) a key was found in. + * Arg names (`arg1`, `count`, …) and `context` are best-effort; curate by hand after. + */ +import { readdirSync } from "node:fs"; +import { join } from "node:path"; +import { APPLE_XCSTRINGS, KMP_RESOURCES, STRINGS_JSON } from "../config"; +import { parseAndroid } from "../lib/android-xml"; +import { parseXcstrings, type XcLocalization } from "../lib/xcstrings"; +import { toCanonical, type ParsedString } from "../lib/placeholders"; +import type { Arg, PluralForms, StringEntry, StringsFile, Target } from "../types"; + +const PLURAL_SUFFIXES = ["zero", "one", "two", "few", "many", "other"] as const; + +interface Draft { + targets: Set; + args?: Arg[]; + translations?: Record; + plural?: Record; + comment?: string; +} + +/** Give a friendlier name to a lone integer arg (common for counts). */ +function prettify(parsed: ParsedString): ParsedString { + if (parsed.args.length === 1 && parsed.args[0]!.type === "int") { + return { + text: parsed.text.replaceAll("{arg1}", "{count}"), + args: [{ name: "count", type: "int" }], + }; + } + return parsed; +} + +function ingest(draft: Draft, lang: string, raw: string) { + const p = prettify(toCanonical(raw)); + if (p.args.length) draft.args = p.args; + (draft.translations ??= {})[lang] = p.text; +} + +// ---- Apple (.xcstrings) ----------------------------------------------------- + +function unitValue(loc: XcLocalization | undefined): string | undefined { + return loc?.stringUnit?.value; +} + +async function readApple(drafts: Map) { + const cat = parseXcstrings(await Bun.file(APPLE_XCSTRINGS).text()); + for (const [key, entry] of Object.entries(cat.strings)) { + const draft = get(drafts, key); + draft.targets.add("apple"); + if (entry.comment) draft.comment = entry.comment; + for (const [lang, loc] of Object.entries(entry.localizations ?? {})) { + const plural = loc.variations?.plural; + if (plural) { + for (const [cat, unit] of Object.entries(plural)) { + const p = prettify(toCanonical(unit.stringUnit.value)); + if (p.args.length) draft.args = p.args; + ((draft.plural ??= {})[lang] ??= { other: "" } as PluralForms)[ + cat as keyof PluralForms + ] = p.text; + } + } else { + const v = unitValue(loc); + if (v !== undefined) ingest(draft, lang, v); + } + } + } +} + +// ---- KMP (compose strings.xml) --------------------------------------------- + +function pluralBaseOf(name: string): { base: string; cat: string } | null { + for (const suf of PLURAL_SUFFIXES) { + if (name.endsWith(`_${suf}`)) return { base: name.slice(0, -(suf.length + 1)), cat: suf }; + } + return null; +} + +async function readKmp(drafts: Map, sourceLanguage: string) { + const dirs = readdirSync(KMP_RESOURCES, { withFileTypes: true }) + .filter((d) => d.isDirectory() && (d.name === "values" || d.name.startsWith("values-"))) + .map((d) => d.name); + + for (const dir of dirs) { + const lang = dir === "values" ? sourceLanguage : dir.slice("values-".length); + const file = join(KMP_RESOURCES, dir, "strings.xml"); + if (!(await Bun.file(file).exists())) continue; + + const { strings, plurals } = parseAndroid(await Bun.file(file).text()); + for (const s of strings) { + const draft = get(drafts, s.name); + draft.targets.add("kmp"); + ingest(draft, lang, s.value); + } + // Real blocks (if any) fold directly into the base key. + for (const p of plurals) { + const draft = get(drafts, p.name); + draft.targets.add("kmp"); + for (const [cat, raw] of Object.entries(p.items)) { + const parsed = prettify(toCanonical(raw)); + if (parsed.args.length) draft.args = parsed.args; + ((draft.plural ??= {})[lang] ??= { other: "" } as PluralForms)[ + cat as keyof PluralForms + ] = parsed.text; + } + } + } +} + +/** + * Global fold: collapse flat `_` sibling keys (e.g. `_one`/`_other`) + * into one plural entry on ``, regardless of which platform they came from. + * Requires `other` plus at least one more category to avoid folding non-plurals. + */ +function foldPlurals(drafts: Map) { + const groups = new Map(); + for (const key of drafts.keys()) { + const pl = pluralBaseOf(key); + if (pl && drafts.get(key)!.translations) { + (groups.get(pl.base) ?? groups.set(pl.base, []).get(pl.base)!).push(key); + } + } + + for (const [base, keys] of groups) { + const cats = new Set(keys.map((k) => pluralBaseOf(k)!.cat)); + if (!cats.has("other") || cats.size < 2) continue; + + const target = get(drafts, base); + for (const key of keys) { + const cat = pluralBaseOf(key)!.cat as keyof PluralForms; + const src = drafts.get(key)!; + src.targets.forEach((t) => target.targets.add(t)); + if (src.args?.length) target.args = src.args; + target.comment ??= src.comment; + for (const [lang, text] of Object.entries(src.translations ?? {})) { + ((target.plural ??= {})[lang] ??= { other: "" } as PluralForms)[cat] = text; + } + drafts.delete(key); + } + } +} + +// ---- assembly --------------------------------------------------------------- + +function get(drafts: Map, key: string): Draft { + let d = drafts.get(key); + if (!d) drafts.set(key, (d = { targets: new Set() })); + return d; +} + +export async function migrate(sourceLanguage = "en") { + const drafts = new Map(); + await readApple(drafts); + await readKmp(drafts, sourceLanguage); + foldPlurals(drafts); + + const langs = new Set([sourceLanguage]); + const strings: Record = {}; + + for (const key of [...drafts.keys()].sort()) { + const d = drafts.get(key)!; + const entry: StringEntry = { context: d.comment ?? "TODO: describe where this appears." }; + + const targets = [...d.targets].sort() as Target[]; + if (targets.length && targets.length < 2) entry.targets = targets; + if (d.args?.length) entry.args = d.args; + + if (d.plural) { + entry.plural = d.plural; + Object.keys(d.plural).forEach((l) => langs.add(l)); + } else if (d.translations) { + entry.translations = d.translations; + Object.keys(d.translations).forEach((l) => langs.add(l)); + } + strings[key] = entry; + } + + const out: StringsFile = { + sourceLanguage, + supportedLanguages: [...langs].sort(), + strings, + }; + await Bun.write( + STRINGS_JSON, + JSON.stringify({ $schema: "./schema.json", ...out }, null, 2) + "\n", + ); + console.log( + `Wrote ${STRINGS_JSON}\n ${Object.keys(strings).length} keys, languages: ${[...langs].sort().join(", ")}`, + ); +} diff --git a/localization/src/commands/validate.ts b/localization/src/commands/validate.ts new file mode 100644 index 0000000..4f20ae8 --- /dev/null +++ b/localization/src/commands/validate.ts @@ -0,0 +1,55 @@ +/** Structural checks on strings.json. Exits non-zero on errors (warnings don't fail). */ +import { STRINGS_JSON } from "../config"; +import type { StringsFile } from "../types"; + +const TOKEN = /\{([A-Za-z_][A-Za-z0-9_]*)\}/g; + +export async function validate() { + const doc = JSON.parse(await Bun.file(STRINGS_JSON).text()) as StringsFile; + const errors: string[] = []; + const warnings: string[] = []; + + for (const [key, entry] of Object.entries(doc.strings)) { + const where = `"${key}"`; + const argNames = new Set((entry.args ?? []).map((a) => a.name)); + + if (entry.translations && entry.plural) { + errors.push(`${where}: has both "translations" and "plural" (pick one).`); + } + if (!entry.translations && !entry.plural) { + warnings.push(`${where}: no translations yet.`); + } + if (!entry.context?.trim() || entry.context.startsWith("TODO")) { + warnings.push(`${where}: missing/placeholder context.`); + } + + const texts: string[] = []; + if (entry.translations) texts.push(...Object.values(entry.translations)); + if (entry.plural) { + for (const [lang, forms] of Object.entries(entry.plural)) { + if (!forms.other?.trim()) errors.push(`${where} [${lang}]: plural missing required "other".`); + texts.push(...Object.values(forms)); + } + } + + for (const text of texts) { + for (const m of text.matchAll(TOKEN)) { + if (!argNames.has(m[1]!)) { + errors.push(`${where}: uses {${m[1]}} but no matching arg is declared.`); + } + } + } + + // Missing-language coverage (warn only). + for (const lang of doc.supportedLanguages) { + const present = entry.plural ? !!entry.plural[lang] : entry.translations?.[lang] !== undefined; + const anyTranslations = entry.translations || entry.plural; + if (anyTranslations && !present) warnings.push(`${where}: missing "${lang}".`); + } + } + + for (const w of warnings) console.warn(`warn ${w}`); + for (const e of errors) console.error(`error ${e}`); + console.log(`\n${Object.keys(doc.strings).length} keys — ${errors.length} error(s), ${warnings.length} warning(s).`); + if (errors.length) process.exit(1); +} diff --git a/localization/src/config.ts b/localization/src/config.ts new file mode 100644 index 0000000..8b9a6fd --- /dev/null +++ b/localization/src/config.ts @@ -0,0 +1,24 @@ +import { dirname, join, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; + +/** Repo root = parent of the localization/ directory. */ +export const REPO_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..", ".."); +export const LOC_DIR = join(REPO_ROOT, "localization"); +export const STRINGS_JSON = join(LOC_DIR, "strings.json"); + +/** Apple String Catalog — one file, all languages nested. */ +export const APPLE_XCSTRINGS = join( + REPO_ROOT, + "apple/VniDrop/Resources/Localizable.xcstrings", +); + +/** KMP / Compose Multiplatform resources root; one values[-lang]/strings.xml per language. */ +export const KMP_RESOURCES = join( + REPO_ROOT, + "shared/src/commonMain/composeResources", +); + +/** Directory holding a language's strings.xml under the compose resources root. */ +export function kmpValuesDir(lang: string, sourceLanguage: string): string { + return lang === sourceLanguage ? "values" : `values-${lang}`; +} diff --git a/localization/src/lib/android-xml.ts b/localization/src/lib/android-xml.ts new file mode 100644 index 0000000..ebe80de --- /dev/null +++ b/localization/src/lib/android-xml.ts @@ -0,0 +1,96 @@ +/** + * Read/write Compose Multiplatform (Android-style) strings.xml. + * The existing file is flat and machine-friendly, so we use a targeted parser + * rather than a full DOM. Handles and with XML entities and + * Android backslash escapes. + */ + +export interface AndroidString { + name: string; + value: string; +} +export interface AndroidPlural { + name: string; + items: Record; // quantity -> value +} + +export interface ParsedAndroid { + strings: AndroidString[]; + plurals: AndroidPlural[]; +} + +function decodeEntities(s: string): string { + return s + .replace(/&#x([0-9a-fA-F]+);/g, (_m, h) => String.fromCodePoint(parseInt(h, 16))) + .replace(/&#(\d+);/g, (_m, d) => String.fromCodePoint(parseInt(d, 10))) + .replaceAll("<", "<") + .replaceAll(">", ">") + .replaceAll(""", '"') + .replaceAll("'", "'") + .replaceAll("&", "&"); +} + +/** Reverse Android's leading/trailing-quote + backslash escapes into plain text. */ +function unescapeAndroid(raw: string): string { + let s = raw.trim(); + if (s.length >= 2 && s.startsWith('"') && s.endsWith('"')) { + s = s.slice(1, -1); + } + s = s + .replace(/\\n/g, "\n") + .replace(/\\t/g, "\t") + .replace(/\\'/g, "'") + .replace(/\\"/g, '"') + .replace(/\\@/g, "@") + .replace(/\\\\/g, "\\"); + return decodeEntities(s); +} + +export function parseAndroid(xml: string): ParsedAndroid { + const strings: AndroidString[] = []; + const plurals: AndroidPlural[] = []; + + const stringRe = /([\s\S]*?)<\/string>/g; + for (const m of xml.matchAll(stringRe)) { + strings.push({ name: m[1]!, value: unescapeAndroid(m[2]!) }); + } + + const pluralRe = /([\s\S]*?)<\/plurals>/g; + const itemRe = /([\s\S]*?)<\/item>/g; + for (const m of xml.matchAll(pluralRe)) { + const items: Record = {}; + for (const it of m[2]!.matchAll(itemRe)) { + items[it[1]!] = unescapeAndroid(it[2]!); + } + plurals.push({ name: m[1]!, items }); + } + + return { strings, plurals }; +} + +function escapeXmlText(s: string): string { + return s.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">"); +} + +/** Android needs apostrophes and quotes escaped, and quotes on leading/trailing whitespace. */ +function escapeAndroidValue(s: string): string { + let out = escapeXmlText(s).replace(/'/g, "\\'").replace(/\n/g, "\\n"); + if (out !== out.trim()) out = `"${out}"`; + return out; +} + +export function renderAndroid(parsed: ParsedAndroid): string { + const lines: string[] = ['', ""]; + for (const s of parsed.strings) { + lines.push(` ${escapeAndroidValue(s.value)}`); + } + for (const p of parsed.plurals) { + lines.push(` `); + for (const [q, v] of Object.entries(p.items)) { + lines.push(` ${escapeAndroidValue(v)}`); + } + lines.push(" "); + } + lines.push("", ""); + return lines.join("\n"); +} diff --git a/localization/src/lib/placeholders.ts b/localization/src/lib/placeholders.ts new file mode 100644 index 0000000..e68e7f6 --- /dev/null +++ b/localization/src/lib/placeholders.ts @@ -0,0 +1,98 @@ +/** + * Canonical placeholder handling. + * + * Canonical form (in strings.json) uses named tokens: `{count}`, `{name}`. + * Platform output uses positional printf tokens, resolved from the entry's + * ordered `args`: + * - Apple (.xcstrings): string -> %N$@ int -> %N$d double -> %N$f + * - Android (.xml): string -> %N$s int -> %N$d double -> %N$f + * A literal percent is written `%%` on both platforms whenever the string + * carries args (printf-active); canonical text stores it as a bare `%`. + */ +import type { Arg, ArgType } from "../types"; + +export type Flavor = "apple" | "android"; + +function stringToken(flavor: Flavor): string { + return flavor === "apple" ? "@" : "s"; +} + +function tokenForType(type: ArgType, flavor: Flavor): string { + switch (type) { + case "string": + return stringToken(flavor); + case "int": + return "d"; + case "double": + return "f"; + } +} + +/** Render canonical `{name}` text into a platform printf string. */ +export function fromCanonical( + text: string, + args: Arg[], + flavor: Flavor, +): string { + const hasArgs = args.length > 0; + const index = new Map(args.map((a, i) => [a.name, { i: i + 1, type: a.type }])); + + // Escape literal % first (only meaningful when printf-active). + let out = hasArgs ? text.replaceAll("%", "%%") : text; + + out = out.replace(/\{([A-Za-z_][A-Za-z0-9_]*)\}/g, (_whole, name: string) => { + const found = index.get(name); + if (!found) { + throw new Error( + `placeholder {${name}} has no matching arg (${args.map((a) => a.name).join(", ") || "none"})`, + ); + } + return `%${found.i}$${tokenForType(found.type, flavor)}`; + }); + + return out; +} + +export interface ParsedString { + /** Canonical text with `{argN}` tokens. */ + text: string; + /** Args discovered, ordered by first positional index. */ + args: Arg[]; +} + +// Matches an escaped literal `%%` or a printf specifier (positional or bare). +const CONV = /%%|%(?:(\d+)\$)?([@sdif])/g; + +function typeForConv(conv: string): ArgType { + if (conv === "@" || conv === "s") return "string"; + if (conv === "d" || conv === "i") return "int"; + return "double"; // f +} + +/** + * Parse a platform printf string back into canonical `{argN}` form, inferring + * arg names (`arg1`, `arg2`, ...) and types from the conversion specifiers. + * Handles positional (`%1$@`), bare (`%@`), and escaped literals (`%%`). + */ +export function toCanonical(raw: string): ParsedString { + const byIndex = new Map(); + let auto = 0; + + const text = raw.replace(CONV, (whole, pos: string | undefined, conv: string) => { + if (whole === "%%") return "%"; // literal percent, not a specifier + const idx = pos ? Number(pos) : ++auto; + const type = typeForConv(conv); + const existing = byIndex.get(idx); + if (existing && existing !== type) { + throw new Error(`arg ${idx} used with conflicting types in "${raw}"`); + } + byIndex.set(idx, type); + return `{arg${idx}}`; + }); + + const args: Arg[] = [...byIndex.keys()] + .sort((a, b) => a - b) + .map((idx) => ({ name: `arg${idx}`, type: byIndex.get(idx)! })); + + return { text, args }; +} diff --git a/localization/src/lib/xcstrings.ts b/localization/src/lib/xcstrings.ts new file mode 100644 index 0000000..dfd3eff --- /dev/null +++ b/localization/src/lib/xcstrings.ts @@ -0,0 +1,45 @@ +/** Read/write Apple String Catalog (.xcstrings) — a single JSON file, all languages nested. */ + +export type XcState = "new" | "translated" | "needs_review" | "stale"; + +export interface XcStringUnit { + state: XcState; + value: string; +} +export interface XcVariations { + plural?: Record; +} +export interface XcLocalization { + stringUnit?: XcStringUnit; + variations?: XcVariations; +} +export interface XcEntry { + comment?: string; + extractionState?: string; + localizations?: Record; +} +export interface XcCatalog { + sourceLanguage: string; + strings: Record; + version: string; +} + +export function parseXcstrings(json: string): XcCatalog { + return JSON.parse(json) as XcCatalog; +} + +/** Stable, Xcode-compatible serialization: 2-space indent, keys sorted, trailing newline. */ +export function renderXcstrings(cat: XcCatalog): string { + const sortDeep = (v: unknown): unknown => { + if (Array.isArray(v)) return v.map(sortDeep); + if (v && typeof v === "object") { + const out: Record = {}; + for (const k of Object.keys(v as Record).sort()) { + out[k] = sortDeep((v as Record)[k]); + } + return out; + } + return v; + }; + return JSON.stringify(sortDeep(cat), null, 2) + "\n"; +} diff --git a/localization/src/types.ts b/localization/src/types.ts new file mode 100644 index 0000000..086522a --- /dev/null +++ b/localization/src/types.ts @@ -0,0 +1,43 @@ +/** Canonical localization data model. See ../README.md for the full spec. */ + +/** Output targets a string can be emitted to. */ +export type Target = "kmp" | "apple"; +export const ALL_TARGETS: Target[] = ["kmp", "apple"]; + +/** Argument types, used to pick the right platform placeholder token. */ +export type ArgType = "string" | "int" | "double"; + +export interface Arg { + /** Name referenced inside translations as `{name}`. */ + name: string; + type: ArgType; +} + +/** CLDR plural categories. `other` is always required when `plural` is used. */ +export type PluralCategory = "zero" | "one" | "two" | "few" | "many" | "other"; +export type PluralForms = Partial> & { + other: string; +}; + +export interface StringEntry { + /** Where the string appears / its purpose. Emitted as xcstrings comment + xml comment. */ + context: string; + /** Defaults to all targets when omitted. */ + targets?: Target[]; + /** Ordered positional args referenced as `{name}` in translations. */ + args?: Arg[]; + /** Flat (non-plural) translations, keyed by language. Mutually exclusive with `plural`. */ + translations?: Record; + /** Plural translations, keyed by language then CLDR category. Mutually exclusive with `translations`. */ + plural?: Record; +} + +export interface StringsFile { + sourceLanguage: string; + supportedLanguages: string[]; + strings: Record; +} + +export function targetsOf(entry: StringEntry): Target[] { + return entry.targets ?? ALL_TARGETS; +} diff --git a/localization/strings.json b/localization/strings.json new file mode 100644 index 0000000..f3d3a68 --- /dev/null +++ b/localization/strings.json @@ -0,0 +1,1741 @@ +{ + "$schema": "./schema.json", + "sourceLanguage": "en", + "supportedLanguages": [ + "en" + ], + "strings": { + "%@": { + "context": "TODO: describe where this appears.", + "targets": [ + "apple" + ] + }, + "%@ %@ · %@": { + "context": "TODO: describe where this appears.", + "targets": [ + "apple" + ], + "args": [ + { + "name": "arg1", + "type": "string" + }, + { + "name": "arg2", + "type": "string" + }, + { + "name": "arg3", + "type": "string" + } + ], + "translations": { + "en": "{arg1} {arg2} · {arg3}" + } + }, + "%@ · %@": { + "context": "TODO: describe where this appears.", + "targets": [ + "apple" + ], + "args": [ + { + "name": "arg1", + "type": "string" + }, + { + "name": "arg2", + "type": "string" + } + ], + "translations": { + "en": "{arg1} · {arg2}" + } + }, + "%@%%": { + "context": "TODO: describe where this appears.", + "targets": [ + "apple" + ] + }, + "about_bug_report": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "Report a bug" + } + }, + "about_description": { + "context": "TODO: describe where this appears.", + "targets": [ + "apple" + ], + "translations": { + "en": "VniDrop moves files and folders straight from one device to another over the network, without uploading them to any file-hosting service. There’s no account to create, and no copy of your transfer is left in the cloud once you’re done." + } + }, + "about_is_direct": { + "context": "TODO: describe where this appears.", + "targets": [ + "apple" + ], + "translations": { + "en": "A direct device-to-device transfer — your files go straight to the receiver." + } + }, + "about_is_encrypted": { + "context": "TODO: describe where this appears.", + "targets": [ + "apple" + ], + "translations": { + "en": "Connections are authenticated and end-to-end encrypted (via Iroh), and incoming files are verified by their content hash." + } + }, + "about_is_in_control": { + "context": "TODO: describe where this appears.", + "targets": [ + "apple" + ], + "translations": { + "en": "You decide who receives: approve each request, or open a transfer to anyone holding the invitation." + } + }, + "about_is_no_account": { + "context": "TODO: describe where this appears.", + "targets": [ + "apple" + ], + "translations": { + "en": "Account-free — there’s nothing to sign up for." + } + }, + "about_is_open": { + "context": "TODO: describe where this appears.", + "targets": [ + "apple" + ], + "translations": { + "en": "Open source, released under the Apache 2.0 license." + } + }, + "about_is_title": { + "context": "TODO: describe where this appears.", + "targets": [ + "apple" + ], + "translations": { + "en": "What VniDrop is" + } + }, + "about_isnt_cloud": { + "context": "TODO: describe where this appears.", + "targets": [ + "apple" + ], + "translations": { + "en": "Not cloud storage — no server holds your files, and nothing waits in the cloud after a transfer." + } + }, + "about_isnt_public": { + "context": "TODO: describe where this appears.", + "targets": [ + "apple" + ], + "translations": { + "en": "Not public broadcasting — an invitation is a private access link, not an announcement to everyone nearby." + } + }, + "about_isnt_sync": { + "context": "TODO: describe where this appears.", + "targets": [ + "apple" + ], + "translations": { + "en": "Not a sync or backup service." + } + }, + "about_isnt_title": { + "context": "TODO: describe where this appears.", + "targets": [ + "apple" + ], + "translations": { + "en": "What VniDrop isn’t" + } + }, + "about_license_label": { + "context": "TODO: describe where this appears.", + "targets": [ + "apple" + ], + "translations": { + "en": "License" + } + }, + "about_privacy": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "Privacy policy" + } + }, + "about_privacy_capability": { + "context": "TODO: describe where this appears.", + "targets": [ + "apple" + ], + "translations": { + "en": "Invitations are capabilities. Treat a QR code, NFC tag, or .vnd file like a private access link and share it only with the people you intend — especially with “Anyone with this transfer.”" + } + }, + "about_privacy_deny": { + "context": "TODO: describe where this appears.", + "targets": [ + "apple" + ], + "translations": { + "en": "Deny by default — VniDrop serves only the content of an active share and rejects unknown requests." + } + }, + "about_privacy_local": { + "context": "TODO: describe where this appears.", + "targets": [ + "apple" + ], + "translations": { + "en": "Received files are saved on your device and never silently overwrite an existing file." + } + }, + "about_privacy_policy_label": { + "context": "TODO: describe where this appears.", + "targets": [ + "apple" + ], + "translations": { + "en": "Privacy policy" + } + }, + "about_privacy_relay": { + "context": "TODO: describe where this appears.", + "targets": [ + "apple" + ], + "translations": { + "en": "If two devices can’t connect directly, the encrypted connection may be relayed. Relays forward encrypted packets only; they never store your files." + } + }, + "about_privacy_title": { + "context": "TODO: describe where this appears.", + "targets": [ + "apple" + ], + "translations": { + "en": "Privacy & security" + } + }, + "about_tagline": { + "context": "TODO: describe where this appears.", + "targets": [ + "apple" + ], + "translations": { + "en": "Send files directly. Stay in control of who receives them." + } + }, + "about_title": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "About" + } + }, + "appearance_auto_description": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "Match this device’s light or dark appearance." + } + }, + "appearance_dark_mode": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "Dark mode" + } + }, + "appearance_light_mode": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "Light mode" + } + }, + "appearance_system_mode": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "System" + } + }, + "appearance_title": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "Appearance" + } + }, + "approval_connection_request": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "Receive request" + } + }, + "approval_endpoint_id": { + "context": "TODO: describe where this appears.", + "args": [ + { + "name": "arg1", + "type": "string" + } + ], + "translations": { + "en": "Device ID: {arg1}" + } + }, + "approval_nearby_device": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "A nearby device" + } + }, + "approval_pending_count": { + "context": "TODO: describe where this appears.", + "args": [ + { + "name": "count", + "type": "int" + } + ], + "translations": { + "en": "{count} requests waiting" + } + }, + "approval_request_body": { + "context": "TODO: describe where this appears.", + "args": [ + { + "name": "arg1", + "type": "string" + }, + { + "name": "arg2", + "type": "string" + } + ], + "translations": { + "en": "{arg1} wants to receive “{arg2}”." + } + }, + "battery_level_title": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "Battery level" + } + }, + "bug_report_contact_hint": { + "context": "TODO: describe where this appears.", + "targets": [ + "apple" + ], + "translations": { + "en": "name@example.com" + } + }, + "bug_report_contact_label": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "Contact email (optional)" + } + }, + "bug_report_description": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "Tell us what went wrong. We attach device info and optional recent logs (with sensitive values redacted)." + } + }, + "bug_report_device_section": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "Device information" + } + }, + "bug_report_expected_hint": { + "context": "TODO: describe where this appears.", + "targets": [ + "apple" + ], + "translations": { + "en": "Describe what you expected to happen" + } + }, + "bug_report_expected_label": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "What did you expect?" + } + }, + "bug_report_include_logs": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "Include recent logs" + } + }, + "bug_report_include_logs_description": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "Helps us diagnose the issue. Sensitive values are redacted before sending." + } + }, + "bug_report_logs_size": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "Log attachment size" + } + }, + "bug_report_missing_expected": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "Please describe what you expected." + } + }, + "bug_report_missing_what": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "Please describe what happened." + } + }, + "bug_report_steps_hint": { + "context": "TODO: describe where this appears.", + "targets": [ + "apple" + ], + "translations": { + "en": "List the steps to reproduce the issue" + } + }, + "bug_report_steps_label": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "Steps to reproduce (optional)" + } + }, + "bug_report_submit": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "Submit report" + } + }, + "bug_report_submit_failed": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "Could not submit the bug report. Try again later." + } + }, + "bug_report_submitted": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "Thanks — your bug report was recorded." + } + }, + "bug_report_submitting": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "Submitting…" + } + }, + "bug_report_what_hint": { + "context": "TODO: describe where this appears.", + "targets": [ + "apple" + ], + "translations": { + "en": "Describe what happened" + } + }, + "bug_report_what_label": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "What happened?" + } + }, + "button_approve": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "Approve" + } + }, + "button_back": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "Back" + } + }, + "button_cancel": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "Cancel" + } + }, + "button_cancel_receive": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "Cancel" + } + }, + "button_change_files": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "Change files" + } + }, + "button_choose_files": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "Choose files" + } + }, + "button_choose_folder": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "Choose folder" + } + }, + "button_clear": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "Clear" + } + }, + "button_close": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "Close" + } + }, + "button_create_new_transfer": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "New transfer" + } + }, + "button_delete_transfer": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "Delete transfer" + } + }, + "button_download_invitation": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "Save .vnd file" + } + }, + "button_native_share": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "Share invitation" + } + }, + "button_open_settings": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "Open Settings" + } + }, + "button_receive": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "Receive" + } + }, + "button_receive_files": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "Start receiving" + } + }, + "button_refuse": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "Refuse" + } + }, + "button_remove_file": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "Remove file" + } + }, + "button_reset_default": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "Use default" + } + }, + "button_retry": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "Retry" + } + }, + "button_share_file": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "Start sharing" + } + }, + "button_sharing_file": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "Preparing transfer…" + } + }, + "button_show_in_files": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "Show in Files" + } + }, + "button_write_nfc": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "Write to NFC tag" + } + }, + "device_model_title": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "Device model" + } + }, + "device_name_title": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "Device name" + } + }, + "diagnostics_description": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "Send anonymous crash reports and usage events so we can improve VniDrop. You can turn this off anytime. Invitations, file paths, and transfer contents are never included." + } + }, + "diagnostics_disabled_message": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "Diagnostics sharing is off." + } + }, + "diagnostics_enabled_message": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "Diagnostics sharing is on." + } + }, + "diagnostics_title": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "Share diagnostics" + } + }, + "error_camera": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "Camera access is required to scan a QR code." + } + }, + "error_device_info": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "Could not load device information." + } + }, + "error_filesystem": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "VniDrop could not access the selected files or folder. Check permissions and try again." + } + }, + "error_generic": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "Something went wrong. Try again." + } + }, + "error_initialization": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "VniDrop could not finish starting up. Close the app and try again." + } + }, + "error_invalid_ticket": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "This invitation could not be read. Ask the sender for a new one." + } + }, + "error_invitation_empty": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "That invitation is empty. Try opening it again." + } + }, + "error_missing_native_library": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "The native VniDrop library is missing from this build." + } + }, + "error_nfc": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "This NFC tag could not be used. Try another tag." + } + }, + "error_permission": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "The sender has not approved this transfer, or it was refused." + } + }, + "error_repository": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "VniDrop could not save transfer data on this device." + } + }, + "error_selection_failed": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "Could not open the selected item. Try choosing it again." + } + }, + "error_share_empty": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "Select at least one item to share." + } + }, + "error_socket_bind": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "VniDrop could not open its network sockets on this device." + } + }, + "error_starting_up": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "VniDrop is still starting. Open the invitation again in a moment." + } + }, + "error_transfer": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "The transfer could not be completed. Check your connection and try again." + } + }, + "field_receiver_name": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "Receiver name" + } + }, + "field_sender_name": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "Sender name" + } + }, + "field_transfer_name": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "Transfer name" + } + }, + "field_username": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "Display name" + } + }, + "folder_status_permission_required": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "Permission needed" + } + }, + "folder_status_unavailable": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "Unavailable" + } + }, + "folder_status_validating": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "Checking folder…" + } + }, + "folder_status_writable": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "Ready" + } + }, + "metadata_files": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "Files" + } + }, + "metadata_size": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "Size" + } + }, + "metadata_status": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "Status" + } + }, + "nav_receive": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "Receive" + } + }, + "nav_send": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "Send" + } + }, + "nav_settings": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "Settings" + } + }, + "network_title": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "Network" + } + }, + "notifications_description": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "Get notified about new receive requests while VniDrop is in the background." + } + }, + "notifications_enabled_message": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "Notifications enabled." + } + }, + "notifications_local_title": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "Allow notifications" + } + }, + "notifications_permission_denied": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "Notifications are turned off for VniDrop. You can enable them in Settings." + } + }, + "notifications_settings_open_failed": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "Could not open notification settings." + } + }, + "notifications_title": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "Notifications" + } + }, + "notifications_unsupported": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "Notifications are not available on this device." + } + }, + "os_version_title": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "Operating system" + } + }, + "preferences_receive_folder_title": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "Save received transfers to" + } + }, + "preferences_title": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "Preferences" + } + }, + "progress_cancelled": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "Cancelled" + } + }, + "progress_completed": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "Completed" + } + }, + "progress_connected": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "Connected" + } + }, + "progress_connecting": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "Connecting" + } + }, + "progress_downloading": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "Downloading" + } + }, + "progress_failed": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "Failed" + } + }, + "progress_getting_ready": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "Getting ready" + } + }, + "progress_interrupted": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "Transfer interrupted" + } + }, + "progress_preparing": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "Preparing" + } + }, + "progress_ready": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "Ready" + } + }, + "progress_receiving": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "Receiving" + } + }, + "progress_requesting_access": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "Requesting access" + } + }, + "progress_saving": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "Saving" + } + }, + "progress_sending": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "Sending" + } + }, + "progress_sending_to_count": { + "context": "TODO: describe where this appears.", + "targets": [ + "apple" + ], + "translations": { + "en": "Sending to %lld" + } + }, + "progress_share_ready": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "Ready to share" + } + }, + "progress_working": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "Working…" + } + }, + "receive_choose_method_body": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "Choose the invitation method available to you." + } + }, + "receive_choose_method_title": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "How would you like to connect?" + } + }, + "receive_clear_history": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "Clear history" + } + }, + "receive_clear_history_description": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "All completed, failed, and cancelled receives will be removed from VniDrop’s history. Downloaded files will remain on this device." + } + }, + "receive_clear_history_title": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "Clear receive history?" + } + }, + "receive_completed": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "Transfer received." + } + }, + "receive_delete_history_description": { + "context": "TODO: describe where this appears.", + "args": [ + { + "name": "arg1", + "type": "string" + } + ], + "translations": { + "en": "“{arg1}” will be removed from VniDrop’s history. The downloaded file will remain on this device." + } + }, + "receive_delete_history_item": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "Delete from receive history" + } + }, + "receive_delete_history_title": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "Remove from history?" + } + }, + "receive_empty_body": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "Open a VniDrop invitation, scan a QR code, or hold near an NFC tag." + } + }, + "receive_empty_title": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "Nothing received yet" + } + }, + "receive_history_cleared": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "Receive history cleared." + } + }, + "receive_history_title": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "History" + } + }, + "receive_method_file": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "Open a .vnd invitation" + } + }, + "receive_method_file_description": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "Choose an invitation saved or shared to this device." + } + }, + "receive_method_nfc": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "Read NFC tag" + } + }, + "receive_method_nfc_description": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "Hold this device near the sender’s invitation tag." + } + }, + "receive_method_scan": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "Scan QR code" + } + }, + "receive_method_scan_description": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "Use the camera to scan the sender’s VniDrop code." + } + }, + "receive_new_subtitle": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "Transfers you’ve received on this device." + } + }, + "receive_nfc_waiting": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "Hold near the NFC tag…" + } + }, + "receive_open_files_failed": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "Couldn’t open VniDrop in Files." + } + }, + "receive_review_title": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "Review transfer" + } + }, + "receive_title": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "Receive" + } + }, + "receive_unknown_transfer": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "VniDrop transfer" + } + }, + "send_access_anyone": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "Anyone with this transfer" + } + }, + "send_access_anyone_description": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "No approval is required. Only use this for items you are comfortable sharing." + } + }, + "send_access_anyone_warning": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "Anyone with the invitation can download until you stop sharing. Do not use this for private or sensitive items." + } + }, + "send_access_approval": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "Ask before each download" + } + }, + "send_access_approval_description": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "You approve or refuse every new receiver." + } + }, + "send_access_title": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "Who can receive it?" + } + }, + "send_choose_file_body": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "Select files or a folder from this device. You can review the selection before creating the transfer." + } + }, + "send_choose_file_title": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "Choose what to share" + } + }, + "send_empty_body": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "Create a transfer, decide who can receive it, then invite them with a QR code, NFC tag, or invitation file." + } + }, + "send_empty_title": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "Nothing shared yet" + } + }, + "send_file_size_unknown": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "Size unavailable" + } + }, + "send_folder_label": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "Folder" + } + }, + "send_new_transfer_description": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "Create a new transfer" + } + }, + "send_new_transfer_title": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "New transfer" + } + }, + "send_review_title": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "Review transfer" + } + }, + "send_selected_files_count": { + "context": "TODO: describe where this appears.", + "args": [ + { + "name": "count", + "type": "int" + } + ], + "translations": { + "en": "{count} files selected" + } + }, + "send_stop_sharing": { + "context": "TODO: describe where this appears.", + "targets": [ + "apple" + ], + "translations": { + "en": "Stop sharing" + } + }, + "send_stop_sharing_description": { + "context": "TODO: describe where this appears.", + "targets": [ + "apple" + ], + "translations": { + "en": "This stops the transfer and interrupts anyone currently downloading it. It stays in your history as Stopped." + } + }, + "send_subtitle": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "Transfers you’re sharing from this device." + } + }, + "send_title": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "Send" + } + }, + "send_transfer_created": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "Transfer created." + } + }, + "send_transfer_details_title": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "Transfer details" + } + }, + "send_transfers_title": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "Your transfers" + } + }, + "settings_subtitle": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "Your name, where transfers are saved, appearance, and notifications." + } + }, + "settings_title": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "Settings" + } + }, + "snackbar_dismiss": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "Dismiss" + } + }, + "status_available": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "Available" + } + }, + "status_cancelled": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "Cancelled" + } + }, + "status_completed": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "Completed" + } + }, + "status_failed": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "Failed" + } + }, + "status_preparing": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "Preparing" + } + }, + "status_receiving": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "Receiving" + } + }, + "status_stopped": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "Stopped" + } + }, + "storage_calculating": { + "context": "TODO: describe where this appears.", + "targets": [ + "apple" + ], + "translations": { + "en": "Calculating…" + } + }, + "storage_delete_transfers": { + "context": "TODO: describe where this appears.", + "targets": [ + "apple" + ], + "translations": { + "en": "Delete all transfers" + } + }, + "storage_delete_transfers_description": { + "context": "TODO: describe where this appears.", + "targets": [ + "apple" + ], + "translations": { + "en": "This clears all sent and received transfer records from your history. Your received files are not deleted. Note: the transfer engine keeps its stored content, so Transfer data may not decrease. This can’t be undone." + } + }, + "storage_deleting": { + "context": "TODO: describe where this appears.", + "targets": [ + "apple" + ], + "translations": { + "en": "Deleting…" + } + }, + "storage_footer": { + "context": "TODO: describe where this appears.", + "targets": [ + "apple" + ], + "translations": { + "en": "Transfer data is managed by the transfer engine — your history plus the content of files you’ve shared. The button below clears your transfer records; the engine keeps its stored content, so this value may not drop. Received files are your downloaded files and are never deleted here." + } + }, + "storage_received_files": { + "context": "TODO: describe where this appears.", + "targets": [ + "apple" + ], + "translations": { + "en": "Received files" + } + }, + "storage_temporary": { + "context": "TODO: describe where this appears.", + "targets": [ + "apple" + ], + "translations": { + "en": "Temporary files" + } + }, + "storage_title": { + "context": "TODO: describe where this appears.", + "targets": [ + "apple" + ], + "translations": { + "en": "Storage" + } + }, + "storage_total": { + "context": "TODO: describe where this appears.", + "targets": [ + "apple" + ], + "translations": { + "en": "Total" + } + }, + "storage_transfer_data": { + "context": "TODO: describe where this appears.", + "targets": [ + "apple" + ], + "translations": { + "en": "Transfer data" + } + }, + "storage_transfers_deleted": { + "context": "TODO: describe where this appears.", + "targets": [ + "apple" + ], + "translations": { + "en": "All transfers deleted" + } + }, + "transfer_activity_description": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "See important updates for this transfer" + } + }, + "transfer_activity_title": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "Activity" + } + }, + "transfer_delete_description": { + "context": "TODO: describe where this appears.", + "args": [ + { + "name": "arg1", + "type": "string" + } + ], + "translations": { + "en": "“{arg1}” will stop being shared and its transfer history will be removed from this device." + } + }, + "transfer_delete_title": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "Delete transfer?" + } + }, + "transfer_deleted": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "Transfer deleted." + } + }, + "transfer_deleting": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "Deleting…" + } + }, + "transfer_details_title": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "Transfer details" + } + }, + "transfer_event_approved": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "Receiver access approved" + } + }, + "transfer_event_completed": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "A receiver completed the transfer" + } + }, + "transfer_event_connecting": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "Connecting to sender" + } + }, + "transfer_event_downloading": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "Downloading" + } + }, + "transfer_event_failed": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "The transfer encountered a problem" + } + }, + "transfer_event_preparing": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "Preparing your transfer" + } + }, + "transfer_event_ready": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "Ready to share" + } + }, + "transfer_event_refused": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "Receiver access refused" + } + }, + "transfer_event_requested": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "A receiver requested access" + } + }, + "transfer_event_saving": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "Saving" + } + }, + "transfer_event_stopped": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "Sharing stopped" + } + }, + "transfer_event_updated": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "Transfer updated" + } + }, + "transfer_file_count": { + "context": "TODO: describe where this appears.", + "args": [ + { + "name": "count", + "type": "int" + } + ], + "plural": { + "en": { + "other": "{count} files", + "one": "{count} file" + } + } + }, + "transfer_invitation_saved": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "Invitation saved." + } + }, + "transfer_nearby_device": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "Nearby device" + } + }, + "transfer_nfc_unavailable": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "NFC tag writing is not available on this device." + } + }, + "transfer_nfc_waiting": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "Hold your device near a writable NFC tag." + } + }, + "transfer_nfc_written": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "Invitation written to the NFC tag." + } + }, + "transfer_no_activity": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "There is no activity to show yet." + } + }, + "transfer_no_receivers": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "Nobody has requested this transfer yet." + } + }, + "transfer_receiver_accepted": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "Approved — waiting for completion" + } + }, + "transfer_receiver_completed": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "Received successfully" + } + }, + "transfer_receiver_expired": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "Request expired" + } + }, + "transfer_receiver_refused": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "Request refused" + } + }, + "transfer_receiver_requested": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "Waiting for your approval" + } + }, + "transfer_receiver_unknown": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "Status unavailable" + } + }, + "transfer_receivers_completed_count": { + "context": "TODO: describe where this appears.", + "args": [ + { + "name": "count", + "type": "int" + } + ], + "translations": { + "en": "{count} completed" + } + }, + "transfer_receivers_description": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "Requests, approvals, and completed deliveries" + } + }, + "transfer_receivers_pending": { + "context": "TODO: describe where this appears.", + "args": [ + { + "name": "count", + "type": "int" + } + ], + "translations": { + "en": "{count} waiting" + } + }, + "transfer_receivers_title": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "Receivers" + } + }, + "transfer_scan_qr": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "Scan with VniDrop to receive this transfer" + } + }, + "transfer_share_description": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "QR code, invitation file, and nearby options" + } + }, + "transfer_share_title": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "Share" + } + }, + "value_unavailable": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "Not available" + } + }, + "version_title": { + "context": "TODO: describe where this appears.", + "translations": { + "en": "App version" + } + } + } +} diff --git a/localization/tsconfig.json b/localization/tsconfig.json new file mode 100644 index 0000000..9b4a7ec --- /dev/null +++ b/localization/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "lib": ["ESNext"], + "target": "ESNext", + "module": "ESNext", + "moduleResolution": "bundler", + "types": ["bun-types"], + "strict": true, + "noUncheckedIndexedAccess": true, + "skipLibCheck": true, + "verbatimModuleSyntax": true + }, + "include": ["src"] +} diff --git a/shared/src/commonMain/composeResources/values/strings.xml b/shared/src/commonMain/composeResources/values/strings.xml index e866f8b..d8b7311 100644 --- a/shared/src/commonMain/composeResources/values/strings.xml +++ b/shared/src/commonMain/composeResources/values/strings.xml @@ -1,218 +1,220 @@ + - Send - Receive - Settings - Send - Transfers you’re sharing from this device. - Nothing shared yet - Create a transfer, decide who can receive it, then invite them with a QR code, NFC tag, or invitation file. - New transfer - Your transfers - New transfer - Choose what to share - Select files or a folder from this device. You can review the selection before creating the transfer. - %1$d files selected - Folder - Remove file - Choose files - Change files - Review transfer - Who can receive it? - Ask before each download - You approve or refuse every new receiver. - Anyone with this transfer - No approval is required. Only use this for items you are comfortable sharing. - Anyone with the invitation can download until you stop sharing. Do not use this for private or sensitive items. - Size unavailable - Transfer created. - Transfer details - Activity - See important updates for this transfer - Receivers - Requests, approvals, and completed deliveries - Share - QR code, invitation file, and nearby options - Delete transfer? - “%1$s” will stop being shared and its transfer history will be removed from this device. - Deleting… - Transfer deleted. - There is no activity to show yet. - Nobody has requested this transfer yet. - Waiting for your approval - Approved — waiting for completion - Request refused - Request expired - Received successfully - Status unavailable - Nearby device - Scan with VniDrop to receive this transfer - Write to NFC tag - Save .vnd file - Share invitation - NFC tag writing is not available on this device. - Hold your device near a writable NFC tag. - Invitation saved. - Invitation written to the NFC tag. - Preparing your transfer - Ready to share - A receiver requested access - Receiver access approved - Receiver access refused - A receiver completed the transfer - Sharing stopped - The transfer encountered a problem - Transfer updated - Start sharing - Preparing transfer… - Create a new transfer - Clear - Transfer details - Transfer name - Sender name - Refuse - Approve - Receive - Transfers you’ve received on this device. - Nothing received yet - Open a VniDrop invitation, scan a QR code, or hold near an NFC tag. - Start receiving - History - Clear history - Delete from receive history - Remove from history? - “%1$s” will be removed from VniDrop’s history. The downloaded file will remain on this device. - Clear receive history? - All completed, failed, and cancelled receives will be removed from VniDrop’s history. Downloaded files will remain on this device. - Receive history cleared. - How would you like to connect? - Choose the invitation method available to you. - Open a .vnd invitation - Choose an invitation saved or shared to this device. - Scan QR code - Use the camera to scan the sender’s VniDrop code. - Read NFC tag - Hold this device near the sender’s invitation tag. - Hold near the NFC tag… - Review transfer - VniDrop transfer - Transfer received. - Show in Files - Couldn’t open VniDrop in Files. - Receiver name - Receive - Retry - Cancel - Receiving - Preparing - Ready - Ready to share - Connecting - Connected - Requesting access - Getting ready - Downloading - Saving - Sending - Completed - Cancelled - Failed - Transfer interrupted - Working… - Preparing - Available - Receiving - Completed - Cancelled - Stopped - Failed - %1$d waiting - %1$d completed - Downloading - Saving - Connecting to sender - %1$d file - %1$d files - - Settings - Your name, where transfers are saved, appearance, and notifications. - Appearance - Preferences - Display name - Save received transfers to - Choose folder - Use default - Back - Close - Cancel - Delete transfer - Ready - Permission needed - Unavailable - Checking folder… - System + Report a bug + Privacy policy + About + Match this device’s light or dark appearance. Dark mode Light mode - Match this device’s light or dark appearance. - About - Privacy policy - Report a bug - Share diagnostics - Send anonymous crash reports and usage events so we can improve VniDrop. You can turn this off anytime. Invitations, file paths, and transfer contents are never included. - Diagnostics sharing is on. - Diagnostics sharing is off. - Tell us what went wrong. We attach device info and optional recent logs (with sensitive values redacted). - What happened? - What did you expect? - Steps to reproduce (optional) + System + Appearance + Receive request + Device ID: %1$s + A nearby device + %1$d requests waiting + %1$s wants to receive “%2$s”. + Battery level Contact email (optional) + Tell us what went wrong. We attach device info and optional recent logs (with sensitive values redacted). + Device information + What did you expect? Include recent logs Helps us diagnose the issue. Sensitive values are redacted before sending. Log attachment size - Device information - Submit report - Submitting… - Thanks — your bug report was recorded. - Could not submit the bug report. Try again later. - Please describe what happened. Please describe what you expected. - App version - Device name - Device model - Operating system - Network - Battery level - Not available - Notifications - Allow notifications - Get notified about new receive requests while VniDrop is in the background. - Notifications are turned off for VniDrop. You can enable them in Settings. - Notifications are not available on this device. - Notifications enabled. - Could not open notification settings. + Please describe what happened. + Steps to reproduce (optional) + Submit report + Could not submit the bug report. Try again later. + Thanks — your bug report was recorded. + Submitting… + What happened? + Approve + Back + Cancel + Cancel + Change files + Choose files + Choose folder + Clear + Close + New transfer + Delete transfer + Save .vnd file + Share invitation Open Settings - Dismiss - Receive request - %1$s wants to receive “%2$s”. - A nearby device - Device ID: %1$s - %1$d requests waiting - Size - Files - Status + Receive + Start receiving + Refuse + Remove file + Use default + Retry + Start sharing + Preparing transfer… + Show in Files + Write to NFC tag + Device model + Device name + Send anonymous crash reports and usage events so we can improve VniDrop. You can turn this off anytime. Invitations, file paths, and transfer contents are never included. + Diagnostics sharing is off. + Diagnostics sharing is on. + Share diagnostics + Camera access is required to scan a QR code. + Could not load device information. + VniDrop could not access the selected files or folder. Check permissions and try again. Something went wrong. Try again. - VniDrop is still starting. Open the invitation again in a moment. + VniDrop could not finish starting up. Close the app and try again. This invitation could not be read. Ask the sender for a new one. That invitation is empty. Try opening it again. - The sender has not approved this transfer, or it was refused. - VniDrop could not access the selected files or folder. Check permissions and try again. - The transfer could not be completed. Check your connection and try again. - VniDrop could not save transfer data on this device. - VniDrop could not finish starting up. Close the app and try again. - VniDrop could not open its network sockets on this device. The native VniDrop library is missing from this build. - Could not open the selected item. Try choosing it again. - Could not load device information. This NFC tag could not be used. Try another tag. - Camera access is required to scan a QR code. + The sender has not approved this transfer, or it was refused. + VniDrop could not save transfer data on this device. + Could not open the selected item. Try choosing it again. Select at least one item to share. + VniDrop could not open its network sockets on this device. + VniDrop is still starting. Open the invitation again in a moment. + The transfer could not be completed. Check your connection and try again. + Receiver name + Sender name + Transfer name + Display name + Permission needed + Unavailable + Checking folder… + Ready + Files + Size + Status + Receive + Send + Settings + Network + Get notified about new receive requests while VniDrop is in the background. + Notifications enabled. + Allow notifications + Notifications are turned off for VniDrop. You can enable them in Settings. + Could not open notification settings. + Notifications + Notifications are not available on this device. + Operating system + Save received transfers to + Preferences + Cancelled + Completed + Connected + Connecting + Downloading + Failed + Getting ready + Transfer interrupted + Preparing + Ready + Receiving + Requesting access + Saving + Sending + Ready to share + Working… + Choose the invitation method available to you. + How would you like to connect? + Clear history + All completed, failed, and cancelled receives will be removed from VniDrop’s history. Downloaded files will remain on this device. + Clear receive history? + Transfer received. + “%1$s” will be removed from VniDrop’s history. The downloaded file will remain on this device. + Delete from receive history + Remove from history? + Open a VniDrop invitation, scan a QR code, or hold near an NFC tag. + Nothing received yet + Receive history cleared. + History + Open a .vnd invitation + Choose an invitation saved or shared to this device. + Read NFC tag + Hold this device near the sender’s invitation tag. + Scan QR code + Use the camera to scan the sender’s VniDrop code. + Transfers you’ve received on this device. + Hold near the NFC tag… + Couldn’t open VniDrop in Files. + Review transfer + Receive + VniDrop transfer + Anyone with this transfer + No approval is required. Only use this for items you are comfortable sharing. + Anyone with the invitation can download until you stop sharing. Do not use this for private or sensitive items. + Ask before each download + You approve or refuse every new receiver. + Who can receive it? + Select files or a folder from this device. You can review the selection before creating the transfer. + Choose what to share + Create a transfer, decide who can receive it, then invite them with a QR code, NFC tag, or invitation file. + Nothing shared yet + Size unavailable + Folder + Create a new transfer + New transfer + Review transfer + %1$d files selected + Transfers you’re sharing from this device. + Send + Transfer created. + Transfer details + Your transfers + Your name, where transfers are saved, appearance, and notifications. + Settings + Dismiss + Available + Cancelled + Completed + Failed + Preparing + Receiving + Stopped + See important updates for this transfer + Activity + “%1$s” will stop being shared and its transfer history will be removed from this device. + Delete transfer? + Transfer deleted. + Deleting… + Transfer details + Receiver access approved + A receiver completed the transfer + Connecting to sender + Downloading + The transfer encountered a problem + Preparing your transfer + Ready to share + Receiver access refused + A receiver requested access + Saving + Sharing stopped + Transfer updated + Invitation saved. + Nearby device + NFC tag writing is not available on this device. + Hold your device near a writable NFC tag. + Invitation written to the NFC tag. + There is no activity to show yet. + Nobody has requested this transfer yet. + Approved — waiting for completion + Received successfully + Request expired + Request refused + Waiting for your approval + Status unavailable + %1$d completed + Requests, approvals, and completed deliveries + %1$d waiting + Receivers + Scan with VniDrop to receive this transfer + QR code, invitation file, and nearby options + Share + Not available + App version + + %1$d files + %1$d file + diff --git a/shared/src/commonMain/kotlin/com/vnidrop/app/ui/state/AppUiModels.kt b/shared/src/commonMain/kotlin/com/vnidrop/app/ui/state/AppUiModels.kt index 3adbf74..75288ba 100644 --- a/shared/src/commonMain/kotlin/com/vnidrop/app/ui/state/AppUiModels.kt +++ b/shared/src/commonMain/kotlin/com/vnidrop/app/ui/state/AppUiModels.kt @@ -5,6 +5,7 @@ import com.vnidrop.app.core.CoreEventModel import com.vnidrop.app.core.Transfer import com.vnidrop.app.core.TransferStatus import kotlin.math.roundToInt +import org.jetbrains.compose.resources.PluralStringResource import org.jetbrains.compose.resources.StringResource import org.jetbrains.compose.resources.stringResource import vnidrop.shared.generated.resources.Res @@ -30,8 +31,7 @@ import vnidrop.shared.generated.resources.status_failed import vnidrop.shared.generated.resources.status_preparing import vnidrop.shared.generated.resources.status_receiving import vnidrop.shared.generated.resources.status_stopped -import vnidrop.shared.generated.resources.transfer_file_count_one -import vnidrop.shared.generated.resources.transfer_file_count_other +import vnidrop.shared.generated.resources.transfer_file_count enum class WindowClass { Phone, @@ -206,9 +206,8 @@ fun summarizeProgress(events: List): List = .take(6) .mapNotNull { progressForTransfer(events, it) } -/** File-count string resource for transfer subtitles (resolve with [stringResource]). */ -fun transferFileCountResource(fileCount: ULong): StringResource = - if (fileCount == 1UL) Res.string.transfer_file_count_one else Res.string.transfer_file_count_other +/** File-count plural resource for transfer subtitles (resolve with [pluralStringResource]). */ +fun transferFileCountResource(): PluralStringResource = Res.plurals.transfer_file_count fun formatBytes(size: ULong): String { val value = size.toDouble() diff --git a/shared/src/commonTest/kotlin/com/vnidrop/app/ui/state/AppUiModelsTest.kt b/shared/src/commonTest/kotlin/com/vnidrop/app/ui/state/AppUiModelsTest.kt index 2a4ffcb..1fa0a42 100644 --- a/shared/src/commonTest/kotlin/com/vnidrop/app/ui/state/AppUiModelsTest.kt +++ b/shared/src/commonTest/kotlin/com/vnidrop/app/ui/state/AppUiModelsTest.kt @@ -20,8 +20,7 @@ import vnidrop.shared.generated.resources.progress_interrupted import vnidrop.shared.generated.resources.progress_saving import vnidrop.shared.generated.resources.progress_sending import vnidrop.shared.generated.resources.progress_working -import vnidrop.shared.generated.resources.transfer_file_count_one -import vnidrop.shared.generated.resources.transfer_file_count_other +import vnidrop.shared.generated.resources.transfer_file_count class AppUiModelsTest { @Test @@ -235,10 +234,8 @@ class AppUiModelsTest { } @Test - fun transferFileCountPicksSingularAndPluralResources() { - assertEquals(Res.string.transfer_file_count_one, transferFileCountResource(1UL)) - assertEquals(Res.string.transfer_file_count_other, transferFileCountResource(2UL)) - assertEquals(Res.string.transfer_file_count_other, transferFileCountResource(0UL)) + fun transferFileCountUsesPluralResource() { + assertEquals(Res.plurals.transfer_file_count, transferFileCountResource()) } @Test