aboutsummaryrefslogtreecommitdiffstats
path: root/server/sonar-web/src/main/js/helpers
diff options
context:
space:
mode:
authorPhilippe Perrin <philippe.perrin@sonarsource.com>2022-01-28 17:39:50 +0100
committersonartech <sonartech@sonarsource.com>2022-03-29 20:03:37 +0000
commitb345bbc09eb8e0bd04951e1db88f56666f5f4c6a (patch)
treed61378aa709f81a1c6a9729b6c792e404700dfcf /server/sonar-web/src/main/js/helpers
parentc1ea1e9df27db899c07948ce50e6aa9cceab348a (diff)
downloadsonarqube-b345bbc09eb8e0bd04951e1db88f56666f5f4c6a.tar.gz
sonarqube-b345bbc09eb8e0bd04951e1db88f56666f5f4c6a.zip
SONAR-15938 Improve code sharing with the license extension
Diffstat (limited to 'server/sonar-web/src/main/js/helpers')
-rw-r--r--server/sonar-web/src/main/js/helpers/__tests__/error-test.ts94
-rw-r--r--server/sonar-web/src/main/js/helpers/__tests__/l10n-test.ts90
-rw-r--r--server/sonar-web/src/main/js/helpers/__tests__/l10nBundle-test.ts71
-rw-r--r--server/sonar-web/src/main/js/helpers/__tests__/measures-test.ts58
-rw-r--r--server/sonar-web/src/main/js/helpers/error.ts45
-rw-r--r--server/sonar-web/src/main/js/helpers/l10n.ts129
-rw-r--r--server/sonar-web/src/main/js/helpers/l10nBundle.ts93
-rw-r--r--server/sonar-web/src/main/js/helpers/measures.ts3
-rw-r--r--server/sonar-web/src/main/js/helpers/mocks/editions.ts41
9 files changed, 406 insertions, 218 deletions
diff --git a/server/sonar-web/src/main/js/helpers/__tests__/error-test.ts b/server/sonar-web/src/main/js/helpers/__tests__/error-test.ts
new file mode 100644
index 00000000000..ca6f81a60e9
--- /dev/null
+++ b/server/sonar-web/src/main/js/helpers/__tests__/error-test.ts
@@ -0,0 +1,94 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2022 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 getStore from '../../app/utils/getStore';
+import { throwGlobalError } from '../error';
+
+beforeAll(() => {
+ jest.useFakeTimers();
+});
+
+afterAll(() => {
+ jest.runOnlyPendingTimers();
+ jest.useRealTimers();
+});
+
+it('should put the error message in the store', async () => {
+ const response = new Response();
+ response.json = jest.fn().mockResolvedValue({ errors: [{ msg: 'error 1' }] });
+
+ // We need to catch because throwGlobalError rethrows after displaying the message
+ await throwGlobalError(response)
+ .then(() => {
+ throw new Error('Should throw');
+ })
+ .catch(() => {});
+
+ expect(getStore().getState().globalMessages[0]).toMatchObject({
+ level: 'ERROR',
+ message: 'error 1'
+ });
+});
+
+it('should put a default error messsage in the store', async () => {
+ const response = new Response();
+ response.json = jest.fn().mockResolvedValue({});
+
+ // We need to catch because throwGlobalError rethrows after displaying the message
+ await throwGlobalError(response)
+ .then(() => {
+ throw new Error('Should throw');
+ })
+ .catch(() => {});
+
+ expect(getStore().getState().globalMessages[0]).toMatchObject({
+ level: 'ERROR',
+ message: 'default_error_message'
+ });
+});
+
+it('should handle weird response types', () => {
+ const response = { weird: 'response type' };
+
+ return throwGlobalError(response)
+ .then(() => {
+ throw new Error('Should throw');
+ })
+ .catch(error => {
+ expect(error).toBe(response);
+ });
+});
+
+it('should unwrap response if necessary', async () => {
+ const response = new Response();
+ response.json = jest.fn().mockResolvedValue({});
+
+ /* eslint-disable-next-line no-console */
+ console.warn = jest.fn();
+
+ // We need to catch because throwGlobalError rethrows after displaying the message
+ await throwGlobalError({ response })
+ .then(() => {
+ throw new Error('Should throw');
+ })
+ .catch(() => {});
+
+ /* eslint-disable-next-line no-console */
+ expect(console.warn).toHaveBeenCalled();
+});
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
index 58d2f94461d..346d097dce8 100644
--- a/server/sonar-web/src/main/js/helpers/__tests__/l10n-test.ts
+++ b/server/sonar-web/src/main/js/helpers/__tests__/l10n-test.ts
@@ -17,84 +17,51 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import { fetchL10nBundle } from '../../api/l10n';
+import { Dict } from '../../types/types';
import {
getLocalizedCategoryMetricName,
getLocalizedMetricDomain,
getLocalizedMetricName,
- getMessages,
getShortMonthName,
getShortWeekDayName,
getWeekDayName,
hasMessage,
- loadL10nBundle,
- resetMessages,
translate,
translateWithParameters
} from '../l10n';
-import { get } from '../storage';
+import { getMessages } from '../l10nBundle';
-beforeEach(() => {
- jest.clearAllMocks();
- jest.spyOn(window.navigator, 'languages', 'get').mockReturnValue(['de']);
-});
+const MSG = 'my_message';
-jest.mock('../../api/l10n', () => ({
- fetchL10nBundle: jest
- .fn()
- .mockResolvedValue({ effectiveLocale: 'de', messages: { test_message: 'test' } })
-}));
+jest.unmock('../l10n');
-jest.mock('../../helpers/storage', () => ({
- get: jest.fn(),
- save: jest.fn()
+jest.mock('../l10nBundle', () => ({
+ getMessages: jest.fn().mockReturnValue({})
}));
-describe('#loadL10nBundle', () => {
- it('should fetch bundle without any timestamp', async () => {
- await loadL10nBundle();
-
- expect(fetchL10nBundle).toHaveBeenCalledWith({ locale: 'de', ts: undefined });
- });
-
- it('should ftech bundle without local storage timestamp if locales are different', async () => {
- const cachedBundle = { timestamp: 'timestamp', locale: 'fr', messages: { cache: 'cache' } };
- (get as jest.Mock).mockReturnValueOnce(JSON.stringify(cachedBundle));
-
- await loadL10nBundle();
-
- expect(fetchL10nBundle).toHaveBeenCalledWith({ locale: 'de', ts: undefined });
- });
-
- it('should fetch bundle with cached bundle timestamp and browser locale', async () => {
- const cachedBundle = { timestamp: 'timestamp', locale: 'de', messages: { cache: 'cache' } };
- (get as jest.Mock).mockReturnValueOnce(JSON.stringify(cachedBundle));
+const resetMessages = (messages: Dict<string>) =>
+ (getMessages as jest.Mock).mockReturnValue(messages);
- await loadL10nBundle();
+beforeEach(() => {
+ resetMessages({});
+});
- expect(fetchL10nBundle).toHaveBeenCalledWith({ locale: 'de', ts: cachedBundle.timestamp });
+describe('hasMessage', () => {
+ it('should return that the message exists', () => {
+ resetMessages({
+ foo: 'foo',
+ 'foo.bar': 'foobar'
+ });
+ expect(hasMessage('foo')).toBe(true);
+ expect(hasMessage('foo', 'bar')).toBe(true);
});
- it('should fallback to cached bundle if the server respond with 304', async () => {
- const cachedBundle = { timestamp: 'timestamp', locale: 'fr', messages: { cache: 'cache' } };
- (fetchL10nBundle as jest.Mock).mockRejectedValueOnce({ status: 304 });
- (get as jest.Mock).mockReturnValueOnce(JSON.stringify(cachedBundle));
-
- const bundle = await loadL10nBundle();
-
- expect(bundle).toEqual(
- expect.objectContaining({ locale: cachedBundle.locale, messages: cachedBundle.messages })
- );
+ it('should return that the message is missing', () => {
+ expect(hasMessage('foo')).toBe(false);
+ expect(hasMessage('foo', 'bar')).toBe(false);
});
});
-const originalMessages = getMessages();
-const MSG = 'my_message';
-
-afterEach(() => {
- resetMessages(originalMessages);
-});
-
describe('translate', () => {
it('should translate simple message', () => {
resetMessages({ my_key: MSG });
@@ -153,19 +120,6 @@ describe('translateWithParameters', () => {
});
});
-describe('hasMessage', () => {
- it('should return that the message exists', () => {
- resetMessages({ foo: 'Foo', 'foo.bar': 'Foo Bar' });
- expect(hasMessage('foo')).toBe(true);
- expect(hasMessage('foo', 'bar')).toBe(true);
- });
-
- it('should return that the message is missing', () => {
- expect(hasMessage('foo')).toBe(false);
- expect(hasMessage('foo', 'bar')).toBe(false);
- });
-});
-
describe('getLocalizedMetricName', () => {
const metric = { key: 'new_code', name: 'new_code_metric_name' };
diff --git a/server/sonar-web/src/main/js/helpers/__tests__/l10nBundle-test.ts b/server/sonar-web/src/main/js/helpers/__tests__/l10nBundle-test.ts
new file mode 100644
index 00000000000..58c1e603b51
--- /dev/null
+++ b/server/sonar-web/src/main/js/helpers/__tests__/l10nBundle-test.ts
@@ -0,0 +1,71 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2022 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 { fetchL10nBundle } from '../../api/l10n';
+import { loadL10nBundle } from '../l10nBundle';
+
+beforeEach(() => {
+ jest.clearAllMocks();
+ jest.spyOn(window.navigator, 'languages', 'get').mockReturnValue(['de']);
+});
+
+jest.mock('../../api/l10n', () => ({
+ fetchL10nBundle: jest.fn().mockResolvedValue({
+ effectiveLocale: 'de',
+ messages: { foo: 'Foo', 'foo.bar': 'Foo Bar' }
+ })
+}));
+
+describe('#loadL10nBundle', () => {
+ it('should fetch bundle without any timestamp', async () => {
+ await loadL10nBundle();
+
+ expect(fetchL10nBundle).toHaveBeenCalledWith({ locale: 'de', ts: undefined });
+ });
+
+ it('should ftech bundle without local storage timestamp if locales are different', async () => {
+ const cachedBundle = { timestamp: 'timestamp', locale: 'fr', messages: { cache: 'cache' } };
+ ((window as unknown) as any).sonarQubeL10nBundle = cachedBundle;
+
+ await loadL10nBundle();
+
+ expect(fetchL10nBundle).toHaveBeenCalledWith({ locale: 'de', ts: undefined });
+ });
+
+ it('should fetch bundle with cached bundle timestamp and browser locale', async () => {
+ const cachedBundle = { timestamp: 'timestamp', locale: 'de', messages: { cache: 'cache' } };
+ ((window as unknown) as any).sonarQubeL10nBundle = cachedBundle;
+
+ await loadL10nBundle();
+
+ expect(fetchL10nBundle).toHaveBeenCalledWith({ locale: 'de', ts: cachedBundle.timestamp });
+ });
+
+ it('should fallback to cached bundle if the server respond with 304', async () => {
+ const cachedBundle = { timestamp: 'timestamp', locale: 'fr', messages: { cache: 'cache' } };
+ (fetchL10nBundle as jest.Mock).mockRejectedValueOnce({ status: 304 });
+ ((window as unknown) as any).sonarQubeL10nBundle = cachedBundle;
+
+ const bundle = await loadL10nBundle();
+
+ expect(bundle).toEqual(
+ expect.objectContaining({ locale: cachedBundle.locale, messages: cachedBundle.messages })
+ );
+ });
+});
diff --git a/server/sonar-web/src/main/js/helpers/__tests__/measures-test.ts b/server/sonar-web/src/main/js/helpers/__tests__/measures-test.ts
index 04a0bfa2ffa..3a301c9b9b6 100644
--- a/server/sonar-web/src/main/js/helpers/__tests__/measures-test.ts
+++ b/server/sonar-web/src/main/js/helpers/__tests__/measures-test.ts
@@ -17,7 +17,9 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import { resetMessages } from '../l10n';
+
+import { Dict } from '../../types/types';
+import { getMessages } from '../l10nBundle';
import {
enhanceConditionWithMeasure,
formatMeasure,
@@ -27,6 +29,36 @@ import {
import { mockQualityGateStatusCondition } from '../mocks/quality-gates';
import { mockMeasureEnhanced, mockMetric } from '../testMocks';
+jest.unmock('../l10n');
+
+jest.mock('../l10nBundle', () => ({
+ getCurrentLocale: jest.fn().mockReturnValue('us'),
+ getMessages: jest.fn().mockReturnValue({})
+}));
+
+const resetMessages = (messages: Dict<string>) =>
+ (getMessages as jest.Mock).mockReturnValue(messages);
+
+beforeAll(() => {
+ resetMessages({
+ 'work_duration.x_days': '{0}d',
+ 'work_duration.x_hours': '{0}h',
+ 'work_duration.x_minutes': '{0}min',
+ 'work_duration.about': '~ {0}',
+ 'metric.level.ERROR': 'Error',
+ 'metric.level.WARN': 'Warning',
+ 'metric.level.OK': 'Ok',
+ 'short_number_suffix.g': 'G',
+ 'short_number_suffix.k': 'k',
+ 'short_number_suffix.m': 'M'
+ });
+});
+
+const HOURS_IN_DAY = 8;
+const ONE_MINUTE = 1;
+const ONE_HOUR = ONE_MINUTE * 60;
+const ONE_DAY = HOURS_IN_DAY * ONE_HOUR;
+
describe('enhanceConditionWithMeasure', () => {
it('should correctly map enhance conditions with measure data', () => {
const measures = [
@@ -71,30 +103,6 @@ describe('isPeriodBestValue', () => {
});
});
-const HOURS_IN_DAY = 8;
-const ONE_MINUTE = 1;
-const ONE_HOUR = ONE_MINUTE * 60;
-const ONE_DAY = HOURS_IN_DAY * ONE_HOUR;
-
-beforeAll(() => {
- resetMessages({
- 'work_duration.x_days': '{0}d',
- 'work_duration.x_hours': '{0}h',
- 'work_duration.x_minutes': '{0}min',
- 'work_duration.about': '~ {0}',
- 'metric.level.ERROR': 'Error',
- 'metric.level.WARN': 'Warning',
- 'metric.level.OK': 'Ok',
- 'short_number_suffix.g': 'G',
- 'short_number_suffix.k': 'k',
- 'short_number_suffix.m': 'M'
- });
-});
-
-afterAll(() => {
- resetMessages({});
-});
-
describe('#formatMeasure()', () => {
it('should format INT', () => {
expect(formatMeasure(0, 'INT')).toBe('0');
diff --git a/server/sonar-web/src/main/js/helpers/error.ts b/server/sonar-web/src/main/js/helpers/error.ts
new file mode 100644
index 00000000000..78f2fa12d9a
--- /dev/null
+++ b/server/sonar-web/src/main/js/helpers/error.ts
@@ -0,0 +1,45 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2022 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 getStore from '../app/utils/getStore';
+import { addGlobalErrorMessage } from '../store/globalMessages';
+import { parseError } from './request';
+
+export function throwGlobalError(param: Response | any): Promise<Response | any> {
+ const store = getStore();
+
+ if (param.response instanceof Response) {
+ /* eslint-disable-next-line no-console */
+ console.warn('DEPRECATED: response should not be wrapped, pass it directly.');
+ param = param.response;
+ }
+
+ if (param instanceof Response) {
+ return parseError(param)
+ .then(
+ message => {
+ store.dispatch(addGlobalErrorMessage(message));
+ },
+ () => {}
+ )
+ .then(() => Promise.reject(param));
+ }
+
+ return Promise.reject(param);
+}
diff --git a/server/sonar-web/src/main/js/helpers/l10n.ts b/server/sonar-web/src/main/js/helpers/l10n.ts
index 9b8fafa258e..149955f2844 100644
--- a/server/sonar-web/src/main/js/helpers/l10n.ts
+++ b/server/sonar-web/src/main/js/helpers/l10n.ts
@@ -17,22 +17,13 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import { fetchL10nBundle } from '../api/l10n';
-import { L10nBundle, L10nBundleRequestParams } from '../types/l10n';
-import { Dict } from '../types/types';
-import { toNotSoISOString } from './dates';
-import { get as loadFromLocalStorage, save as saveInLocalStorage } from './storage';
-export type Messages = Dict<string>;
+import { getMessages } from './l10nBundle';
-export const DEFAULT_LOCALE = 'en';
-export const DEFAULT_MESSAGES = {
- // eslint-disable-next-line camelcase
- default_error_message: 'The request cannot be processed. Try again later.'
-};
-
-let allMessages: Messages = {};
-let locale: string | undefined;
+export function hasMessage(...keys: string[]): boolean {
+ const messageKey = keys.join('.');
+ return getMessages()[messageKey] != null;
+}
export function translate(...keys: string[]): string {
const messageKey = keys.join('.');
@@ -61,23 +52,6 @@ export function translateWithParameters(
return `${messageKey}.${parameters.join('.')}`;
}
-export function hasMessage(...keys: string[]): boolean {
- const messageKey = keys.join('.');
- return getMessages()[messageKey] != null;
-}
-
-export function getMessages() {
- if (typeof allMessages === 'undefined') {
- logWarning('L10n messages are not initialized. Use default messages.');
- return DEFAULT_MESSAGES;
- }
- return allMessages;
-}
-
-export function resetMessages(newMessages: Messages) {
- allMessages = newMessages;
-}
-
export function getLocalizedMetricName(
metric: { key: string; name?: string },
short = false
@@ -101,14 +75,6 @@ export function getLocalizedMetricDomain(domainName: string) {
return hasMessage(bundleKey) ? translate(bundleKey) : domainName;
}
-export function getCurrentLocale() {
- return locale;
-}
-
-export function resetCurrentLocale(newLocale: string) {
- locale = newLocale;
-}
-
export function getShortMonthName(index: number) {
const months = [
'Jan',
@@ -136,88 +102,3 @@ export function getShortWeekDayName(index: number) {
const weekdays = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
return weekdays[index] ? translate(weekdays[index]) : '';
}
-
-const L10N_BUNDLE_LS_KEY = 'l10n.bundle';
-
-export async function loadL10nBundle() {
- const bundle = await getLatestL10nBundle().catch(() => ({
- locale: DEFAULT_LOCALE,
- messages: {}
- }));
-
- resetCurrentLocale(bundle.locale);
- resetMessages(bundle.messages);
-
- return bundle;
-}
-
-export async function getLatestL10nBundle() {
- const browserLocale = getPreferredLanguage();
- const cachedBundle = loadL10nBundleFromLocalStorage();
-
- const params: L10nBundleRequestParams = {};
-
- if (browserLocale) {
- params.locale = browserLocale;
-
- if (
- cachedBundle.locale &&
- browserLocale.startsWith(cachedBundle.locale) &&
- cachedBundle.timestamp &&
- cachedBundle.messages
- ) {
- params.ts = cachedBundle.timestamp;
- }
- }
-
- const { effectiveLocale, messages } = await fetchL10nBundle(params).catch(response => {
- if (response && response.status === 304) {
- return {
- effectiveLocale: cachedBundle.locale || browserLocale || DEFAULT_LOCALE,
- messages: cachedBundle.messages ?? {}
- };
- }
- throw new Error(`Unexpected status code: ${response.status}`);
- });
-
- const bundle = {
- timestamp: toNotSoISOString(new Date()),
- locale: effectiveLocale,
- messages
- };
-
- saveL10nBundleToLocalStorage(bundle);
-
- return bundle;
-}
-
-export function getCurrentL10nBundle() {
- return loadL10nBundleFromLocalStorage();
-}
-
-function getPreferredLanguage() {
- return window.navigator.languages ? window.navigator.languages[0] : window.navigator.language;
-}
-
-function loadL10nBundleFromLocalStorage() {
- let bundle: L10nBundle;
-
- try {
- bundle = JSON.parse(loadFromLocalStorage(L10N_BUNDLE_LS_KEY) ?? '{}');
- } catch {
- bundle = {};
- }
-
- return bundle;
-}
-
-function saveL10nBundleToLocalStorage(bundle: L10nBundle) {
- saveInLocalStorage(L10N_BUNDLE_LS_KEY, JSON.stringify(bundle));
-}
-
-function logWarning(message: string) {
- if (process.env.NODE_ENV !== 'production') {
- // eslint-disable-next-line no-console
- console.warn(message);
- }
-}
diff --git a/server/sonar-web/src/main/js/helpers/l10nBundle.ts b/server/sonar-web/src/main/js/helpers/l10nBundle.ts
new file mode 100644
index 00000000000..fc1a4414d57
--- /dev/null
+++ b/server/sonar-web/src/main/js/helpers/l10nBundle.ts
@@ -0,0 +1,93 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2022 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 { fetchL10nBundle } from '../api/l10n';
+import { L10nBundle, L10nBundleRequestParams } from '../types/l10nBundle';
+import { toNotSoISOString } from './dates';
+
+const DEFAULT_LOCALE = 'en';
+const DEFAULT_MESSAGES = {
+ // eslint-disable-next-line camelcase
+ default_error_message: 'The request cannot be processed. Try again later.'
+};
+
+export function getMessages() {
+ return getL10nBundleFromCache().messages ?? DEFAULT_MESSAGES;
+}
+
+export function getCurrentLocale() {
+ return getL10nBundleFromCache().locale;
+}
+
+export function getCurrentL10nBundle() {
+ return getL10nBundleFromCache();
+}
+
+export async function loadL10nBundle() {
+ const browserLocale = getPreferredLanguage();
+ const cachedBundle = getL10nBundleFromCache();
+
+ const params: L10nBundleRequestParams = {};
+
+ if (browserLocale) {
+ params.locale = browserLocale;
+
+ if (
+ cachedBundle.locale &&
+ browserLocale.startsWith(cachedBundle.locale) &&
+ cachedBundle.timestamp &&
+ cachedBundle.messages
+ ) {
+ params.ts = cachedBundle.timestamp;
+ }
+ }
+
+ const { effectiveLocale, messages } = await fetchL10nBundle(params).catch(response => {
+ if (response && response.status === 304) {
+ return {
+ effectiveLocale: cachedBundle.locale || browserLocale || DEFAULT_LOCALE,
+ messages: cachedBundle.messages ?? {}
+ };
+ }
+ throw new Error(`Unexpected status code: ${response.status}`);
+ });
+
+ const bundle = {
+ timestamp: toNotSoISOString(new Date()),
+ locale: effectiveLocale,
+ messages
+ };
+
+ persistL10nBundleInCache(bundle);
+
+ return bundle;
+}
+
+function getPreferredLanguage() {
+ return window.navigator.languages ? window.navigator.languages[0] : window.navigator.language;
+}
+
+function getL10nBundleFromCache() {
+ return ((window as unknown) as any).sonarQubeL10nBundle ?? {};
+}
+
+function persistL10nBundleInCache(bundle: L10nBundle) {
+ ((window as unknown) as any).sonarQubeL10nBundle = bundle;
+}
diff --git a/server/sonar-web/src/main/js/helpers/measures.ts b/server/sonar-web/src/main/js/helpers/measures.ts
index cedf304914a..31aca41f983 100644
--- a/server/sonar-web/src/main/js/helpers/measures.ts
+++ b/server/sonar-web/src/main/js/helpers/measures.ts
@@ -23,7 +23,8 @@ import {
QualityGateStatusConditionEnhanced
} from '../types/quality-gates';
import { Dict, Measure, MeasureEnhanced, Metric } from '../types/types';
-import { getCurrentLocale, translate, translateWithParameters } from './l10n';
+import { translate, translateWithParameters } from './l10n';
+import { getCurrentLocale } from './l10nBundle';
import { isDefined } from './types';
export function enhanceMeasuresWithMetrics(
diff --git a/server/sonar-web/src/main/js/helpers/mocks/editions.ts b/server/sonar-web/src/main/js/helpers/mocks/editions.ts
new file mode 100644
index 00000000000..58fb0b759cb
--- /dev/null
+++ b/server/sonar-web/src/main/js/helpers/mocks/editions.ts
@@ -0,0 +1,41 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2022 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 { License } from '../../types/editions';
+
+export function mockLicense(override?: Partial<License>) {
+ return {
+ contactEmail: 'contact@sonarsource.com',
+ edition: 'Developer Edition',
+ expiresAt: '2018-05-18',
+ isExpired: false,
+ isValidEdition: true,
+ isValidServerId: true,
+ isOfficialDistribution: true,
+ isSupported: false,
+ loc: 120085,
+ maxLoc: 500000,
+ plugins: ['Branches', 'PLI language'],
+ remainingLocThreshold: 490000,
+ serverId: 'AU-TpxcA-iU5OvuD2FL0',
+ type: 'production',
+ ...override
+ };
+}