Files
h-school/src/HSchool.Client/src/i18n/strings.ts
T
Leonid Pershin 4b236a7916 Merge main into phase/39-school-owners.
Keep school owners and phase 41 opinions/portraits together; ResetAsync wipes all saves so shared AppHost tests stay isolated.
2026-08-20 08:36:13 +03:00

758 lines
32 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { getLocale, intlTag, type Locale } from './locale.ts';
const ru = {
statusConnecting: 'подключение…',
statusConnected: 'сервер на связи',
statusReconnecting: 'переподключение…',
statusClosed: 'соединение закрыто',
pingMs: '{ms} мс',
pingPlaceholder: '-- мс',
language: 'Язык',
sessionTitle: 'Вход',
sessionPassword: 'Пароль альфы',
sessionPasswordPlaceholder: 'Пароль',
sessionUserName: 'Ваше имя',
sessionUserNamePlaceholder: 'Имя в сети',
sessionContinue: 'Продолжить',
sessionEnter: 'Войти',
sessionBadPassword: 'Неверный пароль.',
sessionNameOnline: 'Это имя уже в сети.',
sessionInvalidName: 'Имя должно быть от 1 до 40 символов.',
sessionFailed: 'Не удалось войти. Попробуйте ещё раз.',
sessionLogout: 'Выйти',
schoolsTitle: 'Школы',
schoolsMine: 'Мои',
schoolsOthers: 'Чужие',
schoolOwner: 'Хозяин: {owner}',
schoolOwnerless: '—',
createSchool: 'Создать школу',
settings: 'Настройки',
save: 'Сохранить',
settingsTitle: 'SwarmUI — пресеты портретов',
settingsActivePreset: 'Пресет по умолчанию',
settingsEditPreset: 'Редактируемый пресет',
settingsAddPreset: 'Добавить пресет',
settingsPresetLabel: 'Название пресета',
settingsModel: 'Модель',
settingsSteps: 'Steps',
settingsCfg: 'CFG scale',
settingsClipSkip: 'CLIP skip',
settingsSampler: 'Sampler',
settingsScheduler: 'Scheduler',
settingsSeed: 'Seed',
settingsPositive: 'Базовый positive',
settingsNegative: 'Negative',
settingsPositiveLoras: 'Positive LoRA',
settingsNegativeLoras: 'Negative LoRA',
settingsAddLora: 'Добавить LoRA',
settingsAvatarPreset: 'Аватар',
settingsCustomPreset: 'Свой промпт (размеры)',
settingsFullBodyPreset: 'В полный рост',
settingsWidth: 'Ширина',
settingsHeight: 'Высота',
settingsKindPositive: 'Positive для вида',
settingsAgeRules: 'Правила по возрасту',
settingsAgeMin: 'От',
settingsAgeMax: 'До',
settingsAgePreset: 'Пресет',
settingsAddAgeRule: 'Добавить правило',
settingsRemoveRule: 'Удалить',
settingsRefreshLists: 'Обновить списки из SwarmUI',
settingsSwarmReady: 'SwarmUI подключён — списки моделей и семплеров доступны.',
settingsSwarmOffline: 'SwarmUI недоступен — поля можно заполнить вручную.',
settingsSwarmNotConfigured: 'SwarmUI не настроен на сервере (SwarmUi:BaseUrl).',
settingsLoadFailed: 'Не удалось загрузить настройки.',
settingsSaveFailed: 'Не удалось сохранить настройки.',
settingsDiscoveryFailed: 'Не удалось получить списки из SwarmUI.',
emptySchools: 'Пока ни одной школы. Создайте первую.',
loadSchoolsFailed: 'Не удалось загрузить список школ. Проверьте соединение с сервером.',
createDisabledTitle: 'Удалите одну из школ, чтобы создать новую',
schoolCount: 'Школ: {current} из {max}.',
schoolLimitReached: 'Достигнут лимит: {max} {schoolWord}. Удалите одну, чтобы создать новую.',
deleteSchool: 'Удалить',
deleteSchoolTitle: 'Удалить школу?',
deleteSchoolMessage: '«{name}» будет удалена без возможности восстановления.',
deleteFailed: 'Не удалось удалить «{name}».',
paused: '⏸ на паузе',
newSchool: 'Новая школа',
schoolName: 'Название',
schoolNamePlaceholder: 'Название школы',
randomName: 'Случайное',
randomNameTitle: 'Придумать название',
gameStart: 'Начало игры',
create: 'Создать',
cancel: 'Отмена',
confirm: 'Подтвердить',
randomNameFailed: 'Не удалось получить название с сервера.',
startDateRequired: 'Укажите дату и время начала.',
serverUnavailable: 'Сервер недоступен. Попробуйте ещё раз.',
errorSchoolLimit: 'Достигнут лимит школ — удалите одну, чтобы создать новую.',
errorInvalidName: 'Название должно быть от 1 до 40 символов.',
errorInvalidStartDate: 'Дата начала вне допустимого диапазона.',
errorInvalidMap: 'Карта должна быть связным графом: двор и хотя бы одна комната.',
errorUnknownMod: 'Выбранный мод не найден.',
errorMissingMod: 'Не выбран обязательный мод «{id}».',
errorModCycle: 'Выбранные моды зависят друг от друга по кругу.',
errorInvalidCatalog: 'Не удалось загрузить выбранные моды.',
errorUnknownCountry: 'Выбранная страна не найдена.',
errorUnknownNativeLanguage: 'Выбранный родной язык не входит в эту страну.',
errorInvalidPortraitSettings: 'Пресеты портретов этой школы заданы неверно.',
catalogLoadFailed: 'Не удалось загрузить каталог модов.',
modsTitle: 'Моды',
countryTitle: 'Страна',
nativeLanguageTitle: 'Родной язык',
nativeLanguageRandom: 'Случайный',
randomNativeTitle: 'Выбрать язык наугад',
coreModLocked: '{id} (всегда включён)',
mapEditorTitle: 'Карта',
editMap: 'Редактировать карту',
mapDefaultHint: 'Будет использована карта по умолчанию.',
mapEditedHint: 'Карта изменена.',
portraitPresets: 'Пресеты портретов',
editPortraitPresets: 'Настроить модели',
portraitPresetsHint: 'Модель и промпты сохранятся в этой школе.',
portraitPresetsEdited: 'Заданы для этой школы.',
schoolSeed: 'Сид {seed}',
schoolSeedLabel: 'Сид',
schoolSeedHint: 'Необязательно. Чужой сид даёт ту же школу; пустое поле бросает свой.',
schoolSeedInvalid: 'Сид должен быть целым числом.',
resetMap: 'Сбросить к умолчанию',
done: 'Готово',
editorStructure: 'Структура',
editorSlots: 'Обстановка',
editorPickRoom: 'Выберите комнату в дереве слева.',
editorRooms: 'Комнаты',
editorNoLinks: 'Проходов нет.',
editorLinks: 'Проходы',
addLink: 'Связать',
removeLink: 'Убрать',
addRoom: 'Добавить комнату',
removeRoom: 'Удалить комнату',
slotEmpty: '— пусто —',
slotCount: 'Количество',
editorSeats: 'Мест',
backToMenu: '← В главное меню',
pause: 'Пауза',
resume: 'Продолжить',
mapTitle: 'Карта',
locationName: 'Локация',
personTitle: 'Человек',
locationItems: 'Предметы',
locationCharacters: 'Персонажи',
locationActivities: 'Сейчас',
locationPositions: 'Должности',
itemsEmpty: 'Пусто.',
pupilSlots: 'Ученических мест: {count}',
charactersEmpty: 'Никого нет.',
activitiesEmpty: 'Ничего не происходит.',
positionsEmpty: 'Нет должностей.',
peopleTitle: 'Люди',
peopleRole: 'Роль',
peopleRoleAll: 'Все',
peopleRoleStudent: 'Ученик',
peopleRoleStaff: 'Работник',
peopleRoleParent: 'Родитель',
peopleYear: 'Параллель',
peopleYearAll: 'Все',
peopleLetter: 'Литера',
peopleLetterAll: 'Все',
peoplePosition: 'Должность',
peoplePositionAll: 'Все',
peopleSex: 'Пол',
peopleSexAll: 'Все',
peopleMale: 'муж.',
peopleFemale: 'жен.',
peopleAgeFrom: 'Возраст от',
peopleAgeTo: 'до',
peopleColName: 'ФИО',
peopleColRole: 'Роль',
peopleColPlace: 'Класс / должность',
peopleColAge: 'Возраст',
peopleColSex: 'Пол',
peopleEmpty: 'Никого нет.',
peopleLoadFailed: 'Не удалось загрузить список людей.',
peopleCardFailed: 'Не удалось открыть карточку.',
peoplePickHint: 'Выберите человека в списке.',
peoplePager: 'Стр. {page} из {pages} · {total}',
peoplePrev: 'Назад',
peopleNext: 'Вперёд',
peopleBody: 'Тело',
peopleSkills: 'Навыки',
peopleTraits: 'Черты',
peopleNeeds: 'Нужды',
peopleApparel: 'Одежда',
peopleCarry: 'Ноша',
peopleCarryMass: '{held} / {cap} кг',
peopleFamily: 'Семья',
peopleParents: 'Родители',
peopleChildren: 'Дети',
peopleSiblings: 'Братья и сёстры',
peoplePartners: 'Супруг(а)',
peopleTabOverview: 'Обзор',
peopleTabApparel: 'Одежда',
peopleTabCarry: 'Ноша',
peopleTabNow: 'Сейчас',
peopleTabConnections: 'Связи',
peopleTabPortrait: 'Портрет',
peopleConnectionsFriends: 'Друзья',
peopleConnectionsEnemies: 'Враги',
peopleConnectionsSearch: 'Поиск по связям',
peopleConnectionsEmpty: 'Нет других связей.',
peopleOpinionValue: '{label} ({value})',
peopleAtHome: 'дома',
peopleApparelFit: 'Уместность: {value}',
peopleLockerYes: 'Есть шкафчик',
peopleLockerNone: 'Шкафчика нет',
peopleHomeCount: 'ещё {n} дома',
peopleLogSearch: 'Поиск',
peopleLogNewest: 'Сначала новые',
peopleLogOldest: 'Сначала старые',
peopleLogTime: 'Время',
peopleLogEvent: 'Событие',
peopleLogEmpty: 'Сегодня записей нет.',
peoplePortraitAvatar: 'Аватар',
peoplePortraitFull: 'В полный рост',
peoplePortraitCustom: 'Свой промпт',
peoplePortraitCustomHint: 'Дополнение к базовому промпту',
peoplePortraitCustomPlaceholder: 'Например: standing in a school hallway, soft window light',
peoplePortraitGenerateAvatar: 'Сгенерировать аватар',
peoplePortraitGenerateFull: 'Сгенерировать в полный рост',
peoplePortraitGenerateCustom: 'Сгенерировать',
peoplePortraitRegenerateCustom: 'Перегенерировать',
peoplePortraitGenerating: 'Генерация…',
peoplePortraitMissing: 'Ещё не сгенерировано.',
peoplePortraitUnavailable: 'SwarmUI не настроен на сервере.',
peoplePortraitSwarmDisabled: 'SwarmUI: не настроен',
peoplePortraitSwarmChecking: 'SwarmUI: проверка…',
peoplePortraitSwarmConnected: 'SwarmUI: подключён',
peoplePortraitSwarmDisconnected: 'SwarmUI: нет связи',
peoplePortraitSwarmOffline: 'SwarmUI недоступен — проверьте, что сервис запущен.',
peoplePortraitFailed: 'Не удалось сгенерировать портрет.',
peoplePortraitShowPrompt: 'Показать итоговый промпт',
peoplePortraitHidePrompt: 'Скрыть промпт',
peoplePortraitPromptPositive: 'Positive',
peoplePortraitPromptNegative: 'Negative',
peoplePortraitPromptLoading: 'Сборка промпта…',
peoplePortraitPromptFailed: 'Не удалось получить промпт.',
peoplePortraitPromptPreset: 'Пресет: {label}',
modeOverview: 'Обзор',
modeManage: 'Управление',
staffAllocated: 'Выделено',
staffPayroll: 'Расписано',
staffRemaining: 'Свободно',
staffUncovered: 'Не покрыто',
staffUncoveredEmpty: 'Все предметы существующих параллелей кем-то ведутся.',
staffApplicants: 'Соискатели',
staffApplicantsEmpty: 'Пул пуст.',
staffApplicantsOpen: 'Открыть список',
staffApplicantsCount: 'В пуле: {n}',
staffApplicantsSearch: 'Имя',
staffApplicantsSearchHint: 'Поиск по имени',
staffApplicantsKind: 'Кто',
staffApplicantsKindAll: 'Все',
staffApplicantsKindParent: 'Родители',
staffApplicantsKindOther: 'Со стороны',
staffApplicantsNoMatches: 'Никто не подходит под фильтр.',
staffApplicantsPickHint: 'Выберите соискателя в списке.',
staffHired: 'Штат',
staffHiredEmpty: 'Никого не наняли.',
staffColName: 'ФИО',
staffColAsk: 'Ставка',
staffColSkills: 'Навыки',
staffColPosition: 'Должность',
staffColSubjects: 'Предметы',
staffColHours: 'Часов',
staffHours: '{n} ч/нед',
staffColPay: 'В месяц',
staffAsk: '{hourly}/ч · {monthly}/мес',
staffSubjectRange: '{label} ({min}{max})',
staffUncoveredShort: '{label} ({min}{max}) — не хватает {n}',
staffParent: 'родитель',
staffPickHint: 'Выберите сотрудника в списке или откройте соискателей.',
staffHireTitle: 'Наём',
staffHire: 'Нанять',
staffPosition: 'Должность',
staffSubjectsTitle: 'Предметы',
staffNoSubjects: 'Предметы ещё не назначены.',
staffSubject: 'Предмет',
staffAssign: 'Назначить',
staffUnassign: 'Снять',
staffLoadFailed: 'Не удалось загрузить управление штатом.',
staffActionFailed: 'Не удалось выполнить действие.',
staffErrorPayroll: 'Не хватает денег: расписано {payroll} из {allocated}, свободно {remaining}, нужно {attempted}.',
staffErrorHired: 'Этот человек уже в штате.',
staffErrorNoOpening: 'Нет свободного места на эту должность.',
staffErrorUnknownApplicant: 'Этого человека нет в пуле соискателей.',
staffErrorPosition: 'Такой должности нет.',
staffErrorNotTeacher: 'Предмет можно назначить только учителю.',
staffErrorAssigned: 'Этот предмет уже назначен.',
staffErrorSubject: 'Такого предмета нет.',
manageTabStaff: 'Штат',
manageTabRules: 'Правила',
rulesCurrentTitle: 'Сегодня',
rulesPendingTitle: 'С завтра',
rulesPendingHint: 'Изменения вступают в силу на следующее рабочее утро.',
rulesStudentsRole: 'Ученики',
rulesStaffRole: 'Сотрудники',
rulesStudentsForm: 'Форма учеников',
rulesStudentsColor: 'Цвет учеников',
rulesStaffForm: 'Форма сотрудников',
rulesStaffColor: 'Цвет сотрудников',
rulesRoleSummary: '{role}: {form}, {color}',
rulesApply: 'Применить',
rulesLoadFailed: 'Не удалось загрузить правила одежды.',
rulesSaveFailed: 'Не удалось сохранить правила.',
rulesPickHint: 'Правила действуют на всю школу.',
dressFormRegular: 'обычная',
dressFormShort: 'короткая',
dressFormStrict: 'строгая',
dressColorFree: 'свободно',
dressColorNoBright: 'без яркого',
dressColorWhiteTopBlackBottom: 'белый верх, чёрный низ',
dressErrorUnknownForm: 'Такой формы нет.',
dressErrorUnknownColor: 'Такой цветовой политики нет.',
mapOccupancy: '{name} ({activity})',
mapHeadcount: '{name} ({count})',
mapHeadcountActivity: '{name} ({count} · {activity})',
skipTo: 'Пропустить до {date}',
weatherClear: '{temp} °C',
weatherPrecip: '{temp} °C, {precip}',
precipRain: 'дождь',
precipSnow: 'снег',
presenceAt: '{name}',
presenceWalking: 'в пути ({name})',
presenceAway: 'вне школы',
locationWalking: '{name} (в пути)',
timetableTitle: 'Расписание',
timetableClass: 'Класс',
timetableEmpty: 'Нет уроков.',
timetableLoadFailed: 'Не удалось загрузить расписание.',
timetablePeriod: '{n}',
timetableLocked: 'закреплено',
timetableDay: 'День',
timetablePeriodLabel: 'Урок',
timetableRoom: 'Кабинет',
timetableApply: 'Закрепить здесь',
timetableUnpin: 'Снять закрепление',
timetablePickLesson: 'Выберите урок в сетке, чтобы перенести или сменить кабинет.',
timetableUncovered: '{label} — {hours} ч.',
timetableUncoveredTitle: 'Не покрыто',
timetableErrorPin: 'Сюда поставить нельзя: слот или кабинет заняты, либо класс туда не влезает.',
timetableErrorNoTeacher: 'Некому вести этот предмет.',
timetableErrorUnknown: 'Не удалось изменить урок.',
timetableErrorLesson: 'Этого закрепления уже нет.',
} as const;
type Messages = { [K in keyof typeof ru]: string };
const en: Messages = {
statusConnecting: 'connecting…',
statusConnected: 'connected',
statusReconnecting: 'reconnecting…',
statusClosed: 'connection closed',
pingMs: '{ms} ms',
pingPlaceholder: '-- ms',
language: 'Language',
sessionTitle: 'Sign in',
sessionPassword: 'Alpha password',
sessionPasswordPlaceholder: 'Password',
sessionUserName: 'Your name',
sessionUserNamePlaceholder: 'Display name',
sessionContinue: 'Continue',
sessionEnter: 'Enter',
sessionBadPassword: 'Wrong password.',
sessionNameOnline: 'That name is already online.',
sessionInvalidName: 'The name must be 140 characters.',
sessionFailed: 'Could not sign in. Try again.',
sessionLogout: 'Sign out',
schoolsTitle: 'Schools',
schoolsMine: 'Mine',
schoolsOthers: 'Others',
schoolOwner: 'Owner: {owner}',
schoolOwnerless: '—',
createSchool: 'Create school',
settings: 'Settings',
save: 'Save',
settingsTitle: 'SwarmUI portrait presets',
settingsActivePreset: 'Default preset',
settingsEditPreset: 'Preset to edit',
settingsAddPreset: 'Add preset',
settingsPresetLabel: 'Preset name',
settingsModel: 'Model',
settingsSteps: 'Steps',
settingsCfg: 'CFG scale',
settingsClipSkip: 'CLIP skip',
settingsSampler: 'Sampler',
settingsScheduler: 'Scheduler',
settingsSeed: 'Seed',
settingsPositive: 'Base positive',
settingsNegative: 'Negative',
settingsPositiveLoras: 'Positive LoRA',
settingsNegativeLoras: 'Negative LoRA',
settingsAddLora: 'Add LoRA',
settingsAvatarPreset: 'Avatar',
settingsCustomPreset: 'Custom prompt (size)',
settingsFullBodyPreset: 'Full body',
settingsWidth: 'Width',
settingsHeight: 'Height',
settingsKindPositive: 'Kind positive',
settingsAgeRules: 'Age rules',
settingsAgeMin: 'From',
settingsAgeMax: 'To',
settingsAgePreset: 'Preset',
settingsAddAgeRule: 'Add rule',
settingsRemoveRule: 'Remove',
settingsRefreshLists: 'Refresh lists from SwarmUI',
settingsSwarmReady: 'SwarmUI connected — model and sampler lists are available.',
settingsSwarmOffline: 'SwarmUI unreachable — you can still edit fields manually.',
settingsSwarmNotConfigured: 'SwarmUI is not configured on the server (SwarmUi:BaseUrl).',
settingsLoadFailed: 'Could not load settings.',
settingsSaveFailed: 'Could not save settings.',
settingsDiscoveryFailed: 'Could not fetch lists from SwarmUI.',
emptySchools: 'No schools yet. Create the first one.',
loadSchoolsFailed: 'Could not load the school list. Check the connection to the server.',
createDisabledTitle: 'Delete a school to create a new one',
schoolCount: 'Schools: {current} of {max}.',
schoolLimitReached: 'Limit reached: {max} {schoolWord}. Delete one to create another.',
deleteSchool: 'Delete',
deleteSchoolTitle: 'Delete this school?',
deleteSchoolMessage: '"{name}" will be deleted permanently.',
deleteFailed: 'Could not delete "{name}".',
paused: '⏸ paused',
newSchool: 'New school',
schoolName: 'Name',
schoolNamePlaceholder: 'School name',
randomName: 'Random',
randomNameTitle: 'Suggest a name',
gameStart: 'Game start',
create: 'Create',
cancel: 'Cancel',
confirm: 'Confirm',
randomNameFailed: 'Could not fetch a name from the server.',
startDateRequired: 'Enter a start date and time.',
serverUnavailable: 'The server is unavailable. Try again.',
errorSchoolLimit: 'School limit reached — delete one to create another.',
errorInvalidName: 'The name must be 1 to 40 characters.',
errorInvalidStartDate: 'The start date is outside the allowed range.',
errorInvalidMap: 'The map must be a connected graph: a yard and at least one room.',
errorUnknownMod: 'A selected mod is missing.',
errorMissingMod: 'Required mod “{id}” is not selected.',
errorModCycle: 'The selected mods depend on each other in a cycle.',
errorInvalidCatalog: 'The selected packs could not be loaded.',
errorUnknownCountry: 'The selected country is not in the catalog.',
errorUnknownNativeLanguage: 'The selected native language is not in that country.',
errorInvalidPortraitSettings: 'This school\'s portrait presets are not valid.',
catalogLoadFailed: 'Could not load the mod catalog.',
modsTitle: 'Mods',
countryTitle: 'Country',
nativeLanguageTitle: 'Native language',
nativeLanguageRandom: 'Random',
randomNativeTitle: 'Pick a language at random',
coreModLocked: '{id} (always on)',
mapEditorTitle: 'Map',
editMap: 'Edit map',
mapDefaultHint: 'The default map will be used.',
mapEditedHint: 'The map has been edited.',
portraitPresets: 'Portrait presets',
editPortraitPresets: 'Configure models',
portraitPresetsHint: 'The model and prompts will be stored with this school.',
portraitPresetsEdited: 'Set for this school.',
schoolSeed: 'Seed {seed}',
schoolSeedLabel: 'Seed',
schoolSeedHint: 'Optional. A shared seed recreates the same people; leave blank to roll one.',
schoolSeedInvalid: 'The seed must be a whole number.',
resetMap: 'Reset to default',
done: 'Done',
editorStructure: 'Structure',
editorSlots: 'Furnishing',
editorPickRoom: 'Pick a room in the tree on the left.',
editorRooms: 'Rooms',
editorNoLinks: 'No passages yet.',
editorLinks: 'Passages',
addLink: 'Link',
removeLink: 'Remove',
addRoom: 'Add room',
removeRoom: 'Remove room',
slotEmpty: '— empty —',
slotCount: 'Count',
editorSeats: 'Seats',
backToMenu: '← Main menu',
pause: 'Pause',
resume: 'Resume',
mapTitle: 'Map',
locationName: 'Location',
personTitle: 'Person',
locationItems: 'Items',
locationCharacters: 'People',
locationActivities: 'Now',
locationPositions: 'Positions',
itemsEmpty: 'Empty.',
pupilSlots: 'Pupil places: {count}',
charactersEmpty: 'Nobody here.',
activitiesEmpty: 'Nothing is happening.',
positionsEmpty: 'No positions.',
peopleTitle: 'People',
peopleRole: 'Role',
peopleRoleAll: 'All',
peopleRoleStudent: 'Student',
peopleRoleStaff: 'Staff',
peopleRoleParent: 'Parent',
peopleYear: 'Year',
peopleYearAll: 'All',
peopleLetter: 'Letter',
peopleLetterAll: 'All',
peoplePosition: 'Position',
peoplePositionAll: 'All',
peopleSex: 'Sex',
peopleSexAll: 'All',
peopleMale: 'male',
peopleFemale: 'female',
peopleAgeFrom: 'Age from',
peopleAgeTo: 'to',
peopleColName: 'Name',
peopleColRole: 'Role',
peopleColPlace: 'Class / position',
peopleColAge: 'Age',
peopleColSex: 'Sex',
peopleEmpty: 'Nobody here.',
peopleLoadFailed: 'Could not load the people list.',
peopleCardFailed: 'Could not open the card.',
peoplePickHint: 'Select a person in the list.',
peoplePager: 'Page {page} of {pages} · {total}',
peoplePrev: 'Previous',
peopleNext: 'Next',
peopleBody: 'Body',
peopleSkills: 'Skills',
peopleTraits: 'Traits',
peopleNeeds: 'Needs',
peopleApparel: 'Clothes',
peopleCarry: 'Carried',
peopleCarryMass: '{held} / {cap} kg',
peopleFamily: 'Family',
peopleParents: 'Parents',
peopleChildren: 'Children',
peopleSiblings: 'Siblings',
peoplePartners: 'Spouse',
peopleTabOverview: 'Overview',
peopleTabApparel: 'Clothes',
peopleTabCarry: 'Carried',
peopleTabNow: 'Now',
peopleTabConnections: 'Connections',
peopleTabPortrait: 'Portrait',
peopleConnectionsFriends: 'Friends',
peopleConnectionsEnemies: 'Enemies',
peopleConnectionsSearch: 'Search connections',
peopleConnectionsEmpty: 'No other connections.',
peopleOpinionValue: '{label} ({value})',
peopleAtHome: 'home',
peopleApparelFit: 'Fit: {value}',
peopleLockerYes: 'Has a locker',
peopleLockerNone: 'No locker',
peopleHomeCount: '{n} more at home',
peopleLogSearch: 'Search',
peopleLogNewest: 'Newest first',
peopleLogOldest: 'Oldest first',
peopleLogTime: 'Time',
peopleLogEvent: 'Event',
peopleLogEmpty: 'Nothing logged today.',
peoplePortraitAvatar: 'Avatar',
peoplePortraitFull: 'Full body',
peoplePortraitCustom: 'Custom prompt',
peoplePortraitCustomHint: 'Added to the base prompt',
peoplePortraitCustomPlaceholder: 'For example: standing in a school hallway, soft window light',
peoplePortraitGenerateAvatar: 'Generate avatar',
peoplePortraitGenerateFull: 'Generate full body',
peoplePortraitGenerateCustom: 'Generate',
peoplePortraitRegenerateCustom: 'Regenerate',
peoplePortraitGenerating: 'Generating…',
peoplePortraitMissing: 'Not generated yet.',
peoplePortraitUnavailable: 'SwarmUI is not configured on the server.',
peoplePortraitSwarmDisabled: 'SwarmUI: not configured',
peoplePortraitSwarmChecking: 'SwarmUI: checking…',
peoplePortraitSwarmConnected: 'SwarmUI: connected',
peoplePortraitSwarmDisconnected: 'SwarmUI: unreachable',
peoplePortraitSwarmOffline: 'SwarmUI is unreachable — check that the service is running.',
peoplePortraitFailed: 'Could not generate the portrait.',
peoplePortraitShowPrompt: 'Show final prompt',
peoplePortraitHidePrompt: 'Hide prompt',
peoplePortraitPromptPositive: 'Positive',
peoplePortraitPromptNegative: 'Negative',
peoplePortraitPromptLoading: 'Building prompt…',
peoplePortraitPromptFailed: 'Could not load the prompt.',
peoplePortraitPromptPreset: 'Preset: {label}',
modeOverview: 'Overview',
modeManage: 'Management',
staffAllocated: 'Allocated',
staffPayroll: 'Committed',
staffRemaining: 'Remaining',
staffUncovered: 'Uncovered',
staffUncoveredEmpty: 'Every subject in the existing year-groups has a teacher.',
staffApplicants: 'Applicants',
staffApplicantsEmpty: 'The pool is empty.',
staffApplicantsOpen: 'Open list',
staffApplicantsCount: '{n} in the pool',
staffApplicantsSearch: 'Name',
staffApplicantsSearchHint: 'Search by name',
staffApplicantsKind: 'Who',
staffApplicantsKindAll: 'All',
staffApplicantsKindParent: 'Parents',
staffApplicantsKindOther: 'Outside',
staffApplicantsNoMatches: 'Nobody matches the filter.',
staffApplicantsPickHint: 'Select an applicant in the list.',
staffHired: 'Staff',
staffHiredEmpty: 'Nobody hired yet.',
staffColName: 'Name',
staffColAsk: 'Ask',
staffColSkills: 'Skills',
staffColPosition: 'Position',
staffColSubjects: 'Subjects',
staffColHours: 'Hours',
staffHours: '{n} h/wk',
staffColPay: 'Monthly',
staffAsk: '{hourly}/h · {monthly}/mo',
staffSubjectRange: '{label} ({min}{max})',
staffUncoveredShort: '{label} ({min}{max}) — {n} short',
staffParent: 'parent',
staffPickHint: 'Select a staff member in the list, or open the applicants.',
staffHireTitle: 'Hire',
staffHire: 'Hire',
staffPosition: 'Position',
staffSubjectsTitle: 'Subjects',
staffNoSubjects: 'No subjects assigned yet.',
staffSubject: 'Subject',
staffAssign: 'Assign',
staffUnassign: 'Remove',
staffLoadFailed: 'Could not load staffing.',
staffActionFailed: 'That action failed.',
staffErrorPayroll: 'Not enough money: {payroll} of {allocated} is committed, {remaining} free, {attempted} needed.',
staffErrorHired: 'That person is already on staff.',
staffErrorNoOpening: 'There is no free opening for that position.',
staffErrorUnknownApplicant: 'That person is not in the applicant pool.',
staffErrorPosition: 'That position is not in the catalog.',
staffErrorNotTeacher: 'Only a teacher can be assigned a subject.',
staffErrorAssigned: 'That subject is already assigned.',
staffErrorSubject: 'That subject is not in the catalog.',
manageTabStaff: 'Staff',
manageTabRules: 'Rules',
rulesCurrentTitle: 'Today',
rulesPendingTitle: 'From tomorrow',
rulesPendingHint: 'Changes take effect on the next work morning.',
rulesStudentsRole: 'Students',
rulesStaffRole: 'Staff',
rulesStudentsForm: 'Student form',
rulesStudentsColor: 'Student colours',
rulesStaffForm: 'Staff form',
rulesStaffColor: 'Staff colours',
rulesRoleSummary: '{role}: {form}, {color}',
rulesApply: 'Apply',
rulesLoadFailed: 'Could not load dress rules.',
rulesSaveFailed: 'Could not save the rules.',
rulesPickHint: 'Rules apply to the whole school.',
dressFormRegular: 'regular',
dressFormShort: 'short',
dressFormStrict: 'strict',
dressColorFree: 'free colours',
dressColorNoBright: 'no bright colours',
dressColorWhiteTopBlackBottom: 'white top, black bottom',
dressErrorUnknownForm: 'That form policy is not in the catalog.',
dressErrorUnknownColor: 'That colour policy is not in the catalog.',
mapOccupancy: '{name} ({activity})',
mapHeadcount: '{name} ({count})',
mapHeadcountActivity: '{name} ({count} · {activity})',
skipTo: 'Skip to {date}',
weatherClear: '{temp} °C',
weatherPrecip: '{temp} °C, {precip}',
precipRain: 'rain',
precipSnow: 'snow',
presenceAt: '{name}',
presenceWalking: 'walking ({name})',
presenceAway: 'off campus',
locationWalking: '{name} (walking)',
timetableTitle: 'Timetable',
timetableClass: 'Class',
timetableEmpty: 'No lessons.',
timetableLoadFailed: 'Could not load the timetable.',
timetablePeriod: '{n}',
timetableLocked: 'pinned',
timetableDay: 'Day',
timetablePeriodLabel: 'Period',
timetableRoom: 'Room',
timetableApply: 'Pin here',
timetableUnpin: 'Unpin',
timetablePickLesson: 'Pick a lesson in the grid to move it or change the room.',
timetableUncovered: '{label} — {hours} h.',
timetableUncoveredTitle: 'Uncovered',
timetableErrorPin: 'Cannot place that lesson: the slot or room is taken, or the class does not fit.',
timetableErrorNoTeacher: 'Nobody is assigned that subject.',
timetableErrorUnknown: 'Could not change the lesson.',
timetableErrorLesson: 'That pinned lesson is gone.',
};
const catalogs: Record<Locale, Messages> = { ru, en };
export type MessageKey = keyof Messages;
export function t(key: MessageKey, vars?: Record<string, string | number>): string {
let text: string = catalogs[getLocale()][key];
if (vars !== undefined) {
for (const [name, value] of Object.entries(vars)) {
text = text.replaceAll(`{${name}}`, String(value));
}
}
return text;
}
interface PluralForms {
readonly one: string;
readonly few?: string;
readonly many?: string;
readonly other: string;
}
const schoolWordForms: Record<Locale, PluralForms> = {
ru: { one: 'школа', few: 'школы', many: 'школ', other: 'школ' },
en: { one: 'school', other: 'schools' },
};
export function schoolWord(count: number): string {
const forms = schoolWordForms[getLocale()];
const rule = new Intl.PluralRules(intlTag()).select(count);
switch (rule) {
case 'one':
return forms.one;
case 'few':
return forms.few ?? forms.other;
case 'many':
return forms.many ?? forms.other;
default:
return forms.other;
}
}