cloudiful-bevy-localization 0.1.2

Reusable Bevy localization runtime built around app-provided static registries.
Documentation
# cloudiful-bevy-localization

Reusable Bevy localization runtime for apps that generate their own static
locale registry at build time.

## What it provides

- `LocalizationDefinition`: static description of fallback locale, supported
  locales, locale sources, and declared keys
- `LocaleSource`: one TOML payload bound to a locale + namespace
- `Locale`: runtime locale handle with serialization support
- `TextKey`: runtime text key handle with serialization support
- `Localization`: Bevy resource for lookup, formatting, locale switching, and
  table access
- `LocalizationPlugin`: registers a definition and inserts the `Localization`
  resource
- `LocalizationLoadError`: structured load/validation failure
- `register_definition(...)`: definition registry hook used by the plugin and
  helper types
- `locale_name_key_id(...)`: helper for `common.locale_name.<locale>` key
  generation

## What it does not provide

- scanning downstream `assets/i18n`
- generating app-specific key constants
- embedding downstream `OUT_DIR` artifacts inside the crate
- editor tooling, extraction, or translation workflows

This crate expects the downstream app to generate or hand-author a static
registry and then pass that definition into the plugin.

## Usage

Generate a static registry in the downstream app and pass it into the plugin.
`LocalizationPlugin::new(...)` registers the definition first and then inserts
the `Localization` resource built from that definition.

```rust
use bevy::prelude::*;
use cloudiful_bevy_localization::{
    Locale, LocaleSource, Localization, LocalizationDefinition,
    LocalizationPlugin, TextKey,
};

const KEYS: &[TextKey] = &[TextKey::new("common.hello")];
const SOURCES: &[LocaleSource] = &[
    LocaleSource {
        locale: "en-US",
        namespace: "common",
        contents: r#"
hello = "Hello"
[locale_name]
en_us = "English"
zh_cn = "Chinese"
"#,
    },
    LocaleSource {
        locale: "zh-CN",
        namespace: "common",
        contents: r#"
hello = "你好"
[locale_name]
en_us = "英语"
zh_cn = "中文"
"#,
    },
];

static LOCALIZATION: LocalizationDefinition = LocalizationDefinition {
    fallback_locale: "en-US",
    locales: &["en-US", "zh-CN"],
    sources: SOURCES,
    keys: KEYS,
};

fn main() {
    App::new()
        .add_plugins(LocalizationPlugin::new(&LOCALIZATION))
        .add_systems(Update, read_text);
}

fn read_text(localization: Res<Localization>) {
    assert_eq!(localization.current_locale(), Locale::new("en-US"));
    assert_eq!(localization.text(TextKey::new("common.hello")), "Hello");
}
```

At runtime, use the resource for lookup, formatting, locale switching, and
locale display text:

```rust
use bevy::prelude::*;
use cloudiful_bevy_localization::{Locale, Localization, TextKey};

fn ui_text(mut localization: ResMut<Localization>) {
    let text = localization.text(TextKey::new("common.hello")).to_string();
    let formatted = localization.format_text(TextKey::new("common.hello"), []);

    localization.set_locale(Locale::new("zh-CN"));

    let current = localization.current_locale();
    let available = localization.available_locales();
    let display = localization.locale_display_text(current);

    println!("{text} / {formatted} / {display} / {available:?}");
}
```

## Validation Rules

Definitions are validated at load time. A load fails when any of these rules are
broken:

- the fallback locale table is missing
- a listed locale has no table
- a declared `TextKey` is missing from any locale
- a `common.locale_name.<locale>` entry is missing for any supported locale
- a locale table contains keys that are not declared
- placeholder names differ from the fallback locale for the same key
- a locale source references a locale not listed in `LocalizationDefinition`
- a TOML value is not a string or nested table
- the same flattened key appears more than once

`LocaleSource.contents` TOML is flattened by namespace, so:

```toml
[nested]
label = "Nested"
```

under namespace `common` becomes `common.nested.label`.

## Runtime Behavior

- `Localization::text(...)` looks up the current locale first, then the fallback
  locale, and panics only if both are missing
- `Localization::format_text(...)` performs simple `{name}` replacement on the
  selected text
- `Localization::lookup(...)` and `lookup_id(...)` return `Option<&str>` without
  panicking
- `Localization::table(...)` returns the flattened table for one locale
- `Localization::locale_display_text(...)` looks up
  `common.locale_name.<locale>` in the current locale, then fallback locale, and
  finally returns the raw locale id

## Locale and Key Helpers

- `Locale::new(...)` and `TextKey::new(...)` create static handles
- `Locale::available()` reads available locales from the active registered
  definition
- `Locale::from_serialized(...)` normalizes case and punctuation when matching
  serialized locale ids
- `TextKey::from_id(...)` resolves an id against the active registered
  definition