From 0065527a5e4ddd81dee92b2f1caac279340dc7fd Mon Sep 17 00:00:00 2001 From: lebaudantoine Date: Tue, 9 Jun 2026 18:32:18 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=8C=90(addon)=20internationalize=20the=20?= =?UTF-8?q?addon?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Refactor the plugin to support internationalization and ship it in three languages. Internationalizing requires updating the manifest, which involves touching the Helm chart and releasing a new version. Ship a beta version of the plugin once the i18n work is done to bundle these changes together. --- src/addons/outlook/manifest.xml | 31 ++++++++--- src/addons/outlook/package-lock.json | 43 ++++++++++++++-- src/addons/outlook/package.json | 2 + src/addons/outlook/src/commands/commands.js | 20 +++++--- src/addons/outlook/src/common/i18n.js | 48 +++++++++++++++++ .../outlook/src/common/messageBuilder.js | 25 ++++----- .../outlook/src/locales/de/translation.json | 34 +++++++++++++ .../outlook/src/locales/en/translation.json | 34 +++++++++++++ .../outlook/src/locales/fr/translation.json | 34 +++++++++++++ src/addons/outlook/src/success/success.html | 8 ++- src/addons/outlook/src/success/success.js | 51 +++++++++++-------- src/addons/outlook/src/taskpane/taskpane.html | 26 +++++----- src/addons/outlook/src/taskpane/taskpane.js | 14 +++-- src/addons/outlook/src/transit/transit.html | 4 +- src/addons/outlook/src/transit/transit.js | 7 ++- src/helm/meet/files/outlook-manifest.xml.tpl | 31 ++++++++--- 16 files changed, 328 insertions(+), 84 deletions(-) create mode 100644 src/addons/outlook/src/common/i18n.js create mode 100644 src/addons/outlook/src/locales/de/translation.json create mode 100644 src/addons/outlook/src/locales/en/translation.json create mode 100644 src/addons/outlook/src/locales/fr/translation.json diff --git a/src/addons/outlook/manifest.xml b/src/addons/outlook/manifest.xml index 08b73910..c19b9cd3 100644 --- a/src/addons/outlook/manifest.xml +++ b/src/addons/outlook/manifest.xml @@ -175,15 +175,34 @@ + - - - + + + + + + + + + + + + - - - + + + + + + + + + + + + diff --git a/src/addons/outlook/package-lock.json b/src/addons/outlook/package-lock.json index c3ff2c3b..386af869 100644 --- a/src/addons/outlook/package-lock.json +++ b/src/addons/outlook/package-lock.json @@ -10,6 +10,8 @@ "license": "MIT", "dependencies": { "core-js": "^3.49.0", + "i18next": "26.3.1", + "i18next-browser-languagedetector": "8.2.1", "regenerator-runtime": "^0.14.1" }, "devDependencies": { @@ -2111,9 +2113,7 @@ "version": "7.28.6", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz", "integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==", - "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=6.9.0" } @@ -9363,6 +9363,43 @@ "node": ">=10.18" } }, + "node_modules/i18next": { + "version": "26.3.1", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-26.3.1.tgz", + "integrity": "sha512-txQqd5EULsqEh9OJqRH15aCaOuy/nLJyhw5EHCSKLKJE1aBbb3Zve2+uQIxgWhPm1QqUQoWyQBm2kfmmIrzkcQ==", + "funding": [ + { + "type": "individual", + "url": "https://www.locize.com/i18next" + }, + { + "type": "individual", + "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project" + }, + { + "type": "individual", + "url": "https://www.locize.com" + } + ], + "license": "MIT", + "peerDependencies": { + "typescript": "^5 || ^6" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/i18next-browser-languagedetector": { + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/i18next-browser-languagedetector/-/i18next-browser-languagedetector-8.2.1.tgz", + "integrity": "sha512-bZg8+4bdmaOiApD7N7BPT9W8MLZG+nPTOFlLiJiT8uzKXFjhxw4v2ierCXOwB5sFDMtuA5G4kgYZ0AznZxQ/cw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.23.2" + } + }, "node_modules/iconv-lite": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", @@ -15213,7 +15250,7 @@ "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", diff --git a/src/addons/outlook/package.json b/src/addons/outlook/package.json index 95ec552d..279c5ae6 100644 --- a/src/addons/outlook/package.json +++ b/src/addons/outlook/package.json @@ -27,6 +27,8 @@ }, "dependencies": { "core-js": "^3.49.0", + "i18next": "26.3.1", + "i18next-browser-languagedetector": "8.2.1", "regenerator-runtime": "^0.14.1" }, "devDependencies": { diff --git a/src/addons/outlook/src/commands/commands.js b/src/addons/outlook/src/commands/commands.js index 09480d33..ae4457dc 100644 --- a/src/addons/outlook/src/commands/commands.js +++ b/src/addons/outlook/src/commands/commands.js @@ -5,8 +5,12 @@ const { saveSession, loadSession } = require("../common/session"); const { openTransitDialog } = require("../common/transitDialog"); const { buildMeetingMessage } = require("../common/messageBuilder"); const { applyAppName } = require("../common/helpers"); +const { initI18n, t } = require("../common/i18n"); + +Office.onReady(async function (info) { + + await initI18n() -Office.onReady(function (info) { if (info.host === Office.HostType.Outlook) { applyAppName(); } @@ -32,22 +36,22 @@ function insertMeetingLink(event, session) { return new Promise((resolve, reject) => { item.body.setSelectedDataAsync(text, { coercionType }, (setResult) => { if (setResult.status !== Office.AsyncResultStatus.Succeeded) { - notify(`Erreur d'insertion : ${setResult.error.message}`); + notify(t('meeting.error.details', { message: setResult.error.message })); resolve(); return; } if (item.itemType !== Office.MailboxEnums.ItemType.Appointment) { - notify("Lien de réunion inséré !"); + notify(t("meeting.link_inserted")); resolve(); return; } item.location.setAsync(url, (locationResult) => { if (locationResult.status !== Office.AsyncResultStatus.Succeeded) { - notify(`Erreur de localisation : ${locationResult.error.message}`); + notify(t("meeting.error.details", { message: locationResult.error.message })); } else { - notify("Lien de réunion inséré !"); + notify(t("meeting.link_inserted")); } resolve(); }); @@ -72,11 +76,11 @@ function connect(event) { }); }, onTimeout: () => { - notify("Connexion expirée, veuillez réessayer."); + notify(t("meeting.error.auth")); event.completed(); }, onError: (err) => { - notify("Une erreur est survenue, veuillez ré-essayer"); + notify(t("meeting.error.retry")); event.completed(); }, }); @@ -92,7 +96,7 @@ function connect(event) { }); }) .catch((err) => { - notify(`Erreur : ${err.message}`); + notify(t("meeting.error.details", { message: err.message })); event.completed(); }); } diff --git a/src/addons/outlook/src/common/i18n.js b/src/addons/outlook/src/common/i18n.js new file mode 100644 index 00000000..ce67be4d --- /dev/null +++ b/src/addons/outlook/src/common/i18n.js @@ -0,0 +1,48 @@ +const { APP_NAME } = require("../common"); + +const i18nextModule = require("i18next"); +const i18next = i18nextModule.default || i18nextModule; + +const fr = require("../locales/fr/translation.json"); +const en = require("../locales/en/translation.json"); +const de = require("../locales/de/translation.json"); + +async function initI18n() { + const lng = typeof Office !== "undefined" ? Office.context.displayLanguage : navigator.language; + + await i18next.init({ + lng, + fallbackLng: "fr", + interpolation: { escapeValue: false }, + resources: { + fr: { translation: fr }, + en: { translation: en }, + de: { translation: de }, + }, + }); +} + +function t(key, vars) { + return i18next.t(key, vars); +} + +function translateUI() { + document.querySelectorAll("[data-i18n]").forEach((el) => { + const key = el.getAttribute("data-i18n"); + el.textContent = t(key, { app_name: APP_NAME }); + }); + + document.querySelectorAll("[data-i18n-attr]").forEach((el) => { + const pairs = el.getAttribute("data-i18n-attr").split(","); + pairs.forEach((pair) => { + const [attr, key] = pair.split(":"); + el.setAttribute(attr, t(key, { app_name: APP_NAME })); + }); + }); + + document.querySelectorAll("[data-i18n-aria]").forEach((el) => { + el.setAttribute("aria-label", t(el.getAttribute("data-i18n-aria"))); + }); +} + +module.exports = { initI18n, t, translateUI }; diff --git a/src/addons/outlook/src/common/messageBuilder.js b/src/addons/outlook/src/common/messageBuilder.js index f29184c2..8a0aab2f 100644 --- a/src/addons/outlook/src/common/messageBuilder.js +++ b/src/addons/outlook/src/common/messageBuilder.js @@ -1,4 +1,5 @@ const { APP_NAME } = require("./index"); +const { t } = require("./i18n"); function _formatPin(pin) { if (!pin) return ""; @@ -33,16 +34,17 @@ function buildMeetingMessage(data, isWeb) { let textLines = ""; let phoneLines = []; - phoneLines = phone && - pin && [ - "

