Translations improved

* Translation system in place
* French translations available
This commit is contained in:
Lucas Verney 2016-07-28 23:14:52 +02:00
parent e6b9c3bf07
commit 945b218504
33 changed files with 595 additions and 145 deletions

View File

@ -1,9 +1,3 @@
{
"presets": ["es2015", "react"],
"plugins": [
["react-intl", {
"messagesDir": "./app/dist/i18n/",
"enforceDescriptions": true
}]
]
"presets": ["es2015", "react"]
}

2
.gitignore vendored
View File

@ -4,4 +4,4 @@ app/dist/webpackHotMiddlewareClient.js
app/dist/*.hot-update.json
app/dist/*.hot-update.js
app/dist/i18n/
app/dist/3.3.js
.cache

33
CONTRIBUTING.md Normal file
View 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.

View File

@ -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
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
This code is distributed under an MIT license.

6
TODO
View File

@ -11,19 +11,21 @@
* Move CSS in modules
=> https://github.com/gajus/react-css-modules
# API middleware
* https://github.com/reactjs/redux/issues/1824#issuecomment-228609501
* https://medium.com/@adamrackis/querying-a-redux-store-37db8c7f3b0f#.eezt3dano
* https://github.com/reactjs/redux/issues/644
* https://github.com/peterpme/redux-crud-api-middleware/blob/master/README.md
* https://github.com/madou/armory-front/tree/master/src/app/reducers
* Immutable.js (?) + get rid of lodash
## Global UI
* What happens when JS is off?
=> https://www.allantatter.com/react-js-and-progressive-enhancement/
* Back button?
## Miscellaneous
* See TODOs in the code
* Babel transform runtime
* Webpack chunks?

View File

@ -1,7 +1,12 @@
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) {
super(props);
@ -45,8 +50,10 @@ export class LoginForm extends Component {
}
render () {
const {formatMessage} = this.props.intl;
return (
<div>
{ /* TODO: info/error translation */ }
{
this.props.error ?
<div className="row">
@ -72,17 +79,17 @@ export class LoginForm extends Component {
<div className="row">
<div className="form-group" ref="usernameFormGroup">
<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 className="form-group" ref="passwordFormGroup">
<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 className="form-group" ref="endpointFormGroup">
<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 className="form-group">
@ -90,11 +97,12 @@ export class LoginForm extends Component {
<div className="row">
<div className="col-sm-6 col-xs-12 checkbox">
<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>
</div>
<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>
@ -107,24 +115,32 @@ export class LoginForm extends Component {
}
}
LoginForm.propTypes = {
LoginFormIntl.propTypes = {
username: PropTypes.string,
endpoint: PropTypes.string,
rememberMe: PropTypes.bool,
onSubmit: PropTypes.func.isRequired,
isAuthenticating: PropTypes.bool,
error: PropTypes.string,
info: PropTypes.string
info: PropTypes.string,
intl: intlShape.isRequired,
};
export let LoginForm = injectIntl(LoginFormIntl);
export default class Login extends Component {
render () {
const greeting = (
<p>
<FormattedMessage {...loginMessages["app.login.greeting"]} />
</p>
);
return (
<div className="login text-center container-fluid">
<h1><img src="./app/assets/img/ampache-blue.png" alt="A"/>mpache</h1>
<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">
<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>

View File

@ -1,10 +1,16 @@
import React, { Component, PropTypes } from "react";
import { Link} from "react-router";
import { defineMessages, FormattedMessage } from "react-intl";
import Fuse from "fuse.js";
import FilterBar from "./elements/FilterBar";
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 {
render () {
@ -55,11 +61,21 @@ export class SongsTable extends Component {
<thead>
<tr>
<th></th>
<th>Title</th>
<th>Artist</th>
<th>Album</th>
<th>Genre</th>
<th>Length</th>
<th>
<FormattedMessage {...songsMessages["app.songs.title"]} />
</th>
<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>
</thead>
<tbody>{rows}</tbody>

View File

@ -1,6 +1,12 @@
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) {
super(props);
this.handleChange = this.handleChange.bind(this);
@ -13,13 +19,16 @@ export default class FilterBar extends Component {
}
render () {
const {formatMessage} = this.props.intl;
return (
<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">
<form className="form-inline" onSubmit={this.handleChange} aria-describedby="filterInputDescription">
<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>
</form>
</div>
@ -28,7 +37,11 @@ export default class FilterBar extends Component {
}
}
FilterBar.propTypes = {
FilterBarIntl.propTypes = {
onUserInput: PropTypes.func,
filterText: PropTypes.string
filterText: PropTypes.string,
intl: intlShape.isRequired
};
export let FilterBar = injectIntl(FilterBarIntl);
export default FilterBar;

View File

@ -14,12 +14,17 @@ export class GridItem extends Component {
if (Array.isArray(nSubItems)) {
nSubItems = nSubItems.length;
}
// TODO: i18n
var subItemsLabel = this.props.subItemsType;
if (nSubItems < 2) {
subItemsLabel = subItemsLabel.rstrip("s");
}
const to = "/" + this.props.itemsType.rstrip("s") + "/" + 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";
return (
<div className="grid-item col-xs-6 col-sm-3 placeholders" id={id}>

View File

@ -1,7 +1,14 @@
import React, { Component, PropTypes } from "react";
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) {
super(props);
this.buildLinkTo.bind(this);
@ -61,6 +68,7 @@ export class Pagination extends Component {
}
render () {
const { formatMessage } = this.props.intl;
const { lowerLimit, upperLimit } = this.computePaginationBounds(this.props.currentPage, this.props.nPages);
var pagesButton = [];
var key = 0; // key increment to ensure correct ordering
@ -68,7 +76,9 @@ export class Pagination extends Component {
// Push first page
pagesButton.push(
<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>
);
key++;
@ -88,12 +98,15 @@ export class Pagination extends Component {
var currentSpan = null;
if (this.props.currentPage == i) {
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(
<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>
);
key++;
@ -108,18 +121,20 @@ export class Pagination extends Component {
);
key++;
}
const title = "Go to page " + this.props.nPages;
const title = formatMessage(paginationMessages["app.pagination.goToPageWithoutMarkup"], { pageNumber: this.props.nPages });
// Push last page
pagesButton.push(
<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>
);
}
if (pagesButton.length > 1) {
return (
<div>
<nav className="pagination-nav" aria-label="Page navigation">
<nav className="pagination-nav" aria-label={formatMessage(paginationMessages["app.pagination.pageNavigation"])}>
<ul className="pagination">
{ pagesButton }
</ul>
@ -128,17 +143,23 @@ export class Pagination extends Component {
<div className="modal-dialog" role="document">
<div className="modal-content">
<div className="modal-header">
<button type="button" className="close" data-dismiss="modal" aria-label="Close">&times;</button>
<h4 className="modal-title" id="paginationModalLabel">Page to go to?</h4>
<button type="button" className="close" data-dismiss="modal" aria-label={formatMessage(paginationMessages["app.common.close"])}>&times;</button>
<h4 className="modal-title" id="paginationModalLabel">
<FormattedMessage {...paginationMessages["app.pagination.pageToGoTo"]} />
</h4>
</div>
<div className="modal-body">
<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>
</div>
<div className="modal-footer">
<button type="button" className="btn btn-default" onClick={this.cancelModalBox.bind(this)}>Cancel</button>
<button type="button" className="btn btn-primary" onClick={this.goToPage.bind(this)}>OK</button>
<button type="button" className="btn btn-default" onClick={this.cancelModalBox.bind(this)}>
<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>
@ -150,10 +171,12 @@ export class Pagination extends Component {
}
}
Pagination.propTypes = {
PaginationIntl.propTypes = {
currentPage: PropTypes.number.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;

View File

@ -1,8 +1,17 @@
import React, { Component } from "react";
import React, { Component, PropTypes } from "react";
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 () {
const { formatMessage } = this.props.intl;
const isActive = {
discover: (this.props.location.pathname == "/discover") ? "active" : "",
browse: (this.props.location.pathname == "/browse") ? "active" : "",
@ -20,26 +29,32 @@ export default class SidebarLayout extends Component {
<span className="hidden-sm">mpache</span>
</IndexLink>
</h1>
<nav aria-label="Main navigation menu">
<nav aria-label={formatMessage(sidebarLayoutMessages["app.sidebarLayout.mainNavigationMenu"])}>
<div className="navbar text-center icon-navbar">
<div className="container-fluid">
<ul className="nav navbar-nav icon-navbar-nav">
<li>
<Link to="/" title="Home">
<Link to="/" title={formatMessage(sidebarLayoutMessages["app.sidebarLayout.home"])}>
<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>
</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="sr-only">Settings</span>
<span className="sr-only">
<FormattedMessage {...sidebarLayoutMessages["app.sidebarLayout.settings"]} />
</span>
</Link>
</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="sr-only">Logout</span>
<span className="sr-only">
<FormattedMessage {...sidebarLayoutMessages["app.sidebarLayout.logout"]} />
</span>
</Link>
</li>
</ul>
@ -47,33 +62,37 @@ export default class SidebarLayout extends Component {
</div>
<ul className="nav nav-sidebar">
<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="hidden-sm"> Discover</span>
<span className="hidden-sm">
&nbsp;<FormattedMessage {...sidebarLayoutMessages["app.sidebarLayout.discover"]} />
</span>
</Link>
</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="hidden-sm"> Browse</span>
<span className="hidden-sm">
&nbsp;<FormattedMessage {...sidebarLayoutMessages["app.sidebarLayout.browse"]} />
</span>
</Link>
<ul className="nav nav-list text-center">
<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="sr-only">Artists</span>
<span className="hidden-sm"> Artists</span>
</Link>
</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="sr-only">Albums</span>
<span className="hidden-sm"> Albums</span>
</Link>
</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="sr-only">Songs</span>
<span className="hidden-sm"> Songs</span>
@ -82,9 +101,11 @@ export default class SidebarLayout extends Component {
</ul>
</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="hidden-sm"> Search</span>
<span className="hidden-sm">
&nbsp;<FormattedMessage {...sidebarLayoutMessages["app.sidebarLayout.search"]} />
</span>
</Link>
</li>
</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;

View File

@ -13,6 +13,5 @@ export default class App extends Component {
}
App.propTypes = {
// Injected by React Router
children: PropTypes.node,
};

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

File diff suppressed because one or more lines are too long

2
app/dist/fix.ie9.js vendored
View File

@ -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

File diff suppressed because one or more lines are too long

45
app/dist/index.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,7 +1,35 @@
module.exports = {
"app": {
"login": {
"greeting": "TODO"
}
}
"app.common.album": "Album", // Album
"app.common.artist": "Artist", // Artist
"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)
};

View File

@ -1,3 +1,35 @@
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)
};

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View File

@ -19,3 +19,13 @@ export function getBrowserLocale () {
return locale;
}
export function messagesMap(messagesDescriptorsArray) {
var messagesDescriptorsMap = {};
messagesDescriptorsArray.forEach(function (item) {
messagesDescriptorsMap[item.id] = item;
});
return messagesDescriptorsMap;
}

View File

@ -25,7 +25,7 @@ const history = syncHistoryWithStore(hashHistory, store);
const rootElement = document.getElementById("root");
// i18n
const onWindowIntl = () => {
export const onWindowIntl = () => {
addLocaleData([...en, ...fr]);
const locale = getBrowserLocale();
var strings = rawMessages[locale] ? rawMessages[locale] : rawMessages["en-US"];
@ -39,32 +39,10 @@ const onWindowIntl = () => {
);
};
if (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} />,
rootElement
);
};
render = () => {
try {
renderApp();
} catch (error) {
renderError(error);
}
};
module.hot.accept("./app/containers/Root", () => {
setTimeout(render);
});
}
render();
return render;
};
export const Intl = (render) => {
if (!window.Intl) {
require.ensure([
"intl",
@ -74,8 +52,9 @@ if (!window.Intl) {
require("intl");
require("intl/locale-data/jsonp/en.js");
require("intl/locale-data/jsonp/fr.js");
onWindowIntl();
render();
});
} else {
onWindowIntl();
render();
}
};

View File

@ -4,4 +4,30 @@ import ReactDOM from "react-dom";
var a11y = require("react-a11y");
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);

View File

@ -1 +1,3 @@
require("./index.all.js");
const index = require("./index.all.js");
const render = index.onWindowIntl();
index.Intl(render);

View File

@ -7,8 +7,11 @@
"homepage": "https://github.com/Phyks/ampache_react",
"repository": "git+https://github.com/Phyks/ampache_react.git",
"scripts": {
"build": "./node_modules/webpack/bin/webpack.js --progress",
"watch": "./node_modules/webpack/bin/webpack.js --progress --watch"
"build": "./node_modules/.bin/webpack --progress",
"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": {
"babel-polyfill": "^6.9.1",
@ -38,6 +41,7 @@
},
"devDependencies": {
"autoprefixer": "^6.3.7",
"babel-cli": "^6.11.4",
"babel-core": "^6.10.4",
"babel-loader": "^6.2.4",
"babel-plugin-react-intl": "^2.1.3",
@ -51,6 +55,7 @@
"eventsource-polyfill": "^0.9.6",
"extract-text-webpack-plugin": "^1.0.1",
"file-loader": "^0.9.0",
"glob": "^7.0.5",
"postcss": "^5.1.0",
"postcss-loader": "^0.9.1",
"postcss-reporter": "^1.4.1",
@ -59,6 +64,7 @@
"react-intl-webpack-plugin": "0.0.3",
"redbox-react": "^1.2.10",
"redux-logger": "^2.6.1",
"rimraf": "^2.5.4",
"style-loader": "^0.13.1",
"stylelint": "^7.0.3",
"stylelint-config-standard": "^11.0.0",

View 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("};");

View File

@ -37,7 +37,7 @@ module.exports = {
exclude: /node_modules/,
loader: "babel",
query: {
"cacheDirectory": true
"cacheDirectory": ".cache/"
},
include: __dirname
},