Refactor: remove desktop/Tauri bundle, add i18n, CI/CD pipeline
Nightly Build / build (push) Failing after 6s

- Remove desktop (Tauri) bundle: docs/desktop.md, icons/, tauri.conf.json,
  src/desktop/mod.rs, gen/schemas/
- Remove build.rs (no longer needed)
- Add i18n system (crates/core-api, plugin-mobile-connector, web)
- Refactor config system (src/config.rs, boot_format.rs)
- Add mobile connector features (app, router, device pairing)
- Plugin system improvements (skald-core)
- Update dependencies (Cargo.lock, Cargo.toml)
- CI/CD: Gitea Actions workflows (nightly + release), package.sh,
  verify-version.sh, builds.skaldagent.net config
This commit is contained in:
2026-07-19 22:35:06 +01:00
parent ba911ae8cb
commit fb3eeeeec6
54 changed files with 823 additions and 8002 deletions
+55
View File
@@ -0,0 +1,55 @@
//! Backend localization contract shared by the core and every plugin.
//!
//! Two halves:
//! - [`LocaleBundle`] — what a plugin *declares* (its translation table for one
//! locale), returned from `Plugin::i18n()` and collected into a single catalog
//! at boot. Keys must be namespaced (`plugin.<id>.<key>`) so bundles from
//! different plugins — and the core — merge without clobbering each other, and
//! so the same key can back the frontend fragment's `t()` string.
//! - [`I18nApi`] — what a plugin *calls* at request time to turn a key into text
//! for the caller. Injected into `PluginContext.i18n`; the concrete impl lives
//! in `skald-core` (it owns the locale-resolution chain and the system pool).
//!
//! The core never emits user-facing text through a hardcoded English literal
//! once it can go through this seam — a plugin's own error/generated strings
//! reach the user in the user's language, mirroring the frontend `i18n.js`.
use std::collections::HashMap;
use async_trait::async_trait;
/// One namespace's translation table for a single locale, as declared by a
/// plugin (or the core). Merged into the boot-time catalog keyed by locale;
/// keys collide across bundles only if two authors reuse the same fully
/// qualified key, which the `plugin.<id>.` convention prevents.
#[derive(Debug, Clone)]
pub struct LocaleBundle {
/// Locale code — `"en"`, `"it"`, `"fr"`. Must match a supported locale;
/// anything else is simply never selected by the resolver.
pub locale: String,
/// Fully qualified key → translated string. Placeholders are `{name}`.
pub strings: HashMap<String, String>,
}
impl LocaleBundle {
pub fn new(locale: impl Into<String>, strings: HashMap<String, String>) -> Self {
Self { locale: locale.into(), strings }
}
}
/// Runtime translation, injected into [`crate::plugin::PluginContext`].
///
/// The catalog behind it is built once at boot from every plugin's
/// `Plugin::i18n()`. Resolution mirrors the rest of the system: a user's
/// `users.locale` override → the instance default → built-in English → the raw
/// key as a last resort. Placeholders (`{name}`) are filled from `args`.
#[async_trait]
pub trait I18nApi: Send + Sync {
/// Translate `key` for `user_id`, resolving *their* effective locale. Use
/// this from any request/notification path where the target user is known.
async fn for_user(&self, user_id: &str, key: &str, args: &[(&str, &str)]) -> String;
/// Translate for an already-resolved locale — for contexts with no single
/// user (boot logs, broadcast copy) that have decided a locale by other means.
fn get(&self, locale: &str, key: &str, args: &[(&str, &str)]) -> String;
}
+1
View File
@@ -9,6 +9,7 @@ pub mod chatbot;
pub mod chat_hub;
pub mod command;
pub mod events;
pub mod i18n;
pub mod image_generate;
pub mod inbox;
pub mod interface_tool;
+15 -1
View File
@@ -7,6 +7,7 @@ use tokio::sync::RwLock;
use crate::command::CommandApi;
use crate::config_api::ConfigApi;
use crate::i18n::I18nApi;
use crate::system_bus::SystemEventBus;
use crate::image_generate::ImageGenerateRegistry;
use crate::location::LocationUpdater;
@@ -90,6 +91,10 @@ pub struct PluginContext {
/// Per-user plugin configuration store (`plugin_user_configs` table).
/// Admin-readable — never secrets.
pub user_config: Arc<dyn PluginUserConfigApi>,
/// Backend localization. Turns a plugin's namespaced string key into text in
/// the caller's language (`i18n.for_user(user_id, key, args)`). The catalog
/// is built at boot from every plugin's [`Plugin::i18n`]. See `core_api::i18n`.
pub i18n: Arc<dyn I18nApi>,
pub web_port: u16,
pub remote_slot: Arc<RwLock<Option<Arc<dyn RemoteAccess>>>>,
pub router_factory: RouterFactory,
@@ -174,7 +179,9 @@ pub trait Plugin: Send + Sync {
/// `/api/plugin/<id>/…` — no host APIs are injected;
/// - it runs with the full privileges of the logged-in session (plugins are
/// trusted — they ship in the binary);
/// - it carries its own UI strings (reads the locale from `/api/auth/me`).
/// - it localizes by shipping its own `{en,it,fr}` string table and
/// registering it via `addStrings` into the host's shared `i18n.js`, then
/// using the same `t()`/`I18nMixin` (keys namespaced `plugin.<id>.`).
///
/// Default: no pages.
fn web_pages(&self) -> Vec<PluginPage> { Vec::new() }
@@ -192,6 +199,13 @@ pub trait Plugin: Send + Sync {
/// is stopped. Default: no tools.
fn tools(self: Arc<Self>) -> Vec<Arc<dyn crate::tool::Tool>> { Vec::new() }
/// Backend translation tables this plugin contributes — one
/// [`crate::i18n::LocaleBundle`] per locale it ships. Collected once at boot
/// into the shared catalog behind [`PluginContext::i18n`]. Keys must be
/// namespaced (`plugin.<id>.<key>`). Default: no strings (plugin emits no
/// localized backend text). See `core_api::i18n`.
fn i18n(&self) -> Vec<crate::i18n::LocaleBundle> { Vec::new() }
/// Returns a [`Memory`] backend if this plugin provides one.
fn memory(&self) -> Option<Arc<dyn Memory>> { None }