DEV Community

Cover image for Maneira simples de adicionar suporte a i18n no Rust com exemplos e testes
OnlyCoiners
OnlyCoiners

Posted on • Originally published at onlycoiners.com

Maneira simples de adicionar suporte a i18n no Rust com exemplos e testes

A localização é crucial para o desenvolvimento de software moderno. Ao
suportar múltiplos idiomas, os aplicativos podem alcançar um público mais
amplo e se tornar mais inclusivos, alinhando-se com a missão do OnlyCoiners.

No entanto, gerenciar traduções de forma eficiente em um ambiente
multi-thread pode ser desafiador. Neste post, exploraremos como aproveitar o
OnceCell e o Mutex do Rust para lidar com traduções armazenadas em arquivos
JSON, armazenando-as em memória para facilitar a localização eficaz em todo
o aplicativo.

Você pode estar curioso por que escolhemos não usar uma solução estabelecida
como rust-i18n. Queríamos criar uma versão equivalente ao snippet de código
Python implementado no nosso servidor FastAPI, conforme descrito
neste post,
para reutilizar arquivos de tradução e facilitar o processo de reescrever
parte do código Python no nosso servidor em Rust OnlyCoiners API server.

Você pode testá-lo em produção aqui.

Você pode criar um token de API aqui primeiro
depois de criar uma conta no OnlyCoiners.

Se sua empresa está procurando contratar um desenvolvedor Rust ou fornecer
suporte a outra organização que usa Rust em produção, considere postar uma
vaga no OnlyCoiners.

Encontre e poste vagas de Rust no nosso quadro de empregos.

Entre em contato para dúvidas, e teremos o prazer
de oferecer a você e sua empresa descontos exclusivos e outros benefícios se tá contratando e quer ter uma parceria com nossa empresa!

Sinta-se à vontade para se juntar à nossa plataforma se você for um Rustacean
todos vocês são bem-vindos e também contratamos!

Pode ler versão EN ou ES se quiser.

Pode ver postagem original em nosso site também.

Trecho Completo de Código para translator.rs

Antes de prosseguirmos, gostaríamos de apresentar o trecho completo de
código em Rust para a struct Translator, projetado para facilitar a
internacionalização dentro da sua base de código 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 que Usar Armazenamento Estático para Traduções?

Ao trabalhar em aplicativos multi-thread, lidar com dados globais requer
atenção cuidadosa. Sem a sincronização adequada, você pode enfrentar
condições de corrida, falhas ou outros problemas. O Rust oferece ferramentas
como OnceCell e Mutex para resolver esses problemas de forma segura.

OnceCell garante que um valor seja inicializado apenas uma vez e
fornece acesso a ele entre as threads. Mutex garante acesso seguro e
mutável a dados compartilhados entre threads, bloqueando o acesso quando uma
thread está lendo ou escrevendo.

Ao combinar esses dois, podemos criar um armazenamento global estático que
armazena em cache arquivos de tradução em memória, para que sejam carregados
uma vez e reutilizados durante toda a vida útil do programa. Esta abordagem
evita o carregamento repetido de arquivos do disco e garante que as traduções
sejam tratadas de forma segura em um ambiente concorrente.

Explicação do Código

Vamos mergulhar no código que alimenta este sistema de tradução. Ele utiliza
uma combinação de OnceCell, Mutex e um HashMap aninhado para carregar
e armazenar traduções de arquivos JSON. Uma vez que um arquivo é carregado,
ele é armazenado em memória e reutilizado para solicitações subsequentes.

1. Armazenamento Global de Traduções

Os dados de tradução são armazenados em uma variável estática global,
TRANSLATIONS, que usa OnceCell e Mutex para garantir que os dados
sejam thread-safe e inicializados apenas uma vez. A estrutura do HashMap
permite organizar traduções de forma hierárquica.

  • O primeiro nível armazena traduções por chave de arquivo como error.json.
  • O segundo nível agrupa traduções por chave de seção, como common.
  • O terceiro nível armazena os pares chave-valor da tradução 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

Aqui está como o HashMap aninhado funciona

  • Chave de arquivo, como "error", aponta para um mapa de chaves de seção.
  • Cada chave de seção, como "common", contém as strings de tradução, organizadas por chaves como "internal_server_error", com mensagens correspondentes, como "Erro interno no servidor", conforme você pode ver no arquivo JSON usado em produção no servidor da API 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 o Translator

