Commit 98381a00 authored by Eliot Berriot's avatar Eliot Berriot
Browse files

Merge branch 'i18n' into 'develop'

I18n

Closes #5

See merge request funkwhale/funkwhale!125
parents e608de2c 1341c9aa
......@@ -35,7 +35,6 @@ htmlcov
# Translations
*.mo
*.pot
# Pycharm
.idea
......@@ -75,6 +74,7 @@ api/static
api/.pytest_cache
# Front
front/static/translations
front/node_modules/
front/dist/
front/npm-debug.log*
......
Add internationalization support (#5)
......@@ -13,6 +13,7 @@ services:
- "${WEBPACK_DEVSERVER_PORT-8080}:${WEBPACK_DEVSERVER_PORT-8080}"
volumes:
- './front:/app'
- './po:/po'
postgres:
env_file:
......
......@@ -14,6 +14,8 @@ var webpackConfig = process.env.NODE_ENV === 'testing'
? require('./webpack.prod.conf')
: require('./webpack.dev.conf')
require('./i18n')
// default port where dev server listens for incoming traffic
var port = process.env.PORT || config.dev.port
var host = process.env.HOST || config.dev.host
......
const fs = require('fs');
const path = require('path');
const { gettextToI18next } = require('i18next-conv');
const poDir = path.join(__dirname, '..', '..', 'po')
const outDir = path.join(__dirname, '..', 'static', 'translations')
if (!fs.existsSync(outDir) || !fs.statSync(outDir).isDirectory()) {
fs.mkdirSync(outDir)
}
// Convert .po files to i18next files
fs.readdir(poDir, (err, files) => {
if (err) {
return console.log(err)
}
for (const file of files) {
if (file.endsWith('.po')) {
const lang = file.replace(/\.po$/, '')
const output = path.join(outDir, `${lang}.json`)
fs.readFile(path.join(poDir, file), (err, content) => {
if (err) {
return console.log(err)
}
gettextToI18next(lang, content).then(res => {
fs.writeFile(output, res, err => {
if (err) {
console.log(err)
} else {
console.log(`Wrote translation file: ${output}`)
}
})
})
})
}
}
})
......@@ -15,9 +15,13 @@
"lint": "eslint --ext .js,.vue src test/unit/specs test/e2e/specs"
},
"dependencies": {
"@panter/vue-i18next": "^0.9.1",
"axios": "^0.17.1",
"dateformat": "^2.0.0",
"django-channels": "^1.1.6",
"i18next": "^11.1.1",
"i18next-conv": "^6.0.0",
"i18next-fetch-backend": "^0.1.0",
"js-logger": "^1.3.0",
"jwt-decode": "^2.2.0",
"lodash": "^4.17.4",
......@@ -34,7 +38,7 @@
"vue-upload-component": "^2.7.4",
"vuedraggable": "^2.14.1",
"vuex": "^3.0.1",
"vuex-persistedstate": "^2.4.2",
"vuex-persistedstate": "^2.5.2",
"vuex-router-sync": "^5.0.0"
},
"devDependencies": {
......
......@@ -7,21 +7,25 @@
<div class="ui container">
<div class="ui stackable equal height stackable grid">
<div class="three wide column">
<h4 class="ui header">Links</h4>
<i18next tag="h4" class="ui header" path="Links"></i18next>
<div class="ui link list">
<router-link class="item" to="/about">
About this instance
<i18next path="About this instance" />
</router-link>
<a href="https://funkwhale.audio" class="item" target="_blank">Official website</a>
<a href="https://docs.funkwhale.audio" class="item" target="_blank">Documentation</a>
<a href="https://code.eliotberriot.com/funkwhale/funkwhale" class="item" target="_blank">Source code</a>
<a href="https://code.eliotberriot.com/funkwhale/funkwhale/issues" class="item" target="_blank">Issue tracker</a>
<i18next tag="a" href="https://funkwhale.audio" class="item" target="_blank" path="Official website" />
<i18next tag="a" href="https://docs.funkwhale.audio" class="item" target="_blank" path="Documentation" />
<i18next tag="a" href="https://code.eliotberriot.com/funkwhale/funkwhale" class="item" target="_blank" path="Source code" />
<i18next tag="a" href="https://code.eliotberriot.com/funkwhale/funkwhale/issues" class="item" target="_blank" path="Issue tracker" />
</div>
</div>
<div class="ten wide column">
<h4 class="ui header">About funkwhale</h4>
<p>Funkwhale is a free and open-source project run by volunteers. You can help us improve the platform by reporting bugs, suggesting features and share the project with your friends!</p>
<p>The funkwhale logo was kindly designed and provided by Francis Gading.</p>
<i18next tag="h4" class="ui header" path="About funkwhale" />
<p>
<i18next path="Funkwhale is a free and open-source project run by volunteers. You can help us improve the platform by reporting bugs, suggesting features and share the project with your friends!"/>
</p>
<p>
<i18next path="The funkwhale logo was kindly designed and provided by Francis Gading."/>
</p>
</div>
</div>
</div>
......@@ -31,7 +35,6 @@
:dsn="$store.state.instance.settings.raven.front_dsn.value">
</raven>
<playlist-modal v-if="$store.state.auth.authenticated"></playlist-modal>
</div>
</template>
......
......@@ -5,17 +5,20 @@
</div>
<div class="content">
<div class="summary">
<slot name="user"></slot>
favorited a track
<slot name="date"></slot>
<i18next path="{%0%} favorited a track {%1%}">
<slot name="user"></slot>
<slot name="date"></slot>
</i18next>
</div>
<div class="extra text">
<router-link :to="{name: 'library.tracks.detail', params: {id: event.object.local_id }}">{{ event.object.name }}</router-link>
<template v-if="event.object.album">from album {{ event.object.album }}, by <em>{{ event.object.artist }}</em>
</template>
<template v-else>, by <em>{{ event.object.artist }}</em>
</template>
<i18next path="from album {%0%}, by {%1%}" v-if="event.object.album">
{{ event.object.album }}
<em>{{ event.object.artist }}</em>
</i18next>
<i18next path=", by {%0%}" v-else>
<em>{{ event.object.artist }}</em>
</i18next>
</div>
</div>
</div>
......
......@@ -5,17 +5,20 @@
</div>
<div class="content">
<div class="summary">
<slot name="user"></slot>
listened to a track
<slot name="date"></slot>
<i18next path="{%0%} listened to a track {%1%}">
<slot name="user"></slot>
<slot name="date"></slot>
</i18next>
</div>
<div class="extra text">
<router-link :to="{name: 'library.tracks.detail', params: {id: event.object.local_id }}">{{ event.object.name }}</router-link>
<template v-if="event.object.album">from album {{ event.object.album }}, by <em>{{ event.object.artist }}</em>
</template>
<template v-else>, by <em>{{ event.object.artist }}</em>
</template>
<i18next path="from album {%0%}, by {%1%}" v-if="event.object.album">
{{ event.object.album }}
<em>{{ event.object.artist }}</em>
</i18next>
<i18next path=", by {%0%}" v-else>
<em>{{ event.object.artist }}</em>
</i18next>
</div>
</div>
</div>
......
<template>
<div :class="['ui', {'tiny': discrete}, 'buttons']">
<button
title="Add to current queue"
:title="$t('Add to current queue')"
@click="add"
:class="['ui', {loading: isLoading}, {'mini': discrete}, {disabled: !playable}, 'button']">
<i class="ui play icon"></i>
<template v-if="!discrete"><slot>Play</slot></template>
<template v-if="!discrete"><slot><i18next path="Play"/></slot></template>
</button>
<div v-if="!discrete" :class="['ui', {disabled: !playable}, 'floating', 'dropdown', 'icon', 'button']">
<i class="dropdown icon"></i>
<div class="menu">
<div class="item"@click="add"><i class="plus icon"></i> Add to queue</div>
<div class="item"@click="addNext()"><i class="step forward icon"></i> Play next</div>
<div class="item"@click="addNext(true)"><i class="arrow down icon"></i> Play now</div>
<div class="item"@click="add"><i class="plus icon"></i><i18next path="Add to queue"/></div>
<div class="item"@click="addNext()"><i class="step forward icon"></i><i18next path="Play next"/></div>
<div class="item"@click="addNext(true)"><i class="arrow down icon"></i><i18next path="Play now"/></div>
</div>
</div>
</div>
......
......@@ -57,44 +57,44 @@
<div class="two wide column controls ui grid">
<div
title="Previous track"
:title="$t('Previous track')"
class="two wide column control"
:disabled="emptyQueue">
<i @click="previous" :class="['ui', 'backward', {'disabled': emptyQueue}, 'big', 'icon']"></i>
</div>
<div
v-if="!playing"
title="Play track"
:title="$t('Play track')"
class="two wide column control">
<i @click="togglePlay" :class="['ui', 'play', {'disabled': !currentTrack}, 'big', 'icon']"></i>
</div>
<div
v-else
title="Pause track"
:title="$t('Pause track')"
class="two wide column control">
<i @click="togglePlay" :class="['ui', 'pause', {'disabled': !currentTrack}, 'big', 'icon']"></i>
</div>
<div
title="Next track"
:title="$t('Next track')"
class="two wide column control"
:disabled="!hasNext">
<i @click="next" :class="['ui', {'disabled': !hasNext}, 'step', 'forward', 'big', 'icon']" ></i>
</div>
<div class="two wide column control volume-control">
<i title="Unmute" @click="$store.commit('player/volume', 1)" v-if="volume === 0" class="volume off secondary icon"></i>
<i title="Mute" @click="$store.commit('player/volume', 0)" v-else-if="volume < 0.5" class="volume down secondary icon"></i>
<i title="Mute" @click="$store.commit('player/volume', 0)" v-else class="volume up secondary icon"></i>
<i :title="$t('Unmute')" @click="$store.commit('player/volume', 1)" v-if="volume === 0" class="volume off secondary icon"></i>
<i :title="$t('Mute')" @click="$store.commit('player/volume', 0)" v-else-if="volume < 0.5" class="volume down secondary icon"></i>
<i :title="$t('Mute')" @click="$store.commit('player/volume', 0)" v-else class="volume up secondary icon"></i>
<input type="range" step="0.05" min="0" max="1" v-model="sliderVolume" />
</div>
<div class="two wide column control looping">
<i
title="Looping disabled. Click to switch to single-track looping."
:title="$t('Looping disabled. Click to switch to single-track looping.')"
v-if="looping === 0"
@click="$store.commit('player/looping', 1)"
:disabled="!currentTrack"
:class="['ui', {'disabled': !currentTrack}, 'step', 'repeat', 'secondary', 'icon']"></i>
<i
title="Looping on a single track. Click to switch to whole queue looping."
:title="$t('Looping on a single track. Click to switch to whole queue looping.')"
v-if="looping === 1"
@click="$store.commit('player/looping', 2)"
:disabled="!currentTrack"
......@@ -102,7 +102,7 @@
<span class="ui circular tiny orange label">1</span>
</i>
<i
title="Looping on whole queue. Click to disable looping."
:title="$t('Looping on whole queue. Click to disable looping.')"
v-if="looping === 2"
@click="$store.commit('player/looping', 0)"
:disabled="!currentTrack"
......@@ -111,14 +111,14 @@
</div>
<div
:disabled="queue.tracks.length === 0"
title="Shuffle your queue"
:title="$t('Shuffle your queue')"
class="two wide column control">
<i @click="shuffle()" :class="['ui', 'random', 'secondary', {'disabled': queue.tracks.length === 0}, 'icon']" ></i>
</div>
<div class="one wide column"></div>
<div
:disabled="queue.tracks.length === 0"
title="Clear your queue"
:title="$t('Clear your queue')"
class="two wide column control">
<i @click="clean()" :class="['ui', 'trash', 'secondary', {'disabled': queue.tracks.length === 0}, 'icon']" ></i>
</div>
......
<template>
<div>
<h2>Search for some music</h2>
<h2><i18next path="Search for some music"/></h2>
<div :class="['ui', {'loading': isLoading }, 'search']">
<div class="ui icon big input">
<i class="search icon"></i>
......@@ -8,22 +8,22 @@
</div>
</div>
<template v-if="query.length > 0">
<h3 class="ui title">Artists</h3>
<h3 class="ui title"><i18next path="Artists"/></h3>
<div v-if="results.artists.length > 0" class="ui stackable three column grid">
<div class="column" :key="artist.id" v-for="artist in results.artists">
<artist-card class="fluid" :artist="artist" ></artist-card>
</div>
</div>
<p v-else>Sorry, we did not found any artist matching your query</p>
<p v-else><i18next path="Sorry, we did not found any artist matching your query"/></p>
</template>
<template v-if="query.length > 0">
<h3 class="ui title">Albums</h3>
<h3 class="ui title"><i18next path="Albums"/></h3>
<div v-if="results.albums.length > 0" class="ui stackable three column grid">
<div class="column" :key="album.id" v-for="album in results.albums">
<album-card class="fluid" :album="album" ></album-card>
</div>
</div>
<p v-else>Sorry, we did not found any album matching your query</p>
<p v-else><i18next path="Sorry, we did not found any album matching your query"/></p>
</template>
</div>
</template>
......
......@@ -10,8 +10,10 @@
</div>
<div class="meta">
<span>
By <router-link tag="span" :to="{name: 'library.artists.detail', params: {id: album.artist.id }}">
<i18next path="By {%0%}">
<router-link tag="span" :to="{name: 'library.artists.detail', params: {id: album.artist.id }}">
{{ album.artist.name }}</router-link>
</i18next>
</span><span class="time" v-if="album.release_date">{{ album.release_date | year }}</span>
</div>
<div class="description" v-if="mode === 'rich'">
......@@ -36,16 +38,24 @@
</tbody>
</table>
<div class="center aligned segment" v-if="album.tracks.length > initialTracks">
<em v-if="!showAllTracks" @click="showAllTracks = true" class="expand">Show {{ album.tracks.length - initialTracks }} more tracks</em>
<em v-else @click="showAllTracks = false" class="expand">Collapse</em>
<em v-if="!showAllTracks" @click="showAllTracks = true" class="expand">
<i18next path="Show {%0%} more tracks">{{ album.tracks.length - initialTracks }}</i18next>
</em>
<em v-else @click="showAllTracks = false" class="expand">
<i18next path="Collapse" />
</em>
</div>
</div>
</div>
<div class="extra content">
<play-button class="mini basic orange right floated" :tracks="album.tracks">Play all</play-button>
<play-button class="mini basic orange right floated" :tracks="album.tracks">
<i18next path="Play all"/>
</play-button>
<span>
<i class="music icon"></i>
{{ album.tracks.length }} tracks
<i18next path="{%0%} tracks">
{{ album.tracks.length }}
</i18next>
</span>
</div>
</div>
......
......@@ -27,17 +27,27 @@
</tbody>
</table>
<div class="center aligned segment" v-if="artist.albums.length > initialAlbums">
<em v-if="!showAllAlbums" @click="showAllAlbums = true" class="expand">Show {{ artist.albums.length - initialAlbums }} more albums</em>
<em v-else @click="showAllAlbums = false" class="expand">Collapse</em>
<em v-if="!showAllAlbums" @click="showAllAlbums = true" class="expand">
<i18next path="Show {%0%} more albums">
{{ artist.albums.length - initialAlbums }}
</i18next>
</em>
<em v-else @click="showAllAlbums = false" class="expand">
<i18next path="Collapse"/>
</em>
</div>
</div>
</div>
<div class="extra content">
<span>
<i class="sound icon"></i>
{{ artist.albums.length }} albums
<i18next path="{%0%} albums">
{{ artist.albums.length }}
</i18next>
</span>
<play-button class="mini basic orange right floated" :tracks="allTracks">Play all</play-button>
<play-button class="mini basic orange right floated" :tracks="allTracks">
<i18next path="Play all"/>
</play-button>
</div>
</div>
</template>
......
......@@ -4,9 +4,9 @@
<tr>
<th></th>
<th></th>
<th colspan="6">Title</th>
<th colspan="6">Artist</th>
<th colspan="6">Album</th>
<i18next tag="th" colspan="6" path="Title"/>
<i18next tag="th" colspan="6" path="Artist"/>
<i18next tag="th" colspan="6" path="Album"/>
<th></th>
</tr>
</thead>
......@@ -20,20 +20,18 @@
<tfoot class="full-width">
<tr>
<th colspan="3">
<button @click="showDownloadModal = !showDownloadModal" class="ui basic button">Download...</button>
<button @click="showDownloadModal = !showDownloadModal" class="ui basic button">
<i18next path="Download..."/>
</button>
<modal :show.sync="showDownloadModal">
<div class="header">
Download tracks
</div>
<i18next tag="div" path="Download tracks" class="header" />
<div class="content">
<div class="description">
<p>There is currently no way to download directly multiple tracks from funkwhale as a ZIP archive.
However, you can use a command line tools such as <a href="https://curl.haxx.se/" target="_blank">cURL</a> to easily download a list of tracks.
</p>
<p>Simply copy paste the snippet below into a terminal to launch the download.</p>
<div class="ui warning message">
Keep your PRIVATE_TOKEN secret as it gives access to your account.
</div>
<i18next tag="p" path="There is currently no way to download directly multiple tracks from funkwhale as a ZIP archive. However, you can use a command line tools such as {%0%} to easily download a list of tracks.">
<a href="https://curl.haxx.se/" target="_blank">cURL</a>
</i18next>
<i18next path="Simply copy paste the snippet below into a terminal to launch the download."/>
<i18next tag="div" class="ui warning message" path="Keep your PRIVATE_TOKEN secret as it gives access to your account."/>
<pre>
export PRIVATE_TOKEN="{{ $store.state.auth.token }}"
<template v-for="track in tracks"><template v-if="track.files.length > 0">
......@@ -42,9 +40,7 @@ curl -G -o "{{ track.files[0].filename }}" <template v-if="$store.state.auth.aut
</div>
</div>
<div class="actions">
<div class="ui black deny button">
Cancel
</div>
<i18next tag="div" class="ui black deny button" path="Cancel" />
</div>
</modal>
</th>
......
......@@ -2,17 +2,17 @@
<div class="main pusher" v-title="'Log In'">
<div class="ui vertical stripe segment">
<div class="ui small text container">
<h2>Log in to your Funkwhale account</h2>
<h2><i18next path="Log in to your Funkwhale account"/></h2>
<form class="ui form" @submit.prevent="submit()">
<div v-if="error" class="ui negative message">
<div class="header">We cannot log you in</div>
<div class="header"><i18next path="We cannot log you in"/></div>
<ul class="list">
<li v-if="error == 'invalid_credentials'">Please double-check your username/password couple is correct</li>
<li v-else>An unknown error happend, this can mean the server is down or cannot be reached</li>
<i18next tag="li" v-if="error == 'invalid_credentials'" path="Please double-check your username/password couple is correct"/>
<i18next tag="li" v-else path="An unknown error happend, this can mean the server is down or cannot be reached"/>
</ul>
</div>
<div class="field">
<label>Username or email</label>
<i18next tag="label" path="Username or email"/>
<input
ref="username"
required
......@@ -23,7 +23,7 @@
>
</div>
<div class="field">
<label>Password</label>
<i18next tag="label" path="Password"/>
<input
required
type="password"
......@@ -31,9 +31,9 @@
v-model="credentials.password"
>
</div>
<button :class="['ui', {'loading': isLoading}, 'button']" type="submit">Login</button>
<button :class="['ui', {'loading': isLoading}, 'button']" type="submit"><i18next path="Login"/></button>
<router-link class="ui right floated basic button" :to="{path: '/signup'}">
Create an account
<i18next path="Create an account"/>
</router-link>
</form>
</div>
......
......@@ -2,9 +2,9 @@
<div class="main pusher" v-title="'Log Out'">
<div class="ui vertical stripe segment">
<div class="ui small text container">
<h2>Are you sure you want to log out?</h2>
<p>You are currently logged in as {{ $store.state.auth.username }}</p>
<button class="ui button" @click="$store.dispatch('auth/logout')">Yes, log me out!</button>
<h2><i18next path="Are you sure you want to log out?"/></h2>
<i18next tag="p" path="You are currently logged in as {%0%}">{{ $store.state.auth.username }}</i18next>
<button class="ui button" @click="$store.dispatch('auth/logout')"><i18next path="Yes, log me out!"/></button>
</form>
</div>
</div>
......
......@@ -9,16 +9,17 @@
<i class="circular inverted user green icon"></i>
<div class="content">
{{ $store.state.auth.profile.username }}
<div class="sub header">Registered since {{ signupDate }}</div>
<i18next class="sub header" path="Registered since {%0%}">{{ signupDate }}</i18next>
</div>
</h2>
<div class="ui basic green label">this is you!</div>
<div class="ui basic green label"><i18next path="This is you!"/></div>
<div v-if="$store.state.auth.profile.is_staff" class="ui yellow label">
<i class="star icon"></i>
Staff member
<i18next path="Staff member"/>
</div>
<router-link class="ui tiny basic button" :to="{path: '/settings'}">
<i class="setting icon"> </i>Settings...
<i class="setting icon"> </i>
<i18next path="Settings..."/>
</router-link>
</div>
......
......@@ -2,13 +2,13 @@
<div class="main pusher" v-title="'Account Settings'">
<div class="ui vertical stripe segment">
<div class="ui small text container">
<h2 class="ui header">Account settings</h2>
<h2 class="ui header"><i18next path="Account settings"/></h2>
<form class="ui form" @submit.prevent="submitSettings()">
<div v-if="settings.success" class="ui positive message">
<div class="header">Settings updated</div>
<div class="header"><i18next path="Settings updated"/></div>
</div>
<div v-if="settings.errors.length > 0" class="ui negative message">
<div class="header">We cannot save your settings</div>
<i18next tag="div" class="header" path="We cannot save your settings"/>
<ul class="list">
<li v-for="error in settings.errors">{{ error }}</li>
</ul>
......@@ -20,21 +20,21 @@
<option :value="c.value" v-for="c in f.choices">{{ c.label }}</option>
</select>
</div>
<button :class="['ui', {'loading': isLoading}, 'button']" type="submit">Update settings</button>
<button :class="['ui', {'loading': isLoading}, 'button']" type="submit"><i18next path="Update settings"/></button>
</form>
</div>
<div class="ui hidden divider"></div>
<div class="ui small text container">
<h2 class="ui header">Change my password</h2>
<h2 class="ui header"><i18next path="Change my password"/></h2>
<form class="ui form" @submit.prevent="submitPassword()">
<div v-if="passwordError" class="ui negative message">
<div class="header">Cannot change your password</div>
<div class="header"><i18next path="Cannot change your password"/></div>
<ul class="list">
<li v-if="passwordError == 'invalid_credentials'">Please double-check your password is correct</li>
<i18next tag="li" v-if="passwordError == 'invalid_credentials'" path="Please double-check your password is correct"/>
</ul>
</div>
<div class="field">
<label>Old password</label>
<label><i18next path="Old password"/></label>
<input
required
type="password"
...