summaryrefslogtreecommitdiff
path: root/app/assets/javascripts/locale/index.js
blob: ad01da2eb1724f2dd162fe492bd2c5473490f973 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
import Jed from 'jed';
import ensureSingleLine from './ensure_single_line';
import sprintf from './sprintf';

const GITLAB_FALLBACK_LANGUAGE = 'en';

const languageCode = () =>
  document.querySelector('html').getAttribute('lang') || GITLAB_FALLBACK_LANGUAGE;
const locale = new Jed(window.translations || {});
delete window.translations;

/**
  Translates `text`
  @param text The text to be translated
  @returns {String} The translated text
*/
const gettext = (text) => locale.gettext(ensureSingleLine(text));

/**
  Translate the text with a number
  if the number is more than 1 it will use the `pluralText` translation.
  This method allows for contexts, see below re. contexts

  @param text Singular text to translate (eg. '%d day')
  @param pluralText Plural text to translate (eg. '%d days')
  @param count Number to decide which translation to use (eg. 2)
  @returns {String} Translated text with the number replaced (eg. '2 days')
*/
const ngettext = (text, pluralText, count) => {
  const translated = locale
    .ngettext(ensureSingleLine(text), ensureSingleLine(pluralText), count)
    .replace(/%d/g, count)
    .split('|');

  return translated[translated.length - 1];
};

/**
  Translate context based text
  Either pass in the context translation like `Context|Text to translate`
  or allow for dynamic text by doing passing in the context first & then the text to translate

  @param keyOrContext Can be either the key to translate including the context
                      (eg. 'Context|Text') or just the context for the translation
                      (eg. 'Context')
  @param key Is the dynamic variable you want to be translated
  @returns {String} Translated context based text
*/
const pgettext = (keyOrContext, key) => {
  const normalizedKey = ensureSingleLine(key ? `${keyOrContext}|${key}` : keyOrContext);
  const translated = gettext(normalizedKey).split('|');

  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.

  @param formatOptions for available options, please see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DateTimeFormat
  @returns {Intl.DateTimeFormat}
*/
const createDateTimeFormat = (formatOptions) =>
  Intl.DateTimeFormat(getPreferredLocales(), formatOptions);

/**
 * Formats a number as a string using `toLocaleString`.
 *
 * @param {Number} value - number to be converted
 * @param {options?} options - options to be passed to
 * `toLocaleString` such as `unit` and `style`.
 * @param {langCode?} langCode - If set, forces a different
 * language code from the one currently in the document.
 * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/NumberFormat
 *
 * @returns If value is a number, the formatted value as a string
 */
function formatNumber(value, options = {}, langCode = languageCode()) {
  if (typeof value !== 'number' && typeof value !== 'bigint') {
    return value;
  }
  return value.toLocaleString(langCode, options);
}

export { languageCode };
export { gettext as __ };
export { ngettext as n__ };
export { pgettext as s__ };
export { sprintf };
export { createDateTimeFormat };
export { formatNumber };
export default locale;