DEV Community

Cover image for Manera simple de agregar soporte i18n en Rust con ejemplos y pruebas
OnlyCoiners
OnlyCoiners

Posted on • Originally published at onlycoiners.com

Manera simple de agregar soporte i18n en Rust con ejemplos y pruebas

La localización es crucial para el desarrollo de software moderno. Al soportar
múltiples idiomas, las aplicaciones pueden llegar a una audiencia más amplia y
volverse más inclusivas, alineándose con la misión de OnlyCoiners.

Sin embargo, gestionar traducciones de manera eficiente en un entorno multi-hilo
puede ser un desafío. En este post, exploraremos cómo aprovechar OnceCell y
Mutex de Rust para manejar traducciones almacenadas en archivos JSON,
almacenándolas en memoria para facilitar la localización efectiva en toda la
aplicación.

Podrías preguntarte por qué no utilizamos una solución ya establecida como
rust-i18n. Queríamos crear una versión equivalente al fragmento de código
Python implementado en nuestro servidor FastAPI, como se describe
en este post,
para reutilizar archivos de traducción y simplificar el proceso de reescribir
parte del código Python en nuestro servidor Rust OnlyCoiners API server.

Puedes probarlo en producción aquí.

Puedes crear un token de API aquí primero
después de crear una cuenta en OnlyCoiners.

Si tu empresa busca contratar a un desarrollador de Rust o brindar soporte a otra
organización que utilice Rust en producción, considera publicar una oferta de
trabajo en OnlyCoiners.

Encuentra y publica trabajos de Rust en nuestro tablón de empleos.

Contáctanos para cualquier consulta, estaremos encantados
de ofrecer a ti y a tu empresa descuentos exclusivos y otros beneficios si estás contratando o quieres una colaboración con nuestra empresa..

Sientete libre de unirte a nuestra plataforma si eres un Rustacean, ¡todos son bienvenidos
y también contratamos!

Puedes leer versión EN ou PT se quieres.

Puedes leer publicación original en nuestro sitio web.

Fragmento completo de código para translator.rs

Antes de continuar, nos gustaría presentar el fragmento completo de
código en Rust para la estructura Translator, diseñado para facilitar la
internacionalización dentro de tu base de código en Rust.

use once_cell::sync::OnceCell;
use serde_json;
use std::collections::HashMap;
use std::sync::Mutex;
use std::{env, fs};

// 1. Global static storage for translation modules
static TRANSLATIONS: OnceCell<Mutex<HashMap<String, HashMap<String, HashMap<String, String>>>>> =
    OnceCell::new();

pub struct Translator {
    lang: String,
}

impl Translator {
    // 2. Initialize the translator with a language
    pub fn new(lang: &str) -> Self {
        // Ensure translations are loaded once
        let _ = TRANSLATIONS.get_or_init(|| Mutex::new(HashMap::new()));
        Translator {
            lang: lang.to_string(),
        }
    }

    // 3. Load translations from files or other sources
    fn load_translation_module(
        &self,
        file_key: &str,
    ) -> Option<HashMap<String, HashMap<String, String>>> {
        let mut translations = TRANSLATIONS.get().unwrap().lock().unwrap();

        // Get the current working directory and construct the full path dynamically
        let current_dir = env::current_dir().unwrap();
        let module_path =
            current_dir.join(format!("src/translations/{}/{}.json", self.lang, file_key));

        // If translation is already loaded, return a cloned version
        if let Some(file_translations) = translations.get(file_key) {
            return Some(file_translations.clone());
        }

        // Load the translation file - error.json
        match fs::read_to_string(module_path) {
            Ok(content) => {
                // Parse the JSON into a nested HashMap - error -> common -> internal_server_error
                let file_translations: HashMap<String, HashMap<String, String>> =
                    serde_json::from_str(&content).unwrap_or_default();

                translations.insert(file_key.to_string(), file_translations.clone());
                Some(file_translations)
            }
            Err(e) => {
                tracing::error!("Error loading translation file - {}", e);
                None
            }
        }
    }

