← Volver al blog

Dentro del motor de traducción: Glosarios, reglas de estilo y retraducción inteligente

Un profundo recorrido técnico sobre cómo funciona realmente el proceso de traducción de Rasepi: resolución de glosarios, reglas de estilo e instrucciones personalizadas de DeepL, hash de contenidos y la integración que lo une todo.

Bajo el capó
Dentro del motor de traducción: Glosarios, reglas de estilo y retraducción inteligente

Nuestro post anterior sobre arquitectura cubría los plugins, los guardianes de acción y el sistema de canalización. Éste profundiza en el motor de traducción, la parte que creo que hace que Rasepi sea fundamentalmente diferente de cualquier otra plataforma de documentación.

No el discurso de marketing sobre traducir párrafos en lugar de páginas. El código real. Cómo se resuelven los glosarios por inquilino, cómo las reglas de estilo de DeepL y las instrucciones personalizadas dan forma a cada traducción, cómo el hashing de contenidos impulsa la detección de documentos obsoletos y cómo el orquestador decide qué bloques hay que retraducir.

Motor de traducción: glosarios, reglas de estilo y retraducción inteligente

El proceso de traducción

Cuando un usuario guarda un documento, el sistema no se limita a retraducirlo todo. Ejecuta una secuencia bastante específica:

  1. Analiza el JSON de TipTap en bloques individuales
  2. Compara los hashes de contenido para detectar qué bloques cambiaron realmente
  3. Para los bloques cambiados, resuelva el glosario del inquilino y la lista de reglas de estilo para el par de idiomas
  4. Aplique las reglas de estilo, las instrucciones personalizadas y la formalidad de la configuración del arrendatario
  5. Envíe sólo los bloques modificados a DeepL
  6. Actualice los bloques de traducción y sincronice los hashes de contenido

Cada paso es su propio servicio con su propia interfaz. Esto es importante porque cualquier paso puede cambiarse por otra cosa, un proveedor de traducción diferente, un algoritmo hash diferente, una fuente de glosario diferente.

Resolución del glosario: tenant-scoped, DeepL-synced

DeepL glosarios tienen una limitación que la mayoría de la gente desconoce: son inmutables. No se puede editar un glosario DeepL. Cualquier cambio implica borrar el antiguo y crear uno nuevo.

Rasepi maneja esto tratando la base de datos como la fuente de la verdad y los glosarios de DeepL como artefactos desechables en tiempo de ejecución. La entidad TenantGlossary almacena todo localmente:

public class TenantGlossary : ITenantScoped
{
    public Guid Id { get; set; }
    public Guid TenantId { get; set; }
    public string Name { get; set; }
    public string SourceLanguage { get; set; }     // e.g. "en"
    public string TargetLanguage { get; set; }     // e.g. "de"
    public string? DeepLGlossaryId { get; set; }   // Runtime DeepL ID
    public DateTime? LastSyncedAt { get; set; }
    public bool IsDirty { get; set; } = true;      // Triggers re-sync
    public ICollection<TenantGlossaryEntry> Entries { get; set; }
}

Cuando un usuario añade una entrada en el glosario, por ejemplo asignando "Sprint Review" a "Sprint-Überprüfung" para EN→DE, el registro de la base de datos se actualiza inmediatamente y IsDirty se establece en true. El glosario DeepL no se recrea en ese momento. Se recrea perezosamente, la próxima vez que una traducción lo necesite realmente.

El flujo de sincronización

Antes de cada llamada a traducción, el sistema resuelve el glosario:

public async Task<string?> GetOrSyncDeepLGlossaryIdAsync(
    string sourceLanguage, string targetLanguage,
    CancellationToken ct = default)
{
    var glossary = await _db.TenantGlossaries
        .Include(g => g.Entries)
        .FirstOrDefaultAsync(g =>
            g.SourceLanguage == sourceLanguage &&
            g.TargetLanguage == targetLanguage, ct);

    if (glossary is null || glossary.Entries.Count == 0)
        return null;

    if (!glossary.IsDirty && glossary.DeepLGlossaryId is not null)
        return glossary.DeepLGlossaryId;

    // Dirty - delete old, create new
    if (glossary.DeepLGlossaryId is not null)
        await _deepL.DeleteGlossaryAsync(glossary.DeepLGlossaryId);

    var entries = glossary.Entries
        .ToDictionary(e => e.SourceTerm, e => e.TargetTerm);

    var deepLGlossary = await _deepL.CreateGlossaryAsync(
        $"rasepi-{glossary.Id}",
        glossary.SourceLanguage,
        glossary.TargetLanguage,
        entries);

    glossary.DeepLGlossaryId = deepLGlossary.GlossaryId;
    glossary.IsDirty = false;
    glossary.LastSyncedAt = DateTime.UtcNow;
    await _db.SaveChangesAsync(ct);

    return glossary.DeepLGlossaryId;
}

