rilaykit
Guides

Internationalization

Patterns for building multilingual forms and workflows with RilayKit — localized labels, validation messages, and dynamic form generation.

RilayKit has no built-in i18n system. It is headless and schema-first, so any i18n library (react-intl, next-intl, i18next, etc.) plugs in with the patterns below.

Labels, placeholders, validation messages, and step titles are all data in the config — swap them for localized strings and the rendering logic never changes.

Localized Validation Messages

Built-in validators accept a custom message. Pass your translation function's output, and rebuild the config when the locale changes — messages are baked in at creation time:

import { useMemo } from 'react';
import { required, email } from '@rilaykit/core';
import { form } from '@rilaykit/forms';
import { Form, FormField } from '@rilaykit/forms/react';
import { useTranslation } from 'react-i18next'; // or your i18n library

function LocalizedContactForm() {
  const { t, i18n } = useTranslation();

  const contactForm = useMemo(() => form.create(rilay, 'contact')
    .add({
      id: 'email',
      type: 'input',
      props: { label: t('form.email') },
      validation: { validate: [required(t('validation.required')), email(t('validation.email'))] },
    }),
    [i18n.language] // Rebuild when locale changes
  );

  return (
    <Form formConfig={contactForm} onSubmit={handleSubmit}>
      <FormField fieldId="email" />
    </Form>
  );
}

Localized Component Props

All component props flow through the schema, so translated strings go straight into props — labels, placeholders, option lists, helper text, aria attributes:

import { form } from '@rilaykit/forms';

const signupForm = form.create(rilay, 'signup')
  .add({
    id: 'country',
    type: 'select',
    props: {
      label: t('form.country'),
      placeholder: t('form.selectCountry'),
      options: [
        { value: 'us', label: t('countries.us') },
        { value: 'fr', label: t('countries.fr') },
      ],
    },
  });

Server-Driven Localized Forms

Form configs are plain serializable data, so the server can resolve translations, reorder fields, or add region-specific fields per locale before sending the config down:

// server: generate config with the right locale
const formConfig = generateFormConfig(locale);
return Response.json(formConfig);
import { form } from '@rilaykit/forms';

// client: deserialize and render
const config = await fetch('/api/form?locale=fr').then(r => r.json());
const contactForm = form.create(rilay, 'contact').fromJSON(config);

Localized Workflow Steps

The same pattern applies to workflows. Each step wraps a form config; localize step titles and nested field labels through the same translation function, memoized on the locale:

import { useMemo } from 'react';
import { required } from '@rilaykit/core';
import { form } from '@rilaykit/forms';
import { flow } from '@rilaykit/workflow';
import { useTranslation } from 'react-i18next';

function useLocalizedOnboarding() {
  const { t, i18n } = useTranslation();

  return useMemo(() => flow.create(rilay, 'onboarding', t('onboarding.title'))
    .step({
      id: 'account',
      title: t('onboarding.steps.account'),
      formConfig: form.create(rilay, 'account').add({
        id: 'email',
        type: 'input',
        props: { label: t('form.email') },
        validation: { validate: [required(t('validation.required'))] },
      }),
    }),
    [i18n.language]
  );
}

RTL Support

RTL is purely a CSS/HTML concern handled by your renderers. dir="auto" lets the browser pick the direction per field; for full RTL layouts set dir="rtl" on a parent or <html> element:

import type { ComponentRenderContext } from '@rilaykit/core';

function Input({ id, props, field }: ComponentRenderContext<{ label: string }>) {
  return (
    <div dir="auto">
      <label htmlFor={id}>{props.label}</label>
      <input
        id={id}
        value={(field?.value as string) ?? ''}
        onChange={(e) => field?.onChange(e.target.value)}
      />
    </div>
  );
}

Best Practices

  • Rebuild configs on locale changeuseMemo keyed on the locale.
  • Keep validation messages in your i18n files, and use the library's pluralization ("minimum 1 character" vs "minimum 8 characters") instead of string concatenation.
  • Use server-driven configs for SEO-critical forms so search engines see localized content.
  • Test with RTL locales (Arabic, Hebrew) if you support them.

On this page