refactor(backend): localize validation errors and consolidate schema

Replace hardcoded validation error messages with i18n keys.
Merge ALTER TABLE statements into initial CREATE TABLE definitions.
Clean up obsolete data migration scripts from schema file.
This commit is contained in:
2026-05-03 12:16:26 +08:00
parent ef82fc805d
commit 043ebe392a
3 changed files with 134 additions and 494 deletions

View File

@@ -617,7 +617,7 @@ function requirePositiveInteger(value: unknown, message: string): number {
return numberValue;
}
function cleanName(value: unknown, message = 'Name is required'): string {
function cleanName(value: unknown, message = 'server.validation.nameRequired'): string {
if (typeof value !== 'string' || value.trim() === '') {
throw validationError(message);
}
@@ -656,7 +656,7 @@ function cleanPokemonStats(value: unknown): PokemonStats {
return pokemonStatLabels.reduce((stats, stat) => {
const numberValue = Number(row[stat.key] ?? 0);
if (!Number.isInteger(numberValue) || numberValue < 0) {
throw validationError(`${stat.label} must be a non-negative integer`);
throw validationError('server.validation.statNonNegative');
}
return { ...stats, [stat.key]: numberValue };
@@ -756,7 +756,7 @@ async function reorderTableRows(
);
if (existing.rowCount !== ids.length) {
throw validationError('Record does not exist');
throw validationError('server.validation.recordMissing');
}
for (const [index, id] of ids.entries()) {
@@ -792,14 +792,14 @@ async function recordEditLog(
function cleanLanguagePayload(payload: Record<string, unknown>, requireCode: boolean): LanguagePayload {
const code = typeof payload.code === 'string' ? payload.code.trim() : '';
if (requireCode && !localePattern.test(code)) {
throw validationError('Language code is invalid');
throw validationError('server.validation.languageCodeInvalid');
}
const sortOrder = Number(payload.sortOrder ?? 0);
return {
code,
name: cleanName(payload.name, 'Language name is required'),
name: cleanName(payload.name, 'server.validation.languageNameRequired'),
enabled: payload.enabled !== false,
isDefault: Boolean(payload.isDefault),
sortOrder: Number.isInteger(sortOrder) && sortOrder >= 0 ? sortOrder : 0
@@ -809,7 +809,7 @@ function cleanLanguagePayload(payload: Record<string, unknown>, requireCode: boo
function requireLanguageCode(value: unknown): string {
const code = typeof value === 'string' ? value.trim() : '';
if (!localePattern.test(code)) {
throw validationError('Language code is invalid');
throw validationError('server.validation.languageCodeInvalid');
}
return code;
}
@@ -828,10 +828,10 @@ export async function listLanguages(includeDisabled = false) {
export async function createLanguage(payload: Record<string, unknown>) {
const cleanPayload = cleanLanguagePayload(payload, true);
if (cleanPayload.isDefault && cleanPayload.code !== defaultLocale) {
throw validationError('Default language must be English');
throw validationError('server.validation.defaultLanguageMustBeEnglish');
}
if (!cleanPayload.enabled && cleanPayload.isDefault) {
throw validationError('Default language must be enabled');
throw validationError('server.validation.defaultLanguageMustBeEnabled');
}
await withTransaction(async (client) => {
@@ -855,10 +855,10 @@ export async function updateLanguage(code: string, payload: Record<string, unkno
const locale = requireLanguageCode(code);
const cleanPayload = cleanLanguagePayload({ ...payload, code: locale }, false);
if (cleanPayload.isDefault && locale !== defaultLocale) {
throw validationError('Default language must be English');
throw validationError('server.validation.defaultLanguageMustBeEnglish');
}
if (!cleanPayload.enabled && cleanPayload.isDefault) {
throw validationError('Default language must be enabled');
throw validationError('server.validation.defaultLanguageMustBeEnabled');
}
await withTransaction(async (client) => {
@@ -868,15 +868,15 @@ export async function updateLanguage(code: string, payload: Record<string, unkno
);
if (current.rowCount === 0) {
throw validationError('Language not found');
throw validationError('server.validation.languageNotFound');
}
if (!cleanPayload.enabled && current.rows[0].isDefault) {
throw validationError('Default language must be enabled');
throw validationError('server.validation.defaultLanguageMustBeEnabled');
}
if (current.rows[0].isDefault && !cleanPayload.isDefault) {
throw validationError('A default language is required');
throw validationError('server.validation.defaultLanguageRequired');
}
if (cleanPayload.isDefault) {
@@ -902,7 +902,7 @@ export async function updateLanguage(code: string, payload: Record<string, unkno
export async function deleteLanguage(code: string) {
const locale = requireLanguageCode(code);
if (locale === defaultLocale) {
throw validationError('Default language cannot be deleted');
throw validationError('server.validation.defaultLanguageCannotBeDeleted');
}
return withTransaction(async (client) => {
@@ -917,7 +917,7 @@ export async function deleteLanguage(code: string) {
export async function reorderLanguages(payload: Record<string, unknown>) {
const codes = Array.isArray(payload.codes) ? payload.codes.map(requireLanguageCode) : [];
if (codes.length === 0) {
throw validationError('Please select a language');
throw validationError('server.validation.selectLanguage');
}
await withTransaction(async (client) => {
@@ -927,7 +927,7 @@ export async function reorderLanguages(payload: Record<string, unknown>) {
);
if (existing.rowCount !== codes.length) {
throw validationError('Language does not exist');
throw validationError('server.validation.languageDoesNotExist');
}
for (const [index, code] of codes.entries()) {
@@ -992,7 +992,7 @@ function parseCsv(content: string, fileName: string): CsvRow[] {
const headers = rows[0]?.map((header) => header.replace(/^\uFEFF/, ''));
if (!headers?.length) {
throw validationError(`${fileName} is empty`);
throw validationError('server.validation.pokemonDataFileEmpty');
}
return rows.slice(1).map((values) =>
@@ -1024,7 +1024,7 @@ async function readPokemonDataFile(fileName: string): Promise<string> {
}
}
throw validationError(`Pokemon data file ${fileName} is unavailable`);
throw validationError('server.validation.pokemonDataFileUnavailable');
}
function csvInteger(row: CsvRow, fieldName: string): number {
@@ -1092,7 +1092,7 @@ async function loadPokemonCsvData(): Promise<PokemonCsvData> {
function pokemonDataLookupKey(value: unknown): string {
const rawValue = typeof value === 'number' ? String(value) : typeof value === 'string' ? value.trim() : '';
if (rawValue === '') {
throw validationError('Pokemon identifier is required');
throw validationError('server.validation.pokemonIdentifierRequired');
}
const numericValue = Number(rawValue);
@@ -1362,7 +1362,7 @@ function cleanPokemonImage(value: unknown, pokemonId: number): PokemonImage | nu
const image = pokemonImageCandidateForPath(pokemonId, path);
if (!image) {
throw validationError('Pokemon image path is invalid');
throw validationError('server.validation.pokemonImagePathInvalid');
}
return image;
}
@@ -1427,7 +1427,7 @@ function fetchedPokemonTypeIds(row: CsvRow, data: PokemonCsvData): number[] {
const typeIds = [csvInteger(row, 'type_1_id'), csvInteger(row, 'type_2_id')].filter((typeId) => typeId > 0);
if (typeIds.length === 0 || typeIds.some((typeId) => !data.typesById.has(typeId) || !pokemonTypeIconIds.has(typeId))) {
throw validationError('Pokemon type data is unavailable');
throw validationError('server.validation.pokemonTypeDataUnavailable');
}
return typeIds;
@@ -1496,7 +1496,7 @@ export async function fetchPokemonData(payload: Record<string, unknown>, userId:
const pokemonRow = data.pokemonByLookup.get(lookupKey);
if (!pokemonRow) {
throw validationError('Pokemon data was not found');
throw validationError('server.validation.pokemonDataNotFound');
}
const id = csvInteger(pokemonRow, 'id');
@@ -1532,7 +1532,7 @@ export async function fetchPokemonImageOptions(payload: Record<string, unknown>)
const pokemonRow = data.pokemonByLookup.get(lookupKey);
if (!pokemonRow) {
throw validationError('Pokemon data was not found');
throw validationError('server.validation.pokemonDataNotFound');
}
const id = csvInteger(pokemonRow, 'id');
@@ -1954,7 +1954,7 @@ export async function getOptions(locale = defaultLocale) {
function cleanDailyChecklistPayload(payload: Record<string, unknown>): DailyChecklistPayload {
return {
title: cleanName(payload.title, 'Please enter a task'),
title: cleanName(payload.title, 'server.validation.taskRequired'),
translations: cleanTranslations(payload.translations, ['title'])
};
}
@@ -2042,7 +2042,7 @@ export async function updateDailyChecklistItem(
export async function reorderDailyChecklistItems(payload: Record<string, unknown>, userId: number, locale = defaultLocale) {
const ids = cleanIds(payload.ids);
if (ids.length === 0) {
throw validationError('Please select a task');
throw validationError('server.validation.selectTask');
}
await withTransaction(async (client) => {
@@ -2052,7 +2052,7 @@ export async function reorderDailyChecklistItems(payload: Record<string, unknown
);
if (existing.rowCount !== ids.length) {
throw validationError('Task does not exist');
throw validationError('server.validation.taskDoesNotExist');
}
for (const [index, id] of ids.entries()) {
@@ -2085,9 +2085,9 @@ export async function deleteDailyChecklistItem(id: number, userId: number) {
}
function cleanLifePostPayload(payload: Record<string, unknown>): LifePostPayload {
const body = cleanName(payload.body, 'Please enter a post');
const body = cleanName(payload.body, 'server.validation.postRequired');
if (body.length > 2000) {
throw validationError('Post is too long');
throw validationError('server.validation.postTooLong');
}
const tagIds = cleanIds(payload.tagIds);
if (tagIds.length === 0) {
@@ -2101,9 +2101,9 @@ function cleanLifePostPayload(payload: Record<string, unknown>): LifePostPayload
}
function cleanLifeCommentPayload(payload: Record<string, unknown>): LifeCommentPayload {
const body = cleanName(payload.body, 'Please enter a comment');
const body = cleanName(payload.body, 'server.validation.commentRequired');
if (body.length > 1000) {
throw validationError('Comment is too long');
throw validationError('server.validation.commentTooLong');
}
return { body };
@@ -2124,7 +2124,7 @@ function isLifeReactionType(value: unknown): value is LifeReactionType {
function cleanLifeReactionType(value: unknown): LifeReactionType {
if (!isLifeReactionType(value)) {
throw validationError('Reaction is invalid');
throw validationError('server.validation.reactionInvalid');
}
return value;
@@ -2182,7 +2182,7 @@ function decodeLifePostCursor(value: QueryValue): LifePostCursor | null {
const id = Number(cursor.id);
if (!createdAt || Number.isNaN(new Date(createdAt).getTime()) || !Number.isInteger(id) || id <= 0) {
throw validationError('Cursor is invalid');
throw validationError('server.validation.cursorInvalid');
}
return { createdAt, id };
@@ -2190,7 +2190,7 @@ function decodeLifePostCursor(value: QueryValue): LifePostCursor | null {
if (error instanceof Error && 'statusCode' in error) {
throw error;
}
throw validationError('Cursor is invalid');
throw validationError('server.validation.cursorInvalid');
}
}
@@ -2372,7 +2372,7 @@ export async function listLifePosts(
}
if (tagIdValue) {
const tagId = requirePositiveInteger(tagIdValue, 'Tag is invalid');
const tagId = requirePositiveInteger(tagIdValue, 'server.validation.tagInvalid');
params.push(tagId);
conditions.push(`EXISTS (
SELECT 1
@@ -2633,16 +2633,16 @@ export async function deleteLifeComment(id: number, userId: number, allowAny = f
function cleanDiscussionEntityType(value: unknown): DiscussionEntityType {
if (typeof value !== 'string' || !Object.hasOwn(discussionEntityDefinitions, value)) {
throw validationError('Entity type is invalid');
throw validationError('server.validation.entityTypeInvalid');
}
return value as DiscussionEntityType;
}
function cleanEntityDiscussionCommentPayload(payload: Record<string, unknown>): EntityDiscussionCommentPayload {
const body = cleanName(payload.body, 'Please enter a comment');
const body = cleanName(payload.body, 'server.validation.commentRequired');
if (body.length > 1000) {
throw validationError('Comment is too long');
throw validationError('server.validation.commentTooLong');
}
return { body };
@@ -2724,7 +2724,7 @@ export async function listEntityDiscussionComments(
entityIdValue: number
): Promise<EntityDiscussionComment[] | null> {
const entityType = cleanDiscussionEntityType(entityTypeValue);
const entityId = requirePositiveInteger(entityIdValue, 'Record is invalid');
const entityId = requirePositiveInteger(entityIdValue, 'server.validation.recordInvalid');
if (!(await entityDiscussionExists(pool, entityType, entityId))) {
return null;
@@ -2748,7 +2748,7 @@ export async function createEntityDiscussionComment(
userId: number
): Promise<EntityDiscussionComment | null> {
const entityType = cleanDiscussionEntityType(entityTypeValue);
const entityId = requirePositiveInteger(entityIdValue, 'Record is invalid');
const entityId = requirePositiveInteger(entityIdValue, 'server.validation.recordInvalid');
const cleanPayload = cleanEntityDiscussionCommentPayload(payload);
const id = await withTransaction(async (client) => {
@@ -2779,8 +2779,8 @@ export async function createEntityDiscussionReply(
userId: number
): Promise<EntityDiscussionComment | null> {
const entityType = cleanDiscussionEntityType(entityTypeValue);
const entityId = requirePositiveInteger(entityIdValue, 'Record is invalid');
const commentId = requirePositiveInteger(commentIdValue, 'Comment is invalid');
const entityId = requirePositiveInteger(entityIdValue, 'server.validation.recordInvalid');
const commentId = requirePositiveInteger(commentIdValue, 'server.validation.commentInvalid');
const cleanPayload = cleanEntityDiscussionCommentPayload(payload);
const id = await withTransaction(async (client) => {
@@ -2816,7 +2816,7 @@ export async function createEntityDiscussionReply(
}
export async function deleteEntityDiscussionComment(id: number, userId: number, allowAny = false): Promise<boolean> {
const commentId = requirePositiveInteger(id, 'Comment is invalid');
const commentId = requirePositiveInteger(id, 'server.validation.commentInvalid');
const result = await queryOne<{ id: number }>(
`
UPDATE entity_discussion_comments
@@ -2917,7 +2917,7 @@ export async function reorderConfig(type: ConfigType, payload: Record<string, un
const definition = configDefinitions[type];
const ids = cleanIds(payload.ids);
if (ids.length === 0) {
throw validationError('Please select a record');
throw validationError('server.validation.selectRecord');
}
await withTransaction(async (client) => {
@@ -2992,7 +2992,7 @@ async function reorderContent(type: SortableContentType, payload: Record<string,
const definition = sortableContentDefinitions[type];
const ids = cleanIds(payload.ids);
if (ids.length === 0) {
throw validationError('Please select a record');
throw validationError('server.validation.selectRecord');
}
await withTransaction(async (client) => {
@@ -3234,16 +3234,16 @@ function cleanPokemonPayload(payload: Record<string, unknown>): PokemonPayload {
const skillItemDrops = new Map<string, SkillItemDrop>();
if (typeIds.length === 0) {
throw validationError('Choose at least 1 type');
throw validationError('server.validation.typeMin');
}
if (cleanTypeIds.length > 2) {
throw validationError('Choose at most 2 types');
throw validationError('server.validation.typeMax');
}
if (skillIds.length > 2) {
throw validationError('Choose at most 2 specialities');
throw validationError('server.validation.skillMax');
}
if (favoriteThingIds.length > 6) {
throw validationError('Choose at most 6 favourites');
throw validationError('server.validation.favoriteMax');
}
if (Array.isArray(payload.skillItemDrops)) {
@@ -3257,27 +3257,27 @@ function cleanPokemonPayload(payload: Record<string, unknown>): PokemonPayload {
}
if (!Number.isInteger(skillId) || skillId <= 0 || !selectedSkillIds.has(skillId)) {
throw validationError('Drop items must be linked to selected specialities');
throw validationError('server.validation.dropItemSelectedSkill');
}
skillItemDrops.set(String(skillId), { skillId, itemId });
}
}
const displayId = requirePositiveInteger(payload.displayId ?? payload.id, 'Pokemon ID is required');
const displayId = requirePositiveInteger(payload.displayId, 'server.validation.pokemonIdRequired');
return {
displayId,
isEventItem: Boolean(payload.isEventItem),
name: cleanName(payload.name, 'Pokemon name is required'),
name: cleanName(payload.name, 'server.validation.pokemonNameRequired'),
genus: cleanOptionalText(payload.genus),
details: cleanOptionalText(payload.details),
heightInches: cleanNonNegativeNumber(payload.heightInches, 'Height must be a non-negative number'),
weightPounds: cleanNonNegativeNumber(payload.weightPounds, 'Weight must be a non-negative number'),
heightInches: cleanNonNegativeNumber(payload.heightInches, 'server.validation.heightNonNegative'),
weightPounds: cleanNonNegativeNumber(payload.weightPounds, 'server.validation.weightNonNegative'),
translations: cleanTranslations(payload.translations, ['name', 'details', 'genus']),
typeIds,
stats: cleanPokemonStats(payload.stats),
environmentId: requirePositiveInteger(payload.environmentId, 'Ideal Habitat is required'),
environmentId: requirePositiveInteger(payload.environmentId, 'server.validation.environmentRequired'),
skillIds,
favoriteThingIds,
skillItemDrops: [...skillItemDrops.values()],
@@ -3318,7 +3318,7 @@ async function replacePokemonRelations(client: DbClient, pokemonId: number, payl
const allowedDropSkillIds = new Set(allowedDrops.rows.map((row) => row.id));
if (payload.skillItemDrops.some((drop) => !allowedDropSkillIds.has(drop.skillId))) {
throw validationError('This speciality cannot have a drop item');
throw validationError('server.validation.skillNoDrop');
}
}
@@ -3601,9 +3601,9 @@ function cleanHabitatPayload(payload: Record<string, unknown>): HabitatPayload {
for (const item of appearances) {
const row = item as Record<string, unknown>;
const pokemonId = Number(row.pokemonId);
const mapIds = cleanIdValues(row.mapIds ?? row.mapId);
const selectedTimeOfDays = cleanOptions(row.timeOfDays ?? row.timeOfDay, timeOfDays);
const selectedWeathers = cleanOptions(row.weathers ?? row.weather, weathers);
const mapIds = cleanIdValues(row.mapIds);
const selectedTimeOfDays = cleanOptions(row.timeOfDays, timeOfDays);
const selectedWeathers = cleanOptions(row.weathers, weathers);
const rarity = Number(row.rarity);
if (!Number.isInteger(pokemonId) || pokemonId <= 0 || !Number.isInteger(rarity) || rarity < 1 || rarity > 3) {
@@ -3626,7 +3626,7 @@ function cleanHabitatPayload(payload: Record<string, unknown>): HabitatPayload {
}
return {
name: cleanName(payload.name, 'Habitat name is required'),
name: cleanName(payload.name, 'server.validation.habitatNameRequired'),
translations: cleanTranslations(payload.translations, ['name']),
isEventItem: Boolean(payload.isEventItem),
imagePath: cleanUploadImagePath(payload.imagePath, 'habitats'),
@@ -3973,12 +3973,12 @@ export async function getItem(id: number, locale = defaultLocale) {
function cleanItemPayload(payload: Record<string, unknown>): ItemPayload {
const usageId = payload.usageId === null || payload.usageId === '' || payload.usageId === undefined
? null
: requirePositiveInteger(payload.usageId, 'Usage is required');
: requirePositiveInteger(payload.usageId, 'server.validation.usageRequired');
return {
name: cleanName(payload.name, 'Item name is required'),
name: cleanName(payload.name, 'server.validation.itemNameRequired'),
translations: cleanTranslations(payload.translations, ['name']),
categoryId: requirePositiveInteger(payload.categoryId, 'Category is required'),
categoryId: requirePositiveInteger(payload.categoryId, 'server.validation.categoryRequired'),
usageId,
dyeable: Boolean(payload.dyeable),
dualDyeable: Boolean(payload.dualDyeable),
@@ -3998,7 +3998,7 @@ async function ensureItemCanDisableRecipe(client: DbClient, itemId: number, noRe
const result = await client.query('SELECT 1 FROM recipes WHERE item_id = $1', [itemId]);
if (result.rowCount && result.rowCount > 0) {
throw validationError('An item with a recipe cannot be marked as recipe-free');
throw validationError('server.validation.recipeFreeWithRecipe');
}
}
@@ -4227,7 +4227,7 @@ export async function getRecipe(id: number, locale = defaultLocale) {
function cleanRecipePayload(payload: Record<string, unknown>): RecipePayload {
return {
itemId: requirePositiveInteger(payload.itemId, 'Item is required'),
itemId: requirePositiveInteger(payload.itemId, 'server.validation.itemRequired'),
acquisitionMethodIds: cleanIds(payload.acquisitionMethodIds),
materials: cleanQuantities(payload.materials)
};
@@ -4256,11 +4256,11 @@ async function replaceRecipeRelations(client: DbClient, recipeId: number, payloa
async function ensureItemCanHaveRecipe(client: DbClient, itemId: number): Promise<void> {
const result = await client.query<{ no_recipe: boolean }>('SELECT no_recipe FROM items WHERE id = $1', [itemId]);
if (result.rowCount === 0) {
throw validationError('Item is required');
throw validationError('server.validation.itemRequired');
}
if (result.rows[0].no_recipe) {
throw validationError('This item is marked as recipe-free');
throw validationError('server.validation.recipeFreeItem');
}
}

View File

@@ -21,56 +21,6 @@ const wordingKeyPattern = /^[A-Za-z][A-Za-z0-9]*(\.[A-Za-z][A-Za-z0-9]*)+$/;
const placeholderPattern = /\{([A-Za-z0-9_]+)\}/g;
const surfaces = new Set<SystemWordingSurface>(['frontend', 'backend', 'email']);
const legacyMessageKeys = new Map<string, string>([
['Record does not exist', 'server.validation.recordMissing'],
['Language code is invalid', 'server.validation.languageCodeInvalid'],
['Language name is required', 'server.validation.languageNameRequired'],
['Default language must be English', 'server.validation.defaultLanguageMustBeEnglish'],
['Default language must be enabled', 'server.validation.defaultLanguageMustBeEnabled'],
['Language not found', 'server.validation.languageNotFound'],
['A default language is required', 'server.validation.defaultLanguageRequired'],
['Default language cannot be deleted', 'server.validation.defaultLanguageCannotBeDeleted'],
['Please select a language', 'server.validation.selectLanguage'],
['Language does not exist', 'server.validation.languageDoesNotExist'],
['Pokemon identifier is required', 'server.validation.pokemonIdentifierRequired'],
['Pokemon type data is unavailable', 'server.validation.pokemonTypeDataUnavailable'],
['Pokemon data was not found', 'server.validation.pokemonDataNotFound'],
['Pokemon image path is invalid', 'server.validation.pokemonImagePathInvalid'],
['Please enter a task', 'server.validation.taskRequired'],
['Please select a task', 'server.validation.selectTask'],
['Task does not exist', 'server.validation.taskDoesNotExist'],
['Please enter a post', 'server.validation.postRequired'],
['Post is too long', 'server.validation.postTooLong'],
['Please enter a comment', 'server.validation.commentRequired'],
['Comment is too long', 'server.validation.commentTooLong'],
['Reaction is invalid', 'server.validation.reactionInvalid'],
['Cursor is invalid', 'server.validation.cursorInvalid'],
['Tag is invalid', 'server.validation.tagInvalid'],
['Entity type is invalid', 'server.validation.entityTypeInvalid'],
['Record is invalid', 'server.validation.recordInvalid'],
['Comment is invalid', 'server.validation.commentInvalid'],
['Please select a record', 'server.validation.selectRecord'],
['Choose at least 1 type', 'server.validation.typeMin'],
['Choose at most 2 types', 'server.validation.typeMax'],
['Choose at most 2 specialities', 'server.validation.skillMax'],
['Choose at most 6 favourites', 'server.validation.favoriteMax'],
['Drop items must be linked to selected specialities', 'server.validation.dropItemSelectedSkill'],
['Pokemon ID is required', 'server.validation.pokemonIdRequired'],
['Pokemon name is required', 'server.validation.pokemonNameRequired'],
['Height must be a non-negative number', 'server.validation.heightNonNegative'],
['Weight must be a non-negative number', 'server.validation.weightNonNegative'],
['Ideal Habitat is required', 'server.validation.environmentRequired'],
['This speciality cannot have a drop item', 'server.validation.skillNoDrop'],
['Habitat name is required', 'server.validation.habitatNameRequired'],
['Usage is required', 'server.validation.usageRequired'],
['Item name is required', 'server.validation.itemNameRequired'],
['Category is required', 'server.validation.categoryRequired'],
['An item with a recipe cannot be marked as recipe-free', 'server.validation.recipeFreeWithRecipe'],
['Item is required', 'server.validation.itemRequired'],
['This item is marked as recipe-free', 'server.validation.recipeFreeItem'],
['Name is required', 'server.validation.nameRequired']
]);
function validationError(message: string): ValidationError {
const error = new Error(message) as ValidationError;
error.statusCode = 400;
@@ -145,22 +95,6 @@ function normalizePlaceholders(value: unknown): string[] {
return Array.isArray(value) ? value.map((item) => String(item)).sort() : [];
}
function legacyMessageKey(message: string): string | null {
if (message.startsWith('server.') || message.startsWith('email.')) {
return message;
}
if (message.endsWith(' must be a non-negative integer')) {
return 'server.validation.statNonNegative';
}
if (message.endsWith(' is empty')) {
return 'server.validation.pokemonDataFileEmpty';
}
if (message.startsWith('Pokemon data file ') && message.endsWith(' is unavailable')) {
return 'server.validation.pokemonDataFileUnavailable';
}
return legacyMessageKeys.get(message) ?? null;
}
export async function syncSystemWordingCatalog(): Promise<void> {
const entries = systemWordingCatalogEntries();
const client = await pool.connect();
@@ -232,8 +166,7 @@ export async function systemMessage(
}
export async function localizedStatusMessage(locale: string, message: string): Promise<string> {
const key = legacyMessageKey(message);
return key ? systemMessage(locale, key) : message;
return message.startsWith('server.') || message.startsWith('email.') ? systemMessage(locale, message) : message;
}
export async function getSystemWordings(locale: string) {