remkop / picocli

Picocli is a modern framework for building powerful, user-friendly, GraalVM-enabled command line apps with ease. It supports colors, autocompletion, subcommands, and more. In 1 source file so apps can include as source & avoid adding a dependency. Written in Java, usable from Groovy, Kotlin, Scala, etc.
https://picocli.info
Apache License 2.0
4.95k stars 424 forks source link

Is there any possibility to localize error messages? #1946

Open lukaseckert opened 1 year ago

lukaseckert commented 1 year ago

Hi, first, thanks for this great piece of software! Currently I'm trying to localize the most common messages which a user might see when using a picocli-based app. Via the resource bundle, it was easy to translate options, usage help, etc. šŸ‘ However this doesn't work for error/help messages, resulting in a mix of English and German in the output:

Unknown option: '--help' Aufruf: my-cli-ap ... Hier kommt eine Beschreibung auf Deutsch. Optionen: -n, --node=

I see that "Unknown option:" is hard-coded in CommandLine.UnmatchedArgumentException.describe() (line 18669), and requires some special handling for the plural, which cannot be solved by simple resource files. Then I checked if it would be possible so subclass the Interpreter to override describe(). This is obviously not a good idea as describe() does more than only building the string, and is also impossible as there is not setter for an extended Interpreter implementation.

I wondered if there are any plans to offer an extension point where a custom "MessageProvider" may be set. This interface could then have methods like buildUnknownOptionMessage(String optionName, boolean isPlural) and be implemented for most languages. The same applies for many more methods, e.g. readUserInput() ("Enter value for %s: ")

remkop commented 1 year ago

Indeed, this has been on the todo list for a long time. See #485.

Some error messages are dynamically constructed but it should be possible to capture all patterns in MessageFormat.

It'll be some work though and my time to spend on picocli is extremely limited.

I'd be happy to review pull requests if anyone is interested in working on this.

@lukaseckert will you be able to provide a pull request?

lukaseckert commented 1 year ago

Hi @remkop , I can give it a try, however this may take some time as I'm doing my first steps with picocli and also do have to work on it in my spare time ;) I'm not sure what you mean by "should be possible to capture all patterns in MessageFormat".

remkop commented 1 year ago

I'm not sure what you mean by "should be possible to capture all patterns in MessageFormat".

For example, there is some of the current picocli code, showing a method that dynamically creates an error message:

// current picocli code:
private static String createMissingParameterMessage(ArgSpec argSpec, Range arity, List<PositionalParamSpec> missingList, Stack<String> args, int available) {
    if (arity.min == 1) {
        if (argSpec.isOption()) {
            return "Missing required parameter for " + optionDescription("", argSpec, 0);
        }
        String sep = "";
        String names = ": ";
        String indices = "";
        String infix = " at index ";
        int count = 0;
        for (PositionalParamSpec missing : missingList) {
            if (missing.arity().min > 0) {
                names += sep + "'" + missing.paramLabel() + "'";
                indices += sep + missing.index();
                sep = ", ";
                count++;
            }
        }
        String msg = "Missing required parameter";
        if (count > 1 || arity.min - available > 1) {
            msg += "s";
        }
        if (count > 1) { infix = " at indices "; }
        return System.getProperty("picocli.verbose.errors") != null ? msg + names + infix + indices : msg + names;

    } else if (args.isEmpty()) {
        return optionDescription("", argSpec, 0) +
                " requires at least " + arity.min + " values, but none were specified.";
    } else {
        return optionDescription("", argSpec, 0) +
                " requires at least " + arity.min + " values, but only " + available + " were specified: " + reverse(args);
    }
}

In the resource bundle, we would have to have key-value pairs for all possible permutations that could result from this. The values would all be in MessageFormat:

missingParam1=Missing required parameter for {0}
missingParamMulti1=Missing required parameter {0} at index {1,number}
missingParamMulti=Missing required parameters {0} at indices {1}
missingParamMulti1Short=Missing required parameter {0}
missingParamMultiShort=Missing required parameters {0}
requiredAllMissing={0} requires at least {1,number} values, but none were specified.
requiredSomeMissing={0} requires at least {1,number} values, but only {2,number} were specified: {3}

Then, the logic in that method would have the same conditionals, but, instead of constructing the English message, it would select the correct key, get the pattern from the resource bundle, and then format the result, something like this:

// future picocli code for createMissingParameterMessage
private static String createMissingParameterMessage(ArgSpec argSpec, Range arity, List<PositionalParamSpec> missingList, Stack<String> args, int available) {
...
    } else if (args.isEmpty()) {
        return MessageFormat.format(bundle.getString("requiredAllMissing"), 
                optionDescription("", argSpec, 0), arity.min);
    } else {
        return MessageFormat.format(bundle.getString("requiredSomeMissing"), 
                optionDescription("", argSpec, 0), arity.min, available, reverse(args));
    }
}

I can give it a try, however this may take some time as I'm doing my first steps with picocli and also do have to work on it in my spare time ;)

Sure, any help is welcome. Picocli is entirely done in my spare time also, so I fully understand! šŸ˜