Add plugin translation system
authorChocobozzz <me@florianbigard.com>
Fri, 26 Jul 2019 12:44:50 +0000 (14:44 +0200)
committerChocobozzz <me@florianbigard.com>
Fri, 26 Jul 2019 13:18:30 +0000 (15:18 +0200)
18 files changed:
client/src/app/+admin/plugins/shared/plugin-api.service.ts
client/src/app/core/plugins/plugin.service.ts
client/src/app/shared/video/video.service.ts
client/src/types/register-client-option.model.ts
server/controllers/plugins.ts
server/helpers/custom-validators/plugins.ts
server/lib/plugins/plugin-manager.ts
server/tests/fixtures/peertube-plugin-test-three/package.json
server/tests/fixtures/peertube-plugin-test-two/languages/fr.json [new file with mode: 0644]
server/tests/fixtures/peertube-plugin-test-two/languages/it.json [new file with mode: 0644]
server/tests/fixtures/peertube-plugin-test-two/package.json
server/tests/fixtures/peertube-plugin-test/languages/fr.json [new file with mode: 0644]
server/tests/fixtures/peertube-plugin-test/package.json
server/tests/plugins/index.ts
server/tests/plugins/translations.ts [new file with mode: 0644]
shared/extra-utils/server/plugins.ts
shared/models/plugins/plugin-package-json.model.ts
shared/models/plugins/plugin-translation.model.ts [new file with mode: 0644]

index c360fc1b31951dba87b23ccd1e0252841f13345d..f6ef68e9cc8e3ec8eb9de48359a4ab4edf6457ac 100644 (file)
@@ -1,4 +1,4 @@
-import { catchError } from 'rxjs/operators'
+import { catchError, map, switchMap } from 'rxjs/operators'
 import { HttpClient, HttpParams } from '@angular/common/http'
 import { Injectable } from '@angular/core'
 import { environment } from '../../../../environments/environment'
@@ -6,13 +6,14 @@ import { RestExtractor, RestService } from '../../../shared'
 import { I18n } from '@ngx-translate/i18n-polyfill'
 import { PluginType } from '@shared/models/plugins/plugin.type'
 import { ComponentPagination } from '@app/shared/rest/component-pagination.model'
-import { ResultList } from '@shared/models'
+import { peertubeTranslate, ResultList } from '@shared/models'
 import { PeerTubePlugin } from '@shared/models/plugins/peertube-plugin.model'
 import { ManagePlugin } from '@shared/models/plugins/manage-plugin.model'
 import { InstallOrUpdatePlugin } from '@shared/models/plugins/install-plugin.model'
 import { PeerTubePluginIndex } from '@shared/models/plugins/peertube-plugin-index.model'
 import { RegisteredServerSettings, RegisterServerSettingOptions } from '@shared/models/plugins/register-server-setting.model'
 import { PluginService } from '@app/core/plugins/plugin.service'
+import { Observable } from 'rxjs'
 
 @Injectable()
 export class PluginApiService {
@@ -92,7 +93,10 @@ export class PluginApiService {
     const path = PluginApiService.BASE_PLUGIN_URL + '/' + npmName + '/registered-settings'
 
     return this.authHttp.get<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) {
@@ -129,4 +133,19 @@ export class PluginApiService {
     return this.authHttp.post(PluginApiService.BASE_PLUGIN_URL + '/install', body)
                .pipe(catchError(res => this.restExtractor.handleError(res)))
   }
+
+  private translateSettingsLabel (npmName: string, res: RegisteredServerSettings): Observable<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 }
+        })
+      )
+  }
 }
index cca77917796ae1213e585395404025834cf6f78a..3bb82e8a987f8b32d008a8465d7cc37aad6718d4 100644 (file)
@@ -1,11 +1,11 @@
-import { Injectable, NgZone } from '@angular/core'
+import { Inject, Injectable, LOCALE_ID, NgZone } from '@angular/core'
 import { Router } from '@angular/router'
-import { ServerConfigPlugin } from '@shared/models'
+import { getCompleteLocale, isDefaultLocale, peertubeTranslate, ServerConfigPlugin } from '@shared/models'
 import { ServerService } from '@app/core/server/server.service'
 import { ClientScript } from '@shared/models/plugins/plugin-package-json.model'
 import { ClientScript as ClientScriptModule } from '../../../types/client-script.model'
 import { environment } from '../../../environments/environment'
