Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion aftman.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ luau-lsp = "Quenty/luau-lsp@1.58.0-quenty.1"
lune = "lune-org/lune@0.10.5"
mantle = "blake-mealey/mantle@0.10.7"
moonwave-extractor = "UpliftGames/moonwave@1.3.0"
rojo = "quenty/rojo@7.7.0-rc.2-quenty"
rojo = "quenty/rojo@7.7.0-rc.3-quenty"
run-in-roblox = "rojo-rbx/run-in-roblox@0.3.0"
selene = "Kampfkarren/selene@0.29.0"
stylua = "johnnymorganz/stylua@2.3.1"
2 changes: 1 addition & 1 deletion foreman.toml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
[tools]
rojo = { github = "quenty/rojo", version = "=7.7.0-rc.2-quenty" }
rojo = { github = "quenty/rojo", version = "=7.7.0-rc.3-quenty" }
luau-lsp = { github = "quenty/luau-lsp", version = "=1.58.0-quenty.1" }
stylua = { github = "johnnymorganz/stylua", version = "=2.3.1" }
2 changes: 1 addition & 1 deletion games/integration/aftman.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,6 @@
[tools]
luau-lsp = "Quenty/luau-lsp@1.58.0-quenty.1"
moonwave-extractor = "UpliftGames/moonwave@1.3.0"
rojo = "quenty/rojo@7.7.0-rc.2-quenty"
rojo = "quenty/rojo@7.7.0-rc.3-quenty"
selene = "Kampfkarren/selene@0.29.0"
stylua = "johnnymorganz/stylua@2.3.1"
48 changes: 47 additions & 1 deletion pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions src/binder/src/Shared/Binder.lua
Original file line number Diff line number Diff line change
Expand Up @@ -698,6 +698,12 @@ function Binder._add<T>(self: Binder<T>, inst: Instance)
tostring(type(constructor) == "table" and constructor.ClassName or constructor)
)
)
if MaidTaskUtils.isValidTask(class) then
task.spawn(function()
MaidTaskUtils.doTask(class)
end)
end

return
end

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
--!strict
--[=[
Localizes ordinal numbers, for example `22nd` in English, `22e` in French, `22.` in German or
`第22` in Chinese. Rules are keyed by language subtag and resolved through [ResolveLocaleUtils],
so regional variants such as `en-gb` or `es-mx` land on their language's rule. Locales without
a rule fall back to English with a warning.

To add a language, add one entry to `ORDINAL_RULES`. Every field is required.

@class NumberLocalizationOrdinalUtils
]=]

local require = require(script.Parent.loader).load(script)

local ResolveLocaleUtils = require("ResolveLocaleUtils")

local NumberLocalizationOrdinalUtils = {}

local DEFAULT_LOCALE = "en-us"

--[=[
How a language writes an ordinal: `prefix .. number .. suffix(number)`.

@interface OrdinalRule
.prefix string
.suffix (number) -> string
@within NumberLocalizationOrdinalUtils
]=]
export type OrdinalRule = {
prefix: string,
suffix: (number) -> string,
}

function NumberLocalizationOrdinalUtils._constantSuffix(suffix: string): (number) -> string
return function(_number: number): string
return suffix
end
end

function NumberLocalizationOrdinalUtils._englishSuffix(number: number): string
local hundredRemainder = number % 100
if hundredRemainder >= 11 and hundredRemainder <= 13 then
return "th"
end

local tenRemainder = number % 10
if tenRemainder == 1 then
return "st"
elseif tenRemainder == 2 then
return "nd"
elseif tenRemainder == 3 then
return "rd"
else
return "th"
end
end

function NumberLocalizationOrdinalUtils._frenchSuffix(number: number): string
return if number == 1 then "er" else "e"
end

function NumberLocalizationOrdinalUtils._swedishSuffix(number: number): string
local tenRemainder = number % 10
local hundredRemainder = number % 100
local isTeen = hundredRemainder >= 11 and hundredRemainder <= 12
return if (tenRemainder == 1 or tenRemainder == 2) and not isTeen then ":a" else ":e"
end

