← Voltar ao blog

Por dentro do mecanismo de tradução: Glossários, Regras de Estilo e Retradução Inteligente

Um passo a passo técnico profundo de como o pipeline de tradução do Rasepi realmente funciona: resolução de glossário, DeepL regras de estilo e instruções personalizadas, hashing de conteúdo e a integração que une tudo isso.

Por dentro do mecanismo de tradução: Glossários, Regras de Estilo e Retradução Inteligente

O nosso post de arquitetura anterior cobriu plugins, guardas de ação e o sistema de pipeline. Este vai mais fundo no motor de tradução, a parte que torna o Rasepi fundamentalmente diferente de qualquer outra plataforma de documentação.

Não é o discurso de marketing sobre traduzir parágrafos em vez de páginas. O código real. Como os glossários são resolvidos por locatário, como as regras de estilo do DeepL e as instruções personalizadas moldam cada tradução, como o hashing de conteúdo impulsiona a deteção de desatualização e como o orquestrador decide quais blocos devem ser retraduzidos.

Motor de tradução: glossários, regras de estilo e retradução inteligente

A linha de tradução

Quando um utilizador guarda um documento, o sistema não se limita a retraduzir tudo. Ele executa uma sequência bastante específica:

  1. Analisa o JSON do TipTap em blocos individuais
  2. Compara hashes de conteúdo para detetar quais blocos foram realmente alterados
  3. Para os blocos alterados, resolver o glossário do locatário e a lista de regras de estilo para o par de idiomas
  4. Aplicar regras de estilo, instruções personalizadas e formalidade da configuração do locatário
  5. Enviar apenas blocos alterados para DeepL
  6. Atualizar os blocos de tradução e sincronizar os hashes de conteúdo

Cada passo é o seu próprio serviço com a sua própria interface. Isso é importante porque qualquer etapa pode ser trocada por outra coisa, um fornecedor de tradução diferente, um algoritmo de hashing diferente, uma fonte de glossário diferente.

Resolução do glossário: com escopo do locatário, sincronizado com DeepL

DeepL glossários têm uma restrição que a maioria das pessoas não conhece: eles são imutáveis. Você não pode editar um glossário DeepL. Qualquer mudança significa apagar o antigo e criar um novo.

O Rasepi lida com isto tratando a base de dados como a fonte da verdade e os DeepL glossários como artefactos descartáveis em tempo de execução. A entidade TenantGlossary armazena tudo 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; }
}

Quando um utilizador adiciona uma entrada no glossário, por exemplo, mapeando "Sprint Review" para "Sprint-Überprüfung" para EN→DE, o registo da base de dados é atualizado imediatamente e IsDirty é definido para true. O glossário DeepL não é recriado imediatamente. Ele é recriado preguiçosamente, na próxima vez que uma tradução realmente precisar dele.

O fluxo de sincronização

Antes de cada chamada de tradução, o sistema resolve o glossário:

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;
}

Três coisas dignas de nota aqui:

  1. Sincronização preguiçosa Nós só acessamos a API DeepL quando uma tradução é realmente necessária. A edição de entradas do glossário em massa não desencadeia dezenas de chamadas à API.
  2. **A consulta é executada através de filtros de consulta global EF, então TenantGlossaries é automaticamente escopo. As entradas do glossário do locatário A nunca entram nas traduções do locatário B.
  3. Um glossário por par de línguas DeepL impõe isto de qualquer forma. Um glossário EN→DE, um glossário EN→FR, e assim por diante. O par (SourceLanguage, TargetLanguage) é único por locatário.

Entradas do glossário

As entradas individuais são apenas mapeamentos de termos:

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"
}

A API oferece-lhe CRUD completo e importação/exportação de CSV para gestão em massa:

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

A importação de CSV é muito útil para equipas que migram de sistemas de memória de tradução existentes. Exporte os seus termos, limpe-os, importe-os para o Rasepi, e a próxima tradução usa o novo glossário automaticamente.

Regras de estilo, instruções personalizadas e formalidade

Os glossários lidam com a terminologia. Mas a terminologia é apenas metade da questão. Uma tradução pode usar todas as palavras corretas e mesmo assim soar mal. Tom errado, formato de data errado, convenções de pontuação erradas.

A API de regras de estilo (v3) do DeepL resolve este problema. É possível criar listas de regras de estilo reutilizáveis que combinam dois tipos de controlos:

  1. Regras configuradas, convenções de formatação predefinidas para datas, horas, pontuação, números e muito mais
  2. Instruções personalizadas, diretivas de texto livre que moldam o tom, o fraseado e as convenções específicas do domínio

O Rasepi cria e gere estas instruções por inquilino, por idioma de destino. A entidade TenantStyleRuleList armazena o DeepL style_id juntamente com as regras configuradas e as instruções personalizadas do locatário:

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; }
}

Criar listas de regras de estilo

Quando um administrador define regras de tradução para o alemão, o Rasepi chama a API v3 do DeepL para criar a lista de regras de estilo. Aqui está o 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;
}

Ao contrário dos glossários, as listas de regras de estilo do DeepL são mutáveis. Pode substituir as regras configuradas no local com PUT /v3/style_rules/{style_id}/configured_rules, e as instruções personalizadas podem ser adicionadas, actualizadas ou eliminadas individualmente. Muito mais amigável para refinamento iterativo.

Como são as regras configuradas

As regras configuradas cobrem convenções de formatação que variam de acordo com o idioma ou preferência da empresa. Coisas 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"
  }
}

Estas regras parecem triviais, mas são muito rápidas. Um documento alemão que usa o formato de hora AM/PM e decimais separados por ponto final é lido como "traduzido do inglês" para um leitor alemão. Definir use_24_hour_clock e use_comma para separadores decimais em todas as traduções alemãs elimina isso imediatamente.

Instruções personalizadas: este é o verdadeiro poder

As instruções personalizadas são diretivas de texto livre, até 200 por lista de regras de estilo, cada uma com um máximo de 300 caracteres. Basicamente, diz a DeepL como moldar a tradução em linguagem simples:

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
}

Exemplos reais dos nossos inquilinos:

  • "Use a friendly, diplomatic tone" para uma empresa em fase de arranque que pretende documentos acessíveis
  • "Always use 'Sie' form, never 'du'" para um escritório de advogados alemão
  • "Translate 'deployment' as 'Bereitstellung', never 'Deployment'" para termos que necessitam de tratamento dependente do contexto para além do simples mapeamento do glossário
  • "Use British English spelling (colour, organisation, licence)" para empresas sediadas no Reino Unido que traduzem entre variantes do inglês
  • "Put currency symbols after the numeric amount" para corresponder às convenções europeias

As instruções personalizadas são realmente poderosas para convenções específicas do domínio que não cabem nas entradas do glossário. Um glossário mapeia um termo para outro. Uma instrução personalizada pode dizer "ao traduzir documentos API, use o modo imperativo em vez da voz passiva". Trata-se de um tipo de controlo completamente diferente.

Formalidade

O parâmetro formality de DeepL (default, more, less, prefer_more, prefer_less) ainda está disponível como um controlo separado juntamente com as regras de estilo. Alemão "du" versus "Sie", francês "tu" versus "vous", níveis de polidez japoneses. Estes são definidos por idioma do locatário atravé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; }
}

A formalidade, as regras de estilo e os glossários são todos compostos. Uma única chamada de tradução pode conter todos os três:

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"
};

Duas coisas que vale a pena notar aqui:

  1. **Passamos blocos adjacentes como contexto para melhorar a qualidade da tradução. DeepL usa-o para resolver ambiguidades, mas não o traduz nem o fatura. Um parágrafo sobre "células" é traduzido de forma diferente quando o contexto circundante é um documento de biologia versus um manual de folha de cálculo.
  2. **Seleção do modelo Qualquer pedido com style_id ou custom_instructions utiliza automaticamente o modelo quality_optimized de DeepL. Este é o nível de qualidade mais elevado. Não é possível combiná-los com latency_optimized, o que é uma restrição deliberada de DeepL. A personalização do estilo necessita do modelo completo.

Porque é que isto é mais importante do que se pensa

Imagine uma empresa que escreve documentos internos em alemão com o informal "du" que, de repente, muda para o formal "Sie" numa secção traduzida. Parece inconsistente, na melhor das hipóteses, e pouco profissional, na pior. A formalidade trata disso. Mas a formalidade, por si só, não vai apanhar um documento que usa carimbos de data e hora AM/PM quando o seu escritório alemão usa o formato de 24 horas, ou que coloca o símbolo da moeda antes do número em vez de depois.

Todos estes elementos em conjunto (regras de estilo, instruções personalizadas, formalidade, glossários) produzem traduções que parecem ter sido escritas por alguém da sua equipa. Não como se fossem produzidas por uma máquina que não sabe que a sua empresa existe.

A camada de serviço DeepL

Toda a comunicação DeepL passa pelo IDeepLService. Ele envolve o SDK oficial do DeepL .NET e lida com as chamadas da API v3 para regras 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();
}

A implementação trata da normalização do código da língua. DeepL requer EN-US ou EN-GB em vez de en simples, e PT-PT ou PT-BR em vez de pt:

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

A tradução em lote usa 50-item chunking para ficar dentro dos limites da API do DeepL enquanto maximiza a taxa de transferência:

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
    };
}

Como só enviamos blocos obsoletos, e não documentos inteiros, um lote de tradução típico para uma única edição contém 1-3 blocos em vez de mais de 40. É daí que vem a redução de custos de 94%.

O orquestrador de tradução

O TranslationOrchestrator decide o que fazer com cada bloco quando o documento de origem é alterado. Vamos percorrer a árvore de decisão:

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);
}

A parte chave: **Se um tradutor ajustou manualmente um bloco, talvez adicionando contexto cultural ou reformulando a redação para maior clareza, o sistema respeita esse trabalho. Marca o bloco como obsoleto para que o tradutor saiba que a fonte mudou, mas não substitui silenciosamente as suas edições.

