← Torna al blog

All'interno del motore di traduzione: Glossari, regole di stile e ritraduzione intelligente

Un approfondimento tecnico su come funziona effettivamente la pipeline di traduzione di Rasepi: risoluzione del glossario, regole di stile DeepL e istruzioni personalizzate, hashing dei contenuti e l'integrazione che lega il tutto.

Sotto il cofano
All'interno del motore di traduzione: Glossari, regole di stile e ritraduzione intelligente

Il nostro post precedente sull'architettura ha trattato i plugin, le action guard e il sistema di pipeline. Questo post approfondisce il motore di traduzione, la parte che credo renda Rasepi fondamentalmente diverso da ogni altra piattaforma di documentazione.

Non il marketing che parla di tradurre paragrafi anziché pagine. Il codice vero e proprio. Come i glossari vengono risolti per tenant, come le regole di stile di DeepL e le istruzioni personalizzate modellano ogni traduzione, come l'hashing dei contenuti guida il rilevamento degli stanti e come l'orchestratore decide quali blocchi ritradurre.

Motore di traduzione: glossari, regole di stile e ritraduzione intelligente

La pipeline di traduzione

Quando un utente salva un documento, il sistema non si limita a ritradurre tutto. Esegue una sequenza piuttosto specifica:

  1. Analizza il JSON di TipTap in singoli blocchi.
  2. Confronta gli hash dei contenuti per rilevare quali blocchi sono stati effettivamente modificati.
  3. Per i blocchi modificati, risolvere il glossario dell'inquilino e l'elenco delle regole di stile per la coppia di lingue.
  4. Applica le regole di stile, le istruzioni personalizzate e la formalità dalla configurazione del locatario.
  5. Invia solo i blocchi modificati a DeepL
  6. Aggiorna i blocchi di traduzione e sincronizza gli hash dei contenuti

Ogni fase è un servizio proprio con una propria interfaccia. Questo è importante perché ogni fase può essere sostituita da qualcos'altro, un diverso fornitore di traduzioni, un diverso algoritmo di hashing, una diversa fonte di glossario.

Risoluzione del glossario: in base all'inquilino, DeepL-sincronizzato

I glossari di DeepL hanno un vincolo che la maggior parte delle persone non conosce: **Non si può modificare un glossario DeepL. Qualsiasi modifica comporta la cancellazione del vecchio glossario e la creazione di uno nuovo.

Rasepi gestisce questo aspetto trattando il database come fonte di verità e i glossari DeepL come artefatti di runtime da buttare. L'entità TenantGlossary memorizza tutto a livello locale:

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 un utente aggiunge una voce di glossario, ad esempio la mappatura di "Sprint Review" in "Sprint-Überprüfung" per EN→DE, il record del database si aggiorna immediatamente e IsDirty viene impostato su true. Il glossario DeepL non viene ricreato subito. Viene ricreato pigramente, la prossima volta che una traduzione ne ha effettivamente bisogno.

Il flusso di sincronizzazione

Prima di ogni chiamata di traduzione, il sistema risolve il glossario:

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

Tre cose da notare qui:

  1. Sincronizzazione pigra. Intercettiamo l'API DeepL solo quando è effettivamente necessaria una traduzione. La modifica in blocco delle voci del glossario non comporta decine di chiamate all'API.
  2. **La query passa attraverso i filtri della query globale EF, quindi TenantGlossaries è automaticamente individuato. Le voci del glossario dell'inquilino A non si riversano mai nelle traduzioni dell'inquilino B.
  3. Un glossario per coppia di lingue. DeepL lo impone comunque. Un glossario EN→DE, un glossario EN→FR e così via. La coppia (SourceLanguage, TargetLanguage) è unica per inquilino.

Voci del glossario

Le singole voci sono solo mappature di termini:

CODICEBLOCCO_2

L'API le offre un CRUD completo e l'importazione/esportazione di CSV per una gestione massiva:

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