local ORDINAL_INDICATOR = NumberLocalizationOrdinalUtils._constantSuffix("º")
local TRAILING_PERIOD = NumberLocalizationOrdinalUtils._constantSuffix(".")
local NO_SUFFIX = NumberLocalizationOrdinalUtils._constantSuffix("")

local ORDINAL_RULES: { [string]: OrdinalRule } = {
en = { prefix = "", suffix = NumberLocalizationOrdinalUtils._englishSuffix },
fr = { prefix = "", suffix = NumberLocalizationOrdinalUtils._frenchSuffix },
sv = { prefix = "", suffix = NumberLocalizationOrdinalUtils._swedishSuffix },
nl = { prefix = "", suffix = NumberLocalizationOrdinalUtils._constantSuffix("e") },

-- Masculine ordinal indicator (1º, 2º)
es = { prefix = "", suffix = ORDINAL_INDICATOR },
pt = { prefix = "", suffix = ORDINAL_INDICATOR },
it = { prefix = "", suffix = ORDINAL_INDICATOR },

-- Trailing period (1., 2.)
de = { prefix = "", suffix = TRAILING_PERIOD },
pl = { prefix = "", suffix = TRAILING_PERIOD },
tr = { prefix = "", suffix = TRAILING_PERIOD },
fi = { prefix = "", suffix = TRAILING_PERIOD },
da = { prefix = "", suffix = TRAILING_PERIOD },
nb = { prefix = "", suffix = TRAILING_PERIOD },
no = { prefix = "", suffix = TRAILING_PERIOD },
cs = { prefix = "", suffix = TRAILING_PERIOD },
hu = { prefix = "", suffix = TRAILING_PERIOD },

-- Masculine digit ordinal (1-й, 22-й)
ru = { prefix = "", suffix = NumberLocalizationOrdinalUtils._constantSuffix("-й") },

-- Ordinal prefix (第22, 제22, ke-22, thứ 22, ที่ 22)
zh = { prefix = "第", suffix = NO_SUFFIX },
ja = { prefix = "第", suffix = NO_SUFFIX },
ko = { prefix = "제", suffix = NO_SUFFIX },
id = { prefix = "ke-", suffix = NO_SUFFIX },
vi = { prefix = "thứ ", suffix = NO_SUFFIX },
th = { prefix = "ที่ ", suffix = NO_SUFFIX },

-- Arabic writes digit ordinals as the bare number
ar = { prefix = "", suffix = NO_SUFFIX },
}

for _, rule in pairs(ORDINAL_RULES) do
table.freeze(rule)
end

function NumberLocalizationOrdinalUtils._resolveRuleOrDefault(locale: string?): OrdinalRule
local key = ResolveLocaleUtils.resolveClosestKey(locale, ORDINAL_RULES)
if key then
return ORDINAL_RULES[key]
end

warn(
string.format(
"[NumberLocalizationOrdinalUtils] - No ordinal rule for locale '%s', reverting to '%s' instead.",
tostring(locale),
DEFAULT_LOCALE
)
)
return ORDINAL_RULES[ResolveLocaleUtils.resolveClosestKey(DEFAULT_LOCALE, ORDINAL_RULES) :: string]
end

--[=[
Returns the localized ordinal form of a whole number.

```lua
print(NumberLocalizationOrdinalUtils.localize(22, "en-us")) --> 22nd
print(NumberLocalizationOrdinalUtils.localize(1, "fr-fr")) --> 1er
print(NumberLocalizationOrdinalUtils.localize(22, "zh-cn")) --> 第22
```
]=]
function NumberLocalizationOrdinalUtils.localize(number: number, locale: string): string
assert(type(number) == "number", "Bad number")

local rule = NumberLocalizationOrdinalUtils._resolveRuleOrDefault(locale)
return rule.prefix .. tostring(number) .. rule.suffix(number)
end

--[=[
Returns only the part of the localized ordinal that follows the number, for example `nd` for
22 in English or `.` in German. Languages that mark ordinals with a prefix (Chinese, Japanese,
Korean, Indonesian, Vietnamese, Thai) have an empty suffix; use
[NumberLocalizationOrdinalUtils.localize] for the whole form.
]=]
function NumberLocalizationOrdinalUtils.getSuffix(number: number, locale: string): string
assert(type(number) == "number", "Bad number")

