Skip to content

Commit e5086a2

Browse files
authored
Add localization support (#229)
1 parent ef1898a commit e5086a2

File tree

39 files changed

+633
-284
lines changed

39 files changed

+633
-284
lines changed

CHANGELOG.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,10 @@
88
- Added ability to use unicode NEL character (`\u0085`) to manually break lines in help output ([#214](https://github.com/ajalt/clikt/issues/214))
99
- Added `help("")` extension to options and arguments as an alternative to passing the help as an argument ([#207](https://github.com/ajalt/clikt/issues/207))
1010
- Added `valueSourceKey` parameter to `option`
11-
- Added `check{}` extensions to options and arguments as an alternative to `validate`
11+
- Added `check()` extensions to options and arguments as an alternative to `validate()`
1212
- Added `prompt` and `confirm` functions to `CliktCommand` that call the `TermUi` equivalents with the current console.
1313
- Added `echo()` overload with no parameters to CliktCommand that prints a newline by itself.
14+
- Added localization support. You can set an implementation of the `Localization` interface on your context with your translations. ([#227](https://github.com/ajalt/clikt/issues/227))
1415

1516
### Fixed
1617
- Hidden options will no longer be suggested as possible typo corrections. ([#202](https://github.com/ajalt/clikt/issues/202))
@@ -22,10 +23,14 @@
2223
- `Argument.help` and `Option.help` properties have been renamed to `argumentHelp` and `optionHelp`, respectively. The `help` parameter names to `option()` and `argument()` are unchanged.
2324
- `commandHelp` and `commandHelpEpilog` properties on `CliktCommand` are now `open`, so you can choose to override them instead of passing `help` and `epilog` to the constructor.
2425
- Replaced `MapValueSource.defaultKey` with `ValueSource.getKey()`, which is more customizable.
26+
- `Option.metavar`, `Option.parameterHelp`, `OptionGroup.parameterHelp` and `Argument.parameterHelp` properties are now functions.
27+
- Changed constructor parameters of `CliktHelpFormatter`. Added `localization` and removed `usageTitle`, `optionsTitle`, `argumentsTitle`, `commandsTitle`, `optionsMetavar`, and `commandMetavar`. Those strings are now defined on equivalently named functions on `Localization`.
2528

2629
### Removed
2730
- Removed `envvarSplit` parameter from `option()` and `convert()`. Option values from environment variables are no longer split automatically. ([#177](https://github.com/ajalt/clikt/issues/177))
2831
- Removed public constructors from the following classes: `ProcessedArgument`, `OptionWithValues`, `FlagOption`, `CoOccurringOptionGroup`, `ChoiceGroup`, `MutuallyExclusiveOptions`.
32+
- `MissingParameter` exception replaced with `MissingOption` and `MissingArgument`
33+
- Removed `Context.helpOptionMessage`. Override `Localization.helpOptionMessage` and set it on your context instead.
2934

3035
### Deprecated
3136
- `@ExperimentalCompletionCandidates` and `@ExperimentalValueSourceApi` annotations. These APIs no longer require an opt-in.

clikt/src/commonMain/kotlin/com/github/ajalt/clikt/core/CliktCommand.kt

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ abstract class CliktCommand(
102102

103103
if (currentContext.helpOptionNames.isNotEmpty()) {
104104
val names = currentContext.helpOptionNames - registeredOptionNames()
105-
if (names.isNotEmpty()) _options += helpOption(names, currentContext.helpOptionMessage)
105+
if (names.isNotEmpty()) _options += helpOption(names, currentContext.localization.helpOptionMessage())
106106
}
107107

108108
for (command in _subcommands) {
@@ -113,9 +113,9 @@ abstract class CliktCommand(
113113
}
114114

115115
private fun allHelpParams(): List<ParameterHelp> {
116-
return _options.mapNotNull { it.parameterHelp } +
117-
_arguments.mapNotNull { it.parameterHelp } +
118-
_groups.mapNotNull { it.parameterHelp } +
116+
return _options.mapNotNull { it.parameterHelp(currentContext) } +
117+
_arguments.mapNotNull { it.parameterHelp(currentContext) } +
118+
_groups.mapNotNull { it.parameterHelp(currentContext) } +
119119
_subcommands.map { ParameterHelp.Subcommand(it.commandName, it.shortHelp(), it.helpTags) }
120120
}
121121

@@ -417,7 +417,7 @@ abstract class CliktCommand(
417417
echo(e.message, err = true)
418418
exitProcessMpp(1)
419419
} catch (e: Abort) {
420-
echo("Aborted!", err = true)
420+
echo(currentContext.localization.aborted(), err = true)
421421
exitProcessMpp(if (e.error) 1 else 0)
422422
}
423423
}

clikt/src/commonMain/kotlin/com/github/ajalt/clikt/core/Context.kt

Lines changed: 16 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,9 @@
11
package com.github.ajalt.clikt.core
22

3-
import com.github.ajalt.clikt.output.CliktConsole
4-
import com.github.ajalt.clikt.output.CliktHelpFormatter
5-
import com.github.ajalt.clikt.output.HelpFormatter
6-
import com.github.ajalt.clikt.output.defaultCliktConsole
3+
import com.github.ajalt.clikt.output.*
74
import com.github.ajalt.clikt.sources.ChainedValueSource
85
import com.github.ajalt.clikt.sources.ValueSource
96
import kotlin.properties.ReadOnlyProperty
10-
import kotlin.reflect.KProperty
117

128
typealias TypoSuggestor = (enteredValue: String, possibleValues: List<String>) -> List<String>
139

@@ -27,7 +23,6 @@ typealias TypoSuggestor = (enteredValue: String, possibleValues: List<String>) -
2723
* @property helpOptionNames The names to use for the help option. If any names in the set conflict with other
2824
* options, the conflicting name will not be used for the help option. If the set is empty, or contains no
2925
* unique names, no help option will be added.
30-
* @property helpOptionMessage The description of the help option.
3126
* @property helpFormatter The help formatter for this command.
3227
* @property tokenTransformer An optional transformation function that is called to transform command line
3328
* tokens (options and commands) before parsing. This can be used to implement e.g. case insensitive
@@ -46,14 +41,14 @@ class Context(
4641
val autoEnvvarPrefix: String?,
4742
val printExtraMessages: Boolean,
4843
val helpOptionNames: Set<String>,
49-
val helpOptionMessage: String,
5044
val helpFormatter: HelpFormatter,
5145
val tokenTransformer: Context.(String) -> String,
5246
val console: CliktConsole,
5347
val expandArgumentFiles: Boolean,
5448
val readEnvvarBeforeValueSource: Boolean,
5549
val valueSource: ValueSource?,
56-
val correctionSuggestor: TypoSuggestor
50+
val correctionSuggestor: TypoSuggestor,
51+
val localization: Localization
5752
) {
5853
var invokedSubcommand: CliktCommand? = null
5954
internal set
@@ -118,11 +113,8 @@ class Context(
118113
*/
119114
var helpOptionNames: Set<String> = parent?.helpOptionNames ?: setOf("-h", "--help")
120115

121-
/** The description of the help option.*/
122-
var helpOptionMessage: String = parent?.helpOptionMessage ?: "Show this message and exit"
123-
124-
/** The help formatter for this command*/
125-
var helpFormatter: HelpFormatter = parent?.helpFormatter ?: CliktHelpFormatter()
116+
/** The help formatter for this command, or null to use the default */
117+
var helpFormatter: HelpFormatter? = parent?.helpFormatter
126118

127119
/** An optional transformation function that is called to transform command line */
128120
var tokenTransformer: Context.(String) -> String = parent?.tokenTransformer ?: { it }
@@ -180,6 +172,11 @@ class Context(
180172
* names and filters the list down to values to suggest to the user.
181173
*/
182174
var correctionSuggestor: TypoSuggestor = DEFAULT_CORRECTION_SUGGESTOR
175+
176+
/**
177+
* Localized strings to use for help output and error reporting.
178+
*/
179+
var localization: Localization = defaultLocalization
183180
}
184181

185182
companion object {
@@ -188,10 +185,11 @@ class Context(
188185
block()
189186
val interspersed = allowInterspersedArgs && !command.allowMultipleSubcommands &&
190187
parent?.let { p -> p.ancestors().any { it.command.allowMultipleSubcommands } } != true
188+
val formatter = helpFormatter ?: CliktHelpFormatter(localization)
191189
return Context(
192190
parent, command, interspersed, autoEnvvarPrefix, printExtraMessages,
193-
helpOptionNames, helpOptionMessage, helpFormatter, tokenTransformer, console,
194-
expandArgumentFiles, readEnvvarBeforeValueSource, valueSource, correctionSuggestor
191+
helpOptionNames, formatter, tokenTransformer, console, expandArgumentFiles,
192+
readEnvvarBeforeValueSource, valueSource, correctionSuggestor, localization
195193
)
196194
}
197195
}
@@ -201,21 +199,13 @@ class Context(
201199
/** Find the closest object of type [T], or throw a [NullPointerException] */
202200
@Suppress("unused") // these extensions don't use their receiver, but we want to limit where they can be called
203201
inline fun <reified T : Any> CliktCommand.requireObject(): ReadOnlyProperty<CliktCommand, T> {
204-
return object : ReadOnlyProperty<CliktCommand, T> {
205-
override fun getValue(thisRef: CliktCommand, property: KProperty<*>): T {
206-
return thisRef.currentContext.findObject<T>()!!
207-
}
208-
}
202+
return ReadOnlyProperty<CliktCommand, T> { thisRef, _ -> thisRef.currentContext.findObject<T>()!! }
209203
}
210204

211205
/** Find the closest object of type [T], or null */
212206
@Suppress("unused")
213207
inline fun <reified T : Any> CliktCommand.findObject(): ReadOnlyProperty<CliktCommand, T?> {
214-
return object : ReadOnlyProperty<CliktCommand, T?> {
215-
override fun getValue(thisRef: CliktCommand, property: KProperty<*>): T? {
216-
return thisRef.currentContext.findObject<T>()
217-
}
218-
}
208+
return ReadOnlyProperty { thisRef, _ -> thisRef.currentContext.findObject<T>() }
219209
}
220210

221211
/**
@@ -228,11 +218,7 @@ inline fun <reified T : Any> CliktCommand.findObject(): ReadOnlyProperty<CliktCo
228218
*/
229219
@Suppress("unused")
230220
inline fun <reified T : Any> CliktCommand.findOrSetObject(crossinline default: () -> T): ReadOnlyProperty<CliktCommand, T> {
231-
return object : ReadOnlyProperty<CliktCommand, T> {
232-
override fun getValue(thisRef: CliktCommand, property: KProperty<*>): T {
233-
return thisRef.currentContext.findOrSetObject(default)
234-
}
235-
}
221+
return ReadOnlyProperty { thisRef, _ -> thisRef.currentContext.findOrSetObject(default) }
236222
}
237223

238224
private val DEFAULT_CORRECTION_SUGGESTOR: TypoSuggestor = { enteredValue, possibleValues ->

clikt/src/commonMain/kotlin/com/github/ajalt/clikt/core/exceptions.kt

Lines changed: 67 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.github.ajalt.clikt.core
22

3+
import com.github.ajalt.clikt.output.defaultLocalization
34
import com.github.ajalt.clikt.parameters.arguments.Argument
45
import com.github.ajalt.clikt.parameters.arguments.convert
56
import com.github.ajalt.clikt.parameters.options.Option
@@ -85,7 +86,7 @@ open class UsageError private constructor(
8586

8687
fun helpMessage(): String = buildString {
8788
context?.let { append(it.command.getFormattedUsage()).append("\n\n") }
88-
append("Error: ").append(formatMessage())
89+
append(localization.usageError(formatMessage()))
8990
}
9091

9192
override val message: String? get() = formatMessage()
@@ -98,115 +99,125 @@ open class UsageError private constructor(
9899
argument != null -> argument!!.name
99100
else -> ""
100101
}
102+
103+
protected val localization get() = context?.localization ?: defaultLocalization
101104
}
102105

103106
/**
104107
* A parameter was given the correct number of values, but of invalid format or type.
105108
*/
106-
open class BadParameterValue : UsageError {
109+
class BadParameterValue : UsageError {
107110
constructor(text: String, context: Context? = null) : super(text, null, context)
108111
constructor(text: String, paramName: String, context: Context? = null) : super(text, paramName, context)
109112
constructor(text: String, argument: Argument, context: Context? = null) : super(text, argument, context)
110113
constructor(text: String, option: Option, context: Context? = null) : super(text, option, context)
111114

112115
override fun formatMessage(): String {
113-
val error = if (text.isNullOrBlank()) "" else ": $text"
114-
if (inferParamName().isEmpty()) return "Invalid value$error"
115-
return "Invalid value for \"${inferParamName()}\"$error"
116+
val m = text.takeUnless { it.isNullOrBlank() }
117+
val p = inferParamName().takeIf { it.isNotBlank() }
118+
119+
return when {
120+
m == null && p == null -> localization.badParameter()
121+
m == null && p != null -> localization.badParameterWithParam(p)
122+
m != null && p == null -> localization.badParameterWithMessage(m)
123+
m != null && p != null -> localization.badParameterWithMessageAndParam(p, m)
124+
else -> error("impossible")
125+
}
116126
}
117127
}
118128

119-
/** A required parameter was not provided */
120-
open class MissingParameter : UsageError {
121-
constructor(argument: Argument, context: Context? = null) : super("", argument, context) {
122-
this.paramType = "argument"
123-
}
124-
125-
constructor(option: Option, context: Context? = null) : super("", option, context) {
126-
this.paramType = "option"
127-
}
128-
129-
private val paramType: String
129+
/** A required option was not provided */
130+
class MissingOption(option: Option, context: Context? = null) : UsageError("", option, context) {
131+
override fun formatMessage() = localization.missingOption(inferParamName())
132+
}
130133

131-
override fun formatMessage(): String {
132-
return "Missing $paramType \"${inferParamName()}\"."
133-
}
134+
/** A required argument was not provided */
135+
class MissingArgument(argument: Argument, context: Context? = null) : UsageError("", argument, context) {
136+
override fun formatMessage() = localization.missingArgument(inferParamName())
134137
}
135138

136139
/** A parameter was provided that does not exist. */
137-
open class NoSuchParameter(
138-
protected val parameterType: String,
139-
protected val givenName: String,
140-
protected val possibilities: List<String> = emptyList(),
140+
open class NoSuchParameter protected constructor(context: Context?) : UsageError("", context = context)
141+
142+
/** A subcommand was provided that does not exist. */
143+
class NoSuchSubcommand(
144+
private val givenName: String,
145+
private val possibilities: List<String> = emptyList(),
141146
context: Context? = null
142-
) : UsageError("", context = context) {
147+
) : NoSuchParameter(context) {
143148
override fun formatMessage(): String {
144-
return "no such ${parameterType}: \"$givenName\"." + when {
145-
possibilities.size == 1 -> " Did you mean \"${possibilities[0]}\"?"
146-
possibilities.size > 1 -> possibilities.joinToString(
147-
prefix = " (Possible ${parameterType}s: ", postfix = ")")
148-
else -> ""
149-
}
149+
return localization.noSuchSubcommand(givenName, possibilities)
150150
}
151151
}
152152

153-
/** A subcommand was provided that does not exist. */
154-
open class NoSuchSubcommand(
155-
givenName: String,
156-
possibilities: List<String> = emptyList(),
157-
context: Context? = null
158-
) : NoSuchParameter("subcommand", givenName, possibilities, context)
159-
160153

161154
/** An option was provided that does not exist. */
162-
open class NoSuchOption(
163-
givenName: String,
164-
possibilities: List<String> = emptyList(),
155+
class NoSuchOption(
156+
private val givenName: String,
157+
private val possibilities: List<String> = emptyList(),
165158
context: Context? = null
166-
) : NoSuchParameter("option", givenName, possibilities, context)
159+
) : NoSuchParameter(context) {
160+
override fun formatMessage(): String {
161+
return localization.noSuchOption(givenName, possibilities)
162+
}
163+
}
164+
167165

168166
/** An option was supplied but the number of values supplied to the option was incorrect. */
169-
open class IncorrectOptionValueCount(
167+
class IncorrectOptionValueCount(
170168
option: Option,
171169
private val givenName: String,
172170
context: Context? = null
173171
) : UsageError("", option, context) {
174172
override fun formatMessage(): String {
175-
return when (option!!.nvalues) {
176-
0 -> "$givenName option does not take a value"
177-
1 -> "$givenName option requires an argument"
178-
else -> "$givenName option requires ${option!!.nvalues} arguments"
179-
}
173+
return localization.incorrectOptionValueCount(givenName, option!!.nvalues)
180174
}
181175
}
182176

183177
/** An argument was supplied but the number of values supplied was incorrect. */
184-
open class IncorrectArgumentValueCount(
178+
class IncorrectArgumentValueCount(
185179
argument: Argument,
186180
context: Context? = null
187181
) : UsageError("", argument, context) {
188182
override fun formatMessage(): String {
189-
return "argument ${inferParamName()} takes ${argument!!.nvalues} values"
183+
return localization.incorrectArgumentValueCount(inferParamName(), argument!!.nvalues)
190184
}
191185
}
192186

193-
open class MutuallyExclusiveGroupException(
194-
protected val names: List<String>,
187+
class MutuallyExclusiveGroupException(
188+
private val names: List<String>,
195189
context: Context? = null
196190
) : UsageError("", context = context) {
197191
init {
198192
require(names.size > 1) { "must provide at least two names" }
199193
}
200194

201195
override fun formatMessage(): String {
202-
return "option ${names.first()} cannot be used with ${names.drop(1).joinToString(" or ")}"
196+
return localization.mutexGroupException(names.first(), names.drop(1))
203197
}
204198
}
205199

206200
/** A required configuration file was not found. */
207-
class FileNotFound(filename: String) : UsageError("$filename not found")
201+
class FileNotFound(
202+
private val filename: String,
203+
context: Context? = null
204+
) : UsageError("", context = context) {
205+
override fun formatMessage(): String {
206+
return localization.fileNotFound(filename)
207+
}
208+
}
208209

209210
/** A configuration file failed to parse correctly */
210-
class InvalidFileFormat(filename: String, message: String, lineno: Int? = null) : UsageError(
211-
"incorrect format in file $filename${lineno?.let { " line $it" } ?: ""}}: $message"
212-
)
211+
class InvalidFileFormat(
212+
private val filename: String,
213+
message: String,
214+
private val lineno: Int? = null,
215+
context: Context? = null
216+
) : UsageError(message, context = context) {
217+
override fun formatMessage(): String {
218+
return when (lineno) {
219+
null -> localization.invalidFileFormat(filename, text!!)
220+
else -> localization.invalidFileFormat(filename, lineno, text!!)
221+
}
222+
}
223+
}

0 commit comments

Comments
 (0)