Commit ce46da1b authored by Lukas 'Eipi' Eipert's avatar Lukas 'Eipi' Eipert Committed by Mark Florian

Use configured browser locales for date formatting

This allows us to decide a little better what a user wants as a locale
for using with the Intl browser APIs. If they have set their GitLab to
a language, it will check whether `navigator.languages` contains
matching ones. This function always adds `en` as a fallback in order to
have date renders if all fails before it.

- Example one: GitLab language is `en` and browser languages are:
  `['en-GB', 'en-US']`. We would choose `['en-GB', 'en-US', 'en']` as
  the preferred locales, the Intl APIs would try to format first as
  British English, if that isn't available US or any English.
- Example two: GitLab language is `en` and browser languages are:
  `['de-DE', 'de']`. We would choose `['en']`, so the Intl APIs
  would prefer English formatting in order to not have German dates
  mixed with English GitLab UI texts.
  If the user wants for example British English formatting (24h, etc),
  they could set their browser languages to `['de-DE', 'de', 'en-GB']`.
- Example three: GitLab language is `de` and browser languages are \
  `['en-US', 'en']`. We would choose `['de', 'en']`, aligning German
  dates with the chosen translation of GitLab.

This function is currently only used for absolute dates:
- https://gitlab.com/gitlab-org/gitlab/-/merge_requests/65570

