用户消息中的多元性

107

很多时候,当生成要向用户显示的消息时,该消息将包含一些内容,我希望告知客户。

我举个例子:客户从1个或更多项中选择了许多项目,并单击了删除。现在,我想给客户一个确认消息,并且我想提到他所选的项目数量,以最小化他选择了一堆项目并在只想删除其中一个项目时误点删除的可能性。

一种方法是将通用消息设计如下:

int noofitemsselected = SomeFunction();
string message = "You have selected " + noofitemsselected + " item(s). Are you sure you want to delete it/them?";

这里的“问题”出现在noofitemselected为1的情况下,我们需要写itemit而不是itemsthem

我通常的解决方案是这样的:

int noofitemsselected = SomeFunction();
string message = "You have selected " + noofitemsselected + " " + (noofitemsselected==1?"item" : "items") + ". Are you sure you want to delete " + (noofitemsselected==1?"it" : "them") + "?";

如果代码中有很多对数字复数的引用,那么生成类似这样的消息就会变得非常冗长和丑陋,并且实际消息会变得难以阅读。

所以我的问题很简单。是否有任何更好的方式来生成这样的消息?

编辑

我看到很多人都被我提到消息框中显示消息这个情境所困扰了,他们只是给出了如何避免使用消息框的答案,这也很好。

但请记住,复数问题也适用于程序中其他位置的文本。例如,显示在显示选定网格中的行数的标签旁边的网格中,将具有与复数化相关的相同问题。

因此,这基本上适用于从程序以某种方式输出的大多数文本,那么解决方案并不像只需更改程序以不再输出文本那么简单 :)


6
我不确定每种语言的复数形式是否像“item(s)”一样容易表达。 - Jens
5
你可能应该考虑本地化,这个问题可能会变得更加严重。 - Alex Brown
4
@Jens:通常它们不会这样做,而且方式也往往不太方便。有些语言例如在复数形式上会区分2-4和5-无限大的情况,并且这一切还要取决于性别。 "自然语言:本地化疯狂!即将登陆您身边的电脑!" - Piskvor left the building
29
对于程序员来说,没有什么比错误的复数形式更能让用户感觉他们懒惰了。 - Greg
4
@Øyvind:你的编辑做得很好。这里似乎有许多“回答者”更关注证明你的问题是错误的,而不是提供正确的解决方案。可以说是反解决方案。 - mwolfe02
显示剩余3条评论
25个回答

94

你可以通过删除项目并且提供一个非常好的“撤销”功能来避免所有这些混乱的复数。因为用户从来不会读任何东西,所以你应该在程序中建立一个好的撤消机制

当你创建一个全面的撤销机制时,实际上会得到两个好处。第一个好处是让用户的生活更容易,他/她可以撤消错误并减少阅读量。第二个好处是,你的应用程序反映了现实生活,允许撤消非平凡的工作流程(而不仅仅是错误)。

我曾经写过一款应用程序,完全没有使用对话框或确认消息。它需要认真思考,比使用确认类型的消息要难得多。但是,根据最终用户的测试结果,使用起来相当不错。


12
由于某种原因,我真的觉得我同意这个观点并应该点赞它。 - Cody Gray
4
@Øyvind,原因如下:要求用户进行验证的消息对用户来说是昂贵的,特别是当它们以模态对话框的形式呈现时,直到用户回答对话框之前,一切都会停止。这会让很多用户感到烦恼,以至于他们开始只是点击任何按钮来跳过对话框(你有多少次只是点击“下一步,下一步,下一步”来安装程序?)。因此,如果您只是执行软删除并提供撤消功能,则更安全,并且用户摩擦更小。 Gmail使用这种技术效果非常好。 - Robert Harvey
5
有争议,但无论如何,在除了删除确认消息之外的其他情况下,同样的问题也会出现,因此这个答案与问题实际上不太相关。 - Jay
50
我未读便点赞了这个。我随时可以取消。 - Steven A. Lowe
6
可能是传说,但在很多年前我读过一篇文章,称Lotus曾为Mac发行了40,000份Lotus Notes,但有60,000份被退回。界面太糟糕了,甚至连那些盗版的人也退回了软件。 - Duncan
显示剩余22条评论

