Add image metadata fields to Pokemon schema and API Implement image candidate fetching from pokesprite static tree Add Pokédex-style image picker to edit form and display in details
1272 lines
48 KiB
TypeScript
1272 lines
48 KiB
TypeScript
export type SystemWordingLeaf = string;
|
||
export type SystemWordingTree = { [key: string]: SystemWordingLeaf | SystemWordingTree };
|
||
export type SystemWordingMessages = Record<string, SystemWordingTree>;
|
||
|
||
export const defaultLocale = 'en';
|
||
|
||
export const systemWordingMessages = {
|
||
en: {
|
||
common: {
|
||
add: 'Add',
|
||
admin: 'Admin',
|
||
all: 'All',
|
||
back: 'Back',
|
||
backToList: 'Back to list',
|
||
cancel: 'Cancel',
|
||
close: 'Close',
|
||
create: 'Create',
|
||
delete: 'Delete',
|
||
edit: 'Edit',
|
||
details: 'Details',
|
||
filters: 'Filters',
|
||
loading: 'Loading',
|
||
name: 'Name',
|
||
new: 'New',
|
||
none: 'None',
|
||
save: 'Save',
|
||
saving: 'Saving',
|
||
search: 'Search',
|
||
select: 'Select',
|
||
selected: 'Selected',
|
||
system: 'System',
|
||
noRecords: 'No records',
|
||
fieldForLanguage: '{field} ({language})',
|
||
searchOrSelect: 'Search or select',
|
||
noMatches: 'No matches',
|
||
createNamed: 'Add "{name}"',
|
||
creating: 'Adding',
|
||
inDev: 'In-Dev',
|
||
removeNamed: 'Remove {name}',
|
||
quantity: 'Quantity',
|
||
required: 'Required'
|
||
},
|
||
nav: {
|
||
pokemon: 'Pokemon',
|
||
habitats: 'Habitats',
|
||
items: 'Items',
|
||
recipes: 'Recipes',
|
||
dish: 'Dish',
|
||
events: 'Events',
|
||
actions: 'Actions',
|
||
dreamIsland: 'Dream Island',
|
||
clothes: 'Clothes',
|
||
checklist: 'CheckList',
|
||
life: 'Life',
|
||
admin: 'Admin',
|
||
main: 'Main navigation',
|
||
openMenu: 'Open navigation',
|
||
closeMenu: 'Close navigation',
|
||
language: 'Language',
|
||
login: 'Log in',
|
||
logout: 'Log out',
|
||
register: 'Register'
|
||
},
|
||
auth: {
|
||
email: 'Email',
|
||
password: 'Password',
|
||
displayName: 'Display name',
|
||
loginTitle: 'Log in',
|
||
loginSubtitle: 'Use a verified email to enter Pokopia Wiki.',
|
||
loggingIn: 'Logging in',
|
||
loginFailed: 'Login failed',
|
||
noAccount: 'No account yet?',
|
||
registerTitle: 'Register',
|
||
registerSubtitle: 'Verify your email after creating an account.',
|
||
registerFailed: 'Registration failed',
|
||
sending: 'Sending',
|
||
sendVerification: 'Send verification email',
|
||
hasAccount: 'Already have an account?',
|
||
verifyTitle: 'Email verification',
|
||
verifySubtitle: 'You can log in after verification is complete.',
|
||
verifyingEmail: 'Verifying email',
|
||
invalidVerification: 'The verification link is invalid or expired.',
|
||
verifyFailed: 'Email verification failed',
|
||
goLogin: 'Go to login'
|
||
},
|
||
errors: {
|
||
requestFailed: 'Request failed ({status})',
|
||
operationFailed: 'Operation failed',
|
||
loadFailed: 'Load failed',
|
||
addFailed: 'Add failed',
|
||
saveFailed: 'Save failed',
|
||
completeEmailVerification: 'Please complete email verification first.'
|
||
},
|
||
pages: {
|
||
pokemon: {
|
||
title: 'Pokemon',
|
||
subtitle: 'Search Pokemon and filter by specialities, ideal habitat, and favourites.',
|
||
detailKicker: 'Pokédex Detail',
|
||
editKicker: 'Pokédex Edit',
|
||
editSubtitle: 'Maintain Pokemon profile, details, types, stats, specialities, and favourites.',
|
||
editSections: 'Pokemon edit sections',
|
||
editTabBasic: 'Basic',
|
||
editTabAdvance: 'Advance',
|
||
newTitle: 'New Pokemon',
|
||
editTitle: 'Edit #{id} {name}',
|
||
fetchData: 'Fetch data',
|
||
fetchingData: 'Fetching',
|
||
fetchIdentifier: 'Data identifier',
|
||
fetchIdentifierPlaceholder: 'bulbasaur or 1',
|
||
fetchIdentifierRequired: 'Enter a Pokemon identifier',
|
||
fetchFailed: 'Pokemon data fetch failed',
|
||
fetchIdMismatch: 'Fetched Pokemon ID #{id} does not match this editor.',
|
||
fetchResults: 'Pokemon data results',
|
||
fetchSearching: 'Searching data',
|
||
fetchNoMatches: 'No matching Pokemon data',
|
||
fetchSearchFailed: 'Pokemon data search failed',
|
||
image: 'Image',
|
||
fetchImages: 'Fetch images',
|
||
fetchingImages: 'Fetching',
|
||
imageFetchFailed: 'Pokemon image fetch failed',
|
||
imageNoMatches: 'No available Pokemon images',
|
||
loadingImages: 'Loading Pokemon images',
|
||
selectedImage: 'Selected Pokemon image',
|
||
imageOptions: 'Pokemon image options',
|
||
clearImage: 'Clear image',
|
||
imageEmpty: 'No Pokemon image selected',
|
||
imageAlt: '{name} {variant} image',
|
||
loadingList: 'Loading Pokemon list',
|
||
loadingDetail: 'Loading Pokemon detail',
|
||
loadingEdit: 'Loading Pokemon editor',
|
||
environmentPrefix: 'Ideal Habitat: {name}',
|
||
details: 'Details',
|
||
genus: 'Genus',
|
||
height: 'Height',
|
||
heightInput: 'Height (in)',
|
||
heightImperial: 'ft / in',
|
||
heightMetric: 'm',
|
||
feet: 'ft',
|
||
inches: 'in',
|
||
meters: 'm',
|
||
weight: 'Weight',
|
||
weightInput: 'Weight (lb)',
|
||
pounds: 'lb',
|
||
kilograms: 'kg',
|
||
measurements: 'Height & Weight',
|
||
types: 'Types',
|
||
typeOne: 'Type 1',
|
||
typeTwo: 'Type 2',
|
||
typesAndStats: 'Types & Base stats',
|
||
statsTitle: 'Base stats',
|
||
stats: {
|
||
hp: 'HP',
|
||
attack: 'Attack',
|
||
defense: 'Defense',
|
||
specialAttack: 'Special Attack',
|
||
specialDefense: 'Special Defense',
|
||
speed: 'Speed'
|
||
},
|
||
environment: 'Ideal Habitat',
|
||
skills: 'Specialities',
|
||
skillMatchMode: 'Speciality match mode',
|
||
any: 'Any',
|
||
all: 'All',
|
||
favoriteThings: 'Favourites',
|
||
favoriteThingMatchMode: 'Favourites match mode',
|
||
skillDrops: 'Speciality drops',
|
||
skillDrop: '{name} drop',
|
||
dropItem: 'Drop item',
|
||
searchPokemon: 'Search Pokemon',
|
||
relatedPokemon: 'Related Pokemon',
|
||
relatedHabitat: 'Related Pokemon habitat',
|
||
relatedItems: 'Related items',
|
||
relatedItemCategory: 'Related item category',
|
||
habitats: 'Habitats',
|
||
namePlaceholder: 'Name',
|
||
searchTypes: 'Search types',
|
||
searchEnvironment: 'Search ideal habitats',
|
||
searchSkills: 'Search specialities',
|
||
searchFavoriteThings: 'Search favourites',
|
||
searchItems: 'Search items'
|
||
},
|
||
habitats: {
|
||
title: 'Habitats',
|
||
subtitle: 'View recipes and Pokemon that may appear.',
|
||
detailSubtitle: 'Habitat detail',
|
||
editSubtitle: 'Maintain habitat recipes and possible Pokemon appearances.',
|
||
newTitle: 'New habitat',
|
||
editTitle: 'Edit {name}',
|
||
fallbackName: 'Habitat',
|
||
loadingList: 'Loading habitat list',
|
||
loadingDetail: 'Loading habitat detail',
|
||
loadingEdit: 'Loading habitat editor',
|
||
recipe: 'Recipe',
|
||
recipeList: 'Recipe list',
|
||
possiblePokemon: 'Possible Pokemon',
|
||
addItem: 'Add item',
|
||
addPokemon: 'Add Pokemon',
|
||
maps: 'Maps',
|
||
searchMaps: 'Search maps'
|
||
},
|
||
items: {
|
||
title: 'Items',
|
||
subtitle: 'Browse items by category, usage, and tags.',
|
||
detailKicker: 'Item Detail',
|
||
detailSubtitle: 'Item detail',
|
||
editKicker: 'Item Edit',
|
||
editSubtitle: 'Maintain item category, usage, acquisition methods, customization, and tags.',
|
||
newTitle: 'New item',
|
||
editTitle: 'Edit {name}',
|
||
fallbackName: 'Item',
|
||
loadingList: 'Loading item list',
|
||
loadingDetail: 'Loading item detail',
|
||
loadingEdit: 'Loading item editor',
|
||
category: 'Category',
|
||
usage: 'Usage',
|
||
tags: 'Tags',
|
||
acquisitionMethods: 'Acquisition methods',
|
||
customization: 'Customization',
|
||
dyeable: 'Dyeable',
|
||
dualDyeable: 'Dual dyeable',
|
||
patternEditable: 'Pattern editable',
|
||
noRecipe: 'No recipe',
|
||
recipeInfo: 'Recipe info',
|
||
relatedRecipes: 'Related recipes',
|
||
relatedHabitats: 'Related habitats',
|
||
pokemonDrops: 'Pokemon drops',
|
||
createRecipe: 'Create recipe',
|
||
searchCategory: 'Search categories',
|
||
searchUsage: 'Search usages',
|
||
searchMethods: 'Search acquisition methods',
|
||
searchTags: 'Search tags'
|
||
},
|
||
recipes: {
|
||
title: 'Recipes',
|
||
subtitle: 'Browse recipes by category, usage, and tags.',
|
||
detailKicker: 'Recipe Detail',
|
||
detailSubtitle: 'Recipe detail',
|
||
editKicker: 'Recipe Edit',
|
||
editSubtitle: 'Maintain result item, acquisition methods, and materials.',
|
||
newTitle: 'New recipe',
|
||
editTitle: 'Edit {name}',
|
||
fallbackName: 'Recipe',
|
||
loadingList: 'Loading recipe list',
|
||
loadingDetail: 'Loading recipe detail',
|
||
loadingEdit: 'Loading recipe editor',
|
||
item: 'Item',
|
||
materials: 'Materials',
|
||
addMaterial: 'Add material'
|
||
},
|
||
comingSoon: {
|
||
status: 'In development',
|
||
heading: 'This wiki section is being prepared.',
|
||
previewLabel: 'Section preview',
|
||
sections: {
|
||
dish: {
|
||
kicker: 'Dish',
|
||
title: 'Dish',
|
||
subtitle: 'A future home for cooked dishes and food discoveries.',
|
||
body: 'Dish pages are being shaped for clear browsing, source notes, and useful ingredient links.',
|
||
preview: {
|
||
one: 'Dish records will focus on names, effects, and discovery context.',
|
||
two: 'Ingredient relationships will connect back to items and recipes where useful.',
|
||
three: 'The page will stay browse-first so community edits can grow naturally.'
|
||
}
|
||
},
|
||
events: {
|
||
kicker: 'Events',
|
||
title: 'Events',
|
||
subtitle: 'Seasonal and limited-time game activity records are coming later.',
|
||
body: 'Events will collect timing, rewards, and participation details once the section is ready.',
|
||
preview: {
|
||
one: 'Event cards will make dates and active windows easy to scan.',
|
||
two: 'Rewards and related items will sit close to the event summary.',
|
||
three: 'Archived activities will remain readable after they end.'
|
||
}
|
||
},
|
||
actions: {
|
||
kicker: 'Actions',
|
||
title: 'Actions',
|
||
subtitle: 'Game shortcut actions such as waving and dancing will be documented here.',
|
||
body: 'Actions are being prepared as a quick reference for expressive in-game gestures and shortcuts.',
|
||
preview: {
|
||
one: 'Each action will describe the gesture or shortcut in player-facing language.',
|
||
two: 'Common examples include waving, dancing, and other social actions.',
|
||
three: 'Related unlock or usage details can be linked when the data model is ready.'
|
||
}
|
||
},
|
||
dreamIsland: {
|
||
kicker: 'Dream Island',
|
||
title: 'Dream Island',
|
||
subtitle: 'Dream Island information is being organized for future browsing.',
|
||
body: 'This area will present island details with a calm, destination-style layout when content is ready.',
|
||
preview: {
|
||
one: 'Island notes will prioritize location, availability, and notable discoveries.',
|
||
two: 'Related Pokemon, items, or activities can be connected from the page.',
|
||
three: 'The layout will support browsing without adding another management flow yet.'
|
||
}
|
||
},
|
||
clothes: {
|
||
kicker: 'Clothes',
|
||
title: 'Clothes',
|
||
subtitle: 'Outfit and clothing references are being prepared.',
|
||
body: 'Clothes pages will make it easy to compare appearance, acquisition, and customization details.',
|
||
preview: {
|
||
one: 'Clothing entries will focus on display names and visual categories.',
|
||
two: 'Acquisition and customization details can be connected when available.',
|
||
three: 'The page will keep item-like details readable without mixing them into the item list.'
|
||
}
|
||
}
|
||
}
|
||
},
|
||
checklist: {
|
||
title: 'Daily checklist',
|
||
subtitle: 'See what can be completed each day.',
|
||
sectionTitle: 'Daily tasks',
|
||
empty: 'No daily checklist',
|
||
loading: 'Loading daily checklist',
|
||
task: 'Task',
|
||
newTask: 'New task',
|
||
editTask: 'Edit task'
|
||
},
|
||
life: {
|
||
title: 'Life',
|
||
subtitle: 'Share favourite thoughts, tips, and community finds.',
|
||
kicker: 'Community Feed',
|
||
composerTitle: 'Share something',
|
||
composerPrompt: 'What would you like to share?',
|
||
bodyLabel: 'Post',
|
||
bodyPlaceholder: 'Share a thought, tip, or discovery...',
|
||
newPost: 'New Post',
|
||
tags: 'Tags',
|
||
allTags: 'All',
|
||
tagPlaceholder: 'Select tags',
|
||
searchTags: 'Search tags',
|
||
search: 'Search Life',
|
||
searchPlaceholder: 'Search post content...',
|
||
clearSearch: 'Clear search',
|
||
searchEmpty: 'No posts match your search',
|
||
searchEmptyHint: 'Try another keyword or clear the search.',
|
||
comments: 'Comments',
|
||
commentsCount: '{count} comments',
|
||
comment: 'Comment',
|
||
hideComments: 'Hide comments',
|
||
react: 'Like',
|
||
reactions: 'Reactions',
|
||
reactionsCount: '{count} reactions',
|
||
reactionCountLabel: '{reaction}: {count}',
|
||
reactionLike: 'Like',
|
||
reactionHelpful: 'Helpful',
|
||
reactionFun: 'Fun',
|
||
reactionThanks: 'Thanks',
|
||
chooseReaction: 'Choose reaction',
|
||
reactionMenu: 'Reaction menu',
|
||
removeReaction: 'Remove reaction',
|
||
reactionFailed: 'Reaction failed',
|
||
commentPlaceholder: 'Write a comment...',
|
||
commentReplyPlaceholder: 'Write a reply...',
|
||
postComment: 'Post comment',
|
||
postingComment: 'Posting comment',
|
||
reply: 'Reply',
|
||
postReply: 'Post reply',
|
||
postingReply: 'Posting reply',
|
||
cancelReply: 'Cancel reply',
|
||
noComments: 'No comments yet',
|
||
deleteComment: 'Delete comment',
|
||
deleteCommentConfirm: 'Delete this comment?',
|
||
commentDeleted: 'Comment deleted',
|
||
commentRequired: 'Please enter a comment.',
|
||
commentFailed: 'Comment failed',
|
||
replyFailed: 'Reply failed',
|
||
deleteCommentFailed: 'Delete comment failed',
|
||
publish: 'Post',
|
||
publishing: 'Posting',
|
||
update: 'Update',
|
||
updating: 'Updating',
|
||
cancelEdit: 'Cancel edit',
|
||
empty: 'No posts yet',
|
||
emptyHint: 'Verified members can start the first Life post.',
|
||
loading: 'Loading Life feed',
|
||
retryFeed: 'Retry loading',
|
||
loginPrompt: 'Log in with a verified email to post.',
|
||
verifyPrompt: 'Complete email verification to post.',
|
||
editPost: 'Edit post',
|
||
deletePost: 'Delete post',
|
||
saveEdit: 'Save edit',
|
||
postFailed: 'Post failed',
|
||
saveFailed: 'Save failed',
|
||
deleteFailed: 'Delete failed',
|
||
bodyRequired: 'Please enter a post.',
|
||
byUnknown: 'Community member',
|
||
edited: 'Edited',
|
||
deleteConfirm: 'Delete this post?',
|
||
charactersLeft: '{count} characters left'
|
||
},
|
||
admin: {
|
||
title: 'Admin',
|
||
subtitle: 'Maintain system configuration and manage Wiki records.',
|
||
modules: 'Admin modules',
|
||
loading: 'Loading admin list',
|
||
config: 'System config',
|
||
configType: 'System config type',
|
||
checklist: 'CheckList',
|
||
pokemonList: 'Pokemon list',
|
||
itemList: 'Item list',
|
||
recipeList: 'Recipe list',
|
||
habitatList: 'Habitat list',
|
||
languages: 'Languages',
|
||
newConfig: 'New {name}',
|
||
editConfig: 'Edit {name}',
|
||
hasItemDrop: 'Has item drop',
|
||
dragSort: 'Drag to reorder: {name}',
|
||
dragSortTitle: 'Drag to reorder',
|
||
languageCode: 'Code',
|
||
languageName: 'Language name',
|
||
enabled: 'Enabled',
|
||
defaultLanguage: 'Default language',
|
||
sortOrder: 'Sort order',
|
||
newLanguage: 'New language',
|
||
editLanguage: 'Edit language',
|
||
wordings: 'System wordings',
|
||
wordingLocale: 'Locale',
|
||
wordingModule: 'Module',
|
||
wordingSurface: 'Surface',
|
||
wordingMissingOnly: 'Missing only',
|
||
wordingKey: 'Key',
|
||
wordingValue: 'Wording',
|
||
defaultValue: 'Default wording',
|
||
placeholders: 'Placeholders',
|
||
missingTranslation: 'Missing translation',
|
||
allModules: 'All modules',
|
||
allSurfaces: 'All surfaces',
|
||
surfaceFrontend: 'Frontend',
|
||
surfaceBackend: 'Backend',
|
||
surfaceEmail: 'Email',
|
||
editWording: 'Edit wording'
|
||
}
|
||
},
|
||
config: {
|
||
pokemonTypes: 'Pokemon Types',
|
||
skills: 'Specialities',
|
||
environments: 'Ideal Habitats',
|
||
favoriteThings: 'Favourites / tags',
|
||
itemCategories: 'Item categories',
|
||
itemUsages: 'Item usages',
|
||
acquisitionMethods: 'Acquisition methods',
|
||
maps: 'Maps',
|
||
lifeTags: 'Life tags'
|
||
},
|
||
appearance: {
|
||
time: 'Time',
|
||
weather: 'Weather',
|
||
rarity: 'Rarity',
|
||
map: 'Map',
|
||
maps: 'Maps',
|
||
morning: 'Morning',
|
||
noon: 'Noon',
|
||
evening: 'Evening',
|
||
night: 'Night',
|
||
sunny: 'Sunny',
|
||
cloudy: 'Cloudy',
|
||
rainy: 'Rainy',
|
||
stars: '{count} stars'
|
||
},
|
||
history: {
|
||
title: 'Contribution records',
|
||
createdBy: 'Created by',
|
||
lastEdited: 'Last edited',
|
||
editHistory: 'Edit history',
|
||
before: 'Before',
|
||
after: 'After',
|
||
author: 'Author',
|
||
time: 'Time',
|
||
action: 'Action',
|
||
create: 'Create',
|
||
update: 'Edit',
|
||
delete: 'Delete',
|
||
empty: 'No edit history'
|
||
},
|
||
discussion: {
|
||
title: 'Discussion',
|
||
count: '{count} comments',
|
||
comment: 'Comment',
|
||
commentPlaceholder: 'Write a comment...',
|
||
replyPlaceholder: 'Write a reply...',
|
||
postComment: 'Post comment',
|
||
postingComment: 'Posting comment',
|
||
reply: 'Reply',
|
||
postReply: 'Post reply',
|
||
postingReply: 'Posting reply',
|
||
cancelReply: 'Cancel reply',
|
||
deleteComment: 'Delete comment',
|
||
deleteConfirm: 'Delete this comment?',
|
||
deletedComment: 'Comment deleted',
|
||
commentRequired: 'Please enter a comment.',
|
||
commentFailed: 'Comment failed',
|
||
replyFailed: 'Reply failed',
|
||
deleteFailed: 'Delete failed',
|
||
loading: 'Loading discussion',
|
||
empty: 'No discussion yet',
|
||
emptyHint: 'Start a new discussion now.',
|
||
loginPrompt: 'Log in with a verified email to comment.',
|
||
verifyPrompt: 'Complete email verification to comment.',
|
||
byUnknown: 'Community member',
|
||
charactersLeft: '{count} characters left'
|
||
},
|
||
server: {
|
||
errors: {
|
||
foreignKey: 'Referenced data does not exist or the record is currently in use',
|
||
duplicate: 'A record with the same name or ID already exists',
|
||
invalidField: 'Field value is invalid',
|
||
serverError: 'Server error',
|
||
loginRequired: 'Please log in first',
|
||
verifyEmailFirst: 'Please complete email verification first',
|
||
notFound: 'Not found'
|
||
},
|
||
auth: {
|
||
emailRequired: 'Email is required',
|
||
invalidEmail: 'Email format is invalid',
|
||
displayNameRequired: 'Display name is required',
|
||
displayNameLength: 'Display name must be 1 to 40 characters',
|
||
passwordLength: 'Password must be at least 8 characters',
|
||
invalidToken: 'The verification link is invalid or expired',
|
||
emailAlreadyRegistered: 'This email is already registered',
|
||
checkVerificationEmail: 'Please check your verification email',
|
||
emailVerified: 'Email verified',
|
||
invalidCredentials: 'Email or password is incorrect',
|
||
verifyEmailFirst: 'Please complete email verification first'
|
||
},
|
||
validation: {
|
||
nameRequired: 'Name is required',
|
||
recordMissing: 'Record does not exist',
|
||
languageCodeInvalid: 'Language code is invalid',
|
||
languageNameRequired: 'Language name is required',
|
||
defaultLanguageMustBeEnglish: 'Default language must be English',
|
||
defaultLanguageMustBeEnabled: 'Default language must be enabled',
|
||
languageNotFound: 'Language not found',
|
||
defaultLanguageRequired: 'A default language is required',
|
||
defaultLanguageCannotBeDeleted: 'Default language cannot be deleted',
|
||
selectLanguage: 'Please select a language',
|
||
languageDoesNotExist: 'Language does not exist',
|
||
pokemonIdentifierRequired: 'Pokemon identifier is required',
|
||
pokemonTypeDataUnavailable: 'Pokemon type data is unavailable',
|
||
pokemonDataNotFound: 'Pokemon data was not found',
|
||
pokemonImagePathInvalid: 'Pokemon image path is invalid',
|
||
taskRequired: 'Please enter a task',
|
||
selectTask: 'Please select a task',
|
||
taskDoesNotExist: 'Task does not exist',
|
||
postRequired: 'Please enter a post',
|
||
postTooLong: 'Post is too long',
|
||
commentRequired: 'Please enter a comment',
|
||
commentTooLong: 'Comment is too long',
|
||
reactionInvalid: 'Reaction is invalid',
|
||
cursorInvalid: 'Cursor is invalid',
|
||
tagInvalid: 'Tag is invalid',
|
||
entityTypeInvalid: 'Entity type is invalid',
|
||
recordInvalid: 'Record is invalid',
|
||
commentInvalid: 'Comment is invalid',
|
||
selectRecord: 'Please select a record',
|
||
typeMin: 'Choose at least 1 type',
|
||
typeMax: 'Choose at most 2 types',
|
||
skillMax: 'Choose at most 2 specialities',
|
||
favoriteMax: 'Choose at most 6 favourites',
|
||
dropItemSelectedSkill: 'Drop items must be linked to selected specialities',
|
||
pokemonIdRequired: 'Pokemon ID is required',
|
||
pokemonNameRequired: 'Pokemon name is required',
|
||
heightNonNegative: 'Height must be a non-negative number',
|
||
weightNonNegative: 'Weight must be a non-negative number',
|
||
environmentRequired: 'Ideal Habitat is required',
|
||
skillNoDrop: 'This speciality cannot have a drop item',
|
||
habitatNameRequired: 'Habitat name is required',
|
||
usageRequired: 'Usage is required',
|
||
itemNameRequired: 'Item name is required',
|
||
categoryRequired: 'Category is required',
|
||
recipeFreeWithRecipe: 'An item with a recipe cannot be marked as recipe-free',
|
||
itemRequired: 'Item is required',
|
||
recipeFreeItem: 'This item is marked as recipe-free',
|
||
statNonNegative: 'Base stat must be a non-negative integer',
|
||
pokemonDataFileEmpty: 'Pokemon data file is empty',
|
||
pokemonDataFileUnavailable: 'Pokemon data file is unavailable'
|
||
},
|
||
wordings: {
|
||
keyNotFound: 'System wording key was not found',
|
||
localeRequired: 'Locale is required',
|
||
valueRequired: 'Wording is required',
|
||
placeholderMismatch: 'Placeholders must match the default wording'
|
||
}
|
||
},
|
||
email: {
|
||
auth: {
|
||
verificationSubject: 'Verify your Pokopia Wiki email',
|
||
verificationHtml:
|
||
'<p>Open the link below to verify your email:</p><p><a href="{url}">Verify email</a></p><p>The link expires in {hours} hours.</p>',
|
||
verificationText: 'Open this link to verify your Pokopia Wiki email: {url}\nThe link expires in {hours} hours.'
|
||
}
|
||
},
|
||
},
|
||
'zh-CN': {
|
||
common: {
|
||
add: '添加',
|
||
admin: '管理',
|
||
all: '全部',
|
||
back: '返回',
|
||
backToList: '返回列表',
|
||
cancel: '取消',
|
||
close: '关闭',
|
||
create: '创建',
|
||
delete: '删除',
|
||
edit: '编辑',
|
||
details: '详情',
|
||
filters: '筛选',
|
||
loading: '加载中',
|
||
name: '名称',
|
||
new: '新建',
|
||
none: '无',
|
||
save: '保存',
|
||
saving: '保存中',
|
||
search: '搜索',
|
||
select: '请选择',
|
||
selected: '已选',
|
||
system: '系统',
|
||
noRecords: '暂无记录',
|
||
fieldForLanguage: '{field}({language})',
|
||
searchOrSelect: '搜索或选择',
|
||
noMatches: '没有匹配项',
|
||
createNamed: '添加「{name}」',
|
||
creating: '添加中',
|
||
inDev: '开发中',
|
||
removeNamed: '移除{name}',
|
||
quantity: '数量',
|
||
required: '必填'
|
||
},
|
||
nav: {
|
||
pokemon: 'Pokemon',
|
||
habitats: '栖息地',
|
||
items: '物品',
|
||
recipes: '材料单',
|
||
dish: '料理',
|
||
events: '活动',
|
||
actions: '动作',
|
||
dreamIsland: 'Dream Island',
|
||
clothes: '服装',
|
||
checklist: 'CheckList',
|
||
life: 'Life',
|
||
admin: '管理',
|
||
main: '主导航',
|
||
openMenu: '打开导航',
|
||
closeMenu: '关闭导航',
|
||
language: '语言',
|
||
login: '登录',
|
||
logout: '退出',
|
||
register: '注册'
|
||
},
|
||
auth: {
|
||
email: '邮箱',
|
||
password: '密码',
|
||
displayName: '显示名',
|
||
loginTitle: '登录',
|
||
loginSubtitle: '使用已验证邮箱进入 Pokopia Wiki',
|
||
loggingIn: '登录中',
|
||
loginFailed: '登录失败',
|
||
noAccount: '还没有账号?',
|
||
registerTitle: '注册',
|
||
registerSubtitle: '创建账号后需要完成邮箱验证',
|
||
registerFailed: '注册失败',
|
||
sending: '发送中',
|
||
sendVerification: '发送验证邮件',
|
||
hasAccount: '已有账号?',
|
||
verifyTitle: '邮箱验证',
|
||
verifySubtitle: '完成验证后即可登录',
|
||
verifyingEmail: '正在验证邮箱',
|
||
invalidVerification: '验证链接无效或已过期',
|
||
verifyFailed: '邮箱验证失败',
|
||
goLogin: '去登录'
|
||
},
|
||
errors: {
|
||
requestFailed: '请求失败({status})',
|
||
operationFailed: '操作失败',
|
||
loadFailed: '加载失败',
|
||
addFailed: '添加失败',
|
||
saveFailed: '保存失败',
|
||
completeEmailVerification: '请先完成邮箱验证'
|
||
},
|
||
pages: {
|
||
pokemon: {
|
||
title: 'Pokemon',
|
||
subtitle: '搜索宝可梦,并按特长、环境、喜欢的东西筛选。',
|
||
detailKicker: 'Pokédex Detail',
|
||
editKicker: 'Pokédex Edit',
|
||
editSubtitle: '维护 Pokemon 介绍、属性、六维、特长和喜欢的东西。',
|
||
editSections: 'Pokemon 编辑分区',
|
||
editTabBasic: '基础',
|
||
editTabAdvance: '进阶',
|
||
newTitle: '新增 Pokemon',
|
||
editTitle: '编辑 #{id} {name}',
|
||
fetchData: '获取数据',
|
||
fetchingData: '正在获取',
|
||
fetchIdentifier: '数据标识',
|
||
fetchIdentifierPlaceholder: 'bulbasaur 或 1',
|
||
fetchIdentifierRequired: '请输入 Pokemon 数据标识',
|
||
fetchFailed: 'Pokemon 数据获取失败',
|
||
fetchIdMismatch: '获取到的 Pokemon ID #{id} 与当前编辑内容不一致。',
|
||
fetchResults: 'Pokemon 数据结果',
|
||
fetchSearching: '正在搜索数据',
|
||
fetchNoMatches: '没有匹配的 Pokemon 数据',
|
||
fetchSearchFailed: 'Pokemon 数据搜索失败',
|
||
image: '图片',
|
||
fetchImages: '获取图片',
|
||
fetchingImages: '正在获取',
|
||
imageFetchFailed: 'Pokemon 图片获取失败',
|
||
imageNoMatches: '没有可用的 Pokemon 图片',
|
||
loadingImages: '正在加载 Pokemon 图片',
|
||
selectedImage: '已选择的 Pokemon 图片',
|
||
imageOptions: 'Pokemon 图片选项',
|
||
clearImage: '清除图片',
|
||
imageEmpty: '尚未选择 Pokemon 图片',
|
||
imageAlt: '{name} {variant} 图片',
|
||
loadingList: '正在加载 Pokemon 列表',
|
||
loadingDetail: '正在加载 Pokemon 详情',
|
||
loadingEdit: '正在加载 Pokemon 编辑内容',
|
||
environmentPrefix: '喜欢的环境:{name}',
|
||
details: '介绍',
|
||
genus: '分类',
|
||
height: '身高',
|
||
heightInput: '身高(in)',
|
||
heightImperial: 'ft / in',
|
||
heightMetric: 'm',
|
||
feet: 'ft',
|
||
inches: 'in',
|
||
meters: 'm',
|
||
weight: '体重',
|
||
weightInput: '体重(lb)',
|
||
pounds: 'lb',
|
||
kilograms: 'kg',
|
||
measurements: '身高与体重',
|
||
types: '属性',
|
||
typeOne: '属性 1',
|
||
typeTwo: '属性 2',
|
||
typesAndStats: '属性与六维',
|
||
statsTitle: '六维',
|
||
stats: {
|
||
hp: 'HP',
|
||
attack: '攻击',
|
||
defense: '防御',
|
||
specialAttack: '特攻',
|
||
specialDefense: '特防',
|
||
speed: '速度'
|
||
},
|
||
environment: '喜欢的环境',
|
||
skills: '特长',
|
||
skillMatchMode: '特长匹配方式',
|
||
any: '任意',
|
||
all: '全部',
|
||
favoriteThings: '喜欢的东西',
|
||
favoriteThingMatchMode: '喜欢的东西匹配方式',
|
||
skillDrops: '特长掉落物',
|
||
skillDrop: '{name}掉落物',
|
||
dropItem: '掉落物',
|
||
searchPokemon: '搜索 Pokemon',
|
||
relatedPokemon: '相关 Pokemon',
|
||
relatedHabitat: '相关 Pokemon 栖息地',
|
||
relatedItems: '关联物品',
|
||
relatedItemCategory: '关联物品分类',
|
||
habitats: '栖息地',
|
||
namePlaceholder: '名字',
|
||
searchTypes: '搜索属性',
|
||
searchEnvironment: '搜索喜欢的环境',
|
||
searchSkills: '搜索特长',
|
||
searchFavoriteThings: '搜索喜欢的东西',
|
||
searchItems: '搜索物品'
|
||
},
|
||
habitats: {
|
||
title: '栖息地',
|
||
subtitle: '查看配方和可能出现的宝可梦。',
|
||
detailSubtitle: '栖息地详情',
|
||
editSubtitle: '维护栖息地配方和可能出现的 Pokemon。',
|
||
newTitle: '新增栖息地',
|
||
editTitle: '编辑 {name}',
|
||
fallbackName: '栖息地',
|
||
loadingList: '正在加载栖息地列表',
|
||
loadingDetail: '正在加载栖息地详情',
|
||
loadingEdit: '正在加载栖息地编辑内容',
|
||
recipe: '配方',
|
||
recipeList: '配方列表',
|
||
possiblePokemon: '可能出现的宝可梦',
|
||
addItem: '添加物品',
|
||
addPokemon: '添加 Pokemon',
|
||
maps: '地图',
|
||
searchMaps: '搜索地图'
|
||
},
|
||
items: {
|
||
title: '物品',
|
||
subtitle: '按分类、用途、标签查看物品。',
|
||
detailKicker: 'Item Detail',
|
||
detailSubtitle: '物品详情',
|
||
editKicker: 'Item Edit',
|
||
editSubtitle: '维护物品分类、用途、入手方式、自定义和标签。',
|
||
newTitle: '新增物品',
|
||
editTitle: '编辑 {name}',
|
||
fallbackName: '物品',
|
||
loadingList: '正在加载列表',
|
||
loadingDetail: '正在加载物品详情',
|
||
loadingEdit: '正在加载物品编辑内容',
|
||
category: '分类',
|
||
usage: '用途',
|
||
tags: '标签',
|
||
acquisitionMethods: '入手方式',
|
||
customization: '自定义',
|
||
dyeable: '可染色',
|
||
dualDyeable: '可双区染色',
|
||
patternEditable: '可改花纹',
|
||
noRecipe: '无材料单',
|
||
recipeInfo: '材料单信息',
|
||
relatedRecipes: '相关材料单',
|
||
relatedHabitats: '相关栖息地',
|
||
pokemonDrops: 'Pokemon 掉落',
|
||
createRecipe: '创建材料单',
|
||
searchCategory: '搜索分类',
|
||
searchUsage: '搜索用途',
|
||
searchMethods: '搜索入手方式',
|
||
searchTags: '搜索标签'
|
||
},
|
||
recipes: {
|
||
title: '材料单',
|
||
subtitle: '按分类、用途、标签查看材料单。',
|
||
detailKicker: 'Recipe Detail',
|
||
detailSubtitle: '材料单详情',
|
||
editKicker: 'Recipe Edit',
|
||
editSubtitle: '维护材料单结果物品、入手方式和需要材料。',
|
||
newTitle: '新增材料单',
|
||
editTitle: '编辑 {name}',
|
||
fallbackName: '材料单',
|
||
loadingList: '正在加载材料单列表',
|
||
loadingDetail: '正在加载材料单详情',
|
||
loadingEdit: '正在加载材料单编辑内容',
|
||
item: '物品',
|
||
materials: '需要材料',
|
||
addMaterial: '添加材料'
|
||
},
|
||
comingSoon: {
|
||
status: '正在开发中',
|
||
heading: '这个 Wiki 分区正在准备中。',
|
||
previewLabel: '分区预览',
|
||
sections: {
|
||
dish: {
|
||
kicker: 'Dish',
|
||
title: '料理',
|
||
subtitle: '未来会用于整理料理和食物相关发现。',
|
||
body: '料理页面会围绕清晰浏览、来源记录和材料关联来设计。',
|
||
preview: {
|
||
one: '料理记录会优先呈现名称、效果和发现方式。',
|
||
two: '需要时会把材料关系连接回物品和材料单。',
|
||
three: '页面会先保持浏览友好,后续再自然承接社区编辑内容。'
|
||
}
|
||
},
|
||
events: {
|
||
kicker: 'Events',
|
||
title: '活动',
|
||
subtitle: '季节活动和限时内容资料会在这里整理。',
|
||
body: '活动分区会在准备好后集中展示时间、奖励和参与信息。',
|
||
preview: {
|
||
one: '活动卡片会让日期和开放时间更容易浏览。',
|
||
two: '奖励与关联物品会靠近活动摘要展示。',
|
||
three: '活动结束后,历史记录也会保持可读。'
|
||
}
|
||
},
|
||
actions: {
|
||
kicker: 'Actions',
|
||
title: '动作',
|
||
subtitle: '挥手、跳舞等游戏内快捷动作会记录在这里。',
|
||
body: '动作分区会作为游戏内表情、社交动作和快捷动作的快速参考。',
|
||
preview: {
|
||
one: '每个动作会用面向玩家的语言说明动作或快捷方式。',
|
||
two: '常见内容包括挥手、跳舞和其他社交动作。',
|
||
three: '后续可在数据模型准备好后补充解锁或使用条件。'
|
||
}
|
||
},
|
||
dreamIsland: {
|
||
kicker: 'Dream Island',
|
||
title: 'Dream Island',
|
||
subtitle: 'Dream Island 相关资料正在整理。',
|
||
body: '这个区域未来会用更像目的地资料页的方式展示岛屿信息。',
|
||
preview: {
|
||
one: '岛屿记录会优先整理地点、开放状态和重要发现。',
|
||
two: '可关联的 Pokemon、物品或活动会从页面中连接出来。',
|
||
three: '目前先保持公开浏览入口,不额外增加管理流程。'
|
||
}
|
||
},
|
||
clothes: {
|
||
kicker: 'Clothes',
|
||
title: '服装',
|
||
subtitle: '外观和服装资料正在准备。',
|
||
body: '服装页面会用于对比外观、入手方式和自定义信息。',
|
||
preview: {
|
||
one: '服装条目会优先整理展示名称和视觉分类。',
|
||
two: '入手方式与自定义信息会在资料可用后接入。',
|
||
three: '页面会保持服装资料清晰,不和普通物品列表混在一起。'
|
||
}
|
||
}
|
||
}
|
||
},
|
||
checklist: {
|
||
title: '每日清单',
|
||
subtitle: '查看每天可以完成的事项。',
|
||
sectionTitle: '每日做什么',
|
||
empty: '暂无每日清单',
|
||
loading: '正在加载每日清单',
|
||
task: 'Task',
|
||
newTask: '新增 Task',
|
||
editTask: '编辑 Task'
|
||
},
|
||
life: {
|
||
title: 'Life',
|
||
subtitle: '分享喜欢的心得、想法和社区发现。',
|
||
kicker: '社区动态',
|
||
composerTitle: '分享动态',
|
||
composerPrompt: '想分享什么?',
|
||
bodyLabel: '动态内容',
|
||
bodyPlaceholder: '分享一段想法、心得或发现……',
|
||
newPost: 'New Post',
|
||
tags: '标签',
|
||
allTags: '全部',
|
||
tagPlaceholder: '选择标签',
|
||
searchTags: '搜索标签',
|
||
search: '搜索动态',
|
||
searchPlaceholder: '搜索动态内容……',
|
||
clearSearch: '清除搜索',
|
||
searchEmpty: '没有匹配的动态',
|
||
searchEmptyHint: '换个关键词或清除搜索。',
|
||
comments: '评论',
|
||
commentsCount: '{count} 条评论',
|
||
comment: '评论',
|
||
hideComments: '收起评论',
|
||
react: '点赞',
|
||
reactions: '互动',
|
||
reactionsCount: '{count} 次互动',
|
||
reactionCountLabel: '{reaction}:{count}',
|
||
reactionLike: '喜欢',
|
||
reactionHelpful: '有帮助',
|
||
reactionFun: '有趣',
|
||
reactionThanks: '感谢',
|
||
chooseReaction: '选择互动',
|
||
reactionMenu: '互动菜单',
|
||
removeReaction: '取消互动',
|
||
reactionFailed: '互动失败',
|
||
commentPlaceholder: '写下评论……',
|
||
commentReplyPlaceholder: '写下回复……',
|
||
postComment: '发表评论',
|
||
postingComment: '评论中',
|
||
reply: '回复',
|
||
postReply: '发布回复',
|
||
postingReply: '回复中',
|
||
cancelReply: '取消回复',
|
||
noComments: '暂无评论',
|
||
deleteComment: '删除评论',
|
||
deleteCommentConfirm: '确认删除这条评论?',
|
||
commentDeleted: '评论已删除',
|
||
commentRequired: '请输入评论内容。',
|
||
commentFailed: '评论失败',
|
||
replyFailed: '回复失败',
|
||
deleteCommentFailed: '删除评论失败',
|
||
publish: '发布',
|
||
publishing: '发布中',
|
||
update: '更新',
|
||
updating: '更新中',
|
||
cancelEdit: '取消编辑',
|
||
empty: '暂无动态',
|
||
emptyHint: '已验证成员可以发布第一条 Life 动态。',
|
||
loading: '正在加载 Life 动态',
|
||
retryFeed: '重试加载',
|
||
loginPrompt: '使用已验证邮箱登录后即可发布。',
|
||
verifyPrompt: '完成邮箱验证后即可发布。',
|
||
editPost: '编辑动态',
|
||
deletePost: '删除动态',
|
||
saveEdit: '保存编辑',
|
||
postFailed: '发布失败',
|
||
saveFailed: '保存失败',
|
||
deleteFailed: '删除失败',
|
||
bodyRequired: '请输入动态内容。',
|
||
byUnknown: '社区成员',
|
||
edited: '已编辑',
|
||
deleteConfirm: '确认删除这条动态?',
|
||
charactersLeft: '还可以输入 {count} 个字符'
|
||
},
|
||
admin: {
|
||
title: '管理',
|
||
subtitle: '维护系统配置,查看并删除 Wiki 数据记录。',
|
||
modules: '管理模块',
|
||
loading: '正在加载管理列表',
|
||
config: '系统配置',
|
||
configType: '系统配置类型',
|
||
checklist: 'CheckList',
|
||
pokemonList: 'Pokemon 列表',
|
||
itemList: '物品列表',
|
||
recipeList: '材料单列表',
|
||
habitatList: '栖息地列表',
|
||
languages: '语言',
|
||
newConfig: '新增{name}',
|
||
editConfig: '编辑{name}',
|
||
hasItemDrop: '有掉落物',
|
||
dragSort: '拖曳排序:{name}',
|
||
dragSortTitle: '拖曳排序',
|
||
languageCode: 'Code',
|
||
languageName: '语言名称',
|
||
enabled: '启用',
|
||
defaultLanguage: '默认语言',
|
||
sortOrder: '排序',
|
||
newLanguage: '新增语言',
|
||
editLanguage: '编辑语言',
|
||
wordings: '系统文案',
|
||
wordingLocale: '语言',
|
||
wordingModule: '模块',
|
||
wordingSurface: '端',
|
||
wordingMissingOnly: '只看缺失',
|
||
wordingKey: 'Key',
|
||
wordingValue: '文案',
|
||
defaultValue: '默认文案',
|
||
placeholders: '占位符',
|
||
missingTranslation: '缺少翻译',
|
||
allModules: '全部模块',
|
||
allSurfaces: '全部端',
|
||
surfaceFrontend: '前端',
|
||
surfaceBackend: '后端',
|
||
surfaceEmail: '邮件',
|
||
editWording: '编辑文案'
|
||
}
|
||
},
|
||
config: {
|
||
pokemonTypes: 'Pokemon 属性',
|
||
skills: '特长',
|
||
environments: '喜欢的环境',
|
||
favoriteThings: '喜欢的东西 / 标签',
|
||
itemCategories: '物品分类',
|
||
itemUsages: '物品用途',
|
||
acquisitionMethods: '入手方式',
|
||
maps: '地图',
|
||
lifeTags: 'Life 标签'
|
||
},
|
||
appearance: {
|
||
time: '时段',
|
||
weather: '天气',
|
||
rarity: '稀有度',
|
||
map: '地图',
|
||
maps: '出现地图',
|
||
morning: '早晨',
|
||
noon: '中午',
|
||
evening: '傍晚',
|
||
night: '晚上',
|
||
sunny: '晴天',
|
||
cloudy: '阴天',
|
||
rainy: '雨天',
|
||
stars: '{count} 星'
|
||
},
|
||
history: {
|
||
title: '贡献记录',
|
||
createdBy: '由谁创建',
|
||
lastEdited: '最后编辑',
|
||
editHistory: '编辑历史',
|
||
before: '修改前',
|
||
after: '修改后',
|
||
author: '作者',
|
||
time: '时间',
|
||
action: '动作',
|
||
create: '创建',
|
||
update: '编辑',
|
||
delete: '删除',
|
||
empty: '暂无编辑历史'
|
||
},
|
||
discussion: {
|
||
title: '讨论',
|
||
count: '{count} 条评论',
|
||
comment: '评论',
|
||
commentPlaceholder: '写下评论……',
|
||
replyPlaceholder: '写下回复……',
|
||
postComment: '发表评论',
|
||
postingComment: '评论中',
|
||
reply: '回复',
|
||
postReply: '发布回复',
|
||
postingReply: '回复中',
|
||
cancelReply: '取消回复',
|
||
deleteComment: '删除评论',
|
||
deleteConfirm: '确认删除这条评论?',
|
||
deletedComment: '评论已删除',
|
||
commentRequired: '请输入评论内容。',
|
||
commentFailed: '评论失败',
|
||
replyFailed: '回复失败',
|
||
deleteFailed: '删除失败',
|
||
loading: '正在加载讨论',
|
||
empty: '暂无讨论',
|
||
emptyHint: '现在发起新的讨论。',
|
||
loginPrompt: '使用已验证邮箱登录后即可评论。',
|
||
verifyPrompt: '完成邮箱验证后即可评论。',
|
||
byUnknown: '社区成员',
|
||
charactersLeft: '还可以输入 {count} 个字符'
|
||
},
|
||
server: {
|
||
errors: {
|
||
foreignKey: '引用的数据不存在,或当前记录正在被使用',
|
||
duplicate: '同名或相同 ID 的记录已存在',
|
||
invalidField: '字段值不合法',
|
||
serverError: '服务器错误',
|
||
loginRequired: '请先登录',
|
||
verifyEmailFirst: '请先完成邮箱验证',
|
||
notFound: '未找到记录'
|
||
},
|
||
auth: {
|
||
emailRequired: '请输入邮箱',
|
||
invalidEmail: '邮箱格式不正确',
|
||
displayNameRequired: '请输入显示名',
|
||
displayNameLength: '显示名长度需为 1 到 40 个字符',
|
||
passwordLength: '密码至少需要 8 个字符',
|
||
invalidToken: '验证链接无效或已过期',
|
||
emailAlreadyRegistered: '该邮箱已注册',
|
||
checkVerificationEmail: '请查收验证邮件',
|
||
emailVerified: '邮箱已验证',
|
||
invalidCredentials: '邮箱或密码不正确',
|
||
verifyEmailFirst: '请先完成邮箱验证'
|
||
},
|
||
validation: {
|
||
nameRequired: '请输入名称',
|
||
recordMissing: '记录不存在',
|
||
languageCodeInvalid: '语言 Code 不合法',
|
||
languageNameRequired: '请输入语言名称',
|
||
defaultLanguageMustBeEnglish: '默认语言必须是 English',
|
||
defaultLanguageMustBeEnabled: '默认语言必须启用',
|
||
languageNotFound: '语言不存在',
|
||
defaultLanguageRequired: '必须保留一个默认语言',
|
||
defaultLanguageCannotBeDeleted: '默认语言不能删除',
|
||
selectLanguage: '请选择语言',
|
||
languageDoesNotExist: '语言不存在',
|
||
pokemonIdentifierRequired: '请输入 Pokemon 标识',
|
||
pokemonTypeDataUnavailable: 'Pokemon 属性数据不可用',
|
||
pokemonDataNotFound: '未找到 Pokemon 数据',
|
||
pokemonImagePathInvalid: 'Pokemon 图片路径不合法',
|
||
taskRequired: '请输入任务',
|
||
selectTask: '请选择任务',
|
||
taskDoesNotExist: '任务不存在',
|
||
postRequired: '请输入动态内容',
|
||
postTooLong: '动态内容过长',
|
||
commentRequired: '请输入评论内容',
|
||
commentTooLong: '评论内容过长',
|
||
reactionInvalid: '互动类型不合法',
|
||
cursorInvalid: '分页位置不合法',
|
||
tagInvalid: '标签不合法',
|
||
entityTypeInvalid: '实体类型不合法',
|
||
recordInvalid: '记录不合法',
|
||
commentInvalid: '评论不合法',
|
||
selectRecord: '请选择记录',
|
||
typeMin: '请至少选择 1 个属性',
|
||
typeMax: '最多选择 2 个属性',
|
||
skillMax: '最多选择 2 个特长',
|
||
favoriteMax: '最多选择 6 个喜欢的东西',
|
||
dropItemSelectedSkill: '掉落物必须关联到已选择的特长',
|
||
pokemonIdRequired: '请输入 Pokemon ID',
|
||
pokemonNameRequired: '请输入 Pokemon 名称',
|
||
heightNonNegative: '身高必须是不小于 0 的数字',
|
||
weightNonNegative: '体重必须是不小于 0 的数字',
|
||
environmentRequired: '请选择喜欢的环境',
|
||
skillNoDrop: '这个特长不能设置掉落物',
|
||
habitatNameRequired: '请输入栖息地名称',
|
||
usageRequired: '请选择用途',
|
||
itemNameRequired: '请输入物品名称',
|
||
categoryRequired: '请选择分类',
|
||
recipeFreeWithRecipe: '已有材料单的物品不能标记为无材料单',
|
||
itemRequired: '请选择物品',
|
||
recipeFreeItem: '这个物品已标记为无材料单',
|
||
statNonNegative: '六维必须是不小于 0 的整数',
|
||
pokemonDataFileEmpty: 'Pokemon 数据文件为空',
|
||
pokemonDataFileUnavailable: 'Pokemon 数据文件不可用'
|
||
},
|
||
wordings: {
|
||
keyNotFound: '系统文案 Key 不存在',
|
||
localeRequired: '请选择语言',
|
||
valueRequired: '请输入文案',
|
||
placeholderMismatch: '占位符必须与默认文案一致'
|
||
}
|
||
},
|
||
email: {
|
||
auth: {
|
||
verificationSubject: '验证你的 Pokopia Wiki 邮箱',
|
||
verificationHtml: '<p>请点击下面的链接完成邮箱验证:</p><p><a href="{url}">验证邮箱</a></p><p>链接将在 {hours} 小时后失效。</p>',
|
||
verificationText: '请打开以下链接完成 Pokopia Wiki 邮箱验证:{url}\n链接将在 {hours} 小时后失效。'
|
||
}
|
||
}
|
||
}
|
||
} as const;
|
||
|
||
export type SystemWordingSurface = 'frontend' | 'backend' | 'email';
|
||
|
||
export type SystemWordingCatalogEntry = {
|
||
key: string;
|
||
module: string;
|
||
surface: SystemWordingSurface;
|
||
description: string;
|
||
placeholders: string[];
|
||
values: Record<string, string>;
|
||
};
|
||
|
||
const placeholderPattern = /\{([A-Za-z0-9_]+)\}/g;
|
||
|
||
function isMessageTree(value: SystemWordingLeaf | SystemWordingTree): value is SystemWordingTree {
|
||
return typeof value === 'object' && value !== null;
|
||
}
|
||
|
||
function collectPlaceholders(value: string): string[] {
|
||
return [...new Set([...value.matchAll(placeholderPattern)].map((match) => match[1]))].sort();
|
||
}
|
||
|
||
function mergePlaceholders(values: Record<string, string>): string[] {
|
||
return [...new Set(Object.values(values).flatMap(collectPlaceholders))].sort();
|
||
}
|
||
|
||
function moduleForKey(key: string): string {
|
||
const parts = key.split('.');
|
||
if ((parts[0] === 'pages' || parts[0] === 'server' || parts[0] === 'email') && parts[1]) {
|
||
return `${parts[0]}.${parts[1]}`;
|
||
}
|
||
|
||
return parts[0] ?? 'system';
|
||
}
|
||
|
||
function surfaceForKey(key: string): SystemWordingSurface {
|
||
if (key.startsWith('email.')) return 'email';
|
||
if (key.startsWith('server.')) return 'backend';
|
||
return 'frontend';
|
||
}
|
||
|
||
function flattenMessages(tree: SystemWordingTree, prefix = ''): Record<string, string> {
|
||
const entries: Record<string, string> = {};
|
||
|
||
for (const [key, value] of Object.entries(tree)) {
|
||
const nextKey = prefix ? `${prefix}.${key}` : key;
|
||
if (isMessageTree(value)) {
|
||
Object.assign(entries, flattenMessages(value, nextKey));
|
||
} else {
|
||
entries[nextKey] = value;
|
||
}
|
||
}
|
||
|
||
return entries;
|
||
}
|
||
|
||
export function flattenSystemWordingMessages(messages: SystemWordingMessages = systemWordingMessages): Record<string, Record<string, string>> {
|
||
return Object.fromEntries(Object.entries(messages).map(([locale, tree]) => [locale, flattenMessages(tree)]));
|
||
}
|
||
|
||
export function systemWordingCatalogEntries(messages: SystemWordingMessages = systemWordingMessages): SystemWordingCatalogEntry[] {
|
||
const flattened = flattenSystemWordingMessages(messages);
|
||
const keys = Object.keys(flattened[defaultLocale] ?? {}).sort();
|
||
|
||
return keys.map((key) => {
|
||
const values = Object.fromEntries(
|
||
Object.entries(flattened)
|
||
.map(([locale, localeMessages]) => [locale, localeMessages[key]])
|
||
.filter((entry): entry is [string, string] => typeof entry[1] === 'string' && entry[1].trim() !== '')
|
||
);
|
||
|
||
return {
|
||
key,
|
||
module: moduleForKey(key),
|
||
surface: surfaceForKey(key),
|
||
description: '',
|
||
placeholders: mergePlaceholders(values),
|
||
values
|
||
};
|
||
});
|
||
}
|
||
|
||
export function systemWordingFallback(key: string, locale = defaultLocale): string | undefined {
|
||
const flattened = flattenSystemWordingMessages();
|
||
return flattened[locale]?.[key] ?? flattened[defaultLocale]?.[key];
|
||
}
|