Skip to content

Commit 835ec3c

Browse files
authored
Merge pull request #1605 from andrewn/feature/date-i18n
Translations for Dates (fixes #1584)
2 parents 144f68c + fe7c201 commit 835ec3c

File tree

17 files changed

+119
-51
lines changed

17 files changed

+119
-51
lines changed

client/i18n.js

+13
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import i18n from 'i18next';
22
import { initReactI18next } from 'react-i18next';
33
import LanguageDetector from 'i18next-browser-languagedetector';
44
import Backend from 'i18next-http-backend';
5+
import { enUS, es } from 'date-fns/locale';
56

67
const fallbackLng = ['en-US'];
78
const availableLanguages = ['en-US', 'es-419'];
@@ -14,6 +15,18 @@ export function languageKeyToLabel(lang) {
1415
return languageMap[lang];
1516
}
1617

18+
export function languageKeyToDateLocale(lang) {
19+
const languageMap = {
20+
'en-US': enUS,
21+
'es-419': es
22+
};
23+
return languageMap[lang];
24+
}
25+
26+
export function currentDateLocale() {
27+
return languageKeyToDateLocale(i18n.language);
28+
}
29+
1730
const options = {
1831
loadPath: '/locales/{{lng}}/translations.json',
1932
requestOptions: { // used for fetch, can also be a function (payload) => ({ method: 'GET' })

client/modules/IDE/components/CollectionList/CollectionListRow.jsx

+3-5
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import format from 'date-fns/format';
21
import PropTypes from 'prop-types';
32
import React from 'react';
43
import { connect } from 'react-redux';
@@ -9,11 +8,10 @@ import * as ProjectActions from '../../actions/project';
98
import * as CollectionsActions from '../../actions/collections';
109
import * as IdeActions from '../../actions/ide';
1110
import * as ToastActions from '../../actions/toast';
11+
import dates from '../../../../utils/formatDate';
1212

1313
import DownFilledTriangleIcon from '../../../../images/down-filled-triangle.svg';
1414

15-
const formatDateCell = (date, mobile = false) => format(new Date(date), 'MMM D, YYYY');
16-
1715
class CollectionListRowBase extends React.Component {
1816
static projectInCollection(project, collection) {
1917
return collection.items.find(item => item.project.id === project.id) != null;
@@ -214,8 +212,8 @@ class CollectionListRowBase extends React.Component {
214212
{this.renderCollectionName()}
215213
</span>
216214
</th>
217-
<td>{mobile && 'Created: '}{format(new Date(collection.createdAt), 'MMM D, YYYY')}</td>
218-
<td>{mobile && 'Updated: '}{formatDateCell(collection.updatedAt)}</td>
215+
<td>{mobile && 'Created: '}{dates.format(collection.createdAt)}</td>
216+
<td>{mobile && 'Updated: '}{dates.format(collection.updatedAt)}</td>
219217
<td>{mobile && '# sketches: '}{(collection.items || []).length}</td>
220218
<td className="sketch-list__dropdown-column">
221219
{this.renderActions()}

client/modules/IDE/components/SketchList.jsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import format from 'date-fns/format';
21
import PropTypes from 'prop-types';
32
import React from 'react';
43
import { Helmet } from 'react-helmet';
@@ -8,6 +7,7 @@ import { Link } from 'react-router';
87
import { bindActionCreators } from 'redux';
98
import classNames from 'classnames';
109
import slugify from 'slugify';
10+
import dates from '../../../utils/formatDate';
1111
import * as ProjectActions from '../actions/project';
1212
import * as ProjectsActions from '../actions/projects';
1313
import * as CollectionsActions from '../actions/collections';
@@ -24,7 +24,7 @@ import ArrowDownIcon from '../../../images/sort-arrow-down.svg';
2424
import DownFilledTriangleIcon from '../../../images/down-filled-triangle.svg';
2525

2626

27-
const formatDateCell = (date, mobile = false) => format(new Date(date), mobile ? 'MMM D, YYYY' : 'MMM D, YYYY h:mm A');
27+
const formatDateCell = (date, mobile = false) => dates.format(date, { showTime: !mobile });
2828

2929
class SketchListRowBase extends React.Component {
3030
constructor(props) {

client/modules/IDE/components/Timer.jsx

+3-16
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import classNames from 'classnames';
2-
import differenceInMilliseconds from 'date-fns/difference_in_milliseconds';
3-
import distanceInWordsToNow from 'date-fns/distance_in_words_to_now';
42
import PropTypes from 'prop-types';
53
import React from 'react';
64
import { withTranslation } from 'react-i18next';
75

6+
import dates from '../../../utils/formatDate';
7+
88
class Timer extends React.Component {
99
constructor(props) {
1010
super(props);
@@ -22,20 +22,7 @@ class Timer extends React.Component {
2222
}
2323

2424
showSavedTime() {
25-
const now = new Date();
26-
if (Math.abs(differenceInMilliseconds(now, this.props.projectSavedTime) < 10000)) {
27-
return this.props.t('Timer.SavedJustNow');
28-
} else if (differenceInMilliseconds(now, this.props.projectSavedTime) < 20000) {
29-
return this.props.t('Timer.Saved15Seconds');
30-
} else if (differenceInMilliseconds(now, this.props.projectSavedTime) < 30000) {
31-
return this.props.t('Timer.Saved25Seconds');
32-
} else if (differenceInMilliseconds(now, this.props.projectSavedTime) < 46000) {
33-
return this.props.t('Timer.Saved35Seconds');
34-
}
35-
36-
const timeAgo = distanceInWordsToNow(this.props.projectSavedTime, {
37-
includeSeconds: true
38-
});
25+
const timeAgo = dates.distanceInWordsToNow(this.props.projectSavedTime);
3926
return this.props.t('Timer.SavedAgo', { timeAgo });
4027
}
4128

client/modules/IDE/selectors/collections.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { createSelector } from 'reselect';
2-
import differenceInMilliseconds from 'date-fns/difference_in_milliseconds';
2+
import differenceInMilliseconds from 'date-fns/differenceInMilliseconds';
33
import find from 'lodash/find';
44
import orderBy from 'lodash/orderBy';
55
import { DIRECTION } from '../actions/sorting';

client/modules/IDE/selectors/projects.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { createSelector } from 'reselect';
2-
import differenceInMilliseconds from 'date-fns/difference_in_milliseconds';
2+
import differenceInMilliseconds from 'date-fns/differenceInMilliseconds';
33
import orderBy from 'lodash/orderBy';
44
import { DIRECTION } from '../actions/sorting';
55

client/modules/User/components/APIKeyList.jsx

+3-4
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
import PropTypes from 'prop-types';
22
import React from 'react';
3-
import format from 'date-fns/format';
4-
import distanceInWordsToNow from 'date-fns/distance_in_words_to_now';
53
import orderBy from 'lodash/orderBy';
64

75
import { APIKeyPropType } from './APIKeyForm';
86

7+
import dates from '../../../utils/formatDate';
98
import TrashCanIcon from '../../../images/trash-can.svg';
109

1110
function APIKeyList({ apiKeys, onRemove, t }) {
@@ -22,13 +21,13 @@ function APIKeyList({ apiKeys, onRemove, t }) {
2221
<tbody>
2322
{orderBy(apiKeys, ['createdAt'], ['desc']).map((key) => {
2423
const lastUsed = key.lastUsedAt ?
25-
distanceInWordsToNow(new Date(key.lastUsedAt), { addSuffix: true }) :
24+
dates.distanceInWordsToNow(new Date(key.lastUsedAt)) :
2625
t('APIKeyList.Never');
2726

2827
return (
2928
<tr key={key.id}>
3029
<td>{key.label}</td>
31-
<td>{format(new Date(key.createdAt), 'MMM D, YYYY h:mm A')}</td>
30+
<td>{dates.format(key.createdAt)}</td>
3231
<td>{lastUsed}</td>
3332
<td className="api-key-list__action">
3433
<button

client/modules/User/components/Collection.jsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import format from 'date-fns/format';
21
import PropTypes from 'prop-types';
32
import React, { useState, useRef, useEffect } from 'react';
43
import { Helmet } from 'react-helmet';
@@ -23,6 +22,7 @@ import Overlay from '../../App/components/Overlay';
2322
import AddToCollectionSketchList from '../../IDE/components/AddToCollectionSketchList';
2423
import CopyableInput from '../../IDE/components/CopyableInput';
2524
import { SketchSearchbar } from '../../IDE/components/Searchbar';
25+
import dates from '../../../utils/formatDate';
2626

2727
import ArrowUpIcon from '../../../images/sort-arrow-up.svg';
2828
import ArrowDownIcon from '../../../images/sort-arrow-down.svg';
@@ -101,7 +101,7 @@ const CollectionItemRowBase = ({
101101
<th scope="row">
102102
{name}
103103
</th>
104-
<td>{format(new Date(item.createdAt), 'MMM D, YYYY h:mm A')}</td>
104+
<td>{dates.format(item.createdAt)}</td>
105105
<td>{sketchOwnerUsername}</td>
106106
<td className="collection-row__action-column ">
107107
{isOwner &&

client/utils/formatDate.js

+57
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import formatDistanceToNow from 'date-fns/formatDistanceToNow';
2+
import differenceInMilliseconds from 'date-fns/differenceInMilliseconds';
3+
import format from 'date-fns/format';
4+
import isValid from 'date-fns/isValid';
5+
import parseISO from 'date-fns/parseISO';
6+
import i18next from 'i18next';
7+
8+
import { currentDateLocale } from '../i18n';
9+
10+
function parse(maybeDate) {
11+
const date = maybeDate instanceof Date ? maybeDate : parseISO(maybeDate);
12+
13+
if (isValid(date)) {
14+
return date;
15+
}
16+
17+
return null;
18+
}
19+
20+
export default {
21+
distanceInWordsToNow(date) {
22+
const parsed = parse(date);
23+
24+
if (parsed) {
25+
const now = new Date();
26+
const diffInMs = differenceInMilliseconds(now, parsed);
27+
28+
if (Math.abs(diffInMs < 10000)) {
29+
return i18next.t('formatDate.JustNow');
30+
} else if (diffInMs < 20000) {
31+
return i18next.t('formatDate.15Seconds');
32+
} else if (diffInMs < 30000) {
33+
return i18next.t('formatDate.25Seconds');
34+
} else if (diffInMs < 46000) {
35+
return i18next.t('formatDate.35Seconds');
36+
}
37+
38+
const timeAgo = formatDistanceToNow(parsed, {
39+
includeSeconds: false,
40+
locale: currentDateLocale()
41+
});
42+
return i18next.t('formatDate.Ago', { timeAgo });
43+
}
44+
45+
return '';
46+
},
47+
format(date, { showTime = true } = {}) {
48+
const parsed = parse(date);
49+
const formatType = showTime ? 'PPpp' : 'PP';
50+
51+
if (parsed) {
52+
return format(parsed, formatType, { locale: currentDateLocale() });
53+
}
54+
55+
return '';
56+
}
57+
};

package-lock.json

+11-3
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,7 @@
158158
"cors": "^2.8.5",
159159
"cross-env": "^5.2.1",
160160
"csslint": "^1.0.5",
161-
"date-fns": "^1.30.1",
161+
"date-fns": "^2.16.1",
162162
"decomment": "^0.8.7",
163163
"dotenv": "^2.0.0",
164164
"dropzone": "^4.3.0",

server/controllers/file.controller.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import each from 'async/each';
22
import mime from 'mime-types';
3-
import isBefore from 'date-fns/is_before';
3+
import isBefore from 'date-fns/isBefore';
44
import Project from '../models/project';
55
import { resolvePathToFile } from '../utils/filePath';
66
import { deleteObjectsFromS3, getObjectKey } from './aws.controller';

server/controllers/project.controller.js

+3-3
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import archiver from 'archiver';
22
import format from 'date-fns/format';
33
import isUrl from 'is-url';
44
import jsdom, { serializeDocument } from 'jsdom';
5-
import isAfter from 'date-fns/is_after';
5+
import isAfter from 'date-fns/isAfter';
66
import request from 'request';
77
import slugify from 'slugify';
88
import Project from '../models/project';
@@ -20,7 +20,7 @@ export function updateProject(req, res) {
2020
res.status(403).send({ success: false, message: 'Session does not match owner of project.' });
2121
return;
2222
}
23-
if (req.body.updatedAt && isAfter(new Date(project.updatedAt), req.body.updatedAt)) {
23+
if (req.body.updatedAt && isAfter(new Date(project.updatedAt), new Date(req.body.updatedAt))) {
2424
res.status(409).send({ success: false, message: 'Attempted to save stale version of project.' });
2525
return;
2626
}
@@ -215,7 +215,7 @@ function buildZip(project, req, res) {
215215
res.status(500).send({ error: err.message });
216216
});
217217

218-
const currentTime = format(new Date(), 'YYYY_MM_DD_HH_mm_ss');
218+
const currentTime = format(new Date(), 'yyyy_MM_dd_HH_mm_ss');
219219
project.slug = slugify(project.name, '_');
220220
res.attachment(`${generateFileSystemSafeName(project.slug)}_${currentTime}.zip`);
221221
zip.pipe(res);

server/controllers/project.controller/deleteProject.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import isBefore from 'date-fns/is_before';
1+
import isBefore from 'date-fns/isBefore';
22
import Project from '../../models/project';
33
import { deleteObjectsFromS3, getObjectKey } from '../aws.controller';
44
import createApplicationErrorClass from '../../utils/createApplicationErrorClass';

server/models/__test__/project.test.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import mockingoose from 'mockingoose';
2-
import differenceInSeconds from 'date-fns/difference_in_seconds';
2+
import differenceInSeconds from 'date-fns/differenceInSeconds';
33

44
import Project from '../project';
55

translations/locales/en-US/translations.json

+8-5
Original file line numberDiff line numberDiff line change
@@ -517,11 +517,14 @@
517517
"CurrentLine": " Current line"
518518
},
519519
"Timer": {
520-
"SavedJustNow": "Saved: just now",
521-
"Saved15Seconds": "Saved: 15 seconds ago",
522-
"Saved25Seconds": "Saved: 25 seconds ago",
523-
"Saved35Seconds": "Saved: 35 seconds ago",
524-
"SavedAgo": "Saved: {{timeAgo}} ago"
520+
"SavedAgo": "Saved: {{timeAgo}}"
521+
},
522+
"formatDate": {
523+
"JustNow": "just now",
524+
"15Seconds": "15 seconds ago",
525+
"25Seconds": "25 seconds ago",
526+
"35Seconds": "35 seconds ago",
527+
"Ago": "{{timeAgo}} ago"
525528
},
526529
"AddRemoveButton": {
527530
"AltAddARIA": "Add to collection",

translations/locales/es-419/translations.json

+8-5
Original file line numberDiff line numberDiff line change
@@ -517,11 +517,14 @@
517517
"CurrentLine": " Línea actual"
518518
},
519519
"Timer": {
520-
"SavedJustNow": "Guardado: justo ahora",
521-
"Saved15Seconds": "Guardado: hace 15 segundos",
522-
"Saved25Seconds": "Guardado: hace 25 segundos",
523-
"Saved35Seconds": "Guardado: hace 35 segundos",
524-
"SavedAgo": "Guardado: hace {{timeAgo}}"
520+
"SavedAgo": "Guardado: {{timeAgo}}"
521+
},
522+
"formatDate": {
523+
"JustNow": "justo ahora",
524+
"15Seconds": "hace 15 segundos",
525+
"25Seconds": "hace 25 segundos",
526+
"35Seconds": "hace 35 segundos",
527+
"Ago": "hace {{timeAgo}}"
525528
},
526529
"AddRemoveButton": {
527530
"AltAddARIA": "Agregar a colección",

0 commit comments

Comments
 (0)