Create a little application which manages an l10n messages database #290
@ -5,3 +5,18 @@ description = "This is a label on a button which will save the settings when cli
|
|||||||
locale = "en"
|
locale = "en"
|
||||||
content = "Save Settings"
|
content = "Save Settings"
|
||||||
modified = "2025-02-22T23:44:18.874218939Z"
|
modified = "2025-02-22T23:44:18.874218939Z"
|
||||||
|
|
||||||
|
[variants.eo]
|
||||||
|
locale = "eo"
|
||||||
|
content = "Konservi Agordojn"
|
||||||
|
modified = "2025-02-24T19:32:11.246639077Z"
|
||||||
|
|
||||||
|
[variants.de]
|
||||||
|
locale = "de"
|
||||||
|
content = "Einstellungen Speichern"
|
||||||
|
modified = "2025-02-24T19:33:19.516005843Z"
|
||||||
|
|
||||||
|
[variants.es]
|
||||||
|
locale = "es"
|
||||||
|
content = "Guardar Configuraciones"
|
||||||
|
modified = "2025-02-24T19:33:23.861329923Z"
|
||||||
|
@ -5,3 +5,18 @@ description = "A summary of a workout or many workouts that involve a time and a
|
|||||||
locale = "en"
|
locale = "en"
|
||||||
content = "{distance} of {activity} in {hours, plural, =1 {{hours} hour} other {{hours} hours}} and {minutes, plural, =1 {{minutes} minute} other {{minutes} minutes}}"
|
content = "{distance} of {activity} in {hours, plural, =1 {{hours} hour} other {{hours} hours}} and {minutes, plural, =1 {{minutes} minute} other {{minutes} minutes}}"
|
||||||
modified = "2025-02-24T14:09:17.361641899Z"
|
modified = "2025-02-24T14:09:17.361641899Z"
|
||||||
|
|
||||||
|
[variants.eo]
|
||||||
|
locale = "eo"
|
||||||
|
content = "{distance} de {activity} en {hours, plural, =1 {{hours} horo} other {{hours} horoj}} {minutes, plural, =1 {{minutes} minuto} other {{minutes} minutoj}}"
|
||||||
|
modified = "2025-02-24T19:32:11.246943602Z"
|
||||||
|
|
||||||
|
[variants.de]
|
||||||
|
locale = "de"
|
||||||
|
content = "{distance} von {activity} in {hours, plural, one {}=1 {{hours} Stunde} other {{hours} Stunden}} und {minutes, plural, one {}=1 {{minutes} Minute} other {{minutes} Minuten}}"
|
||||||
|
modified = "2025-02-24T19:33:19.516210807Z"
|
||||||
|
|
||||||
|
[variants.es]
|
||||||
|
locale = "es"
|
||||||
|
content = "{distance} de {activity} en {hours, plural, one {}=1 {{hours} hora} other {{hours} horas}} y {minutes, plural, one {}=1 {{minutes} minuto} other {{minutes} minutos}}"
|
||||||
|
modified = "2025-02-24T19:33:23.861604738Z"
|
||||||
|
@ -1,7 +1,22 @@
|
|||||||
key = "Welcome"
|
key = "Welcome"
|
||||||
description = "This is a welcome content that will be shown on first app opening, before configuration."
|
description = "This is a welcome content that will be shown on first app opening, before configuration."
|
||||||
|
|
||||||
|
[variants.eo]
|
||||||
|
locale = "eo"
|
||||||
|
content = "Bonvenon al FitnessTrax"
|
||||||
|
modified = "2025-02-24T19:32:11.246407627Z"
|
||||||
|
|
||||||
[variants.en]
|
[variants.en]
|
||||||
locale = "en"
|
locale = "en"
|
||||||
content = "Welcome to FitnessTrax"
|
content = "Welcome to FitnessTrax"
|
||||||
modified = "2025-02-22T23:43:24.786544124Z"
|
modified = "2025-02-22T23:43:24.786544124Z"
|
||||||
|
|
||||||
|
[variants.es]
|
||||||
|
locale = "es"
|
||||||
|
content = "Bienvenido a FitnessTrax"
|
||||||
|
modified = "2025-02-24T19:33:23.861143003Z"
|
||||||
|
|
||||||
|
[variants.de]
|
||||||
|
locale = "de"
|
||||||
|
content = "Willkommen bei FitnessTrax"
|
||||||
|
modified = "2025-02-24T19:33:19.515861453Z"
|
||||||
|
@ -7,7 +7,7 @@ use std::{
|
|||||||
use clap::{Parser, Subcommand};
|
use clap::{Parser, Subcommand};
|
||||||
|
|
||||||
use icu_locid::{langid, LanguageIdentifier};
|
use icu_locid::{langid, LanguageIdentifier};
|
||||||
use l10n_db::{self, js, read_file, xliff, Bundle, Editor, ReadError};
|
use l10n_db::{self, js, read_file, xliff::{self, import_file}, Bundle, Editor, ReadError};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
|
||||||
#[derive(Parser)]
|
#[derive(Parser)]
|
||||||
@ -30,6 +30,10 @@ enum Commands {
|
|||||||
ListKeys,
|
ListKeys,
|
||||||
// Search the database
|
// Search the database
|
||||||
// Search { },
|
// Search { },
|
||||||
|
Import {
|
||||||
|
#[arg(short, long)]
|
||||||
|
file: String,
|
||||||
|
},
|
||||||
/// Export the database
|
/// Export the database
|
||||||
Export {
|
Export {
|
||||||
#[arg(short, long)]
|
#[arg(short, long)]
|
||||||
@ -74,6 +78,10 @@ fn main() {
|
|||||||
println!("{}", key);
|
println!("{}", key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Some(Commands::Import { file }) => {
|
||||||
|
import_file(&mut bundle, &PathBuf::from(file)).unwrap();
|
||||||
|
bundle.save();
|
||||||
|
}
|
||||||
Some(Commands::Export { format, locale }) => {
|
Some(Commands::Export { format, locale }) => {
|
||||||
let locale = locale.as_ref().map(|l| l.clone().parse::<LanguageIdentifier>().unwrap()).unwrap_or(langid!("en"));
|
let locale = locale.as_ref().map(|l| l.clone().parse::<LanguageIdentifier>().unwrap()).unwrap_or(langid!("en"));
|
||||||
|
|
||||||
|
@ -16,7 +16,7 @@ pub fn export_fh(bundle: &Bundle, locale: LanguageIdentifier, fh: &mut File) ->
|
|||||||
}).collect::<Vec<(String, String)>>();
|
}).collect::<Vec<(String, String)>>();
|
||||||
|
|
||||||
let messages: BTreeMap<String, String> = messages.into_iter().collect();
|
let messages: BTreeMap<String, String> = messages.into_iter().collect();
|
||||||
fh.write(serde_json::to_string(&messages).unwrap().as_bytes()).unwrap();
|
fh.write(serde_json::to_string_pretty(&messages).unwrap().as_bytes()).unwrap();
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,24 @@
|
|||||||
use std::{fs::File, io, path::Path};
|
use std::{
|
||||||
|
collections::HashMap,
|
||||||
|
fs::File,
|
||||||
|
io::{self, BufReader, Read, Write},
|
||||||
|
path::Path,
|
||||||
|
};
|
||||||
|
|
||||||
use icu_locid::LanguageIdentifier;
|
use chrono::{DateTime, Utc};
|
||||||
use xml::{writer::XmlEvent, EmitterConfig, EventWriter};
|
use icu_locid::{langid, LanguageIdentifier};
|
||||||
|
use xml::{attribute::OwnedAttribute, reader, writer, EmitterConfig, EventReader, EventWriter};
|
||||||
|
|
||||||
use crate::{Bundle, Message, WriteError};
|
use crate::{Bundle, Message, ReadError, WriteError};
|
||||||
|
|
||||||
|
struct PartialMessage {
|
||||||
|
variants: HashMap<LanguageIdentifier, PartialVariant>,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct PartialVariant {
|
||||||
|
content: Option<String>,
|
||||||
|
modified: Option<DateTime<Utc>>,
|
||||||
|
}
|
||||||
|
|
||||||
pub fn export_file(
|
pub fn export_file(
|
||||||
bundle: &Bundle,
|
bundle: &Bundle,
|
||||||
@ -14,33 +29,93 @@ pub fn export_file(
|
|||||||
export_fh(bundle, locale, &mut file)
|
export_fh(bundle, locale, &mut file)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn export_fh(
|
pub fn export_fh<W>(bundle: &Bundle, locale: LanguageIdentifier, fh: W) -> Result<(), WriteError>
|
||||||
bundle: &Bundle,
|
where
|
||||||
locale: LanguageIdentifier,
|
W: Write,
|
||||||
fh: &mut File,
|
{
|
||||||
) -> Result<(), WriteError> {
|
let mut writer = EmitterConfig::new().perform_indent(true).create_writer(fh);
|
||||||
let mut writer = EmitterConfig::new()
|
|
||||||
.perform_indent(true)
|
|
||||||
.create_writer(fh);
|
|
||||||
|
|
||||||
writer
|
writer
|
||||||
.write(
|
.write(
|
||||||
XmlEvent::start_element("xliff")
|
writer::XmlEvent::start_element("xliff")
|
||||||
.attr("xmlns", "urn:oasis:names:tc:xliff:document:2.0")
|
.attr("xmlns", "urn:oasis:names:tc:xliff:document:2.0")
|
||||||
.attr("version", "2.0")
|
.attr("version", "2.0")
|
||||||
.attr("srcLang", &format!("{}", locale)),
|
.attr("srcLang", &format!("{}", locale)),
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
writer
|
writer
|
||||||
.write(XmlEvent::start_element("file").attr("id", "main"))
|
.write(writer::XmlEvent::start_element("file").attr("id", "main"))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
for (key, message) in bundle.message_iter() {
|
for (key, message) in bundle.message_iter() {
|
||||||
write_message(&mut writer, key, message, &locale);
|
write_message(&mut writer, key, message, &locale);
|
||||||
}
|
}
|
||||||
|
|
||||||
writer.write(XmlEvent::end_element()).unwrap();
|
writer.write(writer::XmlEvent::end_element()).unwrap();
|
||||||
writer.write(XmlEvent::end_element()).unwrap();
|
writer.write(writer::XmlEvent::end_element()).unwrap();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn import_file(bundle: &mut Bundle, path: &Path) -> Result<(), ReadError> {
|
||||||
|
let file = File::open(path).unwrap();
|
||||||
|
let file = BufReader::new(file);
|
||||||
|
|
||||||
|
import_reader(bundle, file)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn import_reader<R>(bundle: &mut Bundle, fh: R) -> Result<(), ReadError>
|
||||||
|
where
|
||||||
|
R: Read,
|
||||||
|
{
|
||||||
|
let parser = EventReader::new(fh);
|
||||||
|
|
||||||
|
let mut locale: LanguageIdentifier = langid!("en");
|
||||||
|
let mut current_key = None;
|
||||||
|
let mut current_text: Option<String> = None;
|
||||||
|
let mut in_target = false;
|
||||||
|
|
||||||
|
for event in parser {
|
||||||
|
match event {
|
||||||
|
Ok(reader::XmlEvent::StartElement {
|
||||||
|
name, attributes, ..
|
||||||
|
}) => match name.local_name.as_ref() {
|
||||||
|
"xliff" => {
|
||||||
|
locale = find_attribute(&attributes, "trgLang")
|
||||||
|
.unwrap()
|
||||||
|
.parse::<LanguageIdentifier>()
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
"unit" => current_key = find_attribute(&attributes, "id"),
|
||||||
|
"target" => in_target = true,
|
||||||
|
_ => println!("name: {}", name),
|
||||||
|
},
|
||||||
|
Ok(reader::XmlEvent::EndElement { name }) => match name.local_name.as_ref() {
|
||||||
|
"unit" => {
|
||||||
|
if let Some(key) = current_key {
|
||||||
|
let message = bundle.message(key);
|
||||||
|
let variant = message.variant_mut(locale.clone());
|
||||||
|
if let Some(ref text) = current_text {
|
||||||
|
variant.set_content(text.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
current_key = None;
|
||||||
|
}
|
||||||
|
"target" => in_target = false,
|
||||||
|
_ => {}
|
||||||
|
},
|
||||||
|
Ok(reader::XmlEvent::Characters(data)) => {
|
||||||
|
if in_target {
|
||||||
|
current_text = Some(data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("error: {e}");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -53,19 +128,30 @@ fn write_message<T>(
|
|||||||
T: std::io::Write,
|
T: std::io::Write,
|
||||||
{
|
{
|
||||||
[
|
[
|
||||||
XmlEvent::start_element("unit").attr("id", key).into(),
|
writer::XmlEvent::start_element("unit")
|
||||||
XmlEvent::start_element("notes").into(),
|
.attr("id", key)
|
||||||
XmlEvent::start_element("note").into(),
|
.into(),
|
||||||
XmlEvent::characters(message.description()).into(),
|
writer::XmlEvent::start_element("notes").into(),
|
||||||
XmlEvent::end_element().into(),
|
writer::XmlEvent::start_element("note").into(),
|
||||||
XmlEvent::end_element().into(),
|
writer::XmlEvent::characters(message.description()).into(),
|
||||||
XmlEvent::start_element("segment").into(),
|
writer::XmlEvent::end_element().into(),
|
||||||
XmlEvent::start_element("source").into(),
|
writer::XmlEvent::end_element().into(),
|
||||||
XmlEvent::characters(message.variant(locale).unwrap().content()).into(),
|
writer::XmlEvent::start_element("segment").into(),
|
||||||
XmlEvent::end_element().into(),
|
writer::XmlEvent::start_element("source").into(),
|
||||||
XmlEvent::end_element().into(),
|
writer::XmlEvent::characters(message.variant(locale).unwrap().content()).into(),
|
||||||
XmlEvent::end_element().into(),
|
writer::XmlEvent::end_element().into(),
|
||||||
|
writer::XmlEvent::end_element().into(),
|
||||||
|
writer::XmlEvent::end_element().into(),
|
||||||
]
|
]
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.for_each(|elem: XmlEvent| writer.write(elem).unwrap());
|
.for_each(|elem: writer::XmlEvent| writer.write(elem).unwrap());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn find_attribute(attrs: &Vec<OwnedAttribute>, name: &str) -> Option<String> {
|
||||||
|
for f in attrs {
|
||||||
|
if name == f.name.local_name {
|
||||||
|
return Some(f.value.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user