Browse Source

Translations improved

* Translation system in place
* French translations available
Phyks (Lucas Verney) 5 years ago
parent
commit
945b218504

+ 1
- 7
.babelrc View File

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

+ 1
- 1
.gitignore View File

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

+ 33
- 0
CONTRIBUTING.md View File

@@ -0,0 +1,33 @@
1
+Contributing
2
+============
3
+
4
+## Building
5
+
6
+See `README.md` for instructions on how to build. Build is done with
7
+`webpack`.
8
+
9
+
10
+## Useful scripts
11
+
12
+A few `npm` scripts are provided:
13
+* `npm run build` to trigger a dev build.
14
+* `npm run watch` to trigger a dev build and rebuild on changes.
15
+* `npm run prod` to trigger a production build.
16
+* `npm run clean` to clean the `app/dist` folder.
17
+* `npm run extractTranslations` to generate a translation file (see below).
18
+
19
+
20
+## Translating
21
+
22
+Translations are handled by [react-intl](https://github.com/yahoo/react-intl/).
23
+
24
+`npm run extractTranslations` output a file containing all the english
25
+translations, in the expected form. It is a mapping of ids and strings to
26
+translate, with an extra description provided as a comment at the end of the
27
+line, for some translation context.
28
+
29
+Typically, if you want to translate to another `$LOCALE` (say `fr-FR`), create
30
+a folder `./app/locales/$LOCALE`, put inside the generated file from `npm run
31
+extractTranslations`, called `index.js`. Copy the lines in
32
+`./app/locales/index.js` to include your new translation and translate all the
33
+strings in the `./app/locales/$LOCALE/index.js` file you have just created.

+ 9
- 0
README.md View File

@@ -36,6 +36,15 @@ Please use the Git hooks (in `hooks` folder) to automatically make a build
36 36
 before comitting, as commit should always contain an up to date production
37 37
 build.
38 38
 
39
+Compilation cache is stored in `.cache` at the root of this repo. Remember to
40
+clean it in case of compilation issues.
41
+
42
+
43
+## Contributing
44
+
45
+See `CONTRIBUTING.md` file for extra infos.
46
+
47
+
39 48
 ## License
40 49
 
41 50
 This code is distributed under an MIT license.

+ 4
- 2
TODO View File

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

+ 26
- 10
app/components/Login.jsx View File

@@ -1,7 +1,12 @@
1 1
 import React, { Component, PropTypes } from "react";
2
-import { FormattedMessage } from "react-intl";
2
+import { defineMessages, injectIntl, intlShape, FormattedMessage } from "react-intl";
3 3
 
4
-export class LoginForm extends Component {
4
+import { messagesMap } from "../utils";
5
+import messages from "../locales/messagesDescriptors/Login";
6
+
7
+const loginMessages = defineMessages(messagesMap(messages));
8
+
9
+class LoginFormIntl extends Component {
5 10
     constructor (props) {
6 11
         super(props);
7 12
 
@@ -45,8 +50,10 @@ export class LoginForm extends Component {
45 50
     }
46 51
 
47 52
     render () {
53
+        const {formatMessage} = this.props.intl;
48 54
         return (
49 55
             <div>
56
+                { /* TODO: info/error translation */ }
50 57
                 {
51 58
                     this.props.error ?
52 59
                         <div className="row">
@@ -72,17 +79,17 @@ export class LoginForm extends Component {
72 79
                         <div className="row">
73 80
                             <div className="form-group" ref="usernameFormGroup">
74 81
                                 <div className="col-xs-12">
75
-                                    <input type="text" className="form-control" ref="username" aria-label="Username" placeholder="Username" autoFocus defaultValue={this.props.username} />
82
+                                    <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} />
76 83
                                 </div>
77 84
                             </div>
78 85
                             <div className="form-group" ref="passwordFormGroup">
79 86
                                 <div className="col-xs-12">
80
-                                    <input type="password" className="form-control" ref="password" aria-label="Password" placeholder="Password" />
87
+                                    <input type="password" className="form-control" ref="password" aria-label={formatMessage(loginMessages["app.login.password"])} placeholder={formatMessage(loginMessages["app.login.password"])} />
81 88
                                 </div>
82 89
                             </div>
83 90
                             <div className="form-group" ref="endpointFormGroup">
84 91
                                 <div className="col-xs-12">
85
-                                    <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} />
92
+                                    <input type="text" className="form-control" ref="endpoint" aria-label={formatMessage(loginMessages["app.login.endpointInputAriaLabel"])} placeholder="http://ampache.example.com" defaultValue={this.props.endpoint} />
86 93
                                 </div>
87 94
                             </div>
88 95
                             <div className="form-group">
@@ -90,11 +97,12 @@ export class LoginForm extends Component {
90 97
                                     <div className="row">
91 98
                                         <div className="col-sm-6 col-xs-12 checkbox">
92 99
                                             <label id="rememberMeLabel">
93
-                                                <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" />
100
+                                                <input type="checkbox" ref="rememberMe" defaultChecked={this.props.rememberMe} aria-labelledby="rememberMeLabel" />
101
+                                                <FormattedMessage {...loginMessages["app.login.rememberMe"]} />
94 102
                                             </label>
95 103
                                         </div>
96 104
                                         <div className="col-sm-6 col-sm-12 submit text-right">
97
-                                            <input type="submit" className="btn btn-default" aria-label="Sign in" defaultValue="Sign in" disabled={this.props.isAuthenticating} />
105
+                                            <input type="submit" className="btn btn-default" aria-label={formatMessage(loginMessages["app.login.signIn"])} defaultValue={formatMessage(loginMessages["app.login.signIn"])} disabled={this.props.isAuthenticating} />
98 106
                                         </div>
99 107
                                     </div>
100 108
                                 </div>
@@ -107,24 +115,32 @@ export class LoginForm extends Component {
107 115
     }
108 116
 }
109 117
 
110
-LoginForm.propTypes = {
118
+LoginFormIntl.propTypes = {
111 119
     username: PropTypes.string,
112 120
     endpoint: PropTypes.string,
113 121
     rememberMe: PropTypes.bool,
114 122
     onSubmit: PropTypes.func.isRequired,
115 123
     isAuthenticating: PropTypes.bool,
116 124
     error: PropTypes.string,
117
-    info: PropTypes.string
125
+    info: PropTypes.string,
126
+    intl: intlShape.isRequired,
118 127
 };
119 128
 
129
+export let LoginForm = injectIntl(LoginFormIntl);
130
+
120 131
 
121 132
 export default class Login extends Component {
122 133
     render () {
134
+        const greeting = (
135
+            <p>
136
+                <FormattedMessage {...loginMessages["app.login.greeting"]} />
137
+            </p>
138
+        );
123 139
         return (
124 140
             <div className="login text-center container-fluid">
125 141
                 <h1><img src="./app/assets/img/ampache-blue.png" alt="A"/>mpache</h1>
126 142
                 <hr/>
127
-                <p><FormattedMessage id="app.login.greeting" description="Greeting to welcome the user to the app" defaultMessage="Welcome back on Ampache, let's go!" /></p>
143
+                {(!this.props.error && !this.props.info) ? greeting : null}
128 144
                 <div className="col-sm-9 col-sm-offset-2 col-md-6 col-md-offset-3">
129 145
                     <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} />
130 146
                 </div>

+ 22
- 6
app/components/Songs.jsx View File

@@ -1,10 +1,16 @@
1 1
 import React, { Component, PropTypes } from "react";
2 2
 import { Link} from "react-router";
3
+import { defineMessages, FormattedMessage } from "react-intl";
3 4
 import Fuse from "fuse.js";
4 5
 
5 6
 import FilterBar from "./elements/FilterBar";
6 7
 import Pagination from "./elements/Pagination";
7
-import { formatLength} from "../utils";
8
+import { formatLength, messagesMap } from "../utils";
9
+
10
+import commonMessages from "../locales/messagesDescriptors/common";
11
+import messages from "../locales/messagesDescriptors/Songs";
12
+
13
+const songsMessages = defineMessages(messagesMap(Array.concat([], commonMessages, messages)));
8 14
 
9 15
 export class SongsTableRow extends Component {
10 16
     render () {
@@ -55,11 +61,21 @@ export class SongsTable extends Component {
55 61
                     <thead>
56 62
                         <tr>
57 63
                             <th></th>
58
-                            <th>Title</th>
59
-                            <th>Artist</th>
60
-                            <th>Album</th>
61
-                            <th>Genre</th>
62
-                            <th>Length</th>
64
+                            <th>
65
+                                <FormattedMessage {...songsMessages["app.songs.title"]} />
66
+                            </th>
67
+                            <th>
68
+                                <FormattedMessage {...songsMessages["app.common.artist"]} />
69
+                            </th>
70
+                            <th>
71
+                                <FormattedMessage {...songsMessages["app.common.album"]} />
72
+                            </th>
73
+                            <th>
74
+                                <FormattedMessage {...songsMessages["app.common.genre"]} />
75
+                            </th>
76
+                            <th>
77
+                                <FormattedMessage {...songsMessages["app.songs.length"]} />
78
+                            </th>
63 79
                         </tr>
64 80
                     </thead>
65 81
                     <tbody>{rows}</tbody>

+ 18
- 5
app/components/elements/FilterBar.jsx View File

@@ -1,6 +1,12 @@
1 1
 import React, { Component, PropTypes } from "react";
2
+import { defineMessages, injectIntl, intlShape, FormattedMessage } from "react-intl";
2 3
 
3
-export default class FilterBar extends Component {
4
+import { messagesMap } from "../../utils";
5
+import messages from "../../locales/messagesDescriptors/elements/FilterBar";
6
+
7
+const filterMessages = defineMessages(messagesMap(messages));
8
+
9
+class FilterBarIntl extends Component {
4 10
     constructor (props) {
5 11
         super(props);
6 12
         this.handleChange = this.handleChange.bind(this);
@@ -13,13 +19,16 @@ export default class FilterBar extends Component {
13 19
     }
14 20
 
15 21
     render () {
22
+        const {formatMessage} = this.props.intl;
16 23
         return (
17 24
             <div className="filter">
18
-                <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>
25
+                <p className="col-xs-12 col-sm-6 col-md-4 col-md-offset-1 filter-legend" id="filterInputDescription">
26
+                    <FormattedMessage {...filterMessages["app.filter.whatAreWeListeningToToday"]} />
27
+                </p>
19 28
                 <div className="col-xs-12 col-sm-6 col-md-4 input-group">
20 29
                     <form className="form-inline" onSubmit={this.handleChange} aria-describedby="filterInputDescription">
21 30
                         <div className="form-group">
22
-                            <input type="text" className="form-control filter-input" placeholder="Filter…" aria-label="Filter…" value={this.props.filterText} onChange={this.handleChange} ref="filterTextInput" />
31
+                            <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" />
23 32
                         </div>
24 33
                     </form>
25 34
                 </div>
@@ -28,7 +37,11 @@ export default class FilterBar extends Component {
28 37
     }
29 38
 }
30 39
 
31
-FilterBar.propTypes = {
40
+FilterBarIntl.propTypes = {
32 41
     onUserInput: PropTypes.func,
33
-    filterText: PropTypes.string
42
+    filterText: PropTypes.string,
43
+    intl: intlShape.isRequired
34 44
 };
45
+
46
+export let FilterBar = injectIntl(FilterBarIntl);
47
+export default FilterBar;

+ 5
- 0
app/components/elements/Grid.jsx View File

@@ -14,12 +14,17 @@ export class GridItem extends Component {
14 14
         if (Array.isArray(nSubItems)) {
15 15
             nSubItems = nSubItems.length;
16 16
         }
17
+
18
+        // TODO: i18n
17 19
         var subItemsLabel = this.props.subItemsType;
18 20
         if (nSubItems < 2) {
19 21
             subItemsLabel = subItemsLabel.rstrip("s");
20 22
         }
23
+
21 24
         const to = "/" + this.props.itemsType.rstrip("s") + "/" + this.props.item.id;
22 25
         const id = "grid-item-" + this.props.item.type + "/" + this.props.item.id;
26
+
27
+        // TODO: i18n
23 28
         const title = "Go to " + this.props.itemsType.rstrip("s") + " page";
24 29
         return (
25 30
             <div className="grid-item col-xs-6 col-sm-3 placeholders" id={id}>

+ 39
- 16
app/components/elements/Pagination.jsx View File

@@ -1,7 +1,14 @@
1 1
 import React, { Component, PropTypes } from "react";
2 2
 import { Link, withRouter } from "react-router";
3
+import { defineMessages, injectIntl, intlShape, FormattedMessage } from "react-intl";
3 4
 
4
-export class Pagination extends Component {
5
+import { messagesMap } from "../../utils";
6
+import commonMessages from "../../locales/messagesDescriptors/common";
7
+import messages from "../../locales/messagesDescriptors/elements/Pagination";
8
+
9
+const paginationMessages = defineMessages(messagesMap(Array.concat([], commonMessages, messages)));
10
+
11
+export class PaginationIntl extends Component {
5 12
     constructor(props) {
6 13
         super(props);
7 14
         this.buildLinkTo.bind(this);
@@ -61,6 +68,7 @@ export class Pagination extends Component {
61 68
     }
62 69
 
63 70
     render () {
71
+        const { formatMessage } = this.props.intl;
64 72
         const { lowerLimit, upperLimit } = this.computePaginationBounds(this.props.currentPage, this.props.nPages);
65 73
         var pagesButton = [];
66 74
         var key = 0;  // key increment to ensure correct ordering
@@ -68,7 +76,9 @@ export class Pagination extends Component {
68 76
             // Push first page
69 77
             pagesButton.push(
70 78
                 <li className="page-item" key={key}>
71
-                    <Link className="page-link" title="Go to page 1" to={this.buildLinkTo(1)}><span className="sr-only">Go to page </span>1</Link>
79
+                    <Link className="page-link" title={formatMessage(paginationMessages["app.pagination.goToPageWithoutMarkup"], { pageNumber: 1})} to={this.buildLinkTo(1)}>
80
+                        <FormattedMessage {...paginationMessages["app.pagination.goToPage"]} values={{ pageNumber: 1 }} />
81
+                    </Link>
72 82
                 </li>
73 83
             );
74 84
             key++;
@@ -88,12 +98,15 @@ export class Pagination extends Component {
88 98
             var currentSpan = null;
89 99
             if (this.props.currentPage == i) {
90 100
                 className += " active";
91
-                currentSpan = <span className="sr-only">(current)</span>;
101
+                currentSpan = <span className="sr-only">(<FormattedMessage {...paginationMessages["app.pagination.current"]} />)</span>;
92 102
             }
93
-            const title = "Go to page " + i;
103
+            const title = formatMessage(paginationMessages["app.pagination.goToPageWithoutMarkup"], { pageNumber: i });
94 104
             pagesButton.push(
95 105
                 <li className={className} key={key}>
96
-                    <Link className="page-link" title={title} to={this.buildLinkTo(i)}><span className="sr-only">Go to page </span>{i} {currentSpan}</Link>
106
+                    <Link className="page-link" title={title} to={this.buildLinkTo(i)}>
107
+                        <FormattedMessage {...paginationMessages["app.pagination.goToPage"]} values={{ pageNumber: i }} />
108
+                        {currentSpan}
109
+                    </Link>
97 110
                 </li>
98 111
             );
99 112
             key++;
@@ -108,18 +121,20 @@ export class Pagination extends Component {
108 121
                 );
109 122
                 key++;
110 123
             }
111
-            const title = "Go to page " + this.props.nPages;
124
+            const title = formatMessage(paginationMessages["app.pagination.goToPageWithoutMarkup"], { pageNumber: this.props.nPages });
112 125
             // Push last page
113 126
             pagesButton.push(
114 127
                 <li className="page-item" key={key}>
115
-                    <Link className="page-link" title={title} to={this.buildLinkTo(this.props.nPages)}><span className="sr-only">Go to page </span>{this.props.nPages}</Link>
128
+                    <Link className="page-link" title={title} to={this.buildLinkTo(this.props.nPages)}>
129
+                        <FormattedMessage {...paginationMessages["app.pagination.goToPage"]} values={{ pageNumber: this.props.nPages }} />
130
+                    </Link>
116 131
                 </li>
117 132
             );
118 133
         }
119 134
         if (pagesButton.length > 1) {
120 135
             return (
121 136
                 <div>
122
-                    <nav className="pagination-nav" aria-label="Page navigation">
137
+                    <nav className="pagination-nav" aria-label={formatMessage(paginationMessages["app.pagination.pageNavigation"])}>
123 138
                         <ul className="pagination">
124 139
                             { pagesButton }
125 140
                         </ul>
@@ -128,17 +143,23 @@ export class Pagination extends Component {
128 143
                         <div className="modal-dialog" role="document">
129 144
                             <div className="modal-content">
130 145
                                 <div className="modal-header">
131
-                                    <button type="button" className="close" data-dismiss="modal" aria-label="Close">&times;</button>
132
-                                    <h4 className="modal-title" id="paginationModalLabel">Page to go to?</h4>
146
+                                    <button type="button" className="close" data-dismiss="modal" aria-label={formatMessage(paginationMessages["app.common.close"])}>&times;</button>
147
+                                    <h4 className="modal-title" id="paginationModalLabel">
148
+                                        <FormattedMessage {...paginationMessages["app.pagination.pageToGoTo"]} />
149
+                                    </h4>
133 150
                                 </div>
134 151
                                 <div className="modal-body">
135 152
                                     <form>
136
-                                        <input className="form-control" autoComplete="off" type="number" ref="pageInput" aria-label="Page number to go to" />
153
+                                        <input className="form-control" autoComplete="off" type="number" ref="pageInput" aria-label={formatMessage(paginationMessages["app.pagination.pageToGoTo"])} />
137 154
                                     </form>
138 155
                                 </div>
139 156
                                 <div className="modal-footer">
140
-                                    <button type="button" className="btn btn-default" onClick={this.cancelModalBox.bind(this)}>Cancel</button>
141
-                                    <button type="button" className="btn btn-primary" onClick={this.goToPage.bind(this)}>OK</button>
157
+                                    <button type="button" className="btn btn-default" onClick={this.cancelModalBox.bind(this)}>
158
+                                        <FormattedMessage {...paginationMessages["app.common.cancel"]} />
159
+                                    </button>
160
+                                    <button type="button" className="btn btn-primary" onClick={this.goToPage.bind(this)}>
161
+                                        <FormattedMessage {...paginationMessages["app.common.go"]} />
162
+                                    </button>
142 163
                                 </div>
143 164
                             </div>
144 165
                         </div>
@@ -150,10 +171,12 @@ export class Pagination extends Component {
150 171
     }
151 172
 }
152 173
 
153
-Pagination.propTypes = {
174
+PaginationIntl.propTypes = {
154 175
     currentPage: PropTypes.number.isRequired,
155 176
     location: PropTypes.object.isRequired,
156
-    nPages: PropTypes.number.isRequired
177
+    nPages: PropTypes.number.isRequired,
178
+    intl: intlShape.isRequired,
157 179
 };
158 180
 
159
-export default withRouter(Pagination);
181
+export let Pagination = withRouter(injectIntl(PaginationIntl));
182
+export default Pagination;

+ 48
- 18
app/components/layouts/Sidebar.jsx View File

@@ -1,8 +1,17 @@
1
-import React, { Component } from "react";
1
+import React, { Component, PropTypes } from "react";
2 2
 import { IndexLink, Link} from "react-router";
3
+import { defineMessages, injectIntl, intlShape, FormattedMessage } from "react-intl";
3 4
 
4
-export default class SidebarLayout extends Component {
5
+import { messagesMap } from "../../utils";
6
+import commonMessages from "../../locales/messagesDescriptors/common";
7
+import messages from "../../locales/messagesDescriptors/layouts/Sidebar";
8
+
9
+// TODO: i18n for artist / album / songs
10
+const sidebarLayoutMessages = defineMessages(messagesMap(Array.concat([], commonMessages, messages)));
11
+
12
+export default class SidebarLayoutIntl extends Component {
5 13
     render () {
14
+        const { formatMessage } = this.props.intl;
6 15
         const isActive = {
7 16
             discover: (this.props.location.pathname == "/discover") ? "active" : "",
8 17
             browse: (this.props.location.pathname == "/browse") ? "active" : "",
@@ -20,26 +29,32 @@ export default class SidebarLayout extends Component {
20 29
                             <span className="hidden-sm">mpache</span>
21 30
                         </IndexLink>
22 31
                     </h1>
23
-                    <nav aria-label="Main navigation menu">
32
+                    <nav aria-label={formatMessage(sidebarLayoutMessages["app.sidebarLayout.mainNavigationMenu"])}>
24 33
                         <div className="navbar text-center icon-navbar">
25 34
                             <div className="container-fluid">
26 35
                                 <ul className="nav navbar-nav icon-navbar-nav">
27 36
                                     <li>
28
-                                        <Link to="/" title="Home">
37
+                                        <Link to="/" title={formatMessage(sidebarLayoutMessages["app.sidebarLayout.home"])}>
29 38
                                             <span className="glyphicon glyphicon-home" aria-hidden="true"></span>
30
-                                            <span className="sr-only">Home</span>
39
+                                            <span className="sr-only">
40
+                                                <FormattedMessage {...sidebarLayoutMessages["app.sidebarLayout.home"]} />
41
+                                            </span>
31 42
                                         </Link>
32 43
                                     </li>
33 44
                                     <li>
34
-                                        <Link to="/settings" title="Settings">
45
+                                        <Link to="/settings" title={formatMessage(sidebarLayoutMessages["app.sidebarLayout.settings"])}>
35 46
                                             <span className="glyphicon glyphicon-wrench" aria-hidden="true"></span>
36
-                                            <span className="sr-only">Settings</span>
47
+                                            <span className="sr-only">
48
+                                                <FormattedMessage {...sidebarLayoutMessages["app.sidebarLayout.settings"]} />
49
+                                            </span>
37 50
                                         </Link>
38 51
                                     </li>
39 52
                                     <li>
40
-                                        <Link to="/logout" title="Logout">
53
+                                        <Link to="/logout" title={formatMessage(sidebarLayoutMessages["app.sidebarLayout.logout"])}>
41 54
                                             <span className="glyphicon glyphicon-off" aria-hidden="true"></span>
42
-                                            <span className="sr-only">Logout</span>
55
+                                            <span className="sr-only">
56
+                                                <FormattedMessage {...sidebarLayoutMessages["app.sidebarLayout.logout"]} />
57
+                                            </span>
43 58
                                         </Link>
44 59
                                     </li>
45 60
                                 </ul>
@@ -47,33 +62,37 @@ export default class SidebarLayout extends Component {
47 62
                         </div>
48 63
                         <ul className="nav nav-sidebar">
49 64
                             <li>
50
-                                <Link to="/discover" title="Discover" className={isActive.discover}>
65
+                                <Link to="/discover" title={formatMessage(sidebarLayoutMessages["app.sidebarLayout.discover"])} className={isActive.discover}>
51 66
                                     <span className="glyphicon glyphicon-globe" aria-hidden="true"></span>
52
-                                    <span className="hidden-sm"> Discover</span>
67
+                                    <span className="hidden-sm">
68
+                                        &nbsp;<FormattedMessage {...sidebarLayoutMessages["app.sidebarLayout.discover"]} />
69
+                                    </span>
53 70
                                 </Link>
54 71
                             </li>
55 72
                             <li>
56
-                                <Link to="/browse" title="Browse" className={isActive.browse}>
73
+                                <Link to="/browse" title={formatMessage(sidebarLayoutMessages["app.sidebarLayout.browse"])} className={isActive.browse}>
57 74
                                     <span className="glyphicon glyphicon-headphones" aria-hidden="true"></span>
58
-                                    <span className="hidden-sm"> Browse</span>
75
+                                    <span className="hidden-sm">
76
+                                        &nbsp;<FormattedMessage {...sidebarLayoutMessages["app.sidebarLayout.browse"]} />
77
+                                    </span>
59 78
                                 </Link>
60 79
                                 <ul className="nav nav-list text-center">
61 80
                                     <li>
62
-                                        <Link to="/artists" title="Browse artists" className={isActive.artists}>
81
+                                        <Link to="/artists" title={formatMessage(sidebarLayoutMessages["app.sidebarLayout.browseArtists"])} className={isActive.artists}>
63 82
                                             <span className="glyphicon glyphicon-user" aria-hidden="true"></span>
64 83
                                             <span className="sr-only">Artists</span>
65 84
                                             <span className="hidden-sm"> Artists</span>
66 85
                                         </Link>
67 86
                                     </li>
68 87
                                     <li>
69
-                                        <Link to="/albums" title="Browse albums" className={isActive.albums}>
88
+                                        <Link to="/albums" title={formatMessage(sidebarLayoutMessages["app.sidebarLayout.browseAlbums"])} className={isActive.albums}>
70 89
                                             <span className="glyphicon glyphicon-cd" aria-hidden="true"></span>
71 90
                                             <span className="sr-only">Albums</span>
72 91
                                             <span className="hidden-sm"> Albums</span>
73 92
                                         </Link>
74 93
                                     </li>
75 94
                                     <li>
76
-                                        <Link to="/songs" title="Browse songs" className={isActive.songs}>
95
+                                        <Link to="/songs" title={formatMessage(sidebarLayoutMessages["app.sidebarLayout.browseSongs"])} className={isActive.songs}>
77 96
                                             <span className="glyphicon glyphicon-music" aria-hidden="true"></span>
78 97
                                             <span className="sr-only">Songs</span>
79 98
                                             <span className="hidden-sm"> Songs</span>
@@ -82,9 +101,11 @@ export default class SidebarLayout extends Component {
82 101
                                 </ul>
83 102
                             </li>
84 103
                             <li>
85
-                                <Link to="/search" title="Search" className={isActive.search}>
104
+                                <Link to="/search" title={formatMessage(sidebarLayoutMessages["app.sidebarLayout.search"])} className={isActive.search}>
86 105
                                     <span className="glyphicon glyphicon-search" aria-hidden="true"></span>
87
-                                    <span className="hidden-sm"> Search</span>
106
+                                    <span className="hidden-sm">
107
+                                        &nbsp;<FormattedMessage {...sidebarLayoutMessages["app.sidebarLayout.search"]} />
108
+                                    </span>
88 109
                                 </Link>
89 110
                             </li>
90 111
                         </ul>
@@ -98,3 +119,12 @@ export default class SidebarLayout extends Component {
98 119
         );
99 120
     }
100 121
 }
122
+
123
+
124
+SidebarLayoutIntl.propTypes = {
125
+    children: PropTypes.node,
126
+    intl: intlShape.isRequired
127
+};
128
+
129
+export let SidebarLayout = injectIntl(SidebarLayoutIntl);
130
+export default SidebarLayout;

+ 0
- 1
app/containers/App.jsx View File

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

+ 3
- 3
app/dist/1.1.js
File diff suppressed because it is too large
View File


+ 1
- 1
app/dist/1.1.js.map
File diff suppressed because it is too large
View File


+ 1
- 1
app/dist/fix.ie9.js View File

@@ -1,2 +1,2 @@
1
-!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)}});
1
+!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)}});
2 2
 //# sourceMappingURL=fix.ie9.js.map

+ 1
- 1
app/dist/fix.ie9.js.map
File diff suppressed because it is too large
View File


+ 23
- 22
app/dist/index.js
File diff suppressed because it is too large
View File


+ 1
- 1
app/dist/index.js.map
File diff suppressed because it is too large
View File


+ 33
- 5
app/locales/en-US/index.js View File

@@ -1,7 +1,35 @@
1 1
 module.exports = {
2
-    "app": {
3
-        "login": {
4
-            "greeting": "TODO"
5
-        }
6
-    }
2
+    "app.common.album": "Album",  // Album
3
+    "app.common.artist": "Artist",  // Artist
4
+    "app.common.cancel": "Cancel",  // Cancel
5
+    "app.common.close": "Close",  // Close
6
+    "app.common.go": "Go",  // Go
7
+    "app.common.song": "Song",  // Song
8
+    "app.filter.filter": "Filter…",  // Filtering input placeholder
9
+    "app.filter.whatAreWeListeningToToday": "What are we listening to today?",  // Description for the filter bar
10
+    "app.login.endpointInputAriaLabel": "URL of your Ampache instance (e.g. http://ampache.example.com)",  // ARIA label for the endpoint input
11
+    "app.login.greeting": "Welcome back on Ampache, let's go!",  // Greeting to welcome the user to the app
12
+    "app.login.password": "Password",  // Password input placeholder
13
+    "app.login.rememberMe": "Remember me",  // Remember me checkbox label
14
+    "app.login.signIn": "Sign in",  // Sign in
15
+    "app.login.username": "Username",  // Username input placeholder
16
+    "app.pagination.current": "current",  // Current (page)
17
+    "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
18
+    "app.pagination.goToPageWithoutMarkup": "Go to page {pageNumber}",  // Link title to go to page N
19
+    "app.pagination.pageNavigation": "Page navigation",  // ARIA label for the nav block containing pagination
20
+    "app.pagination.pageToGoTo": "Page to go to?",  // Title of the pagination modal
21
+    "app.sidebarLayout.browse": "Browse",  // Browse
22
+    "app.sidebarLayout.browseAlbums": "Browse albums",  // Browse albums
23
+    "app.sidebarLayout.browseArtists": "Browse artists",  // Browse artists
24
+    "app.sidebarLayout.browseSongs": "Browse songs",  // Browse songs
25
+    "app.sidebarLayout.discover": "Discover",  // Discover
26
+    "app.sidebarLayout.home": "Home",  // Home
27
+    "app.sidebarLayout.logout": "Logout",  // Logout
28
+    "app.sidebarLayout.mainNavigationMenu": "Main navigation menu",  // ARIA label for the main navigation menu
29
+    "app.sidebarLayout.search": "Search",  // Search
30
+    "app.sidebarLayout.settings": "Settings",  // Settings
31
+    "app.songs.album": "Album",  // Album (song)
32
+    "app.songs.genre": "Genre",  // Genre (song)
33
+    "app.songs.length": "Length",  // Length (song)
34
+    "app.songs.title": "Title",  // Title (song)
7 35
 };

+ 33
- 1
app/locales/fr-FR/index.js View File

@@ -1,3 +1,35 @@
1 1
 module.exports = {
2
-    "app.login.greeting": "TODO",
2
+    "app.common.album": "Album",  // Album
3
+    "app.common.artist": "Artiste",  // Artist
4
+    "app.common.cancel": "Annuler",  // Cancel
5
+    "app.common.close": "Fermer",  // Close
6
+    "app.common.go": "Aller",  // Go
7
+    "app.common.song": "Piste",  // Song
8
+    "app.filter.filter": "Filtrer…",  // Filtering input placeholder
9
+    "app.filter.whatAreWeListeningToToday": "Que voulez-vous écouter aujourd'hui\u00a0?",  // Description for the filter bar
10
+    "app.login.endpointInputAriaLabel": "URL de votre Ampache (e.g. http://ampache.example.com)",  // ARIA label for the endpoint input
11
+    "app.login.greeting": "Bon retour sur Ampache, c'est parti\u00a0!",  // Greeting to welcome the user to the app
12
+    "app.login.password": "Mot de passe",  // Password input placeholder
13
+    "app.login.rememberMe": "Se souvenir",  // Remember me checkbox label
14
+    "app.login.signIn": "Connexion",  // Sign in
15
+    "app.login.username": "Utilisateur",  // Username input placeholder
16
+    "app.pagination.current": "actuelle",  // Current (page)
17
+    "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
18
+    "app.pagination.goToPageWithoutMarkup": "Aller à la page {pageNumber}",  // Link title to go to page N
19
+    "app.pagination.pageNavigation": "Navigation entre les pages",  // ARIA label for the nav block containing pagination
20
+    "app.pagination.pageToGoTo": "Page à laquelle aller\u00a0?",  // Title of the pagination modal
21
+    "app.sidebarLayout.browse": "Explorer",  // Browse
22
+    "app.sidebarLayout.browseAlbums": "Parcourir les albums",  // Browse albums
23
+    "app.sidebarLayout.browseArtists": "Parcourir les artistes",  // Browse artists
24
+    "app.sidebarLayout.browseSongs": "Parcourir les pistes",  // Browse songs
25
+    "app.sidebarLayout.discover": "Découvrir",  // Discover
26
+    "app.sidebarLayout.home": "Accueil",  // Home
27
+    "app.sidebarLayout.logout": "Déconnexion",  // Logout
28
+    "app.sidebarLayout.mainNavigationMenu": "Menu principal",  // ARIA label for the main navigation menu
29
+    "app.sidebarLayout.search": "Rechercher",  // Search
30
+    "app.sidebarLayout.settings": "Préférences",  // Settings
31
+    "app.songs.album": "Album",  // Album (song)
32
+    "app.songs.genre": "Genre",  // Genre (song)
33
+    "app.songs.length": "Durée",  // Length (song)
34
+    "app.songs.title": "Titre",  // Title (song)
3 35
 };

+ 34
- 0
app/locales/messagesDescriptors/Login.js View File

@@ -0,0 +1,34 @@
1
+const messages = [
2
+    {
3
+        id: "app.login.username",
4
+        defaultMessage: "Username",
5
+        description: "Username input placeholder"
6
+    },
7
+    {
8
+        id: "app.login.password",
9
+        defaultMessage: "Password",
10
+        description: "Password input placeholder"
11
+    },
12
+    {
13
+        id: "app.login.signIn",
14
+        defaultMessage: "Sign in",
15
+        description: "Sign in"
16
+    },
17
+    {
18
+        id: "app.login.endpointInputAriaLabel",
19
+        defaultMessage: "URL of your Ampache instance (e.g. http://ampache.example.com)",
20
+        description: "ARIA label for the endpoint input"
21
+    },
22
+    {
23
+        id: "app.login.rememberMe",
24
+        description: "Remember me checkbox label",
25
+        defaultMessage: "Remember me"
26
+    },
27
+    {
28
+        id: "app.login.greeting",
29
+        description: "Greeting to welcome the user to the app",
30
+        defaultMessage: "Welcome back on Ampache, let's go!"
31
+    }
32
+];
33
+
34
+export default messages;

+ 24
- 0
app/locales/messagesDescriptors/Songs.js View File

@@ -0,0 +1,24 @@
1
+const messages = [
2
+    {
3
+        "id": "app.songs.title",
4
+        "description": "Title (song)",
5
+        "defaultMessage": "Title"
6
+    },
7
+    {
8
+        "id": "app.songs.album",
9
+        "description": "Album (song)",
10
+        "defaultMessage": "Album"
11
+    },
12
+    {
13
+        "id": "app.songs.genre",
14
+        "description": "Genre (song)",
15
+        "defaultMessage": "Genre"
16
+    },
17
+    {
18
+        "id": "app.songs.length",
19
+        "description": "Length (song)",
20
+        "defaultMessage": "Length"
21
+    }
22
+];
23
+
24
+export default messages;

+ 34
- 0
app/locales/messagesDescriptors/common.js View File

@@ -0,0 +1,34 @@
1
+const messages = [
2
+    {
3
+        id: "app.common.close",
4
+        defaultMessage: "Close",
5
+        description: "Close"
6
+    },
7
+    {
8
+        id: "app.common.cancel",
9
+        description: "Cancel",
10
+        defaultMessage: "Cancel"
11
+    },
12
+    {
13
+        id: "app.common.go",
14
+        description: "Go",
15
+        defaultMessage: "Go"
16
+    },
17
+    {
18
+        id: "app.common.artist",
19
+        description: "Artist",
20
+        defaultMessage: "Artist"
21
+    },
22
+    {
23
+        id: "app.common.album",
24
+        description: "Album",
25
+        defaultMessage: "Album"
26
+    },
27
+    {
28
+        id: "app.common.song",
29
+        description: "Song",
30
+        defaultMessage: "Song"
31
+    },
32
+];
33
+
34
+export default messages;

+ 14
- 0
app/locales/messagesDescriptors/elements/FilterBar.js View File

@@ -0,0 +1,14 @@
1
+const messages = [
2
+    {
3
+        id: "app.filter.filter",
4
+        defaultMessage: "Filter…",
5
+        description: "Filtering input placeholder"
6
+    },
7
+    {
8
+        id: "app.filter.whatAreWeListeningToToday",
9
+        description: "Description for the filter bar",
10
+        defaultMessage: "What are we listening to today?"
11
+    },
12
+];
13
+
14
+export default messages;

+ 29
- 0
app/locales/messagesDescriptors/elements/Pagination.js View File

@@ -0,0 +1,29 @@
1
+const messages = [
2
+    {
3
+        id: "app.pagination.goToPage",
4
+        defaultMessage: "<span className=\"sr-only\">Go to page </span>{pageNumber}",
5
+        description: "Link content to go to page N. span is here for screen-readers"
6
+    },
7
+    {
8
+        id: "app.pagination.goToPageWithoutMarkup",
9
+        defaultMessage: "Go to page {pageNumber}",
10
+        description: "Link title to go to page N"
11
+    },
12
+    {
13
+        id: "app.pagination.pageNavigation",
14
+        defaultMessage: "Page navigation",
15
+        description: "ARIA label for the nav block containing pagination"
16
+    },
17
+    {
18
+        id: "app.pagination.pageToGoTo",
19
+        description: "Title of the pagination modal",
20
+        defaultMessage: "Page to go to?"
21
+    },
22
+    {
23
+        id: "app.pagination.current",
24
+        description: "Current (page)",
25
+        defaultMessage: "current"
26
+    }
27
+];
28
+
29
+export default messages;

+ 54
- 0
app/locales/messagesDescriptors/layouts/Sidebar.js View File

@@ -0,0 +1,54 @@
1
+const messages = [
2
+    {
3
+        id: "app.sidebarLayout.mainNavigationMenu",
4
+        description: "ARIA label for the main navigation menu",
5
+        defaultMessage: "Main navigation menu"
6
+    },
7
+    {
8
+        id: "app.sidebarLayout.home",
9
+        description: "Home",
10
+        defaultMessage: "Home"
11
+    },
12
+    {
13
+        id: "app.sidebarLayout.settings",
14
+        description: "Settings",
15
+        defaultMessage: "Settings"
16
+    },
17
+    {
18
+        id: "app.sidebarLayout.logout",
19
+        description: "Logout",
20
+        defaultMessage: "Logout"
21
+    },
22
+    {
23
+        id: "app.sidebarLayout.discover",
24
+        description: "Discover",
25
+        defaultMessage: "Discover"
26
+    },
27
+    {
28
+        id: "app.sidebarLayout.browse",
29
+        description: "Browse",
30
+        defaultMessage: "Browse"
31
+    },
32
+    {
33
+        id: "app.sidebarLayout.browseArtists",
34
+        description: "Browse artists",
35
+        defaultMessage: "Browse artists"
36
+    },
37
+    {
38
+        id: "app.sidebarLayout.browseAlbums",
39
+        description: "Browse albums",
40
+        defaultMessage: "Browse albums"
41
+    },
42
+    {
43
+        id: "app.sidebarLayout.browseSongs",
44
+        description: "Browse songs",
45
+        defaultMessage: "Browse songs"
46
+    },
47
+    {
48
+        id: "app.sidebarLayout.search",
49
+        description: "Search",
50
+        defaultMessage: "Search"
51
+    }
52
+];
53
+
54
+export default messages;

+ 10
- 0
app/utils/locale.js View File

@@ -19,3 +19,13 @@ export function getBrowserLocale () {
19 19
 
20 20
     return locale;
21 21
 }
22
+
23
+export function messagesMap(messagesDescriptorsArray) {
24
+    var messagesDescriptorsMap = {};
25
+
26
+    messagesDescriptorsArray.forEach(function (item) {
27
+        messagesDescriptorsMap[item.id] = item;
28
+    });
29
+
30
+    return messagesDescriptorsMap;
31
+}

+ 17
- 38
index.all.js View File

@@ -25,7 +25,7 @@ const history = syncHistoryWithStore(hashHistory, store);
25 25
 const rootElement = document.getElementById("root");
26 26
 
27 27
 // i18n
28
-const onWindowIntl = () => {
28
+export const onWindowIntl = () => {
29 29
     addLocaleData([...en, ...fr]);
30 30
     const locale = getBrowserLocale();
31 31
     var strings = rawMessages[locale] ? rawMessages[locale] : rawMessages["en-US"];
@@ -39,43 +39,22 @@ const onWindowIntl = () => {
39 39
         );
40 40
     };
41 41
 
42
-    if (module.hot) {
43
-        // Support hot reloading of components
44
-        // and display an overlay for runtime errors
45
-        const renderApp = render;
46
-        const renderError = (error) => {
47
-            const RedBox = require("redbox-react");
48
-            ReactDOM.render(
49
-                <RedBox error={error} />,
50
-                rootElement
51
-            );
52
-        };
53
-        render = () => {
54
-            try {
55
-                renderApp();
56
-            } catch (error) {
57
-                renderError(error);
58
-            }
59
-        };
60
-        module.hot.accept("./app/containers/Root", () => {
61
-            setTimeout(render);
42
+    return render;
43
+};
44
+
45
+export const Intl = (render) => {
46
+    if (!window.Intl) {
47
+        require.ensure([
48
+            "intl",
49
+            "intl/locale-data/jsonp/en.js",
50
+            "intl/locale-data/jsonp/fr.js"
51
+        ], function (require) {
52
+            require("intl");
53
+            require("intl/locale-data/jsonp/en.js");
54
+            require("intl/locale-data/jsonp/fr.js");
55
+            render();
62 56
         });
57
+    } else {
58
+        render();
63 59
     }
64
-
65
-    render();
66 60
 };
67
-
68
-if (!window.Intl) {
69
-    require.ensure([
70
-        "intl",
71
-        "intl/locale-data/jsonp/en.js",
72
-        "intl/locale-data/jsonp/fr.js"
73
-    ], function (require) {
74
-        require("intl");
75
-        require("intl/locale-data/jsonp/en.js");
76
-        require("intl/locale-data/jsonp/fr.js");
77
-        onWindowIntl();
78
-    });
79
-} else {
80
-    onWindowIntl();
81
-}

+ 27
- 1
index.development.js View File

@@ -4,4 +4,30 @@ import ReactDOM from "react-dom";
4 4
 var a11y = require("react-a11y");
5 5
 a11y(React, { ReactDOM: ReactDOM, includeSrcNode: true });
6 6
 
7
-require("./index.all.js");
7
+const index = require("./index.all.js");
8
+
9
+var render = index.onWindowIntl();
10
+if (process.env.NODE_ENV !== "production" && module.hot) {
11
+    // Support hot reloading of components
12
+    // and display an overlay for runtime errors
13
+    const renderApp = render;
14
+    const renderError = (error) => {
15
+        const RedBox = require("redbox-react");
16
+        ReactDOM.render(
17
+            <RedBox error={error} />,
18
+            index.rootElement
19
+        );
20
+    };
21
+    render = () => {
22
+        try {
23
+            renderApp();
24
+        } catch (error) {
25
+            console.error(error);
26
+            renderError(error);
27
+        }
28
+    };
29
+    module.hot.accept("./app/containers/Root", () => {
30
+        setTimeout(render);
31
+    });
32
+}
33
+index.Intl(render);

+ 3
- 1
index.production.js View File

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

+ 8
- 2
package.json View File

@@ -7,8 +7,11 @@
7 7
   "homepage": "https://github.com/Phyks/ampache_react",
8 8
   "repository": "git+https://github.com/Phyks/ampache_react.git",
9 9
   "scripts": {
10
-    "build": "./node_modules/webpack/bin/webpack.js --progress",
11
-    "watch": "./node_modules/webpack/bin/webpack.js --progress  --watch"
10
+    "build": "./node_modules/.bin/webpack --progress",
11
+    "watch": "./node_modules/.bin/webpack --progress  --watch",
12
+    "prod": "NODE_ENV=production ./node_modules/.bin/webpack --progress",
13
+    "extractTranslations": "./node_modules/.bin/babel-node scripts/extractTranslations.js",
14
+    "clean": "./node_modules/.bin/rimraf app/dist"
12 15
   },
13 16
   "dependencies": {
14 17
     "babel-polyfill": "^6.9.1",
@@ -38,6 +41,7 @@
38 41
   },
39 42
   "devDependencies": {
40 43
     "autoprefixer": "^6.3.7",
44
+    "babel-cli": "^6.11.4",
41 45
     "babel-core": "^6.10.4",
42 46
     "babel-loader": "^6.2.4",
43 47
     "babel-plugin-react-intl": "^2.1.3",
@@ -51,6 +55,7 @@
51 55
     "eventsource-polyfill": "^0.9.6",
52 56
     "extract-text-webpack-plugin": "^1.0.1",
53 57
     "file-loader": "^0.9.0",
58
+    "glob": "^7.0.5",
54 59
     "postcss": "^5.1.0",
55 60
     "postcss-loader": "^0.9.1",
56 61
     "postcss-reporter": "^1.4.1",
@@ -59,6 +64,7 @@
59 64
     "react-intl-webpack-plugin": "0.0.3",
60 65
     "redbox-react": "^1.2.10",
61 66
     "redux-logger": "^2.6.1",
67
+    "rimraf": "^2.5.4",
62 68
     "style-loader": "^0.13.1",
63 69
     "stylelint": "^7.0.3",
64 70
     "stylelint-config-standard": "^11.0.0",

+ 37
- 0
scripts/extractTranslations.js View File

@@ -0,0 +1,37 @@
1
+import * as fs from 'fs';
2
+import {sync as globSync} from 'glob';
3
+
4
+const MESSAGES_PATTERN = './app/locales/messagesDescriptors/**/*.js';
5
+
6
+// Aggregates the default messages that were extracted from the example app's
7
+// React components via the React Intl Babel plugin. An error will be thrown if
8
+// there are messages in different components that use the same `id`. The result
9
+// is a flat collection of `id: message` pairs for the app's default locale.
10
+let defaultMessages = globSync(MESSAGES_PATTERN)
11
+    .map((filename) => require("../" + filename).default)
12
+    .reduce((collection, descriptors) => {
13
+        descriptors.forEach(({id, description, defaultMessage}) => {
14
+            if (collection.hasOwnProperty(id)) {
15
+                throw new Error(`Duplicate message id: ${id}`);
16
+            }
17
+
18
+            collection.push({
19
+                id: id,
20
+                description: description,
21
+                defaultMessage: defaultMessage
22
+            });
23
+        });
24
+
25
+        return collection;
26
+    }, []);
27
+
28
+// Sort by id
29
+defaultMessages = defaultMessages.sort(function (item1, item2) {
30
+    return item1.id.localeCompare(item2.id);
31
+});
32
+
33
+console.log("module.exports = {");
34
+defaultMessages.forEach(function (item) {
35
+    console.log("    " + JSON.stringify(item.id) + ": " + JSON.stringify(item.defaultMessage) + ",  // " + item.description);
36
+});
37
+console.log("};");

+ 1
- 1
webpack.config.base.js View File

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