i18n Keys
Translation key conventions and the reference catalog of plugin i18n keys
Plugins render every user-visible string through useTranslate() from @btst/stack/context:
const t = useTranslate();
t("blog.forms.titleLabel", "Title");
t("blog.list.tagPageTitle", "{{tag}} Posts", { tag: tag.name });Without an i18n provider on StackProvider, t() returns the English default (with {{param}} interpolation), so apps with no provider behave exactly as before. With a provider, your translate(key, defaultValue, params) implementation decides what to render.
Key conventions
- Keys are namespaced
<plugin>.<area>.<name>, e.g.blog.forms.titleLabel. Areas group related UI (common,list,card,post,forms,search, ...). - The second argument is always a string literal English default, so the key catalog can be regenerated mechanically:
pnpm exec tsx packages/stack/scripts/extract-i18n-keys.ts- Parameters use
{{param}}interpolation in the default value. - Plugins that support a legacy per-plugin
localizationoverride object resolve it with higher precedence thant():
localization?.BLOG_FORMS_TITLE_LABEL ?? t("blog.forms.titleLabel", "Title");An app-provided localization override wins byte-for-byte; otherwise the string goes through the i18n provider.
Blog
| Key | Default |
|---|---|
blog.card.draftBadge | Draft |
blog.common.genericErrorMessage | An unexpected error occurred. |
blog.common.genericErrorTitle | Something went wrong |
blog.common.pageNotFoundDescription | The page you are looking for does not exist. |
blog.common.pageNotFoundTitle | Not Found |
blog.common.tagsShowAll | Show all tags |
blog.common.tagsShowLess | Show fewer tags |
blog.forms.cancelButton | Cancel |
blog.forms.contentLabel | Content |
blog.forms.deleteButton | Delete Post |
blog.forms.deleteDialogCancel | Cancel |
blog.forms.deleteDialogConfirm | Delete |
blog.forms.deleteDialogDescription | Are you sure you want to delete this post? This action cannot be undone. |
blog.forms.deleteDialogTitle | Delete Post |
blog.forms.deletePending | Deleting... |
blog.forms.editorPlaceholder | Write something... |
blog.forms.excerptLabel | Excerpt |
blog.forms.excerptPlaceholder | Brief summary of your post... |
blog.forms.featuredImageErrorNotImage | Please select an image file |
blog.forms.featuredImageErrorTooLarge | Image size must be less than 4MB |
blog.forms.featuredImageInputPlaceholder | Image URL or upload below... |
blog.forms.featuredImageLabel | Image |
blog.forms.featuredImagePreviewAlt | Featured image preview |
blog.forms.featuredImageRequiredAsterisk | * |
blog.forms.featuredImageToastFailure | Failed to upload image |
blog.forms.featuredImageToastSuccess | Image uploaded successfully |
blog.forms.featuredImageUploadButton | Upload |
blog.forms.featuredImageUploadingButton | Uploading... |
blog.forms.featuredImageUploadingText | Uploading image... |
blog.forms.publishedDescription | Toggle to publish immediately |
blog.forms.publishedLabel | Published |
blog.forms.requiredAsterisk | * |
blog.forms.slugLabel | Slug |
blog.forms.slugPlaceholder | url-friendly-slug |
blog.forms.submitCreateIdle | Create Post |
blog.forms.submitCreatePending | Creating... |
blog.forms.submitUpdateIdle | Update Post |
blog.forms.submitUpdatePending | Updating... |
blog.forms.tagsLabel | Tags |
blog.forms.tagsPlaceholder | Enter your post tags... |
blog.forms.tagsSearchPlaceholder | Search or create tags... |
blog.forms.titleLabel | Title |
blog.forms.titlePlaceholder | Enter your post title... |
blog.forms.toastCreateSuccess | Post created successfully |
blog.forms.toastDeleteFailure | Failed to delete post |
blog.forms.toastDeleteSuccess | Post deleted successfully |
blog.forms.toastUpdateSuccess | Post updated successfully |
blog.forms.validation.contentRequired | Content is required |
blog.forms.validation.excerptRequired | Excerpt is required |
blog.forms.validation.slugRequired | Slug is required |
blog.forms.validation.titleRequired | Title is required |
blog.list.draftsTitle | Draft Posts |
blog.list.empty | There are no posts here yet. |
blog.list.loadMore | Load more posts |
blog.list.loadingMore | Loading more... |
blog.list.searchButton | Search Posts |
blog.list.searchEmpty | No blog posts found. |
blog.list.searchPlaceholder | Search Blog Posts... |
blog.list.tagNotFound | Tag not found |
blog.list.tagNotFoundDescription | The tag you are looking for does not exist. |
blog.list.tagPageDescription | Browse all posts with this tag |
blog.list.tagPageTitle | {{tag}} Posts |
blog.list.title | Blog Posts |
blog.post.addDescription | Create a new blog post. |
blog.post.addTitle | Add New Post |
blog.post.editDescription | Update your blog post. |
blog.post.editTitle | Edit Post |
blog.post.keepReading | Keep Reading |
blog.post.next | Next |
blog.post.onThisPage | In This Post |
blog.post.previous | Previous |
blog.post.viewAll | View all |
blog.search.button | Search |
blog.search.empty | No results found. |
blog.search.placeholder | Type to search... |
blog.search.searching | Searching... |
AI Chat
Legacy AiChatLocalization values override these catalog entries when both are configured.
| Key | Default |
|---|---|
aiChat.a11y.assistantMessage | AI response |
aiChat.a11y.clearChat | Clear chat |
aiChat.a11y.closeChat | Close chat |
aiChat.a11y.closeSidebar | Close sidebar |
aiChat.a11y.conversationActions | Conversation actions |
aiChat.a11y.openChat | Open chat |
aiChat.a11y.openMenu | Open menu |
aiChat.a11y.openSidebar | Open sidebar |
aiChat.a11y.title | AI Chat |
aiChat.a11y.userMessage | Your message |
aiChat.chat.emptyState | Start a conversation... |
aiChat.chat.error | Something went wrong. Please try again. |
aiChat.chat.loading | Thinking... |
aiChat.chat.placeholder | Type a message... |
aiChat.chat.send | Send |
aiChat.conversation.delete | Delete |
aiChat.conversation.deleteCancel | Cancel |
aiChat.conversation.deleteConfirmButton | Delete |
aiChat.conversation.deleteConfirmDescription | Are you sure you want to delete this conversation? This action cannot be undone. |
aiChat.conversation.deleteConfirmTitle | Delete conversation |
aiChat.conversation.rename | Rename |
aiChat.conversation.renameCancel | Cancel |
aiChat.conversation.renameDescription | Enter a new title for this conversation. |
aiChat.conversation.renamePlaceholder | Enter conversation name |
aiChat.conversation.renameSave | Save |
aiChat.conversation.titleRequired | Title is required |
aiChat.errors.genericMessage | An error occurred while loading the chat. Please try again. |
aiChat.errors.genericTitle | Something went wrong |
aiChat.errors.missingConversation | Conversation is required |
aiChat.errors.notFoundDescription | The conversation you're looking for doesn't exist or has been deleted. |
aiChat.errors.notFoundTitle | Chat not found |
aiChat.files.attach | Attach file |
aiChat.files.fallbackName | File |
aiChat.files.remove | Remove file |
aiChat.files.tooLarge | File must be less than 10MB |
aiChat.files.uploadFailure | Failed to attach file |
aiChat.files.uploadSuccess | File attached |
aiChat.images.attachedAlt | Attached image {{count}} |
aiChat.images.generatedAlt | Image {{count}} |
aiChat.messages.cancel | Cancel |
aiChat.messages.copied | Copied! |
aiChat.messages.copy | Copy message |
aiChat.messages.edit | Edit message |
aiChat.messages.retry | Retry |
aiChat.messages.save | Save |
aiChat.sidebar.empty | No conversations yet |
aiChat.sidebar.newChat | New chat |
aiChat.time.daysAgo | {{count}} days ago |
aiChat.time.hoursAgo | {{count}} hours ago |
aiChat.time.justNow | Just now |
aiChat.time.minutesAgo | {{count}} minutes ago |
aiChat.time.yesterday | Yesterday |
aiChat.toasts.deleteFailure | Failed to delete conversation |
aiChat.toasts.deleteSuccess | Conversation deleted |
aiChat.toasts.renameFailure | Failed to rename conversation |
aiChat.toasts.renameSuccess | Conversation renamed |
aiChat.tools.executionFailed | Tool execution failed |
aiChat.tools.handlerMissing | No client-side handler registered for tool "{{toolName}}". The page context may have changed while the response was streaming. |
aiChat.tools.id | ID: {{id}} |
aiChat.tools.input | Input |
aiChat.tools.output | Output |
aiChat.tools.status.complete | Complete |
aiChat.tools.status.error | Error |
aiChat.tools.status.executing | Executing... |
aiChat.tools.status.pending | Pending |
aiChat.tools.status.running | Running... |
Other plugins adopt the same convention as their phase-2 sweeps land.