///|
/// Return the stable command-line help text.
pub fn cli_usage() -> String {
let usage =
#|MoonL10n 0.2.0
#|Usage:
#| moon run cmd/main -- validate [locale] [--json]
#| moon run cmd/main -- diff [locale] [--json]
#| moon run cmd/main -- render [name=value ...] [--json]
usage
}
///|
fn command_kind(name : String) -> CliCommandKind? {
match name {
"validate" => Some(Validate)
"diff" => Some(Diff)
"render" => Some(Render)
"help" | "--help" | "-h" => Some(Help)
_ => None
}
}
///|
fn parse_cli_value(value : String) -> Argument {
try @string.parse_int(value) catch {
_ => Text(value)
} noraise {
number => Number(number)
}
}
///|
fn split_argument_pair(value : String) -> (String, String)? {
let name = StringBuilder::new()
let content = StringBuilder::new()
let mut separator = false
for ch in value {
if ch == '=' && !separator {
separator = true
} else if separator {
content.write_char(ch)
} else {
name.write_char(ch)
}
}
if !separator || name.is_empty() {
None
} else {
Some((name.to_string(), content.to_string()))
}
}
///|
/// Parse one `name=value` CLI argument.
pub fn parse_cli_argument(
value : String,
) -> (String, Argument) raise MessageError {
match split_argument_pair(value) {
Some((name, content)) => (name, parse_cli_value(content))
None =>
raise InvalidCommand(
"Expected argument in name=value form, got '\{value}'.",
)
}
}
///|
fn collect_render_arguments(
values : Array[String],
) -> Map[String, Argument] raise MessageError {
let arguments : Map[String, Argument] = Map([])
for value in values {
let (name, argument) = parse_cli_argument(value)
if arguments.contains(name) {
raise InvalidCommand("Duplicate render argument '\{name}'.")
}
arguments[name] = argument
}
arguments
}
///|
fn without_json_flag(args : Array[String]) -> (Array[String], Bool) {
let output : Array[String] = []
let mut json = false
for arg in args {
if arg == "--json" {
json = true
} else {
output.push(arg)
}
}
(output, json)
}
///|
fn parse_validate_request(
kind : CliCommandKind,
args : Array[String],
json : Bool,
) -> CliRequest raise MessageError {
if args.length() < 3 || args.length() > 4 {
raise InvalidCommand(
"validate and diff require two catalog paths and an optional locale.",
)
}
let locale = if args.length() == 4 { Some(args[3]) } else { None }
{
kind,
paths: [args[1], args[2]],
locale,
key: None,
arguments: Map([]),
json,
}
}
///|
fn parse_render_request(
args : Array[String],
json : Bool,
) -> CliRequest raise MessageError {
if args.length() < 4 {
raise InvalidCommand(
"render requires a catalog path, locale, key, and optional name=value arguments.",
)
}
let argument_text : Array[String] = []
for index in 4.. CliRequest raise MessageError {
let (clean, json) = without_json_flag(args)
if clean.length() == 0 {
return {
kind: Help,
paths: [],
locale: None,
key: None,
arguments: Map([]),
json,
}
}
match command_kind(clean[0]) {
Some(Help) =>
{
kind: Help,
paths: [],
locale: None,
key: None,
arguments: Map([]),
json,
}
Some(Validate) => parse_validate_request(Validate, clean, json)
Some(Diff) => parse_validate_request(Diff, clean, json)
Some(Render) => parse_render_request(clean, json)
None => raise InvalidCommand("Unknown command '\{clean[0]}'.")
}
}
///|
pub fn CliCommandKind::name(self : CliCommandKind) -> String {
match self {
Validate => "validate"
Diff => "diff"
Render => "render"
Help => "help"
}
}
///|
pub fn CliRequest::kind(self : CliRequest) -> CliCommandKind {
self.kind
}
///|
pub fn CliRequest::paths(self : CliRequest) -> Array[String] {
self.paths
}
///|
pub fn CliRequest::locale(self : CliRequest) -> String? {
self.locale
}
///|
pub fn CliRequest::key(self : CliRequest) -> String? {
self.key
}
///|
pub fn CliRequest::arguments(self : CliRequest) -> Map[String, Argument] {
let output : Map[String, Argument] = Map([])
for name, value in self.arguments {
output[name] = value
}
output
}
///|
pub fn CliRequest::json(self : CliRequest) -> Bool {
self.json
}
///|
pub fn CliRequest::path(self : CliRequest, index : Int) -> String? {
self.paths.get(index)
}
///|
fn cli_success(stdout : String) -> CliOutput {
{ exit_code: 0, stdout, stderr: "" }
}
///|
fn cli_lint_result(report : LintReport, json : Bool) -> CliOutput {
{
exit_code: report.exit_code(),
stdout: if json {
report.to_json()
} else {
report.to_text()
},
stderr: "",
}
}
///|
fn write_string_array_json(
output : StringBuilder,
values : Array[String],
) -> Unit {
output.write_char('[')
for index, value in values {
if index > 0 {
output.write_char(',')
}
write_json_string(output, value)
}
output.write_char(']')
}
///|
fn catalog_diff_json(diff : CatalogDiff) -> String {
let output = StringBuilder::new()
output.write_char('{')
write_json_string(output, "reference_locale")
output.write_char(':')
write_json_string(output, diff.reference_locale)
output.write_char(',')
write_json_string(output, "translation_locale")
output.write_char(':')
write_json_string(output, diff.translation_locale)
output.write_char(',')
write_json_string(output, "shared")
output.write_char(':')
write_string_array_json(output, diff.shared_keys)
output.write_char(',')
write_json_string(output, "missing")
output.write_char(':')
write_string_array_json(output, diff.missing_keys)
output.write_char(',')
write_json_string(output, "extra")
output.write_char(':')
write_string_array_json(output, diff.extra_keys)
output.write_char('}')
output.to_string()
}
///|
fn catalog_diff_text(diff : CatalogDiff) -> String {
if diff.is_equal() {
return "OK"
}
let output = StringBuilder::new()
for key in diff.missing_keys {
output.write_string("missing-key ")
output.write_string(key)
output.write_char('\n')
}
for key in diff.extra_keys {
output.write_string("extra-key ")
output.write_string(key)
output.write_char('\n')
}
output.write_string(diff.coverage().summary())
output.to_string()
}
///|
fn cli_diff_result(diff : CatalogDiff, json : Bool) -> CliOutput {
let exit_code = if diff.missing_keys.length() > 0 {
2
} else if diff.extra_keys.length() > 0 {
1
} else {
0
}
{
exit_code,
stdout: if json {
catalog_diff_json(diff)
} else {
catalog_diff_text(diff)
},
stderr: "",
}
}
///|
/// Validate two in-memory catalog sources.
pub fn cli_validate_sources(
reference_source : String,
translation_source : String,
locale : String,
json? : Bool = false,
) -> CliOutput raise MessageError {
let reference = Catalog::from_json_lenient("reference", reference_source)
let translation = Catalog::from_json_lenient(locale, translation_source)
cli_lint_result(lint_catalogs(reference, translation), json)
}
///|
/// Compare the key sets of two in-memory catalog sources.
pub fn cli_diff_sources(
reference_source : String,
translation_source : String,
locale : String,
json? : Bool = false,
) -> CliOutput raise MessageError {
let reference = Catalog::from_json_lenient("reference", reference_source)
let translation = Catalog::from_json_lenient(locale, translation_source)
cli_diff_result(diff_catalogs(reference, translation), json)
}
///|
fn translation_result_json(result : TranslationResult) -> String {
let output = StringBuilder::new()
output.write_char('{')
write_json_string(output, "key")
output.write_char(':')
write_json_string(output, result.key)
output.write_char(',')
write_json_string(output, "requested_locale")
output.write_char(':')
write_json_string(output, result.requested_locale)
output.write_char(',')
write_json_string(output, "resolved_locale")
output.write_char(':')
write_json_string(output, result.resolved_locale)
output.write_char(',')
write_json_string(output, "used_fallback")
output.write_char(':')
output.write_string(if result.used_fallback { "true" } else { "false" })
output.write_char(',')
write_json_string(output, "value")
output.write_char(':')
write_json_string(output, result.value)
output.write_char('}')
output.to_string()
}
///|
/// Render one key from an in-memory catalog source.
pub fn cli_render_source(
source : String,
locale : String,
key : String,
arguments : Map[String, Argument],
json? : Bool = false,
) -> CliOutput raise MessageError {
let catalog = Catalog::from_json(locale, source)
let translator = Translator::new(locale, locale)
translator.add_catalog(catalog)
let result = translator.translate_detailed(key, arguments, locale~)
cli_success(if json { translation_result_json(result) } else { result.value })
}
///|
pub fn CliOutput::exit_code(self : CliOutput) -> Int {
self.exit_code
}
///|
pub fn CliOutput::stdout(self : CliOutput) -> String {
self.stdout
}
///|
pub fn CliOutput::stderr(self : CliOutput) -> String {
self.stderr
}
///|
pub fn CliOutput::succeeded(self : CliOutput) -> Bool {
self.exit_code == 0
}
///|
pub fn CliOutput::failed(self : CliOutput) -> Bool {
self.exit_code != 0
}
///|
pub fn CliOutput::display(self : CliOutput) -> String {
if self.stderr == "" {
self.stdout
} else if self.stdout == "" {
self.stderr
} else {
self.stdout + "\n" + self.stderr
}
}