@quenty/metricutils 2.4.4 → 2.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/README.md +80 -2
  3. package/deploy.nevermore.json +10 -0
  4. package/package.json +22 -4
  5. package/src/Shared/Definitions/MetricUnitDefinitions.lua +1082 -0
  6. package/src/Shared/Definitions/MetricUnitDefinitions.spec.lua +55 -0
  7. package/src/Shared/Definitions/MetricUnitDimensionUtils.lua +164 -0
  8. package/src/Shared/Definitions/MetricUnitDimensionUtils.spec.lua +49 -0
  9. package/src/Shared/Definitions/MetricUnitKinds.lua +237 -0
  10. package/src/Shared/Definitions/MetricUnitKinds.spec.lua +51 -0
  11. package/src/Shared/Formatting/MetricUnitDisplayUnitUtils.lua +129 -0
  12. package/src/Shared/Formatting/MetricUnitFormatUtils.lua +924 -0
  13. package/src/Shared/Formatting/MetricUnitFormatUtils.spec.lua +280 -0
  14. package/src/Shared/Formatting/RxMetricUnitFormatUtils.lua +62 -0
  15. package/src/Shared/Localization/MetricUnitFallbackStrings.lua +289 -0
  16. package/src/Shared/Localization/MetricUnitFallbackStrings.spec.lua +82 -0
  17. package/src/Shared/Localization/MetricUnitTranslator/ar.json.txt +182 -0
  18. package/src/Shared/Localization/MetricUnitTranslator/de.json.txt +182 -0
  19. package/src/Shared/Localization/MetricUnitTranslator/en.json.txt +182 -0
  20. package/src/Shared/Localization/MetricUnitTranslator/es.json.txt +182 -0
  21. package/src/Shared/Localization/MetricUnitTranslator/fr.json.txt +182 -0
  22. package/src/Shared/Localization/MetricUnitTranslator/id.json.txt +182 -0
  23. package/src/Shared/Localization/MetricUnitTranslator/init.lua +92 -0
  24. package/src/Shared/Localization/MetricUnitTranslator/it.json.txt +182 -0
  25. package/src/Shared/Localization/MetricUnitTranslator/ja.json.txt +182 -0
  26. package/src/Shared/Localization/MetricUnitTranslator/ko.json.txt +182 -0
  27. package/src/Shared/Localization/MetricUnitTranslator/pl.json.txt +182 -0
  28. package/src/Shared/Localization/MetricUnitTranslator/pt.json.txt +182 -0
  29. package/src/Shared/Localization/MetricUnitTranslator/ru.json.txt +182 -0
  30. package/src/Shared/Localization/MetricUnitTranslator/th.json.txt +182 -0
  31. package/src/Shared/Localization/MetricUnitTranslator/tr.json.txt +182 -0
  32. package/src/Shared/Localization/MetricUnitTranslator/vi.json.txt +182 -0
  33. package/src/Shared/Localization/MetricUnitTranslator/zh-cn.json.txt +182 -0
  34. package/src/Shared/Localization/MetricUnitTranslator/zh-tw.json.txt +182 -0
  35. package/src/Shared/Localization/MetricUnitTranslator.spec.lua +309 -0
  36. package/src/Shared/Localization/PluralCategoryUtils.lua +103 -0
  37. package/src/Shared/Localization/PluralCategoryUtils.spec.lua +77 -0
  38. package/src/Shared/Logarithmic/DecibelUtils.lua +72 -0
  39. package/src/Shared/Logarithmic/DecibelUtils.spec.lua +35 -0
  40. package/src/Shared/MetricUnit.Angles.spec.lua +80 -0
  41. package/src/Shared/MetricUnit.Customary.spec.lua +104 -0
  42. package/src/Shared/MetricUnit.Derived.spec.lua +173 -0
  43. package/src/Shared/MetricUnit.Display.spec.lua +153 -0
  44. package/src/Shared/MetricUnit.Electrical.spec.lua +114 -0
  45. package/src/Shared/MetricUnit.Math.spec.lua +112 -0
  46. package/src/Shared/MetricUnit.Roblox.spec.lua +34 -0
  47. package/src/Shared/MetricUnit.SI.spec.lua +127 -0
  48. package/src/Shared/MetricUnit.Spring.spec.lua +94 -0
  49. package/src/Shared/MetricUnit.Temperature.spec.lua +212 -0
  50. package/src/Shared/MetricUnit.Time.spec.lua +202 -0
  51. package/src/Shared/MetricUnit.Units.spec.lua +210 -0
  52. package/src/Shared/MetricUnit.lua +1730 -0
  53. package/src/Shared/MetricUnit.spec.lua +362 -0
  54. package/src/Shared/MetricUtils.lua +46 -15
  55. package/src/Shared/MetricUtils.spec.lua +76 -0
  56. package/src/Shared/Parsing/MetricUnitParseUtils.lua +799 -0
  57. package/src/Shared/Parsing/MetricUnitParseUtils.spec.lua +189 -0
  58. package/src/Shared/Systems/MetricUnitSystem.lua +322 -0
  59. package/src/Shared/Systems/MetricUnitSystem.spec.lua +181 -0
  60. package/src/Shared/Systems/MetricUnitSystems.lua +225 -0
  61. package/src/jest.config.lua +3 -0
  62. package/src/node_modules.project.json +7 -0
  63. package/test/default.project.json +25 -0
  64. package/test/scripts/Server/ServerMain.server.lua +14 -0
