aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorChocobozzz <me@florianbigard.com>2019-07-26 14:44:50 +0200
committerChocobozzz <me@florianbigard.com>2019-07-26 15:18:30 +0200
commitd75db01f14138ea660c4c519e37ab05228b39d13 (patch)
tree85a3da315ea6e1501fec5b70790482504dd64793
parentee286591a5b740702bad66c55cc900740f749e9a (diff)
downloadPeerTube-d75db01f14138ea660c4c519e37ab05228b39d13.tar.gz
PeerTube-d75db01f14138ea660c4c519e37ab05228b39d13.tar.zst
PeerTube-d75db01f14138ea660c4c519e37ab05228b39d13.zip
Add plugin translation system
-rw-r--r--client/src/app/+admin/plugins/shared/plugin-api.service.ts25
-rw-r--r--client/src/app/core/plugins/plugin.service.ts42
-rw-r--r--client/src/app/shared/video/video.service.ts1
-rw-r--r--client/src/types/register-client-option.model.ts12
-rw-r--r--server/controllers/plugins.ts20
-rw-r--r--server/helpers/custom-validators/plugins.ts23
-rw-r--r--server/lib/plugins/plugin-manager.ts44
-rw-r--r--server/tests/fixtures/peertube-plugin-test-three/package.json3
-rw-r--r--server/tests/fixtures/peertube-plugin-test-two/languages/fr.json3
-rw-r--r--server/tests/fixtures/peertube-plugin-test-two/languages/it.json3
-rw-r--r--server/tests/fixtures/peertube-plugin-test-two/package.json6
-rw-r--r--server/tests/fixtures/peertube-plugin-test/languages/fr.json3
-rw-r--r--server/tests/fixtures/peertube-plugin-test/package.json5
-rw-r--r--server/tests/plugins/index.ts1
-rw-r--r--server/tests/plugins/translations.ts113
-rw-r--r--shared/extra-utils/server/plugins.ts16
-rw-r--r--shared/models/plugins/plugin-package-json.model.ts6
-rw-r--r--shared/models/plugins/plugin-translation.model.ts5
18 files changed, 303 insertions, 28 deletions
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 @@
1import { catchError } from 'rxjs/operators' 1import { catchError, map, switchMap } from 'rxjs/operators'
2import { HttpClient, HttpParams } from '@angular/common/http' 2import { HttpClient, HttpParams } from '@angular/common/http'
3import { Injectable } from '@angular/core' 3import { Injectable } from '@angular/core'
4import { environment } from '../../../../environments/environment' 4import { environment } from '../../../../environments/environment'
@@ -6,13 +6,14 @@ import { RestExtractor, RestService } from '../../../shared'
6import { I18n } from '@ngx-translate/i18n-polyfill' 6import { I18n } from '@ngx-translate/i18n-polyfill'
7import { PluginType } from '@shared/models/plugins/plugin.type' 7import { PluginType } from '@shared/models/plugins/plugin.type'
8import { ComponentPagination } from '@app/shared/rest/component-pagination.model' 8import { ComponentPagination } from '@app/shared/rest/component-pagination.model'
9import { ResultList } from '@shared/models' 9import { peertubeTranslate, ResultList } from '@shared/models'
10import { PeerTubePlugin } from '@shared/models/plugins/peertube-plugin.model' 10import { PeerTubePlugin } from '@shared/models/plugins/peertube-plugin.model'
11import { ManagePlugin } from '@shared/models/plugins/manage-plugin.model' 11import { ManagePlugin } from '@shared/models/plugins/manage-plugin.model'
12import { InstallOrUpdatePlugin } from '@shared/models/plugins/install-plugin.model' 12import { InstallOrUpdatePlugin } from '@shared/models/plugins/install-plugin.model'
13import { PeerTubePluginIndex } from '@shared/models/plugins/peertube-plugin-index.model' 13import { PeerTubePluginIndex } from '@shared/models/plugins/peertube-plugin-index.model'
14import { RegisteredServerSettings, RegisterServerSettingOptions } from '@shared/models/plugins/register-server-setting.model' 14import { RegisteredServerSettings, RegisterServerSettingOptions } from '@shared/models/plugins/register-server-setting.model'
15import { PluginService } from '@app/core/plugins/plugin.service' 15import { PluginService } from '@app/core/plugins/plugin.service'
16import { Observable } from 'rxjs'
16 17
17@Injectable() 18@Injectable()
18export class PluginApiService { 19export class PluginApiService {
@@ -92,7 +93,10 @@ export class PluginApiService {
92 const path = PluginApiService.BASE_PLUGIN_URL + '/' + npmName + '/registered-settings' 93 const path = PluginApiService.BASE_PLUGIN_URL + '/' + npmName + '/registered-settings'
93 94
94 return this.authHttp.get<RegisteredServerSettings>(path) 95 return this.authHttp.get<RegisteredServerSettings>(path)
95 .pipe(catchError(res => this.restExtractor.handleError(res))) 96 .pipe(
97 switchMap(res => this.translateSettingsLabel(npmName, res)),
98 catchError(res => this.restExtractor.handleError(res))
99 )
96 } 100 }
97 101
98 updatePluginSettings (pluginName: string, pluginType: PluginType, settings: any) { 102 updatePluginSettings (pluginName: string, pluginType: PluginType, settings: any) {
@@ -129,4 +133,19 @@ export class PluginApiService {
129 return this.authHttp.post(PluginApiService.BASE_PLUGIN_URL + '/install', body) 133 return this.authHttp.post(PluginApiService.BASE_PLUGIN_URL + '/install', body)
130 .pipe(catchError(res => this.restExtractor.handleError(res))) 134 .pipe(catchError(res => this.restExtractor.handleError(res)))
131 } 135 }
136
137 private translateSettingsLabel (npmName: string, res: RegisteredServerSettings): Observable<RegisteredServerSettings> {
138 return this.pluginService.translationsObservable
139 .pipe(
140 map(allTranslations => allTranslations[npmName]),
141 map(translations => {
142 const registeredSettings = res.registeredSettings
143 .map(r => {
144 return Object.assign({}, r, { label: peertubeTranslate(r.label, translations) })
145 })
146
147 return { registeredSettings }
148 })
149 )
150 }
132} 151}
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 @@
1import { Injectable, NgZone } from '@angular/core' 1import { Inject, Injectable, LOCALE_ID, NgZone } from '@angular/core'
2import { Router } from '@angular/router' 2import { Router } from '@angular/router'
3import { ServerConfigPlugin } from '@shared/models' 3import { getCompleteLocale, isDefaultLocale, peertubeTranslate, ServerConfigPlugin } from '@shared/models'
4import { ServerService } from '@app/core/server/server.service' 4import { ServerService } from '@app/core/server/server.service'
5import { ClientScript } from '@shared/models/plugins/plugin-package-json.model' 5import { ClientScript } from '@shared/models/plugins/plugin-package-json.model'
6import { ClientScript as ClientScriptModule } from '../../../types/client-script.model' 6import { ClientScript as ClientScriptModule } from '../../../types/client-script.model'
7import { environment } from '../../../environments/environment' 7import { environment } from '../../../environments/environment'
8import { ReplaySubject } from 'rxjs' 8import { Observable, of, ReplaySubject } from 'rxjs'
9import { catchError, first, map, shareReplay } from 'rxjs/operators' 9import { catchError, first, map, shareReplay } from 'rxjs/operators'
10import { getHookType, internalRunHook } from '@shared/core-utils/plugins/hooks' 10import { getHookType, internalRunHook } from '@shared/core-utils/plugins/hooks'
11import { ClientHook, ClientHookName, clientHookObject } from '@shared/models/plugins/client-hook.model' 11import { ClientHook, ClientHookName, clientHookObject } from '@shared/models/plugins/client-hook.model'
@@ -15,6 +15,9 @@ import { HttpClient } from '@angular/common/http'
15import { RestExtractor } from '@app/shared/rest' 15import { RestExtractor } from '@app/shared/rest'
16import { PluginType } from '@shared/models/plugins/plugin.type' 16import { PluginType } from '@shared/models/plugins/plugin.type'
17import { PublicServerSetting } from '@shared/models/plugins/public-server.setting' 17import { PublicServerSetting } from '@shared/models/plugins/public-server.setting'
18import { getDevLocale, isOnDevLocale } from '@app/shared/i18n/i18n-utils'
19import { RegisterClientHelpers } from '../../../types/register-client-option.model'
20import { PluginTranslation } from '@shared/models/plugins/plugin-translation.model'
18 21
19interface HookStructValue extends RegisterClientHookOptions { 22interface HookStructValue extends RegisterClientHookOptions {
20 plugin: ServerConfigPlugin 23 plugin: ServerConfigPlugin
@@ -30,7 +33,8 @@ type PluginInfo = {
30 33
31@Injectable() 34@Injectable()
32export class PluginService implements ClientHook { 35export class PluginService implements ClientHook {
33 private static BASE_PLUGIN_URL = environment.apiUrl + '/api/v1/plugins' 36 private static BASE_PLUGIN_API_URL = environment.apiUrl + '/api/v1/plugins'
37 private static BASE_PLUGIN_URL = environment.apiUrl + '/plugins'
34 38
35 pluginsBuilt = new ReplaySubject<boolean>(1) 39 pluginsBuilt = new ReplaySubject<boolean>(1)
36 40
@@ -40,6 +44,8 @@ export class PluginService implements ClientHook {
40 'video-watch': new ReplaySubject<boolean>(1) 44 'video-watch': new ReplaySubject<boolean>(1)
41 } 45 }
42 46
47 translationsObservable: Observable<PluginTranslation>
48
43 private plugins: ServerConfigPlugin[] = [] 49 private plugins: ServerConfigPlugin[] = []
44 private scopes: { [ scopeName: string ]: PluginInfo[] } = {} 50 private scopes: { [ scopeName: string ]: PluginInfo[] } = {}
45 private loadedScripts: { [ script: string ]: boolean } = {} 51 private loadedScripts: { [ script: string ]: boolean } = {}
@@ -53,8 +59,10 @@ export class PluginService implements ClientHook {
53 private server: ServerService, 59 private server: ServerService,
54 private zone: NgZone, 60 private zone: NgZone,
55 private authHttp: HttpClient, 61 private authHttp: HttpClient,
56 private restExtractor: RestExtractor 62 private restExtractor: RestExtractor,
63 @Inject(LOCALE_ID) private localeId: string
57 ) { 64 ) {
65 this.loadTranslations()
58 } 66 }
59 67
60 initializePlugins () { 68 initializePlugins () {
@@ -235,8 +243,9 @@ export class PluginService implements ClientHook {
235 } 243 }
236 } 244 }
237 245
238 private buildPeerTubeHelpers (pluginInfo: PluginInfo) { 246 private buildPeerTubeHelpers (pluginInfo: PluginInfo): RegisterClientHelpers {
239 const { plugin } = pluginInfo 247 const { plugin } = pluginInfo
248 const npmName = this.nameToNpmName(pluginInfo.plugin.name, pluginInfo.pluginType)
240 249
241 return { 250 return {
242 getBaseStaticRoute: () => { 251 getBaseStaticRoute: () => {
@@ -245,8 +254,7 @@ export class PluginService implements ClientHook {
245 }, 254 },
246 255
247 getSettings: () => { 256 getSettings: () => {
248 const npmName = this.nameToNpmName(pluginInfo.plugin.name, pluginInfo.pluginType) 257 const path = PluginService.BASE_PLUGIN_API_URL + '/' + npmName + '/public-settings'
249 const path = PluginService.BASE_PLUGIN_URL + '/' + npmName + '/public-settings'
250 258
251 return this.authHttp.get<PublicServerSetting>(path) 259 return this.authHttp.get<PublicServerSetting>(path)
252 .pipe( 260 .pipe(
@@ -254,10 +262,28 @@ export class PluginService implements ClientHook {
254 catchError(res => this.restExtractor.handleError(res)) 262 catchError(res => this.restExtractor.handleError(res))
255 ) 263 )
256 .toPromise() 264 .toPromise()
265 },
266
267 translate: (value: string) => {
268 return this.translationsObservable
269 .pipe(map(allTranslations => allTranslations[npmName]))
270 .pipe(map(translations => peertubeTranslate(value, translations)))
271 .toPromise()
257 } 272 }
258 } 273 }
259 } 274 }
260 275
276 private loadTranslations () {
277 const completeLocale = isOnDevLocale() ? getDevLocale() : getCompleteLocale(this.localeId)
278
279 // Default locale, nothing to translate
280 if (isDefaultLocale(completeLocale)) this.translationsObservable = of({}).pipe(shareReplay())
281
282 this.translationsObservable = this.authHttp
283 .get<PluginTranslation>(PluginService.BASE_PLUGIN_URL + '/translations/' + completeLocale + '.json')
284 .pipe(shareReplay())
285 }
286
261 private getPluginPathPrefix (isTheme: boolean) { 287 private getPluginPathPrefix (isTheme: boolean) {
262 return isTheme ? '/themes' : '/plugins' 288 return isTheme ? '/themes' : '/plugins'
263 } 289 }
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'
31import { UserSubscriptionService } from '@app/shared/user-subscription/user-subscription.service' 31import { UserSubscriptionService } from '@app/shared/user-subscription/user-subscription.service'
32import { VideoChannel } from '@app/shared/video-channel/video-channel.model' 32import { VideoChannel } from '@app/shared/video-channel/video-channel.model'
33import { I18n } from '@ngx-translate/i18n-polyfill' 33import { I18n } from '@ngx-translate/i18n-polyfill'
34import { VideoPlaylist } from '@app/shared/video-playlist/video-playlist.model'
35import { VideoPlaylistService } from '@app/shared/video-playlist/video-playlist.service' 34import { VideoPlaylistService } from '@app/shared/video-playlist/video-playlist.service'
36 35
37export interface VideosProvider { 36export 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
3export type RegisterClientOptions = { 3export type RegisterClientOptions = {
4 registerHook: (options: RegisterClientHookOptions) => void 4 registerHook: (options: RegisterClientHookOptions) => void
5 5
6 peertubeHelpers: { 6 peertubeHelpers: RegisterClientHelpers
7 getBaseStaticRoute: () => string 7}
8
9export type RegisterClientHelpers = {
10 getBaseStaticRoute: () => string
11
12 getSettings: () => Promise<{ [ name: string ]: string }>
8 13
9 getSettings: () => Promise<{ [ name: string ]: string }> 14 translate: (toTranslate: string) => Promise<string>
10 }
11} 15}
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 @@
1import * as express from 'express' 1import * as express from 'express'
2import { PLUGIN_GLOBAL_CSS_PATH } from '../initializers/constants' 2import { PLUGIN_GLOBAL_CSS_PATH } from '../initializers/constants'
3import { join } from 'path' 3import { join } from 'path'
4import { RegisteredPlugin } from '../lib/plugins/plugin-manager' 4import { PluginManager, RegisteredPlugin } from '../lib/plugins/plugin-manager'
5import { servePluginStaticDirectoryValidator } from '../middlewares/validators/plugins' 5import { servePluginStaticDirectoryValidator } from '../middlewares/validators/plugins'
6import { serveThemeCSSValidator } from '../middlewares/validators/themes' 6import { serveThemeCSSValidator } from '../middlewares/validators/themes'
7import { PluginType } from '../../shared/models/plugins/plugin.type' 7import { PluginType } from '../../shared/models/plugins/plugin.type'
8import { isTestInstance } from '../helpers/core-utils' 8import { isTestInstance } from '../helpers/core-utils'
9import { getCompleteLocale, is18nLocale } from '../../shared/models/i18n'
9 10
10const sendFileOptions = { 11const sendFileOptions = {
11 maxAge: '30 days', 12 maxAge: '30 days',
@@ -18,6 +19,10 @@ pluginsRouter.get('/plugins/global.css',
18 servePluginGlobalCSS 19 servePluginGlobalCSS
19) 20)
20 21
22pluginsRouter.get('/plugins/translations/:locale.json',
23 getPluginTranslations
24)
25
21pluginsRouter.get('/plugins/:pluginName/:pluginVersion/static/:staticEndpoint(*)', 26pluginsRouter.get('/plugins/:pluginName/:pluginVersion/static/:staticEndpoint(*)',
22 servePluginStaticDirectoryValidator(PluginType.PLUGIN), 27 servePluginStaticDirectoryValidator(PluginType.PLUGIN),
23 servePluginStaticDirectory 28 servePluginStaticDirectory
@@ -60,6 +65,19 @@ function servePluginGlobalCSS (req: express.Request, res: express.Response) {
60 return res.sendFile(PLUGIN_GLOBAL_CSS_PATH, globalCSSOptions) 65 return res.sendFile(PLUGIN_GLOBAL_CSS_PATH, globalCSSOptions)
61} 66}
62 67
68function getPluginTranslations (req: express.Request, res: express.Response) {
69 const locale = req.params.locale
70
71 if (is18nLocale(locale)) {
72 const completeLocale = getCompleteLocale(locale)
73 const json = PluginManager.Instance.getTranslations(completeLocale)
74
75 return res.json(json)
76 }
77
78 return res.sendStatus(404)
79}
80
63function servePluginStaticDirectory (req: express.Request, res: express.Response) { 81function servePluginStaticDirectory (req: express.Request, res: express.Response) {
64 const plugin: RegisteredPlugin = res.locals.registeredPlugin 82 const plugin: RegisteredPlugin = res.locals.registeredPlugin
65 const staticEndpoint = req.params.staticEndpoint 83 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) {
44 return isUrlValid(value) 44 return isUrlValid(value)
45} 45}
46 46
47function isStaticDirectoriesValid (staticDirs: any) { 47function areStaticDirectoriesValid (staticDirs: any) {
48 if (!exists(staticDirs) || typeof staticDirs !== 'object') return false 48 if (!exists(staticDirs) || typeof staticDirs !== 'object') return false
49 49
50 for (const key of Object.keys(staticDirs)) { 50 for (const key of Object.keys(staticDirs)) {
@@ -54,14 +54,24 @@ function isStaticDirectoriesValid (staticDirs: any) {
54 return true 54 return true
55} 55}
56 56
57function isClientScriptsValid (clientScripts: any[]) { 57function areClientScriptsValid (clientScripts: any[]) {
58 return isArray(clientScripts) && 58 return isArray(clientScripts) &&
59 clientScripts.every(c => { 59 clientScripts.every(c => {
60 return isSafePath(c.script) && isArray(c.scopes) 60 return isSafePath(c.script) && isArray(c.scopes)
61 }) 61 })
62} 62}
63 63
64function isCSSPathsValid (css: any[]) { 64function areTranslationPathsValid (translations: any) {
65 if (!exists(translations) || typeof translations !== 'object') return false
66
67 for (const key of Object.keys(translations)) {
68 if (!isSafePath(translations[key])) return false
69 }
70
71 return true
72}
73
74function areCSSPathsValid (css: any[]) {
65 return isArray(css) && css.every(c => isSafePath(c)) 75 return isArray(css) && css.every(c => isSafePath(c))
66} 76}
67 77
@@ -77,9 +87,10 @@ function isPackageJSONValid (packageJSON: PluginPackageJson, pluginType: PluginT
77 exists(packageJSON.author) && 87 exists(packageJSON.author) &&
78 isUrlValid(packageJSON.bugs) && 88 isUrlValid(packageJSON.bugs) &&
79 (pluginType === PluginType.THEME || isSafePath(packageJSON.library)) && 89 (pluginType === PluginType.THEME || isSafePath(packageJSON.library)) &&
80 isStaticDirectoriesValid(packageJSON.staticDirs) && 90 areStaticDirectoriesValid(packageJSON.staticDirs) &&
81 isCSSPathsValid(packageJSON.css) && 91 areCSSPathsValid(packageJSON.css) &&
82 isClientScriptsValid(packageJSON.clientScripts) 92 areClientScriptsValid(packageJSON.clientScripts) &&
93 areTranslationPathsValid(packageJSON.translations)
83} 94}
84 95
85function isLibraryCodeValid (library: any) { 96function 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'
3import { basename, join } from 'path' 3import { basename, join } from 'path'
4import { CONFIG } from '../../initializers/config' 4import { CONFIG } from '../../initializers/config'
5import { isLibraryCodeValid, isPackageJSONValid } from '../../helpers/custom-validators/plugins' 5import { isLibraryCodeValid, isPackageJSONValid } from '../../helpers/custom-validators/plugins'
6import { ClientScript, PluginPackageJson } from '../../../shared/models/plugins/plugin-package-json.model' 6import {
7 ClientScript,
8 PluginPackageJson,
9 PluginTranslationPaths as PackagePluginTranslations
10} from '../../../shared/models/plugins/plugin-package-json.model'
7import { createReadStream, createWriteStream } from 'fs' 11import { createReadStream, createWriteStream } from 'fs'
8import { PLUGIN_GLOBAL_CSS_PATH, VIDEO_CATEGORIES, VIDEO_LANGUAGES, VIDEO_LICENCES } from '../../initializers/constants' 12import { PLUGIN_GLOBAL_CSS_PATH, VIDEO_CATEGORIES, VIDEO_LANGUAGES, VIDEO_LICENCES } from '../../initializers/constants'
9import { PluginType } from '../../../shared/models/plugins/plugin.type' 13import { PluginType } from '../../../shared/models/plugins/plugin.type'
@@ -21,6 +25,7 @@ import { RegisterServerSettingOptions } from '../../../shared/models/plugins/reg
21import { PluginVideoLanguageManager } from '../../../shared/models/plugins/plugin-video-language-manager.model' 25import { PluginVideoLanguageManager } from '../../../shared/models/plugins/plugin-video-language-manager.model'
22import { PluginVideoCategoryManager } from '../../../shared/models/plugins/plugin-video-category-manager.model' 26import { PluginVideoCategoryManager } from '../../../shared/models/plugins/plugin-video-category-manager.model'
23import { PluginVideoLicenceManager } from '../../../shared/models/plugins/plugin-video-licence-manager.model' 27import { PluginVideoLicenceManager } from '../../../shared/models/plugins/plugin-video-licence-manager.model'
28import { PluginTranslation } from '../../../shared/models/plugins/plugin-translation.model'
24 29
25export interface RegisteredPlugin { 30export interface RegisteredPlugin {
26 npmName: string 31 npmName: string
@@ -60,6 +65,10 @@ type UpdatedVideoConstant = {
60 } 65 }
61} 66}
62 67
68type PluginLocalesTranslations = {
69 [ locale: string ]: PluginTranslation
70}
71
63export class PluginManager implements ServerHook { 72export class PluginManager implements ServerHook {
64 73
65 private static instance: PluginManager 74 private static instance: PluginManager
@@ -67,6 +76,7 @@ export class PluginManager implements ServerHook {
67 private registeredPlugins: { [ name: string ]: RegisteredPlugin } = {} 76 private registeredPlugins: { [ name: string ]: RegisteredPlugin } = {}
68 private settings: { [ name: string ]: RegisterServerSettingOptions[] } = {} 77 private settings: { [ name: string ]: RegisterServerSettingOptions[] } = {}
69 private hooks: { [ name: string ]: HookInformationValue[] } = {} 78 private hooks: { [ name: string ]: HookInformationValue[] } = {}
79 private translations: PluginLocalesTranslations = {}
70 80
71 private updatedVideoConstants: UpdatedVideoConstant = { 81 private updatedVideoConstants: UpdatedVideoConstant = {
72 language: {}, 82 language: {},
@@ -117,6 +127,10 @@ export class PluginManager implements ServerHook {
117 return this.settings[npmName] || [] 127 return this.settings[npmName] || []
118 } 128 }
119 129
130 getTranslations (locale: string) {
131 return this.translations[locale] || {}
132 }
133
120 // ###################### Hooks ###################### 134 // ###################### Hooks ######################
121 135
122 async runHook <T> (hookName: ServerHookName, result?: T, params?: any): Promise<T> { 136 async runHook <T> (hookName: ServerHookName, result?: T, params?: any): Promise<T> {
@@ -173,6 +187,8 @@ export class PluginManager implements ServerHook {
173 delete this.registeredPlugins[plugin.npmName] 187 delete this.registeredPlugins[plugin.npmName]
174 delete this.settings[plugin.npmName] 188 delete this.settings[plugin.npmName]
175 189
190 this.deleteTranslations(plugin.npmName)
191
176 if (plugin.type === PluginType.PLUGIN) { 192 if (plugin.type === PluginType.PLUGIN) {
177 await plugin.unregister() 193 await plugin.unregister()
178 194
@@ -312,6 +328,8 @@ export class PluginManager implements ServerHook {
312 css: packageJSON.css, 328 css: packageJSON.css,
313 unregister: library ? library.unregister : undefined 329 unregister: library ? library.unregister : undefined
314 } 330 }
331
332 await this.addTranslations(plugin, npmName, packageJSON.translations)
315 } 333 }
316 334
317 private async registerPlugin (plugin: PluginModel, pluginPath: string, packageJSON: PluginPackageJson) { 335 private async registerPlugin (plugin: PluginModel, pluginPath: string, packageJSON: PluginPackageJson) {
@@ -337,6 +355,28 @@ export class PluginManager implements ServerHook {
337 return library 355 return library
338 } 356 }
339 357
358 // ###################### Translations ######################
359
360 private async addTranslations (plugin: PluginModel, npmName: string, translationPaths: PackagePluginTranslations) {
361 for (const locale of Object.keys(translationPaths)) {
362 const path = translationPaths[locale]
363 const json = await readJSON(join(this.getPluginPath(plugin.name, plugin.type), path))
364
365 if (!this.translations[locale]) this.translations[locale] = {}
366 this.translations[locale][npmName] = json
367
368 logger.info('Added locale %s of plugin %s.', locale, npmName)
369 }
370 }
371
372 private deleteTranslations (npmName: string) {
373 for (const locale of Object.keys(this.translations)) {
374 delete this.translations[locale][npmName]
375
376 logger.info('Deleted locale %s of plugin %s.', locale, npmName)
377 }
378 }
379
340 // ###################### CSS ###################### 380 // ###################### CSS ######################
341 381
342 private resetCSSGlobalFile () { 382 private resetCSSGlobalFile () {
@@ -455,7 +495,7 @@ export class PluginManager implements ServerHook {
455 deleteLanguage: (key: string) => this.deleteConstant({ npmName, type: 'language', obj: VIDEO_LANGUAGES, key }) 495 deleteLanguage: (key: string) => this.deleteConstant({ npmName, type: 'language', obj: VIDEO_LANGUAGES, key })
456 } 496 }
457 497
458 const videoCategoryManager: PluginVideoCategoryManager= { 498 const videoCategoryManager: PluginVideoCategoryManager = {
459 addCategory: (key: number, label: string) => this.addConstant({ npmName, type: 'category', obj: VIDEO_CATEGORIES, key, label }), 499 addCategory: (key: number, label: string) => this.addConstant({ npmName, type: 'category', obj: VIDEO_CATEGORIES, key, label }),
460 500
461 deleteCategory: (key: number) => this.deleteConstant({ npmName, type: 'category', obj: VIDEO_CATEGORIES, key }) 501 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 @@
15 "library": "./main.js", 15 "library": "./main.js",
16 "staticDirs": {}, 16 "staticDirs": {},
17 "css": [], 17 "css": [],
18 "clientScripts": [] 18 "clientScripts": [],
19 "translations": {}
19} 20}
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 @@
1{
2 "Hello world": "Bonjour le monde"
3}
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 @@
1{
2 "Hello world": "Ciao, mondo!"
3}
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 @@
15 "library": "./main.js", 15 "library": "./main.js",
16 "staticDirs": {}, 16 "staticDirs": {},
17 "css": [], 17 "css": [],
18 "clientScripts": [] 18 "clientScripts": [],
19 "translations": {
20 "fr-FR": "./languages/fr.json",
21 "it-IT": "./languages/it.json"
22 }
19} 23}
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 @@
1{
2 "Hi": "Coucou"
3}
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 @@
15 "library": "./main.js", 15 "library": "./main.js",
16 "staticDirs": {}, 16 "staticDirs": {},
17 "css": [], 17 "css": [],
18 "clientScripts": [] 18 "clientScripts": [],
19 "translations": {
20 "fr-FR": "./languages/fr.json"
21 }
19} 22}
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 @@
1import './action-hooks' 1import './action-hooks'
2import './filter-hooks' 2import './filter-hooks'
3import './translations'
3import './video-constants' 4import './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 @@
1/* tslint:disable:no-unused-expression */
2
3import * as chai from 'chai'
4import 'mocha'
5import {
6 cleanupTests,
7 flushAndRunMultipleServers,
8 flushAndRunServer, killallServers, reRunServer,
9 ServerInfo,
10 waitUntilLog
11} from '../../../shared/extra-utils/server/servers'
12import {
13 addVideoCommentReply,
14 addVideoCommentThread,
15 deleteVideoComment,
16 getPluginTestPath,
17 getVideosList,
18 installPlugin,
19 removeVideo,
20 setAccessTokensToServers,
21 updateVideo,
22 uploadVideo,
23 viewVideo,
24 getVideosListPagination,
25 getVideo,
26 getVideoCommentThreads,
27 getVideoThreadComments,
28 getVideoWithToken,
29 setDefaultVideoChannel,
30 waitJobs,
31 doubleFollow, getVideoLanguages, getVideoLicences, getVideoCategories, uninstallPlugin, getPluginTranslations
32} from '../../../shared/extra-utils'
33import { VideoCommentThreadTree } from '../../../shared/models/videos/video-comment.model'
34import { VideoDetails } from '../../../shared/models/videos'
35import { getYoutubeVideoUrl, importVideo } from '../../../shared/extra-utils/videos/video-imports'
36
37const expect = chai.expect
38
39describe('Test plugin translations', function () {
40 let server: ServerInfo
41
42 before(async function () {
43 this.timeout(30000)
44
45 server = await flushAndRunServer(1)
46 await setAccessTokensToServers([ server ])
47
48 await installPlugin({
49 url: server.url,
50 accessToken: server.accessToken,
51 path: getPluginTestPath()
52 })
53
54 await installPlugin({
55 url: server.url,
56 accessToken: server.accessToken,
57 path: getPluginTestPath('-two')
58 })
59 })
60
61 it('Should not have translations for locale pt', async function () {
62 const res = await getPluginTranslations({ url: server.url, locale: 'pt' })
63
64 expect(res.body).to.deep.equal({})
65 })
66
67 it('Should have translations for locale fr', async function () {
68 const res = await getPluginTranslations({ url: server.url, locale: 'fr-FR' })
69
70 expect(res.body).to.deep.equal({
71 'peertube-plugin-test': {
72 'Hi': 'Coucou'
73 },
74 'peertube-plugin-test-two': {
75 'Hello world': 'Bonjour le monde'
76 }
77 })
78 })
79
80 it('Should have translations of locale it', async function () {
81 const res = await getPluginTranslations({ url: server.url, locale: 'it-IT' })
82
83 expect(res.body).to.deep.equal({
84 'peertube-plugin-test-two': {
85 'Hello world': 'Ciao, mondo!'
86 }
87 })
88 })
89
90 it('Should remove the plugin and remove the locales', async function () {
91 await uninstallPlugin({ url: server.url, accessToken: server.accessToken, npmName: 'peertube-plugin-test-two' })
92
93 {
94 const res = await getPluginTranslations({ url: server.url, locale: 'fr-FR' })
95
96 expect(res.body).to.deep.equal({
97 'peertube-plugin-test': {
98 'Hi': 'Coucou'
99 }
100 })
101 }
102
103 {
104 const res = await getPluginTranslations({ url: server.url, locale: 'it-IT' })
105
106 expect(res.body).to.deep.equal({})
107 }
108 })
109
110 after(async function () {
111 await cleanupTests([ server ])
112 })
113})
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: {
134 }) 134 })
135} 135}
136 136
137function getPluginTranslations (parameters: {
138 url: string,
139 locale: string,
140 expectedStatus?: number
141}) {
142 const { url, locale, expectedStatus = 200 } = parameters
143 const path = '/plugins/translations/' + locale + '.json'
144
145 return makeGetRequest({
146 url,
147 path,
148 statusCodeExpected: expectedStatus
149 })
150}
151
137function installPlugin (parameters: { 152function installPlugin (parameters: {
138 url: string, 153 url: string,
139 accessToken: string, 154 accessToken: string,
@@ -224,6 +239,7 @@ export {
224 listPlugins, 239 listPlugins,
225 listAvailablePlugins, 240 listAvailablePlugins,
226 installPlugin, 241 installPlugin,
242 getPluginTranslations,
227 getPluginsCSS, 243 getPluginsCSS,
228 updatePlugin, 244 updatePlugin,
229 getPlugin, 245 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 @@
1import { PluginClientScope } from './plugin-client-scope.type' 1import { PluginClientScope } from './plugin-client-scope.type'
2 2
3export type PluginTranslationPaths = {
4 [ locale: string ]: string
5}
6
3export type ClientScript = { 7export type ClientScript = {
4 script: string, 8 script: string,
5 scopes: PluginClientScope[] 9 scopes: PluginClientScope[]
@@ -20,4 +24,6 @@ export type PluginPackageJson = {
20 css: string[] 24 css: string[]
21 25
22 clientScripts: ClientScript[] 26 clientScripts: ClientScript[]
27
28 translations: PluginTranslationPaths
23} 29}
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 @@
1export type PluginTranslation = {
2 [ npmName: string ]: {
3 [ key: string ]: string
4 }
5}