diff options
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 @@ | |||
1 | import { Component, OnInit } from '@angular/core' | 1 | import { Component, OnInit } from '@angular/core' |
2 | import { DomSanitizer, SafeHtml } from '@angular/platform-browser' | 2 | import { DomSanitizer, SafeHtml } from '@angular/platform-browser' |
3 | import { GuardsCheckStart, Router, NavigationEnd } from '@angular/router' | 3 | import { GuardsCheckStart, NavigationEnd, Router } from '@angular/router' |
4 | import { AuthService, RedirectService, ServerService } from '@app/core' | 4 | import { AuthService, RedirectService, ServerService } from '@app/core' |
5 | import { isInSmallView } from '@app/shared/misc/utils' | 5 | import { isInSmallView } from '@app/shared/misc/utils' |
6 | import { 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 @@ | |||
1 | import { NgModule } from '@angular/core' | 1 | import { LOCALE_ID, NgModule, TRANSLATIONS, TRANSLATIONS_FORMAT } from '@angular/core' |
2 | import { BrowserModule } from '@angular/platform-browser' | 2 | import { BrowserModule } from '@angular/platform-browser' |
3 | import { AboutModule } from '@app/about' | 3 | import { AboutModule } from '@app/about' |
4 | import { ServerService } from '@app/core' | 4 | import { ServerService } from '@app/core' |
@@ -16,6 +16,7 @@ import { MenuComponent } from './menu' | |||
16 | import { SharedModule } from './shared' | 16 | import { SharedModule } from './shared' |
17 | import { SignupModule } from './signup' | 17 | import { SignupModule } from './signup' |
18 | import { VideosModule } from './videos' | 18 | import { VideosModule } from './videos' |
19 | import { buildFileLocale, getDefaultLocale } from '../../../shared/models/i18n' | ||
19 | 20 | ||
20 | export function metaFactory (serverService: ServerService): MetaLoader { | 21 | export 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 | }) |
66 | export class AppModule {} | 82 | export 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 |
100 | let windowInnerWidth = window.innerWidth | 100 | let windowInnerWidth = window.innerWidth |
101 | // setInterval(() => windowInnerWidth = window.innerWidth, 500) | 101 | setInterval(() => windowInnerWidth = window.innerWidth, 500) |
102 | 102 | ||
103 | function isInSmallView () { | 103 | function 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' | |||
33 | import { VideoService } from './video/video.service' | 33 | import { VideoService } from './video/video.service' |
34 | import { AccountService } from '@app/shared/account/account.service' | 34 | import { AccountService } from '@app/shared/account/account.service' |
35 | import { VideoChannelService } from '@app/shared/video-channel/video-channel.service' | 35 | import { VideoChannelService } from '@app/shared/video-channel/video-channel.service' |
36 | import { 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 | }) |
114 | export class SharedModule { } | 116 | export 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' | |||
23 | import { VideoShareComponent } from './modal/video-share.component' | 23 | import { VideoShareComponent } from './modal/video-share.component' |
24 | import { getVideojsOptions } from '../../../assets/player/peertube-player' | 24 | import { getVideojsOptions } from '../../../assets/player/peertube-player' |
25 | import { ServerService } from '@app/core' | 25 | import { ServerService } from '@app/core' |
26 | import { 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' | |||
8 | import { VideoSortField } from '../../shared/video/sort-field.type' | 8 | import { VideoSortField } from '../../shared/video/sort-field.type' |
9 | import { VideoService } from '../../shared/video/video.service' | 9 | import { VideoService } from '../../shared/video/video.service' |
10 | import { VideoFilter } from '../../../../../shared/models/videos/video-query.type' | 10 | import { VideoFilter } from '../../../../../shared/models/videos/video-query.type' |
11 | import { 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 | }) |
17 | export class VideoLocalComponent extends AbstractVideoList implements OnInit, OnDestroy { | 18 | export 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' | |||
7 | import { AbstractVideoList } from '../../shared/video/abstract-video-list' | 7 | import { AbstractVideoList } from '../../shared/video/abstract-video-list' |
8 | import { VideoSortField } from '../../shared/video/sort-field.type' | 8 | import { VideoSortField } from '../../shared/video/sort-field.type' |
9 | import { VideoService } from '../../shared/video/video.service' | 9 | import { VideoService } from '../../shared/video/video.service' |
10 | import { 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 | }) |
16 | export class VideoRecentlyAddedComponent extends AbstractVideoList implements OnInit, OnDestroy { | 17 | export 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' | |||
8 | import { AuthService } from '../../core/auth' | 8 | import { AuthService } from '../../core/auth' |
9 | import { AbstractVideoList } from '../../shared/video/abstract-video-list' | 9 | import { AbstractVideoList } from '../../shared/video/abstract-video-list' |
10 | import { VideoService } from '../../shared/video/video.service' | 10 | import { VideoService } from '../../shared/video/video.service' |
11 | import { 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 | }) |
17 | export class VideoSearchComponent extends AbstractVideoList implements OnInit, OnDestroy { | 18 | export 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' | |||
7 | import { AbstractVideoList } from '../../shared/video/abstract-video-list' | 7 | import { AbstractVideoList } from '../../shared/video/abstract-video-list' |
8 | import { VideoSortField } from '../../shared/video/sort-field.type' | 8 | import { VideoSortField } from '../../shared/video/sort-field.type' |
9 | import { VideoService } from '../../shared/video/video.service' | 9 | import { VideoService } from '../../shared/video/video.service' |
10 | import { 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 | }) |
16 | export class VideoTrendingComponent extends AbstractVideoList implements OnInit, OnDestroy { | 17 | export 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 :'(</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 | <x id="INTERPOLATION" equiv-text="{{ video.publishedAt | myFromNow }}"/> - <x id="INTERPOLATION_1" equiv-text="{{ video.views | myNumberFormatter }}"/> 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 <strong>@<x id="INTERPOLATION" equiv-text="{{video.account.displayName}}"/>@<x id="INTERPOLATION_1" equiv-text="{{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>.</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 <x id="INTERPOLATION" equiv-text="{{ video.by }}"/></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 <x id="INTERPOLATION" equiv-text="{{ videoName }}"/> 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 <x id="INTERPOLATION" equiv-text="{{ videoName }}"/> 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><x id="INTERPOLATION" equiv-text="{{ likesNumber }}"/> likes / <x id="INTERPOLATION_1" equiv-text="{{ dislikesNumber }}"/> 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 <strong>@<x id="INTERPOLATION" equiv-text="{{video.account.displayName}}"/>@<x id="INTERPOLATION_1" equiv-text="{{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>.</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 <strong>@<x id="INTERPOLATION" equiv-text="{{video.account.displayName}}"/>@<x id="INTERPOLATION_1" equiv-text="{{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>.</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 | ||
4200 | 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: | ||
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 | |||
4192 | glob@^5.0.15: | 4211 | glob@^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 | ||
4212 | 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: | ||
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 | |||
4223 | global-modules@^1.0.0: | 4231 | global-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 | ||
10605 | yargs-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 | |||
10597 | yargs-parser@^9.0.2: | 10611 | yargs-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 | ||
10617 | yargs@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 | |||
10603 | yargs@11.0.0: | 10634 | yargs@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 | ||
7 | rm -rf ./dist ./compiled | 7 | rm -rf ./dist ./compiled |
8 | 8 | ||
9 | npm run ng build -- --prod --stats-json | 9 | defaultLanguage="en-US" |
10 | npm run ng build -- --output-path "dist/$defaultLanguage/" --deploy-url "/client/$defaultLanguage/" --prod --stats-json | ||
11 | mv "./dist/$defaultLanguage/assets" "./dist" | ||
12 | |||
13 | languages="fr" | ||
14 | |||
15 | for 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" | ||
21 | done | ||
22 | |||
10 | NODE_ENV=production npm run webpack -- --config webpack/webpack.video-embed.js --mode production | 23 | NODE_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 | |||
3 | set -eu | ||
4 | |||
5 | cd client | ||
6 | npm run ng -- xi18n --i18n-locale "en-US" --output-path locale/source --out-file messages_en_US.xml | ||
7 | npm 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) | ||
11 | sed -i 's/<x id=\([^\/]\+\?\)\/>/\<x id=\1\/\>/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 | |||
3 | set -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 | ||
7 | sed -i 's/\<x id=\([^\/]\+\?\)\/\>/<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" | |||
57 | git tag -s -a "$version" -m "$version" | 57 | git tag -s -a "$version" -m "$version" |
58 | 58 | ||
59 | npm run build | 59 | npm run build |
60 | rm "./client/dist/stats.json" | 60 | rm "./client/dist/en-US/stats.json" |
61 | 61 | ||
62 | # Creating the archives | 62 | # Creating the archives |
63 | ( | 63 | ( |
@@ -12,7 +12,6 @@ import * as bodyParser from 'body-parser' | |||
12 | import * as express from 'express' | 12 | import * as express from 'express' |
13 | import * as http from 'http' | 13 | import * as http from 'http' |
14 | import * as morgan from 'morgan' | 14 | import * as morgan from 'morgan' |
15 | import * as path from 'path' | ||
16 | import * as bitTorrentTracker from 'bittorrent-tracker' | 15 | import * as bitTorrentTracker from 'bittorrent-tracker' |
17 | import * as cors from 'cors' | 16 | import * as cors from 'cors' |
18 | import { Server as WebSocketServer } from 'ws' | 17 | import { Server as WebSocketServer } from 'ws' |
@@ -156,20 +155,11 @@ app.use('/', activityPubRouter) | |||
156 | app.use('/', feedsRouter) | 155 | app.use('/', feedsRouter) |
157 | app.use('/', webfingerRouter) | 156 | app.use('/', webfingerRouter) |
158 | 157 | ||
159 | // Client files | ||
160 | app.use('/', clientsRouter) | ||
161 | |||
162 | // Static files | 158 | // Static files |
163 | app.use('/', staticRouter) | 159 | app.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! |
166 | app.use('/*', function (req, res) { | 162 | app.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' | |||
3 | import { join } from 'path' | 3 | import { join } from 'path' |
4 | import * as validator from 'validator' | 4 | import * as validator from 'validator' |
5 | import { escapeHTML, readFileBufferPromise, root } from '../helpers/core-utils' | 5 | import { escapeHTML, readFileBufferPromise, root } from '../helpers/core-utils' |
6 | import { CONFIG, EMBED_SIZE, OPENGRAPH_AND_OEMBED_COMMENT, STATIC_MAX_AGE, STATIC_PATHS } from '../initializers' | 6 | import { |
7 | ACCEPT_HEADERS, | ||
8 | CONFIG, | ||
9 | EMBED_SIZE, | ||
10 | OPENGRAPH_AND_OEMBED_COMMENT, | ||
11 | STATIC_MAX_AGE, | ||
12 | STATIC_PATHS | ||
13 | } from '../initializers' | ||
7 | import { asyncMiddleware } from '../middlewares' | 14 | import { asyncMiddleware } from '../middlewares' |
8 | import { VideoModel } from '../models/video/video' | 15 | import { VideoModel } from '../models/video/video' |
9 | import { VideoPrivacy } from '../../shared/models/videos' | 16 | import { VideoPrivacy } from '../../shared/models/videos' |
17 | import { I18N_LOCALES, is18nLocale, getDefaultLocale } from '../../shared/models' | ||
10 | 18 | ||
11 | const clientsRouter = express.Router() | 19 | const clientsRouter = express.Router() |
12 | 20 | ||
13 | const distPath = join(root(), 'client', 'dist') | 21 | const distPath = join(root(), 'client', 'dist') |
14 | const assetsImagesPath = join(root(), 'client', 'dist', 'assets', 'images') | 22 | const assetsImagesPath = join(root(), 'client', 'dist', 'assets', 'images') |
15 | const embedPath = join(distPath, 'standalone', 'videos', 'embed.html') | 23 | const embedPath = join(distPath, 'standalone', 'videos', 'embed.html') |
16 | const 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 | ||
57 | clientsRouter.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 | ||
50 | export { | 67 | export { |
@@ -53,6 +70,19 @@ export { | |||
53 | 70 | ||
54 | // --------------------------------------------------------------------------- | 71 | // --------------------------------------------------------------------------- |
55 | 72 | ||
73 | function 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 | |||
56 | function addOpenGraphAndOEmbedTags (htmlStringPage: string, video: VideoModel) { | 86 | function 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 @@ | |||
1 | export const I18N_LOCALES = { | ||
2 | 'en-US': 'English (US)', | ||
3 | fr: 'French' | ||
4 | } | ||
5 | |||
6 | export function getDefaultLocale () { | ||
7 | return 'en-US' | ||
8 | } | ||
9 | |||
10 | const possiblePaths = Object.keys(I18N_LOCALES).map(l => '/' + l) | ||
11 | export function is18nPath (path: string) { | ||
12 | return possiblePaths.indexOf(path) !== -1 | ||
13 | } | ||
14 | |||
15 | const possibleLanguages = Object.keys(I18N_LOCALES) | ||
16 | export 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 | ||
22 | export 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' | |||
3 | export * from './users' | 3 | export * from './users' |
4 | export * from './videos' | 4 | export * from './videos' |
5 | export * from './feeds' | 5 | export * from './feeds' |
6 | export * from './i18n' | ||
6 | export * from './server/job.model' | 7 | export * from './server/job.model' |
7 | export * from './oauth-client-local.model' | 8 | export * from './oauth-client-local.model' |
8 | export * from './result-list.model' | 9 | export * from './result-list.model' |
@@ -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 | ||
1240 | chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0, chalk@^2.3.0, chalk@^2.3.1, chalk@^2.3.2: | 1240 | chalk@^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 | ||
1520 | 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: | 1520 | 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: |
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 | ||
2178 | error-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 | |||
2184 | es5-ext@^0.10.14, es5-ext@^0.10.35, es5-ext@^0.10.9, es5-ext@~0.10.14: | 2178 | es5-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 | ||
4158 | 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: | 4152 | 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: |
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 | ||
6744 | sass-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 | |||
6756 | sass-lint@^1.12.1: | 6738 | sass-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 | ||
7197 | source-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 | |||
7201 | source-map@0.5.x, source-map@^0.5.6, source-map@~0.5.1: | 7179 | source-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 | ||
7318 | stack-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 | |||
7324 | stack-trace@0.0.x: | 7296 | stack-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 | ||
7332 | stackframe@^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 | |||
7336 | stacktrace-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 | |||
7343 | stacktrace-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 | |||
7351 | staged-git-files@1.1.1: | 7304 | staged-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> | ||