Changelog: changed
parent d028c6d2
...@@ -2,7 +2,10 @@ import Jed from 'jed'; ...@@ -2,7 +2,10 @@ import Jed from 'jed';
import ensureSingleLine from './ensure_single_line'; import ensureSingleLine from './ensure_single_line';
import sprintf from './sprintf'; import sprintf from './sprintf';
const languageCode = () => document.querySelector('html').getAttribute('lang') || 'en'; const GITLAB_FALLBACK_LANGUAGE = 'en';
const languageCode = () =>
document.querySelector('html').getAttribute('lang') || GITLAB_FALLBACK_LANGUAGE;
const locale = new Jed(window.translations || {}); const locale = new Jed(window.translations || {});
delete window.translations; delete window.translations;
...@@ -50,13 +53,53 @@ const pgettext = (keyOrContext, key) => { ...@@ -50,13 +53,53 @@ const pgettext = (keyOrContext, key) => {
return translated[translated.length - 1]; return translated[translated.length - 1];
}; };
/**
* Filters navigator languages by the set GitLab language.
*
* This allows us to decide better what a user wants as a locale, for using with the Intl browser APIs.
* If they have set their GitLab to a language, it will check whether `navigator.languages` contains matching ones.
* This function always adds `en` as a fallback in order to have date renders if all fails before it.
*
* - Example one: GitLab language is `en` and browser languages are:
* `['en-GB', 'en-US']`. This function returns `['en-GB', 'en-US', 'en']` as
* the preferred locales, the Intl APIs would try to format first as British English,
* if that isn't available US or any English.
* - Example two: GitLab language is `en` and browser languages are:
* `['de-DE', 'de']`. This function returns `['en']`, so the Intl APIs would prefer English
* formatting in order to not have German dates mixed with English GitLab UI texts.
* If the user wants for example British English formatting (24h, etc),
* they could set their browser languages to `['de-DE', 'de', 'en-GB']`.
* - Example three: GitLab language is `de` and browser languages are `['en-US', 'en']`.
* This function returns `['de', 'en']`, aligning German dates with the chosen translation of GitLab.
*
* @returns {string[]}
*/
export const getPreferredLocales = () => {
const gitlabLanguage = languageCode();
// The GitLab language may or may not contain a country code,
// so we create the short version as well, e.g. de-AT => de
const lang = gitlabLanguage.substring(0, 2);
const locales = navigator.languages.filter((l) => l.startsWith(lang));
if (!locales.includes(gitlabLanguage)) {
locales.push(gitlabLanguage);
}
if (!locales.includes(lang)) {
locales.push(lang);
}
if (!locales.includes(GITLAB_FALLBACK_LANGUAGE)) {
locales.push(GITLAB_FALLBACK_LANGUAGE);
}
return locales;
};
/** /**
Creates an instance of Intl.DateTimeFormat for the current locale. Creates an instance of Intl.DateTimeFormat for the current locale.
@param formatOptions for available options, please see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DateTimeFormat @param formatOptions for available options, please see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DateTimeFormat
@returns {Intl.DateTimeFormat} @returns {Intl.DateTimeFormat}
*/ */
const createDateTimeFormat = (formatOptions) => Intl.DateTimeFormat(languageCode(), formatOptions); const createDateTimeFormat = (formatOptions) =>
Intl.DateTimeFormat(getPreferredLocales(), formatOptions);
/** /**
* Formats a number as a string using `toLocaleString`. * Formats a number as a string using `toLocaleString`.
......
...@@ -176,7 +176,7 @@ You can select your preferred time format for the GitLab user interface: ...@@ -176,7 +176,7 @@ You can select your preferred time format for the GitLab user interface:
- Relative times, for example, `30 minutes ago`. - Relative times, for example, `30 minutes ago`.
- Absolute times, for example, `May 18, 2021, 3:57 PM`. - Absolute times, for example, `May 18, 2021, 3:57 PM`.
The times are formatted depending on your chosen language. The times are formatted depending on your chosen language and browser locale.
To set your time preference: To set your time preference:
......
import { setLanguage } from 'helpers/locale_helper'; import { setLanguage } from 'helpers/locale_helper';
import { createDateTimeFormat, formatNumber, languageCode } from '~/locale'; import { createDateTimeFormat, formatNumber, languageCode, getPreferredLocales } from '~/locale';
describe('locale', () => { describe('locale', () => {
afterEach(() => setLanguage(null)); afterEach(() => setLanguage(null));
...@@ -18,13 +18,91 @@ describe('locale', () => { ...@@ -18,13 +18,91 @@ describe('locale', () => {
}); });
}); });
describe('getPreferredLocales', () => {
beforeEach(() => {
// Need to spy on window.navigator.languages as it is read-only
jest
.spyOn(window.navigator, 'languages', 'get')
.mockReturnValueOnce(['en-GB', 'en-US', 'de-AT']);
});
it('filters navigator.languages by GitLab language', () => {
setLanguage('en');
expect(getPreferredLocales()).toEqual(['en-GB', 'en-US', 'en']);
});
it('filters navigator.languages by GitLab language without locale and sets English Fallback', () => {
setLanguage('de');
expect(getPreferredLocales()).toEqual(['de-AT', 'de', 'en']);
});
it('filters navigator.languages by GitLab language with locale and sets English Fallback', () => {
setLanguage('de-DE');
expect(getPreferredLocales()).toEqual(['de-AT', 'de-DE', 'de', 'en']);
});
it('adds GitLab language if navigator.languages does not contain it', () => {
setLanguage('es-ES');
expect(getPreferredLocales()).toEqual(['es-ES', 'es', 'en']);
});
});
describe('createDateTimeFormat', () => { describe('createDateTimeFormat', () => {
beforeEach(() => setLanguage('en')); const date = new Date(2015, 0, 3, 15, 13, 22);
const formatOptions = { dateStyle: 'long', timeStyle: 'medium' };
it('creates an instance of Intl.DateTimeFormat', () => { it('creates an instance of Intl.DateTimeFormat', () => {
const dateFormat = createDateTimeFormat({ year: 'numeric', month: 'long', day: 'numeric' }); const dateFormat = createDateTimeFormat(formatOptions);
expect(dateFormat).toBeInstanceOf(Intl.DateTimeFormat);
});
it('falls back to `en` and GitLab language is default', () => {
setLanguage(null);
jest.spyOn(window.navigator, 'languages', 'get').mockReturnValueOnce(['de-AT', 'en-GB']);
const dateFormat = createDateTimeFormat(formatOptions);
expect(dateFormat.format(date)).toBe(
new Intl.DateTimeFormat('en-GB', formatOptions).format(date),
);
});
it('falls back to `en` locale if browser languages are empty', () => {
setLanguage('en');
jest.spyOn(window.navigator, 'languages', 'get').mockReturnValueOnce([]);
const dateFormat = createDateTimeFormat(formatOptions);
expect(dateFormat.format(date)).toBe(
new Intl.DateTimeFormat('en', formatOptions).format(date),
);
});
it('prefers `en-GB` if it is the preferred language and GitLab language is `en`', () => {
setLanguage('en');
jest
.spyOn(window.navigator, 'languages', 'get')
.mockReturnValueOnce(['en-GB', 'en-US', 'en']);
const dateFormat = createDateTimeFormat(formatOptions);
expect(dateFormat.format(date)).toBe(
new Intl.DateTimeFormat('en-GB', formatOptions).format(date),
);
});
it('prefers `de-AT` if it is GitLab language and not part of the browser languages', () => {
setLanguage('de-AT');
jest
.spyOn(window.navigator, 'languages', 'get')
.mockReturnValueOnce(['en-GB', 'en-US', 'en']);
expect(dateFormat.format(new Date(2015, 6, 3))).toBe('July 3, 2015'); const dateFormat = createDateTimeFormat(formatOptions);
expect(dateFormat.format(date)).toBe(
new Intl.DateTimeFormat('de-AT', formatOptions).format(date),
);
}); });
}); });
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment