Skip to content
Merged
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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added
- Initial release
- `TranslatableInput` field: multi-language text/textarea whose value is an array keyed by locale, with a globe button that opens a dialog to edit every locale, per-locale validation via a `TranslationsRule` (errors reported as `name.en`, `name.ar`, `name.ckb`, ...) and locale metadata (native name and text direction) read from the panel's `config('app.available_locales')` list by default, so languages are defined once in `config/app.php`. An optional `laravilt-forms.locales` config key, shipped empty, overrides that list with plain codes or per-locale `name`/`direction`/`label` metadata when content languages differ from UI languages. Shipped for Blade, Vue and React.
- `required()` is now mirrored as a `required` validation rule on every field, so it is enforced even when other rules such as `maxLength()` are set (previously the schema only added it for fields with no other rules).

### Changed

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ php artisan make:component CustomField

## Field Types

**Basic:** TextInput, Textarea, NumberField, Select, Checkbox, CheckboxList, Radio, Toggle, ToggleButtons, Hidden
**Basic:** TextInput, Textarea, TranslatableInput, NumberField, Select, Checkbox, CheckboxList, Radio, Toggle, ToggleButtons, Hidden

**Date & Time:** DatePicker, DateTimePicker, TimePicker, DateRangePicker

Expand Down
35 changes: 35 additions & 0 deletions config/laravilt-forms.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,5 +29,40 @@
'middleware' => ['web', 'auth', 'throttle:120,1'],
],

/*
|--------------------------------------------------------------------------
| Locales
|--------------------------------------------------------------------------
|
| The locales offered by TranslatableInput when a field does not call
| ->locales() itself. Leave this empty to reuse the languages the panel
| already knows about from config('app.available_locales'), the same list
| the Locale & Timezone settings page shows, so a project defines its
| languages once in config/app.php:
|
| 'available_locales' => [
| ['value' => 'en', 'label' => 'English', 'dir' => 'ltr'],
| ['value' => 'ar', 'label' => 'العربية', 'dir' => 'rtl'],
| ],
|
| Set it only when the languages content is written in differ from the
| languages the UI is shown in. Each entry may be a plain code or a
| code => metadata pair with a native "name" and a "direction" (ltr or
| rtl); an optional "label" overrides the short badge on the globe button.
|
| 'locales' => ['en', 'ar', 'ckb'],
| 'locales' => [
| 'en' => ['name' => 'English', 'direction' => 'ltr'],
| 'ar' => ['name' => 'العربية', 'direction' => 'rtl'],
| 'ckb' => ['name' => 'کوردی', 'direction' => 'rtl'],
| ],
|
| A plain code gets the code as its name and "ltr" as its direction. When
| both lists are empty the field falls back to the application locale.
|
*/

'locales' => [],

// Add your configuration options here
];
112 changes: 112 additions & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ php artisan make:form UserForm --resource
### 📝 Basic Fields
- **TextInput**: Single-line text input with validation
- **Textarea**: Multi-line text input
- **TranslatableInput**: Multi-language text input with a per-locale dialog
- **NumberField**: Numeric input with min/max
- **Select**: Dropdown select with search
- **Checkbox**: Single checkbox
Expand Down Expand Up @@ -261,6 +262,116 @@ TextInput::make('name')
->helperText('Enter your full name');
```

### TranslatableInput

A multi-language text field. Its value is an array keyed by locale code
(`['en' => 'Title', 'ar' => 'العنوان', 'ckb' => 'ناونیشان']`). The main input edits the active
locale; a globe button inside the input opens a dialog with one input per
locale (RTL locales render with `dir="rtl"`), so long titles and content stay
readable. The globe is hidden when only one
locale is allowed.

```php
TranslatableInput::make('name')
->label('Name')
->locales(['en', 'ar', 'ckb']) // defaults to the configured locales, see below
->activeLocale('en') // defaults to the app locale when allowed
->required() // every locale is required...
->requiredLocales(['en']) // ...unless you narrow it down
->maxLength(255) // applied per locale
->multiline() // textarea per locale
->rows(4);
```

Locales resolve in this order: explicit `locales()`, then
`config('laravilt-forms.locales')` (published to `config/laravilt-forms.php`),
then `config('app.available_locales')`, then the application locale.

By default the field reuses the languages the panel already knows about. That
is the same list the [Locale & Timezone](../auth/profile/preferences.md)
settings page shows, so a project defines its languages once in
`config/app.php`; `label` becomes the native name shown in the dialog and
`dir` sets `dir` on each input:

```php
// config/app.php
'available_locales' => [
['value' => 'en', 'label' => 'English', 'dir' => 'ltr'],
['value' => 'ar', 'label' => 'العربية', 'dir' => 'rtl'],
['value' => 'ckb', 'label' => 'کوردی', 'dir' => 'rtl'],
],
```

Set `laravilt-forms.locales` only when the languages content is written in
differ from the languages the UI is shown in, for example an English-only
admin panel that manages content in three languages. It accepts plain codes or
per-locale metadata, and an optional `label` overrides the short badge on the
globe button:

```php
// config/laravilt-forms.php
'locales' => [
'en' => ['name' => 'English', 'direction' => 'ltr'],
'ar' => ['name' => 'العربية', 'direction' => 'rtl'],
'ckb' => ['name' => 'کوردی', 'direction' => 'rtl'],
],
// or simply: 'locales' => ['en', 'ar', 'ckb'],
```

Names and directions are looked up in `laravilt-forms.locales` first and
`app.available_locales` second, so plain codes in the forms config still pick
up the names the panel defines. A code found in neither is shown as its own
code, labelled with its uppercased base code, and rendered LTR.

Every locale is validated on its own and errors are reported per locale key
(`name.en`, `name.ar`, ...), which the field shows next to the matching input.
`getValidationRules()` returns one rule list for the field, as the schema
expects, with a `TranslationsRule` carrying the per-locale rules; use
`getLocaleValidationRules()` for flat keys when validating by hand:

```php
$field = TranslatableInput::make('name')->locales(['en', 'ar', 'ckb'])->required()->maxLength(120);

$field->getValidationRules();
// ['required', 'array', TranslationsRule(en|ar|ckb => ['required', 'string', 'max:120'])]

$field->getLocaleValidationRules();
// [
// 'name' => ['required', 'array'],
// 'name.en' => ['required', 'string', 'max:120'],
// 'name.ar' => ['required', 'string', 'max:120'],
// 'name.ckb' => ['required', 'string', 'max:120'],
// ]
```

Messages use the field label plus the locale, e.g. "The Name (EN) field is
required.", and `validationMessages(['required' => '...'])` applies to every
locale (`['en.required' => '...']` targets one).

`hydrateState()` / `dehydrateState()` accept a JSON string, an array or `null`
and always return an array with every allowed locale present, so the field works
with both storage styles:

```php
// spatie/laravel-translatable
class Product extends Model
{
use \Spatie\Translatable\HasTranslations;

public $translatable = ['name'];
}

// or a plain JSON column
class Category extends Model
{
protected $casts = ['name' => 'array'];
}

// Both accept the field's value as-is:
$product->setTranslations('name', $data['name']); // or $product->name = $data['name'];
$category->name = $data['name'];
```

### Select

```php
Expand Down Expand Up @@ -592,6 +703,7 @@ class ProductForm extends Form
### Basic
- TextInput
- Textarea
- TranslatableInput
- NumberField
- Select
- Checkbox
Expand Down
1 change: 1 addition & 0 deletions docs/mcp-server.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ For MCP tools to provide field type information:
### Basic Fields
- TextInput: Single-line text input
- Textarea: Multi-line text input
- TranslatableInput: Multi-language text input (value keyed by locale)
- NumberField: Numeric input
- Select: Dropdown select
- Checkbox: Single checkbox
Expand Down
7 changes: 7 additions & 0 deletions lang/ar/forms.php
Original file line number Diff line number Diff line change
Expand Up @@ -93,4 +93,11 @@
'search_placeholder' => 'بحث في الصلاحيات...',
'no_results' => 'لم يتم العثور على صلاحيات',
],

// TranslatableInput Component
'translatable_input' => [
'translations' => 'الترجمات',
'description' => 'حرّر هذا الحقل بكل اللغات.',
'done' => 'تم',
],
];
7 changes: 7 additions & 0 deletions lang/ckb/forms.php
Original file line number Diff line number Diff line change
Expand Up @@ -93,4 +93,11 @@
'search_placeholder' => 'گەڕان بۆ دەسەڵاتەکان...',
'no_results' => 'هیچ دەسەڵاتێک نەدۆزرایەوە',
],

// TranslatableInput Component
'translatable_input' => [
'translations' => 'وەرگێڕانەکان',
'description' => 'ئەم خانەیە بە هەموو زمانەکان دەستکاری بکە.',
'done' => 'تەواو',
],
];
7 changes: 7 additions & 0 deletions lang/en/forms.php
Original file line number Diff line number Diff line change
Expand Up @@ -93,4 +93,11 @@
'search_placeholder' => 'Search permissions...',
'no_results' => 'No permissions found',
],

// TranslatableInput Component
'translatable_input' => [
'translations' => 'Translations',
'description' => 'Edit this field in every language.',
'done' => 'Done',
],
];
2 changes: 2 additions & 0 deletions resources/js/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import Form from './components/Form.vue'
import FieldWrapper from './components/FieldWrapper.vue'
import TextInput from './components/fields/TextInput.vue'
import Textarea from './components/fields/Textarea.vue'
import TranslatableInput from './components/fields/TranslatableInput.vue'
import Select from './components/fields/Select.vue'
import Checkbox from './components/fields/Checkbox.vue'
import CheckboxList from './components/fields/CheckboxList.vue'
Expand Down Expand Up @@ -43,6 +44,7 @@ export default {
// Field components
app.component('laravilt-text-input', TextInput)
app.component('laravilt-textarea', Textarea)
app.component('laravilt-translatable-input', TranslatableInput)
app.component('laravilt-select', Select)
app.component('laravilt-checkbox', Checkbox)
app.component('laravilt-checkbox-list', CheckboxList)
Expand Down
2 changes: 2 additions & 0 deletions resources/js/components-only.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import LaraviltForm from './components/Form.vue';
import LaraviltFieldWrapper from './components/FieldWrapper.vue';
import LaraviltTextInput from './components/fields/TextInput.vue';
import LaraviltTextarea from './components/fields/Textarea.vue';
import LaraviltTranslatableInput from './components/fields/TranslatableInput.vue';
import LaraviltSelect from './components/fields/SelectWrapper.vue';
import LaraviltCheckbox from './components/fields/Checkbox.vue';
import LaraviltCheckboxList from './components/fields/CheckboxList.vue';
Expand Down Expand Up @@ -45,6 +46,7 @@ export default {
// Field components
app.component('laravilt-text-input', LaraviltTextInput);
app.component('laravilt-textarea', LaraviltTextarea);
app.component('laravilt-translatable-input', LaraviltTranslatableInput);
app.component('laravilt-select', LaraviltSelect);
app.component('laravilt-checkbox', LaraviltCheckbox);
app.component('laravilt-checkbox-list', LaraviltCheckboxList);
Expand Down
Loading