L'importazione CSV è utilissima per i team che migrano da sistemi di memoria di traduzione esistenti. Esporta i termini, li ripulisce, li importa in Rasepi e la successiva esecuzione della traduzione utilizza automaticamente il nuovo glossario.

Regole di stile, istruzioni personalizzate e formalità

I glossari gestiscono la terminologia. Ma la terminologia è solo la metà. Una traduzione può utilizzare tutte le parole giuste e tuttavia suonare male. Tono sbagliato, formato della data sbagliato, convenzioni di punteggiatura sbagliate.

La Style Rules API di DeepL (v3) risolve questo problema. Può creare elenchi di regole di stile riutilizzabili che combinano due tipi di controlli:

  1. Regole configurate, convenzioni di formattazione predefinite per date, orari, punteggiatura, numeri e altro.
  2. Istruzioni personalizzate, direttive di testo libero che modellano il tono, la formulazione e le convenzioni specifiche del dominio.

Rasepi crea e gestisce queste istruzioni per tenant, per lingua di destinazione. L'entità TenantStyleRuleList memorizza il DeepL style_id insieme alle regole configurate e alle istruzioni personalizzate del tenant:

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

Creazione di elenchi di regole di stile

Quando un amministratore imposta le regole di traduzione per il tedesco, Rasepi chiama l'API v3 di DeepL per creare l'elenco di regole di stile. Ecco come appare:

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 differenza dei glossari, gli elenchi di regole di stile di DeepL sono mutabili. Può sostituire le regole configurate sul posto con PUT /v3/style_rules/{style_id}/configured_rules, e le istruzioni personalizzate possono essere aggiunte, aggiornate o eliminate individualmente. Molto più amichevole per il perfezionamento iterativo.

Come appaiono le regole configurate

Le regole configurate coprono le convenzioni di formattazione che variano in base alla lingua o alle preferenze aziendali. Cose come:

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

Sembrano banali, ma si sommano velocemente. Un documento tedesco che utilizza il formato orario AM/PM e i decimali separati da un periodo, viene letto come "tradotto dall'inglese" da un lettore tedesco. L'impostazione di use_24_hour_clock e use_comma per i separatori decimali in tutte le traduzioni in tedesco elimina immediatamente questo problema.

Istruzioni personalizzate: questa è la vera potenza

Le istruzioni personalizzate sono direttive di testo libero, fino a 200 per elenco di regole di stile, ciascuna di 300 caratteri. In pratica, lei dice a DeepL come modellare la traduzione in un linguaggio semplice:

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
}

Esempi reali dai nostri inquilini:

  • "Use a friendly, diplomatic tone" per una startup che desidera documenti accessibili
  • "Always use 'Sie' form, never 'du'" per uno studio legale tedesco
  • "Translate 'deployment' as 'Bereitstellung', never 'Deployment'" per i termini che necessitano di una gestione dipendente dal contesto, al di là della semplice mappatura del glossario
  • "Use British English spelling (colour, organisation, licence)" per le aziende con sede nel Regno Unito che traducono tra varianti dell'inglese
  • CODEBLOCCO_30 per corrispondere alle convenzioni europee

Le istruzioni personalizzate sono davvero potenti per le convenzioni specifiche del dominio che non si adattano alle voci del glossario. Un glossario mappa un termine ad un altro. Un'istruzione personalizzata può dire: "Quando traduce i documenti API, usi l'umore imperativo invece della voce passiva". Si tratta di un tipo di controllo completamente diverso.

Formalità

Il parametro formality di DeepL (default, more, less, prefer_more, prefer_less) è ancora disponibile come controllo separato insieme alle regole di stile. Tedesco "du" contro "Sie", francese "tu" contro "vous", livelli di cortesia giapponese. Questi sono impostati per ogni lingua di inquilino tramite 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; }
}

