Add plugin translation system
This commit is contained in:
parent
ee286591a5
commit
d75db01f14
|
@ -1,4 +1,4 @@
|
||||||
import { catchError } from 'rxjs/operators'
|
import { catchError, map, switchMap } from 'rxjs/operators'
|
||||||
import { HttpClient, HttpParams } from '@angular/common/http'
|
import { HttpClient, HttpParams } from '@angular/common/http'
|
||||||
import { Injectable } from '@angular/core'
|
import { Injectable } from '@angular/core'
|
||||||
import { environment } from '../../../../environments/environment'
|
import { environment } from '../../../../environments/environment'
|
||||||
|
@ -6,13 +6,14 @@ import { RestExtractor, RestService } from '../../../shared'
|
||||||
import { I18n } from '@ngx-translate/i18n-polyfill'
|
import { I18n } from '@ngx-translate/i18n-polyfill'
|
||||||
import { PluginType } from '@shared/models/plugins/plugin.type'
|
import { PluginType } from '@shared/models/plugins/plugin.type'
|
||||||
import { ComponentPagination } from '@app/shared/rest/component-pagination.model'
|
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 { PeerTubePlugin } from '@shared/models/plugins/peertube-plugin.model'
|
||||||
import { ManagePlugin } from '@shared/models/plugins/manage-plugin.model'
|
import { ManagePlugin } from '@shared/models/plugins/manage-plugin.model'
|
||||||
import { InstallOrUpdatePlugin } from '@shared/models/plugins/install-plugin.model'
|
import { InstallOrUpdatePlugin } from '@shared/models/plugins/install-plugin.model'
|
||||||
import { PeerTubePluginIndex } from '@shared/models/plugins/peertube-plugin-index.model'
|
import { PeerTubePluginIndex } from '@shared/models/plugins/peertube-plugin-index.model'
|
||||||
import { RegisteredServerSettings, RegisterServerSettingOptions } from '@shared/models/plugins/register-server-setting.model'
|
import { RegisteredServerSettings, RegisterServerSettingOptions } from '@shared/models/plugins/register-server-setting.model'
|
||||||
import { PluginService } from '@app/core/plugins/plugin.service'
|
import { PluginService } from '@app/core/plugins/plugin.service'
|
||||||
|
import { Observable } from 'rxjs'
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class PluginApiService {
|
export class PluginApiService {
|
||||||
|
@ -92,7 +93,10 @@ export class PluginApiService {
|
||||||
const path = PluginApiService.BASE_PLUGIN_URL + '/' + npmName + '/registered-settings'
|
const path = PluginApiService.BASE_PLUGIN_URL + '/' + npmName + '/registered-settings'
|
||||||
|
|
||||||
return this.authHttp.get<RegisteredServerSettings>(path)
|
return this.authHttp.get<RegisteredServerSettings>(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) {
|
updatePluginSettings (pluginName: string, pluginType: PluginType, settings: any) {
|
||||||
|
@ -129,4 +133,19 @@ export class PluginApiService {
|
||||||
return this.authHttp.post(PluginApiService.BASE_PLUGIN_URL + '/install', body)
|
return this.authHttp.post(PluginApiService.BASE_PLUGIN_URL + '/install', body)
|
||||||
.pipe(catchError(res => this.restExtractor.handleError(res)))
|
.pipe(catchError(res => this.restExtractor.handleError(res)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private translateSettingsLabel (npmName: string, res: RegisteredServerSettings): Observable<RegisteredServerSettings> {
|
||||||
|
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 }
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 { 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 { ServerService } from '@app/core/server/server.service'
|
||||||
import { ClientScript } from '@shared/models/plugins/plugin-package-json.model'
|
import { ClientScript } from '@shared/models/plugins/plugin-package-json.model'
|
||||||
import { ClientScript as ClientScriptModule } from '../../../types/client-script.model'
|
import { ClientScript as ClientScriptModule } from '../../../types/client-script.model'
|
||||||
import { environment } from '../../../environments/environment'
|
import { environment } from '../../../environments/environment'
|
||||||
import { ReplaySubject } from 'rxjs'
|
import { Observable, of, ReplaySubject } from 'rxjs'
|
||||||
import { catchError, first, map, shareReplay } from 'rxjs/operators'
|
import { catchError, first, map, shareReplay } from 'rxjs/operators'
|
||||||
import { getHookType, internalRunHook } from '@shared/core-utils/plugins/hooks'
|
import { getHookType, internalRunHook } from '@shared/core-utils/plugins/hooks'
|
||||||
import { ClientHook, ClientHookName, clientHookObject } from '@shared/models/plugins/client-hook.model'
|
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 { RestExtractor } from '@app/shared/rest'
|
||||||
import { PluginType } from '@shared/models/plugins/plugin.type'
|
import { PluginType } from '@shared/models/plugins/plugin.type'
|
||||||
import { PublicServerSetting } from '@shared/models/plugins/public-server.setting'
|
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 {
|
interface HookStructValue extends RegisterClientHookOptions {
|
||||||
plugin: ServerConfigPlugin
|
plugin: ServerConfigPlugin
|
||||||
|
@ -30,7 +33,8 @@ type PluginInfo = {
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class PluginService implements ClientHook {
|
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<boolean>(1)
|
pluginsBuilt = new ReplaySubject<boolean>(1)
|
||||||
|
|
||||||
|
@ -40,6 +44,8 @@ export class PluginService implements ClientHook {
|
||||||
'video-watch': new ReplaySubject<boolean>(1)
|
'video-watch': new ReplaySubject<boolean>(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
translationsObservable: Observable<PluginTranslation>
|
||||||
|
|
||||||
private plugins: ServerConfigPlugin[] = []
|
private plugins: ServerConfigPlugin[] = []
|
||||||
private scopes: { [ scopeName: string ]: PluginInfo[] } = {}
|
private scopes: { [ scopeName: string ]: PluginInfo[] } = {}
|
||||||
private loadedScripts: { [ script: string ]: boolean } = {}
|
private loadedScripts: { [ script: string ]: boolean } = {}
|
||||||
|
@ -53,8 +59,10 @@ export class PluginService implements ClientHook {
|
||||||
private server: ServerService,
|
private server: ServerService,
|
||||||
private zone: NgZone,
|
private zone: NgZone,
|
||||||
private authHttp: HttpClient,
|
private authHttp: HttpClient,
|
||||||
private restExtractor: RestExtractor
|
private restExtractor: RestExtractor,
|
||||||
|
@Inject(LOCALE_ID) private localeId: string
|
||||||
) {
|
) {
|
||||||
|
this.loadTranslations()
|
||||||
}
|
}
|
||||||
|
|
||||||
initializePlugins () {
|
initializePlugins () {
|
||||||
|
@ -235,8 +243,9 @@ export class PluginService implements ClientHook {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private buildPeerTubeHelpers (pluginInfo: PluginInfo) {
|
private buildPeerTubeHelpers (pluginInfo: PluginInfo): RegisterClientHelpers {
|
||||||
const { plugin } = pluginInfo
|
const { plugin } = pluginInfo
|
||||||
|
const npmName = this.nameToNpmName(pluginInfo.plugin.name, pluginInfo.pluginType)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
getBaseStaticRoute: () => {
|
getBaseStaticRoute: () => {
|
||||||
|
@ -245,8 +254,7 @@ export class PluginService implements ClientHook {
|
||||||
},
|
},
|
||||||
|
|
||||||
getSettings: () => {
|
getSettings: () => {
|
||||||
const npmName = this.nameToNpmName(pluginInfo.plugin.name, pluginInfo.pluginType)
|
const path = PluginService.BASE_PLUGIN_API_URL + '/' + npmName + '/public-settings'
|
||||||
const path = PluginService.BASE_PLUGIN_URL + '/' + npmName + '/public-settings'
|
|
||||||
|
|
||||||
return this.authHttp.get<PublicServerSetting>(path)
|
return this.authHttp.get<PublicServerSetting>(path)
|
||||||
.pipe(
|
.pipe(
|
||||||
|
@ -254,10 +262,28 @@ export class PluginService implements ClientHook {
|
||||||
catchError(res => this.restExtractor.handleError(res))
|
catchError(res => this.restExtractor.handleError(res))
|
||||||
)
|
)
|
||||||
.toPromise()
|
.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<PluginTranslation>(PluginService.BASE_PLUGIN_URL + '/translations/' + completeLocale + '.json')
|
||||||
|
.pipe(shareReplay())
|
||||||
|
}
|
||||||
|
|
||||||
private getPluginPathPrefix (isTheme: boolean) {
|
private getPluginPathPrefix (isTheme: boolean) {
|
||||||
return isTheme ? '/themes' : '/plugins'
|
return isTheme ? '/themes' : '/plugins'
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,7 +31,6 @@ import { ServerService } from '@app/core'
|
||||||
import { UserSubscriptionService } from '@app/shared/user-subscription/user-subscription.service'
|
import { UserSubscriptionService } from '@app/shared/user-subscription/user-subscription.service'
|
||||||
import { VideoChannel } from '@app/shared/video-channel/video-channel.model'
|
import { VideoChannel } from '@app/shared/video-channel/video-channel.model'
|
||||||
import { I18n } from '@ngx-translate/i18n-polyfill'
|
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'
|
import { VideoPlaylistService } from '@app/shared/video-playlist/video-playlist.service'
|
||||||
|
|
||||||
export interface VideosProvider {
|
export interface VideosProvider {
|
||||||
|
|
|
@ -3,9 +3,13 @@ import { RegisterClientHookOptions } from '@shared/models/plugins/register-clien
|
||||||
export type RegisterClientOptions = {
|
export type RegisterClientOptions = {
|
||||||
registerHook: (options: RegisterClientHookOptions) => void
|
registerHook: (options: RegisterClientHookOptions) => void
|
||||||
|
|
||||||
peertubeHelpers: {
|
peertubeHelpers: RegisterClientHelpers
|
||||||
getBaseStaticRoute: () => string
|
}
|
||||||
|
|
||||||
getSettings: () => Promise<{ [ name: string ]: string }>
|
export type RegisterClientHelpers = {
|
||||||
}
|
getBaseStaticRoute: () => string
|
||||||
|
|
||||||
|
getSettings: () => Promise<{ [ name: string ]: string }>
|
||||||
|
|
||||||
|
translate: (toTranslate: string) => Promise<string>
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,12 @@
|
||||||
import * as express from 'express'
|
import * as express from 'express'
|
||||||
import { PLUGIN_GLOBAL_CSS_PATH } from '../initializers/constants'
|
import { PLUGIN_GLOBAL_CSS_PATH } from '../initializers/constants'
|
||||||
import { join } from 'path'
|
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 { servePluginStaticDirectoryValidator } from '../middlewares/validators/plugins'
|
||||||
import { serveThemeCSSValidator } from '../middlewares/validators/themes'
|
import { serveThemeCSSValidator } from '../middlewares/validators/themes'
|
||||||
import { PluginType } from '../../shared/models/plugins/plugin.type'
|
import { PluginType } from '../../shared/models/plugins/plugin.type'
|
||||||
import { isTestInstance } from '../helpers/core-utils'
|
import { isTestInstance } from '../helpers/core-utils'
|
||||||
|
import { getCompleteLocale, is18nLocale } from '../../shared/models/i18n'
|
||||||
|
|
||||||
const sendFileOptions = {
|
const sendFileOptions = {
|
||||||
maxAge: '30 days',
|
maxAge: '30 days',
|
||||||
|
@ -18,6 +19,10 @@ pluginsRouter.get('/plugins/global.css',
|
||||||
servePluginGlobalCSS
|
servePluginGlobalCSS
|
||||||
)
|
)
|
||||||
|
|
||||||
|
pluginsRouter.get('/plugins/translations/:locale.json',
|
||||||
|
getPluginTranslations
|
||||||
|
)
|
||||||
|
|
||||||
pluginsRouter.get('/plugins/:pluginName/:pluginVersion/static/:staticEndpoint(*)',
|
pluginsRouter.get('/plugins/:pluginName/:pluginVersion/static/:staticEndpoint(*)',
|
||||||
servePluginStaticDirectoryValidator(PluginType.PLUGIN),
|
servePluginStaticDirectoryValidator(PluginType.PLUGIN),
|
||||||
servePluginStaticDirectory
|
servePluginStaticDirectory
|
||||||
|
@ -60,6 +65,19 @@ function servePluginGlobalCSS (req: express.Request, res: express.Response) {
|
||||||
return res.sendFile(PLUGIN_GLOBAL_CSS_PATH, globalCSSOptions)
|
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) {
|
function servePluginStaticDirectory (req: express.Request, res: express.Response) {
|
||||||
const plugin: RegisteredPlugin = res.locals.registeredPlugin
|
const plugin: RegisteredPlugin = res.locals.registeredPlugin
|
||||||
const staticEndpoint = req.params.staticEndpoint
|
const staticEndpoint = req.params.staticEndpoint
|
||||||
|
|
|
@ -44,7 +44,7 @@ function isPluginHomepage (value: string) {
|
||||||
return isUrlValid(value)
|
return isUrlValid(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
function isStaticDirectoriesValid (staticDirs: any) {
|
function areStaticDirectoriesValid (staticDirs: any) {
|
||||||
if (!exists(staticDirs) || typeof staticDirs !== 'object') return false
|
if (!exists(staticDirs) || typeof staticDirs !== 'object') return false
|
||||||
|
|
||||||
for (const key of Object.keys(staticDirs)) {
|
for (const key of Object.keys(staticDirs)) {
|
||||||
|
@ -54,14 +54,24 @@ function isStaticDirectoriesValid (staticDirs: any) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
function isClientScriptsValid (clientScripts: any[]) {
|
function areClientScriptsValid (clientScripts: any[]) {
|
||||||
return isArray(clientScripts) &&
|
return isArray(clientScripts) &&
|
||||||
clientScripts.every(c => {
|
clientScripts.every(c => {
|
||||||
return isSafePath(c.script) && isArray(c.scopes)
|
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))
|
return isArray(css) && css.every(c => isSafePath(c))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -77,9 +87,10 @@ function isPackageJSONValid (packageJSON: PluginPackageJson, pluginType: PluginT
|
||||||
exists(packageJSON.author) &&
|
exists(packageJSON.author) &&
|
||||||
isUrlValid(packageJSON.bugs) &&
|
isUrlValid(packageJSON.bugs) &&
|
||||||
(pluginType === PluginType.THEME || isSafePath(packageJSON.library)) &&
|
(pluginType === PluginType.THEME || isSafePath(packageJSON.library)) &&
|
||||||
isStaticDirectoriesValid(packageJSON.staticDirs) &&
|
areStaticDirectoriesValid(packageJSON.staticDirs) &&
|
||||||
isCSSPathsValid(packageJSON.css) &&
|
areCSSPathsValid(packageJSON.css) &&
|
||||||
isClientScriptsValid(packageJSON.clientScripts)
|
areClientScriptsValid(packageJSON.clientScripts) &&
|
||||||
|
areTranslationPathsValid(packageJSON.translations)
|
||||||
}
|
}
|
||||||
|
|
||||||
function isLibraryCodeValid (library: any) {
|
function isLibraryCodeValid (library: any) {
|
||||||
|
|
|
@ -3,7 +3,11 @@ import { logger } from '../../helpers/logger'
|
||||||
import { basename, join } from 'path'
|
import { basename, join } from 'path'
|
||||||
import { CONFIG } from '../../initializers/config'
|
import { CONFIG } from '../../initializers/config'
|
||||||
import { isLibraryCodeValid, isPackageJSONValid } from '../../helpers/custom-validators/plugins'
|
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 { createReadStream, createWriteStream } from 'fs'
|
||||||
import { PLUGIN_GLOBAL_CSS_PATH, VIDEO_CATEGORIES, VIDEO_LANGUAGES, VIDEO_LICENCES } from '../../initializers/constants'
|
import { PLUGIN_GLOBAL_CSS_PATH, VIDEO_CATEGORIES, VIDEO_LANGUAGES, VIDEO_LICENCES } from '../../initializers/constants'
|
||||||
import { PluginType } from '../../../shared/models/plugins/plugin.type'
|
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 { PluginVideoLanguageManager } from '../../../shared/models/plugins/plugin-video-language-manager.model'
|
||||||
import { PluginVideoCategoryManager } from '../../../shared/models/plugins/plugin-video-category-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 { PluginVideoLicenceManager } from '../../../shared/models/plugins/plugin-video-licence-manager.model'
|
||||||
|
import { PluginTranslation } from '../../../shared/models/plugins/plugin-translation.model'
|
||||||
|
|
||||||
export interface RegisteredPlugin {
|
export interface RegisteredPlugin {
|
||||||
npmName: string
|
npmName: string
|
||||||
|
@ -60,6 +65,10 @@ type UpdatedVideoConstant = {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type PluginLocalesTranslations = {
|
||||||
|
[ locale: string ]: PluginTranslation
|
||||||
|
}
|
||||||
|
|
||||||
export class PluginManager implements ServerHook {
|
export class PluginManager implements ServerHook {
|
||||||
|
|
||||||
private static instance: PluginManager
|
private static instance: PluginManager
|
||||||
|
@ -67,6 +76,7 @@ export class PluginManager implements ServerHook {
|
||||||
private registeredPlugins: { [ name: string ]: RegisteredPlugin } = {}
|
private registeredPlugins: { [ name: string ]: RegisteredPlugin } = {}
|
||||||
private settings: { [ name: string ]: RegisterServerSettingOptions[] } = {}
|
private settings: { [ name: string ]: RegisterServerSettingOptions[] } = {}
|
||||||
private hooks: { [ name: string ]: HookInformationValue[] } = {}
|
private hooks: { [ name: string ]: HookInformationValue[] } = {}
|
||||||
|
private translations: PluginLocalesTranslations = {}
|
||||||
|
|
||||||
private updatedVideoConstants: UpdatedVideoConstant = {
|
private updatedVideoConstants: UpdatedVideoConstant = {
|
||||||
language: {},
|
language: {},
|
||||||
|
@ -117,6 +127,10 @@ export class PluginManager implements ServerHook {
|
||||||
return this.settings[npmName] || []
|
return this.settings[npmName] || []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getTranslations (locale: string) {
|
||||||
|
return this.translations[locale] || {}
|
||||||
|
}
|
||||||
|
|
||||||
// ###################### Hooks ######################
|
// ###################### Hooks ######################
|
||||||
|
|
||||||
async runHook <T> (hookName: ServerHookName, result?: T, params?: any): Promise<T> {
|
async runHook <T> (hookName: ServerHookName, result?: T, params?: any): Promise<T> {
|
||||||
|
@ -173,6 +187,8 @@ export class PluginManager implements ServerHook {
|
||||||
delete this.registeredPlugins[plugin.npmName]
|
delete this.registeredPlugins[plugin.npmName]
|
||||||
delete this.settings[plugin.npmName]
|
delete this.settings[plugin.npmName]
|
||||||
|
|
||||||
|
this.deleteTranslations(plugin.npmName)
|
||||||
|
|
||||||
if (plugin.type === PluginType.PLUGIN) {
|
if (plugin.type === PluginType.PLUGIN) {
|
||||||
await plugin.unregister()
|
await plugin.unregister()
|
||||||
|
|
||||||
|
@ -312,6 +328,8 @@ export class PluginManager implements ServerHook {
|
||||||
css: packageJSON.css,
|
css: packageJSON.css,
|
||||||
unregister: library ? library.unregister : undefined
|
unregister: library ? library.unregister : undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await this.addTranslations(plugin, npmName, packageJSON.translations)
|
||||||
}
|
}
|
||||||
|
|
||||||
private async registerPlugin (plugin: PluginModel, pluginPath: string, packageJSON: PluginPackageJson) {
|
private async registerPlugin (plugin: PluginModel, pluginPath: string, packageJSON: PluginPackageJson) {
|
||||||
|
@ -337,6 +355,28 @@ export class PluginManager implements ServerHook {
|
||||||
return library
|
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 ######################
|
// ###################### CSS ######################
|
||||||
|
|
||||||
private resetCSSGlobalFile () {
|
private resetCSSGlobalFile () {
|
||||||
|
@ -455,7 +495,7 @@ export class PluginManager implements ServerHook {
|
||||||
deleteLanguage: (key: string) => this.deleteConstant({ npmName, type: 'language', obj: VIDEO_LANGUAGES, key })
|
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 }),
|
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 })
|
deleteCategory: (key: number) => this.deleteConstant({ npmName, type: 'category', obj: VIDEO_CATEGORIES, key })
|
||||||
|
|
|
@ -15,5 +15,6 @@
|
||||||
"library": "./main.js",
|
"library": "./main.js",
|
||||||
"staticDirs": {},
|
"staticDirs": {},
|
||||||
"css": [],
|
"css": [],
|
||||||
"clientScripts": []
|
"clientScripts": [],
|
||||||
|
"translations": {}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"Hello world": "Bonjour le monde"
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"Hello world": "Ciao, mondo!"
|
||||||
|
}
|
|
@ -15,5 +15,9 @@
|
||||||
"library": "./main.js",
|
"library": "./main.js",
|
||||||
"staticDirs": {},
|
"staticDirs": {},
|
||||||
"css": [],
|
"css": [],
|
||||||
"clientScripts": []
|
"clientScripts": [],
|
||||||
|
"translations": {
|
||||||
|
"fr-FR": "./languages/fr.json",
|
||||||
|
"it-IT": "./languages/it.json"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"Hi": "Coucou"
|
||||||
|
}
|
|
@ -15,5 +15,8 @@
|
||||||
"library": "./main.js",
|
"library": "./main.js",
|
||||||
"staticDirs": {},
|
"staticDirs": {},
|
||||||
"css": [],
|
"css": [],
|
||||||
"clientScripts": []
|
"clientScripts": [],
|
||||||
|
"translations": {
|
||||||
|
"fr-FR": "./languages/fr.json"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
import './action-hooks'
|
import './action-hooks'
|
||||||
import './filter-hooks'
|
import './filter-hooks'
|
||||||
|
import './translations'
|
||||||
import './video-constants'
|
import './video-constants'
|
||||||
|
|
|
@ -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 ])
|
||||||
|
})
|
||||||
|
})
|
|
@ -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: {
|
function installPlugin (parameters: {
|
||||||
url: string,
|
url: string,
|
||||||
accessToken: string,
|
accessToken: string,
|
||||||
|
@ -224,6 +239,7 @@ export {
|
||||||
listPlugins,
|
listPlugins,
|
||||||
listAvailablePlugins,
|
listAvailablePlugins,
|
||||||
installPlugin,
|
installPlugin,
|
||||||
|
getPluginTranslations,
|
||||||
getPluginsCSS,
|
getPluginsCSS,
|
||||||
updatePlugin,
|
updatePlugin,
|
||||||
getPlugin,
|
getPlugin,
|
||||||
|
|
|
@ -1,5 +1,9 @@
|
||||||
import { PluginClientScope } from './plugin-client-scope.type'
|
import { PluginClientScope } from './plugin-client-scope.type'
|
||||||
|
|
||||||
|
export type PluginTranslationPaths = {
|
||||||
|
[ locale: string ]: string
|
||||||
|
}
|
||||||
|
|
||||||
export type ClientScript = {
|
export type ClientScript = {
|
||||||
script: string,
|
script: string,
|
||||||
scopes: PluginClientScope[]
|
scopes: PluginClientScope[]
|
||||||
|
@ -20,4 +24,6 @@ export type PluginPackageJson = {
|
||||||
css: string[]
|
css: string[]
|
||||||
|
|
||||||
clientScripts: ClientScript[]
|
clientScripts: ClientScript[]
|
||||||
|
|
||||||
|
translations: PluginTranslationPaths
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
export type PluginTranslation = {
|
||||||
|
[ npmName: string ]: {
|
||||||
|
[ key: string ]: string
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue