Merge pull request #99 from Human-Connection/i18n-pure

Integrate i18n
This commit is contained in:
Robert Schäfer 2018-12-24 15:30:29 +01:00 committed by GitHub
commit 70739faef4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
61 changed files with 1604 additions and 271 deletions

1
.gitignore vendored
View File

@ -72,6 +72,7 @@ dist
# IDE
.idea
.vscode
# TEMORIRY
static/uploads

View File

@ -39,3 +39,15 @@ All reusable Components (for example avatar) should be done inside the styleguid
``` bash
$ yarn styleguide
```
## Internationalization (i18n)
You can help translating the interface by joining us on [lokalise.co](https://lokalise.co/public/556252725c18dd752dd546.13222042/).
Thanks lokalise.co that we can use your premium account!
<a href="(https://lokalise.co/public/556252725c18dd752dd546.13222042/)."><img src="lokalise.png" alt="localise.co" height="32px" /></a>
## Attributions
<div>Locale Icons made by <a href="http://www.freepik.com/" title="Freepik">Freepik</a> from <a href="https://www.flaticon.com/" title="Flaticon">www.flaticon.com</a> is licensed by <a href="http://creativecommons.org/licenses/by/3.0/" title="Creative Commons BY 3.0" target="_blank">CC 3.0 BY</a></div>

67
components/Dropdown.vue Normal file
View File

@ -0,0 +1,67 @@
<template>
<v-popover
:open.sync="isPopoverOpen"
:open-group="Math.random().toString()"
:placement="placement"
trigger="manual"
:offset="offset"
>
<slot :toggleMenu="toggleMenu" />
<div
slot="popover"
@mouseover="popoverMouseEnter"
@mouseleave="popoveMouseLeave"
>
<slot
name="popover"
:toggleMenu="toggleMenu"
/>
</div>
</v-popover>
</template>
<script>
import { mapGetters } from 'vuex'
let mouseEnterTimer = null
let mouseLeaveTimer = null
export default {
props: {
placement: { type: String, default: 'bottom-end' },
offset: { type: [String, Number], default: '16' }
},
data() {
return {
isPopoverOpen: false
}
},
beforeDestroy() {
clearTimeout(mouseEnterTimer)
clearTimeout(mouseLeaveTimer)
},
methods: {
toggleMenu() {
this.isPopoverOpen = !this.isPopoverOpen
},
popoverMouseEnter() {
clearTimeout(mouseEnterTimer)
clearTimeout(mouseLeaveTimer)
if (!this.isPopoverOpen) {
mouseEnterTimer = setTimeout(() => {
this.isPopoverOpen = true
}, 500)
}
},
popoveMouseLeave() {
clearTimeout(mouseEnterTimer)
clearTimeout(mouseLeaveTimer)
if (this.isPopoverOpen) {
mouseLeaveTimer = setTimeout(() => {
this.isPopoverOpen = false
}, 300)
}
}
}
}
</script>

114
components/LocaleSwitch.vue Normal file
View File

@ -0,0 +1,114 @@
<template>
<dropdown
ref="menu"
:placement="placement"
:offset="offset"
>
<template
slot="default"
slot-scope="{toggleMenu}"
>
<a
class="locale-menu"
href="#"
@click.prevent="toggleMenu()"
>
<img
:alt="current.name"
:title="current.name"
:src="`/img/locale-flags/${current.code}.svg`"
height="26"
>
</a>
</template>
<template slot="popover">
<ul class="locale-menu-popover">
<li
v-for="locale in locales"
:key="locale.code"
>
<a
href="#"
style="display: flex; align-items: center;"
:class="[
locale.code,
current.code === locale.code && 'active'
]"
@click.prevent="changeLanguage(locale.code)"
>
<img
:alt="locale.name"
:title="locale.name"
:src="`/img/locale-flags/${locale.code}.svg`"
width="20"
> {{ locale.name }}
</a>
</li>
</ul>
</template>
</dropdown>
</template>
<script>
import Dropdown from '~/components/Dropdown'
import find from 'lodash/find'
export default {
components: {
Dropdown
},
props: {
placement: { type: String, default: 'bottom-start' },
offset: { type: [String, Number], default: '16' }
},
data() {
return {
locales: process.env.locales
}
},
computed: {
current() {
return find(this.locales, { code: this.$i18n.locale() })
}
},
methods: {
changeLanguage(locale) {
this.$i18n.set(locale)
this.$refs.menu.toggleMenu()
}
}
}
</script>
<style lang="scss">
.locale-menu {
user-select: none;
}
ul.locale-menu-popover {
list-style: none;
padding: 0;
margin: 0;
li {
a {
opacity: 0.8;
display: block;
padding: 0.3rem 0;
img {
margin-right: 8px;
}
&:hover {
opacity: 1;
}
&.active {
opacity: 1;
font-weight: bold;
}
}
}
}
</style>

9
components/mixins/seo.js Normal file
View File

@ -0,0 +1,9 @@
export default {
head() {
return {
htmlAttrs: {
lang: this.$i18n.locale()
}
}
}
}

View File

@ -0,0 +1,24 @@
Feature: Internationalization
As a user who is not very fluent in English
I would like to see the user interface translated to my preferred language
In order to be able to understand the interface
Background:
Given I am on the "login" page
Scenario Outline: I select "<language>" in the language menu and see "<buttonLabel>"
When I select "<language>" in the language menu
Then the whole user interface appears in "<language>"
Then I see a button with the label "<buttonLabel>"
Examples: Login Button
| language | buttonLabel |
| English | Login |
| Deutsch | Einloggen |
| Français | Connexion |
| Nederlands | Inloggen |
Scenario: Keep preferred language after refresh
Given I previously switched the language to "Deutsch"
And I refresh the page
Then the whole user interface appears in "Deutsch"

View File