return NumberLocalizationOrdinalUtils._resolveRuleOrDefault(locale).suffix(number)
end

return NumberLocalizationOrdinalUtils
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
--!strict
local require = require(script.Parent.loader).load(script)

local Jest = require("Jest")
local NumberLocalizationOrdinalUtils = require("NumberLocalizationOrdinalUtils")

local describe = Jest.Globals.describe
local expect = Jest.Globals.expect
local it = Jest.Globals.it

local function checkOrdinal(locale: string, responseMapping: { [number]: string })
for input, output in responseMapping do
expect(NumberLocalizationOrdinalUtils.localize(input, locale)).toBe(output)
end
end

describe("NumberLocalizationOrdinalUtils.localize", function()
it("should use st, nd, rd and th in English, including the teens", function()
checkOrdinal("en-us", {
[0] = "0th",
[1] = "1st",
[2] = "2nd",
[3] = "3rd",
[4] = "4th",
[11] = "11th",
[12] = "12th",
[13] = "13th",
[21] = "21st",
[22] = "22nd",
[23] = "23rd",
[101] = "101st",
[111] = "111th",
[112] = "112th",
})
end)

it("should use er for 1 and e otherwise in French", function()
checkOrdinal("fr-fr", { [1] = "1er", [2] = "2e", [21] = "21e", [22] = "22e" })
end)

it("should use the ordinal indicator in Spanish, Portuguese and Italian", function()
checkOrdinal("es-es", { [1] = "1º", [22] = "22º" })
checkOrdinal("pt-br", { [1] = "1º", [22] = "22º" })
checkOrdinal("it-it", { [1] = "1º", [22] = "22º" })
end)

it("should use a trailing period in German, Polish and Turkish", function()
checkOrdinal("de-de", { [1] = "1.", [22] = "22." })
checkOrdinal("pl-pl", { [1] = "1.", [22] = "22." })
checkOrdinal("tr-tr", { [1] = "1.", [22] = "22." })
end)

it("should use e in Dutch and :a / :e in Swedish", function()
checkOrdinal("nl-nl", { [1] = "1e", [22] = "22e" })
checkOrdinal("sv-se", { [1] = "1:a", [2] = "2:a", [3] = "3:e", [11] = "11:e", [12] = "12:e", [21] = "21:a" })
end)

it("should prefix in Chinese, Japanese and Korean", function()
checkOrdinal("zh-cn", { [1] = "第1", [22] = "第22" })
checkOrdinal("zh-tw", { [22] = "第22" })
checkOrdinal("ja-jp", { [22] = "第22" })
checkOrdinal("ko-kr", { [22] = "제22" })
end)

it("should cover the remaining NumberLocalizationUtils languages", function()
checkOrdinal("ru-ru", { [1] = "1-й", [22] = "22-й" })
checkOrdinal("id-id", { [22] = "ke-22" })
checkOrdinal("vi-vn", { [22] = "thứ 22" })
checkOrdinal("th-th", { [22] = "ที่ 22" })
checkOrdinal("ar", { [22] = "22" })
end)

it("should resolve regional variants to their language", function()
checkOrdinal("en-gb", { [22] = "22nd" })
checkOrdinal("es-mx", { [22] = "22º" })
checkOrdinal("fr-ca", { [1] = "1er" })
end)

it("should fall back to English for an unknown locale", function()
checkOrdinal("xx-yy", { [22] = "22nd" })
end)
end)

describe("NumberLocalizationOrdinalUtils.getSuffix", function()
it("should return only the suffix", function()
expect(NumberLocalizationOrdinalUtils.getSuffix(22, "en-us")).toBe("nd")
expect(NumberLocalizationOrdinalUtils.getSuffix(1, "fr-fr")).toBe("er")
expect(NumberLocalizationOrdinalUtils.getSuffix(22, "de-de")).toBe(".")
expect(NumberLocalizationOrdinalUtils.getSuffix(22, "zh-cn")).toBe("")
end)
end)
Loading
Loading