From d75db01f14138ea660c4c519e37ab05228b39d13 Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Fri, 26 Jul 2019 14:44:50 +0200 Subject: [PATCH] Add plugin translation system --- .../plugins/shared/plugin-api.service.ts | 25 +++- client/src/app/core/plugins/plugin.service.ts | 42 +++++-- client/src/app/shared/video/video.service.ts | 1 - .../src/types/register-client-option.model.ts | 12 +- 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 +- .../languages/fr.json | 3 + .../languages/it.json | 3 + .../peertube-plugin-test-two/package.json | 6 +- .../peertube-plugin-test/languages/fr.json | 3 + .../peertube-plugin-test/package.json | 5 +- server/tests/plugins/index.ts | 1 + server/tests/plugins/translations.ts | 113 ++++++++++++++++++ shared/extra-utils/server/plugins.ts | 16 +++ .../plugins/plugin-package-json.model.ts | 6 + .../plugins/plugin-translation.model.ts | 5 + 18 files changed, 303 insertions(+), 28 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 create mode 100644 shared/models/plugins/plugin-translation.model.ts diff --git a/client/src/app/+admin/plugins/shared/plugin-api.service.ts b/client/src/app/+admin/plugins/shared/plugin-api.service.ts index c360fc1b3..f6ef68e9c 100644 --- a/client/src/app/+admin/plugins/shared/plugin-api.service.ts +++ b/client/src/app/+admin/plugins/shared/plugin-api.service.ts @@ -1,4 +1,4 @@ -import { catchError } from 'rxjs/operators' +import { catchError, map, switchMap } from 'rxjs/operators' import { HttpClient, HttpParams } from '@angular/common/http' import { Injectable } from '@angular/core' import { environment } from '../../../../environments/environment' @@ -6,13 +6,14 @@ import { RestExtractor, RestService } from '../../../shared' import { I18n } from '@ngx-translate/i18n-polyfill' import { PluginType } from '@shared/models/plugins/plugin.type' import { ComponentPagination } from '@app/shared/rest/component-pagination.model' -import { ResultList } from '@shared/models' +import { peertubeTranslate, ResultList } from '@shared/models' import { PeerTubePlugin } from '@shared/models/plugins/peertube-plugin.model' import { ManagePlugin } from '@shared/models/plugins/manage-plugin.model' import { InstallOrUpdatePlugin } from '@shared/models/plugins/install-plugin.model' import { PeerTubePluginIndex } from '@shared/models/plugins/peertube-plugin-index.model' import { RegisteredServerSettings, RegisterServerSettingOptions } from '@shared/models/plugins/register-server-setting.model' import { PluginService } from '@app/core/plugins/plugin.service' +import { Observable } from 'rxjs' @Injectable() export class PluginApiService { @@ -92,7 +93,10 @@ export class PluginApiService { const path = PluginApiService.BASE_PLUGIN_URL + '/' + npmName + '/registered-settings' return this.authHttp.get(path) - .pipe(catchError(res => this.restExtractor.handleError(res))) + .pipe( + switchMap(res => this.translateSettingsLabel(npmName, res)), + catchError(res => this.restExtractor.handleError(res)) + ) } updatePluginSettings (pluginName: string, pluginType: PluginType, settings: any) { @@ -129,4 +133,19 @@ export class PluginApiService { return this.authHttp.post(PluginApiService.BASE_PLUGIN_URL + '/install', body) .pipe(catchError(res => this.restExtractor.handleError(res))) } + + private translateSettingsLabel (npmName: string, res: RegisteredServerSettings): Observable { + return this.pluginService.translationsObservable + .pipe( + map(allTranslations => allTranslations[npmName]), + map(translations => { + const registeredSettings = res.registeredSettings + .map(r => { + return Object.assign({}, r, { label: peertubeTranslate(r.label, translations) }) + }) + + return { registeredSettings } + }) + ) + } } diff --git a/client/src/app/core/plugins/plugin.service.ts b/client/src/app/core/plugins/plugin.service.ts index cca779177..3bb82e8a9 100644 --- a/client/src/app/core/plugins/plugin.service.ts +++ b/client/src/app/core/plugins/plugin.service.ts @@ -1,11 +1,11 @@ -import { Injectable, NgZone } from '@angular/core' +import { Inject, Injectable, LOCALE_ID, NgZone } from '@angular/core' import { Router } from '@angular/router' -import { ServerConfigPlugin } from '@shared/models' +import { getCompleteLocale, isDefaultLocale, peertubeTranslate, ServerConfigPlugin } from '@shared/models' import { ServerService } from '@app/core/server/server.service' import { ClientScript } from '@shared/models/plugins/plugin-package-json.model' import { ClientScript as ClientScriptModule } from '../../../types/client-script.model' import { environment } from '../../../environments/environment' -import { ReplaySubject } from 'rxjs' +import { Observable, of, ReplaySubject } from 'rxjs' import { catchError, first, map, shareReplay } from 'rxjs/operators' import { getHookType, internalRunHook } from '@shared/core-utils/plugins/hooks' import { ClientHook, ClientHookName, clientHookObject } from '@shared/models/plugins/client-hook.model' @@ -15,6 +15,9 @@ import { HttpClient } from '@angular/common/http' import { RestExtractor } from '@app/shared/rest' import { PluginType } from '@shared/models/plugins/plugin.type' import { PublicServerSetting } from '@shared/models/plugins/public-server.setting' +import { getDevLocale, isOnDevLocale } from '@app/shared/i18n/i18n-utils' +import { RegisterClientHelpers } from '../../../types/register-client-option.model' +import { PluginTranslation } from '@shared/models/plugins/plugin-translation.model' interface HookStructValue extends RegisterClientHookOptions { plugin: ServerConfigPlugin @@ -30,7 +33,8 @@ type PluginInfo = { @Injectable() export class PluginService implements ClientHook { - private static BASE_PLUGIN_URL = environment.apiUrl + '/api/v1/plugins' + private static BASE_PLUGIN_API_URL = environment.apiUrl + '/api/v1/plugins' + private static BASE_PLUGIN_URL = environment.apiUrl + '/plugins' pluginsBuilt = new ReplaySubject(1) @@ -40,6 +44,8 @@ export class PluginService implements ClientHook { 'video-watch': new ReplaySubject(1) } + translationsObservable: Observable + private plugins: ServerConfigPlugin[] = [] private scopes: { [ scopeName: string ]: PluginInfo[] } = {} private loadedScripts: { [ script: string ]: boolean } = {} @@ -53,8 +59,10 @@ export class PluginService implements ClientHook { private server: ServerService, private zone: NgZone, private authHttp: HttpClient, - private restExtractor: RestExtractor + private restExtractor: RestExtractor, + @Inject(LOCALE_ID) private localeId: string ) { + this.loadTranslations() } initializePlugins () { @@ -235,8 +243,9 @@ export class PluginService implements ClientHook { } } - private buildPeerTubeHelpers (pluginInfo: PluginInfo) { + private buildPeerTubeHelpers (pluginInfo: PluginInfo): RegisterClientHelpers { const { plugin } = pluginInfo + const npmName = this.nameToNpmName(pluginInfo.plugin.name, pluginInfo.pluginType) return { getBaseStaticRoute: () => { @@ -245,8 +254,7 @@ export class PluginService implements ClientHook { }, getSettings: () => { - const npmName = this.nameToNpmName(pluginInfo.plugin.name, pluginInfo.pluginType) - const path = PluginService.BASE_PLUGIN_URL + '/' + npmName + '/public-settings' + const path = PluginService.BASE_PLUGIN_API_URL + '/' + npmName + '/public-settings' return this.authHttp.get(path) .pipe( @@ -254,10 +262,28 @@ export class PluginService implements ClientHook { catchError(res => this.restExtractor.handleError(res)) ) .toPromise() + }, + + translate: (value: string) => { + return this.translationsObservable + .pipe(map(allTranslations => allTranslations[npmName])) + .pipe(map(translations => peertubeTranslate(value, translations))) + .toPromise() } } } + private loadTranslations () { + const completeLocale = isOnDevLocale() ? getDevLocale() : getCompleteLocale(this.localeId) + + // Default locale, nothing to translate + if (isDefaultLocale(completeLocale)) this.translationsObservable = of({}).pipe(shareReplay()) + + this.translationsObservable = this.authHttp + .get(PluginService.BASE_PLUGIN_URL + '/translations/' + completeLocale + '.json') + .pipe(shareReplay()) + } + private getPluginPathPrefix (isTheme: boolean) { return isTheme ? '/themes' : '/plugins' } diff --git a/client/src/app/shared/video/video.service.ts b/client/src/app/shared/video/video.service.ts index d1af13c93..114b014ad 100644 --- a/client/src/app/shared/video/video.service.ts +++ b/client/src/app/shared/video/video.service.ts @@ -31,7 +31,6 @@ import { ServerService } from '@app/core' import { UserSubscriptionService } from '@app/shared/user-subscription/user-subscription.service' import { VideoChannel } from '@app/shared/video-channel/video-channel.model' import { I18n } from '@ngx-translate/i18n-polyfill' -import { VideoPlaylist } from '@app/shared/video-playlist/video-playlist.model' import { VideoPlaylistService } from '@app/shared/video-playlist/video-playlist.service' export interface VideosProvider { diff --git a/client/src/types/register-client-option.model.ts b/client/src/types/register-client-option.model.ts index 473c2500f..243d74dea 100644 --- a/client/src/types/register-client-option.model.ts +++ b/client/src/types/register-client-option.model.ts @@ -3,9 +3,13 @@ import { RegisterClientHookOptions } from '@shared/models/plugins/register-clien export type RegisterClientOptions = { registerHook: (options: RegisterClientHookOptions) => void - peertubeHelpers: { - getBaseStaticRoute: () => string + peertubeHelpers: RegisterClientHelpers +} + +export type RegisterClientHelpers = { + getBaseStaticRoute: () => string + + getSettings: () => Promise<{ [ name: string ]: string }> - getSettings: () => Promise<{ [ name: string ]: string }> - } + translate: (toTranslate: string) => Promise } 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 ]) + }) +}) diff --git a/shared/extra-utils/server/plugins.ts b/shared/extra-utils/server/plugins.ts index 65d37d69f..5c0d1e511 100644 --- a/shared/extra-utils/server/plugins.ts +++ b/shared/extra-utils/server/plugins.ts @@ -134,6 +134,21 @@ function getPublicSettings (parameters: { }) } +function getPluginTranslations (parameters: { + url: string, + locale: string, + expectedStatus?: number +}) { + const { url, locale, expectedStatus = 200 } = parameters + const path = '/plugins/translations/' + locale + '.json' + + return makeGetRequest({ + url, + path, + statusCodeExpected: expectedStatus + }) +} + function installPlugin (parameters: { url: string, accessToken: string, @@ -224,6 +239,7 @@ export { listPlugins, listAvailablePlugins, installPlugin, + getPluginTranslations, getPluginsCSS, updatePlugin, getPlugin, diff --git a/shared/models/plugins/plugin-package-json.model.ts b/shared/models/plugins/plugin-package-json.model.ts index 87a48e97f..3f3077671 100644 --- a/shared/models/plugins/plugin-package-json.model.ts +++ b/shared/models/plugins/plugin-package-json.model.ts @@ -1,5 +1,9 @@ import { PluginClientScope } from './plugin-client-scope.type' +export type PluginTranslationPaths = { + [ locale: string ]: string +} + export type ClientScript = { script: string, scopes: PluginClientScope[] @@ -20,4 +24,6 @@ export type PluginPackageJson = { css: string[] clientScripts: ClientScript[] + + translations: PluginTranslationPaths } diff --git a/shared/models/plugins/plugin-translation.model.ts b/shared/models/plugins/plugin-translation.model.ts new file mode 100644 index 000000000..a2dd8e560 --- /dev/null +++ b/shared/models/plugins/plugin-translation.model.ts @@ -0,0 +1,5 @@ +export type PluginTranslation = { + [ npmName: string ]: { + [ key: string ]: string + } +} -- 2.41.0