Skip to main content

brink_runtime/
locale.rs

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