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
This commit is contained in:
John Livingston 2023-06-12 19:26:28 +02:00
parent 7285c8b0a8
commit f73ccbbf7e
No known key found for this signature in database
GPG Key ID: B17B5640CE66CDBC
12 changed files with 199 additions and 93 deletions

View File

@ -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.

View File

@ -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

View File

@ -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')

View File

@ -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 = {

View File

@ -76,10 +76,10 @@ function register (registerOptions: RegisterClientOptions): void {
const p = new Promise<void>((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]

View File

@ -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)

View File

@ -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'",

43
server/lib/loc.ts Normal file
View File

@ -0,0 +1,43 @@
import { resolve } from 'path'
import { existsSync, promises as fsPromises } from 'fs'
const locContent: Map<string, string> = new Map<string, string>()
/**
* 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<void> {
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
}

View File

@ -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<string, string> = new Map<string, string>()
/**
* 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<void> {
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<void> {
const { peertubeHelpers, registerSetting, settingsManager } = options
await loadLoc()
// ********** IMPORTANT NOTES
registerSetting({
type: 'html',

View File

@ -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<any> {
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)

View File

@ -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')`'.

View File

@ -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')`.