From d75db01f14138ea660c4c519e37ab05228b39d13 Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Fri, 26 Jul 2019 14:44:50 +0200 Subject: Add plugin translation system --- server/controllers/plugins.ts | 20 +++- server/helpers/custom-validators/plugins.ts | 23 +++-- server/lib/plugins/plugin-manager.ts | 44 +++++++- .../peertube-plugin-test-three/package.json | 3 +- .../peertube-plugin-test-two/languages/fr.json | 3 + .../peertube-plugin-test-two/languages/it.json | 3 + .../fixtures/peertube-plugin-test-two/package.json | 6 +- .../peertube-plugin-test/languages/fr.json | 3 + .../fixtures/peertube-plugin-test/package.json | 5 +- server/tests/plugins/index.ts | 1 + server/tests/plugins/translations.ts | 113 +++++++++++++++++++++ 11 files changed, 212 insertions(+), 12 deletions(-) create mode 100644 server/tests/fixtures/peertube-plugin-test-two/languages/fr.json create mode 100644 server/tests/fixtures/peertube-plugin-test-two/languages/it.json create mode 100644 server/tests/fixtures/peertube-plugin-test/languages/fr.json create mode 100644 server/tests/plugins/translations.ts (limited to 'server') diff --git a/server/controllers/plugins.ts b/server/controllers/plugins.ts index f5285ba3a..1caee9a29 100644 --- a/server/controllers/plugins.ts +++ b/server/controllers/plugins.ts @@ -1,11 +1,12 @@ import * as express from 'express' import { PLUGIN_GLOBAL_CSS_PATH } from '../initializers/constants' import { join } from 'path' -import { RegisteredPlugin } from '../lib/plugins/plugin-manager' +import { PluginManager, RegisteredPlugin } from '../lib/plugins/plugin-manager' import { servePluginStaticDirectoryValidator } from '../middlewares/validators/plugins' import { serveThemeCSSValidator } from '../middlewares/validators/themes' import { PluginType } from '../../shared/models/plugins/plugin.type' import { isTestInstance } from '../helpers/core-utils' +import { getCompleteLocale, is18nLocale } from '../../shared/models/i18n' const sendFileOptions = { maxAge: '30 days', @@ -18,6 +19,10 @@ pluginsRouter.get('/plugins/global.css', servePluginGlobalCSS ) +pluginsRouter.get('/plugins/translations/:locale.json', + getPluginTranslations +) + pluginsRouter.get('/plugins/:pluginName/:pluginVersion/static/:staticEndpoint(*)', servePluginStaticDirectoryValidator(PluginType.PLUGIN), servePluginStaticDirectory @@ -60,6 +65,19 @@ function servePluginGlobalCSS (req: express.Request, res: express.Response) { return res.sendFile(PLUGIN_GLOBAL_CSS_PATH, globalCSSOptions) } +function getPluginTranslations (req: express.Request, res: express.Response) { + const locale = req.params.locale + + if (is18nLocale(locale)) { + const completeLocale = getCompleteLocale(locale) + const json = PluginManager.Instance.getTranslations(completeLocale) + + return res.json(json) + } + + return res.sendStatus(404) +} + function servePluginStaticDirectory (req: express.Request, res: express.Response) { const plugin: RegisteredPlugin = res.locals.registeredPlugin const staticEndpoint = req.params.staticEndpoint diff --git a/server/helpers/custom-validators/plugins.ts b/server/helpers/custom-validators/plugins.ts index e0a6f98a7..b5e32abc2 100644 --- a/server/helpers/custom-validators/plugins.ts +++ b/server/helpers/custom-validators/plugins.ts @@ -44,7 +44,7 @@ function isPluginHomepage (value: string) { return isUrlValid(value) } -function isStaticDirectoriesValid (staticDirs: any) { +function areStaticDirectoriesValid (staticDirs: any) { if (!exists(staticDirs) || typeof staticDirs !== 'object') return false for (const key of Object.keys(staticDirs)) { @@ -54,14 +54,24 @@ function isStaticDirectoriesValid (staticDirs: any) { return true } -function isClientScriptsValid (clientScripts: any[]) { +function areClientScriptsValid (clientScripts: any[]) { return isArray(clientScripts) && clientScripts.every(c => { return isSafePath(c.script) && isArray(c.scopes) }) } -function isCSSPathsValid (css: any[]) { +function areTranslationPathsValid (translations: any) { + if (!exists(translations) || typeof translations !== 'object') return false + + for (const key of Object.keys(translations)) { + if (!isSafePath(translations[key])) return false + } + + return true +} + +function areCSSPathsValid (css: any[]) { return isArray(css) && css.every(c => isSafePath(c)) } @@ -77,9 +87,10 @@ function isPackageJSONValid (packageJSON: PluginPackageJson, pluginType: PluginT exists(packageJSON.author) && isUrlValid(packageJSON.bugs) && (pluginType === PluginType.THEME || isSafePath(packageJSON.library)) && - isStaticDirectoriesValid(packageJSON.staticDirs) && - isCSSPathsValid(packageJSON.css) && - isClientScriptsValid(packageJSON.clientScripts) + areStaticDirectoriesValid(packageJSON.staticDirs) && + areCSSPathsValid(packageJSON.css) && + areClientScriptsValid(packageJSON.clientScripts) && + areTranslationPathsValid(packageJSON.translations) } function isLibraryCodeValid (library: any) { diff --git a/server/lib/plugins/plugin-manager.ts b/server/lib/plugins/plugin-manager.ts index 81554a09e..c9beae268 100644 --- a/server/lib/plugins/plugin-manager.ts +++ b/server/lib/plugins/plugin-manager.ts @@ -3,7 +3,11 @@ import { logger } from '../../helpers/logger' import { basename, join } from 'path' import { CONFIG } from '../../initializers/config' import { isLibraryCodeValid, isPackageJSONValid } from '../../helpers/custom-validators/plugins' -import { ClientScript, PluginPackageJson } from '../../../shared/models/plugins/plugin-package-json.model' +import { + ClientScript, + PluginPackageJson, + PluginTranslationPaths as PackagePluginTranslations +} from '../../../shared/models/plugins/plugin-package-json.model' import { createReadStream, createWriteStream } from 'fs' import { PLUGIN_GLOBAL_CSS_PATH, VIDEO_CATEGORIES, VIDEO_LANGUAGES, VIDEO_LICENCES } from '../../initializers/constants' import { PluginType } from '../../../shared/models/plugins/plugin.type' @@ -21,6 +25,7 @@ import { RegisterServerSettingOptions } from '../../../shared/models/plugins/reg import { PluginVideoLanguageManager } from '../../../shared/models/plugins/plugin-video-language-manager.model' import { PluginVideoCategoryManager } from '../../../shared/models/plugins/plugin-video-category-manager.model' import { PluginVideoLicenceManager } from '../../../shared/models/plugins/plugin-video-licence-manager.model' +import { PluginTranslation } from '../../../shared/models/plugins/plugin-translation.model' export interface RegisteredPlugin { npmName: string @@ -60,6 +65,10 @@ type UpdatedVideoConstant = { } } +type PluginLocalesTranslations = { + [ locale: string ]: PluginTranslation +} + export class PluginManager implements ServerHook { private static instance: PluginManager @@ -67,6 +76,7 @@ export class PluginManager implements ServerHook { private registeredPlugins: { [ name: string ]: RegisteredPlugin } = {} private settings: { [ name: string ]: RegisterServerSettingOptions[] } = {} private hooks: { [ name: string ]: HookInformationValue[] } = {} + private translations: PluginLocalesTranslations = {} private updatedVideoConstants: UpdatedVideoConstant = { language: {}, @@ -117,6 +127,10 @@ export class PluginManager implements ServerHook { return this.settings[npmName] || [] } + getTranslations (locale: string) { + return this.translations[locale] || {} + } + // ###################### Hooks ###################### async runHook (hookName: ServerHookName, result?: T, params?: any): Promise { @@ -173,6 +187,8 @@ export class PluginManager implements ServerHook { delete this.registeredPlugins[plugin.npmName] delete this.settings[plugin.npmName] + this.deleteTranslations(plugin.npmName) + if (plugin.type === PluginType.PLUGIN) { await plugin.unregister() @@ -312,6 +328,8 @@ export class PluginManager implements ServerHook { css: packageJSON.css, unregister: library ? library.unregister : undefined } + + await this.addTranslations(plugin, npmName, packageJSON.translations) } private async registerPlugin (plugin: PluginModel, pluginPath: string, packageJSON: PluginPackageJson) { @@ -337,6 +355,28 @@ export class PluginManager implements ServerHook { return library } + // ###################### Translations ###################### + + private async addTranslations (plugin: PluginModel, npmName: string, translationPaths: PackagePluginTranslations) { + for (const locale of Object.keys(translationPaths)) { + const path = translationPaths[locale] + const json = await readJSON(join(this.getPluginPath(plugin.name, plugin.type), path)) + + if (!this.translations[locale]) this.translations[locale] = {} + this.translations[locale][npmName] = json + + logger.info('Added locale %s of plugin %s.', locale, npmName) + } + } + + private deleteTranslations (npmName: string) { + for (const locale of Object.keys(this.translations)) { + delete this.translations[locale][npmName] + + logger.info('Deleted locale %s of plugin %s.', locale, npmName) + } + } + // ###################### CSS ###################### private resetCSSGlobalFile () { @@ -455,7 +495,7 @@ export class PluginManager implements ServerHook { deleteLanguage: (key: string) => this.deleteConstant({ npmName, type: 'language', obj: VIDEO_LANGUAGES, key }) } - const videoCategoryManager: PluginVideoCategoryManager= { + const videoCategoryManager: PluginVideoCategoryManager = { addCategory: (key: number, label: string) => this.addConstant({ npmName, type: 'category', obj: VIDEO_CATEGORIES, key, label }), deleteCategory: (key: number) => this.deleteConstant({ npmName, type: 'category', obj: VIDEO_CATEGORIES, key }) diff --git a/server/tests/fixtures/peertube-plugin-test-three/package.json b/server/tests/fixtures/peertube-plugin-test-three/package.json index 3f7819db3..41d4c93fe 100644 --- a/server/tests/fixtures/peertube-plugin-test-three/package.json +++ b/server/tests/fixtures/peertube-plugin-test-three/package.json @@ -15,5 +15,6 @@ "library": "./main.js", "staticDirs": {}, "css": [], - "clientScripts": [] + "clientScripts": [], + "translations": {} } diff --git a/server/tests/fixtures/peertube-plugin-test-two/languages/fr.json b/server/tests/fixtures/peertube-plugin-test-two/languages/fr.json new file mode 100644 index 000000000..52d8313df --- /dev/null +++ b/server/tests/fixtures/peertube-plugin-test-two/languages/fr.json @@ -0,0 +1,3 @@ +{ + "Hello world": "Bonjour le monde" +} diff --git a/server/tests/fixtures/peertube-plugin-test-two/languages/it.json b/server/tests/fixtures/peertube-plugin-test-two/languages/it.json new file mode 100644 index 000000000..9e187d83b --- /dev/null +++ b/server/tests/fixtures/peertube-plugin-test-two/languages/it.json @@ -0,0 +1,3 @@ +{ + "Hello world": "Ciao, mondo!" +} diff --git a/server/tests/fixtures/peertube-plugin-test-two/package.json b/server/tests/fixtures/peertube-plugin-test-two/package.json index 52ebb5ac1..926f2d69b 100644 --- a/server/tests/fixtures/peertube-plugin-test-two/package.json +++ b/server/tests/fixtures/peertube-plugin-test-two/package.json @@ -15,5 +15,9 @@ "library": "./main.js", "staticDirs": {}, "css": [], - "clientScripts": [] + "clientScripts": [], + "translations": { + "fr-FR": "./languages/fr.json", + "it-IT": "./languages/it.json" + } } diff --git a/server/tests/fixtures/peertube-plugin-test/languages/fr.json b/server/tests/fixtures/peertube-plugin-test/languages/fr.json new file mode 100644 index 000000000..9e52f7065 --- /dev/null +++ b/server/tests/fixtures/peertube-plugin-test/languages/fr.json @@ -0,0 +1,3 @@ +{ + "Hi": "Coucou" +} diff --git a/server/tests/fixtures/peertube-plugin-test/package.json b/server/tests/fixtures/peertube-plugin-test/package.json index 9d6fe5c90..108f21fd6 100644 --- a/server/tests/fixtures/peertube-plugin-test/package.json +++ b/server/tests/fixtures/peertube-plugin-test/package.json @@ -15,5 +15,8 @@ "library": "./main.js", "staticDirs": {}, "css": [], - "clientScripts": [] + "clientScripts": [], + "translations": { + "fr-FR": "./languages/fr.json" + } } diff --git a/server/tests/plugins/index.ts b/server/tests/plugins/index.ts index 95e358732..f41708055 100644 --- a/server/tests/plugins/index.ts +++ b/server/tests/plugins/index.ts @@ -1,3 +1,4 @@ import './action-hooks' import './filter-hooks' +import './translations' import './video-constants' diff --git a/server/tests/plugins/translations.ts b/server/tests/plugins/translations.ts new file mode 100644 index 000000000..88d91a033 --- /dev/null +++ b/server/tests/plugins/translations.ts @@ -0,0 +1,113 @@ +/* tslint:disable:no-unused-expression */ + +import * as chai from 'chai' +import 'mocha' +import { + cleanupTests, + flushAndRunMultipleServers, + flushAndRunServer, killallServers, reRunServer, + ServerInfo, + waitUntilLog +} from '../../../shared/extra-utils/server/servers' +import { + addVideoCommentReply, + addVideoCommentThread, + deleteVideoComment, + getPluginTestPath, + getVideosList, + installPlugin, + removeVideo, + setAccessTokensToServers, + updateVideo, + uploadVideo, + viewVideo, + getVideosListPagination, + getVideo, + getVideoCommentThreads, + getVideoThreadComments, + getVideoWithToken, + setDefaultVideoChannel, + waitJobs, + doubleFollow, getVideoLanguages, getVideoLicences, getVideoCategories, uninstallPlugin, getPluginTranslations +} from '../../../shared/extra-utils' +import { VideoCommentThreadTree } from '../../../shared/models/videos/video-comment.model' +import { VideoDetails } from '../../../shared/models/videos' +import { getYoutubeVideoUrl, importVideo } from '../../../shared/extra-utils/videos/video-imports' + +const expect = chai.expect + +describe('Test plugin translations', function () { + let server: ServerInfo + + before(async function () { + this.timeout(30000) + + server = await flushAndRunServer(1) + await setAccessTokensToServers([ server ]) + + await installPlugin({ + url: server.url, + accessToken: server.accessToken, + path: getPluginTestPath() + }) + + await installPlugin({ + url: server.url, + accessToken: server.accessToken, + path: getPluginTestPath('-two') + }) + }) + + it('Should not have translations for locale pt', async function () { + const res = await getPluginTranslations({ url: server.url, locale: 'pt' }) + + expect(res.body).to.deep.equal({}) + }) + + it('Should have translations for locale fr', async function () { + const res = await getPluginTranslations({ url: server.url, locale: 'fr-FR' }) + + expect(res.body).to.deep.equal({ + 'peertube-plugin-test': { + 'Hi': 'Coucou' + }, + 'peertube-plugin-test-two': { + 'Hello world': 'Bonjour le monde' + } + }) + }) + + it('Should have translations of locale it', async function () { + const res = await getPluginTranslations({ url: server.url, locale: 'it-IT' }) + + expect(res.body).to.deep.equal({ + 'peertube-plugin-test-two': { + 'Hello world': 'Ciao, mondo!' + } + }) + }) + + it('Should remove the plugin and remove the locales', async function () { + await uninstallPlugin({ url: server.url, accessToken: server.accessToken, npmName: 'peertube-plugin-test-two' }) + + { + const res = await getPluginTranslations({ url: server.url, locale: 'fr-FR' }) + + expect(res.body).to.deep.equal({ + 'peertube-plugin-test': { + 'Hi': 'Coucou' + } + }) + } + + { + const res = await getPluginTranslations({ url: server.url, locale: 'it-IT' }) + + expect(res.body).to.deep.equal({}) + } + }) + + after(async function () { + await cleanupTests([ server ]) + }) +}) -- cgit v1.2.3