use super::{Binding, Emitter};
use crate::ir::Ir;
pub struct ReactEmitter {
pub store: String,
pub binding: Binding,
}
const TEMPLATE: &str = r#"// AUTO-GENERATED by stele — do not edit.
// React / React Native hooks bound to the locale store. Calling setLocale
// anywhere (see the store module) re-renders every __HOOK__() consumer. There's
// no Provider to mount — the store is the single source of truth.
import { useSyncExternalStore } from "react";
import {
__GETTER__,
getLocale,
setLocale,
subscribeLocale,
isFollowingDevice,
type __TY__,
type Locale,
} from "__STORE__";
export function __HOOK__(): __TY__ {
return useSyncExternalStore(subscribeLocale, __GETTER__, __GETTER__);
}
export function useLocale(): [Locale, (locale: Locale) => void] {
const locale = useSyncExternalStore(subscribeLocale, getLocale, getLocale);
return [locale, setLocale];
}
/** Whether the active locale is following the device (for a "System" toggle). */
export function useFollowingDevice(): boolean {
return useSyncExternalStore(
subscribeLocale,
isFollowingDevice,
isFollowingDevice,
);
}
"#;
const REACT_JS: &str = r#"// AUTO-GENERATED by stele — do not edit.
import { useSyncExternalStore } from "react";
import {
__GETTER__,
getLocale,
setLocale,
subscribeLocale,
isFollowingDevice,
} from "./store.js";
export function __HOOK__() {
return useSyncExternalStore(subscribeLocale, __GETTER__, __GETTER__);
}
export function useLocale() {
const locale = useSyncExternalStore(subscribeLocale, getLocale, getLocale);
return [locale, setLocale];
}
export function useFollowingDevice() {
return useSyncExternalStore(
subscribeLocale,
isFollowingDevice,
isFollowingDevice,
);
}
"#;
const REACT_DTS: &str = r#"// AUTO-GENERATED by stele — do not edit.
import type { __TY__, Locale } from "./store.js";
export declare function __HOOK__(): __TY__;
export declare function useLocale(): [Locale, (locale: Locale) => void];
export declare function useFollowingDevice(): boolean;
"#;
pub fn react_js(binding: &Binding) -> String {
REACT_JS
.replace("__GETTER__", &binding.getter())
.replace("__HOOK__", &binding.hook())
}
pub fn react_dts(binding: &Binding) -> String {
REACT_DTS
.replace("__HOOK__", &binding.hook())
.replace("__TY__", &binding.ty)
}
impl Emitter for ReactEmitter {
fn emit(&self, _ir: &Ir) -> String {
TEMPLATE
.replace("__STORE__", &self.store)
.replace("__GETTER__", &self.binding.getter())
.replace("__HOOK__", &self.binding.hook())
.replace("__TY__", &self.binding.ty)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::emit::Binding;
use std::collections::BTreeMap;
fn empty_ir() -> Ir {
Ir {
canonical: "en".into(),
locales: vec!["en".into()],
messages: vec![],
plural_rules: BTreeMap::new(),
}
}
#[test]
fn binds_hooks_to_the_store_with_default_binding() {
let out = ReactEmitter {
store: "./stele.store".into(),
binding: Binding::new("stele"),
}
.emit(&empty_ir());
assert!(out.contains("from \"./stele.store\""));
assert!(out.contains("useSyncExternalStore"));
assert!(out.contains("export function useStele(): Stele"));
assert!(out.contains("useSyncExternalStore(subscribeLocale, getStele, getStele)"));
assert!(out.contains("export function useLocale"));
assert!(!out.contains("SteleProvider"));
assert!(!out.contains("createContext"));
assert!(!out.contains("__"));
}
#[test]
fn binding_renames_the_hook_and_getter() {
let out = ReactEmitter {
store: "./stele.store".into(),
binding: Binding::new("copy"),
}
.emit(&empty_ir());
assert!(out.contains("export function useCopy(): Copy"));
assert!(out.contains("subscribeLocale, getCopy, getCopy"));
}
}