Merge branch '161-i18n-vue-gettext' into 'develop'

Resolve "Industrialize the i18n workflow"

Closes #161 and #167

See merge request funkwhale/funkwhale!286
This commit is contained in:
Eliot Berriot 2018-06-30 18:36:32 +00:00
commit 262c40bc41
109 changed files with 5478 additions and 1887 deletions

1
.gitignore vendored
View File

@ -91,3 +91,4 @@ data/
po/*.po
docs/swagger
_build
front/src/translations.json

View File

@ -19,9 +19,12 @@ review_front:
when: manual
allow_failure: true
before_script:
- apt-get update
- apt-get install jq -y
- cd front
script:
- yarn install
- yarn run i18n-compile
# this is to ensure we don't have any errors in the output,
# cf https://code.eliotberriot.com/funkwhale/funkwhale/issues/169
- INSTANCE_URL=$REVIEW_INSTANCE_URL yarn run build | tee /dev/stderr | (! grep -i 'ERROR in')
@ -175,11 +178,11 @@ build_front:
stage: build
image: node:9
before_script:
- apt-get update
- apt-get install jq -y
- cd front
script:
- yarn install
- yarn run i18n-extract
- yarn run i18n-compile
# this is to ensure we don't have any errors in the output,
# cf https://code.eliotberriot.com/funkwhale/funkwhale/issues/169

View File

@ -289,8 +289,9 @@ Typical workflow for a contribution
Internationalization
--------------------
We're using https://github.com/Polyconseil/vue-gettext to manage i18n in the project.
When working on the front-end, any end-user string should be translated
using either ``<i18next path="yourstring">`` or the ``$t('yourstring')``
using either ``<translate>yourstring</translate>`` or ``$gettext('yourstring')``
function.
Extraction is done by calling ``yarn run i18n-extract``, which

View File

@ -26,4 +26,9 @@ Contribute
----------
Contribution guidelines as well as development installation instructions
are outlined in `CONTRIBUTING <CONTRIBUTING>`_
are outlined in `CONTRIBUTING <CONTRIBUTING>`_.
Translate
^^^^^^^^^
Translators willing to help can refer to `TRANSLATORS <TRANSLATORS>`_ for instructions.

28
TRANSLATORS.rst Normal file
View File

@ -0,0 +1,28 @@
Translating Funkwhale
=====================
Thank you for reading this! If you want to help translate Funkwhale,
you found the proper place :)
Translation is done via our own Weblate instance at https://translate.funkwhale.audio/projects/funkwhale/front/.
You can signup/login using your Gitlab account (from https://code.eliotberriot.com).
Translation workflow
--------------------
Once you're logged-in on the Weblate instance, you can suggest translations. Your suggestions will then be reviewer
by the project maintainer or other translators to ensure consistency.
Guidelines
----------
Respecting those guidelines is mandatory if you want your translation to be included:
- Use gender-neutral language and wording
Requesting a new language
-------------------------
If you'd like to see a new language in Funkwhale, please open an issue here:
https://code.eliotberriot.com/funkwhale/funkwhale/issues

View File

@ -0,0 +1 @@
New translation workflow (#161, #167)

View File

@ -23,6 +23,7 @@ Funkwhale is a self-hosted, modern free and open-source music server, heavily in
api
third-party
contributing
translators
changelog
Indices and tables

1
docs/translators.rst Normal file
View File

@ -0,0 +1 @@
.. include:: ../TRANSLATORS.rst

View File

@ -14,8 +14,6 @@ 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

View File

@ -1,49 +0,0 @@
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}`)
if (lang === 'en') {
// for english, we need to specify that json values are equal to the keys.
// otherwise we end up with empty strings on the front end for english
var contents = fs.readFileSync(output)
var jsonContent = JSON.parse(contents)
var finalContent = {}
Object.keys(jsonContent).forEach(function(key) {
finalContent[key] = key
})
fs.writeFile(output, JSON.stringify(finalContent))
}
}
})
})
})
}
}
})

1315
front/locales/app.pot Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -5,11 +5,11 @@
"author": "Eliot Berriot <contact@eliotberriot.com>",
"private": true,
"scripts": {
"dev": "node build/dev-server.js",
"start": "node build/dev-server.js",
"dev": "scripts/i18n-compile.sh && node build/dev-server.js",
"start": "scripts/i18n-compile.sh && node build/dev-server.js",
"build": "node build/build.js",
"i18n-extract": "find src/ -name '*.vue' | xargs vendor/vue-i18n-xgettext/index.js > ../po/en.po",
"i18n-compile": "node build/i18n.js",
"i18n-extract": "scripts/i18n-extract.sh",
"i18n-compile": "scripts/i18n-compile.sh",
"unit": "cross-env BABEL_ENV=test karma start test/unit/karma.conf.js --single-run",
"unit-watch": "cross-env BABEL_ENV=test karma start test/unit/karma.conf.js",
"e2e": "node test/e2e/runner.js",
@ -21,9 +21,6 @@
"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,6 +31,7 @@
"semantic-ui-css": "^2.2.10",
"showdown": "^1.8.6",
"vue": "^2.5.16",
"vue-gettext": "^2.0.31",
"vue-lazyload": "^1.1.4",
"vue-masonry": "^0.10.16",
"vue-router": "^2.3.1",
@ -61,6 +59,7 @@
"cross-env": "^4.0.0",
"cross-spawn": "^5.0.1",
"css-loader": "^0.28.0",
"easygettext": "^2.5.0",
"es6-promise": "^4.2.2",
"eslint": "^3.19.0",
"eslint-config-standard": "^6.2.1",
@ -104,7 +103,6 @@
"sinon-chai": "^2.8.0",
"sinon-stub-promise": "^4.0.0",
"url-loader": "^0.5.8",
"vue-i18n-xgettext": "^0.0.4",
"vue-loader": "^12.1.0",
"vue-style-loader": "^3.0.1",
"vue-template-compiler": "^2.3.3",

3
front/scripts/i18n-compile.sh Executable file
View File

@ -0,0 +1,3 @@
#!/bin/bash -eux
locales=$(tail -n +2 src/locales.js | sed -e 's/export default //' | jq '.locales[].code' | xargs echo)
find locales -name '*.po' | xargs $(yarn bin gettext-extract)/gettext-compile --output src/translations.json

28
front/scripts/i18n-extract.sh Executable file
View File

@ -0,0 +1,28 @@
#!/bin/bash -eux
locales=$(tail -n +2 src/locales.js | sed -e 's/export default //' | jq '.locales[].code' | xargs echo)
locales_dir="locales"
sources=$(find src -name '*.vue' -o -name '*.html' 2> /dev/null)
js_sources=$(find src -name '*.vue' -o -name '*.js')
touch $locales_dir/app.pot
# Create a main .pot template, then generate .po files for each available language.
# Extract gettext strings from templates files and create a POT dictionary template.
$(yarn bin gettext-extract)/gettext-extract --attribute v-translate --quiet --output $locales_dir/app.pot $sources
xgettext --language=JavaScript --keyword=npgettext:1c,2,3 \
--from-code=utf-8 --join-existing --no-wrap \
--package-name=$(node -e "console.log(require('./package.json').name);") \
--package-version=$(node -e "console.log(require('./package.json').version);") \
--output $locales_dir/app.pot $js_sources
# Fix broken files path/lines in pot
sed -e 's|#: src/|#: front/src/|' -i $locales_dir/app.pot
# Generate .po files for each available language.
echo $locales
for lang in $locales; do \
po_file=$locales_dir/$lang/LC_MESSAGES/app.po; \
echo "msgmerge --update $po_file "; \
mkdir -p $(dirname $po_file); \
[ -f $po_file ] && msgmerge --lang=$lang --update $po_file $locales_dir/app.pot || msginit --no-translator --locale=$lang --input=$locales_dir/app.pot --output-file=$po_file; \
msgattrib --no-wrap --no-obsolete -o $po_file $po_file; \
done;

View File

@ -2,14 +2,14 @@
<div id="app">
<div class="ui main text container instance-chooser" v-if="!$store.state.instance.instanceUrl">
<div class="ui padded segment">
<h1 class="ui header">{{ $t('Choose your instance') }}</h1>
<h1 class="ui header">{{ $gettext('Choose your instance') }}</h1>
<form class="ui form" @submit.prevent="$store.dispatch('instance/setUrl', instanceUrl)">
<p>{{ $t('You need to select an instance in order to continue') }}</p>
<p>{{ $gettext('You need to select an instance in order to continue') }}</p>
<div class="ui action input">
<input type="text" v-model="instanceUrl">
<button type="submit" class="ui button">{{ $t('Submit') }}</button>
<button type="submit" class="ui button">{{ $gettext('Submit') }}</button>
</div>
<p>{{ $t('Suggested choices') }}</p>
<p>{{ $gettext('Suggested choices') }}</p>
<div class="ui bulleted list">
<div class="ui item" v-for="url in suggestedInstances">
<a @click="instanceUrl = url">{{ url }}</a>
@ -27,20 +27,20 @@
<div class="ui container">
<div class="ui stackable equal height stackable grid">
<div class="three wide column">
<i18next tag="h4" class="ui header" path="Links"></i18next>
<h4 v-translate class="ui header">Links</h4>
<div class="ui link list">
<router-link class="item" to="/about">
<i18next path="About this instance" />
{{ $gettext('About this instance') }}
</router-link>
<a href="https://funkwhale.audio" class="item" target="_blank">{{ $t('Official website') }}</a>
<a href="https://docs.funkwhale.audio" class="item" target="_blank">{{ $t('Documentation') }}</a>
<a href="https://funkwhale.audio" class="item" target="_blank">{{ $gettext('Official website') }}</a>
<a href="https://docs.funkwhale.audio" class="item" target="_blank">{{ $gettext('Documentation') }}</a>
<a href="https://code.eliotberriot.com/funkwhale/funkwhale" class="item" target="_blank">
<template v-if="version">{{ $t('Source code ({% version %})', {version: version}) }}</template>
<template v-else>{{ $t('Source code') }}</template>
<translate :translate-params="{version: version}" v-if="version">Source code (%{version})</translate>
<translate v-else>Source code</translate>
</a>
<a href="https://code.eliotberriot.com/funkwhale/funkwhale/issues" class="item" target="_blank">{{ $t('Issue tracker') }}</a>
<a href="https://code.eliotberriot.com/funkwhale/funkwhale/issues" class="item" target="_blank">{{ $gettext('Issue tracker') }}</a>
<a @click="switchInstance" class="item" >
{{ $t('Use another instance') }}
{{ $gettext('Use another instance') }}
<template v-if="$store.state.instance.instanceUrl !== '/'">
<br>
({{ $store.state.instance.instanceUrl }})
@ -49,14 +49,26 @@
</div>
</div>
<div class="ten wide column">
<i18next tag="h4" class="ui header" path="About funkwhale" />
<h4 v-translate class="ui header">About Funkwhale</h4>
<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!"/>
<translate>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!</translate>
</p>
<p>
<i18next path="The funkwhale logo was kindly designed and provided by Francis Gading."/>
<translate>The funkwhale logo was kindly designed and provided by Francis Gading.</translate>
</p>
</div>
<div class="three wide column">
<h4 v-translate class="ui header">Options</h4>
<div class="ui form">
<div class="ui field">
<label>{{ $gettext('Change language') }}</label>
<select class="ui dropdown" v-model="$language.current">
<option v-for="(language, key) in $language.available" :value="key">{{ language }}</option>
</select>
</div>
</div>
</div>
</div>
</div>
</div>
@ -115,7 +127,7 @@ export default {
})
},
switchInstance () {
let confirm = window.confirm(this.$t('This will erase your local data and disconnect you, do you want to continue?'))
let confirm = window.confirm(this.$gettext('This will erase your local data and disconnect you, do you want to continue?'))
if (confirm) {
this.$store.commit('instance/instanceUrl', null)
}
@ -144,6 +156,9 @@ export default {
'$store.state.instance.instanceUrl' () {
this.$store.dispatch('instance/fetchSettings')
this.fetchNodeInfo()
},
'$language.current' (newValue) {
this.$store.commit('ui/currentLanguage', newValue)
}
}
}

View File

@ -3,21 +3,23 @@
<div class="ui vertical center aligned stripe segment">
<div class="ui text container">
<h1 class="ui huge header">
<template v-if="instance.name.value">{{ $t('About {%instance%}', { instance: instance.name.value }) }}</template>
<template v-else="instance.name.value">{{ $t('About this instance') }}</template>
<template v-if="instance.name.value" :template-params="{instance: instance.name}">
About %{ instance }
</template>
<template v-else="instance.name.value">{{ $gettext('About this instance') }}</template>
</h1>
<stats></stats>
</div>
</div>
<div class="ui vertical stripe segment">
<p v-if="!instance.short_description.value && !instance.long_description.value">
{{ $t('Unfortunately, owners of this instance did not yet take the time to complete this page.') }}
{{ $gettext('Unfortunately, owners of this instance did not yet take the time to complete this page.') }}
</p>
<router-link
class="ui button"
v-if="$store.state.auth.availablePermissions['settings']"
:to="{path: '/manage/settings', hash: 'instance'}">
<i class="pencil icon"></i>{{ $t('Edit instance info') }}
<i class="pencil icon"></i>{{ $gettext('Edit instance info') }}
</router-link>
<div
v-if="instance.short_description.value"

View File

@ -3,15 +3,15 @@
<div class="ui vertical center aligned stripe segment">
<div class="ui text container">
<h1 class="ui huge header">
{{ $t('Welcome on Funkwhale') }}
{{ $gettext('Welcome on Funkwhale') }}
</h1>
<p>{{ $t('We think listening to music should be simple.') }}</p>
<p>{{ $gettext('We think listening to music should be simple.') }}</p>
<router-link class="ui icon button" to="/about">
<i class="info icon"></i>
{{ $t('Learn more about this instance') }}
{{ $gettext('Learn more about this instance') }}
</router-link>
<router-link class="ui icon teal button" to="/library">
{{ $t('Get me to the library') }}
{{ $gettext('Get me to the library') }}
<i class="right arrow icon"></i>
</router-link>
</div>
@ -22,9 +22,9 @@
<div class="row">
<div class="eight wide left floated column">
<h2 class="ui header">
{{ $t('Why funkwhale?') }}
{{ $gettext('Why funkwhale?') }}
</h2>
<p>{{ $t('That\'s simple: we loved Grooveshark and we want to build something even better.') }}</p>
<p>{{ $gettext('That\'s simple: we loved Grooveshark and we want to build something even better.') }}</p>
</div>
<div class="four wide left floated column">
<img class="ui medium image" src="../assets/logo/logo.png" />
@ -35,26 +35,26 @@
<div class="ui middle aligned stackable text container">
<div class="ui hidden divider"></div>
<h2 class="ui header">
{{ $t('Unlimited music') }}
{{ $gettext('Unlimited music') }}
</h2>
<p>{{ $t('Funkwhale is designed to make it easy to listen to music you like, or to discover new artists.') }}</p>
<p>{{ $gettext('Funkwhale is designed to make it easy to listen to music you like, or to discover new artists.') }}</p>
<div class="ui list">
<div class="item">
<i class="sound icon"></i>
<div class="content">
{{ $t('Click once, listen for hours using built-in radios') }}
{{ $gettext('Click once, listen for hours using built-in radios') }}
</div>
</div>
<div class="item">
<i class="heart icon"></i>
<div class="content">
{{ $t('Keep a track of your favorite songs') }}
{{ $gettext('Keep a track of your favorite songs') }}
</div>
</div>
<div class="item">
<i class="list icon"></i>
<div class="content">
{{ $t('Playlists? We got them') }}
{{ $gettext('Playlists? We got them') }}
</div>
</div>
</div>
@ -62,28 +62,31 @@
<div class="ui middle aligned stackable text container">
<div class="ui hidden divider"></div>
<h2 class="ui header">
{{ $t('Clean library') }}
{{ $gettext('Clean library') }}
</h2>
<p>{{ $t('Funkwhale takes care of handling your music') }}.</p>
<p>{{ $gettext('Funkwhale takes care of handling your music') }}.</p>
<div class="ui list">
<div class="item">
<i class="download icon"></i>
<div class="content">
{{ $t('Import music from various platforms, such as YouTube or SoundCloud') }}
{{ $gettext('Import music from various platforms, such as YouTube or SoundCloud') }}
</div>
</div>
<div class="item">
<i class="tag icon"></i>
<div class="content">
<i18next path="Get quality metadata about your music thanks to {%0%}">
<a href="https://musicbrainz.org" target="_blank">{{ $t('MusicBrainz') }}</a>
</i18next>
<template v-translate>
Get quality metadata about your music thanks to
<a href="https://musicbrainz.org" target="_blank">
MusicBrainz
</a>
</template>
</div>
</div>
<div class="item">
<i class="plus icon"></i>
<div class="content">
{{ $t('Covers, lyrics, our goal is to have them all ;)') }}
{{ $gettext('Covers, lyrics, our goal is to have them all ;)') }}
</div>
</div>
</div>
@ -91,20 +94,20 @@
<div class="ui middle aligned stackable text container">
<div class="ui hidden divider"></div>
<h2 class="ui header">
{{ $t('Easy to use') }}
{{ $gettext('Easy to use') }}
</h2>
<p>{{ $t('Funkwhale is dead simple to use.') }}</p>
<p>{{ $gettext('Funkwhale is dead simple to use.') }}</p>
<div class="ui list">
<div class="item">
<i class="book icon"></i>
<div class="content">
{{ $t('No add-ons, no plugins : you only need a web library') }}
{{ $gettext('No add-ons, no plugins : you only need a web library') }}
</div>
</div>
<div class="item">
<i class="wizard icon"></i>
<div class="content">
{{ $t('Access your music from a clean interface that focus on what really matters') }}
{{ $gettext('Access your music from a clean interface that focus on what really matters') }}
</div>
</div>
</div>
@ -112,26 +115,26 @@
<div class="ui middle aligned stackable text container">
<div class="ui hidden divider"></div>
<h2 class="ui header">
{{ $t('Your music, your way') }}
{{ $gettext('Your music, your way') }}
</h2>
<p>{{ $t('Funkwhale is free and gives you control on your music.') }}</p>
<p>{{ $gettext('Funkwhale is free and gives you control on your music.') }}</p>
<div class="ui list">
<div class="item">
<i class="smile icon"></i>
<div class="content">
{{ $t('The plaform is free and open-source, you can install it and modify it without worries') }}
{{ $gettext('The plaform is free and open-source, you can install it and modify it without worries') }}
</div>
</div>
<div class="item">
<i class="protect icon"></i>
<div class="content">
{{ $t('We do not track you or bother you with ads') }}
{{ $gettext('We do not track you or bother you with ads') }}
</div>
</div>
<div class="item">
<i class="users icon"></i>
<div class="content">
{{ $t('You can invite friends and family to your instance so they can enjoy your music') }}
{{ $gettext('You can invite friends and family to your instance so they can enjoy your music') }}
</div>
</div>
</div>

View File

@ -5,13 +5,14 @@
<h1 class="ui huge header">
<i class="warning icon"></i>
<div class="content">
<strike>{{ $t('Whale') }}</strike> {{ $t('Page not found!') }}
<strike>{{ $gettext('Whale') }}</strike> {{ $gettext('Page not found!') }}
</div>
</h1>
<p>{{ $t('We\'re sorry, the page you asked for does not exists.') }}</p>
<i18next path="Requested URL: {%0%}"><a :href="path">{{ path }}</a></i18next>
<p>{{ $gettext('We\'re sorry, the page you asked for does not exist:') }}</p>
<a :href="path">{{ path }}</a>
<div class="ui hidden divider"></div>
<router-link class="ui icon button" to="/">
{{ $t('Go to home page') }}
{{ $gettext('Go to home page') }}
<i class="right arrow icon"></i>
</router-link>
</div>

View File

@ -18,13 +18,13 @@
<div class="ui compact fluid two item inverted menu">
<a class="active item" @click="selectedTab = 'library'" data-tab="library">Browse</a>
<a class="item" @click="selectedTab = 'queue'" data-tab="queue">
{{ $t('Queue') }}
{{ $gettext('Queue') }}&nbsp;
<template v-if="queue.tracks.length === 0">
{{ $t('(empty)') }}
</template>
<template v-else>
{{ $t('({%index%} of {%length%})', { index: queue.currentIndex + 1, length: queue.tracks.length }) }}
{{ $gettext('(empty)') }}
</template>
<translate v-else :translate-params="{index: queue.currentIndex + 1, length: queue.tracks.length}">
(%{ index } of %{ length })
</translate>
</a>
</div>
</div>
@ -32,40 +32,45 @@
<div class="ui bottom attached active tab" data-tab="library">
<div class="ui inverted vertical large fluid menu">
<div class="item">
<div class="header">{{ $t('My account') }}</div>
<div class="header">{{ $gettext('My account') }}</div>
<div class="menu">
<router-link class="item" v-if="$store.state.auth.authenticated" :to="{name: 'profile', params: {username: $store.state.auth.username}}"><i class="user icon"></i>{{ $t('Logged in as {%name%}', { name: $store.state.auth.username }) }}</router-link>
<router-link class="item" v-if="$store.state.auth.authenticated" :to="{name: 'logout'}"><i class="sign out icon"></i>{{ $t('Logout') }}</router-link>
<router-link class="item" v-else :to="{name: 'login'}"><i class="sign in icon"></i>{{ $t('Login') }}</router-link>
<router-link class="item" v-if="$store.state.auth.authenticated" :to="{name: 'profile', params: {username: $store.state.auth.username}}">
<i class="user icon"></i>
<translate :translate-params="{username: $store.state.auth.username}">
Logged in as %{ username }
</translate>
</router-link>
<router-link class="item" v-if="$store.state.auth.authenticated" :to="{name: 'logout'}"><i class="sign out icon"></i>{{ $gettext('Logout') }}</router-link>
<router-link class="item" v-else :to="{name: 'login'}"><i class="sign in icon"></i>{{ $gettext('Login') }}</router-link>
</div>
</div>
<div class="item">
<div class="header">{{ $t('Music') }}</div>
<div class="header">{{ $gettext('Music') }}</div>
<div class="menu">
<router-link class="item" :to="{path: '/library'}"><i class="sound icon"> </i>{{ $t('Browse library') }}</router-link>
<router-link class="item" v-if="$store.state.auth.authenticated" :to="{path: '/favorites'}"><i class="heart icon"></i>{{ $t('Favorites') }}</router-link>
<router-link class="item" :to="{path: '/library'}"><i class="sound icon"> </i>{{ $gettext('Browse library') }}</router-link>
<router-link class="item" v-if="$store.state.auth.authenticated" :to="{path: '/favorites'}"><i class="heart icon"></i>{{ $gettext('Favorites') }}</router-link>
<a
@click="$store.commit('playlists/chooseTrack', null)"
v-if="$store.state.auth.authenticated"
class="item">
<i class="list icon"></i>{{ $t('Playlists') }}
<i class="list icon"></i>{{ $gettext('Playlists') }}
</a>
<router-link
v-if="$store.state.auth.authenticated"
class="item" :to="{path: '/activity'}"><i class="bell icon"></i>{{ $t('Activity') }}</router-link>
class="item" :to="{path: '/activity'}"><i class="bell icon"></i>{{ $gettext('Activity') }}</router-link>
</div>
</div>
<div class="item" v-if="showAdmin">
<div class="header">{{ $t('Administration') }}</div>
<div class="header">{{ $gettext('Administration') }}</div>
<div class="menu">
<router-link
class="item"
v-if="$store.state.auth.availablePermissions['library']"
:to="{name: 'manage.library.files'}">
<i class="book icon"></i>{{ $t('Library') }}
<i class="book icon"></i>{{ $gettext('Library') }}
<div
:class="['ui', {'teal': $store.state.ui.notifications.importRequests > 0}, 'label']"
:title="$t('Pending import requests')">
:title="$gettext('Pending import requests')">
{{ $store.state.ui.notifications.importRequests }}</div>
</router-link>
@ -73,29 +78,29 @@
class="item"
v-else-if="$store.state.auth.availablePermissions['upload']"
to="/library/import/launch">
<i class="download icon"></i>{{ $t('Import music') }}
<i class="download icon"></i>{{ $gettext('Import music') }}
</router-link>
<router-link
class="item"
v-if="$store.state.auth.availablePermissions['federation']"
:to="{path: '/manage/federation/libraries'}">
<i class="sitemap icon"></i>{{ $t('Federation') }}
<i class="sitemap icon"></i>{{ $gettext('Federation') }}
<div
:class="['ui', {'teal': $store.state.ui.notifications.federation > 0}, 'label']"
:title="$t('Pending follow requests')">
:title="$gettext('Pending follow requests')">
{{ $store.state.ui.notifications.federation }}</div>
</router-link>
<router-link
class="item"
v-if="$store.state.auth.availablePermissions['settings']"
:to="{path: '/manage/settings'}">
<i class="settings icon"></i>{{ $t('Settings') }}
<i class="settings icon"></i>{{ $gettext('Settings') }}
</router-link>
<router-link
class="item"
v-if="$store.state.auth.availablePermissions['settings']"
:to="{name: 'manage.users.users.list'}">
<i class="users icon"></i>{{ $t('Users') }}
<i class="users icon"></i>{{ $gettext('Users') }}
</router-link>
</div>
</div>
@ -105,12 +110,19 @@
<i class="history icon"></i>
<div class="content">
<div class="header">
{{ $t('Do you want to restore your previous queue?') }}
{{ $gettext('Do you want to restore your previous queue?') }}
</div>
<p>{{ $t('{%count%} tracks', { count: queue.previousQueue.tracks.length }) }}</p>
<p>
<translate
translate-plural="%{ count } tracks"
:translate-n="queue.previousQueue.tracks.length"
:translate-params="{count: queue.previousQueue.tracks.length}">
%{ count } track
</translate>
</p>
<div class="ui two buttons">
<div @click="queue.restore()" class="ui basic inverted green button">{{ $t('Yes') }}</div>
<div @click="queue.removePrevious()" class="ui basic inverted red button">{{ $t('No') }}</div>
<div @click="queue.restore()" class="ui basic inverted green button">{{ $gettext('Yes') }}</div>
<div @click="queue.removePrevious()" class="ui basic inverted red button">{{ $gettext('No') }}</div>
</div>
</div>
</div>
@ -141,10 +153,10 @@
<div v-if="$store.state.radios.running" class="ui black message">
<div class="content">
<div class="header">
<i class="feed icon"></i> {{ $t('You have a radio playing') }}
<i class="feed icon"></i> {{ $gettext('You have a radio playing') }}
</div>
<p>{{ $t('New tracks will be appended here automatically.') }}</p>
<div @click="$store.dispatch('radios/stop')" class="ui basic inverted red button">{{ $t('Stop radio') }}</div>
<p>{{ $gettext('New tracks will be appended here automatically.') }}</p>
<div @click="$store.dispatch('radios/stop')" class="ui basic inverted red button">{{ $gettext('Stop radio') }}</div>
</div>
</div>
</div>

View File

@ -5,20 +5,12 @@
</div>
<div class="content">
<div class="summary">
<i18next path="{%0%} favorited a track">
<username class="user" :username="event.actor.local_id" />
</i18next>
<translate :translate-params="{user: event.actor.local_id}">%{ user } favorited a track</translate>
<human-date class="date" :date="event.published" />
</div>
<div class="extra text">
<router-link :to="{name: 'library.tracks.detail', params: {id: event.object.local_id }}">{{ event.object.name }}</router-link>
<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>
<translate :translate-params="{album: event.object.album, artist: event.object.artist}">from %{ album } by %{ artist }</translate>
</div>
</div>
</div>

View File

@ -5,20 +5,12 @@
</div>
<div class="content">
<div class="summary">
<i18next path="{%0%} listened to a track">
<username class="user" :username="event.actor.local_id" />
</i18next>
<human-date class="date" :date="event.published" />
<translate :translate-params="{user: event.actor.local_id}">%{ user } listened to a track</translate>
<human-date class="date" :date="event.published" />
</div>
<div class="extra text">
<router-link :to="{name: 'library.tracks.detail', params: {id: event.object.local_id }}">{{ event.object.name }}</router-link>
<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>
<translate :translate-params="{album: event.object.album, artist: event.object.artist}">from %{ album } by %{ artist }</translate>
</div>
</div>
</div>

View File

@ -3,13 +3,13 @@
<div class="ui divider" />
<h3 class="ui header">{{ group.label }}</h3>
<div v-if="errors.length > 0" class="ui negative message">
<div class="header">{{ $t('Error while saving settings') }}</div>
<div class="header">{{ $gettext('Error while saving settings') }}</div>
<ul class="list">
<li v-for="error in errors">{{ error }}</li>
</ul>
</div>
<div v-if="result" class="ui positive message">
{{ $t('Settings updated successfully.') }}
{{ $gettext('Settings updated successfully.') }}
</div>
<p v-if="group.help">{{ group.help }}</p>
<div v-for="setting in settings" class="ui field">
@ -61,7 +61,7 @@
<button
type="submit"
:class="['ui', {'loading': isLoading}, 'right', 'floated', 'green', 'button']">
{{ $t('Save') }}
{{ $gettext('Save') }}
</button>
</form>
</template>

View File

@ -1,19 +1,19 @@
<template>
<div :title="title" :class="['ui', {'tiny': discrete}, 'buttons']">
<button
:title="$t('Add to current queue')"
:title="$gettext('Add to current queue')"
@click="addNext(true)"
:disabled="!playable"
:class="['ui', {loading: isLoading}, {'mini': discrete}, {disabled: !playable}, 'button']">
<i class="ui play icon"></i>
<template v-if="!discrete"><slot><i18next path="Play"/></slot></template>
<template v-if="!discrete"><slot>{{ $gettext('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" :disabled="!playable" @click="add"><i class="plus icon"></i><i18next path="Add to queue"/></div>
<div class="item" :disabled="!playable" @click="addNext()"><i class="step forward icon"></i><i18next path="Play next"/></div>
<div class="item" :disabled="!playable" @click="addNext(true)"><i class="arrow down icon"></i><i18next path="Play now"/></div>
<div class="item" :disabled="!playable" @click="add"><i class="plus icon"></i>{{ $gettext('Add to queue') }}</div>
<div class="item" :disabled="!playable" @click="addNext()"><i class="step forward icon"></i>{{ $gettext('Play next') }}</div>
<div class="item" :disabled="!playable" @click="addNext(true)"><i class="arrow down icon"></i>{{ $gettext('Play now') }}</div>
</div>
</div>
</div>
@ -44,10 +44,10 @@ export default {
computed: {
title () {
if (this.playable) {
return this.$t('Play immediatly')
return this.$gettext('Play immediatly')
} else {
if (this.track) {
return this.$t('This track is not imported and cannot be played')
return this.$gettext('This track is not imported and cannot be played')
}
}
},
@ -142,8 +142,9 @@ export default {
if (tracks.length < 1) {
return
}
let msg = this.$ngettext('%{ count } track was added to your queue', '%{ count } tracks were added to your queue', tracks.length)
this.$store.commit('ui/addMessage', {
content: this.$t('{% tracks %} tracks were added to your queue.', {tracks: tracks.length}),
content: this.$gettextInterpolate(msg, {count: tracks.length}),
date: new Date()
})
}

View File

@ -57,44 +57,44 @@
<div class="two wide column controls ui grid">
<div
:title="$t('Previous track')"
:title="$gettext('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="$t('Play track')"
:title="$gettext('Play track')"
class="two wide column control">
<i @click="togglePlay" :class="['ui', 'play', {'disabled': !currentTrack}, 'big', 'icon']"></i>
</div>
<div
v-else
:title="$t('Pause track')"
:title="$gettext('Pause track')"
class="two wide column control">
<i @click="togglePlay" :class="['ui', 'pause', {'disabled': !currentTrack}, 'big', 'icon']"></i>
</div>
<div
:title="$t('Next track')"
:title="$gettext('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="$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>
<i :title="$gettext('Unmute')" @click="$store.commit('player/volume', 1)" v-if="volume === 0" class="volume off secondary icon"></i>
<i :title="$gettext('Mute')" @click="$store.commit('player/volume', 0)" v-else-if="volume < 0.5" class="volume down secondary icon"></i>
<i :title="$gettext('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="$t('Looping disabled. Click to switch to single-track looping.')"
:title="$gettext('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="$t('Looping on a single track. Click to switch to whole queue looping.')"
:title="$gettext('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="$t('Looping on whole queue. Click to disable looping.')"
:title="$gettext('Looping on whole queue. Click to disable looping.')"
v-if="looping === 2"
@click="$store.commit('player/looping', 0)"
:disabled="!currentTrack"
@ -111,7 +111,7 @@
</div>
<div
:disabled="queue.tracks.length === 0"
:title="$t('Shuffle your queue')"
:title="$gettext('Shuffle your queue')"
class="two wide column control">
<div v-if="isShuffling" class="ui inline shuffling inverted small active loader"></div>
<i v-else @click="shuffle()" :class="['ui', 'random', 'secondary', {'disabled': queue.tracks.length === 0}, 'icon']" ></i>
@ -119,7 +119,7 @@
<div class="one wide column"></div>
<div
:disabled="queue.tracks.length === 0"
:title="$t('Clear your queue')"
:title="$gettext('Clear your queue')"
class="two wide column control">
<i @click="clean()" :class="['ui', 'trash', 'secondary', {'disabled': queue.tracks.length === 0}, 'icon']" ></i>
</div>
@ -180,12 +180,13 @@ export default {
return
}
let self = this
let msg = this.$gettext('Queue shuffled!')
this.isShuffling = true
setTimeout(() => {
self.$store.dispatch('queue/shuffle', () => {
self.isShuffling = false
self.$store.commit('ui/addMessage', {
content: self.$t('Queue shuffled!'),
content: msg,
date: new Date()
})
})

View File

@ -1,6 +1,6 @@
<template>
<div>
<h2><i18next path="Search for some music"/></h2>
<h2>{{ $gettext('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"><i18next path="Artists"/></h3>
<h3 class="ui title">{{ $gettext('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><i18next path="Sorry, we did not found any artist matching your query"/></p>
<p v-else>{{ $gettext('Sorry, we did not found any artist matching your query') }}</p>
</template>
<template v-if="query.length > 0">
<h3 class="ui title"><i18next path="Albums"/></h3>
<h3 class="ui title">{{ $gettext('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><i18next path="Sorry, we did not found any album matching your query"/></p>
<p v-else>{{ $gettext('Sorry, we did not found any album matching your query') }}</p>
</template>
</div>
</template>

View File

@ -10,10 +10,9 @@
</div>
<div class="meta">
<span>
<i18next path="By {%0%}">
<router-link tag="span" :to="{name: 'library.artists.detail', params: {id: album.artist.id }}">
{{ album.artist.name }}</router-link>
</i18next>
<router-link tag="span" :to="{name: 'library.artists.detail', params: {id: album.artist.id }}">
<translate :translate-params="{artist: album.artist.name}">By %{ artist }</translate>
</router-link>
</span><span class="time" v-if="album.release_date"> {{ album.release_date | year }}</span>
</div>
<div class="description" v-if="mode === 'rich'">
@ -39,23 +38,21 @@
</table>
<div class="center aligned segment" v-if="album.tracks.length > initialTracks">
<em v-if="!showAllTracks" @click="showAllTracks = true" class="expand">
<i18next path="Show {%0%} more tracks">{{ album.tracks.length - initialTracks }}</i18next>
<translate :translate-params="{count: album.tracks.length - initialTracks}" :translate-n="album.tracks.length - initialTracks" translate-plural="Show %{ count } more tracks">Show 1 more track</translate>
</em>
<em v-else @click="showAllTracks = false" class="expand">
<i18next path="Collapse" />
{{ $gettext('Collapse') }}
</em>
</div>
</div>
</div>
<div class="extra content">
<play-button class="mini basic orange right floated" :tracks="album.tracks">
<i18next path="Play all"/>
{{ $gettext('Play all') }}
</play-button>
<span>
<i class="music icon"></i>
<i18next path="{%0%} tracks">
{{ album.tracks.length }}
</i18next>
<translate :translate-params="{count: album.tracks.length}" :translate-n="album.tracks.length" translate-plural="%{ count } tracks">1 track</translate>
</span>
</div>
</div>

View File

@ -28,12 +28,10 @@
</table>
<div class="center aligned segment" v-if="artist.albums.length > initialAlbums">
<em v-if="!showAllAlbums" @click="showAllAlbums = true" class="expand">
<i18next path="Show {%0%} more albums">
{{ artist.albums.length - initialAlbums }}
</i18next>
<translate :translate-params="{count: artist.albums.length - initialAlbums}" :translate-n="artist.albums.length - initialAlbums" translate-plural="Show %{ count } more albums">Show 1 more album</translate>
</em>
<em v-else @click="showAllAlbums = false" class="expand">
<i18next path="Collapse"/>
{{ $gettext('Collapse') }}
</em>
</div>
</div>
@ -41,12 +39,10 @@
<div class="extra content">
<span>
<i class="sound icon"></i>
<i18next path="{%0%} albums">
{{ artist.albums.length }}
</i18next>
<translate :translate-params="{count: artist.albums.length}" :translate-n="artist.albums.length" translate-plural="%{ count } albums">1 album</translate>
</span>
<play-button class="mini basic orange right floated" :artist="artist.id">
<i18next path="Play all"/>
{{ $gettext('Play all') }}
</play-button>
</div>
</div>

View File

@ -4,9 +4,9 @@
<tr>
<th></th>
<th></th>
<i18next tag="th" colspan="6" path="Title"/>
<i18next tag="th" colspan="6" path="Artist"/>
<i18next tag="th" colspan="6" path="Album"/>
<th colspan="6">{{ $gettext('Title') }}</th>
<th colspan="6">{{ $gettext('Artist') }}</th>
<th colspan="6">{{ $gettext('Album') }}</th>
<th></th>
</tr>
</thead>
@ -21,17 +21,17 @@
<tr>
<th colspan="3">
<button @click="showDownloadModal = !showDownloadModal" class="ui basic button">
<i18next path="Download..."/>
{{ $gettext('Download') }}
</button>
<modal :show.sync="showDownloadModal">
<i18next tag="div" path="Download tracks" class="header" />
<div class="header">{{ $gettext('Download tracks') }}</div>
<div class="content">
<div class="description">
<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."/>
<p>{{ $gettext('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 cURL to easily download a list of tracks.') }}</p>
{{ $gettext('Simply copy paste the snippet below into a terminal to launch the download.') }}
<div class="ui warning message">
{{ $gettext('Keep your PRIVATE_TOKEN secret as it gives access to your account.') }}
</div>
<pre>
export PRIVATE_TOKEN="{{ $store.state.auth.token }}"
<template v-for="track in tracks"><template v-if="track.files.length > 0">
@ -40,7 +40,7 @@ curl -G -o "{{ track.files[0].filename }}" <template v-if="$store.state.auth.aut
</div>
</div>
<div class="actions">
<i18next tag="div" class="ui black deny button" path="Cancel" />
<div class="ui black deny button">{{ $gettext('Cancel') }}</div>
</div>
</modal>
</th>

View File

@ -2,20 +2,20 @@
<div class="main pusher" v-title="'Log In'">
<div class="ui vertical stripe segment">
<div class="ui small text container">
<h2><i18next path="Log in to your Funkwhale account"/></h2>
<h2>{{ $gettext('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"><i18next path="We cannot log you in"/></div>
<div class="header">{{ $gettext('We cannot log you in') }}</div>
<ul class="list">
<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"/>
<li v-if="error == 'invalid_credentials'">{{ $gettext('Please double-check your username/password couple is correct') }}</li>
<li v-else>{{ $gettext('An unknown error happend, this can mean the server is down or cannot be reached') }}</li>
</ul>
</div>
<div class="field">
<label>
{{ $t('Username or email') }} |
{{ $gettext('Username or email') }} |
<router-link :to="{path: '/signup'}">
{{ $t('Create an account') }}
{{ $gettext('Create an account') }}
</router-link>
</label>
<input
@ -30,15 +30,17 @@
</div>
<div class="field">
<label>
{{ $t('Password') }} |
{{ $gettext('Password') }} |
<router-link :to="{name: 'auth.password-reset', query: {email: credentials.username}}">
{{ $t('Reset your password') }}
{{ $gettext('Reset your password') }}
</router-link>
</label>
<password-input :index="2" required v-model="credentials.password" />
</div>
<button tabindex="3" :class="['ui', {'loading': isLoading}, 'right', 'floated', 'green', 'button']" type="submit"><i18next path="Login"/></button>
<button tabindex="3" :class="['ui', {'loading': isLoading}, 'right', 'floated', 'green', 'button']" type="submit">
{{ $gettext('Login') }}
</button>
</form>
</div>
</div>

View File

@ -2,10 +2,11 @@
<div class="main pusher" v-title="'Log Out'">
<div class="ui vertical stripe segment">
<div class="ui small text container">
<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>
<h2>
{{ $gettext('Are you sure you want to log out?') }}
</h2>
<p v-translate="{username: $store.state.auth.username}">You are currently logged in as %{ username }</p>
<button class="ui button" @click="$store.dispatch('auth/logout')">{{ $gettext('Yes, log me out!') }}</button>
</div>
</div>
</div>

View File

@ -9,17 +9,19 @@
<i class="circular inverted user green icon"></i>
<div class="content">
{{ $store.state.auth.profile.username }}
<i18next class="sub header" path="Registered since {%0%}">{{ signupDate }}</i18next>
<div class="sub header" v-translate="{date: signupDate}">Registered since %{ date }</div>
</div>
</h2>
<div class="ui basic green label"><i18next path="This is you!"/></div>
<div class="ui basic green label">
{{ $gettext('This is you!') }}
</div>
<div v-if="$store.state.auth.profile.is_staff" class="ui yellow label">
<i class="star icon"></i>
<i18next path="Staff member"/>
{{ $gettext('Staff member') }}
</div>
<router-link class="ui tiny basic button" :to="{path: '/settings'}">
<i class="setting icon"> </i>
<i18next path="Settings..."/>
{{ $gettext('Settings...') }}
</router-link>
</div>

View File

@ -2,13 +2,17 @@
<div class="main pusher" v-title="'Account Settings'">
<div class="ui vertical stripe segment">
<div class="ui small text container">
<h2 class="ui header"><i18next path="Account settings"/></h2>
<h2 class="ui header">
{{ $gettext('Account settings') }}
</h2>
<form class="ui form" @submit.prevent="submitSettings()">
<div v-if="settings.success" class="ui positive message">
<div class="header"><i18next path="Settings updated"/></div>
<div class="header">
{{ $gettext('Settings updated') }}
</div>
</div>
<div v-if="settings.errors.length > 0" class="ui negative message">
<i18next tag="div" class="header" path="We cannot save your settings"/>
<div class="header">{{ $gettext('We cannot save your settings') }}</div>
<ul class="list">
<li v-for="error in settings.errors">{{ error }}</li>
</ul>
@ -20,46 +24,52 @@
<option :value="c.value" v-for="c in f.choices">{{ c.label }}</option>
</select>
</div>
<button :class="['ui', {'loading': isLoading}, 'button']" type="submit"><i18next path="Update settings"/></button>
<button :class="['ui', {'loading': isLoading}, 'button']" type="submit">
{{ $gettext('Update settings') }}
</button>
</form>
</div>
<div class="ui hidden divider"></div>
<div class="ui small text container">
<h2 class="ui header"><i18next path="Change my password"/></h2>
<h2 class="ui header">
{{ $gettext('Change my password') }}
</h2>
<div class="ui message">
{{ $t('Changing your password will also change your Subsonic API password if you have requested one.') }}
{{ $t('You will have to update your password on your clients that use this password.') }}
{{ $gettext('Changing your password will also change your Subsonic API password if you have requested one.') }}
{{ $gettext('You will have to update your password on your clients that use this password.') }}
</div>
<form class="ui form" @submit.prevent="submitPassword()">
<div v-if="passwordError" class="ui negative message">
<div class="header"><i18next path="Cannot change your password"/></div>
<div class="header">
{{ $gettext('Cannot change your password') }}
</div>
<ul class="list">
<i18next tag="li" v-if="passwordError == 'invalid_credentials'" path="Please double-check your password is correct"/>
<li v-if="passwordError == 'invalid_credentials'">{{ $gettext('Please double-check your password is correct') }}</li>
</ul>
</div>
<div class="field">
<label><i18next path="Old password"/></label>
<label>{{ $gettext('Old password') }}</label>
<password-input required v-model="old_password" />
</div>
<div class="field">
<label><i18next path="New password"/></label>
<label>{{ $gettext('New password') }}</label>
<password-input required v-model="new_password" />
</div>
<dangerous-button
color="yellow"
:class="['ui', {'loading': isLoading}, 'button']"
:action="submitPassword">
{{ $t('Change password') }}
<p slot="modal-header">{{ $t('Change your password?') }}</p>
{{ $gettext('Change password') }}
<p slot="modal-header">{{ $gettext('Change your password?') }}</p>
<div slot="modal-content">
<p>{{ $t("Changing your password will have the following consequences") }}</p>
<p>{{ $gettext("Changing your password will have the following consequences") }}</p>
<ul>
<li>{{ $t('You will be logged out from this session and have to log out with the new one') }}</li>
<li>{{ $t('Your Subsonic password will be changed to a new, random one, logging you out from devices that used the old Subsonic password') }}</li>
<li>{{ $gettext('You will be logged out from this session and have to log out with the new one') }}</li>
<li>{{ $gettext('Your Subsonic password will be changed to a new, random one, logging you out from devices that used the old Subsonic password') }}</li>
</ul>
</div>
<p slot="modal-confirm">{{ $t('Disable access') }}</p>
<p slot="modal-confirm">{{ $gettext('Disable access') }}</p>
</dangerous-button>
</form>
<div class="ui hidden divider" />

View File

@ -2,22 +2,22 @@
<div class="main pusher" v-title="'Sign Up'">
<div class="ui vertical stripe segment">
<div class="ui small text container">
<h2>{{ $t("Create a funkwhale account") }}</h2>
<h2>{{ $gettext("Create a funkwhale account") }}</h2>
<form
:class="['ui', {'loading': isLoadingInstanceSetting}, 'form']"
@submit.prevent="submit()">
<p class="ui message" v-if="!$store.state.instance.settings.users.registration_enabled.value">
{{ $t('Registration are closed on this instance, you will need an invitation code to signup.') }}
{{ $gettext('Registration are closed on this instance, you will need an invitation code to signup.') }}
</p>
<div v-if="errors.length > 0" class="ui negative message">
<div class="header">{{ $t("We cannot create your account") }}</div>
<div class="header">{{ $gettext("We cannot create your account") }}</div>
<ul class="list">
<li v-for="error in errors">{{ error }}</li>
</ul>
</div>
<div class="field">
<label>{{ $t("Username") }}</label>
<label>{{ $gettext("Username") }}</label>
<input
ref="username"
required
@ -27,7 +27,7 @@
v-model="username">
</div>
<div class="field">
<label>{{ $t("Email") }}</label>
<label>{{ $gettext("Email") }}</label>
<input
ref="email"
required
@ -36,20 +36,20 @@
v-model="email">
</div>
<div class="field">
<label>{{ $t("Password") }}</label>
<label>{{ $gettext("Password") }}</label>
<password-input v-model="password" />
</div>
<div class="field">
<label v-if="!$store.state.instance.settings.users.registration_enabled.value">{{ $t("Invitation code") }}</label>
<label v-else>{{ $t("Invitation code (optional)") }}</label>
<label v-if="!$store.state.instance.settings.users.registration_enabled.value">{{ $gettext("Invitation code") }}</label>
<label v-else>{{ $gettext("Invitation code (optional)") }}</label>
<input
:required="!$store.state.instance.settings.users.registration_enabled.value"
type="text"
:placeholder="$t('Enter your invitation code (case insensitive)')"
:placeholder="$gettext('Enter your invitation code (case insensitive)')"
v-model="invitation">
</div>
<button :class="['ui', 'green', {'loading': isLoading}, 'button']" type="submit">
{{ $t("Create my account") }}
{{ $gettext("Create my account") }}
</button>
</form>
</div>

View File

@ -1,24 +1,24 @@
<template>
<form class="ui form" @submit.prevent="requestNewToken()">
<h2>{{ $t('Subsonic API password') }}</h2>
<h2>{{ $gettext('Subsonic API password') }}</h2>
<p class="ui message" v-if="!subsonicEnabled">
{{ $t('The Subsonic API is not available on this Funkwhale instance.') }}
{{ $gettext('The Subsonic API is not available on this Funkwhale instance.') }}
</p>
<p>
{{ $t('Funkwhale is compatible with other music players that support the Subsonic API.') }}
{{ $t('You can use those to enjoy your playlist and music in offline mode, on your smartphone or tablet, for instance.') }}
{{ $gettext('Funkwhale is compatible with other music players that support the Subsonic API.') }}
{{ $gettext('You can use those to enjoy your playlist and music in offline mode, on your smartphone or tablet, for instance.') }}
</p>
<p>
{{ $t('However, accessing Funkwhale from those clients require a separate password you can set below.') }}
{{ $gettext('However, accessing Funkwhale from those clients require a separate password you can set below.') }}
</p>
<p><a href="https://docs.funkwhale.audio/users/apps.html#subsonic-compatible-clients" target="_blank">
{{ $t('Discover how to use Funkwhale from other apps') }}
{{ $gettext('Discover how to use Funkwhale from other apps') }}
</a></p>
<div v-if="success" class="ui positive message">
<div class="header">{{ successMessage }}</div>
</div>
<div v-if="subsonicEnabled && errors.length > 0" class="ui negative message">
<div class="header">{{ $t('Error') }}</div>
<div class="header">{{ $gettext('Error') }}</div>
<ul class="list">
<li v-for="error in errors">{{ error }}</li>
</ul>
@ -32,25 +32,25 @@
color="grey"
:class="['ui', {'loading': isLoading}, 'button']"
:action="requestNewToken">
{{ $t('Request a new password') }}
<p slot="modal-header">{{ $t('Request a new Subsonic API password?') }}</p>
<p slot="modal-content">{{ $t('This will log you out from existing devices that use the current password.') }}</p>
<p slot="modal-confirm">{{ $t('Request a new password') }}</p>
{{ $gettext('Request a new password') }}
<p slot="modal-header">{{ $gettext('Request a new Subsonic API password?') }}</p>
<p slot="modal-content">{{ $gettext('This will log you out from existing devices that use the current password.') }}</p>
<p slot="modal-confirm">{{ $gettext('Request a new password') }}</p>
</dangerous-button>
<button
v-else
color="grey"
:class="['ui', {'loading': isLoading}, 'button']"
@click="requestNewToken">{{ $t('Request a password') }}</button>
@click="requestNewToken">{{ $gettext('Request a password') }}</button>
<dangerous-button
v-if="token"
color="yellow"
:class="['ui', {'loading': isLoading}, 'button']"
:action="disable">
{{ $t('Disable Subsonic access') }}
<p slot="modal-header">{{ $t('Disable Subsonic API access?') }}</p>
<p slot="modal-content">{{ $t('This will completely disable access to the Subsonic API using from account.') }}</p>
<p slot="modal-confirm">{{ $t('Disable access') }}</p>
{{ $gettext('Disable Subsonic access') }}
<p slot="modal-header">{{ $gettext('Disable Subsonic API access?') }}</p>
<p slot="modal-content">{{ $gettext('This will completely disable access to the Subsonic API using from account.') }}</p>
<p slot="modal-confirm">{{ $gettext('Disable access') }}</p>
</dangerous-button>
</template>
</form>
@ -92,7 +92,7 @@ export default {
})
},
requestNewToken () {
this.successMessage = this.$t('Password updated')
this.successMessage = this.$gettext('Password updated')
this.success = false
this.errors = []
this.isLoading = true
@ -108,7 +108,7 @@ export default {
})
},
disable () {
this.successMessage = this.$t('Access disabled')
this.successMessage = this.$gettext('Access disabled')
this.success = false
this.errors = []
this.isLoading = true

View File

@ -6,7 +6,7 @@
<div class="ui small form">
<div class="ui inline fields">
<div class="field">
<label>{{ $t('Actions') }}</label>
<label>{{ $gettext('Actions') }}</label>
<select class="ui dropdown" v-model="currentActionName">
<option v-for="action in actions" :value="action.name">
{{ action.label }}
@ -19,41 +19,75 @@
@click="launchAction"
:disabled="checked.length === 0"
:class="['ui', {disabled: checked.length === 0}, {'loading': actionLoading}, 'button']">
{{ $t('Go') }}</div>
{{ $gettext('Go') }}</div>
<dangerous-button
v-else-if="!currentAction.isDangerous" :class="['ui', {disabled: checked.length === 0}, {'loading': actionLoading}, 'button']"
confirm-color="green"
color=""
@confirm="launchAction">
{{ $t('Go') }}
<p slot="modal-header">{{ $t('Do you want to launch action "{% action %}" on {% total %} elements?', {action: currentActionName, total: objectsData.count}) }}
<p slot="modal-content">
{{ $t('This may affect a lot of elements, please double check this is really what you want.')}}
{{ $gettext('Go') }}
<p slot="modal-header">
<translate
:translate-n="objectsData.count"
:translate-params="{count: objectsData.count, action: currentActionName}"
translate-plural="Do you want to launch %{ action } on %{ count } elements?">
Do you want to launch %{ action } on %{ count } element?
</translate>
</p>
<p slot="modal-confirm">{{ $t('Launch') }}</p>
<p slot="modal-content">
{{ $gettext('This may affect a lot of elements, please double check this is really what you want.')}}
</p>
<p slot="modal-confirm">{{ $gettext('Launch') }}</p>
</dangerous-button>
</div>
<div class="count field">
<span v-if="selectAll">{{ $t('{% count %} on {% total %} selected', {count: objectsData.count, total: objectsData.count}) }}</span>
<span v-else>{{ $t('{% count %} on {% total %} selected', {count: checked.length, total: objectsData.count}) }}</span>
<translate
tag="span"
v-if="selectAll"
:translate-n="objectsData.count"
:translate-params="{count: objectsData.count, total: objectsData.count}"
translate-plural="%{ count } on %{ total } selected">
%{ count } on %{ total } selected
</translate>
<translate
tag="span"
v-else
:translate-n="checked.length"
:translate-params="{count: checked.length, total: objectsData.count}"
translate-plural="%{ count } on %{ total } selected">
%{ count } on %{ total } selected
</translate>
<template v-if="!currentAction.isDangerous && checkable.length > 0 && checkable.length === checked.length">
<a @click="selectAll = true" v-if="!selectAll">
{{ $t('Select all {% total %} elements', {total: objectsData.count}) }}
<translate
:translate-n="objectsData.count"
:translate-params="{total: objectsData.count}"
translate-plural="Select all %{ total } elements">
Select all %{ total } elements
</translate>
</a>
<a @click="selectAll = false" v-else>
{{ $t('Select only current page') }}
{{ $gettext('Select only current page') }}
</a>
</template>
</div>
</div>
<div v-if="actionErrors.length > 0" class="ui negative message">
<div class="header">{{ $t('Error while applying action') }}</div>
<div class="header">{{ $gettext('Error while applying action') }}</div>
<ul class="list">
<li v-for="error in actionErrors">{{ error }}</li>
</ul>
</div>
<div v-if="actionResult" class="ui positive message">
<p>{{ $t('Action {% action %} was launched successfully on {% count %} objects.', {action: actionResult.action, count: actionResult.updated}) }}</p>
<p>
<translate
:translate-n="actionResult.updated"
:translate-params="{count: actionResult.updated, action: actionResult.action}"
translate-plural="Action %{ action } was launched successfully on %{ count } elements">
Action %{ action } was launched successfully on %{ count } element
</translate>
</p>
<slot name="action-success-footer" :result="actionResult">
</slot>
</div>
@ -81,7 +115,6 @@
:disabled="checkable.indexOf(obj.id) === -1"
@click="toggleCheck($event, obj.id, index)"
:checked="checked.indexOf(obj.id) > -1"><label>&nbsp;</label>
</div>
</td>
<slot name="row-cells" :obj="obj"></slot>
</tr>

View File

@ -4,7 +4,9 @@
<modal class="small" :show.sync="showModal">
<div class="header">
<slot name="modal-header"><i18next path="Do you want to confirm this action?"/></slot>
<slot name="modal-header">
{{ $gettext('Do you want to confirm this action?') }}
</slot>
</div>
<div class="scrolling content">
<div class="description">
@ -12,9 +14,13 @@
</div>
</div>
<div class="actions">
<div class="ui cancel button"><i18next path="Cancel"/></div>
<div class="ui cancel button">
{{ $gettext('Cancel') }}
</div>
<div :class="['ui', 'confirm', confirmButtonColor, 'button']" @click="confirm">
<slot name="modal-confirm"><i18next path="Confirm"/></slot>
<slot name="modal-confirm">
{{ $gettext('Confirm') }}
</slot>
</div>
</div>
</modal>

View File

@ -11,13 +11,15 @@
<span
@click="collapsed = false"
v-if="truncated && collapsed"
class="expand"
path="Expand"/>
<i18next
class="expand">
{{ $gettext('Expand') }}
</span>
<span
@click="collapsed = true"
v-if="truncated && !collapsed"
class="collapse"
path="Collapse"/>
class="collapse">
{{ $gettext('Collapse') }}
</span>
</div>
</div>
</div>

View File

@ -2,13 +2,18 @@
<div class="main pusher" v-title="'Your Favorites'">
<div class="ui vertical center aligned stripe segment">
<div :class="['ui', {'active': isLoading}, 'inverted', 'dimmer']">
<div class="ui text loader"><i18next path="Loading your favorites..."/></div>
<div class="ui text loader">
{{ $gettext('Loading your favorites...') }}
</div>
</div>
<h2 v-if="results" class="ui center aligned icon header">
<i class="circular inverted heart pink icon"></i>
<i18next path="{%0%} favorites">
{{ $store.state.favorites.count }}
</i18next>
<translate
translate-plural="%{ count } favorites"
:translate-n="$store.state.favorites.count"
:translate-params="{count: $store.state.favorites.count}">
1 favorite
</translate>
</h2>
<radio-button type="favorites"></radio-button>
</div>
@ -16,7 +21,7 @@
<div :class="['ui', {'loading': isLoading}, 'form']">
<div class="fields">
<div class="field">
<i18next tag="label" path="Ordering"/>
<label>{{ $gettext('Ordering') }}</label>
<select class="ui dropdown" v-model="ordering">
<option v-for="option in orderingOptions" :value="option[0]">
{{ option[1] }}
@ -24,14 +29,14 @@
</select>
</div>
<div class="field">
<i18next tag="label" path="Ordering direction"/>
<label>{{ $gettext('Ordering direction') }}</label>
<select class="ui dropdown" v-model="orderingDirection">
<option value="+"><i18next path="Ascending"/></option>
<option value="-"><i18next path="Descending"/></option>
<option value="+">{{ $gettext('Ascending') }}</option>
<option value="-">{{ $gettext('Descending') }}</option>
</select>
</div>
<div class="field">
<i18next tag="label" path="Results per page"/>
<label>{{ $gettext('Results per page') }}</label>
<select class="ui dropdown" v-model="paginateBy">
<option :value="parseInt(12)">12</option>
<option :value="parseInt(25)">25</option>

View File

@ -1,8 +1,8 @@
<template>
<button @click="$store.dispatch('favorites/toggle', track.id)" v-if="button" :class="['ui', 'pink', {'inverted': isFavorite}, {'favorited': isFavorite}, 'button']">
<i class="heart icon"></i>
<i18next v-if="isFavorite" path="In favorites"/>
<i18next v-else path="Add to favorites"/>
<translate v-if="isFavorite">In favorites</translate>
<translate v-else>Add to favorites</translate>
</button>
<i v-else @click="$store.dispatch('favorites/toggle', track.id)" :class="['favorite-icon', 'heart', {'pink': isFavorite}, {'favorited': isFavorite}, 'link', 'icon']" :title="title"></i>
</template>
@ -16,9 +16,9 @@ export default {
computed: {
title () {
if (this.isFavorite) {
return this.$t('Remove from favorites')
return this.$gettext('Remove from favorites')
} else {
return this.$t('Add to favorites')
return this.$gettext('Add to favorites')
}
},
isFavorite () {

View File

@ -13,39 +13,42 @@
</div>
<div class="content">
<span class="right floated" v-if="following">
<i class="check icon"></i><i18next path="Following"/>
<i class="check icon"></i>{{ $gettext('Following') }}
</span>
<span class="right floated" v-else-if="manuallyApprovesFollowers">
<i class="lock icon"></i><i18next path="Followers only"/>
<i class="lock icon"></i>{{ $gettext('Followers only') }}
</span>
<span class="right floated" v-else>
<i class="open lock icon"></i><i18next path="Open"/>
<i class="open lock icon"></i>{{ $gettext('Open') }}
</span>
<span v-if="totalItems">
<i class="music icon"></i>
<i18next path="{%0%} tracks">
{{ totalItems }}
</i18next>
<translate
translate-plural="%{ count } tracks"
:translate-n="totalItems"
:translate-params="{count: totalItems}">
1 track
</translate>
</span>
</div>
<div class="extra content">
<template v-if="awaitingApproval">
<i class="clock icon"></i>
<i18next path="Follow request pending approval"/>
{{ $gettext('Follow request pending approval') }}
</template>
<div
v-if="!library"
@click="follow"
:disabled="isLoading"
:class="['ui', 'basic', {loading: isLoading}, 'green', 'button']">
<i18next v-if="manuallyApprovesFollowers" path="Send a follow request"/>
<i18next v-else path="Follow"/>
<translate v-if="manuallyApprovesFollowers">Send a follow request</translate>
<translate v-else>Follow</translate>
</div>
<router-link
v-else
class="ui basic button"
:to="{name: 'federation.libraries.detail', params: {id: library.uuid }}">
<i18next path="Detail"/>
{{ $gettext('Detail') }}
</router-link>
</div>
</div>

View File

@ -8,7 +8,9 @@
<div class="ui four wide inline field">
<div class="ui checkbox">
<input v-model="pending" type="checkbox">
<label><i18next path="Pending approval"/></label>
<label>
{{ $gettext('Pending approval') }}
</label>
</div>
</div>
</div>
@ -17,10 +19,10 @@
<table v-if="result" class="ui very basic single line unstackable table">
<thead>
<tr>
<i18next tag="th" path="Actor"/>
<i18next tag="th" path="Creation date"/>
<i18next tag="th" path="Status"/>
<i18next tag="th" path="Actions"/>
<th>{{ $gettext('Actor') }}</th>
<th>{{ $gettext('Creation date') }}</th>
<th>{{ $gettext('Status') }}</th>
<th>{{ $gettext('Actions') }}</th>
</tr>
</thead>
<tbody>
@ -33,36 +35,49 @@
</td>
<td>
<template v-if="follow.approved === true">
<i class="check icon"></i><i18next path="Approved"/>
<i class="check icon"></i>
{{ $gettext('Approved') }}
</template>
<template v-else-if="follow.approved === false">
<i class="x icon"></i><i18next path="Refused"/>
<i class="x icon"></i>
{{ $gettext('Refused') }}
</template>
<template v-else>
<i class="clock icon"></i><i18next path="Pending"/>
<i class="clock icon"></i>
{{ $gettext('Pending') }}
</template>
</td>
<td>
<dangerous-button v-if="follow.approved !== false" class="tiny basic labeled icon" color='red' @confirm="updateFollow(follow, false)">
<i class="x icon"></i><i18next path="Deny"/>
<p slot="modal-header"><i18next path="Deny access?"/></p>
<p slot="modal-content">
<i18next path="By confirming, {%0%}@{%1%} will be denied access to your library.">
{{ follow.actor.preferred_username }}
{{ follow.actor.domain }}
</i18next>
<i class="x icon"></i>
{{ $gettext('Deny') }}
<p slot="modal-header">
{{ $gettext('Deny access?') }}
</p>
<p slot="modal-content">
<translate
:translate-params="{username: follow.actor.preferred_username + '@' + follow.actor.domain}">
By confirming, %{ username } will be denied access to your library.
</translate>
</p>
<p slot="modal-confirm">
{{ $gettext('Deny') }}
</p>
<p slot="modal-confirm"><i18next path="Deny"/></p>
</dangerous-button>
<dangerous-button v-if="follow.approved !== true" class="tiny basic labeled icon" color='green' @confirm="updateFollow(follow, true)">
<i class="check icon"></i> <i18next path="Approve"/>
<p slot="modal-header"><i18next path="Approve access?"/></p>
<i class="check icon"></i>
{{ $gettext('Approve') }}
<p slot="modal-header">
{{ $gettext('Approve access?') }}
</p>
<p slot="modal-content">
<i18next path="By confirming, {%0%}@{%1%} will be granted access to your library.">
{{ follow.actor.preferred_username }}
{{ follow.actor.domain }}
</i18next>
<p slot="modal-confirm"><i18next path="Approve"/></p>
<translate
:translate-params="{username: follow.actor.preferred_username + '@' + follow.actor.domain}">
By confirming, %{ username } will be granted access to your library.
</translate>
<p slot="modal-confirm">
{{ $gettext('Approve') }}
</p>
</dangerous-button>
</td>
</tr>
@ -80,11 +95,10 @@
></pagination>
</th>
<th v-if="result && result.results.length > 0">
<i18next path="Showing results {%0%}-{%1%} on {%2%}">
{{ ((page-1) * paginateBy) + 1 }}
{{ ((page-1) * paginateBy) + result.results.length }}
{{ result.count }}
</i18next>
<translate
:translate-params="{start: ((page-1) * paginateBy) + 1, end: ((page-1) * paginateBy) + result.results.length, total: result.count}">
Showing results %{ start }-%{ end } on %{ total }
</translate>
</th>
<th></th>
<th></th>

View File

@ -1,9 +1,15 @@
<template>
<form class="ui form" @submit.prevent="fetchInstanceInfo">
<h3 class="ui header"><i18next path="Federate with a new instance"/></h3>
<p><i18next path="Use this form to scan an instance and setup federation."/></p>
<h3 class="ui header">
{{ $gettext('Federate with a new instance') }}
</h3>
<p>
{{ $gettext('Use this form to scan an instance and setup federation.') }}
</p>
<div v-if="errors.length > 0 || scanErrors.length > 0" class="ui negative message">
<div class="header"><i18next path="Error while scanning library"/></div>
<div class="header">
{{ $gettext('Error while scanning library') }}
</div>
<ul class="list">
<li v-for="error in errors">{{ error }}</li>
<li v-for="error in scanErrors">{{ error }}</li>
@ -11,7 +17,9 @@
</div>
<div class="ui two fields">
<div class="ui field">
<label><i18next path="Library name"/></label>
<label>
{{ $gettext('Library name') }}
</label>
<input v-model="libraryUsername" type="text" placeholder="library@demo.funkwhale.audio" />
</div>
<div class="ui field">
@ -21,7 +29,7 @@
:disabled="isLoading"
:class="['ui', 'icon', {loading: isLoading}, 'button']">
<i class="search icon"></i>
<i18next path="Launch scan"/>
{{ $gettext('Launch scan') }}
</button>
</div>
</div>

View File

@ -3,16 +3,16 @@
<div class="ui inline form">
<div class="fields">
<div class="ui field">
<label>{{ $t('Search') }}</label>
<label>{{ $gettext('Search') }}</label>
<input type="text" v-model="search" placeholder="Search by title, artist, domain..." />
</div>
<div class="ui field">
<label>{{ $t('Import status') }}</label>
<label>{{ $gettext('Import status') }}</label>
<select class="ui dropdown" v-model="importedFilter">
<option :value="null">{{ $t('Any') }}</option>
<option :value="'imported'">{{ $t('Imported') }}</option>
<option :value="'not_imported'">{{ $t('Not imported') }}</option>
<option :value="'import_pending'">{{ $t('Import pending') }}</option>
<option :value="null">{{ $gettext('Any') }}</option>
<option :value="'imported'">{{ $gettext('Imported') }}</option>
<option :value="'not_imported'">{{ $gettext('Not imported') }}</option>
<option :value="'import_pending'">{{ $gettext('Import pending') }}</option>
</select>
</div>
</div>
@ -29,25 +29,28 @@
:action-url="'federation/library-tracks/action/'"
:filters="actionFilters">
<template slot="header-cells">
<th>{{ $t('Status') }}</th>
<th>{{ $t('Title') }}</th>
<th>{{ $t('Artist') }}</th>
<th>{{ $t('Album') }}</th>
<th>{{ $t('Published date') }}</th>
<th v-if="showLibrary">{{ $t('Library') }}</th>
<th>{{ $gettext('Status') }}</th>
<th>{{ $gettext('Title') }}</th>
<th>{{ $gettext('Artist') }}</th>
<th>{{ $gettext('Album') }}</th>
<th>{{ $gettext('Published date') }}</th>
<th v-if="showLibrary">{{ $gettext('Library') }}</th>
</template>
<template slot="action-success-footer" slot-scope="scope">
<router-link
v-if="scope.result.action === 'import'"
:to="{name: 'library.import.batches.detail', params: {id: scope.result.result.batch.id }}">
{{ $t('Import #{% id %} launched', {id: scope.result.result.batch.id}) }}
<translate
:translate-params="{id: scope.result.result.batch.id}">
Import #%{ id } launched
</translate>
</router-link>
</template>
<template slot="row-cells" slot-scope="scope">
<td>
<span v-if="scope.obj.status === 'imported'" class="ui basic green label">{{ $t('In library') }}</span>
<span v-else-if="scope.obj.status === 'import_pending'" class="ui basic yellow label">{{ $t('Import pending') }}</span>
<span v-else class="ui basic label">{{ $t('Not imported') }}</span>
<span v-if="scope.obj.status === 'imported'" class="ui basic green label">{{ $gettext('In library') }}</span>
<span v-else-if="scope.obj.status === 'import_pending'" class="ui basic yellow label">{{ $gettext('Import pending') }}</span>
<span v-else class="ui basic label">{{ $gettext('Not imported') }}</span>
</td>
<td>
<span :title="scope.obj.title">{{ scope.obj.title|truncate(30) }}</span>
@ -78,7 +81,10 @@
></pagination>
<span v-if="result && result.results.length > 0">
{{ $t('Showing results {%start%}-{%end%} on {%total%}', {start: ((page-1) * paginateBy) + 1 , end: ((page-1) * paginateBy) + result.results.length, total: result.count})}}
<translate
:translate-params="{start: ((page-1) * paginateBy) + 1, end: ((page-1) * paginateBy) + result.results.length, total: result.count}">
Showing results %{ start }-%{ end } on %{ total }
</translate>
</span>
</div>
</div>
@ -150,10 +156,11 @@ export default {
}
},
actions () {
let msg = this.$gettext('Import')
return [
{
name: 'import',
label: this.$t('Import'),
label: msg,
filterCheckable: (obj) => { return obj.status === 'not_imported' }
}
]

View File

@ -6,7 +6,7 @@
:type="passwordInputType"
@input="$emit('input', $event.target.value)"
:value="value">
<span @click="showPassword = !showPassword" :title="$t('Show/hide password')" class="ui icon button">
<span @click="showPassword = !showPassword" :title="$gettext('Show/hide password')" class="ui icon button">
<i class="eye icon"></i>
</span>
</div>

View File

@ -2,26 +2,28 @@
<div>
<div v-if="stats" class="ui stackable two column grid">
<div class="column">
<h3 class="ui left aligned header"><i18next path="User activity"/></h3>
<h3 class="ui left aligned header">
{{ $gettext('User activity') }}
</h3>
<div v-if="stats" class="ui mini horizontal statistics">
<div class="statistic">
<div class="value">
<i class="green user icon"></i>
{{ stats.users }}
</div>
<i18next tag="div" class="label" path="users"/>
<div class="label">{{ $gettext('users') }}</div>
</div>
<div class="statistic">
<div class="value">
<i class="orange sound icon"></i> {{ stats.listenings }}
</div>
<i18next tag="div" class="label" path="tracks listened"/>
<div class="label">{{ $gettext('tracks listened') }}</div>
</div>
<div class="statistic">
<div class="value">
<i class="pink heart icon"></i> {{ stats.trackFavorites }}
</div>
<i18next tag="div" class="label" path="Tracks favorited"/>
<div class="label">{{ $gettext('Tracks favorited') }}</div>
</div>
</div>
</div>
@ -32,25 +34,25 @@
<div class="value">
{{ parseInt(stats.musicDuration) }}
</div>
<i18next tag="div" class="label" path="hours of music"/>
<div class="label">{{ $gettext('Hours of music') }}</div>
</div>
<div class="statistic">
<div class="value">
{{ stats.artists }}
</div>
<i18next tag="div" class="label" path="Artists"/>
<div class="label">{{ $gettext('Artists') }}</div>
</div>
<div class="statistic">
<div class="value">
{{ stats.albums }}
</div>
<i18next tag="div" class="label" path="Albums"/>
<div class="label">{{ $gettext('Albums') }}</div>
</div>
<div class="statistic">
<div class="value">
{{ stats.tracks }}
</div>
<i18next tag="div" class="label" path="tracks"/>
<div class="label">{{ $gettext('tracks') }}</div>
</div>
</div>
</div>

View File

@ -10,30 +10,39 @@
<i class="circular inverted sound yellow icon"></i>
<div class="content">
{{ album.title }}
<i18next tag="div" class="sub header" path="Album containing {%0%} tracks, by {%1%}">
{{ album.tracks.length }}
<router-link :to="{name: 'library.artists.detail', params: {id: album.artist.id }}">
{{ album.artist.name }}
</router-link>
</i18next>
<translate
tag="div"
translate-plural="Album containing %{ count } tracks, by %{ artist }"
:translate-n="album.tracks.length"
:translate-params="{count: album.tracks.length, artist: album.artist.name}">
Album containing %{ count } track, by %{ artist }
</translate>
</div>
<div class="ui basic buttons">
<router-link class="ui button" :to="{name: 'library.artists.detail', params: {id: album.artist.id }}">
{{ $gettext('Artist page') }}
</router-link>
</div>
</h2>
<div class="ui hidden divider"></div>
</button>
<play-button class="orange" :tracks="album.tracks"><i18next path="Play all"/></play-button>
<play-button class="orange" :tracks="album.tracks">
{{ $gettext('Play all') }}
</play-button>
<a :href="wikipediaUrl" target="_blank" class="ui button">
<i class="wikipedia icon"></i>
<i18next path="Search on Wikipedia"/>
{{ $gettext('Search on Wikipedia') }}
</a>
<a :href="musicbrainzUrl" target="_blank" class="ui button">
<i class="external icon"></i>
<i18next path="View on MusicBrainz"/>
{{ $gettext('View on MusicBrainz') }}
</a>
</div>
</div>
<div class="ui vertical stripe segment">
<h2><i18next path="Tracks"/></h2>
<h2>
{{ $gettext('Tracks') }}
</h2>
<track-table v-if="album" :display-position="true" :tracks="album.tracks"></track-table>
</div>
</template>

View File

@ -11,22 +11,29 @@
<div class="content">
{{ artist.name }}
<div class="sub header" v-if="albums">
{{ $t('{% track_count %} tracks in {% album_count %} albums', {track_count: totalTracks, album_count: albums.length})}}
<translate
tag="div"
translate-plural="%{ count } tracks in %{ albumsCount } albums"
:translate-n="totalTracks"
:translate-params="{count: totalTracks, albumsCount: albums.length}">
%{ count } track in %{ albumsCount } albums
</translate>
</div>
</div>
</h2>
<div class="ui hidden divider"></div>
<radio-button type="artist" :object-id="artist.id"></radio-button>
</button>
<play-button class="orange" :artist="artist.id"><i18next path="Play all albums"/></play-button>
<play-button class="orange" :artist="artist.id">
{{ $gettext('Play all albums') }}
</play-button>
<a :href="wikipediaUrl" target="_blank" class="ui button">
<i class="wikipedia icon"></i>
<i18next path="Search on Wikipedia"/>
{{ $gettext('Search on Wikipedia') }}
</a>
<a :href="musicbrainzUrl" target="_blank" class="ui button">
<i class="external icon"></i>
<i18next path="View on MusicBrainz"/>
{{ $gettext('View on MusicBrainz') }}
</a>
</div>
</div>
@ -34,7 +41,9 @@
<div :class="['ui', 'centered', 'active', 'inline', 'loader']"></div>
</div>
<div v-else-if="albums" class="ui vertical stripe segment">
<h2><i18next path="Albums by this artist"/></h2>
<h2>
{{ $gettext('Albums by this artist') }}
</h2>
<div class="ui stackable doubling three column grid">
<div class="column" :key="album.id" v-for="album in albums">
<album-card :mode="'rich'" class="fluid" :album="album"></album-card>

View File

@ -1,15 +1,19 @@
<template>
<div v-title="'Artists'">
<div class="ui vertical stripe segment">
<h2 class="ui header"><i18next path="Browsing artists"/></h2>
<h2 class="ui header">
{{ $gettext('Browsing artists') }}
</h2>
<div :class="['ui', {'loading': isLoading}, 'form']">
<div class="fields">
<div class="field">
<label><i18next path="Search"/></label>
<label>
{{ $gettext('Search') }}
</label>
<input type="text" v-model="query" placeholder="Enter an artist name..."/>
</div>
<div class="field">
<i18next tag="label" path="Ordering"/>
<label>{{ $gettext('Ordering') }}</label>
<select class="ui dropdown" v-model="ordering">
<option v-for="option in orderingOptions" :value="option[0]">
{{ option[1] }}
@ -17,14 +21,14 @@
</select>
</div>
<div class="field">
<i18next tag="label" path="Ordering direction"/>
<label>{{ $gettext('Ordering direction') }}</label>
<select class="ui dropdown" v-model="orderingDirection">
<option value="+">Ascending</option>
<option value="-">Descending</option>
</select>
</div>
<div class="field">
<i18next tag="label" path="Results per page"/>
<label>{{ $gettext('Results per page') }}</label>
<select class="ui dropdown" v-model="paginateBy">
<option :value="parseInt(12)">12</option>
<option :value="parseInt(25)">25</option>

View File

@ -6,20 +6,26 @@
<div class="ui vertical stripe segment">
<div class="ui stackable three column grid">
<div class="column">
<h2 class="ui header"><i18next path="Latest artists"/></h2>
<h2 class="ui header">
{{ $gettext('Latest artists') }}
</h2>
<div :class="['ui', {'active': isLoadingArtists}, 'inline', 'loader']"></div>
<div v-if="artists.length > 0" v-for="artist in artists.slice(0, 3)" :key="artist.id" class="ui cards">
<artist-card :artist="artist"></artist-card>
</div>
</div>
<div class="column">
<h2 class="ui header"><i18next path="Radios"/></h2>
<h2 class="ui header">
{{ $gettext('Radios') }}
</h2>
<radio-card :type="'favorites'"></radio-card>
<radio-card :type="'random'"></radio-card>
<radio-card :type="'less-listened'"></radio-card>
</div>
<div class="column">
<h2 class="ui header"><i18next path="Music requests"/></h2>
<h2 class="ui header">
{{ $gettext('Music requests') }}
</h2>
<request-form v-if="$store.state.auth.authenticated"></request-form>
</div>
</div>

View File

@ -1,16 +1,24 @@
<template>
<div class="main library pusher">
<div class="ui secondary pointing menu">
<router-link class="ui item" to="/library" exact><i18next path="Browse"/></router-link>
<router-link class="ui item" to="/library/artists" exact><i18next path="Artists"/></router-link>
<router-link class="ui item" to="/library/radios" exact><i18next path="Radios"/></router-link>
<router-link class="ui item" to="/library/playlists" exact><i18next path="Playlists"/></router-link>
<router-link class="ui item" to="/library" exact>
{{ $gettext('Browse') }}
</router-link>
<router-link class="ui item" to="/library/artists" exact>
{{ $gettext('Artists') }}
</router-link>
<router-link class="ui item" to="/library/radios" exact>
{{ $gettext('Radios') }}
</router-link>
<router-link class="ui item" to="/library/playlists" exact>
{{ $gettext('Playlists') }}
</router-link>
<div class="ui secondary right menu">
<router-link v-if="showImports" class="ui item" to="/library/import/launch" exact>
<i18next path="Import"/>
{{ $gettext('Import') }}
</router-link>
<router-link v-if="showImports" class="ui item" to="/library/import/batches">
<i18next path="Import batches"/>
{{ $gettext('Import batches') }}
</router-link>
</div>
</div>

View File

@ -1,19 +1,21 @@
<template>
<div v-title="'Radios'">
<div class="ui vertical stripe segment">
<h2 class="ui header"><i18next path="Browsing radios"/></h2>
<h2 class="ui header">
{{ $gettext('Browsing radios') }}
</h2>
<router-link class="ui green basic button" to="/library/radios/build" exact>
<i18next path="Create your own radio"/>
{{ $gettext('Create your own radio') }}
</router-link>
<div class="ui hidden divider"></div>
<div :class="['ui', {'loading': isLoading}, 'form']">
<div class="fields">
<div class="field">
<i18next tag="label" path="Search"/>
<label>{{ $gettext('Search') }}</label>
<input type="text" v-model="query" placeholder="Enter a radio name..."/>
</div>
<div class="field">
<i18next tag="label" path="Ordering"/>
<label>{{ $gettext('Ordering') }}</label>
<select class="ui dropdown" v-model="ordering">
<option v-for="option in orderingOptions" :value="option[0]">
{{ option[1] }}
@ -21,14 +23,18 @@
</select>
</div>
<div class="field">
<i18next tag="label" path="Ordering direction"/>
<label>{{ $gettext('Ordering direction') }}</label>
<select class="ui dropdown" v-model="orderingDirection">
<option value="+"><i18next path="Ascending"/></option>
<option value="-"><i18next path="Descending"/></option>
<option value="+">
{{ $gettext('Ascending') }}
</option>
<option value="-">
{{ $gettext('Descending') }}
</option>
</select>
</div>
<div class="field">
<i18next tag="label" path="Results per page"/>
<label>{{ $gettext('Results per page') }}</label>
<select class="ui dropdown" v-model="paginateBy">
<option :value="parseInt(12)">12</option>
<option :value="parseInt(25)">25</option>

View File

@ -11,18 +11,25 @@
<div class="content">
{{ track.title }}
<div class="sub header">
<i18next path="From album {%0%} by {%1%}">
<router-link :to="{name: 'library.albums.detail', params: {id: track.album.id }}">
{{ track.album.title }}
</router-link><router-link :to="{name: 'library.artists.detail', params: {id: track.artist.id }}">
{{ track.artist.name }}
</router-link>
</i18next>
<translate :translate-params="{album: track.album.title, artist: track.artist.name}">
From album %{ album } by %{ artist }
</translate>
</div>
<br>
<div class="ui basic buttons">
<router-link class="ui button" :to="{name: 'library.albums.detail', params: {id: track.album.id }}">
{{ $gettext('Album page') }}
</router-link>
<router-link class="ui button" :to="{name: 'library.artists.detail', params: {id: track.artist.id }}">
{{ $gettext('Artist page') }}
</router-link>
</div>
</div>
</h2>
<play-button class="orange" :track="track"><i18next path="Play"/></play-button>
<play-button class="orange" :track="track">
{{ $gettext('Play') }}
</play-button>
<track-favorite-icon :track="track" :button="true"></track-favorite-icon>
<track-playlist-icon
:button="true"
@ -31,70 +38,72 @@
<a :href="wikipediaUrl" target="_blank" class="ui button">
<i class="wikipedia icon"></i>
<i18next path="Search on Wikipedia"/>
{{ $gettext('Search on Wikipedia') }}
</a>
<a :href="musicbrainzUrl" target="_blank" class="ui button">
<i class="external icon"></i>
<i18next path="View on MusicBrainz"/>
{{ $gettext('View on MusicBrainz') }}
</a>
<a v-if="downloadUrl" :href="downloadUrl" target="_blank" class="ui button">
<i class="download icon"></i>
<i18next path="Download"/>
{{ $gettext('Download') }}
</a>
</div>
</div>
<div v-if="file" class="ui vertical stripe center aligned segment">
<h2 class="ui header">{{ $t('Track information') }}</h2>
<h2 class="ui header">{{ $gettext('Track information') }}</h2>
<table class="ui very basic collapsing celled center aligned table">
<tbody>
<tr>
<td>
{{ $t('Duration') }}
{{ $gettext('Duration') }}
</td>
<td v-if="file.duration">
{{ time.parse(file.duration) }}
</td>
<td v-else>
{{ $t('N/A') }}
{{ $gettext('N/A') }}
</td>
</tr>
<tr>
<td>
{{ $t('Size') }}
{{ $gettext('Size') }}
</td>
<td v-if="file.size">
{{ file.size | humanSize }}
</td>
<td v-else>
{{ $t('N/A') }}
{{ $gettext('N/A') }}
</td>
</tr>
<tr>
<td>
{{ $t('Bitrate') }}
{{ $gettext('Bitrate') }}
</td>
<td v-if="file.bitrate">
{{ file.bitrate | humanSize }}/s
</td>
<td v-else>
{{ $t('N/A') }}
{{ $gettext('N/A') }}
</td>
</tr>
</tbody>
</table>
</div>
<div class="ui vertical stripe center aligned segment">
<h2><i18next path="Lyrics"/></h2>
<h2>
{{ $gettext('Lyrics') }}
</h2>
<div v-if="isLoadingLyrics" class="ui vertical segment">
<div :class="['ui', 'centered', 'active', 'inline', 'loader']"></div>
</div>
<div v-if="lyrics" v-html="lyrics.content_rendered">
</div>
<template v-if="!isLoadingLyrics & !lyrics">
<i18next tag="p" path="No lyrics available for this track."/>
<p>{{ $gettext('No lyrics available for this track.') }}</p>
<a class="ui button" target="_blank" :href="lyricsSearchUrl">
<i class="search icon"></i>
<i18next path="Search on lyrics.wikia.com"/>
{{ $gettext('Search on lyrics.wikia.com') }}
</a>
</template>
</div>

View File

@ -4,7 +4,9 @@
<a :href="getMusicbrainzUrl('artist', metadata.id)" target="_blank" title="View on MusicBrainz">{{ metadata.name }}</a>
</h3>
<form class="ui form" @submit.prevent="">
<h6 class="ui header"><i18next path="Filter album types"/></h6>
<h6 class="ui header">
{{ $gettext('Filter album types') }}
</h6>
<div class="inline fields">
<div class="field" v-for="t in availableReleaseTypes">
<div class="ui checkbox">
@ -13,7 +15,7 @@
</div>
</div>
<div class="field">
<i18next tag="label" path="Query template"/>
<label>{{ $gettext('Query template') }}</label>
<input v-model="customQueryTemplate" />
</div>
</div>

View File

@ -8,7 +8,7 @@
<tbody>
<tr>
<td>
<strong>{{ $t('Import batch') }}</strong>
<strong>{{ $gettext('Import batch') }}</strong>
</td>
<td>
#{{ batch.id }}
@ -16,7 +16,7 @@
</tr>
<tr>
<td>
<strong>{{ $t('Launch date') }}</strong>
<strong>{{ $gettext('Launch date') }}</strong>
</td>
<td>
<human-date :date="batch.creation_date"></human-date>
@ -24,22 +24,22 @@
</tr>
<tr v-if="batch.user">
<td>
<strong>{{ $t('Submitted by') }}</strong>
<strong>{{ $gettext('Submitted by') }}</strong>
</td>
<td>
<username :username="batch.user.username" />
</td>
</tr>
<tr v-if="stats">
<td><strong>{{ $t('Pending') }}</strong></td>
<td><strong>{{ $gettext('Pending') }}</strong></td>
<td>{{ stats.pending }}</td>
</tr>
<tr v-if="stats">
<td><strong>{{ $t('Skipped') }}</strong></td>
<td><strong>{{ $gettext('Skipped') }}</strong></td>
<td>{{ stats.skipped }}</td>
</tr>
<tr v-if="stats">
<td><strong>{{ $t('Errored') }}</strong></td>
<td><strong>{{ $gettext('Errored') }}</strong></td>
<td>
{{ stats.errored }}
<button
@ -47,12 +47,12 @@
v-if="stats.errored > 0"
class="ui tiny basic icon button">
<i class="redo icon" />
{{ $t('Rerun errored jobs')}}
{{ $gettext('Rerun errored jobs')}}
</button>
</td>
</tr>
<tr v-if="stats">
<td><strong>{{ $t('Finished') }}</strong></td>
<td><strong>{{ $gettext('Finished') }}</strong></td>
<td>{{ stats.finished }}/{{ stats.count}}</td>
</tr>
</tbody>
@ -60,17 +60,17 @@
<div class="ui inline form">
<div class="fields">
<div class="ui field">
<label>{{ $t('Search') }}</label>
<label>{{ $gettext('Search') }}</label>
<input type="text" v-model="jobFilters.search" placeholder="Search by source..." />
</div>
<div class="ui field">
<label>{{ $t('Status') }}</label>
<label>{{ $gettext('Status') }}</label>
<select class="ui dropdown" v-model="jobFilters.status">
<option :value="null">{{ $t('Any') }}</option>
<option :value="'pending'">{{ $t('Pending') }}</option>
<option :value="'errored'">{{ $t('Errored') }}</option>
<option :value="'finished'">{{ $t('Success') }}</option>
<option :value="'skipped'">{{ $t('Skipped') }}</option>
<option :value="null">{{ $gettext('Any') }}</option>
<option :value="'pending'">{{ $gettext('Pending') }}</option>
<option :value="'errored'">{{ $gettext('Errored') }}</option>
<option :value="'finished'">{{ $gettext('Success') }}</option>
<option :value="'skipped'">{{ $gettext('Skipped') }}</option>
</select>
</div>
</div>
@ -78,11 +78,11 @@
<table v-if="jobResult" class="ui unstackable table">
<thead>
<tr>
<th>{{ $t('Job ID') }}</th>
<th>{{ $t('Recording MusicBrainz ID') }}</th>
<th>{{ $t('Source') }}</th>
<th>{{ $t('Status') }}</th>
<th>{{ $t('Track') }}</th>
<th>{{ $gettext('Job ID') }}</th>
<th>{{ $gettext('Recording MusicBrainz ID') }}</th>
<th>{{ $gettext('Source') }}</th>
<th>{{ $gettext('Status') }}</th>
<th>{{ $gettext('Track') }}</th>
</tr>
</thead>
<tbody>
@ -103,7 +103,7 @@
<button
@click="rerun({batches: [], jobs: [job.id]})"
v-if="job.status === 'errored'"
:title="$t('Rerun job')"
:title="$gettext('Rerun job')"
class="ui tiny basic icon button">
<i class="redo icon" />
</button>
@ -126,7 +126,10 @@
></pagination>
</th>
<th v-if="jobResult && jobResult.results.length > 0">
{{ $t('Showing results {%start%}-{%end%} on {%total%}', {start: ((jobFilters.page-1) * jobFilters.paginateBy) + 1 , end: ((jobFilters.page-1) * jobFilters.paginateBy) + jobResult.results.length, total: jobResult.count})}}
<translate
:translate-params="{start: ((jobFilters.page-1) * jobFilters.paginateBy) + 1, end: ((jobFilters.page-1) * jobFilters.paginateBy) + jobResult.results.length, total: jobResult.count}">
Showing results %{ start }-%{ end } on %{ total }
</translate>
<th>
<th></th>
<th></th>

View File

@ -5,25 +5,25 @@
<div class="ui inline form">
<div class="fields">
<div class="ui field">
<label>{{ $t('Search') }}</label>
<label>{{ $gettext('Search') }}</label>
<input type="text" v-model="filters.search" placeholder="Search by submitter, source..." />
</div>
<div class="ui field">
<label>{{ $t('Status') }}</label>
<label>{{ $gettext('Status') }}</label>
<select class="ui dropdown" v-model="filters.status">
<option :value="null">{{ $t('Any') }}</option>
<option :value="'pending'">{{ $t('Pending') }}</option>
<option :value="'errored'">{{ $t('Errored') }}</option>
<option :value="'finished'">{{ $t('Success') }}</option>
<option :value="null">{{ $gettext('Any') }}</option>
<option :value="'pending'">{{ $gettext('Pending') }}</option>
<option :value="'errored'">{{ $gettext('Errored') }}</option>
<option :value="'finished'">{{ $gettext('Success') }}</option>
</select>
</div>
<div class="ui field">
<label>{{ $t('Import source') }}</label>
<label>{{ $gettext('Import source') }}</label>
<select class="ui dropdown" v-model="filters.source">
<option :value="null">{{ $t('Any') }}</option>
<option :value="'shell'">{{ $t('CLI') }}</option>
<option :value="'api'">{{ $t('API') }}</option>
<option :value="'federation'">{{ $t('Federation') }}</option>
<option :value="null">{{ $gettext('Any') }}</option>
<option :value="'shell'">{{ $gettext('CLI') }}</option>
<option :value="'api'">{{ $gettext('API') }}</option>
<option :value="'federation'">{{ $gettext('Federation') }}</option>
</select>
</div>
</div>
@ -32,12 +32,12 @@
<table v-if="result && result.results.length > 0" class="ui unstackable table">
<thead>
<tr>
<th>{{ $t('ID') }}</th>
<th>{{ $t('Launch date') }}</th>
<th>{{ $t('Jobs') }}</th>
<th>{{ $t('Status') }}</th>
<th>{{ $t('Source') }}</th>
<th>{{ $t('Submitted by') }}</th>
<th>{{ $gettext('ID') }}</th>
<th>{{ $gettext('Launch date') }}</th>
<th>{{ $gettext('Jobs') }}</th>
<th>{{ $gettext('Status') }}</th>
<th>{{ $gettext('Source') }}</th>
<th>{{ $gettext('Submitted by') }}</th>
</tr>
</thead>
<tbody>
@ -71,7 +71,10 @@
></pagination>
</th>
<th v-if="result && result.results.length > 0">
{{ $t('Showing results {%start%}-{%end%} on {%total%}', {start: ((filters.page-1) * filters.paginateBy) + 1 , end: ((filters.page-1) * filters.paginateBy) + result.results.length, total: result.count})}}
<translate
:translate-params="{start: ((filters.page-1) * filters.paginateBy) + 1, end: ((filters.page-1) * filters.paginateBy) + result.results.length, total: result.count}">
Showing results %{ start }-%{ end } on %{ total }
</translate>
<th>
<th></th>
<th></th>

View File

@ -2,8 +2,8 @@
<div>
<div v-if="batch" class="ui container">
<div class="ui message">
{{ $t('Ensure your music files are properly tagged before uploading them.') }}
<a href="http://picard.musicbrainz.org/" target='_blank'>{{ $t('We recommend using Picard for that purpose.') }}</a>
{{ $gettext('Ensure your music files are properly tagged before uploading them.') }}
<a href="http://picard.musicbrainz.org/" target='_blank'>{{ $gettext('We recommend using Picard for that purpose.') }}</a>
</div>
<file-upload-widget
:class="['ui', 'icon', 'left', 'floated', 'button']"
@ -20,31 +20,30 @@
@input-file="inputFile"
ref="upload">
<i class="upload icon"></i>
<i18next path="Select files to upload..."/>
{{ $gettext('Select files to upload...') }}
</file-upload-widget>
<button
:class="['ui', 'right', 'floated', 'icon', {disabled: files.length === 0}, 'button']"
v-if="!$refs.upload || !$refs.upload.active" @click.prevent="startUpload()">
<i class="play icon" aria-hidden="true"></i>
<i18next path="Start Upload"/>
{{ $gettext('Start Upload') }}
</button>
<button type="button" class="ui right floated icon yellow button" v-else @click.prevent="$refs.upload.active = false">
<i class="pause icon" aria-hidden="true"></i>
<i18next path="Stop Upload"/>
{{ $gettext('Stop Upload') }}
</button>
</div>
<div class="ui hidden clearing divider"></div>
<i18next v-if="batch" path="Once all your files are uploaded, simply head over {%0%} to check the import status.">
<router-link :to="{name: 'library.import.batches.detail', params: {id: batch.id }}">
<i18next path="import detail page"/>
</router-link>
</i18next>
<template v-if="batch">{{ $gettext('Once all your files are uploaded, simply click the following button to check the import status.') }}</template>
<router-link class="ui basic button" v-if="batch" :to="{name: 'library.import.batches.detail', params: {id: batch.id }}">
{{ $gettext('Import detail page') }}
</router-link>
<table class="ui single line table">
<thead>
<tr>
<i18next tag="th" path="File name"/>
<i18next tag="th" path="Size"/>
<i18next tag="th" path="Status"/>
<th>{{ $gettext('File name') }}</th>
<th>{{ $gettext('Size') }}</th>
<th>{{ $gettext('Status') }}</th>
</tr>
</thead>
<tbody>
@ -55,10 +54,10 @@
<span v-if="file.error" class="ui red label">
{{ file.error }}
</span>
<i18next v-else-if="file.success" class="ui green label" path="Success"/>
<i18next v-else-if="file.active" class="ui yellow label" path="Uploading..."/>
<span v-else-if="file.success" class="ui green label">{{ $gettext('Success') }}</span>
<span v-else-if="file.active" class="ui yellow label">{{ $gettext('Uploading...') }}</span>
<template v-else>
<i18next class="ui label" path="Pending"/>
<span class="ui label">{{ $gettext('Pending') }}</span>
<button class="ui tiny basic red icon button" @click.prevent="$refs.upload.remove(file)"><i class="delete icon"></i></button>
</template>
</td>

View File

@ -4,34 +4,44 @@
<div class="ui top three attached ordered steps">
<a @click="currentStep = 0" :class="['step', {'active': currentStep === 0}, {'completed': currentStep > 0}]">
<div class="content">
<i18next tag="div" class="title" path="Import source"/>
<i18next tag="div" class="description" path="Uploaded files or external source"/>
<div class="title">{{ $gettext('Import source') }}</div>
<div class="description">{{ $gettext('Uploaded files or external source') }}</div>
</div>
</a>
<a @click="currentStep = 1" :class="['step', {'active': currentStep === 1}, {'completed': currentStep > 1}]">
<div class="content">
<i18next tag="div" class="title" path="Metadata"/>
<i18next tag="div" class="description" path="Grab corresponding metadata"/>
<div class="title">{{ $gettext('Metadata') }}</div>
<div class="description">{{ $gettext('Grab corresponding metadata') }}</div>
</div>
</a>
<a @click="currentStep = 2" :class="['step', {'active': currentStep === 2}, {'completed': currentStep > 2}]">
<div class="content">
<i18next tag="div" class="title" path="Music"/>
<i18next tag="div" class="description" path="Select relevant sources or files for import"/>
<div class="title">{{ $gettext('Music') }}</div>
<div class="description">{{ $gettext('Select relevant sources or files for import') }}</div>
</div>
</a>
</div>
<div class="ui hidden divider"></div>
<div class="ui centered buttons">
<button @click="currentStep -= 1" :disabled="currentStep === 0" class="ui icon button"><i class="left arrow icon"></i><i18next path="Previous step"/></button>
<button @click="nextStep()" v-if="currentStep < 2" class="ui icon button"><i18next path="Next step"/><i class="right arrow icon"></i></button>
<button @click="currentStep -= 1" :disabled="currentStep === 0" class="ui icon button"><i class="left arrow icon"></i>
{{ $gettext('Previous step') }}
</button>
<button @click="nextStep()" v-if="currentStep < 2" class="ui icon button">
{{ $gettext('Next step') }}
<i class="right arrow icon"></i>
</button>
<button
@click="$refs.import.launchImport()"
v-if="currentStep === 2 && currentSource != 'upload'"
:class="['ui', 'positive', 'icon', {'loading': isImporting}, 'button']"
:disabled="isImporting || importData.count === 0"
>
<i18next path="Import {%0%} tracks">{{ importData.count }}</i18next>
<translate
:translate-params="{count: importData.count || 0}"
:translate-n="importData.count || 0"
translate-plural="Import %{ count } tracks">
Import %{ count } track
</translate>
<i class="check icon"></i>
</button>
<button
@ -40,20 +50,20 @@
:class="['ui', 'positive', 'icon', {'disabled': !importBatch}, 'button']"
:disabled="!importBatch"
>
{{ $t('Finish import' )}}
{{ $gettext('Finish import' )}}
<i class="check icon"></i>
</button>
</div>
<div class="ui hidden divider"></div>
<div class="ui attached segment">
<template v-if="currentStep === 0">
<i18next tag="p" path="First, choose where you want to import the music from"/>
<p>{{ $gettext('First, choose where you want to import the music from') }}</p>
<form class="ui form">
<div class="field">
<div class="ui radio checkbox">
<input type="radio" id="external" value="external" v-model="currentSource">
<label for="external">
<i18next path="External source. Supported backends"/>
{{ $gettext('External source. Supported backends') }}
<div v-for="backend in backends" class="ui basic label">
<i v-if="backend.icon" :class="[backend.icon, 'icon']"></i>
{{ backend.label }}
@ -64,7 +74,7 @@
<div class="field">
<div class="ui radio checkbox">
<input type="radio" id="upload" value="upload" v-model="currentSource">
<i18next tag="label" for="upload" path="File upload" />
<label for="upload">{{ $gettext('File upload') }}</label>
</div>
</div>
</form>
@ -73,7 +83,7 @@
<div class="column">
<form class="ui form" @submit.prevent="">
<div class="field">
<i18next tag="label" path="Search an entity you want to import:"/>
<label>{{ $gettext('Search an entity you want to import:') }}</label>
<metadata-search
:mb-type="mbType"
:mb-id="mbId"
@ -81,29 +91,35 @@
@type-changed="updateType"></metadata-search>
</div>
</form>
<i18next tag="div" class="ui horizontal divider" path="Or"/>
<div class="ui horizontal divider">{{ $gettext('Or') }}</div>
<form class="ui form" @submit.prevent="">
<div class="field">
<i18next tag="label" path="Input a MusicBrainz ID manually:"/>
<label>{{ $gettext('Input a MusicBrainz ID manually:') }}</label>
<input type="text" v-model="currentId" />
</div>
</form>
<div class="ui hidden divider"></div>
<template v-if="currentType && currentId">
<h4 class="ui header"><i18next path="You will import:"/></h4>
<h4 class="ui header">
{{ $gettext('You will import:') }}
</h4>
<component
:mbId="currentId"
:is="metadataComponent"
@metadata-changed="this.updateMetadata"
></component>
</template>
<i18next tag="p" path="You can also skip this step and enter metadata manually."/>
<p>{{ $gettext('You can also skip this step and enter metadata manually.') }}</p>
</div>
<div class="column">
<h5 class="ui header">What is metadata?</h5>
<i18next tag="p" path="Metadata is the data related to the music you want to import. This includes all the information about the artists, albums and tracks. In order to have a high quality library, it is recommended to grab data from the {%0%} project, which you can think about as the Wikipedia of music.">
<a href="http://musicbrainz.org/" target="_blank">MusicBrainz</a>
</i18next>
<template v-translate>
Metadata is the data related to the music you want to import. This includes all the information about the artists, albums and tracks. In order to have a high quality library, it is recommended to grab data from the
<a href="https://musicbrainz.org" target="_blank">
MusicBrainz
</a>
project, which you can think about as the Wikipedia of music.
</template>
</div>
</div>
<div v-if="currentStep === 2">
@ -128,8 +144,10 @@
</div>
</div>
<div class="ui vertical stripe segment" v-if="currentRequest">
<h3 class="ui header"><i18next path="Music request"/></h3>
<i18next tag="p" path="This import will be associated with the music request below. After the import is finished, the request will be marked as fulfilled."/>
<h3 class="ui header">
{{ $gettext('Music request') }}
</h3>
<p>{{ $gettext('This import will be associated with the music request below. After the import is finished, the request will be marked as fulfilled.') }}</p>
<request-card :request="currentRequest" :import-action="false"></request-card>
</div>

View File

@ -1,16 +1,18 @@
<template>
<div>
<h3 class="ui dividing block header">
<i18next path="Album {%0%} ({%1%} tracks) by {%2%}">
<a :href="getMusicbrainzUrl('release', metadata.id)" target="_blank" title="View on MusicBrainz">{{ metadata.title }}</a>
({{ tracks.length}} tracks)
<a :href="getMusicbrainzUrl('artist', metadata['artist-credit'][0]['artist']['id'])" target="_blank" title="View on MusicBrainz">{{ metadata['artist-credit-phrase'] }}</a>
</i18next>
<translate
tag="div"
translate-plural="Album %{ title } (%{ count } tracks) by %{ artist }"
:translate-n="tracks.length"
:translate-params="{count: tracks.length, title: metadata.title, artist: metadata['artist-credit-phrase']}">
Album %{ title } (%{ count } track) by %{ artist }
</translate>
<div class="ui divider"></div>
<div class="sub header">
<div class="ui toggle checkbox">
<input type="checkbox" v-model="enabled" />
<i18next tag="label" path="Import this release"/>
<label>{{ $gettext('Import this release') }}</label>
</div>
</div>
</h3>

View File

@ -9,13 +9,13 @@
</h5>
<div class="ui toggle checkbox">
<input type="checkbox" v-model="enabled" />
<i18next tag="label" path="Import this track"/>
<label>{{ $gettext('Import this track') }}</label>
</div>
</div>
<div class="three wide column" v-if="enabled">
<form class="ui mini form" @submit.prevent="">
<div class="field">
<i18next tag="label" path="Source"/>
<label>{{ $gettext('Source') }}</label>
<select v-model="currentBackendId">
<option v-for="backend in backends" :value="backend.id">
{{ backend.label }}
@ -28,10 +28,10 @@
<button @click="currentResultIndex -= 1" class="ui basic tiny icon button" :disabled="currentResultIndex === 0">
<i class="left arrow icon"></i>
</button>
<i18next path="Result {%0%}/{%1%}">
{{ currentResultIndex + 1 }}
{{ results.length }}
</i18next>
{{ results.total }}
<translate :translate-params="{current: currentResultIndex + 1, total: results.length}">
Result %{ current }/%{ total }
</translate>
<button @click="currentResultIndex += 1" class="ui basic tiny icon button" :disabled="currentResultIndex + 1 === results.length">
<i class="right arrow icon"></i>
</button>
@ -40,9 +40,9 @@
<div class="four wide column" v-if="enabled">
<form class="ui mini form" @submit.prevent="">
<div class="field">
<i18next tag="label" path="Search query"/>
<label>{{ $gettext('Search query') }}</label>
<input type="text" v-model="query" />
<i18next tag="label" path="Imported URL"/>
<label>{{ $gettext('Imported URL') }}</label>
<input type="text" v-model="importedUrl" />
</div>
</form>

View File

@ -2,30 +2,40 @@
<div class="ui vertical stripe segment" v-title="'Radio Builder'">
<div>
<div>
<h2 class="ui header"><i18next path="Builder"/></h2>
<i18next tag="p" path="You can use this interface to build your own custom radio, which will play tracks according to your criteria"/>
<h2 class="ui header">
{{ $gettext('Builder') }}
</h2>
<p>{{ $gettext('You can use this interface to build your own custom radio, which will play tracks according to your criteria.') }}</p>
<div class="ui form">
<div class="inline fields">
<div class="field">
<i18next tag="label" for="name" path="Radio name"/>
<label for="name">{{ $gettext('Radio name') }}</label>
<input id="name" type="text" v-model="radioName" placeholder="My awesome radio" />
</div>
<div class="field">
<input id="public" type="checkbox" v-model="isPublic" />
<i18next tag="label" for="public" path="Display publicly"/>
<label for="public">{{ $gettext('Display publicly') }}</label>
</div>
<button :disabled="!canSave" @click="save" class="ui green button"><i18next path="Save"/></button>
<button :disabled="!canSave" @click="save" class="ui green button">
{{ $gettext('Save') }}
</button>
<radio-button v-if="id" type="custom" :custom-radio-id="id"></radio-button>
</div>
</div>
<div class="ui form">
<p><i18next path="Add filters to customize your radio"/></p>
<p>
{{ $gettext('Add filters to customize your radio') }}
</p>
<div class="inline field">
<select class="ui dropdown" v-model="currentFilterType">
<option value=""><i18next path="Select a filter"/></option>
<option value="">
{{ $gettext('Select a filter') }}
</option>
<option v-for="f in availableFilters" :value="f.type">{{ f.label }}</option>
</select>
<button :disabled="!currentFilterType" @click="add" class="ui button"><i18next path="Add filter"/></button>
<button :disabled="!currentFilterType" @click="add" class="ui button">
{{ $gettext('Add filter') }}
</button>
</div>
<p v-if="currentFilter">
{{ currentFilter.help_text }}
@ -34,11 +44,11 @@
<table class="ui table">
<thead>
<tr>
<i18next tag="th" class="two wide" path="Filter name"/>
<i18next tag="th" class="one wide" path="Exclude"/>
<i18next tag="th" class="six wide" path="Config"/>
<i18next tag="th" class="five wide" path="Candidates"/>
<i18next tag="th" class="two wide" path="Actions"/>
<th class="two wide">{{ $gettext('Filter name') }}</th>
<th class="one wide">{{ $gettext('Exclude') }}</th>
<th class="six wide">{{ $gettext('Config') }}</th>
<th class="five wide">{{ $gettext('Candidates') }}</th>
<th class="two wide">{{ $gettext('Actions') }}</th>
</tr>
</thead>
<tbody>
@ -54,9 +64,13 @@
</tbody>
</table>
<template v-if="checkResult">
<i18next tag="h3" class="ui header" path="{%0%} tracks matching combined filters">
{{ checkResult.candidates.count }}
</i18next>
<h3
class="ui header"
v-translate="{count: checkResult.candidates.count}"
:translate-n="checkResult.candidates.count"
translate-plural="%{ count } tracks matching combined filters">
%{ count } track matching combined filters
</h3>
<track-table v-if="checkResult.candidates.sample" :tracks="checkResult.candidates.sample"></track-table>
</template>
</div>

View File

@ -42,7 +42,7 @@
</span>
<modal v-if="checkResult" :show.sync="showCandidadesModal">
<div class="header">
<i18next path="Track matching filter"/>
{{ $gettext('Track matching filter') }}
</div>
<div class="content">
<div class="description">
@ -51,13 +51,13 @@
</div>
<div class="actions">
<div class="ui black deny button">
<i18next path="Cancel"/>
{{ $gettext('Cancel') }}
</div>
</div>
</modal>
</td>
<td>
<button @click="$emit('delete', index)" class="ui basic red button"><i18next path="Remove"/></button>
<button @click="$emit('delete', index)" class="ui basic red button">{{ $gettext('Remove') }}</button>
</td>
</tr>
</template>

View File

@ -3,11 +3,11 @@
<div class="ui inline form">
<div class="fields">
<div class="ui field">
<label>{{ $t('Search') }}</label>
<label>{{ $gettext('Search') }}</label>
<input type="text" v-model="search" placeholder="Search by title, artist, domain..." />
</div>
<div class="field">
<i18next tag="label" path="Ordering"/>
<label>{{ $gettext('Ordering') }}</label>
<select class="ui dropdown" v-model="ordering">
<option v-for="option in orderingOptions" :value="option[0]">
{{ option[1] }}
@ -15,7 +15,7 @@
</select>
</div>
<div class="field">
<i18next tag="label" path="Ordering direction"/>
<label>{{ $gettext('Ordering direction') }}</label>
<select class="ui dropdown" v-model="orderingDirection">
<option value="+">Ascending</option>
<option value="-">Descending</option>
@ -35,14 +35,14 @@
:action-url="'manage/library/track-files/action/'"
:filters="actionFilters">
<template slot="header-cells">
<th>{{ $t('Title') }}</th>
<th>{{ $t('Artist') }}</th>
<th>{{ $t('Album') }}</th>
<th>{{ $t('Import date') }}</th>
<th>{{ $t('Type') }}</th>
<th>{{ $t('Bitrate') }}</th>
<th>{{ $t('Duration') }}</th>
<th>{{ $t('Size') }}</th>
<th>{{ $gettext('Title') }}</th>
<th>{{ $gettext('Artist') }}</th>
<th>{{ $gettext('Album') }}</th>
<th>{{ $gettext('Import date') }}</th>
<th>{{ $gettext('Type') }}</th>
<th>{{ $gettext('Bitrate') }}</th>
<th>{{ $gettext('Duration') }}</th>
<th>{{ $gettext('Size') }}</th>
</template>
<template slot="row-cells" slot-scope="scope">
<td>
@ -61,25 +61,25 @@
{{ scope.obj.audio_mimetype }}
</td>
<td v-else>
{{ $t('N/A') }}
{{ $gettext('N/A') }}
</td>
<td v-if="scope.obj.bitrate">
{{ scope.obj.bitrate | humanSize }}/s
</td>
<td v-else>
{{ $t('N/A') }}
{{ $gettext('N/A') }}
</td>
<td v-if="scope.obj.duration">
{{ time.parse(scope.obj.duration) }}
</td>
<td v-else>
{{ $t('N/A') }}
{{ $gettext('N/A') }}
</td>
<td v-if="scope.obj.size">
{{ scope.obj.size | humanSize }}
</td>
<td v-else>
{{ $t('N/A') }}
{{ $gettext('N/A') }}
</td>
</template>
</action-table>
@ -95,7 +95,10 @@
></pagination>
<span v-if="result && result.results.length > 0">
{{ $t('Showing results {%start%}-{%end%} on {%total%}', {start: ((page-1) * paginateBy) + 1 , end: ((page-1) * paginateBy) + result.results.length, total: result.count})}}
<translate
:translate-params="{start: ((page-1) * paginateBy) + 1, end: ((page-1) * paginateBy) + result.results.length, total: result.count}">
Showing results %{ start }-%{ end } on %{ total }
</translate>
</span>
</div>
</div>
@ -178,10 +181,11 @@ export default {
}
},
actions () {
let msg = this.$gettext('Delete')
return [
{
name: 'delete',
label: this.$t('Delete'),
label: msg,
isDangerous: true
}
]

View File

@ -3,11 +3,11 @@
<div class="ui inline form">
<div class="fields">
<div class="ui field">
<label>{{ $t('Search') }}</label>
<label>{{ $gettext('Search') }}</label>
<input type="text" v-model="search" placeholder="Search by artist, username, comment..." />
</div>
<div class="field">
<i18next tag="label" path="Ordering"/>
<label>{{ $gettext('Ordering') }}</label>
<select class="ui dropdown" v-model="ordering">
<option v-for="option in orderingOptions" :value="option[0]">
{{ option[1] }}
@ -15,20 +15,20 @@
</select>
</div>
<div class="field">
<i18next tag="label" path="Ordering direction"/>
<label>{{ $gettext('Ordering direction') }}</label>
<select class="ui dropdown" v-model="orderingDirection">
<option value="+">Ascending</option>
<option value="-">Descending</option>
</select>
</div>
<div class="field">
<label>{{ $t("Status") }}</label>
<label>{{ $gettext("Status") }}</label>
<select class="ui dropdown" v-model="status">
<option :value="null">{{ $t('All') }}</option>
<option :value="'pending'">{{ $t('Pending') }}</option>
<option :value="'accepted'">{{ $t('Accepted') }}</option>
<option :value="'imported'">{{ $t('Imported') }}</option>
<option :value="'closed'">{{ $t('Closed') }}</option>
<option :value="null">{{ $gettext('All') }}</option>
<option :value="'pending'">{{ $gettext('Pending') }}</option>
<option :value="'accepted'">{{ $gettext('Accepted') }}</option>
<option :value="'imported'">{{ $gettext('Imported') }}</option>
<option :value="'closed'">{{ $gettext('Closed') }}</option>
</select>
</div>
</div>
@ -45,48 +45,48 @@
:action-url="'manage/requests/import-requests/action/'"
:filters="actionFilters">
<template slot="header-cells">
<th>{{ $t('User') }}</th>
<th>{{ $t('Status') }}</th>
<th>{{ $t('Artist') }}</th>
<th>{{ $t('Albums') }}</th>
<th>{{ $t('Comment') }}</th>
<th>{{ $t('Creation date') }}</th>
<th>{{ $t('Import date') }}</th>
<th>{{ $t('Actions') }}</th>
<th>{{ $gettext('User') }}</th>
<th>{{ $gettext('Status') }}</th>
<th>{{ $gettext('Artist') }}</th>
<th>{{ $gettext('Albums') }}</th>
<th>{{ $gettext('Comment') }}</th>
<th>{{ $gettext('Creation date') }}</th>
<th>{{ $gettext('Import date') }}</th>
<th>{{ $gettext('Actions') }}</th>
</template>
<template slot="row-cells" slot-scope="scope">
<td>
{{ scope.obj.user.username }}
</td>
<td>
<span class="ui green basic label" v-if="scope.obj.status === 'imported'">{{ $t('Imported') }}</span>
<span class="ui pink basic label" v-else-if="scope.obj.status === 'accepted'">{{ $t('Accepted') }}</span>
<span class="ui yellow basic label" v-else-if="scope.obj.status === 'pending'">{{ $t('Pending') }}</span>
<span class="ui red basic label" v-else-if="scope.obj.status === 'closed'">{{ $t('Closed') }}</span>
<span class="ui green basic label" v-if="scope.obj.status === 'imported'">{{ $gettext('Imported') }}</span>
<span class="ui pink basic label" v-else-if="scope.obj.status === 'accepted'">{{ $gettext('Accepted') }}</span>
<span class="ui yellow basic label" v-else-if="scope.obj.status === 'pending'">{{ $gettext('Pending') }}</span>
<span class="ui red basic label" v-else-if="scope.obj.status === 'closed'">{{ $gettext('Closed') }}</span>
</td>
<td>
<span :title="scope.obj.artist_name">{{ scope.obj.artist_name|truncate(30) }}</span>
</td>
<td>
<span v-if="scope.obj.albums" :title="scope.obj.albums">{{ scope.obj.albums|truncate(30) }}</span>
<template v-else>{{ $t('N/A') }}</template>
<template v-else>{{ $gettext('N/A') }}</template>
</td>
<td>
<span v-if="scope.obj.comment" :title="scope.obj.comment">{{ scope.obj.comment|truncate(30) }}</span>
<template v-else>{{ $t('N/A') }}</template>
<template v-else>{{ $gettext('N/A') }}</template>
</td>
<td>
<human-date :date="scope.obj.creation_date"></human-date>
</td>
<td>
<human-date v-if="scope.obj.imported_date" :date="scope.obj.creation_date"></human-date>
<template v-else>{{ $t('N/A') }}</template>
<template v-else>{{ $gettext('N/A') }}</template>
</td>
<td>
<router-link
class="ui tiny basic button"
:to="{name: 'library.import.launch', query: {request: scope.obj.id}}"
v-if="scope.obj.status === 'pending'">{{ $t('Create import') }}</router-link>
v-if="scope.obj.status === 'pending'">{{ $gettext('Create import') }}</router-link>
</td>
</template>
</action-table>
@ -102,7 +102,10 @@
></pagination>
<span v-if="result && result.results.length > 0">
{{ $t('Showing results {%start%}-{%end%} on {%total%}', {start: ((page-1) * paginateBy) + 1 , end: ((page-1) * paginateBy) + result.results.length, total: result.count})}}
<translate
:translate-params="{start: ((page-1) * paginateBy) + 1, end: ((page-1) * paginateBy) + result.results.length, total: result.count}">
Showing results %{ start }-%{ end } on %{ total }
</translate>
</span>
</div>
</div>
@ -183,21 +186,25 @@ export default {
}
},
actions () {
// somehow, extraction fails otherwise
let deleteLabel = this.$gettext('Delete')
let markImportedLabel = this.$gettext('Mark as imported')
let markClosedLabel = this.$gettext('Mark as closed')
return [
{
name: 'delete',
label: this.$t('Delete'),
label: deleteLabel,
isDangerous: true
},
{
name: 'mark_imported',
label: this.$t('Mark as imported'),
label: markImportedLabel,
filterCheckable: (obj) => { return ['pending', 'accepted'].indexOf(obj.status) > -1 },
isDangerous: true
},
{
name: 'mark_closed',
label: this.$t('Mark as closed'),
label: markClosedLabel,
filterCheckable: (obj) => { return ['pending', 'accepted'].indexOf(obj.status) > -1 },
isDangerous: true
}

View File

@ -2,19 +2,19 @@
<div>
<form class="ui form" @submit.prevent="submit">
<div v-if="errors.length > 0" class="ui negative message">
<div class="header">{{ $t('Error while creating invitation') }}</div>
<div class="header">{{ $gettext('Error while creating invitation') }}</div>
<ul class="list">
<li v-for="error in errors">{{ error }}</li>
</ul>
</div>
<div class="inline fields">
<div class="ui field">
<label>{{ $t('Invitation code')}}</label>
<input type="text" v-model="code" :placeholder="$t('Leave empty for a random code')" />
<label>{{ $gettext('Invitation code')}}</label>
<input type="text" v-model="code" :placeholder="$gettext('Leave empty for a random code')" />
</div>
<div class="ui field">
<button :class="['ui', {loading: isLoading}, 'button']" :disabled="isLoading" type="submit">
{{ $t('Get a new invitation') }}
{{ $gettext('Get a new invitation') }}
</button>
</div>
</div>
@ -24,8 +24,8 @@
<table class="ui ui basic table">
<thead>
<tr>
<th>{{ $t('Code') }}</th>
<th>{{ $t('Share link') }}</th>
<th>{{ $gettext('Code') }}</th>
<th>{{ $gettext('Share link') }}</th>
</tr>
</thead>
<tbody>
@ -35,7 +35,7 @@
</tr>
</tbody>
</table>
<button class="ui basic button" @click="invitations = []">{{ $t('Clear') }}</button>
<button class="ui basic button" @click="invitations = []">{{ $gettext('Clear') }}</button>
</div>
</div>
</template>

View File

@ -3,11 +3,11 @@
<div class="ui inline form">
<div class="fields">
<div class="ui field">
<label>{{ $t('Search') }}</label>
<label>{{ $gettext('Search') }}</label>
<input type="text" v-model="search" placeholder="Search by username, email, code..." />
</div>
<div class="field">
<label>{{ $t("Ordering") }}</label>
<label>{{ $gettext("Ordering") }}</label>
<select class="ui dropdown" v-model="ordering">
<option v-for="option in orderingOptions" :value="option[0]">
{{ option[1] }}
@ -15,11 +15,11 @@
</select>
</div>
<div class="field">
<label>{{ $t("Status") }}</label>
<label>{{ $gettext("Status") }}</label>
<select class="ui dropdown" v-model="isOpen">
<option :value="null">{{ $t('All') }}</option>
<option :value="true">{{ $t('Open') }}</option>
<option :value="false">{{ $t('Expired/used') }}</option>
<option :value="null">{{ $gettext('All') }}</option>
<option :value="true">{{ $gettext('Open') }}</option>
<option :value="false">{{ $gettext('Expired/used') }}</option>
</select>
</div>
</div>
@ -36,20 +36,20 @@
:action-url="'manage/users/invitations/action/'"
:filters="actionFilters">
<template slot="header-cells">
<th>{{ $t('Owner') }}</th>
<th>{{ $t('Status') }}</th>
<th>{{ $t('Creation date') }}</th>
<th>{{ $t('Expiration date') }}</th>
<th>{{ $t('Code') }}</th>
<th>{{ $gettext('Owner') }}</th>
<th>{{ $gettext('Status') }}</th>
<th>{{ $gettext('Creation date') }}</th>
<th>{{ $gettext('Expiration date') }}</th>
<th>{{ $gettext('Code') }}</th>
</template>
<template slot="row-cells" slot-scope="scope">
<td>
<router-link :to="{name: 'manage.users.users.detail', params: {id: scope.obj.id }}">{{ scope.obj.owner.username }}</router-link>
</td>
<td>
<span v-if="scope.obj.users.length > 0" class="ui green basic label">{{ $t('Used') }}</span>
<span v-else-if="moment().isAfter(scope.obj.expiration_date)" class="ui red basic label">{{ $t('Expired') }}</span>
<span v-else class="ui basic label">{{ $t('Not used') }}</span>
<span v-if="scope.obj.users.length > 0" class="ui green basic label">{{ $gettext('Used') }}</span>
<span v-else-if="moment().isAfter(scope.obj.expiration_date)" class="ui red basic label">{{ $gettext('Expired') }}</span>
<span v-else class="ui basic label">{{ $gettext('Not used') }}</span>
</td>
<td>
<human-date :date="scope.obj.creation_date"></human-date>
@ -74,7 +74,10 @@
></pagination>
<span v-if="result && result.results.length > 0">
{{ $t('Showing results {%start%}-{%end%} on {%total%}', {start: ((page-1) * paginateBy) + 1 , end: ((page-1) * paginateBy) + result.results.length, total: result.count})}}
<translate
:translate-params="{start: ((page-1) * paginateBy) + 1, end: ((page-1) * paginateBy) + result.results.length, total: result.count}">
Showing results %{ start }-%{ end } on %{ total }
</translate>
</span>
</div>
</div>
@ -155,10 +158,11 @@ export default {
}
},
actions () {
let deleteLabel = this.$gettext('Delete')
return [
{
name: 'delete',
label: this.$t('Delete'),
label: deleteLabel,
filterCheckable: (obj) => {
return obj.users.length === 0 && moment().isBefore(obj.expiration_date)
}

View File

@ -3,11 +3,11 @@
<div class="ui inline form">
<div class="fields">
<div class="ui field">
<label>{{ $t('Search') }}</label>
<label>{{ $gettext('Search') }}</label>
<input type="text" v-model="search" placeholder="Search by username, email, name..." />
</div>
<div class="field">
<i18next tag="label" path="Ordering"/>
<label>{{ $gettext('Ordering') }}</label>
<select class="ui dropdown" v-model="ordering">
<option v-for="option in orderingOptions" :value="option[0]">
{{ option[1] }}
@ -15,10 +15,10 @@
</select>
</div>
<div class="field">
<i18next tag="label" path="Ordering direction"/>
<label>{{ $gettext('Ordering direction') }}</label>
<select class="ui dropdown" v-model="orderingDirection">
<option value="+">{{ $t('Ascending') }}</option>
<option value="-">{{ $t('Descending') }}</option>
<option value="+">{{ $gettext('Ascending') }}</option>
<option value="-">{{ $gettext('Descending') }}</option>
</select>
</div>
</div>
@ -35,13 +35,13 @@
:action-url="'manage/library/track-files/action/'"
:filters="actionFilters">
<template slot="header-cells">
<th>{{ $t('Username') }}</th>
<th>{{ $t('Email') }}</th>
<th>{{ $t('Account status') }}</th>
<th>{{ $t('Sign-up') }}</th>
<th>{{ $t('Last activity') }}</th>
<th>{{ $t('Permissions') }}</th>
<th>{{ $t('Status') }}</th>
<th>{{ $gettext('Username') }}</th>
<th>{{ $gettext('Email') }}</th>
<th>{{ $gettext('Account status') }}</th>
<th>{{ $gettext('Sign-up') }}</th>
<th>{{ $gettext('Last activity') }}</th>
<th>{{ $gettext('Permissions') }}</th>
<th>{{ $gettext('Status') }}</th>
</template>
<template slot="row-cells" slot-scope="scope">
<td>
@ -51,15 +51,15 @@
<span>{{ scope.obj.email }}</span>
</td>
<td>
<span v-if="scope.obj.is_active" class="ui basic green label">{{ $t('Active') }}</span>
<span v-else class="ui basic grey label">{{ $t('Inactive') }}</span>
<span v-if="scope.obj.is_active" class="ui basic green label">{{ $gettext('Active') }}</span>
<span v-else class="ui basic grey label">{{ $gettext('Inactive') }}</span>
</td>
<td>
<human-date :date="scope.obj.date_joined"></human-date>
</td>
<td>
<human-date v-if="scope.obj.last_activity" :date="scope.obj.last_activity"></human-date>
<template v-else>{{ $t('N/A') }}</template>
<template v-else>{{ $gettext('N/A') }}</template>
</td>
<td>
<template v-for="p in permissions">
@ -67,9 +67,9 @@
</template>
</td>
<td>
<span v-if="scope.obj.is_superuser" class="ui pink label">{{ $t('Admin') }}</span>
<span v-else-if="scope.obj.is_staff" class="ui purple label">{{ $t('Staff member') }}</span>
<span v-else class="ui basic label">{{ $t('regular user') }}</span>
<span v-if="scope.obj.is_superuser" class="ui pink label">{{ $gettext('Admin') }}</span>
<span v-else-if="scope.obj.is_staff" class="ui purple label">{{ $gettext('Staff member') }}</span>
<span v-else class="ui basic label">{{ $gettext('regular user') }}</span>
</td>
</template>
</action-table>
@ -85,7 +85,10 @@
></pagination>
<span v-if="result && result.results.length > 0">
{{ $t('Showing results {%start%}-{%end%} on {%total%}', {start: ((page-1) * paginateBy) + 1 , end: ((page-1) * paginateBy) + result.results.length, total: result.count})}}
<translate
:translate-params="{start: ((page-1) * paginateBy) + 1, end: ((page-1) * paginateBy) + result.results.length, total: result.count}">
Showing results %{ start }-%{ end } on %{ total }
</translate>
</span>
</div>
</div>
@ -161,19 +164,19 @@ export default {
return [
{
'code': 'upload',
'label': this.$t('Upload')
'label': this.$gettext('Upload')
},
{
'code': 'library',
'label': this.$t('Library')
'label': this.$gettext('Library')
},
{
'code': 'federation',
'label': this.$t('Federation')
'label': this.$gettext('Federation')
},
{
'code': 'settings',
'label': this.$t('Settings')
'label': this.$gettext('Settings')
}
]
},
@ -191,7 +194,7 @@ export default {
return [
// {
// name: 'delete',
// label: this.$t('Delete'),
// label: this.$gettext('Delete'),
// isDangerous: true
// }
]

View File

@ -16,7 +16,7 @@
{{ group['first-release-date'] }}
</td>
<td colspan="3">
<a :href="getMusicbrainzUrl('release-group', group.id)" class="discrete link" target="_blank" :title="$t('View on MusicBrainz')">
<a :href="getMusicbrainzUrl('release-group', group.id)" class="discrete link" target="_blank" :title="$gettext('View on MusicBrainz')">
{{ group.title }}
</a>
</td>

View File

@ -19,7 +19,7 @@
{{ track.position }}
</td>
<td colspan="3">
<a :href="getMusicbrainzUrl('recording', track.id)" class="discrete link" target="_blank" :title="$t('View on MusicBrainz')">
<a :href="getMusicbrainzUrl('recording', track.id)" class="discrete link" target="_blank" :title="$gettext('View on MusicBrainz')">
{{ track.recording.title }}
</a>
</td>

View File

@ -12,7 +12,7 @@
</div>
<div class="ui fluid search">
<div class="ui icon input">
<input class="prompt" :placeholder="$t('Enter your search query...')" type="text">
<input class="prompt" :placeholder="$gettext('Enter your search query...')" type="text">
<i class="search icon"></i>
</div>
<div class="results"></div>
@ -122,15 +122,15 @@ export default {
return [
{
value: 'artist',
label: this.$t('Artist')
label: this.$gettext('Artist')
},
{
value: 'release',
label: this.$t('Album')
label: this.$gettext('Album')
},
{
value: 'recording',
label: this.$t('Track')
label: this.$gettext('Track')
}
]
}

View File

@ -11,15 +11,18 @@
</div>
<div class="meta">
<i class="clock icon"></i>
<i18next path="Updated {%0%}">
<human-date :date="playlist.modification_date" />
</i18next>
<human-date :date="playlist.modification_date" />
</div>
</div>
<div class="extra content">
<span>
<i class="sound icon"></i>
{{ $t('{%count%} tracks', { count: playlist.tracks_count }) }}
<translate
translate-plural="%{ count } tracks"
:translate-n="playlist.tracks_count"
:translate-params="{count: playlist.tracks_count}">
%{ count} track
</translate>
</span>
<play-button class="mini basic orange right floated" :playlist="playlist">Play all</play-button>
</div>

View File

@ -2,16 +2,16 @@
<div class="ui text container">
<playlist-form @updated="$emit('playlist-updated', $event)" :title="false" :playlist="playlist"></playlist-form>
<h3 class="ui top attached header">
{{ $t('Playlist editor') }}
{{ $gettext('Playlist editor') }}
</h3>
<div class="ui attached segment">
<template v-if="status === 'loading'">
<div class="ui active tiny inline loader"></div>
{{ $t('Syncing changes to server...') }}
{{ $gettext('Syncing changes to server...') }}
</template>
<template v-else-if="status === 'errored'">
<i class="red close icon"></i>
{{ $t('An error occured while saving your changes') }}
{{ $gettext('An error occured while saving your changes') }}
<div v-if="errors.length > 0" class="ui negative message">
<ul class="list">
<li v-for="error in errors">{{ error }}</li>
@ -19,7 +19,7 @@
</div>
</template>
<template v-else-if="status === 'saved'">
<i class="green check icon"></i> {{ $t('Changes synced with server') }}
<i class="green check icon"></i> {{ $gettext('Changes synced with server') }}
</template>
</div>
<div class="ui bottom attached segment">
@ -29,14 +29,21 @@
:class="['ui', {disabled: queueTracks.length === 0}, 'labeled', 'icon', 'button']"
title="Copy tracks from current queue to playlist">
<i class="plus icon"></i>
{{ $t('Insert from queue ({%count%} tracks)', { count: queueTracks.length }) }}
<translate
translate-plural="Insert from queue (%{ count } tracks)"
:translate-n="queueTracks.length"
:translate-params="{count: queueTracks.length}">
Insert from queue (%{ count } track)
</translate>
</div>
<dangerous-button :disabled="plts.length === 0" class="labeled right floated icon" color='yellow' :action="clearPlaylist">
<i class="eraser icon"></i> {{ $t('Clear playlist') }}
<p slot="modal-header">{{ $t('Do you want to clear the playlist "{%name%}"?', { name: playlist.name }) }}</p>
<p slot="modal-content">{{ $t('This will remove all tracks from this playlist and cannot be undone.') }}</p>
<p slot="modal-confirm">{{ $t('Clear playlist') }}</p>
<i class="eraser icon"></i> {{ $gettext('Clear playlist') }}
<p slot="modal-header">
<translate :translate-params="{playlist: playlist.name}">Do you want to clear the playlist "%{ playlist }"?</translate>
</p>
<p slot="modal-content">{{ $gettext('This will remove all tracks from this playlist and cannot be undone.') }}</p>
<p slot="modal-confirm">{{ $gettext('Clear playlist') }}</p>
</dangerous-button>
<div class="ui hidden divider"></div>
<template v-if="plts.length > 0">

View File

@ -1,29 +1,29 @@
<template>
<form class="ui form" @submit.prevent="submit()">
<h4 v-if="title" class="ui header">{{ $t('Create a new playlist') }}</h4>
<h4 v-if="title" class="ui header">{{ $gettext('Create a new playlist') }}</h4>
<div v-if="success" class="ui positive message">
<div class="header">
<template v-if="playlist">
{{ $t('Playlist updated') }}
{{ $gettext('Playlist updated') }}
</template>
<template v-else>
{{ $t('Playlist created') }}
{{ $gettext('Playlist created') }}
</template>
</div>
</div>
<div v-if="errors.length > 0" class="ui negative message">
<div class="header">{{ $t('We cannot create the playlist') }}</div>
<div class="header">{{ $gettext('We cannot create the playlist') }}</div>
<ul class="list">
<li v-for="error in errors">{{ error }}</li>
</ul>
</div>
<div class="three fields">
<div class="field">
<label>{{ $t('Playlist name') }}</label>
<label>{{ $gettext('Playlist name') }}</label>
<input v-model="name" required type="text" placeholder="My awesome playlist" />
</div>
<div class="field">
<label>{{ $t('Playlist visibility') }}</label>
<label>{{ $gettext('Playlist visibility') }}</label>
<select class="ui dropdown" v-model="privacyLevel">
<option :value="c.value" v-for="c in privacyLevelChoices">{{ c.label }}</option>
</select>
@ -31,8 +31,8 @@
<div class="field">
<label>&nbsp;</label>
<button :class="['ui', 'fluid', {'loading': isLoading}, 'button']" type="submit">
<template v-if="playlist">{{ $t('Update playlist') }}</template>
<template v-else>{{ $t('Create playlist') }}</template>
<template v-if="playlist">{{ $gettext('Update playlist') }}</template>
<template v-else>{{ $gettext('Create playlist') }}</template>
</button>
</div>
</div>
@ -73,15 +73,15 @@ export default {
return [
{
value: 'me',
label: this.$t('Nobody except me')
label: this.$gettext('Nobody except me')
},
{
value: 'instance',
label: this.$t('Everyone on this instance')
label: this.$gettext('Everyone on this instance')
},
{
value: 'everyone',
label: this.$t('Everyone')
label: this.$gettext('Everyone')
}
]
}

View File

@ -1,33 +1,37 @@
<template>
<modal @update:show="update" :show="$store.state.playlists.showModal">
<div class="header">
{{ $t('Manage playlists') }}
{{ $gettext('Manage playlists') }}
</div>
<div class="scrolling content">
<div class="description">
<template v-if="track">
<h4 class="ui header">{{ $t('Current track') }}</h4>
<div v-html='trackDisplay'></div>
<h4 class="ui header">{{ $gettext('Current track') }}</h4>
<div
v-translate="{artist: track.artist.name, title: track.title}"
:template-params="{artist: track.artist.name, title: track.title}">
"%{ title }", by %{ artist }
</div>
<div class="ui divider"></div>
</template>
<playlist-form></playlist-form>
<div class="ui divider"></div>
<div v-if="errors.length > 0" class="ui negative message">
<div class="header">{{ $t('We cannot add the track to a playlist') }}</div>
<div class="header">{{ $gettext('We cannot add the track to a playlist') }}</div>
<ul class="list">
<li v-for="error in errors">{{ error }}</li>
</ul>
</div>
</div>
<h4 class="ui header">{{ $t('Available playlists') }}</h4>
<h4 class="ui header">{{ $gettext('Available playlists') }}</h4>
<table class="ui unstackable very basic table">
<thead>
<tr>
<th></th>
<th>{{ $t('Name') }}</th>
<th class="sorted descending">{{ $t('Last modification') }}</th>
<th>{{ $t('Tracks') }}</th>
<th>{{ $gettext('Name') }}</th>
<th class="sorted descending">{{ $gettext('Last modification') }}</th>
<th>{{ $gettext('Tracks') }}</th>
<th></th>
</tr>
</thead>
@ -46,9 +50,9 @@
<div
v-if="track"
class="ui green icon basic small right floated button"
:title="$t('Add to this playlist')"
:title="$gettext('Add to this playlist')"
@click="addToPlaylist(playlist.id)">
<i class="plus icon"></i> {{ $t('Add track') }}
<i class="plus icon"></i> {{ $gettext('Add track') }}
</div>
</td>
</tr>
@ -57,7 +61,7 @@
</div>
</div>
<div class="actions">
<div class="ui cancel button">{{ $t('Cancel') }}</div>
<div class="ui cancel button">{{ $gettext('Cancel') }}</div>
</div>
</modal>
</template>
@ -110,12 +114,6 @@ export default {
let p = _.sortBy(this.playlists, [(e) => { return e.modification_date }])
p.reverse()
return p
},
trackDisplay () {
return this.$t('"{%title%}" by {%artist%}', {
title: this.track.title,
artist: this.track.artist.name }
)
}
},
watch: {

View File

@ -4,13 +4,13 @@
v-if="button"
:class="['ui', 'button']">
<i class="list icon"></i>
{{ $t('Add to playlist...') }}
{{ $gettext('Add to playlist...') }}
</button>
<i
v-else
@click="$store.commit('playlists/chooseTrack', track)"
:class="['playlist-icon', 'list', 'link', 'icon']"
:title="$t('Add to playlist...')">
:title="$gettext('Add to playlist...')">
</i>
</template>

View File

@ -1,8 +1,8 @@
<template>
<button @click="toggleRadio" :class="['ui', 'blue', {'inverted': running}, 'button']">
<i class="ui feed icon"></i>
<template v-if="running">{{ $t('Stop') }}</template>
<template v-else>{{ $t('Start') }}</template>
<template v-if="running">{{ $gettext('Stop') }}</template>
<template v-else>{{ $gettext('Start') }}</template>
radio
</button>
</template>

View File

@ -18,7 +18,7 @@
class="ui basic yellow button"
v-if="$store.state.auth.authenticated && type === 'custom' && customRadio.user === $store.state.auth.profile.id"
:to="{name: 'library.radios.edit', params: {id: customRadioId }}">
{{ $t('Edit...') }}
{{ $gettext('Edit...') }}
</router-link>
<radio-button class="right floated button" :type="type" :custom-radio-id="customRadioId"></radio-button>
</div>

View File

@ -23,7 +23,7 @@
<button
@click="createImport"
v-if="request.status === 'pending' && importAction && $store.state.auth.availablePermissions['library']"
class="ui mini basic green right floated button">{{ $t('Create import') }}</button>
class="ui mini basic green right floated button">{{ $gettext('Create import') }}</button>
</div>
</div>

View File

@ -1,30 +1,30 @@
<template>
<div>
<form v-if="!over" class="ui form" @submit.prevent="submit">
<p>{{ $t('Something\'s missing in the library? Let us know what you would like to listen!') }}</p>
<p>{{ $gettext('Something\'s missing in the library? Let us know what you would like to listen!') }}</p>
<div class="required field">
<label>{{ $t('Artist name') }}</label>
<label>{{ $gettext('Artist name') }}</label>
<input v-model="currentArtistName" placeholder="The Beatles, Mickael Jackson…" required maxlength="200">
</div>
<div class="field">
<label>{{ $t('Albums') }}</label>
<p>{{ $t('Leave this field empty if you\'re requesting the whole discography.') }}</p>
<label>{{ $gettext('Albums') }}</label>
<p>{{ $gettext('Leave this field empty if you\'re requesting the whole discography.') }}</p>
<input v-model="currentAlbums" placeholder="The White Album, Thriller…" maxlength="2000">
</div>
<div class="field">
<label>{{ $t('Comment') }}</label>
<label>{{ $gettext('Comment') }}</label>
<textarea v-model="currentComment" rows="3" placeholder="Use this comment box to add details to your request if needed" maxlength="2000"></textarea>
</div>
<button class="ui submit button" type="submit">{{ $t('Submit') }}</button>
<button class="ui submit button" type="submit">{{ $gettext('Submit') }}</button>
</form>
<div v-else class="ui success message">
<div class="header">Request submitted!</div>
<p>{{ $t('We\'ve received your request, you\'ll get some groove soon ;)') }}</p>
<button @click="reset" class="ui button">{{ $t('Submit another request') }}</button>
<p>{{ $gettext('We\'ve received your request, you\'ll get some groove soon ;)') }}</p>
<button @click="reset" class="ui button">{{ $gettext('Submit another request') }}</button>
</div>
<div v-if="requests.length > 0">
<div class="ui divider"></div>
<h3 class="ui header">{{ $t('Pending requests') }}</h3>
<h3 class="ui header">{{ $gettext('Pending requests') }}</h3>
<div class="ui list">
<div v-for="request in requests" class="item">
<div class="content">

13
front/src/locales.js Normal file
View File

@ -0,0 +1,13 @@
/* eslint-disable */
export default {
"locales": [
{
"code": "en_US",
"label": "English (United-States)"
},
{
"code": "fr_FR",
"label": "Français"
}
]
}

View File

@ -11,11 +11,12 @@ import router from './router'
import axios from 'axios'
import {VueMasonryPlugin} from 'vue-masonry'
import VueLazyload from 'vue-lazyload'
import i18next from 'i18next'
import i18nextFetch from 'i18next-fetch-backend'
import VueI18Next from '@panter/vue-i18next'
import store from './store'
import GetTextPlugin from 'vue-gettext'
import { sync } from 'vuex-router-sync'
import translations from './translations.json'
import locales from '@/locales'
import filters from '@/filters' // eslint-disable-line
import globals from '@/components/globals' // eslint-disable-line
@ -28,8 +29,31 @@ window.$ = window.jQuery = require('jquery')
// require('./semantic/semantic.css')
require('semantic-ui-css/semantic.js')
require('masonry-layout')
let availableLanguages = (function () {
let l = {}
locales.locales.forEach(c => {
l[c.code] = c.label
})
return l
})()
let defaultLanguage = 'en_US'
if (availableLanguages[store.state.ui.currentLanguage]) {
defaultLanguage = store.state.ui.currentLanguage
}
Vue.use(GetTextPlugin, {
availableLanguages: availableLanguages,
defaultLanguage: defaultLanguage,
languageVmMixin: {
computed: {
currentKebabCase: function () {
return this.current.toLowerCase().replace('_', '-')
}
}
},
translations: translations,
silent: true
})
Vue.use(VueI18Next)
Vue.use(VueMasonryPlugin)
Vue.use(VueLazyload)
Vue.config.productionTip = false
@ -96,35 +120,17 @@ axios.interceptors.response.use(function (response) {
}
}
if (error.backendErrors.length === 0) {
error.backendErrors.push(i18next.t('An unknown error occured, ensure your are connected to the internet and your funkwhale instance is up and running'))
error.backendErrors.push('An unknown error occured, ensure your are connected to the internet and your funkwhale instance is up and running')
}
// Do something with response error
return Promise.reject(error)
})
// i18n
i18next
.use(i18nextFetch)
.init({
lng: navigator.language,
fallbackLng: ['en'],
preload: [navigator.language, 'en'],
backend: {
loadPath: '/static/translations/{%lng%}.json'
},
interpolation: {
prefix: '{%',
suffix: '%}'
}
})
const i18n = new VueI18Next(i18next)
/* eslint-disable no-new */
new Vue({
el: '#app',
router,
store,
i18n,
template: '<App/>',
components: { App }
})