53

如果这个应用程序将来有可能需要翻译成其他语言,无论机会有多小,那么这两种方法都是错误的。正确的做法是:

string message = ( noofitemsselected==1 ?
  "You have selected " + noofitemsselected + " item. Are you sure you want to delete it?":
  "You have selected " + noofitemsselected + " items. Are you sure you want to delete them?"
);
这是因为不同的语言处理复数形式的方式不同。例如,像马来语这样的语言甚至没有句法复数形式,因此这些字符串通常是相同的。将这两个字符串分开使得以后更容易支持其他语言。
否则,如果这个应用程序旨在被广泛使用并且应该用户友好,则第二种方法更可取。很抱歉我真的不知道有更简短的方法可以做到这一点。
如果这个应用程序仅在您公司内部使用,则可以使用缩写“item(s)”的方法。编写企业代码时,您不必给任何人留下深刻印象。但我建议不要在公共应用程序中这样做,因为这会给人留下程序员懒惰的印象,从而降低他们对应用程序质量的评价。相信我,这些小事情很重要。

35
虽然我同意这个前提,但您忽略了具有超过两种复数形式的语言。 (以俄语为例。根据所描述的事物是单数、小于5还是大于等于5,有三种不同的表达方式,甚至这也取决于你在谈论什么)。基本上我想说您需要更强的条件语句而不仅仅是三目运算符。 - crasic
4
在这个例子中,你甚至可以优化掉noofitemsselected的第一个选项,因为你知道它是1。 - default
16
顺便提一下,希伯来语在表示复数时会对1、2、3-10以及11以上的数字采用不同的形式。 - Joel Spolsky
3
@Joel,希伯来语可以有这么多不同的复数形式吗? לא ידעתי(我不知道)!现代希伯来语的复数形式与英语类似(尽管古代希伯来语也有双数形式)。 - Ken Bloom
3
在英语中,零会被当作复数处理(0个物品、2个物品);在希伯来语、俄语和罗马尼亚语中,零会发生什么情况?是否有一种简单的方法来确定范围?我猜这些信息必须针对每种语言进行确定,所以任何基于消息文件的系统都必须处理一组消息中不同数量的复数形式。设置翻译也会很棘手——不同的语言需要不同数量的消息。 - Jonathan Leffler
显示剩余9条评论

39

那就这样:

string message = "Are you sure you want to delete " + noofitemsselected + " item(s)?"

这样做,您可以消除数量协议的困难,并获得更简洁、更直接的错误信息,这对用户来说是额外的好处。我们都知道用户不会读错误消息。它们越短,用户就越有可能浏览文本。

或者,凭借这种用户不会读取错误消息的知识,您可以采用不同的方法来处理。跳过确认消息,只提供一个撤销功能,无论删除了什么内容,都可以正常工作。大多数用户已经习惯了在注意到所操作内容与期望不符时撤销操作,他们很可能认为这种行为比另一个烦人的弹出窗口更自然。


1
好的回答。然而,在这种情况下,客户要求为许多情况提供错误消息,因为他认为这是最好的方法。但更改文本至少可以最小化问题并使其变得不那么混乱。 - Øyvind Bråthen
@Øyvind:好的,说得对。既然你必须遵循客户的要求,我建议你可以采用我提出的第一种方法,或者使用一个带有处理函数的资源文件。我只是觉得值得提一下另一种选择,因为它很容易被忽视。天哪,我甚至在提交了我的第一个回答之后才想到这个方法。 - Cody Gray
欢迎提供其他选择。这也是我给它+1的原因 :) - Øyvind Bråthen
1
+1 是为了不试图解决无法解决的问题。并且记住史蒂夫·克鲁格的法则:删除一半的单词。然后再做一遍。我使用的是:“删除{x}个项目?” - Serge Wautier

20

Java有多年历史的解决方案:java.text.MessageFormat和ChoiceFormat?请参阅http://download.oracle.com/javase/1.4.2/docs/api/java/text/MessageFormat.html获取更多信息。

