From 20e701b754e615c787a8dae9441b25ce1f57d1db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20FAMIBELLE-PRONZOLA?= Date: Mon, 15 Jun 2026 20:19:30 +0400 Subject: [PATCH] feat: add bulkTranslate --- src/api/parole/controllers/parole.js | 5 ++ src/api/parole/services/parole.js | 73 ++++++++++++++++++++++++++++ 2 files changed, 78 insertions(+) diff --git a/src/api/parole/controllers/parole.js b/src/api/parole/controllers/parole.js index cdf097d..8bdfb08 100644 --- a/src/api/parole/controllers/parole.js +++ b/src/api/parole/controllers/parole.js @@ -39,6 +39,11 @@ module.exports = createCoreController('api::parole.parole', ({strapi}) => ({ ctx.body = lines.join('\n') }, + async bulkTranslate(ctx) { + const result = await strapi.service('api::parole.parole').bulkTranslateMissing() + return ctx.send(result) + }, + async findOne(documentId) { const parole = await strapi.documents('api::parole.parole').findOne({ documentId, diff --git a/src/api/parole/services/parole.js b/src/api/parole/services/parole.js index 0c3bc3d..eabab3d 100644 --- a/src/api/parole/services/parole.js +++ b/src/api/parole/services/parole.js @@ -42,6 +42,8 @@ function suspectFrench(text) { return frCount / words.length > 0.04 } +const sleep = ms => new Promise(resolve => setTimeout(resolve, ms)) + class Translator { constructor() { this.deeplApi = process.env.DEEPL_URL || 'api-free.deepl.com' @@ -196,6 +198,77 @@ module.exports = createCoreService('api::parole.parole', ({strapi}) => ({ return { metadata, pairs } }, + async bulkTranslateMissing() { + const TARGET_LANGS = [ + { lang: 'en', field: 'anglais', deeplTarget: 'EN', suffix: '\n\n(Translated by DeepL)' }, + { lang: 'es', field: 'espagnol', deeplTarget: 'ES', suffix: '\n\n(Traducido por DeepL)' }, + { lang: 'de', field: 'allemand', deeplTarget: 'DE', suffix: '\n\n(Übersetzt von DeepL)' }, + { lang: 'it', field: 'italien', deeplTarget: 'IT', suffix: '\n\n(Tradotto da DeepL)' }, + ] + + const pageSize = 100 + let start = 0 + const all = [] + while (true) { + const batch = await strapi.documents('api::parole.parole').findMany({ + status: 'published', + populate: ['traductions'], + fields: ['documentId', 'slug', 'titre', 'transcription', 'langueSource'], + limit: pageSize, + start, + }) + all.push(...batch) + if (batch.length < pageSize) break + start += pageSize + } + + const translator = new Translator() + const translated = [] + const skipped = [] + const errors = [] + + for (const parole of all) { + const sourceFR = parole.traductions?.francais + || (parole.langueSource === 'fr' ? parole.transcription : null) + + if (!sourceFR) { skipped.push(parole.slug); continue } + + const missing = TARGET_LANGS.filter(({ field }) => !parole.traductions?.[field]) + if (missing.length === 0) { skipped.push(parole.slug); continue } + + const { id: _id, ...tradData } = parole.traductions || {} + const updatedTrad = { ...tradData } + const addedLangs = [] + + for (const { lang, field, deeplTarget, suffix } of missing) { + try { + await sleep(700) + const result = await translator.get('FR', deeplTarget, sourceFR) + const text = result?.translations?.[0]?.text + if (text) { + updatedTrad[field] = text + suffix + addedLangs.push(lang) + } + } catch (err) { + errors.push({ slug: parole.slug, lang: deeplTarget, error: err.message }) + } + } + + if (addedLangs.length > 0) { + await strapi.documents('api::parole.parole').update({ + documentId: parole.documentId, + data: { traductions: updatedTrad }, + }) + await strapi.documents('api::parole.parole').publish({ + documentId: parole.documentId, + }) + translated.push({ slug: parole.slug, langs: addedLangs }) + } + } + + return { translated, skipped, errors } + }, + parolesDiff(titre = '', oldString, newString) { const patch = Diff.createPatch(titre, oldString, newString, 'supprimée', 'ajoutée') const parsePatch = Diff.parsePatch(patch)