{"_id":"@astratra/i18n-server","_rev":"3-6e63f3119d188bf3264a710657ee9f72","name":"@astratra/i18n-server","dist-tags":{"latest":"1.0.0"},"versions":{"0.1.0":{"name":"@astratra/i18n-server","version":"0.1.0","keywords":["astratra","i18n","localization","error-messages"],"license":"MIT","_id":"@astratra/i18n-server@0.1.0","maintainers":[{"name":"emch99","email":"emchkongo@gmail.com"}],"dist":{"shasum":"b71896206045ac2927a49628349b1f8f25edbc88","tarball":"https://registry.npmjs.org/@astratra/i18n-server/-/i18n-server-0.1.0.tgz","fileCount":9,"integrity":"sha512-vxNHmnHUkJC5DISyJtvgceHrpkiINNpN1ec91hlEya0yNWonoOSOpKJXcpv47sYNLii+dKIscspEiGvU6ry9yA==","signatures":[{"sig":"MEQCIC37UniNjzMJLjREjHGMlVd5ucL1xDW/B76OVoKPK4VPAiABHUhWX0HR9mv1qWMNtKufddd5AuSFT3R6P4Cg8J5cvA==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":22757},"jest":{"testMatch":["**/__tests__/**/*.test.js"]},"main":"src/index.js","type":"commonjs","types":"src/index.d.ts","engines":{"node":">=20"},"gitHead":"a3f7c737294561ba8651e14d507a18b41e199999","scripts":{"test":"jest"},"_npmUser":{"name":"emch99","email":"emchkongo@gmail.com"},"_npmVersion":"11.13.0","description":"Server-side message translation and a readability audit for the sentences your API returns.","directories":{},"_nodeVersion":"24.16.0","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"jest":"30.4.2","express":"^4.18.3","supertest":"^7.0.0"},"_npmOperationalInternal":{"tmp":"tmp/i18n-server_0.1.0_1787666112271_0.18340008007644215","host":"s3://npm-registry-packages-npm-production"}},"0.2.0":{"name":"@astratra/i18n-server","version":"0.2.0","keywords":["astratra","i18n","localization","error-messages"],"license":"MIT","_id":"@astratra/i18n-server@0.2.0","maintainers":[{"name":"emch99","email":"emchkongo@gmail.com"}],"dist":{"shasum":"b6a3607288dc4059f307136bb4b003a4a8cf5f3f","tarball":"https://registry.npmjs.org/@astratra/i18n-server/-/i18n-server-0.2.0.tgz","fileCount":12,"integrity":"sha512-Z+lQ2ALCKB5oLyiWK2regobPlhWVM6JX8e5+hDIgPDgSPPHBn1ZFLjOWs0kQXIdnCjJUib3C+rpTzjAu0Mdugw==","signatures":[{"sig":"MEYCIQD+0RglnGOUVnTQOBZ/bTaSRIssqmERbpgt6LLD3NQWawIhAKMp45tM7gcePgxCZh2eY4t+pBqdQHyfmwr6OpctoeZv","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"},{"sig":"MEUCIQDu6aM81koexrTklB8CPVDeM5RmDxeZn+Znjlhq/PO23gIgOVrK9Ih03Ucck8afTBabaFhyLpZIajo99/R4iDuGsas=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":59486},"jest":{"testMatch":["**/__tests__/**/*.test.js"]},"main":"src/index.js","type":"commonjs","types":"src/index.d.ts","engines":{"node":">=20"},"gitHead":"a6eda5bf009d1d47caff00955940c6d2303bee2c","scripts":{"test":"jest"},"_npmUser":{"name":"emch99","email":"emchkongo@gmail.com"},"_npmVersion":"11.13.0","description":"Server-side message translation and a readability audit for the sentences your API returns.","directories":{},"_nodeVersion":"24.16.0","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"jest":"30.4.2","express":"^4.18.3","supertest":"^7.0.0"},"_npmOperationalInternal":{"tmp":"tmp/i18n-server_0.2.0_1789991468195_0.5276297560578178","host":"s3://npm-registry-packages-npm-production"}},"1.0.0":{"_id":"@astratra/i18n-server@1.0.0","dist":{"shasum":"f20451032651dfe2f9d86d8af886da29ccb8ceba","tarball":"https://registry.npmjs.org/@astratra/i18n-server/-/i18n-server-1.0.0.tgz","fileCount":12,"integrity":"sha512-rdl5v2xNRbE7ufkQhrQztKEYSKUyLTaZrEZJFUn+s9rctVj5Jf8zTX1vcZykrd0oGGBtfqaMDuwGKVO6+HNMww==","signatures":[{"sig":"MEYCIQDzvwBMhm7j6+yBnYOYvXMDtL+az2bZolq1YyMK4DHtEgIhAPTq+dgtuTTAx5P+/PNrCEpZ4mSfLUpjQ7TsA1PtLRqw","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"},{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCICfYsQoHrg589Ag6vgqlwlDPj3a78fWwBKLlQrTWNBL6AiEA9g/dHA/020zjCuLKOhHYSH42hlYJvcRI2TmM6vVwqiU="}],"unpackedSize":59486},"jest":{"testMatch":["**/__tests__/**/*.test.js"]},"main":"src/index.js","name":"@astratra/i18n-server","type":"commonjs","types":"src/index.d.ts","engines":{"node":">=20"},"gitHead":"28e680aaed8dfdf7c5aa00c7dfbde74ab8e377de","license":"MIT","scripts":{"test":"jest"},"version":"1.0.0","_npmUser":{"name":"emch99","email":"emchkongo@gmail.com"},"keywords":["astratra","i18n","localization","error-messages"],"_npmVersion":"11.13.0","description":"Server-side message translation and a readability audit for the sentences your API returns.","directories":{},"maintainers":[{"name":"emch99","email":"emchkongo@gmail.com"}],"_nodeVersion":"24.16.0","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"jest":"30.4.2","express":"^4.18.3","supertest":"^7.0.0"},"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/i18n-server_1.0.0_1790882283196_0.87591658172981"}}},"time":{"created":"2026-08-25T13:55:12.065Z","modified":"2026-10-01T19:18:03.538Z","0.1.0":"2026-08-25T13:55:12.410Z","0.2.0":"2026-09-21T11:51:08.297Z","1.0.0":"2026-10-01T19:18:03.287Z"},"license":"MIT","keywords":["astratra","i18n","localization","error-messages"],"description":"Server-side message translation and a readability audit for the sentences your API returns.","maintainers":[{"name":"emch99","email":"emchkongo@gmail.com"}],"readme":"# @astratra/i18n-server\n\nLes textes de l'interface sont traduits côté client. Les messages d'erreur, non :\nils viennent de l'API et s'affichent tels quels. Une application en anglais\nrépond donc « Cet élève est introuvable. » à un parent anglophone.\n\nC'est le genre de trou que personne ne remarque, jusqu'à ce que quelqu'un qui ne\nlit pas ta langue tombe sur une erreur.\n\nCe package traduit ce que le serveur renvoie, et fournit l'audit qui empêche ces\nphrases de redevenir illisibles.\n\n## L'astuce qui rend l'adoption gratuite\n\n**La clé, c'est la phrase source elle-même.** Aucun identifiant à inventer,\naucun appel à modifier, et une phrase absente du catalogue revient dans la\nlangue d'origine — c'est-à-dire exactement le comportement actuel.\n\nTu peux donc brancher ça sur un produit en production et remplir le catalogue\nensuite, sans aucune régression entre les deux.\n\n## Mise en place\n\n```js\nconst {\n  createMessageCatalog,\n  createLanguageResolver,\n  createTranslationMiddleware\n} = require('@astratra/i18n-server');\n\nconst catalog = createMessageCatalog({\n  languages: ['fr', 'en', 'es'],\n  defaultLanguage: 'fr',\n  messages: {\n    'Cet élève est introuvable.': {\n      en: 'This student could not be found.',\n      es: 'No se encuentra a este alumno.',\n    },\n    'Connectez-vous pour continuer.': {\n      en: 'Sign in to continue.',\n      // l'espagnol viendra plus tard — en attendant, le français s'affiche\n    },\n  },\n});\n\nconst resolver = createLanguageResolver({\n  languages: ['fr', 'en', 'es'],\n  // Une préférence enregistrée l'emporte sur celle du navigateur.\n  read: (req) => req.user?.language,\n});\n\napp.use(createTranslationMiddleware({ catalog, resolver }));\n```\n\nEt c'est tout. Tes contrôleurs continuent d'écrire leurs phrases comme avant ;\nla traduction se fait une fois, à la sortie, en enveloppant `res.json`.\n\n## Ce que le middleware ne touche pas\n\n**Jamais les données.** Seuls les champs que tu nommes — `message` par défaut —\nsont traduits. Traduire une valeur de `data` la corromprait : un nom d'élève\nn'a pas à passer par un catalogue.\n\n```js\ncreateTranslationMiddleware({ catalog, resolver, fields: ['message', 'title'] });\n```\n\nLa langue résolue est posée sur la requête (`req.language`) pour les cas où un\ncontrôleur en a besoin.\n\n## La langue demandée\n\n`Accept-Language` est déjà envoyé par tous les navigateurs, donc aucun client\nn'a à changer. Une langue que tu ne sers pas retombe sur la langue source :\nmieux vaut une phrase compréhensible dans une autre langue qu'une clé technique.\n\nLa première langue **reconnue** l'emporte, même précédée d'une inconnue —\n`de,es;q=0.8` donne `es`, pas la valeur par défaut.\n\n## Savoir où en est le catalogue\n\n```js\ncatalog.coverage();\n// { fr: { translated: 2, total: 2, missing: [] },\n//   en: { translated: 2, total: 2, missing: [] },\n//   es: { translated: 1, total: 2, missing: ['Connectez-vous pour continuer.'] } }\n```\n\nUn catalogue que personne ne mesure est un catalogue qu'on arrête de remplir.\n`missing` te donne la liste de travail.\n\n## L'audit — la partie qui vaut le plus\n\nLes messages d'erreur sont lus par des clients, des parents, des commerçants.\nLaissés seuls, ils dérivent vers le terminal : « payload invalide », « token\nexpiré », « introuvable ». Chacun est exact, et chacun laisse le lecteur sans\nrien à faire.\n\nCe n'est pas un contrôle de style. Il cherche deux échecs précis : des mots qui\nn'existent que pour un développeur, et des phrases si courtes qu'elles\nn'apprennent rien.\n\n```js\nconst { createMessageAudit, collectMessages } = require('@astratra/i18n-server');\n\ntest('ce qu\\'un client peut lire', () => {\n  const messages = collectMessages({\n    root: path.join(__dirname, '..', 'src'),\n    // Le motif est à toi, parce que la forme de tes appels est à toi.\n    // Il doit exposer le message en premier groupe de capture.\n    pattern: /apiResponse\\(\\s*res\\s*,\\s*[45]\\d{2}\\s*,\\s*\"([^\"]+)\"/g,\n  });\n\n  const audit = createMessageAudit();\n  const findings = audit.inspect(messages);\n\n  expect(audit.describe(findings)).toEqual([]);\n});\n```\n\nMets-le dans ta suite de tests, et la règle se défend toute seule à partir de\nlà. C'est cette discipline qui a de la valeur, plus que le code.\n\nLe vocabulaire banni est configurable, et `allow` existe pour le cas rare où le\nmot technique EST le plus clair :\n\n```js\ncreateMessageAudit({\n  jargon: [...DEFAULT_JARGON, /\\bwidget\\b/i],\n  minWords: 4,\n  allow: [\"Votre jeton d'accès a expiré. Reconnectez-vous.\"],\n});\n```\n\n\n## La langue du courrier n'est pas celle de l'interface\n\n`createLanguageResolver` répond à « quelle langue l'appelant demande-t-il ? ».\nUn e-mail pose une autre question : « quelle langue lit la personne à qui\nj'écris ? ». La plupart des courriels partent d'une tâche planifiée, d'une file,\nou d'un geste fait par **quelqu'un d'autre** — un administrateur qui\nréinitialise un mot de passe, un enseignant qui écrit à une famille. L'en-tête\nde la requête, s'il existe, est celui de l'expéditeur.\n\nEt l'on peut travailler dans l'application en anglais et vouloir son courrier\nen français. D'où un champ à part (`emailLang` par défaut), lu **en premier**,\navec la valeur `auto` qui veut dire « comme l'interface ».\n\n```js\nconst { createRecipientLanguage } = require('@astratra/i18n-server');\n\nconst mailLanguage = createRecipientLanguage({\n  languages: ['fr', 'en', 'es'],\n  // mailField: 'emailLang', interfaceFields: ['lang'], followValue: 'auto'\n});\n\nmailLanguage.languageOf({ lang: 'fr', emailLang: 'en' });   // 'en'\nmailLanguage.languageOf({ lang: 'fr', emailLang: 'auto' }); // 'fr'\nmailLanguage.languageOf(null, req);                         // Accept-Language, puis le défaut\n\n// Pour l'écran de réglages : refuser plutôt qu'enregistrer une langue inconnue.\nmailLanguage.choices;        // ['auto', 'fr', 'en', 'es']\nmailLanguage.isChoice('de'); // false\n```\n\nOrdre : le choix du courrier (sauf `auto`), puis les champs de l'interface,\npuis l'en-tête, puis la langue par défaut. Une valeur enregistrée qu'on ne sert\npas **passe au suivant** au lieu d'imposer le défaut.\n\n## Relire le destinataire avant de lui écrire\n\nC'est la règle qui a coûté le plus cher. Le jeton de session porte un\nidentifiant et un rôle, **jamais la langue**. Un compte lu avec une projection\nécrite pour autre chose (`email fullName`) l'a perdue aussi. Dans les deux cas\nle résolveur ne voit rien, retombe sur le défaut, et le courrier part dans la\nmauvaise langue — sans bruit, puisqu'un e-mail dans la mauvaise langue\n« fonctionne ».\n\n```js\nconst { createRecipientReloader } = require('@astratra/i18n-server');\n\nconst recipients = createRecipientReloader({\n  // Ton accès aux données. `fields` est la liste EXACTE à sélectionner.\n  load: (id, fields, role) => modelFor(role).findById(id).select(fields.join(' ')).lean(),\n  fields: ['email', 'fullName'],\n  language: mailLanguage, // ses champs (emailLang, lang) sont TOUJOURS ajoutés\n});\n\nconst recipient = await recipients.reload(req.user, req.user.role);\nconst lang = mailLanguage.languageOf(recipient);\n```\n\n- Ce que dit la base l'emporte sur ce que portait le jeton : une adresse dans\n  un jeton peut être périmée, jamais plus fraîche.\n- **Ne lève jamais.** Un compte illisible reçoit quand même son courrier, dans\n  la langue de repli : mieux vaut un e-mail dans la mauvaise langue que pas\n  d'e-mail, et ce dont il parle (un code, une réinitialisation) a déjà eu lieu.\n\n## Une clé répétée dans un catalogue\n\nUne clé écrite deux fois dans un littéral JavaScript garde la **dernière**\nvaleur, sans rien dire. Une phrase avait deux traductions dans une même langue,\nla première relue et juste, la seconde ancienne et fausse : seule la fausse\nétait servie. `Object.keys()` ne peut pas le voir — une fois l'objet construit,\nla première valeur n'existe plus. Seule la source montre les deux.\n\n```js\nconst { findDuplicateKeys } = require('@astratra/i18n-server');\n\ntest('aucune phrase cataloguée deux fois', () => {\n  const source = fs.readFileSync(require.resolve('../src/messages'), 'utf8');\n  const { keys, duplicates } = findDuplicateKeys(source, { start: 'const CATALOG = {' });\n\n  expect(duplicates).toEqual([]);\n  // La preuve que l'audit a lu tout l'objet :\n  expect(keys).toHaveLength(Object.keys(CATALOG).length);\n});\n```\n\nSeules les clés directes de l'objet sont comptées : le `en:` de chaque entrée\nn'est pas un doublon. Les chaînes, gabarits et commentaires sont masqués avant\nla lecture. Ce n'est pas un analyseur complet : une accolade dans une\nexpression régulière **à l'intérieur** de l'objet fausserait la profondeur.\n\n## Pas d'objet d'e-mail écrit en dur\n\nUn corps traduit autour d'un objet tapé dans le code se lit comme un bug — et\nl'objet est la seule ligne que tout le monde voit.\n\n```js\nconst { findHardcodedSubjects, scanSourceTree } = require('@astratra/i18n-server');\n\ntest('les objets viennent du catalogue', () => {\n  const findings = scanSourceTree({\n    root: path.join(__dirname, '..', 'src', 'modules'),\n    ignore: ['node_modules', 'monitoring/alerts.js'], // alertes internes : exemptées\n    inspect: (source) => findHardcodedSubjects(source, {\n      callee: 'sendEmail',\n      argument: 1,                  // sendEmail(to, subject, …)\n      ignore: [/TEAM_EMAIL/],       // un destinataire interne fixe\n    }),\n  });\n  expect(findings).toEqual([]);\n});\n\n// Forme objet : mailer.send({ to, subject: '…' })\nfindHardcodedSubjects(source, { callee: 'mailer.send', property: 'subject' });\n```\n\nPar défaut, tout littéral contenant une lettre est signalé. `test` le\nrestreint à une langue si ton code garde légitimement des objets internes dans\nune autre. Un appel dans un commentaire ou une chaîne n'est pas un appel.\n\n## Aucun mot d'une autre langue dans un e-mail rendu\n\nLe défaut visé n'est pas la traduction manquante — elle se voit. C'est l'e-mail\ntraduit avec **une** ligne restée dans la langue source : un pied de page\nfabriqué par une aide que personne n'a pensé à traduire, une date formatée avec\nla locale du serveur.\n\n```js\nconst { createLanguageLeakCheck } = require('@astratra/i18n-server');\n\nconst leak = createLanguageLeakCheck({\n  // Des mots qui n'existent QUE dans chaque langue. Pronoms et salutations\n  // sont fiables ; « message », « code » ou « date » sont partagés.\n  markers: {\n    fr: ['vous', 'votre', 'bonjour'],\n    en: ['your', 'hello'],\n  },\n});\n\ntest('interface en français, courrier en anglais', async () => {\n  const mail = await renderResetMail({ lang: 'fr', emailLang: 'en' });\n  const result = leak.inspect(mail, 'en'); // { subject, text, html }\n  expect(leak.describe(result)).toEqual([]);\n});\n```\n\nRends toujours l'e-mail pour un compte dont l'interface et le courrier\ndiffèrent : c'est le seul cas qui distingue les deux champs. Le HTML est lu\ncomme la personne le lit (styles, scripts et balises retirés), et un\n`<html lang>` d'une autre langue est signalé aussi (`requireHtmlLang: true`\npour exiger qu'il existe).\n\n## Ce que ce package ne fait pas\n\n- Il n'a **aucune langue** par défaut. `fr`/`en`/`es` est le choix d'un produit.\n- Il ne traduit **pas** ton interface : ça, c'est le travail du client.\n- Il n'appelle aucun service de traduction. Les phrases sont écrites par des\n  humains, une fois.\n- Il ne fournit pas de dictionnaire à clés nommées pour tes e-mails : le\n  catalogue garde la phrase source pour clé, et chaque produit a déjà le sien.\n- Aucune dépendance à l'exécution.\n\n## Tests\n\n```bash\nnpm test --workspace @astratra/i18n-server\n```\n","readmeFilename":"README.md"}