Prepare i18n files
authorChocobozzz <me@florianbigard.com>
Thu, 31 May 2018 16:12:15 +0000 (18:12 +0200)
committerChocobozzz <me@florianbigard.com>
Tue, 5 Jun 2018 06:43:01 +0000 (08:43 +0200)
28 files changed:
.gitignore
client/package.json
client/src/app/app.component.ts
client/src/app/app.module.ts
client/src/app/core/routing/redirect.service.ts
client/src/app/shared/misc/utils.ts
client/src/app/shared/shared.module.ts
client/src/app/videos/+video-watch/video-watch.component.html
client/src/app/videos/+video-watch/video-watch.component.ts
client/src/app/videos/video-list/video-local.component.ts
client/src/app/videos/video-list/video-recently-added.component.ts
client/src/app/videos/video-list/video-search.component.ts
client/src/app/videos/video-list/video-trending.component.ts
client/src/locale/source/messages_en_US.xml [new file with mode: 0644]
client/src/locale/target/messages_fr.xml [new file with mode: 0644]
client/yarn.lock
package.json
scripts/build/client.sh
scripts/i18n/generate.sh [new file with mode: 0755]
scripts/i18n/pull-hook.sh [new file with mode: 0755]
scripts/release.sh
server.ts
server/controllers/client.ts
shared/models/i18n/i18n.ts [new file with mode: 0644]
shared/models/i18n/index.ts [new file with mode: 0644]
shared/models/index.ts
yarn.lock
zanata.xml [new file with mode: 0644]

index 5b50250441e95b11f8b24992bc3757a39c1226b6..92af7631045a74ea0effd33818e4782f98e7452e 100644 (file)
@@ -25,3 +25,4 @@
 /logs/
 /server/tools/import-mediacore.ts
 /docker-volume/
+/.zanata-cache
index 61f94758aa383c27352f3eefb5391bc71ad0139e..b79a090b3a0d586582611169b60facd3cdb2a3fb 100644 (file)
@@ -19,7 +19,8 @@
     "ng": "ng",
     "postinstall": "npm rebuild node-sass && test -f angular-cli-patch.js && node angular-cli-patch.js || true",
     "webpack-bundle-analyzer": "webpack-bundle-analyzer",
