Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
456 changes: 456 additions & 0 deletions Cargo.lock

Large diffs are not rendered by default.

7 changes: 7 additions & 0 deletions crates/cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ name = "next-intl-extractor-cli"
version = "0.1.1"
edition = "2021"

[lib]
crate-type = ["cdylib", "rlib"]

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
Expand All @@ -14,6 +17,10 @@ tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
serde_json = "1.0.120"
notify = { version = "7.0.0", features = ["serde"] }
wasm-bindgen = "0.2"
wasm-bindgen-cli-support = "0.2"
wasm-bindgen-futures = "0.4"
getrandom = { version = "0.2", features = ["js"] }

[dev-dependencies]
tempfile = "3.10.1"
44 changes: 44 additions & 0 deletions crates/cli/src/fs.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
use std::path::Path;
use anyhow::Result;

/// Trait for abstracting file system operations
/// This is due to the wasm approach where we need to use JS hooks to access the filesystem
pub trait FileSystem {
/// Read a file's contents as a string
fn read_to_string(&self, path: &Path) -> Result<String>;

/// Write a string to a file, creating the file if it doesn't exist
fn write(&self, path: &Path, contents: &str) -> Result<()>;

/// Create a directory and all its parent components if they are missing
fn create_dir_all(&self, path: &Path) -> Result<()>;

/// Check if a path exists
fn exists(&self, path: &Path) -> bool;
}

/// Default implementation using std::fs
pub struct StdFileSystem;

impl FileSystem for StdFileSystem {
fn read_to_string(&self, path: &Path) -> Result<String> {
Ok(std::fs::read_to_string(path)?)
}

fn write(&self, path: &Path, contents: &str) -> Result<()> {
Ok(std::fs::write(path, contents)?)
}

fn create_dir_all(&self, path: &Path) -> Result<()> {
Ok(std::fs::create_dir_all(path)?)
}

fn exists(&self, path: &Path) -> bool {
path.exists()
}
}

/// Get the default file system implementation
pub fn default_fs() -> StdFileSystem {
StdFileSystem
}
3 changes: 3 additions & 0 deletions crates/cli/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
pub mod messages;
pub mod fs;
pub mod watch;
6 changes: 5 additions & 1 deletion crates/cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,14 @@ use crate::watch::watch;
use anyhow::{anyhow, Context, Error};
use clap::{arg, command, Parser};
use next_intl_resolver::extract_translations;
use fs::{default_fs, StdFileSystem};

use tracing::{error, info, span, Level};

pub mod files;
pub mod messages;
pub mod watch;
pub mod fs;

#[derive(Parser, Debug)]
#[command(name = "next-intl-resolver")]
Expand Down Expand Up @@ -62,8 +64,10 @@ fn run() -> Result<(), Error> {
);
}

let fs = default_fs();

// Initialize message handler
let mut message_handler = MessageHandler::new(&args.output_path)?;
let mut message_handler = MessageHandler::new(&args.output_path, fs)?;

// Find and process files
let files = find_files(&args.pattern)?;
Expand Down
48 changes: 26 additions & 22 deletions crates/cli/src/messages.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
use anyhow::{Context, Result};
use serde_json::{Map, Value};
use std::collections::{HashMap, HashSet};
use std::fs;
use std::path::Path;

use crate::fs::FileSystem;

