Продвинутый

ultraworkers/claw-code: Автономный музейный экспонат Rust

Алексей Кузнецов
Алексей Кузнецов
Системный администратор1 ноября 2026 г.13 мин чтения

Проект ultraworkers/claw-code — музейный экспонат на Rust, управляемый агентом, который разрабатывается и обслуживается без участия человека.

ultraworkers/claw-code: Автономный музейный экспонат Rust

Проект ultraworkers/claw-code — музейный экспонат на Rust, управляемый агентом, который разрабатывается и обслуживается без участия человека.

Проект и его концепция

ultraworkers/claw-code: Автономный музейный экспонат на Rust

Что такое ultraworkers/claw-code

Ultraworkers/claw-code — это уникальный музейный экспонат, написанный на Rust, который демонстрирует концепцию полностью автономных систем. Проект представляет собой самодостаточный агент, способный к самообслуживанию и развитию без человеческого вмешательства.

Экспонат был создан как экспериментальный проект, чтобы показать, как можно реализовать концепцию "самодостаточного агента" в реальном программном обеспечении. Проект демонстрирует несколько ключевых технических решений:

  • Самодостаточность: агент может обновлять себя, диагностировать проблемы и восстанавливать работоспособность
  • Автономность: полное отсутствие необходимости в человеческом управлении после начальной настройки
  • Самообучение: экспонат может адаптироваться к новым условиям и требованиям
rust
// Пример конфигурации агента claw-code
struct AgentConfig {
    name: String,
    auto_update: bool,
    health_check_interval: Duration,
    self_diagnostic: bool,
    log_retention_days: u32,
}

impl AgentConfig {
    fn new() -> Self {
        Self {
            name: "claw-code-exhibit".to_string(),
            auto_update: true,
            health_check_interval: Duration::from_secs(3600),
            self_diagnostic: true,
            log_retention_days: 30,
        }
    }
}

Цели и мотивация

Основные цели проекта

  1. Демонстрация возможностей Rust для создания высокопроизводительных, безопасных на уровне памяти автономных систем
  2. Исследование концепции самообслуживающихся агентов и их применимости в реальных сценариях
  3. Создание образовательного экспоната, который может быть использован для изучения современных подходов к разработке автономных систем

Мотивация для создания

Проект был инициирован из нескольких соображений:

  • Технический вызов: Создание системы, которая действительно может существовать без человеческого вмешательства, представляет собой сложную инженерную задачу
  • Демонстрация возможностей Rust: Язык предлагает уникальные возможности для создания безопасных, высокопроизводительных автономных систем
  • Образовательная ценность: Экспонат может служить практическим примером для разработчиков, изучающих концепции автономных агентов и самообслуживающихся систем
rust
// Пример механизма самообновления агента
async fn self_update_cycle(config: &AgentConfig) -> Result<(), UpdateError> {
    if !config.auto_update {
        return Ok(());
    }

    // Проверка наличия обновлений
    let updates = check_for_updates().await?;
    
    if !updates.is_empty() {
        println!("Найдены обновления: {:?}", updates);
        apply_updates(updates).await?;
        println!("Обновления успешно применены");
    } else {
        println!("Обновления отсутствуют");
    }
    
    Ok(())
}

Техническое обоснование

В современной IT-инфраструктуре все чаще появляются системы, которые должны работать автономно:

  • Удаленные устройства: IoT-устройства и edge-компьютеры, которые не всегда доступны для физического обслуживания
  • Космические системы: Аппараты, которые должны работать в течение многих лет без возможности обновления
  • Критически важные системы: Системы, где простои недопустимы и где ручное вмешательство представляет риск

Ultraworkers/claw-code демонстрирует, как можно реализовать концепцию автономного агента, который может:

  • Самодиагностика: Обнаруживать и диагностировать проблемы без внешнего вмешательства
  • Самоисцеление: Восстанавливать работоспособность после обнаруженных проблем
  • Адаптация: Изменять свое поведение в ответ на изменения окружающей среды
  • Обслуживание: Выполнять рутинные операции по обслуживанию без человеческого вмешательства
rust
// Пример механизма самообслуживания
async fn self_maintenance_cycle(config: &AgentConfig) -> Result<(), MaintenanceError> {
    // Проверка здоровья системы
    if config.self_diagnostic {
        let health_report = perform_self_diagnosis().await?;
        
        if !health_report.is_healthy() {
            println!("Обнаружены проблемы со здоровьем: {:?}", health_report.issues);
            attempt_self_healing(&health_report).await?;
        }
    }
    
    // Очистка логов
    cleanup_old_logs(config.log_retention_days).await?;
    
    Ok(())
}

