]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blobdiff - server/lib/plugins/plugin-manager.ts
Merge branch 'release/2.1.0' into develop
[github/Chocobozzz/PeerTube.git] / server / lib / plugins / plugin-manager.ts
index 81554a09e7b1156f6383766273d9b516ffb31a48..73f7a71ceafee1627ac2c435d59def472555d059 100644 (file)
@@ -3,7 +3,11 @@ import { logger } from '../../helpers/logger'
 import { basename, join } from 'path'
 import { CONFIG } from '../../initializers/config'
 import { isLibraryCodeValid, isPackageJSONValid } from '../../helpers/custom-validators/plugins'
-import { ClientScript, PluginPackageJson } from '../../../shared/models/plugins/plugin-package-json.model'
+import {
+  ClientScript,
+  PluginPackageJson,
+  PluginTranslationPaths as PackagePluginTranslations
+} from '../../../shared/models/plugins/plugin-package-json.model'
 import { createReadStream, createWriteStream } from 'fs'
 import { PLUGIN_GLOBAL_CSS_PATH, VIDEO_CATEGORIES, VIDEO_LANGUAGES, VIDEO_LICENCES } from '../../initializers/constants'
 import { PluginType } from '../../../shared/models/plugins/plugin.type'
@@ -21,6 +25,7 @@ import { RegisterServerSettingOptions } from '../../../shared/models/plugins/reg
 import { PluginVideoLanguageManager } from '../../../shared/models/plugins/plugin-video-language-manager.model'
 import { PluginVideoCategoryManager } from '../../../shared/models/plugins/plugin-video-category-manager.model'
 import { PluginVideoLicenceManager } from '../../../shared/models/plugins/plugin-video-licence-manager.model'
