diff --git a/.babelrc b/.babelrc index 567f9ae..d4e5f8d 100644 --- a/.babelrc +++ b/.babelrc @@ -1,9 +1,3 @@ { - "presets": ["es2015", "react"], - "plugins": [ - ["react-intl", { - "messagesDir": "./app/dist/i18n/", - "enforceDescriptions": true - }] - ] + "presets": ["es2015", "react"] } diff --git a/.gitignore b/.gitignore index 77f0c6e..5d7a93c 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..c2c711e --- /dev/null +++ b/CONTRIBUTING.md @@ -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. diff --git a/README.md b/README.md index f98aa77..5c56c33 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/TODO b/TODO index 77b1a1a..8fbe713 100644 --- a/TODO +++ b/TODO @@ -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? diff --git a/app/components/Login.jsx b/app/components/Login.jsx index 12b59b9..8c00f7e 100644 --- a/app/components/Login.jsx +++ b/app/components/Login.jsx @@ -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 (
+ { /* TODO: info/error translation */ } { this.props.error ?
@@ -72,17 +79,17 @@ export class LoginForm extends Component {
- +
- +
- +
@@ -90,11 +97,12 @@ export class LoginForm extends Component {
- +
@@ -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 = ( +

+ +

+ ); return (

Ampache


-

+ {(!this.props.error && !this.props.info) ? greeting : null}
diff --git a/app/components/Songs.jsx b/app/components/Songs.jsx index 5d364cf..82b774b 100644 --- a/app/components/Songs.jsx +++ b/app/components/Songs.jsx @@ -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 { - Title - Artist - Album - Genre - Length + + + + + + + + + + + + + + + {rows} diff --git a/app/components/elements/FilterBar.jsx b/app/components/elements/FilterBar.jsx index e0da87e..b7fc551 100644 --- a/app/components/elements/FilterBar.jsx +++ b/app/components/elements/FilterBar.jsx @@ -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 (
-

What are we listening to today?

+

+ +

- +
@@ -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; diff --git a/app/components/elements/Grid.jsx b/app/components/elements/Grid.jsx index 1ffcef3..d69d03a 100644 --- a/app/components/elements/Grid.jsx +++ b/app/components/elements/Grid.jsx @@ -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 (
diff --git a/app/components/elements/Pagination.jsx b/app/components/elements/Pagination.jsx index 7ba447e..bd0dc44 100644 --- a/app/components/elements/Pagination.jsx +++ b/app/components/elements/Pagination.jsx @@ -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(
  • - Go to page 1 + + +
  • ); key++; @@ -88,12 +98,15 @@ export class Pagination extends Component { var currentSpan = null; if (this.props.currentPage == i) { className += " active"; - currentSpan = (current); + currentSpan = (); } - const title = "Go to page " + i; + const title = formatMessage(paginationMessages["app.pagination.goToPageWithoutMarkup"], { pageNumber: i }); pagesButton.push(
  • - Go to page {i} {currentSpan} + + + {currentSpan} +
  • ); 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(
  • - Go to page {this.props.nPages} + + +
  • ); } if (pagesButton.length > 1) { return (
    -