aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--.gitignore1
-rw-r--r--client/package.json4
-rw-r--r--client/src/app/app.component.ts7
-rw-r--r--client/src/app/app.module.ts20
-rw-r--r--client/src/app/core/routing/redirect.service.ts4
-rw-r--r--client/src/app/shared/misc/utils.ts2
-rw-r--r--client/src/app/shared/shared.module.ts4
-rw-r--r--client/src/app/videos/+video-watch/video-watch.component.html66
-rw-r--r--client/src/app/videos/+video-watch/video-watch.component.ts41
-rw-r--r--client/src/app/videos/video-list/video-local.component.ts20
-rw-r--r--client/src/app/videos/video-list/video-recently-added.component.ts20
-rw-r--r--client/src/app/videos/video-list/video-search.component.ts21
-rw-r--r--client/src/app/videos/video-list/video-trending.component.ts20
-rw-r--r--client/src/locale/source/messages_en_US.xml354
-rw-r--r--client/src/locale/target/messages_fr.xml191
-rw-r--r--client/yarn.lock53
-rw-r--r--package.json1
-rwxr-xr-xscripts/build/client.sh16
-rwxr-xr-xscripts/i18n/generate.sh11
-rwxr-xr-xscripts/i18n/pull-hook.sh7
-rwxr-xr-xscripts/release.sh2
-rw-r--r--server.ts14
-rw-r--r--server/controllers/client.ts40
-rw-r--r--shared/models/i18n/i18n.ts30
-rw-r--r--shared/models/i18n/index.ts1
-rw-r--r--shared/models/index.ts1
-rw-r--r--yarn.lock53
-rw-r--r--zanata.xml15
28 files changed, 853 insertions, 166 deletions
diff --git a/.gitignore b/.gitignore
index 5b5025044..92af76310 100644
--- a/.gitignore
+++ b/.gitignore
@@ -25,3 +25,4 @@
25/logs/ 25/logs/
26/server/tools/import-mediacore.ts 26/server/tools/import-mediacore.ts
27/docker-volume/ 27/docker-volume/
28/.zanata-cache
diff --git a/client/package.json b/client/package.json
index 61f94758a..b79a090b3 100644
--- a/client/package.json
+++ b/client/package.json
@@ -19,7 +19,8 @@
19 "ng": "ng", 19 "ng": "ng",
20 "postinstall": "npm rebuild node-sass && test -f angular-cli-patch.js && node angular-cli-patch.js || true", 20 "postinstall": "npm rebuild node-sass && test -f angular-cli-patch.js && node angular-cli-patch.js || true",
21 "webpack-bundle-analyzer": "webpack-bundle-analyzer", 21 "webpack-bundle-analyzer": "webpack-bundle-analyzer",
22 "webdriver-manager": "webdriver-manager" 22 "webdriver-manager": "webdriver-manager",
23 "ngx-extractor": "ngx-extractor"
23 }, 24 },
24 "license": "GPLv3", 25 "license": "GPLv3",
25 "resolutions": { 26 "resolutions": {
@@ -47,6 +48,7 @@
47 "@ngx-loading-bar/http-client": "^2.0.0", 48 "@ngx-loading-bar/http-client": "^2.0.0",
48 "@ngx-loading-bar/router": "^2.0.0", 49 "@ngx-loading-bar/router": "^2.0.0",
49 "@ngx-meta/core": "^6.0.0-rc.1", 50 "@ngx-meta/core": "^6.0.0-rc.1",
51 "@ngx-translate/i18n-polyfill": "^1.0.0",
50 "@types/core-js": "^0.9.28", 52 "@types/core-js": "^0.9.28",
51 "@types/jasmine": "^2.8.7", 53 "@types/jasmine": "^2.8.7",
52 "@types/jasminewd2": "^2.0.3", 54 "@types/jasminewd2": "^2.0.3",
diff --git a/client/src/app/app.component.ts b/client/src/app/app.component.ts
index 0bd127063..6087dbf80 100644
--- a/client/src/app/app.component.ts
+++ b/client/src/app/app.component.ts
@@ -1,8 +1,9 @@
1import { Component, OnInit } from '@angular/core' 1import { Component, OnInit } from '@angular/core'
2import { DomSanitizer, SafeHtml } from '@angular/platform-browser' 2import { DomSanitizer, SafeHtml } from '@angular/platform-browser'
3import { GuardsCheckStart, Router, NavigationEnd } from '@angular/router' 3import { GuardsCheckStart, NavigationEnd, Router } from '@angular/router'
4import { AuthService, RedirectService, ServerService } from '@app/core' 4import { AuthService, RedirectService, ServerService } from '@app/core'
5import { isInSmallView } from '@app/shared/misc/utils' 5import { isInSmallView } from '@app/shared/misc/utils'
6import { is18nPath } from '../../../shared/models/i18n'
6 7
7@Component({ 8@Component({
8 selector: 'my-app', 9 selector: 'my-app',
@@ -33,7 +34,7 @@ export class AppComponent implements OnInit {
33 private serverService: ServerService, 34 private serverService: ServerService,
34 private domSanitizer: DomSanitizer, 35 private domSanitizer: DomSanitizer,
35 private redirectService: RedirectService 36 private redirectService: RedirectService
36 ) {} 37 ) { }
37 38
38 get serverVersion () { 39 get serverVersion () {
39 return this.serverService.getConfig().serverVersion 40 return this.serverService.getConfig().serverVersion
@@ -53,7 +54,7 @@ export class AppComponent implements OnInit {
53 this.router.events.subscribe(e => { 54 this.router.events.subscribe(e => {
54 if (e instanceof NavigationEnd) { 55 if (e instanceof NavigationEnd) {
55 const pathname = window.location.pathname 56 const pathname = window.location.pathname
56 if (!pathname || pathname === '/') { 57 if (!pathname || pathname === '/' || is18nPath(pathname)) {
57 this.redirectService.redirectToHomepage() 58 this.redirectService.redirectToHomepage()
58 } 59 }
59 } 60 }
diff --git a/client/src/app/app.module.ts b/client/src/app/app.module.ts
index cf533629f..44552021f 100644
--- a/client/src/app/app.module.ts
+++ b/client/src/app/app.module.ts
@@ -1,4 +1,4 @@
1import { NgModule } from '@angular/core' 1import { LOCALE_ID, NgModule, TRANSLATIONS, TRANSLATIONS_FORMAT } from '@angular/core'
2import { BrowserModule } from '@angular/platform-browser' 2import { BrowserModule } from '@angular/platform-browser'
3import { AboutModule } from '@app/about' 3import { AboutModule } from '@app/about'
4import { ServerService } from '@app/core' 4import { ServerService } from '@app/core'
@@ -16,6 +16,7 @@ import { MenuComponent } from './menu'
16import { SharedModule } from './shared' 16import { SharedModule } from './shared'
17import { SignupModule } from './signup' 17import { SignupModule } from './signup'
18import { VideosModule } from './videos' 18import { VideosModule } from './videos'
19import { buildFileLocale, getDefaultLocale } from '../../../shared/models/i18n'
19 20
20export function metaFactory (serverService: ServerService): MetaLoader { 21export function metaFactory (serverService: ServerService): MetaLoader {
21 return new MetaStaticLoader({ 22 return new MetaStaticLoader({
@@ -61,6 +62,21 @@ export function metaFactory (serverService: ServerService): MetaLoader {
61 62
62 AppRoutingModule // Put it after all the module because it has the 404 route 63 AppRoutingModule // Put it after all the module because it has the 404 route
63 ], 64 ],
64 providers: [ ] 65 providers: [
66 {
67 provide: TRANSLATIONS,
68 useFactory: (locale) => {
69 const fileLocale = buildFileLocale(locale)
70
71 // Default locale, nothing to translate
72 const defaultFileLocale = buildFileLocale(getDefaultLocale())
73 if (fileLocale === defaultFileLocale) return ''
74
75 return require(`raw-loader!../locale/target/messages_${fileLocale}.xml`)
76 },
77 deps: [ LOCALE_ID ]
78 },
79 { provide: TRANSLATIONS_FORMAT, useValue: 'xlf' }
80 ]
65}) 81})
66export class AppModule {} 82export class AppModule {}
diff --git a/client/src/app/core/routing/redirect.service.ts b/client/src/app/core/routing/redirect.service.ts
index 844f184b4..b7803cce2 100644
--- a/client/src/app/core/routing/redirect.service.ts
+++ b/client/src/app/core/routing/redirect.service.ts
@@ -31,7 +31,7 @@ export class RedirectService {
31 redirectToHomepage () { 31 redirectToHomepage () {
32 console.log('Redirecting to %s...', RedirectService.DEFAULT_ROUTE) 32 console.log('Redirecting to %s...', RedirectService.DEFAULT_ROUTE)
33 33
34 this.router.navigate([ RedirectService.DEFAULT_ROUTE ], { replaceUrl: true }) 34 this.router.navigate([ RedirectService.DEFAULT_ROUTE ], { skipLocationChange: true })
35 .catch(() => { 35 .catch(() => {
36 console.error( 36 console.error(
37 'Cannot navigate to %s, resetting default route to %s.', 37 'Cannot navigate to %s, resetting default route to %s.',
@@ -40,7 +40,7 @@ export class RedirectService {
40 ) 40 )
41 41
42 RedirectService.DEFAULT_ROUTE = RedirectService.INIT_DEFAULT_ROUTE 42 RedirectService.DEFAULT_ROUTE = RedirectService.INIT_DEFAULT_ROUTE
43 return this.router.navigate([ RedirectService.DEFAULT_ROUTE ], { replaceUrl: true }) 43 return this.router.navigate([ RedirectService.DEFAULT_ROUTE ], { skipLocationChange: true })
44 }) 44 })
45 45
46 } 46 }
diff --git a/client/src/app/shared/misc/utils.ts b/client/src/app/shared/misc/utils.ts
index 11933e90b..2219ac802 100644
--- a/client/src/app/shared/misc/utils.ts
+++ b/client/src/app/shared/misc/utils.ts
@@ -98,7 +98,7 @@ function lineFeedToHtml (obj: object, keyToNormalize: string) {
98 98
99// Try to cache a little bit window.innerWidth 99// Try to cache a little bit window.innerWidth
100let windowInnerWidth = window.innerWidth 100let windowInnerWidth = window.innerWidth
101// setInterval(() => windowInnerWidth = window.innerWidth, 500) 101setInterval(() => windowInnerWidth = window.innerWidth, 500)
102 102
103function isInSmallView () { 103function isInSmallView () {
104 return windowInnerWidth < 600 104 return windowInnerWidth < 600
diff --git a/client/src/app/shared/shared.module.ts b/client/src/app/shared/shared.module.ts
index 20019e47a..fba099401 100644
--- a/client/src/app/shared/shared.module.ts
+++ b/client/src/app/shared/shared.module.ts
@@ -33,6 +33,7 @@ import { VideoThumbnailComponent } from './video/video-thumbnail.component'
33import { VideoService } from './video/video.service' 33import { VideoService } from './video/video.service'
34import { AccountService } from '@app/shared/account/account.service' 34import { AccountService } from '@app/shared/account/account.service'
35import { VideoChannelService } from '@app/shared/video-channel/video-channel.service' 35import { VideoChannelService } from '@app/shared/video-channel/video-channel.service'
36import { I18n } from '@ngx-translate/i18n-polyfill'
36 37
37@NgModule({ 38@NgModule({
38 imports: [ 39 imports: [
@@ -108,7 +109,8 @@ import { VideoChannelService } from '@app/shared/video-channel/video-channel.ser
108 VideoService, 109 VideoService,
109 AccountService, 110 AccountService,
110 MarkdownService, 111 MarkdownService,
111 VideoChannelService 112 VideoChannelService,
113 I18n
112 ] 114 ]
113}) 115})
114export class SharedModule { } 116export class SharedModule { }
diff --git a/client/src/app/videos/+video-watch/video-watch.component.html b/client/src/app/videos/+video-watch/video-watch.component.html
index 583a97562..202a12fb0 100644
--- a/client/src/app/videos/+video-watch/video-watch.component.html
+++ b/client/src/app/videos/+video-watch/video-watch.component.html
@@ -3,7 +3,7 @@
3 <div [hidden]="videoNotFound" id="video-element-wrapper"> 3 <div [hidden]="videoNotFound" id="video-element-wrapper">
4 </div> 4 </div>
5 5
6 <div *ngIf="videoNotFound" id="video-not-found">Video not found :'(</div> 6 <div i18n *ngIf="videoNotFound" id="video-not-found">Video not found :'(</div>
7 7
8 <!-- Video information --> 8 <!-- Video information -->
9 <div *ngIf="video" class="margin-content video-bottom"> 9 <div *ngIf="video" class="margin-content video-bottom">
@@ -12,21 +12,21 @@
12 <div> 12 <div>
13 <div class="video-info-name">{{ video.name }}</div> 13 <div class="video-info-name">{{ video.name }}</div>
14 14
15 <div class="video-info-date-views"> 15 <div i18n class="video-info-date-views">
16 {{ video.publishedAt | myFromNow }} - {{ video.views | myNumberFormatter }} views 16 {{ video.publishedAt | myFromNow }} - {{ video.views | myNumberFormatter }} views
17 </div> 17 </div>
18 18
19 <div class="video-info-channel"> 19 <div class="video-info-channel">
20 <a [routerLink]="[ '/video-channels', video.channel.id ]" title="Go the channel page"> 20 <a [routerLink]="[ '/video-channels', video.channel.id ]" i18n-title title="Go the channel page">
21 {{ video.channel.displayName }} 21 {{ video.channel.displayName }}
22 </a> 22 </a>
23 <!-- Here will be the subscribe button --> 23 <!-- Here will be the subscribe button -->
24 <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> 24 <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>
25 </div> 25 </div>
26 26
27 <div class="video-info-by"> 27 <div class="video-info-by">
28 <a [routerLink]="[ '/accounts', video.by ]" title="Go the account page"> 28 <a [routerLink]="[ '/accounts', video.by ]" i18n-title title="Go the account page">
29 <span>By {{ video.by }}</span> 29 <span i18n>By {{ video.by }}</span>
30 <img [src]="video.accountAvatarUrl" alt="Account avatar" /> 30 <img [src]="video.accountAvatarUrl" alt="Account avatar" />
31 </a> 31 </a>
32 </div> 32 </div>
@@ -38,24 +38,24 @@
38 *ngIf="isUserLoggedIn()" [ngClass]="{ 'activated': userRating === 'like' }" (click)="setLike()" 38 *ngIf="isUserLoggedIn()" [ngClass]="{ 'activated': userRating === 'like' }" (click)="setLike()"
39 class="action-button action-button-like" 39 class="action-button action-button-like"
40 > 40 >
41 <span class="icon icon-like" title="Like this video" ></span> 41 <span class="icon icon-like" i18n-title title="Like this video" ></span>
42 </div> 42 </div>
43 43
44 <div 44 <div
45 *ngIf="isUserLoggedIn()" [ngClass]="{ 'activated': userRating === 'dislike' }" (click)="setDislike()" 45 *ngIf="isUserLoggedIn()" [ngClass]="{ 'activated': userRating === 'dislike' }" (click)="setDislike()"
46 class="action-button action-button-dislike" 46 class="action-button action-button-dislike"
47 > 47 >
48 <span class="icon icon-dislike" title="Dislike this video"></span> 48 <span class="icon icon-dislike" i18n-title title="Dislike this video"></span>
49 </div> 49 </div>
50 50
51 <div *ngIf="video.support" (click)="showSupportModal()" class="action-button action-button-support"> 51 <div *ngIf="video.support" (click)="showSupportModal()" class="action-button action-button-support">
52 <span class="icon icon-support"></span> 52 <span class="icon icon-support"></span>
53 <span class="icon-text">Support</span> 53 <span class="icon-text" i18n>Support</span>
54 </div> 54 </div>
55 55
56 <div (click)="showShareModal()" class="action-button action-button-share"> 56 <div (click)="showShareModal()" class="action-button action-button-share">
57 <span class="icon icon-share"></span> 57 <span class="icon icon-share"></span>
58 <span class="icon-text">Share</span> 58 <span class="icon-text" i18n>Share</span>
59 </div> 59 </div>
60 60
61 <div class="action-more" dropdown dropup="true" placement="right"> 61 <div class="action-more" dropdown dropup="true" placement="right">
@@ -65,32 +65,32 @@
65 65
66 <ul *dropdownMenu class="dropdown-menu" id="more-menu" role="menu" aria-labelledby="single-button"> 66 <ul *dropdownMenu class="dropdown-menu" id="more-menu" role="menu" aria-labelledby="single-button">
67 <li role="menuitem"> 67 <li role="menuitem">
68 <a class="dropdown-item" title="Download the video" href="#" (click)="showDownloadModal($event)"> 68 <a class="dropdown-item" i18n-title title="Download the video" href="#" (click)="showDownloadModal($event)">
69 <span class="icon icon-download"></span> Download 69 <span class="icon icon-download"></span> <ng-container i18n>Download</ng-container>
70 </a> 70 </a>
71 </li> 71 </li>
72 72
73 <li *ngIf="isUserLoggedIn()" role="menuitem"> 73 <li *ngIf="isUserLoggedIn()" role="menuitem">
74 <a class="dropdown-item" title="Report this video" href="#" (click)="showReportModal($event)"> 74 <a class="dropdown-item" i18n-title title="Report this video" href="#" (click)="showReportModal($event)">
75 <span class="icon icon-alert"></span> Report 75 <span class="icon icon-alert"></span> <ng-container i18n>Report</ng-container>
76 </a> 76 </a>
77 </li> 77 </li>
78 78
79 <li *ngIf="isVideoBlacklistable()" role="menuitem"> 79 <li *ngIf="isVideoBlacklistable()" role="menuitem">
80 <a class="dropdown-item" title="Blacklist this video" href="#" (click)="blacklistVideo($event)"> 80 <a class="dropdown-item" i18n-title title="Blacklist this video" href="#" (click)="blacklistVideo($event)">
81 <span class="icon icon-blacklist"></span> Blacklist 81 <span class="icon icon-blacklist"></span> <ng-container i18n>Blacklist</ng-container>
82 </a> 82 </a>
83 </li> 83 </li>
84 84
85 <li *ngIf="isVideoUpdatable()" role="menuitem"> 85 <li *ngIf="isVideoUpdatable()" role="menuitem">
86 <a class="dropdown-item" title="Update this video" href="#" [routerLink]="[ '/videos/update', video.uuid ]"> 86 <a class="dropdown-item" i18n-title title="Update this video" href="#" [routerLink]="[ '/videos/update', video.uuid ]">
87 <span class="icon icon-edit"></span> Update 87 <span class="icon icon-edit"></span> <ng-container i18n>Update</ng-container>
88 </a> 88 </a>
89 </li> 89 </li>
90 90
91 <li *ngIf="isVideoRemovable()" role="menuitem"> 91 <li *ngIf="isVideoRemovable()" role="menuitem">
92 <a class="dropdown-item" title="Delete this video" href="#" (click)="removeVideo($event)"> 92 <a class="dropdown-item" i18n-title title="Delete this video" href="#" (click)="removeVideo($event)">
93 <span class="icon icon-blacklist"></span> Delete 93 <span class="icon icon-blacklist"></span> <ng-container i18n>Delete</ng-container>
94 </a> 94 </a>
95 </li> 95 </li>
96 </ul> 96 </ul>
@@ -109,20 +109,20 @@
109 <div class="video-info-description-html" [innerHTML]="videoHTMLDescription"></div> 109 <div class="video-info-description-html" [innerHTML]="videoHTMLDescription"></div>
110 110
111 <div class="video-info-description-more" *ngIf="completeDescriptionShown === false && video.description?.length >= 250" (click)="showMoreDescription()"> 111 <div class="video-info-description-more" *ngIf="completeDescriptionShown === false && video.description?.length >= 250" (click)="showMoreDescription()">
112 Show more 112 <ng-container i18n>Show more</ng-container>
113 <span *ngIf="descriptionLoading === false" class="glyphicon glyphicon-menu-down"></span> 113 <span *ngIf="descriptionLoading === false" class="glyphicon glyphicon-menu-down"></span>
114 <my-loader class="description-loading" [loading]="descriptionLoading"></my-loader> 114 <my-loader class="description-loading" [loading]="descriptionLoading"></my-loader>
115 </div> 115 </div>
116 116
117 <div *ngIf="completeDescriptionShown === true" (click)="showLessDescription()" class="video-info-description-more"> 117 <div *ngIf="completeDescriptionShown === true" (click)="showLessDescription()" class="video-info-description-more">
118 Show less 118 <ng-container i18n>Show less</ng-container>
119 <span *ngIf="descriptionLoading === false" class="glyphicon glyphicon-menu-up"></span> 119 <span *ngIf="descriptionLoading === false" class="glyphicon glyphicon-menu-up"></span>
120 </div> 120 </div>
121 </div> 121 </div>
122 122
123 <div class="video-attributes"> 123 <div class="video-attributes">
124 <div class="video-attribute"> 124 <div class="video-attribute">
125 <span class="video-attribute-label"> 125 <span i18n class="video-attribute-label">
126 Privacy 126 Privacy
127 </span> 127 </span>
128 <span class="video-attribute-value"> 128 <span class="video-attribute-value">
@@ -131,7 +131,7 @@
131 </div> 131 </div>
132 132
133 <div class="video-attribute"> 133 <div class="video-attribute">
134 <span class="video-attribute-label"> 134 <span i18n class="video-attribute-label">
135 Category 135 Category
136 </span> 136 </span>
137 <span class="video-attribute-value"> 137 <span class="video-attribute-value">
@@ -140,7 +140,7 @@
140 </div> 140 </div>
141 141
142 <div class="video-attribute"> 142 <div class="video-attribute">
143 <span class="video-attribute-label"> 143 <span i18n class="video-attribute-label">
144 Licence 144 Licence
145 </span> 145 </span>
146 <span class="video-attribute-value"> 146 <span class="video-attribute-value">
@@ -149,7 +149,7 @@
149 </div> 149 </div>
150 150
151 <div class="video-attribute"> 151 <div class="video-attribute">
152 <span class="video-attribute-label"> 152 <span i18n class="video-attribute-label">
153 Language 153 Language
154 </span> 154 </span>
155 <span class="video-attribute-value"> 155 <span class="video-attribute-value">
@@ -158,7 +158,7 @@
158 </div> 158 </div>
159 159
160 <div class="video-attribute"> 160 <div class="video-attribute">
161 <span class="video-attribute-label"> 161 <span i18n class="video-attribute-label">
162 Tags 162 Tags
163 </span> 163 </span>
164 164
@@ -172,7 +172,7 @@
172 </div> 172 </div>
173 173
174 <div class="other-videos"> 174 <div class="other-videos">
175 <div class="title-page title-page-single"> 175 <div i18n class="title-page title-page-single">
176 Other videos 176 Other videos
177 </div> 177 </div>
178 178
@@ -184,13 +184,15 @@
184 184
185 185
186 <div class="privacy-concerns" *ngIf="hasAlreadyAcceptedPrivacyConcern === false"> 186 <div class="privacy-concerns" *ngIf="hasAlreadyAcceptedPrivacyConcern === false">
187 <strong>Friendly Reminder:</strong> 187 <strong i18n>Friendly Reminder:</strong>
188 <div class="privacy-concerns-text"> 188 <div class="privacy-concerns-text">
189 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. 189 <ng-container i18n>
190 <a title="Get more information" target="_blank" rel="noopener noreferrer" href="/about#p2p-privacy">More information</a> 190 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.
191 </ng-container>
192 <a i18n i18n-title title="Get more information" target="_blank" rel="noopener noreferrer" href="/about#p2p-privacy">More information</a>
191 </div> 193 </div>
192 194
193 <div class="privacy-concerns-okay" (click)="acceptedPrivacyConcern()"> 195 <div i18n class="privacy-concerns-okay" (click)="acceptedPrivacyConcern()">
194 OK 196 OK
195 </div> 197 </div>
196 </div> 198 </div>
diff --git a/client/src/app/videos/+video-watch/video-watch.component.ts b/client/src/app/videos/+video-watch/video-watch.component.ts
index ad572ef58..f3b4f7a2b 100644
--- a/client/src/app/videos/+video-watch/video-watch.component.ts
+++ b/client/src/app/videos/+video-watch/video-watch.component.ts
@@ -23,6 +23,7 @@ import { VideoReportComponent } from './modal/video-report.component'
23import { VideoShareComponent } from './modal/video-share.component' 23import { VideoShareComponent } from './modal/video-share.component'
24import { getVideojsOptions } from '../../../assets/player/peertube-player' 24import { getVideojsOptions } from '../../../assets/player/peertube-player'
25import { ServerService } from '@app/core' 25import { ServerService } from '@app/core'
26import { I18n } from '@ngx-translate/i18n-polyfill'
26 27
27@Component({ 28@Component({
28 selector: 'my-video-watch', 29 selector: 'my-video-watch',
@@ -70,7 +71,8 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
70 private notificationsService: NotificationsService, 71 private notificationsService: NotificationsService,
71 private markdownService: MarkdownService, 72 private markdownService: MarkdownService,
72 private zone: NgZone, 73 private zone: NgZone,
73 private redirectService: RedirectService 74 private redirectService: RedirectService,
75 private i18n: I18n
74 ) {} 76 ) {}
75 77
76 get user () { 78 get user () {
@@ -153,17 +155,20 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
153 async blacklistVideo (event: Event) { 155 async blacklistVideo (event: Event) {
154 event.preventDefault() 156 event.preventDefault()
155 157
156 const res = await this.confirmService.confirm('Do you really want to blacklist this video?', 'Blacklist') 158 const res = await this.confirmService.confirm(this.i18n('Do you really want to blacklist this video?'), this.i18n('Blacklist'))
157 if (res === false) return 159 if (res === false) return
158 160
159 this.videoBlacklistService.blacklistVideo(this.video.id) 161 this.videoBlacklistService.blacklistVideo(this.video.id)
160 .subscribe( 162 .subscribe(
161 status => { 163 status => {
162 this.notificationsService.success('Success', `Video ${this.video.name} had been blacklisted.`) 164 this.notificationsService.success(
165 this.i18n('Success'),
166 this.i18n('Video {{ videoName }} had been blacklisted.', { videoName: this.video.name })
167 )
163 this.redirectService.redirectToHomepage() 168 this.redirectService.redirectToHomepage()
164 }, 169 },
165 170
166 error => this.notificationsService.error('Error', error.message) 171 error => this.notificationsService.error(this.i18n('Error'), error.message)
167 ) 172 )
168 } 173 }
169 174
@@ -198,7 +203,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
198 203
199 error => { 204 error => {
200 this.descriptionLoading = false 205 this.descriptionLoading = false
201 this.notificationsService.error('Error', error.message) 206 this.notificationsService.error(this.i18n('Error'), error.message)
202 } 207 }
203 ) 208 )
204 } 209 }
@@ -252,19 +257,22 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
252 async removeVideo (event: Event) { 257 async removeVideo (event: Event) {
253 event.preventDefault() 258 event.preventDefault()
254 259
255 const res = await this.confirmService.confirm('Do you really want to delete this video?', 'Delete') 260 const res = await this.confirmService.confirm(this.i18n('Do you really want to delete this video?'), this.i18n('Delete'))
256 if (res === false) return 261 if (res === false) return
257 262
258 this.videoService.removeVideo(this.video.id) 263 this.videoService.removeVideo(this.video.id)
259 .subscribe( 264 .subscribe(
260 status => { 265 status => {
261 this.notificationsService.success('Success', `Video ${this.video.name} deleted.`) 266 this.notificationsService.success(
267 this.i18n('Success'),
268 this.i18n('Video {{ videoName }} deleted.', { videoName: this.video.name })
269 )
262 270
263 // Go back to the video-list. 271 // Go back to the video-list.
264 this.redirectService.redirectToHomepage() 272 this.redirectService.redirectToHomepage()
265 }, 273 },
266 274
267 error => this.notificationsService.error('Error', error.message) 275 error => this.notificationsService.error(this.i18n('Error'), error.message)
268 ) 276 )
269 } 277 }
270 278
@@ -288,7 +296,10 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
288 } 296 }
289 297
290 private setVideoLikesBarTooltipText () { 298 private setVideoLikesBarTooltipText () {
291 this.likesBarTooltipText = `${this.video.likes} likes / ${this.video.dislikes} dislikes` 299 this.likesBarTooltipText = this.i18n(
300 '{{ likesNumber }} likes / {{ dislikesNumber }} dislikes',
301 { likesNumber: this.video.likes, dislikes: this.video.dislikes }
302 )
292 } 303 }
293 304
294 private handleError (err: any) { 305 private handleError (err: any) {
@@ -298,12 +309,12 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
298 let message = '' 309 let message = ''
299 310
300 if (errorMessage.indexOf('http error') !== -1) { 311 if (errorMessage.indexOf('http error') !== -1) {
301 message = 'Cannot fetch video from server, maybe down.' 312 message = this.i18n('Cannot fetch video from server, maybe down.')
302 } else { 313 } else {
303 message = errorMessage 314 message = errorMessage
304 } 315 }
305 316
306 this.notificationsService.error('Error', message) 317 this.notificationsService.error(this.i18n('Error'), message)
307 } 318 }
308 319
309 private checkUserRating () { 320 private checkUserRating () {
@@ -318,7 +329,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
318 } 329 }
319 }, 330 },
320 331
321 err => this.notificationsService.error('Error', err.message) 332 err => this.notificationsService.error(this.i18n('Error'), err.message)
322 ) 333 )
323 } 334 }
324 335
@@ -333,8 +344,8 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
333 344
334 if (this.video.isVideoNSFWForUser(this.user, this.serverService.getConfig())) { 345 if (this.video.isVideoNSFWForUser(this.user, this.serverService.getConfig())) {
335 const res = await this.confirmService.confirm( 346 const res = await this.confirmService.confirm(
336 'This video contains mature or explicit content. Are you sure you want to watch it?', 347 this.i18n('This video contains mature or explicit content. Are you sure you want to watch it?'),
337 'Mature or explicit content' 348 this.i18n('Mature or explicit content')
338 ) 349 )
339 if (res === false) return this.redirectService.redirectToHomepage() 350 if (res === false) return this.redirectService.redirectToHomepage()
340 } 351 }
@@ -399,7 +410,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
399 this.updateVideoRating(this.userRating, nextRating) 410 this.updateVideoRating(this.userRating, nextRating)
400 this.userRating = nextRating 411 this.userRating = nextRating
401 }, 412 },
402 err => this.notificationsService.error('Error', err.message) 413 err => this.notificationsService.error(this.i18n('Error'), err.message)
403 ) 414 )
404 } 415 }
405 416
diff --git a/client/src/app/videos/video-list/video-local.component.ts b/client/src/app/videos/video-list/video-local.component.ts
index abab7504f..03568b618 100644
--- a/client/src/app/videos/video-list/video-local.component.ts
+++ b/client/src/app/videos/video-list/video-local.component.ts
@@ -8,6 +8,7 @@ import { AbstractVideoList } from '../../shared/video/abstract-video-list'
8import { VideoSortField } from '../../shared/video/sort-field.type' 8import { VideoSortField } from '../../shared/video/sort-field.type'
9import { VideoService } from '../../shared/video/video.service' 9import { VideoService } from '../../shared/video/video.service'
10import { VideoFilter } from '../../../../../shared/models/videos/video-query.type' 10import { VideoFilter } from '../../../../../shared/models/videos/video-query.type'
11import { I18n } from '@ngx-translate/i18n-polyfill'
11 12
12@Component({ 13@Component({
13 selector: 'my-videos-local', 14 selector: 'my-videos-local',
@@ -15,18 +16,23 @@ import { VideoFilter } from '../../../../../shared/models/videos/video-query.typ
15 templateUrl: '../../shared/video/abstract-video-list.html' 16 templateUrl: '../../shared/video/abstract-video-list.html'
16}) 17})
17export class VideoLocalComponent extends AbstractVideoList implements OnInit, OnDestroy { 18export class VideoLocalComponent extends AbstractVideoList implements OnInit, OnDestroy {
18 titlePage = 'Local videos' 19 titlePage: string
19 currentRoute = '/videos/local' 20 currentRoute = '/videos/local'
20 sort = '-publishedAt' as VideoSortField 21 sort = '-publishedAt' as VideoSortField
21 filter: VideoFilter = 'local' 22 filter: VideoFilter = 'local'
22 23
23 constructor (protected router: Router, 24 constructor (
24 protected route: ActivatedRoute, 25 protected router: Router,
25 protected notificationsService: NotificationsService, 26 protected route: ActivatedRoute,
26 protected authService: AuthService, 27 protected notificationsService: NotificationsService,
27 protected location: Location, 28 protected authService: AuthService,
28 private videoService: VideoService) { 29 protected location: Location,
30 private videoService: VideoService,
31 private i18n: I18n
32 ) {
29 super() 33 super()
34
35 this.titlePage = i18n('Local videos')
30 } 36 }
31 37
32 ngOnInit () { 38 ngOnInit () {
diff --git a/client/src/app/videos/video-list/video-recently-added.component.ts b/client/src/app/videos/video-list/video-recently-added.component.ts
index d064d9628..5768d9fe0 100644
--- a/client/src/app/videos/video-list/video-recently-added.component.ts
+++ b/client/src/app/videos/video-list/video-recently-added.component.ts
@@ -7,6 +7,7 @@ import { AuthService } from '../../core/auth'
7import { AbstractVideoList } from '../../shared/video/abstract-video-list' 7import { AbstractVideoList } from '../../shared/video/abstract-video-list'
8import { VideoSortField } from '../../shared/video/sort-field.type' 8import { VideoSortField } from '../../shared/video/sort-field.type'
9import { VideoService } from '../../shared/video/video.service' 9import { VideoService } from '../../shared/video/video.service'
10import { I18n } from '@ngx-translate/i18n-polyfill'
10 11
11@Component({ 12@Component({
12 selector: 'my-videos-recently-added', 13 selector: 'my-videos-recently-added',
@@ -14,17 +15,22 @@ import { VideoService } from '../../shared/video/video.service'
14 templateUrl: '../../shared/video/abstract-video-list.html' 15 templateUrl: '../../shared/video/abstract-video-list.html'
15}) 16})
16export class VideoRecentlyAddedComponent extends AbstractVideoList implements OnInit, OnDestroy { 17export class VideoRecentlyAddedComponent extends AbstractVideoList implements OnInit, OnDestroy {
17 titlePage = 'Recently added' 18 titlePage: string
18 currentRoute = '/videos/recently-added' 19 currentRoute = '/videos/recently-added'
19 sort: VideoSortField = '-publishedAt' 20 sort: VideoSortField = '-publishedAt'
20 21
21 constructor (protected router: Router, 22 constructor (
22 protected route: ActivatedRoute, 23 protected router: Router,
23 protected location: Location, 24 protected route: ActivatedRoute,
24 protected notificationsService: NotificationsService, 25 protected location: Location,
25 protected authService: AuthService, 26 protected notificationsService: NotificationsService,
26 private videoService: VideoService) { 27 protected authService: AuthService,
28 private videoService: VideoService,
29 private i18n: I18n
30 ) {
27 super() 31 super()
32
33 this.titlePage = i18n('Recently added')
28 } 34 }
29 35
30 ngOnInit () { 36 ngOnInit () {
diff --git a/client/src/app/videos/video-list/video-search.component.ts b/client/src/app/videos/video-list/video-search.component.ts
index aab896d84..35566a7bd 100644
--- a/client/src/app/videos/video-list/video-search.component.ts
+++ b/client/src/app/videos/video-list/video-search.component.ts
@@ -8,6 +8,7 @@ import { Subscription } from 'rxjs'
8import { AuthService } from '../../core/auth' 8import { AuthService } from '../../core/auth'
9import { AbstractVideoList } from '../../shared/video/abstract-video-list' 9import { AbstractVideoList } from '../../shared/video/abstract-video-list'
10import { VideoService } from '../../shared/video/video.service' 10import { VideoService } from '../../shared/video/video.service'
11import { I18n } from '@ngx-translate/i18n-polyfill'
11 12
12@Component({ 13@Component({
13 selector: 'my-videos-search', 14 selector: 'my-videos-search',
@@ -15,7 +16,7 @@ import { VideoService } from '../../shared/video/video.service'
15 templateUrl: '../../shared/video/abstract-video-list.html' 16 templateUrl: '../../shared/video/abstract-video-list.html'
16}) 17})
17export class VideoSearchComponent extends AbstractVideoList implements OnInit, OnDestroy { 18export class VideoSearchComponent extends AbstractVideoList implements OnInit, OnDestroy {
18 titlePage = 'Search' 19 titlePage: string
19 currentRoute = '/videos/search' 20 currentRoute = '/videos/search'
20 loadOnInit = false 21 loadOnInit = false
21 22
@@ -24,15 +25,19 @@ export class VideoSearchComponent extends AbstractVideoList implements OnInit, O
24 } 25 }
25 private subActivatedRoute: Subscription 26 private subActivatedRoute: Subscription
26 27
27 constructor (protected router: Router, 28 constructor (
28 protected route: ActivatedRoute, 29 protected router: Router,
29 protected notificationsService: NotificationsService, 30 protected route: ActivatedRoute,
30 protected authService: AuthService, 31 protected notificationsService: NotificationsService,
31 protected location: Location, 32 protected authService: AuthService,
32 private videoService: VideoService, 33 protected location: Location,
33 private redirectService: RedirectService 34 private videoService: VideoService,
35 private redirectService: RedirectService,
36 private i18n: I18n
34 ) { 37 ) {
35 super() 38 super()
39
40 this.titlePage = i18n('Search')
36 } 41 }
37 42
38 ngOnInit () { 43 ngOnInit () {
diff --git a/client/src/app/videos/video-list/video-trending.component.ts b/client/src/app/videos/video-list/video-trending.component.ts
index ea65070f9..760470e8c 100644
--- a/client/src/app/videos/video-list/video-trending.component.ts
+++ b/client/src/app/videos/video-list/video-trending.component.ts
@@ -7,6 +7,7 @@ import { AuthService } from '../../core/auth'
7import { AbstractVideoList } from '../../shared/video/abstract-video-list' 7import { AbstractVideoList } from '../../shared/video/abstract-video-list'
8import { VideoSortField } from '../../shared/video/sort-field.type' 8import { VideoSortField } from '../../shared/video/sort-field.type'
9import { VideoService } from '../../shared/video/video.service' 9import { VideoService } from '../../shared/video/video.service'
10import { I18n } from '@ngx-translate/i18n-polyfill'
10 11
11@Component({ 12@Component({
12 selector: 'my-videos-trending', 13 selector: 'my-videos-trending',
@@ -14,17 +15,22 @@ import { VideoService } from '../../shared/video/video.service'
14 templateUrl: '../../shared/video/abstract-video-list.html' 15 templateUrl: '../../shared/video/abstract-video-list.html'
15}) 16})
16export class VideoTrendingComponent extends AbstractVideoList implements OnInit, OnDestroy { 17export class VideoTrendingComponent extends AbstractVideoList implements OnInit, OnDestroy {
17 titlePage = 'Trending' 18 titlePage: string
18 currentRoute = '/videos/trending' 19 currentRoute = '/videos/trending'
19 defaultSort: VideoSortField = '-views' 20 defaultSort: VideoSortField = '-views'
20 21
21 constructor (protected router: Router, 22 constructor (
22 protected route: ActivatedRoute, 23 protected router: Router,
23 protected notificationsService: NotificationsService, 24 protected route: ActivatedRoute,
24 protected authService: AuthService, 25 protected notificationsService: NotificationsService,
25 protected location: Location, 26 protected authService: AuthService,
26 private videoService: VideoService) { 27 protected location: Location,
28 private videoService: VideoService,
29 private i18n: I18n
30 ) {
27 super() 31 super()
32
33 this.titlePage = i18n('Trending')
28 } 34 }
29 35
30 ngOnInit () { 36 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
index 000000000..6c355a97f
--- /dev/null
+++ b/client/src/locale/source/messages_en_US.xml
@@ -0,0 +1,354 @@
1<?xml version="1.0" encoding="UTF-8" ?>
2<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
3 <file source-language="en-US" datatype="plaintext" original="ng2.template">
4 <body>
5 <trans-unit id="298cb43759c99e11e2ca5f92c768a145ddaa323f" datatype="html">
6 <source>
7 My public profile
8 </source>
9 <context-group purpose="location">
10 <context context-type="sourcefile">app/menu/menu.component.ts</context>
11 <context context-type="linenumber">17</context>
12 </context-group>
13 </trans-unit><trans-unit id="5f60990802486b7906b422d80aace6a1b19dcc02" datatype="html">
14 <source>Video not found :&apos;(</source>
15 <context-group purpose="location">
16 <context context-type="sourcefile">app/videos/+video-watch/video-watch.component.ts</context>
17 <context context-type="linenumber">6</context>
18 </context-group>
19 </trans-unit><trans-unit id="643ab402461b1169eebbe2ed790e12a9a83551aa" datatype="html">
20 <source>
21 &lt;x id="INTERPOLATION" equiv-text="{{ video.publishedAt | myFromNow }}"/&gt; - &lt;x id="INTERPOLATION_1" equiv-text="{{ video.views | myNumberFormatter }}"/&gt; views
22 </source>
23 <context-group purpose="location">
24 <context context-type="sourcefile">app/videos/+video-watch/video-watch.component.ts</context>
25 <context context-type="linenumber">15</context>
26 </context-group>
27 </trans-unit><trans-unit id="5cb397241041f7ad70997806227bafcdf7eb1b33" datatype="html">
28 <source>Go the channel page</source>
29 <context-group purpose="location">
30 <context context-type="sourcefile">app/videos/+video-watch/video-watch.component.ts</context>
31 <context context-type="linenumber">20</context>
32 </context-group>
33 </trans-unit><trans-unit id="912f005563d20191efc188dccedd35a7c4e6b396" datatype="html">
34 <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>
35 <context-group purpose="location">
36 <context context-type="sourcefile">app/videos/+video-watch/video-watch.component.ts</context>
37 <context context-type="linenumber">24</context>
38 </context-group>
39 </trans-unit><trans-unit id="ccc07df383b7a32be3e2e105faa5488caf261c1c" datatype="html">
40 <source>By &lt;x id="INTERPOLATION" equiv-text="{{ video.by }}"/&gt;</source>
41 <context-group purpose="location">
42 <context context-type="sourcefile">app/videos/+video-watch/video-watch.component.ts</context>
43 <context context-type="linenumber">29</context>
44 </context-group>
45 </trans-unit><trans-unit id="e88300c71e0cb0f346d5a72eb37c920f2aadae8a" datatype="html">
46 <source>Go the account page</source>
47 <context-group purpose="location">
48 <context context-type="sourcefile">app/videos/+video-watch/video-watch.component.ts</context>
49 <context context-type="linenumber">28</context>
50 </context-group>
51 </trans-unit><trans-unit id="82b59049f3f89d900c98da9319e156dd513e3ced" datatype="html">
52 <source>Like this video</source>
53 <context-group purpose="location">
54 <context context-type="sourcefile">app/videos/+video-watch/video-watch.component.ts</context>
55 <context context-type="linenumber">41</context>
56 </context-group>
57 </trans-unit><trans-unit id="623698f075025b2b2fc2e0c59fd95f4f4662a509" datatype="html">
58 <source>Dislike this video</source>
59 <context-group purpose="location">
60 <context context-type="sourcefile">app/videos/+video-watch/video-watch.component.ts</context>
61 <context context-type="linenumber">48</context>
62 </context-group>
63 </trans-unit><trans-unit id="b5629d298ff1a69b8db19a4ba2995c76b52da604" datatype="html">
64 <source>Support</source>
65 <context-group purpose="location">
66 <context context-type="sourcefile">app/videos/+video-watch/video-watch.component.ts</context>
67 <context context-type="linenumber">53</context>
68 </context-group>
69 </trans-unit><trans-unit id="0bd8b27f60a1f098a53e06328426d818e3508ff9" datatype="html">
70 <source>Share</source>
71 <context-group purpose="location">
72 <context context-type="sourcefile">app/videos/+video-watch/video-watch.component.ts</context>
73 <context context-type="linenumber">58</context>
74 </context-group>
75 </trans-unit><trans-unit id="dc75033a5238fdc4f462212c847a45ba8018a3fd" datatype="html">
76 <source>Download</source>
77 <context-group purpose="location">
78 <context context-type="sourcefile">app/videos/+video-watch/video-watch.component.ts</context>
79 <context context-type="linenumber">69</context>
80 </context-group>
81 </trans-unit><trans-unit id="144fff5c40b85414d59e644d8dee7cfefba925a2" datatype="html">
82 <source>Download the video</source>
83 <context-group purpose="location">
84 <context context-type="sourcefile">app/videos/+video-watch/video-watch.component.ts</context>
85 <context context-type="linenumber">68</context>
86 </context-group>
87 </trans-unit><trans-unit id="f72992030f134408b675152c397f9d0ec00f3b2a" datatype="html">
88 <source>Report</source>
89 <context-group purpose="location">
90 <context context-type="sourcefile">app/videos/+video-watch/video-watch.component.ts</context>
91 <context context-type="linenumber">75</context>
92 </context-group>
93 </trans-unit><trans-unit id="2f4894617d9c44010f87473e583bd4604b7d6ecf" datatype="html">
94 <source>Report this video</source>
95 <context-group purpose="location">
96 <context context-type="sourcefile">app/videos/+video-watch/video-watch.component.ts</context>
97 <context context-type="linenumber">74</context>
98 </context-group>
99 </trans-unit><trans-unit id="007ab5fa2aae8a7372307d3fc45a2dbcb11ffd61" datatype="html">
100 <source>Blacklist</source>
101 <context-group purpose="location">
102 <context context-type="sourcefile">app/videos/+video-watch/video-watch.component.ts</context>
103 <context context-type="linenumber">81</context>
104 </context-group>
105 </trans-unit><trans-unit id="803c6317abd2dbafcc93226c4e273c62932e3037" datatype="html">
106 <source>Blacklist this video</source>
107 <context-group purpose="location">
108 <context context-type="sourcefile">app/videos/+video-watch/video-watch.component.ts</context>
109 <context context-type="linenumber">80</context>
110 </context-group>
111 </trans-unit><trans-unit id="047f50bc5b5d17b5bec0196355953e1a5c590ddb" datatype="html">
112 <source>Update</source>
113 <context-group purpose="location">
114 <context context-type="sourcefile">app/videos/+video-watch/video-watch.component.ts</context>
115 <context context-type="linenumber">87</context>
116 </context-group>
117 </trans-unit><trans-unit id="cd27f761b923a5bdb16ba9844da632edd878f1b1" datatype="html">
118 <source>Update this video</source>
119 <context-group purpose="location">
120 <context context-type="sourcefile">app/videos/+video-watch/video-watch.component.ts</context>
121 <context context-type="linenumber">86</context>
122 </context-group>
123 </trans-unit><trans-unit id="826b25211922a1b46436589233cb6f1a163d89b7" datatype="html">
124 <source>Delete</source>
125 <context-group purpose="location">
126 <context context-type="sourcefile">app/videos/+video-watch/video-watch.component.ts</context>
127 <context context-type="linenumber">93</context>
128 </context-group>
129 </trans-unit><trans-unit id="3dbfdc68f83d91cb360172eb65578cae94e7cbe5" datatype="html">
130 <source>Delete this video</source>
131 <context-group purpose="location">
132 <context context-type="sourcefile">app/videos/+video-watch/video-watch.component.ts</context>
133 <context context-type="linenumber">92</context>
134 </context-group>
135 </trans-unit><trans-unit id="f0c5f6f270e70cbe063b5368fcf48f9afc1abd9b" datatype="html">
136 <source>Show more</source>
137 <context-group purpose="location">
138 <context context-type="sourcefile">app/videos/+video-watch/video-watch.component.ts</context>
139 <context context-type="linenumber">112</context>
140 </context-group>
141 </trans-unit><trans-unit id="5403a767248e304199592271bba3366d2ca3f903" datatype="html">
142 <source>Show less</source>
143 <context-group purpose="location">
144 <context context-type="sourcefile">app/videos/+video-watch/video-watch.component.ts</context>
145 <context context-type="linenumber">118</context>
146 </context-group>
147 </trans-unit><trans-unit id="8057a9b7f9e908ff350edfd71417b96c174e5911" datatype="html">
148 <source>
149 Privacy
150 </source>
151 <context-group purpose="location">
152 <context context-type="sourcefile">app/videos/+video-watch/video-watch.component.ts</context>
153 <context context-type="linenumber">125</context>
154 </context-group>
155 </trans-unit><trans-unit id="bd407eca607a8905a26a9e30c9d0cd70f4465db8" datatype="html">
156 <source>
157 Category
158 </source>
159 <context-group purpose="location">
160 <context context-type="sourcefile">app/videos/+video-watch/video-watch.component.ts</context>
161 <context context-type="linenumber">134</context>
162 </context-group>
163 </trans-unit><trans-unit id="af5072bd79ea3cd767ab74a6622d2eee791b3832" datatype="html">
164 <source>
165 Licence
166 </source>
167 <context-group purpose="location">
168 <context context-type="sourcefile">app/videos/+video-watch/video-watch.component.ts</context>
169 <context context-type="linenumber">143</context>
170 </context-group>
171 </trans-unit><trans-unit id="a911eee019174741b0aec6fcf3fbd5752fab3e67" datatype="html">
172 <source>
173 Language
174 </source>
175 <context-group purpose="location">
176 <context context-type="sourcefile">app/videos/+video-watch/video-watch.component.ts</context>
177 <context context-type="linenumber">152</context>
178 </context-group>
179 </trans-unit><trans-unit id="ecf7007c2842cc26a7b91d08d48c7a4f5f749fb3" datatype="html">
180 <source>
181 Tags
182 </source>
183 <context-group purpose="location">
184 <context context-type="sourcefile">app/videos/+video-watch/video-watch.component.ts</context>
185 <context context-type="linenumber">161</context>
186 </context-group>
187 </trans-unit><trans-unit id="7ce8b0d7cc34d4c1ef4a21e990b0a001337bedd1" datatype="html">
188 <source>
189 Other videos
190 </source>
191 <context-group purpose="location">
192 <context context-type="sourcefile">app/videos/+video-watch/video-watch.component.ts</context>
193 <context context-type="linenumber">175</context>
194 </context-group>
195 </trans-unit><trans-unit id="fb779d2b25c4d0ffa7d52c823a240717e8c1fe6c" datatype="html">
196 <source>Friendly Reminder:</source>
197 <context-group purpose="location">
198 <context context-type="sourcefile">app/videos/+video-watch/video-watch.component.ts</context>
199 <context context-type="linenumber">187</context>
200 </context-group>
201 </trans-unit><trans-unit id="4c2fca29fd9d7e85abe85a206958a4226f403be2" datatype="html">
202 <source>
203 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.
204 </source>
205 <context-group purpose="location">
206 <context context-type="sourcefile">app/videos/+video-watch/video-watch.component.ts</context>
207 <context context-type="linenumber">189</context>
208 </context-group>
209 </trans-unit><trans-unit id="e60c11e1b1dfbbeda577364b8de39ded2d796c5e" datatype="html">
210 <source>More information</source>
211 <context-group purpose="location">
212 <context context-type="sourcefile">app/videos/+video-watch/video-watch.component.ts</context>
213 <context context-type="linenumber">192</context>
214 </context-group>
215 </trans-unit><trans-unit id="bd499ca7913bb5408fd139a4cb4f863852d5f318" datatype="html">
216 <source>Get more information</source>
217 <context-group purpose="location">
218 <context context-type="sourcefile">app/videos/+video-watch/video-watch.component.ts</context>
219 <context context-type="linenumber">192</context>
220 </context-group>
221 </trans-unit><trans-unit id="20fc98888baf65b5ba9fe9622dc036fa8dec6a5f" datatype="html">
222 <source>
223 OK
224 </source>
225 <context-group purpose="location">
226 <context context-type="sourcefile">app/videos/+video-watch/video-watch.component.ts</context>
227 <context context-type="linenumber">195</context>
228 </context-group>
229 </trans-unit>
230 <trans-unit id="23b2c2f4dd69e29c3bff00469e259dcb01de5633" datatype="html">
231 <source>Do you really want to blacklist this video?</source>
232 <context-group purpose="location">
233 <context context-type="sourcefile">src/app/videos/+video-watch/video-watch.component.ts</context>
234 <context context-type="linenumber">1</context>
235 </context-group>
236 </trans-unit>
237 <trans-unit id="1e035e6ccfab771cad4226b2ad230cb0d4a88cba" datatype="html">
238 <source>Success</source>
239 <context-group purpose="location">
240 <context context-type="sourcefile">src/app/videos/+video-watch/video-watch.component.ts</context>
241 <context context-type="linenumber">1</context>
242 </context-group>
243 <context-group purpose="location">
244 <context context-type="sourcefile">src/app/videos/+video-watch/video-watch.component.ts</context>
245 <context context-type="linenumber">1</context>
246 </context-group>
247 </trans-unit>
248 <trans-unit id="085d56464b75ae5c1e370f5290e4c4cf23961a61" datatype="html">
249 <source>Video &lt;x id="INTERPOLATION" equiv-text="{{ videoName }}"/&gt; had been blacklisted.</source>
250 <context-group purpose="location">
251 <context context-type="sourcefile">src/app/videos/+video-watch/video-watch.component.ts</context>
252 <context context-type="linenumber">1</context>
253 </context-group>
254 </trans-unit>
255 <trans-unit id="6080b77234e92ad41bb52653b239c4c4f851317d" datatype="html">
256 <source>Error</source>
257 <context-group purpose="location">
258 <context context-type="sourcefile">src/app/videos/+video-watch/video-watch.component.ts</context>
259 <context context-type="linenumber">1</context>
260 </context-group>
261 <context-group purpose="location">
262 <context context-type="sourcefile">src/app/videos/+video-watch/video-watch.component.ts</context>
263 <context context-type="linenumber">1</context>
264 </context-group>
265 <context-group purpose="location">
266 <context context-type="sourcefile">src/app/videos/+video-watch/video-watch.component.ts</context>
267 <context context-type="linenumber">1</context>
268 </context-group>
269 <context-group purpose="location">
270 <context context-type="sourcefile">src/app/videos/+video-watch/video-watch.component.ts</context>
271 <context context-type="linenumber">1</context>
272 </context-group>
273 <context-group purpose="location">
274 <context context-type="sourcefile">src/app/videos/+video-watch/video-watch.component.ts</context>
275 <context context-type="linenumber">1</context>
276 </context-group>
277 <context-group purpose="location">
278 <context context-type="sourcefile">src/app/videos/+video-watch/video-watch.component.ts</context>
279 <context context-type="linenumber">1</context>
280 </context-group>
281 </trans-unit>
282 <trans-unit id="f1abd89c9280323209e939fa9c30f6e5cda20c95" datatype="html">
283 <source>Do you really want to delete this video?</source>
284 <context-group purpose="location">
285 <context context-type="sourcefile">src/app/videos/+video-watch/video-watch.component.ts</context>
286 <context context-type="linenumber">1</context>
287 </context-group>
288 </trans-unit>
289 <trans-unit id="007c1d7080cf6da1ac264b23705246f0c53e3114" datatype="html">
290 <source>Video &lt;x id="INTERPOLATION" equiv-text="{{ videoName }}"/&gt; deleted.</source>
291 <context-group purpose="location">
292 <context context-type="sourcefile">src/app/videos/+video-watch/video-watch.component.ts</context>
293 <context context-type="linenumber">1</context>
294 </context-group>
295 </trans-unit>
296 <trans-unit id="cf9a064824f2fa3f01fd5544ad21032e33e60dca" datatype="html">
297 <source>&lt;x id="INTERPOLATION" equiv-text="{{ likesNumber }}"/&gt; likes / &lt;x id="INTERPOLATION_1" equiv-text="{{ dislikesNumber }}"/&gt; dislikes</source>
298 <context-group purpose="location">
299 <context context-type="sourcefile">src/app/videos/+video-watch/video-watch.component.ts</context>
300 <context context-type="linenumber">1</context>
301 </context-group>
302 </trans-unit>
303 <trans-unit id="4a400b174208188dcb46f2c23f4af9accfabaa3f" datatype="html">
304 <source>Cannot fetch video from server, maybe down.</source>
305 <context-group purpose="location">
306 <context context-type="sourcefile">src/app/videos/+video-watch/video-watch.component.ts</context>
307 <context context-type="linenumber">1</context>
308 </context-group>
309 </trans-unit>
310 <trans-unit id="ed013c2c29216501c688e9cb5f3a1c9fd9147b71" datatype="html">
311 <source>This video contains mature or explicit content. Are you sure you want to watch it?</source>
312 <context-group purpose="location">
313 <context context-type="sourcefile">src/app/videos/+video-watch/video-watch.component.ts</context>
314 <context context-type="linenumber">1</context>
315 </context-group>
316 </trans-unit>
317 <trans-unit id="5ba3d522e4146eefcbd5c222247c1e2423d27cd8" datatype="html">
318 <source>Mature or explicit content</source>
319 <context-group purpose="location">
320 <context context-type="sourcefile">src/app/videos/+video-watch/video-watch.component.ts</context>
321 <context context-type="linenumber">1</context>
322 </context-group>
323 </trans-unit>
324 <trans-unit id="b6307f83d9f43bff8d5129a7888e89964ddc3f7f" datatype="html">
325 <source>Local videos</source>
326 <context-group purpose="location">
327 <context context-type="sourcefile">src/app/videos/video-list/video-local.component.ts</context>
328 <context context-type="linenumber">1</context>
329 </context-group>
330 </trans-unit>
331 <trans-unit id="8d20c5f5dd30acbe71316544dab774393fd9c3c1" datatype="html">
332 <source>Recently added</source>
333 <context-group purpose="location">
334 <context context-type="sourcefile">src/app/videos/video-list/video-recently-added.component.ts</context>
335 <context context-type="linenumber">1</context>
336 </context-group>
337 </trans-unit>
338 <trans-unit id="7e892ba15f2c6c17e83510e273b3e10fc32ea016" datatype="html">
339 <source>Search</source>
340 <context-group purpose="location">
341 <context context-type="sourcefile">src/app/videos/video-list/video-search.component.ts</context>
342 <context context-type="linenumber">1</context>
343 </context-group>
344 </trans-unit>
345 <trans-unit id="b6b7986bc3721ac483baf20bc9a320529075c807" datatype="html">
346 <source>Trending</source>
347 <context-group purpose="location">
348 <context context-type="sourcefile">src/app/videos/video-list/video-trending.component.ts</context>
349 <context context-type="linenumber">1</context>
350 </context-group>
351 </trans-unit>
352 </body>
353 </file>
354</xliff>
diff --git a/client/src/locale/target/messages_fr.xml b/client/src/locale/target/messages_fr.xml
new file mode 100644
index 000000000..3a55922ba
--- /dev/null
+++ b/client/src/locale/target/messages_fr.xml
@@ -0,0 +1,191 @@
1<?xml version="1.0" encoding="utf-8"?>
2<!--XLIFF document generated by Zanata. Visit http://zanata.org for more infomation.-->
3<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">
4 <file source-language="en-US" datatype="plaintext" original="" target-language="fr">
5 <body>
6 <trans-unit id="298cb43759c99e11e2ca5f92c768a145ddaa323f">
7 <source>
8 My public profile
9 </source>
10 <target>Mon profile public</target>
11 <context-group name="null">
12 <context context-type="linenumber">17</context>
13 </context-group>
14 </trans-unit>
15 <trans-unit id="5f60990802486b7906b422d80aace6a1b19dcc02">
16 <source>Video not found :'(</source>
17 <target>Vidéo non trouvée :'(</target>
18 <context-group name="null">
19 <context context-type="linenumber">6</context>
20 </context-group>
21 </trans-unit>
22 <trans-unit id="643ab402461b1169eebbe2ed790e12a9a83551aa">
23 <source>
24 <x id="INTERPOLATION" equiv-text="{{ video.publishedAt | myFromNow }}"/> - <x id="INTERPOLATION_1" equiv-text="{{ video.views | myNumberFormatter }}"/> views
25 </source>
26 <target>
27 <x id="INTERPOLATION" equiv-text="{{ video.publishedAt | myFromNow }}"/> - <x id="INTERPOLATION_1" equiv-text="{{ video.views | myNumberFormatter }}"/> vues </target>
28 <context-group name="null">
29 <context context-type="linenumber">15</context>
30 </context-group>
31 </trans-unit>
32 <trans-unit id="912f005563d20191efc188dccedd35a7c4e6b396">
33 <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>
34 <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>
35 <context-group name="null">
36 <context context-type="linenumber">24</context>
37 </context-group>
38 </trans-unit>
39 <trans-unit id="ccc07df383b7a32be3e2e105faa5488caf261c1c">
40 <source>By <x id="INTERPOLATION" equiv-text="{{ video.by }}"/></source>
41 <target>Par <x id="INTERPOLATION" equiv-text="{{ video.by }}"/></target>
42 <context-group name="null">
43 <context context-type="linenumber">29</context>
44 </context-group>
45 </trans-unit>
46 <trans-unit id="e88300c71e0cb0f346d5a72eb37c920f2aadae8a">
47 <source>Go the account page</source>
48 <target>Aller sur la page du compte</target>
49 <context-group name="null">
50 <context context-type="linenumber">28</context>
51 </context-group>
52 </trans-unit>
53 <trans-unit id="82b59049f3f89d900c98da9319e156dd513e3ced">
54 <source>Like this video</source>
55 <target>J'aime cette vidéo</target>
56 <context-group name="null">
57 <context context-type="linenumber">41</context>
58 </context-group>
59 </trans-unit>
60 <trans-unit id="623698f075025b2b2fc2e0c59fd95f4f4662a509">
61 <source>Dislike this video</source>
62 <target>Je n'aime pas cette vidéo</target>
63 <context-group name="null">
64 <context context-type="linenumber">48</context>
65 </context-group>
66 </trans-unit>
67 <trans-unit id="b5629d298ff1a69b8db19a4ba2995c76b52da604">
68 <source>Support</source>
69 <target>Supporter</target>
70 <context-group name="null">
71 <context context-type="linenumber">53</context>
72 </context-group>
73 </trans-unit>
74 <trans-unit id="0bd8b27f60a1f098a53e06328426d818e3508ff9">
75 <source>Share</source>
76 <target>Partager</target>
77 <context-group name="null">
78 <context context-type="linenumber">58</context>
79 </context-group>
80 </trans-unit>
81 <trans-unit id="dc75033a5238fdc4f462212c847a45ba8018a3fd">
82 <source>Download</source>
83 <target>Télécharger</target>
84 <context-group name="null">
85 <context context-type="linenumber">69</context>
86 </context-group>
87 </trans-unit>
88 <trans-unit id="144fff5c40b85414d59e644d8dee7cfefba925a2">
89 <source>Download the video</source>
90 <target>Télécharger la vidéo</target>
91 <context-group name="null">
92 <context context-type="linenumber">68</context>
93 </context-group>
94 </trans-unit>
95 <trans-unit id="f72992030f134408b675152c397f9d0ec00f3b2a">
96 <source>Report</source>
97 <target>Signaler</target>
98 <context-group name="null">
99 <context context-type="linenumber">75</context>
100 </context-group>
101 </trans-unit>
102 <trans-unit id="2f4894617d9c44010f87473e583bd4604b7d6ecf">
103 <source>Report this video</source>
104 <target>Signaler cette vidéo</target>
105 <context-group name="null">
106 <context context-type="linenumber">74</context>
107 </context-group>
108 </trans-unit>
109 <trans-unit id="007ab5fa2aae8a7372307d3fc45a2dbcb11ffd61">
110 <source>Blacklist</source>
111 <target>Blacklister</target>
112 <context-group name="null">
113 <context context-type="linenumber">81</context>
114 </context-group>
115 </trans-unit>
116 <trans-unit id="803c6317abd2dbafcc93226c4e273c62932e3037">
117 <source>Blacklist this video</source>
118 <target>Blacklister cette vidéo</target>
119 <context-group name="null">
120 <context context-type="linenumber">80</context>
121 </context-group>
122 </trans-unit>
123 <trans-unit id="047f50bc5b5d17b5bec0196355953e1a5c590ddb">
124 <source>Update</source>
125 <target>Mettre à jour</target>
126 <context-group name="null">
127 <context context-type="linenumber">87</context>
128 </context-group>
129 </trans-unit>
130 <trans-unit id="cd27f761b923a5bdb16ba9844da632edd878f1b1">
131 <source>Update this video</source>
132 <target>Mettre à jour cette vidéo</target>
133 <context-group name="null">
134 <context context-type="linenumber">86</context>
135 </context-group>
136 </trans-unit>
137 <trans-unit id="826b25211922a1b46436589233cb6f1a163d89b7">
138 <source>Delete</source>
139 <target>Supprimer</target>
140 <context-group name="null">
141 <context context-type="linenumber">93</context>
142 </context-group>
143 </trans-unit>
144 <trans-unit id="3dbfdc68f83d91cb360172eb65578cae94e7cbe5">
145 <source>Delete this video</source>
146 <target>Supprimer cette vidéo</target>
147 <context-group name="null">
148 <context context-type="linenumber">92</context>
149 </context-group>
150 </trans-unit>
151 <trans-unit id="f0c5f6f270e70cbe063b5368fcf48f9afc1abd9b">
152 <source>Show more</source>
153 <target>Montrer plus</target>
154 <context-group name="null">
155 <context context-type="linenumber">112</context>
156 </context-group>
157 </trans-unit>
158 <trans-unit id="5403a767248e304199592271bba3366d2ca3f903">
159 <source>Show less</source>
160 <target>Montrer moins</target>
161 <context-group name="null">
162 <context context-type="linenumber">118</context>
163 </context-group>
164 </trans-unit>
165 <trans-unit id="8057a9b7f9e908ff350edfd71417b96c174e5911">
166 <source>
167 Privacy
168 </source>
169 <target>Visibilité</target>
170 <context-group name="null">
171 <context context-type="linenumber">125</context>
172 </context-group>
173 </trans-unit>
174 <trans-unit id="bd407eca607a8905a26a9e30c9d0cd70f4465db8">
175 <source>
176 Category
177 </source>
178 <target>Catégorie</target>
179 <context-group name="null">
180 <context context-type="linenumber">134</context>
181 </context-group>
182 </trans-unit>
183 <trans-unit id="b6b7986bc3721ac483baf20bc9a320529075c807">
184 <source>Trending</source>
185 <target>Tendances</target>
186 <context-group name="null">
187 <context context-type="linenumber">1</context>
188 </context-group>
189 </trans-unit>
190 </body>
191 </file></xliff> \ No newline at end of file
diff --git a/client/yarn.lock b/client/yarn.lock
index fe2e040d8..e2d0da541 100644
--- a/client/yarn.lock
+++ b/client/yarn.lock
@@ -242,6 +242,14 @@
242 dependencies: 242 dependencies:
243 tslib "~1.9.0" 243 tslib "~1.9.0"
244 244
245"@ngx-translate/i18n-polyfill@^1.0.0":
246 version "1.0.0"
247 resolved "https://registry.yarnpkg.com/@ngx-translate/i18n-polyfill/-/i18n-polyfill-1.0.0.tgz#145edb28bcfc1332e1bc25279eadf9d4ed0a20f8"
248 dependencies:
249 glob "7.1.2"
250 tslib "^1.9.0"
251 yargs "10.0.3"
252
245"@nodelib/fs.stat@^1.0.1": 253"@nodelib/fs.stat@^1.0.1":
246 version "1.1.0" 254 version "1.1.0"
247 resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-1.1.0.tgz#50c1e2260ac0ed9439a181de3725a0168d59c48a" 255 resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-1.1.0.tgz#50c1e2260ac0ed9439a181de3725a0168d59c48a"
@@ -4189,6 +4197,17 @@ glob@7.0.x:
4189 once "^1.3.0" 4197 once "^1.3.0"
4190 path-is-absolute "^1.0.0" 4198 path-is-absolute "^1.0.0"
4191 4199
4200glob@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:
4201 version "7.1.2"
4202 resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15"
4203 dependencies:
4204 fs.realpath "^1.0.0"
4205 inflight "^1.0.4"
4206 inherits "2"
4207 minimatch "^3.0.4"
4208 once "^1.3.0"
4209 path-is-absolute "^1.0.0"
4210
4192glob@^5.0.15: 4211glob@^5.0.15:
4193 version "5.0.15" 4212 version "5.0.15"
4194 resolved "https://registry.yarnpkg.com/glob/-/glob-5.0.15.tgz#1bc936b9e02f4a603fcc222ecf7633d30b8b93b1" 4213 resolved "https://registry.yarnpkg.com/glob/-/glob-5.0.15.tgz#1bc936b9e02f4a603fcc222ecf7633d30b8b93b1"
@@ -4209,17 +4228,6 @@ glob@^6.0.4:
4209 once "^1.3.0" 4228 once "^1.3.0"
4210 path-is-absolute "^1.0.0" 4229 path-is-absolute "^1.0.0"
4211 4230
4212glob@^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:
4213 version "7.1.2"
4214 resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15"
4215 dependencies:
4216 fs.realpath "^1.0.0"
4217 inflight "^1.0.4"
4218 inherits "2"
4219 minimatch "^3.0.4"
4220 once "^1.3.0"
4221 path-is-absolute "^1.0.0"
4222
4223global-modules@^1.0.0: 4231global-modules@^1.0.0:
4224 version "1.0.0" 4232 version "1.0.0"
4225 resolved "https://registry.yarnpkg.com/global-modules/-/global-modules-1.0.0.tgz#6d770f0eb523ac78164d72b5e71a8877265cc3ea" 4233 resolved "https://registry.yarnpkg.com/global-modules/-/global-modules-1.0.0.tgz#6d770f0eb523ac78164d72b5e71a8877265cc3ea"
@@ -10594,12 +10602,35 @@ yargs-parser@^7.0.0:
10594 dependencies: 10602 dependencies:
10595 camelcase "^4.1.0" 10603 camelcase "^4.1.0"
10596 10604
10605yargs-parser@^8.0.0:
10606 version "8.1.0"
10607 resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-8.1.0.tgz#f1376a33b6629a5d063782944da732631e966950"
10608 dependencies:
10609 camelcase "^4.1.0"
10610
10597yargs-parser@^9.0.2: 10611yargs-parser@^9.0.2:
10598 version "9.0.2" 10612 version "9.0.2"
10599 resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-9.0.2.tgz#9ccf6a43460fe4ed40a9bb68f48d43b8a68cc077" 10613 resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-9.0.2.tgz#9ccf6a43460fe4ed40a9bb68f48d43b8a68cc077"
10600 dependencies: 10614 dependencies:
10601 camelcase "^4.1.0" 10615 camelcase "^4.1.0"
10602 10616
10617yargs@10.0.3:
10618 version "10.0.3"
10619 resolved "https://registry.yarnpkg.com/yargs/-/yargs-10.0.3.tgz#6542debd9080ad517ec5048fb454efe9e4d4aaae"
10620 dependencies:
10621 cliui "^3.2.0"
10622 decamelize "^1.1.1"
10623 find-up "^2.1.0"
10624 get-caller-file "^1.0.1"
10625 os-locale "^2.0.0"
10626 require-directory "^2.1.1"
10627 require-main-filename "^1.0.1"
10628 set-blocking "^2.0.0"
10629 string-width "^2.0.0"
10630 which-module "^2.0.0"
10631 y18n "^3.2.1"
10632 yargs-parser "^8.0.0"
10633
10603yargs@11.0.0: 10634yargs@11.0.0:
10604 version "11.0.0" 10635 version "11.0.0"
10605 resolved "https://registry.yarnpkg.com/yargs/-/yargs-11.0.0.tgz#c052931006c5eee74610e5fc0354bedfd08a201b" 10636 resolved "https://registry.yarnpkg.com/yargs/-/yargs-11.0.0.tgz#c052931006c5eee74610e5fc0354bedfd08a201b"
diff --git a/package.json b/package.json
index 608646e7d..21701e664 100644
--- a/package.json
+++ b/package.json
@@ -29,6 +29,7 @@
29 "danger:clean:dev": "scripty", 29 "danger:clean:dev": "scripty",
30 "danger:clean:prod": "scripty", 30 "danger:clean:prod": "scripty",
31 "danger:clean:modules": "scripty", 31 "danger:clean:modules": "scripty",
32 "i18n:generate": "scripty",
32 "reset-password": "node ./dist/scripts/reset-password.js", 33 "reset-password": "node ./dist/scripts/reset-password.js",
33 "play": "scripty", 34 "play": "scripty",
34 "dev": "scripty", 35 "dev": "scripty",
diff --git a/scripts/build/client.sh b/scripts/build/client.sh
index 305af1e5f..61ba4ea99 100755
--- a/scripts/build/client.sh
+++ b/scripts/build/client.sh
@@ -6,5 +6,19 @@ cd client
6 6
7rm -rf ./dist ./compiled 7rm -rf ./dist ./compiled
8 8
9npm run ng build -- --prod --stats-json 9defaultLanguage="en-US"
10npm run ng build -- --output-path "dist/$defaultLanguage/" --deploy-url "/client/$defaultLanguage/" --prod --stats-json
11mv "./dist/$defaultLanguage/assets" "./dist"
12
13languages="fr"
14
15for lang in "$languages"; do
16 npm run ng build -- --prod --i18n-file "./src/locale/target/messages_$lang.xml" --i18n-format xlf --i18n-locale "$lang" \
17 --output-path "dist/$lang/" --deploy-url "/client/$lang/"
18
19 # Do no duplicate assets
20 rm -r "./dist/$lang/assets"
21done
22
10NODE_ENV=production npm run webpack -- --config webpack/webpack.video-embed.js --mode production 23NODE_ENV=production npm run webpack -- --config webpack/webpack.video-embed.js --mode production
24
diff --git a/scripts/i18n/generate.sh b/scripts/i18n/generate.sh
new file mode 100755
index 000000000..429523ba4
--- /dev/null
+++ b/scripts/i18n/generate.sh
@@ -0,0 +1,11 @@
1#!/bin/sh
2
3set -eu
4
5cd client
6npm run ng -- xi18n --i18n-locale "en-US" --output-path locale/source --out-file messages_en_US.xml
7npm run ngx-extractor -- --locale "en-US" -i 'src/**/*.ts' -f xlf -o src/locale/source/messages_en_US.xml
8
9# Zanata does not support inner elements in <source>, so we hack these special elements
10# This regex translate the Angular elements to special entities (that we will reconvert on pull)
11sed -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
index 000000000..cb969f83c
--- /dev/null
+++ b/scripts/i18n/pull-hook.sh
@@ -0,0 +1,7 @@
1#!/bin/sh
2
3set -eu
4
5# Zanata does not support inner elements in <source>, so we hack these special elements
6# This regex translate the converted elements to initial Angular elements
7sed -i 's/\&lt;x id=\([^\/]\+\?\)\/\&gt;/<x id=\1\/>/g' client/src/locale/target/* \ No newline at end of file
diff --git a/scripts/release.sh b/scripts/release.sh
index 8c73a1fd6..393955264 100755
--- a/scripts/release.sh
+++ b/scripts/release.sh
@@ -57,7 +57,7 @@ git commit package.json client/package.json -m "Bumped to version $version"
57git tag -s -a "$version" -m "$version" 57git tag -s -a "$version" -m "$version"
58 58
59npm run build 59npm run build
60rm "./client/dist/stats.json" 60rm "./client/dist/en-US/stats.json"
61 61
62# Creating the archives 62# Creating the archives
63( 63(
diff --git a/server.ts b/server.ts
index bdcbb7988..c0e679b02 100644
--- a/server.ts
+++ b/server.ts
@@ -12,7 +12,6 @@ import * as bodyParser from 'body-parser'
12import * as express from 'express' 12import * as express from 'express'
13import * as http from 'http' 13import * as http from 'http'
14import * as morgan from 'morgan' 14import * as morgan from 'morgan'
15import * as path from 'path'
16import * as bitTorrentTracker from 'bittorrent-tracker' 15import * as bitTorrentTracker from 'bittorrent-tracker'
17import * as cors from 'cors' 16import * as cors from 'cors'
18import { Server as WebSocketServer } from 'ws' 17import { Server as WebSocketServer } from 'ws'
@@ -156,20 +155,11 @@ app.use('/', activityPubRouter)
156app.use('/', feedsRouter) 155app.use('/', feedsRouter)
157app.use('/', webfingerRouter) 156app.use('/', webfingerRouter)
158 157
159// Client files
160app.use('/', clientsRouter)
161
162// Static files 158// Static files
163app.use('/', staticRouter) 159app.use('/', staticRouter)
164 160
165// Always serve index client page (the client is a single page application, let it handle routing) 161// Client files, last valid routes!
166app.use('/*', function (req, res) { 162app.use('/', clientsRouter)
167 if (req.accepts(ACCEPT_HEADERS) === 'html') {
168 return res.sendFile(path.join(__dirname, '../client/dist/index.html'))
169 }
170
171 return res.status(404).end()
172})
173 163
174// ----------- Errors ----------- 164// ----------- Errors -----------
175 165
diff --git a/server/controllers/client.ts b/server/controllers/client.ts
index aff00fe6e..a29b51c51 100644
--- a/server/controllers/client.ts
+++ b/server/controllers/client.ts
@@ -3,17 +3,24 @@ import * as express from 'express'
3import { join } from 'path' 3import { join } from 'path'
4import * as validator from 'validator' 4import * as validator from 'validator'
5import { escapeHTML, readFileBufferPromise, root } from '../helpers/core-utils' 5import { escapeHTML, readFileBufferPromise, root } from '../helpers/core-utils'
6import { CONFIG, EMBED_SIZE, OPENGRAPH_AND_OEMBED_COMMENT, STATIC_MAX_AGE, STATIC_PATHS } from '../initializers' 6import {
7 ACCEPT_HEADERS,
8 CONFIG,
9 EMBED_SIZE,
10 OPENGRAPH_AND_OEMBED_COMMENT,
11 STATIC_MAX_AGE,
12 STATIC_PATHS
13} from '../initializers'
7import { asyncMiddleware } from '../middlewares' 14import { asyncMiddleware } from '../middlewares'
8import { VideoModel } from '../models/video/video' 15import { VideoModel } from '../models/video/video'
9import { VideoPrivacy } from '../../shared/models/videos' 16import { VideoPrivacy } from '../../shared/models/videos'
17import { I18N_LOCALES, is18nLocale, getDefaultLocale } from '../../shared/models'
10 18
11const clientsRouter = express.Router() 19const clientsRouter = express.Router()
12 20
13const distPath = join(root(), 'client', 'dist') 21const distPath = join(root(), 'client', 'dist')
14const assetsImagesPath = join(root(), 'client', 'dist', 'assets', 'images') 22const assetsImagesPath = join(root(), 'client', 'dist', 'assets', 'images')
15const embedPath = join(distPath, 'standalone', 'videos', 'embed.html') 23const embedPath = join(distPath, 'standalone', 'videos', 'embed.html')
16const indexPath = join(distPath, 'index.html')
17 24
18// Special route that add OpenGraph and oEmbed tags 25// Special route that add OpenGraph and oEmbed tags
19// Do not use a template engine for a so little thing 26// 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
45 res.sendStatus(404) 52 res.sendStatus(404)
46}) 53})
47 54
55// Always serve index client page (the client is a single page application, let it handle routing)
56// Try to provide the right language index.html
57clientsRouter.use('/(:language)?', function (req, res) {
58 if (req.accepts(ACCEPT_HEADERS) === 'html') {
59 return res.sendFile(getIndexPath(req, req.params.language))
60 }
61
62 return res.status(404).end()
63})
64
48// --------------------------------------------------------------------------- 65// ---------------------------------------------------------------------------
49 66
50export { 67export {
@@ -53,6 +70,19 @@ export {
53 70
54// --------------------------------------------------------------------------- 71// ---------------------------------------------------------------------------
55 72
73function getIndexPath (req: express.Request, paramLang?: string) {
74 let lang: string
75
76 // Check param lang validity
77 if (paramLang && is18nLocale(paramLang)) {
78 lang = paramLang
79 } else {
80 lang = req.acceptsLanguages(Object.keys(I18N_LOCALES)) || getDefaultLocale()
81 }
82
83 return join(__dirname, '../../../client/dist/' + lang + '/index.html')
84}
85
56function addOpenGraphAndOEmbedTags (htmlStringPage: string, video: VideoModel) { 86function addOpenGraphAndOEmbedTags (htmlStringPage: string, video: VideoModel) {
57 const previewUrl = CONFIG.WEBSERVER.URL + STATIC_PATHS.PREVIEWS + video.getPreviewName() 87 const previewUrl = CONFIG.WEBSERVER.URL + STATIC_PATHS.PREVIEWS + video.getPreviewName()
58 const videoUrl = CONFIG.WEBSERVER.URL + '/videos/watch/' + video.uuid 88 const videoUrl = CONFIG.WEBSERVER.URL + '/videos/watch/' + video.uuid
@@ -142,18 +172,18 @@ async function generateWatchHtmlPage (req: express.Request, res: express.Respons
142 } else if (validator.isInt(videoId)) { 172 } else if (validator.isInt(videoId)) {
143 videoPromise = VideoModel.loadAndPopulateAccountAndServerAndTags(+videoId) 173 videoPromise = VideoModel.loadAndPopulateAccountAndServerAndTags(+videoId)
144 } else { 174 } else {
145 return res.sendFile(indexPath) 175 return res.sendFile(getIndexPath(req))
146 } 176 }
147 177
148 let [ file, video ] = await Promise.all([ 178 let [ file, video ] = await Promise.all([
149 readFileBufferPromise(indexPath), 179 readFileBufferPromise(getIndexPath(req)),
150 videoPromise 180 videoPromise
151 ]) 181 ])
152 182
153 const html = file.toString() 183 const html = file.toString()
154 184
155 // Let Angular application handle errors 185 // Let Angular application handle errors
156 if (!video || video.privacy === VideoPrivacy.PRIVATE) return res.sendFile(indexPath) 186 if (!video || video.privacy === VideoPrivacy.PRIVATE) return res.sendFile(getIndexPath(req))
157 187
158 const htmlStringPageWithTags = addOpenGraphAndOEmbedTags(html, video) 188 const htmlStringPageWithTags = addOpenGraphAndOEmbedTags(html, video)
159 res.set('Content-Type', 'text/html; charset=UTF-8').send(htmlStringPageWithTags) 189 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
index 000000000..2d3a1d3e2
--- /dev/null
+++ b/shared/models/i18n/i18n.ts
@@ -0,0 +1,30 @@
1export const I18N_LOCALES = {
2 'en-US': 'English (US)',
3 fr: 'French'
4}
5
6export function getDefaultLocale () {
7 return 'en-US'
8}
9
10const possiblePaths = Object.keys(I18N_LOCALES).map(l => '/' + l)
11export function is18nPath (path: string) {
12 return possiblePaths.indexOf(path) !== -1
13}
14
15const possibleLanguages = Object.keys(I18N_LOCALES)
16export function is18nLocale (locale: string) {
17 return possibleLanguages.indexOf(locale) !== -1
18}
19
20// Only use in dev mode, so relax
21// In production, the locale always match with a I18N_LANGUAGES key
22export function buildFileLocale (locale: string) {
23 if (!is18nLocale(locale)) {
24 // Some working examples for development purpose
25 if (locale.split('-')[ 0 ] === 'en') return 'en_US'
26 else if (locale === 'fr') return 'fr'
27 }
28
29 return locale.replace('-', '_')
30}
diff --git a/shared/models/i18n/index.ts b/shared/models/i18n/index.ts
new file mode 100644
index 000000000..8f7cbe2c7
--- /dev/null
+++ b/shared/models/i18n/index.ts
@@ -0,0 +1 @@
export * from './i18n'
diff --git a/shared/models/index.ts b/shared/models/index.ts
index 95bc402d6..c8ce71f17 100644
--- a/shared/models/index.ts
+++ b/shared/models/index.ts
@@ -3,6 +3,7 @@ export * from './activitypub'
3export * from './users' 3export * from './users'
4export * from './videos' 4export * from './videos'
5export * from './feeds' 5export * from './feeds'
6export * from './i18n'
6export * from './server/job.model' 7export * from './server/job.model'
7export * from './oauth-client-local.model' 8export * from './oauth-client-local.model'
8export * from './result-list.model' 9export * from './result-list.model'
diff --git a/yarn.lock b/yarn.lock
index c1fed9c60..eb06faac0 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1237,7 +1237,7 @@ chalk@^1.0.0, chalk@^1.1.1, chalk@^1.1.3:
1237 strip-ansi "^3.0.0" 1237 strip-ansi "^3.0.0"
1238 supports-color "^2.0.0" 1238 supports-color "^2.0.0"
1239 1239
1240chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0, chalk@^2.3.0, chalk@^2.3.1, chalk@^2.3.2: 1240chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0, chalk@^2.3.0, chalk@^2.3.1:
1241 version "2.4.1" 1241 version "2.4.1"
1242 resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.1.tgz#18c49ab16a037b6eb0152cc83e3471338215b66e" 1242 resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.1.tgz#18c49ab16a037b6eb0152cc83e3471338215b66e"
1243 dependencies: 1243 dependencies:
@@ -1517,7 +1517,7 @@ command-exists@^1.2.2:
1517 version "1.2.6" 1517 version "1.2.6"
1518 resolved "https://registry.yarnpkg.com/command-exists/-/command-exists-1.2.6.tgz#577f8e5feb0cb0f159cd557a51a9be1bdd76e09e" 1518 resolved "https://registry.yarnpkg.com/command-exists/-/command-exists-1.2.6.tgz#577f8e5feb0cb0f159cd557a51a9be1bdd76e09e"
1519 1519
1520commander@*, 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: 1520commander@*, commander@2.15.1, commander@^2.12.1, commander@^2.13.0, commander@^2.14.1, commander@^2.8.1, commander@^2.9.0:
1521 version "2.15.1" 1521 version "2.15.1"
1522 resolved "https://registry.yarnpkg.com/commander/-/commander-2.15.1.tgz#df46e867d0fc2aec66a34662b406a9ccafff5b0f" 1522 resolved "https://registry.yarnpkg.com/commander/-/commander-2.15.1.tgz#df46e867d0fc2aec66a34662b406a9ccafff5b0f"
1523 1523
@@ -2175,12 +2175,6 @@ error-ex@^1.2.0, error-ex@^1.3.1:
2175 dependencies: 2175 dependencies:
2176 is-arrayish "^0.2.1" 2176 is-arrayish "^0.2.1"
2177 2177
2178error-stack-parser@^2.0.1:
2179 version "2.0.1"
2180 resolved "https://registry.yarnpkg.com/error-stack-parser/-/error-stack-parser-2.0.1.tgz#a3202b8fb03114aa9b40a0e3669e48b2b65a010a"
2181 dependencies:
2182 stackframe "^1.0.3"
2183
2184es5-ext@^0.10.14, es5-ext@^0.10.35, es5-ext@^0.10.9, es5-ext@~0.10.14: 2178es5-ext@^0.10.14, es5-ext@^0.10.35, es5-ext@^0.10.9, es5-ext@~0.10.14:
2185 version "0.10.43" 2179 version "0.10.43"
2186 resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.43.tgz#c705e645253210233a270869aa463a2333b7ca64" 2180 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:
4155 version "3.0.2" 4149 version "3.0.2"
4156 resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b" 4150 resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b"
4157 4151
4158js-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: 4152js-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:
4159 version "3.11.0" 4153 version "3.11.0"
4160 resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.11.0.tgz#597c1a8bd57152f26d622ce4117851a51f5ebaef" 4154 resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.11.0.tgz#597c1a8bd57152f26d622ce4117851a51f5ebaef"
4161 dependencies: 4155 dependencies:
@@ -6741,18 +6735,6 @@ sass-graph@^2.2.4:
6741 scss-tokenizer "^0.2.3" 6735 scss-tokenizer "^0.2.3"
6742 yargs "^7.0.0" 6736 yargs "^7.0.0"
6743 6737
6744sass-lint-auto-fix@^0.9.0:
6745 version "0.9.2"
6746 resolved "https://registry.yarnpkg.com/sass-lint-auto-fix/-/sass-lint-auto-fix-0.9.2.tgz#b8b6eb95644f7919dfea33d04c1fc19ae8f07a11"
6747 dependencies:
6748 chalk "^2.3.2"
6749 commander "^2.15.1"
6750 glob "^7.1.2"
6751 gonzales-pe-sl "^4.2.3"
6752 js-yaml "^3.11.0"
6753 sass-lint "^1.12.1"
6754 stacktrace-js "^2.0.0"
6755
6756sass-lint@^1.12.1: 6738sass-lint@^1.12.1:
6757 version "1.12.1" 6739 version "1.12.1"
6758 resolved "https://registry.yarnpkg.com/sass-lint/-/sass-lint-1.12.1.tgz#630f69c216aa206b8232fb2aa907bdf3336b6d83" 6740 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:
7194 dependencies: 7176 dependencies:
7195 amdefine ">=0.0.4" 7177 amdefine ">=0.0.4"
7196 7178
7197source-map@0.5.6:
7198 version "0.5.6"
7199 resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.6.tgz#75ce38f52bf0733c5a7f0c118d81334a2bb5f412"
7200
7201source-map@0.5.x, source-map@^0.5.6, source-map@~0.5.1: 7179source-map@0.5.x, source-map@^0.5.6, source-map@~0.5.1:
7202 version "0.5.7" 7180 version "0.5.7"
7203 resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" 7181 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:
7315 version "1.3.7" 7293 version "1.3.7"
7316 resolved "https://registry.yarnpkg.com/stack-chain/-/stack-chain-1.3.7.tgz#d192c9ff4ea6a22c94c4dd459171e3f00cea1285" 7294 resolved "https://registry.yarnpkg.com/stack-chain/-/stack-chain-1.3.7.tgz#d192c9ff4ea6a22c94c4dd459171e3f00cea1285"
7317 7295
7318stack-generator@^2.0.1:
7319 version "2.0.2"
7320 resolved "https://registry.yarnpkg.com/stack-generator/-/stack-generator-2.0.2.tgz#3c13d952a596ab9318fec0669d0a1df8b87176c7"
7321 dependencies:
7322 stackframe "^1.0.4"
7323
7324stack-trace@0.0.x: 7296stack-trace@0.0.x:
7325 version "0.0.10" 7297 version "0.0.10"
7326 resolved "https://registry.yarnpkg.com/stack-trace/-/stack-trace-0.0.10.tgz#547c70b347e8d32b4e108ea1a2a159e5fdde19c0" 7298 resolved "https://registry.yarnpkg.com/stack-trace/-/stack-trace-0.0.10.tgz#547c70b347e8d32b4e108ea1a2a159e5fdde19c0"
@@ -7329,25 +7301,6 @@ stack-utils@^1.0.1:
7329 version "1.0.1" 7301 version "1.0.1"
7330 resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-1.0.1.tgz#d4f33ab54e8e38778b0ca5cfd3b3afb12db68620" 7302 resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-1.0.1.tgz#d4f33ab54e8e38778b0ca5cfd3b3afb12db68620"
7331 7303
7332stackframe@^1.0.3, stackframe@^1.0.4:
7333 version "1.0.4"
7334 resolved "https://registry.yarnpkg.com/stackframe/-/stackframe-1.0.4.tgz#357b24a992f9427cba6b545d96a14ed2cbca187b"
7335
7336stacktrace-gps@^3.0.1:
7337 version "3.0.2"
7338 resolved "https://registry.yarnpkg.com/stacktrace-gps/-/stacktrace-gps-3.0.2.tgz#33f8baa4467323ab2bd1816efa279942ba431ccc"
7339 dependencies:
7340 source-map "0.5.6"
7341 stackframe "^1.0.4"
7342
7343stacktrace-js@^2.0.0:
7344 version "2.0.0"
7345 resolved "https://registry.yarnpkg.com/stacktrace-js/-/stacktrace-js-2.0.0.tgz#776ca646a95bc6c6b2b90776536a7fc72c6ddb58"
7346 dependencies:
7347 error-stack-parser "^2.0.1"
7348 stack-generator "^2.0.1"
7349 stacktrace-gps "^3.0.1"
7350
7351staged-git-files@1.1.1: 7304staged-git-files@1.1.1:
7352 version "1.1.1" 7305 version "1.1.1"
7353 resolved "https://registry.yarnpkg.com/staged-git-files/-/staged-git-files-1.1.1.tgz#37c2218ef0d6d26178b1310719309a16a59f8f7b" 7306 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
index 000000000..d68b3a3ba
--- /dev/null
+++ b/zanata.xml
@@ -0,0 +1,15 @@
1<?xml version='1.0' encoding='UTF-8' standalone='yes'?>
2<config xmlns="http://zanata.org/namespace/config/">
3 <url>https://trad.framasoft.org/zanata/</url>
4 <project>peertube</project>
5 <project-version>develop</project-version>
6 <project-type>xliff</project-type>
7 <src-dir>./client/src/locale/source</src-dir>
8 <trans-dir>./client/src/locale/target</trans-dir>
9
10 <hooks>
11 <hook command="pull">
12 <after>./scripts/i18n/pull-hook.sh</after>
13 </hook>
14 </hooks>
15</config>