Translations improved
* Translation system in place * French translations available
This commit is contained in:
parent
e6b9c3bf07
commit
945b218504
8
.babelrc
8
.babelrc
@ -1,9 +1,3 @@
|
|||||||
{
|
{
|
||||||
"presets": ["es2015", "react"],
|
"presets": ["es2015", "react"]
|
||||||
"plugins": [
|
|
||||||
["react-intl", {
|
|
||||||
"messagesDir": "./app/dist/i18n/",
|
|
||||||
"enforceDescriptions": true
|
|
||||||
}]
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
2
.gitignore
vendored
2
.gitignore
vendored
@ -4,4 +4,4 @@ app/dist/webpackHotMiddlewareClient.js
|
|||||||
app/dist/*.hot-update.json
|
app/dist/*.hot-update.json
|
||||||
app/dist/*.hot-update.js
|
app/dist/*.hot-update.js
|
||||||
app/dist/i18n/
|
app/dist/i18n/
|
||||||
app/dist/3.3.js
|
.cache
|
||||||
|
33
CONTRIBUTING.md
Normal file
33
CONTRIBUTING.md
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
Contributing
|
||||||
|
============
|
||||||
|
|
||||||
|
## Building
|
||||||
|
|
||||||
|
See `README.md` for instructions on how to build. Build is done with
|
||||||
|
`webpack`.
|
||||||
|
|
||||||
|
|
||||||
|
## Useful scripts
|
||||||
|
|
||||||
|
A few `npm` scripts are provided:
|
||||||
|
* `npm run build` to trigger a dev build.
|
||||||
|
* `npm run watch` to trigger a dev build and rebuild on changes.
|
||||||
|
* `npm run prod` to trigger a production build.
|
||||||
|
* `npm run clean` to clean the `app/dist` folder.
|
||||||
|
* `npm run extractTranslations` to generate a translation file (see below).
|
||||||
|
|
||||||
|
|
||||||
|
## Translating
|
||||||
|
|
||||||
|
Translations are handled by [react-intl](https://github.com/yahoo/react-intl/).
|
||||||
|
|
||||||
|
`npm run extractTranslations` output a file containing all the english
|
||||||
|
translations, in the expected form. It is a mapping of ids and strings to
|
||||||
|
translate, with an extra description provided as a comment at the end of the
|
||||||
|
line, for some translation context.
|
||||||
|
|
||||||
|
Typically, if you want to translate to another `$LOCALE` (say `fr-FR`), create
|
||||||
|
a folder `./app/locales/$LOCALE`, put inside the generated file from `npm run
|
||||||
|
extractTranslations`, called `index.js`. Copy the lines in
|
||||||
|
`./app/locales/index.js` to include your new translation and translate all the
|
||||||
|
strings in the `./app/locales/$LOCALE/index.js` file you have just created.
|
@ -36,6 +36,15 @@ Please use the Git hooks (in `hooks` folder) to automatically make a build
|
|||||||
before comitting, as commit should always contain an up to date production
|
before comitting, as commit should always contain an up to date production
|
||||||
build.
|
build.
|
||||||
|
|
||||||
|
Compilation cache is stored in `.cache` at the root of this repo. Remember to
|
||||||
|
clean it in case of compilation issues.
|
||||||
|
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
See `CONTRIBUTING.md` file for extra infos.
|
||||||
|
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
This code is distributed under an MIT license.
|
This code is distributed under an MIT license.
|
||||||
|
6
TODO
6
TODO
@ -11,19 +11,21 @@
|
|||||||
* Move CSS in modules
|
* Move CSS in modules
|
||||||
=> https://github.com/gajus/react-css-modules
|
=> https://github.com/gajus/react-css-modules
|
||||||
|
|
||||||
|
|
||||||
# API middleware
|
# API middleware
|
||||||
* https://github.com/reactjs/redux/issues/1824#issuecomment-228609501
|
* https://github.com/reactjs/redux/issues/1824#issuecomment-228609501
|
||||||
* https://medium.com/@adamrackis/querying-a-redux-store-37db8c7f3b0f#.eezt3dano
|
* https://medium.com/@adamrackis/querying-a-redux-store-37db8c7f3b0f#.eezt3dano
|
||||||
* https://github.com/reactjs/redux/issues/644
|
* https://github.com/reactjs/redux/issues/644
|
||||||
* https://github.com/peterpme/redux-crud-api-middleware/blob/master/README.md
|
* https://github.com/peterpme/redux-crud-api-middleware/blob/master/README.md
|
||||||
* https://github.com/madou/armory-front/tree/master/src/app/reducers
|
* https://github.com/madou/armory-front/tree/master/src/app/reducers
|
||||||
|
* Immutable.js (?) + get rid of lodash
|
||||||
|
|
||||||
|
|
||||||
## Global UI
|
## Global UI
|
||||||
* What happens when JS is off?
|
* What happens when JS is off?
|
||||||
=> https://www.allantatter.com/react-js-and-progressive-enhancement/
|
=> https://www.allantatter.com/react-js-and-progressive-enhancement/
|
||||||
* Back button?
|
|
||||||
|
|
||||||
## Miscellaneous
|
## Miscellaneous
|
||||||
* See TODOs in the code
|
|
||||||
* Babel transform runtime
|
* Babel transform runtime
|
||||||
|
* Webpack chunks?
|
||||||
|
@ -1,7 +1,12 @@
|
|||||||
import React, { Component, PropTypes } from "react";
|
import React, { Component, PropTypes } from "react";
|
||||||
import { FormattedMessage } from "react-intl";
|
import { defineMessages, injectIntl, intlShape, FormattedMessage } from "react-intl";
|
||||||
|
|
||||||
export class LoginForm extends Component {
|
import { messagesMap } from "../utils";
|
||||||
|
import messages from "../locales/messagesDescriptors/Login";
|
||||||
|
|
||||||
|
const loginMessages = defineMessages(messagesMap(messages));
|
||||||
|
|
||||||
|
class LoginFormIntl extends Component {
|
||||||
constructor (props) {
|
constructor (props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
@ -45,8 +50,10 @@ export class LoginForm extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
|
const {formatMessage} = this.props.intl;
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
{ /* TODO: info/error translation */ }
|
||||||
{
|
{
|
||||||
this.props.error ?
|
this.props.error ?
|
||||||
<div className="row">
|
<div className="row">
|
||||||
@ -72,17 +79,17 @@ export class LoginForm extends Component {
|
|||||||
<div className="row">
|
<div className="row">
|
||||||
<div className="form-group" ref="usernameFormGroup">
|
<div className="form-group" ref="usernameFormGroup">
|
||||||
<div className="col-xs-12">
|
<div className="col-xs-12">
|
||||||
<input type="text" className="form-control" ref="username" aria-label="Username" placeholder="Username" autoFocus defaultValue={this.props.username} />
|
<input type="text" className="form-control" ref="username" aria-label={formatMessage(loginMessages["app.login.username"])} placeholder={formatMessage(loginMessages["app.login.username"])} autoFocus defaultValue={this.props.username} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="form-group" ref="passwordFormGroup">
|
<div className="form-group" ref="passwordFormGroup">
|
||||||
<div className="col-xs-12">
|
<div className="col-xs-12">
|
||||||
<input type="password" className="form-control" ref="password" aria-label="Password" placeholder="Password" />
|
<input type="password" className="form-control" ref="password" aria-label={formatMessage(loginMessages["app.login.password"])} placeholder={formatMessage(loginMessages["app.login.password"])} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="form-group" ref="endpointFormGroup">
|
<div className="form-group" ref="endpointFormGroup">
|
||||||
<div className="col-xs-12">
|
<div className="col-xs-12">
|
||||||
<input type="text" className="form-control" ref="endpoint" aria-label="URL of your Ampache instance (e.g. http://ampache.example.com)" placeholder="http://ampache.example.com" defaultValue={this.props.endpoint} />
|
<input type="text" className="form-control" ref="endpoint" aria-label={formatMessage(loginMessages["app.login.endpointInputAriaLabel"])} placeholder="http://ampache.example.com" defaultValue={this.props.endpoint} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
@ -90,11 +97,12 @@ export class LoginForm extends Component {
|
|||||||
<div className="row">
|
<div className="row">
|
||||||
<div className="col-sm-6 col-xs-12 checkbox">
|
<div className="col-sm-6 col-xs-12 checkbox">
|
||||||
<label id="rememberMeLabel">
|
<label id="rememberMeLabel">
|
||||||
<input type="checkbox" ref="rememberMe" defaultChecked={this.props.rememberMe} aria-labelledby="rememberMeLabel" /> <FormattedMessage id="app.login.rememberMe" description="Remember me checkbox label" defaultMessage="Remember me" />
|
<input type="checkbox" ref="rememberMe" defaultChecked={this.props.rememberMe} aria-labelledby="rememberMeLabel" />
|
||||||
|
<FormattedMessage {...loginMessages["app.login.rememberMe"]} />
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div className="col-sm-6 col-sm-12 submit text-right">
|
<div className="col-sm-6 col-sm-12 submit text-right">
|
||||||
<input type="submit" className="btn btn-default" aria-label="Sign in" defaultValue="Sign in" disabled={this.props.isAuthenticating} />
|
<input type="submit" className="btn btn-default" aria-label={formatMessage(loginMessages["app.login.signIn"])} defaultValue={formatMessage(loginMessages["app.login.signIn"])} disabled={this.props.isAuthenticating} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -107,24 +115,32 @@ export class LoginForm extends Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
LoginForm.propTypes = {
|
LoginFormIntl.propTypes = {
|
||||||
username: PropTypes.string,
|
username: PropTypes.string,
|
||||||
endpoint: PropTypes.string,
|
endpoint: PropTypes.string,
|
||||||
rememberMe: PropTypes.bool,
|
rememberMe: PropTypes.bool,
|
||||||
onSubmit: PropTypes.func.isRequired,
|
onSubmit: PropTypes.func.isRequired,
|
||||||
isAuthenticating: PropTypes.bool,
|
isAuthenticating: PropTypes.bool,
|
||||||
error: PropTypes.string,
|
error: PropTypes.string,
|
||||||
info: PropTypes.string
|
info: PropTypes.string,
|
||||||
|
intl: intlShape.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export let LoginForm = injectIntl(LoginFormIntl);
|
||||||
|
|
||||||
|
|
||||||
export default class Login extends Component {
|
export default class Login extends Component {
|
||||||
render () {
|
render () {
|
||||||
|
const greeting = (
|
||||||
|
<p>
|
||||||
|
<FormattedMessage {...loginMessages["app.login.greeting"]} />
|
||||||
|
</p>
|
||||||
|
);
|
||||||
return (
|
return (
|
||||||
<div className="login text-center container-fluid">
|
<div className="login text-center container-fluid">
|
||||||
<h1><img src="./app/assets/img/ampache-blue.png" alt="A"/>mpache</h1>
|
<h1><img src="./app/assets/img/ampache-blue.png" alt="A"/>mpache</h1>
|
||||||
<hr/>
|
<hr/>
|
||||||
<p><FormattedMessage id="app.login.greeting" description="Greeting to welcome the user to the app" defaultMessage="Welcome back on Ampache, let's go!" /></p>
|
{(!this.props.error && !this.props.info) ? greeting : null}
|
||||||
<div className="col-sm-9 col-sm-offset-2 col-md-6 col-md-offset-3">
|
<div className="col-sm-9 col-sm-offset-2 col-md-6 col-md-offset-3">
|
||||||
<LoginForm onSubmit={this.props.onSubmit} username={this.props.username} endpoint={this.props.endpoint} rememberMe={this.props.rememberMe} isAuthenticating={this.props.isAuthenticating} error={this.props.error} info={this.props.info} />
|
<LoginForm onSubmit={this.props.onSubmit} username={this.props.username} endpoint={this.props.endpoint} rememberMe={this.props.rememberMe} isAuthenticating={this.props.isAuthenticating} error={this.props.error} info={this.props.info} />
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,10 +1,16 @@
|
|||||||
import React, { Component, PropTypes } from "react";
|
import React, { Component, PropTypes } from "react";
|
||||||
import { Link} from "react-router";
|
import { Link} from "react-router";
|
||||||
|
import { defineMessages, FormattedMessage } from "react-intl";
|
||||||
import Fuse from "fuse.js";
|
import Fuse from "fuse.js";
|
||||||
|
|
||||||
import FilterBar from "./elements/FilterBar";
|
import FilterBar from "./elements/FilterBar";
|
||||||
import Pagination from "./elements/Pagination";
|
import Pagination from "./elements/Pagination";
|
||||||
import { formatLength} from "../utils";
|
import { formatLength, messagesMap } from "../utils";
|
||||||
|
|
||||||
|
import commonMessages from "../locales/messagesDescriptors/common";
|
||||||
|
import messages from "../locales/messagesDescriptors/Songs";
|
||||||
|
|
||||||
|
const songsMessages = defineMessages(messagesMap(Array.concat([], commonMessages, messages)));
|
||||||
|
|
||||||
export class SongsTableRow extends Component {
|
export class SongsTableRow extends Component {
|
||||||
render () {
|
render () {
|
||||||
@ -55,11 +61,21 @@ export class SongsTable extends Component {
|
|||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th></th>
|
<th></th>
|
||||||
<th>Title</th>
|
<th>
|
||||||
<th>Artist</th>
|
<FormattedMessage {...songsMessages["app.songs.title"]} />
|
||||||
<th>Album</th>
|
</th>
|
||||||
<th>Genre</th>
|
<th>
|
||||||
<th>Length</th>
|
<FormattedMessage {...songsMessages["app.common.artist"]} />
|
||||||
|
</th>
|
||||||
|
<th>
|
||||||
|
<FormattedMessage {...songsMessages["app.common.album"]} />
|
||||||
|
</th>
|
||||||
|
<th>
|
||||||
|
<FormattedMessage {...songsMessages["app.common.genre"]} />
|
||||||
|
</th>
|
||||||
|
<th>
|
||||||
|
<FormattedMessage {...songsMessages["app.songs.length"]} />
|
||||||
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>{rows}</tbody>
|
<tbody>{rows}</tbody>
|
||||||
|
@ -1,6 +1,12 @@
|
|||||||
import React, { Component, PropTypes } from "react";
|
import React, { Component, PropTypes } from "react";
|
||||||
|
import { defineMessages, injectIntl, intlShape, FormattedMessage } from "react-intl";
|
||||||
|
|
||||||
export default class FilterBar extends Component {
|
import { messagesMap } from "../../utils";
|
||||||
|
import messages from "../../locales/messagesDescriptors/elements/FilterBar";
|
||||||
|
|
||||||
|
const filterMessages = defineMessages(messagesMap(messages));
|
||||||
|
|
||||||
|
class FilterBarIntl extends Component {
|
||||||
constructor (props) {
|
constructor (props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.handleChange = this.handleChange.bind(this);
|
this.handleChange = this.handleChange.bind(this);
|
||||||
@ -13,13 +19,16 @@ export default class FilterBar extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
|
const {formatMessage} = this.props.intl;
|
||||||
return (
|
return (
|
||||||
<div className="filter">
|
<div className="filter">
|
||||||
<p className="col-xs-12 col-sm-6 col-md-4 col-md-offset-1 filter-legend" id="filterInputDescription">What are we listening to today?</p>
|
<p className="col-xs-12 col-sm-6 col-md-4 col-md-offset-1 filter-legend" id="filterInputDescription">
|
||||||
|
<FormattedMessage {...filterMessages["app.filter.whatAreWeListeningToToday"]} />
|
||||||
|
</p>
|
||||||
<div className="col-xs-12 col-sm-6 col-md-4 input-group">
|
<div className="col-xs-12 col-sm-6 col-md-4 input-group">
|
||||||
<form className="form-inline" onSubmit={this.handleChange} aria-describedby="filterInputDescription">
|
<form className="form-inline" onSubmit={this.handleChange} aria-describedby="filterInputDescription">
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<input type="text" className="form-control filter-input" placeholder="Filter…" aria-label="Filter…" value={this.props.filterText} onChange={this.handleChange} ref="filterTextInput" />
|
<input type="text" className="form-control filter-input" placeholder={formatMessage(filterMessages["app.filter.filter"])} aria-label={formatMessage(filterMessages["app.filter.filter"])} value={this.props.filterText} onChange={this.handleChange} ref="filterTextInput" />
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
@ -28,7 +37,11 @@ export default class FilterBar extends Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
FilterBar.propTypes = {
|
FilterBarIntl.propTypes = {
|
||||||
onUserInput: PropTypes.func,
|
onUserInput: PropTypes.func,
|
||||||
filterText: PropTypes.string
|
filterText: PropTypes.string,
|
||||||
|
intl: intlShape.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export let FilterBar = injectIntl(FilterBarIntl);
|
||||||
|
export default FilterBar;
|
||||||
|
@ -14,12 +14,17 @@ export class GridItem extends Component {
|
|||||||
if (Array.isArray(nSubItems)) {
|
if (Array.isArray(nSubItems)) {
|
||||||
nSubItems = nSubItems.length;
|
nSubItems = nSubItems.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: i18n
|
||||||
var subItemsLabel = this.props.subItemsType;
|
var subItemsLabel = this.props.subItemsType;
|
||||||
if (nSubItems < 2) {
|
if (nSubItems < 2) {
|
||||||
subItemsLabel = subItemsLabel.rstrip("s");
|
subItemsLabel = subItemsLabel.rstrip("s");
|
||||||
}
|
}
|
||||||
|
|
||||||
const to = "/" + this.props.itemsType.rstrip("s") + "/" + this.props.item.id;
|
const to = "/" + this.props.itemsType.rstrip("s") + "/" + this.props.item.id;
|
||||||
const id = "grid-item-" + this.props.item.type + "/" + this.props.item.id;
|
const id = "grid-item-" + this.props.item.type + "/" + this.props.item.id;
|
||||||
|
|
||||||
|
// TODO: i18n
|
||||||
const title = "Go to " + this.props.itemsType.rstrip("s") + " page";
|
const title = "Go to " + this.props.itemsType.rstrip("s") + " page";
|
||||||
return (
|
return (
|
||||||
<div className="grid-item col-xs-6 col-sm-3 placeholders" id={id}>
|
<div className="grid-item col-xs-6 col-sm-3 placeholders" id={id}>
|
||||||
|
@ -1,7 +1,14 @@
|
|||||||
import React, { Component, PropTypes } from "react";
|
import React, { Component, PropTypes } from "react";
|
||||||
import { Link, withRouter } from "react-router";
|
import { Link, withRouter } from "react-router";
|
||||||
|
import { defineMessages, injectIntl, intlShape, FormattedMessage } from "react-intl";
|
||||||
|
|
||||||
export class Pagination extends Component {
|
import { messagesMap } from "../../utils";
|
||||||
|
import commonMessages from "../../locales/messagesDescriptors/common";
|
||||||
|
import messages from "../../locales/messagesDescriptors/elements/Pagination";
|
||||||
|
|
||||||
|
const paginationMessages = defineMessages(messagesMap(Array.concat([], commonMessages, messages)));
|
||||||
|
|
||||||
|
export class PaginationIntl extends Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.buildLinkTo.bind(this);
|
this.buildLinkTo.bind(this);
|
||||||
@ -61,6 +68,7 @@ export class Pagination extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
|
const { formatMessage } = this.props.intl;
|
||||||
const { lowerLimit, upperLimit } = this.computePaginationBounds(this.props.currentPage, this.props.nPages);
|
const { lowerLimit, upperLimit } = this.computePaginationBounds(this.props.currentPage, this.props.nPages);
|
||||||
var pagesButton = [];
|
var pagesButton = [];
|
||||||
var key = 0; // key increment to ensure correct ordering
|
var key = 0; // key increment to ensure correct ordering
|
||||||
@ -68,7 +76,9 @@ export class Pagination extends Component {
|
|||||||
// Push first page
|
// Push first page
|
||||||
pagesButton.push(
|
pagesButton.push(
|
||||||
<li className="page-item" key={key}>
|
<li className="page-item" key={key}>
|
||||||
<Link className="page-link" title="Go to page 1" to={this.buildLinkTo(1)}><span className="sr-only">Go to page </span>1</Link>
|
<Link className="page-link" title={formatMessage(paginationMessages["app.pagination.goToPageWithoutMarkup"], { pageNumber: 1})} to={this.buildLinkTo(1)}>
|
||||||
|
<FormattedMessage {...paginationMessages["app.pagination.goToPage"]} values={{ pageNumber: 1 }} />
|
||||||
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
key++;
|
key++;
|
||||||
@ -88,12 +98,15 @@ export class Pagination extends Component {
|
|||||||
var currentSpan = null;
|
var currentSpan = null;
|
||||||
if (this.props.currentPage == i) {
|
if (this.props.currentPage == i) {
|
||||||
className += " active";
|
className += " active";
|
||||||
currentSpan = <span className="sr-only">(current)</span>;
|
currentSpan = <span className="sr-only">(<FormattedMessage {...paginationMessages["app.pagination.current"]} />)</span>;
|
||||||
}
|
}
|
||||||
const title = "Go to page " + i;
|
const title = formatMessage(paginationMessages["app.pagination.goToPageWithoutMarkup"], { pageNumber: i });
|
||||||
pagesButton.push(
|
pagesButton.push(
|
||||||
<li className={className} key={key}>
|
<li className={className} key={key}>
|
||||||
<Link className="page-link" title={title} to={this.buildLinkTo(i)}><span className="sr-only">Go to page </span>{i} {currentSpan}</Link>
|
<Link className="page-link" title={title} to={this.buildLinkTo(i)}>
|
||||||
|
<FormattedMessage {...paginationMessages["app.pagination.goToPage"]} values={{ pageNumber: i }} />
|
||||||
|
{currentSpan}
|
||||||
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
key++;
|
key++;
|
||||||
@ -108,18 +121,20 @@ export class Pagination extends Component {
|
|||||||
);
|
);
|
||||||
key++;
|
key++;
|
||||||
}
|
}
|
||||||
const title = "Go to page " + this.props.nPages;
|
const title = formatMessage(paginationMessages["app.pagination.goToPageWithoutMarkup"], { pageNumber: this.props.nPages });
|
||||||
// Push last page
|
// Push last page
|
||||||
pagesButton.push(
|
pagesButton.push(
|
||||||
<li className="page-item" key={key}>
|
<li className="page-item" key={key}>
|
||||||
<Link className="page-link" title={title} to={this.buildLinkTo(this.props.nPages)}><span className="sr-only">Go to page </span>{this.props.nPages}</Link>
|
<Link className="page-link" title={title} to={this.buildLinkTo(this.props.nPages)}>
|
||||||
|
<FormattedMessage {...paginationMessages["app.pagination.goToPage"]} values={{ pageNumber: this.props.nPages }} />
|
||||||
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (pagesButton.length > 1) {
|
if (pagesButton.length > 1) {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<nav className="pagination-nav" aria-label="Page navigation">
|
<nav className="pagination-nav" aria-label={formatMessage(paginationMessages["app.pagination.pageNavigation"])}>
|
||||||
<ul className="pagination">
|
<ul className="pagination">
|
||||||
{ pagesButton }
|
{ pagesButton }
|
||||||
</ul>
|
</ul>
|
||||||
@ -128,17 +143,23 @@ export class Pagination extends Component {
|
|||||||
<div className="modal-dialog" role="document">
|
<div className="modal-dialog" role="document">
|
||||||
<div className="modal-content">
|
<div className="modal-content">
|
||||||
<div className="modal-header">
|
<div className="modal-header">
|
||||||
<button type="button" className="close" data-dismiss="modal" aria-label="Close">×</button>
|
<button type="button" className="close" data-dismiss="modal" aria-label={formatMessage(paginationMessages["app.common.close"])}>×</button>
|
||||||
<h4 className="modal-title" id="paginationModalLabel">Page to go to?</h4>
|
<h4 className="modal-title" id="paginationModalLabel">
|
||||||
|
<FormattedMessage {...paginationMessages["app.pagination.pageToGoTo"]} />
|
||||||
|
</h4>
|
||||||
</div>
|
</div>
|
||||||
<div className="modal-body">
|
<div className="modal-body">
|
||||||
<form>
|
<form>
|
||||||
<input className="form-control" autoComplete="off" type="number" ref="pageInput" aria-label="Page number to go to" />
|
<input className="form-control" autoComplete="off" type="number" ref="pageInput" aria-label={formatMessage(paginationMessages["app.pagination.pageToGoTo"])} />
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<div className="modal-footer">
|
<div className="modal-footer">
|
||||||
<button type="button" className="btn btn-default" onClick={this.cancelModalBox.bind(this)}>Cancel</button>
|
<button type="button" className="btn btn-default" onClick={this.cancelModalBox.bind(this)}>
|
||||||
<button type="button" className="btn btn-primary" onClick={this.goToPage.bind(this)}>OK</button>
|
<FormattedMessage {...paginationMessages["app.common.cancel"]} />
|
||||||
|
</button>
|
||||||
|
<button type="button" className="btn btn-primary" onClick={this.goToPage.bind(this)}>
|
||||||
|
<FormattedMessage {...paginationMessages["app.common.go"]} />
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -150,10 +171,12 @@ export class Pagination extends Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Pagination.propTypes = {
|
PaginationIntl.propTypes = {
|
||||||
currentPage: PropTypes.number.isRequired,
|
currentPage: PropTypes.number.isRequired,
|
||||||
location: PropTypes.object.isRequired,
|
location: PropTypes.object.isRequired,
|
||||||
nPages: PropTypes.number.isRequired
|
nPages: PropTypes.number.isRequired,
|
||||||
|
intl: intlShape.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default withRouter(Pagination);
|
export let Pagination = withRouter(injectIntl(PaginationIntl));
|
||||||
|
export default Pagination;
|
||||||
|
@ -1,8 +1,17 @@
|
|||||||
import React, { Component } from "react";
|
import React, { Component, PropTypes } from "react";
|
||||||
import { IndexLink, Link} from "react-router";
|
import { IndexLink, Link} from "react-router";
|
||||||
|
import { defineMessages, injectIntl, intlShape, FormattedMessage } from "react-intl";
|
||||||
|
|
||||||
export default class SidebarLayout extends Component {
|
import { messagesMap } from "../../utils";
|
||||||
|
import commonMessages from "../../locales/messagesDescriptors/common";
|
||||||
|
import messages from "../../locales/messagesDescriptors/layouts/Sidebar";
|
||||||
|
|
||||||
|
// TODO: i18n for artist / album / songs
|
||||||
|
const sidebarLayoutMessages = defineMessages(messagesMap(Array.concat([], commonMessages, messages)));
|
||||||
|
|
||||||
|
export default class SidebarLayoutIntl extends Component {
|
||||||
render () {
|
render () {
|
||||||
|
const { formatMessage } = this.props.intl;
|
||||||
const isActive = {
|
const isActive = {
|
||||||
discover: (this.props.location.pathname == "/discover") ? "active" : "",
|
discover: (this.props.location.pathname == "/discover") ? "active" : "",
|
||||||
browse: (this.props.location.pathname == "/browse") ? "active" : "",
|
browse: (this.props.location.pathname == "/browse") ? "active" : "",
|
||||||
@ -20,26 +29,32 @@ export default class SidebarLayout extends Component {
|
|||||||
<span className="hidden-sm">mpache</span>
|
<span className="hidden-sm">mpache</span>
|
||||||
</IndexLink>
|
</IndexLink>
|
||||||
</h1>
|
</h1>
|
||||||
<nav aria-label="Main navigation menu">
|
<nav aria-label={formatMessage(sidebarLayoutMessages["app.sidebarLayout.mainNavigationMenu"])}>
|
||||||
<div className="navbar text-center icon-navbar">
|
<div className="navbar text-center icon-navbar">
|
||||||
<div className="container-fluid">
|
<div className="container-fluid">
|
||||||
<ul className="nav navbar-nav icon-navbar-nav">
|
<ul className="nav navbar-nav icon-navbar-nav">
|
||||||
<li>
|
<li>
|
||||||
<Link to="/" title="Home">
|
<Link to="/" title={formatMessage(sidebarLayoutMessages["app.sidebarLayout.home"])}>
|
||||||
<span className="glyphicon glyphicon-home" aria-hidden="true"></span>
|
<span className="glyphicon glyphicon-home" aria-hidden="true"></span>
|
||||||
<span className="sr-only">Home</span>
|
<span className="sr-only">
|
||||||
|
<FormattedMessage {...sidebarLayoutMessages["app.sidebarLayout.home"]} />
|
||||||
|
</span>
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<Link to="/settings" title="Settings">
|
<Link to="/settings" title={formatMessage(sidebarLayoutMessages["app.sidebarLayout.settings"])}>
|
||||||
<span className="glyphicon glyphicon-wrench" aria-hidden="true"></span>
|
<span className="glyphicon glyphicon-wrench" aria-hidden="true"></span>
|
||||||
<span className="sr-only">Settings</span>
|
<span className="sr-only">
|
||||||
|
<FormattedMessage {...sidebarLayoutMessages["app.sidebarLayout.settings"]} />
|
||||||
|
</span>
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<Link to="/logout" title="Logout">
|
<Link to="/logout" title={formatMessage(sidebarLayoutMessages["app.sidebarLayout.logout"])}>
|
||||||
<span className="glyphicon glyphicon-off" aria-hidden="true"></span>
|
<span className="glyphicon glyphicon-off" aria-hidden="true"></span>
|
||||||
<span className="sr-only">Logout</span>
|
<span className="sr-only">
|
||||||
|
<FormattedMessage {...sidebarLayoutMessages["app.sidebarLayout.logout"]} />
|
||||||
|
</span>
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
@ -47,33 +62,37 @@ export default class SidebarLayout extends Component {
|
|||||||
</div>
|
</div>
|
||||||
<ul className="nav nav-sidebar">
|
<ul className="nav nav-sidebar">
|
||||||
<li>
|
<li>
|
||||||
<Link to="/discover" title="Discover" className={isActive.discover}>
|
<Link to="/discover" title={formatMessage(sidebarLayoutMessages["app.sidebarLayout.discover"])} className={isActive.discover}>
|
||||||
<span className="glyphicon glyphicon-globe" aria-hidden="true"></span>
|
<span className="glyphicon glyphicon-globe" aria-hidden="true"></span>
|
||||||
<span className="hidden-sm"> Discover</span>
|
<span className="hidden-sm">
|
||||||
|
<FormattedMessage {...sidebarLayoutMessages["app.sidebarLayout.discover"]} />
|
||||||
|
</span>
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<Link to="/browse" title="Browse" className={isActive.browse}>
|
<Link to="/browse" title={formatMessage(sidebarLayoutMessages["app.sidebarLayout.browse"])} className={isActive.browse}>
|
||||||
<span className="glyphicon glyphicon-headphones" aria-hidden="true"></span>
|
<span className="glyphicon glyphicon-headphones" aria-hidden="true"></span>
|
||||||
<span className="hidden-sm"> Browse</span>
|
<span className="hidden-sm">
|
||||||
|
<FormattedMessage {...sidebarLayoutMessages["app.sidebarLayout.browse"]} />
|
||||||
|
</span>
|
||||||
</Link>
|
</Link>
|
||||||
<ul className="nav nav-list text-center">
|
<ul className="nav nav-list text-center">
|
||||||
<li>
|
<li>
|
||||||
<Link to="/artists" title="Browse artists" className={isActive.artists}>
|
<Link to="/artists" title={formatMessage(sidebarLayoutMessages["app.sidebarLayout.browseArtists"])} className={isActive.artists}>
|
||||||
<span className="glyphicon glyphicon-user" aria-hidden="true"></span>
|
<span className="glyphicon glyphicon-user" aria-hidden="true"></span>
|
||||||
<span className="sr-only">Artists</span>
|
<span className="sr-only">Artists</span>
|
||||||
<span className="hidden-sm"> Artists</span>
|
<span className="hidden-sm"> Artists</span>
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<Link to="/albums" title="Browse albums" className={isActive.albums}>
|
<Link to="/albums" title={formatMessage(sidebarLayoutMessages["app.sidebarLayout.browseAlbums"])} className={isActive.albums}>
|
||||||
<span className="glyphicon glyphicon-cd" aria-hidden="true"></span>
|
<span className="glyphicon glyphicon-cd" aria-hidden="true"></span>
|
||||||
<span className="sr-only">Albums</span>
|
<span className="sr-only">Albums</span>
|
||||||
<span className="hidden-sm"> Albums</span>
|
<span className="hidden-sm"> Albums</span>
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<Link to="/songs" title="Browse songs" className={isActive.songs}>
|
<Link to="/songs" title={formatMessage(sidebarLayoutMessages["app.sidebarLayout.browseSongs"])} className={isActive.songs}>
|
||||||
<span className="glyphicon glyphicon-music" aria-hidden="true"></span>
|
<span className="glyphicon glyphicon-music" aria-hidden="true"></span>
|
||||||
<span className="sr-only">Songs</span>
|
<span className="sr-only">Songs</span>
|
||||||
<span className="hidden-sm"> Songs</span>
|
<span className="hidden-sm"> Songs</span>
|
||||||
@ -82,9 +101,11 @@ export default class SidebarLayout extends Component {
|
|||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<Link to="/search" title="Search" className={isActive.search}>
|
<Link to="/search" title={formatMessage(sidebarLayoutMessages["app.sidebarLayout.search"])} className={isActive.search}>
|
||||||
<span className="glyphicon glyphicon-search" aria-hidden="true"></span>
|
<span className="glyphicon glyphicon-search" aria-hidden="true"></span>
|
||||||
<span className="hidden-sm"> Search</span>
|
<span className="hidden-sm">
|
||||||
|
<FormattedMessage {...sidebarLayoutMessages["app.sidebarLayout.search"]} />
|
||||||
|
</span>
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
@ -98,3 +119,12 @@ export default class SidebarLayout extends Component {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
SidebarLayoutIntl.propTypes = {
|
||||||
|
children: PropTypes.node,
|
||||||
|
intl: intlShape.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export let SidebarLayout = injectIntl(SidebarLayoutIntl);
|
||||||
|
export default SidebarLayout;
|
||||||
|
@ -13,6 +13,5 @@ export default class App extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
App.propTypes = {
|
App.propTypes = {
|
||||||
// Injected by React Router
|
|
||||||
children: PropTypes.node,
|
children: PropTypes.node,
|
||||||
};
|
};
|
||||||
|
6
app/dist/1.1.js
vendored
6
app/dist/1.1.js
vendored
File diff suppressed because one or more lines are too long
2
app/dist/1.1.js.map
vendored
2
app/dist/1.1.js.map
vendored
File diff suppressed because one or more lines are too long
2
app/dist/fix.ie9.js
vendored
2
app/dist/fix.ie9.js
vendored
@ -1,2 +1,2 @@
|
|||||||
!function(e){function t(r){if(n[r])return n[r].exports;var a=n[r]={exports:{},id:r,loaded:!1};return e[r].call(a.exports,a,a.exports,t),a.loaded=!0,a.exports}var n={};return t.m=e,t.c=n,t.p="./",t(0)}({0:function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var r=n(520);Object.keys(r).forEach(function(e){"default"!==e&&Object.defineProperty(t,e,{enumerable:!0,get:function(){return r[e]}})})},520:function(e,t){!function(t,n){function r(e,t){var n=e.createElement("p"),r=e.getElementsByTagName("head")[0]||e.documentElement;return n.innerHTML="x<style>"+t+"</style>",r.insertBefore(n.lastChild,r.firstChild)}function a(){var e=b.elements;return"string"==typeof e?e.split(" "):e}function o(e,t){var n=b.elements;"string"!=typeof n&&(n=n.join(" ")),"string"!=typeof e&&(e=e.join(" ")),b.elements=n+" "+e,s(t)}function c(e){var t=E[e[v]];return t||(t={},y++,e[v]=y,E[y]=t),t}function i(e,t,r){if(t||(t=n),f)return t.createElement(e);r||(r=c(t));var a;return a=r.cache[e]?r.cache[e].cloneNode():g.test(e)?(r.cache[e]=r.createElem(e)).cloneNode():r.createElem(e),!a.canHaveChildren||p.test(e)||a.tagUrn?a:r.frag.appendChild(a)}function l(e,t){if(e||(e=n),f)return e.createDocumentFragment();t=t||c(e);for(var r=t.frag.cloneNode(),o=0,i=a(),l=i.length;o<l;o++)r.createElement(i[o]);return r}function u(e,t){t.cache||(t.cache={},t.createElem=e.createElement,t.createFrag=e.createDocumentFragment,t.frag=t.createFrag()),e.createElement=function(n){return b.shivMethods?i(n,e,t):t.createElem(n)},e.createDocumentFragment=Function("h,f","return function(){var n=f.cloneNode(),c=n.createElement;h.shivMethods&&("+a().join().replace(/[\w\-:]+/g,function(e){return t.createElem(e),t.frag.createElement(e),'c("'+e+'")'})+");return n}")(b,t.frag)}function s(e){e||(e=n);var t=c(e);return!b.shivCSS||d||t.hasCSS||(t.hasCSS=!!r(e,"article,aside,dialog,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}mark{background:#FF0;color:#000}template{display:none}")),f||u(e,t),e}var d,f,m="3.7.3-pre",h=t.html5||{},p=/^<|^(?:button|map|select|textarea|object|iframe|option|optgroup)$/i,g=/^(?:a|b|code|div|fieldset|h1|h2|h3|h4|h5|h6|i|label|li|ol|p|q|span|strong|style|table|tbody|td|th|tr|ul)$/i,v="_html5shiv",y=0,E={};!function(){try{var e=n.createElement("a");e.innerHTML="<xyz></xyz>",d="hidden"in e,f=1==e.childNodes.length||function(){n.createElement("a");var e=n.createDocumentFragment();return"undefined"==typeof e.cloneNode||"undefined"==typeof e.createDocumentFragment||"undefined"==typeof e.createElement}()}catch(t){d=!0,f=!0}}();var b={elements:h.elements||"abbr article aside audio bdi canvas data datalist details dialog figcaption figure footer header hgroup main mark meter nav output picture progress section summary template time video",version:m,shivCSS:h.shivCSS!==!1,supportsUnknownElements:f,shivMethods:h.shivMethods!==!1,type:"default",shivDocument:s,createElement:i,createDocumentFragment:l,addElements:o};t.html5=b,s(n),"object"==typeof e&&e.exports&&(e.exports=b)}("undefined"!=typeof window?window:this,document)}});
|
!function(e){function t(r){if(n[r])return n[r].exports;var a=n[r]={exports:{},id:r,loaded:!1};return e[r].call(a.exports,a,a.exports,t),a.loaded=!0,a.exports}var n={};return t.m=e,t.c=n,t.p="./",t(0)}({0:function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var r=n(526);Object.keys(r).forEach(function(e){"default"!==e&&Object.defineProperty(t,e,{enumerable:!0,get:function(){return r[e]}})})},526:function(e,t){!function(t,n){function r(e,t){var n=e.createElement("p"),r=e.getElementsByTagName("head")[0]||e.documentElement;return n.innerHTML="x<style>"+t+"</style>",r.insertBefore(n.lastChild,r.firstChild)}function a(){var e=b.elements;return"string"==typeof e?e.split(" "):e}function o(e,t){var n=b.elements;"string"!=typeof n&&(n=n.join(" ")),"string"!=typeof e&&(e=e.join(" ")),b.elements=n+" "+e,s(t)}function c(e){var t=E[e[v]];return t||(t={},y++,e[v]=y,E[y]=t),t}function i(e,t,r){if(t||(t=n),f)return t.createElement(e);r||(r=c(t));var a;return a=r.cache[e]?r.cache[e].cloneNode():g.test(e)?(r.cache[e]=r.createElem(e)).cloneNode():r.createElem(e),!a.canHaveChildren||p.test(e)||a.tagUrn?a:r.frag.appendChild(a)}function l(e,t){if(e||(e=n),f)return e.createDocumentFragment();t=t||c(e);for(var r=t.frag.cloneNode(),o=0,i=a(),l=i.length;o<l;o++)r.createElement(i[o]);return r}function u(e,t){t.cache||(t.cache={},t.createElem=e.createElement,t.createFrag=e.createDocumentFragment,t.frag=t.createFrag()),e.createElement=function(n){return b.shivMethods?i(n,e,t):t.createElem(n)},e.createDocumentFragment=Function("h,f","return function(){var n=f.cloneNode(),c=n.createElement;h.shivMethods&&("+a().join().replace(/[\w\-:]+/g,function(e){return t.createElem(e),t.frag.createElement(e),'c("'+e+'")'})+");return n}")(b,t.frag)}function s(e){e||(e=n);var t=c(e);return!b.shivCSS||d||t.hasCSS||(t.hasCSS=!!r(e,"article,aside,dialog,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}mark{background:#FF0;color:#000}template{display:none}")),f||u(e,t),e}var d,f,m="3.7.3-pre",h=t.html5||{},p=/^<|^(?:button|map|select|textarea|object|iframe|option|optgroup)$/i,g=/^(?:a|b|code|div|fieldset|h1|h2|h3|h4|h5|h6|i|label|li|ol|p|q|span|strong|style|table|tbody|td|th|tr|ul)$/i,v="_html5shiv",y=0,E={};!function(){try{var e=n.createElement("a");e.innerHTML="<xyz></xyz>",d="hidden"in e,f=1==e.childNodes.length||function(){n.createElement("a");var e=n.createDocumentFragment();return"undefined"==typeof e.cloneNode||"undefined"==typeof e.createDocumentFragment||"undefined"==typeof e.createElement}()}catch(t){d=!0,f=!0}}();var b={elements:h.elements||"abbr article aside audio bdi canvas data datalist details dialog figcaption figure footer header hgroup main mark meter nav output picture progress section summary template time video",version:m,shivCSS:h.shivCSS!==!1,supportsUnknownElements:f,shivMethods:h.shivMethods!==!1,type:"default",shivDocument:s,createElement:i,createDocumentFragment:l,addElements:o};t.html5=b,s(n),"object"==typeof e&&e.exports&&(e.exports=b)}("undefined"!=typeof window?window:this,document)}});
|
||||||
//# sourceMappingURL=fix.ie9.js.map
|
//# sourceMappingURL=fix.ie9.js.map
|
2
app/dist/fix.ie9.js.map
vendored
2
app/dist/fix.ie9.js.map
vendored
File diff suppressed because one or more lines are too long
45
app/dist/index.js
vendored
45
app/dist/index.js
vendored
File diff suppressed because one or more lines are too long
2
app/dist/index.js.map
vendored
2
app/dist/index.js.map
vendored
File diff suppressed because one or more lines are too long
@ -1,7 +1,35 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
"app": {
|
"app.common.album": "Album", // Album
|
||||||
"login": {
|
"app.common.artist": "Artist", // Artist
|
||||||
"greeting": "TODO"
|
"app.common.cancel": "Cancel", // Cancel
|
||||||
}
|
"app.common.close": "Close", // Close
|
||||||
}
|
"app.common.go": "Go", // Go
|
||||||
|
"app.common.song": "Song", // Song
|
||||||
|
"app.filter.filter": "Filter…", // Filtering input placeholder
|
||||||
|
"app.filter.whatAreWeListeningToToday": "What are we listening to today?", // Description for the filter bar
|
||||||
|
"app.login.endpointInputAriaLabel": "URL of your Ampache instance (e.g. http://ampache.example.com)", // ARIA label for the endpoint input
|
||||||
|
"app.login.greeting": "Welcome back on Ampache, let's go!", // Greeting to welcome the user to the app
|
||||||
|
"app.login.password": "Password", // Password input placeholder
|
||||||
|
"app.login.rememberMe": "Remember me", // Remember me checkbox label
|
||||||
|
"app.login.signIn": "Sign in", // Sign in
|
||||||
|
"app.login.username": "Username", // Username input placeholder
|
||||||
|
"app.pagination.current": "current", // Current (page)
|
||||||
|
"app.pagination.goToPage": "<span className=\"sr-only\">Go to page </span>{pageNumber}", // Link content to go to page N. span is here for screen-readers
|
||||||
|
"app.pagination.goToPageWithoutMarkup": "Go to page {pageNumber}", // Link title to go to page N
|
||||||
|
"app.pagination.pageNavigation": "Page navigation", // ARIA label for the nav block containing pagination
|
||||||
|
"app.pagination.pageToGoTo": "Page to go to?", // Title of the pagination modal
|
||||||
|
"app.sidebarLayout.browse": "Browse", // Browse
|
||||||
|
"app.sidebarLayout.browseAlbums": "Browse albums", // Browse albums
|
||||||
|
"app.sidebarLayout.browseArtists": "Browse artists", // Browse artists
|
||||||
|
"app.sidebarLayout.browseSongs": "Browse songs", // Browse songs
|
||||||
|
"app.sidebarLayout.discover": "Discover", // Discover
|
||||||
|
"app.sidebarLayout.home": "Home", // Home
|
||||||
|
"app.sidebarLayout.logout": "Logout", // Logout
|
||||||
|
"app.sidebarLayout.mainNavigationMenu": "Main navigation menu", // ARIA label for the main navigation menu
|
||||||
|
"app.sidebarLayout.search": "Search", // Search
|
||||||
|
"app.sidebarLayout.settings": "Settings", // Settings
|
||||||
|
"app.songs.album": "Album", // Album (song)
|
||||||
|
"app.songs.genre": "Genre", // Genre (song)
|
||||||
|
"app.songs.length": "Length", // Length (song)
|
||||||
|
"app.songs.title": "Title", // Title (song)
|
||||||
};
|
};
|
||||||
|
@ -1,3 +1,35 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
"app.login.greeting": "TODO",
|
"app.common.album": "Album", // Album
|
||||||
|
"app.common.artist": "Artiste", // Artist
|
||||||
|
"app.common.cancel": "Annuler", // Cancel
|
||||||
|
"app.common.close": "Fermer", // Close
|
||||||
|
"app.common.go": "Aller", // Go
|
||||||
|
"app.common.song": "Piste", // Song
|
||||||
|
"app.filter.filter": "Filtrer…", // Filtering input placeholder
|
||||||
|
"app.filter.whatAreWeListeningToToday": "Que voulez-vous écouter aujourd'hui\u00a0?", // Description for the filter bar
|
||||||
|
"app.login.endpointInputAriaLabel": "URL de votre Ampache (e.g. http://ampache.example.com)", // ARIA label for the endpoint input
|
||||||
|
"app.login.greeting": "Bon retour sur Ampache, c'est parti\u00a0!", // Greeting to welcome the user to the app
|
||||||
|
"app.login.password": "Mot de passe", // Password input placeholder
|
||||||
|
"app.login.rememberMe": "Se souvenir", // Remember me checkbox label
|
||||||
|
"app.login.signIn": "Connexion", // Sign in
|
||||||
|
"app.login.username": "Utilisateur", // Username input placeholder
|
||||||
|
"app.pagination.current": "actuelle", // Current (page)
|
||||||
|
"app.pagination.goToPage": "<span className=\"sr-only\">Aller à la page </span>{pageNumber}", // Link content to go to page N. span is here for screen-readers
|
||||||
|
"app.pagination.goToPageWithoutMarkup": "Aller à la page {pageNumber}", // Link title to go to page N
|
||||||
|
"app.pagination.pageNavigation": "Navigation entre les pages", // ARIA label for the nav block containing pagination
|
||||||
|
"app.pagination.pageToGoTo": "Page à laquelle aller\u00a0?", // Title of the pagination modal
|
||||||
|
"app.sidebarLayout.browse": "Explorer", // Browse
|
||||||
|
"app.sidebarLayout.browseAlbums": "Parcourir les albums", // Browse albums
|
||||||
|
"app.sidebarLayout.browseArtists": "Parcourir les artistes", // Browse artists
|
||||||
|
"app.sidebarLayout.browseSongs": "Parcourir les pistes", // Browse songs
|
||||||
|
"app.sidebarLayout.discover": "Découvrir", // Discover
|
||||||
|
"app.sidebarLayout.home": "Accueil", // Home
|
||||||
|
"app.sidebarLayout.logout": "Déconnexion", // Logout
|
||||||
|
"app.sidebarLayout.mainNavigationMenu": "Menu principal", // ARIA label for the main navigation menu
|
||||||
|
"app.sidebarLayout.search": "Rechercher", // Search
|
||||||
|
"app.sidebarLayout.settings": "Préférences", // Settings
|
||||||
|
"app.songs.album": "Album", // Album (song)
|
||||||
|
"app.songs.genre": "Genre", // Genre (song)
|
||||||
|
"app.songs.length": "Durée", // Length (song)
|
||||||
|
"app.songs.title": "Titre", // Title (song)
|
||||||
};
|
};
|
||||||
|
34
app/locales/messagesDescriptors/Login.js
Normal file
34
app/locales/messagesDescriptors/Login.js
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
const messages = [
|
||||||
|
{
|
||||||
|
id: "app.login.username",
|
||||||
|
defaultMessage: "Username",
|
||||||
|
description: "Username input placeholder"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "app.login.password",
|
||||||
|
defaultMessage: "Password",
|
||||||
|
description: "Password input placeholder"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "app.login.signIn",
|
||||||
|
defaultMessage: "Sign in",
|
||||||
|
description: "Sign in"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "app.login.endpointInputAriaLabel",
|
||||||
|
defaultMessage: "URL of your Ampache instance (e.g. http://ampache.example.com)",
|
||||||
|
description: "ARIA label for the endpoint input"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "app.login.rememberMe",
|
||||||
|
description: "Remember me checkbox label",
|
||||||
|
defaultMessage: "Remember me"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "app.login.greeting",
|
||||||
|
description: "Greeting to welcome the user to the app",
|
||||||
|
defaultMessage: "Welcome back on Ampache, let's go!"
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
export default messages;
|
24
app/locales/messagesDescriptors/Songs.js
Normal file
24
app/locales/messagesDescriptors/Songs.js
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
const messages = [
|
||||||
|
{
|
||||||
|
"id": "app.songs.title",
|
||||||
|
"description": "Title (song)",
|
||||||
|
"defaultMessage": "Title"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "app.songs.album",
|
||||||
|
"description": "Album (song)",
|
||||||
|
"defaultMessage": "Album"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "app.songs.genre",
|
||||||
|
"description": "Genre (song)",
|
||||||
|
"defaultMessage": "Genre"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "app.songs.length",
|
||||||
|
"description": "Length (song)",
|
||||||
|
"defaultMessage": "Length"
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
export default messages;
|
34
app/locales/messagesDescriptors/common.js
Normal file
34
app/locales/messagesDescriptors/common.js
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
const messages = [
|
||||||
|
{
|
||||||
|
id: "app.common.close",
|
||||||
|
defaultMessage: "Close",
|
||||||
|
description: "Close"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "app.common.cancel",
|
||||||
|
description: "Cancel",
|
||||||
|
defaultMessage: "Cancel"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "app.common.go",
|
||||||
|
description: "Go",
|
||||||
|
defaultMessage: "Go"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "app.common.artist",
|
||||||
|
description: "Artist",
|
||||||
|
defaultMessage: "Artist"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "app.common.album",
|
||||||
|
description: "Album",
|
||||||
|
defaultMessage: "Album"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "app.common.song",
|
||||||
|
description: "Song",
|
||||||
|
defaultMessage: "Song"
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export default messages;
|
14
app/locales/messagesDescriptors/elements/FilterBar.js
Normal file
14
app/locales/messagesDescriptors/elements/FilterBar.js
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
const messages = [
|
||||||
|
{
|
||||||
|
id: "app.filter.filter",
|
||||||
|
defaultMessage: "Filter…",
|
||||||
|
description: "Filtering input placeholder"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "app.filter.whatAreWeListeningToToday",
|
||||||
|
description: "Description for the filter bar",
|
||||||
|
defaultMessage: "What are we listening to today?"
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export default messages;
|
29
app/locales/messagesDescriptors/elements/Pagination.js
Normal file
29
app/locales/messagesDescriptors/elements/Pagination.js
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
const messages = [
|
||||||
|
{
|
||||||
|
id: "app.pagination.goToPage",
|
||||||
|
defaultMessage: "<span className=\"sr-only\">Go to page </span>{pageNumber}",
|
||||||
|
description: "Link content to go to page N. span is here for screen-readers"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "app.pagination.goToPageWithoutMarkup",
|
||||||
|
defaultMessage: "Go to page {pageNumber}",
|
||||||
|
description: "Link title to go to page N"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "app.pagination.pageNavigation",
|
||||||
|
defaultMessage: "Page navigation",
|
||||||
|
description: "ARIA label for the nav block containing pagination"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "app.pagination.pageToGoTo",
|
||||||
|
description: "Title of the pagination modal",
|
||||||
|
defaultMessage: "Page to go to?"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "app.pagination.current",
|
||||||
|
description: "Current (page)",
|
||||||
|
defaultMessage: "current"
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
export default messages;
|
54
app/locales/messagesDescriptors/layouts/Sidebar.js
Normal file
54
app/locales/messagesDescriptors/layouts/Sidebar.js
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
const messages = [
|
||||||
|
{
|
||||||
|
id: "app.sidebarLayout.mainNavigationMenu",
|
||||||
|
description: "ARIA label for the main navigation menu",
|
||||||
|
defaultMessage: "Main navigation menu"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "app.sidebarLayout.home",
|
||||||
|
description: "Home",
|
||||||
|
defaultMessage: "Home"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "app.sidebarLayout.settings",
|
||||||
|
description: "Settings",
|
||||||
|
defaultMessage: "Settings"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "app.sidebarLayout.logout",
|
||||||
|
description: "Logout",
|
||||||
|
defaultMessage: "Logout"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "app.sidebarLayout.discover",
|
||||||
|
description: "Discover",
|
||||||
|
defaultMessage: "Discover"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "app.sidebarLayout.browse",
|
||||||
|
description: "Browse",
|
||||||
|
defaultMessage: "Browse"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "app.sidebarLayout.browseArtists",
|
||||||
|
description: "Browse artists",
|
||||||
|
defaultMessage: "Browse artists"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "app.sidebarLayout.browseAlbums",
|
||||||
|
description: "Browse albums",
|
||||||
|
defaultMessage: "Browse albums"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "app.sidebarLayout.browseSongs",
|
||||||
|
description: "Browse songs",
|
||||||
|
defaultMessage: "Browse songs"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "app.sidebarLayout.search",
|
||||||
|
description: "Search",
|
||||||
|
defaultMessage: "Search"
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
export default messages;
|
@ -19,3 +19,13 @@ export function getBrowserLocale () {
|
|||||||
|
|
||||||
return locale;
|
return locale;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function messagesMap(messagesDescriptorsArray) {
|
||||||
|
var messagesDescriptorsMap = {};
|
||||||
|
|
||||||
|
messagesDescriptorsArray.forEach(function (item) {
|
||||||
|
messagesDescriptorsMap[item.id] = item;
|
||||||
|
});
|
||||||
|
|
||||||
|
return messagesDescriptorsMap;
|
||||||
|
}
|
||||||
|
57
index.all.js
57
index.all.js
@ -25,7 +25,7 @@ const history = syncHistoryWithStore(hashHistory, store);
|
|||||||
const rootElement = document.getElementById("root");
|
const rootElement = document.getElementById("root");
|
||||||
|
|
||||||
// i18n
|
// i18n
|
||||||
const onWindowIntl = () => {
|
export const onWindowIntl = () => {
|
||||||
addLocaleData([...en, ...fr]);
|
addLocaleData([...en, ...fr]);
|
||||||
const locale = getBrowserLocale();
|
const locale = getBrowserLocale();
|
||||||
var strings = rawMessages[locale] ? rawMessages[locale] : rawMessages["en-US"];
|
var strings = rawMessages[locale] ? rawMessages[locale] : rawMessages["en-US"];
|
||||||
@ -39,43 +39,22 @@ const onWindowIntl = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
if (module.hot) {
|
return render;
|
||||||
// Support hot reloading of components
|
|
||||||
// and display an overlay for runtime errors
|
|
||||||
const renderApp = render;
|
|
||||||
const renderError = (error) => {
|
|
||||||
const RedBox = require("redbox-react");
|
|
||||||
ReactDOM.render(
|
|
||||||
<RedBox error={error} />,
|
|
||||||
rootElement
|
|
||||||
);
|
|
||||||
};
|
|
||||||
render = () => {
|
|
||||||
try {
|
|
||||||
renderApp();
|
|
||||||
} catch (error) {
|
|
||||||
renderError(error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
module.hot.accept("./app/containers/Root", () => {
|
|
||||||
setTimeout(render);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
render();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!window.Intl) {
|
export const Intl = (render) => {
|
||||||
require.ensure([
|
if (!window.Intl) {
|
||||||
"intl",
|
require.ensure([
|
||||||
"intl/locale-data/jsonp/en.js",
|
"intl",
|
||||||
"intl/locale-data/jsonp/fr.js"
|
"intl/locale-data/jsonp/en.js",
|
||||||
], function (require) {
|
"intl/locale-data/jsonp/fr.js"
|
||||||
require("intl");
|
], function (require) {
|
||||||
require("intl/locale-data/jsonp/en.js");
|
require("intl");
|
||||||
require("intl/locale-data/jsonp/fr.js");
|
require("intl/locale-data/jsonp/en.js");
|
||||||
onWindowIntl();
|
require("intl/locale-data/jsonp/fr.js");
|
||||||
});
|
render();
|
||||||
} else {
|
});
|
||||||
onWindowIntl();
|
} else {
|
||||||
}
|
render();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
@ -4,4 +4,30 @@ import ReactDOM from "react-dom";
|
|||||||
var a11y = require("react-a11y");
|
var a11y = require("react-a11y");
|
||||||
a11y(React, { ReactDOM: ReactDOM, includeSrcNode: true });
|
a11y(React, { ReactDOM: ReactDOM, includeSrcNode: true });
|
||||||
|
|
||||||
require("./index.all.js");
|
const index = require("./index.all.js");
|
||||||
|
|
||||||
|
var render = index.onWindowIntl();
|
||||||
|
if (process.env.NODE_ENV !== "production" && module.hot) {
|
||||||
|
// Support hot reloading of components
|
||||||
|
// and display an overlay for runtime errors
|
||||||
|
const renderApp = render;
|
||||||
|
const renderError = (error) => {
|
||||||
|
const RedBox = require("redbox-react");
|
||||||
|
ReactDOM.render(
|
||||||
|
<RedBox error={error} />,
|
||||||
|
index.rootElement
|
||||||
|
);
|
||||||
|
};
|
||||||
|
render = () => {
|
||||||
|
try {
|
||||||
|
renderApp();
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
renderError(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
module.hot.accept("./app/containers/Root", () => {
|
||||||
|
setTimeout(render);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
index.Intl(render);
|
||||||
|
@ -1 +1,3 @@
|
|||||||
require("./index.all.js");
|
const index = require("./index.all.js");
|
||||||
|
const render = index.onWindowIntl();
|
||||||
|
index.Intl(render);
|
||||||
|
10
package.json
10
package.json
@ -7,8 +7,11 @@
|
|||||||
"homepage": "https://github.com/Phyks/ampache_react",
|
"homepage": "https://github.com/Phyks/ampache_react",
|
||||||
"repository": "git+https://github.com/Phyks/ampache_react.git",
|
"repository": "git+https://github.com/Phyks/ampache_react.git",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "./node_modules/webpack/bin/webpack.js --progress",
|
"build": "./node_modules/.bin/webpack --progress",
|
||||||
"watch": "./node_modules/webpack/bin/webpack.js --progress --watch"
|
"watch": "./node_modules/.bin/webpack --progress --watch",
|
||||||
|
"prod": "NODE_ENV=production ./node_modules/.bin/webpack --progress",
|
||||||
|
"extractTranslations": "./node_modules/.bin/babel-node scripts/extractTranslations.js",
|
||||||
|
"clean": "./node_modules/.bin/rimraf app/dist"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"babel-polyfill": "^6.9.1",
|
"babel-polyfill": "^6.9.1",
|
||||||
@ -38,6 +41,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"autoprefixer": "^6.3.7",
|
"autoprefixer": "^6.3.7",
|
||||||
|
"babel-cli": "^6.11.4",
|
||||||
"babel-core": "^6.10.4",
|
"babel-core": "^6.10.4",
|
||||||
"babel-loader": "^6.2.4",
|
"babel-loader": "^6.2.4",
|
||||||
"babel-plugin-react-intl": "^2.1.3",
|
"babel-plugin-react-intl": "^2.1.3",
|
||||||
@ -51,6 +55,7 @@
|
|||||||
"eventsource-polyfill": "^0.9.6",
|
"eventsource-polyfill": "^0.9.6",
|
||||||
"extract-text-webpack-plugin": "^1.0.1",
|
"extract-text-webpack-plugin": "^1.0.1",
|
||||||
"file-loader": "^0.9.0",
|
"file-loader": "^0.9.0",
|
||||||
|
"glob": "^7.0.5",
|
||||||
"postcss": "^5.1.0",
|
"postcss": "^5.1.0",
|
||||||
"postcss-loader": "^0.9.1",
|
"postcss-loader": "^0.9.1",
|
||||||
"postcss-reporter": "^1.4.1",
|
"postcss-reporter": "^1.4.1",
|
||||||
@ -59,6 +64,7 @@
|
|||||||
"react-intl-webpack-plugin": "0.0.3",
|
"react-intl-webpack-plugin": "0.0.3",
|
||||||
"redbox-react": "^1.2.10",
|
"redbox-react": "^1.2.10",
|
||||||
"redux-logger": "^2.6.1",
|
"redux-logger": "^2.6.1",
|
||||||
|
"rimraf": "^2.5.4",
|
||||||
"style-loader": "^0.13.1",
|
"style-loader": "^0.13.1",
|
||||||
"stylelint": "^7.0.3",
|
"stylelint": "^7.0.3",
|
||||||
"stylelint-config-standard": "^11.0.0",
|
"stylelint-config-standard": "^11.0.0",
|
||||||
|
37
scripts/extractTranslations.js
Normal file
37
scripts/extractTranslations.js
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import * as fs from 'fs';
|
||||||
|
import {sync as globSync} from 'glob';
|
||||||
|
|
||||||
|
const MESSAGES_PATTERN = './app/locales/messagesDescriptors/**/*.js';
|
||||||
|
|
||||||
|
// Aggregates the default messages that were extracted from the example app's
|
||||||
|
// React components via the React Intl Babel plugin. An error will be thrown if
|
||||||
|
// there are messages in different components that use the same `id`. The result
|
||||||
|
// is a flat collection of `id: message` pairs for the app's default locale.
|
||||||
|
let defaultMessages = globSync(MESSAGES_PATTERN)
|
||||||
|
.map((filename) => require("../" + filename).default)
|
||||||
|
.reduce((collection, descriptors) => {
|
||||||
|
descriptors.forEach(({id, description, defaultMessage}) => {
|
||||||
|
if (collection.hasOwnProperty(id)) {
|
||||||
|
throw new Error(`Duplicate message id: ${id}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
collection.push({
|
||||||
|
id: id,
|
||||||
|
description: description,
|
||||||
|
defaultMessage: defaultMessage
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return collection;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Sort by id
|
||||||
|
defaultMessages = defaultMessages.sort(function (item1, item2) {
|
||||||
|
return item1.id.localeCompare(item2.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("module.exports = {");
|
||||||
|
defaultMessages.forEach(function (item) {
|
||||||
|
console.log(" " + JSON.stringify(item.id) + ": " + JSON.stringify(item.defaultMessage) + ", // " + item.description);
|
||||||
|
});
|
||||||
|
console.log("};");
|
@ -37,7 +37,7 @@ module.exports = {
|
|||||||
exclude: /node_modules/,
|
exclude: /node_modules/,
|
||||||
loader: "babel",
|
loader: "babel",
|
||||||
query: {
|
query: {
|
||||||
"cacheDirectory": true
|
"cacheDirectory": ".cache/"
|
||||||
},
|
},
|
||||||
include: __dirname
|
include: __dirname
|
||||||
},
|
},
|
||||||
|
Loading…
Reference in New Issue
Block a user