Formalità, regole di stile e glossari si compongono. Una singola chiamata di traduzione può contenere tutte e tre le cose:

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

Due cose da notare qui:

  1. Il parametro context. Passiamo i blocchi adiacenti come contesto per migliorare la qualità della traduzione. DeepL lo utilizza per risolvere le ambiguità, ma non traduce né fattura per questo. Un paragrafo sulle "celle" si traduce in modo diverso quando il contesto circostante è un documento di biologia rispetto ad un manuale di foglio elettronico.
  2. Selezione del modello. Qualsiasi richiesta con style_id o custom_instructions utilizza automaticamente il modello quality_optimized di DeepL. Questo è il livello di qualità più alto. Non è possibile combinarli con latency_optimized, e questo è un vincolo intenzionale di DeepL. La personalizzazione dello stile richiede il modello completo.

Perché questo è importante più di quanto si pensi

Immagini un'azienda che scrive documenti interni in tedesco con l'informale "du" che improvvisamente passa al formale "Sie" in una sezione tradotta. Nel migliore dei casi sembra incoerente, nel peggiore poco professionale. La formalità gestisce questo aspetto. Ma la formalità, da sola, non può catturare un documento che utilizza i timestamp AM/PM quando l'ufficio tedesco usa il formato 24 ore, o che mette il simbolo della valuta prima del numero invece che dopo.

Tutti questi elementi (regole di stile, istruzioni personalizzate, formalità, glossari) producono traduzioni che sembrano scritte da qualcuno del suo team. Non come se fossero prodotte da una macchina che non sa dell'esistenza della sua azienda.

Il livello di servizio DeepL

Tutte le comunicazioni di DeepL passano attraverso IDeepLService. Questo strato avvolge l'SDK ufficiale di DeepL .NET e gestisce le chiamate API v3 per le regole di stile:

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

L'implementazione gestisce la normalizzazione del codice linguistico. DeepL richiede EN-US o EN-GB invece di en nudo, e PT-PT o PT-BR invece di pt:

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

La traduzione batch utilizza un chunking di 50 elementi per rimanere entro i limiti dell'API di DeepL, massimizzando al contempo il throughput:

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

Dato che inviamo solo blocchi di dati, non interi documenti, un tipico batch di traduzione per una singola redazione contiene 1-3 blocchi invece di 40+. Ecco da dove deriva la riduzione dei costi del 94%.

L'orchestratore di traduzione

Il TranslationOrchestrator decide cosa fare con ogni blocco quando il documento sorgente cambia. Esaminiamo l'albero delle decisioni:

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

Il punto chiave: **Se un traduttore ha modificato manualmente un blocco, magari aggiungendo un contesto culturale o riformulando per chiarezza, il sistema rispetta il suo lavoro. Contrassegna il blocco come obsoleto, in modo che il traduttore sappia che la fonte è cambiata, ma non sostituisce silenziosamente le sue modifiche.

I blocchi tradotti a macchina con AlwaysTranslate abilitato vengono ritradotti immediatamente. I blocchi tradotti a macchina con TranslateOnFirstVisit sono contrassegnati come stantii e tradotti quando qualcuno apre effettivamente il documento in quella lingua.

Inneschi di traduzione: quando avvengono le traduzioni

Ogni lingua ha un TranslationTrigger che controlla la tempistica:

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

Il AlwaysTranslate è utile per le lingue ad alta priorità, dove vuole che le traduzioni siano immediatamente attuali. Francese per un'azienda con una grande sede a Parigi. Tedesco per un'azienda con sede a Monaco.

TranslateOnFirstVisit è utile per le lingue che sono occasionalmente necessarie ma che non valgono il costo API di mantenere sempre perfettamente aggiornate. Quando qualcuno apre il documento in quella lingua, i blocchi obsoleti vengono tradotti al volo.

Entrambe le modalità utilizzano la stessa risoluzione del glossario, le stesse impostazioni di formalità e lo stesso hashing dei contenuti. L'unica differenza è la tempistica.

