Skip to content

Commit eeeccb2

Browse files
authored
Merge pull request #142 from better-stack-ai/feat/blog-phase2-sweep
feat(blog): phase-2 core-primitives adoption sweep
2 parents 9456691 + 49d9753 commit eeeccb2

33 files changed

Lines changed: 1255 additions & 342 deletions

‎docs/content/docs/i18n.mdx‎

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
---
2+
title: i18n Keys
3+
description: Translation key conventions and the reference catalog of plugin i18n keys
4+
---
5+
6+
Plugins render every user-visible string through `useTranslate()` from `@btst/stack/context`:
7+
8+
```tsx
9+
const t = useTranslate();
10+
11+
t("blog.forms.titleLabel", "Title");
12+
t("blog.list.tagPageTitle", "{{tag}} Posts", { tag: tag.name });
13+
```
14+
15+
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.
16+
17+
## Key conventions
18+
19+
- Keys are namespaced `<plugin>.<area>.<name>`, e.g. `blog.forms.titleLabel`. Areas group related UI (`common`, `list`, `card`, `post`, `forms`, `search`, ...).
20+
- The second argument is always a **string literal** English default, so the key catalog can be regenerated mechanically:
21+
22+
```bash
23+
pnpm exec tsx packages/stack/scripts/extract-i18n-keys.ts
24+
```
25+
26+
- Parameters use `{{param}}` interpolation in the default value.
27+
- Plugins that support a legacy per-plugin `localization` override object resolve it with higher precedence than `t()`:
28+
29+
```tsx
30+
localization?.BLOG_FORMS_TITLE_LABEL ?? t("blog.forms.titleLabel", "Title");
31+
```
32+
33+
An app-provided `localization` override wins byte-for-byte; otherwise the string goes through the i18n provider.
34+
35+
## Blog
36+
37+
| Key | Default |
38+
| --- | --- |
39+
| `blog.card.draftBadge` | Draft |
40+
| `blog.common.genericErrorMessage` | An unexpected error occurred. |
41+
| `blog.common.genericErrorTitle` | Something went wrong |
42+
| `blog.common.pageNotFoundDescription` | The page you are looking for does not exist. |
43+
| `blog.common.pageNotFoundTitle` | Not Found |
44+
| `blog.common.tagsShowAll` | Show all tags |
45+
| `blog.common.tagsShowLess` | Show fewer tags |
46+
| `blog.forms.cancelButton` | Cancel |
47+
| `blog.forms.contentLabel` | Content |
48+
| `blog.forms.deleteButton` | Delete Post |
49+
| `blog.forms.deleteDialogCancel` | Cancel |
50+
| `blog.forms.deleteDialogConfirm` | Delete |
51+
| `blog.forms.deleteDialogDescription` | Are you sure you want to delete this post? This action cannot be undone. |
52+
| `blog.forms.deleteDialogTitle` | Delete Post |
53+
| `blog.forms.deletePending` | Deleting... |
54+
| `blog.forms.editorPlaceholder` | Write something... |
55+
| `blog.forms.excerptLabel` | Excerpt |
56+
| `blog.forms.excerptPlaceholder` | Brief summary of your post... |
57+
| `blog.forms.featuredImageErrorNotImage` | Please select an image file |
58+
| `blog.forms.featuredImageErrorTooLarge` | Image size must be less than 4MB |
59+
| `blog.forms.featuredImageInputPlaceholder` | Image URL or upload below... |
60+
| `blog.forms.featuredImageLabel` | Image |
61+
| `blog.forms.featuredImagePreviewAlt` | Featured image preview |
62+
| `blog.forms.featuredImageRequiredAsterisk` | &nbsp;* |
63+
| `blog.forms.featuredImageToastFailure` | Failed to upload image |
64+
| `blog.forms.featuredImageToastSuccess` | Image uploaded successfully |
65+
| `blog.forms.featuredImageUploadButton` | Upload |
66+
| `blog.forms.featuredImageUploadingButton` | Uploading... |
67+
| `blog.forms.featuredImageUploadingText` | Uploading image... |
68+
| `blog.forms.publishedDescription` | Toggle to publish immediately |
69+
| `blog.forms.publishedLabel` | Published |
70+
| `blog.forms.requiredAsterisk` | &nbsp;* |
71+
| `blog.forms.slugLabel` | Slug |
72+
| `blog.forms.slugPlaceholder` | url-friendly-slug |
73+
| `blog.forms.submitCreateIdle` | Create Post |
74+
| `blog.forms.submitCreatePending` | Creating... |
75+
| `blog.forms.submitUpdateIdle` | Update Post |
76+
| `blog.forms.submitUpdatePending` | Updating... |
77+
| `blog.forms.tagsLabel` | Tags |
78+
| `blog.forms.tagsPlaceholder` | Enter your post tags... |
79+
| `blog.forms.tagsSearchPlaceholder` | Search or create tags... |
80+
| `blog.forms.titleLabel` | Title |
81+
| `blog.forms.titlePlaceholder` | Enter your post title... |
82+
| `blog.forms.toastCreateSuccess` | Post created successfully |
83+
| `blog.forms.toastDeleteFailure` | Failed to delete post |
84+
| `blog.forms.toastDeleteSuccess` | Post deleted successfully |
85+
| `blog.forms.toastUpdateSuccess` | Post updated successfully |
86+
| `blog.forms.validation.contentRequired` | Content is required |
87+
| `blog.forms.validation.excerptRequired` | Excerpt is required |
88+
| `blog.forms.validation.slugRequired` | Slug is required |
89+
| `blog.forms.validation.titleRequired` | Title is required |
90+
| `blog.list.draftsTitle` | Draft Posts |
91+
| `blog.list.empty` | There are no posts here yet. |
92+
| `blog.list.loadMore` | Load more posts |
93+
| `blog.list.loadingMore` | Loading more... |
94+
| `blog.list.searchButton` | Search Posts |
95+
| `blog.list.searchEmpty` | No blog posts found. |
96+
| `blog.list.searchPlaceholder` | Search Blog Posts... |
97+
| `blog.list.tagNotFound` | Tag not found |
98+
| `blog.list.tagNotFoundDescription` | The tag you are looking for does not exist. |
99+
| `blog.list.tagPageDescription` | Browse all posts with this tag |
100+
| `blog.list.tagPageTitle` | \{\{tag\}\} Posts |
101+
| `blog.list.title` | Blog Posts |
102+
| `blog.post.addDescription` | Create a new blog post. |
103+
| `blog.post.addTitle` | Add New Post |
104+
| `blog.post.editDescription` | Update your blog post. |
105+
| `blog.post.editTitle` | Edit Post |
106+
| `blog.post.keepReading` | Keep Reading |
107+
| `blog.post.next` | Next |
108+
| `blog.post.onThisPage` | In This Post |
109+
| `blog.post.previous` | Previous |
110+
| `blog.post.viewAll` | View all |
111+
| `blog.search.button` | Search |
112+
| `blog.search.empty` | No results found. |
113+
| `blog.search.placeholder` | Type to search... |
114+
| `blog.search.searching` | Searching... |
115+
116+
Other plugins adopt the same convention as their phase-2 sweeps land.

‎docs/content/docs/meta.json‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
"databases/adapters",
2828
"---[BookOpenCheck]Concepts---",
2929
"auth",
30+
"i18n",
3031
"cli",
3132
"api-reference",
3233
"standalone-components",

‎e2e/tests/smoke.ui-builder.spec.ts‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -128,8 +128,11 @@ test.describe("UI Builder Plugin - Admin Pages", () => {
128128
timeout: 10000,
129129
});
130130

131-
// Wait for first toast to disappear before proceeding
132-
await expect(page.locator("text=/saved/i")).not.toBeVisible({
131+
// Wait for the toast(s) to disappear before proceeding. Use .first()
132+
// like the visibility check above: the UI Builder embeds its own
133+
// sonner Toaster, so the same toast can render twice (strict mode).
134+
// All copies share one store and dismiss together.
135+
await expect(page.locator("text=/saved/i").first()).not.toBeVisible({
133136
timeout: 10000,
134137
});
135138

0 commit comments

Comments
 (0)