MessageFormat form = new MessageFormat("The disk \"{1}\" contains {0}.");
form.applyPattern(
   "There {0,choice,0#are no files|1#is one file|1<are {0,number,integer} files}.");

Object[] testArgs = {new Long(12373), "MyDisk"};

System.out.println(form.format(testArgs));

// output, with different testArgs
output: The disk "MyDisk" are no files.
output: The disk "MyDisk" is one file.
output: The disk "MyDisk" are 1,273 files.
在您的情况下,您想要一些更简单的东西:
MessageFormat form = new MessageFormat("Are you sure you want to delete {0,choice,1#one item,1<{0,number.integer} files}?");

这种方法的优点在于它与i18n包很好地配合,您可以为没有单数或复数概念的语言(如日语)正确提供翻译。


1
这种方法适用于处理复数,但遗憾的是无法处理性别。您需要为每种可删除的事物准备不同的字符串。 - Mr. Shiny and New 安宇
如果你知道名词,你可以用正确的形容词构造句子。但是,如果你学过古希腊语,你会根据阳性、阴性或中性名词有不同的单词形式(形容词采取修改的名词形式),并且其形式基于其在句子中的功能(主语、宾语等)而不同。我们总能找到一些解决方案有效而另一些无效的情况。这是Java人员尝试解决问题的方式。你仍然需要小心地制定消息格式的方式。 - Berin Loritsch
您可以始终向Java解决方案提供两个整数,其中第二个整数表示性别。(但实际上,性别协议通常不是一个问题,因为将不同的对象适配到消息中的用例比将相同对象的不同数量适配到消息中的用例更少见) - Ken Bloom

12

我建议不要将消息硬编码在代码中,而是提供两个消息在一个单独的资源文件中。例如:

string DELETE_SINGLE = "You have selected {0} item. Are you sure you want to delete it?";
string DELETE_MULTI = "You have selected {0} items. Are you sure you want to delete them?";

然后将它们传递给String.Format,例如:

if(noofitemsselected == 1)
    messageTemplate = MessageResources.DELETE_SINGLE;
else
    messageTemplate = MessageResources.DELETE_MULTI;

string message = String.Format(messageTemplate, noofitemsselected)

我认为这种方法更易于本地化和维护。所有UI消息都将位于单个位置。


+1,我喜欢这种方法。不过,你需要跟踪资源名称的等效项。 - default

11

您可以通过以不同的措辞表达信息来完全避开这个问题。

string message = "The number of selected items is " + noofitemsselected + ". Are you sure you want to delete everything in this selection?";

10

我建议的第一步是:使用 string.Format。 这可以让你像这样做:

int numOfItems = GetNumOfItems();
string msgTemplate;
msgTemplate = numOfItems == 1 ? "You selected only {0} item." : "Wow, you selected {0} items!";
string msg = string.Format(msgTemplate, numOfItems);

此外,在 WPF 应用程序中,我看到过一些系统将资源字符串使用管道分隔符分为两个消息:单数消息和复数消息(甚至是零/单个/多个消息)。然后可以使用自定义转换器来解析此资源并使用相关的(格式化)字符串,因此您的 Xaml 如下所示:

<TextBlock Text="{Binding numOfItems, Converter={StaticResource c:NumericMessageFormatter}, ConverterParameter={StaticResource s:SuitableMessageTemplate}}" />

7

对于英文,上面已经有很多答案了。对于其他语言来说,复数形式取决于名词的性别和词尾。以下是一些法语例子:

常规阳性:

Vous avez choisi 1 compte. Voulez-vous vraiment le supprimer.
Vous avez choisi 2 comptes. Voulez-vous vraiment les supprimer.

常规女性

Vous avez choisi 1 table. Voulez-vous vraiment la supprimer.
Vous avez choisi 2 tables. Voulez-vous vraiment les supprimer.

不规则的阳性词(以's'结尾)

Vous avez choisi 1 pays. Voulez-vous vraiment le supprimer.
Vous avez choisi 2 pays. Voulez-vous vraiment les supprimer?

大多数拉丁语言中都存在同样的问题,而在德语或俄语中情况变得更糟,因为它们有三种性别(阳性、阴性和中性)。

如果您的目标是处理除英语以外的其他语言,则需要注意。