+import { PluginTranslation } from '../../../shared/models/plugins/plugin-translation.model'
 
 export interface RegisteredPlugin {
   npmName: string
@@ -50,25 +55,30 @@ export interface HookInformationValue {
 }
 
 type AlterableVideoConstant = 'language' | 'licence' | 'category'
-type VideoConstant = { [ key in number | string ]: string }
+type VideoConstant = { [key in number | string]: string }
 type UpdatedVideoConstant = {
-  [ name in AlterableVideoConstant ]: {
-    [ npmName: string ]: {
-      added: { key: number | string, label: string }[],
+  [name in AlterableVideoConstant]: {
+    [npmName: string]: {
+      added: { key: number | string, label: string }[]
       deleted: { key: number | string, label: string }[]
     }
   }
 }
 
+type PluginLocalesTranslations = {
+  [locale: string]: PluginTranslation
+}
+
 export class PluginManager implements ServerHook {
 
   private static instance: PluginManager
 
-  private registeredPlugins: { [ name: string ]: RegisteredPlugin } = {}
-  private settings: { [ name: string ]: RegisterServerSettingOptions[] } = {}
-  private hooks: { [ name: string ]: HookInformationValue[] } = {}
+  private registeredPlugins: { [name: string]: RegisteredPlugin } = {}
+  private settings: { [name: string]: RegisterServerSettingOptions[] } = {}
+  private hooks: { [name: string]: HookInformationValue[] } = {}
+  private translations: PluginLocalesTranslations = {}
 
-  private updatedVideoConstants: UpdatedVideoConstant = {
+  private readonly updatedVideoConstants: UpdatedVideoConstant = {
     language: {},
     licence: {},
     category: {}
@@ -117,9 +127,13 @@ export class PluginManager implements ServerHook {
     return this.settings[npmName] || []
   }
 
+  getTranslations (locale: string) {
+    return this.translations[locale] || {}
+  }
+
   // ###################### Hooks ######################
 
-  async runHook <T> (hookName: ServerHookName, result?: T, params?: any): Promise<T> {
+  async runHook<T> (hookName: ServerHookName, result?: T, params?: any): Promise<T> {
     if (!this.hooks[hookName]) return Promise.resolve(result)
 
     const hookType = getHookType(hookName)
@@ -173,12 +187,14 @@ export class PluginManager implements ServerHook {
     delete this.registeredPlugins[plugin.npmName]
     delete this.settings[plugin.npmName]
 
+    this.deleteTranslations(plugin.npmName)
+
     if (plugin.type === PluginType.PLUGIN) {
       await plugin.unregister()
 
       // Remove hooks of this plugin
       for (const key of Object.keys(this.hooks)) {
-        this.hooks[key] = this.hooks[key].filter(h => h.pluginName !== npmName)
+        this.hooks[key] = this.hooks[key].filter(h => h.npmName !== npmName)
       }
 
       this.reinitVideoConstants(plugin.npmName)
@@ -206,9 +222,8 @@ export class PluginManager implements ServerHook {
       const pluginName = PluginModel.normalizePluginName(npmName)
 
       const packageJSON = await this.getPackageJSON(pluginName, pluginType)
-      if (!isPackageJSONValid(packageJSON, pluginType)) {
-        throw new Error('PackageJSON is invalid.')
-      }
+
+      this.sanitizeAndCheckPackageJSONOrThrow(packageJSON, pluginType);
 
       [ plugin ] = await PluginModel.upsert({
         name: pluginName,
@@ -285,9 +300,7 @@ export class PluginManager implements ServerHook {
     const packageJSON = await this.getPackageJSON(plugin.name, plugin.type)
     const pluginPath = this.getPluginPath(plugin.name, plugin.type)
 
-    if (!isPackageJSONValid(packageJSON, plugin.type)) {
-      throw new Error('Package.JSON is invalid.')
-    }
+    this.sanitizeAndCheckPackageJSONOrThrow(packageJSON, plugin.type)
 
     let library: PluginLibrary
     if (plugin.type === PluginType.PLUGIN) {
@@ -299,7 +312,7 @@ export class PluginManager implements ServerHook {
       clientScripts[c.script] = c
     }
 
-    this.registeredPlugins[ npmName ] = {
+    this.registeredPlugins[npmName] = {
       npmName,
       name: plugin.name,
       type: plugin.type,
@@ -312,6 +325,8 @@ export class PluginManager implements ServerHook {
       css: packageJSON.css,
       unregister: library ? library.unregister : undefined
     }
+
+    await this.addTranslations(plugin, npmName, packageJSON.translations)
   }
 
   private async registerPlugin (plugin: PluginModel, pluginPath: string, packageJSON: PluginPackageJson) {
@@ -337,6 +352,28 @@ export class PluginManager implements ServerHook {
     return library
   }
 
+  // ###################### Translations ######################
+
+  private async addTranslations (plugin: PluginModel, npmName: string, translationPaths: PackagePluginTranslations) {
+    for (const locale of Object.keys(translationPaths)) {
+      const path = translationPaths[locale]
+      const json = await readJSON(join(this.getPluginPath(plugin.name, plugin.type), path))
+
+      if (!this.translations[locale]) this.translations[locale] = {}
+      this.translations[locale][npmName] = json
+
+      logger.info('Added locale %s of plugin %s.', locale, npmName)
+    }
+  }
+
+  private deleteTranslations (npmName: string) {
+    for (const locale of Object.keys(this.translations)) {
+      delete this.translations[locale][npmName]
+
+      logger.info('Deleted locale %s of plugin %s.', locale, npmName)
+    }
+  }
+
   // ###################### CSS ######################
 
   private resetCSSGlobalFile () {
@@ -368,9 +405,7 @@ export class PluginManager implements ServerHook {
   private async regeneratePluginGlobalCSS () {
     await this.resetCSSGlobalFile()
 
-    for (const key of Object.keys(this.getRegisteredPlugins())) {
-      const plugin = this.registeredPlugins[key]
-
+    for (const plugin of this.getRegisteredPlugins()) {
       await this.addCSSToGlobalFile(plugin.path, plugin.css)
     }
   }
@@ -403,7 +438,7 @@ export class PluginManager implements ServerHook {
     const plugins: RegisteredPlugin[] = []
 
     for (const npmName of Object.keys(this.registeredPlugins)) {
-      const plugin = this.registeredPlugins[ npmName ]
+      const plugin = this.registeredPlugins[npmName]
       if (plugin.type !== type) continue
 
       plugins.push(plugin)
@@ -455,7 +490,7 @@ export class PluginManager implements ServerHook {
       deleteLanguage: (key: string) => this.deleteConstant({ npmName, type: 'language', obj: VIDEO_LANGUAGES, key })
     }
 
-    const videoCategoryManager: PluginVideoCategoryManager= {
+    const videoCategoryManager: PluginVideoCategoryManager = {
       addCategory: (key: number, label: string) => this.addConstant({ npmName, type: 'category', obj: VIDEO_CATEGORIES, key, label }),
 
       deleteCategory: (key: number) => this.deleteConstant({ npmName, type: 'category', obj: VIDEO_CATEGORIES, key })
@@ -483,11 +518,11 @@ export class PluginManager implements ServerHook {
     }
   }
 
-  private addConstant <T extends string | number> (parameters: {
-    npmName: string,
-    type: AlterableVideoConstant,
-    obj: VideoConstant,
-    key: T,
+  private addConstant<T extends string | number> (parameters: {
+    npmName: string
+    type: AlterableVideoConstant
+    obj: VideoConstant
+    key: T
     label: string
   }) {
     const { npmName, type, obj, key, label } = parameters
@@ -510,10 +545,10 @@ export class PluginManager implements ServerHook {
     return true
   }
 
-  private deleteConstant <T extends string | number> (parameters: {
-    npmName: string,
-    type: AlterableVideoConstant,
-    obj: VideoConstant,
+  private deleteConstant<T extends string | number> (parameters: {
+    npmName: string
+    type: AlterableVideoConstant
+    obj: VideoConstant
     key: T
   }) {
     const { npmName, type, obj, key } = parameters
@@ -560,6 +595,21 @@ export class PluginManager implements ServerHook {
     }
   }
 
+  private sanitizeAndCheckPackageJSONOrThrow (packageJSON: PluginPackageJson, pluginType: PluginType) {
+    if (!packageJSON.staticDirs) packageJSON.staticDirs = {}
+    if (!packageJSON.css) packageJSON.css = []
+    if (!packageJSON.clientScripts) packageJSON.clientScripts = []
+    if (!packageJSON.translations) packageJSON.translations = {}
+
+    const { result: packageJSONValid, badFields } = isPackageJSONValid(packageJSON, pluginType)
+    if (!packageJSONValid) {
+      const formattedFields = badFields.map(f => `"${f}"`)
+                                       .join(', ')
+
+      throw new Error(`PackageJSON is invalid (invalid fields: ${formattedFields}).`)
+    }
+  }
+
   static get Instance () {
     return this.instance || (this.instance = new this())
   }