From e945b184a0f29b47c33bbd05578f3493ca9c8e6c Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Wed, 6 Jun 2018 14:23:40 +0200 Subject: [PATCH] Localize player --- .../+video-watch/video-watch.component.ts | 19 +- .../src/assets/player/peertube-link-button.ts | 2 +- client/src/assets/player/peertube-player.ts | 67 ++-- .../assets/player/peertube-videojs-plugin.ts | 10 +- .../assets/player/resolution-menu-button.ts | 7 +- .../src/assets/player/settings-menu-button.ts | 2 +- .../src/assets/player/settings-menu-item.ts | 4 +- client/src/assets/player/utils.ts | 2 + .../assets/player/webtorrent-info-button.ts | 7 +- client/src/locale/source/player_en_US.xml | 378 +++++++++++++++++ client/src/locale/source/videojs_en_US.json | 85 ++++ client/src/locale/target/player_fr.json | 1 + client/src/locale/target/player_fr.xml | 379 ++++++++++++++++++ client/src/standalone/videos/embed.ts | 12 +- package.json | 3 + scripts/i18n/create-custom-files.ts | 49 +++ scripts/i18n/generate.sh | 6 +- scripts/i18n/pull-hook.sh | 6 +- scripts/i18n/xliff2json.ts | 42 ++ scripts/watch/server.sh | 5 + server/controllers/client.ts | 8 + shared/models/i18n/i18n.ts | 4 + yarn.lock | 12 + 23 files changed, 1049 insertions(+), 61 deletions(-) create mode 100644 client/src/locale/source/player_en_US.xml create mode 100644 client/src/locale/source/videojs_en_US.json create mode 100644 client/src/locale/target/player_fr.json create mode 100644 client/src/locale/target/player_fr.xml create mode 100755 scripts/i18n/create-custom-files.ts create mode 100755 scripts/i18n/xliff2json.ts 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 23d74494c..d3e16c4cf 100644 --- a/client/src/app/videos/+video-watch/video-watch.component.ts +++ b/client/src/app/videos/+video-watch/video-watch.component.ts @@ -1,5 +1,5 @@ import { catchError } from 'rxjs/operators' -import { Component, ElementRef, NgZone, OnDestroy, OnInit, ViewChild } from '@angular/core' +import { Component, ElementRef, LOCALE_ID, NgZone, OnDestroy, OnInit, ViewChild, Inject } from '@angular/core' import { ActivatedRoute, Router } from '@angular/router' import { RedirectService } from '@app/core/routing/redirect.service' import { peertubeLocalStorage } from '@app/shared/misc/peertube-local-storage' @@ -21,9 +21,10 @@ import { MarkdownService } from '../shared' import { VideoDownloadComponent } from './modal/video-download.component' import { VideoReportComponent } from './modal/video-report.component' import { VideoShareComponent } from './modal/video-share.component' -import { getVideojsOptions } from '../../../assets/player/peertube-player' +import { getVideojsOptions, loadLocale, addContextMenu } from '../../../assets/player/peertube-player' import { ServerService } from '@app/core' import { I18n } from '@ngx-translate/i18n-polyfill' +import { environment } from '../../../environments/environment' @Component({ selector: 'my-video-watch', @@ -54,6 +55,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy { likesBarTooltipText = '' hasAlreadyAcceptedPrivacyConcern = false + private videojsLocaleLoaded = false private otherVideos: Video[] = [] private paramsSub: Subscription @@ -72,7 +74,8 @@ export class VideoWatchComponent implements OnInit, OnDestroy { private markdownService: MarkdownService, private zone: NgZone, private redirectService: RedirectService, - private i18n: I18n + private i18n: I18n, + @Inject(LOCALE_ID) private localeId: string ) {} get user () { @@ -365,7 +368,6 @@ export class VideoWatchComponent implements OnInit, OnDestroy { inactivityTimeout: 2500, videoFiles: this.video.files, playerElement: this.playerElement, - videoEmbedUrl: this.video.embedUrl, videoViewUrl: this.videoService.getVideoViewUrl(this.video.uuid), videoDuration: this.video.duration, enableHotkeys: true, @@ -374,11 +376,18 @@ export class VideoWatchComponent implements OnInit, OnDestroy { startTime }) + if (this.videojsLocaleLoaded === false) { + await loadLocale(environment.apiUrl, videojs, environment.production === true ? this.localeId : 'fr') + this.videojsLocaleLoaded = true + } + const self = this - this.zone.runOutsideAngular(() => { + this.zone.runOutsideAngular(async () => { videojs(this.playerElement, videojsOptions, function () { self.player = this this.on('customError', (event, data) => self.handleError(data.err)) + + addContextMenu(self.player, self.video.embedUrl) }) }) diff --git a/client/src/assets/player/peertube-link-button.ts b/client/src/assets/player/peertube-link-button.ts index a13815d61..26f8b9d73 100644 --- a/client/src/assets/player/peertube-link-button.ts +++ b/client/src/assets/player/peertube-link-button.ts @@ -24,7 +24,7 @@ class PeerTubeLinkButton extends Button { const el = videojsUntyped.dom.createEl('a', { href: buildVideoLink(), innerHTML: 'PeerTube', - title: 'Go to the video page', + title: this.player_.localize('Go to the video page'), className: 'vjs-peertube-link', target: '_blank' }) diff --git a/client/src/assets/player/peertube-player.ts b/client/src/assets/player/peertube-player.ts index d204b9703..b604097fa 100644 --- a/client/src/assets/player/peertube-player.ts +++ b/client/src/assets/player/peertube-player.ts @@ -12,6 +12,7 @@ import './peertube-videojs-plugin' import './peertube-load-progress-bar' import { videojsUntyped } from './peertube-videojs-typings' import { buildVideoEmbed, buildVideoLink, copyToClipboard } from './utils' +import { is18nLocale, isDefaultLocale } from '../../../../shared/models/i18n/i18n' // Change 'Playback Rate' to 'Speed' (smaller for our settings menu) videojsUntyped.getComponent('PlaybackRateMenuButton').prototype.controlText_ = 'Speed' @@ -20,7 +21,6 @@ function getVideojsOptions (options: { autoplay: boolean, playerElement: HTMLVideoElement, videoViewUrl: string, - videoEmbedUrl: string, videoDuration: number, videoFiles: VideoFile[], enableHotkeys: boolean, @@ -43,29 +43,6 @@ function getVideojsOptions (options: { videoViewUrl: options.videoViewUrl, videoDuration: options.videoDuration, startTime: options.startTime - }, - contextmenuUI: { - content: [ - { - label: 'Copy the video URL', - listener: function () { - copyToClipboard(buildVideoLink()) - } - }, - { - label: 'Copy the video URL at the current time', - listener: function () { - const player = this - copyToClipboard(buildVideoLink(player.currentTime())) - } - }, - { - label: 'Copy embed code', - listener: () => { - copyToClipboard(buildVideoEmbed(options.videoEmbedUrl)) - } - } - ] } }, controlBar: { @@ -135,4 +112,44 @@ function getControlBarChildren (options: { return children } -export { getVideojsOptions } +function addContextMenu (player: any, videoEmbedUrl: string) { + console.log(videoEmbedUrl) + + player.contextmenuUI({ + content: [ + { + label: player.localize('Copy the video URL'), + listener: function () { + copyToClipboard(buildVideoLink()) + } + }, + { + label: player.localize('Copy the video URL at the current time'), + listener: function () { + const player = this + copyToClipboard(buildVideoLink(player.currentTime())) + } + }, + { + label: player.localize('Copy embed code'), + listener: () => { + copyToClipboard(buildVideoEmbed(videoEmbedUrl)) + } + } + ] + }) +} + +function loadLocale (serverUrl: string, videojs: any, locale: string) { + if (!is18nLocale(locale) || isDefaultLocale(locale)) return undefined + + return fetch(serverUrl + '/client/locales/' + locale + '/player.json') + .then(res => res.json()) + .then(json => videojs.addLanguage(locale, json)) +} + +export { + loadLocale, + getVideojsOptions, + addContextMenu +} diff --git a/client/src/assets/player/peertube-videojs-plugin.ts b/client/src/assets/player/peertube-videojs-plugin.ts index 79df42a53..68e98f170 100644 --- a/client/src/assets/player/peertube-videojs-plugin.ts +++ b/client/src/assets/player/peertube-videojs-plugin.ts @@ -4,15 +4,7 @@ import { VideoFile } from '../../../../shared/models/videos/video.model' import { renderVideo } from './video-renderer' import './settings-menu-button' import { PeertubePluginOptions, VideoJSComponentInterface, videojsUntyped } from './peertube-videojs-typings' -import { - getAverageBandwidth, - getStoredMute, - getStoredVolume, - isMobile, - saveAverageBandwidth, - saveMuteInStore, - saveVolumeInStore -} from './utils' +import { getAverageBandwidth, getStoredMute, getStoredVolume, saveAverageBandwidth, saveMuteInStore, saveVolumeInStore } from './utils' import minBy from 'lodash-es/minBy' import maxBy from 'lodash-es/maxBy' import * as CacheChunkStore from 'cache-chunk-store' diff --git a/client/src/assets/player/resolution-menu-button.ts b/client/src/assets/player/resolution-menu-button.ts index 2efc8de69..d317a5efc 100644 --- a/client/src/assets/player/resolution-menu-button.ts +++ b/client/src/assets/player/resolution-menu-button.ts @@ -8,10 +8,7 @@ class ResolutionMenuButton extends MenuButton { label: HTMLElement constructor (player: videojs.Player, options) { - options.label = 'Quality' super(player, options) - - this.controlText_ = 'Quality' this.player = player player.peertube().on('videoFileUpdate', () => this.updateLabel()) @@ -51,7 +48,7 @@ class ResolutionMenuButton extends MenuButton { this.player_, { id: -1, - label: 'Auto', + label: this.player_.localize('Auto'), src: null } )) @@ -77,4 +74,6 @@ class ResolutionMenuButton extends MenuButton { return this.player_.peertube().getCurrentResolutionLabel() } } +ResolutionMenuButton.prototype.controlText_ = 'Quality' + MenuButton.registerComponent('ResolutionMenuButton', ResolutionMenuButton) diff --git a/client/src/assets/player/settings-menu-button.ts b/client/src/assets/player/settings-menu-button.ts index bf6ac145a..b51c52506 100644 --- a/client/src/assets/player/settings-menu-button.ts +++ b/client/src/assets/player/settings-menu-button.ts @@ -275,7 +275,7 @@ class SettingsDialog extends Component { } -SettingsButton.prototype.controlText_ = 'Settings Button' +SettingsButton.prototype.controlText_ = 'Settings' Component.registerComponent('SettingsButton', SettingsButton) Component.registerComponent('SettingsDialog', SettingsDialog) diff --git a/client/src/assets/player/settings-menu-item.ts b/client/src/assets/player/settings-menu-item.ts index 048c88533..f595fd459 100644 --- a/client/src/assets/player/settings-menu-item.ts +++ b/client/src/assets/player/settings-menu-item.ts @@ -132,7 +132,7 @@ class SettingsMenuItem extends MenuItem { const button = this.subMenu.menu.addChild('MenuItem', {}, 0) button.name_ = 'BackButton' button.addClass('vjs-back-button') - button.el_.innerHTML = this.subMenu.controlText_ + button.el_.innerHTML = this.player_.localize(this.subMenu.controlText_) } /** @@ -201,7 +201,7 @@ class SettingsMenuItem extends MenuItem { saveUpdateLabel.call(this.subMenu) } - this.settingsSubMenuTitleEl_.innerHTML = this.subMenu.controlText_ + this.settingsSubMenuTitleEl_.innerHTML = this.player_.localize(this.subMenu.controlText_) this.settingsSubMenuEl_.appendChild(this.subMenu.menu.el_) this.panelChildEl.appendChild(this.settingsSubMenuEl_) this.update() diff --git a/client/src/assets/player/utils.ts b/client/src/assets/player/utils.ts index 487b3a1be..ce7aaea2a 100644 --- a/client/src/assets/player/utils.ts +++ b/client/src/assets/player/utils.ts @@ -1,3 +1,5 @@ +import { is18nLocale, isDefaultLocale } from '../../../../shared/models/i18n/i18n' + function toTitleCase (str: string) { return str.charAt(0).toUpperCase() + str.slice(1) } diff --git a/client/src/assets/player/webtorrent-info-button.ts b/client/src/assets/player/webtorrent-info-button.ts index baeb22b64..10945c665 100644 --- a/client/src/assets/player/webtorrent-info-button.ts +++ b/client/src/assets/player/webtorrent-info-button.ts @@ -60,13 +60,8 @@ class WebtorrentInfoButton extends Button { className: 'peers-number', textContent: 'HTTP' }) - const subDivFallbackText = videojsUntyped.dom.createEl('span', { - className: 'peers-text', - textContent: ' fallback' - }) subDivHttp.appendChild(subDivHttpText) - subDivHttp.appendChild(subDivFallbackText) div.appendChild(subDivHttp) this.player_.peertube().on('torrentInfo', (event, data) => { @@ -89,7 +84,7 @@ class WebtorrentInfoButton extends Button { uploadSpeedUnit.textContent = ' ' + uploadSpeed[ 1 ] peersNumber.textContent = numPeers - peersText.textContent = ' peers' + peersText.textContent = ' ' + this.player_.localize('peers') subDivHttp.className = 'vjs-peertube-hidden' subDivWebtorrent.className = 'vjs-peertube-displayed' diff --git a/client/src/locale/source/player_en_US.xml b/client/src/locale/source/player_en_US.xml new file mode 100644 index 000000000..5bb6afdf7 --- /dev/null +++ b/client/src/locale/source/player_en_US.xml @@ -0,0 +1,378 @@ + + + + + Audio Player + undefined + + + Video Player + undefined + + + Play + undefined + + + Pause + undefined + + + Replay + undefined + + + Current Time + undefined + + + Duration + undefined + + + Remaining Time + undefined + + + Stream Type + undefined + + + LIVE + undefined + + + Loaded + undefined + + + Progress + undefined + + + Progress Bar + undefined + + + {1} of {2} + undefined + + + Fullscreen + undefined + + + Non-Fullscreen + undefined + + + Mute + undefined + + + Unmute + undefined + + + Playback Rate + undefined + + + Subtitles + undefined + + + subtitles off + undefined + + + Captions + undefined + + + captions off + undefined + + + Chapters + undefined + + + Descriptions + undefined + + + descriptions off + undefined + + + Audio Track + undefined + + + Volume Level + undefined + + + You aborted the media playback + undefined + + + A network error caused the media download to fail part-way. + undefined + + + The media could not be loaded, either because the server or network failed or because the format is not supported. + undefined + + + The media playback was aborted due to a corruption problem or because the media used features your browser did not support. + undefined + + + No compatible source was found for this media. + undefined + + + The media is encrypted and we do not have the keys to decrypt it. + undefined + + + Play Video + undefined + + + Close + undefined + + + Close Modal Dialog + undefined + + + Modal Window + undefined + + + This is a modal window + undefined + + + This modal can be closed by pressing the Escape key or activating the close button. + undefined + + + , opens captions settings dialog + undefined + + + , opens subtitles settings dialog + undefined + + + , opens descriptions settings dialog + undefined + + + , selected + undefined + + + captions settings + undefined + + + subititles settings + undefined + + + descriptions settings + undefined + + + Text + undefined + + + White + undefined + + + Black + undefined + + + Red + undefined + + + Green + undefined + + + Blue + undefined + + + Yellow + undefined + + + Magenta + undefined + + + Cyan + undefined + + + Background + undefined + + + Window + undefined + + + Transparent + undefined + + + Semi-Transparent + undefined + + + Opaque + undefined + + + Font Size + undefined + + + Text Edge Style + undefined + + + None + undefined + + + Raised + undefined + + + Depressed + undefined + + + Uniform + undefined + + + Dropshadow + undefined + + + Font Family + undefined + + + Proportional Sans-Serif + undefined + + + Monospace Sans-Serif + undefined + + + Proportional Serif + undefined + + + Monospace Serif + undefined + + + Casual + undefined + + + Script + undefined + + + Small Caps + undefined + + + Reset + undefined + + + restore all settings to the default values + undefined + + + Done + undefined + + + Caption Settings Dialog + undefined + + + Beginning of dialog window. Escape will cancel and close the window. + undefined + + + End of dialog window. + undefined + + + {1} is loading. + undefined + + + Quality + undefined + + + Auto + undefined + + + Speed + undefined + + + peers + undefined + + + Go to the video page + undefined + + + Settings + undefined + + + Uses P2P, others may know you are watching this video. + undefined + + + Copy the video URL + undefined + + + Copy the video URL at the current time + undefined + + + Copy embed code + undefined + + + + \ No newline at end of file diff --git a/client/src/locale/source/videojs_en_US.json b/client/src/locale/source/videojs_en_US.json new file mode 100644 index 000000000..92caaa683 --- /dev/null +++ b/client/src/locale/source/videojs_en_US.json @@ -0,0 +1,85 @@ +{ + "Audio Player": "Audio Player", + "Video Player": "Video Player", + "Play": "Play", + "Pause": "Pause", + "Replay": "Replay", + "Current Time": "Current Time", + "Duration": "Duration", + "Remaining Time": "Remaining Time", + "Stream Type": "Stream Type", + "LIVE": "LIVE", + "Loaded": "Loaded", + "Progress": "Progress", + "Progress Bar": "Progress Bar", + "progress bar timing: currentTime={1} duration={2}": "{1} of {2}", + "Fullscreen": "Fullscreen", + "Non-Fullscreen": "Non-Fullscreen", + "Mute": "Mute", + "Unmute": "Unmute", + "Playback Rate": "Playback Rate", + "Subtitles": "Subtitles", + "subtitles off": "subtitles off", + "Captions": "Captions", + "captions off": "captions off", + "Chapters": "Chapters", + "Descriptions": "Descriptions", + "descriptions off": "descriptions off", + "Audio Track": "Audio Track", + "Volume Level": "Volume Level", + "You aborted the media playback": "You aborted the media playback", + "A network error caused the media download to fail part-way.": "A network error caused the media download to fail part-way.", + "The media could not be loaded, either because the server or network failed or because the format is not supported.": "The media could not be loaded, either because the server or network failed or because the format is not supported.", + "The media playback was aborted due to a corruption problem or because the media used features your browser did not support.": "The media playback was aborted due to a corruption problem or because the media used features your browser did not support.", + "No compatible source was found for this media.": "No compatible source was found for this media.", + "The media is encrypted and we do not have the keys to decrypt it.": "The media is encrypted and we do not have the keys to decrypt it.", + "Play Video": "Play Video", + "Close": "Close", + "Close Modal Dialog": "Close Modal Dialog", + "Modal Window": "Modal Window", + "This is a modal window": "This is a modal window", + "This modal can be closed by pressing the Escape key or activating the close button.": "This modal can be closed by pressing the Escape key or activating the close button.", + ", opens captions settings dialog": ", opens captions settings dialog", + ", opens subtitles settings dialog": ", opens subtitles settings dialog", + ", opens descriptions settings dialog": ", opens descriptions settings dialog", + ", selected": ", selected", + "captions settings": "captions settings", + "subtitles settings": "subititles settings", + "descriptions settings": "descriptions settings", + "Text": "Text", + "White": "White", + "Black": "Black", + "Red": "Red", + "Green": "Green", + "Blue": "Blue", + "Yellow": "Yellow", + "Magenta": "Magenta", + "Cyan": "Cyan", + "Background": "Background", + "Window": "Window", + "Transparent": "Transparent", + "Semi-Transparent": "Semi-Transparent", + "Opaque": "Opaque", + "Font Size": "Font Size", + "Text Edge Style": "Text Edge Style", + "None": "None", + "Raised": "Raised", + "Depressed": "Depressed", + "Uniform": "Uniform", + "Dropshadow": "Dropshadow", + "Font Family": "Font Family", + "Proportional Sans-Serif": "Proportional Sans-Serif", + "Monospace Sans-Serif": "Monospace Sans-Serif", + "Proportional Serif": "Proportional Serif", + "Monospace Serif": "Monospace Serif", + "Casual": "Casual", + "Script": "Script", + "Small Caps": "Small Caps", + "Reset": "Reset", + "restore all settings to the default values": "restore all settings to the default values", + "Done": "Done", + "Caption Settings Dialog": "Caption Settings Dialog", + "Beginning of dialog window. Escape will cancel and close the window.": "Beginning of dialog window. Escape will cancel and close the window.", + "End of dialog window.": "End of dialog window.", + "{1} is loading.": "{1} is loading." +} diff --git a/client/src/locale/target/player_fr.json b/client/src/locale/target/player_fr.json new file mode 100644 index 000000000..6c399fc3f --- /dev/null +++ b/client/src/locale/target/player_fr.json @@ -0,0 +1 @@ +{"Audio Player":"Lecteur audio","Video Player":"Lecteur vidéo","Play":"Lecture","Pause":"Pause","Replay":"Revoir","Current Time":"Temps actuel","Duration":"Durée","Remaining Time":"Temps restant","Stream Type":"Type de flux","LIVE":"EN DIRECT","Loaded":"Chargé","Progress":"Progression","Progress Bar":"Barre de progression","progress bar timing: currentTime={1} duration={2}":"{1} de {2}","Fullscreen":"Plein écran","Non-Fullscreen":"Fenêtré","Mute":"Sourdine","Unmute":"Son activé","Playback Rate":"Vitesse de lecture","Subtitles":"Sous-titres","subtitles off":"Sous-titres désactivés","Captions":"Sous-titres transcrits","captions off":"Sous-titres transcrits désactivés","Chapters":"Chapitres","Descriptions":"Descriptions","descriptions off":"descriptions désactivées","Audio Track":"Piste audio","Volume Level":"Niveau de volume","You aborted the media playback":"Vous avez interrompu la lecture de la vidéo.","A network error caused the media download to fail part-way.":"Une erreur de réseau a interrompu le téléchargement de la vidéo.","The media could not be loaded, either because the server or network failed or because the format is not supported.":"Cette vidéo n'a pas pu être chargée, soit parce que le serveur ou le réseau a échoué ou parce que le format n'est pas reconnu.","The media playback was aborted due to a corruption problem or because the media used features your browser did not support.":"La lecture de la vidéo a été interrompue à cause d'un problème de corruption ou parce que la vidéo utilise des fonctionnalités non prises en charge par votre navigateur.","No compatible source was found for this media.":"Aucune source compatible n'a été trouvée pour cette vidéo.","The media is encrypted and we do not have the keys to decrypt it.":"Le média est chiffré et nous n'avons pas les clés pour le déchiffrer.","Play Video":"Lire la vidéo","Close":"Fermer","Close Modal Dialog":"Fermer la boîte de dialogue modale","Modal Window":"Fenêtre modale","This is a modal window":"Ceci est une fenêtre modale","This modal can be closed by pressing the Escape key or activating the close button.":"Ce modal peut être fermé en appuyant sur la touche Échap ou activer le bouton de fermeture.",", opens captions settings dialog":", ouvrir les paramètres des sous-titres transcrits",", opens subtitles settings dialog":", ouvrir les paramètres des sous-titres",", opens descriptions settings dialog":", ouvrir les paramètres des descriptions",", selected":", sélectionné","captions settings":"Paramètres des sous-titres transcrits","subtitles settings":"Paramètres des sous-titres","descriptions settings":"Paramètres des descriptions","Text":"Texte","White":"Blanc","Black":"Noir","Red":"Rouge","Green":"Vert","Blue":"Bleu","Yellow":"Jaune","Magenta":"Magenta","Cyan":"Cyan","Background":"Arrière-plan","Window":"Fenêtre","Transparent":"Transparent","Semi-Transparent":"Semi-transparent","Opaque":"Opaque","Font Size":"Taille des caractères","Text Edge Style":"Style des contours du texte","None":"Aucun","Raised":"Élevé","Depressed":"Enfoncé","Uniform":"Uniforme","Dropshadow":"Ombre portée","Font Family":"Familles de polices","Proportional Sans-Serif":"Polices à chasse variable sans empattement (Proportional Sans-Serif)","Monospace Sans-Serif":"Polices à chasse fixe sans empattement (Monospace Sans-Serif)","Proportional Serif":"Polices à chasse variable avec empattement (Proportional Serif)","Monospace Serif":"Polices à chasse fixe avec empattement (Monospace Serif)","Casual":"Manuscrite","Script":"Scripte","Small Caps":"Petites capitales","Reset":"Réinitialiser","restore all settings to the default values":"Restaurer tous les paramètres aux valeurs par défaut","Done":"Terminé","Caption Settings Dialog":"Boîte de dialogue des paramètres des sous-titres transcrits","Beginning of dialog window. Escape will cancel and close the window.":"Début de la fenêtre de dialogue. La touche d'échappement annulera et fermera la fenêtre.","End of dialog window.":"Fin de la fenêtre de dialogue.","{1} is loading.":"{1} est en train de charger","Quality":"Qualité","Auto":"Auto","Speed":"Vitesse","peers":"pairs","Go to the video page":"Aller sur la page de la vidéo","Settings":"Paramètres","Uses P2P, others may know you are watching this video.":"Utilise le P2P, d'autres personnes pourraient savoir que vous regardez cette vidéo.","Copy the video URL":"Copier le lien de la vidéo","Copy the video URL at the current time":"Copier le lien de la vidéo à partir de cette séquence","Copy embed code":"Copier le code d'intégration"} \ No newline at end of file diff --git a/client/src/locale/target/player_fr.xml b/client/src/locale/target/player_fr.xml new file mode 100644 index 000000000..eafa4baff --- /dev/null +++ b/client/src/locale/target/player_fr.xml @@ -0,0 +1,379 @@ + + + + + + + Audio Player + Lecteur audio + + + Video Player + Lecteur vidéo + + + Play + Lecture + + + Pause + Pause + + + Replay + Revoir + + + Current Time + Temps actuel + + + Duration + Durée + + + Remaining Time + Temps restant + + + Stream Type + Type de flux + + + LIVE + EN DIRECT + + + Loaded + Chargé + + + Progress + Progression + + + Progress Bar + Barre de progression + + + {1} of {2} + {1} de {2} + + + Fullscreen + Plein écran + + + Non-Fullscreen + Fenêtré + + + Mute + Sourdine + + + Unmute + Son activé + + + Playback Rate + Vitesse de lecture + + + Subtitles + Sous-titres + + + subtitles off + Sous-titres désactivés + + + Captions + Sous-titres transcrits + + + captions off + Sous-titres transcrits désactivés + + + Chapters + Chapitres + + + Descriptions + Descriptions + + + descriptions off + descriptions désactivées + + + Audio Track + Piste audio + + + Volume Level + Niveau de volume + + + You aborted the media playback + Vous avez interrompu la lecture de la vidéo. + + + A network error caused the media download to fail part-way. + Une erreur de réseau a interrompu le téléchargement de la vidéo. + + + The media could not be loaded, either because the server or network failed or because the format is not supported. + Cette vidéo n'a pas pu être chargée, soit parce que le serveur ou le réseau a échoué ou parce que le format n'est pas reconnu. + + + The media playback was aborted due to a corruption problem or because the media used features your browser did not support. + La lecture de la vidéo a été interrompue à cause d'un problème de corruption ou parce que la vidéo utilise des fonctionnalités non prises en charge par votre navigateur. + + + No compatible source was found for this media. + Aucune source compatible n'a été trouvée pour cette vidéo. + + + The media is encrypted and we do not have the keys to decrypt it. + Le média est chiffré et nous n'avons pas les clés pour le déchiffrer. + + + Play Video + Lire la vidéo + + + Close + Fermer + + + Close Modal Dialog + Fermer la boîte de dialogue modale + + + Modal Window + Fenêtre modale + + + This is a modal window + Ceci est une fenêtre modale + + + This modal can be closed by pressing the Escape key or activating the close button. + Ce modal peut être fermé en appuyant sur la touche Échap ou activer le bouton de fermeture. + + + , opens captions settings dialog + , ouvrir les paramètres des sous-titres transcrits + + + , opens subtitles settings dialog + , ouvrir les paramètres des sous-titres + + + , opens descriptions settings dialog + , ouvrir les paramètres des descriptions + + + , selected + , sélectionné + + + captions settings + Paramètres des sous-titres transcrits + + + subititles settings + Paramètres des sous-titres + + + descriptions settings + Paramètres des descriptions + + + Text + Texte + + + White + Blanc + + + Black + Noir + + + Red + Rouge + + + Green + Vert + + + Blue + Bleu + + + Yellow + Jaune + + + Magenta + Magenta + + + Cyan + Cyan + + + Background + Arrière-plan + + + Window + Fenêtre + + + Transparent + Transparent + + + Semi-Transparent + Semi-transparent + + + Opaque + Opaque + + + Font Size + Taille des caractères + + + Text Edge Style + Style des contours du texte + + + None + Aucun + + + Raised + Élevé + + + Depressed + Enfoncé + + + Uniform + Uniforme + + + Dropshadow + Ombre portée + + + Font Family + Familles de polices + + + Proportional Sans-Serif + Polices à chasse variable sans empattement (Proportional Sans-Serif) + + + Monospace Sans-Serif + Polices à chasse fixe sans empattement (Monospace Sans-Serif) + + + Proportional Serif + Polices à chasse variable avec empattement (Proportional Serif) + + + Monospace Serif + Polices à chasse fixe avec empattement (Monospace Serif) + + + Casual + Manuscrite + + + Script + Scripte + + + Small Caps + Petites capitales + + + Reset + Réinitialiser + + + restore all settings to the default values + Restaurer tous les paramètres aux valeurs par défaut + + + Done + Terminé + + + Caption Settings Dialog + Boîte de dialogue des paramètres des sous-titres transcrits + + + Beginning of dialog window. Escape will cancel and close the window. + Début de la fenêtre de dialogue. La touche d'échappement annulera et fermera la fenêtre. + + + End of dialog window. + Fin de la fenêtre de dialogue. + + + {1} is loading. + {1} est en train de charger + + + Quality + Qualité + + + Auto + Auto + + + Speed + Vitesse + + + peers + pairs + + + Go to the video page + Aller sur la page de la vidéo + + + Settings + Paramètres + + + Uses P2P, others may know you are watching this video. + Utilise le P2P, d'autres personnes pourraient savoir que vous regardez cette vidéo. + + + Copy the video URL + Copier le lien de la vidéo + + + Copy the video URL at the current time + Copier le lien de la vidéo à partir de cette séquence + + + Copy embed code + Copier le code d'intégration + + + \ No newline at end of file diff --git a/client/src/standalone/videos/embed.ts b/client/src/standalone/videos/embed.ts index d603690ca..166013226 100644 --- a/client/src/standalone/videos/embed.ts +++ b/client/src/standalone/videos/embed.ts @@ -14,14 +14,14 @@ import 'core-js/es6/regexp' import 'core-js/es6/map' import 'core-js/es6/weak-map' import 'core-js/es6/set' - // For google bot that uses Chrome 41 and does not understand fetch import 'whatwg-fetch' import * as videojs from 'video.js' import { VideoDetails } from '../../../../shared' -import { getVideojsOptions } from '../../assets/player/peertube-player' +import { addContextMenu, getVideojsOptions, loadLocale } from '../../assets/player/peertube-player' +import { environment } from '../../environments/environment' function getVideoUrl (id: string) { return window.location.origin + '/api/v1/videos/' + id @@ -61,7 +61,8 @@ function videoFetchError (videoElement: HTMLVideoElement) { const urlParts = window.location.href.split('/') const videoId = urlParts[urlParts.length - 1] -loadVideoInfo(videoId) +loadLocale(environment.apiUrl, videojs, navigator.language) + .then(() => loadVideoInfo(videoId)) .then(async response => { const videoContainerId = 'video-container' const videoElement = document.getElementById(videoContainerId) as HTMLVideoElement @@ -91,7 +92,6 @@ loadVideoInfo(videoId) const videojsOptions = getVideojsOptions({ autoplay, inactivityTimeout: 1500, - videoEmbedUrl: window.location.origin + videoInfo.embedPath, videoViewUrl: getVideoUrl(videoId) + '/views', playerElement: videoElement, videoFiles: videoInfo.files, @@ -106,8 +106,10 @@ loadVideoInfo(videoId) player.dock({ title: videoInfo.name, - description: 'Uses P2P, others may know you are watching this video.' + description: player.localize('Uses P2P, others may know you are watching this video.') }) + + addContextMenu(player, window.location.origin + videoInfo.embedPath) }) }) .catch(err => console.error(err)) diff --git a/package.json b/package.json index 21701e664..707579af3 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,8 @@ "danger:clean:prod": "scripty", "danger:clean:modules": "scripty", "i18n:generate": "scripty", + "i18n:xliff2json": "node ./dist/scripts/i18n/xliff2json.js", + "i18n:create-custom-files": "node ./dist/scripts/i18n/create-custom-files.js", "reset-password": "node ./dist/scripts/reset-password.js", "play": "scripty", "dev": "scripty", @@ -174,6 +176,7 @@ "tslint-config-standard": "^7.0.0", "typescript": "^2.5.2", "webtorrent": "^0.100.0", + "xliff": "^3.0.1", "youtube-dl": "^1.12.2" }, "scripty": { diff --git a/scripts/i18n/create-custom-files.ts b/scripts/i18n/create-custom-files.ts new file mode 100755 index 000000000..3895b3b9d --- /dev/null +++ b/scripts/i18n/create-custom-files.ts @@ -0,0 +1,49 @@ +import * as jsToXliff12 from 'xliff/jsToXliff12' +import { writeFile } from 'fs' +import { join } from 'path' + +// First, the player +const playerSource = join(__dirname, '../../../client/src/locale/source/videojs_en_US.json') +const playerTarget = join(__dirname, '../../../client/src/locale/source/player_en_US.xml') + +const videojs = require(playerSource) +const playerKeys = { + 'Quality': 'Quality', + 'Auto': 'Auto', + 'Speed': 'Speed', + 'peers': 'peers', + 'Go to the video page': 'Go to the video page', + 'Settings': 'Settings', + 'Uses P2P, others may know you are watching this video.': 'Uses P2P, others may know you are watching this video.', + 'Copy the video URL': 'Copy the video URL', + 'Copy the video URL at the current time': 'Copy the video URL at the current time', + 'Copy embed code': 'Copy embed code' +} + +const obj = { + resources: { + namespace1: {} + } +} + +for (const sourceObject of [ videojs, playerKeys ]) { + Object.keys(sourceObject).forEach(k => obj.resources.namespace1[ k ] = { source: sourceObject[ k ] }) +} + +jsToXliff12(obj, (err, res) => { + if (err) { + console.error(err) + process.exit(-1) + } + + writeFile(playerTarget, res, err => { + if (err) { + console.error(err) + process.exit(-1) + } + + process.exit(0) + }) +}) + +// Then, the server strings diff --git a/scripts/i18n/generate.sh b/scripts/i18n/generate.sh index f8ad8a3c1..0a5b6dee1 100755 --- a/scripts/i18n/generate.sh +++ b/scripts/i18n/generate.sh @@ -9,4 +9,8 @@ npm run ngx-extractor -- --locale "en-US" -i 'src/**/*.ts' -f xlf -o src/locale/ # 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 -perl -pi -e 's||<x id=\1/>|g' src/locale/source/messages_en_US.xml \ No newline at end of file +perl -pi -e 's||<x id=\1/>|g' src/locale/source/messages_en_US.xml + +# Add our strings too +cd ../ +npm run i18n:create-custom-files \ No newline at end of file diff --git a/scripts/i18n/pull-hook.sh b/scripts/i18n/pull-hook.sh index bbe4a813e..dec426b88 100755 --- a/scripts/i18n/pull-hook.sh +++ b/scripts/i18n/pull-hook.sh @@ -7,5 +7,7 @@ set -eu #sed -i 's/\<x id=\(.\+\?\)\/\>//g' client/src/locale/target/* for i in 1 2 3; do - perl -pi -e 's|<x id=(.+?)/>([^"])|\2|g' client/src/locale/target/* -done \ No newline at end of file + perl -pi -e 's|<x id=(.+?)/>([^"])|\2|g' client/src/locale/target/*.xml +done + +npm run i18n:xliff2json \ No newline at end of file diff --git a/scripts/i18n/xliff2json.ts b/scripts/i18n/xliff2json.ts new file mode 100755 index 000000000..34784ac11 --- /dev/null +++ b/scripts/i18n/xliff2json.ts @@ -0,0 +1,42 @@ +import * as xliff12ToJs from 'xliff/xliff12ToJs' +import { readFileSync, writeFile } from 'fs' +import { join } from 'path' + +// First, the player +const playerSource = join(__dirname, '../../../client/src/locale/target/player_fr.xml') +const playerTarget = join(__dirname, '../../../client/src/locale/target/player_fr.json') + +// Remove the two first lines our xliff module does not like +let playerFile = readFileSync(playerSource).toString() +playerFile = removeFirstLine(playerFile) +playerFile = removeFirstLine(playerFile) + +xliff12ToJs(playerFile, (err, res) => { + if (err) { + console.error(err) + process.exit(-1) + } + + const json = createJSONString(res) + writeFile(playerTarget, json, err => { + if (err) { + console.error(err) + process.exit(-1) + } + + process.exit(0) + }) +}) + +function removeFirstLine (str: string) { + return str.substring(str.indexOf('\n') + 1) +} + +function createJSONString (obj: any) { + const res: any = {} + const strings = obj.resources[''] + + Object.keys(strings).forEach(k => res[k] = strings[k].target) + + return JSON.stringify(res) +} diff --git a/scripts/watch/server.sh b/scripts/watch/server.sh index 6250fb9a4..badbf3da0 100755 --- a/scripts/watch/server.sh +++ b/scripts/watch/server.sh @@ -2,6 +2,11 @@ set -eu +# Copy locales +mkdir -p "./client/dist" +rm -r "./client/dist/locale" +cp -r "./client/src/locale/target" "./client/dist/locale" + NODE_ENV=test concurrently -k \ "npm run tsc -- --sourceMap && npm run nodemon -- --delay 2 --watch ./dist dist/server" \ "npm run tsc -- --sourceMap --preserveWatchOutput -w" diff --git a/server/controllers/client.ts b/server/controllers/client.ts index 4b37b5fa6..b153f6086 100644 --- a/server/controllers/client.ts +++ b/server/controllers/client.ts @@ -47,6 +47,14 @@ for (const staticClientFile of staticClientFiles) { clientsRouter.use('/client', express.static(distPath, { maxAge: STATIC_MAX_AGE })) clientsRouter.use('/client/assets/images', express.static(assetsImagesPath, { maxAge: STATIC_MAX_AGE })) +clientsRouter.use('/client/locales/:locale/:file.json', function (req, res) { + if (req.params.locale === 'fr' && req.params.file === 'player') { + return res.sendFile(join(__dirname, '../../../client/dist/locale/player_fr.json')) + } + + return res.sendStatus(404) +}) + // 404 for static files not found clientsRouter.use('/client/*', (req: express.Request, res: express.Response, next: express.NextFunction) => { res.sendStatus(404) diff --git a/shared/models/i18n/i18n.ts b/shared/models/i18n/i18n.ts index 2d3a1d3e2..4d50bc36e 100644 --- a/shared/models/i18n/i18n.ts +++ b/shared/models/i18n/i18n.ts @@ -7,6 +7,10 @@ export function getDefaultLocale () { return 'en-US' } +export function isDefaultLocale (locale: string) { + return locale === getDefaultLocale() +} + const possiblePaths = Object.keys(I18N_LOCALES).map(l => '/' + l) export function is18nPath (path: string) { return possiblePaths.indexOf(path) !== -1 diff --git a/yarn.lock b/yarn.lock index eb06faac0..3b6c7574e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8402,6 +8402,18 @@ xhr2@^0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/xhr2/-/xhr2-0.1.4.tgz#7f87658847716db5026323812f818cadab387a5f" +xliff@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/xliff/-/xliff-3.0.1.tgz#ea0f5840011727aecbddf111e5c26d8590dcca9b" + dependencies: + xml-js "1.6.2" + +xml-js@1.6.2: + version "1.6.2" + resolved "https://registry.yarnpkg.com/xml-js/-/xml-js-1.6.2.tgz#4c4cb8413998f73701a202a1b8b2f17c985a72c5" + dependencies: + sax "^1.2.4" + xml-name-validator@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-3.0.0.tgz#6ae73e06de4d8c6e47f9fb181f78d648ad457c6a" -- 2.41.0