Aquí hay tres cosas que merece la pena destacar:

  1. Sincronización perezosa. Sólo llamamos a la API DeepL cuando realmente se necesita una traducción. La edición masiva de entradas del glosario no desencadena docenas de llamadas a la API.
  2. **La consulta se ejecuta a través de los filtros de consulta globales de EF, por lo que TenantGlossaries se delimita automáticamente. Las entradas del glosario del arrendatario A nunca se filtran a las traducciones del arrendatario B.
  3. Un glosario por par de idiomas. DeepL impone esto de todos modos. Un glosario EN→DE, un glosario EN→FR, y así sucesivamente. El par (SourceLanguage, TargetLanguage) es único por arrendatario.

Entradas del glosario

Las entradas individuales son sólo mapeos de términos:

public class TenantGlossaryEntry
{
    public Guid Id { get; set; }
    public Guid GlossaryId { get; set; }
    public string SourceTerm { get; set; }   // e.g. "Sprint Review"
    public string TargetTerm { get; set; }   // e.g. "Sprint-Überprüfung"
}

La API le ofrece CRUD completo además de importación/exportación CSV para la gestión en bloque:

POST   /api/admin/glossaries                       Create glossary
POST   /api/admin/glossaries/{id}/entries           Add term
PUT    /api/admin/glossaries/{id}/entries/{entryId}  Update term
DELETE /api/admin/glossaries/{id}/entries/{entryId}  Remove term
POST   /api/admin/glossaries/{id}/import            Import CSV
GET    /api/admin/glossaries/{id}/export            Export CSV
POST   /api/admin/glossaries/{id}/sync              Force DeepL sync

La importación CSV es súper útil para los equipos que migran desde sistemas de memorias de traducción existentes. Exporte sus términos, límpielos, impórtelos en Rasepi y la siguiente ejecución de traducción utilizará el nuevo glosario automáticamente.

Reglas de estilo, instrucciones personalizadas y formalidad

Los glosarios se ocupan de la terminología. Pero la terminología es sólo la mitad. Una traducción puede utilizar todas las palabras correctas y aun así sonar mal. Un tono incorrecto, un formato de fecha incorrecto, convenciones de puntuación incorrectas.

La API de reglas de estilo (v3) de DeepL lo soluciona. Puede crear listas de reglas de estilo reutilizables que combinan dos tipos de controles:

  1. Reglas configuradas, convenciones de formato predefinidas para fechas, horas, puntuación, números, etc.
  2. Instrucciones personalizadas, directivas de texto libre que dan forma al tono, la redacción y las convenciones específicas del dominio.

Rasepi las crea y gestiona por inquilino, por idioma de destino. La entidad TenantStyleRuleList almacena el DeepL style_id junto con las reglas configuradas y las instrucciones personalizadas del inquilino:

public class TenantStyleRuleList : ITenantScoped
{
    public Guid Id { get; set; }
    public Guid TenantId { get; set; }
    public string Name { get; set; }
    public string TargetLanguage { get; set; }      // e.g. "de"
    public string? DeepLStyleId { get; set; }       // Runtime DeepL style_id
    public string ConfiguredRulesJson { get; set; }  // Serialized configured rules
    public bool IsDirty { get; set; } = true;
    public DateTime? LastSyncedAt { get; set; }
    public ICollection<TenantCustomInstruction> CustomInstructions { get; set; }
}

Creación de listas de reglas de estilo

Cuando un administrador configura reglas de traducción para el alemán, Rasepi llama a la API v3 de DeepL para crear la lista de reglas de estilo. Esto es lo que parece:

