Prepare i18n files

pull/619/head
Chocobozzz 2018-05-31 18:12:15 +02:00
parent 1dd59831f8
commit 989e526abf
No known key found for this signature in database
GPG Key ID: 583A612D890159BE
28 changed files with 853 additions and 166 deletions

1
.gitignore vendored
View File

@ -25,3 +25,4 @@
/logs/
/server/tools/import-mediacore.ts
/docker-volume/
/.zanata-cache

View File

@ -19,7 +19,8 @@
"ng": "ng",
"postinstall": "npm rebuild node-sass && test -f angular-cli-patch.js && node angular-cli-patch.js || true",
"webpack-bundle-analyzer": "webpack-bundle-analyzer",
"webdriver-manager": "webdriver-manager"
"webdriver-manager": "webdriver-manager",
"ngx-extractor": "ngx-extractor"
},
"license": "GPLv3",
"resolutions": {
@ -47,6 +48,7 @@
"@ngx-loading-bar/http-client": "^2.0.0",
"@ngx-loading-bar/router": "^2.0.0",
"@ngx-meta/core": "^6.0.0-rc.1",
"@ngx-translate/i18n-polyfill": "^1.0.0",
"@types/core-js": "^0.9.28",
"@types/jasmine": "^2.8.7",
"@types/jasminewd2": "^2.0.3",

View File

@ -1,8 +1,9 @@
import { Component, OnInit } from '@angular/core'
import { DomSanitizer, SafeHtml } from '@angular/platform-browser'
import { GuardsCheckStart, Router, NavigationEnd } from '@angular/router'
import { GuardsCheckStart, NavigationEnd, Router } from '@angular/router'
import { AuthService, RedirectService, ServerService } from '@app/core'
import { isInSmallView } from '@app/shared/misc/utils'
import { is18nPath } from '../../../shared/models/i18n'
@Component({
selector: 'my-app',
@ -33,7 +34,7 @@ export class AppComponent implements OnInit {
private serverService: ServerService,
private domSanitizer: DomSanitizer,
private redirectService: RedirectService
) {}
) { }
get serverVersion () {
return this.serverService.getConfig().serverVersion
@ -53,7 +54,7 @@ export class AppComponent implements OnInit {
this.router.events.subscribe(e => {
if (e instanceof NavigationEnd) {
const pathname = window.location.pathname
if (!pathname || pathname === '/') {
if (!pathname || pathname === '/' || is18nPath(pathname)) {
this.redirectService.redirectToHomepage()
}
}

View File

@ -1,4 +1,4 @@
import { NgModule } from '@angular/core'
import { LOCALE_ID, NgModule, TRANSLATIONS, TRANSLATIONS_FORMAT } from '@angular/core'
import { BrowserModule } from '@angular/platform-browser'
import { AboutModule } from '@app/about'
import { ServerService } from '@app/core'
@ -16,6 +16,7 @@ import { MenuComponent } from './menu'
import { SharedModule } from './shared'
import { SignupModule } from './signup'
import { VideosModule } from './videos'
import { buildFileLocale, getDefaultLocale } from '../../../shared/models/i18n'
export function metaFactory (serverService: ServerService): MetaLoader {
return new MetaStaticLoader({
@ -61,6 +62,21 @@ export function metaFactory (serverService: ServerService): MetaLoader {
AppRoutingModule // Put it after all the module because it has the 404 route
],
providers: [ ]
providers: [
{
provide: TRANSLATIONS,
useFactory: (locale) => {
const fileLocale = buildFileLocale(locale)
// Default locale, nothing to translate
const defaultFileLocale = buildFileLocale(getDefaultLocale())
if (fileLocale === defaultFileLocale) return ''
return require(`raw-loader!../locale/target/messages_${fileLocale}.xml`)
},
deps: [ LOCALE_ID ]
},
{ provide: TRANSLATIONS_FORMAT, useValue: 'xlf' }
]
})
export class AppModule {}

View File

@ -31,7 +31,7 @@ export class RedirectService {
redirectToHomepage () {
console.log('Redirecting to %s...', RedirectService.DEFAULT_ROUTE)
this.router.navigate([ RedirectService.DEFAULT_ROUTE ], { replaceUrl: true })
this.router.navigate([ RedirectService.DEFAULT_ROUTE ], { skipLocationChange: true })
.catch(() => {
console.error(
'Cannot navigate to %s, resetting default route to %s.',
@ -40,7 +40,7 @@ export class RedirectService {
)
RedirectService.DEFAULT_ROUTE = RedirectService.INIT_DEFAULT_ROUTE
return this.router.navigate([ RedirectService.DEFAULT_ROUTE ], { replaceUrl: true })
return this.router.navigate([ RedirectService.DEFAULT_ROUTE ], { skipLocationChange: true })
})
}

View File

@ -98,7 +98,7 @@ function lineFeedToHtml (obj: object, keyToNormalize: string) {
// Try to cache a little bit window.innerWidth
let windowInnerWidth = window.innerWidth
// setInterval(() => windowInnerWidth = window.innerWidth, 500)
setInterval(() => windowInnerWidth = window.innerWidth, 500)
function isInSmallView () {
return windowInnerWidth < 600

View File

@ -33,6 +33,7 @@ import { VideoThumbnailComponent } from './video/video-thumbnail.component'
import { VideoService } from './video/video.service'
import { AccountService } from '@app/shared/account/account.service'
import { VideoChannelService } from '@app/shared/video-channel/video-channel.service'
import { I18n } from '@ngx-translate/i18n-polyfill'
@NgModule({
imports: [
@ -108,7 +109,8 @@ import { VideoChannelService } from '@app/shared/video-channel/video-channel.ser
VideoService,
AccountService,
MarkdownService,
VideoChannelService
VideoChannelService,
I18n
]
})
export class SharedModule { }

View File

@ -3,7 +3,7 @@
<div [hidden]="videoNotFound" id="video-element-wrapper">
</div>
<div *ngIf="videoNotFound" id="video-not-found">Video not found :'(</div>
<div i18n *ngIf="videoNotFound" id="video-not-found">Video not found :'(</div>
<!-- Video information -->
<div *ngIf="video" class="margin-content video-bottom">
@ -12,21 +12,21 @@
<div>
<div class="video-info-name">{{ video.name }}</div>
<div class="video-info-date-views">
<div i18n class="video-info-date-views">
{{ video.publishedAt | myFromNow }} - {{ video.views | myNumberFormatter }} views
</div>
<div class="video-info-channel">
<a [routerLink]="[ '/video-channels', video.channel.id ]" title="Go the channel page">
<a [routerLink]="[ '/video-channels', video.channel.id ]" i18n-title title="Go the channel page">
{{ video.channel.displayName }}
</a>
<!-- Here will be the subscribe button -->
<my-help helpType="custom" customHtml="You can subscribe to this account via any ActivityPub-capable fediverse instance. For instance with Mastodon or Pleroma you can type in the search box <strong>@{{video.account.displayName}}@{{video.account.host}}</strong> and subscribe there. Subscription as a PeerTube user is being worked on in <a href='https://github.com/Chocobozzz/PeerTube/issues/470'>#470</a>."></my-help>
<my-help helpType="custom" i18n-customHtml customHtml="You can subscribe to this account via any ActivityPub-capable fediverse instance. For instance with Mastodon or Pleroma you can type in the search box <strong>@{{video.account.displayName}}@{{video.account.host}}</strong> and subscribe there. Subscription as a PeerTube user is being worked on in <a href='https://github.com/Chocobozzz/PeerTube/issues/470'>#470</a>."></my-help>
</div>
<div class="video-info-by">
<a [routerLink]="[ '/accounts', video.by ]" title="Go the account page">
<span>By {{ video.by }}</span>
<a [routerLink]="[ '/accounts', video.by ]" i18n-title title="Go the account page">
<span i18n>By {{ video.by }}</span>
<img [src]="video.accountAvatarUrl" alt="Account avatar" />
</a>
</div>
@ -38,24 +38,24 @@
*ngIf="isUserLoggedIn()" [ngClass]="{ 'activated': userRating === 'like' }" (click)="setLike()"
class="action-button action-button-like"
>
<span class="icon icon-like" title="Like this video" ></span>
<span class="icon icon-like" i18n-title title="Like this video" ></span>
</div>
<div
*ngIf="isUserLoggedIn()" [ngClass]="{ 'activated': userRating === 'dislike' }" (click)="setDislike()"
class="action-button action-button-dislike"
>
<span class="icon icon-dislike" title="Dislike this video"></span>
<span class="icon icon-dislike" i18n-title title="Dislike this video"></span>
</div>
<div *ngIf="video.support" (click)="showSupportModal()" class="action-button action-button-support">
<span class="icon icon-support"></span>
<span class="icon-text">Support</span>
<span class="icon-text" i18n>Support</span>
</div>
<div (click)="showShareModal()" class="action-button action-button-share">
<span class="icon icon-share"></span>
<span class="icon-text">Share</span>
<span class="icon-text" i18n>Share</span>
</div>
<div class="action-more" dropdown dropup="true" placement="right">
@ -65,32 +65,32 @@
<ul *dropdownMenu class="dropdown-menu" id="more-menu" role="menu" aria-labelledby="single-button">
<li role="menuitem">
<a class="dropdown-item" title="Download the video" href="#" (click)="showDownloadModal($event)">
<span class="icon icon-download"></span> Download
<a class="dropdown-item" i18n-title title="Download the video" href="#" (click)="showDownloadModal($event)">
<span class="icon icon-download"></span> <ng-container i18n>Download</ng-container>
</a>
</li>
<li *ngIf="isUserLoggedIn()" role="menuitem">
<a class="dropdown-item" title="Report this video" href="#" (click)="showReportModal($event)">
<span class="icon icon-alert"></span> Report
<a class="dropdown-item" i18n-title title="Report this video" href="#" (click)="showReportModal($event)">
<span class="icon icon-alert"></span> <ng-container i18n>Report</ng-container>
</a>
</li>
<li *ngIf="isVideoBlacklistable()" role="menuitem">
<a class="dropdown-item" title="Blacklist this video" href="#" (click)="blacklistVideo($event)">
<span class="icon icon-blacklist"></span> Blacklist
<a class="dropdown-item" i18n-title title="Blacklist this video" href="#" (click)="blacklistVideo($event)">
<span class="icon icon-blacklist"></span> <ng-container i18n>Blacklist</ng-container>
</a>
</li>
<li *ngIf="isVideoUpdatable()" role="menuitem">
<a class="dropdown-item" title="Update this video" href="#" [routerLink]="[ '/videos/update', video.uuid ]">
<span class="icon icon-edit"></span> Update
<a class="dropdown-item" i18n-title title="Update this video" href="#" [routerLink]="[ '/videos/update', video.uuid ]">
<span class="icon icon-edit"></span> <ng-container i18n>Update</ng-container>
</a>
</li>
<li *ngIf="isVideoRemovable()" role="menuitem">
<a class="dropdown-item" title="Delete this video" href="#" (click)="removeVideo($event)">
<span class="icon icon-blacklist"></span> Delete
<a class="dropdown-item" i18n-title title="Delete this video" href="#" (click)="removeVideo($event)">
<span class="icon icon-blacklist"></span> <ng-container i18n>Delete</ng-container>
</a>
</li>
</ul>
@ -109,20 +109,20 @@
<div class="video-info-description-html" [innerHTML]="videoHTMLDescription"></div>
<div class="video-info-description-more" *ngIf="completeDescriptionShown === false && video.description?.length >= 250" (click)="showMoreDescription()">
Show more
<ng-container i18n>Show more</ng-container>
<span *ngIf="descriptionLoading === false" class="glyphicon glyphicon-menu-down"></span>
<my-loader class="description-loading" [loading]="descriptionLoading"></my-loader>
</div>
<div *ngIf="completeDescriptionShown === true" (click)="showLessDescription()" class="video-info-description-more">
Show less
<ng-container i18n>Show less</ng-container>
<span *ngIf="descriptionLoading === false" class="glyphicon glyphicon-menu-up"></span>
</div>
</div>
<div class="video-attributes">
<div class="video-attribute">
<span class="video-attribute-label">
<span i18n class="video-attribute-label">
Privacy
</span>
<span class="video-attribute-value">
@ -131,7 +131,7 @@
</div>
<div class="video-attribute">
<span class="video-attribute-label">
<span i18n class="video-attribute-label">
Category
</span>
<span class="video-attribute-value">
@ -140,7 +140,7 @@
</div>
<div class="video-attribute">
<span class="video-attribute-label">
<span i18n class="video-attribute-label">
Licence
</span>
<span class="video-attribute-value">
@ -149,7 +149,7 @@
</div>
<div class="video-attribute">
<span class="video-attribute-label">
<span i18n class="video-attribute-label">
Language
</span>
<span class="video-attribute-value">
@ -158,7 +158,7 @@
</div>
<div class="video-attribute">
<span class="video-attribute-label">
<span i18n class="video-attribute-label">
Tags
</span>
@ -172,7 +172,7 @@
</div>
<div class="other-videos">
<div class="title-page title-page-single">
<div i18n class="title-page title-page-single">
Other videos
</div>
@ -184,13 +184,15 @@
<div class="privacy-concerns" *ngIf="hasAlreadyAcceptedPrivacyConcern === false">
<strong>Friendly Reminder:</strong>
<strong i18n>Friendly Reminder:</strong>
<div class="privacy-concerns-text">
The sharing system used by this video implies that some technical information about your system (such as a public IP address) can be accessed publicly.
<a title="Get more information" target="_blank" rel="noopener noreferrer" href="/about#p2p-privacy">More information</a>
<ng-container i18n>
The sharing system used by this video implies that some technical information about your system (such as a public IP address) can be accessed publicly.
</ng-container>
<a i18n i18n-title title="Get more information" target="_blank" rel="noopener noreferrer" href="/about#p2p-privacy">More information</a>
</div>
<div class="privacy-concerns-okay" (click)="acceptedPrivacyConcern()">
<div i18n class="privacy-concerns-okay" (click)="acceptedPrivacyConcern()">
OK
</div>
</div>

View File

@ -23,6 +23,7 @@ import { VideoReportComponent } from './modal/video-report.component'
import { VideoShareComponent } from './modal/video-share.component'
import { getVideojsOptions } from '../../../assets/player/peertube-player'
import { ServerService } from '@app/core'
import { I18n } from '@ngx-translate/i18n-polyfill'
@Component({
selector: 'my-video-watch',
@ -70,7 +71,8 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
private notificationsService: NotificationsService,
private markdownService: MarkdownService,
private zone: NgZone,
private redirectService: RedirectService
private redirectService: RedirectService,
private i18n: I18n
) {}
get user () {
@ -153,17 +155,20 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
async blacklistVideo (event: Event) {
event.preventDefault()
const res = await this.confirmService.confirm('Do you really want to blacklist this video?', 'Blacklist')
const res = await this.confirmService.confirm(this.i18n('Do you really want to blacklist this video?'), this.i18n('Blacklist'))
if (res === false) return
this.videoBlacklistService.blacklistVideo(this.video.id)
.subscribe(
status => {
this.notificationsService.success('Success', `Video ${this.video.name} had been blacklisted.`)
this.notificationsService.success(
this.i18n('Success'),
this.i18n('Video {{ videoName }} had been blacklisted.', { videoName: this.video.name })
)
this.redirectService.redirectToHomepage()
},
error => this.notificationsService.error('Error', error.message)
error => this.notificationsService.error(this.i18n('Error'), error.message)
)
}
@ -198,7 +203,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
error => {
this.descriptionLoading = false
this.notificationsService.error('Error', error.message)
this.notificationsService.error(this.i18n('Error'), error.message)
}
)
}
@ -252,19 +257,22 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
async removeVideo (event: Event) {
event.preventDefault()
const res = await this.confirmService.confirm('Do you really want to delete this video?', 'Delete')
const res = await this.confirmService.confirm(this.i18n('Do you really want to delete this video?'), this.i18n('Delete'))
if (res === false) return
this.videoService.removeVideo(this.video.id)
.subscribe(
status => {
this.notificationsService.success('Success', `Video ${this.video.name} deleted.`)
this.notificationsService.success(
this.i18n('Success'),
this.i18n('Video {{ videoName }} deleted.', { videoName: this.video.name })
)
// Go back to the video-list.
this.redirectService.redirectToHomepage()
},
error => this.notificationsService.error('Error', error.message)
error => this.notificationsService.error(this.i18n('Error'), error.message)
)
}
@ -288,7 +296,10 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
}
private setVideoLikesBarTooltipText () {
this.likesBarTooltipText = `${this.video.likes} likes / ${this.video.dislikes} dislikes`
this.likesBarTooltipText = this.i18n(
'{{ likesNumber }} likes / {{ dislikesNumber }} dislikes',
{ likesNumber: this.video.likes, dislikes: this.video.dislikes }
)
}
private handleError (err: any) {
@ -298,12 +309,12 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
let message = ''
if (errorMessage.indexOf('http error') !== -1) {
message = 'Cannot fetch video from server, maybe down.'
message = this.i18n('Cannot fetch video from server, maybe down.')
} else {
message = errorMessage
}
this.notificationsService.error('Error', message)
this.notificationsService.error(this.i18n('Error'), message)
}
private checkUserRating () {
@ -318,7 +329,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
}
},
err => this.notificationsService.error('Error', err.message)
err => this.notificationsService.error(this.i18n('Error'), err.message)
)
}
@ -333,8 +344,8 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
if (this.video.isVideoNSFWForUser(this.user, this.serverService.getConfig())) {
const res = await this.confirmService.confirm(
'This video contains mature or explicit content. Are you sure you want to watch it?',
'Mature or explicit content'
this.i18n('This video contains mature or explicit content. Are you sure you want to watch it?'),
this.i18n('Mature or explicit content')
)
if (res === false) return this.redirectService.redirectToHomepage()
}
@ -399,7 +410,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
this.updateVideoRating(this.userRating, nextRating)
this.userRating = nextRating
},
err => this.notificationsService.error('Error', err.message)
err => this.notificationsService.error(this.i18n('Error'), err.message)
)
}

View File

@ -8,6 +8,7 @@ import { AbstractVideoList } from '../../shared/video/abstract-video-list'
import { VideoSortField } from '../../shared/video/sort-field.type'
import { VideoService } from '../../shared/video/video.service'
import { VideoFilter } from '../../../../../shared/models/videos/video-query.type'
import { I18n } from '@ngx-translate/i18n-polyfill'
@Component({
selector: 'my-videos-local',
@ -15,18 +16,23 @@ import { VideoFilter } from '../../../../../shared/models/videos/video-query.typ
templateUrl: '../../shared/video/abstract-video-list.html'
})
export class VideoLocalComponent extends AbstractVideoList implements OnInit, OnDestroy {
titlePage = 'Local videos'
titlePage: string
currentRoute = '/videos/local'
sort = '-publishedAt' as VideoSortField
filter: VideoFilter = 'local'
constructor (protected router: Router,
protected route: ActivatedRoute,
protected notificationsService: NotificationsService,
protected authService: AuthService,
protected location: Location,
private videoService: VideoService) {
constructor (
protected router: Router,
protected route: ActivatedRoute,
protected notificationsService: NotificationsService,
protected authService: AuthService,
protected location: Location,
private videoService: VideoService,
private i18n: I18n
) {
super()
this.titlePage = i18n('Local videos')
}
ngOnInit () {

View File

@ -7,6 +7,7 @@ import { AuthService } from '../../core/auth'
import { AbstractVideoList } from '../../shared/video/abstract-video-list'
import { VideoSortField } from '../../shared/video/sort-field.type'
import { VideoService } from '../../shared/video/video.service'
import { I18n } from '@ngx-translate/i18n-polyfill'
@Component({
selector: 'my-videos-recently-added',
@ -14,17 +15,22 @@ import { VideoService } from '../../shared/video/video.service'
templateUrl: '../../shared/video/abstract-video-list.html'
})
export class VideoRecentlyAddedComponent extends AbstractVideoList implements OnInit, OnDestroy {
titlePage = 'Recently added'
titlePage: string
currentRoute = '/videos/recently-added'
sort: VideoSortField = '-publishedAt'
constructor (protected router: Router,
protected route: ActivatedRoute,
protected location: Location,
protected notificationsService: NotificationsService,
protected authService: AuthService,
private videoService: VideoService) {
constructor (
protected router: Router,
protected route: ActivatedRoute,
protected location: Location,
protected notificationsService: NotificationsService,
protected authService: AuthService,
private videoService: VideoService,
private i18n: I18n
) {
super()
this.titlePage = i18n('Recently added')
}
ngOnInit () {

View File

@ -8,6 +8,7 @@ import { Subscription } from 'rxjs'
import { AuthService } from '../../core/auth'
import { AbstractVideoList } from '../../shared/video/abstract-video-list'
import { VideoService } from '../../shared/video/video.service'
import { I18n } from '@ngx-translate/i18n-polyfill'
@Component({
selector: 'my-videos-search',
@ -15,7 +16,7 @@ import { VideoService } from '../../shared/video/video.service'
templateUrl: '../../shared/video/abstract-video-list.html'
})
export class VideoSearchComponent extends AbstractVideoList implements OnInit, OnDestroy {
titlePage = 'Search'
titlePage: string
currentRoute = '/videos/search'
loadOnInit = false
@ -24,15 +25,19 @@ export class VideoSearchComponent extends AbstractVideoList implements OnInit, O
}
private subActivatedRoute: Subscription
constructor (protected router: Router,
protected route: ActivatedRoute,
protected notificationsService: NotificationsService,
protected authService: AuthService,
protected location: Location,
private videoService: VideoService,
private redirectService: RedirectService
constructor (
protected router: Router,
protected route: ActivatedRoute,
protected notificationsService: NotificationsService,
protected authService: AuthService,
protected location: Location,
private videoService: VideoService,
private redirectService: RedirectService,
private i18n: I18n
) {
super()
this.titlePage = i18n('Search')
}
ngOnInit () {

View File

@ -7,6 +7,7 @@ import { AuthService } from '../../core/auth'
import { AbstractVideoList } from '../../shared/video/abstract-video-list'
import { VideoSortField } from '../../shared/video/sort-field.type'
import { VideoService } from '../../shared/video/video.service'
import { I18n } from '@ngx-translate/i18n-polyfill'
@Component({
selector: 'my-videos-trending',
@ -14,17 +15,22 @@ import { VideoService } from '../../shared/video/video.service'
templateUrl: '../../shared/video/abstract-video-list.html'
})
export class VideoTrendingComponent extends AbstractVideoList implements OnInit, OnDestroy {
titlePage = 'Trending'
titlePage: string
currentRoute = '/videos/trending'
defaultSort: VideoSortField = '-views'
constructor (protected router: Router,
protected route: ActivatedRoute,
protected notificationsService: NotificationsService,
protected authService: AuthService,
protected location: Location,
private videoService: VideoService) {
constructor (
protected router: Router,
protected route: ActivatedRoute,
protected notificationsService: NotificationsService,
protected authService: AuthService,
protected location: Location,
private videoService: VideoService,
private i18n: I18n
) {
super()
this.titlePage = i18n('Trending')
}
ngOnInit () {

View File

@ -0,0 +1,354 @@
<?xml version="1.0" encoding="UTF-8" ?>
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
<file source-language="en-US" datatype="plaintext" original="ng2.template">
<body>
<trans-unit id="298cb43759c99e11e2ca5f92c768a145ddaa323f" datatype="html">
<source>
My public profile
</source>
<context-group purpose="location">
<context context-type="sourcefile">app/menu/menu.component.ts</context>
<context context-type="linenumber">17</context>
</context-group>
</trans-unit><trans-unit id="5f60990802486b7906b422d80aace6a1b19dcc02" datatype="html">
<source>Video not found :&apos;(</source>
<context-group purpose="location">
<context context-type="sourcefile">app/videos/+video-watch/video-watch.component.ts</context>
<context context-type="linenumber">6</context>
</context-group>
</trans-unit><trans-unit id="643ab402461b1169eebbe2ed790e12a9a83551aa" datatype="html">
<source>
&lt;x id="INTERPOLATION" equiv-text="{{ video.publishedAt | myFromNow }}"/&gt; - &lt;x id="INTERPOLATION_1" equiv-text="{{ video.views | myNumberFormatter }}"/&gt; views
</source>
<context-group purpose="location">
<context context-type="sourcefile">app/videos/+video-watch/video-watch.component.ts</context>
<context context-type="linenumber">15</context>
</context-group>
</trans-unit><trans-unit id="5cb397241041f7ad70997806227bafcdf7eb1b33" datatype="html">
<source>Go the channel page</source>
<context-group purpose="location">
<context context-type="sourcefile">app/videos/+video-watch/video-watch.component.ts</context>
<context context-type="linenumber">20</context>
</context-group>
</trans-unit><trans-unit id="912f005563d20191efc188dccedd35a7c4e6b396" datatype="html">
<source>You can subscribe to this account via any ActivityPub-capable fediverse instance. For instance with Mastodon or Pleroma you can type in the search box &lt;strong&gt;@&lt;x id="INTERPOLATION" equiv-text="{{video.account.displayName}}"/&gt;@&lt;x id="INTERPOLATION_1" equiv-text="{{video.account.host}}"/&gt;&lt;/strong&gt; and subscribe there. Subscription as a PeerTube user is being worked on in &lt;a href=&apos;https://github.com/Chocobozzz/PeerTube/issues/470&apos;&gt;#470&lt;/a&gt;.</source>
<context-group purpose="location">
<context context-type="sourcefile">app/videos/+video-watch/video-watch.component.ts</context>
<context context-type="linenumber">24</context>
</context-group>
</trans-unit><trans-unit id="ccc07df383b7a32be3e2e105faa5488caf261c1c" datatype="html">
<source>By &lt;x id="INTERPOLATION" equiv-text="{{ video.by }}"/&gt;</source>
<context-group purpose="location">
<context context-type="sourcefile">app/videos/+video-watch/video-watch.component.ts</context>
<context context-type="linenumber">29</context>
</context-group>
</trans-unit><trans-unit id="e88300c71e0cb0f346d5a72eb37c920f2aadae8a" datatype="html">
<source>Go the account page</source>
<context-group purpose="location">
<context context-type="sourcefile">app/videos/+video-watch/video-watch.component.ts</context>
<context context-type="linenumber">28</context>
</context-group>
</trans-unit><trans-unit id="82b59049f3f89d900c98da9319e156dd513e3ced" datatype="html">
<source>Like this video</source>
<context-group purpose="location">
<context context-type="sourcefile">app/videos/+video-watch/video-watch.component.ts</context>
<context context-type="linenumber">41</context>
</context-group>
</trans-unit><trans-unit id="623698f075025b2b2fc2e0c59fd95f4f4662a509" datatype="html">
<source>Dislike this video</source>
<context-group purpose="location">
<context context-type="sourcefile">app/videos/+video-watch/video-watch.component.ts</context>
<context context-type="linenumber">48</context>
</context-group>
</trans-unit><trans-unit id="b5629d298ff1a69b8db19a4ba2995c76b52da604" datatype="html">
<source>Support</source>
<context-group purpose="location">
<context context-type="sourcefile">app/videos/+video-watch/video-watch.component.ts</context>
<context context-type="linenumber">53</context>
</context-group>
</trans-unit><trans-unit id="0bd8b27f60a1f098a53e06328426d818e3508ff9" datatype="html">
<source>Share</source>
<context-group purpose="location">
<context context-type="sourcefile">app/videos/+video-watch/video-watch.component.ts</context>
<context context-type="linenumber">58</context>
</context-group>
</trans-unit><trans-unit id="dc75033a5238fdc4f462212c847a45ba8018a3fd" datatype="html">
<source>Download</source>
<context-group purpose="location">
<context context-type="sourcefile">app/videos/+video-watch/video-watch.component.ts</context>
<context context-type="linenumber">69</context>
</context-group>
</trans-unit><trans-unit id="144fff5c40b85414d59e644d8dee7cfefba925a2" datatype="html">
<source>Download the video</source>
<context-group purpose="location">
<context context-type="sourcefile">app/videos/+video-watch/video-watch.component.ts</context>
<context context-type="linenumber">68</context>
</context-group>
</trans-unit><trans-unit id="f72992030f134408b675152c397f9d0ec00f3b2a" datatype="html">
<source>Report</source>
<context-group purpose="location">
<context context-type="sourcefile">app/videos/+video-watch/video-watch.component.ts</context>
<context context-type="linenumber">75</context>
</context-group>
</trans-unit><trans-unit id="2f4894617d9c44010f87473e583bd4604b7d6ecf" datatype="html">
<source>Report this video</source>
<context-group purpose="location">
<context context-type="sourcefile">app/videos/+video-watch/video-watch.component.ts</context>
<context context-type="linenumber">74</context>
</context-group>
</trans-unit><trans-unit id="007ab5fa2aae8a7372307d3fc45a2dbcb11ffd61" datatype="html">
<source>Blacklist</source>
<context-group purpose="location">
<context context-type="sourcefile">app/videos/+video-watch/video-watch.component.ts</context>
<context context-type="linenumber">81</context>
</context-group>
</trans-unit><trans-unit id="803c6317abd2dbafcc93226c4e273c62932e3037" datatype="html">
<source>Blacklist this video</source>
<context-group purpose="location">
<context context-type="sourcefile">app/videos/+video-watch/video-watch.component.ts</context>
<context context-type="linenumber">80</context>
</context-group>
</trans-unit><trans-unit id="047f50bc5b5d17b5bec0196355953e1a5c590ddb" datatype="html">
<source>Update</source>
<context-group purpose="location">
<context context-type="sourcefile">app/videos/+video-watch/video-watch.component.ts</context>
<context context-type="linenumber">87</context>
</context-group>
</trans-unit><trans-unit id="cd27f761b923a5bdb16ba9844da632edd878f1b1" datatype="html">
<source>Update this video</source>
<context-group purpose="location">
<context context-type="sourcefile">app/videos/+video-watch/video-watch.component.ts</context>
<context context-type="linenumber">86</context>
</context-group>
</trans-unit><trans-unit id="826b25211922a1b46436589233cb6f1a163d89b7" datatype="html">
<source>Delete</source>
<context-group purpose="location">
<context context-type="sourcefile">app/videos/+video-watch/video-watch.component.ts</context>
<context context-type="linenumber">93</context>
</context-group>
</trans-unit><trans-unit id="3dbfdc68f83d91cb360172eb65578cae94e7cbe5" datatype="html">
<source>Delete this video</source>
<context-group purpose="location">
<context context-type="sourcefile">app/videos/+video-watch/video-watch.component.ts</context>
<context context-type="linenumber">92</context>
</context-group>
</trans-unit><trans-unit id="f0c5f6f270e70cbe063b5368fcf48f9afc1abd9b" datatype="html">
<source>Show more</source>
<context-group purpose="location">
<context context-type="sourcefile">app/videos/+video-watch/video-watch.component.ts</context>
<context context-type="linenumber">112</context>
</context-group>
</trans-unit><trans-unit id="5403a767248e304199592271bba3366d2ca3f903" datatype="html">
<source>Show less</source>
<context-group purpose="location">
<context context-type="sourcefile">app/videos/+video-watch/video-watch.component.ts</context>
<context context-type="linenumber">118</context>
</context-group>
</trans-unit><trans-unit id="8057a9b7f9e908ff350edfd71417b96c174e5911" datatype="html">
<source>
Privacy
</source>
<context-group purpose="location">
<context context-type="sourcefile">app/videos/+video-watch/video-watch.component.ts</context>
<context context-type="linenumber">125</context>
</context-group>
</trans-unit><trans-unit id="bd407eca607a8905a26a9e30c9d0cd70f4465db8" datatype="html">
<source>
Category
</source>
<context-group purpose="location">
<context context-type="sourcefile">app/videos/+video-watch/video-watch.component.ts</context>
<context context-type="linenumber">134</context>
</context-group>
</trans-unit><trans-unit id="af5072bd79ea3cd767ab74a6622d2eee791b3832" datatype="html">
<source>
Licence
</source>
<context-group purpose="location">
<context context-type="sourcefile">app/videos/+video-watch/video-watch.component.ts</context>
<context context-type="linenumber">143</context>
</context-group>
</trans-unit><trans-unit id="a911eee019174741b0aec6fcf3fbd5752fab3e67" datatype="html">
<source>
Language
</source>
<context-group purpose="location">
<context context-type="sourcefile">app/videos/+video-watch/video-watch.component.ts</context>
<context context-type="linenumber">152</context>
</context-group>
</trans-unit><trans-unit id="ecf7007c2842cc26a7b91d08d48c7a4f5f749fb3" datatype="html">
<source>
Tags
</source>
<context-group purpose="location">
<context context-type="sourcefile">app/videos/+video-watch/video-watch.component.ts</context>
<context context-type="linenumber">161</context>
</context-group>
</trans-unit><trans-unit id="7ce8b0d7cc34d4c1ef4a21e990b0a001337bedd1" datatype="html">
<source>
Other videos
</source>
<context-group purpose="location">
<context context-type="sourcefile">app/videos/+video-watch/video-watch.component.ts</context>
<context context-type="linenumber">175</context>
</context-group>
</trans-unit><trans-unit id="fb779d2b25c4d0ffa7d52c823a240717e8c1fe6c" datatype="html">
<source>Friendly Reminder:</source>
<context-group purpose="location">
<context context-type="sourcefile">app/videos/+video-watch/video-watch.component.ts</context>
<context context-type="linenumber">187</context>
</context-group>
</trans-unit><trans-unit id="4c2fca29fd9d7e85abe85a206958a4226f403be2" datatype="html">
<source>
The sharing system used by this video implies that some technical information about your system (such as a public IP address) can be accessed publicly.
</source>
<context-group purpose="location">
<context context-type="sourcefile">app/videos/+video-watch/video-watch.component.ts</context>
<context context-type="linenumber">189</context>
</context-group>
</trans-unit><trans-unit id="e60c11e1b1dfbbeda577364b8de39ded2d796c5e" datatype="html">
<source>More information</source>
<context-group purpose="location">
<context context-type="sourcefile">app/videos/+video-watch/video-watch.component.ts</context>
<context context-type="linenumber">192</context>
</context-group>
</trans-unit><trans-unit id="bd499ca7913bb5408fd139a4cb4f863852d5f318" datatype="html">
<source>Get more information</source>
<context-group purpose="location">
<context context-type="sourcefile">app/videos/+video-watch/video-watch.component.ts</context>
<context context-type="linenumber">192</context>
</context-group>
</trans-unit><trans-unit id="20fc98888baf65b5ba9fe9622dc036fa8dec6a5f" datatype="html">
<source>
OK
</source>
<context-group purpose="location">
<context context-type="sourcefile">app/videos/+video-watch/video-watch.component.ts</context>
<context context-type="linenumber">195</context>
</context-group>
</trans-unit>
<trans-unit id="23b2c2f4dd69e29c3bff00469e259dcb01de5633" datatype="html">
<source>Do you really want to blacklist this video?</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/videos/+video-watch/video-watch.component.ts</context>
<context context-type="linenumber">1</context>
</context-group>
</trans-unit>
<trans-unit id="1e035e6ccfab771cad4226b2ad230cb0d4a88cba" datatype="html">
<source>Success</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/videos/+video-watch/video-watch.component.ts</context>
<context context-type="linenumber">1</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/videos/+video-watch/video-watch.component.ts</context>
<context context-type="linenumber">1</context>
</context-group>
</trans-unit>
<trans-unit id="085d56464b75ae5c1e370f5290e4c4cf23961a61" datatype="html">
<source>Video &lt;x id="INTERPOLATION" equiv-text="{{ videoName }}"/&gt; had been blacklisted.</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/videos/+video-watch/video-watch.component.ts</context>
<context context-type="linenumber">1</context>
</context-group>
</trans-unit>
<trans-unit id="6080b77234e92ad41bb52653b239c4c4f851317d" datatype="html">
<source>Error</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/videos/+video-watch/video-watch.component.ts</context>
<context context-type="linenumber">1</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/videos/+video-watch/video-watch.component.ts</context>
<context context-type="linenumber">1</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/videos/+video-watch/video-watch.component.ts</context>
<context context-type="linenumber">1</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/videos/+video-watch/video-watch.component.ts</context>
<context context-type="linenumber">1</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/videos/+video-watch/video-watch.component.ts</context>
<context context-type="linenumber">1</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/videos/+video-watch/video-watch.component.ts</context>
<context context-type="linenumber">1</context>
</context-group>
</trans-unit>
<trans-unit id="f1abd89c9280323209e939fa9c30f6e5cda20c95" datatype="html">
<source>Do you really want to delete this video?</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/videos/+video-watch/video-watch.component.ts</context>
<context context-type="linenumber">1</context>
</context-group>
</trans-unit>
<trans-unit id="007c1d7080cf6da1ac264b23705246f0c53e3114" datatype="html">
<source>Video &lt;x id="INTERPOLATION" equiv-text="{{ videoName }}"/&gt; deleted.</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/videos/+video-watch/video-watch.component.ts</context>
<context context-type="linenumber">1</context>
</context-group>
</trans-unit>
<trans-unit id="cf9a064824f2fa3f01fd5544ad21032e33e60dca" datatype="html">
<source>&lt;x id="INTERPOLATION" equiv-text="{{ likesNumber }}"/&gt; likes / &lt;x id="INTERPOLATION_1" equiv-text="{{ dislikesNumber }}"/&gt; dislikes</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/videos/+video-watch/video-watch.component.ts</context>
<context context-type="linenumber">1</context>
</context-group>
</trans-unit>
<trans-unit id="4a400b174208188dcb46f2c23f4af9accfabaa3f" datatype="html">
<source>Cannot fetch video from server, maybe down.</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/videos/+video-watch/video-watch.component.ts</context>
<context context-type="linenumber">1</context>
</context-group>
</trans-unit>
<trans-unit id="ed013c2c29216501c688e9cb5f3a1c9fd9147b71" datatype="html">
<source>This video contains mature or explicit content. Are you sure you want to watch it?</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/videos/+video-watch/video-watch.component.ts</context>
<context context-type="linenumber">1</context>
</context-group>
</trans-unit>
<trans-unit id="5ba3d522e4146eefcbd5c222247c1e2423d27cd8" datatype="html">
<source>Mature or explicit content</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/videos/+video-watch/video-watch.component.ts</context>
<context context-type="linenumber">1</context>
</context-group>
</trans-unit>
<trans-unit id="b6307f83d9f43bff8d5129a7888e89964ddc3f7f" datatype="html">
<source>Local videos</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/videos/video-list/video-local.component.ts</context>
<context context-type="linenumber">1</context>
</context-group>
</trans-unit>
<trans-unit id="8d20c5f5dd30acbe71316544dab774393fd9c3c1" datatype="html">
<source>Recently added</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/videos/video-list/video-recently-added.component.ts</context>
<context context-type="linenumber">1</context>
</context-group>
</trans-unit>
<trans-unit id="7e892ba15f2c6c17e83510e273b3e10fc32ea016" datatype="html">
<source>Search</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/videos/video-list/video-search.component.ts</context>
<context context-type="linenumber">1</context>
</context-group>
</trans-unit>
<trans-unit id="b6b7986bc3721ac483baf20bc9a320529075c807" datatype="html">
<source>Trending</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/videos/video-list/video-trending.component.ts</context>
<context context-type="linenumber">1</context>
</context-group>
</trans-unit>
</body>
</file>
</xliff>

View File

@ -0,0 +1,191 @@
<?xml version="1.0" encoding="utf-8"?>
<!--XLIFF document generated by Zanata. Visit http://zanata.org for more infomation.-->
<xliff xmlns="urn:oasis:names:tc:xliff:document:1.1" xmlns:xyz="urn:appInfo:Items" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="urn:oasis:names:tc:xliff:document:1.1 http://www.oasis-open.org/committees/xliff/documents/xliff-core-1.1.xsd" version="1.1">
<file source-language="en-US" datatype="plaintext" original="" target-language="fr">
<body>
<trans-unit id="298cb43759c99e11e2ca5f92c768a145ddaa323f">
<source>
My public profile
</source>
<target>Mon profile public</target>
<context-group name="null">
<context context-type="linenumber">17</context>
</context-group>
</trans-unit>
<trans-unit id="5f60990802486b7906b422d80aace6a1b19dcc02">
<source>Video not found :'(</source>
<target>Vidéo non trouvée :'(</target>
<context-group name="null">
<context context-type="linenumber">6</context>
</context-group>
</trans-unit>
<trans-unit id="643ab402461b1169eebbe2ed790e12a9a83551aa">
<source>
<x id="INTERPOLATION" equiv-text="{{ video.publishedAt | myFromNow }}"/> - <x id="INTERPOLATION_1" equiv-text="{{ video.views | myNumberFormatter }}"/> views
</source>
<target>
<x id="INTERPOLATION" equiv-text="{{ video.publishedAt | myFromNow }}"/> - <x id="INTERPOLATION_1" equiv-text="{{ video.views | myNumberFormatter }}"/> vues </target>
<context-group name="null">
<context context-type="linenumber">15</context>
</context-group>
</trans-unit>
<trans-unit id="912f005563d20191efc188dccedd35a7c4e6b396">
<source>You can subscribe to this account via any ActivityPub-capable fediverse instance. For instance with Mastodon or Pleroma you can type in the search box &lt;strong&gt;@<x id="INTERPOLATION" equiv-text="{{video.account.displayName}}"/>@<x id="INTERPOLATION_1" equiv-text="{{video.account.host}}"/>&lt;/strong&gt; and subscribe there. Subscription as a PeerTube user is being worked on in &lt;a href='https://github.com/Chocobozzz/PeerTube/issues/470'&gt;#470&lt;/a&gt;.</source>
<target>You can subscribe to this account via any ActivityPub-capable fediverse instance. For instance with Mastodon or Pleroma you can type in the search box &lt;strong&gt;@<x id="INTERPOLATION" equiv-text="{{video.account.displayName}}"/>@<x id="INTERPOLATION_1" equiv-text="{{video.account.host}}"/>&lt;/strong&gt; and subscribe there. Subscription as a PeerTube user is being worked on in &lt;a href='https://github.com/Chocobozzz/PeerTube/issues/470'&gt;#470&lt;/a&gt;.</target>
<context-group name="null">
<context context-type="linenumber">24</context>
</context-group>
</trans-unit>
<trans-unit id="ccc07df383b7a32be3e2e105faa5488caf261c1c">
<source>By <x id="INTERPOLATION" equiv-text="{{ video.by }}"/></source>
<target>Par <x id="INTERPOLATION" equiv-text="{{ video.by }}"/></target>
<context-group name="null">
<context context-type="linenumber">29</context>
</context-group>
</trans-unit>
<trans-unit id="e88300c71e0cb0f346d5a72eb37c920f2aadae8a">
<source>Go the account page</source>
<target>Aller sur la page du compte</target>
<context-group name="null">
<context context-type="linenumber">28</context>
</context-group>
</trans-unit>
<trans-unit id="82b59049f3f89d900c98da9319e156dd513e3ced">
<source>Like this video</source>
<target>J'aime cette vidéo</target>
<context-group name="null">
<context context-type="linenumber">41</context>
</context-group>
</trans-unit>
<trans-unit id="623698f075025b2b2fc2e0c59fd95f4f4662a509">
<source>Dislike this video</source>
<target>Je n'aime pas cette vidéo</target>
<context-group name="null">
<context context-type="linenumber">48</context>
</context-group>
</trans-unit>
<trans-unit id="b5629d298ff1a69b8db19a4ba2995c76b52da604">
<source>Support</source>
<target>Supporter</target>
<context-group name="null">
<context context-type="linenumber">53</context>
</context-group>
</trans-unit>
<trans-unit id="0bd8b27f60a1f098a53e06328426d818e3508ff9">
<source>Share</source>
<target>Partager</target>
<context-group name="null">
<context context-type="linenumber">58</context>
</context-group>
</trans-unit>
<trans-unit id="dc75033a5238fdc4f462212c847a45ba8018a3fd">
<source>Download</source>
<target>Télécharger</target>
<context-group name="null">
<context context-type="linenumber">69</context>
</context-group>
</trans-unit>
<trans-unit id="144fff5c40b85414d59e644d8dee7cfefba925a2">
<source>Download the video</source>
<target>Télécharger la vidéo</target>
<context-group name="null">
<context context-type="linenumber">68</context>
</context-group>
</trans-unit>
<trans-unit id="f72992030f134408b675152c397f9d0ec00f3b2a">
<source>Report</source>
<target>Signaler</target>
<context-group name="null">
<context context-type="linenumber">75</context>
</context-group>
</trans-unit>
<trans-unit id="2f4894617d9c44010f87473e583bd4604b7d6ecf">
<source>Report this video</source>
<target>Signaler cette vidéo</target>
<context-group name="null">
<context context-type="linenumber">74</context>
</context-group>
</trans-unit>
<trans-unit id="007ab5fa2aae8a7372307d3fc45a2dbcb11ffd61">
<source>Blacklist</source>
<target>Blacklister</target>
<context-group name="null">
<context context-type="linenumber">81</context>
</context-group>
</trans-unit>
<trans-unit id="803c6317abd2dbafcc93226c4e273c62932e3037">
<source>Blacklist this video</source>
<target>Blacklister cette vidéo</target>
<context-group name="null">
<context context-type="linenumber">80</context>
</context-group>
</trans-unit>
<trans-unit id="047f50bc5b5d17b5bec0196355953e1a5c590ddb">
<source>Update</source>
<target>Mettre à jour</target>
<context-group name="null">
<context context-type="linenumber">87</context>
</context-group>
</trans-unit>
<trans-unit id="cd27f761b923a5bdb16ba9844da632edd878f1b1">
<source>Update this video</source>
<target>Mettre à jour cette vidéo</target>
<context-group name="null">
<context context-type="linenumber">86</context>
</context-group>
</trans-unit>
<trans-unit id="826b25211922a1b46436589233cb6f1a163d89b7">
<source>Delete</source>
<target>Supprimer</target>
<context-group name="null">
<context context-type="linenumber">93</context>
</context-group>
</trans-unit>
<trans-unit id="3dbfdc68f83d91cb360172eb65578cae94e7cbe5">
<source>Delete this video</source>
<target>Supprimer cette vidéo</target>
<context-group name="null">
<context context-type="linenumber">92</context>
</context-group>
</trans-unit>
<trans-unit id="f0c5f6f270e70cbe063b5368fcf48f9afc1abd9b">
<source>Show more</source>
<target>Montrer plus</target>
<context-group name="null">
<context context-type="linenumber">112</context>
</context-group>
</trans-unit>
<trans-unit id="5403a767248e304199592271bba3366d2ca3f903">
<source>Show less</source>
<target>Montrer moins</target>
<context-group name="null">
<context context-type="linenumber">118</context>
</context-group>
</trans-unit>
<trans-unit id="8057a9b7f9e908ff350edfd71417b96c174e5911">
<source>
Privacy
</source>
<target>Visibilité</target>
<context-group name="null">
<context context-type="linenumber">125</context>
</context-group>
</trans-unit>
<trans-unit id="bd407eca607a8905a26a9e30c9d0cd70f4465db8">
<source>
Category
</source>
<target>Catégorie</target>
<context-group name="null">
<context context-type="linenumber">134</context>
</context-group>
</trans-unit>
<trans-unit id="b6b7986bc3721ac483baf20bc9a320529075c807">
<source>Trending</source>
<target>Tendances</target>
<context-group name="null">
<context context-type="linenumber">1</context>
</context-group>
</trans-unit>
</body>
</file></xliff>

View File

@ -242,6 +242,14 @@
dependencies:
tslib "~1.9.0"
"@ngx-translate/i18n-polyfill@^1.0.0":
version "1.0.0"
resolved "https://registry.yarnpkg.com/@ngx-translate/i18n-polyfill/-/i18n-polyfill-1.0.0.tgz#145edb28bcfc1332e1bc25279eadf9d4ed0a20f8"
dependencies:
glob "7.1.2"
tslib "^1.9.0"
yargs "10.0.3"
"@nodelib/fs.stat@^1.0.1":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-1.1.0.tgz#50c1e2260ac0ed9439a181de3725a0168d59c48a"
@ -4189,6 +4197,17 @@ glob@7.0.x:
once "^1.3.0"
path-is-absolute "^1.0.0"
glob@7.1.2, glob@^7.0.0, glob@^7.0.3, glob@^7.0.5, glob@^7.0.6, glob@^7.1.1, glob@^7.1.2, glob@~7.1.1:
version "7.1.2"
resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15"
dependencies:
fs.realpath "^1.0.0"
inflight "^1.0.4"
inherits "2"
minimatch "^3.0.4"
once "^1.3.0"
path-is-absolute "^1.0.0"
glob@^5.0.15:
version "5.0.15"
resolved "https://registry.yarnpkg.com/glob/-/glob-5.0.15.tgz#1bc936b9e02f4a603fcc222ecf7633d30b8b93b1"
@ -4209,17 +4228,6 @@ glob@^6.0.4:
once "^1.3.0"
path-is-absolute "^1.0.0"
glob@^7.0.0, glob@^7.0.3, glob@^7.0.5, glob@^7.0.6, glob@^7.1.1, glob@^7.1.2, glob@~7.1.1:
version "7.1.2"
resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15"
dependencies:
fs.realpath "^1.0.0"
inflight "^1.0.4"
inherits "2"
minimatch "^3.0.4"
once "^1.3.0"
path-is-absolute "^1.0.0"
global-modules@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/global-modules/-/global-modules-1.0.0.tgz#6d770f0eb523ac78164d72b5e71a8877265cc3ea"
@ -10594,12 +10602,35 @@ yargs-parser@^7.0.0:
dependencies:
camelcase "^4.1.0"
yargs-parser@^8.0.0:
version "8.1.0"
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-8.1.0.tgz#f1376a33b6629a5d063782944da732631e966950"
dependencies:
camelcase "^4.1.0"
yargs-parser@^9.0.2:
version "9.0.2"
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-9.0.2.tgz#9ccf6a43460fe4ed40a9bb68f48d43b8a68cc077"
dependencies:
camelcase "^4.1.0"
yargs@10.0.3:
version "10.0.3"
resolved "https://registry.yarnpkg.com/yargs/-/yargs-10.0.3.tgz#6542debd9080ad517ec5048fb454efe9e4d4aaae"
dependencies:
cliui "^3.2.0"
decamelize "^1.1.1"
find-up "^2.1.0"
get-caller-file "^1.0.1"
os-locale "^2.0.0"
require-directory "^2.1.1"
require-main-filename "^1.0.1"
set-blocking "^2.0.0"
string-width "^2.0.0"
which-module "^2.0.0"
y18n "^3.2.1"
yargs-parser "^8.0.0"
yargs@11.0.0:
version "11.0.0"
resolved "https://registry.yarnpkg.com/yargs/-/yargs-11.0.0.tgz#c052931006c5eee74610e5fc0354bedfd08a201b"

View File

@ -29,6 +29,7 @@
"danger:clean:dev": "scripty",
"danger:clean:prod": "scripty",
"danger:clean:modules": "scripty",
"i18n:generate": "scripty",
"reset-password": "node ./dist/scripts/reset-password.js",
"play": "scripty",
"dev": "scripty",

View File

@ -6,5 +6,19 @@ cd client
rm -rf ./dist ./compiled
npm run ng build -- --prod --stats-json
defaultLanguage="en-US"
npm run ng build -- --output-path "dist/$defaultLanguage/" --deploy-url "/client/$defaultLanguage/" --prod --stats-json
mv "./dist/$defaultLanguage/assets" "./dist"
languages="fr"
for lang in "$languages"; do
npm run ng build -- --prod --i18n-file "./src/locale/target/messages_$lang.xml" --i18n-format xlf --i18n-locale "$lang" \
--output-path "dist/$lang/" --deploy-url "/client/$lang/"
# Do no duplicate assets
rm -r "./dist/$lang/assets"
done
NODE_ENV=production npm run webpack -- --config webpack/webpack.video-embed.js --mode production

11
scripts/i18n/generate.sh Executable file
View File

@ -0,0 +1,11 @@
#!/bin/sh
set -eu
cd client
npm run ng -- xi18n --i18n-locale "en-US" --output-path locale/source --out-file messages_en_US.xml
npm run ngx-extractor -- --locale "en-US" -i 'src/**/*.ts' -f xlf -o src/locale/source/messages_en_US.xml
# Zanata does not support inner elements in <source>, so we hack these special elements
# This regex translate the Angular elements to special entities (that we will reconvert on pull)
sed -i 's/<x id=\([^\/]\+\?\)\/>/\&lt;x id=\1\/\&gt;/g' src/locale/source/messages_en_US.xml

7
scripts/i18n/pull-hook.sh Executable file
View File

@ -0,0 +1,7 @@
#!/bin/sh
set -eu
# Zanata does not support inner elements in <source>, so we hack these special elements
# This regex translate the converted elements to initial Angular elements
sed -i 's/\&lt;x id=\([^\/]\+\?\)\/\&gt;/<x id=\1\/>/g' client/src/locale/target/*

View File

@ -57,7 +57,7 @@ git commit package.json client/package.json -m "Bumped to version $version"
git tag -s -a "$version" -m "$version"
npm run build
rm "./client/dist/stats.json"
rm "./client/dist/en-US/stats.json"
# Creating the archives
(

View File

@ -12,7 +12,6 @@ import * as bodyParser from 'body-parser'
import * as express from 'express'
import * as http from 'http'
import * as morgan from 'morgan'
import * as path from 'path'
import * as bitTorrentTracker from 'bittorrent-tracker'
import * as cors from 'cors'
import { Server as WebSocketServer } from 'ws'
@ -156,20 +155,11 @@ app.use('/', activityPubRouter)
app.use('/', feedsRouter)
app.use('/', webfingerRouter)
// Client files
app.use('/', clientsRouter)
// Static files
app.use('/', staticRouter)
// Always serve index client page (the client is a single page application, let it handle routing)
app.use('/*', function (req, res) {
if (req.accepts(ACCEPT_HEADERS) === 'html') {
return res.sendFile(path.join(__dirname, '../client/dist/index.html'))
}
return res.status(404).end()
})
// Client files, last valid routes!
app.use('/', clientsRouter)
// ----------- Errors -----------

View File

@ -3,17 +3,24 @@ import * as express from 'express'
import { join } from 'path'
import * as validator from 'validator'
import { escapeHTML, readFileBufferPromise, root } from '../helpers/core-utils'
import { CONFIG, EMBED_SIZE, OPENGRAPH_AND_OEMBED_COMMENT, STATIC_MAX_AGE, STATIC_PATHS } from '../initializers'
import {
ACCEPT_HEADERS,
CONFIG,
EMBED_SIZE,
OPENGRAPH_AND_OEMBED_COMMENT,
STATIC_MAX_AGE,
STATIC_PATHS
} from '../initializers'
import { asyncMiddleware } from '../middlewares'
import { VideoModel } from '../models/video/video'
import { VideoPrivacy } from '../../shared/models/videos'
import { I18N_LOCALES, is18nLocale, getDefaultLocale } from '../../shared/models'
const clientsRouter = express.Router()
const distPath = join(root(), 'client', 'dist')
const assetsImagesPath = join(root(), 'client', 'dist', 'assets', 'images')
const embedPath = join(distPath, 'standalone', 'videos', 'embed.html')
const indexPath = join(distPath, 'index.html')
// Special route that add OpenGraph and oEmbed tags
// Do not use a template engine for a so little thing
@ -45,6 +52,16 @@ clientsRouter.use('/client/*', (req: express.Request, res: express.Response, nex
res.sendStatus(404)
})
// Always serve index client page (the client is a single page application, let it handle routing)
// Try to provide the right language index.html
clientsRouter.use('/(:language)?', function (req, res) {
if (req.accepts(ACCEPT_HEADERS) === 'html') {
return res.sendFile(getIndexPath(req, req.params.language))
}
return res.status(404).end()
})
// ---------------------------------------------------------------------------
export {
@ -53,6 +70,19 @@ export {
// ---------------------------------------------------------------------------
function getIndexPath (req: express.Request, paramLang?: string) {
let lang: string
// Check param lang validity
if (paramLang && is18nLocale(paramLang)) {
lang = paramLang
} else {
lang = req.acceptsLanguages(Object.keys(I18N_LOCALES)) || getDefaultLocale()
}
return join(__dirname, '../../../client/dist/' + lang + '/index.html')
}
function addOpenGraphAndOEmbedTags (htmlStringPage: string, video: VideoModel) {
const previewUrl = CONFIG.WEBSERVER.URL + STATIC_PATHS.PREVIEWS + video.getPreviewName()
const videoUrl = CONFIG.WEBSERVER.URL + '/videos/watch/' + video.uuid
@ -142,18 +172,18 @@ async function generateWatchHtmlPage (req: express.Request, res: express.Respons
} else if (validator.isInt(videoId)) {
videoPromise = VideoModel.loadAndPopulateAccountAndServerAndTags(+videoId)
} else {
return res.sendFile(indexPath)
return res.sendFile(getIndexPath(req))
}
let [ file, video ] = await Promise.all([
readFileBufferPromise(indexPath),
readFileBufferPromise(getIndexPath(req)),
videoPromise
])
const html = file.toString()
// Let Angular application handle errors
if (!video || video.privacy === VideoPrivacy.PRIVATE) return res.sendFile(indexPath)
if (!video || video.privacy === VideoPrivacy.PRIVATE) return res.sendFile(getIndexPath(req))
const htmlStringPageWithTags = addOpenGraphAndOEmbedTags(html, video)
res.set('Content-Type', 'text/html; charset=UTF-8').send(htmlStringPageWithTags)

View File

@ -0,0 +1,30 @@
export const I18N_LOCALES = {
'en-US': 'English (US)',
fr: 'French'
}
export function getDefaultLocale () {
return 'en-US'
}
const possiblePaths = Object.keys(I18N_LOCALES).map(l => '/' + l)
export function is18nPath (path: string) {
return possiblePaths.indexOf(path) !== -1
}
const possibleLanguages = Object.keys(I18N_LOCALES)
export function is18nLocale (locale: string) {
return possibleLanguages.indexOf(locale) !== -1
}
// Only use in dev mode, so relax
// In production, the locale always match with a I18N_LANGUAGES key
export function buildFileLocale (locale: string) {
if (!is18nLocale(locale)) {
// Some working examples for development purpose
if (locale.split('-')[ 0 ] === 'en') return 'en_US'
else if (locale === 'fr') return 'fr'
}
return locale.replace('-', '_')
}

View File

@ -0,0 +1 @@
export * from './i18n'

View File

@ -3,6 +3,7 @@ export * from './activitypub'
export * from './users'
export * from './videos'
export * from './feeds'
export * from './i18n'
export * from './server/job.model'
export * from './oauth-client-local.model'
export * from './result-list.model'

View File

@ -1237,7 +1237,7 @@ chalk@^1.0.0, chalk@^1.1.1, chalk@^1.1.3:
strip-ansi "^3.0.0"
supports-color "^2.0.0"
chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0, chalk@^2.3.0, chalk@^2.3.1, chalk@^2.3.2:
chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0, chalk@^2.3.0, chalk@^2.3.1:
version "2.4.1"
resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.1.tgz#18c49ab16a037b6eb0152cc83e3471338215b66e"
dependencies:
@ -1517,7 +1517,7 @@ command-exists@^1.2.2:
version "1.2.6"
resolved "https://registry.yarnpkg.com/command-exists/-/command-exists-1.2.6.tgz#577f8e5feb0cb0f159cd557a51a9be1bdd76e09e"
commander@*, commander@2.15.1, commander@^2.12.1, commander@^2.13.0, commander@^2.14.1, commander@^2.15.1, commander@^2.8.1, commander@^2.9.0:
commander@*, commander@2.15.1, commander@^2.12.1, commander@^2.13.0, commander@^2.14.1, commander@^2.8.1, commander@^2.9.0:
version "2.15.1"
resolved "https://registry.yarnpkg.com/commander/-/commander-2.15.1.tgz#df46e867d0fc2aec66a34662b406a9ccafff5b0f"
@ -2175,12 +2175,6 @@ error-ex@^1.2.0, error-ex@^1.3.1:
dependencies:
is-arrayish "^0.2.1"
error-stack-parser@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/error-stack-parser/-/error-stack-parser-2.0.1.tgz#a3202b8fb03114aa9b40a0e3669e48b2b65a010a"
dependencies:
stackframe "^1.0.3"
es5-ext@^0.10.14, es5-ext@^0.10.35, es5-ext@^0.10.9, es5-ext@~0.10.14:
version "0.10.43"
resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.43.tgz#c705e645253210233a270869aa463a2333b7ca64"
@ -4155,7 +4149,7 @@ js-tokens@^3.0.0, js-tokens@^3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b"
js-yaml@^3.11.0, js-yaml@^3.4.6, js-yaml@^3.5.1, js-yaml@^3.5.4, js-yaml@^3.7.0, js-yaml@^3.8.2, js-yaml@^3.8.3, js-yaml@^3.9.0:
js-yaml@^3.4.6, js-yaml@^3.5.1, js-yaml@^3.5.4, js-yaml@^3.7.0, js-yaml@^3.8.2, js-yaml@^3.8.3, js-yaml@^3.9.0:
version "3.11.0"
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.11.0.tgz#597c1a8bd57152f26d622ce4117851a51f5ebaef"
dependencies:
@ -6741,18 +6735,6 @@ sass-graph@^2.2.4:
scss-tokenizer "^0.2.3"
yargs "^7.0.0"
sass-lint-auto-fix@^0.9.0:
version "0.9.2"
resolved "https://registry.yarnpkg.com/sass-lint-auto-fix/-/sass-lint-auto-fix-0.9.2.tgz#b8b6eb95644f7919dfea33d04c1fc19ae8f07a11"
dependencies:
chalk "^2.3.2"
commander "^2.15.1"
glob "^7.1.2"
gonzales-pe-sl "^4.2.3"
js-yaml "^3.11.0"
sass-lint "^1.12.1"
stacktrace-js "^2.0.0"
sass-lint@^1.12.1:
version "1.12.1"
resolved "https://registry.yarnpkg.com/sass-lint/-/sass-lint-1.12.1.tgz#630f69c216aa206b8232fb2aa907bdf3336b6d83"
@ -7194,10 +7176,6 @@ source-map@0.4.x, source-map@^0.4.2, source-map@^0.4.4:
dependencies:
amdefine ">=0.0.4"
source-map@0.5.6:
version "0.5.6"
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.6.tgz#75ce38f52bf0733c5a7f0c118d81334a2bb5f412"
source-map@0.5.x, source-map@^0.5.6, source-map@~0.5.1:
version "0.5.7"
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc"
@ -7315,12 +7293,6 @@ stack-chain@1.3.x, stack-chain@~1.3.1:
version "1.3.7"
resolved "https://registry.yarnpkg.com/stack-chain/-/stack-chain-1.3.7.tgz#d192c9ff4ea6a22c94c4dd459171e3f00cea1285"
stack-generator@^2.0.1:
version "2.0.2"
resolved "https://registry.yarnpkg.com/stack-generator/-/stack-generator-2.0.2.tgz#3c13d952a596ab9318fec0669d0a1df8b87176c7"
dependencies:
stackframe "^1.0.4"
stack-trace@0.0.x:
version "0.0.10"
resolved "https://registry.yarnpkg.com/stack-trace/-/stack-trace-0.0.10.tgz#547c70b347e8d32b4e108ea1a2a159e5fdde19c0"
@ -7329,25 +7301,6 @@ stack-utils@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-1.0.1.tgz#d4f33ab54e8e38778b0ca5cfd3b3afb12db68620"
stackframe@^1.0.3, stackframe@^1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/stackframe/-/stackframe-1.0.4.tgz#357b24a992f9427cba6b545d96a14ed2cbca187b"
stacktrace-gps@^3.0.1:
version "3.0.2"
resolved "https://registry.yarnpkg.com/stacktrace-gps/-/stacktrace-gps-3.0.2.tgz#33f8baa4467323ab2bd1816efa279942ba431ccc"
dependencies:
source-map "0.5.6"
stackframe "^1.0.4"
stacktrace-js@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/stacktrace-js/-/stacktrace-js-2.0.0.tgz#776ca646a95bc6c6b2b90776536a7fc72c6ddb58"
dependencies:
error-stack-parser "^2.0.1"
stack-generator "^2.0.1"
stacktrace-gps "^3.0.1"
staged-git-files@1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/staged-git-files/-/staged-git-files-1.1.1.tgz#37c2218ef0d6d26178b1310719309a16a59f8f7b"

15
zanata.xml Normal file
View File

@ -0,0 +1,15 @@
<?xml version='1.0' encoding='UTF-8' standalone='yes'?>
<config xmlns="http://zanata.org/namespace/config/">
<url>https://trad.framasoft.org/zanata/</url>
<project>peertube</project>
<project-version>develop</project-version>
<project-type>xliff</project-type>
<src-dir>./client/src/locale/source</src-dir>
<trans-dir>./client/src/locale/target</trans-dir>
<hooks>
<hook command="pull">
<after>./scripts/i18n/pull-hook.sh</after>
</hook>
</hooks>
</config>