From 8afc19a6121569da054462c7cb351a3f13030a32 Mon Sep 17 00:00:00 2001
From: Chocobozzz <me@florianbigard.com>
Date: Thu, 28 Jun 2018 13:59:48 +0200
Subject: [PATCH] Add ability to choose the language

---
 client/src/app/app.component.html             |   4 +-
 client/src/app/app.component.scss             |  11 --
 client/src/app/app.module.ts                  |   2 +
 .../app/menu/language-chooser.component.html  |  15 +++
 .../app/menu/language-chooser.component.scss  |  15 +++
 .../app/menu/language-chooser.component.ts    |  32 +++++
 client/src/app/menu/menu.component.html       | 126 ++++++++++--------
 client/src/app/menu/menu.component.scss       |  47 ++++++-
 client/src/app/menu/menu.component.ts         |  10 +-
 client/src/assets/images/menu/language.png    | Bin 0 -> 10937 bytes
 client/src/sass/application.scss              |   2 +-
 client/src/sass/include/_variables.scss       |   2 +
 package.json                                  |   1 +
 server.ts                                     |   3 +
 server/controllers/client.ts                  |  37 +++--
 shared/models/i18n/i18n.ts                    |   7 +-
 yarn.lock                                     |   7 +
 17 files changed, 231 insertions(+), 90 deletions(-)
 create mode 100644 client/src/app/menu/language-chooser.component.html
 create mode 100644 client/src/app/menu/language-chooser.component.scss
 create mode 100644 client/src/app/menu/language-chooser.component.ts
 create mode 100644 client/src/assets/images/menu/language.png

diff --git a/client/src/app/app.component.html b/client/src/app/app.component.html
index e505466339..09b2c15be7 100644
--- a/client/src/app/app.component.html
+++ b/client/src/app/app.component.html
@@ -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 }">
 
diff --git a/client/src/app/app.component.scss b/client/src/app/app.component.scss
index 6edf966f99..9eca313203 100644
--- a/client/src/app/app.component.scss
+++ b/client/src/app/app.component.scss
@@ -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;
diff --git a/client/src/app/app.module.ts b/client/src/app/app.module.ts
index 9cffdd31e7..48886fd4e8 100644
--- a/client/src/app/app.module.ts
+++ b/client/src/app/app.module.ts
@@ -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: [
diff --git a/client/src/app/menu/language-chooser.component.html b/client/src/app/menu/language-chooser.component.html
new file mode 100644
index 0000000000..f941e32f81
--- /dev/null
+++ b/client/src/app/menu/language-chooser.component.html
@@ -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>
diff --git a/client/src/app/menu/language-chooser.component.scss b/client/src/app/menu/language-chooser.component.scss
new file mode 100644
index 0000000000..4574f78c69
--- /dev/null
+++ b/client/src/app/menu/language-chooser.component.scss
@@ -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;
+  }
+}
\ No newline at end of file
diff --git a/client/src/app/menu/language-chooser.component.ts b/client/src/app/menu/language-chooser.component.ts
new file mode 100644
index 0000000000..3de6a129d3
--- /dev/null
+++ b/client/src/app/menu/language-chooser.component.ts
@@ -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
+  }
+
+}
diff --git a/client/src/app/menu/menu.component.html b/client/src/app/menu/menu.component.html
index 8e3b295f77..784b5cd857 100644
--- a/client/src/app/menu/menu.component.html
+++ b/client/src/app/menu/menu.component.html
@@ -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>
+        <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>
+        <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>
+          <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 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>
+              <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 *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>
 
-  <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/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/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>
 
-    <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>
 
-  <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 *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="footer">
+      <span class="language">
+        <span (click)="openLanguageChooser()" i18n-title title="Change the language" class="icon icon-language"></span>
+      </span>
+    </div>
+  </menu>
+</div>
 
-    <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>
\ No newline at end of file
diff --git a/client/src/app/menu/menu.component.scss b/client/src/app/menu/menu.component.scss
index c36a7aa36c..e61f4acd33 100644
--- a/client/src/app/menu/menu.component.scss
+++ b/client/src/app/menu/menu.component.scss
@@ -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;
+        }
+      }
+    }
+  }
 }
diff --git a/client/src/app/menu/menu.component.ts b/client/src/app/menu/menu.component.ts
index c0aea89b33..dded6b4d5a 100644
--- a/client/src/app/menu/menu.component.ts
+++ b/client/src/app/menu/menu.component.ts
@@ -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()
 
diff --git a/client/src/assets/images/menu/language.png b/client/src/assets/images/menu/language.png
new file mode 100644
index 0000000000000000000000000000000000000000..60e6fec00fd20a3beae24b37729d8f16d1d7d476
GIT binary patch
literal 10937
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
b+EL;^pfkr?BO-mc00000NkvXXu0mjfh7l6b

literal 0
HcmV?d00001

diff --git a/client/src/sass/application.scss b/client/src/sass/application.scss
index dae0c52c21..96602dc38d 100644
--- a/client/src/sass/application.scss
+++ b/client/src/sass/application.scss
@@ -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;
diff --git a/client/src/sass/include/_variables.scss b/client/src/sass/include/_variables.scss
index 092f8ed242..f1f7551264 100644
--- a/client/src/sass/include/_variables.scss
+++ b/client/src/sass/include/_variables.scss
@@ -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;
diff --git a/package.json b/package.json
index edb14ff591..254281df50 100644
--- a/package.json
+++ b/package.json
@@ -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",
diff --git a/server.ts b/server.ts
index fb01ed572b..5511c54358 100644
--- a/server.ts
+++ b/server.ts
@@ -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 -----------
 
diff --git a/server/controllers/client.ts b/server/controllers/client.ts
index 385757fa6b..dfffe5487d 100644
--- a/server/controllers/client.ts
+++ b/server/controllers/client.ts
@@ -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)
diff --git a/shared/models/i18n/i18n.ts b/shared/models/i18n/i18n.ts
index e2b4409008..14b02a01d5 100644
--- a/shared/models/i18n/i18n.ts
+++ b/shared/models/i18n/i18n.ts
@@ -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
 }
diff --git a/yarn.lock b/yarn.lock
index 65b78b4fa5..8c79ab282a 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -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"
-- 
GitLab