View File

@ -36,6 +36,10 @@ export default new Vuex.Store({
key: 'instance',
paths: ['instance.events', 'instance.instanceUrl']
}),
createPersistedState({
key: 'ui',
paths: ['ui.currentLanguage']
}),
createPersistedState({
key: 'radios',
paths: ['radios'],

View File

@ -3,6 +3,7 @@ import axios from 'axios'
export default {
namespaced: true,
state: {
currentLanguage: 'en_US',
lastDate: new Date(),
maxMessages: 100,
messageDisplayDuration: 10000,
@ -13,6 +14,9 @@ export default {
}
},
mutations: {
currentLanguage: (state, value) => {
state.currentLanguage = value
},
computeLastDate: (state) => {
state.lastDate = new Date()
},

View File

@ -0,0 +1 @@
{}

View File

@ -1,5 +1,5 @@
<template>
<div class="main pusher" v-title="$t('Instance settings')">
<div class="main pusher" v-title="$gettext('Instance settings')">
<div class="ui vertical stripe segment">
<div class="ui text container">
<div :class="['ui', {'loading': isLoading}, 'form']"></div>
@ -13,7 +13,7 @@
</div>
<div class="four wide column">
<div class="ui sticky vertical secondary menu">
<div class="header item">{{ $t('Sections') }}</div>
<div class="header item">{{ $gettext('Sections') }}</div>
<a :class="['menu', {active: group.id === current}, 'item']"
@click.prevent="scrollTo(group.id)"
:href="'#' + group.id"
@ -71,9 +71,18 @@ export default {
},
computed: {
groups () {
// somehow, extraction fails if in the return block directly
let instanceLabel = this.$gettext('Instance information')
let usersLabel = this.$gettext('Users')
let importsLabel = this.$gettext('Imports')
let playlistsLabel = this.$gettext('Playlists')
let federationLabel = this.$gettext('Federation')
let subsonicLabel = this.$gettext('Subsonic')
let statisticsLabel = this.$gettext('Statistics')
let errorLabel = this.$gettext('Error reporting')
return [
{
label: this.$t('Instance information'),
label: instanceLabel,
id: 'instance',
settings: [
'instance__name',
@ -82,7 +91,7 @@ export default {
]
},
{
label: this.$t('Users'),
label: usersLabel,
id: 'users',
settings: [
'users__registration_enabled',
@ -91,21 +100,21 @@ export default {
]
},
{
label: this.$t('Imports'),
label: importsLabel,
id: 'imports',
settings: [
'providers_youtube__api_key'
]
},
{
label: this.$t('Playlists'),
label: playlistsLabel,
id: 'playlists',
settings: [
'playlists__max_tracks'
]
},
{
label: this.$t('Federation'),
label: federationLabel,
id: 'federation',
settings: [
'federation__enabled',
@ -116,14 +125,14 @@ export default {
]
},
{
label: this.$t('Subsonic'),
label: subsonicLabel,
id: 'subsonic',
settings: [
'subsonic__enabled'
]
},
{
label: this.$t('Statistics'),
label: statisticsLabel,
id: 'statistics',
settings: [
'instance__nodeinfo_enabled',
@ -132,7 +141,7 @@ export default {
]
},
{
label: this.$t('Error reporting'),
label: errorLabel,
id: 'reporting',
settings: [
'raven__front_enabled',

View File

@ -3,14 +3,14 @@
<div class="ui secondary pointing menu">
<router-link
class="ui item"
:to="{name: 'manage.library.files'}">{{ $t('Files') }}</router-link>
:to="{name: 'manage.library.files'}">{{ $gettext('Files') }}</router-link>
<router-link
class="ui item"
:to="{name: 'manage.library.requests'}">
{{ $t('Import requests') }}
{{ $gettext('Import requests') }}
<div
:class="['ui', {'teal': $store.state.ui.notifications.importRequests > 0}, 'label']"
:title="$t('Pending import requests')">
:title="$gettext('Pending import requests')">
{{ $store.state.ui.notifications.importRequests }}</div>
</router-link>
</div>

View File

@ -1,7 +1,7 @@
<template>
<div v-title="'Files'">
<div class="ui vertical stripe segment">
<h2 class="ui header">{{ $t('Library files') }}</h2>
<h2 class="ui header">{{ $gettext('Library files') }}</h2>
<div class="ui hidden divider"></div>
<library-files-table :show-library="true"></library-files-table>
</div>

View File

@ -1,7 +1,7 @@
<template>
<div v-title="$t('Import requests')">
<div v-title="$gettext('Import requests')">
<div class="ui vertical stripe segment">
<h2 class="ui header">{{ $t('Import requests') }}</h2>
<h2 class="ui header">{{ $gettext('Import requests') }}</h2>
<div class="ui hidden divider"></div>
<library-requests-table></library-requests-table>
</div>

View File

@ -1,12 +1,12 @@
<template>
<div class="main pusher" v-title="$t('Manage users')">
<div class="main pusher" v-title="$gettext('Manage users')">
<div class="ui secondary pointing menu">
<router-link
class="ui item"
:to="{name: 'manage.users.users.list'}">{{ $t('Users') }}</router-link>
:to="{name: 'manage.users.users.list'}">{{ $gettext('Users') }}</router-link>
<router-link
class="ui item"
:to="{name: 'manage.users.invitations.list'}">{{ $t('Invitations') }}</router-link>
:to="{name: 'manage.users.invitations.list'}">{{ $gettext('Invitations') }}</router-link>
</div>
<router-view :key="$route.fullPath"></router-view>
</div>

View File

@ -1,7 +1,7 @@
<template>
<div v-title="$t('Invitations')">
<div v-title="$gettext('Invitations')">
<div class="ui vertical stripe segment">
<h2 class="ui header">{{ $t('Invitations') }}</h2>
<h2 class="ui header">{{ $gettext('Invitations') }}</h2>
<invitation-form></invitation-form>
<div class="ui hidden divider"></div>
<invitations-table></invitations-table>

View File

@ -19,7 +19,7 @@
<tbody>
<tr>
<td>
{{ $t('Name') }}
{{ $gettext('Name') }}
</td>
<td>
{{ object.name }}
@ -27,7 +27,7 @@
</tr>
<tr>
<td>
{{ $t('Email address') }}
{{ $gettext('Email address') }}
</td>
<td>
{{ object.email }}
@ -35,7 +35,7 @@
</tr>
<tr>
<td>
{{ $t('Sign-up') }}
{{ $gettext('Sign-up') }}
</td>
<td>
<human-date :date="object.date_joined"></human-date>
@ -43,17 +43,17 @@
</tr>
<tr>
<td>
{{ $t('Last activity') }}
{{ $gettext('Last activity') }}
</td>
<td>
<human-date v-if="object.last_activity" :date="object.last_activity"></human-date>
<template v-else>{{ $t('N/A') }}</template>
<template v-else>{{ $gettext('N/A') }}</template>
</td>
</tr>
<tr>
<td>
{{ $t('Account active') }}
<span :data-tooltip="$t('Determine if the user account is active or not. Inactive users cannot login or user the service.')"><i class="question circle icon"></i></span>
{{ $gettext('Account active') }}
<span :data-tooltip="$gettext('Determine if the user account is active or not. Inactive users cannot login or user the service.')"><i class="question circle icon"></i></span>
</td>
<td>
<div class="ui toggle checkbox">
@ -66,7 +66,7 @@
</tr>
<tr>
<td>
{{ $t('Permissions') }}
{{ $gettext('Permissions') }}
</td>
<td>
<select
@ -82,7 +82,7 @@
</table>
</div>
<div class="ui hidden divider"></div>
<button @click="fetchData" class="ui basic button">{{ $t('Refresh') }}</button>
<button @click="fetchData" class="ui basic button">{{ $gettext('Refresh') }}</button>
</div>
</template>
</div>
@ -145,19 +145,19 @@ export default {
return [
{
'code': 'upload',
'label': this.$t('Upload')
'label': this.$gettext('Upload')
},
{
'code': 'library',
'label': this.$t('Library')
'label': this.$gettext('Library')
},
{
'code': 'federation',
'label': this.$t('Federation')
'label': this.$gettext('Federation')
},
{
'code': 'settings',
'label': this.$t('Settings')
'label': this.$gettext('Settings')
}
]
}

View File

@ -1,7 +1,7 @@
<template>
<div v-title="$t('Users')">
<div v-title="$gettext('Users')">
<div class="ui vertical stripe segment">
<h2 class="ui header">{{ $t('Users') }}</h2>
<h2 class="ui header">{{ $gettext('Users') }}</h2>
<div class="ui hidden divider"></div>
<users-table></users-table>
</div>

View File

@ -1,30 +1,30 @@
<template>
<div class="main pusher" v-title="$t('Confirm your email')">
<div class="main pusher" v-title="$gettext('Confirm your email')">
<div class="ui vertical stripe segment">
<div class="ui small text container">
<h2>{{ $t('Confirm your email') }}</h2>
<h2>{{ $gettext('Confirm your email') }}</h2>
<form v-if="!success" class="ui form" @submit.prevent="submit()">
<div v-if="errors.length > 0" class="ui negative message">
<div class="header">{{ $t('Error while confirming your email') }}</div>
<div class="header">{{ $gettext('Error while confirming your email') }}</div>
<ul class="list">
<li v-for="error in errors">{{ error }}</li>
</ul>
</div>
<div class="field">
<label>{{ $t('Confirmation code') }}</label>
<label>{{ $gettext('Confirmation code') }}</label>
<input type="text" required v-model="key" />
</div>
<router-link :to="{path: '/login'}">
{{ $t('Back to login') }}
{{ $gettext('Back to login') }}
</router-link>
<button :class="['ui', {'loading': isLoading}, 'right', 'floated', 'green', 'button']" type="submit">
{{ $t('Confirm your email') }}</button>
{{ $gettext('Confirm your email') }}</button>
</form>
<div v-else class="ui positive message">
<div class="header">{{ $t('Email confirmed') }}</div>
<p>{{ $t('Your email address was confirmed, you can now use the service without limitations.') }}</p>
<div class="header">{{ $gettext('Email confirmed') }}</div>
<p>{{ $gettext('Your email address was confirmed, you can now use the service without limitations.') }}</p>
<router-link :to="{name: 'login'}">
{{ $t('Proceed to login') }}
{{ $gettext('Proceed to login') }}
</router-link>
</div>
</div>

View File

@ -1,31 +1,31 @@
<template>
<div class="main pusher" v-title="$t('Reset your password')">
<div class="main pusher" v-title="$gettext('Reset your password')">
<div class="ui vertical stripe segment">
<div class="ui small text container">
<h2>{{ $t('Reset your password') }}</h2>
<h2>{{ $gettext('Reset your password') }}</h2>
<form class="ui form" @submit.prevent="submit()">
<div v-if="errors.length > 0" class="ui negative message">
<div class="header">{{ $t('Error while asking for a password reset') }}</div>
<div class="header">{{ $gettext('Error while asking for a password reset') }}</div>
<ul class="list">
<li v-for="error in errors">{{ error }}</li>
</ul>
</div>
<p>{{ $t('Use this form to request a password reset. We will send an email to the given address with instructions to reset your password.') }}</p>
<p>{{ $gettext('Use this form to request a password reset. We will send an email to the given address with instructions to reset your password.') }}</p>
<div class="field">
<label>{{ $t('Account\'s email') }}</label>
<label>{{ $gettext('Account\'s email') }}</label>
<input
required
ref="email"
type="email"
autofocus
:placeholder="$t('Input the email address binded to your account')"
:placeholder="$gettext('Input the email address binded to your account')"
v-model="email">
</div>
<router-link :to="{path: '/login'}">
{{ $t('Back to login') }}
{{ $gettext('Back to login') }}
</router-link>
<button :class="['ui', {'loading': isLoading}, 'right', 'floated', 'green', 'button']" type="submit">
{{ $t('Ask for a password reset') }}</button>
{{ $gettext('Ask for a password reset') }}</button>
</form>
</div>
</div>

View File

@ -1,35 +1,35 @@
<template>
<div class="main pusher" v-title="$t('Change your password')">
<div class="main pusher" v-title="$gettext('Change your password')">
<div class="ui vertical stripe segment">
<div class="ui small text container">
<h2>{{ $t('Change your password') }}</h2>
<h2>{{ $gettext('Change your password') }}</h2>
<form v-if="!success" class="ui form" @submit.prevent="submit()">
<div v-if="errors.length > 0" class="ui negative message">
<div class="header">{{ $t('Error while changing your password') }}</div>
<div class="header">{{ $gettext('Error while changing your password') }}</div>
<ul class="list">
<li v-for="error in errors">{{ error }}</li>
</ul>
</div>
<template v-if="token && uid">
<div class="field">
<label>{{ $t('New password') }}</label>
<label>{{ $gettext('New password') }}</label>
<password-input v-model="newPassword" />
</div>
<router-link :to="{path: '/login'}">
{{ $t('Back to login') }}
{{ $gettext('Back to login') }}
</router-link>
<button :class="['ui', {'loading': isLoading}, 'right', 'floated', 'green', 'button']" type="submit">
{{ $t('Update your password') }}</button>
{{ $gettext('Update your password') }}</button>
</template>
<template v-else>
<p>{{ $t('If the email address provided in the previous step is valid and binded to a user account, you should receive an email with reset instructions in the next couple of minutes.') }}</p>
<p>{{ $gettext('If the email address provided in the previous step is valid and binded to a user account, you should receive an email with reset instructions in the next couple of minutes.') }}</p>
</template>
</form>
<div v-else class="ui positive message">
<div class="header">{{ $t('Password updated successfully') }}</div>
<p>{{ $t('Your password has been updated successfully.') }}</p>
<div class="header">{{ $gettext('Password updated successfully') }}</div>
<p>{{ $gettext('Your password has been updated successfully.') }}</p>
<router-link :to="{name: 'login'}">
{{ $t('Proceed to login') }}
{{ $gettext('Proceed to login') }}
</router-link>
</div>
</div>

View File

@ -3,16 +3,16 @@
<div class="ui secondary pointing menu">
<router-link
class="ui item"
:to="{name: 'federation.libraries.list'}">{{ $t('Libraries') }}</router-link>
:to="{name: 'federation.libraries.list'}">{{ $gettext('Libraries') }}</router-link>
<router-link
class="ui item"
:to="{name: 'federation.tracks.list'}">{{ $t('Tracks') }}</router-link>
:to="{name: 'federation.tracks.list'}">{{ $gettext('Tracks') }}</router-link>
<div class="ui secondary right menu">
<router-link
class="ui item"
:to="{name: 'federation.followers.list'}">
{{ $t('Followers') }}
<div class="ui teal label" :title="$t('Pending requests')">{{ requestsCount }}</div>
{{ $gettext('Followers') }}
<div class="ui teal label" :title="$gettext('Pending requests')">{{ requestsCount }}</div>
</router-link>
</div>
</div>

View File

@ -19,18 +19,18 @@
<tbody>
<tr>
<td >
{{ $t('Follow status') }}
<span :data-tooltip="$t('This indicate if the remote library granted you access')"><i class="question circle icon"></i></span>
{{ $gettext('Follow status') }}
<span :data-tooltip="$gettext('This indicate if the remote library granted you access')"><i class="question circle icon"></i></span>
</td>
<td>
<template v-if="object.follow.approved === null">
<i class="loading icon"></i> {{ $t('Pending approval') }}
<i class="loading icon"></i> {{ $gettext('Pending approval') }}
</template>
<template v-else-if="object.follow.approved === true">
<i class="check icon"></i> {{ $t('Following') }}
<i class="check icon"></i> {{ $gettext('Following') }}
</template>
<template v-else-if="object.follow.approved === false">
<i class="x icon"></i> {{ $t('Not following') }}
<i class="x icon"></i> {{ $gettext('Not following') }}
</template>
</td>
<td>
@ -38,8 +38,8 @@
</tr>
<tr>
<td>
{{ $t('Federation') }}
<span :data-tooltip="$t('Use this flag to enable/disable federation with this library')"><i class="question circle icon"></i></span>
{{ $gettext('Federation') }}
<span :data-tooltip="$gettext('Use this flag to enable/disable federation with this library')"><i class="question circle icon"></i></span>
</td>
<td>
<div class="ui toggle checkbox">
@ -54,8 +54,8 @@
</tr>
<tr>
<td>
{{ $t('Auto importing') }}
<span :data-tooltip="$t('When enabled, auto importing will automatically import new tracks published in this library')"><i class="question circle icon"></i></span>
{{ $gettext('Auto importing') }}
<span :data-tooltip="$gettext('When enabled, auto importing will automatically import new tracks published in this library')"><i class="question circle icon"></i></span>
</td>
<td>
<div class="ui toggle checkbox">
@ -82,19 +82,24 @@
</tr>
-->
<tr>
<td>{{ $t('Library size') }}</td>
<td>{{ $gettext('Library size') }}</td>
<td>
<template v-if="object.tracks_count">
{{ $t('{%count%} tracks', { count: object.tracks_count }) }}
<translate
translate-plural="%{ count } tracks"
:translate-n="object.tracks_count"
:translate-params="{count: object.tracks_count}">
%{ count } track
</translate>
</template>
<template v-else>
{{ $t('Unkwnown') }}
{{ $gettext('Unkwnown') }}
</template>
</td>
<td></td>
</tr>
<tr>
<td>{{ $t('Last fetched') }}</td>
<td>{{ $gettext('Last fetched') }}</td>
<td>
<human-date v-if="object.fetched_date" :date="object.fetched_date"></human-date>
<template v-else>Never</template>
@ -102,10 +107,10 @@
@click="scan"
v-if="!scanTrigerred"
:class="['ui', 'basic', {loading: isScanLoading}, 'button']">
<i class="sync icon"></i> {{ $t('Trigger scan') }}
<i class="sync icon"></i> {{ $gettext('Trigger scan') }}
</button>
<button v-else class="ui success button">
<i class="check icon"></i> {{ $t('Scan triggered!') }}
<i class="check icon"></i> {{ $gettext('Scan triggered!') }}
</button>
</td>
@ -115,10 +120,10 @@
</table>
</div>
<div class="ui hidden divider"></div>
<button @click="fetchData" class="ui basic button">{{ $t('Refresh') }}</button>
<button @click="fetchData" class="ui basic button">{{ $gettext('Refresh') }}</button>
</div>
<div class="ui vertical stripe segment">
<h2>{{ $t('Tracks available in this library') }}</h2>
<h2>{{ $gettext('Tracks available in this library') }}</h2>
<library-track-table v-if="!isLoading" :filters="{library: id}"></library-track-table>
</div>
</template>

View File

@ -1,9 +1,9 @@
<template>
<div v-title="'Followers'">
<div class="ui vertical stripe segment">
<h2 class="ui header">{{ $t('Browsing followers') }}</h2>
<h2 class="ui header">{{ $gettext('Browsing followers') }}</h2>
<p>
{{ $t('Be careful when accepting follow requests, as it means the follower will have access to your entire library.') }}
{{ $gettext('Be careful when accepting follow requests, as it means the follower will have access to your entire library.') }}
</p>
<div class="ui hidden divider"></div>
<library-follow-table></library-follow-table>

View File

@ -1,22 +1,22 @@
<template>
<div v-title="'Libraries'">
<div class="ui vertical stripe segment">
<h2 class="ui header">{{ $t('Browsing libraries') }}</h2>
<h2 class="ui header">{{ $gettext('Browsing libraries') }}</h2>
<router-link
class="ui basic green button"
:to="{name: 'federation.libraries.scan'}">
<i class="plus icon"></i>
{{ $t('Add a new library') }}
{{ $gettext('Add a new library') }}
</router-link>
<div class="ui hidden divider"></div>
<div :class="['ui', {'loading': isLoading}, 'form']">
<div class="fields">
<div class="field">
<label>{{ $t('Search') }}</label>
<label>{{ $gettext('Search') }}</label>
<input class="search" type="text" v-model="query" placeholder="Enter an library domain name..."/>
</div>
<div class="field">
<label>{{ $t('Ordering') }}</label>
<label>{{ $gettext('Ordering') }}</label>
<select class="ui dropdown" v-model="ordering">
<option v-for="option in orderingOptions" :value="option[0]">
{{ option[1] }}
@ -24,14 +24,14 @@
</select>
</div>
<div class="field">
<label>{{ $t('Ordering direction') }}</label>
<label>{{ $gettext('Ordering direction') }}</label>
<select class="ui dropdown" v-model="orderingDirection">
<option value="+">{{ $t('Ascending') }}</option>
<option value="-">{{ $t('Descending') }}</option>
<option value="+">{{ $gettext('Ascending') }}</option>
<option value="-">{{ $gettext('Descending') }}</option>
</select>
</div>
<div class="field">
<label>{{ $t('Results per page') }}</label>
<label>{{ $gettext('Results per page') }}</label>
<select class="ui dropdown" v-model="paginateBy">
<option :value="parseInt(12)">12</option>
<option :value="parseInt(25)">25</option>

Some files were not shown because too many files have changed in this diff Show More