-    "webdriver-manager": "webdriver-manager"
+    "webdriver-manager": "webdriver-manager",
+    "ngx-extractor": "ngx-extractor"
   },
   "license": "GPLv3",
   "resolutions": {
@@ -47,6 +48,7 @@
     "@ngx-loading-bar/http-client": "^2.0.0",
     "@ngx-loading-bar/router": "^2.0.0",
     "@ngx-meta/core": "^6.0.0-rc.1",
+    "@ngx-translate/i18n-polyfill": "^1.0.0",
     "@types/core-js": "^0.9.28",
     "@types/jasmine": "^2.8.7",
     "@types/jasminewd2": "^2.0.3",
index 0bd127063b2e16a6f313fe79957bc495ee794edb..6087dbf8038bf337022495dde014f3e4336458f9 100644 (file)
@@ -1,8 +1,9 @@
 import { Component, OnInit } from '@angular/core'
 import { DomSanitizer, SafeHtml } from '@angular/platform-browser'
-import { GuardsCheckStart, Router, NavigationEnd } from '@angular/router'
+import { GuardsCheckStart, NavigationEnd, Router } from '@angular/router'
 import { AuthService, RedirectService, ServerService } from '@app/core'
 import { isInSmallView } from '@app/shared/misc/utils'
+import { is18nPath } from '../../../shared/models/i18n'
 
 @Component({
   selector: 'my-app',
@@ -33,7 +34,7 @@ export class AppComponent implements OnInit {
     private serverService: ServerService,
     private domSanitizer: DomSanitizer,
     private redirectService: RedirectService
-  ) {}
+  ) { }
 
   get serverVersion () {
     return this.serverService.getConfig().serverVersion
@@ -53,7 +54,7 @@ export class AppComponent implements OnInit {
     this.router.events.subscribe(e => {
       if (e instanceof NavigationEnd) {
         const pathname = window.location.pathname
-        if (!pathname || pathname === '/') {
+        if (!pathname || pathname === '/' || is18nPath(pathname)) {
           this.redirectService.redirectToHomepage()
         }
       }
index cf533629fb24a3cc308f2dcfc547c23781562134..44552021f3aedcbb351f35c52d4c00b239c14552 100644 (file)
@@ -1,4 +1,4 @@
-import { NgModule } from '@angular/core'
+import { LOCALE_ID, NgModule, TRANSLATIONS, TRANSLATIONS_FORMAT } from '@angular/core'
 import { BrowserModule } from '@angular/platform-browser'
 import { AboutModule } from '@app/about'
 import { ServerService } from '@app/core'
@@ -16,6 +16,7 @@ import { MenuComponent } from './menu'
 import { SharedModule } from './shared'
 import { SignupModule } from './signup'
 import { VideosModule } from './videos'
+import { buildFileLocale, getDefaultLocale } from '../../../shared/models/i18n'
 
 export function metaFactory (serverService: ServerService): MetaLoader {
   return new MetaStaticLoader({
@@ -61,6 +62,21 @@ export function metaFactory (serverService: ServerService): MetaLoader {
 
     AppRoutingModule // Put it after all the module because it has the 404 route
   ],
-  providers: [ ]
+  providers: [
+    {
+      provide: TRANSLATIONS,
+      useFactory: (locale) => {
+        const fileLocale = buildFileLocale(locale)
+
+        // Default locale, nothing to translate
+        const defaultFileLocale = buildFileLocale(getDefaultLocale())
+        if (fileLocale === defaultFileLocale) return ''
+
+        return require(`raw-loader!../locale/target/messages_${fileLocale}.xml`)
+      },
+      deps: [ LOCALE_ID ]
+    },
+    { provide: TRANSLATIONS_FORMAT, useValue: 'xlf' }
+  ]
 })
 export class AppModule {}
index 844f184b414ac6cb3baad62fcfa13c2b20d7450a..b7803cce2f9f6df0946cbc9adce5e5e45b1dffea 100644 (file)
@@ -31,7 +31,7 @@ export class RedirectService {
   redirectToHomepage () {
     console.log('Redirecting to %s...', RedirectService.DEFAULT_ROUTE)
 
-    this.router.navigate([ RedirectService.DEFAULT_ROUTE ], { replaceUrl: true })
+    this.router.navigate([ RedirectService.DEFAULT_ROUTE ], { skipLocationChange: true })
         .catch(() => {
           console.error(
             'Cannot navigate to %s, resetting default route to %s.',
@@ -40,7 +40,7 @@ export class RedirectService {
           )
 
           RedirectService.DEFAULT_ROUTE = RedirectService.INIT_DEFAULT_ROUTE
-          return this.router.navigate([ RedirectService.DEFAULT_ROUTE ], { replaceUrl: true })
+          return this.router.navigate([ RedirectService.DEFAULT_ROUTE ], { skipLocationChange: true })
         })
 
   }
index 11933e90b48c218494718f7b37706a032a7ccc03..2219ac802dd0693149c9752f4df3a958fe23dd9b 100644 (file)
@@ -98,7 +98,7 @@ function lineFeedToHtml (obj: object, keyToNormalize: string) {
 
 // Try to cache a little bit window.innerWidth
 let windowInnerWidth = window.innerWidth
-// setInterval(() => windowInnerWidth = window.innerWidth, 500)
+setInterval(() => windowInnerWidth = window.innerWidth, 500)
 
 function isInSmallView () {
   return windowInnerWidth < 600
index 20019e47ae75d4af31cae7784eeb49b09d7a50b3..fba099401a02225682a227f0eb5fdb840e760696 100644 (file)
@@ -33,6 +33,7 @@ import { VideoThumbnailComponent } from './video/video-thumbnail.component'
 import { VideoService } from './video/video.service'
 import { AccountService } from '@app/shared/account/account.service'
 import { VideoChannelService } from '@app/shared/video-channel/video-channel.service'
+import { I18n } from '@ngx-translate/i18n-polyfill'
 
 @NgModule({
   imports: [
@@ -108,7 +109,8 @@ import { VideoChannelService } from '@app/shared/video-channel/video-channel.ser
     VideoService,
     AccountService,
     MarkdownService,
-    VideoChannelService
+    VideoChannelService,
+    I18n
   ]
 })
 export class SharedModule { }
index 583a97562655fc3fbd7e68ed52e2ac585403bcee..202a12fb05002a6cfcb4f4516b2fbc88328dd090 100644 (file)
@@ -3,7 +3,7 @@
   <div [hidden]="videoNotFound" id="video-element-wrapper">
   </div>
 
-  <div *ngIf="videoNotFound" id="video-not-found">Video not found :'(</div>
+  <div i18n *ngIf="videoNotFound" id="video-not-found">Video not found :'(</div>
 
   <!-- Video information -->
   <div *ngIf="video" class="margin-content video-bottom">
         <div>
           <div class="video-info-name">{{ video.name }}</div>
 
-          <div class="video-info-date-views">
+          <div i18n class="video-info-date-views">
             {{ video.publishedAt | myFromNow }} - {{ video.views | myNumberFormatter }} views
           </div>
 
           <div class="video-info-channel">
-            <a [routerLink]="[ '/video-channels', video.channel.id ]" title="Go the channel page">
+            <a [routerLink]="[ '/video-channels', video.channel.id ]" i18n-title title="Go the channel page">
               {{ video.channel.displayName }}
             </a>
             <!-- Here will be the subscribe button -->
-            <my-help helpType="custom" customHtml="You can subscribe to this account via any ActivityPub-capable fediverse instance. For instance with Mastodon or Pleroma you can type in the search box <strong>@{{video.account.displayName}}@{{video.account.host}}</strong> and subscribe there. Subscription as a PeerTube user is being worked on in <a href='https://github.com/Chocobozzz/PeerTube/issues/470'>#470</a>."></my-help>
+            <my-help helpType="custom" i18n-customHtml customHtml="You can subscribe to this account via any ActivityPub-capable fediverse instance. For instance with Mastodon or Pleroma you can type in the search box <strong>@{{video.account.displayName}}@{{video.account.host}}</strong> and subscribe there. Subscription as a PeerTube user is being worked on in <a href='https://github.com/Chocobozzz/PeerTube/issues/470'>#470</a>."></my-help>
           </div>
 
           <div class="video-info-by">
-            <a [routerLink]="[ '/accounts', video.by ]" title="Go the account page">
-              <span>By {{ video.by }}</span>
+            <a [routerLink]="[ '/accounts', video.by ]" i18n-title title="Go the account page">
+              <span i18n>By {{ video.by }}</span>
               <img [src]="video.accountAvatarUrl" alt="Account avatar" />
             </a>
           </div>
                 *ngIf="isUserLoggedIn()" [ngClass]="{ 'activated': userRating === 'like' }" (click)="setLike()"
                 class="action-button action-button-like"
             >
-              <span class="icon icon-like" title="Like this video" ></span>
+              <span class="icon icon-like" i18n-title title="Like this video" ></span>
             </div>
 
             <div
                 *ngIf="isUserLoggedIn()" [ngClass]="{ 'activated': userRating === 'dislike' }" (click)="setDislike()"
                 class="action-button action-button-dislike"
             >
-              <span class="icon icon-dislike" title="Dislike this video"></span>
+              <span class="icon icon-dislike" i18n-title title="Dislike this video"></span>
             </div>
 
             <div *ngIf="video.support" (click)="showSupportModal()" class="action-button action-button-support">
               <span class="icon icon-support"></span>
-              <span class="icon-text">Support</span>
+              <span class="icon-text" i18n>Support</span>
             </div>
 
             <div (click)="showShareModal()" class="action-button action-button-share">
               <span class="icon icon-share"></span>
-              <span class="icon-text">Share</span>
+              <span class="icon-text" i18n>Share</span>
             </div>
 
             <div class="action-more" dropdown dropup="true" placement="right">
 
               <ul *dropdownMenu class="dropdown-menu" id="more-menu" role="menu" aria-labelledby="single-button">
                 <li role="menuitem">
-                  <a class="dropdown-item" title="Download the video" href="#" (click)="showDownloadModal($event)">
-                    <span class="icon icon-download"></span> Download
+                  <a class="dropdown-item" i18n-title title="Download the video" href="#" (click)="showDownloadModal($event)">
+                    <span class="icon icon-download"></span> <ng-container i18n>Download</ng-container>
                   </a>
                 </li>
 
                 <li *ngIf="isUserLoggedIn()" role="menuitem">
-                  <a class="dropdown-item" title="Report this video" href="#" (click)="showReportModal($event)">
-                    <span class="icon icon-alert"></span> Report
+                  <a class="dropdown-item" i18n-title title="Report this video" href="#" (click)="showReportModal($event)">
+                    <span class="icon icon-alert"></span> <ng-container i18n>Report</ng-container>
                   </a>
                 </li>
 
                 <li *ngIf="isVideoBlacklistable()" role="menuitem">
-                  <a class="dropdown-item" title="Blacklist this video" href="#" (click)="blacklistVideo($event)">
-                    <span class="icon icon-blacklist"></span> Blacklist
+                  <a class="dropdown-item" i18n-title title="Blacklist this video" href="#" (click)="blacklistVideo($event)">
+                    <span class="icon icon-blacklist"></span> <ng-container i18n>Blacklist</ng-container>
                   </a>
                 </li>
 
                 <li *ngIf="isVideoUpdatable()" role="menuitem">
-                  <a class="dropdown-item" title="Update this video" href="#" [routerLink]="[ '/videos/update', video.uuid ]">
-                    <span class="icon icon-edit"></span> Update
+                  <a class="dropdown-item" i18n-title title="Update this video" href="#" [routerLink]="[ '/videos/update', video.uuid ]">
+                    <span class="icon icon-edit"></span> <ng-container i18n>Update</ng-container>
                   </a>
                 </li>
 
                 <li *ngIf="isVideoRemovable()" role="menuitem">
-                  <a class="dropdown-item" title="Delete this video" href="#" (click)="removeVideo($event)">
-                    <span class="icon icon-blacklist"></span> Delete
+                  <a class="dropdown-item" i18n-title title="Delete this video" href="#" (click)="removeVideo($event)">
+                    <span class="icon icon-blacklist"></span> <ng-container i18n>Delete</ng-container>
                   </a>
                 </li>
               </ul>
         <div class="video-info-description-html" [innerHTML]="videoHTMLDescription"></div>
 
         <div class="video-info-description-more" *ngIf="completeDescriptionShown === false && video.description?.length >= 250" (click)="showMoreDescription()">
-          Show more
+          <ng-container i18n>Show more</ng-container>
           <span *ngIf="descriptionLoading === false" class="glyphicon glyphicon-menu-down"></span>
           <my-loader class="description-loading" [loading]="descriptionLoading"></my-loader>
         </div>
 
         <div *ngIf="completeDescriptionShown === true" (click)="showLessDescription()" class="video-info-description-more">
-          Show less
+          <ng-container i18n>Show less</ng-container>
           <span *ngIf="descriptionLoading === false" class="glyphicon glyphicon-menu-up"></span>
         </div>
       </div>
 
       <div class="video-attributes">
         <div class="video-attribute">
-          <span class="video-attribute-label">
+          <span i18n class="video-attribute-label">
             Privacy
           </span>
           <span class="video-attribute-value">
         </div>
 
         <div class="video-attribute">
-          <span class="video-attribute-label">
+          <span i18n class="video-attribute-label">
             Category
           </span>
           <span class="video-attribute-value">
         </div>
 
         <div class="video-attribute">
-          <span class="video-attribute-label">
+          <span i18n class="video-attribute-label">
             Licence
           </span>
           <span class="video-attribute-value">
         </div>
 
         <div class="video-attribute">
-          <span class="video-attribute-label">
+          <span i18n class="video-attribute-label">
             Language
           </span>
           <span class="video-attribute-value">
         </div>
 
         <div class="video-attribute">
-          <span class="video-attribute-label">
+          <span i18n class="video-attribute-label">
             Tags
           </span>
 
     </div>
 
     <div class="other-videos">
-      <div class="title-page title-page-single">
+      <div i18n class="title-page title-page-single">
         Other videos
       </div>
 
 
 
   <div class="privacy-concerns" *ngIf="hasAlreadyAcceptedPrivacyConcern === false">
-    <strong>Friendly Reminder:</strong>
+    <strong i18n>Friendly Reminder:</strong>
     <div class="privacy-concerns-text">
-      The sharing system used by this video implies that some technical information about your system (such as a public IP address) can be accessed publicly.
-      <a title="Get more information" target="_blank" rel="noopener noreferrer" href="/about#p2p-privacy">More information</a>
+      <ng-container i18n>
+        The sharing system used by this video implies that some technical information about your system (such as a public IP address) can be accessed publicly.
+      </ng-container>
+      <a i18n i18n-title title="Get more information" target="_blank" rel="noopener noreferrer" href="/about#p2p-privacy">More information</a>
     </div>
 
-    <div class="privacy-concerns-okay" (click)="acceptedPrivacyConcern()">
+    <div i18n class="privacy-concerns-okay" (click)="acceptedPrivacyConcern()">
       OK
     </div>
   </div>
index ad572ef58263c7cfa9b5e4cc3474a55933479cf3..f3b4f7a2b0b6d5851634f08f69ef02f619377c7a 100644 (file)
@@ -23,6 +23,7 @@ import { VideoReportComponent } from './modal/video-report.component'
 import { VideoShareComponent } from './modal/video-share.component'
 import { getVideojsOptions } from '../../../assets/player/peertube-player'
 import { ServerService } from '@app/core'
+import { I18n } from '@ngx-translate/i18n-polyfill'
 
 @Component({
   selector: 'my-video-watch',
@@ -70,7 +71,8 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
     private notificationsService: NotificationsService,
     private markdownService: MarkdownService,
     private zone: NgZone,
-    private redirectService: RedirectService
+    private redirectService: RedirectService,
+    private i18n: I18n
   ) {}
 
   get user () {
@@ -153,17 +155,20 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
   async blacklistVideo (event: Event) {
     event.preventDefault()
 
-    const res = await this.confirmService.confirm('Do you really want to blacklist this video?', 'Blacklist')
+    const res = await this.confirmService.confirm(this.i18n('Do you really want to blacklist this video?'), this.i18n('Blacklist'))
     if (res === false) return
 
     this.videoBlacklistService.blacklistVideo(this.video.id)
                               .subscribe(
                                 status => {
-                                  this.notificationsService.success('Success', `Video ${this.video.name} had been blacklisted.`)
+                                  this.notificationsService.success(
+                                    this.i18n('Success'),
+                                    this.i18n('Video {{ videoName }} had been blacklisted.', { videoName: this.video.name })
+                                  )
                                   this.redirectService.redirectToHomepage()
                                 },
 
-                                error => this.notificationsService.error('Error', error.message)
+                                error => this.notificationsService.error(this.i18n('Error'), error.message)
                               )
   }
 
@@ -198,7 +203,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
 
         error => {
           this.descriptionLoading = false
-          this.notificationsService.error('Error', error.message)
+          this.notificationsService.error(this.i18n('Error'), error.message)
         }
       )
   }
@@ -252,19 +257,22 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
   async removeVideo (event: Event) {
     event.preventDefault()
 
-    const res = await this.confirmService.confirm('Do you really want to delete this video?', 'Delete')
+    const res = await this.confirmService.confirm(this.i18n('Do you really want to delete this video?'), this.i18n('Delete'))
     if (res === false) return
 
     this.videoService.removeVideo(this.video.id)
       .subscribe(
         status => {
-          this.notificationsService.success('Success', `Video ${this.video.name} deleted.`)
+          this.notificationsService.success(
+            this.i18n('Success'),
+            this.i18n('Video {{ videoName }} deleted.', { videoName: this.video.name })
+          )
 
           // Go back to the video-list.
           this.redirectService.redirectToHomepage()
         },
 
-        error => this.notificationsService.error('Error', error.message)
+        error => this.notificationsService.error(this.i18n('Error'), error.message)
       )
   }
 
@@ -288,7 +296,10 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
   }
 
   private setVideoLikesBarTooltipText () {
-    this.likesBarTooltipText = `${this.video.likes} likes / ${this.video.dislikes} dislikes`
+    this.likesBarTooltipText = this.i18n(
+      '{{ likesNumber }} likes / {{ dislikesNumber }} dislikes',
+      { likesNumber: this.video.likes, dislikes: this.video.dislikes }
+    )
   }
 
   private handleError (err: any) {
@@ -298,12 +309,12 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
     let message = ''
 
     if (errorMessage.indexOf('http error') !== -1) {
-      message = 'Cannot fetch video from server, maybe down.'
+      message = this.i18n('Cannot fetch video from server, maybe down.')
     } else {
       message = errorMessage
     }
 
-    this.notificationsService.error('Error', message)
+    this.notificationsService.error(this.i18n('Error'), message)
   }
 
   private checkUserRating () {
@@ -318,7 +329,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
                          }
                        },
 
-                       err => this.notificationsService.error('Error', err.message)
+                       err => this.notificationsService.error(this.i18n('Error'), err.message)
                       )
   }
 
@@ -333,8 +344,8 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
 
     if (this.video.isVideoNSFWForUser(this.user, this.serverService.getConfig())) {
       const res = await this.confirmService.confirm(
-        'This video contains mature or explicit content. Are you sure you want to watch it?',
-        'Mature or explicit content'
+        this.i18n('This video contains mature or explicit content. Are you sure you want to watch it?'),
+        this.i18n('Mature or explicit content')
       )
       if (res === false) return this.redirectService.redirectToHomepage()
     }
@@ -399,7 +410,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
         this.updateVideoRating(this.userRating, nextRating)
         this.userRating = nextRating
       },
-      err => this.notificationsService.error('Error', err.message)
+      err => this.notificationsService.error(this.i18n('Error'), err.message)
      )
   }
 
index abab7504fed4e34efb68d6ed4a01f5ab47286f50..03568b618d4781f80f7b9aedf2e7de39a0329745 100644 (file)
@@ -8,6 +8,7 @@ import { AbstractVideoList } from '../../shared/video/abstract-video-list'
 import { VideoSortField } from '../../shared/video/sort-field.type'
 import { VideoService } from '../../shared/video/video.service'
 import { VideoFilter } from '../../../../../shared/models/videos/video-query.type'
+import { I18n } from '@ngx-translate/i18n-polyfill'
 
 @Component({
   selector: 'my-videos-local',
@@ -15,18 +16,23 @@ import { VideoFilter } from '../../../../../shared/models/videos/video-query.typ
   templateUrl: '../../shared/video/abstract-video-list.html'
 })
 export class VideoLocalComponent extends AbstractVideoList implements OnInit, OnDestroy {
-  titlePage = 'Local videos'
+  titlePage: string
   currentRoute = '/videos/local'
   sort = '-publishedAt' as VideoSortField
   filter: VideoFilter = 'local'
 
-  constructor (protected router: Router,
-               protected route: ActivatedRoute,
-               protected notificationsService: NotificationsService,
-               protected authService: AuthService,
-               protected location: Location,
-               private videoService: VideoService) {
+  constructor (
+    protected router: Router,
+    protected route: ActivatedRoute,
+    protected notificationsService: NotificationsService,
+    protected authService: AuthService,
+    protected location: Location,
+    private videoService: VideoService,
+    private i18n: I18n
+  ) {
     super()
+
+    this.titlePage = i18n('Local videos')
   }
 
   ngOnInit () {
index d064d9628ab16f1f33c7f08fb850185f91b6207a..5768d9fe0c8a2853db270927d008c7e78328204b 100644 (file)
@@ -7,6 +7,7 @@ import { AuthService } from '../../core/auth'
 import { AbstractVideoList } from '../../shared/video/abstract-video-list'
 import { VideoSortField } from '../../shared/video/sort-field.type'
 import { VideoService } from '../../shared/video/video.service'
+import { I18n } from '@ngx-translate/i18n-polyfill'
 
 @Component({
   selector: 'my-videos-recently-added',
@@ -14,17 +15,22 @@ import { VideoService } from '../../shared/video/video.service'
   templateUrl: '../../shared/video/abstract-video-list.html'
 })
 export class VideoRecentlyAddedComponent extends AbstractVideoList implements OnInit, OnDestroy {
-  titlePage = 'Recently added'
+  titlePage: string
   currentRoute = '/videos/recently-added'
   sort: VideoSortField = '-publishedAt'
 
-  constructor (protected router: Router,
-               protected route: ActivatedRoute,
-               protected location: Location,
-               protected notificationsService: NotificationsService,
-               protected authService: AuthService,
-               private videoService: VideoService) {
+  constructor (
+    protected router: Router,
+    protected route: ActivatedRoute,
+    protected location: Location,
+    protected notificationsService: NotificationsService,
+    protected authService: AuthService,
+    private videoService: VideoService,
+    private i18n: I18n
+  ) {
     super()
+
+    this.titlePage = i18n('Recently added')
   }
 
   ngOnInit () {
index aab896d84356302514e440e8ac62294b45dda3de..35566a7bd9d05574f8b971b91260e2349f81279f 100644 (file)
@@ -8,6 +8,7 @@ import { Subscription } from 'rxjs'
 import { AuthService } from '../../core/auth'
 import { AbstractVideoList } from '../../shared/video/abstract-video-list'
 import { VideoService } from '../../shared/video/video.service'
+import { I18n } from '@ngx-translate/i18n-polyfill'
 
 @Component({
   selector: 'my-videos-search',
@@ -15,7 +16,7 @@ import { VideoService } from '../../shared/video/video.service'
   templateUrl: '../../shared/video/abstract-video-list.html'
 })
 export class VideoSearchComponent extends AbstractVideoList implements OnInit, OnDestroy {
-  titlePage = 'Search'
+  titlePage: string
   currentRoute = '/videos/search'
   loadOnInit = false
 
@@ -24,15 +25,19 @@ export class VideoSearchComponent extends AbstractVideoList implements OnInit, O
   }
   private subActivatedRoute: Subscription
 
-  constructor (protected router: Router,
-               protected route: ActivatedRoute,
-               protected notificationsService: NotificationsService,
-               protected authService: AuthService,
-               protected location: Location,
-               private videoService: VideoService,
-               private redirectService: RedirectService
+  constructor (
+    protected router: Router,
+    protected route: ActivatedRoute,
+    protected notificationsService: NotificationsService,
+    protected authService: AuthService,
+    protected location: Location,
+    private videoService: VideoService,
+    private redirectService: RedirectService,
+    private i18n: I18n
   ) {
     super()
+
+    this.titlePage = i18n('Search')
   }
 
   ngOnInit () {
index ea65070f9c3c416f64af7664d568fa340f0feb8a..760470e8c24721787eefd5993c5938b53cc30856 100644 (file)
@@ -7,6 +7,7 @@ import { AuthService } from '../../core/auth'
 import { AbstractVideoList } from '../../shared/video/abstract-video-list'
 import { VideoSortField } from '../../shared/video/sort-field.type'
 import { VideoService } from '../../shared/video/video.service'
+import { I18n } from '@ngx-translate/i18n-polyfill'
 
 @Component({
   selector: 'my-videos-trending',
@@ -14,17 +15,22 @@ import { VideoService } from '../../shared/video/video.service'
   templateUrl: '../../shared/video/abstract-video-list.html'
 })
 export class VideoTrendingComponent extends AbstractVideoList implements OnInit, OnDestroy {
-  titlePage = 'Trending'
+  titlePage: string
   currentRoute = '/videos/trending'
   defaultSort: VideoSortField = '-views'
 
-  constructor (protected router: Router,
-               protected route: ActivatedRoute,
-               protected notificationsService: NotificationsService,
-               protected authService: AuthService,
-               protected location: Location,
-               private videoService: VideoService) {
+  constructor (
+    protected router: Router,
+    protected route: ActivatedRoute,
+    protected notificationsService: NotificationsService,
+    protected authService: AuthService,
+    protected location: Location,
+    private videoService: VideoService,
+    private i18n: I18n
+  ) {
     super()
+
+    this.titlePage = i18n('Trending')
   }
 
   ngOnInit () {
diff --git a/client/src/locale/source/messages_en_US.xml b/client/src/locale/source/messages_en_US.xml
new file mode 100644 (file)
index 0000000..6c355a9
--- /dev/null
@@ -0,0 +1,354 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
+  <file source-language="en-US" datatype="plaintext" original="ng2.template">
+    <body>
+      <trans-unit id="298cb43759c99e11e2ca5f92c768a145ddaa323f" datatype="html">
+        <source>
+           My public profile
+          </source>
+        <context-group purpose="location">
+          <context context-type="sourcefile">app/menu/menu.component.ts</context>
+          <context context-type="linenumber">17</context>
+        </context-group>
+      </trans-unit><trans-unit id="5f60990802486b7906b422d80aace6a1b19dcc02" datatype="html">
+        <source>Video not found :&apos;(</source>
+        <context-group purpose="location">
+          <context context-type="sourcefile">app/videos/+video-watch/video-watch.component.ts</context>
+          <context context-type="linenumber">6</context>
+        </context-group>
+      </trans-unit><trans-unit id="643ab402461b1169eebbe2ed790e12a9a83551aa" datatype="html">
+        <source>
+            &lt;x id="INTERPOLATION" equiv-text="{{ video.publishedAt | myFromNow }}"/&gt; - &lt;x id="INTERPOLATION_1" equiv-text="{{ video.views | myNumberFormatter }}"/&gt; views
+          </source>
+        <context-group purpose="location">
+          <context context-type="sourcefile">app/videos/+video-watch/video-watch.component.ts</context>
+          <context context-type="linenumber">15</context>
+        </context-group>
+      </trans-unit><trans-unit id="5cb397241041f7ad70997806227bafcdf7eb1b33" datatype="html">
+        <source>Go the channel page</source>
+        <context-group purpose="location">
+          <context context-type="sourcefile">app/videos/+video-watch/video-watch.component.ts</context>
+          <context context-type="linenumber">20</context>
+        </context-group>
+      </trans-unit><trans-unit id="912f005563d20191efc188dccedd35a7c4e6b396" datatype="html">
+        <source>You can subscribe to this account via any ActivityPub-capable fediverse instance. For instance with Mastodon or Pleroma you can type in the search box &lt;strong&gt;@&lt;x id="INTERPOLATION" equiv-text="{{video.account.displayName}}"/&gt;@&lt;x id="INTERPOLATION_1" equiv-text="{{video.account.host}}"/&gt;&lt;/strong&gt; and subscribe there. Subscription as a PeerTube user is being worked on in &lt;a href=&apos;https://github.com/Chocobozzz/PeerTube/issues/470&apos;&gt;#470&lt;/a&gt;.</source>
+        <context-group purpose="location">
+          <context context-type="sourcefile">app/videos/+video-watch/video-watch.component.ts</context>
+          <context context-type="linenumber">24</context>
+        </context-group>
+      </trans-unit><trans-unit id="ccc07df383b7a32be3e2e105faa5488caf261c1c" datatype="html">
+        <source>By &lt;x id="INTERPOLATION" equiv-text="{{ video.by }}"/&gt;</source>
+        <context-group purpose="location">
+          <context context-type="sourcefile">app/videos/+video-watch/video-watch.component.ts</context>
+          <context context-type="linenumber">29</context>
+        </context-group>
+      </trans-unit><trans-unit id="e88300c71e0cb0f346d5a72eb37c920f2aadae8a" datatype="html">
+        <source>Go the account page</source>
+        <context-group purpose="location">
+          <context context-type="sourcefile">app/videos/+video-watch/video-watch.component.ts</context>
+          <context context-type="linenumber">28</context>
+        </context-group>
+      </trans-unit><trans-unit id="82b59049f3f89d900c98da9319e156dd513e3ced" datatype="html">
+        <source>Like this video</source>
+        <context-group purpose="location">
+          <context context-type="sourcefile">app/videos/+video-watch/video-watch.component.ts</context>
+          <context context-type="linenumber">41</context>
+        </context-group>
+      </trans-unit><trans-unit id="623698f075025b2b2fc2e0c59fd95f4f4662a509" datatype="html">
+        <source>Dislike this video</source>
+        <context-group purpose="location">
+          <context context-type="sourcefile">app/videos/+video-watch/video-watch.component.ts</context>
+          <context context-type="linenumber">48</context>
+        </context-group>
+      </trans-unit><trans-unit id="b5629d298ff1a69b8db19a4ba2995c76b52da604" datatype="html">
+        <source>Support</source>
+        <context-group purpose="location">
+          <context context-type="sourcefile">app/videos/+video-watch/video-watch.component.ts</context>
+          <context context-type="linenumber">53</context>
+        </context-group>
+      </trans-unit><trans-unit id="0bd8b27f60a1f098a53e06328426d818e3508ff9" datatype="html">
+        <source>Share</source>
+        <context-group purpose="location">
+          <context context-type="sourcefile">app/videos/+video-watch/video-watch.component.ts</context>
+          <context context-type="linenumber">58</context>
+        </context-group>
+      </trans-unit><trans-unit id="dc75033a5238fdc4f462212c847a45ba8018a3fd" datatype="html">
+        <source>Download</source>
+        <context-group purpose="location">
+          <context context-type="sourcefile">app/videos/+video-watch/video-watch.component.ts</context>
+          <context context-type="linenumber">69</context>
+        </context-group>
+      </trans-unit><trans-unit id="144fff5c40b85414d59e644d8dee7cfefba925a2" datatype="html">
+        <source>Download the video</source>
+        <context-group purpose="location">
+          <context context-type="sourcefile">app/videos/+video-watch/video-watch.component.ts</context>
+          <context context-type="linenumber">68</context>
+        </context-group>
+      </trans-unit><trans-unit id="f72992030f134408b675152c397f9d0ec00f3b2a" datatype="html">
+        <source>Report</source>
+        <context-group purpose="location">
+          <context context-type="sourcefile">app/videos/+video-watch/video-watch.component.ts</context>
+          <context context-type="linenumber">75</context>
+        </context-group>
+      </trans-unit><trans-unit id="2f4894617d9c44010f87473e583bd4604b7d6ecf" datatype="html">
+        <source>Report this video</source>
+        <context-group purpose="location">
+          <context context-type="sourcefile">app/videos/+video-watch/video-watch.component.ts</context>
+          <context context-type="linenumber">74</context>
+        </context-group>
+      </trans-unit><trans-unit id="007ab5fa2aae8a7372307d3fc45a2dbcb11ffd61" datatype="html">
+        <source>Blacklist</source>
+        <context-group purpose="location">
+          <context context-type="sourcefile">app/videos/+video-watch/video-watch.component.ts</context>
+          <context context-type="linenumber">81</context>
+        </context-group>
+      </trans-unit><trans-unit id="803c6317abd2dbafcc93226c4e273c62932e3037" datatype="html">
+        <source>Blacklist this video</source>
+        <context-group purpose="location">
+          <context context-type="sourcefile">app/videos/+video-watch/video-watch.component.ts</context>
+          <context context-type="linenumber">80</context>
+        </context-group>
+      </trans-unit><trans-unit id="047f50bc5b5d17b5bec0196355953e1a5c590ddb" datatype="html">
+        <source>Update</source>
+        <context-group purpose="location">
+          <context context-type="sourcefile">app/videos/+video-watch/video-watch.component.ts</context>
+          <context context-type="linenumber">87</context>
+        </context-group>
+      </trans-unit><trans-unit id="cd27f761b923a5bdb16ba9844da632edd878f1b1" datatype="html">
+        <source>Update this video</source>
+        <context-group purpose="location">
+          <context context-type="sourcefile">app/videos/+video-watch/video-watch.component.ts</context>
+          <context context-type="linenumber">86</context>
+        </context-group>
+      </trans-unit><trans-unit id="826b25211922a1b46436589233cb6f1a163d89b7" datatype="html">
+        <source>Delete</source>
+        <context-group purpose="location">
+          <context context-type="sourcefile">app/videos/+video-watch/video-watch.component.ts</context>
+          <context context-type="linenumber">93</context>
+        </context-group>
+      </trans-unit><trans-unit id="3dbfdc68f83d91cb360172eb65578cae94e7cbe5" datatype="html">
+        <source>Delete this video</source>
+        <context-group purpose="location">
+          <context context-type="sourcefile">app/videos/+video-watch/video-watch.component.ts</context>
+          <context context-type="linenumber">92</context>
+        </context-group>
+      </trans-unit><trans-unit id="f0c5f6f270e70cbe063b5368fcf48f9afc1abd9b" datatype="html">
+        <source>Show more</source>
+        <context-group purpose="location">
+          <context context-type="sourcefile">app/videos/+video-watch/video-watch.component.ts</context>
+          <context context-type="linenumber">112</context>
+        </context-group>
+      </trans-unit><trans-unit id="5403a767248e304199592271bba3366d2ca3f903" datatype="html">
+        <source>Show less</source>
+        <context-group purpose="location">
+          <context context-type="sourcefile">app/videos/+video-watch/video-watch.component.ts</context>
+          <context context-type="linenumber">118</context>
+        </context-group>
+      </trans-unit><trans-unit id="8057a9b7f9e908ff350edfd71417b96c174e5911" datatype="html">
+        <source>
+            Privacy
+          </source>
+        <context-group purpose="location">
+          <context context-type="sourcefile">app/videos/+video-watch/video-watch.component.ts</context>
+          <context context-type="linenumber">125</context>
+        </context-group>
+      </trans-unit><trans-unit id="bd407eca607a8905a26a9e30c9d0cd70f4465db8" datatype="html">
+        <source>
+            Category
+          </source>
+        <context-group purpose="location">
+          <context context-type="sourcefile">app/videos/+video-watch/video-watch.component.ts</context>
+          <context context-type="linenumber">134</context>
+        </context-group>
+      </trans-unit><trans-unit id="af5072bd79ea3cd767ab74a6622d2eee791b3832" datatype="html">
+        <source>
+            Licence
+          </source>
+        <context-group purpose="location">
+          <context context-type="sourcefile">app/videos/+video-watch/video-watch.component.ts</context>
+          <context context-type="linenumber">143</context>
+        </context-group>
+      </trans-unit><trans-unit id="a911eee019174741b0aec6fcf3fbd5752fab3e67" datatype="html">
+        <source>
+            Language
+          </source>
+        <context-group purpose="location">
+          <context context-type="sourcefile">app/videos/+video-watch/video-watch.component.ts</context>
+          <context context-type="linenumber">152</context>
+        </context-group>
+      </trans-unit><trans-unit id="ecf7007c2842cc26a7b91d08d48c7a4f5f749fb3" datatype="html">
+        <source>
+            Tags
+          </source>
+        <context-group purpose="location">
+          <context context-type="sourcefile">app/videos/+video-watch/video-watch.component.ts</context>
+          <context context-type="linenumber">161</context>
+        </context-group>
+      </trans-unit><trans-unit id="7ce8b0d7cc34d4c1ef4a21e990b0a001337bedd1" datatype="html">
+        <source>
+        Other videos
+      </source>
+        <context-group purpose="location">
+          <context context-type="sourcefile">app/videos/+video-watch/video-watch.component.ts</context>
+          <context context-type="linenumber">175</context>
+        </context-group>
+      </trans-unit><trans-unit id="fb779d2b25c4d0ffa7d52c823a240717e8c1fe6c" datatype="html">
+        <source>Friendly Reminder:</source>
+        <context-group purpose="location">
+          <context context-type="sourcefile">app/videos/+video-watch/video-watch.component.ts</context>
+          <context context-type="linenumber">187</context>
+        </context-group>
+      </trans-unit><trans-unit id="4c2fca29fd9d7e85abe85a206958a4226f403be2" datatype="html">
+        <source>
+        The sharing system used by this video implies that some technical information about your system (such as a public IP address) can be accessed publicly.
+      </source>
+        <context-group purpose="location">
+          <context context-type="sourcefile">app/videos/+video-watch/video-watch.component.ts</context>
+          <context context-type="linenumber">189</context>
+        </context-group>
+      </trans-unit><trans-unit id="e60c11e1b1dfbbeda577364b8de39ded2d796c5e" datatype="html">
+        <source>More information</source>
+        <context-group purpose="location">
+          <context context-type="sourcefile">app/videos/+video-watch/video-watch.component.ts</context>
+          <context context-type="linenumber">192</context>
+        </context-group>
+      </trans-unit><trans-unit id="bd499ca7913bb5408fd139a4cb4f863852d5f318" datatype="html">
+        <source>Get more information</source>
+        <context-group purpose="location">
+          <context context-type="sourcefile">app/videos/+video-watch/video-watch.component.ts</context>
+          <context context-type="linenumber">192</context>
+        </context-group>
+      </trans-unit><trans-unit id="20fc98888baf65b5ba9fe9622dc036fa8dec6a5f" datatype="html">
+        <source>
+      OK
+    </source>
+        <context-group purpose="location">
+          <context context-type="sourcefile">app/videos/+video-watch/video-watch.component.ts</context>
+          <context context-type="linenumber">195</context>
+        </context-group>
+      </trans-unit>
+      <trans-unit id="23b2c2f4dd69e29c3bff00469e259dcb01de5633" datatype="html">
+        <source>Do you really want to blacklist this video?</source>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/videos/+video-watch/video-watch.component.ts</context>
+          <context context-type="linenumber">1</context>
+        </context-group>
+      </trans-unit>
+      <trans-unit id="1e035e6ccfab771cad4226b2ad230cb0d4a88cba" datatype="html">
+        <source>Success</source>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/videos/+video-watch/video-watch.component.ts</context>
+          <context context-type="linenumber">1</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/videos/+video-watch/video-watch.component.ts</context>
+          <context context-type="linenumber">1</context>
+        </context-group>
+      </trans-unit>
+      <trans-unit id="085d56464b75ae5c1e370f5290e4c4cf23961a61" datatype="html">
+        <source>Video &lt;x id="INTERPOLATION" equiv-text="{{ videoName }}"/&gt; had been blacklisted.</source>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/videos/+video-watch/video-watch.component.ts</context>
+          <context context-type="linenumber">1</context>
+        </context-group>
+      </trans-unit>
+      <trans-unit id="6080b77234e92ad41bb52653b239c4c4f851317d" datatype="html">
+        <source>Error</source>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/videos/+video-watch/video-watch.component.ts</context>
+          <context context-type="linenumber">1</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/videos/+video-watch/video-watch.component.ts</context>
+          <context context-type="linenumber">1</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/videos/+video-watch/video-watch.component.ts</context>
+          <context context-type="linenumber">1</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/videos/+video-watch/video-watch.component.ts</context>
+          <context context-type="linenumber">1</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/videos/+video-watch/video-watch.component.ts</context>
+          <context context-type="linenumber">1</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/videos/+video-watch/video-watch.component.ts</context>
+          <context context-type="linenumber">1</context>
+        </context-group>
+      </trans-unit>
+      <trans-unit id="f1abd89c9280323209e939fa9c30f6e5cda20c95" datatype="html">
+        <source>Do you really want to delete this video?</source>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/videos/+video-watch/video-watch.component.ts</context>
+          <context context-type="linenumber">1</context>
+        </context-group>
+      </trans-unit>
+      <trans-unit id="007c1d7080cf6da1ac264b23705246f0c53e3114" datatype="html">
+        <source>Video &lt;x id="INTERPOLATION" equiv-text="{{ videoName }}"/&gt; deleted.</source>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/videos/+video-watch/video-watch.component.ts</context>
+          <context context-type="linenumber">1</context>
+        </context-group>
+      </trans-unit>
+      <trans-unit id="cf9a064824f2fa3f01fd5544ad21032e33e60dca" datatype="html">
+        <source>&lt;x id="INTERPOLATION" equiv-text="{{ likesNumber }}"/&gt; likes / &lt;x id="INTERPOLATION_1" equiv-text="{{ dislikesNumber }}"/&gt; dislikes</source>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/videos/+video-watch/video-watch.component.ts</context>
+          <context context-type="linenumber">1</context>
+        </context-group>
+      </trans-unit>
+      <trans-unit id="4a400b174208188dcb46f2c23f4af9accfabaa3f" datatype="html">
+        <source>Cannot fetch video from server, maybe down.</source>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/videos/+video-watch/video-watch.component.ts</context>
+          <context context-type="linenumber">1</context>
+        </context-group>
+      </trans-unit>
+      <trans-unit id="ed013c2c29216501c688e9cb5f3a1c9fd9147b71" datatype="html">
+        <source>This video contains mature or explicit content. Are you sure you want to watch it?</source>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/videos/+video-watch/video-watch.component.ts</context>
+          <context context-type="linenumber">1</context>
+        </context-group>
+      </trans-unit>
+      <trans-unit id="5ba3d522e4146eefcbd5c222247c1e2423d27cd8" datatype="html">
+        <source>Mature or explicit content</source>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/videos/+video-watch/video-watch.component.ts</context>
+          <context context-type="linenumber">1</context>
+        </context-group>
+      </trans-unit>
+      <trans-unit id="b6307f83d9f43bff8d5129a7888e89964ddc3f7f" datatype="html">
+        <source>Local videos</source>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/videos/video-list/video-local.component.ts</context>
+          <context context-type="linenumber">1</context>
+        </context-group>
+      </trans-unit>
+      <trans-unit id="8d20c5f5dd30acbe71316544dab774393fd9c3c1" datatype="html">
+        <source>Recently added</source>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/videos/video-list/video-recently-added.component.ts</context>
+          <context context-type="linenumber">1</context>
+        </context-group>
+      </trans-unit>
+      <trans-unit id="7e892ba15f2c6c17e83510e273b3e10fc32ea016" datatype="html">
+        <source>Search</source>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/videos/video-list/video-search.component.ts</context>
+          <context context-type="linenumber">1</context>
+        </context-group>
+      </trans-unit>
+      <trans-unit id="b6b7986bc3721ac483baf20bc9a320529075c807" datatype="html">
+        <source>Trending</source>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/videos/video-list/video-trending.component.ts</context>
+          <context context-type="linenumber">1</context>
+        </context-group>
+      </trans-unit>
+    </body>
+  </file>
+</xliff>
diff --git a/client/src/locale/target/messages_fr.xml b/client/src/locale/target/messages_fr.xml
new file mode 100644 (file)
index 0000000..3a55922
--- /dev/null
@@ -0,0 +1,191 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--XLIFF document generated by Zanata. Visit http://zanata.org for more infomation.-->
+<xliff xmlns="urn:oasis:names:tc:xliff:document:1.1" xmlns:xyz="urn:appInfo:Items" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="urn:oasis:names:tc:xliff:document:1.1 http://www.oasis-open.org/committees/xliff/documents/xliff-core-1.1.xsd" version="1.1">
+  <file source-language="en-US" datatype="plaintext" original="" target-language="fr">
+    <body>
+      <trans-unit id="298cb43759c99e11e2ca5f92c768a145ddaa323f">
+        <source>
+           My public profile
+          </source>
+        <target>Mon profile public</target>
+        <context-group name="null">
+          <context context-type="linenumber">17</context>
+        </context-group>
+      </trans-unit>
+      <trans-unit id="5f60990802486b7906b422d80aace6a1b19dcc02">
+        <source>Video not found :'(</source>
+        <target>Vidéo non trouvée :'(</target>
+        <context-group name="null">
+          <context context-type="linenumber">6</context>
+        </context-group>
+      </trans-unit>
+      <trans-unit id="643ab402461b1169eebbe2ed790e12a9a83551aa">
+        <source>
+            <x id="INTERPOLATION" equiv-text="{{ video.publishedAt | myFromNow }}"/> - <x id="INTERPOLATION_1" equiv-text="{{ video.views | myNumberFormatter }}"/> views
+          </source>
+        <target>
+            <x id="INTERPOLATION" equiv-text="{{ video.publishedAt | myFromNow }}"/> - <x id="INTERPOLATION_1" equiv-text="{{ video.views | myNumberFormatter }}"/> vues          </target>
+        <context-group name="null">
+          <context context-type="linenumber">15</context>
+        </context-group>
+      </trans-unit>
+      <trans-unit id="912f005563d20191efc188dccedd35a7c4e6b396">
+        <source>You can subscribe to this account via any ActivityPub-capable fediverse instance. For instance with Mastodon or Pleroma you can type in the search box &lt;strong&gt;@<x id="INTERPOLATION" equiv-text="{{video.account.displayName}}"/>@<x id="INTERPOLATION_1" equiv-text="{{video.account.host}}"/>&lt;/strong&gt; and subscribe there. Subscription as a PeerTube user is being worked on in &lt;a href='https://github.com/Chocobozzz/PeerTube/issues/470'&gt;#470&lt;/a&gt;.</source>
+        <target>You can subscribe to this account via any ActivityPub-capable fediverse instance. For instance with Mastodon or Pleroma you can type in the search box &lt;strong&gt;@<x id="INTERPOLATION" equiv-text="{{video.account.displayName}}"/>@<x id="INTERPOLATION_1" equiv-text="{{video.account.host}}"/>&lt;/strong&gt; and subscribe there. Subscription as a PeerTube user is being worked on in &lt;a href='https://github.com/Chocobozzz/PeerTube/issues/470'&gt;#470&lt;/a&gt;.</target>
+        <context-group name="null">
+          <context context-type="linenumber">24</context>
+        </context-group>
+      </trans-unit>
+      <trans-unit id="ccc07df383b7a32be3e2e105faa5488caf261c1c">
+        <source>By <x id="INTERPOLATION" equiv-text="{{ video.by }}"/></source>
+        <target>Par <x id="INTERPOLATION" equiv-text="{{ video.by }}"/></target>
+        <context-group name="null">
+          <context context-type="linenumber">29</context>
+        </context-group>
+      </trans-unit>
+      <trans-unit id="e88300c71e0cb0f346d5a72eb37c920f2aadae8a">
+        <source>Go the account page</source>
+        <target>Aller sur la page du compte</target>
+        <context-group name="null">
+          <context context-type="linenumber">28</context>
+        </context-group>
+      </trans-unit>
+      <trans-unit id="82b59049f3f89d900c98da9319e156dd513e3ced">
+        <source>Like this video</source>
+        <target>J'aime cette vidéo</target>
+        <context-group name="null">
+          <context context-type="linenumber">41</context>
+        </context-group>
+      </trans-unit>
+      <trans-unit id="623698f075025b2b2fc2e0c59fd95f4f4662a509">
+        <source>Dislike this video</source>
+        <target>Je n'aime pas cette vidéo</target>
+        <context-group name="null">
+          <context context-type="linenumber">48</context>
+        </context-group>
+      </trans-unit>
+      <trans-unit id="b5629d298ff1a69b8db19a4ba2995c76b52da604">
+        <source>Support</source>
+        <target>Supporter</target>
+        <context-group name="null">
+          <context context-type="linenumber">53</context>
+        </context-group>
+      </trans-unit>
+      <trans-unit id="0bd8b27f60a1f098a53e06328426d818e3508ff9">
+        <source>Share</source>
+        <target>Partager</target>
+        <context-group name="null">
+          <context context-type="linenumber">58</context>
+        </context-group>
+      </trans-unit>
+      <trans-unit id="dc75033a5238fdc4f462212c847a45ba8018a3fd">
+        <source>Download</source>
+        <target>Télécharger</target>
+        <context-group name="null">
+          <context context-type="linenumber">69</context>
+        </context-group>
+      </trans-unit>
+      <trans-unit id="144fff5c40b85414d59e644d8dee7cfefba925a2">
+        <source>Download the video</source>
+        <target>Télécharger la vidéo</target>
+        <context-group name="null">
+          <context context-type="linenumber">68</context>
+        </context-group>
+      </trans-unit>
+      <trans-unit id="f72992030f134408b675152c397f9d0ec00f3b2a">
+        <source>Report</source>
+        <target>Signaler</target>
+        <context-group name="null">
+          <context context-type="linenumber">75</context>
+        </context-group>
+      </trans-unit>
+      <trans-unit id="2f4894617d9c44010f87473e583bd4604b7d6ecf">
+        <source>Report this video</source>
+        <target>Signaler cette vidéo</target>
+        <context-group name="null">
+          <context context-type="linenumber">74</context>
+        </context-group>
+      </trans-unit>
+      <trans-unit id="007ab5fa2aae8a7372307d3fc45a2dbcb11ffd61">
+        <source>Blacklist</source>
+        <target>Blacklister</target>
+        <context-group name="null">
+          <context context-type="linenumber">81</context>
+        </context-group>
+      </trans-unit>
+      <trans-unit id="803c6317abd2dbafcc93226c4e273c62932e3037">
+        <source>Blacklist this video</source>
+        <target>Blacklister cette vidéo</target>
+        <context-group name="null">
+          <context context-type="linenumber">80</context>
+        </context-group>
+      </trans-unit>
+      <trans-unit id="047f50bc5b5d17b5bec0196355953e1a5c590ddb">
+        <source>Update</source>
+        <target>Mettre à jour</target>
+        <context-group name="null">
+          <context context-type="linenumber">87</context>
+        </context-group>
+      </trans-unit>
+      <trans-unit id="cd27f761b923a5bdb16ba9844da632edd878f1b1">
+        <source>Update this video</source>
+        <target>Mettre à jour cette vidéo</target>
+        <context-group name="null">
+          <context context-type="linenumber">86</context>
+        </context-group>
+      </trans-unit>
+      <trans-unit id="826b25211922a1b46436589233cb6f1a163d89b7">
+        <source>Delete</source>
+        <target>Supprimer</target>
+        <context-group name="null">
+          <context context-type="linenumber">93</context>
+        </context-group>
+      </trans-unit>
+      <trans-unit id="3dbfdc68f83d91cb360172eb65578cae94e7cbe5">
+        <source>Delete this video</source>
+        <target>Supprimer cette vidéo</target>
+        <context-group name="null">
+          <context context-type="linenumber">92</context>
+        </context-group>
+      </trans-unit>
+      <trans-unit id="f0c5f6f270e70cbe063b5368fcf48f9afc1abd9b">
+        <source>Show more</source>
+        <target>Montrer plus</target>
+        <context-group name="null">
+          <context context-type="linenumber">112</context>
+        </context-group>
+      </trans-unit>
+      <trans-unit id="5403a767248e304199592271bba3366d2ca3f903">
+        <source>Show less</source>
+        <target>Montrer moins</target>
+        <context-group name="null">
+          <context context-type="linenumber">118</context>
+        </context-group>
+      </trans-unit>
+      <trans-unit id="8057a9b7f9e908ff350edfd71417b96c174e5911">
+        <source>
+            Privacy
+          </source>
+        <target>Visibilité</target>
+        <context-group name="null">
+          <context context-type="linenumber">125</context>
+        </context-group>
+      </trans-unit>
+      <trans-unit id="bd407eca607a8905a26a9e30c9d0cd70f4465db8">
+        <source>
+            Category
+          </source>
+        <target>Catégorie</target>
+        <context-group name="null">
+          <context context-type="linenumber">134</context>
+        </context-group>
+      </trans-unit>
+      <trans-unit id="b6b7986bc3721ac483baf20bc9a320529075c807">
+        <source>Trending</source>
+        <target>Tendances</target>
+        <context-group name="null">
+          <context context-type="linenumber">1</context>
+        </context-group>
+      </trans-unit>
+    </body>
+  </file></xliff>
\ No newline at end of file
index fe2e040d8d6e8bda129294e64d0c1f4ff203bcf5..e2d0da541362c11e276036a88d818dc3e75908f8 100644 (file)
   dependencies:
     tslib "~1.9.0"
 
+"@ngx-translate/i18n-polyfill@^1.0.0":
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/@ngx-translate/i18n-polyfill/-/i18n-polyfill-1.0.0.tgz#145edb28bcfc1332e1bc25279eadf9d4ed0a20f8"
+  dependencies:
+    glob "7.1.2"
+    tslib "^1.9.0"
+    yargs "10.0.3"
+
 "@nodelib/fs.stat@^1.0.1":
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-1.1.0.tgz#50c1e2260ac0ed9439a181de3725a0168d59c48a"
@@ -4189,6 +4197,17 @@ glob@7.0.x:
     once "^1.3.0"
     path-is-absolute "^1.0.0"
 
+glob@7.1.2, glob@^7.0.0, glob@^7.0.3, glob@^7.0.5, glob@^7.0.6, glob@^7.1.1, glob@^7.1.2, glob@~7.1.1:
+  version "7.1.2"
+  resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15"
+  dependencies:
+    fs.realpath "^1.0.0"
+    inflight "^1.0.4"
+    inherits "2"
+    minimatch "^3.0.4"
+    once "^1.3.0"
+    path-is-absolute "^1.0.0"
+
 glob@^5.0.15:
   version "5.0.15"
   resolved "https://registry.yarnpkg.com/glob/-/glob-5.0.15.tgz#1bc936b9e02f4a603fcc222ecf7633d30b8b93b1"
@@ -4209,17 +4228,6 @@ glob@^6.0.4:
     once "^1.3.0"
     path-is-absolute "^1.0.0"
 
-glob@^7.0.0, glob@^7.0.3, glob@^7.0.5, glob@^7.0.6, glob@^7.1.1, glob@^7.1.2, glob@~7.1.1:
-  version "7.1.2"
-  resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15"
-  dependencies:
-    fs.realpath "^1.0.0"
-    inflight "^1.0.4"
-    inherits "2"
-    minimatch "^3.0.4"
-    once "^1.3.0"
-    path-is-absolute "^1.0.0"
-
 global-modules@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/global-modules/-/global-modules-1.0.0.tgz#6d770f0eb523ac78164d72b5e71a8877265cc3ea"
@@ -10594,12 +10602,35 @@ yargs-parser@^7.0.0:
   dependencies:
     camelcase "^4.1.0"
 
+yargs-parser@^8.0.0:
+  version "8.1.0"
+  resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-8.1.0.tgz#f1376a33b6629a5d063782944da732631e966950"
+  dependencies:
+    camelcase "^4.1.0"
+
 yargs-parser@^9.0.2:
   version "9.0.2"
   resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-9.0.2.tgz#9ccf6a43460fe4ed40a9bb68f48d43b8a68cc077"
   dependencies:
     camelcase "^4.1.0"
 
+yargs@10.0.3:
+  version "10.0.3"
+  resolved "https://registry.yarnpkg.com/yargs/-/yargs-10.0.3.tgz#6542debd9080ad517ec5048fb454efe9e4d4aaae"
+  dependencies:
+    cliui "^3.2.0"
+    decamelize "^1.1.1"
+    find-up "^2.1.0"
+    get-caller-file "^1.0.1"
+    os-locale "^2.0.0"
+    require-directory "^2.1.1"
+    require-main-filename "^1.0.1"
+    set-blocking "^2.0.0"
+    string-width "^2.0.0"
+    which-module "^2.0.0"
+    y18n "^3.2.1"
+    yargs-parser "^8.0.0"
+
 yargs@11.0.0:
   version "11.0.0"
   resolved "https://registry.yarnpkg.com/yargs/-/yargs-11.0.0.tgz#c052931006c5eee74610e5fc0354bedfd08a201b"
index 608646e7d84422b5783b40e982e1e130d9ec272a..21701e6649b225c8afc8f9cf5fe33fd90f959498 100644 (file)
@@ -29,6 +29,7 @@
     "danger:clean:dev": "scripty",
     "danger:clean:prod": "scripty",
     "danger:clean:modules": "scripty",
+    "i18n:generate": "scripty",
     "reset-password": "node ./dist/scripts/reset-password.js",
     "play": "scripty",
     "dev": "scripty",
index 305af1e5fd4ece5c320e7539a4cd5ab13857bb93..61ba4ea99068c9e381244bbb399fcb8fa68eb1ca 100755 (executable)
@@ -6,5 +6,19 @@ cd client
 
 rm -rf ./dist ./compiled
 
-npm run ng build -- --prod --stats-json
+defaultLanguage="en-US"
+npm run ng build -- --output-path "dist/$defaultLanguage/" --deploy-url "/client/$defaultLanguage/" --prod --stats-json
+mv "./dist/$defaultLanguage/assets" "./dist"
+
+languages="fr"
+
+for lang in "$languages"; do
+    npm run ng build -- --prod --i18n-file "./src/locale/target/messages_$lang.xml" --i18n-format xlf --i18n-locale "$lang" \
+        --output-path "dist/$lang/" --deploy-url "/client/$lang/"
+
+    # Do no duplicate assets
+    rm -r "./dist/$lang/assets"
+done
+
 NODE_ENV=production npm run webpack -- --config webpack/webpack.video-embed.js --mode production
+
diff --git a/scripts/i18n/generate.sh b/scripts/i18n/generate.sh
new file mode 100755 (executable)
index 0000000..429523b
--- /dev/null
@@ -0,0 +1,11 @@
+#!/bin/sh
+
+set -eu
+
+cd client
+npm run ng -- xi18n --i18n-locale "en-US" --output-path locale/source --out-file messages_en_US.xml
+npm run ngx-extractor -- --locale "en-US" -i 'src/**/*.ts' -f xlf -o src/locale/source/messages_en_US.xml
+
+# Zanata does not support inner elements in <source>, so we hack these special elements
+# This regex translate the Angular elements to special entities (that we will reconvert on pull)
+sed -i 's/<x id=\([^\/]\+\?\)\/>/\&lt;x id=\1\/\&gt;/g' src/locale/source/messages_en_US.xml
\ No newline at end of file
diff --git a/scripts/i18n/pull-hook.sh b/scripts/i18n/pull-hook.sh
new file mode 100755 (executable)
index 0000000..cb969f8
--- /dev/null
@@ -0,0 +1,7 @@
+#!/bin/sh
+
+set -eu
+
+# Zanata does not support inner elements in <source>, so we hack these special elements
+# This regex translate the converted elements to initial Angular elements
+sed -i 's/\&lt;x id=\([^\/]\+\?\)\/\&gt;/<x id=\1\/>/g' client/src/locale/target/*
\ No newline at end of file
index 8c73a1fd61b7dbbabbbfa4c1d5f8c1195d1c3e56..393955264b2155ecbc745fcec88c08da47d3fd10 100755 (executable)
@@ -57,7 +57,7 @@ git commit package.json client/package.json -m "Bumped to version $version"
 git tag -s -a "$version" -m "$version"
 
 npm run build
-rm "./client/dist/stats.json"
+rm "./client/dist/en-US/stats.json"
 
 # Creating the archives
 (
index bdcbb79886fe874c57d31ebef2853853fc2e1a5e..c0e679b02539195cc97bb3583e58af9b8dda5cf3 100644 (file)
--- a/server.ts
+++ b/server.ts
@@ -12,7 +12,6 @@ import * as bodyParser from 'body-parser'
 import * as express from 'express'
 import * as http from 'http'
 import * as morgan from 'morgan'
-import * as path from 'path'
 import * as bitTorrentTracker from 'bittorrent-tracker'
 import * as cors from 'cors'
 import { Server as WebSocketServer } from 'ws'
@@ -156,20 +155,11 @@ app.use('/', activityPubRouter)
 app.use('/', feedsRouter)
 app.use('/', webfingerRouter)
 
-// Client files
-app.use('/', clientsRouter)
-
 // Static files
 app.use('/', staticRouter)
 
-// Always serve index client page (the client is a single page application, let it handle routing)
-app.use('/*', function (req, res) {
-  if (req.accepts(ACCEPT_HEADERS) === 'html') {
-    return res.sendFile(path.join(__dirname, '../client/dist/index.html'))
-  }
-
-  return res.status(404).end()
-})
+// Client files, last valid routes!
+app.use('/', clientsRouter)
 
 // ----------- Errors -----------
 
index aff00fe6e7b4bbbe7ba211792ddc6a3caf910e59..a29b51c51d57a4b8d9acf45387aab3b2b25312a6 100644 (file)
@@ -3,17 +3,24 @@ import * as express from 'express'
 import { join } from 'path'
 import * as validator from 'validator'
 import { escapeHTML, readFileBufferPromise, root } from '../helpers/core-utils'
-import { CONFIG, EMBED_SIZE, OPENGRAPH_AND_OEMBED_COMMENT, STATIC_MAX_AGE, STATIC_PATHS } from '../initializers'
+import {
+  ACCEPT_HEADERS,
+  CONFIG,
+  EMBED_SIZE,
+  OPENGRAPH_AND_OEMBED_COMMENT,
+  STATIC_MAX_AGE,
+  STATIC_PATHS
+} from '../initializers'
 import { asyncMiddleware } from '../middlewares'
 import { VideoModel } from '../models/video/video'
 import { VideoPrivacy } from '../../shared/models/videos'
+import { I18N_LOCALES, is18nLocale, getDefaultLocale } from '../../shared/models'
 
 const clientsRouter = express.Router()
 
 const distPath = join(root(), 'client', 'dist')
 const assetsImagesPath = join(root(), 'client', 'dist', 'assets', 'images')
 const embedPath = join(distPath, 'standalone', 'videos', 'embed.html')
-const indexPath = join(distPath, 'index.html')
 
 // Special route that add OpenGraph and oEmbed tags
 // Do not use a template engine for a so little thing
@@ -45,6 +52,16 @@ clientsRouter.use('/client/*', (req: express.Request, res: express.Response, nex
   res.sendStatus(404)
 })
 
+// Always serve index client page (the client is a single page application, let it handle routing)
+// Try to provide the right language index.html
+clientsRouter.use('/(:language)?', function (req, res) {
+  if (req.accepts(ACCEPT_HEADERS) === 'html') {
+    return res.sendFile(getIndexPath(req, req.params.language))
+  }
+
+  return res.status(404).end()
+})
+
 // ---------------------------------------------------------------------------
 
 export {
@@ -53,6 +70,19 @@ export {
 
 // ---------------------------------------------------------------------------
 
+function getIndexPath (req: express.Request, paramLang?: string) {
+  let lang: string
+
+  // Check param lang validity
+  if (paramLang && is18nLocale(paramLang)) {
+    lang = paramLang
+  } else {
+    lang = req.acceptsLanguages(Object.keys(I18N_LOCALES)) || getDefaultLocale()
+  }
+
+  return join(__dirname, '../../../client/dist/' + lang + '/index.html')
+}
+
 function addOpenGraphAndOEmbedTags (htmlStringPage: string, video: VideoModel) {
   const previewUrl = CONFIG.WEBSERVER.URL + STATIC_PATHS.PREVIEWS + video.getPreviewName()
   const videoUrl = CONFIG.WEBSERVER.URL + '/videos/watch/' + video.uuid
@@ -142,18 +172,18 @@ async function generateWatchHtmlPage (req: express.Request, res: express.Respons
   } else if (validator.isInt(videoId)) {
     videoPromise = VideoModel.loadAndPopulateAccountAndServerAndTags(+videoId)
   } else {
-    return res.sendFile(indexPath)
+    return res.sendFile(getIndexPath(req))
   }
 
   let [ file, video ] = await Promise.all([
-    readFileBufferPromise(indexPath),
+    readFileBufferPromise(getIndexPath(req)),
     videoPromise
   ])
 
   const html = file.toString()
 
   // Let Angular application handle errors
-  if (!video || video.privacy === VideoPrivacy.PRIVATE) return res.sendFile(indexPath)
+  if (!video || video.privacy === VideoPrivacy.PRIVATE) return res.sendFile(getIndexPath(req))
 
   const htmlStringPageWithTags = addOpenGraphAndOEmbedTags(html, video)
   res.set('Content-Type', 'text/html; charset=UTF-8').send(htmlStringPageWithTags)
diff --git a/shared/models/i18n/i18n.ts b/shared/models/i18n/i18n.ts
new file mode 100644 (file)
index 0000000..2d3a1d3
--- /dev/null
@@ -0,0 +1,30 @@
+export const I18N_LOCALES = {
+  'en-US': 'English (US)',
+  fr: 'French'
+}
+
+export function getDefaultLocale () {
+  return 'en-US'
+}
+
+const possiblePaths = Object.keys(I18N_LOCALES).map(l => '/' + l)
+export function is18nPath (path: string) {
+  return possiblePaths.indexOf(path) !== -1
+}
+
+const possibleLanguages = Object.keys(I18N_LOCALES)
+export function is18nLocale (locale: string) {
+  return possibleLanguages.indexOf(locale) !== -1
+}
+
+// Only use in dev mode, so relax
+// In production, the locale always match with a I18N_LANGUAGES key
+export function buildFileLocale (locale: string) {
+  if (!is18nLocale(locale)) {
+    // Some working examples for development purpose
+    if (locale.split('-')[ 0 ] === 'en') return 'en_US'
+    else if (locale === 'fr') return 'fr'
+  }
+
+  return locale.replace('-', '_')
+}
diff --git a/shared/models/i18n/index.ts b/shared/models/i18n/index.ts
new file mode 100644 (file)
index 0000000..8f7cbe2
--- /dev/null
@@ -0,0 +1 @@
+export * from './i18n'
index 95bc402d613bd3059f0b6cc22ed31f3a384132e4..c8ce71f1727d7457a054336a428618d21f4a55ca 100644 (file)
@@ -3,6 +3,7 @@ export * from './activitypub'
 export * from './users'
 export * from './videos'
 export * from './feeds'
+export * from './i18n'
 export * from './server/job.model'
 export * from './oauth-client-local.model'
 export * from './result-list.model'
index c1fed9c6001125866e582b9a614c2086809b5dd8..eb06faac058c48cbfcc5764ff34b795877168a53 100644 (file)
--- a/yarn.lock
+++ b/yarn.lock
@@ -1237,7 +1237,7 @@ chalk@^1.0.0, chalk@^1.1.1, chalk@^1.1.3:
     strip-ansi "^3.0.0"
     supports-color "^2.0.0"
 
-chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0, chalk@^2.3.0, chalk@^2.3.1, chalk@^2.3.2:
+chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0, chalk@^2.3.0, chalk@^2.3.1:
   version "2.4.1"
   resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.1.tgz#18c49ab16a037b6eb0152cc83e3471338215b66e"
   dependencies:
@@ -1517,7 +1517,7 @@ command-exists@^1.2.2:
   version "1.2.6"
   resolved "https://registry.yarnpkg.com/command-exists/-/command-exists-1.2.6.tgz#577f8e5feb0cb0f159cd557a51a9be1bdd76e09e"
 
-commander@*, commander@2.15.1, commander@^2.12.1, commander@^2.13.0, commander@^2.14.1, commander@^2.15.1, commander@^2.8.1, commander@^2.9.0:
+commander@*, commander@2.15.1, commander@^2.12.1, commander@^2.13.0, commander@^2.14.1, commander@^2.8.1, commander@^2.9.0:
   version "2.15.1"
   resolved "https://registry.yarnpkg.com/commander/-/commander-2.15.1.tgz#df46e867d0fc2aec66a34662b406a9ccafff5b0f"
 
@@ -2175,12 +2175,6 @@ error-ex@^1.2.0, error-ex@^1.3.1:
   dependencies:
     is-arrayish "^0.2.1"
 
-error-stack-parser@^2.0.1:
-  version "2.0.1"
-  resolved "https://registry.yarnpkg.com/error-stack-parser/-/error-stack-parser-2.0.1.tgz#a3202b8fb03114aa9b40a0e3669e48b2b65a010a"
-  dependencies:
-    stackframe "^1.0.3"
-
 es5-ext@^0.10.14, es5-ext@^0.10.35, es5-ext@^0.10.9, es5-ext@~0.10.14:
   version "0.10.43"
   resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.43.tgz#c705e645253210233a270869aa463a2333b7ca64"
@@ -4155,7 +4149,7 @@ js-tokens@^3.0.0, js-tokens@^3.0.2:
   version "3.0.2"
   resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b"
 
-js-yaml@^3.11.0, js-yaml@^3.4.6, js-yaml@^3.5.1, js-yaml@^3.5.4, js-yaml@^3.7.0, js-yaml@^3.8.2, js-yaml@^3.8.3, js-yaml@^3.9.0:
+js-yaml@^3.4.6, js-yaml@^3.5.1, js-yaml@^3.5.4, js-yaml@^3.7.0, js-yaml@^3.8.2, js-yaml@^3.8.3, js-yaml@^3.9.0:
   version "3.11.0"
   resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.11.0.tgz#597c1a8bd57152f26d622ce4117851a51f5ebaef"
   dependencies:
@@ -6741,18 +6735,6 @@ sass-graph@^2.2.4:
     scss-tokenizer "^0.2.3"
     yargs "^7.0.0"
 
-sass-lint-auto-fix@^0.9.0:
-  version "0.9.2"
-  resolved "https://registry.yarnpkg.com/sass-lint-auto-fix/-/sass-lint-auto-fix-0.9.2.tgz#b8b6eb95644f7919dfea33d04c1fc19ae8f07a11"
-  dependencies:
-    chalk "^2.3.2"
-    commander "^2.15.1"
-    glob "^7.1.2"
-    gonzales-pe-sl "^4.2.3"
-    js-yaml "^3.11.0"
-    sass-lint "^1.12.1"
-    stacktrace-js "^2.0.0"
-
 sass-lint@^1.12.1:
   version "1.12.1"
   resolved "https://registry.yarnpkg.com/sass-lint/-/sass-lint-1.12.1.tgz#630f69c216aa206b8232fb2aa907bdf3336b6d83"
@@ -7194,10 +7176,6 @@ source-map@0.4.x, source-map@^0.4.2, source-map@^0.4.4:
   dependencies:
     amdefine ">=0.0.4"
 
-source-map@0.5.6:
-  version "0.5.6"
-  resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.6.tgz#75ce38f52bf0733c5a7f0c118d81334a2bb5f412"
-
 source-map@0.5.x, source-map@^0.5.6, source-map@~0.5.1:
   version "0.5.7"
   resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc"
@@ -7315,12 +7293,6 @@ stack-chain@1.3.x, stack-chain@~1.3.1:
   version "1.3.7"
   resolved "https://registry.yarnpkg.com/stack-chain/-/stack-chain-1.3.7.tgz#d192c9ff4ea6a22c94c4dd459171e3f00cea1285"
 
-stack-generator@^2.0.1:
-  version "2.0.2"
-  resolved "https://registry.yarnpkg.com/stack-generator/-/stack-generator-2.0.2.tgz#3c13d952a596ab9318fec0669d0a1df8b87176c7"
-  dependencies:
-    stackframe "^1.0.4"
-
 stack-trace@0.0.x:
   version "0.0.10"
   resolved "https://registry.yarnpkg.com/stack-trace/-/stack-trace-0.0.10.tgz#547c70b347e8d32b4e108ea1a2a159e5fdde19c0"
@@ -7329,25 +7301,6 @@ stack-utils@^1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-1.0.1.tgz#d4f33ab54e8e38778b0ca5cfd3b3afb12db68620"
 
-stackframe@^1.0.3, stackframe@^1.0.4:
-  version "1.0.4"
-  resolved "https://registry.yarnpkg.com/stackframe/-/stackframe-1.0.4.tgz#357b24a992f9427cba6b545d96a14ed2cbca187b"
-
-stacktrace-gps@^3.0.1:
-  version "3.0.2"
-  resolved "https://registry.yarnpkg.com/stacktrace-gps/-/stacktrace-gps-3.0.2.tgz#33f8baa4467323ab2bd1816efa279942ba431ccc"
-  dependencies:
-    source-map "0.5.6"
-    stackframe "^1.0.4"
-
-stacktrace-js@^2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/stacktrace-js/-/stacktrace-js-2.0.0.tgz#776ca646a95bc6c6b2b90776536a7fc72c6ddb58"
-  dependencies:
-    error-stack-parser "^2.0.1"
-    stack-generator "^2.0.1"
-    stacktrace-gps "^3.0.1"
-
 staged-git-files@1.1.1:
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/staged-git-files/-/staged-git-files-1.1.1.tgz#37c2218ef0d6d26178b1310719309a16a59f8f7b"
diff --git a/zanata.xml b/zanata.xml
new file mode 100644 (file)
index 0000000..d68b3a3
--- /dev/null
@@ -0,0 +1,15 @@
+<?xml version='1.0' encoding='UTF-8' standalone='yes'?>
+<config xmlns="http://zanata.org/namespace/config/">
+  <url>https://trad.framasoft.org/zanata/</url>
+  <project>peertube</project>
+  <project-version>develop</project-version>
+  <project-type>xliff</project-type>
+  <src-dir>./client/src/locale/source</src-dir>
+  <trans-dir>./client/src/locale/target</trans-dir>
+
+  <hooks>
+    <hook command="pull">
+      <after>./scripts/i18n/pull-hook.sh</after>
+    </hook>
+  </hooks>
+</config>