stelegen 0.0.6

JSON-first, type-safe i18n codegen with pluggable per-language emitters
use super::Emitter;
use crate::ir::Ir;

/// Emits reactive React / React Native bindings over the generated copy. The
/// output is generic over the catalog (it just wraps `createCopy` in a Context +
/// `useState`), so it ignores the IR entirely — only the import path to the core
/// module varies. Uses `createElement` rather than JSX so the file needs no JSX
/// build step and works the same in React DOM and React Native.
pub struct ReactEmitter {
    pub core: String,
}

const TEMPLATE: &str = r#"// AUTO-GENERATED by stele — do not edit.
// Reactive React / React Native bindings over the generated copy.
// Changing the locale via setLocale re-renders every useCopy() consumer.
import {
  createContext,
  createElement,
  useContext,
  useMemo,
  useState,
  type ReactNode,
} from "react";
import { createCopy, type Copy, type Locale } from "__CORE__";

type CopyContextValue = {
  copy: Copy;
  locale: Locale;
  setLocale: (locale: Locale) => void;
};

const CopyContext = createContext<CopyContextValue | null>(null);

export function CopyProvider(props: { locale: Locale; children: ReactNode }) {
  const [locale, setLocale] = useState<Locale>(props.locale);
  const copy = useMemo(() => createCopy(locale), [locale]);
  const value = useMemo(() => ({ copy, locale, setLocale }), [copy, locale]);
  return createElement(CopyContext.Provider, { value }, props.children);
}

export function useCopy(): Copy {
  const ctx = useContext(CopyContext);
  if (ctx === null) {
    throw new Error("useCopy must be used within a <CopyProvider>");
  }
  return ctx.copy;
}

export function useLocale(): [Locale, (locale: Locale) => void] {
  const ctx = useContext(CopyContext);
  if (ctx === null) {
    throw new Error("useLocale must be used within a <CopyProvider>");
  }
  return [ctx.locale, ctx.setLocale];
}
"#;

impl Emitter for ReactEmitter {
    fn emit(&self, _ir: &Ir) -> String {
        TEMPLATE.replace("__CORE__", &self.core)
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::collections::BTreeMap;

    fn empty_ir() -> Ir {
        Ir {
            canonical: "en".into(),
            locales: vec!["en".into()],
            messages: vec![],
            plural_rules: BTreeMap::new(),
        }
    }

    #[test]
    fn substitutes_core_and_exports_bindings() {
        let out = ReactEmitter {
            core: "./my-copy".into(),
        }
        .emit(&empty_ir());
        assert!(out.contains("from \"./my-copy\""));
        assert!(out.contains("export function CopyProvider"));
        assert!(out.contains("export function useCopy"));
        assert!(out.contains("export function useLocale"));
    }
}