Skip to main content

rimloc_core/
lib.rs

1use color_eyre::eyre::eyre;
2use std::path::PathBuf;
3
4use serde::{Deserialize, Serialize};
5use thiserror::Error;
6
7/// Workspace-wide result alias.
8pub type Result<T> = color_eyre::eyre::Result<T>;
9
10/// Schema version for RimLoc data outputs (JSON/PO headers).
11pub const RIMLOC_SCHEMA_VERSION: u32 = 1;
12
13/// Minimal unit used across crates to represent a single translation entry
14/// scanned from RimWorld XML (Keyed/DefInjected) or produced by tools.
15#[derive(Debug, Clone, Serialize, Deserialize)]
16pub struct TransUnit {
17    pub key: String,
18    /// Source string (may be missing for keys detected without text)
19    pub source: Option<String>,
20    /// Absolute or relative path to the file where this unit comes from
21    pub path: PathBuf,
22    /// 1-based line number if available
23    pub line: Option<usize>,
24}
25
26/// Simple PO entry used by import/export utilities and tests.
27#[derive(Debug, Clone, Serialize, Deserialize)]
28pub struct PoEntry {
29    pub key: String,
30    pub value: String,
31    /// Optional reference like
32    /// "…/Languages/English/Keyed/Some.xml:42" used to reconstruct paths.
33    pub reference: Option<String>,
34}
35
36/// Keep a lightweight error type for crates that still import it.
37#[derive(Debug, Error)]
38pub enum RimLocError {
39    #[error("{0}")]
40    Other(String),
41}
42
43/// Parse a minimal subset of PO syntax used across the workspace.
44/// Supports single-line `msgid`/`msgstr` pairs and optional reference lines (`#: ...`).
45pub fn parse_simple_po(input: &str) -> Result<Vec<PoEntry>> {
46    let mut entries = Vec::new();
47    let mut cur_ref: Option<String> = None;
48    let mut cur_id: Option<String> = None;
49
50    fn unquote(raw: &str) -> String {
51        let trimmed = raw.trim();
52        if trimmed.starts_with('"') && trimmed.ends_with('"') && trimmed.len() >= 2 {
53            trimmed[1..trimmed.len() - 1].to_string()
54        } else {
55            trimmed.to_string()
56        }
57    }
58
59    for line in input.lines() {
60        let trimmed = line.trim();
61        if trimmed.is_empty() {
62            continue;
63        }
64        if let Some(rest) = trimmed.strip_prefix("#:") {
65            cur_ref = Some(rest.trim().to_string());
66            continue;
67        }
68        if let Some(rest) = trimmed.strip_prefix("msgid") {
69            let eq = rest
70                .trim_start()
71                .strip_prefix(' ')
72                .unwrap_or(rest)
73                .trim_start_matches('=');
74            cur_id = Some(unquote(eq.trim()));
75            continue;
76        }
77        if let Some(rest) = trimmed.strip_prefix("msgstr") {
78            let eq = rest
79                .trim_start()
80                .strip_prefix(' ')
81                .unwrap_or(rest)
82                .trim_start_matches('=');
83            let val = unquote(eq.trim());
84            if let Some(id) = cur_id.take() {
85                entries.push(PoEntry {
86                    key: id,
87                    value: val,
88                    reference: cur_ref.take(),
89                });
90            } else {
91                return Err(eyre!("Malformed PO entry: msgstr without msgid"));
92            }
93        }
94    }
95
96    Ok(entries)
97}