Add ability to choose the language
authorChocobozzz <me@florianbigard.com>
Thu, 28 Jun 2018 11:59:48 +0000 (13:59 +0200)
committerChocobozzz <me@florianbigard.com>
Thu, 28 Jun 2018 13:53:12 +0000 (15:53 +0200)
17 files changed:
client/src/app/app.component.html
client/src/app/app.component.scss
client/src/app/app.module.ts
client/src/app/menu/language-chooser.component.html [new file with mode: 0644]
client/src/app/menu/language-chooser.component.scss [new file with mode: 0644]
client/src/app/menu/language-chooser.component.ts [new file with mode: 0644]
client/src/app/menu/menu.component.html
client/src/app/menu/menu.component.scss
client/src/app/menu/menu.component.ts
client/src/assets/images/menu/language.png [new file with mode: 0644]
client/src/sass/application.scss
client/src/sass/include/_variables.scss
package.json
server.ts
server/controllers/client.ts
shared/models/i18n/i18n.ts
yarn.lock

index e5054663396c99d4bac7ab35be48520ad5cda7e1..09b2c15be7225cbf9fae9ae6964a5ffe39ff7f24 100644 (file)
@@ -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 }">
 
index 6edf966f99717b6fe2f4e6fda9eec80387517390..9eca313203eb4ee8c15a012eacd57c59a00f4b52 100644 (file)
@@ -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;
index 9cffdd31e7ccc8fe136202a4b2477d2b6e4980ef..48886fd4e865a3f0620417f898c0242ffbd62757 100644 (file)
@@ -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 (file)
index 0000000..f941e32
--- /dev/null
@@ -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 (file)
index 0000000..4574f78
--- /dev/null
@@ -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 (file)
index 0000000..3de6a12
--- /dev/null
@@ -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
+  }
+
+}
index 8e3b295f77bcab42407db800bbdd27d0cf8df168..784b5cd85705539b375f0f3aabc975dcd8b32373 100644 (file)
@@ -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
index c36a7aa36c89590780d9f2bf9c6a96eb88c3fbab..e61f4acd33fbce7e3e167c46777f1d8a18af6a37 100644 (file)
@@ -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;
+        }
+      }
+    }
+  }
 }
index c0aea89b33d9bc2d408858ce4ff857eb04d4b324..dded6b4d5a6b6e2453437cddba32b9646d64d9d1 100644 (file)
@@ -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 (file)
index 0000000..60e6fec
Binary files /dev/null and b/client/src/assets/images/menu/language.png differ
index dae0c52c213e2b654b16ff2ab61abd9739d52865..96602dc38de98bb65d5c293e36a3b14d8e7e2c70 100644 (file)
@@ -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;
index 092f8ed2422f183e9e879f86da7225c91345fec9..f1f7551264508849e215ce9e62418df5a97c7cb7 100644 (file)
@@ -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;
index edb14ff591c8d8be39ec5a83636f579dba078c02..254281df5060c64c55424f90640847b68f5a8b69 100644 (file)
@@ -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",
index fb01ed572bcdd9ac15ea3b2fef103f73ce257e85..5511c5435856fec72698f20295fa35e754d44267 100644 (file)
--- 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 -----------
 
index 385757fa6bb45729433b142458f918e9a815589e..dfffe5487d024b9781f3f6e115ff835a7af8545d 100644 (file)
@@ -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)
index e2b4409008cebb5e4d62cba366e2fea0a1a99158..14b02a01d57129933ae6a41e8e87241940769932 100644 (file)
@@ -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
 }
index 65b78b4fa504191c718569f6cae1953b07e8782a..8c79ab282a9a8f50280e8e21c7a046ac1fcd0a64 100644 (file)
--- 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"