diff options
author | Chocobozzz <me@florianbigard.com> | 2019-07-26 14:44:50 +0200 |
---|---|---|
committer | Chocobozzz <me@florianbigard.com> | 2019-07-26 15:18:30 +0200 |
commit | d75db01f14138ea660c4c519e37ab05228b39d13 (patch) | |
tree | 85a3da315ea6e1501fec5b70790482504dd64793 | |
parent | ee286591a5b740702bad66c55cc900740f749e9a (diff) | |
download | PeerTube-d75db01f14138ea660c4c519e37ab05228b39d13.tar.gz PeerTube-d75db01f14138ea660c4c519e37ab05228b39d13.tar.zst PeerTube-d75db01f14138ea660c4c519e37ab05228b39d13.zip |
Add plugin translation system
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 @@ | |||
1 | import { catchError } from 'rxjs/operators' | 1 | import { catchError, map, switchMap } from 'rxjs/operators' |
2 | import { HttpClient, HttpParams } from '@angular/common/http' | 2 | import { HttpClient, HttpParams } from '@angular/common/http' |
3 | import { Injectable } from '@angular/core' | 3 | import { Injectable } from '@angular/core' |
4 | import { environment } from '../../../../environments/environment' | 4 | import { environment } from '../../../../environments/environment' |
@@ -6,13 +6,14 @@ import { RestExtractor, RestService } from '../../../shared' | |||
6 | import { I18n } from '@ngx-translate/i18n-polyfill' | 6 | import { I18n } from '@ngx-translate/i18n-polyfill' |
7 | import { PluginType } from '@shared/models/plugins/plugin.type' | 7 | import { PluginType } from '@shared/models/plugins/plugin.type' |
8 | import { ComponentPagination } from '@app/shared/rest/component-pagination.model' | 8 | import { ComponentPagination } from '@app/shared/rest/component-pagination.model' |
9 | import { ResultList } from '@shared/models' | 9 | import { peertubeTranslate, ResultList } from '@shared/models' |
10 | import { PeerTubePlugin } from '@shared/models/plugins/peertube-plugin.model' | 10 | import { PeerTubePlugin } from '@shared/models/plugins/peertube-plugin.model' |
11 | import { ManagePlugin } from '@shared/models/plugins/manage-plugin.model' | 11 | import { ManagePlugin } from '@shared/models/plugins/manage-plugin.model' |
12 | import { InstallOrUpdatePlugin } from '@shared/models/plugins/install-plugin.model' | 12 | import { InstallOrUpdatePlugin } from '@shared/models/plugins/install-plugin.model' |
13 | import { PeerTubePluginIndex } from '@shared/models/plugins/peertube-plugin-index.model' | 13 | import { PeerTubePluginIndex } from '@shared/models/plugins/peertube-plugin-index.model' |
14 | import { RegisteredServerSettings, RegisterServerSettingOptions } from '@shared/models/plugins/register-server-setting.model' | 14 | import { RegisteredServerSettings, RegisterServerSettingOptions } from '@shared/models/plugins/register-server-setting.model' |
15 | import { PluginService } from '@app/core/plugins/plugin.service' | 15 | import { PluginService } from '@app/core/plugins/plugin.service' |
16 | import { Observable } from 'rxjs' | ||
16 | 17 | ||
17 | @Injectable() | 18 | @Injectable() |
18 | export class PluginApiService { | 19 | export 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 @@ | |||
1 | import { Injectable, NgZone } from '@angular/core' | 1 | import { Inject, Injectable, LOCALE_ID, NgZone } from '@angular/core' |
2 | import { Router } from '@angular/router' | 2 | import { Router } from '@angular/router' |
3 | import { ServerConfigPlugin } from '@shared/models' | 3 | import { getCompleteLocale, isDefaultLocale, peertubeTranslate, ServerConfigPlugin } from '@shared/models' |
4 | import { ServerService } from '@app/core/server/server.service' | 4 | import { ServerService } from '@app/core/server/server.service' |
5 | import { ClientScript } from '@shared/models/plugins/plugin-package-json.model' | 5 | import { ClientScript } from '@shared/models/plugins/plugin-package-json.model' |
6 | import { ClientScript as ClientScriptModule } from '../../../types/client-script.model' | 6 | import { ClientScript as ClientScriptModule } from '../../../types/client-script.model' |
7 | import { environment } from '../../../environments/environment' | 7 | import { environment } from '../../../environments/environment' |
8 | import { ReplaySubject } from 'rxjs' | 8 | import { Observable, of, ReplaySubject } from 'rxjs' |
9 | import { catchError, first, map, shareReplay } from 'rxjs/operators' | 9 | import { catchError, first, map, shareReplay } from 'rxjs/operators' |
10 | import { getHookType, internalRunHook } from '@shared/core-utils/plugins/hooks' | 10 | import { getHookType, internalRunHook } from '@shared/core-utils/plugins/hooks' |
11 | import { ClientHook, ClientHookName, clientHookObject } from '@shared/models/plugins/client-hook.model' | 11 | import { ClientHook, ClientHookName, clientHookObject } from '@shared/models/plugins/client-hook.model' |
@@ -15,6 +15,9 @@ import { HttpClient } from '@angular/common/http' | |||
15 | import { RestExtractor } from '@app/shared/rest' | 15 | import { RestExtractor } from '@app/shared/rest' |
16 | import { PluginType } from '@shared/models/plugins/plugin.type' | 16 | import { PluginType } from '@shared/models/plugins/plugin.type' |
17 | import { PublicServerSetting } from '@shared/models/plugins/public-server.setting' | 17 | import { PublicServerSetting } from '@shared/models/plugins/public-server.setting' |
18 | import { getDevLocale, isOnDevLocale } from '@app/shared/i18n/i18n-utils' | ||
19 | import { RegisterClientHelpers } from '../../../types/register-client-option.model' | ||
20 | import { PluginTranslation } from '@shared/models/plugins/plugin-translation.model' | ||
18 | 21 | ||
19 | interface HookStructValue extends RegisterClientHookOptions { | 22 | interface HookStructValue extends RegisterClientHookOptions { |
20 | plugin: ServerConfigPlugin | 23 | plugin: ServerConfigPlugin |
@@ -30,7 +33,8 @@ type PluginInfo = { | |||
30 | 33 | ||
31 | @Injectable() | 34 | @Injectable() |
32 | export class PluginService implements ClientHook { | 35 | export 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' | |||
31 | import { UserSubscriptionService } from '@app/shared/user-subscription/user-subscription.service' | 31 | import { UserSubscriptionService } from '@app/shared/user-subscription/user-subscription.service' |
32 | import { VideoChannel } from '@app/shared/video-channel/video-channel.model' | 32 | import { VideoChannel } from '@app/shared/video-channel/video-channel.model' |
33 | import { I18n } from '@ngx-translate/i18n-polyfill' | 33 | import { I18n } from '@ngx-translate/i18n-polyfill' |
34 | import { VideoPlaylist } from '@app/shared/video-playlist/video-playlist.model' | ||
35 | import { VideoPlaylistService } from '@app/shared/video-playlist/video-playlist.service' | 34 | import { VideoPlaylistService } from '@app/shared/video-playlist/video-playlist.service' |
36 | 35 | ||
37 | export interface VideosProvider { | 36 | 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 | |||
3 | export type RegisterClientOptions = { | 3 | export type RegisterClientOptions = { |
4 | registerHook: (options: RegisterClientHookOptions) => void | 4 | registerHook: (options: RegisterClientHookOptions) => void |
5 | 5 | ||
6 | peertubeHelpers: { | 6 | peertubeHelpers: RegisterClientHelpers |
7 | getBaseStaticRoute: () => string | 7 | } |
8 | |||
9 | export 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 @@ | |||
1 | import * as express from 'express' | 1 | import * as express from 'express' |
2 | import { PLUGIN_GLOBAL_CSS_PATH } from '../initializers/constants' | 2 | import { PLUGIN_GLOBAL_CSS_PATH } from '../initializers/constants' |
3 | import { join } from 'path' | 3 | import { join } from 'path' |
4 | import { RegisteredPlugin } from '../lib/plugins/plugin-manager' | 4 | import { PluginManager, RegisteredPlugin } from '../lib/plugins/plugin-manager' |
5 | import { servePluginStaticDirectoryValidator } from '../middlewares/validators/plugins' | 5 | import { servePluginStaticDirectoryValidator } from '../middlewares/validators/plugins' |
6 | import { serveThemeCSSValidator } from '../middlewares/validators/themes' | 6 | import { serveThemeCSSValidator } from '../middlewares/validators/themes' |
7 | import { PluginType } from '../../shared/models/plugins/plugin.type' | 7 | import { PluginType } from '../../shared/models/plugins/plugin.type' |
8 | import { isTestInstance } from '../helpers/core-utils' | 8 | import { isTestInstance } from '../helpers/core-utils' |
9 | import { getCompleteLocale, is18nLocale } from '../../shared/models/i18n' | ||
9 | 10 | ||
10 | const sendFileOptions = { | 11 | const 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 | ||
22 | pluginsRouter.get('/plugins/translations/:locale.json', | ||
23 | getPluginTranslations | ||
24 | ) | ||
25 | |||
21 | pluginsRouter.get('/plugins/:pluginName/:pluginVersion/static/:staticEndpoint(*)', | 26 | pluginsRouter.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 | ||
68 | function 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 | |||
63 | function servePluginStaticDirectory (req: express.Request, res: express.Response) { | 81 | function 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 | ||
47 | function isStaticDirectoriesValid (staticDirs: any) { | 47 | function 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 | ||
57 | function isClientScriptsValid (clientScripts: any[]) { | 57 | function 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 | ||
64 | function isCSSPathsValid (css: any[]) { | 64 | function 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 | |||
74 | function 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 | ||
85 | function isLibraryCodeValid (library: any) { | 96 | 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' | |||
3 | import { basename, join } from 'path' | 3 | import { basename, join } from 'path' |
4 | import { CONFIG } from '../../initializers/config' | 4 | import { CONFIG } from '../../initializers/config' |
5 | import { isLibraryCodeValid, isPackageJSONValid } from '../../helpers/custom-validators/plugins' | 5 | import { isLibraryCodeValid, isPackageJSONValid } from '../../helpers/custom-validators/plugins' |
6 | import { ClientScript, PluginPackageJson } from '../../../shared/models/plugins/plugin-package-json.model' | 6 | import { |
7 | ClientScript, | ||
8 | PluginPackageJson, | ||
9 | PluginTranslationPaths as PackagePluginTranslations | ||
10 | } from '../../../shared/models/plugins/plugin-package-json.model' | ||
7 | import { createReadStream, createWriteStream } from 'fs' | 11 | import { createReadStream, createWriteStream } from 'fs' |
8 | import { PLUGIN_GLOBAL_CSS_PATH, VIDEO_CATEGORIES, VIDEO_LANGUAGES, VIDEO_LICENCES } from '../../initializers/constants' | 12 | import { PLUGIN_GLOBAL_CSS_PATH, VIDEO_CATEGORIES, VIDEO_LANGUAGES, VIDEO_LICENCES } from '../../initializers/constants' |
9 | import { PluginType } from '../../../shared/models/plugins/plugin.type' | 13 | import { PluginType } from '../../../shared/models/plugins/plugin.type' |
@@ -21,6 +25,7 @@ import { RegisterServerSettingOptions } from '../../../shared/models/plugins/reg | |||
21 | import { PluginVideoLanguageManager } from '../../../shared/models/plugins/plugin-video-language-manager.model' | 25 | import { PluginVideoLanguageManager } from '../../../shared/models/plugins/plugin-video-language-manager.model' |
22 | import { PluginVideoCategoryManager } from '../../../shared/models/plugins/plugin-video-category-manager.model' | 26 | import { PluginVideoCategoryManager } from '../../../shared/models/plugins/plugin-video-category-manager.model' |
23 | import { PluginVideoLicenceManager } from '../../../shared/models/plugins/plugin-video-licence-manager.model' | 27 | import { PluginVideoLicenceManager } from '../../../shared/models/plugins/plugin-video-licence-manager.model' |
28 | import { PluginTranslation } from '../../../shared/models/plugins/plugin-translation.model' | ||
24 | 29 | ||
25 | export interface RegisteredPlugin { | 30 | export interface RegisteredPlugin { |
26 | npmName: string | 31 | npmName: string |
@@ -60,6 +65,10 @@ type UpdatedVideoConstant = { | |||
60 | } | 65 | } |
61 | } | 66 | } |
62 | 67 | ||
68 | type PluginLocalesTranslations = { | ||
69 | [ locale: string ]: PluginTranslation | ||
70 | } | ||
71 | |||
63 | export class PluginManager implements ServerHook { | 72 | export 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 @@ | |||
1 | import './action-hooks' | 1 | import './action-hooks' |
2 | import './filter-hooks' | 2 | import './filter-hooks' |
3 | import './translations' | ||
3 | import './video-constants' | 4 | 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 @@ | |||
1 | /* tslint:disable:no-unused-expression */ | ||
2 | |||
3 | import * as chai from 'chai' | ||
4 | import 'mocha' | ||
5 | import { | ||
6 | cleanupTests, | ||
7 | flushAndRunMultipleServers, | ||
8 | flushAndRunServer, killallServers, reRunServer, | ||
9 | ServerInfo, | ||
10 | waitUntilLog | ||
11 | } from '../../../shared/extra-utils/server/servers' | ||
12 | import { | ||
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' | ||
33 | import { VideoCommentThreadTree } from '../../../shared/models/videos/video-comment.model' | ||
34 | import { VideoDetails } from '../../../shared/models/videos' | ||
35 | import { getYoutubeVideoUrl, importVideo } from '../../../shared/extra-utils/videos/video-imports' | ||
36 | |||
37 | const expect = chai.expect | ||
38 | |||
39 | describe('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 | ||
137 | function 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 | |||
137 | function installPlugin (parameters: { | 152 | function 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 @@ | |||
1 | import { PluginClientScope } from './plugin-client-scope.type' | 1 | import { PluginClientScope } from './plugin-client-scope.type' |
2 | 2 | ||
3 | export type PluginTranslationPaths = { | ||
4 | [ locale: string ]: string | ||
5 | } | ||
6 | |||
3 | export type ClientScript = { | 7 | export 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 @@ | |||
1 | export type PluginTranslation = { | ||
2 | [ npmName: string ]: { | ||
3 | [ key: string ]: string | ||
4 | } | ||
5 | } | ||