    // 4. Translate based on key and optional variables
    pub fn t(&self, key: &str, variables: Option<HashMap<&str, &str>>) -> String {
        let parts: Vec<&str> = key.split('.').collect();
        let file_key = parts.get(0).unwrap_or(&""); // "error"
        let section_key = parts.get(1).unwrap_or(&""); // "common"
        let translation_keys = &parts[2..]; // "INTERNAL_SERVER_ERROR"

        // Load the correct translation module (e.g., "error.json")
        if let Some(translation_module) = self.load_translation_module(file_key) {
            if let Some(section) = translation_module.get(*section_key) {
                let mut current_value: Option<&String> = None;

                // Traverse the translation keys to get the final string value
                for translation_key in translation_keys {
                    if current_value.is_none() {
                        // At the beginning, current_value is None, so we access the section (a HashMap)
                        if let Some(next_value) = section.get(*translation_key) {
                            current_value = Some(next_value);
                        } else {
                            return format!("Key '{}' not found in '{}' locale", key, self.lang);
                        }
                    }
                }

                // At this point, current_value should be a &String
                if let Some(translation_string) = current_value {
                    let mut translated_text = translation_string.clone();

                    // Handle variables if present
                    if let Some(variables) = variables {
                        for (variable, value) in variables {
                            let variable_format = format!("{{{}}}", variable);
                            translated_text = translated_text.replace(&variable_format, value);
                        }
                    }
                    translated_text
                } else {
                    format!("Key '{}' not found in '{}' locale", key, self.lang)
                }
            } else {
                format!(
                    "Section '{}' not found in '{}' locale",
                    section_key, self.lang
                )
            }
        } else {
            format!("Module '{}' not found for '{}' locale", file_key, self.lang)
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

¿Por qué usar almacenamiento estático para traducciones?

Al trabajar en aplicaciones multi-hilo, manejar datos globales requiere
especial cuidado. Sin la sincronización adecuada, podrías enfrentar
condiciones de carrera, fallos u otros problemas. Rust ofrece herramientas
como OnceCell y Mutex para resolver estos problemas de manera segura.

OnceCell garantiza que un valor se inicialice solo una vez y
proporciona acceso a él entre los hilos. Mutex garantiza acceso seguro y
mutable a los datos compartidos entre hilos, bloqueando el acceso cuando un
hilo está leyendo o escribiendo.

Al combinar estos dos, podemos crear un almacenamiento global estático que
almacena en caché los archivos de traducción en memoria, para que se carguen
una vez y se reutilicen durante toda la vida útil del programa. Este enfoque
evita cargar repetidamente archivos desde el disco y garantiza que las
traducciones se manejen de manera segura en un entorno concurrente.

Explicación del Código

Vamos a sumergirnos en el código que impulsa este sistema de traducción.
Utiliza una combinación de OnceCell, Mutex y un HashMap anidado para
cargar y almacenar traducciones de archivos JSON. Una vez que se carga un
archivo, se almacena en memoria y se reutiliza para solicitudes posteriores.

1. Almacenamiento Global de Traducciones

Los datos de traducción se almacenan en una variable estática global,
TRANSLATIONS, que utiliza OnceCell y Mutex para garantizar que los datos
sean seguros para hilos y se inicialicen solo una vez. La estructura del
HashMap permite organizar las traducciones de manera jerárquica.

  • El primer nivel almacena traducciones por clave de archivo, como error.json.
  • El segundo nivel agrupa traducciones por clave de sección, como common.
  • El tercer nivel almacena los pares clave-valor de la traducción real.
use once_cell::sync::OnceCell;
use std::collections::HashMap;
use std::sync::Mutex;

static TRANSLATIONS: OnceCell<Mutex<HashMap<String, HashMap<String, HashMap<String, String>>>>> =
    OnceCell::new();
Enter fullscreen mode Exit fullscreen mode

Aquí está cómo funciona el HashMap anidado:

  • Clave de archivo, como "error", apunta a un mapa de claves de sección.
  • Cada clave de sección, como "common", contiene las cadenas de traducción, organizadas por claves como "internal_server_error", con mensajes correspondientes, como "Error interno del servidor", como puedes ver en el archivo JSON utilizado en producción en el servidor de API de OnlyCoiners.
src/translations/en/error.json

{
  "common": {
    "internal_server_error": "Internal server error",
    "not_authorized": "You are not authorized to use this resource",
    "not_found": "{resource} not found"
  },
  "token": {
    "no_api_user_token": "API-USER-TOKEN header is not included",
    "invalid_api_user_token": "API-USER-TOKEN header is not valid",
    "no_api_admin_token": "API-ADMIN-TOKEN header is not included",
    "unable_to_read_api_token": "Unable to read API Token"
  },
  "database": {
    "unable_to_query_database": "Unable to query database"
  }
}
Enter fullscreen mode Exit fullscreen mode

2. Inicializando el Translator

La estructura Translator representa un objeto vinculado a un idioma específico,
como "en" para inglés o "pt" para portugués. Cuando creamos una instancia
de Translator, la variable global TRANSLATIONS se inicializa, en caso de
que aún no lo haya sido.

pub struct Translator {
    lang: String,
}

impl Translator {
    pub fn new(lang: &str) -> Self {
        let _ = TRANSLATIONS.get_or_init(|| Mutex::new(HashMap::new()));
        Translator {
            lang: lang.to_string(),
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Esto garantiza que el almacenamiento global para traducciones esté configurado y
listo para ser utilizado. El campo lang en la estructura Translator almacena
el código del idioma, como "en" para inglés o "es" para español, y se usa
al cargar archivos de traducción.

3. Cargando Archivos de Traducción

La función load_translation_module es responsable de cargar los datos de
traducción de un archivo, como src/translations/en/error.json. Lee el
archivo JSON, analiza los datos y los almacena en el mapa global
TRANSLATIONS para uso futuro. Si el archivo ya ha sido cargado, simplemente
devuelve la versión almacenada en caché.

use std::{env, fs};

fn load_translation_module(
    &self,
    file_key: &str,
) -> Option<HashMap<String, HashMap<String, String>>> {
    let mut translations = TRANSLATIONS.get().unwrap().lock().unwrap();
    let current_dir = env::current_dir().unwrap();
    let module_path =
        current_dir.join(format!("src/translations/{}/{}.json", self.lang, file_key));

    if let Some(file_translations) = translations.get(file_key) {
        return Some(file_translations.clone());
    }

    match fs::read_to_string(module_path) {
        Ok(content) => {
            let file_translations: HashMap<String, HashMap<String, String>> =
                serde_json::from_str(&content).unwrap_or_default();
            translations.insert(file_key.to_string(), file_translations.clone());
            Some(file_translations)
        }
        Err(e) => {
            tracing::error!("Error loading translation file - {}", e);
            None
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Esta función hace lo siguiente:

  1. Verifica si el archivo ya está cargado: Si lo está, devuelve los datos almacenados en caché en el mapa TRANSLATIONS.
  2. Carga el archivo de traducción: Si el archivo aún no ha sido cargado, lee el archivo JSON en la ruta src/translations/{lang}/{file}.json, analiza el contenido en un HashMap y lo almacena en memoria.
  3. Maneja errores: Si el archivo no se puede leer, por ejemplo, si no existe, se registra un mensaje de error y la función devuelve None.

4. Traduciendo Claves con Variables

Una vez que las traducciones están cargadas, puedes recuperarlas usando la
función t. Esta función recibe una clave, que es una cadena separada por
puntos. Por ejemplo, "error.common.internal_server_error", y recupera la
cadena de traducción correspondiente. También admite la sustitución de
variables, lo que permite insertar valores dinámicos en la traducción.

use serde_json;

pub fn t(&self, key: &str, variables: Option<HashMap<&str, &str>>) -> String {
    let parts: Vec<&str> = key.split('.').collect();
    let file_key = parts.get(0).unwrap_or(&"");
    let section_key = parts.get(1).unwrap_or(&"");
    let translation_keys = &parts[2..];

    if let Some(translation_module) = self.load_translation_module(file_key) {
        if let Some(section) = translation_module.get(*section_key) {
            let mut current_value: Option<&String> = None;

            for translation_key in translation_keys {
                if current_value.is_none() {
                    if let Some(next_value) = section.get(*translation_key) {
                        current_value = Some(next_value);
                    } else {
                        return format!("Key '{}' not found in '{}' locale", key, self.lang);
                    }
                }
            }

            if let Some(translation_string) = current_value {
                let mut translated_text = translation_string.clone();
                if let Some(variables) = variables {
                    for (variable, value) in variables {
                        let variable_format = format!("{{{}}}", variable);
                        translated_text = translated_text.replace(&variable_format, value);
                    }
                }
                translated_text
            } else {
                format!("Key '{}' not found in '{}' locale", key, self.lang)
            }
        } else {
            format!(
                "Section '{}' not found in '{}' locale",
                section_key, self.lang
            )
        }
    } else {
        format!("Module '{}' not found for '{}' locale", file_key, self.lang)
    }
}
Enter fullscreen mode Exit fullscreen mode

Esta función hace lo siguiente:

  1. Divide la clave en partes: file_key, section_key y la clave de traducción real.
  2. Carga el archivo de traducción: Llama a load_translation_module para garantizar que se cargue el archivo correcto.
  3. Recorre las claves: Navega a través del HashMap del archivo para encontrar la cadena de traducción deseada.
  4. Maneja variables dinámicas: Si la traducción contiene variables como {username}, estas se reemplazan por los valores proporcionados en el mapa variables.

Por ejemplo, si la cadena de traducción es "{username}, Crea, Gana y Conéctate
con OnlyCoiners!"
y proporcionas {"username": "Rust"}, el resultado final será
"Rust, Crea, Gana y Conéctate con OnlyCoiners!".

Manejo de Errores

El sistema está diseñado para proporcionar mensajes de error útiles cuando
no se encuentran traducciones. Por ejemplo, si falta una sección o clave,
retorna un mensaje como:

Key 'error.common.INTERNAL_SERVER_ERROR' not found in 'en' locale
Enter fullscreen mode Exit fullscreen mode

Esto garantiza que los desarrolladores puedan identificar fácilmente las
traducciones faltantes durante el desarrollo.

Ejemplos de uso en producción

El módulo de traducción se utiliza en producción en el OnlyCoiners API server.

Proporcionaremos algunos fragmentos de código que puedes usar como referencia.
Puedes comenzar creando un middleware como este para axum.

// #[derive(Clone)]
// pub struct Language(pub String);

use std::collections::HashSet;

use crate::{constants::language::{ALLOWED_LANGUAGE_LIST, EN}, schemas::language::Language};
use axum::{extract::Request, middleware::Next, response::Response};

pub async fn extract_client_language(
    mut request: Request, // mutable borrow for later modification
    next: Next,
) -> Result<Response, String> {
    let accept_language = {
        // Temporarily borrow the request immutably to get the header
        request
            .headers()
            .get("Accept-Language")
            .and_then(|value| value.to_str().ok())
            .unwrap_or("")
            .to_string() // convert to String to end the borrow
    };

    let mut locale = accept_language.split(',').next().unwrap_or(EN).to_string();
    // Remove any region specifier like en-US to en
    locale = locale.split('-').next().unwrap_or(EN).to_string();

    // Create a set of allowed languages for quick lookup
    let allowed_languages: HashSet<&str> = ALLOWED_LANGUAGE_LIST.iter().cloned().collect();

    // Verify if the extracted locale is allowed; if not, default to the default language
    if !allowed_languages.contains(locale.as_str()) {
        locale = EN.to_string();
    }

    // Insert the language into request extensions with mutable borrow
    request.extensions_mut().insert(Language(locale));

    // Proceed to the next middleware or handler
    let response = next.run(request).await;

    Ok(response)
}
Enter fullscreen mode Exit fullscreen mode

Puedes incluir esto en tu aplicación axum.

let app = Router::new()
        .route("/", get(root))
        // Attach `/api` routes
        .nest("/bot", bot_routes)
        .nest("/admin", admin_routes)
        .nest("/api", api_routes)
        .layer(from_fn(extract_client_language))
Enter fullscreen mode Exit fullscreen mode

A continuación, usa esto dentro de tu handler.

pub async fn find_user_list(
    Extension(session): Extension<SessionData>,
    Extension(language): Extension<Language>,
) -> Result<Json<Vec<UserListing>>, (StatusCode, Json<ErrorMessage>)> {
    let translator = Translator::new(&language.0);
    let not_authorized = translator.t("error.common.not_authorized", None);

    Err((
        StatusCode::UNAUTHORIZED,
        Json(ErrorMessage {
            text: not_authorized,
        }),
    ))
}
Enter fullscreen mode Exit fullscreen mode

Opcionalmente, puedes crear pruebas para el módulo Translator y usar $cargo test para probarlo.

#[cfg(test)]
mod tests {
    use crate::translations::translator::Translator;

    use super::*;
    use std::collections::HashMap;

    #[test]
    fn test_translation_for_english_locale() {
        let translator = Translator::new("en");

        let translation = translator.t("error.common.internal_server_error", None);
        assert_eq!(translation, "Internal server error");

        let not_found = translator.t("error.common.non_existent", None);
        assert_eq!(not_found, "Key 'error.common.non_existent' not found in 'en' locale");
    }

    #[test]
    fn test_translation_for_portuguese_locale() {
        let translator = Translator::new("pt");

        // Test known translation
        let translation = translator.t("error.common.internal_server_error", None);
        println!("translation {}", translation);
        assert_eq!(translation, "Erro interno no servidor");

        // Test key not found
        let not_found = translator.t("error.common.non_existent", None);
        assert_eq!(not_found, "Key 'error.common.non_existent' not found in 'pt' locale");
    }

    #[test]
    fn test_translation_with_variables() {
        let translator = Translator::new("en");

        let mut variables = HashMap::new();
        variables.insert("resource", "User");

        let translation_with_vars = translator.t("error.common.not_found", Some(variables));
        assert_eq!(translation_with_vars, "User not found");
    }

    #[test]
    fn test_translation_module_not_found() {
        let translator = Translator::new("es");

        // Test loading a non-existent module
        let translation = translator.t("non_existent_module.common.internal_server_error", None);
        assert_eq!(
            translation,
            "Module 'non_existent_module' not found for 'es' locale"
        );
    }

    #[test]
    fn test_translation_section_not_found() {
        let translator = Translator::new("en");

        // Test section not found in translation file
        let translation = translator.t("error.non_existent_section.internal_server_error", None);
        assert_eq!(
            translation,
            "Section 'non_existent_section' not found in 'en' locale"
        );
    }
}
Enter fullscreen mode Exit fullscreen mode

También puedes probar el middleware.

pub async fn test_handler(Extension(language): Extension<Language>) -> Json<serde_json::Value> {
    // Return a JSON response with the extracted language
    let response_message = match language.0.as_str() {
        "en" => "en",
        "pt" => "pt",
        "es" => "es",
        _ => "deafult",
    };

    Json(json!({ "message": response_message }))
}

#[cfg(test)]
mod tests {
    use crate::{
        constants::language::{EN, EN_US, ES, PT}, mdware::language::extract_client_language, tests::{test_handler, TRANSLATOR}  
    };
    use axum::{
        body::{to_bytes, Body}, http::Request, middleware::from_fn, Router
    };
    use hyper::StatusCode;
    use tower::ServiceExt; 
    use serde_json::{json, Value};

    #[tokio::test]
    async fn test_with_valid_accept_language_header() {
        let app = Router::new()
            .route("/", axum::routing::get(test_handler))
            .layer(from_fn(extract_client_language)); 

        // Simulate a request with a valid Accept-Language header like
        let request = Request::builder()
            .header("Accept-Language", EN_US) // "en-US"
            .body(Body::empty())
            .unwrap();

        let response = app.oneshot(request).await.unwrap();
        assert_eq!(response.status(), StatusCode::OK);

        let body = to_bytes(response.into_body(), usize::MAX).await.unwrap();
        let body_str = std::str::from_utf8(&body).unwrap();
        // println!("Response Body: {:?}", body_str);

        let body_json: Value = serde_json::from_str(body_str).unwrap();
        assert_eq!(body_json, json!({ "message": EN }));
    }

    #[tokio::test]
    async fn test_with_valid_accept_language_header_wiht_pt() {
        let app = Router::new()
            .route("/", axum::routing::get(test_handler))
            .layer(from_fn(extract_client_language)); 

        let request = Request::builder()
            .header("Accept-Language", "pt")
            .body(Body::empty())
            .unwrap();

        let response = app.oneshot(request).await.unwrap();
        assert_eq!(response.status(), StatusCode::OK);

        let body = to_bytes(response.into_body(), usize::MAX).await.unwrap();
        let body_str = std::str::from_utf8(&body).unwrap();

        let body_json: Value = serde_json::from_str(body_str).unwrap();
        assert_eq!(body_json, json!({ "message": PT }));
    }

    #[tokio::test]
    async fn test_with_valid_accept_language_header_wiht_pt_br() {
        let app = Router::new()
            .route("/", axum::routing::get(test_handler))
            .layer(from_fn(extract_client_language)); 

        let request = Request::builder()
            .header("Accept-Language", "pt-BR")
            .body(Body::empty())
            .unwrap();

        let response = app.oneshot(request).await.unwrap();
        assert_eq!(response.status(), StatusCode::OK);

        let body = to_bytes(response.into_body(), usize::MAX).await.unwrap();
        let body_str = std::str::from_utf8(&body).unwrap();

        let body_json: Value = serde_json::from_str(body_str).unwrap();
        assert_eq!(body_json, json!({ "message": PT }));
    }

    #[tokio::test]
    async fn test_with_valid_accept_language_header_wiht_es() {
        let app = Router::new()
            .route("/", axum::routing::get(test_handler))
            .layer(from_fn(extract_client_language)); 

        let request = Request::builder()
            .header("Accept-Language", "es")
            .body(Body::empty())
            .unwrap();

        let response = app.oneshot(request).await.unwrap();
        assert_eq!(response.status(), StatusCode::OK);

        let body = to_bytes(response.into_body(), usize::MAX).await.unwrap();
        let body_str = std::str::from_utf8(&body).unwrap();
        let body_json: Value = serde_json::from_str(body_str).unwrap();
        assert_eq!(body_json, json!({ "message": ES }));
    }

    #[tokio::test]
    async fn test_with_unsupported_language() {
        let app = Router::new()
            .route("/", axum::routing::get(test_handler))
            .layer(from_fn(extract_client_language)); 

        let request = Request::builder()
            .header("Accept-Language", "fr")
            .body(Body::empty())
            .unwrap();

        let response = app.oneshot(request).await.unwrap();
        assert_eq!(response.status(), StatusCode::OK);

        let body = to_bytes(response.into_body(), usize::MAX).await.unwrap();
        let body_str = std::str::from_utf8(&body).unwrap();

        let body_json: Value = serde_json::from_str(body_str).unwrap();
        assert_eq!(body_json, json!({ "message": EN }));
    }

    #[tokio::test]
    async fn test_without_accept_language_header() {
        let app = Router::new()
            .route("/", axum::routing::get(test_handler))
            .layer(from_fn(extract_client_language)); 

        let request = Request::builder()
            .body(Body::empty())
            .unwrap();

        let response = app.oneshot(request).await.unwrap();
        assert_eq!(response.status(), StatusCode::OK);

        let body = to_bytes(response.into_body(), usize::MAX).await.unwrap();
        let body_str = std::str::from_utf8(&body).unwrap();

        let body_json: Value = serde_json::from_str(body_str).unwrap();
        assert_eq!(body_json, json!({ "message": EN }));
    }
}
Enter fullscreen mode Exit fullscreen mode

Puedes usar estos archivos de traducción JSON como referencia.

en.json
{
  "common": {
    "internal_server_error": "Internal server error",
    "not_authorized": "You are not authorized to use this resource",
    "not_found": "{resource} not found"
  },
}
pt.json
{
  "common": {
    "internal_server_error": "Erro interno no servidor",
    "not_authorized": "Você não está autorizado a usar este recurso",
    "not_found": "{resource} não encontrado"
  },
}
es.json
{
  "common": {
    "internal_server_error": "Error interno del servidor",
    "not_authorized": "No estás autorizado para usar este recurso",
    "not_found": "{resource} no encontrado"
  },
}
Enter fullscreen mode Exit fullscreen mode

Conclusión

Este sistema de traducción maneja eficientemente las traducciones en una
aplicación Rust, utilizando almacenamiento estático y acceso seguro para
hilos. Al aprovechar OnceCell y Mutex, podemos garantizar que los archivos
de traducción se carguen una vez y se almacenen en caché, mejorando el
rendimiento y reduciendo el acceso al disco. La función t permite la
recuperación flexible de traducciones con soporte para variables dinámicas,
lo que la convierte en una herramienta poderosa para la localización.

Si estás construyendo una aplicación que requiere localización, este
enfoque ofrece una solución simple, escalable y eficiente para la gestión de
traducciones. Al utilizar las características de seguridad de memoria de Rust,
garantizas que tus traducciones se manejen de manera segura y eficiente en
múltiples hilos.

Esperamos que este post te haya ayudado a implementar un sistema simple de
traducción utilizando Rust. Utilizamos Rust activamente en producción y estamos
buscando contratar más desarrolladores de Rust.

Sientete libre de unirte a nuestra plataforma si te gustó este post y
apoya a una empresa que utiliza Rust en producción!

Top comments (0)