feat(frontend): russian and english interface with a language choice in settings, soon stubs removed

This commit is contained in:
hh committed 2026-10-19 01:54:34 +00:00
1 parent 3d2f670e87
commit 3460246749
13 files changed
+313 -132

No files matched your search

+80
View File
@@ -0,0 +1,80 @@
import { type MessageKey, messages } from "#lib/i18n/messages.js";
import type { Locale, Text } from "#lib/i18n/types.js";
import { browser } from "$app/env";
type Preference = Locale | "system";
type Params = Record<string, number | string>;
const STORAGE_KEY = "bg.locale";
const PARAM = /\{(\w+)\}/g;
const TAGS: Record<Locale, string> = { en: "en-GB", ru: "ru-RU" };
const plurals: Record<Locale, Intl.PluralRules> = {
en: new Intl.PluralRules(TAGS.en),
ru: new Intl.PluralRules(TAGS.ru),
};
function readPreference(): Preference {
if (!browser) {
return "system";
}
const stored = localStorage.getItem(STORAGE_KEY);
return stored === "en" || stored === "ru" ? stored : "system";
}
function systemLocale(): Locale {
if (!browser) {
return "ru";
}
for (const language of navigator.languages) {
const base = language.toLowerCase().slice(0, 2);
if (base === "ru" || base === "en") {
return base;
}
}
return "en";
}
function createLocale() {
let preference = $state<Preference>(readPreference());
const current = $derived<Locale>(
preference === "system" ? systemLocale() : preference
);
return {
get current() {
return current;
},
get preference() {
return preference;
},
set(value: Preference) {
preference = value;
if (browser) {
localStorage.setItem(STORAGE_KEY, value);
}
},
get tag() {
return TAGS[current];
},
};
}
export const locale = createLocale();
function pick(text: Text, count: number | undefined): string {
if (typeof text === "string") {
return text;
}
return text[plurals[locale.current].select(count ?? 0)] ?? text.other;
}
export function t(key: MessageKey, params?: Params): string {
const count = typeof params?.count === "number" ? params.count : undefined;
const text = pick(messages[key][locale.current], count);
if (!params) {
return text;
}
return text.replace(PARAM, (match, name: string) =>
name in params ? String(params[name]) : match
);
}
+9
View File
@@ -0,0 +1,9 @@
import { common } from "#lib/i18n/messages/common.js";
import { settings } from "#lib/i18n/messages/settings.js";
export const messages = {
...common,
...settings,
};
export type MessageKey = keyof typeof messages;
+9
View File
@@ -0,0 +1,9 @@
import type { Messages } from "#lib/i18n/types.js";
export const common = {
"common.back": { en: "Back", ru: "Назад" },
"common.cancel": { en: "Cancel", ru: "Отмена" },
"common.close": { en: "Close", ru: "Закрыть" },
"common.delete": { en: "Delete", ru: "Удалить" },
"common.save": { en: "Save", ru: "Сохранить" },
} satisfies Messages;
@@ -0,0 +1,25 @@
import type { Messages } from "#lib/i18n/types.js";
export const settings = {
"settings.about.license": { en: "GPL-3.0 license", ru: "Лицензия GPL-3.0" },
"settings.about.port": {
en: "UI ported from Telegram A",
ru: "UI портирован из Telegram A",
},
"settings.about.version": { en: "Version {version}", ru: "Версия {version}" },
"settings.alerts": { en: "Alerts", ru: "Оповещения" },
"settings.appearance": { en: "Appearance", ru: "Внешний вид" },
"settings.bots": { en: "Bots", ru: "Боты" },
"settings.jobs": { en: "Backfill and jobs", ru: "Бэкфилл и задачи" },
"settings.language": { en: "Language", ru: "Язык" },
"settings.language.system": { en: "System", ru: "Как в системе" },
"settings.policy": { en: "Capture policy", ru: "Политика захвата" },
"settings.shares": { en: "Files by link", ru: "Файлы по ссылке" },
"settings.storage": { en: "Storage", ru: "Хранилище" },
"settings.stories": { en: "Stories", ru: "Сторис" },
"settings.theme.dark": { en: "Dark", ru: "Тёмная" },
"settings.theme.light": { en: "Light", ru: "Светлая" },
"settings.theme.system": { en: "System", ru: "Системная" },
"settings.title": { en: "Settings", ru: "Настройки" },
"settings.watches": { en: "Watches", ru: "Наблюдения" },
} satisfies Messages;
+9
View File
@@ -0,0 +1,9 @@
export type Locale = "en" | "ru";
export type Plural = Partial<Record<Intl.LDMLPluralRule, string>> & {
other: string;
};
export type Text = string | Plural;
export type Messages = Record<string, Record<Locale, Text>>;