-import { ReplaySubject } from 'rxjs'
+import { Observable, of, ReplaySubject } from 'rxjs'
 import { catchError, first, map, shareReplay } from 'rxjs/operators'
 import { getHookType, internalRunHook } from '@shared/core-utils/plugins/hooks'
 import { ClientHook, ClientHookName, clientHookObject } from '@shared/models/plugins/client-hook.model'
@@ -15,6 +15,9 @@ import { HttpClient } from '@angular/common/http'
 import { RestExtractor } from '@app/shared/rest'
 import { PluginType } from '@shared/models/plugins/plugin.type'
 import { PublicServerSetting } from '@shared/models/plugins/public-server.setting'
+import { getDevLocale, isOnDevLocale } from '@app/shared/i18n/i18n-utils'
+import { RegisterClientHelpers } from '../../../types/register-client-option.model'
+import { PluginTranslation } from '@shared/models/plugins/plugin-translation.model'
 
 interface HookStructValue extends RegisterClientHookOptions {
   plugin: ServerConfigPlugin
@@ -30,7 +33,8 @@ type PluginInfo = {
 
 @Injectable()
 export class PluginService implements ClientHook {
-  private static BASE_PLUGIN_URL = environment.apiUrl + '/api/v1/plugins'
+  private static BASE_PLUGIN_API_URL = environment.apiUrl + '/api/v1/plugins'
+  private static BASE_PLUGIN_URL = environment.apiUrl + '/plugins'
 
   pluginsBuilt = new ReplaySubject<boolean>(1)
 
@@ -40,6 +44,8 @@ export class PluginService implements ClientHook {
     'video-watch': new ReplaySubject<boolean>(1)
   }
 
+  translationsObservable: Observable<PluginTranslation>
+
   private plugins: ServerConfigPlugin[] = []
   private scopes: { [ scopeName: string ]: PluginInfo[] } = {}
   private loadedScripts: { [ script: string ]: boolean } = {}
@@ -53,8 +59,10 @@ export class PluginService implements ClientHook {
     private server: ServerService,
     private zone: NgZone,
     private authHttp: HttpClient,
-    private restExtractor: RestExtractor
+    private restExtractor: RestExtractor,
+    @Inject(LOCALE_ID) private localeId: string
   ) {
+    this.loadTranslations()
   }
 
   initializePlugins () {
@@ -235,8 +243,9 @@ export class PluginService implements ClientHook {
     }
   }
 
-  private buildPeerTubeHelpers (pluginInfo: PluginInfo) {
+  private buildPeerTubeHelpers (pluginInfo: PluginInfo): RegisterClientHelpers {
     const { plugin } = pluginInfo
+    const npmName = this.nameToNpmName(pluginInfo.plugin.name, pluginInfo.pluginType)
 
     return {
       getBaseStaticRoute: () => {
@@ -245,8 +254,7 @@ export class PluginService implements ClientHook {
       },
 
       getSettings: () => {
-        const npmName = this.nameToNpmName(pluginInfo.plugin.name, pluginInfo.pluginType)
-        const path = PluginService.BASE_PLUGIN_URL + '/' + npmName + '/public-settings'
+        const path = PluginService.BASE_PLUGIN_API_URL + '/' + npmName + '/public-settings'
 
         return this.authHttp.get<PublicServerSetting>(path)
                    .pipe(
@@ -254,10 +262,28 @@ export class PluginService implements ClientHook {
                      catchError(res => this.restExtractor.handleError(res))
                    )
                    .toPromise()
+      },
+
+      translate: (value: string) => {
+        return this.translationsObservable
+            .pipe(map(allTranslations => allTranslations[npmName]))
+            .pipe(map(translations => peertubeTranslate(value, translations)))
+            .toPromise()
       }
     }
   }
 
+  private loadTranslations () {
+    const completeLocale = isOnDevLocale() ? getDevLocale() : getCompleteLocale(this.localeId)
+
+    // Default locale, nothing to translate
+    if (isDefaultLocale(completeLocale)) this.translationsObservable = of({}).pipe(shareReplay())
+
+    this.translationsObservable = this.authHttp
+        .get<PluginTranslation>(PluginService.BASE_PLUGIN_URL + '/translations/' + completeLocale + '.json')
+        .pipe(shareReplay())
+  }
+
   private getPluginPathPrefix (isTheme: boolean) {
     return isTheme ? '/themes' : '/plugins'
   }
index d1af13c93c6cde3f14a063619f00762ca52fbbe4..114b014ad198d5d0c716beface38bc1d3dcc7270 100644 (file)
@@ -31,7 +31,6 @@ import { ServerService } from '@app/core'
 import { UserSubscriptionService } from '@app/shared/user-subscription/user-subscription.service'
 import { VideoChannel } from '@app/shared/video-channel/video-channel.model'
 import { I18n } from '@ngx-translate/i18n-polyfill'
-import { VideoPlaylist } from '@app/shared/video-playlist/video-playlist.model'
 import { VideoPlaylistService } from '@app/shared/video-playlist/video-playlist.service'
 
 export interface VideosProvider {
index 473c2500f0d82e947b47702d0a97d77d444fa808..243d74dea7257a85d6a41211ba2d5277c7cc0bb1 100644 (file)
@@ -3,9 +3,13 @@ import { RegisterClientHookOptions } from '@shared/models/plugins/register-clien
 export type RegisterClientOptions = {
   registerHook: (options: RegisterClientHookOptions) => void
 
-  peertubeHelpers: {
-    getBaseStaticRoute: () => string
+  peertubeHelpers: RegisterClientHelpers
+}
+
+export type RegisterClientHelpers = {
+  getBaseStaticRoute: () => string
+
+  getSettings: () => Promise<{ [ name: string ]: string }>
 
-    getSettings: () => Promise<{ [ name: string ]: string }>
-  }
+  translate: (toTranslate: string) => Promise<string>
 }
index f5285ba3ae3ae9967d533f73332f547e1c535327..1caee9a29fcbb10d2fdd5fbb0f09121f154b72fe 100644 (file)
@@ -1,11 +1,12 @@
 import * as express from 'express'
 import { PLUGIN_GLOBAL_CSS_PATH } from '../initializers/constants'
 import { join } from 'path'
-import { RegisteredPlugin } from '../lib/plugins/plugin-manager'
+import { PluginManager, RegisteredPlugin } from '../lib/plugins/plugin-manager'
 import { servePluginStaticDirectoryValidator } from '../middlewares/validators/plugins'
 import { serveThemeCSSValidator } from '../middlewares/validators/themes'
 import { PluginType } from '../../shared/models/plugins/plugin.type'
 import { isTestInstance } from '../helpers/core-utils'
+import { getCompleteLocale, is18nLocale } from '../../shared/models/i18n'
 
 const sendFileOptions = {
   maxAge: '30 days',
@@ -18,6 +19,10 @@ pluginsRouter.get('/plugins/global.css',
   servePluginGlobalCSS
 )
 
+pluginsRouter.get('/plugins/translations/:locale.json',
+  getPluginTranslations
+)
+
 pluginsRouter.get('/plugins/:pluginName/:pluginVersion/static/:staticEndpoint(*)',
   servePluginStaticDirectoryValidator(PluginType.PLUGIN),
   servePluginStaticDirectory
@@ -60,6 +65,19 @@ function servePluginGlobalCSS (req: express.Request, res: express.Response) {
   return res.sendFile(PLUGIN_GLOBAL_CSS_PATH, globalCSSOptions)
 }
 
+function getPluginTranslations (req: express.Request, res: express.Response) {
+  const locale = req.params.locale
+
+  if (is18nLocale(locale)) {
+    const completeLocale = getCompleteLocale(locale)
+    const json = PluginManager.Instance.getTranslations(completeLocale)
+
+    return res.json(json)
+  }
+
+  return res.sendStatus(404)
+}
+
 function servePluginStaticDirectory (req: express.Request, res: express.Response) {
   const plugin: RegisteredPlugin = res.locals.registeredPlugin
   const staticEndpoint = req.params.staticEndpoint
index e0a6f98a7a5717eb599f2f174291acebbbbbfed7..b5e32abc26839c70eba277499b7488409ddd3bb1 100644 (file)
@@ -44,7 +44,7 @@ function isPluginHomepage (value: string) {
   return isUrlValid(value)
 }
 
-function isStaticDirectoriesValid (staticDirs: any) {
+function areStaticDirectoriesValid (staticDirs: any) {
   if (!exists(staticDirs) || typeof staticDirs !== 'object') return false
 
   for (const key of Object.keys(staticDirs)) {
@@ -54,14 +54,24 @@ function isStaticDirectoriesValid (staticDirs: any) {
   return true
 }
 
-function isClientScriptsValid (clientScripts: any[]) {
+function areClientScriptsValid (clientScripts: any[]) {
   return isArray(clientScripts) &&
     clientScripts.every(c => {
       return isSafePath(c.script) && isArray(c.scopes)
     })
 }
 
-function isCSSPathsValid (css: any[]) {
+function areTranslationPathsValid (translations: any) {
+  if (!exists(translations) || typeof translations !== 'object') return false
+
+  for (const key of Object.keys(translations)) {
+    if (!isSafePath(translations[key])) return false
+  }
+
+  return true
+}
+
+function areCSSPathsValid (css: any[]) {
   return isArray(css) && css.every(c => isSafePath(c))
 }
 
@@ -77,9 +87,10 @@ function isPackageJSONValid (packageJSON: PluginPackageJson, pluginType: PluginT
     exists(packageJSON.author) &&
     isUrlValid(packageJSON.bugs) &&
     (pluginType === PluginType.THEME || isSafePath(packageJSON.library)) &&
-    isStaticDirectoriesValid(packageJSON.staticDirs) &&
-    isCSSPathsValid(packageJSON.css) &&
-    isClientScriptsValid(packageJSON.clientScripts)
+    areStaticDirectoriesValid(packageJSON.staticDirs) &&
+    areCSSPathsValid(packageJSON.css) &&
+    areClientScriptsValid(packageJSON.clientScripts) &&
+    areTranslationPathsValid(packageJSON.translations)
 }
 
 function isLibraryCodeValid (library: any) {
index 81554a09e7b1156f6383766273d9b516ffb31a48..c9beae2689fd875b7aa819bb68a3f21cf598daa2 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
@@ -60,6 +65,10 @@ type UpdatedVideoConstant = {
   }
 }
 
+type PluginLocalesTranslations = {
+  [ locale: string ]: PluginTranslation
+}
+
 export class PluginManager implements ServerHook {
 
   private static instance: PluginManager
@@ -67,6 +76,7 @@ export class PluginManager implements ServerHook {
   private registeredPlugins: { [ name: string ]: RegisteredPlugin } = {}
   private settings: { [ name: string ]: RegisterServerSettingOptions[] } = {}
   private hooks: { [ name: string ]: HookInformationValue[] } = {}
+  private translations: PluginLocalesTranslations = {}
 
   private updatedVideoConstants: UpdatedVideoConstant = {
     language: {},
@@ -117,6 +127,10 @@ export class PluginManager implements ServerHook {
     return this.settings[npmName] || []
   }
 
+  getTranslations (locale: string) {
+    return this.translations[locale] || {}
+  }
+
   // ###################### Hooks ######################
 
   async runHook <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.settings[plugin.npmName]
 
+    this.deleteTranslations(plugin.npmName)
+
     if (plugin.type === PluginType.PLUGIN) {
       await plugin.unregister()
 
@@ -312,6 +328,8 @@ export class PluginManager implements ServerHook {
       css: packageJSON.css,
       unregister: library ? library.unregister : undefined
     }
+
+    await this.addTranslations(plugin, npmName, packageJSON.translations)
   }
 
   private async registerPlugin (plugin: PluginModel, pluginPath: string, packageJSON: PluginPackageJson) {
@@ -337,6 +355,28 @@ export class PluginManager implements ServerHook {
     return library
   }
 
+  // ###################### Translations ######################
+
+  private async addTranslations (plugin: PluginModel, npmName: string, translationPaths: PackagePluginTranslations) {
+    for (const locale of Object.keys(translationPaths)) {
+      const path = translationPaths[locale]
+      const json = await readJSON(join(this.getPluginPath(plugin.name, plugin.type), path))
+
+      if (!this.translations[locale]) this.translations[locale] = {}
+      this.translations[locale][npmName] = json
+
+      logger.info('Added locale %s of plugin %s.', locale, npmName)
+    }
+  }
+
+  private deleteTranslations (npmName: string) {
+    for (const locale of Object.keys(this.translations)) {
+      delete this.translations[locale][npmName]
+
+      logger.info('Deleted locale %s of plugin %s.', locale, npmName)
+    }
+  }
+
   // ###################### CSS ######################
 
   private resetCSSGlobalFile () {
@@ -455,7 +495,7 @@ export class PluginManager implements ServerHook {
       deleteLanguage: (key: string) => this.deleteConstant({ npmName, type: 'language', obj: VIDEO_LANGUAGES, key })
     }
 
-    const videoCategoryManager: PluginVideoCategoryManager= {
+    const videoCategoryManager: PluginVideoCategoryManager = {
       addCategory: (key: number, label: string) => this.addConstant({ npmName, type: 'category', obj: VIDEO_CATEGORIES, key, label }),
 
       deleteCategory: (key: number) => this.deleteConstant({ npmName, type: 'category', obj: VIDEO_CATEGORIES, key })
index 3f7819db319780901815ffd02b7dfd0d534b8609..41d4c93fe43205e3844b45000619eee63acd379f 100644 (file)
@@ -15,5 +15,6 @@
   "library": "./main.js",
   "staticDirs": {},
   "css": [],
-  "clientScripts": []
+  "clientScripts": [],
+  "translations": {}
 }
diff --git a/server/tests/fixtures/peertube-plugin-test-two/languages/fr.json b/server/tests/fixtures/peertube-plugin-test-two/languages/fr.json
new file mode 100644 (file)
index 0000000..52d8313
--- /dev/null
@@ -0,0 +1,3 @@
+{
+  "Hello world": "Bonjour le monde"
+}
diff --git a/server/tests/fixtures/peertube-plugin-test-two/languages/it.json b/server/tests/fixtures/peertube-plugin-test-two/languages/it.json
new file mode 100644 (file)
index 0000000..9e187d8
--- /dev/null
@@ -0,0 +1,3 @@
+{
+  "Hello world": "Ciao, mondo!"
+}
index 52ebb5ac1992b6a855aabb8e6cdf462a5f19d338..926f2d69b641c5a2339ae71850da58fac9f9297a 100644 (file)
@@ -15,5 +15,9 @@
   "library": "./main.js",
   "staticDirs": {},
   "css": [],
-  "clientScripts": []
+  "clientScripts": [],
+  "translations": {
+    "fr-FR": "./languages/fr.json",
+    "it-IT": "./languages/it.json"
+  }
 }
diff --git a/server/tests/fixtures/peertube-plugin-test/languages/fr.json b/server/tests/fixtures/peertube-plugin-test/languages/fr.json
new file mode 100644 (file)
index 0000000..9e52f70
--- /dev/null
@@ -0,0 +1,3 @@
+{
+  "Hi": "Coucou"
+}
index 9d6fe5c90b2658e7bbf05476406b57fc6a30e0ff..108f21fd65d888bf252867a82e7680cc9c843b35 100644 (file)
@@ -15,5 +15,8 @@
   "library": "./main.js",
   "staticDirs": {},
   "css": [],
-  "clientScripts": []
+  "clientScripts": [],
+  "translations": {
+    "fr-FR": "./languages/fr.json"
+  }
 }
index 95e358732901f04e158ef740d1a591011910d55f..f41708055f6c040c133aae6c936885781acaa1dd 100644 (file)
@@ -1,3 +1,4 @@
 import './action-hooks'
 import './filter-hooks'
+import './translations'
 import './video-constants'
diff --git a/server/tests/plugins/translations.ts b/server/tests/plugins/translations.ts
new file mode 100644 (file)
index 0000000..88d91a0
--- /dev/null
@@ -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 ])
+  })
+})
index 65d37d69f094c5134a509d8cbad3c0ec4049c694..5c0d1e511c1812daba288d9890850c0c842e3f65 100644 (file)
@@ -134,6 +134,21 @@ function getPublicSettings (parameters: {
   })
 }
 
+function getPluginTranslations (parameters: {
+  url: string,
+  locale: string,
+  expectedStatus?: number
+}) {
+  const { url, locale, expectedStatus = 200 } = parameters
+  const path = '/plugins/translations/' + locale + '.json'
+
+  return makeGetRequest({
+    url,
+    path,
+    statusCodeExpected: expectedStatus
+  })
+}
+
 function installPlugin (parameters: {
   url: string,
   accessToken: string,
@@ -224,6 +239,7 @@ export {
   listPlugins,
   listAvailablePlugins,
   installPlugin,
+  getPluginTranslations,
   getPluginsCSS,
   updatePlugin,
   getPlugin,
index 87a48e97f78c57aef7c7972b04dc9e38bead0c8d..3f30776713fd39608406d922818a6317ff03b784 100644 (file)
@@ -1,5 +1,9 @@
 import { PluginClientScope } from './plugin-client-scope.type'
 
+export type PluginTranslationPaths = {
+  [ locale: string ]: string
+}
+
 export type ClientScript = {
   script: string,
   scopes: PluginClientScope[]
@@ -20,4 +24,6 @@ export type PluginPackageJson = {
   css: string[]
 
   clientScripts: ClientScript[]
+
+  translations: PluginTranslationPaths
 }
diff --git a/shared/models/plugins/plugin-translation.model.ts b/shared/models/plugins/plugin-translation.model.ts
new file mode 100644 (file)
index 0000000..a2dd8e5
--- /dev/null
@@ -0,0 +1,5 @@
+export type PluginTranslation = {
+  [ npmName: string ]: {
+    [ key: string ]: string
+  }
+}