Compare commits
5 Commits
a8a61cf03f
...
a07ecae04a
Author | SHA1 | Date | |
---|---|---|---|
a07ecae04a | |||
76de75210f | |||
e5b3c7e4e1 | |||
704009b76c | |||
cd5837a437 |
@ -1,7 +1,22 @@
|
||||
key = "SaveSettings"
|
||||
description = "This is a label on a button which will save the settings when clicked"
|
||||
|
||||
[variants.eo]
|
||||
locale = "eo"
|
||||
content = "Konservi Agordojn"
|
||||
modified = "2025-02-24T19:32:11.246639077Z"
|
||||
|
||||
[variants.es]
|
||||
locale = "es"
|
||||
content = "Guardar Configuraciones"
|
||||
modified = "2025-02-24T19:33:23.861329923Z"
|
||||
|
||||
[variants.en]
|
||||
locale = "en"
|
||||
content = "Save Settings"
|
||||
modified = "2025-02-22T23:44:18.874218939Z"
|
||||
|
||||
[variants.de]
|
||||
locale = "de"
|
||||
content = "Einstellungen Speichern"
|
||||
modified = "2025-02-24T19:33:19.516005843Z"
|
||||
|
@ -1,7 +1,22 @@
|
||||
key = "TimeDistance"
|
||||
description = "A summary of a workout or many workouts that involve a time and a distance"
|
||||
|
||||
[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"
|
||||
|
||||
[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.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"
|
||||
|
||||
[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"
|
||||
|
@ -3,5 +3,20 @@ description = "This is a welcome content that will be shown on first app opening
|
||||
|
||||
[variants.en]
|
||||
locale = "en"
|
||||
content = "Welcome to FitnessTrax"
|
||||
modified = "2025-02-22T23:43:24.786544124Z"
|
||||
content = "Welcome to FitnessTrax, the privacy-centered fitness tracker"
|
||||
modified = "2025-02-25T02:12:25.757240004Z"
|
||||
|
||||
[variants.eo]
|
||||
locale = "eo"
|
||||
content = "Bonvenon al FitnessTrax"
|
||||
modified = "2025-02-24T19:32:11.246407627Z"
|
||||
|
||||
[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"
|
||||
|
@ -1,13 +1,15 @@
|
||||
use std::{
|
||||
io::{BufReader, Read, Write},
|
||||
path::PathBuf,
|
||||
process::Command,
|
||||
fmt, io::{BufReader, Read, Write}, path::PathBuf, process::Command
|
||||
};
|
||||
|
||||
use clap::{Parser, Subcommand};
|
||||
|
||||
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;
|
||||
|
||||
#[derive(Parser)]
|
||||
@ -30,6 +32,10 @@ enum Commands {
|
||||
ListKeys,
|
||||
// Search the database
|
||||
// Search { },
|
||||
Import {
|
||||
#[arg(short, long)]
|
||||
file: String,
|
||||
},
|
||||
/// Export the database
|
||||
Export {
|
||||
#[arg(short, long)]
|
||||
@ -37,6 +43,7 @@ enum Commands {
|
||||
#[arg(short, long)]
|
||||
locale: Option<String>,
|
||||
},
|
||||
Report,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
@ -52,6 +59,38 @@ fn edit_key(bundle: &mut Bundle, key: String, locale: LanguageIdentifier, editor
|
||||
bundle.save();
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
struct Report {
|
||||
keys: Vec<String>,
|
||||
source_deleted: Vec<String>,
|
||||
out_of_date: Vec<String>,
|
||||
}
|
||||
|
||||
impl fmt::Display for Report {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "Out of date messages\n")?;
|
||||
for key in self.out_of_date.iter() {
|
||||
write!(f, "\t{}\n", key)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn generate_report(
|
||||
bundle: &Bundle,
|
||||
base_locale: &LanguageIdentifier,
|
||||
locales: Vec<LanguageIdentifier>,
|
||||
) -> Report {
|
||||
let mut report: Report = Default::default();
|
||||
for (key, message) in bundle.message_iter() {
|
||||
if message.variants_out_of_date(base_locale).len() > 0 {
|
||||
report.out_of_date.push(key.to_owned())
|
||||
}
|
||||
}
|
||||
|
||||
report
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let editor = std::env::var("EDITOR").expect("Set EDITOR to the path to your favorite editor");
|
||||
|
||||
@ -74,15 +113,28 @@ fn main() {
|
||||
println!("{}", key);
|
||||
}
|
||||
}
|
||||
Some(Commands::Import { file }) => {
|
||||
import_file(&mut bundle, &PathBuf::from(file)).unwrap();
|
||||
bundle.save();
|
||||
}
|
||||
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"));
|
||||
|
||||
match format.as_ref() {
|
||||
"js" => js::export_file(&bundle, locale, &PathBuf::from("output.json")).unwrap(),
|
||||
"xliff" => xliff::export_file(&bundle, locale, &PathBuf::from("output.xliff")).unwrap(),
|
||||
"xliff" => {
|
||||
xliff::export_file(&bundle, locale, &PathBuf::from("output.xliff")).unwrap()
|
||||
}
|
||||
_ => todo!(),
|
||||
}
|
||||
},
|
||||
}
|
||||
Some(Commands::Report) => {
|
||||
let report = generate_report(&bundle, &config.base_locale, config.locales);
|
||||
println!("{}", report);
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,8 @@
|
||||
use std::{collections::HashMap, fs::File, io::Write, path::PathBuf};
|
||||
use std::{collections::HashMap, fs::File, io::{BufReader, Read, Write}, path::{Path, PathBuf}};
|
||||
|
||||
use crate::{Message, WriteError};
|
||||
use chrono::{DateTime, Utc};
|
||||
use icu_locid::LanguageIdentifier;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
pub struct Bundle {
|
||||
path: PathBuf,
|
||||
@ -48,5 +50,94 @@ fn save_file(path: &PathBuf, s: &[u8]) {
|
||||
f.write(s).unwrap();
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug)]
|
||||
pub struct Message {
|
||||
key: String,
|
||||
description: String,
|
||||
variants: HashMap<LanguageIdentifier, Variant>,
|
||||
}
|
||||
|
||||
impl Message {
|
||||
pub fn new(key: String) -> Self {
|
||||
Self {
|
||||
key,
|
||||
description: "".to_owned(),
|
||||
variants: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_file(path: &Path) -> Message {
|
||||
let file = std::fs::File::open(path).unwrap();
|
||||
let mut content = Vec::new();
|
||||
let mut reader = BufReader::new(file);
|
||||
let _ = reader.read_to_end(&mut content);
|
||||
toml::from_str(&String::from_utf8(content).unwrap()).unwrap()
|
||||
}
|
||||
|
||||
pub fn set_description(&mut self, desc: String) {
|
||||
self.description = desc;
|
||||
}
|
||||
|
||||
pub fn description(&self) -> &str {
|
||||
&self.description
|
||||
}
|
||||
|
||||
pub fn variant(&self, locale: &LanguageIdentifier) -> Option<&Variant> {
|
||||
self.variants.get(locale)
|
||||
}
|
||||
|
||||
pub fn variant_mut(&mut self, locale: LanguageIdentifier) -> &mut Variant {
|
||||
self.variants.entry(locale.clone()).or_insert(Variant {
|
||||
locale,
|
||||
content: "".to_owned(),
|
||||
modified: Utc::now(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn variants_out_of_date(
|
||||
&self,
|
||||
base_locale: &LanguageIdentifier,
|
||||
) -> Vec<LanguageIdentifier> {
|
||||
match self
|
||||
.variants
|
||||
.get(base_locale)
|
||||
.map(|variant| variant.modified())
|
||||
{
|
||||
Some(base_date) => self
|
||||
.variants
|
||||
.iter()
|
||||
.filter(|(_, value)| base_date > value.modified())
|
||||
.map(|(locale, _)| locale.clone())
|
||||
.collect(),
|
||||
None => vec![],
|
||||
}
|
||||
}
|
||||
|
||||
// pub fn missing_variants(&self, locals: Vec<LanguageIdentifier>) -> Vec<LanguageIdentifier> {}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug, Clone)]
|
||||
pub struct Variant {
|
||||
locale: LanguageIdentifier,
|
||||
content: String,
|
||||
modified: DateTime<Utc>,
|
||||
}
|
||||
|
||||
impl Variant {
|
||||
pub fn content(&self) -> &str {
|
||||
&self.content
|
||||
}
|
||||
|
||||
pub fn set_content(&mut self, content: String) {
|
||||
self.content = content;
|
||||
self.modified = Utc::now();
|
||||
}
|
||||
|
||||
pub fn modified(&self) -> DateTime<Utc> {
|
||||
self.modified
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {}
|
||||
|
@ -3,7 +3,7 @@ use std::{io::{BufReader, Read, Write}, path::Path, process::Command};
|
||||
use icu_locid::{langid, LanguageIdentifier};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{read_fh, Message, Variant};
|
||||
use crate::{read_fh, Message};
|
||||
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
|
@ -16,7 +16,7 @@ pub fn export_fh(bundle: &Bundle, locale: LanguageIdentifier, fh: &mut File) ->
|
||||
}).collect::<Vec<(String, String)>>();
|
||||
|
||||
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(())
|
||||
}
|
||||
|
@ -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 xml::{writer::XmlEvent, EmitterConfig, EventWriter};
|
||||
use chrono::{DateTime, Utc};
|
||||
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(
|
||||
bundle: &Bundle,
|
||||
@ -14,33 +29,93 @@ pub fn export_file(
|
||||
export_fh(bundle, locale, &mut file)
|
||||
}
|
||||
|
||||
pub fn export_fh(
|
||||
bundle: &Bundle,
|
||||
locale: LanguageIdentifier,
|
||||
fh: &mut File,
|
||||
) -> Result<(), WriteError> {
|
||||
let mut writer = EmitterConfig::new()
|
||||
.perform_indent(true)
|
||||
.create_writer(fh);
|
||||
pub fn export_fh<W>(bundle: &Bundle, locale: LanguageIdentifier, fh: W) -> Result<(), WriteError>
|
||||
where
|
||||
W: Write,
|
||||
{
|
||||
let mut writer = EmitterConfig::new().perform_indent(true).create_writer(fh);
|
||||
|
||||
writer
|
||||
.write(
|
||||
XmlEvent::start_element("xliff")
|
||||
writer::XmlEvent::start_element("xliff")
|
||||
.attr("xmlns", "urn:oasis:names:tc:xliff:document:2.0")
|
||||
.attr("version", "2.0")
|
||||
.attr("srcLang", &format!("{}", locale)),
|
||||
)
|
||||
.unwrap();
|
||||
writer
|
||||
.write(XmlEvent::start_element("file").attr("id", "main"))
|
||||
.write(writer::XmlEvent::start_element("file").attr("id", "main"))
|
||||
.unwrap();
|
||||
|
||||
for (key, message) in bundle.message_iter() {
|
||||
write_message(&mut writer, key, message, &locale);
|
||||
}
|
||||
|
||||
writer.write(XmlEvent::end_element()).unwrap();
|
||||
writer.write(XmlEvent::end_element()).unwrap();
|
||||
writer.write(writer::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(())
|
||||
}
|
||||
|
||||
@ -53,19 +128,30 @@ fn write_message<T>(
|
||||
T: std::io::Write,
|
||||
{
|
||||
[
|
||||
XmlEvent::start_element("unit").attr("id", key).into(),
|
||||
XmlEvent::start_element("notes").into(),
|
||||
XmlEvent::start_element("note").into(),
|
||||
XmlEvent::characters(message.description()).into(),
|
||||
XmlEvent::end_element().into(),
|
||||
XmlEvent::end_element().into(),
|
||||
XmlEvent::start_element("segment").into(),
|
||||
XmlEvent::start_element("source").into(),
|
||||
XmlEvent::characters(message.variant(locale).unwrap().content()).into(),
|
||||
XmlEvent::end_element().into(),
|
||||
XmlEvent::end_element().into(),
|
||||
XmlEvent::end_element().into(),
|
||||
writer::XmlEvent::start_element("unit")
|
||||
.attr("id", key)
|
||||
.into(),
|
||||
writer::XmlEvent::start_element("notes").into(),
|
||||
writer::XmlEvent::start_element("note").into(),
|
||||
writer::XmlEvent::characters(message.description()).into(),
|
||||
writer::XmlEvent::end_element().into(),
|
||||
writer::XmlEvent::end_element().into(),
|
||||
writer::XmlEvent::start_element("segment").into(),
|
||||
writer::XmlEvent::start_element("source").into(),
|
||||
writer::XmlEvent::characters(message.variant(locale).unwrap().content()).into(),
|
||||
writer::XmlEvent::end_element().into(),
|
||||
writer::XmlEvent::end_element().into(),
|
||||
writer::XmlEvent::end_element().into(),
|
||||
]
|
||||
.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
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
mod bundle;
|
||||
pub use bundle::Bundle;
|
||||
pub use bundle::{Bundle, Message, Variant};
|
||||
|
||||
mod editor;
|
||||
pub use editor::Editor;
|
||||
@ -7,9 +7,5 @@ pub use editor::Editor;
|
||||
mod formats;
|
||||
pub use formats::{js, xliff};
|
||||
|
||||
mod types;
|
||||
pub use types::{Message, Variant};
|
||||
|
||||
mod utils;
|
||||
pub use utils::*;
|
||||
|
||||
|
@ -1,83 +0,0 @@
|
||||
use std::{collections::HashMap, io::{BufReader, Read}, path::Path};
|
||||
|
||||
use chrono::{DateTime, Utc};
|
||||
use icu_locid::LanguageIdentifier;
|
||||
use serde::{de, Deserialize, Deserializer, Serialize, Serializer};
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug)]
|
||||
pub struct Message {
|
||||
key: String,
|
||||
description: String,
|
||||
variants: HashMap<LanguageIdentifier, Variant>,
|
||||
}
|
||||
|
||||
impl Message {
|
||||
pub fn new(key: String) -> Self {
|
||||
Self {
|
||||
key,
|
||||
description: "".to_owned(),
|
||||
variants: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_file(path: &Path) -> Message {
|
||||
let file = std::fs::File::open(path).unwrap();
|
||||
let mut content = Vec::new();
|
||||
let mut reader = BufReader::new(file);
|
||||
let _ = reader.read_to_end(&mut content);
|
||||
toml::from_str(&String::from_utf8(content).unwrap()).unwrap()
|
||||
}
|
||||
|
||||
pub fn set_description(&mut self, desc: String) {
|
||||
self.description = desc;
|
||||
}
|
||||
|
||||
pub fn description(&self) -> &str {
|
||||
&self.description
|
||||
}
|
||||
|
||||
pub fn variant(&self, locale: &LanguageIdentifier) -> Option<&Variant> {
|
||||
self.variants.get(locale)
|
||||
}
|
||||
|
||||
pub fn variant_mut(&mut self, locale: LanguageIdentifier) -> &mut Variant {
|
||||
self.variants.entry(locale.clone()).or_insert(Variant {
|
||||
locale,
|
||||
content: "".to_owned(),
|
||||
modified: Utc::now(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug, Clone)]
|
||||
pub struct Variant {
|
||||
locale: LanguageIdentifier,
|
||||
content: String,
|
||||
modified: DateTime<Utc>,
|
||||
}
|
||||
|
||||
impl Variant {
|
||||
pub fn content(&self) -> &str {
|
||||
&self.content
|
||||
}
|
||||
|
||||
pub fn set_content(&mut self, content: String) {
|
||||
self.content = content;
|
||||
self.modified = Utc::now();
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
fn time_to_number<S>(time: &DateTime, s: S) -> Result<S::Ok, S::Error>
|
||||
where S: Serializer {
|
||||
let seconds: u64 = time.as_secs();
|
||||
s.serialize_u64(seconds)
|
||||
}
|
||||
|
||||
fn number_to_time<'de, D>(d: D) -> Result<DateTime, D::Error>
|
||||
where D: Deserializer<'de> {
|
||||
let buf = String::deserialize(d)?;
|
||||
let num = buf.parse::<u64>().unwrap();
|
||||
Ok(DateTime::try_from(num).unwrap())
|
||||
}
|
||||
*/
|
Loading…
Reference in New Issue
Block a user