Ссылки

Этот музейный экспонат демонстрирует, как современные технологии Rust могут быть использованы для создания по-настоящему автономных систем, которые могут существовать и развиваться без человеческого вмешательства. Проект служит как техническим экспериментом, так и образовательным ресурсом для изучения концепций автономных агентов и самообслуживающихся систем.

Техническая реализация

Язык Rust в экспонате

Проект ultraworkers/claw-code использует Rust для создания автономного агента, который может развиваться и обслуживать себя без человеческого вмешательства. Выбор Rust не случаен — язык обеспечивает необходимую предсказуемость и безопасность для критически важных систем, где надежность важнее всего.

rust
// Пример структуры агента, управляющего экспонатом
struct ExhibitAgent {
    state: AgentState,
    config: AgentConfig,
    storage: Box<dyn Storage>,
    monitor: MonitoringSystem,
}

struct AgentState {
    current_version: String,
    last_update: SystemTime,
    health_status: HealthStatus,
    adaptation_rules: Vec<AdaptationRule>,
}

impl ExhibitAgent {
    fn self_maintenance_cycle(&mut self) -> Result<(), AgentError> {
        // Проверка здоровья системы
        let health = self.monitor.check_health().await?;
        
        if !health.is_healthy() {
            self.apply_adaptation_rules(&health).await?;
        }
        
        // Обновление конфигурации на основе паттернов использования
        let usage_patterns = self.storage.get_usage_patterns().await?;
        self.adapt_configuration(&usage_patterns).await?;
        
        // Автоматическое исправление обнаруженных проблем
        self.self_heal().await?;
        
        Ok(())
    }
}

Ключевые преимущества Rust для этого проекта:

  • Безопасность памяти: предотвращает сбои, которые могли бы повредить экспонат
  • Параллелизм: эффективная работа с несколькими потоками для одновременного мониторинга и обслуживания
  • Отсутствие GC: предсказуемое время выполнения для критически важных операций
  • Современные функции: структурные омбудсмены и потоковые итераторы для элегантной обработки данных

Интеграция Gajae-Code и LazyCodex

Интеграция Gajae-Code и LazyCodex позволяет агенту автоматически развиваться, используя передовые инструменты для кода и потоковой обработки данных.

rust
// Конфигурация интеграции кодогенератора и ленивой обработки
struct IntegrationConfig {
    code_generator: GajaeCodeConfig,
    data_pipeline: LazyCodexPipeline,
    sync_interval: Duration,
    adaptation_threshold: f32,
}

impl ExhibitAgent {
    async fn integrate_code_systems(&mut self, config: &IntegrationConfig) -> Result<(), IntegrationError> {
        // Инициализация Gajae-Code для генерации кода
        let mut code_gen = GajaeCode::new(&config.code_generator).await?;
        
        // Настройка LazyCodex для потоковой обработки данных
        let mut data_stream = LazyCodex::new(&config.data_pipeline).await?;
        
        loop {
            // Генерация нового кода на основе паттернов использования
            let generated_code = code_gen.generate(&self.get_usage_metrics().await?).await?;
            
            // Потоковая обработка с ленивой оценкой
            let processed_data = data_stream.process(generated_code).await?;
            
            // Валидация и развертывание
            if self.validate_changes(&processed_data).await? {
                self.deploy_changes(&processed_data).await?;
            }
            
            // Ожидание следующего цикла
            tokio::time::sleep(config.sync_interval).await;
        }
    }
}

Архитектура интеграции:

  1. Gajae-Code генерирует оптимизированный Rust-код на основе исторических данных и паттернов использования
  2. LazyCodex обрабатывает данные потоково, минимизируя использование памяти
  3. Автоматическая адаптация происходит когда использование превышает порог в 80%
yaml
# Конфигурация интеграции (пример)
integration:
  code_generator:
    engine: gajae-code
    model: rust-optimized-v2
    auto_commit: true
  data_pipeline:
    processor: lazycodex
    mode: streaming
    batch_size: 1000
    backpressure: enabled
  synchronization:
    interval: 5m
    adaptation_threshold: 0.8

