/logs/
/server/tools/import-mediacore.ts
/docker-volume/
+/.zanata-cache
"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": {
"@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",
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',
private serverService: ServerService,
private domSanitizer: DomSanitizer,
private redirectService: RedirectService
- ) {}
+ ) { }
get serverVersion () {
return this.serverService.getConfig().serverVersion
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()
}
}
-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'
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({
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 {}
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.',
)
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 })
})
}
// 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
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: [
VideoService,
AccountService,
MarkdownService,
- VideoChannelService
+ VideoChannelService,
+ I18n
]
})
export class SharedModule { }
<div [hidden]="videoNotFound" id="video-element-wrapper">
</div>
- <div *ngIf="videoNotFound" id="video-not-found">Video not found :'(</div>
+ <div i18n *ngIf="videoNotFound" id="video-not-found">Video not found :'(</div>
<!-- Video information -->
<div *ngIf="video" class="margin-content video-bottom">
<div>
<div class="video-info-name">{{ video.name }}</div>
- <div class="video-info-date-views">
+ <div i18n class="video-info-date-views">
{{ video.publishedAt | myFromNow }} - {{ video.views | myNumberFormatter }} views
</div>
<div class="video-info-channel">
- <a [routerLink]="[ '/video-channels', video.channel.id ]" title="Go the channel page">
+ <a [routerLink]="[ '/video-channels', video.channel.id ]" i18n-title title="Go the channel page">
{{ video.channel.displayName }}
</a>
<!-- Here will be the subscribe button -->
- <my-help helpType="custom" customHtml="You can subscribe to this account via any ActivityPub-capable fediverse instance. For instance with Mastodon or Pleroma you can type in the search box <strong>@{{video.account.displayName}}@{{video.account.host}}</strong> and subscribe there. Subscription as a PeerTube user is being worked on in <a href='https://github.com/Chocobozzz/PeerTube/issues/470'>#470</a>."></my-help>
+ <my-help helpType="custom" i18n-customHtml customHtml="You can subscribe to this account via any ActivityPub-capable fediverse instance. For instance with Mastodon or Pleroma you can type in the search box <strong>@{{video.account.displayName}}@{{video.account.host}}</strong> and subscribe there. Subscription as a PeerTube user is being worked on in <a href='https://github.com/Chocobozzz/PeerTube/issues/470'>#470</a>."></my-help>
</div>
<div class="video-info-by">
- <a [routerLink]="[ '/accounts', video.by ]" title="Go the account page">
- <span>By {{ video.by }}</span>
+ <a [routerLink]="[ '/accounts', video.by ]" i18n-title title="Go the account page">
+ <span i18n>By {{ video.by }}</span>
<img [src]="video.accountAvatarUrl" alt="Account avatar" />
</a>
</div>
*ngIf="isUserLoggedIn()" [ngClass]="{ 'activated': userRating === 'like' }" (click)="setLike()"
class="action-button action-button-like"
>
- <span class="icon icon-like" title="Like this video" ></span>
+ <span class="icon icon-like" i18n-title title="Like this video" ></span>
</div>
<div
*ngIf="isUserLoggedIn()" [ngClass]="{ 'activated': userRating === 'dislike' }" (click)="setDislike()"
class="action-button action-button-dislike"
>
- <span class="icon icon-dislike" title="Dislike this video"></span>
+ <span class="icon icon-dislike" i18n-title title="Dislike this video"></span>
</div>
<div *ngIf="video.support" (click)="showSupportModal()" class="action-button action-button-support">
<span class="icon icon-support"></span>
- <span class="icon-text">Support</span>
+ <span class="icon-text" i18n>Support</span>
</div>
<div (click)="showShareModal()" class="action-button action-button-share">
<span class="icon icon-share"></span>
- <span class="icon-text">Share</span>
+ <span class="icon-text" i18n>Share</span>
</div>
<div class="action-more" dropdown dropup="true" placement="right">
<ul *dropdownMenu class="dropdown-menu" id="more-menu" role="menu" aria-labelledby="single-button">
<li role="menuitem">
- <a class="dropdown-item" title="Download the video" href="#" (click)="showDownloadModal($event)">
- <span class="icon icon-download"></span> Download
+ <a class="dropdown-item" i18n-title title="Download the video" href="#" (click)="showDownloadModal($event)">
+ <span class="icon icon-download"></span> <ng-container i18n>Download</ng-container>
</a>
</li>
<li *ngIf="isUserLoggedIn()" role="menuitem">
- <a class="dropdown-item" title="Report this video" href="#" (click)="showReportModal($event)">
- <span class="icon icon-alert"></span> Report
+ <a class="dropdown-item" i18n-title title="Report this video" href="#" (click)="showReportModal($event)">
+ <span class="icon icon-alert"></span> <ng-container i18n>Report</ng-container>
</a>
</li>
<li *ngIf="isVideoBlacklistable()" role="menuitem">
- <a class="dropdown-item" title="Blacklist this video" href="#" (click)="blacklistVideo($event)">
- <span class="icon icon-blacklist"></span> Blacklist
+ <a class="dropdown-item" i18n-title title="Blacklist this video" href="#" (click)="blacklistVideo($event)">
+ <span class="icon icon-blacklist"></span> <ng-container i18n>Blacklist</ng-container>
</a>
</li>
<li *ngIf="isVideoUpdatable()" role="menuitem">
- <a class="dropdown-item" title="Update this video" href="#" [routerLink]="[ '/videos/update', video.uuid ]">
- <span class="icon icon-edit"></span> Update
+ <a class="dropdown-item" i18n-title title="Update this video" href="#" [routerLink]="[ '/videos/update', video.uuid ]">
+ <span class="icon icon-edit"></span> <ng-container i18n>Update</ng-container>
</a>
</li>
<li *ngIf="isVideoRemovable()" role="menuitem">
- <a class="dropdown-item" title="Delete this video" href="#" (click)="removeVideo($event)">
- <span class="icon icon-blacklist"></span> Delete
+ <a class="dropdown-item" i18n-title title="Delete this video" href="#" (click)="removeVideo($event)">
+ <span class="icon icon-blacklist"></span> <ng-container i18n>Delete</ng-container>
</a>
</li>
</ul>
<div class="video-info-description-html" [innerHTML]="videoHTMLDescription"></div>
<div class="video-info-description-more" *ngIf="completeDescriptionShown === false && video.description?.length >= 250" (click)="showMoreDescription()">
- Show more
+ <ng-container i18n>Show more</ng-container>
<span *ngIf="descriptionLoading === false" class="glyphicon glyphicon-menu-down"></span>
<my-loader class="description-loading" [loading]="descriptionLoading"></my-loader>
</div>
<div *ngIf="completeDescriptionShown === true" (click)="showLessDescription()" class="video-info-description-more">
- Show less
+ <ng-container i18n>Show less</ng-container>
<span *ngIf="descriptionLoading === false" class="glyphicon glyphicon-menu-up"></span>
</div>
</div>
<div class="video-attributes">
<div class="video-attribute">
- <span class="video-attribute-label">
+ <span i18n class="video-attribute-label">
Privacy
</span>
<span class="video-attribute-value">
</div>
<div class="video-attribute">
- <span class="video-attribute-label">
+ <span i18n class="video-attribute-label">
Category
</span>
<span class="video-attribute-value">
</div>
<div class="video-attribute">
- <span class="video-attribute-label">
+ <span i18n class="video-attribute-label">
Licence
</span>
<span class="video-attribute-value">
</div>
<div class="video-attribute">
- <span class="video-attribute-label">
+ <span i18n class="video-attribute-label">
Language
</span>
<span class="video-attribute-value">
</div>
<div class="video-attribute">
- <span class="video-attribute-label">
+ <span i18n class="video-attribute-label">
Tags
</span>
</div>
<div class="other-videos">
- <div class="title-page title-page-single">
+ <div i18n class="title-page title-page-single">
Other videos
</div>
<div class="privacy-concerns" *ngIf="hasAlreadyAcceptedPrivacyConcern === false">
- <strong>Friendly Reminder:</strong>
+ <strong i18n>Friendly Reminder:</strong>
<div class="privacy-concerns-text">
- The sharing system used by this video implies that some technical information about your system (such as a public IP address) can be accessed publicly.
- <a title="Get more information" target="_blank" rel="noopener noreferrer" href="/about#p2p-privacy">More information</a>
+ <ng-container i18n>
+ The sharing system used by this video implies that some technical information about your system (such as a public IP address) can be accessed publicly.
+ </ng-container>
+ <a i18n i18n-title title="Get more information" target="_blank" rel="noopener noreferrer" href="/about#p2p-privacy">More information</a>
</div>
- <div class="privacy-concerns-okay" (click)="acceptedPrivacyConcern()">
+ <div i18n class="privacy-concerns-okay" (click)="acceptedPrivacyConcern()">
OK
</div>
</div>
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',
private notificationsService: NotificationsService,
private markdownService: MarkdownService,
private zone: NgZone,
- private redirectService: RedirectService
+ private redirectService: RedirectService,
+ private i18n: I18n
) {}
get user () {
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)
)
}
error => {
this.descriptionLoading = false
- this.notificationsService.error('Error', error.message)
+ this.notificationsService.error(this.i18n('Error'), error.message)
}
)
}
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)
)
}
}
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) {
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 () {
}
},
- err => this.notificationsService.error('Error', err.message)
+ err => this.notificationsService.error(this.i18n('Error'), err.message)
)
}
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()
}
this.updateVideoRating(this.userRating, nextRating)
this.userRating = nextRating
},
- err => this.notificationsService.error('Error', err.message)
+ err => this.notificationsService.error(this.i18n('Error'), err.message)
)
}
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',
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 () {
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',
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 () {
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',
templateUrl: '../../shared/video/abstract-video-list.html'
})
export class VideoSearchComponent extends AbstractVideoList implements OnInit, OnDestroy {
- titlePage = 'Search'
+ titlePage: string
currentRoute = '/videos/search'
loadOnInit = false
}
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 () {
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',
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 () {
--- /dev/null
+<?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 :'(</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>
+ <x id="INTERPOLATION" equiv-text="{{ video.publishedAt | myFromNow }}"/> - <x id="INTERPOLATION_1" equiv-text="{{ video.views | myNumberFormatter }}"/> 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 <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>
+ <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 <x id="INTERPOLATION" equiv-text="{{ video.by }}"/></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 <x id="INTERPOLATION" equiv-text="{{ videoName }}"/> 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 <x id="INTERPOLATION" equiv-text="{{ videoName }}"/> 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><x id="INTERPOLATION" equiv-text="{{ likesNumber }}"/> likes / <x id="INTERPOLATION_1" equiv-text="{{ dislikesNumber }}"/> 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>
--- /dev/null
+<?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 <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>
+ <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>
+ <context-group name="null">
+ <context context-type="linenumber">24</context>
+ </context-group>
+ </trans-unit>
+ <trans-unit id="ccc07df383b7a32be3e2e105faa5488caf261c1c">
+ <source>By <x id="INTERPOLATION" equiv-text="{{ video.by }}"/></source>
+ <target>Par <x id="INTERPOLATION" equiv-text="{{ video.by }}"/></target>
+ <context-group name="null">
+ <context context-type="linenumber">29</context>
+ </context-group>
+ </trans-unit>
+ <trans-unit id="e88300c71e0cb0f346d5a72eb37c920f2aadae8a">
+ <source>Go the account page</source>
+ <target>Aller sur la page du compte</target>
+ <context-group name="null">
+ <context context-type="linenumber">28</context>
+ </context-group>
+ </trans-unit>
+ <trans-unit id="82b59049f3f89d900c98da9319e156dd513e3ced">
+ <source>Like this video</source>
+ <target>J'aime cette vidéo</target>
+ <context-group name="null">
+ <context context-type="linenumber">41</context>
+ </context-group>
+ </trans-unit>
+ <trans-unit id="623698f075025b2b2fc2e0c59fd95f4f4662a509">
+ <source>Dislike this video</source>
+ <target>Je n'aime pas cette vidéo</target>
+ <context-group name="null">
+ <context context-type="linenumber">48</context>
+ </context-group>
+ </trans-unit>
+ <trans-unit id="b5629d298ff1a69b8db19a4ba2995c76b52da604">
+ <source>Support</source>
+ <target>Supporter</target>
+ <context-group name="null">
+ <context context-type="linenumber">53</context>
+ </context-group>
+ </trans-unit>
+ <trans-unit id="0bd8b27f60a1f098a53e06328426d818e3508ff9">
+ <source>Share</source>
+ <target>Partager</target>
+ <context-group name="null">
+ <context context-type="linenumber">58</context>
+ </context-group>
+ </trans-unit>
+ <trans-unit id="dc75033a5238fdc4f462212c847a45ba8018a3fd">
+ <source>Download</source>
+ <target>Télécharger</target>
+ <context-group name="null">
+ <context context-type="linenumber">69</context>
+ </context-group>
+ </trans-unit>
+ <trans-unit id="144fff5c40b85414d59e644d8dee7cfefba925a2">
+ <source>Download the video</source>
+ <target>Télécharger la vidéo</target>
+ <context-group name="null">
+ <context context-type="linenumber">68</context>
+ </context-group>
+ </trans-unit>
+ <trans-unit id="f72992030f134408b675152c397f9d0ec00f3b2a">
+ <source>Report</source>
+ <target>Signaler</target>
+ <context-group name="null">
+ <context context-type="linenumber">75</context>
+ </context-group>
+ </trans-unit>
+ <trans-unit id="2f4894617d9c44010f87473e583bd4604b7d6ecf">
+ <source>Report this video</source>
+ <target>Signaler cette vidéo</target>
+ <context-group name="null">
+ <context context-type="linenumber">74</context>
+ </context-group>
+ </trans-unit>
+ <trans-unit id="007ab5fa2aae8a7372307d3fc45a2dbcb11ffd61">
+ <source>Blacklist</source>
+ <target>Blacklister</target>
+ <context-group name="null">
+ <context context-type="linenumber">81</context>
+ </context-group>
+ </trans-unit>
+ <trans-unit id="803c6317abd2dbafcc93226c4e273c62932e3037">
+ <source>Blacklist this video</source>
+ <target>Blacklister cette vidéo</target>
+ <context-group name="null">
+ <context context-type="linenumber">80</context>
+ </context-group>
+ </trans-unit>
+ <trans-unit id="047f50bc5b5d17b5bec0196355953e1a5c590ddb">
+ <source>Update</source>
+ <target>Mettre à jour</target>
+ <context-group name="null">
+ <context context-type="linenumber">87</context>
+ </context-group>
+ </trans-unit>
+ <trans-unit id="cd27f761b923a5bdb16ba9844da632edd878f1b1">
+ <source>Update this video</source>
+ <target>Mettre à jour cette vidéo</target>
+ <context-group name="null">
+ <context context-type="linenumber">86</context>
+ </context-group>
+ </trans-unit>
+ <trans-unit id="826b25211922a1b46436589233cb6f1a163d89b7">
+ <source>Delete</source>
+ <target>Supprimer</target>
+ <context-group name="null">
+ <context context-type="linenumber">93</context>
+ </context-group>
+ </trans-unit>
+ <trans-unit id="3dbfdc68f83d91cb360172eb65578cae94e7cbe5">
+ <source>Delete this video</source>
+ <target>Supprimer cette vidéo</target>
+ <context-group name="null">
+ <context context-type="linenumber">92</context>
+ </context-group>
+ </trans-unit>
+ <trans-unit id="f0c5f6f270e70cbe063b5368fcf48f9afc1abd9b">
+ <source>Show more</source>
+ <target>Montrer plus</target>
+ <context-group name="null">
+ <context context-type="linenumber">112</context>
+ </context-group>
+ </trans-unit>
+ <trans-unit id="5403a767248e304199592271bba3366d2ca3f903">
+ <source>Show less</source>
+ <target>Montrer moins</target>
+ <context-group name="null">
+ <context context-type="linenumber">118</context>
+ </context-group>
+ </trans-unit>
+ <trans-unit id="8057a9b7f9e908ff350edfd71417b96c174e5911">
+ <source>
+ Privacy
+ </source>
+ <target>Visibilité</target>
+ <context-group name="null">
+ <context context-type="linenumber">125</context>
+ </context-group>
+ </trans-unit>
+ <trans-unit id="bd407eca607a8905a26a9e30c9d0cd70f4465db8">
+ <source>
+ Category
+ </source>
+ <target>Catégorie</target>
+ <context-group name="null">
+ <context context-type="linenumber">134</context>
+ </context-group>
+ </trans-unit>
+ <trans-unit id="b6b7986bc3721ac483baf20bc9a320529075c807">
+ <source>Trending</source>
+ <target>Tendances</target>
+ <context-group name="null">
+ <context context-type="linenumber">1</context>
+ </context-group>
+ </trans-unit>
+ </body>
+ </file></xliff>
\ No newline at end of file
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"
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"
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"
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"
"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",
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
+
--- /dev/null
+#!/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=\([^\/]\+\?\)\/>/\<x id=\1\/\>/g' src/locale/source/messages_en_US.xml
\ No newline at end of file
--- /dev/null
+#!/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/\<x id=\([^\/]\+\?\)\/\>/<x id=\1\/>/g' client/src/locale/target/*
\ No newline at end of file
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
(
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'
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 -----------
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
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 {
// ---------------------------------------------------------------------------
+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
} 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)
--- /dev/null
+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('-', '_')
+}
--- /dev/null
+export * from './i18n'
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'
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:
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"
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"
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:
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"
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"
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"
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"
--- /dev/null
+<?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>