Adattamento unico del contenuto e della struttura

È qui che l'architettura dà i suoi frutti, al di là della semplice traduzione.

Quando un traduttore tedesco aggiunge una sezione di conformità DSGVO che non esiste in inglese, la aggiunge come nuovo blocco nella versione tedesca. Quel blocco non ha SourceBlockId, è contrassegnato come contenuto unico. Il sistema non lo invia mai per la ritraduzione, perché non esiste una fonte da cui tradurre. Esiste solo in tedesco.

Quando un traduttore giapponese cambia un elenco puntato in un elenco numerato (una convenzione comune nella scrittura tecnica giapponese), il flag IsStructureAdapted del blocco lo conserva per i futuri cicli di ritraduzione:

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

Il flag IsNoTranslate gestisce i contenuti che devono essere copiati alla lettera: blocchi di codice, URL, nomi di prodotti, notazioni matematiche. Il fornitore di traduzione li salta completamente.

Mettere tutto insieme

Vediamo il flusso completo. Un utente a Londra modifica un paragrafo nel documento sorgente inglese, e il suo ufficio di Monaco ha il tedesco impostato su AlwaysTranslate:

  1. L'utente salva. TipTap invia JSON all'API.
  2. Estrazione dei blocchi e rilevamento delle modifiche. CreateBlocksFromDocumentAsync analizza JSON, ricalcola gli hash dei contenuti e confronta gli hash vecchi e nuovi per identificare i blocchi effettivamente modificati.
  3. **Trova il EntryTranslation tedesco, controlla il blocco tedesco. È tradotto a macchina, non bloccato, non modificato dall'uomo, quindi è idoneo per la ritraduzione.
  4. Configurazione della traduzione caricata. ID del glossario risolto tramite GetOrSyncDeepLGlossaryIdAsync("en", "de"), regole di stile tramite GetOrSyncStyleRuleListAsync("de"), formalità impostata su "more" (formale "Sie"), blocchi adiacenti passati come contesto per la disambiguazione.
  5. Chiamata a DeepL. Blocco singolo inviato con ID glossario, ID stile, formalità e contesto.
  6. Aggiornamento del blocco. Contenuto tradotto memorizzato, SourceContentHash sincronizzato, stato impostato su UpToDate. Un blocco tradotto invece di 40+. I 39 blocchi rimanenti? Non sono stati toccati.

Nel frattempo, il suo ufficio di Tokyo ha il giapponese impostato su TranslateOnFirstVisit. La stessa modifica contrassegna il blocco di traduzione giapponese come Stale. Quando qualcuno a Tokyo apre il documento, i passaggi 5-9 avvengono al volo. L'adattamento della struttura (elenco numerato) viene conservato. I loro blocchi unici rimangono esattamente dove sono.


Credo che il motore di traduzione sia la parte di Rasepi che offre il valore più visibile. Traduzioni che utilizzano la sua terminologia, seguono le sue convenzioni di formattazione, obbediscono alle sue istruzioni personalizzate, corrispondono al suo tono, rispettano il lavoro dei suoi traduttori e costano una frazione di quanto costerebbe una ritraduzione di un documento completo. L'architettura rende tutto questo automatico, e non si intromette quando l'uomo vuole intervenire.

Lo stesso motore di DeepL che alimenta le traduzioni scritte alimenta anche Talk to Docs, la nostra interfaccia di documentazione conversazionale, con DeepL Voice che gestisce l'interazione parlata. Stessi glossari, stesse regole di stile, stessa formalità, stessa coerenza. Che il suo team legga la documentazione o ci parli, la qualità del linguaggio è identica.

Esplora l'API di traduzione →

Mantieni la tua documentazione aggiornata. Automaticamente.

Rasepi impone date di revisione, monitora la qualità dei contenuti e pubblica in oltre 40 lingue.

Inizia gratis →