Search code examples
lualocalizationformattingrobloxluau

Formatting strings with plurals for localization


I have a simple command that I want to localize : "Select 5 objects". I had thought that I might template this as : "Select %d %s", where the number and objects might be formatted later. But this raised the question of how do you appropriately pluralize a string?

When the object can have multiple plurals :

  • 0 : no objects
  • 1 : 1 object
  • 2 or more : 2 objects

What is a good way to structure your string templates to accommodate for optional plurality? This is especially important for localization later, so I'm trying to be smart about it now.

Reading online, a lot of the top hits for localization best practices rely on Apple's NSLocalizedString library, which allow for string dictionaries with custom rules around the cardinality of the word. But is there a good way to handle this when all of my strings are loaded into a CSV? Roblox provides a TranslationService that comes with a Translator class that can format strings by keys. So I've structured my resources like this :

localization.csv

Key, Example, en, fr, es
Command.SelectTemplate, "Select 3 apples", "Select {total : number} {thing : string}", "Sélectionnez {total : number} {thing : string}" "Selecciona {total : number} {thing : string}"
Object.AppleZero, "there are zero apples", "apples", "pommes", "manzanas"
Object.AppleOne, "there is one apple", "apple", "pomme", "manzana"
Object.AppleFew, "there are a few apples", "apples", "pommes", "manzanas"

example Script

local LocalizationService = game:GetService("LocalizationService")

local LOCALE = "en"

local res, translator = pcall(function()
    return LocalizationService:GetTranslatorForLocaleAsync(LOCALE)
end)
if not res then
    warn(string.format("Failed to load the translator with error %s", tostring(translator)))
    return
end

local function getSelectionString(numToSelect : number, objectKey : string)
    -- TODO : FIND A BETTER WAY TO DO THIS
    assert(numToSelect >= 0, "Cannot select a negative number")
    local lookupKey
    if numToSelect == 0 then
        lookupKey = objectKey .. "Zero"
    elseif numToSelect == 1 then
        lookupKey = objectKey .. "One"
    else
        lookupKey = objectKey .. "Few"
    end

    local objectString = translator:FormatByKey(lookupKey)
    local formattingArgs = {
        total = numToSelect,
        thing = objectString,
    }
    local commandString = translator:FormatByKey("Command.SelectTemplate", formattingArgs)

    return commandString
end

-- Test it out
local objectKey = "Object.Apple"
print(getSelectionString(0, objectKey)) -- "Select 0 apples"
print(getSelectionString(1, objectKey)) -- "Select 1 apple"
print(getSelectionString(2, objectKey)) -- "Select 2 apples"

While this technically works, it requires defining multiple keys for every single object that may have plurality in other languages, and it assumes a very English way pluralizing objects. So to reiterate my question from earlier, what is a good way to structure string templates to accommodate for optional plurality?


Solution

  • One of the best ways of handling plurals is ICU message syntax. ICU means International Components for Unicode – a widely used set of C/C++, Java, and other libraries providing Unicode and globalization support for software and applications.

    ICU format allows you to create user-friendly texts that combine the use of different plural, gender, date and time forms in one string, which will vary depending on who is the user. For example, “Sam sent you 2 messages” and “Emma sent you 1 message”.

    For example:

    You have {count, plural,
      =0 {no apples}
      one {one apple}
      other {# apples}
     }
    

    You can store these strings in different file formats, even in CSV. Different languages have different pluralization rules and different numbers of plural forms. So translators should translate ICU strings according to the target language rules.

    Also, there are some localization file formats that have native plurals support, for example, Android XML, iOS Strings.

    Modern Localization Management platforms support the ICU syntax. For example, Crowdin. Here you can read more about ICU Message Syntax support in Crowdin. In addition - The Ultimate Guide to ICU Message Format.