@@ -0,0 +1,280 @@
1
+ --!strict
2
+ --[[
3
+ @class MetricUnitFormatUtils.spec.lua
4
+ ]]
5
+
6
+ local require = require(script.Parent.loader).load(script)
7
+
8
+ local Jest = require("Jest")
9
+ local MetricUnit = require("MetricUnit")
10
+ local MetricUnitFormatUtils = require("MetricUnitFormatUtils")
11
+
12
+ local describe = Jest.Globals.describe
13
+ local expect = Jest.Globals.expect
14
+ local it = Jest.Globals.it
15
+
16
+ -- Formatting puts a no-break space between a number and its unit; these compare it as a plain space
17
+ local function plain(text: string): string
18
+ return (string.gsub(text, "\u{00A0}", " "))
19
+ end
20
+
21
+ local function formatUnit(unit: any, amount: number, options: any?): string
22
+ return plain(MetricUnitFormatUtils.formatUnit(unit, amount, options))
23
+ end
24
+
25
+ local function formatData(value: any, options: any?): string
26
+ return plain(MetricUnitFormatUtils.format(value, options))
27
+ end
28
+
29
+ local function format(value: any, options: MetricUnit.FormatOptions?): string
30
+ return plain(MetricUnit.format(value, options))
31
+ end
32
+
33
+ describe("MetricUnitFormatUtils.formatNumber", function()
34
+ it("rounds to three decimals and groups thousands by default", function()
35
+ expect(MetricUnitFormatUtils.formatNumber(1234.5678)).toBe("1,234.568")
36
+ expect(MetricUnitFormatUtils.formatNumber(-1234.5)).toBe("-1,234.5")
37
+ expect(MetricUnitFormatUtils.formatNumber(2)).toBe("2")
38
+ expect(MetricUnitFormatUtils.formatNumber(-0.0004)).toBe("0")
39
+ end)
40
+
41
+ it("uses the locale's grouping and decimal mark", function()
42
+ expect(MetricUnitFormatUtils.formatNumber(1234.5, { locale = "de-de" })).toBe("1 234,5")
43
+ expect(MetricUnitFormatUtils.formatNumber(1234.5, { locale = "es-es" })).toBe("1.234,5")
44
+ expect(MetricUnitFormatUtils.formatNumber(1234.5, { locale = "en-gb" })).toBe("1,234.5")
45
+ end)
46
+
47
+ it("takes Intl-style digit options", function()
48
+ expect(MetricUnitFormatUtils.formatNumber(2, { minimumFractionDigits = 2 })).toBe("2.00")
49
+ expect(MetricUnitFormatUtils.formatNumber(2.345, { maximumFractionDigits = 1 })).toBe("2.3")
50
+ expect(MetricUnitFormatUtils.formatNumber(2.35, { maximumFractionDigits = 0 })).toBe("2")
51
+ expect(MetricUnitFormatUtils.formatNumber(123456, { maximumSignificantDigits = 2 })).toBe("120,000")
52
+ expect(MetricUnitFormatUtils.formatNumber(0.012345, { maximumSignificantDigits = 2 })).toBe("0.012")
53
+ expect(MetricUnitFormatUtils.formatNumber(1234.5, { useGrouping = false })).toBe("1234.5")
54
+ end)
55
+ end)
56
+
57
+ describe("MetricUnitFormatUtils.format units", function()
58
+ it("picks the unit that suits the size, or the one given", function()
59
+ local length = MetricUnit.meters(2500)
60
+ expect(format(length)).toBe("2.5 km")
61
+ expect(format(length, { unit = "meter" })).toBe("2,500 m")
62
+ expect(format(length, { autoUnit = false })).toBe("2,500 m")
63
+ expect(format(length, { maximumSignificantDigits = 1 })).toBe("3 km")
64
+ expect(format(MetricUnit.meters(0))).toBe("0 m")
65
+ end)
66
+
67
+ it("writes long names, pluralized, in American or Commonwealth spelling", function()
68
+ expect(format(MetricUnit.meters(2500), { unitDisplay = "long" })).toBe("2.5 kilometers")
69
+ expect(format(MetricUnit.kilometers(1), { unitDisplay = "long" })).toBe("1 kilometer")
70
+ expect(format(MetricUnit.kilometers(1), { unitDisplay = "long", minimumFractionDigits = 1 })).toBe(
71
+ "1.0 kilometers"
72
+ )
73
+ expect(format(MetricUnit.meters(2500), { unitDisplay = "long", locale = "en-gb" })).toBe("2.5 kilometres")
74
+ expect(format(MetricUnit.new(2, "liter"), { unitDisplay = "long", locale = "en-au" })).toBe("2 litres")
75
+ expect(format(MetricUnit.tonnes(2), { unitDisplay = "long" })).toBe("2 metric tons")
76
+ expect(format(MetricUnit.tonnes(2), { unitDisplay = "long", locale = "en-gb" })).toBe("2 tonnes")
77
+ end)
78
+
79
+ it("writes SI symbols with the locale's numbers in any locale", function()
80
+ expect(format(MetricUnit.meters(1234.5), { unit = "meter", locale = "de-de" })).toBe("1 234,5 m")
81
+ expect(format(MetricUnit.kilograms(2.5), { locale = "fr-fr" })).toBe("2,5 kg")
82
+ end)
83
+
84
+ it("writes derived units", function()
85
+ expect(format(MetricUnit.new(1500, "newton"))).toBe("1.5 kN")
86
+ expect(format(MetricUnit.new(1500, "newton"), { unitDisplay = "long" })).toBe("1.5 kilonewtons")
87
+ expect(format(MetricUnit.new(2500, "watt"), { unitDisplay = "long" })).toBe("2.5 kilowatts")
88
+ expect(format(MetricUnit.new(3, "kilowattHour"), { unit = "kilowattHour" })).toBe("3 kWh")
89
+ expect(format(MetricUnit.new(25000, "squareMeter"), { unit = "hectare" })).toBe("2.5 ha")
90
+ expect(format(MetricUnit.new(0.25, "liter"))).toBe("250 mL")
91
+ expect(format(MetricUnit.new(4096, "kibibyte"), { unit = "mebibyte" })).toBe("4 MiB")
92
+ expect(format(MetricUnit.new(9.81, "meterPerSecondSquared"), { unitDisplay = "long" })).toBe(
93
+ "9.81 meters per second squared"
94
+ )
95
+ end)
96
+
97
+ it("takes phrase overrides", function()
98
+ local length = MetricUnit.meters(2500)
99
+ expect(format(length, { strings = { kilometer = { short = { one = "%s klick", other = "%s klicks" } } } })).toBe(
100
+ "2.5 klicks"
101
+ )
102
+ expect(format(length, {
103
+ strings = {
104
+ kilometer = {
105
+ short = function(_amount: number, numberText: string): string
106
+ return numberText .. "!"
107
+ end,
108
+ },
109
+ },
110
+ })).toBe("2.5!")
111
+ end)
112
+
113
+ it("errors when the unit given is of another dimension", function()
114
+ expect(function()
115
+ format(MetricUnit.meters(1), { unit = "kilogram" })
116
+ end).toThrow("kilogram is mass, not length")
117
+ end)
118
+
119
+ it("writes dimensionless values and unnamed compounds", function()
120
+ expect(format(MetricUnit.none(0.25))).toBe("0.25")
121
+ expect(format(MetricUnit.kilograms(2) * MetricUnit.new(3, "meterPerSecond"))).toBe("6 m·kg/s")
122
+ expect(format(MetricUnit.kilograms(2) / MetricUnit.kilometers(1))).toBe("0.002 kg/m")
123
+ end)
124
+ end)
125
+
126
+ describe("MetricUnitFormatUtils.format rates", function()
127
+ it("writes the numerator in its own display unit, per the shortest time unit it fills", function()
128
+ expect(format(MetricUnit.new(5, "meterPerSecond"))).toBe("5 m/s")
129
+ expect(format(MetricUnit.new(5, "meterPerSecond"), { unitDisplay = "long" })).toBe("5 meters per second")
130
+ expect(format(MetricUnit.new(1600, "kilobytePerSecond"))).toBe("1.6 MB/s")
131
+ expect(format(MetricUnit.new(1600, "kilobytePerSecond"), { unitDisplay = "long" })).toBe(
132
+ "1.6 megabytes per second"
133
+ )
134
+ expect(format(MetricUnit.perDay(5))).toBe("5/d")
135
+ expect(format(MetricUnit.perDay(5), { unitDisplay = "long" })).toBe("5 per day")
136
+ end)
137
+
138
+ it("takes a whole rate unit, a numerator unit or a per unit", function()
139
+ expect(format(MetricUnit.new(18, "kilometerPerHour"), { unit = "kilometerPerHour" })).toBe("18 km/h")
140
+ expect(format(MetricUnit.new(18, "kilometerPerHour"), { unit = "kilometerPerHour", unitDisplay = "long" })).toBe(
141
+ "18 kilometers per hour"
142
+ )
143
+ expect(format(MetricUnit.new(1600, "kilobytePerSecond"), { unit = "kilobyte" })).toBe("1,600 kB/s")
144
+ expect(format(MetricUnit.new(1600, "kilobytePerSecond"), { perUnit = "minute" })).toBe("96 MB/min")
145
+ expect(format(MetricUnit.new(18, "kilometerPerHour"), { unit = "kilometer", perUnit = "hour" })).toBe("18 km/h")
146
+ expect(format(MetricUnit.new(60, "hertz"), { unit = "hertz" })).toBe("60 Hz")
147
+ end)
148
+
149
+ it("takes per phrase overrides", function()
150
+ expect(format(MetricUnit.perHour(3), { perStrings = { hour = { short = "%s an hour" } } })).toBe("3 an hour")
151
+ end)
152
+ end)
153
+
154
+ describe("MetricUnitFormatUtils.format temperatures", function()
155
+ it("writes an absolute temperature on a scale, °C by default", function()
156
+ expect(format(MetricUnit.celsius(21.5))).toBe("21.5 °C")
157
+ expect(format(MetricUnit.celsius(100), { unit = "fahrenheit" })).toBe("212 °F")
158
+ expect(format(MetricUnit.celsius(0), { unit = "kelvin" })).toBe("273.15 K")
159
+ expect(format(MetricUnit.celsius(1), { unitDisplay = "long" })).toBe("1 degree Celsius")
160
+ expect(format(MetricUnit.celsius(20), { unitDisplay = "long" })).toBe("20 degrees Celsius")
161
+ end)
162
+
163
+ it("writes a difference without an offset, K by default", function()
164
+ expect(format(MetricUnit.difference(3, "celsius"))).toBe("3 K")
165
+ expect(format(MetricUnit.difference(3, "celsius"), { unit = "celsius" })).toBe("3 °C")
166
+ expect(format(MetricUnit.difference(5, "celsius"), { unit = "fahrenheit" })).toBe("9 °F")
167
+ expect(format(MetricUnit.celsius(25) - MetricUnit.celsius(20), { unitDisplay = "long" })).toBe("5 kelvins")
168
+ end)
169
+
170
+ it("errors when the unit given is not a temperature", function()
171
+ expect(function()
172
+ format(MetricUnit.celsius(5), { unit = "meter" })
173
+ end).toThrow("meter is length, not temperature")
174
+ end)
175
+ end)
176
+
177
+ describe("MetricUnitFormatUtils.formatUnit", function()
178
+ it("writes an amount already in the unit", function()
179
+ expect(formatUnit("kilometerPerHour", 90)).toBe("90 km/h")
180
+ expect(formatUnit("liter", 1, { unitDisplay = "long" })).toBe("1 liter")
181
+ expect(formatUnit("celsius", 20)).toBe("20 °C")
182
+ expect(formatUnit("hour", 1.5)).toBe("1.5 hours")
183
+ expect(formatUnit("day", 1.5, { locale = "es-es" })).toBe("1,5 días")
184
+ end)
185
+ end)
186
+
187
+ describe("MetricUnitFormatUtils.format with a translator", function()
188
+ local function fakeTranslator(locale: string, entries: { [string]: string }): any
189
+ return {
190
+ GetLocaleId = function()
191
+ return locale
192
+ end,
193
+ FormatByKey = function(_self: any, key: string, args: { [string]: any }?): string
194
+ local text = entries[key]
195
+ if not text then
196
+ error("No key " .. key)
197
+ end
198
+ return (string.gsub(text, "{n}", function()
199
+ return (args :: any).n
200
+ end))
201
+ end,
202
+ ObserveFormatByKey = function()
203
+ error("unused")
204
+ end,
205
+ }
206
+ end
207
+
208
+ local spanish = fakeTranslator("es-es", {
209
+ ["units.mile.long.one"] = "{n} milla",
210
+ ["units.mile.long.other"] = "{n} millas",
211
+ ["units.mile.short.other"] = "{n} mi",
212
+ ["units.kilometer.long.other"] = "{n} kilómetros",
213
+ ["per.hour.long"] = "{n} por hora",
214
+ })
215
+
216
+ it("writes long names in the translator's language, with its plural rules and numbers", function()
217
+ expect(format(MetricUnit.new(3, "mile"):ToUSCustomary(), { unitDisplay = "long", translator = spanish })).toBe(
218
+ "3 millas"
219
+ )
220
+ expect(format(MetricUnit.new(1, "mile"):ToUSCustomary(), { unitDisplay = "long", translator = spanish })).toBe(
221
+ "1 milla"
222
+ )
223
+ expect(format(MetricUnit.new(1.5, "mile"):ToUSCustomary(), { unitDisplay = "long", translator = spanish })).toBe(
224
+ "1,5 millas"
225
+ )
226
+ expect(format(MetricUnit.new(5, "kilometerPerHour"), {
227
+ unitDisplay = "long",
228
+ unit = "kilometer",
229
+ perUnit = "hour",
230
+ translator = spanish,
231
+ })).toBe("5 kilómetros por hora")
232
+ end)
233
+
234
+ it("keeps symbols for short display, and English when a key is missing", function()
235
+ expect(format(MetricUnit.new(3, "mile"):ToUSCustomary(), { translator = spanish })).toBe("3 mi")
236
+ expect(format(MetricUnit.kilograms(3), { unitDisplay = "long", translator = spanish })).toBe("3 kilograms")
237
+ local broken = fakeTranslator("es-es", {})
238
+ expect(format(MetricUnit.new(3, "mile"):ToUSCustomary(), { unitDisplay = "long", translator = broken })).toBe(
239
+ "3 miles"
240
+ )
241
+ end)
242
+ end)
243
+
244
+ describe("MetricUnitFormatUtils.format input", function()
245
+ it("takes a MetricUnit or its plain data", function()
246
+ expect(formatData(MetricUnit.kilometers(1))).toBe("1 km")
247
+ expect(formatData(MetricUnit.toData(MetricUnit.kilometers(1)))).toBe("1 km")
248
+ expect(formatData({ amount = 278.15, dimensions = { temperature = 1 }, absolute = true })).toBe("5 °C")
249
+ expect(function()
250
+ formatData({ hello = true })
251
+ end).toThrow("expected a MetricUnit or MetricUnitData")
252
+ end)
253
+
254
+ it("writes bare rates by their per id, and frequencies in hertz", function()
255
+ expect(format(MetricUnit.perHour(3), { unit = "perHour" })).toBe("3/h")
256
+ expect(format(MetricUnit.perHour(3), { unit = "perHour", unitDisplay = "long" })).toBe("3 per hour")
257
+ expect(format(MetricUnit.perHour(3), { perUnit = "h" })).toBe("3/h")
258
+ expect(formatUnit("perDay", 5)).toBe("5/d")
259
+ end)
260
+ end)
261
+
262
+ describe("MetricUnitFormatUtils.format rates written as asked", function()
263
+ it("writes a rate in the numerator unit asked for, whatever the value's system shows", function()
264
+ expect(format(MetricUnit.new(60, "milePerHour"), { unit = "kilometer" })).toBe("0.027 km/s")
265
+ expect(format(MetricUnit.new(60, "milePerHour"), { unit = "kilometer", perUnit = "hour" })).toBe("96.561 km/h")
266
+ expect(format(MetricUnit.new(10, "studPerSecond"), { unit = "meter" })).toBe("2.8 m/s")
267
+ end)
268
+
269
+ it("writes a rate per the time unit asked for, whatever the value's system shows", function()
270
+ expect(format(MetricUnit.new(60, "milePerHour"), { perUnit = "second" })).toBe("88 ft/s")
271
+ expect(format(MetricUnit.new(60, "milePerHour"), { unit = "meter", perUnit = "second" })).toBe("26.822 m/s")
272
+ end)
273
+ end)
274
+
275
+ describe("MetricUnitFormatUtils plural forms by locale", function()
276
+ it("picks duration words by the locale's plural rule", function()
277
+ expect(format(MetricUnit.hours(1.5), { locale = "fr-fr", unit = "hour" })).toBe("1,5 heure")
278
+ expect(format(MetricUnit.hours(1.5), { locale = "en-us", unit = "hour" })).toBe("1.5 hours")
279
+ end)
280
+ end)
@@ -0,0 +1,62 @@
1
+ --!strict
2
+ --[=[
3
+ Observable formatting, for UI that should follow the player's language. Kept apart from
4
+ [MetricUnitFormatUtils] so formatting itself needs no Rx and runs anywhere, servers and specs included.
5
+
6
+ @class RxMetricUnitFormatUtils
7
+ ]=]
8
+
9
+ local require = require(script.Parent.loader).load(script)
10
+
11
+ local MetricUnitFormatUtils = require("MetricUnitFormatUtils")
12
+ local Observable = require("Observable")
13
+ local Rx = require("Rx")
14
+
15
+ local RxMetricUnitFormatUtils = {}
16
+
17
+ local ANY_TRANSLATION_KEY = "units.meter.other"
18
+
19
+ --[=[
20
+ Observes [MetricUnitFormatUtils.format] in the translator's language: emits again when the player's locale
21
+ changes or its translations finish streaming in, with English names until then. Numbers follow the translator's
22
+ locale unless `options.locale` is given.
23
+
24
+ ```lua
25
+ local translator = serviceBag:GetService(require("MetricUnitTranslator"))
26
+ maid:GiveTask(RxMetricUnitFormatUtils.observeFormat(translator, MetricUnit.new(3, "mile"), {
27
+ unitDisplay = "long",
28
+ }):Subscribe(function(text)
29
+ label.Text = text -- "3 miles", "3 millas", "3 Meilen"
30
+ end))
31
+ ```
32
+
33
+ @param translator MetricUnitTranslatorLike -- [MetricUnitTranslator]
34
+ @param value MetricUnit | MetricUnitData
35
+ @param options MetricUnitFormatOptions?
36
+ @return Observable<string>
37
+ ]=]
38
+ function RxMetricUnitFormatUtils.observeFormat(
39
+ translator: MetricUnitFormatUtils.MetricUnitTranslatorLike,
40
+ value: any,
41
+ options: MetricUnitFormatUtils.MetricUnitFormatOptions?
42
+ ): Observable.Observable<string>
43
+ assert(type(translator) == "table" and translator.ObserveFormatByKey, "Bad translator")
44
+
45
+ local formatOptions: MetricUnitFormatUtils.MetricUnitFormatOptions = table.clone(options or {})
46
+ formatOptions.translator = translator
47
+
48
+ return RxMetricUnitFormatUtils._observeTranslationChanges(translator):Pipe({
49
+ Rx.map(function()
50
+ return MetricUnitFormatUtils.format(value, formatOptions)
51
+ end) :: any,
52
+ Rx.distinct() :: any,
53
+ }) :: any
54
+ end
55
+
56
+ function RxMetricUnitFormatUtils._observeTranslationChanges(
57
+ translator: MetricUnitFormatUtils.MetricUnitTranslatorLike
58
+ ): Observable.Observable<string>
59
+ return translator:ObserveFormatByKey(ANY_TRANSLATION_KEY, { n = "" })
60
+ end
61
+
62
+ return table.freeze(RxMetricUnitFormatUtils)
@@ -0,0 +1,289 @@
1
+ --!strict
2
+ --[=[
3
+ English unit names for [MetricUnitFormatUtils] when no translator is given. They need no translator, so specs,
4
+ servers and anything formatting before a translator loads always get readable text. Translated names stream to clients through [MetricUnitTranslator], a
5
+ `JSONTranslator` holding the same keys.
6
+
7
+ Lookups resolve through [ResolveLocaleUtils], so `en-us` lands on American English and `en-au` on Commonwealth
8
+ English. Without a translator, short phrases are the symbols in [MetricUnitDefinitions]; with one, they are the player's language's
9
+ (`5 км`, `5公里`). A no-break space joins a number to its unit either way.
10
+
11
+ @class MetricUnitFallbackStrings
12
+ ]=]
13
+
14
+ local require = require(script.Parent.loader).load(script)
15
+
16
+ local MetricUnitDefinitions = require("MetricUnitDefinitions")
17
+ local ResolveLocaleUtils = require("ResolveLocaleUtils")
18
+
19
+ local MetricUnitFallbackStrings = {}
20
+
21
+ local DEFAULT_LOCALE = "en-us"
22
+
23
+ --[=[
24
+ A unit phrase with `%s` for the already formatted number: a singular and plural pair, or a function of the
25
+ rounded amount and the formatted number for languages with more plural forms.
26
+
27
+ @type UnitPhrase { one: string, other: string } | (amount: number, numberText: string) -> string
28
+ @within MetricUnitFallbackStrings
29
+ ]=]
30
+ export type UnitPhrase = { one: string, other: string } | (number, string) -> string
31
+
32
+ --[=[
33
+ The short and long phrase for one unit, such as `"%s km"` and `"%s kilometers"`.
34
+
35
+ @interface UnitNames
36
+ .short UnitPhrase
37
+ .long UnitPhrase
38
+ @within MetricUnitFallbackStrings
39
+ ]=]
40
+ export type UnitNames = { short: UnitPhrase, long: UnitPhrase }
41
+
42
+ --[=[
43
+ [UnitNames] for every unit written by name, keyed by unit id: every unit [MetricUnitDefinitions] gives `names`. Time
44
+ units are written with [TimeLocalizationUtils]' duration words instead, and bare rates with [PerStrings].
45
+
46
+ @type UnitStrings { [UnitId]: UnitNames }
47
+ @within MetricUnitFallbackStrings
48
+ ]=]
49
+ export type UnitStrings = { [string]: UnitNames }
50
+
51
+ --[=[
52
+ How to write something per a time unit, with `%s` for the numerator already written: `"%s/h"` and
53
+ `"%s per hour"`.
54
+
55
+ @interface PerNames
56
+ .short string
57
+ .long string
58
+ @within MetricUnitFallbackStrings
59
+ ]=]
60
+ export type PerNames = { short: string, long: string }
61
+
62
+ --[=[
63
+ [PerNames] for every time unit a rate can be written per.
64
+
65
+ @interface PerStrings
66
+ .second PerNames
67
+ .minute PerNames
68
+ .hour PerNames
69
+ .day PerNames
70
+ .week PerNames
71
+ .month PerNames
72
+ .year PerNames
73
+ @within MetricUnitFallbackStrings
74
+ ]=]
75
+ export type PerStrings = {
76
+ second: PerNames,
77
+ minute: PerNames,
78
+ hour: PerNames,
79
+ day: PerNames,
80
+ week: PerNames,
81
+ month: PerNames,
82
+ year: PerNames,
83
+ }
84
+
85
+ --[=[
86
+ Unit phrases a caller passes to override a locale's, keyed by unit id: `{ kilometer = { short = "%s klicks" } }`.
87
+
88
+ @type UnitStringOverrides { [string]: { short: UnitPhrase?, long: UnitPhrase? } }
89
+ @within MetricUnitFallbackStrings
90
+ ]=]
91
+ export type UnitStringOverrides = { [string]: { short: UnitPhrase?, long: UnitPhrase? } }
92
+
93
+ --[=[
94
+ Per phrases a caller passes to override a locale's, keyed by time unit id.
95
+
96
+ @type PerStringOverrides { [string]: { short: string?, long: string? } }
97
+ @within MetricUnitFallbackStrings
98
+ ]=]
99
+ export type PerStringOverrides = { [string]: { short: string?, long: string? } }
100
+
101
+ --[=[
102
+ Everything one locale needs.
103
+
104
+ @interface UnitLocale
105
+ .units UnitStrings
106
+ .per PerStrings
107
+ @within MetricUnitFallbackStrings
108
+ ]=]
109
+ export type UnitLocale = {
110
+ units: UnitStrings,
111
+ per: PerStrings,
112
+ }
113
+
114
+ -- U+00A0, not CLDR's narrow U+202F: create.roblox.com does not document how TextLabels render U+202F
115
+ local NO_BREAK_SPACE = "\u{00A0}"
116
+ MetricUnitFallbackStrings.NO_BREAK_SPACE = NO_BREAK_SPACE
117
+
118
+ local function withNoBreakSpaceAfterNumber(template: string): string
119
+ return (string.gsub(template, "^%%s ", "%%s" .. NO_BREAK_SPACE))
120
+ end
121
+
122
+ type Spelling = (string) -> string
123
+
124
+ local function american(text: string): string
125
+ return text
126
+ end
127
+
128
+ --[=[
129
+ `text` in Commonwealth English spelling: metre for meter, litre for liter, tonne for metric ton.
130
+
131
+ @param text string
132
+ @return string
133
+ ]=]
134
+ function MetricUnitFallbackStrings.toCommonwealthSpelling(text: string): string
135
+ text = string.gsub(text, "metric tons?", { ["metric ton"] = "tonne", ["metric tons"] = "tonnes" })
136
+ text = string.gsub(text, "meter", "metre")
137
+ text = string.gsub(text, "liter", "litre")
138
+ return text
139
+ end
140
+
141
+ function MetricUnitFallbackStrings._lazyEnglishUnits(spelling: Spelling): UnitStrings
142
+ return setmetatable({}, {
143
+ __index = function(self, id: string): UnitNames?
144
+ local unit = MetricUnitDefinitions.getUnit(id)
145
+ if not (unit and unit.names) then
146
+ return nil
147
+ end
148
+ local names = MetricUnitFallbackStrings._englishNamesOf(unit, spelling)
149
+ rawset(self, id, names)
150
+ return names
151
+ end,
152
+ }) :: any
153
+ end
154
+
155
+ function MetricUnitFallbackStrings._englishNamesOf(
156
+ unit: MetricUnitDefinitions.UnitDefinition,
157
+ spelling: Spelling
158
+ ): UnitNames
159
+ local short = "%s" .. NO_BREAK_SPACE .. unit.symbol
160
+ local templates = unit.shortTemplates
161
+ local names = unit.names or { one = unit.symbol, other = unit.symbol }
162
+ return table.freeze({
163
+ short = table.freeze({
164
+ one = if templates then withNoBreakSpaceAfterNumber(templates.one) else short,
165
+ other = if templates then withNoBreakSpaceAfterNumber(templates.other) else short,
166
+ }),
167
+ long = table.freeze({
168
+ one = "%s" .. NO_BREAK_SPACE .. spelling(names.one),
169
+ other = "%s" .. NO_BREAK_SPACE .. spelling(names.other),
170
+ }),
171
+ })
172
+ end
173
+
174
+ local PER_TIME_UNIT_IDS: { [string]: boolean } =
175
+ table.freeze({ second = true, minute = true, hour = true, day = true, week = true, month = true, year = true })
176
+
177
+ function MetricUnitFallbackStrings._lazyEnglishPerStrings(): PerStrings
178
+ return setmetatable({}, {
179
+ __index = function(self, id: string): PerNames?
180
+ local unit = if PER_TIME_UNIT_IDS[id] then MetricUnitDefinitions.getUnit(id) else nil
181
+ if not unit then
182
+ return nil
183
+ end
184
+ local names = table.freeze({ short = "%s/" .. unit.symbol, long = "%s per " .. id })
185
+ rawset(self, id, names)
186
+ return names
187
+ end,
188
+ }) :: any
189
+ end
190
+
191
+ local AMERICAN_ENGLISH: UnitLocale = table.freeze({
192
+ units = MetricUnitFallbackStrings._lazyEnglishUnits(american),
193
+ per = MetricUnitFallbackStrings._lazyEnglishPerStrings(),
194
+ })
195
+
196
+ local COMMONWEALTH_ENGLISH: UnitLocale = table.freeze({
197
+ units = MetricUnitFallbackStrings._lazyEnglishUnits(MetricUnitFallbackStrings.toCommonwealthSpelling),
198
+ per = MetricUnitFallbackStrings._lazyEnglishPerStrings(),
199
+ })
200
+
201
+ local LOCALES: { [string]: UnitLocale } = table.freeze({
202
+ en = AMERICAN_ENGLISH,
203
+ ["en-gb"] = COMMONWEALTH_ENGLISH,
204
+ ["en-au"] = COMMONWEALTH_ENGLISH,
205
+ ["en-ca"] = COMMONWEALTH_ENGLISH,
206
+ ["en-ie"] = COMMONWEALTH_ENGLISH,
207
+ ["en-nz"] = COMMONWEALTH_ENGLISH,
208
+ }) :: any
209
+
210
+ local warnedLocales: { [string]: boolean } = {}
211
+
212
+ --[=[
213
+ Whether a locale has its own unit names here, directly or through a locale of the same language.
214
+
215
+ @param locale string?
216
+ @return boolean
217
+ ]=]
218
+ function MetricUnitFallbackStrings.hasLocale(locale: string?): boolean
219
+ return ResolveLocaleUtils.resolveClosestKey(locale or DEFAULT_LOCALE, LOCALES) ~= nil
220
+ end
221
+
222
+ --[=[
223
+ Returns the read-only [UnitLocale] for a locale, defaulting to American English. Warns once per unknown locale.
224
+
225
+ @param locale string?
226
+ @return UnitLocale
227
+ ]=]
228
+ function MetricUnitFallbackStrings.getLocale(locale: string?): UnitLocale
229
+ local key = ResolveLocaleUtils.resolveClosestKey(locale or DEFAULT_LOCALE, LOCALES)
230
+ if key then
231
+ return LOCALES[key]
232
+ end
233
+
234
+ local name = tostring(locale)
235
+ if not warnedLocales[name] then
236
+ warnedLocales[name] = true
237
+ warn(
238
+ string.format(
239
+ "[MetricUnitFallbackStrings] - No unit names for locale '%s', reverting to '%s' instead.",
240
+ name,
241
+ DEFAULT_LOCALE
242
+ )
243
+ )
244
+ end
245
+ return LOCALES[ResolveLocaleUtils.resolveClosestKey(DEFAULT_LOCALE, LOCALES) :: string]
246
+ end
247
+
248
+ --[=[
249
+ The [UnitNames] of `unit` in a locale: the locale's own, or, for a unit a game's own [MetricUnitSystem] declares,
250
+ its English names and symbol from its definition.
251
+
252
+ @param locale string?
253
+ @param unit UnitDefinition
254
+ @return UnitNames
255
+ ]=]
256
+ function MetricUnitFallbackStrings.getUnitNames(locale: string?, unit: MetricUnitDefinitions.UnitDefinition): UnitNames
257
+ local unitLocale = MetricUnitFallbackStrings.getLocale(locale)
258
+ local names = unitLocale.units[unit.id]
259
+ if names then
260
+ return names
261
+ end
262
+ local spelling = if unitLocale == COMMONWEALTH_ENGLISH
263
+ then MetricUnitFallbackStrings.toCommonwealthSpelling
264
+ else american
265
+ return MetricUnitFallbackStrings._englishNamesOf(unit, spelling)
266
+ end
267
+
268
+ --[=[
269
+ Returns the read-only [UnitStrings] for a locale, defaulting to American English. Each unit's names are built the
270
+ first time they are read.
271
+
272
+ @param locale string?
273
+ @return UnitStrings
274
+ ]=]
275
+ function MetricUnitFallbackStrings.getUnitStringsForLocale(locale: string?): UnitStrings
276
+ return MetricUnitFallbackStrings.getLocale(locale).units
277
+ end
278
+
279
+ --[=[
280
+ Returns the read-only [PerStrings] for a locale, defaulting to American English.
281
+
282
+ @param locale string?
283
+ @return PerStrings
284
+ ]=]
285
+ function MetricUnitFallbackStrings.getPerStringsForLocale(locale: string?): PerStrings
286
+ return MetricUnitFallbackStrings.getLocale(locale).per
287
+ end
288
+
289
+ return table.freeze(MetricUnitFallbackStrings)