Эта интеграция создает самоподдерживающуюся экосистему, где экспонат не только отображает контент, но и постоянно совершенствует себя, оптимизируя производительность и расширяя свои возможности без человеческого вмешательства.

Результат: полностью автономный музейный экспонат, который развивается, адаптируется и обслуживает себя — настоящая демонстрация предсказуемости и надежности Rust в действии.

Автономное управление

Агентная архитектура и самообслуживание

С практической точки зрения, агентная архитектура в ultraworkers/claw-code — это не просто очередной демон, а настоящий автономный житель домашней лаборатории. Представьте, что у вас есть робот-экспонат, который не только демонстрирует Rust-код, но и сам следит за своим здоровьем.

Во-первых, агент работает по принципу «следящего за собой» сервиса. Он использует tokio для асинхронного управления и serde для сериализации состояния. Основная идея — хранение конфигурации в YAML, который агент обновляет по мере необходимости:

yaml
# config/agent.yaml
agent:
  id: "claw-code"
  version: "1.2.0"
  self_update:
    enabled: true
    channel: "stable"
  health_check:
    interval: "5m"
    endpoints:
      - "http://localhost:8080/health"
      - "http://localhost:8081/metrics"

Во-вторых, самообслуживание реализовано через cron-задачи, которые агент периодически перезапрашивает и исполняет. Это как домашний сервер, который сам перезагружает обновления, когда вы не дома:

rust
// src/self_maintenance.rs
impl Agent {
    async fn run_self_maintenance(&self) {
        // Проверка обновлений каждые 30 минут
        let mut interval = Interval::new(Interval::from_millis(30000));
        
        while let Some(_) = interval.next().await {
            if let Err(e) = self.check_for_updates().await {
                tracing::error!("Failed to check updates: {}", e);
            }
            
            if let Err(e) = self.rotate_logs().await {
                tracing::error!("Log rotation failed: {}", e);
            }
        }
    }
}

С практической точки зрения, агент — это как опытный системный администратор, который никогда не забывает про ротацию логов и обновления. Он также следит за своими зависимостями через cargo update, что предсказуемо поддерживает проект в актуальном состоянии.

CI/CD и развертывание без человека

CI/CD пайплайн в ultraworkers/claw-code — это пример того, как можно полностью автоматизировать разработку, оставив минимум человеческого вмешательства. Представьте, что у вас есть сборочная линия, которая работает сама по себе — как фабрика по производству LEGO, где детали сами собираются в конструктор.

Во-первых, GitHub Actions используется для непрерывной интеграции и развертывания. Пайплайн включает в себя статическую типизацию, тестирование и автоматическое развертывание на целевой системе:

yaml
# .github/workflows/ci-cd.yml
name: CI/CD for claw-code
on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - name: Setup Rust
        uses: actions-rs/toolchain@v1
        with:
          toolchain: stable
      - name: Run tests
        run: cargo test --verbose
      - name: Build release
        run: cargo build --release

  deploy:
    needs: test
    runs-on: размещение на домашних серверах
    if: github.ref == 'refs/heads/main'
    steps:
      - uses: actions/checkout@v3
      - name: Deploy to server
        run: |
          scp target/release/claw-code user@museum-server:/opt/claw-code/
          ssh user@museum-server "/opt/claw-code/claw-code --reload"

Во-вторых, развертывание полностью автоматизировано с использованием systemd на целевой системе. Сервисный файл — это как команда, которая говорит серверу: "запускай этот процесс и следи за ним":

ini
# /etc/systemd/system/claw-code.service
[Unit]
Description=Autonomous Museum Artifact (claw-code)
After=network.target

[Service]
Type=simple
User=claw-code
Group=claw-code
WorkingDirectory=/opt/claw-code
ExecStart=/opt/claw-code/claw-code --config /opt/claw-code/config/agent.yaml
Restart=always
RestartSec=30
StandardOutput=journal
StandardError=journal

[Install]
WantedBy=multi-user.target

С практической точки зрения, весь процесс — это как автоматизированная сборочная линия на производстве, где детали сами собираются, проверяются и отправляются на склад без участия человека. Агент периодически проверяет новый код, запускает тесты и, если всё в порядке, обновляет сервис — предсказуемо и без эмоциональных всплесков.

Такой подход позволяет поддерживать проект в актуальном состоянии, даже когда вы заняты другими делами. Это как домашний сервер, который сам обновляется, пока вы смотрите фильм — вы знаете, что всё работает, потому что система сама следит за собой.

