robisim74/qwik-speak

how to inline translate in Zod schema?

Closed this issue · 3 comments

I wonder how to useTranslate out of component$

/* eslint-disable @typescript-eslint/no-unused-vars */
import { $, component$ } from '@builder.io/qwik';
import { routeLoader$, z } from '@builder.io/qwik-city';
import type { InitialValues, SubmitHandler } from '@modular-forms/qwik';
import { formAction$, useForm, zodForm$ } from '@modular-forms/qwik';

const loginSchema = z.object({
  email: z
    .string()
    .min(1, 'Please enter your email.') // =====> how to get localized message  out of component ?
    .email('The email address is badly formatted.'),
  password: z
    .string()
    .min(1, 'Please enter your password.')
    .min(8, 'You password must have 8 characters or more.'),
});
type LoginForm = z.infer<typeof loginSchema>;

export const useFormLoader = routeLoader$<InitialValues<LoginForm>>(() => ({
  email: '',
  password: '',
}));

// loginSchema can not move into component$ because useFormLoader refers loginSchema and should exported see https://qwik.builder.io/docs/route-loader/

export default component$(() => {
  const [loginForm, { Form, Field }] = useForm<LoginForm>({
    validate: zodForm$(loginSchema),
  });
 return <Form></Form>;
});

inlineTranslate function needs to get locale context , also not work out of component

duplicate #54

Hi @hafeyang ,

dynamic values in Zod schema have some known issues: fabian-hiller/modular-forms#38

So a valid TypeScript solution like this will not work:

// Transform in function
const loginSchema = (ctx: SpeakState) => z.object({
  email: z
    .string()
    .min(1, inlineTranslate('form.email@@Please enter your email', ctx))
    .email(inlineTranslate('form.emailFormatted@@The email address is badly formatted', ctx))
});
// Use ReturnType
type LoginForm = z.infer<ReturnType<typeof loginSchema>>;

export const useFormLoader = routeLoader$<InitialValues<LoginForm>>(() => ({
  email: '',
  password: '',
}));

export default component$(() => {
  const ctx = useSpeakContext();

  const [loginForm, { Form, Field }] = useForm<LoginForm>({
    validate: zodForm$(() => loginSchema(ctx)),
    loader: useFormLoader()
  });

  return <Form>
    <Field name="email">
      {(field, props) => (
        <div>
          <input {...props} type="email" value={field.value} />
          {field.error && <div>{field.error}</div>}
        </div>
      )}
    </Field>
    <button type="submit">Login</button>
  </Form>;
});

I think the best and clearest solution is for the schema to do the schema, and translate into the html:

Solution A

// Use only the keys in the schema
const loginSchema = z.object({
  email: z
    .string()
    .min(1, 'form.email@@Please enter your email')
    .email('form.emailFormatted@@The email address is badly formatted')
});
type LoginForm = z.infer<typeof loginSchema>;

export const useFormLoader = routeLoader$<InitialValues<LoginForm>>(() => ({
  email: '',
  password: '',
}));

export default component$(() => {
  const t = useTranslate();

  const [loginForm, { Form, Field }] = useForm<LoginForm>({
    validate: zodForm$(loginSchema),
    loader: useFormLoader()
  });

  return <Form>
    <Field name="email">
      {(field, props) => (
        <div>
          <input {...props} type="email" value={field.value} />
          {field.error && <div>{t(field.error)}</div>} {/* Translate where the translation is displayed */}
        </div>
      )}
    </Field>
    <button type="submit">Login</button>
  </Form>;
});

In this case you have to assign form asset to runtimeAssets: it will not be inlined, but requested only when needed.

Alternatively, to inline the translation, you'd have to drop the schema and put the validation inside the html:

Solution B

type LoginForm = {
  email: string;
  password: string;
};

export const useFormLoader = routeLoader$<InitialValues<LoginForm>>(() => ({
  email: '',
  password: '',
}));

export default component$(() => {
  const t = useTranslate();

  const [loginForm, { Form, Field }] = useForm<LoginForm>({
    loader: useFormLoader()
  });

  return <Form>
    <Field name="email"
      validate={[
        required(t('form.email@@Please enter your email')),
        email(t('form.emailFormatted@@The email address is badly formatted')),
      ]}>
      {(field, props) => (
        <div>
          <input {...props} type="email" value={field.value} />
          {field.error && <div>{field.error}</div>}
        </div>
      )}
    </Field>
    <button type="submit">Login</button>
  </Form>;
});

thanks a lot!