mirror of
https://github.com/Chocobozzz/PeerTube.git
synced 2024-08-17 17:30:32 +03:00
Add ability to choose the language
This commit is contained in:
parent
3ea9a1c311
commit
8afc19a612
@ -18,9 +18,7 @@
|
||||
</div>
|
||||
|
||||
<div class="sub-header-container">
|
||||
<div *ngIf="isMenuDisplayed" class="title-menu-left">
|
||||
<my-menu></my-menu>
|
||||
</div>
|
||||
<my-menu *ngIf="isMenuDisplayed"></my-menu>
|
||||
|
||||
<div class="main-col container-fluid" [ngClass]="{ expanded: isMenuDisplayed === false }">
|
||||
|
||||
|
@ -9,17 +9,6 @@
|
||||
margin-top: $header-height;
|
||||
}
|
||||
|
||||
.title-menu-left {
|
||||
position: fixed;
|
||||
height: calc(100vh - #{$header-height});
|
||||
padding: 0;
|
||||
width: $menu-width;
|
||||
|
||||
.title-menu-left-block.menu {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.header {
|
||||
height: $header-height;
|
||||
position: fixed;
|
||||
|
@ -17,6 +17,7 @@ import { SignupModule } from './signup'
|
||||
import { VideosModule } from './videos'
|
||||
import { buildFileLocale, getCompleteLocale, isDefaultLocale } from '../../../shared/models/i18n'
|
||||
import { getDevLocale, isOnDevLocale } from '@app/shared/i18n/i18n-utils'
|
||||
import { LanguageChooserComponent } from '@app/menu/language-chooser.component'
|
||||
|
||||
export function metaFactory (serverService: ServerService): MetaLoader {
|
||||
return new MetaStaticLoader({
|
||||
@ -36,6 +37,7 @@ export function metaFactory (serverService: ServerService): MetaLoader {
|
||||
AppComponent,
|
||||
|
||||
MenuComponent,
|
||||
LanguageChooserComponent,
|
||||
HeaderComponent
|
||||
],
|
||||
imports: [
|
||||
|
15
client/src/app/menu/language-chooser.component.html
Normal file
15
client/src/app/menu/language-chooser.component.html
Normal file
@ -0,0 +1,15 @@
|
||||
<div bsModal #modal="bs-modal" class="modal" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
|
||||
<div class="modal-header">
|
||||
<span class="close" aria-hidden="true" (click)="hide()"></span>
|
||||
<h4 i18n class="modal-title">Change the language</h4>
|
||||
</div>
|
||||
|
||||
<div class="modal-body" *ngFor="let lang of languages">
|
||||
<a [href]="buildLanguageLink(lang)">{{ lang.label }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
15
client/src/app/menu/language-chooser.component.scss
Normal file
15
client/src/app/menu/language-chooser.component.scss
Normal file
@ -0,0 +1,15 @@
|
||||
@import '_variables';
|
||||
@import '_mixins';
|
||||
|
||||
.modal-title {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
text-align: center;
|
||||
|
||||
a {
|
||||
font-size: 16px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
}
|
32
client/src/app/menu/language-chooser.component.ts
Normal file
32
client/src/app/menu/language-chooser.component.ts
Normal file
@ -0,0 +1,32 @@
|
||||
import { Component, ViewChild } from '@angular/core'
|
||||
import { ModalDirective } from 'ngx-bootstrap/modal'
|
||||
import { I18N_LOCALES } from '../../../../shared'
|
||||
|
||||
@Component({
|
||||
selector: 'my-language-chooser',
|
||||
templateUrl: './language-chooser.component.html',
|
||||
styleUrls: [ './language-chooser.component.scss' ]
|
||||
})
|
||||
export class LanguageChooserComponent {
|
||||
@ViewChild('modal') modal: ModalDirective
|
||||
|
||||
languages: { [ id: string ]: string }[] = []
|
||||
|
||||
constructor () {
|
||||
this.languages = Object.keys(I18N_LOCALES)
|
||||
.map(k => ({ id: k, label: I18N_LOCALES[k] }))
|
||||
}
|
||||
|
||||
show () {
|
||||
this.modal.show()
|
||||
}
|
||||
|
||||
hide () {
|
||||
this.modal.hide()
|
||||
}
|
||||
|
||||
buildLanguageLink (lang: { id: string }) {
|
||||
return window.location.origin + '/' + lang.id
|
||||
}
|
||||
|
||||
}
|
@ -1,70 +1,82 @@
|
||||
<menu>
|
||||
<div *ngIf="isLoggedIn" class="logged-in-block">
|
||||
<a routerLink="/my-account/settings">
|
||||
<img [src]="user.accountAvatarUrl" alt="Avatar" />
|
||||
</a>
|
||||
<div class="menu-wrapper">
|
||||
<menu>
|
||||
<div class="top-menu">
|
||||
<div *ngIf="isLoggedIn" class="logged-in-block">
|
||||
<a routerLink="/my-account/settings">
|
||||
<img [src]="user.accountAvatarUrl" alt="Avatar" />
|
||||
</a>
|
||||
|
||||
<div class="logged-in-info">
|
||||
<a routerLink="/my-account/settings" class="logged-in-username">{{ user.account?.displayName }}</a>
|
||||
<div class="logged-in-email">{{ user.email }}</div>
|
||||
<div class="logged-in-info">
|
||||
<a routerLink="/my-account/settings" class="logged-in-username">{{ user.account?.displayName }}</a>
|
||||
<div class="logged-in-email">{{ user.email }}</div>
|
||||
</div>
|
||||
|
||||
<div class="logged-in-more" dropdown placement="right" container="body">
|
||||
<span class="glyphicon glyphicon-option-vertical" dropdownToggle></span>
|
||||
|
||||
<ul *dropdownMenu class="dropdown-menu">
|
||||
<li>
|
||||
<a i18n [routerLink]="[ '/accounts', user.account?.nameWithHost ]" class="dropdown-item" title="My public profile">
|
||||
My public profile
|
||||
</a>
|
||||
|
||||
<a i18n routerLink="/my-account" class="dropdown-item" title="My account">
|
||||
My account
|
||||
</a>
|
||||
|
||||
<a i18n (click)="logout($event)" class="dropdown-item" title="Log out" href="#">
|
||||
Log out
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div *ngIf="!isLoggedIn" class="button-block">
|
||||
<a i18n routerLink="/login" class="login-button">Login</a>
|
||||
<a i18n *ngIf="isRegistrationAllowed()" routerLink="/signup" class="create-account-button">Create an account</a>
|
||||
</div>
|
||||
|
||||
<div class="panel-block">
|
||||
<div i18n class="block-title">Videos</div>
|
||||
|
||||
<a routerLink="/videos/trending" routerLinkActive="active">
|
||||
<span class="icon icon-videos-trending"></span>
|
||||
<ng-container i18n>Trending</ng-container>
|
||||
</a>
|
||||
|
||||
<a routerLink="/videos/recently-added" routerLinkActive="active">
|
||||
<span class="icon icon-videos-recently-added"></span>
|
||||
<ng-container i18n>Recently added</ng-container>
|
||||
</a>
|
||||
|
||||
<a routerLink="/videos/local" routerLinkActive="active">
|
||||
<span class="icon icon-videos-local"></span>
|
||||
<ng-container i18n>Local</ng-container>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="panel-block">
|
||||
<div class="block-title">More</div>
|
||||
|
||||
<a *ngIf="userHasAdminAccess" [routerLink]="getFirstAdminRouteAvailable()" routerLinkActive="active">
|
||||
<span class="icon icon-administration"></span>
|
||||
<ng-container i18n>Administration</ng-container>
|
||||
</a>
|
||||
|
||||
<a routerLink="/about" routerLinkActive="active">
|
||||
<span class="icon icon-about"></span>
|
||||
<ng-container i18n>About</ng-container>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="logged-in-more" dropdown placement="right" container="body">
|
||||
<span class="glyphicon glyphicon-option-vertical" dropdownToggle></span>
|
||||
|
||||
<ul *dropdownMenu class="dropdown-menu">
|
||||
<li>
|
||||
<a i18n [routerLink]="[ '/accounts', user.account?.nameWithHost ]" class="dropdown-item" title="My public profile">
|
||||
My public profile
|
||||
</a>
|
||||
|
||||
<a i18n routerLink="/my-account" class="dropdown-item" title="My account">
|
||||
My account
|
||||
</a>
|
||||
|
||||
<a i18n (click)="logout($event)" class="dropdown-item" title="Log out" href="#">
|
||||
Log out
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="footer">
|
||||
<span class="language">
|
||||
<span (click)="openLanguageChooser()" i18n-title title="Change the language" class="icon icon-language"></span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</menu>
|
||||
</div>
|
||||
|
||||
<div *ngIf="!isLoggedIn" class="button-block">
|
||||
<a i18n routerLink="/login" class="login-button">Login</a>
|
||||
<a i18n *ngIf="isRegistrationAllowed()" routerLink="/signup" class="create-account-button">Create an account</a>
|
||||
</div>
|
||||
|
||||
<div class="panel-block">
|
||||
<div i18n class="block-title">Videos</div>
|
||||
|
||||
<a routerLink="/videos/trending" routerLinkActive="active">
|
||||
<span class="icon icon-videos-trending"></span>
|
||||
<ng-container i18n>Trending</ng-container>
|
||||
</a>
|
||||
|
||||
<a routerLink="/videos/recently-added" routerLinkActive="active">
|
||||
<span class="icon icon-videos-recently-added"></span>
|
||||
<ng-container i18n>Recently added</ng-container>
|
||||
</a>
|
||||
|
||||
<a routerLink="/videos/local" routerLinkActive="active">
|
||||
<span class="icon icon-videos-local"></span>
|
||||
<ng-container i18n>Local</ng-container>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="panel-block">
|
||||
<div class="block-title">More</div>
|
||||
|
||||
<a *ngIf="userHasAdminAccess" [routerLink]="getFirstAdminRouteAvailable()" routerLinkActive="active">
|
||||
<span class="icon icon-administration"></span>
|
||||
<ng-container i18n>Administration</ng-container>
|
||||
</a>
|
||||
|
||||
<a routerLink="/about" routerLinkActive="active">
|
||||
<span class="icon icon-about"></span>
|
||||
<ng-container i18n>About</ng-container>
|
||||
</a>
|
||||
</div>
|
||||
</menu>
|
||||
<my-language-chooser #languageChooserModal></my-language-chooser>
|
@ -1,6 +1,13 @@
|
||||
@import '_variables';
|
||||
@import '_mixins';
|
||||
|
||||
.menu-wrapper {
|
||||
position: fixed;
|
||||
height: calc(100vh - #{$header-height});
|
||||
padding: 0;
|
||||
width: $menu-width;
|
||||
}
|
||||
|
||||
menu {
|
||||
background-color: $black-background;
|
||||
margin: 0;
|
||||
@ -11,6 +18,13 @@ menu {
|
||||
overflow: hidden;
|
||||
z-index: 1000;
|
||||
color: $menu-color;
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.top-menu {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.logged-in-block {
|
||||
height: 100px;
|
||||
@ -100,7 +114,7 @@ menu {
|
||||
a {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-left: 26px;
|
||||
padding-left: $menu-left-padding;
|
||||
color: $menu-color;
|
||||
cursor: pointer;
|
||||
height: 40px;
|
||||
@ -155,4 +169,35 @@ menu {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.footer {
|
||||
margin-bottom: 15px;
|
||||
padding-left: $menu-left-padding;
|
||||
|
||||
.language {
|
||||
display: inline-block;
|
||||
color: $menu-bottom-color;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
font-weight: $font-semibold;
|
||||
|
||||
.icon {
|
||||
@include icon(28px);
|
||||
opacity: 0.9;
|
||||
|
||||
&.icon-language {
|
||||
position: relative;
|
||||
top: -1px;
|
||||
width: 28px;
|
||||
height: 24px;
|
||||
|
||||
background-image: url('../../assets/images/menu/language.png');
|
||||
}
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,8 +1,8 @@
|
||||
import { Component, OnInit } from '@angular/core'
|
||||
import { Router } from '@angular/router'
|
||||
import { Component, OnInit, ViewChild } from '@angular/core'
|
||||
import { UserRight } from '../../../../shared/models/users/user-right.enum'
|
||||
import { AuthService, AuthStatus, RedirectService, ServerService } from '../core'
|
||||
import { User } from '../shared/users/user.model'
|
||||
import { LanguageChooserComponent } from '@app/menu/language-chooser.component'
|
||||
|
||||
@Component({
|
||||
selector: 'my-menu',
|
||||
@ -10,6 +10,8 @@ import { User } from '../shared/users/user.model'
|
||||
styleUrls: [ './menu.component.scss' ]
|
||||
})
|
||||
export class MenuComponent implements OnInit {
|
||||
@ViewChild('languageChooserModal') languageChooserModal: LanguageChooserComponent
|
||||
|
||||
user: User
|
||||
isLoggedIn: boolean
|
||||
userHasAdminAccess = false
|
||||
@ -90,6 +92,10 @@ export class MenuComponent implements OnInit {
|
||||
this.redirectService.redirectToHomepage()
|
||||
}
|
||||
|
||||
openLanguageChooser () {
|
||||
this.languageChooserModal.show()
|
||||
}
|
||||
|
||||
private computeIsUserHasAdminAccess () {
|
||||
const right = this.getFirstAdminRightAvailable()
|
||||
|
||||
|
BIN
client/src/assets/images/menu/language.png
Normal file
BIN
client/src/assets/images/menu/language.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 11 KiB |
@ -288,7 +288,7 @@ table {
|
||||
|
||||
// On small screen, menu is absolute
|
||||
@media screen and (max-width: 600px) {
|
||||
.title-menu-left {
|
||||
.menu-wrapper {
|
||||
width: 100% !important;
|
||||
position: absolute !important;
|
||||
z-index: 10000;
|
||||
|
@ -22,7 +22,9 @@ $header-border-color: #e9eff6;
|
||||
$search-input-width: 375px;
|
||||
|
||||
$menu-color: #fff;
|
||||
$menu-bottom-color: #C6C6C6;
|
||||
$menu-width: 240px;
|
||||
$menu-left-padding: 26px;
|
||||
|
||||
$footer-height: 30px;
|
||||
$footer-margin: 30px;
|
||||
|
@ -84,6 +84,7 @@
|
||||
"commander": "^2.13.0",
|
||||
"concurrently": "^3.5.1",
|
||||
"config": "^1.14.0",
|
||||
"cookie-parser": "^1.4.3",
|
||||
"cors": "^2.8.1",
|
||||
"create-torrent": "^3.24.5",
|
||||
"express": "^4.12.4",
|
||||
|
@ -12,6 +12,7 @@ import * as bodyParser from 'body-parser'
|
||||
import * as express from 'express'
|
||||
import * as morgan from 'morgan'
|
||||
import * as cors from 'cors'
|
||||
import * as cookieParser from 'cookie-parser'
|
||||
|
||||
process.title = 'peertube'
|
||||
|
||||
@ -112,6 +113,8 @@ app.use(bodyParser.json({
|
||||
type: [ 'application/json', 'application/*+json' ],
|
||||
limit: '500kb'
|
||||
}))
|
||||
// Cookies
|
||||
app.use(cookieParser())
|
||||
|
||||
// ----------- Views, routes and static files -----------
|
||||
|
||||
|
@ -7,8 +7,14 @@ import { ACCEPT_HEADERS, CONFIG, EMBED_SIZE, OPENGRAPH_AND_OEMBED_COMMENT, STATI
|
||||
import { asyncMiddleware } from '../middlewares'
|
||||
import { VideoModel } from '../models/video/video'
|
||||
import { VideoPrivacy } from '../../shared/models/videos'
|
||||
import { buildFileLocale, getCompleteLocale, getDefaultLocale, is18nLocale } from '../../shared/models'
|
||||
import { LOCALE_FILES } from '../../shared/models/i18n/i18n'
|
||||
import {
|
||||
buildFileLocale,
|
||||
getCompleteLocale,
|
||||
getDefaultLocale,
|
||||
is18nLocale,
|
||||
LOCALE_FILES,
|
||||
POSSIBLE_LOCALES
|
||||
} from '../../shared/models/i18n/i18n'
|
||||
|
||||
const clientsRouter = express.Router()
|
||||
|
||||
@ -22,7 +28,8 @@ clientsRouter.use('/videos/watch/:id',
|
||||
asyncMiddleware(generateWatchHtmlPage)
|
||||
)
|
||||
|
||||
clientsRouter.use('/videos/embed', (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
clientsRouter.use('' +
|
||||
'/videos/embed', (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
res.sendFile(embedPath)
|
||||
})
|
||||
|
||||
@ -63,7 +70,7 @@ clientsRouter.use('/client/*', (req: express.Request, res: express.Response, nex
|
||||
// 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.sendFile(getIndexPath(req, res, req.params.language))
|
||||
}
|
||||
|
||||
return res.status(404).end()
|
||||
@ -77,16 +84,24 @@ export {
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function getIndexPath (req: express.Request, paramLang?: string) {
|
||||
function getIndexPath (req: express.Request, res: express.Response, paramLang?: string) {
|
||||
let lang: string
|
||||
|
||||
// Check param lang validity
|
||||
if (paramLang && is18nLocale(paramLang)) {
|
||||
lang = paramLang
|
||||
|
||||
// Save locale in cookies
|
||||
res.cookie('clientLanguage', lang, {
|
||||
secure: CONFIG.WEBSERVER.SCHEME === 'https',
|
||||
sameSite: true,
|
||||
maxAge: 1000 * 3600 * 24 * 90 // 3 months
|
||||
})
|
||||
|
||||
} else if (req.cookies.clientLanguage && is18nLocale(req.cookies.clientLanguage)) {
|
||||
lang = req.cookies.clientLanguage
|
||||
} else {
|
||||
// lang = req.acceptsLanguages(POSSIBLE_LOCALES) || getDefaultLocale()
|
||||
// Disable auto language for now
|
||||
lang = getDefaultLocale()
|
||||
lang = req.acceptsLanguages(POSSIBLE_LOCALES) || getDefaultLocale()
|
||||
}
|
||||
|
||||
return join(__dirname, '../../../client/dist/' + buildFileLocale(lang) + '/index.html')
|
||||
@ -181,18 +196,18 @@ async function generateWatchHtmlPage (req: express.Request, res: express.Respons
|
||||
} else if (validator.isInt(videoId)) {
|
||||
videoPromise = VideoModel.loadAndPopulateAccountAndServerAndTags(+videoId)
|
||||
} else {
|
||||
return res.sendFile(getIndexPath(req))
|
||||
return res.sendFile(getIndexPath(req, res))
|
||||
}
|
||||
|
||||
let [ file, video ] = await Promise.all([
|
||||
readFileBufferPromise(getIndexPath(req)),
|
||||
readFileBufferPromise(getIndexPath(req, res)),
|
||||
videoPromise
|
||||
])
|
||||
|
||||
const html = file.toString()
|
||||
|
||||
// Let Angular application handle errors
|
||||
if (!video || video.privacy === VideoPrivacy.PRIVATE) return res.sendFile(getIndexPath(req))
|
||||
if (!video || video.privacy === VideoPrivacy.PRIVATE) return res.sendFile(getIndexPath(req, res))
|
||||
|
||||
const htmlStringPageWithTags = addOpenGraphAndOEmbedTags(html, video)
|
||||
res.set('Content-Type', 'text/html; charset=UTF-8').send(htmlStringPageWithTags)
|
||||
|
@ -1,8 +1,8 @@
|
||||
export const LOCALE_FILES = [ 'player', 'server' ]
|
||||
|
||||
export const I18N_LOCALES = {
|
||||
'en-US': 'English (US)',
|
||||
'fr-FR': 'Français (France)'
|
||||
'en-US': 'English',
|
||||
'fr-FR': 'Français'
|
||||
}
|
||||
|
||||
const I18N_LOCALE_ALIAS = {
|
||||
@ -13,8 +13,6 @@ const I18N_LOCALE_ALIAS = {
|
||||
export const POSSIBLE_LOCALES = Object.keys(I18N_LOCALES)
|
||||
.concat(Object.keys(I18N_LOCALE_ALIAS))
|
||||
|
||||
const possiblePaths = POSSIBLE_LOCALES.map(l => '/' + l)
|
||||
|
||||
export function getDefaultLocale () {
|
||||
return 'en-US'
|
||||
}
|
||||
@ -23,6 +21,7 @@ export function isDefaultLocale (locale: string) {
|
||||
return getCompleteLocale(locale) === getCompleteLocale(getDefaultLocale())
|
||||
}
|
||||
|
||||
const possiblePaths = POSSIBLE_LOCALES.map(l => '/' + l)
|
||||
export function is18nPath (path: string) {
|
||||
return possiblePaths.indexOf(path) !== -1
|
||||
}
|
||||
|
@ -1590,6 +1590,13 @@ content-type@~1.0.1, content-type@~1.0.4:
|
||||
version "1.0.4"
|
||||
resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b"
|
||||
|
||||
cookie-parser@^1.4.3:
|
||||
version "1.4.3"
|
||||
resolved "https://registry.yarnpkg.com/cookie-parser/-/cookie-parser-1.4.3.tgz#0fe31fa19d000b95f4aadf1f53fdc2b8a203baa5"
|
||||
dependencies:
|
||||
cookie "0.3.1"
|
||||
cookie-signature "1.0.6"
|
||||
|
||||
cookie-signature@1.0.6:
|
||||
version "1.0.6"
|
||||
resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c"
|
||||
|
Loading…
Reference in New Issue
Block a user