Come traduco questo blog con l'IA
Sono madrelingua spagnola. Vivo negli Stati Uniti, quindi uso l'inglese ogni giorno. Scrivo i miei post in inglese per esercitarmi e raggiungere più persone.
Google Analytics mi ha mostrato qualcosa di importante. Circa il 30-40% dei miei visitatori proviene da paesi in cui l'inglese non è la lingua principale. Questo mi ha fatto capire che stavo perdendo delle opportunità. Quindi ho deciso di tradurre il blog con l'intelligenza artificiale.
La mia configurazione
- Come archivio i post: Ogni post del blog è un semplice file con titolo, data, contenuto e altri dettagli. Pensateci come a un ricettario digitale.
- Il framework del mio sito web: Uso Next.js con strumenti di internazionalizzazione che aiutano a gestire automaticamente più lingue.
- L'IA che utilizzo: L'intelligenza artificiale Gemini di Google esegue la traduzione. Le do istruzioni precise di tradurre solo il contenuto testuale e di lasciare intatte tutte le parti tecniche (come link e codice).
- Un comando fa tutto: Eseguo un singolo script che invia il mio post in inglese all'intelligenza artificiale Gemini di Google, che lo traduce in tutte e 8 le lingue simultaneamente. Lo script quindi organizza queste traduzioni in file separati, aggiorna tutti i link tra le lingue in modo che i lettori possano passare da una lingua all'altra senza problemi e verifica che ogni versione linguistica esista e sia completa.
- Cambio lingua: Quando i lettori cliccano per cambiare lingua, vengono automaticamente indirizzati alla versione corretta del post.
Nota importante: Lo script si chiama
translate-gemini.ts
. È uno script di traduzione generico che può tradurre in qualsiasi lingua. Utilizza la variabile di ambiente
TARGET_LOCALE
per determinare in quale lingua tradurre. Quindi è necessario solo questo script, non script separati per ogni lingua.
Il mio flusso di lavoro
- Scrivo il post in inglese come
app/[locale]/blog/posts/
. Il campo
<slug>/index.jsoncontent
è HTML. I blocchi di codice sono racchiusi tra delimitatori. - Traduco con un solo comando. Il modello traduce
title
,excerpt
,content
ereadTime
. I metadati comedate
,tags
ecategories
rimangono invariati. - Tengo gli slug sincronizzati con una mappa
translatedSlugs
in ogni file, inglese e traduzioni. - Valido che ogni lingua esista e che la mappa degli slug sia completa.
Perché l'IA funziona per me
- Segui le istruzioni: Il prompt protegge HTML, codice e link.
- Adattamento culturale: L'IA adatta riferimenti e esempi culturali in modo che siano pertinenti per ogni pubblico linguistico, cosa che non posso fare manualmente perché non conosco tutte le sfumature culturali.
- Veloce e buono: Un modello veloce con un fallback automatico.
- Tono coerente: Una bassa temperatura mantiene lo stile costante tra le lingue.
Parametri di sicurezza
- Traduzione sicura per HTML: Invio solo i valori delle chiavi JSON, non tag o attributi.
- Disciplina degli slug:
translatedSlugs
contiene lo slug peren
,es
,fr
,de
,ru
,nl
,it
ezh
. - Validazione: Un comando verifica che ogni file linguistico esista e che la mappa degli slug sia completa.
- Cortesia verso l'API: Aggiungo un piccolo ritardo tra le chiamate.
Traduzioni e SEO
Molti pensano che la SEO multilingue consista solo nella traduzione dei contenuti. Questo è solo l'inizio. Ecco cosa conta davvero:
Le basi tecniche
- Tag hreflang: Questi indicano ai motori di ricerca quale pagina linguistica mostrare a quali utenti. Senza di essi, Google potrebbe mostrare la lingua sbagliata o trattare le traduzioni come contenuti duplicati.
- Struttura degli URL: Ogni lingua necessita del proprio percorso URL. Uso
/es/blog/
,/fr/blog/
, ecc. Questo aiuta i motori di ricerca a comprendere la gerarchia linguistica. - Sitemap: Includi tutte le versioni linguistiche nella tua sitemap con le annotazioni linguistiche appropriate. I motori di ricerca devono scoprire e indicizzare ogni versione.
- Tag canonical: Punta ogni versione linguistica a se stessa per evitare confusione su quale sia la versione "principale".
Oltre la traduzione: adattamento culturale
- Parole chiave in ogni lingua: Le persone cercano in modo diverso nelle diverse lingue. "Come imparare a programmare" diventa "cómo aprender programación" in spagnolo, non solo una traduzione letterale.
- Contesto culturale: Alcuni concetti non si traducono direttamente. Mi affido all'IA per adattare esempi e riferimenti in modo che siano pertinenti per ogni pubblico, poiché non conosco tutte le sfumature culturali di ogni lingua.
- Il volume di ricerca varia: Alcuni argomenti sono più popolari in determinate lingue. Do priorità alle lingue in cui i miei contenuti hanno il potenziale di maggiore diffusione.
Segnali di esperienza utente
- Rilevazione della lingua: Rileva automaticamente la lingua dell'utente dalle impostazioni del browser, ma consente di modificarla.
- Navigazione coerente: Il selettore della lingua deve funzionare su ogni pagina e mantenere il contesto dell'utente.
- Velocità di caricamento: Ogni versione linguistica dovrebbe caricarsi velocemente come l'originale. Non lasciare che la traduzione rallenti il tuo sito.
- Esperienza mobile: Il cambio lingua deve funzionare bene sui dispositivi mobili dove lo spazio sullo schermo è limitato.
Considerazioni sulla strategia dei contenuti
- Non tutto necessita di traduzione: Alcuni post potrebbero essere più pertinenti per i parlanti di inglese. Traduco selettivamente in base al pubblico potenziale.
- Sincronizzazione degli aggiornamenti: Quando aggiorno la versione inglese, tutte le traduzioni devono essere aggiornate. Le traduzioni obsolete danneggiano la SEO.
- Qualità piuttosto che quantità: È meglio avere meno traduzioni di alta qualità che molte pagine tradotte male.
- Backlink locali: I contenuti tradotti possono ottenere backlink da siti in quelle lingue, migliorando l'autorità di dominio complessiva.
L'intuizione chiave: la SEO multilingue consiste nel creare esperienze separate e ottimizzate per ogni pubblico linguistico, non solo nel tradurre le parole.
I comandi che eseguo
# Traduci un post in tutte le lingue\nnpm run translate-all translate my-new-post\n\n# Verifica che esistano tutti i file linguistici e le mappe degli slug\nnpm run translate-all validate my-new-post\n\n# Riesecuzione e sovrascrittura delle traduzioni dopo la modifica dell'inglese\nnpm run translate-all translate my-new-post --force
Questo è tutto. Uno per tradurre, uno per convalidare. Il selettore funziona perché ogni file JSON elenca lo slug per lingua.
Il sistema di traduzione completo
Il mio sistema di traduzione è costituito da due script che lavorano insieme. Lo script principale gestisce più lingue, mentre lo script worker esegue la traduzione AI. Ecco entrambi gli script:
Script principale (translate-all-languages.ts)
Questo è lo script che si esegue direttamente. Coordina la traduzione in tutte e 8 le lingue e gestisce le mappature degli slug:
Fai clic per espandere/comprimere lo script di orchestrazione
import fs from 'fs-extra';\nimport path from 'path';\nimport { execSync } from 'child_process';\n\ntype PostData = Record;\n\nconst SUPPORTED_LOCALES = ['en', 'es', 'fr', 'de', 'ru', 'nl', 'it', 'zh'] as const;\nconst SOURCE_LOCALE = 'en';\n\ninterface TranslationConfig {\n locale: string;\n scriptCommand: string;\n}\n\nconst TRANSLATION_CONFIGS: TranslationConfig[] = [\n { locale: 'es', scriptCommand: 'npm run translate-es-gemini' },\n { locale: 'fr', scriptCommand: 'npm run translate-fr-gemini' },\n { locale: 'de', scriptCommand: 'npm run translate-de-gemini' },\n { locale: 'ru', scriptCommand: 'npm run translate-ru-gemini' },\n { locale: 'nl', scriptCommand: 'npm run translate-nl-gemini' },\n { locale: 'it', scriptCommand: 'npm run translate-it-gemini' },\n { locale: 'zh', scriptCommand: 'npm run translate-zh-gemini' },\n];\n\n/**\n * Genera mappature di slug dai nomi dei file tradotti\n */\nasync function generateSlugMappings(postSlug: string): Promise> {\n const postsDir = path.join(process.cwd(), 'app/[locale]/blog/posts', postSlug);\n const slugMappings: Record = {};\n\n if (!(await fs.pathExists(postsDir))) {\n throw new Error(\`Post directory not found: ${postsDir}\`);\n }\n\n // Inizia con lo slug inglese\n slugMappings[SOURCE_LOCALE] = postSlug;\n\n // Controlla i file di traduzione esistenti per estrarre gli slug\n for (const locale of SUPPORTED_LOCALES) {\n if (locale === SOURCE_LOCALE) continue;\n\n const translationFile = path.join(postsDir, \`index.${locale}.json\`);\n if (await fs.pathExists(translationFile)) {\n try {\n const translationData: PostData = await fs.readJson(translationFile);\n const translatedSlugs = translationData.translatedSlugs;\n if (translatedSlugs && translatedSlugs[locale]) {\n slugMappings[locale] = translatedSlugs[locale];\n } else {\n console.warn(\`Warning: Missing translated slug for ${locale} in ${postSlug}\`);\n slugMappings[locale] = postSlug; // Fallback allo slug inglese\n }\n } catch (error) {\n console.warn(\`Warning: Could not read translation file for ${locale}: ${translationFile}\`);\n slugMappings[locale] = postSlug; // Fallback allo slug inglese\n }\n } else {\n console.warn(\`Warning: Translation file missing for ${locale}: ${translationFile}\`);\n slugMappings[locale] = postSlug; // Fallback allo slug inglese\n }\n }\n\n return slugMappings;\n}\n\n/**\n * Aggiorna tutti i file di traduzione con mappature di slug complete\n */\nasync function updateSlugMappings(postSlug: string, slugMappings: Record) {\n const postsDir = path.join(process.cwd(), 'app/[locale]/blog/posts', postSlug);\n\n for (const locale of SUPPORTED_LOCALES) {\n const fileName = locale === SOURCE_LOCALE ? 'index.json' : \`index.${locale}.json\`;\n const filePath = path.join(postsDir, fileName);\n\n if (await fs.pathExists(filePath)) {\n try {\n const data: PostData = await fs.readJson(filePath);\n data.translatedSlugs = slugMappings;\n await fs.writeJson(filePath, data, { spaces: 2 });\n console.log(\`✅ Aggiornamento mappature slug in ${fileName}\`);\n } catch (error) {\n console.error(\`❌ Impossibile aggiornare le mappature slug in ${fileName}:\`, error);\n }\n }\n }\n}\n\n/**\n * Traduci un singolo post in tutte le lingue\n */\nasync function translatePost(postSlug: string, force: boolean = false) {\n const postsDir = path.join(process.cwd(), 'app/[locale]/blog/posts', postSlug);\n const sourceFile = path.join(postsDir, 'index.json');\n\n // Verifica che il file sorgente esista\n if (!(await fs.pathExists(sourceFile))) {\n throw new Error(\`File sorgente non trovato: ${sourceFile}\`);\n }\n\n console.log(\`\\n🌍 Traduzione post: ${postSlug}\`);\n console.log(\`📁 Directory: ${postsDir}\`);\n\n // Traduci in ogni lingua di destinazione\n for (const config of TRANSLATION_CONFIGS) {\n const targetFile = path.join(postsDir, \`index.${config.locale}.json\`);\n const exists = await fs.pathExists(targetFile);\n\n if (!exists || force) {\n try {\n console.log(\`\\n🔄 Traduzione in ${config.locale}...\`);\n const command = \`${config.scriptCommand} -- --only ${postSlug} ${force ? '--force' : ''}\`;\n execSync(command, { stdio: 'inherit', cwd: process.cwd() });\n console.log(\`✅ Traduzione in ${config.locale} completata\`);\n \n // Breve pausa per essere cortesi con l'API\n await new Promise(resolve => setTimeout(resolve, 500));\n } catch (error) {\n console.error(\`❌ Impossibile tradurre in ${config.locale}:\`, error);\n }\n } else {\n console.log(\`⏭️ Salto ${config.locale} (già esistente, usa --force per sovrascrivere)\`);\n }\n }\n\n // Genera e aggiorna le mappature degli slug\n console.log(\`\\n🔗 Aggiornamento mappature slug...\`);\n try {\n const slugMappings = await generateSlugMappings(postSlug);\n await updateSlugMappings(postSlug, slugMappings);\n console.log(\`✅ Mappature slug aggiornate per tutte le lingue\`);\n } catch (error) {\n console.error(\`❌ Impossibile aggiornare le mappature slug:\`, error);\n }\n}\n\n/**\n * Trova tutti i post del blog\n */\nasync function findAllPosts(): Promise {\n const postsDir = path.join(process.cwd(), 'app/[locale]/blog/posts');\n \n if (!(await fs.pathExists(postsDir))) {\n throw new Error(\`Directory dei post non trovata: ${postsDir}\`);\n }\n\n const entries = await fs.readdir(postsDir, { withFileTypes: true });\n return entries\n .filter(entry => entry.isDirectory())\n .map(entry => entry.name)\n .filter(name => !name.startsWith('.'));\n}\n\n/**\n * Convalida la completezza della traduzione\n */\nasync function validateTranslations(postSlug?: string) {\n const posts = postSlug ? [postSlug] : await findAllPosts();\n let allValid = true;\n\n console.log(\`\\n🔍 Convalida delle traduzioni per ${posts.length} post(s)...\`);\n\n for (const slug of posts) {\n const postsDir = path.join(process.cwd(), 'app/[locale]/blog/posts', slug);\n console.log(\`\\n📋 Controllo: ${slug}\`);\n\n for (const locale of SUPPORTED_LOCALES) {\n const fileName = locale === SOURCE_LOCALE ? 'index.json' : \`index.${locale}.json\`;\n const filePath = path.join(postsDir, fileName);\n \n if (await fs.pathExists(filePath)) {\n try {\n const data: PostData = await fs.readJson(filePath);\n \n // Controlla se translatedSlugs è completo\n const translatedSlugs = data.translatedSlugs || {};\n const missingLanguages = SUPPORTED_LOCALES.filter(lang => !translatedSlugs[lang]);\n \n if (missingLanguages.length > 0) {\n console.log(\` ⚠️ ${locale}: Mappature slug mancanti per ${missingLanguages.join(', ')}\`);\n allValid = false;\n } else {\n console.log(\` ✅ ${locale}: Completo\`);\n }\n } catch (error) {\n console.log(\` ❌ ${locale}: JSON non valido\`);\n allValid = false;\n }\n } else {\n console.log(\` ❌ ${locale}: File mancante\`);\n allValid = false;\n }\n }\n }\n\n return allValid;\n}\n\nasync function main() {\n const args = process.argv.slice(2);\n const command = args[0];\n \n try {\n switch (command) {\n case 'translate': {\n const postSlug = args.find(arg => !arg.startsWith('--') && arg !== 'translate');\n const force = args.includes('--force');\n const all = args.includes('--all');\n\n if (all) {\n const allPosts = await findAllPosts();\n console.log(\`🚀 Traduzione di tutti i ${allPosts.length} post...\`);\n \n for (const slug of allPosts) {\n await translatePost(slug, force);\n }\n } else if (postSlug) {\n await translatePost(postSlug, force);\n } else {\n console.error('❌ Specificare uno slug del post o utilizzare --all');\n process.exit(1);\n }\n break;\n }\n\n case 'validate': {\n const postSlug = args.find(arg => !arg.startsWith('--') && arg !== 'validate');\n const isValid = await validateTranslations(postSlug);\n \n if (isValid) {\n console.log('\\n✅ Tutte le traduzioni sono complete e valide!');\n } else {\n console.log('\\n⚠️ Alcune traduzioni sono incomplete o non valide.');\n process.exit(1);\n }\n break;\n }\n\n case 'update-slugs': {\n const postSlug = args.find(arg => !arg.startsWith('--') && arg !== 'update-slugs');\n \n if (postSlug) {\n const slugMappings = await generateSlugMappings(postSlug);\n await updateSlugMappings(postSlug, slugMappings);\n console.log(\`✅ Mappature slug aggiornate per ${postSlug}\`);\n } else {\n console.error('❌ Specificare uno slug del post');\n process.exit(1);\n }\n break;\n }\n\n default:\n console.log(\`\n🌍 Gestore di traduzioni del blog\n\nUtilizzo:\n npm run translate-all translate <post-slug> [--force] # Traduci post specifico\n npm run translate-all translate --all [--force] # Traduci tutti i post\n npm run translate-all validate [post-slug] # Convalida traduzioni\n npm run translate-all update-slugs <post-slug> # Aggiorna mappature slug\n\nEsempi:\n npm run translate-all translate how-to-read-nginx-access-logs\n npm run translate-all translate --all --force\n npm run translate-all validate\n npm run translate-all update-slugs how-to-read-nginx-access-logs\n \`);\n break;\n }\n } catch (error) {\n console.error('❌ Errore:', error);\n process.exit(1);\n }\n}\n\nmain().catch(console.error);
Come funziona lo script
Lo script fa diverse cose:
- Orchestrazione della traduzione: Chiama gli script di traduzione delle singole lingue per ogni lingua di destinazione
- Gestione degli slug: Genera e aggiorna la mappatura
translatedSlugs
in ogni file - Validazione: Verifica che tutti i file linguistici esistano e abbiano mappature di slug complete
- Gestione degli errori: Gestisce correttamente gli errori e continua con le altre lingue
L'intuizione chiave è che ogni file linguistico deve conoscere lo slug di ogni altra lingua. Questo permette al selettore della lingua di funzionare correttamente.
Configurazione dell'API AI
Ecco come configuro le chiamate all'API Gemini per una traduzione affidabile:
Limiti e parametri dei token
- maxOutputTokens: 8192: Questo è fondamentale per i contenuti di grandi dimensioni. Il limite predefinito è molto inferiore, il che causava risposte troncate e errori di analisi. Con 8192 token, posso tradurre fino a ~6.000 parole (inglese), che coprono la maggior parte dei post e degli articoli del blog.
- temperature: 0.2: Una bassa temperatura mantiene le traduzioni coerenti e prevedibili tra le lingue.
- topP: 0.95: Controlla la diversità mantenendo la qualità.
- topK: 40: Limita le scelte di vocabolario per traduzioni più mirate.
Conversione token-parola
- Input inglese: ~0,75 parole per token, quindi 8192 token ≈ 6.000-6.500 parole
- Espansione della traduzione: L'output è solitamente più lungo del 10-30% rispetto all'input (il tedesco può essere più lungo del 20-30%, lo spagnolo del 10-20%)
- Markup HTML: Occupa token ma non si traduce, quindi il contenuto effettivamente traducibile è inferiore
- Esempio reale: Questo post del blog (~2.400 parole) utilizza ~3.000 token di input e ~4.000-5.000 token di output
Strategia di gestione dei contenuti
- Campi piccoli per primi: Traduci titolo, estratto e readTime in una singola chiamata API (veloce e affidabile).
- Traduzione completa del contenuto: Per contenuti superiori a 2.000 caratteri, traduci l'intero contenuto in una singola chiamata con limiti di token elevati.
- Fallback a frammentazione: Se la traduzione completa fallisce, torna automaticamente a suddividere il contenuto in blocchi di circa 1.500 caratteri.
- Analisi JSON intelligente: Gestisci le risposte racchiuse in blocchi di codice markdown (```json) che l'IA a volte restituisce.
Gestione degli errori
- Fallback del modello: Se il modello principale (gemini-2.5-flash-lite) non è disponibile, passa automaticamente a gemini-1.5-flash-latest.
- Degradazione graduale: Se un blocco fallisce, usa il contenuto inglese originale per quella sezione e continua.
- Cortesia dell'API: Aggiungi dei ritardi tra le chiamate per rispettare i limiti di velocità.
La lezione chiave: imposta sempre
maxOutputTokens
esplicitamente. I limiti predefiniti sono troppo bassi per i contenuti del mondo reale, causando misteriosi errori di analisi che sembrano problemi dell'API ma sono in realtà problemi di limite di token.
Consigli se provi questo
- Scrivi in modo semplice. Le frasi brevi si traducono bene.
- Delimita i blocchi di codice e includi il codice inline in
<code>
. - Evita i giochi di parole se non rivedrai il testo di destinazione.
- Usa HTML semantico. Titoli e elenchi aiutano i lettori e i modelli.
- Mantieni i tuoi contenuti sorgente puliti e ben strutturati.
FAQ
Modifico le traduzioni?
Solo per lo spagnolo, poiché è l'unica lingua che conosco oltre l'inglese. Per titoli e introduzioni, voglio un punto di partenza veloce, poi rifinisco dove conta. Per le altre lingue, mi affido alla qualità della traduzione AI.
Privacy?
Il contenuto va all'API Gemini. Non invio segreti. Le chiavi API si trovano in
.env.local
(
GOOGLE_API_KEY
o
GEMINI_API_KEY
).
Modelli?
Uso un modello Gemini veloce e torno indietro automaticamente se non è disponibile. Puoi sovrascrivere con
GEMINI_MODEL
.
E gli URL vecchi?
Mantengo gli URL vecchi funzionanti con i reindirizzamenti. Il sistema di mappatura degli slug gestisce questo automaticamente.
Conclusione
L'IA non sostituisce la tua voce. Elimina il lavoro ripetitivo. Mantieni la tua fonte pulita, automatizza le parti ripetitive, convalida e spedisci. Lo script che ho condiviso gestisce la complessità in modo da poterti concentrare sulla scrittura.