From f73ccbbf7eac47b0d25a28a4dc575724f85a8644 Mon Sep 17 00:00:00 2001 From: John Livingston Date: Mon, 12 Jun 2023 19:26:28 +0200 Subject: [PATCH] Localization refactoring: * the front-end now use global constants, based on the translation key * build-client.js use the ESBuild "define" directive to replace these globals at compile time, by the english value * build:client must now be called after build:languages * moving the loadLoc and loc backend functions in a separate lib --- build-client.js | 34 +++++++++++++-- client/@types/global.d.ts | 29 +++++++++++++ client/admin-plugin-client-plugin.ts | 12 +++--- client/common-client-plugin.ts | 4 +- client/videowatch-client-plugin.ts | 8 ++-- client/videowatch/share.ts | 32 +++++++------- package.json | 2 +- server/lib/loc.ts | 43 +++++++++++++++++++ server/lib/settings.ts | 41 +----------------- server/main.ts | 4 ++ .../contributing/translate/_index.en.md | 40 +++++++++++++---- .../contributing/translate/_index.fr.md | 43 ++++++++++++++----- 12 files changed, 199 insertions(+), 93 deletions(-) create mode 100644 server/lib/loc.ts diff --git a/build-client.js b/build-client.js index 477e4389..bfd22c01 100644 --- a/build-client.js +++ b/build-client.js @@ -1,5 +1,6 @@ const path = require('path') const esbuild = require('esbuild') +const fs = require('fs') const packagejson = require('./package.json') const sourcemap = process.env.NODE_ENV === 'dev' ? 'inline' : false @@ -11,15 +12,40 @@ const clientFiles = [ 'admin-plugin-client-plugin' ] +function loadLocs() { + // Loading english strings, so we can inject them as constants. + const refFile = path.resolve(__dirname, 'dist', 'languages', 'en.reference.json') + if (!fs.existsSync(refFile)) { + throw new Error('Missing english reference file, please run "npm run build:languages" before building the client') + } + const english = require(refFile) + + // Reading client/@types/global.d.ts, to have a list of needed localized strings. + const r = {} + const globalFile = path.resolve(__dirname, 'client', '@types', 'global.d.ts') + const globalFileContent = '' + fs.readFileSync(globalFile) + const matches = globalFileContent.matchAll(/^declare const LOC_(\w+)\b/gm) + for (const match of matches) { + const key = match[1].toLowerCase() + if (!(key in english) || (typeof english[key] !== 'string')) { + throw new Error('Missing english string key=' + key) + } + r['LOC_' + match[1]] = JSON.stringify(english[key]) + } + return r +} + +const define = Object.assign({ + PLUGIN_CHAT_PACKAGE_NAME: JSON.stringify(packagejson.name), + PLUGIN_CHAT_SHORT_NAME: JSON.stringify(packagejson.name.replace(/^peertube-plugin-/, '')) +}, loadLocs()) + const configs = clientFiles.map(f => ({ entryPoints: [ path.resolve(__dirname, 'client', f + '.ts') ], alias: { 'shared': path.resolve(__dirname, 'shared/') }, - define: { - PLUGIN_CHAT_PACKAGE_NAME: JSON.stringify(packagejson.name), - PLUGIN_CHAT_SHORT_NAME: JSON.stringify(packagejson.name.replace(/^peertube-plugin-/, '')) - }, + define, bundle: true, minify: true, // FIXME: sourcemap:`true` does not work for now, because peertube does not serve static files. diff --git a/client/@types/global.d.ts b/client/@types/global.d.ts index 2308ea4d..12454a93 100644 --- a/client/@types/global.d.ts +++ b/client/@types/global.d.ts @@ -1,2 +1,31 @@ declare const PLUGIN_CHAT_PACKAGE_NAME: string declare const PLUGIN_CHAT_SHORT_NAME: string + +// Constants that begins with "LOC_" are loaded by build-client.js, reading the english locale file. +// See the online documentation: https://johnxlivingston.github.io/peertube-plugin-livechat/contributing/translate/ +declare const LOC_OPEN_CHAT: string +declare const LOC_OPEN_CHAT_NEW_WINDOW: string +declare const LOC_CLOSE_CHAT: string +declare const LOC_USE_CHAT: string +declare const LOC_USE_CHAT_HELP: string +declare const LOC_SHARE_CHAT_LINK: string +declare const LOC_READ_ONLY: string +declare const LOC_SHOW_SCROLLBARR: string +declare const LOC_TRANSPARENT_BACKGROUND: string +declare const LOC_TIPS_FOR_STREAMERS: string +declare const LOC_COPY: string +declare const LOC_LINK_COPIED: string +declare const LOC_ERROR: string +declare const LOC_OPEN: string +declare const LOC_USE_CURRENT_THEME_COLOR: string +declare const LOC_GENERATE_IFRAME: string +declare const LOC_CHAT_FOR_LIVE_STREAM: string +declare const LOC_ROOM_NAME: string +declare const LOC_ROOM_DESCRIPTION: string +declare const LOC_NOT_FOUND: string +declare const LOC_VIDEO: string +declare const LOC_CHANNEL: string +declare const LOC_LAST_ACTIVITY: string +declare const LOC_WEB: string +declare const LOC_CONNECT_USING_XMPP: string +declare const LOC_CONNECT_USING_XMPP_HELP: string diff --git a/client/admin-plugin-client-plugin.ts b/client/admin-plugin-client-plugin.ts index 67646642..a5d7a59f 100644 --- a/client/admin-plugin-client-plugin.ts +++ b/client/admin-plugin-client-plugin.ts @@ -100,12 +100,12 @@ function register ({ registerHook, registerSettingsScript, peertubeHelpers }: Re table.classList.add('peertube-plugin-livechat-prosody-list-rooms') container.append(table) const labels: any = { - RoomName: await peertubeHelpers.translate('Room name'), - RoomDescription: await peertubeHelpers.translate('Room description'), - NotFound: await peertubeHelpers.translate('Not found'), - Video: await peertubeHelpers.translate('Video'), - Channel: await peertubeHelpers.translate('Channel'), - LastActivity: await peertubeHelpers.translate('Last activity') + RoomName: await peertubeHelpers.translate(LOC_ROOM_NAME), + RoomDescription: await peertubeHelpers.translate(LOC_ROOM_DESCRIPTION), + NotFound: await peertubeHelpers.translate(LOC_NOT_FOUND), + Video: await peertubeHelpers.translate(LOC_VIDEO), + Channel: await peertubeHelpers.translate(LOC_CHANNEL), + LastActivity: await peertubeHelpers.translate(LOC_LAST_ACTIVITY) } const titleLineEl = document.createElement('tr') diff --git a/client/common-client-plugin.ts b/client/common-client-plugin.ts index 68a70e48..4767696a 100644 --- a/client/common-client-plugin.ts +++ b/client/common-client-plugin.ts @@ -22,8 +22,8 @@ async function register ({ peertubeHelpers, registerHook, registerVideoField }: }) const [label, description, settings] = await Promise.all([ - peertubeHelpers.translate('Use chat'), - peertubeHelpers.translate('If enabled, there will be a chat next to the video.'), + peertubeHelpers.translate(LOC_USE_CHAT), + peertubeHelpers.translate(LOC_USE_CHAT_HELP), peertubeHelpers.getSettings() ]) const webchatFieldOptions: RegisterClientFormFieldOptions = { diff --git a/client/videowatch-client-plugin.ts b/client/videowatch-client-plugin.ts index 73ccc3a4..2620f70e 100644 --- a/client/videowatch-client-plugin.ts +++ b/client/videowatch-client-plugin.ts @@ -76,10 +76,10 @@ function register (registerOptions: RegisterClientOptions): void { const p = new Promise((resolve, reject) => { // eslint-disable-next-line @typescript-eslint/no-floating-promises Promise.all([ - peertubeHelpers.translate('Open chat'), - peertubeHelpers.translate('Open chat in a new window'), - peertubeHelpers.translate('Close chat'), - peertubeHelpers.translate('Share chat link') + peertubeHelpers.translate(LOC_OPEN_CHAT), + peertubeHelpers.translate(LOC_OPEN_CHAT_NEW_WINDOW), + peertubeHelpers.translate(LOC_CLOSE_CHAT), + peertubeHelpers.translate(LOC_SHARE_CHAT_LINK) ]).then(labels => { const labelOpen = labels[0] const labelOpenBlank = labels[1] diff --git a/client/videowatch/share.ts b/client/videowatch/share.ts index c7dac710..f8446a61 100644 --- a/client/videowatch/share.ts +++ b/client/videowatch/share.ts @@ -40,23 +40,21 @@ async function shareChatUrl (registerOptions: RegisterClientOptions, settings: a labelGenerateIframe, labelChatFor ] = await Promise.all([ - peertubeHelpers.translate('Share chat link'), - peertubeHelpers.translate('Web'), - peertubeHelpers.translate('Connect using XMPP'), - // eslint-disable-next-line max-len - peertubeHelpers.translate('You can connect to the room using an external XMPP account, and your favorite XMPP client.'), - peertubeHelpers.translate('Read-only'), - peertubeHelpers.translate('Show the scrollbar'), - peertubeHelpers.translate('Transparent background (for stream integration, with OBS for example)'), - // eslint-disable-next-line max-len - peertubeHelpers.translate('Tips for streamers: To add the chat to your OBS, generate a read-only link and use it as a browser source.'), - peertubeHelpers.translate('Copy'), - peertubeHelpers.translate('Link copied'), - peertubeHelpers.translate('Error'), - peertubeHelpers.translate('Open'), - peertubeHelpers.translate('Use current theme colors'), - peertubeHelpers.translate('Generate an iframe to embed the chat in a website'), - peertubeHelpers.translate('Chat for live stream:') + peertubeHelpers.translate(LOC_SHARE_CHAT_LINK), + peertubeHelpers.translate(LOC_WEB), + peertubeHelpers.translate(LOC_CONNECT_USING_XMPP), + peertubeHelpers.translate(LOC_CONNECT_USING_XMPP_HELP), + peertubeHelpers.translate(LOC_READ_ONLY), + peertubeHelpers.translate(LOC_SHOW_SCROLLBARR), + peertubeHelpers.translate(LOC_TRANSPARENT_BACKGROUND), + peertubeHelpers.translate(LOC_TIPS_FOR_STREAMERS), + peertubeHelpers.translate(LOC_COPY), + peertubeHelpers.translate(LOC_LINK_COPIED), + peertubeHelpers.translate(LOC_ERROR), + peertubeHelpers.translate(LOC_OPEN), + peertubeHelpers.translate(LOC_USE_CURRENT_THEME_COLOR), + peertubeHelpers.translate(LOC_GENERATE_IFRAME), + peertubeHelpers.translate(LOC_CHAT_FOR_LIVE_STREAM) ]) const defaultUri = getIframeUri(registerOptions, settings, video) diff --git a/package.json b/package.json index b10c484b..e04ba865 100644 --- a/package.json +++ b/package.json @@ -96,7 +96,7 @@ "build:prosodymodules": "mkdir -p dist/server/prosody-modules && cp -r prosody-modules/* dist/server/prosody-modules/", "build:styles": "sass assets/styles:dist/assets/styles", "build:languages": "node ./build-languages.js", - "build": "npm-run-all -s clean:light check:client:tsc -p build:converse build:prosody build:images build:avatars build:client build:server build:languages build:serverconverse build:prosodymodules build:styles", + "build": "npm-run-all -s clean:light build:languages check:client:tsc -p build:converse build:prosody build:images build:avatars build:client build:server build:serverconverse build:prosodymodules build:styles", "lint": "npm-run-all -s lint:script lint:styles", "lint:script": "npx eslint --ext .js --ext .ts .", "lint:styles": "stylelint 'conversejs/**/*.scss' 'assets/styles/**/*.css'", diff --git a/server/lib/loc.ts b/server/lib/loc.ts new file mode 100644 index 00000000..6ff49e0a --- /dev/null +++ b/server/lib/loc.ts @@ -0,0 +1,43 @@ +import { resolve } from 'path' +import { existsSync, promises as fsPromises } from 'fs' + +const locContent: Map = new Map() + +/** + * Currently, the Peertube plugin system assumes that settings strings + * are localized in english, and will be translated on front-end. + * This system make it hard to have complex strings (with html, newlines, ...). + * See https://github.com/Chocobozzz/PeerTube/issues/4523 + * + * Waiting for a better solution, we implemented a custom solution: + * - We are using keys to identify strings + * - the `loc` function gets the english segment for the key + * - the build-languages.js script builds all needed files. + * @param key The key to translate + */ +function loc (key: string): string { + return locContent.get(key) ?? key +} + +async function loadLoc (): Promise { + const filePath = resolve(__dirname, '..', '..', 'languages', 'en.reference.json') + if (!existsSync(filePath)) { + throw new Error(`File ${filePath} missing, can't load plugin loc strings`) + } + const content = await fsPromises.readFile(filePath, 'utf8') + const json = JSON.parse(content ?? '{}') + if (typeof json !== 'object') { + throw new Error(`File ${filePath} invalid, can't load plugin loc strings`) + } + for (const k in json) { + const v = json[k] + if (typeof v === 'string') { + locContent.set(k, v) + } + } +} + +export { + loc, + loadLoc +} diff --git a/server/lib/settings.ts b/server/lib/settings.ts index f50e3462..8768937c 100644 --- a/server/lib/settings.ts +++ b/server/lib/settings.ts @@ -1,50 +1,11 @@ import type { RegisterServerOptions } from '@peertube/peertube-types' import { ensureProsodyRunning } from './prosody/ctl' import type { ConverseJSTheme } from '../../shared/lib/types' -import { existsSync, promises as fsPromises } from 'fs' -import { resolve } from 'path' - -const locContent: Map = new Map() - -/** - * Currently, the Peertube plugin system assumes that settings strings - * are localized in english, and will be translated on front-end. - * This system make it hard to have complex strings (with html, newlines, ...). - * See https://github.com/Chocobozzz/PeerTube/issues/4523 - * - * Waiting for a better solution, we implemented a custom solution: - * - We are using keys to identify setting strings - * - the `loc` function gets the english segment for the key - * - the build-languages.js script builds all needed files. - * @param key The key to translate - */ -function loc (key: string): string { - return locContent.get(key) ?? key -} - -async function loadLoc (): Promise { - const filePath = resolve(__dirname, '..', '..', 'languages', 'en.reference.json') - if (!existsSync(filePath)) { - throw new Error(`File ${filePath} missing, can't load plugin settings`) - } - const content = await fsPromises.readFile(filePath, 'utf8') - const json = JSON.parse(content ?? '{}') - if (typeof json !== 'object') { - throw new Error(`File ${filePath} invalid, can't load plugin settings`) - } - for (const k in json) { - const v = json[k] - if (typeof v === 'string') { - locContent.set(k, v) - } - } -} +import { loc } from './loc' async function initSettings (options: RegisterServerOptions): Promise { const { peertubeHelpers, registerSetting, settingsManager } = options - await loadLoc() - // ********** IMPORTANT NOTES registerSetting({ type: 'html', diff --git a/server/main.ts b/server/main.ts index 7927ae40..beb1e95d 100644 --- a/server/main.ts +++ b/server/main.ts @@ -6,6 +6,7 @@ import { initRouters } from './lib/routers/index' import { initFederation } from './lib/federation/init' import { prepareProsody, ensureProsodyRunning, ensureProsodyNotRunning } from './lib/prosody/ctl' import { unloadDebugMode } from './lib/debug' +import { loadLoc } from './lib/loc' import decache from 'decache' // FIXME: Peertube unregister don't have any parameter. @@ -20,6 +21,9 @@ async function register (options: RegisterServerOptions): Promise { throw new Error('Your peertube version is not correct. This plugin is not compatible with Peertube < 3.2.0.') } + // First: load languages files, so we can localize strings. + await loadLoc() + await migrateSettings(options) await initSettings(options) diff --git a/support/documentation/content/contributing/translate/_index.en.md b/support/documentation/content/contributing/translate/_index.en.md index 2e412010..1a46e5d8 100644 --- a/support/documentation/content/contributing/translate/_index.en.md +++ b/support/documentation/content/contributing/translate/_index.en.md @@ -45,14 +45,38 @@ If you are working on new features, and need new strings, you can create them di The english version is mandatory. Start with it. Each string is linked to a key (for example `use_chat`). -Choose an explicit key in english. +Choose an explicit key in english, lower case. -To use a string in front-end, you need (for now) to call `peertubeHelpers.translate` with the english string. -This means we can't change english strings without updating the code. -This is not optimal, but will change in a near future. +If you have to test new strings without waiting for a Weblate merge, you can modify `languages/*.yml` files, +but avoid to commit these changes (to minimize conflict risks). -For backend, for now the only file where there is localisation is -`server/lib/settings.ts`. There is a `loc` function to call, passing as parameter the localisation key. +### Use translations in front-end code -If you have to test new strings without waiting for a Weblate merge, you can modify `languages/*.yml` files, but avoid to commit these change -(to minimize conflict risks). +Before using a string in front-end, you need to declare a new constant in `client/@types/global.d.ts`. +The constant name must: + +* start with the prefix "LOC_" +* use the string key, upper cased +* you just have to declare its type, not its value + +For example, to use "use_chat", you have to declare: + +```typescript +declare const LOC_USE_CHAT: string +``` + +The `build-client.js` script will read the `client/@types/global.d.ts`, +search for such constants, and load their values from the languages files. + +Now, you can simply call `peertubeHelpers.translate(LOC_USE_CHAT)` in your code. + +### Use translations in back-end code + +In theory, the only parts of the backend code where you need localization is the +settings declaration. Here we need to get english strings from the translation key. + +Note: you should never need another language translation from backend code. +Localization must be done on front-end. + +There is a `lib/loc.ts` module providing a `loc()` function. +Just pass it the key to have the english string: `loc('diagnostic')`'. diff --git a/support/documentation/content/contributing/translate/_index.fr.md b/support/documentation/content/contributing/translate/_index.fr.md index 292d9649..0ddf4486 100644 --- a/support/documentation/content/contributing/translate/_index.fr.md +++ b/support/documentation/content/contributing/translate/_index.fr.md @@ -50,18 +50,39 @@ créez les directement dans Weblate. La version anglaise est obligatoire, commencez par celle-ci. Chaque segment est lié à une clé (par exemple `use_chat`). -Choisissez une clé en anglais, suffisamment explicite. - -Pour utiliser un segment coté front-end, il faut (pour l'instant), appeler `peertubeHelpers.translate` -avec la version anglaise du texte. Attention, cela veut-dire qu'il faut éviter de changer un segment anglais -existant. -Cette solution n'est pas optimale, mais devrais bientôt changer. - -Coté backend, le seul endroit (pour l'instant) qui a besoin de localiser des choses, est la déclaration -des settings du plugin. -Il y a pour cela une fonction `loc` dédiée dans `server/lib/settings.ts` à appeler en lui fournissant -la clé de la phrase à utiliser. +Choisissez une clé en anglais, suffisamment explicite, et en minuscule. Si vous avez besoin de tester vos localisations sans attendre la fusion venant de Weblate, vous pouvez modifier les fichiers `languages/*.yml`, mais évitez de les commit (pour minimiser le risque de conflits). + +### Utiliser un segment dans le code front-end + +Avant d'utiliser une chaîne en front-end, il faut déclarer une nouvelle constante dans `client/@types/global.d.ts`. +La constante doit : + +* commencer par le préfixe "LOC_" +* utiliser la clé de la chaîne, en majuscule +* vous ne devez déclarer que son type, pas sa valeur + +Par exemple, pour utiliser "use_chat", vous devez déclarer : +e, to use "use_chat", you have to declare: + +```typescript +declare const LOC_USE_CHAT: string +``` + +Le script `build-client.js` va lire ce fichier `client/@types/global.d.ts`, chercher pour de telles constantes, et charger leurs valeurs depuis le fichier de langue. + +Vous pouvez maintenant utiliser `peertubeHelpers.translate(LOC_USE_CHAT)` dans votre code. + +### Utiliser un segment dans le code back-end + +En théorie, les seules parties du code qui ont besoin de traductions sont les déclarations de paramètres. +Ici on a besoin de récupérer les chaînes anglaises à partir des clés de traduction. + +Note: vous ne devriez jamais avoir besoin d'autres langues que l'anglais pour le code backend. +Les traductions doivent se faire coté front-end. + +Il y a un module `lib/loc.ts` qui fourni une function `loc()`. +Passez juste la clé pour récupérer la phrase anglaise: `loc('diagnostic')`.