Improving User Messages
Better Messages Please
The new product owner was determined to make an impression on the site as quickly as possible, so she produced a list of "quick wins" for us to work on. Number one on her list was the quality of our error messages.
Like pretty much every piece of software, our error messages suck. When users enter data, we validate it and tell them if there were any problems. We display messages like "1 error(s) were found". It's down to programmer laziness of course. And the wrong kind of laziness at that. But it seemed to be a simple enough thing to fix.
I moved the ticket into "in progress" and reached for Lingua::EN::Inflect, only to find something interesting: Lingua::EN::Inflect is now in maintenance mode and has been superceeded by a new module called Lingua::EN::Inflexion. The notice in Lingua::EN::Inflect says that the new module "offers a cleaner and more convenient interface, has many more features (including plural->singular inflexions), and is also much better tested". It's written by Damian Conway, so those claims seem likely to be accurate. I settled down to read the documentation.
Singulars and Plurals
The problem with the message is that it doesn't know whether it needs to use singular or plural versions of the words in the text. Lingua::EN::Inflexion makes it easy to start with a singular noun and get the plural version.
Which gives us:
cow -> cows pig -> pigs sheep -> sheep
We can also go in the other direction.
cows -> cow pigs -> pig sheep -> sheep
You can also ask the object whether its invocant was singular or plural.
This will tell us:
cow is singular pigs is plural sheep is singular sheep is plural
Notice that, unsurprisingly, "sheep" is both singular and plural.
You can also use the
as_regex() to get a regex that matches both the singular and plural versions of the noun.
$& will contain "cows", not "cow". And it's even cleverer than that.
Because "kine" is an obscure old plural for "cow".
If you print the value returned from
noun('cow')->as_regex, you'll see that it is
(?^i:kine|cows|cow) - so it's case insensitive too.
Verbs and Adjectives
Lingua::EN::Inflexion doesn't just work on nouns. You can inflect verbs and adjectives too using the
The plural of is is are The plural of has is have The plural of sits is sit
The plural of my is our The plural of your is your The plural of his is their
For more complex requirements, you can get the present participle, past tense and past participle of verbs.
Building a Message
So now we have all of the pieces that we need to construct our message. We need to know how many errors were found. Let's assume that's in
$count. Then our code looks like this:
Now you can start to see why so many systems make do with the terrible messages that we are trying to get rid of here. It's just too complicated to write code like this every time you want to display a message to the user.
You can try to make it a little simpler, I suppose.
But it's really not that much better. There has to be a better way. And, of course, that's really why I'm writing this article.
Lingua::EN::Inflexion exports four routines. We've seen three of them (
adj()). The fourth one is called
inflect() and that's the one which solves our problem and gives us our "quick win".
The subroutine takes a single string argument, where the string contains some special markup defining how you want the string processed. This string is expanded into a new string which is then returned.
In the simplest case, you would use it like this:
The output is:
0 errors were found 1 error was found 2 errors were found 3 errors were found
Simply by changing the number that is interpolated into the string, the noun and verb have both been changed appropriately.
We have used three of
inflect()'s special mark-up tags here.
<#:...> sets and displays the number which will be used in the rest of the output and
<V:...> can be used to insert nouns and verbs which will be inflected. There's a fourth tag,
<A:...> which can be used for adjectives, as you can see in this (slightly contrived) example.
Which produces the following output:
The report had 1 author my recommendations are ... The report had 2 authors our recommendations are ... The report had 3 authors our recommendations are ...
Improving the Output
This is already much better than the output than you get from many programs, but there are easy ways to make it better. We can start by displaying "No" rather than "0". This is achieved by adding an
n option to the
<#:...> tags. Options are added between the command character (the
A) and the colon.
This gives us:
no errors were found 1 error was found 2 errors were found 3 errors were found
Some people prefer that zero items are displayed as singular rather than plural. We can accommodate that by using
s instead of
The output then changes to:
no error was found 1 error was found 2 errors were found 3 errors were found
If you would rather have "a" or "an" when the count is one, then you can use
a (and you can stack options, so you can use it in addition to either
Which gives us:
no errors were found an error was found 2 errors were found 3 errors were found
It's quite common to spell out the numbers when the count is small. If you use the
w option, then
inflect() will do that for numbers up to 10.
The output is:
no errors were found an error was found two errors were found nine errors were found ten errors were found 11 errors were found
Finally, you can use
f to get fuzzy descriptions of the numbers.
Which gives us:
no errors were found one error was found a couple of errors were found a few errors were found several errors were found many errors were found
Finally - A Real Quick Win
Users have become used to the "1 error(s) was found" style of message because it is ubiquitous. And that's because fixing the problem isn't exactly hard, it's just tedious. There's too much code to write to fix one small niggle in the user experience. Mostly, we think it's not worth the effort.
inflect() subroutine fixes that. Now getting it right is as trivial as it should be. That's the right kind of laziness. Damian was obviously getting bored of writing all of that code every time he wanted a grammatically satisfying user message, so he decided to fix the problem by writing a solution that he (and, through the wonder of CPAN, everyone else) could use to easily produce vastly improved messages.
So our product manager got her quick win. And now yours can too.
And your users will get a much better user experience.