refactor: memoize Zod schemas and internationalize form labels and messages in demo components

This commit is contained in:
Firman Ramdhani
2026-06-22 11:34:25 +07:00
parent 1ceceb6759
commit 4d2558cf71
5 changed files with 217 additions and 76 deletions
@@ -109,7 +109,7 @@ export default function AllFieldsDemo() {
<Stack gap="xl">
{/* --- Text & Numbers --- */}
<div>
<Title order={5} mb="sm" c="brand">Text & Numbers</Title>
<Title order={5} mb="sm" c="brand">{t.sections.textAndNumbers}</Title>
<Divider mb="md" />
<Group grow align="flex-start" mb="md">
<FieldTextInput name="customerName" control={control} label={t.fields.customerName} />
@@ -131,7 +131,7 @@ export default function AllFieldsDemo() {
{/* --- Selections --- */}
<div>
<Title order={5} mb="sm" c="brand">Selections</Title>
<Title order={5} mb="sm" c="brand">{t.sections.selections}</Title>
<Divider mb="md" />
<Group grow align="flex-start" mb="md">
<FieldSelect
@@ -151,13 +151,13 @@ export default function AllFieldsDemo() {
<FieldAutocomplete
name="country"
control={control}
label="Country"
label={t.fields.country}
data={['Indonesia', 'Singapore', 'Malaysia']}
/>
<FieldMultiSelect
name="categories"
control={control}
label="Categories"
label={t.fields.categories}
data={['Electronics', 'Fashion', 'Food']}
/>
</Group>
@@ -165,8 +165,8 @@ export default function AllFieldsDemo() {
<FieldLocalSelect
name="localSelect"
control={control}
label="Local Select"
placeholder="Select a complex object"
label={t.fields.localSelect}
placeholder={t.placeholders.selectComplexObject}
options={[
{ id: 1, name: 'Apple', type: 'Fruit' },
{ id: 2, name: 'Carrot', type: 'Vegetable' },
@@ -180,8 +180,8 @@ export default function AllFieldsDemo() {
multiple
name="multiLocalSelect"
control={control}
label="Multi Local Select"
placeholder="Select multiple objects"
label={t.fields.multiLocalSelect}
placeholder={t.placeholders.selectMultipleObjects}
options={[
{ id: 1, name: 'Red', hex: '#f00' },
{ id: 2, name: 'Green', hex: '#0f0' },
@@ -196,8 +196,8 @@ export default function AllFieldsDemo() {
<FieldAsyncSelect
name="asyncSelect"
control={control}
label="Async Select (Mock API)"
placeholder="Search pokemon..."
label={t.fields.asyncSelectMock}
placeholder={t.placeholders.searchPokemon}
loadOptions={loadMockPokemonOptions}
valueKey="id"
labelKey="name"
@@ -207,8 +207,8 @@ export default function AllFieldsDemo() {
multiple
name="multiAsyncSelect"
control={control}
label="Multi Async Select"
placeholder="Select multiple pokemon..."
label={t.fields.multiAsyncSelect}
placeholder={t.placeholders.selectMultiplePokemon}
loadOptions={loadMockPokemonOptions}
valueKey="id"
labelKey="name"
@@ -219,8 +219,8 @@ export default function AllFieldsDemo() {
<FieldAsyncSelect
name="realPokeSelect"
control={control}
label="Real PokeAPI (Single - Tests Deduplication)"
placeholder="Scroll to test deduplication..."
label={t.fields.realPokeSingle}
placeholder={t.placeholders.scrollDeduplication}
loadOptions={loadRealPokemonOptions}
valueKey="id"
labelKey="name"
@@ -230,8 +230,8 @@ export default function AllFieldsDemo() {
multiple
name="multiRealPokeSelect"
control={control}
label="Real PokeAPI (Multi - Tests Deduplication)"
placeholder="Scroll to test deduplication..."
label={t.fields.realPokeMulti}
placeholder={t.placeholders.scrollDeduplication}
loadOptions={loadRealPokemonOptions}
valueKey="id"
labelKey="name"
@@ -240,13 +240,13 @@ export default function AllFieldsDemo() {
</Group>
<FieldTagsInput name="tags" control={control} label={t.fields.tags} />
<Title order={5} mb="sm" mt="lg" c="brand">Advanced Object Selects (Custom Labels & Default Values)</Title>
<Title order={5} mb="sm" mt="lg" c="brand">{t.sections.advancedObjectSelects}</Title>
<Divider mb="md" />
<Group grow align="flex-start" mb="md">
<FieldLocalSelect
name="localSelectEmpty"
control={control}
label="Local Empty"
label={t.fields.localEmpty}
options={MOCK_VENDORS}
valueKey="id"
renderLabel={(item) => `[${item.code}] ${item.name}`}
@@ -255,7 +255,7 @@ export default function AllFieldsDemo() {
<FieldLocalSelect
name="localSelectPrefilled"
control={control}
label="Local Prefilled"
label={t.fields.localPrefilled}
options={MOCK_VENDORS}
valueKey="id"
renderLabel={(item) => `[${item.code}] ${item.name}`}
@@ -266,7 +266,7 @@ export default function AllFieldsDemo() {
<FieldAsyncSelect
name="asyncSelectEmpty"
control={control}
label="Async Empty"
label={t.fields.asyncEmpty}
loadOptions={loadMockVendorsOptions}
valueKey="id"
renderLabel={(item) => `[${item.code}] ${item.name}`}
@@ -275,7 +275,7 @@ export default function AllFieldsDemo() {
<FieldAsyncSelect
name="asyncSelectPrefilled"
control={control}
label="Async Prefilled (Edit Mode)"
label={t.fields.asyncPrefilled}
loadOptions={loadMockVendorsOptions}
valueKey="id"
renderLabel={(item) => `[${item.code}] ${item.name}`}
@@ -284,14 +284,14 @@ export default function AllFieldsDemo() {
/>
</Group>
<Title order={5} mb="sm" mt="lg" c="brand">Multi-Select Edit Mode (No defaultOptions fallback)</Title>
<Title order={5} mb="sm" mt="lg" c="brand">{t.sections.multiSelectEditMode}</Title>
<Divider mb="md" />
<Group grow align="flex-start" mb="md">
<FieldLocalSelect
multiple
name="localMultiPrefilled"
control={control}
label="Local Multi Prefilled"
label={t.fields.localMultiPrefilled}
options={MOCK_VENDORS}
valueKey="id"
renderLabel={(item) => `[${item.code}] ${item.name}`}
@@ -301,7 +301,7 @@ export default function AllFieldsDemo() {
multiple
name="asyncMultiPrefilled"
control={control}
label="Async Multi Prefilled (Ghost Items)"
label={t.fields.asyncMultiPrefilled}
loadOptions={loadMockVendorsOptions}
valueKey="id"
renderLabel={(item) => `[${item.code}] ${item.name}`}
@@ -309,27 +309,27 @@ export default function AllFieldsDemo() {
/>
</Group>
<Title order={5} mb="sm" mt="lg" c="brand">Rich Text Editor (TipTap)</Title>
<Title order={5} mb="sm" mt="lg" c="brand">{t.sections.richTextEditor}</Title>
<Divider mb="md" />
<FieldRichTextEditor
name="richTextEmpty"
control={control}
label="Rich Text (Empty)"
description="A fresh TipTap editor instance"
label={t.fields.richTextEmpty}
description={t.descriptions.freshTipTap}
/>
<div style={{ marginTop: '16px' }}>
<FieldRichTextEditor
name="richTextPrefilled"
control={control}
label="Rich Text (Prefilled / Edit Mode)"
description="HTML string successfully loaded from default values"
label={t.fields.richTextPrefilled}
description={t.descriptions.htmlStringLoaded}
/>
</div>
</div>
{/* --- Toggles & Choices --- */}
<div>
<Title order={5} mb="sm" c="brand">Toggles & Choices</Title>
<Title order={5} mb="sm" c="brand">{t.sections.togglesAndChoices}</Title>
<Divider mb="md" />
<Group mb="md">
<FieldCheckbox name="terms" control={control} label={t.fields.terms} />
@@ -372,11 +372,11 @@ export default function AllFieldsDemo() {
{/* --- Ranges & Specialized --- */}
<div>
<Title order={5} mb="sm" c="brand">Ranges & Specialized</Title>
<Title order={5} mb="sm" c="brand">{t.sections.rangesAndSpecialized}</Title>
<Divider mb="md" />
<Group grow align="flex-start" mb="md">
<FieldSlider name="satisfaction" control={control} label={t.fields.rating} />
<FieldRangeSlider name="priceRange" control={control} label="Price Range" />
<FieldRangeSlider name="priceRange" control={control} label={t.fields.priceRange} />
</Group>
<Group grow align="flex-start" mb="md">
<FieldColorInput name="themeColor" control={control} label={t.fields.themeColor} />
@@ -7,7 +7,7 @@ import { useConditionalField } from '@repo/ui/hooks';
import { compose, required, emailValidator } from '@repo/ui/validators';
import { useFormDemoTranslation } from '../i18n/useFormDemoTranslation';
import { Info } from 'lucide-react';
import { useEffect, useCallback, useRef } from 'react';
import { useEffect, useCallback, useRef, useMemo } from 'react';
interface Region {
id: string;
@@ -52,10 +52,10 @@ export default function ReactiveWatchDemo() {
// Define atomic validators for conditional fields
const taxIdValidator = compose(z.string(), required(t.watch.corporateTaxId));
const spouseNameValidator = compose(z.string(), required(t.watch.spouseName));
const newsletterEmailValidator = compose(z.string(), required('Newsletter Email'), emailValidator());
const roleValidator = compose(z.string(), required('Role'));
const newsletterEmailValidator = compose(z.string(), required(t.fields.email), emailValidator());
const roleValidator = compose(z.string(), required(t.fields.role));
const reactiveSchema = z
const reactiveSchema = useMemo(() => z
.object({
userType: z.enum(['PERSONAL', 'CORPORATE']),
corporateTaxId: z.string().optional(),
@@ -92,7 +92,7 @@ export default function ReactiveWatchDemo() {
z.object({ department: z.string().min(1), role: roleValidator }),
z.object({ department: z.union([z.literal(''), z.undefined(), z.null()]).optional() }),
]),
);
), [t, taxIdValidator, spouseNameValidator, newsletterEmailValidator, roleValidator]);
const { control, handleSubmit, setValue, unregister, clearErrors } = useForm<any>({
resolver: zodResolver(reactiveSchema as any),
@@ -214,7 +214,7 @@ export default function ReactiveWatchDemo() {
<form onSubmit={handleSubmit(onSubmit)}>
<Stack gap="md">
<Title order={5} mb="sm" c="brand">
Dynamic Fields & Validation
{t.sections.reactiveWatchCascading}
</Title>
<Divider mb="sm" />
@@ -264,8 +264,8 @@ export default function ReactiveWatchDemo() {
<FieldSelect
name="department"
control={control}
label="Department"
placeholder="Select a department"
label={t.fields.department}
placeholder={t.placeholders.selectComplexObject}
data={[
{ value: 'IT', label: 'Information Technology' },
{ value: 'HR', label: 'Human Resources' },
@@ -286,15 +286,15 @@ export default function ReactiveWatchDemo() {
key={`role-select-${department}`}
name="role"
control={control}
label="Role"
placeholder="Select a role"
label={t.fields.role}
placeholder={t.placeholders.selectComplexObject}
disabled={!department}
data={currentRoleOptions}
withAsterisk={!!department}
/>
<Title order={5} mb="sm" c="brand" mt="lg">
Cascading Object Selects
{t.sections.reactiveWatchCascading}
</Title>
<Divider mb="sm" />
@@ -302,7 +302,7 @@ export default function ReactiveWatchDemo() {
multiple
name="regions"
control={control as any}
label="Regions"
label={t.fields.regions}
options={REGIONS}
valueKey="id"
labelKey="code"
@@ -314,7 +314,7 @@ export default function ReactiveWatchDemo() {
key={`warehouse-select-${regions?.map((r: any) => r.id).join(',')}`}
name="warehouses"
control={control as any}
label="Warehouses"
label={t.fields.warehouses}
disabled={!regions || regions.length === 0}
loadOptions={useCallback(async (search, page) => {
if (!regions || regions.length === 0) return { options: [], hasMore: false };
@@ -327,31 +327,31 @@ export default function ReactiveWatchDemo() {
{regions && regions.length > 0 && (
<Alert mt="sm" color="teal">
Selected regions tax rates: {regions.map((r: any) => `${r.code} (${(r.taxRate * 100).toFixed(0)}%)`).join(', ')}
{t.descriptions.selectedRegionsTax} {regions.map((r: any) => `${r.code} (${(r.taxRate * 100).toFixed(0)}%)`).join(', ')}
</Alert>
)}
<Title order={5} mb="sm" c="brand" mt="lg">
Reactive Rich Text Preview
{t.sections.reactiveRichTextPreview}
</Title>
<Divider mb="sm" />
<FieldRichTextEditor
name="richTextLive"
control={control as any}
label="Live Editor"
description="Type to see instantaneous reactive rendering below"
label={t.fields.liveEditor}
description={t.descriptions.typeToSeePreview}
/>
<Paper p="md" withBorder radius="md" mt="sm">
<Title order={6} mb="xs">Live HTML Preview Render</Title>
<Title order={6} mb="xs">{t.sections.liveHtmlPreview}</Title>
<TypographyStylesProvider>
<div dangerouslySetInnerHTML={{ __html: watchedRichTextLive }} />
</TypographyStylesProvider>
</Paper>
<Button type="submit" mt="md">
{t.common?.submit || 'Submit Reactive Form'}
{t.common.submitReactive}
</Button>
</Stack>
</form>
@@ -1,3 +1,4 @@
import { useMemo } from 'react';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
@@ -63,20 +64,20 @@ export default function ValidationBankDemo() {
const t = useFormDemoTranslation();
// Compose the Zod schema using the atomic validators
const validationSchema = z.object({
const validationSchema = useMemo(() => z.object({
username: compose(z.string(), required(t.fields.customerName), rangeLength(3, 15, t.fields.customerName)),
simplePass: compose(z.string(), required(t.validation.simplePassword), simplePassword(6)),
complexPass: compose(z.string(), required(t.validation.complexPassword), complexPassword(8)),
age: compose(z.number(), required(t.fields.age), rangeValue(18, 65, t.fields.age)),
score: compose(z.number(), required(t.validation.score), positiveNumber(t.validation.score)),
phone: compose(z.string(), required(t.validation.phone), phoneValidator()),
department: z.object({ code: z.string(), name: z.string() }, { required_error: 'Department is required' }),
assignees: z.array(z.object({ id: z.number(), email: z.string() })).min(2, "Select at least 2 assignees"),
prefilledVendor: z.object({ id: z.union([z.string(), z.number()]), code: z.string(), name: z.string() }, { required_error: 'Vendor is required' }),
emptyVendor: z.object({ id: z.union([z.string(), z.number()]), code: z.string(), name: z.string() }, { required_error: 'Vendor is required' }),
prefilledAsyncMulti: z.array(z.object({ id: z.union([z.string(), z.number()]), code: z.string(), name: z.string() })).min(1, "Select at least 1 vendor"),
richTextNotes: z.string().min(15, "Notes must be at least 15 characters long (including HTML tags)"),
});
department: z.object({ code: z.string(), name: z.string() }, { required_error: t.errors.departmentRequired }),
assignees: z.array(z.object({ id: z.number(), email: z.string() })).min(2, t.errors.min2Assignees),
prefilledVendor: z.object({ id: z.union([z.string(), z.number()]), code: z.string(), name: z.string() }, { required_error: t.errors.vendorRequired }),
emptyVendor: z.object({ id: z.union([z.string(), z.number()]), code: z.string(), name: z.string() }, { required_error: t.errors.vendorRequired }),
prefilledAsyncMulti: z.array(z.object({ id: z.union([z.string(), z.number()]), code: z.string(), name: z.string() })).min(1, t.errors.min1Vendor),
richTextNotes: z.string().min(15, t.errors.notesMin15),
}), [t]);
type ValidationFormValues = z.infer<typeof validationSchema>;
@@ -109,7 +110,7 @@ export default function ValidationBankDemo() {
<Paper p="xl" withBorder radius="md">
<form onSubmit={handleSubmit(onSubmit)}>
<Stack gap="md">
<Title order={5} c="brand">Validation Bank (Atomic Registry)</Title>
<Title order={5} c="brand">{t.sections.validationBankTitle}</Title>
<Divider mb="sm" />
<FieldTextInput
@@ -125,14 +126,14 @@ export default function ValidationBankDemo() {
name="simplePass"
control={control}
label={t.validation.simplePassword}
description="Min 6 chars"
description={t.descriptions.min6Chars}
withAsterisk
/>
<FieldPasswordInput
name="complexPass"
control={control}
label={t.validation.complexPassword}
description="Min 8, 1 uppercase, 1 number, 1 special"
description={t.descriptions.min8Complex}
withAsterisk
/>
</Group>
@@ -149,7 +150,7 @@ export default function ValidationBankDemo() {
name="score"
control={control}
label={t.validation.score}
description="Must be > 0"
description={t.descriptions.mustBePositive}
withAsterisk
/>
</Group>
@@ -158,17 +159,17 @@ export default function ValidationBankDemo() {
name="phone"
control={control}
label={t.validation.phone}
description="Format: +62..."
description={t.descriptions.formatPhone}
withAsterisk
/>
<Title order={5} c="brand" mt="md">Object Level Validations (Local & Async)</Title>
<Title order={5} c="brand" mt="md">{t.sections.objectLevelValidations}</Title>
<Divider mb="sm" />
<FieldLocalSelect<Department>
name="department"
control={control as any}
label="Department"
label={t.fields.department}
options={MOCK_DEPARTMENTS}
valueKey="code"
renderLabel={(item) => `[${item.code}] ${item.name}`}
@@ -180,7 +181,7 @@ export default function ValidationBankDemo() {
multiple
name="assignees"
control={control as any}
label="Assignees"
label={t.fields.assignees}
loadOptions={mockFetchUsers}
valueKey="id"
labelKey="email"
@@ -189,14 +190,14 @@ export default function ValidationBankDemo() {
withAsterisk
/>
<Title order={5} c="brand" mt="lg">Validated Prefilled Objects</Title>
<Title order={5} c="brand" mt="lg">{t.sections.validatedPrefilledObjects}</Title>
<Divider mb="sm" />
<Group grow align="flex-start">
<FieldAsyncSelect
name="emptyVendor"
control={control as any}
label="Empty Vendor"
label={t.fields.emptyVendor}
loadOptions={mockFetchVendors}
valueKey="id"
renderLabel={(item) => `[${item.code}] ${item.name}`}
@@ -206,7 +207,7 @@ export default function ValidationBankDemo() {
<FieldAsyncSelect
name="prefilledVendor"
control={control as any}
label="Prefilled Vendor"
label={t.fields.prefilledVendor}
loadOptions={mockFetchVendors}
valueKey="id"
renderLabel={(item) => `[${item.code}] ${item.name}`}
@@ -220,7 +221,7 @@ export default function ValidationBankDemo() {
multiple
name="prefilledAsyncMulti"
control={control as any}
label="Prefilled Async Multi (No Fallback)"
label={t.fields.prefilledAsyncMulti}
loadOptions={mockFetchVendors}
valueKey="id"
renderLabel={(item) => `[${item.code}] ${item.name}`}
@@ -228,14 +229,14 @@ export default function ValidationBankDemo() {
withAsterisk
/>
<Title order={5} c="brand" mt="lg">Rich Text Editor Validations</Title>
<Title order={5} c="brand" mt="lg">{t.sections.richTextValidations}</Title>
<Divider mb="sm" />
<FieldRichTextEditor
name="richTextNotes"
control={control as any}
label="Important Notes"
description="This uses Zod minimum length string validation"
label={t.fields.importantNotes}
description={t.descriptions.zodMinLengthString}
withAsterisk
/>
@@ -6,9 +6,26 @@
},
"common": {
"submit": "Submit Data",
"submitReactive": "Submit Reactive Form",
"reset": "Reset Form",
"submittedData": "Submitted Data"
},
"sections": {
"validationBankTitle": "Validation Bank (Atomic Registry)",
"objectLevelValidations": "Object Level Validations (Local & Async)",
"validatedPrefilledObjects": "Validated Prefilled Objects",
"richTextValidations": "Rich Text Editor Validations",
"textAndNumbers": "Text & Numbers",
"selections": "Selections",
"advancedObjectSelects": "Advanced Object Selects (Custom Labels & Default Values)",
"multiSelectEditMode": "Multi-Select Edit Mode (No defaultOptions fallback)",
"richTextEditor": "Rich Text Editor (TipTap)",
"togglesAndChoices": "Toggles & Choices",
"rangesAndSpecialized": "Ranges & Specialized",
"reactiveWatchCascading": "Reactive Watch (Cascading)",
"reactiveRichTextPreview": "Reactive Rich Text Preview",
"liveHtmlPreview": "Live HTML Preview Render"
},
"fields": {
"customerName": "Customer Name",
"email": "Email Address",
@@ -26,7 +43,60 @@
"orderType": "Order Type",
"quantity": "Quantity",
"fabricColor": "Fabric Color",
"pin": "Security PIN"
"pin": "Security PIN",
"department": "Department",
"assignees": "Assignees",
"emptyVendor": "Empty Vendor",
"prefilledVendor": "Prefilled Vendor",
"prefilledAsyncMulti": "Prefilled Async Multi (No Fallback)",
"importantNotes": "Important Notes",
"country": "Country",
"categories": "Categories",
"localSelect": "Local Select",
"multiLocalSelect": "Multi Local Select",
"asyncSelectMock": "Async Select (Mock API)",
"multiAsyncSelect": "Multi Async Select",
"realPokeSingle": "Real PokeAPI (Single - Tests Deduplication)",
"realPokeMulti": "Real PokeAPI (Multi - Tests Deduplication)",
"localEmpty": "Local Empty",
"localPrefilled": "Local Prefilled",
"asyncEmpty": "Async Empty",
"asyncPrefilled": "Async Prefilled (Edit Mode)",
"localMultiPrefilled": "Local Multi Prefilled",
"asyncMultiPrefilled": "Async Multi Prefilled (Ghost Items)",
"richTextEmpty": "Rich Text (Empty)",
"richTextPrefilled": "Rich Text (Prefilled / Edit Mode)",
"priceRange": "Price Range",
"role": "Role",
"regions": "Regions",
"warehouses": "Warehouses",
"liveEditor": "Live Editor"
},
"placeholders": {
"selectComplexObject": "Select a complex object",
"selectMultipleObjects": "Select multiple objects",
"searchPokemon": "Search pokemon...",
"selectMultiplePokemon": "Select multiple pokemon...",
"scrollDeduplication": "Scroll to test deduplication..."
},
"descriptions": {
"min6Chars": "Min 6 chars",
"min8Complex": "Min 8, 1 uppercase, 1 number, 1 special",
"mustBePositive": "Must be > 0",
"formatPhone": "Format: +62...",
"zodMinLengthString": "This uses Zod minimum length string validation",
"freshTipTap": "A fresh TipTap editor instance",
"htmlStringLoaded": "HTML string successfully loaded from default values",
"typeToSeePreview": "Type to see instantaneous reactive rendering below",
"selectedRegionsTax": "Selected regions tax rates:"
},
"errors": {
"departmentRequired": "Department is required",
"vendorRequired": "Vendor is required",
"min2Assignees": "Select at least 2 assignees",
"min1Vendor": "Select at least 1 vendor",
"notesMin15": "Notes must be at least 15 characters long (including HTML tags)",
"selectRegionFirst": "Select a region first to load warehouses"
},
"validation": {
"simplePassword": "Simple Password",
@@ -6,9 +6,26 @@
},
"common": {
"submit": "Kirim Data",
"submitReactive": "Kirim Form Reaktif",
"reset": "Reset Form",
"submittedData": "Data Terkirim"
},
"sections": {
"validationBankTitle": "Bank Validasi (Registri Atomik)",
"objectLevelValidations": "Validasi Tingkat Objek (Lokal & Async)",
"validatedPrefilledObjects": "Objek Terisi yang Divalidasi",
"richTextValidations": "Validasi Rich Text Editor",
"textAndNumbers": "Teks & Angka",
"selections": "Pilihan",
"advancedObjectSelects": "Pemilihan Objek Tingkat Lanjut (Label Kustom & Nilai Default)",
"multiSelectEditMode": "Mode Edit Multi-Select (Tanpa fallback defaultOptions)",
"richTextEditor": "Rich Text Editor (TipTap)",
"togglesAndChoices": "Tombol Sakelar & Pilihan",
"rangesAndSpecialized": "Rentang & Khusus",
"reactiveWatchCascading": "Reactive Watch (Berjenjang)",
"reactiveRichTextPreview": "Pratinjau Rich Text Reaktif",
"liveHtmlPreview": "Render Pratinjau HTML Langsung"
},
"fields": {
"customerName": "Nama Pelanggan",
"email": "Alamat Email",
@@ -26,7 +43,60 @@
"orderType": "Tipe Pesanan",
"quantity": "Jumlah",
"fabricColor": "Warna Kain",
"pin": "PIN Keamanan"
"pin": "PIN Keamanan",
"department": "Departemen",
"assignees": "Penerima Tugas",
"emptyVendor": "Vendor Kosong",
"prefilledVendor": "Vendor Terisi",
"prefilledAsyncMulti": "Multi Async Terisi (Tanpa Fallback)",
"importantNotes": "Catatan Penting",
"country": "Negara",
"categories": "Kategori",
"localSelect": "Pilihan Lokal",
"multiLocalSelect": "Pilihan Lokal Multi",
"asyncSelectMock": "Pilihan Async (Mock API)",
"multiAsyncSelect": "Pilihan Async Multi",
"realPokeSingle": "API Pokemon Asli (Tunggal - Uji Deduplikasi)",
"realPokeMulti": "API Pokemon Asli (Multi - Uji Deduplikasi)",
"localEmpty": "Lokal Kosong",
"localPrefilled": "Lokal Terisi",
"asyncEmpty": "Async Kosong",
"asyncPrefilled": "Async Terisi (Mode Edit)",
"localMultiPrefilled": "Multi Lokal Terisi",
"asyncMultiPrefilled": "Multi Async Terisi (Item Hantu)",
"richTextEmpty": "Rich Text (Kosong)",
"richTextPrefilled": "Rich Text (Terisi / Mode Edit)",
"priceRange": "Rentang Harga",
"role": "Peran",
"regions": "Wilayah",
"warehouses": "Gudang",
"liveEditor": "Editor Langsung"
},
"placeholders": {
"selectComplexObject": "Pilih objek yang kompleks",
"selectMultipleObjects": "Pilih beberapa objek",
"searchPokemon": "Cari pokemon...",
"selectMultiplePokemon": "Pilih beberapa pokemon...",
"scrollDeduplication": "Gulir untuk menguji deduplikasi..."
},
"descriptions": {
"min6Chars": "Minimal 6 karakter",
"min8Complex": "Min 8, 1 huruf besar, 1 angka, 1 karakter khusus",
"mustBePositive": "Harus > 0",
"formatPhone": "Format: +62...",
"zodMinLengthString": "Ini menggunakan validasi panjang string minimum Zod",
"freshTipTap": "Instance editor TipTap yang baru",
"htmlStringLoaded": "String HTML berhasil dimuat dari nilai default",
"typeToSeePreview": "Ketik untuk melihat render reaktif seketika di bawah",
"selectedRegionsTax": "Tarif pajak wilayah yang dipilih:"
},
"errors": {
"departmentRequired": "Departemen wajib diisi",
"vendorRequired": "Vendor wajib diisi",
"min2Assignees": "Pilih minimal 2 penerima tugas",
"min1Vendor": "Pilih minimal 1 vendor",
"notesMin15": "Catatan minimal harus terdiri dari 15 karakter (termasuk tag HTML)",
"selectRegionFirst": "Pilih wilayah terlebih dahulu untuk memuat gudang"
},
"validation": {
"simplePassword": "Sandi Sederhana",