Read-only mirrors. Development, issues and pull requests are on GitHub.

summaryrefslogtreecommitdiffhomepage
path: root/frontend/src/locales.ts
blob: 4d99d09d09de013beb94b4b13e42e87694971e1d (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
// SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
// SPDX-License-Identifier: AGPL-3.0-or-later WITH AdditionRef-Progmasoft-Patent-Grant-1.1

export const supportedLocales = ["en-US", "de-DE", "ru-RU", "he-IL"] as const;
export type Locale = (typeof supportedLocales)[number];

/** The language of a visitor whose browser asks for none that is published. */
export const fallbackLocale: Locale = "en-US";

/** The short label of each language in the language menu. */
export const localeLabels: Record<Locale, string> = {
  "en-US": "EN",
  "de-DE": "DE",
  "ru-RU": "RU",
  "he-IL": "HE",
};

// Primary language subtags and the published locale each one selects. `iw` is
// the former code for Hebrew, which some browsers still report.
const localeByLanguage = new Map<string, Locale>([
  ["en", "en-US"],
  ["de", "de-DE"],
  ["ru", "ru-RU"],
  ["he", "he-IL"],
  ["iw", "he-IL"],
]);

export function isLocale(value: unknown): value is Locale {
  return supportedLocales.some((supported) => supported === value);
}

/**
 * The first published locale among the languages a browser prefers.
 *
 * The argument is the list a browser reports, most preferred first. Only the
 * primary language of each entry is compared, so `de-AT` selects German.
 */
export function browserLocale(languages: readonly string[]): Locale {
  for (const language of languages) {
    const primary = String(language).split("-", 1)[0] ?? "";
    const locale = localeByLanguage.get(primary.toLowerCase());
    if (locale) return locale;
  }
  return fallbackLocale;
}

/**
 * The language a page starts in: the language the visitor chose, and without
 * a choice the language of the browser.
 */
export function initialLocale(chosen: string | null, languages: readonly string[]): Locale {
  return isLocale(chosen) ? chosen : browserLocale(languages);
}

/**
 * Whether a language change may be remembered as the visitor's choice.
 *
 * A choice needs a visitor: a change that reaches the page before anyone has
 * clicked, tapped or typed in it did not come from the language menu, so it
 * changes the page but is not stored. The argument is the browser's
 * `navigator.userActivation`; a browser without it cannot tell, and there a
 * change counts as a choice.
 */
export function isVisitorChoice(activation: { hasBeenActive: boolean } | undefined): boolean {
  return activation?.hasBeenActive ?? true;
}

/**
 * The direction in which the text of a language runs.
 *
 * Only text follows it. The page layout, the navigation and code samples keep
 * their left-to-right arrangement in every language.
 */
export function textDirection(locale: Locale): "ltr" | "rtl" {
  return locale === "he-IL" ? "rtl" : "ltr";
}

/** The arrow that points onward in the reading direction of a language. */
export function forwardArrow(locale: Locale): string {
  return textDirection(locale) === "rtl" ? "←" : "→";
}