[2.2.0] International - Make i18n easier, from number formatting to plurals

International, a module for simple language-sensitive number formatting, date & time formatting, relative time formatting and plurals rules.

2.0.0 changelog

The following features are removed to save space but don’t worry, I’m working on an upcoming alternative module that supports these features:

  • Rule-based number formatting
  • Locale text direction and line direction
  • Regional stuff

NumberFormat Format function are re-worked, and locales are handled differently.
It’s now more memory efficient overall, toLocaleString no longer hogs up memory when used multiple times.

Where to get it?

From GitHub, the file is too large to be uploaded here.

Features

toLocaleString

A formatter that supports date formatting, number formatting, list formatting. This is a sugar syntax, for Intl.NumberFormat.new(locale, options):Format(value) if the value is determined to be numeric, Intl.DateFormat.new(locale, options):Format(value), if the value is determined to be a date, and Intl.ListFormat.new(locale, options):Format(value) if the value is determined to be a list.

Locale - Intl.Locale

A powerful locale class, all input are valid as long it’s a valid ITEF BCP 47 tag (no -t- extension, and -x- extension are ignored but are still required to be valid).
It supports the -u- extension.

Number formatting - Intl.NumberFormat

A powerful language sensitive number formatting, even only with the en locale, it has:

  • The ability to zero pad numbers (minimumIntegerDigits)
  • Round to certain dedimal places (minimumFractionDigits, maximumFractionDigits)
  • Round to significant digits (minimumSignificantDigits and maximumSignificantDigits)
  • Currency formatting (style = "currency"), input any valid currency code as the currency option (e.g USD) and it’ll do it right away
  • Percent formatting (style = "percent") considered too
  • Number abbreviation/shortening/compact number (notation = "compact"), oh you can customise the decimal place with (minimumFractionDigits, maximumFractionDigits, minimumSignificantDigits and maximumSignificantDigits)
  • Not just number abbreivation, long compact numbers too (compactDisplay = "long") so thousand instead of K
  • Scientific notation support too.
  • Formatting it to parts for more advanced programmer, so you can it make it look like 1,000.50 or 1.2K with enough knowledge, oh it’s laid out in the similar manner to ECMA 402 (and globalize.js) number part formatting, so if you’re used to ECMA 402, no problem
{
    { type = "integer", value = "1"},
    { type = "group", value = ","},
    { type = "integer", value = "234"},
    { type = "decimal", value = "."},
    { type = "fraction", value = "56"}
}
  • Negaitve number and decimal support
  • Oh, not just that, it also support infinity and nan, and that is language sensitive too.
intl.NumberFormat.new('en'):Format(math.huge) --> ∞
intl.NumberFormat.new('en'):Format(0/0) --> NaN
intl.NumberFormat.new('ar'):Format(0/0) --> ليس رقمًا
  • Unit support too (style = "unit"), with units like inches (unit = "inch"), metres (unit = "meter"), kilograms (unit = "kilogram"), celsius (unit = "celsius"), fahrenehit (unit = "fanreheit") seconds (unit = "second"), etc.
  • More numbering system, it’s not just 123456789, but ١٢٣٤٥٦٧٨٩, 一二三四五六七八九, ௧௨௩௪௫௬௭௮௯, ၁၂၃၄၅၆၇၈၉, etc.
-- With -u- extension and the numberingSystem option, this isn't specific to locale :)
intl.NumberFormat.new('en-u-nu-arab'):Format(12345) --> ١٢,٣٤٥
  • The ability to let you choose to group the digit or not, (useGrouping = false to disable it)

With surprising features:

  • Infinite precision (If BigDecimal or a numeric string inputted)
  • BigNum/BigInteger support (Yes BigNum/BigInteger works here unlike most number formatting modules)
  • In fact a numeric string support, so it’s not limited to double
print(intl.NumberFormat.new('en'):Format('9007199254740993')) --> 9,007,199,254,740,993
-- This module is capable of this :)
print(intl.NumberFormat.new('en'):Format(BigInteger.new(10) ^ 1000)) --> 10,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000
-- Don't forget it's locale aware
print(intl.NumberFormat.new('de'):Format(BigInteger.new(10) ^ 1000)) --> 10.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000

…and be careful, it comes with many quirks, you might not expect expect:

  • For Spanish, Polish, etc. locales 1000 aren’t grouped but 10 000 are