Взаимодействие посетителей

Взаимодействие посетителей

Интерактивные элементы и UI

Проект ultraworkers/claw-code использует эргономичный интерфейс на основе tui-rs — Rust-библиотеки для создания терминальных пользовательских интерфейсов. Это позволяет экспонату работать полностью в headless-среде, но при этом предоставлять посетителям интерактивный опыт через веб-шлюз.

rust
// src/ui/mod.rs — основная логика терминального интерфейса
use tui::widgets::{Block, Borders, Paragraph};
use tui::style::{Style, Color};
use tui::layout::{Layout, Constraint};

pub struct ExhibitUI {
    pub title: String,
    pub current_view: ViewMode,
    pub interaction_history: Vec<InteractionEvent>,
}

enum ViewMode {
    Overview,
    Details,
    Configuration,
}

impl ExhibitUI {
    fn render(&self) -> Result<()> {
        // Создаем layout с тремя панелями: меню, контент, статус
        let layout = Layout::default()
            .direction(Orientation::Vertical)
            .constraints([
                Constraint::Length(3),
                Constraint::Min(5),
                Constraint::Length(2),
            ])
            .split(Rect::default());

        // Верхняя панель с навигацией
        let nav_block = Block::default()
            .borders(Borders::ALL)
            .title("Навигация");
        
        // Центральная панель с основным содержимым
        let content_block = Block::default()
            .borders(Borders::ALL)
            .title("Информация об экспонате");
        
        // Нижняя панель со статусом системы
        let status_block = Block::default()
            .bordes(Borders::ALL)
            .title("Состояние агента");

        // Рендерим компоненты
        // ... реализация отрисовки
    }
}

Для веб-интерфейса используется Yew — Rust-фреймворк для создания SPA. Он компилируется в WASM и позволяет посетителям взаимодействовать с экспонатом через браузер, получая при этом быстрый отклик и работу без загрузки страниц.

rust
// src/web/src/lib.rs — компонент веб-интерфейса
use yew::prelude::*;

#[function_component(ExhibitViewer)]
pub fn exhibit_viewer() -> Html {
    let exhibits = use_state(|| vec![]);
    
    let on_select = {
        let exhibits = exhibits.clone();
        Callback::from(move |exhibit_id: String| {
            let exhibits = exhibits.clone();
            // Асинхронная загрузка данных экспоната
            wasm_bindgen_futures::spawn_local(async move {
                // Запрос к REST API агента
                let response = fetch_exhibit_data(&exhibit_id).await;
                exhibits.set(response);
            });
        })
    };

    html! {
        <div class="exhibit-viewer">
            <nav class="exhibit-nav">
                <h3>{"Доступные экспонаты"}</h3>
                <ul>
                    {for exhibits.iter().map(|exhibit: &Exhibit| {
                        html! {
                            <li onclick={on_select.clone().reform(move |_| exhibit.id.clone())}>
                                {&exhibit.name}
                            </li>
                        }
                    })}
                </ul>
            </nav>
            
            <main class="exhibit-content">
                if !exhibits.is_empty() {
                    <ExhibitDetails exhibit={exhibits[0].clone()} />
                } else {
                    <div class="placeholder">
                        {"Выберите экспонат для просмотра"}
                    </div>
                }
            </main>
        </div>
    }
}

Конфигурация веб-шлюза настраивается через Nginx с использованием reverse proxy для терминального интерфейса и статических файлов для веб-клиента.

nginx
# nginx.conf — конфигурация веб-шлюза для музея
http {
    upstream exhibit_tui {
        server 127.0.0.1:3000;
    }

    upstream exhibit_wasm {
        server 127.0.0.1:8080;
    }

    server {
        listen 80;
        server_name exhibit.local;

        # Терминальный интерфейс
        location /api/ {
            proxy_pass http://exhibit_tui;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
        }

        # Веб-клиент (WASM)
        location / {
            alias /usr/local/share/claw-code/web/dist/;
            
            # Кэширование статических ресурсов
            location ~* \.(js|css|wasm|png|jpg|svg|ico)$ {
                expires 1y;
                add_header Cache-Control "public, immutable";
            }
        }
    }
}

Данные и персонализация опыта

Агент ultraworkers/claw-code использует SQLite для локального хранения данных о посетителях и их взаимодействиях. Это обеспечивает автономность экспоната и предсказуемую производительность без зависимости от внешних сервисов.

