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"], "presets": ["es2015", "react"]
"plugins": [
["react-intl", {
"messagesDir": "./app/dist/i18n/",
"enforceDescriptions": true
}]
]
} }

2
.gitignore vendored
View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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">&times;</button> <button type="button" className="close" data-dismiss="modal" aria-label={formatMessage(paginationMessages["app.common.close"])}>&times;</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;

View File

@ -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">
&nbsp;<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">
&nbsp;<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">
&nbsp;<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;

View File

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

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 //# 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 = { 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)
}; };

View File

@ -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)
}; };

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; 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"); 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,33 +39,11 @@ 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) => {
if (!window.Intl) {
require.ensure([ require.ensure([
"intl", "intl",
"intl/locale-data/jsonp/en.js", "intl/locale-data/jsonp/en.js",
@ -74,8 +52,9 @@ if (!window.Intl) {
require("intl"); require("intl");
require("intl/locale-data/jsonp/en.js"); require("intl/locale-data/jsonp/en.js");
require("intl/locale-data/jsonp/fr.js"); require("intl/locale-data/jsonp/fr.js");
onWindowIntl(); render();
}); });
} else { } else {
onWindowIntl(); render();
} }
};

View File

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

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", "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",

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/, exclude: /node_modules/,
loader: "babel", loader: "babel",
query: { query: {
"cacheDirectory": true "cacheDirectory": ".cache/"
}, },
include: __dirname include: __dirname
}, },