local formatter = intl.NumberFormat.new('es')
print(formatter:Format(1234)) --> 1234
print(formatter:Format(12345)) --> 12.345
  • Additionally, if the notation is compact, regardless of locale, 1000 isn’t grouped but 10 000 are
  • and 1 quadrillion is abbreviated as 1000T instead of 1Q or 1q in the English Locale, and 10 quadrillion is abbreivated as 10,000T.

Date formatting - Intl.DateTimeFormat

A powerful yet flexibile language-sensitive date formatting. It finds the closest pattern and with the so-called smart date format negotiation determined by what value options is inputted.
If no/empty table option is included it defaults to { dateStyle = 'medium' } (keep in mind depending on the options, many date parts might be ignored)

print(intl.DateTimeFormat.new('en'):Format{ year = 2012, month = 3, day = 4, hour = 12, min = 34 }) --> Mar 4, 2012 12:34:00 PM
print(intl.DateTimeFormat.new('en', { dateStyle = "medium" }):Format{ year = 2012, month = 3, day = 4 }) --> Mar 4, 2012
print(intl.DateTimeFormat.new('en', { dateStyle = "full" }):Format{ year = 2012, month = 3, day = 4 }) --> Sunday, March 4, 2012
print(intl.DateTimeFormat.new('en', { month = "short", day = "numeric" }):Format{ year = 2012, month = 3, day = 4 }) --> Mar 4
print(intl.DateTimeFormat.new('en', { hour = "numeric", minute = "2-digit" }):Format{ year = 2012, month = 3, day = 4, hour = 13, minute = 45 }) --> 1:45 PM
-- 12 and 24 hours isn't tied by locale, with -u- extension and the hourCycle and hour12 option :)
print(intl.DateTimeFormat.new('en', { hour12 = false, hour = "numeric", minute = "2-digit" }):Format{ year = 2012, month = 3, day = 4, hour = 13, minute = 45 }) --> 13:45
print(intl.DateTimeFormat.new('en-u-hc-h23', { hour = "numeric", minute = "2-digit" }):Format{ year = 2012, month = 3, day = 4, hour = 13, minute = 45 }) --> 13:45

It doesn’t only support gregorian, it supports the following calendar:

  • Islamic (might be one day off)
  • Japanese
  • Republic of China
  • Buddhist

Relative time formatting - Intl.RelativeTimeFormat

Language sensitive relative time formatting, positive values (and 0) indicates future and negative values indicates past, this doesn’t accept NaN but it does accept Infinity, it supports the following:

  • years
  • quarters
  • months
  • mondays to sundays
  • week
  • days
  • hour
  • minute
  • second

(Unlike intl.NumberFormat, this doesn’t accept BigNum/BigInt, and only accepts number values that’s not NaN or strings that can be converted to number)

local formatter = intl.RelativeTimeFormat.new('en', { numeric = "auto" })
print(formatter:Format(-2, 'day')) --> 2 days ago
print(formatter:Format(-1, 'day')) --> yesterday
print(formatter:Format(0, 'day')) --> today
print(formatter:Format(1, 'day')) --> tomorrow
print(formatter:Format(2, 'day')) --> in 2 days
-- Inserting NaN will throw an error
print(formatter:Format(tonumber('nan'), 'day')) --> Value must not be NaN

Plural rule - Intl.PluralRules

A language-sensitive plural rule handling, can only return the following:

  • zero
  • one
  • two
  • few
  • many
  • other
    In English, only one and other can be returned, one for singular, other for plural.
local englishPlural = intl.PluralRules.new('en');
print(englishPlural:Select(1)) --> one
print(englishPlural:Select(2)) --> other

Summary

  • Complex Locale matcher, and better Locale system.
  • Language-sensitive number formatting with an options to round to decimal placss and significant figures, with compact numbers (both short and long) with BigNum/BigInt support
  • Language-senstiive date formatting with options.
  • Plural rule handling
19 Likes

Hi This Looks Cool But The Post Is Very Large And Messy. I Recommend Moving The Documentation To GitHub Pages. Kampfkarren Does This For DataStore2 And It Looks Good. I Recommend You Try It.

2.0.0 update, I’ve revamped many parts of the module. For anyone using Version 1, here:

Version 1 post

International, a module for i18n. hope you may find this useful.
Preceded by my module, FormatNumber and CLDR.

FormatNumber
             → International
CLDRTools (obsolete)

What’s this?

