mirror of
https://github.com/str4d/rage.git
synced 2025-04-04 19:37:51 +03:00
rage: Tag strings for translation
This commit is contained in:
parent
8a83de08a8
commit
d4c87cd669
7 changed files with 215 additions and 58 deletions
4
.github/workflows/interop.yml
vendored
4
.github/workflows/interop.yml
vendored
|
@ -22,12 +22,12 @@ jobs:
|
|||
toolchain: 1.45.0
|
||||
override: true
|
||||
- name: cargo build
|
||||
run: cargo build --features unstable
|
||||
run: cargo build --release --features unstable
|
||||
working-directory: ./rage
|
||||
- uses: actions/upload-artifact@v1
|
||||
with:
|
||||
name: rage
|
||||
path: target/debug/rage
|
||||
path: target/release/rage
|
||||
|
||||
- name: Update FiloSottile/age status with result
|
||||
if: github.event.action == 'age-interop-request'
|
||||
|
|
1
i18n.toml
Normal file
1
i18n.toml
Normal file
|
@ -0,0 +1 @@
|
|||
fallback_language = "en-US"
|
|
@ -10,6 +10,7 @@ to 1.0.0 are beta releases.
|
|||
|
||||
## [Unreleased]
|
||||
### Added
|
||||
- Internationalization (i18n) support!
|
||||
- `ssh` feature flag, enabled by default. It can be disabled to remove support
|
||||
for `ssh-rsa` and `ssh-ed25519` recipients and identities.
|
||||
|
||||
|
|
4
rage/i18n.toml
Normal file
4
rage/i18n.toml
Normal file
|
@ -0,0 +1,4 @@
|
|||
fallback_language = "en-US"
|
||||
|
||||
[fluent]
|
||||
assets_dir = "i18n"
|
83
rage/i18n/en-US/rage.ftl
Normal file
83
rage/i18n/en-US/rage.ftl
Normal file
|
@ -0,0 +1,83 @@
|
|||
# Copyright 2020 Jack Grigg
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
|
||||
# http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
|
||||
# <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
|
||||
# option. This file may not be copied, modified, or distributed
|
||||
# except according to those terms.
|
||||
|
||||
### Localization for strings in the rage CLI tool
|
||||
|
||||
## CLI flags
|
||||
|
||||
-flag-armor = -a/--armor
|
||||
-flag-decrypt = -d/--decrypt
|
||||
-flag-identity = -i/--identity
|
||||
-flag-recipient = -r/--recipient
|
||||
-flag-passphrase = -p/--passphrase
|
||||
-flag-max-work-factor = --max-work-factor
|
||||
-flag-unstable = --features unstable
|
||||
|
||||
## Encryption messages
|
||||
|
||||
autogenerated-passphrase = Using an autogenerated passphrase:
|
||||
type-passphrase = Type passphrase
|
||||
prompt-passphrase = Passphrase
|
||||
|
||||
## General errors
|
||||
|
||||
err-passphrase-timed-out = Timed out waiting for passphrase input.
|
||||
|
||||
err-ux-A = Did rage not do what you expected? Could an error be more useful?
|
||||
err-ux-B = Tell us
|
||||
# Put (len(A) - len(B) - 32) spaces here.
|
||||
err-ux-C = {" "}
|
||||
|
||||
## Encryption errors
|
||||
|
||||
err-enc-broken-stdout = Could not write to stdout: {$err}
|
||||
rec-enc-broken-stdout = Are you piping to a program that isn't reading from stdin?
|
||||
|
||||
err-enc-broken-file = Could not write to file: {$err}
|
||||
|
||||
err-enc-identity = {-flag-identity} can't be used in encryption mode.
|
||||
rec-enc-identity = Did you forget to specify {-flag-decrypt}?
|
||||
|
||||
err-enc-invalid-recipient = Invalid recipient '{$recipient}'
|
||||
|
||||
err-enc-missing-recipients = Missing recipients.
|
||||
rec-enc-missing-recipients = Did you forget to specify {-flag-recipient}?
|
||||
|
||||
err-enc-mixed-recipient-passphrase = {-flag-recipient} can't be used with {-flag-passphrase}
|
||||
err-enc-passphrase-without-file = File to encrypt must be passed as an argument when using {-flag-passphrase}
|
||||
err-enc-unknown-alias = Unknown {$alias}
|
||||
|
||||
## Decryption errors
|
||||
|
||||
rec-dec-excessive-work = To decrypt, retry with {-flag-max-work-factor} {$wf}
|
||||
|
||||
err-dec-armor-flag = {-flag-armor} can't be used with {-flag-decrypt}.
|
||||
rec-dec-armor-flag = Note that armored files are detected automatically.
|
||||
|
||||
err-dec-identity-not-found = Identity file not found: {$filename}
|
||||
|
||||
err-dec-missing-identities = Missing identities.
|
||||
rec-dec-missing-identities-1 = Did you forget to specify {-flag-identity}?
|
||||
rec-dec-missing-identities-2 = You can also store default identities in this file:
|
||||
|
||||
err-dec-passphrase-flag = {-flag-passphrase} can't be used with {-flag-decrypt}.
|
||||
rec-dec-passphrase-flag = Note that passphrase-encrypted files are detected automatically.
|
||||
|
||||
err-dec-passphrase-without-file-win =
|
||||
This file requires a passphrase, and on Windows the
|
||||
file to decrypt must be passed as a positional argument
|
||||
when decrypting with a passphrase.
|
||||
|
||||
err-dec-recipient-flag = {-flag-recipient} can't be used with {-flag-decrypt}.
|
||||
rec-dec-recipient-flag = Did you mean to use {-flag-identity} to specify a private key?
|
||||
|
||||
## Unstable features
|
||||
|
||||
unstable-aliases = Aliases are unstable.
|
||||
unstable-github = GitHub lookups are unstable, ignoring recipient.
|
||||
test-unstable = To test this, build rage with {-flag-unstable}.
|
|
@ -1,6 +1,19 @@
|
|||
use i18n_embed_fl::fl;
|
||||
use std::fmt;
|
||||
use std::io;
|
||||
|
||||
macro_rules! wfl {
|
||||
($f:ident, $message_id:literal) => {
|
||||
write!($f, "{}", $crate::fl!($message_id))
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! wlnfl {
|
||||
($f:ident, $message_id:literal) => {
|
||||
writeln!($f, "{}", $crate::fl!($message_id))
|
||||
};
|
||||
}
|
||||
|
||||
pub(crate) enum EncryptError {
|
||||
BrokenPipe { is_stdout: bool, source: io::Error },
|
||||
IdentityFlag,
|
||||
|
@ -9,8 +22,8 @@ pub(crate) enum EncryptError {
|
|||
Minreq(minreq::Error),
|
||||
MissingRecipients,
|
||||
MixedRecipientAndPassphrase,
|
||||
PassphraseTimedOut,
|
||||
PassphraseWithoutFileArgument,
|
||||
TimedOut(String),
|
||||
UnknownAlias(String),
|
||||
}
|
||||
|
||||
|
@ -39,35 +52,63 @@ impl fmt::Display for EncryptError {
|
|||
match self {
|
||||
EncryptError::BrokenPipe { is_stdout, source } => {
|
||||
if *is_stdout {
|
||||
writeln!(f, "Could not write to stdout: {}", source)?;
|
||||
writeln!(
|
||||
f,
|
||||
"{}",
|
||||
fl!(
|
||||
crate::LANGUAGE_LOADER,
|
||||
"err-enc-broken-stdout",
|
||||
err = source.to_string()
|
||||
)
|
||||
)?;
|
||||
wfl!(f, "rec-enc-broken-stdout")
|
||||
} else {
|
||||
write!(
|
||||
f,
|
||||
"Are you piping to a program that isn't reading from stdin?"
|
||||
"{}",
|
||||
fl!(
|
||||
crate::LANGUAGE_LOADER,
|
||||
"err-enc-broken-file",
|
||||
err = source.to_string()
|
||||
)
|
||||
)
|
||||
} else {
|
||||
write!(f, "Could not write to file: {}", source)
|
||||
}
|
||||
}
|
||||
EncryptError::IdentityFlag => {
|
||||
writeln!(f, "-i/--identity can't be used in encryption mode.")?;
|
||||
write!(f, "Did you forget to specify -d/--decrypt?")
|
||||
wlnfl!(f, "err-enc-identity")?;
|
||||
wfl!(f, "rec-enc-identity")
|
||||
}
|
||||
EncryptError::InvalidRecipient(r) => write!(f, "Invalid recipient '{}'", r),
|
||||
EncryptError::InvalidRecipient(recipient) => write!(
|
||||
f,
|
||||
"{}",
|
||||
fl!(
|
||||
crate::LANGUAGE_LOADER,
|
||||
"err-enc-invalid-recipient",
|
||||
recipient = recipient.as_str()
|
||||
)
|
||||
),
|
||||
EncryptError::Io(e) => write!(f, "{}", e),
|
||||
EncryptError::Minreq(e) => write!(f, "{}", e),
|
||||
EncryptError::MissingRecipients => {
|
||||
writeln!(f, "Missing recipients.")?;
|
||||
write!(f, "Did you forget to specify -r/--recipient?")
|
||||
wlnfl!(f, "err-enc-missing-recipients")?;
|
||||
wfl!(f, "rec-enc-missing-recipients")
|
||||
}
|
||||
EncryptError::MixedRecipientAndPassphrase => {
|
||||
write!(f, "-r/--recipient can't be used with -p/--passphrase")
|
||||
wfl!(f, "err-enc-mixed-recipient-passphrase")
|
||||
}
|
||||
EncryptError::PassphraseWithoutFileArgument => write!(
|
||||
EncryptError::PassphraseTimedOut => wfl!(f, "err-passphrase-timed-out"),
|
||||
EncryptError::PassphraseWithoutFileArgument => {
|
||||
wfl!(f, "err-enc-passphrase-without-file")
|
||||
}
|
||||
EncryptError::UnknownAlias(alias) => write!(
|
||||
f,
|
||||
"File to encrypt must be passed as an argument when using -p/--passphrase"
|
||||
"{}",
|
||||
fl!(
|
||||
crate::LANGUAGE_LOADER,
|
||||
"err-enc-unknown-alias",
|
||||
alias = alias.as_str()
|
||||
)
|
||||
),
|
||||
EncryptError::TimedOut(source) => write!(f, "Timed out waiting for {}", source),
|
||||
EncryptError::UnknownAlias(alias) => write!(f, "Unknown {}", alias),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -79,10 +120,10 @@ pub(crate) enum DecryptError {
|
|||
Io(io::Error),
|
||||
MissingIdentities(String),
|
||||
PassphraseFlag,
|
||||
PassphraseTimedOut,
|
||||
#[cfg(not(unix))]
|
||||
PassphraseWithoutFileArgument,
|
||||
RecipientFlag,
|
||||
TimedOut(String),
|
||||
#[cfg(feature = "ssh")]
|
||||
UnsupportedKey(String, age::ssh::UnsupportedKey),
|
||||
}
|
||||
|
@ -105,45 +146,51 @@ impl fmt::Display for DecryptError {
|
|||
DecryptError::Age(e) => match e {
|
||||
age::DecryptError::ExcessiveWork { required, .. } => {
|
||||
writeln!(f, "{}", e)?;
|
||||
write!(f, "To decrypt, retry with --max-work-factor {}", required)
|
||||
write!(
|
||||
f,
|
||||
"{}",
|
||||
fl!(
|
||||
crate::LANGUAGE_LOADER,
|
||||
"rec-dec-excessive-work",
|
||||
wf = required
|
||||
)
|
||||
)
|
||||
}
|
||||
_ => write!(f, "{}", e),
|
||||
},
|
||||
DecryptError::ArmorFlag => {
|
||||
writeln!(f, "-a/--armor can't be used with -d/--decrypt.")?;
|
||||
write!(f, "Note that armored files are detected automatically.")
|
||||
}
|
||||
DecryptError::IdentityNotFound(filename) => {
|
||||
write!(f, "Identity file not found: {}", filename)
|
||||
wlnfl!(f, "err-dec-armor-flag")?;
|
||||
wfl!(f, "rec-dec-armor-flag")
|
||||
}
|
||||
DecryptError::IdentityNotFound(filename) => write!(
|
||||
f,
|
||||
"{}",
|
||||
fl!(
|
||||
crate::LANGUAGE_LOADER,
|
||||
"err-dec-identity-not-found",
|
||||
filename = filename.as_str()
|
||||
)
|
||||
),
|
||||
DecryptError::Io(e) => write!(f, "{}", e),
|
||||
DecryptError::MissingIdentities(default_filename) => {
|
||||
writeln!(f, "Missing identities.")?;
|
||||
writeln!(f, "Did you forget to specify -i/--identity?")?;
|
||||
writeln!(f, "You can also store default identities in this file:")?;
|
||||
wlnfl!(f, "err-dec-missing-identities")?;
|
||||
wlnfl!(f, "rec-dec-missing-identities-1")?;
|
||||
wlnfl!(f, "rec-dec-missing-identities-2")?;
|
||||
write!(f, " {}", default_filename)
|
||||
}
|
||||
DecryptError::PassphraseFlag => {
|
||||
writeln!(f, "-p/--passphrase can't be used with -d/--decrypt.")?;
|
||||
write!(
|
||||
f,
|
||||
"Note that passphrase-encrypted files are detected automatically."
|
||||
)
|
||||
wlnfl!(f, "err-dec-passphrase-flag")?;
|
||||
wfl!(f, "rec-dec-passphrase-flag")
|
||||
}
|
||||
DecryptError::PassphraseTimedOut => wfl!(f, "err-passphrase-timed-out"),
|
||||
#[cfg(not(unix))]
|
||||
DecryptError::PassphraseWithoutFileArgument => {
|
||||
writeln!(f, "This file requires a passphrase, and on Windows the")?;
|
||||
writeln!(f, "file to decrypt must be passed as a positional argument")?;
|
||||
write!(f, "when decrypting with a passphrase.")
|
||||
wfl!(f, "err-dec-passphrase-without-file-win")
|
||||
}
|
||||
DecryptError::RecipientFlag => {
|
||||
writeln!(f, "-r/--recipient can't be used with -d/--decrypt.")?;
|
||||
write!(
|
||||
f,
|
||||
"Did you mean to use -i/--identity to specify a private key?"
|
||||
)
|
||||
wlnfl!(f, "err-dec-recipient-flag")?;
|
||||
wfl!(f, "rec-dec-recipient-flag")
|
||||
}
|
||||
DecryptError::TimedOut(source) => write!(f, "Timed out waiting for {}", source),
|
||||
#[cfg(feature = "ssh")]
|
||||
DecryptError::UnsupportedKey(filename, k) => k.display(f, Some(filename.as_str())),
|
||||
}
|
||||
|
@ -176,13 +223,12 @@ impl fmt::Debug for Error {
|
|||
Error::Encryption(e) => writeln!(f, "{}", e)?,
|
||||
}
|
||||
writeln!(f)?;
|
||||
writeln!(
|
||||
f,
|
||||
"[ Did rage not do what you expected? Could an error be more useful? ]"
|
||||
)?;
|
||||
writeln!(f, "[ {} ]", crate::fl!("err-ux-A"))?;
|
||||
write!(
|
||||
f,
|
||||
"[ Tell us: https://str4d.xyz/rage/report ]"
|
||||
"[ {}: https://str4d.xyz/rage/report {} ]",
|
||||
crate::fl!("err-ux-B"),
|
||||
crate::fl!("err-ux-C")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,7 +9,13 @@ use age::{
|
|||
Recipient,
|
||||
};
|
||||
use gumdrop::{Options, ParsingStyle};
|
||||
use i18n_embed::{
|
||||
fluent::{fluent_language_loader, FluentLanguageLoader},
|
||||
DesktopLanguageRequester,
|
||||
};
|
||||
use lazy_static::lazy_static;
|
||||
use log::{error, warn};
|
||||
use rust_embed::RustEmbed;
|
||||
use secrecy::ExposeSecret;
|
||||
use std::collections::HashMap;
|
||||
use std::fs::{read_to_string, File};
|
||||
|
@ -20,6 +26,23 @@ mod error;
|
|||
const ALIAS_PREFIX: &str = "alias:";
|
||||
const GITHUB_PREFIX: &str = "github:";
|
||||
|
||||
#[derive(RustEmbed)]
|
||||
#[folder = "i18n"]
|
||||
struct Translations;
|
||||
|
||||
const TRANSLATIONS: Translations = Translations {};
|
||||
|
||||
lazy_static! {
|
||||
static ref LANGUAGE_LOADER: FluentLanguageLoader = fluent_language_loader!();
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! fl {
|
||||
($message_id:literal) => {{
|
||||
i18n_embed_fl::fl!($crate::LANGUAGE_LOADER, $message_id)
|
||||
}};
|
||||
}
|
||||
|
||||
/// Load map of aliases from the given file, or the default system location
|
||||
/// otherwise.
|
||||
///
|
||||
|
@ -118,8 +141,8 @@ fn read_recipients(
|
|||
} else if arg.starts_with(ALIAS_PREFIX) {
|
||||
#[cfg(not(feature = "unstable"))]
|
||||
{
|
||||
eprintln!("Aliases are unstable.");
|
||||
eprintln!("To test this, build rage with --features unstable");
|
||||
eprintln!("{}", fl!("unstable-aliases"));
|
||||
eprintln!("{}", fl!("test-unstable"));
|
||||
}
|
||||
|
||||
if seen_aliases.contains(&arg) {
|
||||
|
@ -136,8 +159,8 @@ fn read_recipients(
|
|||
} else if arg.starts_with(GITHUB_PREFIX) {
|
||||
#[cfg(not(feature = "unstable"))]
|
||||
{
|
||||
eprintln!("GitHub lookups are unstable, ignoring recipient.");
|
||||
eprintln!("To test this, build rage with --features unstable");
|
||||
eprintln!("{}", fl!("unstable-github"));
|
||||
eprintln!("{}", fl!("test-unstable"));
|
||||
continue;
|
||||
}
|
||||
|
||||
|
@ -235,14 +258,12 @@ fn encrypt(opts: AgeOptions) -> Result<(), error::EncryptError> {
|
|||
match read_or_generate_passphrase() {
|
||||
Ok(Passphrase::Typed(passphrase)) => age::Encryptor::with_user_passphrase(passphrase),
|
||||
Ok(Passphrase::Generated(new_passphrase)) => {
|
||||
eprintln!("Using an autogenerated passphrase:");
|
||||
eprintln!("{}", fl!("autogenerated-passphrase"));
|
||||
eprintln!(" {}", new_passphrase.expose_secret());
|
||||
age::Encryptor::with_user_passphrase(new_passphrase)
|
||||
}
|
||||
Err(pinentry::Error::Cancelled) => return Ok(()),
|
||||
Err(pinentry::Error::Timeout) => {
|
||||
return Err(error::EncryptError::TimedOut("passphrase input".to_owned()))
|
||||
}
|
||||
Err(pinentry::Error::Timeout) => return Err(error::EncryptError::PassphraseTimedOut),
|
||||
Err(pinentry::Error::Encoding(e)) => {
|
||||
// Pretend it is an I/O error
|
||||
return Err(error::EncryptError::Io(io::Error::new(
|
||||
|
@ -347,15 +368,13 @@ fn decrypt(opts: AgeOptions) -> Result<(), error::DecryptError> {
|
|||
}
|
||||
}
|
||||
|
||||
match read_secret("Type passphrase", "Passphrase", None) {
|
||||
match read_secret(&fl!("type-passphrase"), &fl!("prompt-passphrase"), None) {
|
||||
Ok(passphrase) => decryptor
|
||||
.decrypt(&passphrase, opts.max_work_factor)
|
||||
.map_err(|e| e.into())
|
||||
.and_then(|input| write_output(input, output)),
|
||||
Err(pinentry::Error::Cancelled) => Ok(()),
|
||||
Err(pinentry::Error::Timeout) => {
|
||||
Err(error::DecryptError::TimedOut("passphrase input".to_owned()))
|
||||
}
|
||||
Err(pinentry::Error::Timeout) => Err(error::DecryptError::PassphraseTimedOut),
|
||||
Err(pinentry::Error::Encoding(e)) => {
|
||||
// Pretend it is an I/O error
|
||||
Err(error::DecryptError::Io(io::Error::new(
|
||||
|
@ -397,6 +416,9 @@ fn main() -> Result<(), error::Error> {
|
|||
|
||||
env_logger::builder().format_timestamp(None).init();
|
||||
|
||||
let requested_languages = DesktopLanguageRequester::requested_languages();
|
||||
i18n_embed::select(&*LANGUAGE_LOADER, &TRANSLATIONS, &requested_languages).unwrap();
|
||||
|
||||
let args = args().collect::<Vec<_>>();
|
||||
|
||||
let opts = AgeOptions::parse_args(&args[1..], ParsingStyle::default()).unwrap_or_else(|e| {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue