From f6276b3b6fecce2b160ed8bdc62a3e87439249e4 Mon Sep 17 00:00:00 2001 From: Grégoire Aubert Date: Fri, 18 Aug 2017 17:47:37 +0200 Subject: SONAR-9385 SONAR-9436 Replace moment with react-intl --- .../src/main/js/helpers/__tests__/dates-test.ts | 69 +++++++++ .../src/main/js/helpers/__tests__/l10n-test.js | 75 ---------- .../src/main/js/helpers/__tests__/l10n-test.ts | 75 ++++++++++ .../src/main/js/helpers/__tests__/query-test.js | 3 +- server/sonar-web/src/main/js/helpers/dates.ts | 90 ++++++++++++ .../sonar-web/src/main/js/helpers/handlebars/d.js | 8 +- .../sonar-web/src/main/js/helpers/handlebars/dt.js | 10 +- .../src/main/js/helpers/handlebars/fromNow.js | 4 +- server/sonar-web/src/main/js/helpers/l10n.js | 144 ------------------- server/sonar-web/src/main/js/helpers/l10n.ts | 155 +++++++++++++++++++++ server/sonar-web/src/main/js/helpers/periods.js | 3 +- server/sonar-web/src/main/js/helpers/query.js | 12 +- server/sonar-web/src/main/js/helpers/testUtils.ts | 9 +- 13 files changed, 420 insertions(+), 237 deletions(-) create mode 100644 server/sonar-web/src/main/js/helpers/__tests__/dates-test.ts delete mode 100644 server/sonar-web/src/main/js/helpers/__tests__/l10n-test.js create mode 100644 server/sonar-web/src/main/js/helpers/__tests__/l10n-test.ts create mode 100644 server/sonar-web/src/main/js/helpers/dates.ts delete mode 100644 server/sonar-web/src/main/js/helpers/l10n.js create mode 100644 server/sonar-web/src/main/js/helpers/l10n.ts (limited to 'server/sonar-web/src/main/js/helpers') diff --git a/server/sonar-web/src/main/js/helpers/__tests__/dates-test.ts b/server/sonar-web/src/main/js/helpers/__tests__/dates-test.ts new file mode 100644 index 00000000000..455886d15a8 --- /dev/null +++ b/server/sonar-web/src/main/js/helpers/__tests__/dates-test.ts @@ -0,0 +1,69 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import * as dates from '../dates'; + +const recentDate = new Date('2017-08-16T12:00:00.000Z'); +const recentDate2 = new Date('2016-12-16T12:00:00.000Z'); +const oldDate = new Date('2014-01-12T12:00:00.000Z'); + +it('toShortNotSoISOString', () => { + expect(dates.toShortNotSoISOString(recentDate)).toBe('2017-08-16'); +}); + +it('toNotSoISOString', () => { + expect(dates.toNotSoISOString(recentDate)).toBe('2017-08-16T12:00:00+0000'); +}); + +it('startOfDay', () => { + expect(dates.startOfDay(recentDate).toTimeString()).toContain('00:00:00'); + expect(dates.startOfDay(recentDate)).not.toBe(recentDate); +}); + +it('isValidDate', () => { + expect(dates.isValidDate(recentDate)).toBeTruthy(); + expect(dates.isValidDate(new Date())).toBeTruthy(); + expect(dates.isValidDate(new Date('foo'))).toBeFalsy(); +}); + +it('isSameDay', () => { + expect(dates.isSameDay(recentDate, new Date(recentDate))).toBeTruthy(); + expect(dates.isSameDay(recentDate, recentDate2)).toBeFalsy(); + expect(dates.isSameDay(recentDate, oldDate)).toBeFalsy(); + expect(dates.isSameDay(recentDate, new Date('2016-08-16T12:00:00.000Z'))).toBeFalsy(); +}); + +it('differenceInYears', () => { + expect(dates.differenceInYears(recentDate, recentDate2)).toBe(0); + expect(dates.differenceInYears(recentDate, oldDate)).toBe(3); + expect(dates.differenceInYears(oldDate, recentDate)).toBe(-3); +}); + +it('differenceInDays', () => { + expect(dates.differenceInDays(recentDate, new Date('2017-08-01T12:00:00.000Z'))).toBe(15); + expect(dates.differenceInDays(recentDate, new Date('2017-08-15T23:00:00.000Z'))).toBe(0); + expect(dates.differenceInDays(recentDate, recentDate2)).toBe(243); + expect(dates.differenceInDays(recentDate, oldDate)).toBe(1312); +}); + +it('differenceInSeconds', () => { + expect(dates.differenceInSeconds(recentDate, new Date('2017-08-16T10:00:00.000Z'))).toBe(7200); + expect(dates.differenceInSeconds(recentDate, new Date('2017-08-16T12:00:00.500Z'))).toBe(0); + expect(dates.differenceInSeconds(recentDate, oldDate)).toBe(113356800); +}); diff --git a/server/sonar-web/src/main/js/helpers/__tests__/l10n-test.js b/server/sonar-web/src/main/js/helpers/__tests__/l10n-test.js deleted file mode 100644 index 3763be42db6..00000000000 --- a/server/sonar-web/src/main/js/helpers/__tests__/l10n-test.js +++ /dev/null @@ -1,75 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2017 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -import { resetBundle, translate, translateWithParameters } from '../l10n'; - -afterEach(() => { - resetBundle({}); -}); - -describe('#translate', () => { - it('should translate simple message', () => { - resetBundle({ my_key: 'my message' }); - expect(translate('my_key')).toBe('my message'); - }); - - it('should translate message with composite key', () => { - resetBundle({ 'my.composite.message': 'my message' }); - expect(translate('my', 'composite', 'message')).toBe('my message'); - expect(translate('my.composite', 'message')).toBe('my message'); - expect(translate('my', 'composite.message')).toBe('my message'); - expect(translate('my.composite.message')).toBe('my message'); - }); - - it('should not translate message but return its key', () => { - expect(translate('random')).toBe('random'); - expect(translate('random', 'key')).toBe('random.key'); - expect(translate('composite.random', 'key')).toBe('composite.random.key'); - }); -}); - -describe('#translateWithParameters', () => { - it('should translate message with one parameter in the beginning', () => { - resetBundle({ x_apples: '{0} apples' }); - expect(translateWithParameters('x_apples', 5)).toBe('5 apples'); - }); - - it('should translate message with one parameter in the middle', () => { - resetBundle({ x_apples: 'I have {0} apples' }); - expect(translateWithParameters('x_apples', 5)).toBe('I have 5 apples'); - }); - - it('should translate message with one parameter in the end', () => { - resetBundle({ x_apples: 'Apples: {0}' }); - expect(translateWithParameters('x_apples', 5)).toBe('Apples: 5'); - }); - - it('should translate message with several parameters', () => { - resetBundle({ x_apples: '{0}: I have {2} apples in my {1} baskets - {3}' }); - expect(translateWithParameters('x_apples', 1, 2, 3, 4)).toBe( - '1: I have 3 apples in my 2 baskets - 4' - ); - }); - - it('should not translate message but return its key', () => { - expect(translateWithParameters('random', 5)).toBe('random.5'); - expect(translateWithParameters('random', 1, 2, 3)).toBe('random.1.2.3'); - expect(translateWithParameters('composite.random', 1, 2)).toBe('composite.random.1.2'); - }); -}); diff --git a/server/sonar-web/src/main/js/helpers/__tests__/l10n-test.ts b/server/sonar-web/src/main/js/helpers/__tests__/l10n-test.ts new file mode 100644 index 00000000000..3763be42db6 --- /dev/null +++ b/server/sonar-web/src/main/js/helpers/__tests__/l10n-test.ts @@ -0,0 +1,75 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import { resetBundle, translate, translateWithParameters } from '../l10n'; + +afterEach(() => { + resetBundle({}); +}); + +describe('#translate', () => { + it('should translate simple message', () => { + resetBundle({ my_key: 'my message' }); + expect(translate('my_key')).toBe('my message'); + }); + + it('should translate message with composite key', () => { + resetBundle({ 'my.composite.message': 'my message' }); + expect(translate('my', 'composite', 'message')).toBe('my message'); + expect(translate('my.composite', 'message')).toBe('my message'); + expect(translate('my', 'composite.message')).toBe('my message'); + expect(translate('my.composite.message')).toBe('my message'); + }); + + it('should not translate message but return its key', () => { + expect(translate('random')).toBe('random'); + expect(translate('random', 'key')).toBe('random.key'); + expect(translate('composite.random', 'key')).toBe('composite.random.key'); + }); +}); + +describe('#translateWithParameters', () => { + it('should translate message with one parameter in the beginning', () => { + resetBundle({ x_apples: '{0} apples' }); + expect(translateWithParameters('x_apples', 5)).toBe('5 apples'); + }); + + it('should translate message with one parameter in the middle', () => { + resetBundle({ x_apples: 'I have {0} apples' }); + expect(translateWithParameters('x_apples', 5)).toBe('I have 5 apples'); + }); + + it('should translate message with one parameter in the end', () => { + resetBundle({ x_apples: 'Apples: {0}' }); + expect(translateWithParameters('x_apples', 5)).toBe('Apples: 5'); + }); + + it('should translate message with several parameters', () => { + resetBundle({ x_apples: '{0}: I have {2} apples in my {1} baskets - {3}' }); + expect(translateWithParameters('x_apples', 1, 2, 3, 4)).toBe( + '1: I have 3 apples in my 2 baskets - 4' + ); + }); + + it('should not translate message but return its key', () => { + expect(translateWithParameters('random', 5)).toBe('random.5'); + expect(translateWithParameters('random', 1, 2, 3)).toBe('random.1.2.3'); + expect(translateWithParameters('composite.random', 1, 2)).toBe('composite.random.1.2'); + }); +}); diff --git a/server/sonar-web/src/main/js/helpers/__tests__/query-test.js b/server/sonar-web/src/main/js/helpers/__tests__/query-test.js index 982f9375a36..11d7b289cae 100644 --- a/server/sonar-web/src/main/js/helpers/__tests__/query-test.js +++ b/server/sonar-web/src/main/js/helpers/__tests__/query-test.js @@ -17,7 +17,6 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import moment from 'moment'; import * as query from '../query'; describe('queriesEqual', () => { @@ -79,7 +78,7 @@ describe('parseAsDate', () => { }); describe('serializeDate', () => { - const date = moment.utc('2016-06-20T13:09:48.256Z'); + const date = new Date('2016-06-20T13:09:48.256Z'); it('should serialize string correctly', () => { expect(query.serializeDate(date)).toBe('2016-06-20T13:09:48+0000'); expect(query.serializeDate('')).toBeUndefined(); diff --git a/server/sonar-web/src/main/js/helpers/dates.ts b/server/sonar-web/src/main/js/helpers/dates.ts new file mode 100644 index 00000000000..5bbb50b3bff --- /dev/null +++ b/server/sonar-web/src/main/js/helpers/dates.ts @@ -0,0 +1,90 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +const MILLISECONDS_IN_MINUTE = 60 * 1000; +const MILLISECONDS_IN_DAY = MILLISECONDS_IN_MINUTE * 60 * 24; + +function pad(number: number) { + if (number < 10) { + return '0' + number; + } + return number; +} + +function compareDateAsc(dateLeft: Date, dateRight: Date): number { + var timeLeft = dateLeft.getTime(); + var timeRight = dateRight.getTime(); + + if (timeLeft < timeRight) { + return -1; + } else if (timeLeft > timeRight) { + return 1; + } else { + return 0; + } +} + +export function toShortNotSoISOString(date: Date): string { + return date.getFullYear() + '-' + pad(date.getMonth() + 1) + '-' + pad(date.getDate()); +} + +export function toNotSoISOString(date: Date): string { + return date.toISOString().replace(/\..+Z$/, '+0000'); +} + +export function startOfDay(date: Date): Date { + const startDay = new Date(date); + startDay.setHours(0, 0, 0, 0); + return startDay; +} + +export function isValidDate(date: Date): boolean { + return !isNaN(date.getTime()); +} + +export function isSameDay(dateLeft: Date, dateRight: Date): boolean { + const startDateLeft = startOfDay(dateLeft); + const startDateRight = startOfDay(dateRight); + return startDateLeft.getTime() === startDateRight.getTime(); +} + +export function differenceInYears(dateLeft: Date, dateRight: Date): number { + const sign = compareDateAsc(dateLeft, dateRight); + const diff = Math.abs(dateLeft.getFullYear() - dateRight.getFullYear()); + const tmpLeftDate = new Date(dateLeft); + tmpLeftDate.setFullYear(dateLeft.getFullYear() - sign * diff); + const isLastYearNotFull = compareDateAsc(tmpLeftDate, dateRight) === -sign; + return sign * (diff - (isLastYearNotFull ? 1 : 0)); +} + +export function differenceInDays(dateLeft: Date, dateRight: Date): number { + const startDateLeft = startOfDay(dateLeft); + const startDateRight = startOfDay(dateRight); + const timestampLeft = + startDateLeft.getTime() - startDateLeft.getTimezoneOffset() * MILLISECONDS_IN_MINUTE; + const timestampRight = + startDateRight.getTime() - startDateRight.getTimezoneOffset() * MILLISECONDS_IN_MINUTE; + return Math.round((timestampLeft - timestampRight) / MILLISECONDS_IN_DAY); +} + +export function differenceInSeconds(dateLeft: Date, dateRight: Date): number { + const diff = (dateLeft.getTime() - dateRight.getTime()) / 1000; + return diff > 0 ? Math.floor(diff) : Math.ceil(diff); +} diff --git a/server/sonar-web/src/main/js/helpers/handlebars/d.js b/server/sonar-web/src/main/js/helpers/handlebars/d.js index d457edd9fdb..ef43101b332 100644 --- a/server/sonar-web/src/main/js/helpers/handlebars/d.js +++ b/server/sonar-web/src/main/js/helpers/handlebars/d.js @@ -17,8 +17,10 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -const moment = require('moment'); - module.exports = function(date) { - return moment(date).format('LL'); + return new Intl.DateTimeFormat(localStorage.getItem('l10n.locale') || 'en', { + year: 'numeric', + month: 'long', + day: 'numeric' + }).format(new Date(date)); }; diff --git a/server/sonar-web/src/main/js/helpers/handlebars/dt.js b/server/sonar-web/src/main/js/helpers/handlebars/dt.js index 708be097e33..3af77ae1d6c 100644 --- a/server/sonar-web/src/main/js/helpers/handlebars/dt.js +++ b/server/sonar-web/src/main/js/helpers/handlebars/dt.js @@ -17,8 +17,12 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -const moment = require('moment'); - module.exports = function(date) { - return moment(date).format('LLL'); + return new Intl.DateTimeFormat(localStorage.getItem('l10n.locale') || 'en', { + year: 'numeric', + month: 'long', + day: 'numeric', + hour: 'numeric', + minute: 'numeric' + }).format(new Date(date)); }; diff --git a/server/sonar-web/src/main/js/helpers/handlebars/fromNow.js b/server/sonar-web/src/main/js/helpers/handlebars/fromNow.js index dc607b8dca2..ea25726d79f 100644 --- a/server/sonar-web/src/main/js/helpers/handlebars/fromNow.js +++ b/server/sonar-web/src/main/js/helpers/handlebars/fromNow.js @@ -17,8 +17,8 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -const moment = require('moment'); +const IntlRelativeFormat = require('intl-relativeformat'); module.exports = function(date) { - return moment(date).fromNow(); + return new IntlRelativeFormat(localStorage.getItem('l10n.locale') || 'en').format(date); }; diff --git a/server/sonar-web/src/main/js/helpers/l10n.js b/server/sonar-web/src/main/js/helpers/l10n.js deleted file mode 100644 index 1f5ebda6796..00000000000 --- a/server/sonar-web/src/main/js/helpers/l10n.js +++ /dev/null @@ -1,144 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2017 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -/* @flow */ -import moment from 'moment'; -import { getJSON } from './request'; - -let messages = {}; - -export function translate(...keys /*: string[] */) { - const messageKey = keys.join('.'); - return messages[messageKey] || messageKey; -} - -export function translateWithParameters( - messageKey /*: string */, - ...parameters /*: Array */ -) { - const message = messages[messageKey]; - if (message) { - return parameters - .map(parameter => String(parameter)) - .reduce((acc, parameter, index) => acc.replace(`{${index}}`, parameter), message); - } else { - return `${messageKey}.${parameters.join('.')}`; - } -} - -export function hasMessage(...keys /*: string[] */) { - const messageKey = keys.join('.'); - return messages[messageKey] != null; -} - -export function configureMoment(language /*: ?string */) { - moment.locale(language || getPreferredLanguage()); -} - -function getPreferredLanguage() { - return window.navigator.languages ? window.navigator.languages[0] : window.navigator.language; -} - -function checkCachedBundle() { - const cached = localStorage.getItem('l10n.bundle'); - - if (!cached) { - return false; - } - - try { - const parsed = JSON.parse(cached); - return parsed != null && typeof parsed === 'object'; - } catch (e) { - return false; - } -} - -function getL10nBundle(params) { - const url = '/api/l10n/index'; - return getJSON(url, params); -} - -export function requestMessages() { - const browserLocale = getPreferredLanguage(); - const cachedLocale = localStorage.getItem('l10n.locale'); - const params = {}; - - if (browserLocale) { - params.locale = browserLocale; - - if (browserLocale.startsWith(cachedLocale)) { - const bundleTimestamp = localStorage.getItem('l10n.timestamp'); - if (bundleTimestamp !== null && checkCachedBundle()) { - params.ts = bundleTimestamp; - } - } - } - - return getL10nBundle(params).then( - ({ effectiveLocale, messages }) => { - try { - const currentTimestamp = moment().format('YYYY-MM-DDTHH:mm:ssZZ'); - localStorage.setItem('l10n.timestamp', currentTimestamp); - localStorage.setItem('l10n.locale', effectiveLocale); - localStorage.setItem('l10n.bundle', JSON.stringify(messages)); - } catch (e) { - // do nothing - } - configureMoment(effectiveLocale); - resetBundle(messages); - }, - ({ response }) => { - if (response && response.status === 304) { - configureMoment(cachedLocale || browserLocale); - resetBundle(JSON.parse(localStorage.getItem('l10n.bundle') || '{}')); - } else { - throw new Error('Unexpected status code: ' + response.status); - } - } - ); -} - -export function resetBundle(bundle /*: Object */) { - messages = bundle; -} - -export function installGlobal() { - window.t = translate; - window.tp = translateWithParameters; - window.requestMessages = requestMessages; -} - -export function getLocalizedDashboardName(baseName /*: string */) { - const l10nKey = `dashboard.${baseName}.name`; - const l10nLabel = translate(l10nKey); - return l10nLabel !== l10nKey ? l10nLabel : baseName; -} - -export function getLocalizedMetricName(metric /*: { key: string, name: string } */) { - const bundleKey = `metric.${metric.key}.name`; - const fromBundle = translate(bundleKey); - return fromBundle !== bundleKey ? fromBundle : metric.name; -} - -export function getLocalizedMetricDomain(domainName /*: string */) { - const bundleKey = `metric_domain.${domainName}`; - const fromBundle = translate(bundleKey); - return fromBundle !== bundleKey ? fromBundle : domainName; -} diff --git a/server/sonar-web/src/main/js/helpers/l10n.ts b/server/sonar-web/src/main/js/helpers/l10n.ts new file mode 100644 index 00000000000..57f51949074 --- /dev/null +++ b/server/sonar-web/src/main/js/helpers/l10n.ts @@ -0,0 +1,155 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import { getJSON } from './request'; +import { toNotSoISOString } from './dates'; + +interface LanguageBundle { + [name: string]: string; +} + +interface BundleRequestParams { + locale?: string; + ts?: string; +} + +interface BundleRequestResponse { + effectiveLocale: string; + messages: LanguageBundle; +} + +let messages: LanguageBundle = {}; + +export const DEFAULT_LANGUAGE = 'en'; + +export function translate(...keys: string[]): string { + const messageKey = keys.join('.'); + return messages[messageKey] || messageKey; +} + +export function translateWithParameters( + messageKey: string, + ...parameters: Array +): string { + const message = messages[messageKey]; + if (message) { + return parameters + .map(parameter => String(parameter)) + .reduce((acc, parameter, index) => acc.replace(`{${index}}`, parameter), message); + } else { + return `${messageKey}.${parameters.join('.')}`; + } +} + +export function hasMessage(...keys: string[]): boolean { + const messageKey = keys.join('.'); + return messages[messageKey] != null; +} + +function getPreferredLanguage(): string | undefined { + return window.navigator.languages ? window.navigator.languages[0] : window.navigator.language; +} + +function checkCachedBundle(): boolean { + const cached = localStorage.getItem('l10n.bundle'); + + if (!cached) { + return false; + } + + try { + const parsed = JSON.parse(cached); + return parsed != null && typeof parsed === 'object'; + } catch (e) { + return false; + } +} + +function getL10nBundle(params: BundleRequestParams): Promise { + const url = '/api/l10n/index'; + return getJSON(url, params); +} + +export function requestMessages(): Promise { + const browserLocale = getPreferredLanguage(); + const cachedLocale = localStorage.getItem('l10n.locale'); + const params: BundleRequestParams = {}; + + if (browserLocale) { + params.locale = browserLocale; + + if (cachedLocale && browserLocale.startsWith(cachedLocale)) { + const bundleTimestamp = localStorage.getItem('l10n.timestamp'); + if (bundleTimestamp !== null && checkCachedBundle()) { + params.ts = bundleTimestamp; + } + } + } + + return getL10nBundle(params).then( + ({ effectiveLocale, messages }: BundleRequestResponse) => { + try { + const currentTimestamp = toNotSoISOString(new Date()); + localStorage.setItem('l10n.timestamp', currentTimestamp); + localStorage.setItem('l10n.locale', effectiveLocale); + localStorage.setItem('l10n.bundle', JSON.stringify(messages)); + } catch (e) { + // do nothing + } + resetBundle(messages); + return effectiveLocale || browserLocale || DEFAULT_LANGUAGE; + }, + ({ response }) => { + if (response && response.status === 304) { + resetBundle(JSON.parse(localStorage.getItem('l10n.bundle') || '{}')); + } else { + throw new Error('Unexpected status code: ' + response.status); + } + return cachedLocale || browserLocale || DEFAULT_LANGUAGE; + } + ); +} + +export function resetBundle(bundle: LanguageBundle) { + messages = bundle; +} + +export function installGlobal() { + (window as any).t = translate; + (window as any).tp = translateWithParameters; + (window as any).requestMessages = requestMessages; +} + +export function getLocalizedDashboardName(baseName: string) { + const l10nKey = `dashboard.${baseName}.name`; + const l10nLabel = translate(l10nKey); + return l10nLabel !== l10nKey ? l10nLabel : baseName; +} + +export function getLocalizedMetricName(metric: { key: string; name: string }) { + const bundleKey = `metric.${metric.key}.name`; + const fromBundle = translate(bundleKey); + return fromBundle !== bundleKey ? fromBundle : metric.name; +} + +export function getLocalizedMetricDomain(domainName: string) { + const bundleKey = `metric_domain.${domainName}`; + const fromBundle = translate(bundleKey); + return fromBundle !== bundleKey ? fromBundle : domainName; +} diff --git a/server/sonar-web/src/main/js/helpers/periods.js b/server/sonar-web/src/main/js/helpers/periods.js index 0677d81c13c..4c5ac1c876d 100644 --- a/server/sonar-web/src/main/js/helpers/periods.js +++ b/server/sonar-web/src/main/js/helpers/periods.js @@ -17,7 +17,6 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import moment from 'moment'; import { translate, translateWithParameters } from './l10n'; export function getPeriod(periods, index) { @@ -51,7 +50,7 @@ export function getPeriodDate(period) { return null; } - return moment(period.date).toDate(); + return new Date(period.date); } export function getLeakPeriodLabel(periods) { diff --git a/server/sonar-web/src/main/js/helpers/query.js b/server/sonar-web/src/main/js/helpers/query.js index f7c0f2b6a9c..a87eefe5e3f 100644 --- a/server/sonar-web/src/main/js/helpers/query.js +++ b/server/sonar-web/src/main/js/helpers/query.js @@ -19,7 +19,7 @@ */ // @flow import { isNil, omitBy } from 'lodash'; -import moment from 'moment'; +import { isValidDate, toNotSoISOString } from './dates'; /*:: export type RawQuery = { [string]: any }; @@ -65,9 +65,11 @@ export function parseAsBoolean( } export function parseAsDate(value /*: ?string */) /*: Date | void */ { - const date = moment(value); - if (value && date) { - return date.toDate(); + if (value) { + const date = new Date(value); + if (isValidDate(date)) { + return date; + } } } @@ -85,7 +87,7 @@ export function parseAsArray(value /*: ?string */, itemParser /*: string => * */ export function serializeDate(value /*: ?Date */) /*: string | void */ { if (value != null && value.toISOString) { - return moment(value).format('YYYY-MM-DDTHH:mm:ssZZ'); + return toNotSoISOString(value); } } diff --git a/server/sonar-web/src/main/js/helpers/testUtils.ts b/server/sonar-web/src/main/js/helpers/testUtils.ts index deed3501e74..a0931769355 100644 --- a/server/sonar-web/src/main/js/helpers/testUtils.ts +++ b/server/sonar-web/src/main/js/helpers/testUtils.ts @@ -17,7 +17,8 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import { ShallowWrapper } from 'enzyme'; +import { shallow, ShallowWrapper } from 'enzyme'; +import { IntlProvider } from 'react-intl'; export const mockEvent = { target: { blur() {} }, @@ -69,3 +70,9 @@ export function doAsync(fn: Function): Promise { }, 0); }); } + +const intlProvider = new IntlProvider({ locale: 'en' }, {}); +const { intl } = intlProvider.getChildContext(); +export function shallowWithIntl(node, options = {}) { + return shallow(node, { ...options, context: { intl, ...options.context } }); +} -- cgit v1.2.3