Add ability to choose the language

This commit is contained in:
Chocobozzz 2018-06-28 13:59:48 +02:00
parent 3ea9a1c311
commit 8afc19a612
No known key found for this signature in database
GPG Key ID: 583A612D890159BE
17 changed files with 239 additions and 98 deletions

View File

@ -18,9 +18,7 @@
</div> </div>
<div class="sub-header-container"> <div class="sub-header-container">
<div *ngIf="isMenuDisplayed" class="title-menu-left"> <my-menu *ngIf="isMenuDisplayed"></my-menu>
<my-menu></my-menu>
</div>
<div class="main-col container-fluid" [ngClass]="{ expanded: isMenuDisplayed === false }"> <div class="main-col container-fluid" [ngClass]="{ expanded: isMenuDisplayed === false }">

View File

@ -9,17 +9,6 @@
margin-top: $header-height; 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 { .header {
height: $header-height; height: $header-height;
position: fixed; position: fixed;

View File

@ -17,6 +17,7 @@ import { SignupModule } from './signup'
import { VideosModule } from './videos' import { VideosModule } from './videos'
import { buildFileLocale, getCompleteLocale, isDefaultLocale } from '../../../shared/models/i18n' import { buildFileLocale, getCompleteLocale, isDefaultLocale } from '../../../shared/models/i18n'
import { getDevLocale, isOnDevLocale } from '@app/shared/i18n/i18n-utils' import { getDevLocale, isOnDevLocale } from '@app/shared/i18n/i18n-utils'
import { LanguageChooserComponent } from '@app/menu/language-chooser.component'
export function metaFactory (serverService: ServerService): MetaLoader { export function metaFactory (serverService: ServerService): MetaLoader {
return new MetaStaticLoader({ return new MetaStaticLoader({
@ -36,6 +37,7 @@ export function metaFactory (serverService: ServerService): MetaLoader {
AppComponent, AppComponent,
MenuComponent, MenuComponent,
LanguageChooserComponent,
HeaderComponent HeaderComponent
], ],
imports: [ imports: [

View 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>

View 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;
}
}

View 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
}
}

View File

@ -1,70 +1,82 @@
<menu> <div class="menu-wrapper">
<div *ngIf="isLoggedIn" class="logged-in-block"> <menu>
<a routerLink="/my-account/settings"> <div class="top-menu">
<img [src]="user.accountAvatarUrl" alt="Avatar" /> <div *ngIf="isLoggedIn" class="logged-in-block">
</a> <a routerLink="/my-account/settings">
<img [src]="user.accountAvatarUrl" alt="Avatar" />
</a>
<div class="logged-in-info"> <div class="logged-in-info">
<a routerLink="/my-account/settings" class="logged-in-username">{{ user.account?.displayName }}</a> <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-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>
<div class="logged-in-more" dropdown placement="right" container="body"> <div class="footer">
<span class="glyphicon glyphicon-option-vertical" dropdownToggle></span> <span class="language">
<span (click)="openLanguageChooser()" i18n-title title="Change the language" class="icon icon-language"></span>
<ul *dropdownMenu class="dropdown-menu"> </span>
<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> </menu>
</div>
<div *ngIf="!isLoggedIn" class="button-block"> <my-language-chooser #languageChooserModal></my-language-chooser>
<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>

View File

@ -1,6 +1,13 @@
@import '_variables'; @import '_variables';
@import '_mixins'; @import '_mixins';
.menu-wrapper {
position: fixed;
height: calc(100vh - #{$header-height});
padding: 0;
width: $menu-width;
}
menu { menu {
background-color: $black-background; background-color: $black-background;
margin: 0; margin: 0;
@ -11,6 +18,13 @@ menu {
overflow: hidden; overflow: hidden;
z-index: 1000; z-index: 1000;
color: $menu-color; color: $menu-color;
overflow-y: auto;
display: flex;
flex-direction: column;
.top-menu {
flex-grow: 1;
}
.logged-in-block { .logged-in-block {
height: 100px; height: 100px;
@ -100,7 +114,7 @@ menu {
a { a {
display: flex; display: flex;
align-items: center; align-items: center;
padding-left: 26px; padding-left: $menu-left-padding;
color: $menu-color; color: $menu-color;
cursor: pointer; cursor: pointer;
height: 40px; 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;
}
}
}
}
} }

View File

@ -1,8 +1,8 @@
import { Component, OnInit } from '@angular/core' import { Component, OnInit, ViewChild } from '@angular/core'
import { Router } from '@angular/router'
import { UserRight } from '../../../../shared/models/users/user-right.enum' import { UserRight } from '../../../../shared/models/users/user-right.enum'
import { AuthService, AuthStatus, RedirectService, ServerService } from '../core' import { AuthService, AuthStatus, RedirectService, ServerService } from '../core'
import { User } from '../shared/users/user.model' import { User } from '../shared/users/user.model'
import { LanguageChooserComponent } from '@app/menu/language-chooser.component'
@Component({ @Component({
selector: 'my-menu', selector: 'my-menu',
@ -10,6 +10,8 @@ import { User } from '../shared/users/user.model'
styleUrls: [ './menu.component.scss' ] styleUrls: [ './menu.component.scss' ]
}) })
export class MenuComponent implements OnInit { export class MenuComponent implements OnInit {
@ViewChild('languageChooserModal') languageChooserModal: LanguageChooserComponent
user: User user: User
isLoggedIn: boolean isLoggedIn: boolean
userHasAdminAccess = false userHasAdminAccess = false
@ -90,6 +92,10 @@ export class MenuComponent implements OnInit {
this.redirectService.redirectToHomepage() this.redirectService.redirectToHomepage()
} }
openLanguageChooser () {
this.languageChooserModal.show()
}
private computeIsUserHasAdminAccess () { private computeIsUserHasAdminAccess () {
const right = this.getFirstAdminRightAvailable() const right = this.getFirstAdminRightAvailable()

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View File

@ -288,7 +288,7 @@ table {
// On small screen, menu is absolute // On small screen, menu is absolute
@media screen and (max-width: 600px) { @media screen and (max-width: 600px) {
.title-menu-left { .menu-wrapper {
width: 100% !important; width: 100% !important;
position: absolute !important; position: absolute !important;
z-index: 10000; z-index: 10000;

View File

@ -22,7 +22,9 @@ $header-border-color: #e9eff6;
$search-input-width: 375px; $search-input-width: 375px;
$menu-color: #fff; $menu-color: #fff;
$menu-bottom-color: #C6C6C6;
$menu-width: 240px; $menu-width: 240px;
$menu-left-padding: 26px;
$footer-height: 30px; $footer-height: 30px;
$footer-margin: 30px; $footer-margin: 30px;

View File

@ -84,6 +84,7 @@
"commander": "^2.13.0", "commander": "^2.13.0",
"concurrently": "^3.5.1", "concurrently": "^3.5.1",
"config": "^1.14.0", "config": "^1.14.0",
"cookie-parser": "^1.4.3",
"cors": "^2.8.1", "cors": "^2.8.1",
"create-torrent": "^3.24.5", "create-torrent": "^3.24.5",
"express": "^4.12.4", "express": "^4.12.4",

View File

@ -12,6 +12,7 @@ import * as bodyParser from 'body-parser'
import * as express from 'express' import * as express from 'express'
import * as morgan from 'morgan' import * as morgan from 'morgan'
import * as cors from 'cors' import * as cors from 'cors'
import * as cookieParser from 'cookie-parser'
process.title = 'peertube' process.title = 'peertube'
@ -112,6 +113,8 @@ app.use(bodyParser.json({
type: [ 'application/json', 'application/*+json' ], type: [ 'application/json', 'application/*+json' ],
limit: '500kb' limit: '500kb'
})) }))
// Cookies
app.use(cookieParser())
// ----------- Views, routes and static files ----------- // ----------- Views, routes and static files -----------

View File

@ -7,8 +7,14 @@ import { ACCEPT_HEADERS, CONFIG, EMBED_SIZE, OPENGRAPH_AND_OEMBED_COMMENT, STATI
import { asyncMiddleware } from '../middlewares' import { asyncMiddleware } from '../middlewares'
import { VideoModel } from '../models/video/video' import { VideoModel } from '../models/video/video'
import { VideoPrivacy } from '../../shared/models/videos' import { VideoPrivacy } from '../../shared/models/videos'
import { buildFileLocale, getCompleteLocale, getDefaultLocale, is18nLocale } from '../../shared/models' import {
import { LOCALE_FILES } from '../../shared/models/i18n/i18n' buildFileLocale,
getCompleteLocale,
getDefaultLocale,
is18nLocale,
LOCALE_FILES,
POSSIBLE_LOCALES
} from '../../shared/models/i18n/i18n'
const clientsRouter = express.Router() const clientsRouter = express.Router()
@ -22,7 +28,8 @@ clientsRouter.use('/videos/watch/:id',
asyncMiddleware(generateWatchHtmlPage) 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) 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 // Try to provide the right language index.html
clientsRouter.use('/(:language)?', function (req, res) { clientsRouter.use('/(:language)?', function (req, res) {
if (req.accepts(ACCEPT_HEADERS) === 'html') { 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() 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 let lang: string
// Check param lang validity // Check param lang validity
if (paramLang && is18nLocale(paramLang)) { if (paramLang && is18nLocale(paramLang)) {
lang = 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 { } else {
// lang = req.acceptsLanguages(POSSIBLE_LOCALES) || getDefaultLocale() lang = req.acceptsLanguages(POSSIBLE_LOCALES) || getDefaultLocale()
// Disable auto language for now
lang = getDefaultLocale()
} }
return join(__dirname, '../../../client/dist/' + buildFileLocale(lang) + '/index.html') 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)) { } else if (validator.isInt(videoId)) {
videoPromise = VideoModel.loadAndPopulateAccountAndServerAndTags(+videoId) videoPromise = VideoModel.loadAndPopulateAccountAndServerAndTags(+videoId)
} else { } else {
return res.sendFile(getIndexPath(req)) return res.sendFile(getIndexPath(req, res))
} }
let [ file, video ] = await Promise.all([ let [ file, video ] = await Promise.all([
readFileBufferPromise(getIndexPath(req)), readFileBufferPromise(getIndexPath(req, res)),
videoPromise videoPromise
]) ])
const html = file.toString() const html = file.toString()
// Let Angular application handle errors // 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) const htmlStringPageWithTags = addOpenGraphAndOEmbedTags(html, video)
res.set('Content-Type', 'text/html; charset=UTF-8').send(htmlStringPageWithTags) res.set('Content-Type', 'text/html; charset=UTF-8').send(htmlStringPageWithTags)

View File

@ -1,8 +1,8 @@
export const LOCALE_FILES = [ 'player', 'server' ] export const LOCALE_FILES = [ 'player', 'server' ]
export const I18N_LOCALES = { export const I18N_LOCALES = {
'en-US': 'English (US)', 'en-US': 'English',
'fr-FR': 'Français (France)' 'fr-FR': 'Français'
} }
const I18N_LOCALE_ALIAS = { const I18N_LOCALE_ALIAS = {
@ -13,8 +13,6 @@ const I18N_LOCALE_ALIAS = {
export const POSSIBLE_LOCALES = Object.keys(I18N_LOCALES) export const POSSIBLE_LOCALES = Object.keys(I18N_LOCALES)
.concat(Object.keys(I18N_LOCALE_ALIAS)) .concat(Object.keys(I18N_LOCALE_ALIAS))
const possiblePaths = POSSIBLE_LOCALES.map(l => '/' + l)
export function getDefaultLocale () { export function getDefaultLocale () {
return 'en-US' return 'en-US'
} }
@ -23,6 +21,7 @@ export function isDefaultLocale (locale: string) {
return getCompleteLocale(locale) === getCompleteLocale(getDefaultLocale()) return getCompleteLocale(locale) === getCompleteLocale(getDefaultLocale())
} }
const possiblePaths = POSSIBLE_LOCALES.map(l => '/' + l)
export function is18nPath (path: string) { export function is18nPath (path: string) {
return possiblePaths.indexOf(path) !== -1 return possiblePaths.indexOf(path) !== -1
} }

View File

@ -1590,6 +1590,13 @@ content-type@~1.0.1, content-type@~1.0.4:
version "1.0.4" version "1.0.4"
resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b" 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: cookie-signature@1.0.6:
version "1.0.6" version "1.0.6"
resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c"