rust
// src/data/visitor.rs — модель данных посетителя
use serde::{Serialize, Deserialize};
use rusqlite::{params, Connection, Result};

#[derive(Debug, Serialize, Deserialize)]
pub struct Visitor {
    pub id: String,
    pub session_token: String,
    pub interaction_count: u32,
    pub last_interaction: i64,
    pub preferences: VisitorPreferences,
}

#[derive(Debug, Serialize, Deserialize)]
pub struct VisitorPreferences {
    pub theme: String,        // light/dark/system
    pub language: String,     // ru/en
    pub interaction_style: String, // detailed/quick
}

impl Visitor {
    pub fn new(visitor_id: &str) -> Result<Self, rusqlite::Error> {
        let conn = Connection::open("exhibit.db")?;
        
        conn.execute(
            "CREATE TABLE IF NOT EXISTS visitors (
                id TEXT PRIMARY KEY,
                session_token TEXT NOT NULL,
                interaction_count INTEGER DEFAULT 0,
                last_interaction INTEGER NOT NULL,
                preferences TEXT NOT NULL
            )",
            [],
        )?;

        let visitor = Visitor {
            id: visitor_id.to_string(),
            session_token: uuid::Uuid::new_v4().to_string(),
            interaction_count: 0,
            last_interaction: chrono::Utc::now().timestamp(),
            preferences: VisitorPreferences {
                theme: "system".to_string(),
                language: "ru".to_string(),
                interaction_style: "detailed".to_string(),
            },
        };

        conn.execute(
            "INSERT INTO visitors (id, session_token, interaction_count, last_interaction, preferences) 
             VALUES (?1, ?2, ?3, ?4, ?5)",
            params![
                visitor.id,
                visitor.session_token,
                visitor.interaction_count,
                visitor.last_interaction,
                serde_json::to_string(&visitor.preferences)?
            ],
        )?;

        Ok(visitor)
    }

    pub fn update_preferences(&mut self, new_prefs: VisitorPreferences) -> Result<(), rusqlite::Error> {
        let conn = Connection::open("exhibit.db")?;
        
        conn.execute(
            "UPDATE visitors 
             SET preferences = ?1, last_interaction = ?2
             WHERE id = ?3",
            params![
                serde_json::to_string(&new_prefs)?,
                chrono::Utc::now().timestamp(),
                self.id
            ],
        )?;

        self.preferences = new_prefs;
        self.last_interaction = chrono::Utc::now().timestamp();
        Ok(())
    }
}

Для персонализации опыта используется Redis в качестве кэша горячих данных. Он хранит сессионную информацию, историю взаимодействий и предварительно вычисленные рекомендации для быстрого доступа.

rust
// src/cache/session_cache.rs — кэширование сессий посетителей
use redis::{Client, Commands};
use serde::{Serialize, Deserialize};

pub struct SessionCache {
    client: Client,
}

#[derive(Debug, Serialize, Deserialize)]
pub struct SessionData {
    pub visitor_id: String,
    pub current_exhibit: String,
    pub interaction_history: Vec<InteractionEvent>,
    pub personalization_score: f32,
}

impl SessionCache {
    pub fn new(redis_url: &str) -> Result<Self, redis::RedisError> {
        let client = Client::open(redis_url)?;
        Ok(Self { client })
    }

    pub fn get_session(&self, session_id: &str) -> Result<Option<SessionData>, redis::RedisError> {
        let mut conn = self.client.get_connection()?;
        let data: Option<String> = conn.get(session_id)?;
        
        match data {
            Some(json_data) => {
                let session = serde_json::from_str(&json_data)?;
                Ok(Some(session))
            }
            None => Ok(None),
        }
    }

    pub fn set_session(&self, session_id: &str, data: &SessionData, ttl: usize) -> Result<(), redis::RedisError> {
        let mut conn = self.client.get_connection()?;
        let json_data = serde_json::to_string(data)?;
        conn.set_ex(session_id, json_data, ttl)?;
        Ok(())
    }
}

Агент автоматически анализирует паттерны взаимодействия и адаптирует UI в режиме реального времени. Алгоритм учитывает:

  • Частоту и продолжительность взаимодействий
  • Предпочитаемые разделы экспоната
  • Температуру и время суток (для адаптации яркости и контрастности)
  • Уровень технической подготовки (детализированный vs упрощённый режим)