public async Task<string> CreateOrSyncStyleRuleListAsync(
    TenantStyleRuleList ruleList, CancellationToken ct = default)
{
    if (!ruleList.IsDirty && ruleList.DeepLStyleId is not null)
        return ruleList.DeepLStyleId;

    // DeepL style rule lists are mutable - we can update in place
    if (ruleList.DeepLStyleId is not null)
    {
        // Replace configured rules on existing list
        await _httpClient.PutAsJsonAsync(
            $"v3/style_rules/{ruleList.DeepLStyleId}/configured_rules",
            JsonSerializer.Deserialize<JsonElement>(ruleList.ConfiguredRulesJson),
            ct);

        // Sync custom instructions
        await SyncCustomInstructionsAsync(ruleList, ct);

        ruleList.IsDirty = false;
        ruleList.LastSyncedAt = DateTime.UtcNow;
        return ruleList.DeepLStyleId;
    }

    // Create new style rule list
    var payload = new
    {
        name = $"rasepi-{ruleList.TenantId}-{ruleList.TargetLanguage}",
        language = ruleList.TargetLanguage,
        configured_rules = JsonSerializer.Deserialize<JsonElement>(
            ruleList.ConfiguredRulesJson),
        custom_instructions = ruleList.CustomInstructions.Select(ci => new
        {
            label = ci.Label,
            prompt = ci.Prompt,
            source_language = ci.SourceLanguage
        })
    };

    var response = await _httpClient.PostAsJsonAsync("v3/style_rules", payload, ct);
    var result = await response.Content.ReadFromJsonAsync<StyleRuleResponse>(ct);

    ruleList.DeepLStyleId = result.StyleId;
    ruleList.IsDirty = false;
    ruleList.LastSyncedAt = DateTime.UtcNow;
    await _db.SaveChangesAsync(ct);

    return ruleList.DeepLStyleId;
}

A diferencia de los glosarios, las listas de reglas de estilo de DeepL son mutables. Puede reemplazar las reglas configuradas en su lugar con PUT /v3/style_rules/{style_id}/configured_rules, y las instrucciones personalizadas pueden añadirse, actualizarse o eliminarse individualmente. Mucho más amigable para el refinamiento iterativo.

Qué aspecto tienen las reglas configuradas

Las reglas configuradas cubren convenciones de formato que varían según el idioma o las preferencias de la empresa. Cosas como

{
  "dates_and_times": {
    "time_format": "use_24_hour_clock",
    "calendar_era": "use_bc_and_ad"
  },
  "punctuation": {
    "periods_in_academic_degrees": "do_not_use"
  },
  "numbers": {
    "decimal_separator": "use_comma"
  }
}

Parecen triviales, pero se complican rápidamente. Un documento alemán que utiliza el formato de hora AM/PM y decimales separados por puntos sólo se lee como "traducido del inglés" para un lector alemán. Establecer use_24_hour_clock y use_comma para los separadores decimales en todas las traducciones al alemán elimina eso inmediatamente.

Instrucciones personalizadas: éste es el verdadero poder

Las instrucciones personalizadas son directivas de texto libre, hasta 200 por lista de reglas de estilo, cada una de hasta 300 caracteres. Básicamente le dicen a DeepL cómo dar forma a la traducción en lenguaje llano:

public class TenantCustomInstruction
{
    public Guid Id { get; set; }
    public Guid StyleRuleListId { get; set; }
    public string Label { get; set; }              // e.g. "Tone instruction"
    public string Prompt { get; set; }             // e.g. "Use a friendly, diplomatic tone"
    public string? SourceLanguage { get; set; }    // Optional source lang filter
}

Ejemplos reales de nuestros inquilinos:

  • "Use a friendly, diplomatic tone" para una startup que quiere documentos accesibles
  • "Always use 'Sie' form, never 'du'" para un bufete de abogados alemán
  • "Translate 'deployment' as 'Bereitstellung', never 'Deployment'" para términos que necesitan un tratamiento dependiente del contexto más allá del simple mapeo del glosario
  • "Use British English spelling (colour, organisation, licence)" para empresas con sede en el Reino Unido que traducen entre variantes del inglés
  • "Put currency symbols after the numeric amount" para adaptarse a las convenciones europeas

Las instrucciones personalizadas son realmente potentes para las convenciones específicas de un dominio que no caben en las entradas de un glosario. Un glosario asigna un término a otro. Una instrucción personalizada puede decir "cuando traduzca documentos de la API, utilice el modo imperativo en lugar de la voz pasiva". Ese es un tipo de control completamente diferente.

Formalidad

El parámetro formality de DeepL (default, more, less, prefer_more, prefer_less) sigue estando disponible como control independiente junto con las reglas de estilo. El "du" alemán frente al "Sie", el "tu" francés frente al "vous", los niveles de cortesía japoneses. Estos se establecen por idioma del inquilino a través de TenantLanguageConfig:

