Keep school owners and phase 41 opinions/portraits together; ResetAsync wipes all saves so shared AppHost tests stay isolated.
758 lines
32 KiB
TypeScript
758 lines
32 KiB
TypeScript
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 1–40 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;
|
||
}
|
||
}
|