Ou appelez (audio uniquement)", - `
(FR) ${phone}`, - `
Code ${pin}` - ]; + const join = t("meeting_message.join", { app_name: APP_NAME }); + const phoneOnly = t("meeting_message.phone_only"); + const phoneFr = t("meeting_message.phone_fr", { phone }); + const pinCode = t("meeting_message.pin_code", { pin }); + + if (isWeb) { + phoneLines = phone && pin ? [`

${phoneOnly}`, `
${phoneFr}`, `
${pinCode}`] : []; textLines = [ "

────────────────────────────────────────", - `
Rejoindre la réunion ${APP_NAME}`, + `
${join}`, `

${url}`, ...phoneLines, "
────────────────────────────────────────
", @@ -50,16 +52,11 @@ function buildMeetingMessage(data, isWeb) { } else { - phoneLines = phone && - pin && [ - "\n\nOu appelez (audio uniquement)", - `\n(FR) ${phone}`, - `\nCode ${pin}` - ]; + phoneLines = phone && pin ? [`\n\n${phoneOnly}`, `\n${phoneFr}`, `\n${pinCode}`] : []; textLines = [ "\n\n────────────────────────────────────────", - `\nRejoindre la réunion ${APP_NAME}`, + `\n${join}`, `\n\n${url}`, ...phoneLines, "\n────────────────────────────────────────\n", diff --git a/src/addons/outlook/src/locales/de/translation.json b/src/addons/outlook/src/locales/de/translation.json new file mode 100644 index 00000000..a86aa9b6 --- /dev/null +++ b/src/addons/outlook/src/locales/de/translation.json @@ -0,0 +1,34 @@ +{ + "app": { + "sideload": "Laden Sie das Add-In.", + "loading": "Wird geladen..." + }, + "unauth": { + "intro": "Fügen Sie Ihren Outlook-Terminen ganz einfach einen {{app_name}}-Besprechungslink hinzu.", + "proconnect_btn": "Aanmelden met ProConnect", + "proconnect_link": "Wat is ProConnect?", + "proconnect_link_title": "Wat is ProConnect? - nieuw venster" + }, + "success": { + "close_window": "Falls sich dieses Fenster nicht automatisch schließt, schließen Sie es bitte manuell." + }, + "auth": { + "disconnect": "Abmelden" + }, + "meeting": { + "link_inserted": "Besprechungslink erfolgreich eingefügt", + "generating": "Wird erstellt...", + "add_meeting": "{{app_name}}-Besprechung hinzufügen", + "error": { + "auth": "Ihre Sitzung ist abgelaufen. Bitte versuchen Sie es erneut.", + "retry": "Es ist ein Fehler aufgetreten. Bitte versuchen Sie es erneut.", + "details": "Fehler: {{message}}" + } + }, + "meeting_message": { + "join": "An der {{app_name}}-Besprechung teilnehmen", + "phone_only": "Oder per Telefon teilnehmen (nur Audio)", + "phone_fr": "(FR) {{phone}}", + "pin_code": "Code {{pin}}" + } +} diff --git a/src/addons/outlook/src/locales/en/translation.json b/src/addons/outlook/src/locales/en/translation.json new file mode 100644 index 00000000..ad49e8d6 --- /dev/null +++ b/src/addons/outlook/src/locales/en/translation.json @@ -0,0 +1,34 @@ +{ + "app": { + "sideload": "Please load the add-in.", + "loading": "Loading..." + }, + "unauth": { + "intro": "Easily add a {{app_name}} meeting link to your Outlook events.", + "proconnect_btn": "Sign in with ProConnect", + "proconnect_link": "What is ProConnect?", + "proconnect_link_title": "What is ProConnect? - new window" + }, + "success": { + "close_window": "If this window does not close automatically, please close it manually." + }, + "auth": { + "disconnect": "Sign out" + }, + "meeting": { + "link_inserted": "Meeting link inserted successfully", + "generating": "Generating...", + "add_meeting": "Add a {{app_name}} meeting", + "error": { + "auth": "Your session has expired. Please try again.", + "retry": "An error occurred. Please try again.", + "details": "Error: {{message}}" + } + }, + "meeting_message": { + "join": "Join the {{app_name}} meeting", + "phone_only": "Or call in (audio only)", + "phone_fr": "(FR) {{phone}}", + "pin_code": "Code {{pin}}" + } +} diff --git a/src/addons/outlook/src/locales/fr/translation.json b/src/addons/outlook/src/locales/fr/translation.json new file mode 100644 index 00000000..b636f922 --- /dev/null +++ b/src/addons/outlook/src/locales/fr/translation.json @@ -0,0 +1,34 @@ +{ + "app": { + "sideload": "Veuillez charger le complément.", + "loading": "Chargement..." + }, + "unauth": { + "intro": "Ajoutez facilement un lien de réunion {{app_name}} à vos événements Outlook.", + "proconnect_btn": "S'identifier avec ProConnect", + "proconnect_link": "Qu'est-ce que ProConnect ?", + "proconnect_link_title": "Qu'est-ce que ProConnect ? - nouvelle fenêtre" + }, + "success": { + "close_window": "Si cette fenêtre ne se ferme pas toute seule, veuillez la fermer manuellement." + }, + "auth": { + "disconnect": "Se déconnecter" + }, + "meeting": { + "link_inserted": "Lien de réunion inséré avec succès", + "generating": "Génération...", + "add_meeting": "Ajouter une réunion {{app_name}}", + "error": { + "auth": "Connexion expirée, veuillez réessayer.", + "retry": "Une erreur est survenue, veuillez ré-essayer", + "details": "Erreur : {{message}}" + } + }, + "meeting_message": { + "join": "Rejoindre la réunion {{app_name}}", + "phone_only": "Ou appelez (audio uniquement)", + "phone_fr": "(FR) {{phone}}", + "pin_code": "Code {{pin}}" + } +} diff --git a/src/addons/outlook/src/success/success.html b/src/addons/outlook/src/success/success.html index d1603e88..09bbd3eb 100644 --- a/src/addons/outlook/src/success/success.html +++ b/src/addons/outlook/src/success/success.html @@ -9,10 +9,10 @@ -
Veuillez charger le complément.
+
- + diff --git a/src/addons/outlook/src/success/success.js b/src/addons/outlook/src/success/success.js index ccd0082d..0d5c2e44 100644 --- a/src/addons/outlook/src/success/success.js +++ b/src/addons/outlook/src/success/success.js @@ -1,28 +1,35 @@ const { applyAppName } = require("../common/helpers"); const { exchangeSession } = require("../common/api"); const { consume } = require("../common/transitToken"); +const { initI18n, translateUI } = require("../common/i18n"); -applyAppName(); +(async () => { + await initI18n(); -const transitToken = consume(); + applyAppName(); + translateUI(); -if (!transitToken) { - console.error("Transit token not found in sessionStorage"); - window.close(); -} else { - exchangeSession(transitToken) - .then(() => { - document.querySelector(".spinner-container").style.display = "none"; - document.querySelector("#close-msg").style.display = "block"; - }) - .catch((e) => { - console.error(`Error occured: ${e}`); - }) - .finally(() => { - // NOTE: doesn't work with the desktop client — the browser considers - // this window wasn't opened by this script (it was opened externally), - // so it blocks window.close() for security reasons. The "#close-msg" - // shown above is the fallback for that case.g - window.close(); - }); -} + const transitToken = consume(); + + if (!transitToken) { + console.error("Transit token not found in sessionStorage"); + window.close(); + } else { + exchangeSession(transitToken) + .then(() => { + document.querySelector(".spinner-container").style.display = "none"; + document.querySelector("#close-msg").style.display = "block"; + }) + .catch((e) => { + console.error(`Error occured: ${e}`); + }) + .finally(() => { + // NOTE: doesn't work with the desktop client — the browser considers + // this window wasn't opened by this script (it was opened externally), + // so it blocks window.close() for security reasons. The "#close-msg" + // shown above is the fallback for that case.g + + window.close(); + }); + } +})(); diff --git a/src/addons/outlook/src/taskpane/taskpane.html b/src/addons/outlook/src/taskpane/taskpane.html index c815179a..d2782b26 100644 --- a/src/addons/outlook/src/taskpane/taskpane.html +++ b/src/addons/outlook/src/taskpane/taskpane.html @@ -10,41 +10,39 @@ -
Veuillez charger le complément.
+
-

Chargement...

+

diff --git a/src/addons/outlook/src/taskpane/taskpane.js b/src/addons/outlook/src/taskpane/taskpane.js index 77c22007..7e1e50da 100644 --- a/src/addons/outlook/src/taskpane/taskpane.js +++ b/src/addons/outlook/src/taskpane/taskpane.js @@ -7,6 +7,8 @@ const { openTransitDialog } = require("../common/transitDialog"); const { loadSession, saveSession, clearSession } = require("../common/session"); const { buildMeetingMessage } = require("../common/messageBuilder"); +const { initI18n, t, translateUI } = require("../common/i18n"); + // todo - support loading view while polling // todo - support error view @@ -50,13 +52,15 @@ function disconnect() { function _setButtonLoading() { const btn = document.getElementById("btn-generate"); btn.disabled = true; - btn.textContent = "Génération..."; + btn.textContent = t("meeting.generating"); } function _setButtonIdle() { const btn = document.getElementById("btn-generate"); btn.disabled = false; - btn.textContent = `Ajouter une réunion ${APP_NAME}`; + btn.textContent = t("meeting.add_meeting", { + app_name: APP_NAME, + }); } function generateMeetingLink() { @@ -99,7 +103,11 @@ function generateMeetingLink() { }); } -Office.onReady((info) => { +Office.onReady(async (info) => { + + await initI18n(); + translateUI(); + if (info.host === Office.HostType.Outlook) { applyAppName(); document.getElementById("sideload-msg").style.display = "none"; diff --git a/src/addons/outlook/src/transit/transit.html b/src/addons/outlook/src/transit/transit.html index d5aad24e..58b8d5fe 100644 --- a/src/addons/outlook/src/transit/transit.html +++ b/src/addons/outlook/src/transit/transit.html @@ -10,11 +10,11 @@ -
Veuillez charger le complément.
+
+ - - - + + + + + + + + + + + + - - - + + + + + + + + + + + +