Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Forward all variables to parameterized term #369

Open
pschiffmann opened this issue Apr 25, 2024 · 0 comments
Open

Forward all variables to parameterized term #369

pschiffmann opened this issue Apr 25, 2024 · 0 comments

Comments

@pschiffmann
Copy link

I'm building a React web app, and I'm looking for a way to make form validation error messages extensible and customizable. To do that, I'd like to forward all variables from one Fluent message to another message, similar to how you can spread function arguments into another function call in JS.

-formal-greeting = Good morning, { $user }!
-personal-greeting = Howdy, { $user }!

# `...arguments` passes all variables from the current context to the parameterized term.
# If the `greeting` message had a `$user` variable in scope, it will also be in scope for
# the terms.
greeting = { $mood ->
   [personal] { -personal-greeting(...arguments) }
  *[formal] { -formal-greeting(...arguments) }
}

Use case

My app contains a form where the user can schedule events. The form contains two fields relevant for this use case:

  1. An <input type="email"> for inviting participants. Only members of my organization may be invited, which is validated by a pattern="@example.org$" validator.
  2. An <input type="date"> for the date. Events can only be scheduled within the next two weeks, but not on a weekend, which is enforced by 4 validators: required, min={today}, max={today.add({ days: 14 }), and a custom callback function that checks whether the user input is on a weekend.

I want to keep the error message language consistent with the remaining UI. For this reason I want to translate all error messages with Fluent, and not use the translations provided by the constraint validation API.

Sharing common error messages

Most of my form fields use only default validators, like required, min, and so on. I defined a reusable Fluent message for all of the common errors (I borrowed the texts from the Firefox translations).

default-errors = { $error -> 
   [ValueMissing] Please fill out this field.
   [NumberRangeUnderflow] Please select a value that is no less than { $min }.
   [NumberRangeOverflow] Please select a value that is no more than { $max }.
   [DateRangeUnderflow] Please select a value that is no earlier than { $date }.
   [DateRangeOverflow] Please select a value that is no later than { $date }.
   [TooShort] Please use at least { $minLength } characters (you are currently using { $length } characters).
   [TooLong] Please shorten this text to { $maxLength } characters or less (you are currently using { $length } characters).
   [PatternMismatch] Please match the requested format.
   [EmailMismatch] Please enter an email address.
   [UrlMismatch] Please enter a URL.
  *[other] Unknown error ({ $reason }).
}

Next, I implemented validation functions for these common errors.

type FieldValidationError =
  | { error: "ValueMissing" }
  | { error: "NumberRangeUnderflow"; min: number }
  | { error: "TooShort"; minLength: number; length: number }
  | ...

function required(value: string) {
  return !value ? { error: "ValueMissing" } : null;
}
function pattern(p: RegExp) {
  return (value: string) => !p.test(value) ? { error: "PatternMismatch" } : null;
}
function minLength(min: number) {
  return (value: string) => value.length < min ? { error: "TooShort", min, length: value.length } : null;
}

I wrote a hook that lets me define a form schema, and pass an array of validators to each field.

const today = Temporal.Now.plainDateISO();
const form = useForm({
  eventName: field<string>([required, minLength(3)]),
  participant: field<string>([required, email, pattern(/@example.com$/)]),
  date: field<Temporal.PlainDate>([required, min(today), max(today.add({ days: 14 })]),
});

When the user enters a value, the hook iterates over the validators for that field until one validator returns a non-null value. That error object is then passed to Fluent like this:

function translateError(error: FieldValidationError, bundle: FluentBundle, messageId = "common-errors") {
  const pattern = bundle.getMessage(messageId)!.value!;
  return bundle.formatPattern(pattern, error, []); // Pass the error object as `args`, exposing all error properties to the Fluent message.
}

For common error messages, this system works fine.

Customizing the email pattern error message

So far, when a user tries to invite an email address that doesn't end in @example.com, they see the generic error message Please match the requested format. I want to override the error message for this one field only with a custom message. Here is how that could work with spread arguments:

participant-field-errors = { $error ->
   [PatternMismatch] You can only invite @example.com emails.
  *[other] { common-errors(...arguments) }
}

If the user input is rejected by the pattern validator, I can display a custom message. But if the user input is rejected by the required or email validator, I display the default error message.

Implementing the "not-on-weekends" validator

Events mustn't be scheduled on weekends. This can be implemented with a custom validator.

function weekdaysOnly(value: Temporal.PlainDate) {
  return value.dayOfWeek >= 6 ? { error: "Weekend" } : null;
}

And here is the corresponding Fluent message:

date-field-errors = { $error ->
   [Weekend] Please select a weekday.
  *[other] { common-errors(...arguments) }
}

In this situation, it's important to pass all variables from the current scope to the other message, because the DateRangeUnderflow message needs access to the $date variable.

How to proceed?

I'm aware that Fluent is more or less in maintenance mode at the moment. I'm also aware that even passing single variables to terms is still an open discussion. Because of this, I don't expect a short-term decision or even implemention for this feature request.

My main goal is to point out this use case. Hopefully, I was able to demonstrate that this feature has a legitimate use case, and I hope it will be considered when Fluent development picks up again. I'm happy to contribute to the JS implementation and documentation when you're ready to move this forward.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant