BTST

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 localization override object resolve it with higher precedence than t():
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

KeyDefault
blog.card.draftBadgeDraft
blog.common.genericErrorMessageAn unexpected error occurred.
blog.common.genericErrorTitleSomething went wrong
blog.common.pageNotFoundDescriptionThe page you are looking for does not exist.
blog.common.pageNotFoundTitleNot Found
blog.common.tagsShowAllShow all tags
blog.common.tagsShowLessShow fewer tags
blog.forms.cancelButtonCancel
blog.forms.contentLabelContent
blog.forms.deleteButtonDelete Post
blog.forms.deleteDialogCancelCancel
blog.forms.deleteDialogConfirmDelete
blog.forms.deleteDialogDescriptionAre you sure you want to delete this post? This action cannot be undone.
blog.forms.deleteDialogTitleDelete Post
blog.forms.deletePendingDeleting...
blog.forms.editorPlaceholderWrite something...
blog.forms.excerptLabelExcerpt
blog.forms.excerptPlaceholderBrief summary of your post...
blog.forms.featuredImageErrorNotImagePlease select an image file
blog.forms.featuredImageErrorTooLargeImage size must be less than 4MB
blog.forms.featuredImageInputPlaceholderImage URL or upload below...
blog.forms.featuredImageLabelImage
blog.forms.featuredImagePreviewAltFeatured image preview
blog.forms.featuredImageRequiredAsterisk *
blog.forms.featuredImageToastFailureFailed to upload image
blog.forms.featuredImageToastSuccessImage uploaded successfully
blog.forms.featuredImageUploadButtonUpload
blog.forms.featuredImageUploadingButtonUploading...
blog.forms.featuredImageUploadingTextUploading image...
blog.forms.publishedDescriptionToggle to publish immediately
blog.forms.publishedLabelPublished
blog.forms.requiredAsterisk *
blog.forms.slugLabelSlug
blog.forms.slugPlaceholderurl-friendly-slug
blog.forms.submitCreateIdleCreate Post
blog.forms.submitCreatePendingCreating...
blog.forms.submitUpdateIdleUpdate Post
blog.forms.submitUpdatePendingUpdating...
blog.forms.tagsLabelTags
blog.forms.tagsPlaceholderEnter your post tags...
blog.forms.tagsSearchPlaceholderSearch or create tags...
blog.forms.titleLabelTitle
blog.forms.titlePlaceholderEnter your post title...
blog.forms.toastCreateSuccessPost created successfully
blog.forms.toastDeleteFailureFailed to delete post
blog.forms.toastDeleteSuccessPost deleted successfully
blog.forms.toastUpdateSuccessPost updated successfully
blog.forms.validation.contentRequiredContent is required
blog.forms.validation.excerptRequiredExcerpt is required
blog.forms.validation.slugRequiredSlug is required
blog.forms.validation.titleRequiredTitle is required
blog.list.draftsTitleDraft Posts
blog.list.emptyThere are no posts here yet.
blog.list.loadMoreLoad more posts
blog.list.loadingMoreLoading more...
blog.list.searchButtonSearch Posts
blog.list.searchEmptyNo blog posts found.
blog.list.searchPlaceholderSearch Blog Posts...
blog.list.tagNotFoundTag not found
blog.list.tagNotFoundDescriptionThe tag you are looking for does not exist.
blog.list.tagPageDescriptionBrowse all posts with this tag
blog.list.tagPageTitle{{tag}} Posts
blog.list.titleBlog Posts
blog.post.addDescriptionCreate a new blog post.
blog.post.addTitleAdd New Post
blog.post.editDescriptionUpdate your blog post.
blog.post.editTitleEdit Post
blog.post.keepReadingKeep Reading
blog.post.nextNext
blog.post.onThisPageIn This Post
blog.post.previousPrevious
blog.post.viewAllView all
blog.search.buttonSearch
blog.search.emptyNo results found.
blog.search.placeholderType to search...
blog.search.searchingSearching...

AI Chat

Legacy AiChatLocalization values override these catalog entries when both are configured.

KeyDefault
aiChat.a11y.assistantMessageAI response
aiChat.a11y.clearChatClear chat
aiChat.a11y.closeChatClose chat
aiChat.a11y.closeSidebarClose sidebar
aiChat.a11y.conversationActionsConversation actions
aiChat.a11y.openChatOpen chat
aiChat.a11y.openMenuOpen menu
aiChat.a11y.openSidebarOpen sidebar
aiChat.a11y.titleAI Chat
aiChat.a11y.userMessageYour message
aiChat.chat.emptyStateStart a conversation...
aiChat.chat.errorSomething went wrong. Please try again.
aiChat.chat.loadingThinking...
aiChat.chat.placeholderType a message...
aiChat.chat.sendSend
aiChat.conversation.deleteDelete
aiChat.conversation.deleteCancelCancel
aiChat.conversation.deleteConfirmButtonDelete
aiChat.conversation.deleteConfirmDescriptionAre you sure you want to delete this conversation? This action cannot be undone.
aiChat.conversation.deleteConfirmTitleDelete conversation
aiChat.conversation.renameRename
aiChat.conversation.renameCancelCancel
aiChat.conversation.renameDescriptionEnter a new title for this conversation.
aiChat.conversation.renamePlaceholderEnter conversation name
aiChat.conversation.renameSaveSave
aiChat.conversation.titleRequiredTitle is required
aiChat.errors.genericMessageAn error occurred while loading the chat. Please try again.
aiChat.errors.genericTitleSomething went wrong
aiChat.errors.missingConversationConversation is required
aiChat.errors.notFoundDescriptionThe conversation you're looking for doesn't exist or has been deleted.
aiChat.errors.notFoundTitleChat not found
aiChat.files.attachAttach file
aiChat.files.fallbackNameFile
aiChat.files.removeRemove file
aiChat.files.tooLargeFile must be less than 10MB
aiChat.files.uploadFailureFailed to attach file
aiChat.files.uploadSuccessFile attached
aiChat.images.attachedAltAttached image {{count}}
aiChat.images.generatedAltImage {{count}}
aiChat.messages.cancelCancel
aiChat.messages.copiedCopied!
aiChat.messages.copyCopy message
aiChat.messages.editEdit message
aiChat.messages.retryRetry
aiChat.messages.saveSave
aiChat.sidebar.emptyNo conversations yet
aiChat.sidebar.newChatNew chat
aiChat.time.daysAgo{{count}} days ago
aiChat.time.hoursAgo{{count}} hours ago
aiChat.time.justNowJust now
aiChat.time.minutesAgo{{count}} minutes ago
aiChat.time.yesterdayYesterday
aiChat.toasts.deleteFailureFailed to delete conversation
aiChat.toasts.deleteSuccessConversation deleted
aiChat.toasts.renameFailureFailed to rename conversation
aiChat.toasts.renameSuccessConversation renamed
aiChat.tools.executionFailedTool execution failed
aiChat.tools.handlerMissingNo client-side handler registered for tool "{{toolName}}". The page context may have changed while the response was streaming.
aiChat.tools.idID: {{id}}
aiChat.tools.inputInput
aiChat.tools.outputOutput
aiChat.tools.status.completeComplete
aiChat.tools.status.errorError
aiChat.tools.status.executingExecuting...
aiChat.tools.status.pendingPending
aiChat.tools.status.runningRunning...

Other plugins adopt the same convention as their phase-2 sweeps land.