A struct Translator representa um objeto vinculado a um idioma específico,
como "en" para Inglês ou "pt" para Português. Quando criamos uma instância
de Translator, a variável global TRANSLATIONS é inicializada, caso ainda
não tenha 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

Isso garante que o armazenamento global para traduções esteja configurado e
pronto para ser utilizado. O campo lang na struct Translator armazena o
código do idioma, como "en" para Inglês ou "es" para Espanhol, e é usado
ao carregar arquivos de tradução.

3. Carregando Arquivos de Tradução

A função load_translation_module é responsável por carregar os dados de
tradução de um arquivo, como src/translations/en/error.json. Ela lê o
arquivo JSON, faz o parsing dos dados e os armazena no mapa global
TRANSLATIONS para uso futuro. Se o arquivo já foi carregado, ela simplesmente
retorna a versão armazenada em cache.

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 função faz o seguinte:

  1. Verifica se o arquivo já está carregado: Se estiver, ela retorna os dados armazenados em cache no mapa TRANSLATIONS.
  2. Carrega o arquivo de tradução: Se o arquivo ainda não foi carregado, ela lê o arquivo JSON no caminho src/translations/{lang}/{file}.json, faz o parsing do conteúdo em um HashMap e o armazena em memória.
  3. Lida com erros: Se o arquivo não puder ser lido, por exemplo, se não existir, uma mensagem de erro é registrada, e a função retorna None.

4. Traduzindo Chaves com Variáveis

Uma vez que as traduções estão carregadas, você pode recuperá-las usando a
função t. Esta função recebe uma chave, que é uma string separada por
pontos. Por exemplo, "error.common.internal_server_error", e recupera a
string de tradução correspondente. Ela também suporta a substituição de
variáveis, permitindo inserir valores dinâmicos na tradução.

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 função faz o seguinte:

  1. Divide a chave em partes: file_key, section_key e a chave de tradução real.
  2. Carrega o arquivo de tradução: Ela chama load_translation_module para garantir que o arquivo correto seja carregado.
  3. Percorre as chaves: Navega pelo HashMap do arquivo para encontrar a string de tradução desejada.
  4. Lida com variáveis dinâmicas: Se a tradução contém variáveis como {username}, elas são substituídas pelos valores passados no mapa variables.

Por exemplo, se a string de tradução for "{username}, Crie, Ganhe e Conecte-se
com OnlyCoiners!"
e você fornecer {"username": "Rust"}, o resultado final será
"Rust, Crie, Ganhe e Conecte-se com OnlyCoiners!".

Tratamento de Erros

O sistema foi projetado para fornecer mensagens de erro úteis quando
traduções não são encontradas. Por exemplo, se uma seção ou chave estiver
faltando, ele retorna uma mensagem como:

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

Isso garante que os desenvolvedores possam identificar facilmente traduções ausentes durante o desenvolvimento.

Exemplos de uso em produção

O módulo de tradução é utilizado em produção no OnlyCoiners API server.

Vamos fornecer alguns trechos de código que você pode usar como referência.
Você pode primeiro criar um middleware como este para o 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

Você pode incluir isso no seu aplicativo 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

Em seguida, use isso dentro do seu 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, você pode criar testes para o módulo Translator e usar $cargo test para testá-lo.

#[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

Você também pode testar o 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

Você pode usar esses arquivos de tradução JSON como referência.

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

Conclusão

Este sistema de tradução lida de forma eficiente com traduções em uma
aplicação Rust, utilizando armazenamento estático e acesso thread-safe.
Ao aproveitar OnceCell e Mutex, podemos garantir que os arquivos de
tradução sejam carregados uma vez e armazenados em cache, melhorando o
desempenho e reduzindo o acesso ao disco. A função t permite a
recuperação flexível de traduções com suporte para variáveis dinâmicas,
tornando-a uma ferramenta poderosa para a localização.

Se você está construindo um aplicativo que requer localização, essa
abordagem oferece uma solução simples, escalável e eficiente para o
gerenciamento de traduções. Ao usar os recursos de segurança de memória
do Rust, você garante que suas traduções sejam tratadas com segurança e
eficiência em múltiplas threads.

Esperamos que este post tenha ajudado você a implementar um sistema simples
de tradução utilizando Rust. Usamos Rust ativamente em produção e estamos
procurando contratar mais desenvolvedores Rust.

Sinta-se à vontade para se juntar à nossa plataforma se gostou deste post e
apoie uma empresa que usa Rust em produção!

Top comments (0)