public class TenantLanguageConfig : ITenantScoped
{
    public string LanguageCode { get; set; }
    public string DisplayName { get; set; }
    public bool IsEnabled { get; set; }
    public TranslationTrigger Trigger { get; set; }
    public string? Formality { get; set; }         // "more", "less", "prefer_more", etc.
    public string? StyleRuleListId { get; set; }   // Links to TenantStyleRuleList
    public string? TranslationProvider { get; set; }
    public int SortOrder { get; set; }
    public bool IsDefault { get; set; }
}

La formalidad, las normas de estilo y los glosarios se componen. Una sola llamada de traducción puede llevar los tres:

var glossaryId = await GetOrSyncDeepLGlossaryIdAsync(sourceLang, targetLang, ct);
var styleId = await GetOrSyncStyleRuleListAsync(targetLang, ct);
var formality = tenantLanguageConfig.Formality ?? "default";

// Build the v2/translate request payload
var payload = new
{
    text = new[] { blockContent },
    source_lang = NormalizeLanguageCode(sourceLang),
    target_lang = NormalizeLanguageCode(targetLang),
    glossary_id = glossaryId,
    style_id = styleId,
    formality = formality,
    preserve_formatting = true,
    context = surroundingContext,  // Adjacent blocks, not billed
    model_type = "quality_optimized"
};

Aquí hay que señalar dos cosas:

  1. El parámetro context. Pasamos bloques adyacentes como contexto para mejorar la calidad de la traducción. DeepL lo utiliza para resolver ambigüedades, pero no traduce ni factura por ello. Un párrafo sobre "celdas" se traduce de forma diferente cuando el contexto circundante es un documento de biología frente a un manual de hojas de cálculo.
  2. Selección del modelo. Cualquier solicitud con style_id o custom_instructions utiliza automáticamente el modelo quality_optimized de DeepL. Este es el nivel de calidad más alto. No puede combinarlos con latency_optimized, y es una restricción deliberada de DeepL. La personalización del estilo necesita el modelo completo.

Por qué esto importa más de lo que cree

Imagine una empresa que escribe documentos internos en alemán con un "du" informal que de repente cambia a un "Sie" formal en una sección traducida. Parece incoherente en el mejor de los casos, poco profesional en el peor. La formalidad se encarga de eso. Pero la formalidad por sí sola no detectará un documento que utiliza marcas de tiempo AM/PM cuando su oficina alemana utiliza el formato de 24 horas, o que pone el símbolo de la moneda antes del número en lugar de después.

Todas estas capas juntas (reglas de estilo, instrucciones personalizadas, formalidad, glosarios) producen traducciones que se leen como si alguien de su equipo las hubiera escrito. No como salidas de una máquina que no sabe que su empresa existe.

La capa de servicio DeepL

Toda la comunicación de DeepL pasa por IDeepLService. Envuelve el SDK .NET oficial de DeepL y gestiona las llamadas a la API v3 para las reglas de estilo:

public interface IDeepLService
{
    // Text translation (v2)
    Task<TextResult> TranslateTextAsync(
        string text, string sourceLanguage, string targetLanguage,
        string? options = null);

    Task<TextResult[]> TranslateTextBatchAsync(
        IEnumerable<string> texts, string sourceLanguage,
        string targetLanguage, string? options = null);

    // Glossary management (v2)
    Task<GlossaryInfo> CreateGlossaryAsync(
        string name, string sourceLang, string targetLang,
        Dictionary<string, string> entries);
    Task DeleteGlossaryAsync(string glossaryId);
    Task<GlossaryInfo> GetGlossaryAsync(string glossaryId);
    Task<GlossaryInfo[]> ListGlossariesAsync();
    Task<Dictionary<string, string>> GetGlossaryEntriesAsync(
        string glossaryId);

    // Style rules (v3)
    Task<StyleRuleResponse> CreateStyleRuleListAsync(
        string name, string language,
        JsonElement configuredRules,
        IEnumerable<CustomInstructionRequest> customInstructions);
    Task ReplaceConfiguredRulesAsync(
        string styleId, JsonElement configuredRules);
    Task<CustomInstructionResponse> AddCustomInstructionAsync(
        string styleId, string label, string prompt,
        string? sourceLanguage = null);
    Task DeleteCustomInstructionAsync(
        string styleId, string instructionId);
    Task DeleteStyleRuleListAsync(string styleId);

