Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/api/src/locales/@vitnode/core/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@
"go_back": "Go back",
"select_option": "Select an option",
"select_options": "Select options",
"select_language": "Select language",
"go_to_prev_page": "Go to previous page",
"go_to_next_page": "Go to next page",
"errors": {
Expand Down
76 changes: 76 additions & 0 deletions apps/docs/content/docs/ui/auto-form.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,82 @@ const formSchema = z.object({
});
```

## Multi-language fields

Some values - a category name, a page title - need a translation per language.
`AutoFormInput` and `AutoFormEditor` accept a `multiLang` prop that collects the
value in every enabled language. The field renders a language **select** (shown
only when more than one language is enabled) that switches which language you
are editing; it does **not** change the app-wide locale, only the value inside
that field.

The value is stored as an array matching the
[`core_languages_words`](/docs/dev/working-with-users/roles) table - one
`{ languageCode, value }` entry per language. Declare the field with the
`multiLangValueSchema` helper so the Zod schema matches that array shape (its
`minLength` / `maxLength` apply to each language's value):

```ts
import { multiLangValueSchema } from "@vitnode/core/lib/helpers/multi-lang";

const formSchema = z.object({
name: multiLangValueSchema({ minLength: 1, maxLength: 255 }).min(1),
});
```

```tsx
<AutoForm
formSchema={formSchema}
fields={[
{
id: "name",
component: props => <AutoFormInput {...props} label="Name" multiLang />,
},
]}
onSubmit={values => {
// values.name => [{ languageCode: "en", value: "News" }, ...]
}}
/>
```

### Saving to the backend

The form only produces `{ languageCode, value }[]`. On the backend, persist it
with `saveLanguageWords`, which fills the remaining `core_languages_words`
columns - the `(pluginCode, tableName, variable, itemId)` tuple that identifies
the field - and replaces the existing rows in a single transaction:

```ts title="plugins/{plugin_name}/src/api/routes/create-category.route.ts"
import { buildRoute } from "@vitnode/core/api/lib/route";
import { saveLanguageWords } from "@vitnode/core/api/lib/save-language-words";

export const createCategoryRoute = buildRoute({
handler: async c => {
const { name } = c.req.valid("json");

const [category] = await c
.get("db")
.insert(blog_categories)
.values({})
.returning({ id: blog_categories.id });

await saveLanguageWords(c, {
pluginCode: "blog",
tableName: "blog_categories",
variable: "name",
itemId: category.id,
values: name, // the { languageCode, value }[] from the form
});

return c.json({ id: category.id }, 201);
},
});
```

To read the translations back for an edit form, query `core_languages_words` by
the same tuple and return `{ languageCode, value }[]` - the shape
[`resolveRoleNames`](/docs/dev/working-with-users/roles) produces for role names.

## Custom Fields

Because Auto Form fields are controlled entirely by the `component` property function, creating a custom field component is just a matter of rendering your own UI using the provided props. The props passed to the `component` function contain the field properties managed by `react-hook-form` along with Zod validation details.
Expand Down
42 changes: 42 additions & 0 deletions apps/docs/content/docs/ui/editor.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,42 @@ const [content, setContent] = useState("<p>Hello World! 🌎️</p>");
</Tab>
</Tabs>

## Multi-language

Set the `multiLang` prop to edit the content in every enabled language. A
language **select** (shown only when more than one language is enabled) switches
which language you are editing - it does **not** change the app-wide locale,
only the content inside this editor.

The value becomes an array matching the `core_languages_words` table:

```ts
[
{ languageCode: "en", value: "<p>Hello</p>" },
{ languageCode: "pl", value: "<p>Cześć</p>" },
];
```

Declare the field with the `multiLangValueSchema` helper and enable `multiLang`:

```ts
import { multiLangValueSchema } from "@vitnode/core/lib/helpers/multi-lang";

const formSchema = z.object({
content: multiLangValueSchema({ minLength: 1 }).min(1),
});
```

```tsx
{
id: "content",
component: props => <AutoFormEditor {...props} label="Content" multiLang />,
}
```

Persist the array on the backend with `saveLanguageWords`. See
[Auto Form - Multi-language fields](/docs/ui/auto-form#multi-language-fields).

## Rendering Content

Use `EditorContent` to render the stored HTML as read-only content outside of
Expand Down Expand Up @@ -108,5 +144,11 @@ import { TypeTable } from "fumadocs-ui/components/type-table";
type: "string",
default: "",
},
multiLang: {
description:
"Edit the content per language. The value becomes a { languageCode, value }[] array and a language select is shown when more than one language is enabled.",
type: "boolean",
default: "false",
},
}}
/>
45 changes: 45 additions & 0 deletions apps/docs/content/docs/ui/input.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,45 @@ import { Input } from '@vitnode/core/components/ui/input';
</Tab>
</Tabs>

## Multi-language

Set the `multiLang` prop to collect the value in every enabled language. The
field renders a language **select** (shown only when more than one language is
enabled) that switches which language you are editing - it does **not** change
the app-wide locale, only the value inside this input.

The value becomes an array matching the `core_languages_words` table, one entry
per language you edited:

```ts
[
{ languageCode: "en", value: "Category" },
{ languageCode: "pl", value: "Kategoria" },
];
```

Declare the field with the `multiLangValueSchema` helper so the schema matches
the array shape (its `minLength` / `maxLength` apply to each language's value):

```ts
import { multiLangValueSchema } from "@vitnode/core/lib/helpers/multi-lang";

const formSchema = z.object({
name: multiLangValueSchema({ minLength: 1, maxLength: 255 }).min(1),
});
```

```tsx
{
id: "name",
component: props => <AutoFormInput {...props} label="Name" multiLang />,
}
```

On the backend, persist the array with `saveLanguageWords` - it fills the
remaining `core_languages_words` columns. See
[Auto Form - Multi-language fields](/docs/ui/auto-form#multi-language-fields).

## Props

import { TypeTable } from "fumadocs-ui/components/type-table";
Expand Down Expand Up @@ -97,5 +136,11 @@ import { TypeTable } from "fumadocs-ui/components/type-table";
type: "string",
default: "",
},
multiLang: {
description:
"Collect the value per language. The value becomes a { languageCode, value }[] array and a language select is shown when more than one language is enabled.",
type: "boolean",
default: "false",
},
}}
/>
80 changes: 80 additions & 0 deletions apps/docs/src/locales/@vitnode/blog/pl.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
{
"@vitnode/blog": {
"title": "Blog",
"admin": {
"nav": {
"posts": "Wpisy",
"categories": "Kategorie"
},
"categories": {
"desc": "Zarządzaj kategoriami wpisów na blogu.",
"table": {
"title": "Tytuł",
"updated_at": "Zaktualizowano"
},
"delete": {
"title": "Usuń kategorię",
"desc": "Czy na pewno chcesz usunąć kategorię <title></title>? Tej akcji nie można cofnąć.",
"confirm": "Tak, usuń tę kategorię",
"success": "Kategoria została pomyślnie usunięta."
},
"create": {
"title": "Utwórz kategorię",
"desc": "Nowa kategoria dla wpisów na blogu.",
"form": {
"title": {
"label": "Tytuł",
"already_exists": "Kategoria o tym tytule już istnieje."
}
},
"submit": "Utwórz"
},
"edit": {
"title": "Edytuj kategorię",
"submit": "Zapisz zmiany"
}
},
"posts": {
"desc": "Twórz wpisy na blogu i zarządzaj nimi.",
"table": {
"title": "Tytuł",
"category": "Kategoria",
"updated_at": "Zaktualizowano"
},
"create": {
"title": "Utwórz wpis",
"desc": "Napisz nowy artykuł na swój blog.",
"form": {
"title": {
"label": "Tytuł",
"already_exists": "Wpis o tym tytule już istnieje."
},
"content": "Treść",
"category": "Kategoria"
},
"submit": "Utwórz wpis"
},
"edit": {
"title": "Edytuj wpis",
"submit": "Zapisz zmiany"
},
"delete": {
"title": "Usuń wpis",
"desc": "Czy na pewno chcesz usunąć wpis <title></title>? Tej akcji nie można cofnąć.",
"confirm": "Tak, usuń ten wpis",
"success": "Wpis został pomyślnie usunięty."
}
}
}
},
"@vitnode/blog:posts": "Wpisy",
"@vitnode/blog:posts:can_view": "Wyświetlanie listy wpisów",
"@vitnode/blog:posts:can_create": "Tworzenie wpisów",
"@vitnode/blog:posts:can_edit": "Edytowanie wpisów",
"@vitnode/blog:posts:can_delete": "Usuwanie wpisów",
"@vitnode/blog:categories": "Kategorie",
"@vitnode/blog:categories:can_view": "Wyświetlanie listy kategorii",
"@vitnode/blog:categories:can_create": "Tworzenie kategorii",
"@vitnode/blog:categories:can_edit": "Edytowanie kategorii",
"@vitnode/blog:categories:can_delete": "Usuwanie kategorii"
}
1 change: 1 addition & 0 deletions apps/docs/src/locales/@vitnode/core/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@
"go_back": "Go back",
"select_option": "Select an option",
"select_options": "Select options",
"select_language": "Select language",
"go_to_prev_page": "Go to previous page",
"go_to_next_page": "Go to next page",
"errors": {
Expand Down
Loading
Loading