-
Friendly Reminder:
+
Friendly Reminder:
- 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.
-
More information
+
+ 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.
+
+
More information
-
diff --git a/client/src/app/videos/+video-watch/video-watch.component.ts b/client/src/app/videos/+video-watch/video-watch.component.ts
index ad572ef58..f3b4f7a2b 100644
--- a/client/src/app/videos/+video-watch/video-watch.component.ts
+++ b/client/src/app/videos/+video-watch/video-watch.component.ts
@@ -23,6 +23,7 @@ import { VideoReportComponent } from './modal/video-report.component'
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)
)
}
diff --git a/client/src/app/videos/video-list/video-local.component.ts b/client/src/app/videos/video-list/video-local.component.ts
index abab7504f..03568b618 100644
--- a/client/src/app/videos/video-list/video-local.component.ts
+++ b/client/src/app/videos/video-list/video-local.component.ts
@@ -8,6 +8,7 @@ import { AbstractVideoList } from '../../shared/video/abstract-video-list'
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 () {
diff --git a/client/src/app/videos/video-list/video-recently-added.component.ts b/client/src/app/videos/video-list/video-recently-added.component.ts
index d064d9628..5768d9fe0 100644
--- a/client/src/app/videos/video-list/video-recently-added.component.ts
+++ b/client/src/app/videos/video-list/video-recently-added.component.ts
@@ -7,6 +7,7 @@ import { AuthService } from '../../core/auth'
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 () {
diff --git a/client/src/app/videos/video-list/video-search.component.ts b/client/src/app/videos/video-list/video-search.component.ts
index aab896d84..35566a7bd 100644
--- a/client/src/app/videos/video-list/video-search.component.ts
+++ b/client/src/app/videos/video-list/video-search.component.ts
@@ -8,6 +8,7 @@ import { Subscription } from 'rxjs'
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 () {
diff --git a/client/src/app/videos/video-list/video-trending.component.ts b/client/src/app/videos/video-list/video-trending.component.ts
index ea65070f9..760470e8c 100644
--- a/client/src/app/videos/video-list/video-trending.component.ts
+++ b/client/src/app/videos/video-list/video-trending.component.ts
@@ -7,6 +7,7 @@ import { AuthService } from '../../core/auth'
import { AbstractVideoList } from '../../shared/video/abstract-video-list'
import { VideoSortField } from '../../shared/video/sort-field.type'
import { VideoService } from '../../shared/video/video.service'
+import { I18n } from '@ngx-translate/i18n-polyfill'
@Component({
selector: 'my-videos-trending',
@@ -14,17 +15,22 @@ import { VideoService } from '../../shared/video/video.service'
templateUrl: '../../shared/video/abstract-video-list.html'
})
export class VideoTrendingComponent extends AbstractVideoList implements OnInit, OnDestroy {
- titlePage = 'Trending'
+ titlePage: string
currentRoute = '/videos/trending'
defaultSort: VideoSortField = '-views'
- constructor (protected router: Router,
- protected route: ActivatedRoute,
- protected notificationsService: NotificationsService,
- protected authService: AuthService,
- protected location: Location,
- private videoService: VideoService) {
+ constructor (
+ protected router: Router,
+ protected route: ActivatedRoute,
+ protected notificationsService: NotificationsService,
+ protected authService: AuthService,
+ protected location: Location,
+ private videoService: VideoService,
+ private i18n: I18n
+ ) {
super()
+
+ this.titlePage = i18n('Trending')
}
ngOnInit () {
diff --git a/client/src/locale/source/messages_en_US.xml b/client/src/locale/source/messages_en_US.xml
new file mode 100644
index 000000000..6c355a97f
--- /dev/null
+++ b/client/src/locale/source/messages_en_US.xml
@@ -0,0 +1,354 @@
+
+
+
+
+
+
+ My public profile
+
+
+ app/menu/menu.component.ts
+ 17
+
+
+ Video not found :'(
+
+ app/videos/+video-watch/video-watch.component.ts
+ 6
+
+
+
+ <x id="INTERPOLATION" equiv-text="{{ video.publishedAt | myFromNow }}"/> - <x id="INTERPOLATION_1" equiv-text="{{ video.views | myNumberFormatter }}"/> views
+
+
+ app/videos/+video-watch/video-watch.component.ts
+ 15
+
+
+ Go the channel page
+
+ app/videos/+video-watch/video-watch.component.ts
+ 20
+
+
+ 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>.
+
+ app/videos/+video-watch/video-watch.component.ts
+ 24
+
+
+ By <x id="INTERPOLATION" equiv-text="{{ video.by }}"/>
+
+ app/videos/+video-watch/video-watch.component.ts
+ 29
+
+
+ Go the account page
+
+ app/videos/+video-watch/video-watch.component.ts
+ 28
+
+
+ Like this video
+
+ app/videos/+video-watch/video-watch.component.ts
+ 41
+
+
+ Dislike this video
+
+ app/videos/+video-watch/video-watch.component.ts
+ 48
+
+
+ Support
+
+ app/videos/+video-watch/video-watch.component.ts
+ 53
+
+
+ Share
+
+ app/videos/+video-watch/video-watch.component.ts
+ 58
+
+
+ Download
+
+ app/videos/+video-watch/video-watch.component.ts
+ 69
+
+
+ Download the video
+
+ app/videos/+video-watch/video-watch.component.ts
+ 68
+
+
+ Report
+
+ app/videos/+video-watch/video-watch.component.ts
+ 75
+
+
+ Report this video
+
+ app/videos/+video-watch/video-watch.component.ts
+ 74
+
+
+ Blacklist
+
+ app/videos/+video-watch/video-watch.component.ts
+ 81
+
+
+ Blacklist this video
+
+ app/videos/+video-watch/video-watch.component.ts
+ 80
+
+
+ Update
+
+ app/videos/+video-watch/video-watch.component.ts
+ 87
+
+
+ Update this video
+
+ app/videos/+video-watch/video-watch.component.ts
+ 86
+
+
+ Delete
+
+ app/videos/+video-watch/video-watch.component.ts
+ 93
+
+
+ Delete this video
+
+ app/videos/+video-watch/video-watch.component.ts
+ 92
+
+
+ Show more
+
+ app/videos/+video-watch/video-watch.component.ts
+ 112
+
+
+ Show less
+
+ app/videos/+video-watch/video-watch.component.ts
+ 118
+
+
+
+ Privacy
+
+
+ app/videos/+video-watch/video-watch.component.ts
+ 125
+
+
+
+ Category
+
+
+ app/videos/+video-watch/video-watch.component.ts
+ 134
+
+
+
+ Licence
+
+
+ app/videos/+video-watch/video-watch.component.ts
+ 143
+
+
+
+ Language
+
+
+ app/videos/+video-watch/video-watch.component.ts
+ 152
+
+
+
+ Tags
+
+
+ app/videos/+video-watch/video-watch.component.ts
+ 161
+
+
+
+ Other videos
+
+
+ app/videos/+video-watch/video-watch.component.ts
+ 175
+
+
+ Friendly Reminder:
+
+ app/videos/+video-watch/video-watch.component.ts
+ 187
+
+
+
+ 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.
+
+
+ app/videos/+video-watch/video-watch.component.ts
+ 189
+
+
+ More information
+
+ app/videos/+video-watch/video-watch.component.ts
+ 192
+
+
+ Get more information
+
+ app/videos/+video-watch/video-watch.component.ts
+ 192
+
+
+
+ OK
+
+
+ app/videos/+video-watch/video-watch.component.ts
+ 195
+
+
+
+ Do you really want to blacklist this video?
+
+ src/app/videos/+video-watch/video-watch.component.ts
+ 1
+
+
+
+ Success
+
+ src/app/videos/+video-watch/video-watch.component.ts
+ 1
+
+
+ src/app/videos/+video-watch/video-watch.component.ts
+ 1
+
+
+
+ Video <x id="INTERPOLATION" equiv-text="{{ videoName }}"/> had been blacklisted.
+
+ src/app/videos/+video-watch/video-watch.component.ts
+ 1
+
+
+
+ Error
+
+ src/app/videos/+video-watch/video-watch.component.ts
+ 1
+
+
+ src/app/videos/+video-watch/video-watch.component.ts
+ 1
+
+
+ src/app/videos/+video-watch/video-watch.component.ts
+ 1
+
+
+ src/app/videos/+video-watch/video-watch.component.ts
+ 1
+
+
+ src/app/videos/+video-watch/video-watch.component.ts
+ 1
+
+
+ src/app/videos/+video-watch/video-watch.component.ts
+ 1
+
+
+
+ Do you really want to delete this video?
+
+ src/app/videos/+video-watch/video-watch.component.ts
+ 1
+
+
+
+ Video <x id="INTERPOLATION" equiv-text="{{ videoName }}"/> deleted.
+
+ src/app/videos/+video-watch/video-watch.component.ts
+ 1
+
+
+
+ <x id="INTERPOLATION" equiv-text="{{ likesNumber }}"/> likes / <x id="INTERPOLATION_1" equiv-text="{{ dislikesNumber }}"/> dislikes
+
+ src/app/videos/+video-watch/video-watch.component.ts
+ 1
+
+
+
+ Cannot fetch video from server, maybe down.
+
+ src/app/videos/+video-watch/video-watch.component.ts
+ 1
+
+
+
+ This video contains mature or explicit content. Are you sure you want to watch it?
+
+ src/app/videos/+video-watch/video-watch.component.ts
+ 1
+
+
+
+ Mature or explicit content
+
+ src/app/videos/+video-watch/video-watch.component.ts
+ 1
+
+
+
+ Local videos
+
+ src/app/videos/video-list/video-local.component.ts
+ 1
+
+
+
+ Recently added
+
+ src/app/videos/video-list/video-recently-added.component.ts
+ 1
+
+
+
+ Search
+
+ src/app/videos/video-list/video-search.component.ts
+ 1
+
+
+
+ Trending
+
+ src/app/videos/video-list/video-trending.component.ts
+ 1
+
+
+
+
+
diff --git a/client/src/locale/target/messages_fr.xml b/client/src/locale/target/messages_fr.xml
new file mode 100644
index 000000000..3a55922ba
--- /dev/null
+++ b/client/src/locale/target/messages_fr.xml
@@ -0,0 +1,191 @@
+
+
+
+
+
+
+
+ My public profile
+
+ Mon profile public
+
+ 17
+
+
+
+ Video not found :'(
+ Vidéo non trouvée :'(
+
+ 6
+
+
+
+
+ - views
+
+
+ - vues
+
+ 15
+
+
+
+ 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>@ @ </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>.
+ 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>@ @ </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>.
+
+ 24
+
+
+
+ By
+ Par
+
+ 29
+
+
+
+ Go the account page
+ Aller sur la page du compte
+
+ 28
+
+
+
+ Like this video
+ J'aime cette vidéo
+
+ 41
+
+
+
+ Dislike this video
+ Je n'aime pas cette vidéo
+
+ 48
+
+
+
+ Support
+ Supporter
+
+ 53
+
+
+
+ Share
+ Partager
+
+ 58
+
+
+
+ Download
+ Télécharger
+
+ 69
+
+
+
+ Download the video
+ Télécharger la vidéo
+
+ 68
+
+
+
+ Report
+ Signaler
+
+ 75
+
+
+
+ Report this video
+ Signaler cette vidéo
+
+ 74
+
+
+
+ Blacklist
+ Blacklister
+
+ 81
+
+
+
+ Blacklist this video
+ Blacklister cette vidéo
+
+ 80
+
+
+
+ Update
+ Mettre à jour
+
+ 87
+
+
+
+ Update this video
+ Mettre à jour cette vidéo
+
+ 86
+
+
+
+ Delete
+ Supprimer
+
+ 93
+
+
+
+ Delete this video
+ Supprimer cette vidéo
+
+ 92
+
+
+
+ Show more
+ Montrer plus
+
+ 112
+
+
+
+ Show less
+ Montrer moins
+
+ 118
+
+
+
+
+ Privacy
+
+ Visibilité
+
+ 125
+
+
+
+
+ Category
+
+ Catégorie
+
+ 134
+
+
+
+ Trending
+ Tendances
+
+ 1
+
+
+
+
\ No newline at end of file
diff --git a/client/yarn.lock b/client/yarn.lock
index fe2e040d8..e2d0da541 100644
--- a/client/yarn.lock
+++ b/client/yarn.lock
@@ -242,6 +242,14 @@
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"
diff --git a/package.json b/package.json
index 608646e7d..21701e664 100644
--- a/package.json
+++ b/package.json
@@ -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",
diff --git a/scripts/build/client.sh b/scripts/build/client.sh
index 305af1e5f..61ba4ea99 100755
--- a/scripts/build/client.sh
+++ b/scripts/build/client.sh
@@ -6,5 +6,19 @@ cd client
rm -rf ./dist ./compiled
-npm run ng build -- --prod --stats-json
+defaultLanguage="en-US"
+npm run ng build -- --output-path "dist/$defaultLanguage/" --deploy-url "/client/$defaultLanguage/" --prod --stats-json
+mv "./dist/$defaultLanguage/assets" "./dist"
+
+languages="fr"
+
+for lang in "$languages"; do
+ npm run ng build -- --prod --i18n-file "./src/locale/target/messages_$lang.xml" --i18n-format xlf --i18n-locale "$lang" \
+ --output-path "dist/$lang/" --deploy-url "/client/$lang/"
+
+ # Do no duplicate assets
+ rm -r "./dist/$lang/assets"
+done
+
NODE_ENV=production npm run webpack -- --config webpack/webpack.video-embed.js --mode production
+
diff --git a/scripts/i18n/generate.sh b/scripts/i18n/generate.sh
new file mode 100755
index 000000000..429523ba4
--- /dev/null
+++ b/scripts/i18n/generate.sh
@@ -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
, 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=\1\/\>/g' src/locale/source/messages_en_US.xml
\ No newline at end of file
diff --git a/scripts/i18n/pull-hook.sh b/scripts/i18n/pull-hook.sh
new file mode 100755
index 000000000..cb969f83c
--- /dev/null
+++ b/scripts/i18n/pull-hook.sh
@@ -0,0 +1,7 @@
+#!/bin/sh
+
+set -eu
+
+# Zanata does not support inner elements in , so we hack these special elements
+# This regex translate the converted elements to initial Angular elements
+sed -i 's/\<x id=\([^\/]\+\?\)\/\>//g' client/src/locale/target/*
\ No newline at end of file
diff --git a/scripts/release.sh b/scripts/release.sh
index 8c73a1fd6..393955264 100755
--- a/scripts/release.sh
+++ b/scripts/release.sh
@@ -57,7 +57,7 @@ git commit package.json client/package.json -m "Bumped to version $version"
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
(
diff --git a/server.ts b/server.ts
index bdcbb7988..c0e679b02 100644
--- a/server.ts
+++ b/server.ts
@@ -12,7 +12,6 @@ import * as bodyParser from 'body-parser'
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 -----------
diff --git a/server/controllers/client.ts b/server/controllers/client.ts
index aff00fe6e..a29b51c51 100644
--- a/server/controllers/client.ts
+++ b/server/controllers/client.ts
@@ -3,17 +3,24 @@ import * as express from 'express'
import { join } from 'path'
import * as validator from 'validator'
import { escapeHTML, readFileBufferPromise, root } from '../helpers/core-utils'
-import { CONFIG, EMBED_SIZE, OPENGRAPH_AND_OEMBED_COMMENT, STATIC_MAX_AGE, STATIC_PATHS } from '../initializers'
+import {
+ ACCEPT_HEADERS,
+ CONFIG,
+ EMBED_SIZE,
+ OPENGRAPH_AND_OEMBED_COMMENT,
+ STATIC_MAX_AGE,
+ STATIC_PATHS
+} from '../initializers'
import { asyncMiddleware } from '../middlewares'
import { VideoModel } from '../models/video/video'
import { VideoPrivacy } from '../../shared/models/videos'
+import { I18N_LOCALES, is18nLocale, getDefaultLocale } from '../../shared/models'
const clientsRouter = express.Router()
const distPath = join(root(), 'client', 'dist')
const assetsImagesPath = join(root(), 'client', 'dist', 'assets', 'images')
const embedPath = join(distPath, 'standalone', 'videos', 'embed.html')
-const indexPath = join(distPath, 'index.html')
// Special route that add OpenGraph and oEmbed tags
// Do not use a template engine for a so little thing
@@ -45,6 +52,16 @@ clientsRouter.use('/client/*', (req: express.Request, res: express.Response, nex
res.sendStatus(404)
})
+// Always serve index client page (the client is a single page application, let it handle routing)
+// Try to provide the right language index.html
+clientsRouter.use('/(:language)?', function (req, res) {
+ if (req.accepts(ACCEPT_HEADERS) === 'html') {
+ return res.sendFile(getIndexPath(req, req.params.language))
+ }
+
+ return res.status(404).end()
+})
+
// ---------------------------------------------------------------------------
export {
@@ -53,6 +70,19 @@ export {
// ---------------------------------------------------------------------------
+function getIndexPath (req: express.Request, paramLang?: string) {
+ let lang: string
+
+ // Check param lang validity
+ if (paramLang && is18nLocale(paramLang)) {
+ lang = paramLang
+ } else {
+ lang = req.acceptsLanguages(Object.keys(I18N_LOCALES)) || getDefaultLocale()
+ }
+
+ return join(__dirname, '../../../client/dist/' + lang + '/index.html')
+}
+
function addOpenGraphAndOEmbedTags (htmlStringPage: string, video: VideoModel) {
const previewUrl = CONFIG.WEBSERVER.URL + STATIC_PATHS.PREVIEWS + video.getPreviewName()
const videoUrl = CONFIG.WEBSERVER.URL + '/videos/watch/' + video.uuid
@@ -142,18 +172,18 @@ async function generateWatchHtmlPage (req: express.Request, res: express.Respons
} else if (validator.isInt(videoId)) {
videoPromise = VideoModel.loadAndPopulateAccountAndServerAndTags(+videoId)
} else {
- return res.sendFile(indexPath)
+ return res.sendFile(getIndexPath(req))
}
let [ file, video ] = await Promise.all([
- readFileBufferPromise(indexPath),
+ readFileBufferPromise(getIndexPath(req)),
videoPromise
])
const html = file.toString()
// Let Angular application handle errors
- if (!video || video.privacy === VideoPrivacy.PRIVATE) return res.sendFile(indexPath)
+ if (!video || video.privacy === VideoPrivacy.PRIVATE) return res.sendFile(getIndexPath(req))
const htmlStringPageWithTags = addOpenGraphAndOEmbedTags(html, video)
res.set('Content-Type', 'text/html; charset=UTF-8').send(htmlStringPageWithTags)
diff --git a/shared/models/i18n/i18n.ts b/shared/models/i18n/i18n.ts
new file mode 100644
index 000000000..2d3a1d3e2
--- /dev/null
+++ b/shared/models/i18n/i18n.ts
@@ -0,0 +1,30 @@
+export const I18N_LOCALES = {
+ 'en-US': 'English (US)',
+ fr: 'French'
+}
+
+export function getDefaultLocale () {
+ return 'en-US'
+}
+
+const possiblePaths = Object.keys(I18N_LOCALES).map(l => '/' + l)
+export function is18nPath (path: string) {
+ return possiblePaths.indexOf(path) !== -1
+}
+
+const possibleLanguages = Object.keys(I18N_LOCALES)
+export function is18nLocale (locale: string) {
+ return possibleLanguages.indexOf(locale) !== -1
+}
+
+// Only use in dev mode, so relax
+// In production, the locale always match with a I18N_LANGUAGES key
+export function buildFileLocale (locale: string) {
+ if (!is18nLocale(locale)) {
+ // Some working examples for development purpose
+ if (locale.split('-')[ 0 ] === 'en') return 'en_US'
+ else if (locale === 'fr') return 'fr'
+ }
+
+ return locale.replace('-', '_')
+}
diff --git a/shared/models/i18n/index.ts b/shared/models/i18n/index.ts
new file mode 100644
index 000000000..8f7cbe2c7
--- /dev/null
+++ b/shared/models/i18n/index.ts
@@ -0,0 +1 @@
+export * from './i18n'
diff --git a/shared/models/index.ts b/shared/models/index.ts
index 95bc402d6..c8ce71f17 100644
--- a/shared/models/index.ts
+++ b/shared/models/index.ts
@@ -3,6 +3,7 @@ export * from './activitypub'
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'
diff --git a/yarn.lock b/yarn.lock
index c1fed9c60..eb06faac0 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1237,7 +1237,7 @@ chalk@^1.0.0, chalk@^1.1.1, chalk@^1.1.3:
strip-ansi "^3.0.0"
supports-color "^2.0.0"
-chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0, chalk@^2.3.0, chalk@^2.3.1, chalk@^2.3.2:
+chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0, chalk@^2.3.0, chalk@^2.3.1:
version "2.4.1"
resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.1.tgz#18c49ab16a037b6eb0152cc83e3471338215b66e"
dependencies:
@@ -1517,7 +1517,7 @@ command-exists@^1.2.2:
version "1.2.6"
resolved "https://registry.yarnpkg.com/command-exists/-/command-exists-1.2.6.tgz#577f8e5feb0cb0f159cd557a51a9be1bdd76e09e"
-commander@*, commander@2.15.1, commander@^2.12.1, commander@^2.13.0, commander@^2.14.1, commander@^2.15.1, commander@^2.8.1, commander@^2.9.0:
+commander@*, commander@2.15.1, commander@^2.12.1, commander@^2.13.0, commander@^2.14.1, commander@^2.8.1, commander@^2.9.0:
version "2.15.1"
resolved "https://registry.yarnpkg.com/commander/-/commander-2.15.1.tgz#df46e867d0fc2aec66a34662b406a9ccafff5b0f"
@@ -2175,12 +2175,6 @@ error-ex@^1.2.0, error-ex@^1.3.1:
dependencies:
is-arrayish "^0.2.1"
-error-stack-parser@^2.0.1:
- version "2.0.1"
- resolved "https://registry.yarnpkg.com/error-stack-parser/-/error-stack-parser-2.0.1.tgz#a3202b8fb03114aa9b40a0e3669e48b2b65a010a"
- dependencies:
- stackframe "^1.0.3"
-
es5-ext@^0.10.14, es5-ext@^0.10.35, es5-ext@^0.10.9, es5-ext@~0.10.14:
version "0.10.43"
resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.43.tgz#c705e645253210233a270869aa463a2333b7ca64"
@@ -4155,7 +4149,7 @@ js-tokens@^3.0.0, js-tokens@^3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b"
-js-yaml@^3.11.0, js-yaml@^3.4.6, js-yaml@^3.5.1, js-yaml@^3.5.4, js-yaml@^3.7.0, js-yaml@^3.8.2, js-yaml@^3.8.3, js-yaml@^3.9.0:
+js-yaml@^3.4.6, js-yaml@^3.5.1, js-yaml@^3.5.4, js-yaml@^3.7.0, js-yaml@^3.8.2, js-yaml@^3.8.3, js-yaml@^3.9.0:
version "3.11.0"
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.11.0.tgz#597c1a8bd57152f26d622ce4117851a51f5ebaef"
dependencies:
@@ -6741,18 +6735,6 @@ sass-graph@^2.2.4:
scss-tokenizer "^0.2.3"
yargs "^7.0.0"
-sass-lint-auto-fix@^0.9.0:
- version "0.9.2"
- resolved "https://registry.yarnpkg.com/sass-lint-auto-fix/-/sass-lint-auto-fix-0.9.2.tgz#b8b6eb95644f7919dfea33d04c1fc19ae8f07a11"
- dependencies:
- chalk "^2.3.2"
- commander "^2.15.1"
- glob "^7.1.2"
- gonzales-pe-sl "^4.2.3"
- js-yaml "^3.11.0"
- sass-lint "^1.12.1"
- stacktrace-js "^2.0.0"
-
sass-lint@^1.12.1:
version "1.12.1"
resolved "https://registry.yarnpkg.com/sass-lint/-/sass-lint-1.12.1.tgz#630f69c216aa206b8232fb2aa907bdf3336b6d83"
@@ -7194,10 +7176,6 @@ source-map@0.4.x, source-map@^0.4.2, source-map@^0.4.4:
dependencies:
amdefine ">=0.0.4"
-source-map@0.5.6:
- version "0.5.6"
- resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.6.tgz#75ce38f52bf0733c5a7f0c118d81334a2bb5f412"
-
source-map@0.5.x, source-map@^0.5.6, source-map@~0.5.1:
version "0.5.7"
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc"
@@ -7315,12 +7293,6 @@ stack-chain@1.3.x, stack-chain@~1.3.1:
version "1.3.7"
resolved "https://registry.yarnpkg.com/stack-chain/-/stack-chain-1.3.7.tgz#d192c9ff4ea6a22c94c4dd459171e3f00cea1285"
-stack-generator@^2.0.1:
- version "2.0.2"
- resolved "https://registry.yarnpkg.com/stack-generator/-/stack-generator-2.0.2.tgz#3c13d952a596ab9318fec0669d0a1df8b87176c7"
- dependencies:
- stackframe "^1.0.4"
-
stack-trace@0.0.x:
version "0.0.10"
resolved "https://registry.yarnpkg.com/stack-trace/-/stack-trace-0.0.10.tgz#547c70b347e8d32b4e108ea1a2a159e5fdde19c0"
@@ -7329,25 +7301,6 @@ stack-utils@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-1.0.1.tgz#d4f33ab54e8e38778b0ca5cfd3b3afb12db68620"
-stackframe@^1.0.3, stackframe@^1.0.4:
- version "1.0.4"
- resolved "https://registry.yarnpkg.com/stackframe/-/stackframe-1.0.4.tgz#357b24a992f9427cba6b545d96a14ed2cbca187b"
-
-stacktrace-gps@^3.0.1:
- version "3.0.2"
- resolved "https://registry.yarnpkg.com/stacktrace-gps/-/stacktrace-gps-3.0.2.tgz#33f8baa4467323ab2bd1816efa279942ba431ccc"
- dependencies:
- source-map "0.5.6"
- stackframe "^1.0.4"
-
-stacktrace-js@^2.0.0:
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/stacktrace-js/-/stacktrace-js-2.0.0.tgz#776ca646a95bc6c6b2b90776536a7fc72c6ddb58"
- dependencies:
- error-stack-parser "^2.0.1"
- stack-generator "^2.0.1"
- stacktrace-gps "^3.0.1"
-
staged-git-files@1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/staged-git-files/-/staged-git-files-1.1.1.tgz#37c2218ef0d6d26178b1310719309a16a59f8f7b"
diff --git a/zanata.xml b/zanata.xml
new file mode 100644
index 000000000..d68b3a3ba
--- /dev/null
+++ b/zanata.xml
@@ -0,0 +1,15 @@
+
+
+ https://trad.framasoft.org/zanata/
+ peertube
+ develop
+ xliff
+ ./client/src/locale/source
+ ./client/src/locale/target
+
+
+
+ ./scripts/i18n/pull-hook.sh
+
+
+