Blocos traduzidos por máquina com AlwaysTranslate ativado são retraduzidos imediatamente. Os blocos traduzidos automaticamente com TranslateOnFirstVisit são marcados como obsoletos e traduzidos quando alguém abre efetivamente o documento nessa língua.

Gatilhos de tradução: quando as traduções acontecem

Cada língua tem um TranslationTrigger que controla o tempo:

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

O AlwaysTranslate é útil para línguas de alta prioridade onde quer que as traduções sejam imediatamente actuais. Francês para uma empresa com um grande escritório em Paris. Alemão para uma empresa com sede em Munique.

TranslateOnFirstVisit é útil para línguas que são ocasionalmente necessárias, mas que não valem o custo da API para manter perfeitamente actualizadas em todos os momentos. Quando alguém abre o documento nessa língua, os blocos obsoletos são traduzidos em tempo real.

Ambos os modos usam a mesma resolução de glossário, as mesmas configurações de formalidade e o mesmo hashing de conteúdo. A única diferença é o tempo.

Adaptação única de conteúdo e estrutura

É aqui que a arquitetura compensa realmente, para além da simples tradução.

Quando um tradutor alemão adiciona uma secção de conformidade DSGVO que não existe em inglês, adiciona-a como um novo bloco na versão alemã. Esse bloco não tem SourceBlockId, é assinalado como conteúdo único. O sistema nunca o envia para retradução porque não existe uma fonte a partir da qual traduzir. Só existe em alemão.

Quando um tradutor japonês muda uma lista de marcadores para uma lista numerada (uma convenção comum na escrita técnica japonesa), a marca IsStructureAdapted do bloco preserva-o em futuros ciclos de retradução:

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,
};

O sinalizador IsNoTranslate lida com o conteúdo que deve ser copiado literalmente: blocos de código, URLs, nomes de produtos, notação matemática. O fornecedor de tradução ignora-os completamente.

Colocando tudo junto

Vamos percorrer o fluxo completo. Um utilizador em Londres edita um parágrafo no documento de origem em inglês e o seu escritório em Munique tem o alemão definido como AlwaysTranslate:

  1. **O utilizador guarda. O TipTap envia JSON para a API
  2. **Extração de blocos: CreateBlocksFromDocumentAsync analisa o JSON e recalcula os hashes de conteúdo
  3. **Deteção de alterações: o sistema compara os hashes antigos e novos e identifica o bloco alterado
  4. **O orquestrador é executado. Encontra o EntryTranslation alemão, verifica o bloco alemão
  5. **O bloco é traduzido por máquina. Não bloqueado, não editado por humanos → elegível para retradução
  6. Resolução do glossário GetOrSyncDeepLGlossaryIdAsync("en", "de") devolve o ID do glossário (sincroniza se estiver sujo)
  7. Resolução de regras de estilo GetOrSyncStyleRuleListAsync("de") devolve o DeepL style_id com regras de formatação configuradas e instruções personalizadas
  8. Formalidade + contexto. Formalidade definida para "mais" (formal "Sie"), blocos adjacentes passados como contexto para desambiguação, preservar a formatação em
  9. **Chamada DeepL. Bloco único enviado com ID do glossário, ID do estilo, formalidade e contexto
  10. **Conteúdo traduzido armazenado, SourceContentHash sincronizado, estado definido para UpToDate
  11. **Um bloco traduzido em vez de mais de 40. Os restantes 39 blocos? Inalterados.

Entretanto, o seu escritório em Tóquio tem o japonês definido como TranslateOnFirstVisit. A mesma edição marca o bloco de tradução em japonês como Stale. Quando alguém em Tóquio abre o documento, os passos 5-9 acontecem na hora. A adaptação da sua estrutura (lista numerada) é preservada. Os seus blocos únicos permanecem exatamente onde estão.


O motor de tradução é a parte do Rasepi que oferece o valor mais visível. Traduções que utilizam a sua terminologia, seguem as suas convenções de formatação, obedecem às suas instruções personalizadas, correspondem ao seu tom, respeitam o trabalho dos seus tradutores e custam uma fração do que custaria a retradução de um documento completo. A arquitetura torna tudo isto automático e mantém-se fora do caminho quando os humanos querem assumir o controlo.

O mesmo motor DeepL que alimenta as traduções escritas também alimenta o Talk to Docs, a nossa interface de documentação conversacional, com o DeepL Voice a tratar da interação falada. Os mesmos glossários, as mesmas regras de estilo, a mesma formalidade, a mesma consistência. Quer a sua equipa leia a documentação ou fale com ela, a qualidade da linguagem é idêntica.

Explore a API de tradução →

Mantenha a sua documentação atualizada. Automaticamente.

O Rasepi impõe datas de revisão, monitoriza a qualidade do conteúdo e publica em mais de 40 idiomas.

Comece gratuitamente →