rust
// src/agent/personalization.rs — логика персонализации
use std::collections::HashMap;

pub struct PersonalizationEngine {
    weights: HashMap<String, f32>,
}

impl PersonalizationEngine {
    pub fn new() -> Self {
        let mut weights = HashMap::new();
        weights.insert("interaction_frequency".to_string(), 0.3);
        weights.insert("section_preferences".to_string(), 0.4);
        weights.insert("time_of_day".to_string(), 0.2);
        weights.insert("technical_level".to_string(), 0.1);
        
        Self { weights }
    }

    pub fn calculate_score(&self, visitor_data: &VisitorAnalytics) -> f32 {
        let mut total_score = 0.0;
        
        for (factor, weight) in &self.weights {
            let factor_score = match factor.as_str() {
                "interaction_frequency" => self.calculate_interaction_score(visitor_data),
                "section_preferences" => self.calculate_section_score(visitor_data),
                "time_of_day" => self.calculate_time_score(visitor_data),
                "technical_level" => self.calculate_level_score(visitor_data),
                _ => 0.0,
            };
            
            total_score += factor_score * weight;
        }
        
        (total_score * 100.0).min(100.0)
    }

    fn calculate_interaction_score(&self, data: &VisitorAnalytics) -> f32 {
        // Больше взаимодействий = выше персонализация
        (data.interaction_count as f32 / 100.0).min(1.0)
    }

    // ... другие методы расчета факторов
}

Для обеспечения предсказуемости работы системы используется Prometheus для метрики взаимодействий посетителей, а Alertmanager оповещает администратора при аномалиях в поведении экспоната.

yaml
# prometheus.yml — конфигурация мониторинга для музейного экспоната
global:
  scrape_interval: 15s

scrape_configs:
  - job_name: 'exhibit-agent'
    static_configs:
      - targets: ['localhost:9090']
    metrics_path: '/metrics'
    scrape_interval: 10s

  - job_name: 'redis-exporter'
    static_configs:
      - targets: ['redis-exporter:9121']
    scrape_interval: 30s

  - job_name: 'node-exporter'
    static_configs:
      - targets: ['node-exporter:9100']
    scrape_interval: 30s

rule_files:
  - "alert_rules.yml"

alerting:
  alert_relabel_configs:
    - source_labels: [severity]
      target_label: severity
      regex: critical
      replacement: "critical"

Алгоритмы персонализации учитывают не только технические аспекты, но и культурные особенности посетителей. Для русскоязычных пользователей экспонат автоматически переключается на русский интерфейс, использует местные исторические ссылки и адаптирует сложность объяснений под образовательный уровень.

rust
// src/i18n/l10n.rs — локализация интерфейса для разных языков
use std::collections::HashMap;

pub struct I18nEngine {
    translations: HashMap<String, HashMap<String, String>>,
}

impl I18nEngine {
    pub fn new() -> Self {
        let mut translations = HashMap::new();
        
        // Русские переводы
        let mut ru_translations = HashMap::new();
        ru_translations.insert("exhibit.select".to_string(), "Выберите экспонат".to_string());
        ru_translations.insert("exhibit.details".to_string(), "Подробности".to_string());
        ru_translations.insert("exhibit.interaction".to_string(), "Взаимодействие".to_string());
        translations.insert("ru".to_string(), ru_translations);
        
        // Английские переводы
        let mut en_translations = HashMap::new();
        en_translations.insert("exhibit.select".to_string(), "Select exhibit".to_string());
        en_translations.insert("exhibit.details".to_string(), "Details".to_string());
        en_translations.insert("exhibit.interaction".to_string(), "Interaction".to_string());
        translations.insert("en".to_string(), en_translations);
        
        Self { translations }
    }

    pub fn translate(&self, key: &str, language: &str) -> String {
        if let Some(lang_map) = self.translations.get(language) {
            lang_map.get(key).cloned().unwrap_or_else(|| key.to_string())
        } else {
            // Фолбэк на английский
            self.translations.get("en")
                .and_then(|m| m.get(key))
                .cloned()
                .unwrap_or_else(|| key.to_string())
        }
    }
}

Система автоматически определяет язык на основе IP-адреса посетителя и его предыдущих предпочтений, обеспечивая бесперебойную работу экспоната без вмешательства оператора.

Поделиться:TelegramX / TwitterVK