This is an international module that’s capable of the following in many langauges:

  • Formatting/abbreviating numbers
  • Formatting dates (gregorian calendar only for versions ≤1.0.0a2, only the Gregorian, Republic of China, Japanese and Buddhist for the current version)
  • Formatting lists
  • Getting plurals
  • Getting locale display names

This module takes a lot of space, and is not completely finished.

If you come from FormatNumber module and CLDRTools:

i18n wise:

  • This has been replaced by International Module
  • I suggest you move to this module if you can.

If you’re not doing i18n:

  • I wouldn’t recommend moving to this module, this and even FormatNumber module is overkill and more than enough for this, as this module is heavy

Some question answered

Where did you get all this data from?
Unicode CLDR. I got the json version, you can get the xml version from here too.

Why doesn’t 1000 group on Spanish locale but 10.000 groups?
This has been asked before.
The Minimum Grouping Digits has been set to 2 in the Spanish locale at least according to CLDR Survey Tool.
This can help: http://cldr.unicode.org/translation/-core-data/numbering-systems

Can you make this module group 1000 for Spanish locale?
Sorry, but no. If you are really that desperate, you might want to ask Unicode CLDR to change this, or use my previous module.

Why doesn’t German group 1000 for compact numbers?
Compact numbers (number abbreviation) override Minimum Grouping Digits to 2 if it’s lower than 2, regardless of locale.

Where to get it?

You can get it from GitHub (1.0.0b2)
1.0.0b2: International1.0.0b2.zip (8·3 MiB)
1.0.0b2 (Lighest, only has the en_US, en and root locale): InternationalLightest.rbxm (212·8 KiB)
1.0.0b: International1.0.0b.zip (8.3 MiB)
1.0.0a: International1.0.0a.zip (7·8 MiB)

You are free to remove all (except the root and anything from the core data) the locale data if you want to, that’s something I’d actually kinda encourage as this moudle takes a lot of space :wink:

API

string intl.ToLocaleString(object value, Locale locale, table options) (intl.tolocalestring for versions ≤1.0.0b)
Formats the value depending on the type and locale

