-
-
Notifications
You must be signed in to change notification settings - Fork 1.1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
POC of Approach to Supporting Internationalization #5853
base: master
Are you sure you want to change the base?
Conversation
**Problem:** Visible alias completions were causing duplicate case conditions when offering subcommand completions, due to the fact that they share the same function name as the command they are aliasing. **Solution:** Deduplicate them so as to not redefine the exact same case statement logic.
@@ -189,6 +189,8 @@ snapbox = "0.6.16" | |||
shlex = "1.3.0" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just adding a new example to demonstrate, and some dev-deps to support the example.
@@ -1,6 +1,6 @@ | |||
#![allow(elided_lifetimes_in_paths)] // needed for divan | |||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You'll see a lot of cases of text_provider::DEFAULT_TEXT_PROVIDER
being added throughout this PR. This is just to resolve compile issues. The final implementation would ideally be structured in a way to avoid needing to make these changes; however, to keep the POC simple I opted to not resolve that issue until later.
@@ -67,6 +67,8 @@ anstyle = "1.0.8" | |||
terminal_size = { version = "0.4.0", optional = true } | |||
backtrace = { version = "0.3.73", optional = true } | |||
unicode-width = { version = "0.2.0", optional = true } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
New depts. serde_yml is for deserializing the external config file where the hardcoded texts are extracted to.
@@ -4729,7 +4734,7 @@ impl Command { | |||
.help("Print help (see more with '--help')") | |||
.long_help("Print help (see a summary with '-h')"); | |||
} else { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is an example of one case where we have removed hardcoded English text and replaced it with a template string containing a text lookup key.
@@ -29,6 +31,11 @@ impl StyledStr { | |||
Self(String::new()) | |||
} | |||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This new interpolate function replaces the text lookup keys with the actual texts. There is probably a better place to put this, but for the purposes of demonstrating the approach, I thought this was good enough.
@@ -28,15 +29,22 @@ use std::ffi::OsString; | |||
pub trait Parser: FromArgMatches + CommandFactory + Sized { | |||
/// Parse from `std::env::args_os()`, [exit][Error::exit] on error. | |||
fn parse() -> Self { | |||
let mut matches = <Self as CommandFactory>::command().get_matches(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
New parse_with_texts is the API which allows a consumer to substitute the OOTB English texts with their own texts. This could perhaps be adjusted to allow a reference to the texts to be added to the Parser struct itself so that we don't need to pass them down through many layers of function calls as we do here. This change might have required making changes to clap_derive in order to get a minimally working example ready, so I decided against it.
@@ -198,7 +198,7 @@ impl<'cmd, 'writer> HelpTemplate<'cmd, 'writer> { | |||
"usage-heading" => { | |||
let _ = write!( | |||
self.writer, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
More hardcoded texts replaced with text lookup keys.
@@ -0,0 +1,48 @@ | |||
//! Utilities for loading help texts from external sources. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Simple implementation of what the API for allowing consumers to pass in their own texts might look like.
@@ -0,0 +1,90 @@ | |||
use crate::text_provider::TextProvider; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Simple template engine for interpolating the actual texts into the templates using the text lookup keys. Hand-rolled this to avoid pulling in a whole templating engine, which would include many more features that what we actually need for this case.
Some improvements to this might be to allow escaping {
. Also would need some tests since this hastilly written example might have a few bugs lurking in it. But again, this is just a minimal example for the POC.
@@ -0,0 +1,10 @@ | |||
clap.usage.header-text: Usage |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These are the hard-coded default texts that I extracted. You'll note that there are many more texts than these -- I just extracted enough to demonstrate using the example that I included in this PR.
use rust_i18n::Backend; | ||
use std::path::PathBuf; | ||
use clap::{Parser, Subcommand}; | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For this example, we use the rust-i18n crate to demonstrate how we can use the TextProvider API to achieve internationalization. I chose this crate because it allows looking translations at runtime, which is probably what most people would want since it avoids packaging languages into the binary that the end user will never use.
#[derive(Parser)] | ||
#[command(version, about, long_about = None)] | ||
struct Cli { | ||
/// {name-help-text} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Notice that we need to provide a template string with the text lookup key inside in order to have Clap interpolate the text. This may not be ideal, but it was the simplest implementation as it didn't require making any changes to clap_derive.
} | ||
|
||
fn main() { | ||
let locale = env::var("LOCALE").unwrap_or("en".into()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Allow the user to change the locale using an environment variable.
let cli = Cli::parse_with_texts(&I18n); | ||
// You can check the value provided by positional arguments, or option arguments | ||
if let Some(name) = cli.name.as_deref() { | ||
println!("Value for name: {name}"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The user would be free to continue using rust-i18n to internationalize this and the following printlines as well.
@@ -137,6 +137,9 @@ fn arg_required_else_help_with_default() { | |||
); | |||
} | |||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This test shows a weakness of the current simple implementation -- the end user is able to extract the text from the error message without calling the print method, which is where I currently carry out interpolation. Will need to find a more ideal place to put that logic so that users can never extract text from Clap that hasn't been interpolated.
Support for Internationalization
Issue: #380
Provides a POC demonstrating a method for adding i18n support to Clap in a minimally invasive way. This POC is not a complete solution and only aims to illustrate the approach for evaluation of the project's maintainers. Additional work will be needed to produce a production-ready solution.
General Notes
For context, this approach is not an internationlization solution. Rather, it is a solution which extracts the hardcoded English text of the library into external configuration files, and then offers an API for consumers to replace the texts with their own. It does not care about the content of the texts, giving the consumer the freedom to substitute content for any reason and under any conditions they choose; only one such use of this is for internationalization.
Trying It Out
To get a quick look at the results, you can run the accompanying example. You'll note that the alignment of the of the columns is a bit off -- this is a consequence of the current POC implementation. If the general approach is accepted, then I will consider how to resolve this.
Approach
The main design goal of this implementation is to be minimally invasive. By minimally invasive, I mean that I do not want to significantly impact how Clap already works.
Current Behavior
Clap currently produces output in the following stages:
This implementation changes step 2 and adds a new step between it and step 3.
{clap.default-help-text}
Note that the current implementation tends to break column alignments since the widths of the text keys are often different from the actual text, and it appears that Step 2 aligns the columes based on the input text. Some consideration will be needed to determine how best to resolve this issue but this can be deferred until I get the go ahead from the Clap team.