    // Usage tracking
    Task<Usage> GetUsageAsync();
    Task<Language[]> GetSourceLanguagesAsync();
    Task<Language[]> GetTargetLanguagesAsync();
}

La implementación gestiona la normalización del código del lenguaje. DeepL requiere EN-US o EN-GB en lugar de en desnudo, y PT-PT o PT-BR en lugar de pt:

private static string NormalizeLanguageCode(string code)
    => code.ToLower() switch
    {
        "en" => "EN-US",
        "pt" => "PT-PT",
        _ => code.ToUpper()
    };

La traducción por lotes utiliza el troceado de 50 elementos para mantenerse dentro de los límites de la API de DeepL al tiempo que se maximiza el rendimiento:

public async Task<TranslationBatchResult> TranslateBatchAsync(
    Dictionary<string, string> texts,
    string sourceLanguage, string targetLanguage)
{
    var translations = new Dictionary<string, string>();
    long totalChars = 0;

    foreach (var chunk in texts.Chunk(50))
    {
        var results = await _deepL.TranslateTextBatchAsync(
            chunk.Select(kv => kv.Value),
            sourceLanguage, targetLanguage);

        for (int i = 0; i < chunk.Length; i++)
        {
            translations[chunk[i].Key] = results[i].Text;
            totalChars += chunk[i].Value.Length;
        }
    }

    return new TranslationBatchResult
    {
        Translations = translations,
        BilledCharacters = totalChars
    };
}

Dado que sólo enviamos bloques anticuados, no documentos enteros, un lote de traducción típico para una sola edición contiene de 1 a 3 bloques en lugar de más de 40. De ahí procede la reducción de costes del 94%.

El orquestador de traducción

El TranslationOrchestrator decide qué hacer con cada bloque cuando cambia el documento fuente. Recorramos el árbol de decisiones:

public async Task OrchestrateTranslationAsync(
    Guid entryId, List<Guid> changedBlockIds,
    CancellationToken ct = default)
{
    var entry = await _db.Entries
        .FirstOrDefaultAsync(e => e.Id == entryId, ct);

    var translations = await _db.EntryTranslations
        .Where(t => t.EntryId == entryId)
        .ToListAsync(ct);

    foreach (var translation in translations)
    {
        var langConfig = await GetLanguageConfigAsync(
            translation.Language, ct);

        var translationBlocks = await _db.TranslationBlocks
            .Where(tb => changedBlockIds.Contains(tb.SourceBlockId)
                      && tb.Language == translation.Language)
            .ToListAsync(ct);

        foreach (var block in translationBlocks)
        {
            if (block.IsLocked || block.TranslatedById is not null)
            {
                // Human-edited or locked - mark stale, don't overwrite
                block.Status = TranslationStatus.Stale;
            }
            else if (langConfig.Trigger == TranslationTrigger.AlwaysTranslate)
            {
                // Machine-translated, auto mode - retranslate now
                await RetranslateBlockAsync(block, translation.Language, ct);
            }
            else
            {
                // TranslateOnFirstVisit - mark stale, translate later
                block.Status = TranslationStatus.Stale;
            }
        }
    }

    await _db.SaveChangesAsync(ct);
}

El bit clave: Los bloques editados por humanos nunca se sobrescriben automáticamente. Si un traductor ajustó manualmente un bloque, quizá añadiendo contexto cultural o reformulando la redacción para mayor claridad, el sistema respeta ese trabajo. Marca el bloque como obsoleto para que el traductor sepa que la fuente ha cambiado, pero no reemplazará silenciosamente sus ediciones.

Los bloques traducidos automáticamente con AlwaysTranslate activado se retraducen inmediatamente. Los bloques traducidos automáticamente con TranslateOnFirstVisit se marcan como obsoletos y se traducen cuando alguien abre el documento en ese idioma.

Activadores de traducción: cuándo se producen las traducciones

Cada idioma tiene un TranslationTrigger que controla los tiempos:

public enum TranslationTrigger
{
    AlwaysTranslate,         // Translate on every save
    TranslateOnFirstVisit    // Translate when first opened in that language
}

AlwaysTranslate es útil para los idiomas de alta prioridad en los que desea que las traducciones estén inmediatamente actualizadas. Francés para una empresa con una gran oficina en París. Alemán para una empresa con sede en Múnich.