-- If the locale argument is nil, it'll get the locale automatically
print(intl.ToLocaleString(1234.56) --> The number will depend on locale

-- The number format most commonly used in English
print(intl.ToLocaleString(1234.56, 'en_US')) --> 1,234.56

-- Germans use full stops and commas the same as we use commas and full stops
print(intl.ToLocaleString(1234.56, 'de')) --> 1.234,56

-- French uses spaces
print(intl.toLocaleString(1234.56, 'fr')) --> 1 234,56

-- Indian English groups digits differently
print(intl.ToLocaleString(1234567, 'en_IN')) --> 12,34,567

-- We know it's the Eastern Arabic numeral
print(intl.toLocaleString(1234.56, 'ar')) --> ١٬٢٣٤٫٥٦

-- We know Burmese numerals too
print(intl.ToLocaleString(1234.56, 'my')) --> ၁,၂၃၄.၅၆

-- Abbreviating numbers too
print(intl.ToLocaleString(12345, 'en', { notation = "compact" })) --> 12K
print(intl.ToLocaleString(1234567, 'de', { notation = "compact" })) --> 1,2 Mio.
-- We can handle this too
print(intl.ToLocaleString(12345, 'ja', { notation = "compact" })) --> 1.2万
-- Prefixes? No problem
print(intl.ToLocaleString(1234, 'sw', { notation = "compact" }) --> elfu 1.2

-- This is normal behaviour!
-- In the es locale, the minimum grouping digits are set to 2!
print(intl.ToLocaleString(1234, 'es')) --> 1234
print(intl.ToLocaleString(12345, 'es')) --> 12.345

-- American English uses the month > day < year order
print(intl.ToLocaleString({ year = 2012, month = 3, day = 4 }, 'en_US')) --> Mar 4, 2012, 12:00:00 AM

-- With the _u_ extension, the hour cycle doesn't have to be tied by the locale
print(intl.ToLocaleString({ year = 2012, month = 3, day = 4 }, 'en_US_u_hc_h23')) --> Mar 4, 2012, 00:00:00

-- British English uses the day < month < year order
print(intl.ToLocaleString({ year = 2012, month = 3, day = 4 }, 'en_GB')) --> 4 Mar 2012, 00:00:00

-- Japanese uses the year > month > day order
print(intl.ToLocaleString({ year = 2012, month = 3, day = 4 }, 'ja')) --> 2012/03/04 0:00:00

-- It doesn't have to be gregorian.
print(intl.ToLocaleString({ year = 2012, month = 3, day = 4 }, 'ja_JP_u_ca_japanese')) --> 平成24年3月4日 0:00:00

-- We supports Buddhist calendar too.
print(intl.ToLocaleString({ year = 2012, month = 3, day = 4 }, 'th_TH'))) --> 4 มี.ค. 2555 00:00:00

string/nil intl.GetType(object intldata)
Gets the type string of the intldata, if it’s not an intldata, it’ll return nil.

print(intl.GetType(NumberFormat.new())) --> NumberFormat
print(intl.GetType(Locale.new('en'))) --> Locale
print(intl.GetType('not an intldata')) --> nil

table intl.GetData(Locale locale, table/Folder/ModuleScript data, bool inherit = true)
Get data of the locales, the key value of the table must be an locale identifier in strings.
If you don’t want the locale parent’s data but want the locale data, there’s an optional inherit argument, you can set to false.

string intl.TestFormat(Locale locale)
Number & date format example

Locale

Locale intl.Locale.new(string language, string script = nil, string region = nil, string variant = nil, table u_extension = { })
Locale intl.Locale.new(table options)
Creates a Locale, out of the paramters. If the language parameter contain _ or -, it’ll be parsed

print(Locale.new('en', 'US')) --> Locale English (United States): en_US
print(Locale.new('zh', 'Hant', 'TW')) --> Locale Chinese (Traditional, Taiwan): zh_Hant_TW
print(Locale.new('de_AT')) --> Locale German (Austria): de_AT

Locale intl.Locale.fromIdentifier(string identifier)
Creates a Locale based on the identifier.

Locale/nil intl.Locale.negotiate(table/list/tuple preferred, table/list/tuple available = all locales)
Finds the best match between available and preferred locale

Locale intl.Locale.root
Returns the root locale

Locale intl.Locale.RobloxLocale
Returns the locale set on Roblox

Locale intl.Locale.SystemLocale
Returns the locale set on the System (not accurate as it uses Roblox’s localisation service)

Locale intl.Locale.GetLocale()
Get the locale

Locale intl.Locale.SetLocale(Locale locale)
Set the locale, so that it’ll automatically fallback to that locale if the locale value is nil

Properties and class methods

string Locale.language
Gets the language of the locale

string Locale.script
Gets the script of the locale

string Locale.region
Gets the region of the locale

string Locale.variant
Gets the variant of the locale

string Locale.u_extension
Gets the u extension of the locale

string Locale.Name
Gets the base name of the locale (no u_extension)

string Locale.uName
Gets the locale name including the u extension

string Locale.EnglishName
Gets the English name of the locale

string Locale.NativeName
Gets the native name of the locale

string Locale.CharacterOrder
The character order of the locale it can return either left-to-right, right-to-left, top-to-bottom or bottom-to-top

string Locale.LineOrder
The line order of the locale, it can return either left-to-right, right-to-left, top-to-bottom or bottom-to-top

table Locale.MeasurementSystemNames
The measurement system names of the locale

Locale Locale.Parent
Gets the parent of the locale

print(Locale.new('de_DE').Parent) --> Locale German: de
print(Locale.new('es_MX').Parent) --> Locale Spanish (Latin America): es_419
-- Roots don't have a parent!
print(Locale.new('root').Parent) --> nil

table Locale:GetChildren()
Gets all the children of the locale, also accpets Locale.GetChildren() (only for version ≤1.0.0a3)

table Locale:GetDescendants()
Gets all the children and its children of the locale, also accepts Locale.GetDescendants() (only for version ≤1.0.0a3)

Regional

These properties will return nil if no region is provided in the locale.

string Locale.MeasurementSystem
The measurement system used in the region of the locale, can either return metric, UK or US.

string Locale.TemperatureSystem
The temperature system used in the region of the locale, can either return metric, UK or US

string Locale.PaperSize
The paper size used in the region of the locale, can either return A4 or US-Letter

number Locale.WeekdayStart
The weekday the locale starts, 1 is Monday and 7 is Sunday.

number Locale.WeekendStart
The weekend of the locale starts, 1 is Monday and 7 is Sunday.

number Locale.WeekendEnd
The weekend of the locale ends, 1 is Monday and 7 is Sunday.

Territory

Terrtiory intl.Territory.new(string territory_code)
Creates a new territory

Territory intl.Territory.World
Territory intl.Territory.Africa
Territory intl.Territory.NorthAmerica
Territory intl.Territory.SouthAmerica
Territory intl.Territory.Oceania
Territory intl.Territory.WesternAfrica
Territory intl.Territory.CentralAmerica
Territory intl.Territory.EasternAfrica
Territory intl.Territory.NorthernAfrica
Territory intl.Territory.MiddleAfrica
Territory intl.Territory.SouthernAfrica
Territory intl.Territory.Americas
Territory intl.Territory.NorthernAmerica
Territory intl.Territory.Caribbean
Territory intl.Territory.EasternAsia
Territory intl.Territory.SouthernAsia
Territory intl.Territory.SoutheastAsia
Territory intl.Territory.SouthernEurope
Territory intl.Territory.Australasia
Territory intl.Territory.Melanesia
Territory intl.Territory.MicronesianRegion
Territory intl.Territory.Polynesia
Territory intl.Territory.Asia
Territory intl.Territory.CentralAsia
Territory intl.Territory.WesternAsia
Territory intl.Territory.Europe
Territory intl.Territory.EasternEurope
Territory intl.Territory.NorthernEurope
Territory intl.Territory.WesternEurope
Territory intl.Territory.SubSaharanAfrica
Territory intl.Territory.LatinAmerica
These are self-explanitory

Properties and class methods

string Territory.MeasurementSystem
The measurement system used in the territory

string Territory.TemperatureSystem
The temperature used in the territory

string Territory.PaperSize
The paper size used in the territory

The following below source are recieved through Unicode CLDR, If you want an accurate source, don’t use these

number Territory.GDP
The GDP of the territory

number Territory.Literacy
The literacy rate of the territory. (1 = 100%, 0.5 = 50%)

number Territory.Population
The population of the territory

table Territory:GetLanguages
Get all languages spoken in that territory

table Territory:GetOfficialLanguages
Gets all official langauges spoken in that territory

NumberFormat

NumberFormat intl.NumberFormat.new(Locale locale, table options)
Creates a NumberFormat userdata

Properties and class methods

NumberFormatParts NumberFormat:FormatToParts(number/BigInteger/BigNum value)
Formats the numbers in parts, to iter NumberFormatParts use NumberFormatParts:iter() instead of pairs(NumberFormatParts)

string NumberFormat:Format(number/BigInteger/BigNum value)
Formats the number based on the NumberFormat

DisplayNames

DisplayNames DisplayNames.new(Locale locale, table options)
Create a new locale display names

Properties and class methods

string DisplayNames:NameOf(Locale locale) (:of for versions ≤1.0.0a3)
Gets the display name of the locale paramter in the locale of the DisplayNames userdata.

DateTimeFormat

DateTimeFormat intl.DateTimeFormat.new(Locale locale, table options)
Create a DateTime format userdata

Properties and class methods

DateTimeFormatParts DateTimeFormat:FormatToParts(DateTime/os.date table date)
Formats the date in parts, to iter DateTimeFormatParts use DateTimeFormatParts:iter() instead of pairs(DateTimeFormatParts)

string DateTimeFormat:Format(DateTime/os.date table date)
Formats the dates based on the DateTimeFormat.

PluralRule

PluralRule intl.PluralRule.new(Locale locale, table options)
Creates a new pluralrule userdata.

Properties and class methods

string PluralRule:Select(number/BigInteger/BigNum value) (:select for versions ≤1.0.0a3)
Gets the plural of the locale, can either return zero, one, two, few, many or other

More parts will be documented later

Version 2.1 update

  • Formatting large numbers is now much faster.
  • useGrouping option is now enumerated, with 4 options (always, auto, min2 and never), true will map to always and false will map to never.
local formatter = Intl.NumberFormat.new('en', { useGrouping = "min2" });
print(formatter:Format(1234)) --> 1234
print(formatter:Format(12345)) --> 12,345
local formatter = Intl.NumberFormat.new('en');
print(formatter:Format(1234)) --> 1,234
print(formatter:Format(12345)) --> 12,345
  • -per- unit support.
  • Currency code is handled differently.

You can get it from here.

Version 2.2 update

  • FormatRange and FormatRangeToParts
  • Intl.toLocaleDateString and Intl.toLocaleTimeString added
  • Many bugs fixed and formatting numbers is faster.

You can get it from here