#[derive(Default, Clone)]
pub struct MessageMap {
messages: HashMap<String, Either<MessageInfo, Box<MessageMap>>>,
Expand All @@ -21,10 +22,11 @@ pub enum Either<L, R> {
Right(R),
}

pub struct MessageHandler {
pub struct MessageHandler<F: FileSystem> {
source_messages: Map<String, Value>,
extracted_messages: MessageMap,
conflicts: Vec<NamespaceConflict>,
fs: F,
}

#[derive(Debug)]
Expand All @@ -34,14 +36,22 @@ pub struct NamespaceConflict {
pub files: Vec<String>,
}

impl MessageHandler {
pub fn new(source_path: &Path) -> Result<Self> {
let source_messages = load_source_messages(source_path)?;
Ok(Self {
source_messages,
extracted_messages: MessageMap::default(),
conflicts: Vec::new(),
})
impl<F: FileSystem> MessageHandler<F> {
pub fn new(source_path: &Path, fs: F) -> Result<Self> {
let content = fs.read_to_string(source_path)
.with_context(|| format!("Failed to read source file: {}", source_path.display()))?;
let json: Value = serde_json::from_str(&content)
.with_context(|| format!("Failed to parse JSON from: {}", source_path.display()))?;

match json {
Value::Object(map) => Ok(Self {
source_messages: map,
extracted_messages: MessageMap::default(),
conflicts: Vec::new(),
fs,
}),
_ => anyhow::bail!("Source file does not contain a JSON object"),
}
}

/// Add a new message to the extracted messages
Expand Down Expand Up @@ -162,7 +172,7 @@ impl MessageHandler {
pub fn write_merged_messages(&self, output_path: &Path) -> Result<()> {
let messages = self.merge_messages();
let json = serde_json::to_string_pretty(&messages)?;
fs::write(output_path, json)?;
self.fs.write(output_path, &json)?;
Ok(())
}

Expand All @@ -189,24 +199,17 @@ fn remove_messages(
});
}

fn load_source_messages(path: &Path) -> Result<Map<String, Value>> {
let content = fs::read_to_string(path)
.with_context(|| format!("Failed to read source file: {}", path.display()))?;
let json: Value = serde_json::from_str(&content)
.with_context(|| format!("Failed to parse JSON from: {}", path.display()))?;

match json {
Value::Object(map) => Ok(map),
_ => anyhow::bail!("Source file does not contain a JSON object"),
}
// Example of how to create a MessageHandler with the standard filesystem
pub fn create_message_handler(source_path: &Path) -> Result<MessageHandler<crate::fs::StdFileSystem>> {
MessageHandler::new(source_path, crate::fs::default_fs())
}

#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;

fn create_test_message_handler() -> MessageHandler {
fn create_test_message_handler() -> MessageHandler<crate::fs::StdFileSystem> {
let source_messages = json!({
"namespace1": {
"key1": "value1",
Expand All @@ -223,6 +226,7 @@ mod tests {
source_messages: source_messages.as_object().unwrap().clone(),
extracted_messages: MessageMap::default(),
conflicts: Vec::new(),
fs: crate::fs::default_fs(),
}
}

Expand Down
14 changes: 9 additions & 5 deletions crates/cli/src/watch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,12 @@ use std::time::Duration;
use tracing::{debug, error, info};

use crate::messages::MessageHandler;
use crate::fs::StdFileSystem;
use next_intl_resolver::extract_translations;

fn process_file_change(
path: &PathBuf,
message_handler: &mut MessageHandler,
message_handler: &mut MessageHandler<StdFileSystem>,
output_path: &Path,
) -> Result<()> {
info!("Processing changed file: {:?}", path);
Expand All @@ -25,7 +26,7 @@ fn process_file_change(

fn process_file_removal(
path: &PathBuf,
message_handler: &mut MessageHandler,
message_handler: &mut MessageHandler<StdFileSystem>,
output_path: &Path,
) -> Result<()> {
info!("Processing removed file: {:?}", path);
Expand All @@ -40,7 +41,7 @@ fn process_file_removal(
pub fn watch(
pattern: &str,
output_path: &Path,
message_handler: &mut MessageHandler,
message_handler: &mut MessageHandler<StdFileSystem>,
) -> Result<()> {
let glob_pattern = Pattern::new(pattern).context("Failed to create glob pattern")?;
debug!("Created glob pattern: {:?}", glob_pattern);
Expand Down Expand Up @@ -115,15 +116,18 @@ mod tests {
use super::*;
use std::fs;
use tempfile::TempDir;
use crate::fs::{default_fs, StdFileSystem};

fn setup_test_env() -> Result<(TempDir, PathBuf, MessageHandler)> {
fn setup_test_env() -> Result<(TempDir, PathBuf, MessageHandler<StdFileSystem>)> {
let temp_dir = TempDir::new()?;
let output_path = temp_dir.path().join("messages.json");

// Create initial messages.json
fs::write(&output_path, "{}")?;

let message_handler = MessageHandler::new(&output_path)?;
let filesystem = default_fs();

let message_handler = MessageHandler::new(&output_path, filesystem)?;

Ok((temp_dir, output_path, message_handler))
}
Expand Down
14 changes: 14 additions & 0 deletions crates/wasm/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
[package]
name = "next-intl-extractor-wasm"
version = "0.1.0"
edition = "2021"

[lib]
crate-type = ["cdylib"]

[dependencies]
next-intl-extractor-cli = { path = "../cli" }
wasm-bindgen = "0.2"
wasm-bindgen-futures = "0.4"
getrandom = { version = "0.2", features = ["js"] }
js-sys = "0.3"
45 changes: 45 additions & 0 deletions crates/wasm/src/fs.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
use anyhow::{Error, Result};
use std::path::Path;
use wasm_bindgen::prelude::*;
use next_intl_extractor_cli::fs::FileSystem;

#[wasm_bindgen]
extern "C" {
#[wasm_bindgen(js_namespace = ["globalThis", "__WASM_HOOKS"])]
fn write_file(path: &str, content: &str) -> bool;

#[wasm_bindgen(js_namespace = ["globalThis", "__WASM_HOOKS"])]
fn read_file(path: &str) -> Option<String>;

#[wasm_bindgen(js_namespace = ["globalThis", "__WASM_HOOKS"])]
fn ensure_dir(path: &str) -> bool;
}

pub struct WasmFileSystem;

impl FileSystem for WasmFileSystem {
fn read_to_string(&self, path: &Path) -> Result<String> {
read_file(path.to_str().unwrap_or(""))
.ok_or_else(|| Error::msg("Failed to read file"))
}

fn write(&self, path: &Path, contents: &str) -> Result<()> {
if write_file(path.to_str().unwrap_or(""), contents) {
Ok(())
} else {
Err(Error::msg("Failed to write file"))
}
}

fn create_dir_all(&self, path: &Path) -> Result<()> {
if ensure_dir(path.to_str().unwrap_or("")) {
Ok(())
} else {
Err(Error::msg("Failed to create directory"))
}
}

fn exists(&self, path: &Path) -> bool {
read_file(path.to_str().unwrap_or("")).is_some()
}
}
78 changes: 78 additions & 0 deletions crates/wasm/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
use std::path::PathBuf;
use wasm_bindgen::prelude::*;

mod fs;
use fs::WasmFileSystem;

#[wasm_bindgen]
extern "C" {
#[wasm_bindgen(js_namespace = console)]
fn log(s: &str);
}

macro_rules! console_log {
($($t:tt)*) => (log(&format!($($t)*)))
}

// Create our own CLI arguments struct that mirrors the one in the CLI
#[derive(Debug)]
struct WasmCliArgs {
watch: bool,
output_path: PathBuf,
pattern: String,
}

impl WasmCliArgs {
fn parse_from(args: Vec<String>) -> Result<Self, String> {
let mut watch = false;
let mut output_path = None;
let mut pattern = None;

let mut args_iter = args.iter().skip(1); // Skip the binary name
while let Some(arg) = args_iter.next() {
match arg.as_str() {
"-w" | "--watch" => watch = true,
"-o" | "--output-path" => {
output_path = args_iter.next().map(|s| PathBuf::from(s));
}
"-p" | "--pattern" => {
pattern = args_iter.next().map(|s| s.to_string());
}
_ => {}
}
}

let output_path = output_path.ok_or_else(|| "Missing required argument: --output-path".to_string())?;
let pattern = pattern.unwrap_or_else(|| "**/*.{tsx,ts}".to_string());

Ok(WasmCliArgs {
watch,
output_path,
pattern,
})
}
}

#[wasm_bindgen]
pub async fn run(args: Vec<String>) -> Result<(), JsValue> {
console_log!("Starting with args: {:?}", args);

// Parse arguments using our wasm-specific argument parser
let args = WasmCliArgs::parse_from(args)
.map_err(|e| JsValue::from_str(&format!("Failed to parse arguments: {}", e)))?;

console_log!("Parsed arguments: {:?}", args);

// Create a wasm-specific file system implementation
let fs = WasmFileSystem;

// Initialize message handler with our wasm file system
let mut message_handler = next_intl_extractor_cli::messages::MessageHandler::new(
&args.output_path,
fs,
).map_err(|e| JsValue::from_str(&e.to_string()))?;

// ... rest of the CLI logic using message_handler ...

Ok(())
}
Loading
Loading