TranslateOnFirstVisit es útil para idiomas que se necesitan ocasionalmente pero no merece la pena el coste API de mantener perfectamente actualizados en todo momento. Cuando alguien abre el documento en ese idioma, los bloques obsoletos se traducen sobre la marcha.

Ambos modos utilizan la misma resolución de glosario, los mismos ajustes de formalidad y el mismo hashing de contenido. La única diferencia es el tiempo.

Adaptación única del contenido y la estructura

Aquí es donde la arquitectura realmente vale la pena más allá de la mera traducción.

Cuando un traductor alemán añade una sección de cumplimiento de la DSGVO que no existe en inglés, la añade como un nuevo bloque en la versión alemana. Ese bloque no tiene SourceBlockId, está marcado como contenido único. El sistema nunca lo envía para retraducción porque no hay fuente de la que traducir. Sólo existe en alemán.

Cuando un traductor japonés cambia una lista con viñetas por una lista numerada (una convención común en la escritura técnica japonesa), la bandera IsStructureAdapted del bloque lo conserva en futuros ciclos de retraducción:

var translation = new TranslationBlock
{
    SourceBlockId = sourceBlockId,
    Language = targetLanguage,
    BlockType = translatedBlockType,
    SourceBlockType = sourceBlock.BlockType,
    IsStructureAdapted = translatedBlockType != sourceBlock.BlockType,
    StructureAdaptationNotes = "Numbered list preferred in JP technical docs",
    SourceContentHash = sourceBlock.ContentHash,
    Status = TranslationStatus.UpToDate,
};

La bandera IsNoTranslate se encarga del contenido que debe copiarse textualmente: bloques de código, URL, nombres de productos, notación matemática. El proveedor de traducción se los salta por completo.

Ponerlo todo junto

Recorramos el flujo completo. Un usuario en Londres edita un párrafo en el documento fuente en inglés, y su oficina de Múnich tiene el alemán configurado en AlwaysTranslate:

  1. El usuario guarda. TipTap envía JSON a la API.
  2. Extracción de bloques y detección de cambios. CreateBlocksFromDocumentAsync analiza JSON, recalcula los hashes de contenido y compara los hashes antiguos y nuevos para identificar qué bloques cambiaron realmente.
  3. El orquestador se ejecuta. Encuentra el EntryTranslation alemán, comprueba el bloque alemán. Está traducido por máquina, no está bloqueado y no ha sido editado por humanos, por lo que es apto para la retraducción.
  4. Configuración de traducción cargada. ID del glosario resuelto mediante GetOrSyncDeepLGlossaryIdAsync("en", "de"), reglas de estilo mediante GetOrSyncStyleRuleListAsync("de"), formalidad establecida en "más" (formal "Sie"), bloques adyacentes pasados como contexto para desambiguación.
  5. Llamada DeepL. Se envía un único bloque con el ID del glosario, el ID del estilo, la formalidad y el contexto.
  6. Bloque actualizado. Contenido traducido almacenado, SourceContentHash sincronizado, estado establecido en UpToDate. Un bloque traducido en lugar de los más de 40. ¿Los 39 bloques restantes? Intactos.

Mientras tanto, su oficina de Tokio tiene el japonés fijado en TranslateOnFirstVisit. La misma edición marca el bloque de traducción japonés como Stale. Cuando alguien en Tokio abre el documento, los pasos 5-9 suceden sobre la marcha. La adaptación de su estructura (lista numerada) se conserva. Sus bloques únicos permanecen exactamente donde están.


Creo que el motor de traducción es la parte de Rasepi que aporta el valor más visible. Traducciones que utilizan su terminología, siguen sus convenciones de formato, obedecen sus instrucciones personalizadas, coinciden con su tono, respetan el trabajo de sus traductores y cuestan una fracción de lo que costaría la retraducción de un documento completo. La arquitectura hace que todo eso sea automático, y se mantiene al margen cuando los humanos quieren tomar el relevo.

El mismo motor DeepL que impulsa las traducciones escritas también impulsa Talk to Docs, nuestra interfaz de documentación conversacional, con DeepL Voice encargándose de la interacción hablada. Los mismos glosarios, las mismas normas de estilo, la misma formalidad, la misma coherencia. Tanto si su equipo lee la documentación como si habla con ella, la calidad lingüística es idéntica.

Explore la API de traducción →

Mantén tu documentación actualizada. Automáticamente.

Rasepi impone fechas de revisión, supervisa la salud del contenido y publica en más de 40 idiomas.

Empieza gratis →