diff --git a/src/admin/app.js b/src/admin/app.js index 69d3340..950041d 100644 --- a/src/admin/app.js +++ b/src/admin/app.js @@ -15,14 +15,14 @@ export default { fr: { 'Auth.form.welcome.subtitle': `Connectez-vous à votre compte ${process.env.STRAPI_ADMIN_SITE_NAME || 'OKI'} API`, 'Auth.form.welcome.title': `Bienvenue sur ${process.env.STRAPI_ADMIN_SITE_NAME || 'OKI'} API !`, - 'LeftMenu.navbrand.title': 'Tableau de bord', - 'LeftMenu.navbrand.workplace': 'Menu', + 'LeftMenu.navbrand.title': 'Tableau de bord', + 'LeftMenu.navbrand.workplace': 'Menu', }, en: { - 'Auth.form.welcome.subtitle': `Log in to your ${process.env.STRAPI_ADMIN_SITE_NAME || 'OKI'} API account`, - 'Auth.form.welcome.title': `Welcome to ${process.env.STRAPI_ADMIN_SITE_NAME || 'OKI'} API !`, - 'LeftMenu.navbrand.title': 'Dashboard', - 'LeftMenu.navbrand.workplace': 'Workplace', + 'Auth.form.welcome.subtitle': `Log in to your ${process.env.STRAPI_ADMIN_SITE_NAME || 'OKI'} API account`, + 'Auth.form.welcome.title': `Welcome to ${process.env.STRAPI_ADMIN_SITE_NAME || 'OKI'} API !`, + 'LeftMenu.navbrand.title': 'Dashboard', + 'LeftMenu.navbrand.workplace': 'Workplace', } }, menu: { @@ -30,9 +30,9 @@ export default { }, theme: { colors: { - buttonNeutral0: '#ffffff', - buttonPrimary500: '#4caf50', - buttonPrimary600: '#43a047', + buttonNeutral0: '#ffffff', + buttonPrimary500: '#4caf50', + buttonPrimary600: '#43a047', primary100: '#c8e6c9', primary200: '#a5d6a7', primary500: '#4caf50', @@ -54,18 +54,18 @@ export default { succes600: '#7cb342', succes700: '#689f38', danger700: '#d32f2f', - neutral0: '#ffffff', - neutral100: '#f6f6f9', - neutral1000: '#181826', - neutral150: '#eaeaef', - neutral200: '#dcdce4', - neutral300: '#c0c0cf', - neutral400: '#a5a5ba', - neutral500: '#8e8ea9', - neutral600: '#666687', - neutral700: '#4a4a6a', - neutral800: '#32324d', - neutral900: '#212134' + neutral0: '#ffffff', + neutral100: '#f6f6f9', + neutral1000: '#181826', + neutral150: '#eaeaef', + neutral200: '#dcdce4', + neutral300: '#c0c0cf', + neutral400: '#a5a5ba', + neutral500: '#8e8ea9', + neutral600: '#666687', + neutral700: '#4a4a6a', + neutral800: '#32324d', + neutral900: '#212134' }, }, tutorials: false, diff --git a/src/api/commentaire/content-types/commentaire/lifecycles.js b/src/api/commentaire/content-types/commentaire/lifecycles.js index eb2a599..2ee4b10 100644 --- a/src/api/commentaire/content-types/commentaire/lifecycles.js +++ b/src/api/commentaire/content-types/commentaire/lifecycles.js @@ -1,57 +1,57 @@ 'use strict'; -const { ApplicationError, NotFoundError } = require("@strapi/utils").errors +const { ApplicationError, NotFoundError } = require('@strapi/utils').errors; const jwennUserEpiId = async userId => { if (!userId) { - return null + return null; } const user = await strapi.db.query('plugin::users-permissions.user').findOne({ where: {id: userId} - }) + }); - return user -} + return user; +}; const jwennParoleEpiId = async paroleId => { if (!paroleId) { - return null + return null; } const parole = await strapi.db.query('api::parole.parole').findOne({ where: {id: paroleId} - }) + }); - return parole -} + return parole; +}; const validateCommentaire = data => { if (!data.contenu && !data.datePublication) { - throw new ApplicationError('Mauvaise requête, contenu et datePublication sont obligatoires') + throw new ApplicationError('Mauvaise requête, contenu et datePublication sont obligatoires'); } if (!data.contenu || data.contenu.trim().length === 0) { - throw new ApplicationError('Champ obligatoire. Veuillez renseigner le contenu du commentaire.') + throw new ApplicationError('Champ obligatoire. Veuillez renseigner le contenu du commentaire.'); } if (data.contenu.trim().length > 500) { - throw new ApplicationError('Le commentaire doit contenir 500 caractères maximum.') + throw new ApplicationError('Le commentaire doit contenir 500 caractères maximum.'); } -} +}; module.exports = { beforeCreate: async event => { - const {data} = event.params - validateCommentaire(data) + const {data} = event.params; + validateCommentaire(data); }, afterCreate: async event => { - const {data, result} = event.params - const user = await jwennUserEpiId(data.user) - const parole = await jwennParoleEpiId(data.parole) + const {data} = event.params; + const user = await jwennUserEpiId(data.user); + const parole = await jwennParoleEpiId(data.parole); if (!parole) { - throw new NotFoundError('Texte introuvable.') + throw new NotFoundError('Texte introuvable.'); } if (user) { @@ -60,7 +60,7 @@ module.exports = { to: process.env.SMTP_SEND_TO, subject: `Commentaire de ${user.username} sur "${parole.titre}"`, text: data.contenu - }) + }); } } }; diff --git a/src/api/parole/services/parole.js b/src/api/parole/services/parole.js index 536b9e4..d329e32 100644 --- a/src/api/parole/services/parole.js +++ b/src/api/parole/services/parole.js @@ -1,9 +1,8 @@ 'use strict'; -const qs = require('qs') -const Diff = require('diff') +const Diff = require('diff'); const { createCoreService } = require('@strapi/strapi').factories; -const { ApplicationError } = require("@strapi/utils").errors +const { ApplicationError } = require('@strapi/utils').errors; const LANG_MAP = { fr: { field: 'francais', targetLang: 'fr', userPrompt: 'Tradui an fransé' }, @@ -14,43 +13,43 @@ const LANG_MAP = { pt: { field: 'portugais', targetLang: 'pt', userPrompt: 'Traduza para o português', deeplTarget: 'PT-BR', suffix: '\n\n (Traduzido pela DeepL)' }, ja: { field: 'japonais', targetLang: 'ja', userPrompt: '日本語に翻訳して', deeplTarget: 'JA', suffix: '\n\n (DeepLによる翻訳)' }, ko: { field: 'coreen', targetLang: 'ko', userPrompt: '한국어로 번역해줘', deeplTarget: 'KO', suffix: '\n\n (DeepL 번역)' }, -} +}; -const ALL_LANGS = Object.keys(LANG_MAP) +const ALL_LANGS = Object.keys(LANG_MAP); function stripMarkdown(text) { - if (!text) return '' + if (!text) return ''; return text .replace(/#{1,6}\s+/g, '') .replace(/\*\*(.*?)\*\*/gs, '$1') .replace(/\*(.*?)\*/gs, '$1') .replace(/__(.*?)__/gs, '$1') .replace(/_(.*?)_/gs, '$1') - .replace(/\[([^\]]+)\]\([^\)]+\)/g, '$1') - .replace(/^[>\-\*\+]\s+/gm, '') + .replace(/\[([^\]]+)\]\([^)]+\)/g, '$1') + .replace(/^[>\-*+]\s+/gm, '') .replace(/\n{3,}/g, '\n\n') - .trim() + .trim(); } // Détecte si une transcription est probablement en français plutôt qu'en KA. // Heuristique : si les pronoms personnels français représentent > 4 % des mots. -const FR_PRONOUNS = new Set(['je', 'tu', 'il', 'elle', 'nous', 'vous', 'ils', 'elles']) +const FR_PRONOUNS = new Set(['je', 'tu', 'il', 'elle', 'nous', 'vous', 'ils', 'elles']); function suspectFrench(text) { - if (!text) return false - const words = text.toLowerCase().match(/\b[a-zàâäéèêëîïôöùûüç]+\b/g) || [] - if (words.length < 10) return false - const frCount = words.filter(w => FR_PRONOUNS.has(w)).length - return frCount / words.length > 0.04 + if (!text) return false; + const words = text.toLowerCase().match(/\b[a-zàâäéèêëîïôöùûüç]+\b/g) || []; + if (words.length < 10) return false; + const frCount = words.filter(w => FR_PRONOUNS.has(w)).length; + return frCount / words.length > 0.04; } -const sleep = ms => new Promise(resolve => setTimeout(resolve, ms)) +const sleep = ms => new Promise(resolve => setTimeout(resolve, ms)); class Translator { constructor() { - this.deeplApi = process.env.DEEPL_URL || 'api-free.deepl.com' - this.deeplKey = process.env.DEEPL_KEY - this.urlRequest = `https://${this.deeplApi}/v2/translate` + this.deeplApi = process.env.DEEPL_URL || 'api-free.deepl.com'; + this.deeplKey = process.env.DEEPL_KEY; + this.urlRequest = `https://${this.deeplApi}/v2/translate`; } async get(origin, target, text) { @@ -66,40 +65,40 @@ class Translator { target_lang: target, }), signal: AbortSignal.timeout(15_000) - }) + }); if (!response.ok) { - const body = await response.text() - console.error('DeepL error:', body) - throw new Error(`DeepL ${response.status}: ${body}`) + const body = await response.text(); + console.error('DeepL error:', body); + throw new Error(`DeepL ${response.status}: ${body}`); } - return await response.json() + return await response.json(); } } -const translator = new Translator() +const translator = new Translator(); module.exports = createCoreService('api::parole.parole', ({strapi}) => ({ async translate(origin, target, text) { - const data = await translator.get(origin, target, text) - return data.translations[0].text + const data = await translator.get(origin, target, text); + return data.translations[0].text; }, async translateLyrics(parolesFR) { - const result = { francais: parolesFR } + const result = { francais: parolesFR }; for (const lang of ALL_LANGS) { - if (lang === 'fr') continue - const { field, deeplTarget, suffix } = LANG_MAP[lang] + if (lang === 'fr') continue; + const { field, deeplTarget, suffix } = LANG_MAP[lang]; try { - const translated = await this.translate('FR', deeplTarget, parolesFR) - result[field] = translated + suffix + const translated = await this.translate('FR', deeplTarget, parolesFR); + result[field] = translated + suffix; } catch (err) { - strapi.log.error(`DeepL (${deeplTarget}): ${err.message}`) + strapi.log.error(`DeepL (${deeplTarget}): ${err.message}`); } } - return result + return result; }, validateParoles(titre, transcription) { if (!titre || titre.trim().length === 0) { @@ -107,77 +106,77 @@ module.exports = createCoreService('api::parole.parole', ({strapi}) => ({ } if (!transcription || transcription.trim().length === 0) { - throw new ApplicationError('Champ obligatoire. Veuillez renseigner la transcription.') + throw new ApplicationError('Champ obligatoire. Veuillez renseigner la transcription.'); } if (transcription.trim().length < 10) { - throw new ApplicationError('La transcription doit contenir au moins 10 caractères.') + throw new ApplicationError('La transcription doit contenir au moins 10 caractères.'); } }, async fetchAllParoles() { - const pageSize = 100 - let start = 0 - const all = [] + const pageSize = 100; + let start = 0; + const all = []; - while (true) { + for (;;) { const batch = await strapi.documents('api::parole.parole').findMany({ status: 'published', populate: ['artistes', 'traductions'], fields: ['documentId', 'titre', 'slug', 'transcription', 'annee', 'langueSource'], limit: pageSize, start, - }) - all.push(...batch) - if (batch.length < pageSize) break - start += pageSize + }); + all.push(...batch); + if (batch.length < pageSize) break; + start += pageSize; } - return all + return all; }, buildExport(paroles, type, langs) { - const targetLangs = langs && langs.length ? langs : ALL_LANGS - const pairs = [] - const missing = [] - const nonKa = [] - const langCounts = {} + const targetLangs = langs && langs.length ? langs : ALL_LANGS; + const pairs = []; + const missing = []; + const nonKa = []; + const langCounts = {}; for (const parole of paroles) { - const source = stripMarkdown(parole.transcription) - const sourceLang = parole.langueSource || 'ka' - const artists = (parole.artistes || []).map(a => a.alias) - const paroleMeta = { title: parole.titre, artists } + const source = stripMarkdown(parole.transcription); + const sourceLang = parole.langueSource || 'ka'; + const artists = (parole.artistes || []).map(a => a.alias); + const paroleMeta = { title: parole.titre, artists }; if (sourceLang !== 'ka') { - nonKa.push({ documentId: parole.documentId, slug: parole.slug, ...paroleMeta, suspected_lang: sourceLang }) + nonKa.push({ documentId: parole.documentId, slug: parole.slug, ...paroleMeta, suspected_lang: sourceLang }); } else if (suspectFrench(source)) { - nonKa.push({ documentId: parole.documentId, slug: parole.slug, ...paroleMeta, suspected_lang: 'fr' }) + nonKa.push({ documentId: parole.documentId, slug: parole.slug, ...paroleMeta, suspected_lang: 'fr' }); } - const missingLangs = ALL_LANGS.filter(lang => !parole.traductions?.[LANG_MAP[lang].field]) + const missingLangs = ALL_LANGS.filter(lang => !parole.traductions?.[LANG_MAP[lang].field]); if (missingLangs.length > 0) { - missing.push({ documentId: parole.documentId, slug: parole.slug, ...paroleMeta, missing: missingLangs }) + missing.push({ documentId: parole.documentId, slug: parole.slug, ...paroleMeta, missing: missingLangs }); } for (const lang of targetLangs) { - const { field, targetLang, userPrompt } = LANG_MAP[lang] - if (lang === sourceLang) continue - const target = stripMarkdown(parole.traductions?.[field]) - if (!target) continue + const { field, targetLang, userPrompt } = LANG_MAP[lang]; + if (lang === sourceLang) continue; + const target = stripMarkdown(parole.traductions?.[field]); + if (!target) continue; - langCounts[lang] = (langCounts[lang] || 0) + 1 + langCounts[lang] = (langCounts[lang] || 0) + 1; if (type === 'instruct') { const systemPrompt = sourceLang === 'ka' ? 'Tu es un expert en langue KA (créole guadeloupéen/martiniquais). Traduis le texte KA suivant.' - : `Tu es un expert en traduction. Traduis le texte suivant (langue source : ${sourceLang}).` + : `Tu es un expert en traduction. Traduis le texte suivant (langue source : ${sourceLang}).`; pairs.push({ messages: [ { role: 'system', content: systemPrompt }, { role: 'user', content: `${userPrompt} :\n\n${source}` }, { role: 'assistant', content: target }, ], - }) + }); } else { pairs.push({ source_lang: sourceLang, @@ -185,7 +184,7 @@ module.exports = createCoreService('api::parole.parole', ({strapi}) => ({ source, target, ...paroleMeta, - }) + }); } } } @@ -197,60 +196,61 @@ module.exports = createCoreService('api::parole.parole', ({strapi}) => ({ languages: langCounts, missing_translations: missing, non_ka_transcriptions: nonKa, - } + }; - return { metadata, pairs } + return { metadata, pairs }; }, async bulkTranslateMissing() { const TARGET_LANGS = ALL_LANGS .filter(lang => lang !== 'fr') - .map(lang => ({ lang, ...LANG_MAP[lang] })) + .map(lang => ({ lang, ...LANG_MAP[lang] })); - const pageSize = 100 - let start = 0 - const all = [] - while (true) { + const pageSize = 100; + let start = 0; + const all = []; + for (;;) { 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 + }); + all.push(...batch); + if (batch.length < pageSize) break; + start += pageSize; } - const translated = [] - const skipped = [] - const errors = [] + const translated = []; + const skipped = []; + const errors = []; for (const parole of all) { const sourceFR = parole.traductions?.francais - || (parole.langueSource === 'fr' ? parole.transcription : null) + || (parole.langueSource === 'fr' ? parole.transcription : null); - if (!sourceFR) { skipped.push(parole.slug); continue } + 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 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 = [] + // eslint-disable-next-line no-unused-vars -- id exclu intentionnellement du spread + 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 + 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) + updatedTrad[field] = text + suffix; + addedLangs.push(lang); } } catch (err) { - errors.push({ slug: parole.slug, lang: deeplTarget, error: err.message }) + errors.push({ slug: parole.slug, lang: deeplTarget, error: err.message }); } } @@ -258,28 +258,28 @@ module.exports = createCoreService('api::parole.parole', ({strapi}) => ({ 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 }) + }); + translated.push({ slug: parole.slug, langs: addedLangs }); } } - return { translated, skipped, errors } + return { translated, skipped, errors }; }, parolesDiff(titre = '', oldString, newString) { - const patch = Diff.createPatch(titre, oldString, newString, 'supprimée', 'ajoutée') - const parsePatch = Diff.parsePatch(patch) + const patch = Diff.createPatch(titre, oldString, newString, 'supprimée', 'ajoutée'); + const parsePatch = Diff.parsePatch(patch); if (parsePatch[0].hunks.length > 0) { - const jsonDiff = Diff.diffWords(oldString, newString) + const jsonDiff = Diff.diffWords(oldString, newString); return { patch, jsonDiff - } + }; } } })); diff --git a/src/api/stats/controllers/stats.js b/src/api/stats/controllers/stats.js index 3a7946e..aaa7731 100644 --- a/src/api/stats/controllers/stats.js +++ b/src/api/stats/controllers/stats.js @@ -1,15 +1,15 @@ 'use strict'; module.exports = { - async count(ctx, next) { + async count() { const countArtiste = await strapi.documents('api::artiste.artiste').count({ publicationState: 'live' - }) + }); const countParole = await strapi.documents('api::parole.parole').count({ publicationState: 'live' - }) + }); - return {countArtiste, countParole} + return {countArtiste, countParole}; } -} +};