@ -1,8 +1,30 @@
import { Given, When, Then } from 'cypress-cucumber-preprocessor/steps'
import find from 'lodash/find'
import { eq } from 'semver';
/* global cy */
const baseUrl = 'http://localhost:3000'
const username = 'Peter Lustig'
const locales = require('../../../locales')
const getLangByName = function(name) {
return find(locales, { name })
}
const openPage = function(page) {
if (page === 'landing') {
page = ''
}
cy.visit(`${baseUrl}/${page}`)
}
const switchLanguage = function(name) {
cy.get('.login-locale-switch a').click()
cy.contains('.locale-menu-popover a', name).click()
}
const login = (email, password) => {
cy.visit(`${baseUrl}/login`)
cy.get('input[name=email]')
@ -38,10 +60,14 @@ Given('my user account has the role {string}', (role) => {
// TODO: use db factories instead of seed data
})
When('I log out', logout)
When(`I visit the {string} page`, route => {
cy.visit(`${baseUrl}/${route}`)
When('I visit the {string} page', page => {
openPage(page)
})
Given('I am on the {string} page', page => {
openPage(page)
})
When('I fill in my email and password combination and click submit', () => {
@ -62,6 +88,7 @@ When('I log out through the menu in the top right corner', () => {
Then('I can click on my profile picture in the top right corner', () => {
cy.get('.avatar-menu').click()
cy.get('.avatar-menu-popover')
})
Then('I can see my name {string} in the dropdown menu', () => {
@ -70,9 +97,7 @@ Then('I can see my name {string} in the dropdown menu', () => {
Then('I see the login screen again', () => {
cy.location('pathname').should('contain', '/login')
cy.contains(
'Wenn Du ein Konto bei Human Connection hast, melde Dich bitte hier an.'
)
cy.contains('If you already have a human-connection account, login here.')
})
Then('I am still logged in', () => {
@ -80,16 +105,33 @@ Then('I am still logged in', () => {
cy.get('.avatar-menu-popover').contains(username)
})
When('I navigate to the administration dashboard', () => {
cy.get('.avatar-menu').click()
cy.get('a').contains('Systemverwaltung').click()
When('I select {string} in the language menu', name => {
switchLanguage(name)
})
Given('I previously switched the language to {string}', name => {
switchLanguage(name)
})
Then('the whole user interface appears in {string}', name => {
const lang = getLangByName(name)
cy.get(`html[lang=${lang.code}]`)
cy.getCookie('locale').should('have.property', 'value', lang.code)
})
Then('I see a button with the label {string}', label => {
cy.contains('button', label)
})
When(`I click on {string}`, (linkOrButton) => {
When('I navigate to the administration dashboard', () => {
cy.get('.avatar-menu').click()
cy.get('.avatar-menu-popover')
.contains('Admin')
.click()
})
When('I click on {string}', linkOrButton => {
cy.contains(linkOrButton).click()
})
Then('I can see a list of categories ordered by post count:', (table) => {
Then('I can see a list of categories ordered by post count:', table => {
// TODO: match the table in the feature with the html table
cy.get('thead').find('tr th').should('have.length', 3)
const last_column = cy.get('tbody').find('tr td:last-child').then((last_column) => {
@ -100,7 +142,7 @@ Then('I can see a list of categories ordered by post count:', (table) => {
})
})
Then('I can see a list of tags ordered by user and post count:', (table) => {
Then('I can see a list of tags ordered by user and post count:', table => {
// TODO: match the table in the feature with the html table
cy.get('thead').find('tr th').should('have.length', 4)
const last_column = cy.get('tbody').find('tr td:last-child').then((last_column) => {

View File

@ -7,3 +7,11 @@
</ds-container>
</div>
</template>
<script>
import seo from '~/components/mixins/seo'
export default {
mixins: [seo]
}
</script>

View File

@ -8,55 +8,63 @@
>
<ds-logo />
</a>
<template v-if="isLoggedIn">
<div style="float: right">
<no-ssr>
<v-popover
:open.sync="isPopoverOpen"
:open-group="Math.random().toString()"
placement="bottom-end"
trigger="manual"
offset="10"
style="float: right"
>
<a
class="avatar-menu"
:href="$router.resolve({name: 'profile-slug', params: {slug: user.slug}}).href"
@click.prevent="toggleMenu()"
>
<ds-avatar
:image="user.avatar"
:name="user.name"
size="42"
/>
</a>
<div
slot="popover"
class="avatar-menu-popover"
style="padding-top: .5rem; padding-bottom: .5rem;"
@mouseover="popoverMouseEnter"
@mouseleave="popoveMouseLeave">
Hallo <b>{{ user.name }}</b>
<ds-menu
:routes="routes"
:is-exact="isExact"
style="margin-left: -15px; margin-right: -15px; padding-top: 1rem; padding-bottom: 1rem;">
<ds-menu-item
slot="Navigation"
slot-scope="item"
:route="item.route"
:parents="item.parents"
@click.native="toggleMenu">
<ds-icon :name="item.route.icon" /> {{ item.route.name }}
</ds-menu-item>
</ds-menu>
<ds-space margin="xx-small" />
<nuxt-link :to="{ name: 'logout'}">
<ds-icon name="sign-out" /> Logout
</nuxt-link>
</div>
</v-popover>
<locale-switch
class="topbar-locale-switch"
placement="bottom"
offset="24"
/>
</no-ssr>
</template>
<template v-if="isLoggedIn">
<no-ssr>
<dropdown class="avatar-menu">
<template
slot="default"
slot-scope="{toggleMenu}"
>
<a
class="avatar-menu-trigger"
:href="$router.resolve({name: 'profile-slug', params: {slug: user.slug}}).href"
@click.prevent="toggleMenu()"
>
<ds-avatar
:image="user.avatar"
:name="user.name"
size="42"
/>
</a>
</template>
<template
slot="popover"
slot-scope="{toggleMenu}"
>
<div class="avatar-menu-popover">
{{ $t('login.hello') }} <b>{{ user.name }}</b>
<ds-menu
:routes="routes"
:is-exact="isExact"
>
<ds-menu-item
slot="Navigation"
slot-scope="item"
:route="item.route"
:parents="item.parents"
@click.native="toggleMenu"
>
<ds-icon :name="item.route.icon" /> {{ item.route.name }}
</ds-menu-item>
</ds-menu>
<ds-space margin="xx-small" />
<nuxt-link :to="{ name: 'logout'}">
<ds-icon name="sign-out" /> {{ $t('login.logout') }}
</nuxt-link>
</div>
</template>
</dropdown>
</no-ssr>
</template>
</div>
</ds-container>
</div>
<ds-container>
@ -69,16 +77,16 @@
<script>
import { mapGetters } from 'vuex'
import { setTimeout } from 'timers'
let mouseEnterTimer = null
let mouseLeaveTimer = null
import LocaleSwitch from '~/components/LocaleSwitch'
import Dropdown from '~/components/Dropdown'
import seo from '~/components/mixins/seo'
export default {
data() {
return {
isPopoverOpen: false
}
components: {
Dropdown,
LocaleSwitch
},
mixins: [seo],
computed: {
...mapGetters({
user: 'auth/user',
@ -91,19 +99,19 @@ export default {
}
let routes = [
{
name: 'Mein Profil',
name: this.$t('profile.name'),
path: `/profile/${this.user.slug}`,
icon: 'user'
},
{
name: 'Einstellungen',
name: this.$t('settings.name'),
path: `/settings`,
icon: 'cogs'
}
]
if (this.isAdmin) {
routes.push({
name: 'Systemverwaltung',
name: this.$t('admin.name'),
path: `/admin`,
icon: 'shield'
})
@ -111,35 +119,38 @@ export default {
return routes
}
},
beforeDestroy() {
clearTimeout(mouseEnterTimer)
clearTimeout(mouseLeaveTimer)
},
methods: {
toggleMenu() {
this.isPopoverOpen = !this.isPopoverOpen
},
isExact(url) {
return this.$route.path.indexOf(url) === 0
},
popoverMouseEnter() {
clearTimeout(mouseEnterTimer)
clearTimeout(mouseLeaveTimer)
if (!this.isPopoverOpen) {
mouseEnterTimer = setTimeout(() => {
this.isPopoverOpen = true
}, 500)
}
},
popoveMouseLeave() {
clearTimeout(mouseEnterTimer)
clearTimeout(mouseLeaveTimer)
if (this.isPopoverOpen) {
mouseLeaveTimer = setTimeout(() => {
this.isPopoverOpen = false
}, 300)
}
}
}
}
</script>
<style lang="scss">
.topbar-locale-switch {
display: inline-block;
top: 8px;
right: 10px;
position: relative;
}
.avatar-menu {
float: right;
}
.avatar-menu-trigger {
user-select: none;
}
.avatar-menu-popover {
display: inline-block;
padding-top: 0.5rem;
padding-bottom: 0.5rem;
nav {
margin-left: -15px;
margin-right: -15px;
padding-top: 1rem;
padding-bottom: 1rem;
}
}
</style>

97
locales/de.json Normal file
View File

@ -0,0 +1,97 @@
{
"login": {
"copy": "Wenn Du ein Konto bei Human Connection hast, melde Dich bitte hier an.",
"login": "Einloggen",
"logout": "Ausloggen",
"email": "Deine E-Mail",
"password": "Dein Passwort",
"moreInfo": "Was ist Human Connection?",
"hello": "Hallo"
},
"profile": {
"name": "Mein Profil",
"memberSince": "Mitglied seit",
"follow": "Folgen",
"followers": "Folgen",
"following": "Folgt"
},
"settings": {
"name": "Einstellungen",
"data": {
"name": "Deine Daten"
},
"security": {
"name": "Sicherheit"
},
"invites": {
"name": "Einladungen"
},
"download": {
"name": "Daten herunterladen"
},
"delete": {
"name": "Konto löschen"
},
"organizations": {
"name": "Meine Organisationen"
},
"languages": {
"name": "Sprachen"
}
},
"admin": {
"name": "Systemverwaltung",
"dashboard": {
"name": "Startzentrale",
"users": "Benutzer",
"posts": "Beiträge",
"comments": "Kommentare",
"notifications": "Benachrichtigungen",
"organizations": "Organisationen",
"projects": "Projekte",
"invites": "Einladungen",
"follows": "Folgen",
"shouts": "Shouts"
},
"organizations": {
"name": "Organisationen"
},
"users": {
"name": "Benutzer"
},
"pages": {
"name": "Seiten"
},
"notifications": {
"name": "Benachrichtigungen"
},
"categories": {
"name": "Kategorien",
"categoryName": "Name",
"postCount": "Beiträge"
},
"tags": {
"name": "Schlagworte",
"tagCountUnique": "Benutzer",
"tagCount": "Beiträge"
},
"settings": {
"name": "Einstellungen"
}
},
"post": {
"name": "Beitrag",
"moreInfo": {
"name": "Mehr Info"
},
"takeAction": {
"name": "Aktiv werden"
}
},
"quotes": {
"african": {
"quote": "Viele kleine Leute, an vielen kleinen Orten, die viele kleine Dinge tun, werden das Antlitz dieser Welt verändern.",
"author": "Afrikanisches Sprichwort"
}
}
}

97
locales/en.json Normal file
View File

@ -0,0 +1,97 @@
{
"login": {
"copy": "If you already have a human-connection account, login here.",
"login": "Login",
"logout": "Logout",
"email": "Your Email",
"password": "Your Password",
"moreInfo": "What is Human Connection?",
"hello": "Hello"
},
"profile": {
"name": "My Profile",
"memberSince": "Member since",
"follow": "Follow",
"followers": "Followers",
"following": "Following"
},
"settings": {
"name": "Settings",
"data": {
"name": "Your data"
},
"security": {
"name": "Security"
},
"invites": {
"name": "Invites"
},
"download": {
"name": "Download Data"
},
"delete": {
"name": "Delete Account"
},
"organizations": {
"name": "My Organizations"
},
"languages": {
"name": "Languages"
}
},
"admin": {
"name": "Admin",
"dashboard": {
"name": "Dashboard",
"users": "Users",
"posts": "Posts",
"comments": "Comments",
"notifications": "Notifications",
"organizations": "Organizations",
"projects": "Projects",
"invites": "Invites",
"follows": "Follows",
"shouts": "Shouts"
},
"organizations": {
"name": "Organizations"
},
"users": {
"name": "Users"
},
"pages": {
"name": "Pages"
},
"notifications": {
"name": "Notifications"
},
"categories": {
"name": "Categories",
"categoryName": "Name",
"postCount": "Posts"
},
"tags": {
"name": "Tags",
"tagCountUnique": "Users",
"tagCount": "Posts"
},
"settings": {
"name": "Settings"
}
},
"post": {
"name": "Post",
"moreInfo": {
"name": "More info"
},
"takeAction": {
"name": "Take action"
}
},
"quotes": {
"african": {
"quote": "Many small people in many small places do many small things, that can alter the face of the world.",
"author": "African proverb"
}
}
}

79
locales/es.json Normal file
View File

@ -0,0 +1,79 @@
{
"login": {
"copy": "Si ya tiene una cuenta de Human Connection, inicie sesión aquí.",
"logout": "Cierre de sesión",
"email": "Tu correo electrónico",
"password": "Tu contraseña",
"moreInfo": "¿Qué es Human Connection?",
"hello": "Hola"
},
"profile": {
"name": "Mi perfil",
"memberSince": "Miembro desde",
"followers": "Seguidores"
},
"settings": {
"data": {
"name": "Sus datos"
},
"security": {
"name": "Seguridad"
},
"invites": {
"name": "Invita"
},
"download": {
"name": "Descargar datos"
},
"organizations": {
"name": "Mis organizaciones"
},
"languages": {
"name": "Idiomas"
}
},
"admin": {
"name": "Admin",
"dashboard": {
"users": "Usuarios",
"comments": "Comentarios",
"organizations": "Organizaciones",
"projects": "Proyectos",
"invites": "Invita",
"follows": "Sigue"
},
"organizations": {
"name": "Organizaciones"
},
"users": {
"name": "Usuarios"
},
"pages": {
"name": "Páginas"
},
"notifications": {
"name": "Notificaciones"
},
"categories": {
"name": "Categorías",
"categoryName": "Nombre"
},
"tags": {
"name": "Etiquetas",
"tagCountUnique": "Usuarios"
}
},
"post": {
"moreInfo": {
"name": "Más info"
},
"takeAction": {
"name": "Tomar acción"
}
},
"quotes": {
"african": {
"author": "Proverbio africano"
}
}
}

97
locales/fr.json Normal file
View File

@ -0,0 +1,97 @@
{
"login": {
"copy": "Si vous avez déjà un compte human-connection, connectez-vous ici.",
"login": "Connexion",
"logout": "Déconnexion",
"email": "Votre Message électronique",
"password": "Votre mot de passe",
"moreInfo": "Qu'est-ce que Human Connection?",
"hello": "Bonjour"
},
"profile": {
"name": "Mon profil",
"memberSince": "Membre depuis",
"follow": "Suivre",
"followers": "Suiveurs",
"following": "Suivant"
},
"settings": {
"name": "Paramètres",
"data": {
"name": "Vos données"
},
"security": {
"name": "Sécurité"
},
"invites": {
"name": "Invite"
},
"download": {
"name": "Télécharger les données"
},
"delete": {
"name": "Supprimer un compte"
},
"organizations": {
"name": "Mes organisations"
},
"languages": {
"name": "Langues"
}
},
"admin": {
"name": "Admin",
"dashboard": {
"name": "Tableau de bord",
"users": "Utilisateurs",
"posts": "Postes",
"comments": "Commentaires",
"notifications": "Notifications",
"organizations": "Organisations",
"projects": "Projets",
"invites": "Invite",
"follows": "Suit",
"shouts": "Cris"
},
"organizations": {
"name": "Organisations"
},
"users": {
"name": "Utilisateurs"
},
"pages": {
"name": "Pages"
},
"notifications": {
"name": "Notifications"
},
"categories": {
"name": "Catégories",
"categoryName": "Nom",
"postCount": "Postes"
},
"tags": {
"name": "Étiquettes",
"tagCountUnique": "Utilisateurs",
"tagCount": "Postes"
},
"settings": {
"name": "Paramètres"
}
},
"post": {
"name": "Post",
"moreInfo": {
"name": "Plus d'infos"
},
"takeAction": {
"name": "Passez à l'action"
}
},
"quotes": {
"african": {
"quote": "Beaucoup de petites personnes dans beaucoup de petits endroits font beaucoup de petites choses, qui peuvent changer la face du monde.",
"author": "Proverbe africain"
}
}
}

50
locales/index.js Normal file
View File

@ -0,0 +1,50 @@
module.exports = [
{
name: 'English',
code: 'en',
iso: 'en-US',
enabled: true
},
{
name: 'Deutsch',
code: 'de',
iso: 'de-DE',
enabled: true
},
{
name: 'Nederlands',
code: 'nl',
iso: 'nl-NL',
enabled: true
},
{
name: 'Français',
code: 'fr',
iso: 'fr-FR',
enabled: true
},
{
name: 'Italiano',
code: 'it',
iso: 'it-IT',
enabled: true
},
{
name: 'Español',
code: 'es',
iso: 'es-ES',
enabled: true
},
{
name: 'Portuguese',
code: 'pt',
iso: 'pt-PT',
enabled: true
},
{
name: 'Polski',
code: 'pl',
iso: 'pl-PL',
enabled: true
}
]

89
locales/it.json Normal file
View File

@ -0,0 +1,89 @@
{
"login": {
"copy": "Se hai già un account di Human Connection, accedi qui.",
"logout": "Logout",
"email": "La tua email",
"password": "La tua password",
"moreInfo": "Che cosa è Human Connection?",
"hello": "Ciao"
},
"profile": {
"name": "Il mio profilo",
"follow": "Seguire",
"followers": "Seguaci"
},
"settings": {
"name": "Impostazioni",
"data": {
"name": "I tuoi dati"
},
"security": {
"name": "Sicurezza"
},
"invites": {
"name": "Inviti"
},
"download": {
"name": "Scaricare i dati"
},
"delete": {
"name": "Elimina Account"
},
"organizations": {
"name": "Mie organizzazioni"
},
"languages": {
"name": "Lingue"
}
},
"admin": {
"name": "Admin",
"dashboard": {
"name": "Cruscotto",
"users": "Utenti",
"comments": "Commenti",
"notifications": "Notifiche",
"organizations": "Organizzazioni",
"projects": "Progetti",
"invites": "Inviti",
"follows": "Segue"
},
"organizations": {
"name": "Organizzazioni"
},
"users": {
"name": "Utenti"
},
"pages": {
"name": "Pagine"
},
"notifications": {
"name": "Notifiche"
},
"categories": {
"name": "Categorie",
"categoryName": "Nome"
},
"tags": {
"name": "Tag",
"tagCountUnique": "Utenti",
"tagCount": "Messaggi"
},
"settings": {
"name": "Impostazioni"
}
},
"post": {
"moreInfo": {
"name": "Ulteriori informazioni"
},
"takeAction": {
"name": "Agire"
}
},
"quotes": {
"african": {
"author": "Proverbio africano"
}
}
}

90
locales/nl.json Normal file
View File

@ -0,0 +1,90 @@
{
"login": {
"copy": "Als u al een mini-aansluiting account heeft, log dan hier in.",
"login": "Inloggen",
"logout": "Uitloggen",
"email": "Uw E-mail",
"password": "Uw Wachtwoord",
"moreInfo": "Wat is Human Connection?",
"hello": "Hallo"
},
"profile": {
"name": "Mijn profiel",
"memberSince": "Lid sinds",
"follow": "Volgen",
"followers": "Volgelingen",
"following": "Volgt"
},
"settings": {
"name": "Instellingen",
"data": {
"name": "Uw gegevens"
},
"security": {
"name": "Veiligheid"
},
"download": {
"name": "Gegevens downloaden"
},
"delete": {
"name": "Account verwijderen"
},
"organizations": {
"name": "Mijn Organisaties"
},
"languages": {
"name": "Talen"
}
},
"admin": {
"name": "Admin",
"dashboard": {
"name": "Dashboard",
"users": "Gebruikers",
"posts": "Berichten",
"comments": "Opmerkingen",
"notifications": "Meldingen",
"organizations": "Organisaties",
"projects": "Projecten",
"follows": "Volgt",
"shouts": "Shouts"
},
"organizations": {
"name": "Organisaties"
},
"users": {
"name": "Gebruikers"
},
"notifications": {
"name": "Meldingen"
},
"categories": {
"name": "Categorieën",
"categoryName": "Naam",
"postCount": "Berichten"
},
"tags": {
"name": "Tags",
"tagCountUnique": "Gebruikers",
"tagCount": "Berichten"
},
"settings": {
"name": "Instellingen"
}
},
"post": {
"name": "Post",
"moreInfo": {
"name": "Meer info"
},
"takeAction": {
"name": "Onderneem actie"
}
},
"quotes": {
"african": {
"quote": "Veel kleine mensen op veel kleine plaatsen doen veel kleine dingen, die het gezicht van de wereld kunnen veranderen.",
"author": "Afrikaans spreekwoord"
}
}
}

97
locales/pl.json Normal file
View File

@ -0,0 +1,97 @@
{
"login": {
"copy": "Jeśli masz już konto Human Connection, zaloguj się tutaj.",
"login": "Login",
"logout": "Wyloguj się",
"email": "Twój adres e-mail",
"password": "Twoje hasło",
"moreInfo": "Co to jest Human Connection?",
"hello": "Cześć"
},
"profile": {
"name": "Mój profil",
"memberSince": "Członek od",
"follow": "Obserwuj",
"followers": "Obserwujący",
"following": "Obserwowani"
},
"settings": {
"name": "Ustawienia",
"data": {
"name": "Twoje dane"
},
"security": {
"name": "Bezpieczeństwo"
},
"invites": {
"name": "Zaproszenia"
},
"download": {
"name": "Pobierz dane"
},
"delete": {
"name": "Usuń konto"
},
"organizations": {
"name": "Moje organizacje"
},
"languages": {
"name": "Języki"
}
},
"admin": {
"name": "Admin",
"dashboard": {
"name": "Tablica rozdzielcza",
"users": "Użytkownicy",
"posts": "Posty",
"comments": "Komentarze",
"notifications": "Powiadomienia",
"organizations": "Organizacje",
"projects": "Projekty",
"invites": "Zaproszenia",
"follows": "Obserwowań",
"shouts": "Wykrzyki"
},
"organizations": {
"name": "Organizacje"
},
"users": {
"name": "Użytkownicy"
},
"pages": {
"name": "Strony"
},
"notifications": {
"name": "Powiadomienia"
},
"categories": {
"name": "Kategorie",
"categoryName": "Nazwa",
"postCount": "Posty"
},
"tags": {
"name": "Tagi",
"tagCountUnique": "Użytkownicy",
"tagCount": "Posty"
},
"settings": {
"name": "Ustawienia"
}
},
"post": {
"name": "Post",
"moreInfo": {
"name": "Więcej informacji"
},
"takeAction": {
"name": "Podejmij działanie"
}
},
"quotes": {
"african": {
"quote": "Wielu małych ludzi w wielu małych miejscach robi wiele małych rzeczy, które mogą zmienić oblicze świata.",
"author": "Afrykańskie przysłowie"
}
}
}

96
locales/pt.json Normal file
View File

@ -0,0 +1,96 @@
{
"login": {
"copy": "Se você já tem uma conta no Human Connection, faça o login aqui.",
"login": "Login",
"logout": "Sair",
"email": "Seu email",
"password": "Sua senha",
"moreInfo": "O que é o Human Connection?",
"hello": "Olá"
},
"profile": {
"name": "Meu perfil",
"memberSince": "Membro desde",
"follow": "Seguir",
"followers": "Seguidores",
"following": "Seguindo"
},
"settings": {
"name": "Configurações",
"data": {
"name": "Seus Dados"
},
"security": {
"name": "Segurança"
},
"invites": {
"name": "Convites"
},
"download": {
"name": "Baixar dados"
},
"delete": {
"name": "Deletar conta"
},
"organizations": {
"name": "Minhas Organizações"
},
"languages": {
"name": "Linguagens"
}
},
"admin": {
"name": "Administrator",
"dashboard": {
"name": "Painel de controle",
"users": "Usuários",
"posts": "Postagens",
"comments": "Comentários",
"notifications": "Notificações",
"organizations": "Organizações",
"projects": "Projetos",
"invites": "Convites",
"follows": "Seguidores"
},
"organizations": {
"name": "Organizações"
},
"users": {
"name": "Usuários"
},
"pages": {
"name": "Páginas"
},
"notifications": {
"name": "Notificações"
},
"categories": {
"name": "Categorias",
"categoryName": "Nome",
"postCount": "Postagens"
},
"tags": {
"name": "Etiquetas",
"tagCountUnique": "Usuários",
"tagCount": "Postagens"
},
"settings": {
"name": "Configurações"
}
},
"post": {
"name": "Postar",
"moreInfo": {
"name": "Mais informações"
},
"takeAction": {
"name": "Tomar uma ação"
}
},
"quotes": {
"african": {
"quote": "Pequenos grupos de pessoas, em pequenos locais podem fazer várias coisas pequenas, mas que podem alterar o mundo ao nosso redor.",
"author": "Provérbio Africano"
}
}
}

BIN
lokalise.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

View File

@ -1,4 +1,4 @@
import { isEmpty } from 'lodash'
import isEmpty from 'lodash/isEmpty'
export default async ({ store, env, route, redirect }) => {
let publicPages = env.publicPages

View File

@ -26,7 +26,9 @@ module.exports = {
'pages-slug'
],
// pages to keep alive
keepAlivePages: ['index']
keepAlivePages: ['index'],
// active locales
locales: require('./locales')
},
/*
** Headers of the page
@ -59,6 +61,7 @@ module.exports = {
** Plugins to load before mounting the App
*/
plugins: [
{ src: '~/plugins/i18n.js', ssr: true },
{ src: '~/plugins/keep-alive.js', ssr: false },
{ src: '~/plugins/design-system.js', ssr: true },
{ src: '~/plugins/vue-directives.js', ssr: false },
@ -71,30 +74,6 @@ module.exports = {
middleware: ['authenticated'],
linkActiveClass: 'router-active-link'
},
/* router: {
routes: [
{
name: 'index',
path: '/',
component: 'pages/index.vue'
},
{
name: 'post-slug',
path: '/post/:slug',
component: 'pages/post/_slug.vue',
children: [
{
path: 'more-info',
component: 'pages/post/_slug.vue'
},
{
path: 'take-action',
component: 'pages/post/_slug.vue'
}
]
}
]
}, */
/*
** Nuxt.js modules
@ -102,6 +81,7 @@ module.exports = {
modules: [
['@nuxtjs/dotenv', { only: envWhitelist }],
['nuxt-env', { keys: envWhitelist }],
'cookie-universal-nuxt',
'@nuxtjs/apollo',
'@nuxtjs/axios',
[
@ -172,6 +152,7 @@ module.exports = {
*/
build: {
/*
* TODO: import the polyfill instead of using the deprecated vendor key
* Polyfill missing ES6 & 7 Methods to work on older Browser
*/
vendor: ['@babel/polyfill'],

View File

@ -42,6 +42,7 @@
"@nuxtjs/axios": "^5.3.6",
"@nuxtjs/dotenv": "^1.3.0",
"accounting": "^0.4.1",
"cookie-universal-nuxt": "^2.0.11",
"cross-env": "^5.2.0",
"date-fns": "^2.0.0-alpha.26",
"express": "^4.16.3",
@ -52,7 +53,8 @@
"nuxt-env": "^0.0.4",
"v-tooltip": "^2.0.0-rc.33",
"vue-count-to": "^1.0.13",
"vue-izitoast": "1.1.2"
"vue-izitoast": "1.1.2",
"vuex-i18n": "^1.10.5"
},
"devDependencies": {
"@vue/eslint-config-prettier": "^4.0.1",

View File

@ -1,7 +1,7 @@
<template>
<div>
<ds-heading tag="h1">
Administartion
{{ $t('admin.name') }}
</ds-heading>
<ds-flex gutter="small">
<ds-flex-item :width="{ base: '200px' }">
@ -21,39 +21,39 @@
<script>
export default {
data() {
return {
routes: [
computed: {
routes() {
return [
{
name: 'Dashboard',
name: this.$t('admin.dashboard.name'),
path: `/admin`
},
{
name: 'Users',
name: this.$t('admin.users.name'),
path: `/admin/users`
},
{
name: 'Organizations',
name: this.$t('admin.organizations.name'),
path: `/admin/organizations`
},
{
name: 'Pages',
name: this.$t('admin.pages.name'),
path: `/admin/pages`
},
{
name: 'Notifications',
name: this.$t('admin.notifications.name'),
path: `/admin/notifications`
},
{
name: 'Categories',
name: this.$t('admin.categories.name'),
path: `/admin/categories`
},
{
name: 'Tags',
name: this.$t('admin.tags.name'),
path: `/admin/tags`
},
{
name: 'Settings',
name: this.$t('admin.settings.name'),
path: `/admin/settings`
}
]

View File

@ -1,11 +1,11 @@
<template>
<ds-card space="small">
<ds-heading tag="h3">
Themen / Kategorien
{{ $t('admin.categories.name') }}
</ds-heading>
<ds-table
:data="Category"
:fields="['icon', 'name', 'postCount']"
:fields="fields"
condensed
>
<template
@ -27,6 +27,18 @@ export default {
Category: []
}
},
computed: {
fields() {
return {
icon: ' ',
name: this.$t('admin.categories.categoryName'),
postCount: {
label: this.$t('admin.categories.postCount'),
align: 'right'
}
}
}
},
apollo: {
Category: {
query: gql(`

View File

@ -7,7 +7,7 @@
<ds-space margin="small">
<ds-number
:count="0"
label="Users"
:label="$t('admin.dashboard.users')"
size="x-large"
uppercase
>
@ -24,7 +24,7 @@
<ds-space margin="small">
<ds-number
:count="0"
label="Posts"
:label="$t('admin.dashboard.posts')"
size="x-large"
uppercase
>
@ -41,7 +41,7 @@
<ds-space margin="small">
<ds-number
:count="0"
label="Comments"
:label="$t('admin.dashboard.comments')"
size="x-large"
uppercase
>
@ -58,7 +58,7 @@
<ds-space margin="small">
<ds-number
:count="0"
label="Notifications"
:label="$t('admin.dashboard.notifications')"
size="x-large"
uppercase
>
@ -75,7 +75,7 @@
<ds-space margin="small">
<ds-number
:count="0"
label="Organization"
:label="$t('admin.dashboard.organizations')"
size="x-large"
uppercase
>
@ -92,7 +92,7 @@
<ds-space margin="small">
<ds-number
:count="0"
label="Projects"
:label="$t('admin.dashboard.projects')"
size="x-large"
uppercase
>
@ -109,7 +109,7 @@
<ds-space margin="small">
<ds-number
:count="0"
label="Open Invites"
:label="$t('admin.dashboard.invites')"
size="x-large"
uppercase
>
@ -126,7 +126,7 @@
<ds-space margin="small">
<ds-number
:count="0"
label="Follows"
:label="$t('admin.dashboard.follows')"
size="x-large"
uppercase
>
@ -143,7 +143,7 @@
<ds-space margin="small">
<ds-number
:count="0"
label="Shouts"
:label="$t('admin.dashboard.shouts')"
size="x-large"
uppercase
>

View File

@ -1,7 +1,7 @@
<template>
<ds-card>
<ds-space margin="small">
Notifications...
</ds-space>
<ds-card space="small">
<ds-heading tag="h3">
{{ $t('admin.notifications.name') }}
</ds-heading>
</ds-card>
</template>

View File

@ -1,7 +1,7 @@
<template>
<ds-card>
<ds-space margin="small">
Organizations...
{{ $t('admin.organizations.name') }}
</ds-space>
</ds-card>
</template>

View File

@ -1,7 +1,7 @@
<template>
<ds-card>
<ds-space margin="small">
Pages...
{{ $t('admin.pages.name') }}
</ds-space>
</ds-card>
</template>

View File

@ -1,7 +1,7 @@
<template>
<ds-card>
<ds-space margin="small">
Settings...
{{ $t('admin.settings.name') }}
</ds-space>
</ds-card>
</template>

View File

@ -1,7 +1,7 @@
<template>
<ds-card space="small">
<ds-heading tag="h3">
Tags
{{ $t('admin.tags.name') }}
</ds-heading>
<ds-table
:data="Tag"
@ -24,12 +24,22 @@ import gql from 'graphql-tag'
export default {
data() {
return {
Tag: [],
fields: {
id: { label: '#' },
name: { label: 'Name' },
taggedCountUnique: { label: 'Nutzer' },
taggedCount: { label: 'Beiträge' }
Tag: []
}
},
computed: {
fields() {
return {
id: '#',
name: 'Name',
taggedCountUnique: {
label: this.$t('admin.tags.tagCountUnique'),
align: 'right'
},
taggedCount: {
label: this.$t('admin.tags.tagCount'),
align: 'right'
}
}
}
},

View File

@ -1,7 +1,7 @@
<template>
<ds-card>
<ds-space margin="small">
Users...
{{ $t('admin.users.name') }}
</ds-space>
</ds-card>
</template>

View File

@ -1,93 +1,112 @@
<template>
<ds-container width="small">
<ds-space margin="small">
<blockquote>
<p>
Viele kleine Leute, an vielen kleinen Orten, die viele kleine Dinge tun, werden das Antlitz dieser Welt verändern.
</p>
<b>- Afrikanisches Sprichwort</b>
</blockquote>
</ds-space>
<ds-card>
<ds-flex gutter="small">
<ds-flex-item
:width="{ base: '100%', sm: '50%' }"
center
>
<ds-space
margin-top="small"
margin-bottom="xxx-small"
<transition
name="fade"
appear
>
<ds-container
v-if="ready"
width="small"
>
<ds-space margin="small">
<blockquote>
<p>{{ $t('quotes.african.quote') }}</p>
<b>- {{ $t('quotes.african.author') }}</b>
</blockquote>
</ds-space>
<ds-card class="login-card">
<ds-flex gutter="small">
<ds-flex-item
:width="{ base: '100%', sm: '50%' }"
center
>
<img
class="login-image"
src="/img/sign-up/humanconnection.svg"
alt="Human Connection"
<no-ssr>
<locale-switch
class="login-locale-switch"
offset="5"
/>
</no-ssr>
<ds-space
margin-top="small"
margin-bottom="xxx-small"
center
>
</ds-space>
</ds-flex-item>
<ds-flex-item
:width="{ base: '100%', sm: '50%' }"
center
>
<ds-space margin="small">
<ds-text size="small">
Wenn Du ein Konto bei Human Connection hast, melde Dich bitte hier an.
</ds-text>
</ds-space>
<form
:disabled="pending"
@submit.prevent="onSubmit"
>
<ds-input
v-model="form.email"
:disabled="pending"
placeholder="Deine E-Mail"
type="email"
name="email"
icon="envelope"
/>
<ds-input
v-model="form.password"
:disabled="pending"
placeholder="Dein Password"
icon="lock"
icon-right="question-circle"
name="password"
type="password"
/>
<ds-button
:loading="pending"
primary
full-width
name="submit"
type="submit"
>
Anmelden
</ds-button>
<ds-space margin="x-small">
<a
href="https://human-connection.org"
title="zur Präsentationsseite"
target="_blank"
<img
class="login-image"
src="/img/sign-up/humanconnection.svg"
alt="Human Connection"
>
Was ist Human Connection?
</a>
</ds-space>
</form>
</ds-flex-item>
</ds-flex>
</ds-card>
</ds-container>
</ds-flex-item>
<ds-flex-item
:width="{ base: '100%', sm: '50%' }"
center
>
<ds-space margin="small">
<ds-text size="small">
{{ $t('login.copy') }}
</ds-text>
</ds-space>
<form
:disabled="pending"
@submit.prevent="onSubmit"
>
<ds-input
v-model="form.email"
:disabled="pending"
:placeholder="$t('login.email')"
type="email"
name="email"
icon="envelope"
/>
<ds-input
v-model="form.password"
:disabled="pending"
:placeholder="$t('login.password')"
icon="lock"
icon-right="question-circle"
name="password"
type="password"
/>
<ds-button
:loading="pending"
primary
full-width
name="submit"
type="submit"
icon="sign-in"
>
{{ $t('login.login') }}
</ds-button>
<ds-space margin="x-small">
<a
href="https://human-connection.org"
title="zur Präsentationsseite"
target="_blank"
>
{{ $t('login.moreInfo') }}
</a>
</ds-space>
</form>
</ds-flex-item>
</ds-flex>
</ds-card>
</ds-container>
</transition>
</template>
<script>
import LocaleSwitch from '~/components/LocaleSwitch'
import gql from 'graphql-tag'
export default {
components: {
LocaleSwitch
},
layout: 'blank',
data() {
return {
ready: false,
form: {
email: '',
password: ''
@ -104,6 +123,13 @@ export default {
return this.$store.getters['auth/pending']
}
},
mounted() {
setTimeout(() => {
// NOTE: quick fix for jumping flexbox implementation
// will be fixed in a future update of the styleguide
this.ready = true
}, 50)
},
methods: {
async onSubmit() {
try {
@ -123,4 +149,12 @@ export default {
width: 90%;
max-width: 200px;
}
.login-card {
position: relative;
}
.login-locale-switch {
position: absolute;
top: 1em;
left: 1em;
}
</style>

View File

@ -30,7 +30,7 @@
color="soft"
size="small"
>
Mitglied seit {{ user.createdAt | date('MMMM yyyy') }}
{{ $t('profile.memberSince') }} {{ user.createdAt | date('MMMM yyyy') }}
</ds-text>
</ds-space>
<ds-space
@ -44,7 +44,7 @@
<ds-flex>
<ds-flex-item>
<no-ssr>
<ds-number label="Folgen">
<ds-number :label="$t('profile.following')">
<hc-count-to
slot="count"
:end-val="followedByCount"
@ -54,7 +54,7 @@
</ds-flex-item>
<ds-flex-item>
<no-ssr>
<ds-number label="Folgt">
<ds-number :label="$t('profile.followers')">
<hc-count-to
slot="count"
:end-val="Number(user.followingCount) || 0"

View File

@ -1,7 +1,7 @@
<template>
<div>
<ds-heading tag="h1">
Settings
{{ $t('settings.name') }}
</ds-heading>
<ds-flex gutter="small">
<ds-flex-item :width="{ base: '200px' }">
@ -21,36 +21,36 @@
<script>
export default {
data() {
return {
routes: [
computed: {
routes() {
return [
{
name: 'Your Data',
name: this.$t('settings.data.name'),
path: `/settings`
},
{
name: 'Password',
path: `/settings/password`
name: this.$t('settings.security.name'),
path: `/settings/security`
},
{
name: 'Invites',
name: this.$t('settings.invites.name'),
path: `/settings/invites`
},
{
name: 'Data Download',
name: this.$t('settings.download.name'),
path: `/settings/data-download`
},
{
name: 'Delete Account',
name: this.$t('settings.delete.name'),
path: `/settings/delete-account`
},
{
name: 'My Organizations',
name: this.$t('settings.organizations.name'),
path: `/settings/my-organizations`
},
{
name: 'Settings',
path: `/settings/settings`
name: this.$t('settings.languages.name'),
path: `/settings/languages`
}
]
}

View File

@ -1,7 +1,7 @@
<template>
<ds-card>
<ds-space margin="small">
Download my Data...
{{ $t('settings.download.name') }}
</ds-space>
</ds-card>
</template>

View File

@ -1,7 +1,7 @@
<template>
<ds-card>
<ds-space margin="small">
Delete my Account...
{{ $t('settings.delete.name') }}
</ds-space>
</ds-card>
</template>

View File

@ -1,5 +1,7 @@
<template>
<ds-card>
<p>My Data...</p>
<ds-space margin="small">
{{ $t('settings.data.name') }}
</ds-space>
</ds-card>
</template>

View File

@ -1,7 +1,7 @@
<template>
<ds-card>
<ds-space margin="small">
Invites...
{{ $t('settings.invites.name') }}
</ds-space>
</ds-card>
</template>

View File

@ -1,7 +1,7 @@
<template>
<ds-card>
<ds-space margin="small">
Settings...
{{ $t('settings.languages.name') }}
</ds-space>
</ds-card>
</template>

View File

@ -1,7 +1,7 @@
<template>
<ds-card>
<ds-space margin="small">
My Organizations...
{{ $t('settings.organizations.name') }}
</ds-space>
</ds-card>
</template>

View File

@ -1,7 +1,7 @@
<template>
<ds-card>
<ds-space margin="small">
Change my Password...
{{ $t('settings.security.name') }}
</ds-space>
</ds-card>
</template>

102
plugins/i18n.js Normal file
View File

@ -0,0 +1,102 @@
import Vue from 'vue'
import Vuex from 'vuex'
import vuexI18n from 'vuex-i18n/dist/vuex-i18n.umd.js'
import { debounce, isEmpty, find } from 'lodash'
/**
* TODO: Refactor and simplify browser detection
* and implement the user preference logic
*/
export default ({ app, req, cookie, store }) => {
const debug = app.$env.NODE_ENV !== 'production'
const key = 'locale'
const changeHandler = async mutation => {
if (process.server) return
const newLocale = mutation.payload.locale
const currentLocale = await app.$cookies.get(key)
const isDifferent = newLocale !== currentLocale
if (!isDifferent) {
return
}
app.$cookies.set(key, newLocale)
if (!app.$i18n.localeExists(newLocale)) {
import(`~/locales/${newLocale}.json`).then(res => {
app.$i18n.add(newLocale, res.default)
})
}
const user = store.getters['auth/user']
const token = store.getters['auth/token']
// persist language if it differs from last value
if (user && user._id && token) {
// TODO: SAVE LOCALE
// store.dispatch('usersettings/patch', {
// uiLanguage: newLocale
// }, { root: true })
}
}
// const i18nStore = new Vuex.Store({
// strict: debug
// })
Vue.use(vuexI18n.plugin, store, {
onTranslationNotFound: function(locale, key) {
if (debug) {
console.warn(
`vuex-i18n :: Key '${key}' not found for locale '${locale}'`
)
}
}
})
// register the fallback locales
Vue.i18n.add('en', require('~/locales/en.json'))
let userLocale = 'en'
const localeCookie = app.$cookies.get(key)
/* const userSettings = store.getters['auth/userSettings']
if (userSettings && userSettings.uiLanguage) {
// try to get saved user preference
userLocale = userSettings.uiLanguage
} else */
if (!isEmpty(localeCookie)) {
userLocale = localeCookie
} else {
userLocale = process.browser
? navigator.language || navigator.userLanguage
: req.locale
if (userLocale && !isEmpty(userLocale.language)) {
userLocale = userLocale.language.substr(0, 2)
}
}
const availableLocales = process.env.locales.filter(lang => !!lang.enabled)
const locale = find(availableLocales, ['code', userLocale])
? userLocale
: 'en'
if (locale !== 'en') {
Vue.i18n.add(locale, require(`~/locales/${locale}.json`))
}
// Set the start locale to use
Vue.i18n.set(locale)
Vue.i18n.fallback('en')
if (process.browser) {
store.subscribe(mutation => {
if (mutation.type === 'i18n/SET_LOCALE') {
changeHandler(mutation)
}
})
}
app.$i18n = Vue.i18n
return store
}

View File

@ -1,25 +1,44 @@
import Vue from 'vue'
import { en, de } from 'date-fns/locale'
import { enUS, de, nl, fr, es } from 'date-fns/locale'
import format from 'date-fns/format'
import formatRelative from 'date-fns/formatRelative'
import addSeconds from 'date-fns/addSeconds'
import accounting from 'accounting'
export default ({ app }) => {
const locales = {
en: enUS,
de: de,
nl: nl,
fr: fr,
es: es,
pt: es,
pl: de
}
const getLocalizedFormat = () => {
let locale = app.$i18n.locale()
locale = locales[locale] ? locale : 'en'
return locales[locale]
}
app.$filters = Object.assign(app.$filters || {}, {
date: (value, fmt = 'dd. MMM yyyy') => {
if (!value) return ''
return format(new Date(value), fmt, { locale: de })
return format(new Date(value), fmt, {
locale: getLocalizedFormat()
})
},
dateTime: (value, fmt = 'dd. MMM yyyy HH:mm') => {
if (!value) return ''
return format(new Date(value), fmt, { locale: de })
return format(new Date(value), fmt, {
locale: getLocalizedFormat()
})
},
relativeDateTime: value => {
if (!value) return ''
return formatRelative(new Date(value), new Date(), { locale: de })
return formatRelative(new Date(value), new Date(), {
locale: getLocalizedFormat()
})
},
number: (
value,

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path d="M16 345a256 256 0 0 0 480 0l-240-22.2L16 345z" fill="#ffda44"/><path d="M256 0A256 256 0 0 0 16 167l240 22.2L496 167A256 256 0 0 0 256 0z"/><path d="M16 167a255.5 255.5 0 0 0 0 178h480a255.4 255.4 0 0 0 0-178H16z" fill="#d80027"/></svg>

After

Width:  |  Height:  |  Size: 307 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="512" height="512"><defs><path id="a" d="M0 512L512 0v512z"/></defs><g fill="none" fill-rule="evenodd"><circle cx="256" cy="256" r="256" fill="#F0F0F0"/><path fill="#D80027" fill-rule="nonzero" d="M327.7 189.2H245V256h15.6l67.2-66.8zm146.7-66.8a257.3 257.3 0 0 0-59-66.7H244.9v66.7h229.5zM127.8 389.6l67.4-66.8H8.8c6.4 23.5 16 46 28.8 66.8h90.2z"/><path fill="#0052B4" fill-rule="nonzero" d="M118.6 40h23.3l-21.7 15.7 8.3 25.6-21.7-15.8-21.7 15.8 7.2-22a257.4 257.4 0 0 0-49.7 55.3h7.5l-13.8 10c-2.2 3.6-4.2 7.2-6.2 11l6.6 20.2-12.3-9c-3.1 6.6-5.9 13.2-8.4 20l7.3 22.3H50L28.4 205l8.3 25.5L15 214.6l-13 9.5C.7 234.7 0 245.3 0 256h256V0c-50.6 0-97.7 14.7-137.4 40zm9.9 190.4l-21.7-15.8-21.7 15.8 8.3-25.5L71.7 189h26.8l8.3-25.5 8.3 25.5h26.8L120.2 205l8.3 25.5zm-8.3-100l8.3 25.4-21.7-15.7-21.7 15.7 8.3-25.5-21.7-15.7h26.8l8.3-25.6 8.3 25.6h26.8l-21.7 15.7zm100.1 100l-21.7-15.8-21.7 15.8 8.3-25.5-21.7-15.8h26.8l8.3-25.5 8.3 25.5h26.8L212 205l8.3 25.5zm-8.3-100l8.3 25.4-21.7-15.7-21.7 15.7 8.3-25.5-21.7-15.7h26.8l8.3-25.6 8.3 25.6h26.8L212 130.3zm0-74.7l8.3 25.6-21.7-15.8L177 81.3l8.3-25.6L163.5 40h26.8l8.3-25.5L207 40h26.8L212 55.7z"/><mask id="b" fill="#fff"><use xlink:href="#a"/></mask><g mask="url(#b)"><circle cx="256" cy="256" r="256" fill="#F0F0F0"/><path fill="#0052B4" fill-rule="nonzero" d="M503.2 189.2a255 255 0 0 0-44.1-89l-89.1 89h133.2zm-403 269.9a255 255 0 0 0 89 44V370l-89 89zm222.6 44a255 255 0 0 0 89-44l-89-89.1v133.2zM370 322.9l89 89a255 255 0 0 0 44.2-89H370z"/><path fill="#D80027" d="M509.8 222.6H222.4l.2 287.2c22.2 3 44.6 3 66.8 0V289.4h220.4a258.5 258.5 0 0 0 0-66.8z"/><path fill="#D80027" fill-rule="nonzero" d="M322.8 322.8L437 437c5.3-5.2 10.3-10.7 15-16.4l-97.7-97.8h-31.5zm-133.6 0L75 437c5.2 5.3 10.7 10.3 16.4 15l97.8-97.7v-31.5z"/></g></g></svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path d="M0 256c0 31.3 5.6 61.3 16 89l240 22.3L496 345a255.5 255.5 0 0 0 0-178l-240-22.3L16 167a255.5 255.5 0 0 0-16 89z" fill="#ffda44"/><path d="M496 167a256 256 0 0 0-480 0h480zM16 345a256 256 0 0 0 480 0H16z" fill="#d80027"/></svg>

After

Width:  |  Height:  |  Size: 297 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><circle cx="256" cy="256" r="256" fill="#f0f0f0"/><path d="M512 256A256 256 0 0 0 345 16v480a256 256 0 0 0 167-240z" fill="#d80027"/><path d="M0 256a256 256 0 0 0 167 240V16A256 256 0 0 0 0 256z" fill="#0052b4"/></svg>

After

Width:  |  Height:  |  Size: 280 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><circle cx="256" cy="256" r="256" fill="#f0f0f0"/><path d="M512 256A256 256 0 0 0 345 16v480a256 256 0 0 0 167-240z" fill="#d80027"/><path d="M0 256a256 256 0 0 0 167 240V16A256 256 0 0 0 0 256z" fill="#6da544"/></svg>

After

Width:  |  Height:  |  Size: 280 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><circle cx="256" cy="256" r="256" fill="#f0f0f0"/><path d="M256 0A256 256 0 0 0 16 167h480A256 256 0 0 0 256 0z" fill="#a2001d"/><path d="M256 512a256 256 0 0 0 240-167H16a256 256 0 0 0 240 167z" fill="#0052b4"/></svg>

After

Width:  |  Height:  |  Size: 280 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><circle cx="256" cy="256" r="256" fill="#f0f0f0"/><path d="M512 256a256 256 0 0 1-512 0" fill="#d80027"/></svg>

After

Width:  |  Height:  |  Size: 173 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path d="M0 256a256 256 0 0 0 167 240l22.2-240L167 16A256 256 0 0 0 0 256z" fill="#6da544"/><path d="M512 256A256 256 0 0 0 167 16v480a256 256 0 0 0 345-240z" fill="#d80027"/><circle cx="167" cy="256" r="89" fill="#ffda44"/><path d="M116.9 211.5V267a50 50 0 1 0 100.1 0v-55.6H117z" fill="#d80027"/><path d="M167 283.8c-9.2 0-16.7-7.5-16.7-16.7V245h33.4V267c0 9.2-7.5 16.7-16.7 16.7z" fill="#f0f0f0"/></svg>

After

Width:  |  Height:  |  Size: 468 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 88 KiB

After

Width:  |  Height:  |  Size: 220 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 202 KiB

After

Width:  |  Height:  |  Size: 180 KiB

View File

@ -20,7 +20,8 @@
<tr>
<ds-table-head-col
v-for="header in headers"
:key="header.key">
:key="header.key"
:align="align(header.key)">
{{ header.label }}
</ds-table-head-col>
</tr>
@ -31,7 +32,8 @@
:key="row.key || index">
<ds-table-col
v-for="col in row"
:key="col.key">
:key="col.key"
:align="align(col.key)">
<!-- @slot Slots are named by fields -->
<slot
:name="col.key"
@ -152,6 +154,9 @@ export default {
}
},
methods: {
align(colKey) {
return this.fields && this.fields[colKey] ? this.fields[colKey].align : null
},
parseLabel(label) {
return startCase(label)
}

View File

@ -1,5 +1,10 @@
<template>
<td class="ds-table-col">
<!-- eslint-disable -->
<td
class="ds-table-col"
:class="[
align && `ds-table-col-${align}`
]">
<slot/>
</td>
</template>
@ -25,6 +30,17 @@ export default {
width: {
type: [String, Number, Object],
default: null
},
/**
* The column align
* `left, center, right`
*/
align: {
type: String,
default: null,
validator: value => {
return value.match(/(left|center|right)/)
}
}
},
computed: {}

View File

@ -1,5 +1,9 @@
<template>
<th class="ds-table-head-col">
<th
class="ds-table-head-col"
:class="[
align && `ds-table-head-col-${align}`
]">
<slot>
{{ label }}
</slot>
@ -36,6 +40,17 @@ export default {
width: {
type: [String, Number, Object],
default: null
},
/**
* The column align
* `left, center, right`
*/
align: {
type: String,
default: null,
validator: value => {
return value.match(/(left|center|right)/)
}
}
},
computed: {}

View File

@ -100,7 +100,8 @@ The value can be a string representing the fields label or an object with option
name: 'Hero',
type: {
label: 'Job',
width: '300px'
width: '300px',
align: 'right'
}
},
tableData: [
@ -191,4 +192,4 @@ Via scoped slots you have access to the columns `row`, `index` and `col`.
}
}
</script>
```
```

View File

@ -42,3 +42,19 @@
padding-bottom: $space-x-small;
}
}
.ds-table-col,
.ds-table-head-col {
&.ds-table-col-left,
&.ds-table-head-col-left {
text-align: left;
}
&.ds-table-col-center,
&.ds-table-head-col-center {
text-align: center;
}
&.ds-table-col-right,
&.ds-table-head-col-right {
text-align: right;
}
}

View File

@ -46,7 +46,10 @@ export default {
default: null
},
align: {
/**
* Center content vertacally and horizontally
*/
center: {
type: Boolean,
default: false
},

View File

@ -44,6 +44,7 @@ ul.ds-menu-list {
text-decoration: none;
padding: $space-x-small $space-small;
transition: color $duration-short $ease-out;
border-left: 2px solid transparent;
&.router-link-active,
&.nuxt-link-active {
@ -56,8 +57,9 @@ ul.ds-menu-list {
&.router-link-exact-active,
&.nuxt-link-exact-active {
color: $text-color-link-active;
background-color: $background-color-soft;
color: $text-color-link;
// background-color: $background-color-soft;
border-left: 2px solid $color-primary;
}
.ds-menu-item-inverse & {

View File

@ -1225,6 +1225,11 @@
dependencies:
"@types/node" "*"
"@types/cookie@^0.3.1":
version "0.3.2"
resolved "https://registry.yarnpkg.com/@types/cookie/-/cookie-0.3.2.tgz#453f4b14b25da6a8ea4494842dedcbf0151deef9"
integrity sha512-aHQA072E10/8iUQsPH7mQU/KUyQBZAGzTVRCUvnSz8mSvbrYsP4xEO2RSA0Pjltolzi0j8+8ixrm//Hr4umPzw==
"@types/cors@^2.8.4":
version "2.8.4"
resolved "https://registry.yarnpkg.com/@types/cors/-/cors-2.8.4.tgz#50991a759a29c0b89492751008c6af7a7c8267b0"
@ -3788,6 +3793,21 @@ cookie-signature@1.0.6:
resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c"
integrity sha1-4wOogrNCzD7oylE6eZmXNNqzriw=
cookie-universal-nuxt@^2.0.11:
version "2.0.11"
resolved "https://registry.yarnpkg.com/cookie-universal-nuxt/-/cookie-universal-nuxt-2.0.11.tgz#f7f2ea1d63a497b24d5e630cee4e999475e777f4"
integrity sha512-GmzXjkIpCRAe+fIpYavHyFliEDRofiJMbBd17kZJia+nLVEt2CJxjOqcQRqUOXmvtuB0oHDYy+fVflxs863T0w==
dependencies:
"@types/cookie" "^0.3.1"
cookie-universal "^2.0.11"
cookie-universal@^2.0.11:
version "2.0.11"
resolved "https://registry.yarnpkg.com/cookie-universal/-/cookie-universal-2.0.11.tgz#525f6324b6698a713b46be77a3440daff68e4a6a"
integrity sha512-5IA+uAPwBuphUfuk+lpFEajfrUTZA3RmMXfoT/KWs29WMd2ZifHOafAr+w8WdlSixddjTEkOgUWrk27+ErueUg==
dependencies:
cookie "^0.3.1"
cookie@0.3.1, cookie@^0.3.1:
version "0.3.1"
resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.3.1.tgz#e7e0a1f9ef43b4c8ba925c5c5a96e806d16873bb"
@ -11662,6 +11682,11 @@ vue@^2.5.17:
resolved "https://registry.yarnpkg.com/vue/-/vue-2.5.17.tgz#0f8789ad718be68ca1872629832ed533589c6ada"
integrity sha512-mFbcWoDIJi0w0Za4emyLiW72Jae0yjANHbCVquMKijcavBGypqlF7zHRgMa5k4sesdv7hv2rB4JPdZfR+TPfhQ==
vuex-i18n@^1.10.5:
version "1.10.5"
resolved "https://registry.yarnpkg.com/vuex-i18n/-/vuex-i18n-1.10.5.tgz#635ea2204e0aa3f8fd512f0fab7f6b994d3f666c"
integrity sha1-Y16iIE4Ko/j9US8Pq39rmU0/Zmw=
vuex@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/vuex/-/vuex-3.0.1.tgz#e761352ebe0af537d4bb755a9b9dc4be3df7efd2"