Skip to main content

brink_runtime/
locale.rs

1//! Locale overlay loading.
2
3use std::collections::HashMap;
4
5use brink_format::{DefinitionId, LineEntry, LocaleData};
6
7use crate::error::RuntimeError;
8use crate::program::Program;
9
10/// Controls how missing scopes are handled when applying a locale overlay.
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub enum LocaleMode {
13    /// Every scope in the base must appear in the locale. Missing scopes
14    /// produce a `LocaleScopeMissing` error.
15    Strict,
16    /// Missing scopes keep their base line tables unchanged.
17    Overlay,
18}
19
20/// Apply a locale overlay to a set of base line tables.
21///
22/// Returns a new set of line tables with locale content replacing matching
23/// scopes. The `Program` is used only for structural metadata (scope IDs,
24/// checksum) — it is not mutated.
25pub fn apply_locale(
26    program: &Program,
27    locale: &LocaleData,
28    base: &[Vec<LineEntry>],
29    mode: LocaleMode,
30) -> Result<Vec<Vec<LineEntry>>, RuntimeError> {
31    if locale.base_checksum != program.source_checksum {
32        return Err(RuntimeError::LocaleChecksumMismatch {
33            expected: program.source_checksum,
34            actual: locale.base_checksum,
35        });
36    }
37
38    // Build scope_id → line_tables index.
39    let scope_idx_map: HashMap<DefinitionId, usize> = program
40        .scope_ids
41        .iter()
42        .enumerate()
43        .map(|(i, &id)| (id, i))
44        .collect();
45
46    // Start with a clone of the base tables.
47    let mut result = base.to_vec();
48    let mut covered = vec![false; program.scope_ids.len()];
49
50    for locale_scope in &locale.line_tables {
51        let Some(&idx) = scope_idx_map.get(&locale_scope.scope_id) else {
52            return Err(RuntimeError::LocaleScopeNotInBase(locale_scope.scope_id));
53        };
54
55        // Convert LocaleLineEntry → LineEntry (source_hash=0 for locale entries).
56        let entries: Vec<LineEntry> = locale_scope
57            .lines
58            .iter()
59            .map(|le| {
60                let flags = brink_format::LineFlags::from_content(&le.content);
61                LineEntry {
62                    content: le.content.clone(),
63                    flags,
64                    source_hash: 0,
65                    audio_ref: le.audio_ref.clone(),
66                    slot_info: Vec::new(),
67                    source_location: None,
68                }
69            })
70            .collect();
71
72        result[idx] = entries;
73        covered[idx] = true;
74    }
75
76    if matches!(mode, LocaleMode::Strict) {
77        for (i, was_covered) in covered.iter().enumerate() {
78            if !was_covered {
79                return Err(RuntimeError::LocaleScopeMissing(program.scope_ids[i]));
80            }
81        }
82    }
83
84    Ok(result)
85}