Compare commits
No commits in common. "l10n-db" and "main" have entirely different histories.
629
Cargo.lock
generated
629
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -33,4 +33,4 @@ members = [
|
|||||||
"tree",
|
"tree",
|
||||||
"visions/server",
|
"visions/server",
|
||||||
"gm-dash/server"
|
"gm-dash/server"
|
||||||
, "l10n-db"]
|
]
|
||||||
|
@ -1,23 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "l10n-db"
|
|
||||||
version = "0.1.0"
|
|
||||||
edition = "2021"
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
chrono = { version = "0.4.39", features = ["serde"] }
|
|
||||||
clap = { version = "4.5.30", features = ["derive"] }
|
|
||||||
icu_locid = { version = "1.5.0", features = ["serde"] }
|
|
||||||
serde = { version = "1.0.218", features = ["derive"] }
|
|
||||||
serde_json = "1.0.139"
|
|
||||||
tempfile = "3.17.1"
|
|
||||||
thiserror = "2.0.11"
|
|
||||||
toml = "0.8.20"
|
|
||||||
|
|
||||||
# [lib]
|
|
||||||
# name = "l10n_db"
|
|
||||||
# path = "src/lib.rs"
|
|
||||||
#
|
|
||||||
# [[bin]]
|
|
||||||
# name = "l10n-db"
|
|
||||||
# path = "src/main.rs"
|
|
||||||
|
|
@ -1,8 +0,0 @@
|
|||||||
db_path = "./i18n"
|
|
||||||
base_locale = "en"
|
|
||||||
locales = [
|
|
||||||
"en",
|
|
||||||
"eo",
|
|
||||||
"de",
|
|
||||||
"es",
|
|
||||||
]
|
|
@ -1,7 +0,0 @@
|
|||||||
key = "SaveSettings"
|
|
||||||
description = "This is a label on a button which will save the settings when clicked"
|
|
||||||
|
|
||||||
[variants.en]
|
|
||||||
locale = "en"
|
|
||||||
content = "Save Settings"
|
|
||||||
modified = "2025-02-22T23:44:18.874218939Z"
|
|
@ -1,7 +0,0 @@
|
|||||||
key = "Welcome"
|
|
||||||
description = "This is a welcome content that will be shown on first app opening, before configuration."
|
|
||||||
|
|
||||||
[variants.en]
|
|
||||||
locale = "en"
|
|
||||||
content = "Welcome to FitnessTrax"
|
|
||||||
modified = "2025-02-22T23:43:24.786544124Z"
|
|
@ -1 +0,0 @@
|
|||||||
{"SaveSettings":"Save Settings","Welcome":"Welcome to FitnessTrax"}
|
|
@ -1,83 +0,0 @@
|
|||||||
use std::{
|
|
||||||
io::{BufReader, Read, Write},
|
|
||||||
path::PathBuf,
|
|
||||||
process::Command,
|
|
||||||
};
|
|
||||||
|
|
||||||
use clap::{Parser, Subcommand};
|
|
||||||
|
|
||||||
use icu_locid::{langid, LanguageIdentifier};
|
|
||||||
use l10n_db::{self, export_file, read_file, Bundle, Editor, ReadError};
|
|
||||||
use serde::Deserialize;
|
|
||||||
|
|
||||||
#[derive(Parser)]
|
|
||||||
#[command(version, about, long_about = None)]
|
|
||||||
struct Cli {
|
|
||||||
#[command(subcommand)]
|
|
||||||
command: Option<Commands>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Subcommand)]
|
|
||||||
enum Commands {
|
|
||||||
/// Edit, potentially creating, a key
|
|
||||||
EditKey {
|
|
||||||
#[arg(short, long)]
|
|
||||||
name: String,
|
|
||||||
#[arg(short, long)]
|
|
||||||
locale: String,
|
|
||||||
},
|
|
||||||
/// List al keys in the database
|
|
||||||
ListKeys,
|
|
||||||
// Search the database
|
|
||||||
// Search { },
|
|
||||||
/// Export the database
|
|
||||||
Export {
|
|
||||||
#[arg(short, long)]
|
|
||||||
format: String,
|
|
||||||
#[arg(short, long)]
|
|
||||||
locale: Option<String>,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
|
||||||
struct Config {
|
|
||||||
db_path: PathBuf,
|
|
||||||
base_locale: LanguageIdentifier,
|
|
||||||
locales: Vec<LanguageIdentifier>,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn edit_key(bundle: &mut Bundle, key: String, locale: LanguageIdentifier, editor: &str) {
|
|
||||||
let message = bundle.message(key);
|
|
||||||
Editor::edit(message, locale, editor);
|
|
||||||
bundle.save();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn main() {
|
|
||||||
let editor = std::env::var("EDITOR").expect("Set EDITOR to the path to your favorite editor");
|
|
||||||
|
|
||||||
let config: Config = read_file(&PathBuf::from("./config.toml"))
|
|
||||||
.and_then(|bytes| String::from_utf8(bytes).map_err(|_| ReadError::InvalidFormat))
|
|
||||||
.and_then(|content| toml::from_str(&content).map_err(|_| ReadError::InvalidFormat))
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let cli = Cli::parse();
|
|
||||||
|
|
||||||
let mut bundle = Bundle::load_from_disk(PathBuf::from(&config.db_path));
|
|
||||||
|
|
||||||
match &cli.command {
|
|
||||||
Some(Commands::EditKey { name, locale }) => {
|
|
||||||
let identifier = locale.parse::<LanguageIdentifier>().unwrap();
|
|
||||||
edit_key(&mut bundle, name.to_owned(), identifier, &editor)
|
|
||||||
}
|
|
||||||
Some(Commands::ListKeys) => {
|
|
||||||
for (key, _) in bundle.message_iter() {
|
|
||||||
println!("{}", key);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Some(Commands::Export { format, locale }) => {
|
|
||||||
let locale = locale.as_ref().map(|l| l.clone().parse::<LanguageIdentifier>().unwrap()).unwrap_or(langid!("en"));
|
|
||||||
export_file(&bundle, locale, &PathBuf::from("output.json")).unwrap();
|
|
||||||
},
|
|
||||||
None => {}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,52 +0,0 @@
|
|||||||
use std::{collections::HashMap, fs::File, io::Write, path::PathBuf};
|
|
||||||
|
|
||||||
use crate::{Message, WriteError};
|
|
||||||
|
|
||||||
pub struct Bundle {
|
|
||||||
path: PathBuf,
|
|
||||||
messages: HashMap<String, Message>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Bundle {
|
|
||||||
pub fn load_from_disk(path: PathBuf) -> Self {
|
|
||||||
let mut messages = HashMap::new();
|
|
||||||
if path.is_dir() {
|
|
||||||
for entry in std::fs::read_dir(&path).unwrap() {
|
|
||||||
let entry = entry.unwrap();
|
|
||||||
let path = entry.path().clone();
|
|
||||||
let key = path.file_stem().unwrap();
|
|
||||||
|
|
||||||
let message = Message::from_file(&entry.path());
|
|
||||||
messages.insert(key.to_str().unwrap().to_owned(), message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Self { path, messages }
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn message_iter(&self) -> impl Iterator<Item = (&String, &Message)> {
|
|
||||||
self.messages.iter()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn message(&mut self, name: String) -> &mut Message {
|
|
||||||
self.messages
|
|
||||||
.entry(name.to_owned())
|
|
||||||
.or_insert(Message::new(name.to_owned()))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn save(&self) {
|
|
||||||
self.messages.iter().for_each(|(key, value)| {
|
|
||||||
let mut path = self.path.clone();
|
|
||||||
path.push(key);
|
|
||||||
path.set_extension("toml");
|
|
||||||
save_file(&path, toml::to_string(value).unwrap().as_bytes());
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn save_file(path: &PathBuf, s: &[u8]) {
|
|
||||||
let mut f = File::create(path).unwrap();
|
|
||||||
f.write(s).unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod test {}
|
|
@ -1,56 +0,0 @@
|
|||||||
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};
|
|
||||||
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
|
||||||
struct EditorMessage {
|
|
||||||
description: String,
|
|
||||||
source: String,
|
|
||||||
content: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
impl EditorMessage {
|
|
||||||
fn from_variant(description: String, variant: &Variant) -> Self {
|
|
||||||
Self {
|
|
||||||
description,
|
|
||||||
content: variant.content().to_string()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
pub struct Editor {
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Editor {
|
|
||||||
pub fn edit(msg: &mut Message, locale: LanguageIdentifier, editor: &str) {
|
|
||||||
let description = msg.description().to_owned();
|
|
||||||
let source_string = msg.variant_mut(langid!("en")).content().to_owned();
|
|
||||||
let variant = msg.variant_mut(locale);
|
|
||||||
// let editable_content = EditorMessage::from_variant(description, &variant);
|
|
||||||
let editable_content = EditorMessage {
|
|
||||||
description,
|
|
||||||
source: source_string,
|
|
||||||
content: variant.content().to_owned(),
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut file = tempfile::NamedTempFile::new().unwrap();
|
|
||||||
let _ = file.write(toml::to_string(&editable_content).unwrap().as_bytes());
|
|
||||||
let _ = file.flush();
|
|
||||||
let mut cmd = Command::new(editor).args([file.path()]).spawn().unwrap();
|
|
||||||
cmd.wait().unwrap();
|
|
||||||
let file = file.reopen().unwrap();
|
|
||||||
let content = read_fh(&file).unwrap();
|
|
||||||
|
|
||||||
let new_variant: EditorMessage = toml::from_str(&String::from_utf8(content).unwrap()).unwrap();
|
|
||||||
|
|
||||||
variant.set_content(new_variant.content);
|
|
||||||
msg.set_description(new_variant.description);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,22 +0,0 @@
|
|||||||
use std::{collections::BTreeMap, fs::File, io::Write, path::Path};
|
|
||||||
|
|
||||||
use icu_locid::LanguageIdentifier;
|
|
||||||
|
|
||||||
use crate::{Bundle, WriteError};
|
|
||||||
|
|
||||||
pub fn export_file(bundle: &Bundle, locale: LanguageIdentifier, path: &Path) -> Result<(), WriteError> {
|
|
||||||
let mut file = File::create(path).unwrap();
|
|
||||||
export_fh(bundle, locale, &mut file)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn export_fh(bundle: &Bundle, locale: LanguageIdentifier, fh: &mut File) -> Result<(), WriteError> {
|
|
||||||
let messages = bundle.message_iter().map(|(key, message)| {
|
|
||||||
let content = message.variant(&locale).unwrap().content().to_owned();
|
|
||||||
(key.to_owned(), content)
|
|
||||||
}).collect::<Vec<(String, String)>>();
|
|
||||||
|
|
||||||
let messages: BTreeMap<String, String> = messages.into_iter().collect();
|
|
||||||
fh.write(serde_json::to_string(&messages).unwrap().as_bytes()).unwrap();
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
@ -1,29 +0,0 @@
|
|||||||
mod bundle;
|
|
||||||
pub use bundle::Bundle;
|
|
||||||
|
|
||||||
mod editor;
|
|
||||||
pub use editor::Editor;
|
|
||||||
|
|
||||||
mod js_format;
|
|
||||||
pub use js_format::{export_file, export_fh};
|
|
||||||
|
|
||||||
mod types;
|
|
||||||
pub use types::{Message, Variant};
|
|
||||||
|
|
||||||
mod utils;
|
|
||||||
pub use utils::*;
|
|
||||||
|
|
||||||
/*
|
|
||||||
#[cfg(test)]
|
|
||||||
mod test {
|
|
||||||
#[test]
|
|
||||||
fn it_can_represent_an_untranslated_message() {
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn it_can_represent_a_partially_translated_message() {
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
*/
|
|
@ -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())
|
|
||||||
}
|
|
||||||
*/
|
|
@ -1,37 +0,0 @@
|
|||||||
use std::{fs::File, io::{BufReader, ErrorKind, Read}, path::Path};
|
|
||||||
use thiserror::Error;
|
|
||||||
|
|
||||||
#[derive(Debug, Error)]
|
|
||||||
pub enum ReadError {
|
|
||||||
#[error("file not found")]
|
|
||||||
FileNotFound,
|
|
||||||
|
|
||||||
#[error("invalid file format")]
|
|
||||||
InvalidFormat,
|
|
||||||
|
|
||||||
#[error("unhandled read error")]
|
|
||||||
Unhandled(ErrorKind),
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn read_file(path: &Path) -> Result<Vec<u8>, ReadError> {
|
|
||||||
let file = File::open(path).map_err(|err| {
|
|
||||||
match err.kind() {
|
|
||||||
ErrorKind::NotFound => ReadError::FileNotFound,
|
|
||||||
_ => ReadError::Unhandled( err.kind()),
|
|
||||||
}
|
|
||||||
})?;
|
|
||||||
read_fh(&file)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn read_fh(file: &File) -> Result<Vec<u8>, ReadError> {
|
|
||||||
let mut content = Vec::new();
|
|
||||||
let mut reader = BufReader::new(file);
|
|
||||||
reader.read_to_end(&mut content).map_err(|err| ReadError::Unhandled(err.kind()))?;
|
|
||||||
Ok(content)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Error)]
|
|
||||||
pub enum WriteError {
|
|
||||||
#[error("unhandled write error")]
|
|
||||||
Unhandled(ErrorKind),
|
|
||||||
}
|
|
Loading…
Reference in New Issue
Block a user