至少对我来说,这并不是一个真正的问题。对于我想插入数字的每个文本,当生成字符串时,我将知道名词是什么。但是为了使解决方案更加通用,我同意你的观点,让它适用于“所有”语言可能是一项非常困难,甚至是不可能的任务。 - Øyvind Bråthen
1
或者以波兰语为例,名词的复数形式取决于数量而不同! :/ - UpTheCreek

5
为了能够拥有复数消息并且能够正确本地化,我认为首先需要在数字和消息之间创建一个间接层。例如,使用某种常量来指定要显示的消息。使用一些函数来获取消息,以隐藏实现细节。
get_message(DELETE_WARNING, quantity)

接下来,创建一个字典,保存可能的消息和变化,并让变化知道何时应该使用。

DELETE_WARNING = {
   1: 'Are you sure you want to delete %s item',
   >1: 'Are you sure you want to delete %s items'
   >5: 'My language has special plural above five, do you wish to delete it?'
}

现在你可以简单地找到与quantity相对应的键,并用该消息插值quantity的值。
这只是一个过度简化和幼稚的例子,但我真的没有看到其他理智的方法来做到这一点,并能够为L10N和I18N提供良好的支持。

5
你需要将以下函数从VBA翻译为C#,但你的使用方法会改变为:
int noofitemsselected = SomeFunction();
string message = Pluralize("You have selected # item[s]. Are you sure you want to delete [it/them]?", noofitemsselected);

我有一个在MS Access中使用的VBA函数,可以完全实现你所说的功能。虽然我知道发布VBA代码可能会被批评,但还是放上来了。从注释中应该能看出算法:

'---------------------------------------------------------------------------------------'
' Procedure : Pluralize'
' Purpose   : Formats an English phrase to make verbs agree in number.'
' Usage     : Msg = "There [is/are] # record[s].  [It/They] consist[s/] of # part[y/ies] each."'
'             Pluralize(Msg, 1) --> "There is 1 record.  It consists of 1 party each."'
'             Pluralize(Msg, 6) --> "There are 6 records.  They consist of 6 parties each."'
'---------------------------------------------------------------------------------------'
''
Function Pluralize(Text As String, Num As Variant, Optional NumToken As String = "#")
Const OpeningBracket = "\["
Const ClosingBracket = "\]"
Const DividingSlash = "/"
Const CharGroup = "([^\]]*)"  'Group of 0 or more characters not equal to closing bracket'
Dim IsPlural As Boolean, Msg As String, Pattern As String

    On Error GoTo Err_Pluralize

    If IsNumeric(Num) Then
        IsPlural = (Num <> 1)
    End If

    Msg = Text

    'Replace the number token with the actual number'
    Msg = Replace(Msg, NumToken, Num)

    'Replace [y/ies] style references'
    Pattern = OpeningBracket & CharGroup & DividingSlash & CharGroup & ClosingBracket
    Msg = RegExReplace(Pattern, Msg, "$" & IIf(IsPlural, 2, 1))

    'Replace [s] style references'
    Pattern = OpeningBracket & CharGroup & ClosingBracket
    Msg = RegExReplace(Pattern, Msg, IIf(IsPlural, "$1", ""))

    'Return the modified message'    
    Pluralize = Msg
End Function

Function RegExReplace(SearchPattern As String, _
                      TextToSearch As String, _
                      ReplacePattern As String) As String
Dim RE As Object

    Set RE = CreateObject("vbscript.regexp")
    With RE
        .MultiLine = False
        .Global = True
        .IgnoreCase = False
        .Pattern = SearchPattern
    End With

    RegExReplace = RE.Replace(TextToSearch, ReplacePattern)
End Function

由于上面的代码注释中使用不够清晰,因此我将在此重复一遍:

Msg = "There [is/are] # record[s]. [It/They] consist[s/] of # part[y/ies] each."

Pluralize(Msg, 1) --> "There is 1 record.  It consists of 1 party each."
Pluralize(Msg, 6) --> "There are 6 records.  They consist of 6 parties each."

是的,这个解决方案忽略了非英语语言。是否重要取决于您的要求。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接