{-# LANGUAGE CPP #-}
module Main (main) where
import Data.Either (isLeft)
import Data.Time (UTCTime (..), fromGregorian, secondsToDiffTime)
import Miso.Fluent
import Miso.Prelude
import Test.Hspec
( Expectation
, HasCallStack
, Spec
, describe
, hspec
, it
, shouldBe
, shouldSatisfy
)
#ifdef WASM
foreign export javascript "hs_start" main :: IO ()
#endif
main :: IO ()
main = hspec spec
spec :: Spec
spec = do
describe "Locale" do
describe "from a tag" do
it "known" do
fromCode "en-GB" `shouldBe` Just (Locale "en-GB")
fromCode "ja" `shouldBe` Just (Locale "ja")
it "mixed case" $ fromCode "EN-gb" `shouldBe` Just (Locale "en-GB")
it "unknown" $ fromCode "banana" `shouldBe` (Nothing :: Maybe Locale)
it "no tag at all" $ fromCode "en_GB" `shouldBe` (Nothing :: Maybe Locale)
it "display language" $ displayLanguage ja deCH `shouldBe` Just "Japanisch"
it "local display language" $ localDisplayLanguage ja `shouldBe` Just "日本語"
describe "Number" do
describe "grouping" do
it "grouped digits"
$ translate "value" ("arg", NumberValue 1234567 numberOptions) enGB resource
`shouldBe` Right "1,234,567"
it "no grouping"
$ translate "value" ("arg", NumberValue 1234567 numberOptions{useGrouping = False}) enGB resource
`shouldBe` Right "1234567"
describe "digits" do
it "at least three integer digits"
$ translate "value" ("arg", NumberValue 5 numberOptions{minimumIntegerDigits = 3}) enGB resource
`shouldBe` Right "005"
it "no fraction digits"
$ translate
"value"
("arg", NumberValue 1.5 numberOptions{maximumFractionDigits = Just 0})
enGB
resource
`shouldBe` Right "2"
it "two significant digits"
$ translate
"value"
("arg", NumberValue 1234 numberOptions{maximumSignificantDigits = Just 2})
enGB
resource
`shouldBe` Right "1,200"
it "at least four significant digits"
$ translate
"value"
("arg", NumberValue 1.5 numberOptions{minimumSignificantDigits = Just 4})
enGB
resource
`shouldBe` Right "1.500"
describe "style" do
it "a percentage"
$ translate "value" ("arg", NumberValue 0.25 numberOptions{style = Percent}) enGB resource
`shouldBe` Right "25%"
it "an amount of money"
$ translate "value" ("arg", NumberValue 12.5 numberOptions{style = Currency "EUR"}) enGB resource
`shouldBe` Right "€12.50"
it "an amount of a unit"
$ translate
"value"
("arg", NumberValue 5 numberOptions{style = Unit "kilometre-per-hour"})
enGB
resource
`shouldBe` Right "5 km/h"
it "an unknown unit"
$ shouldFail
$ translate "value" ("arg", NumberValue 5 numberOptions{style = Unit "banana"}) enGB resource
describe "currency" do
it "a code beside the amount"
$ translate
"value"
("arg", NumberValue 12.5 numberOptions{style = Currency "EUR", currencyDisplay = Code})
enGB
resource
`shouldBe` Right "EUR\160\&12.50"
it "a name beside the amount"
$ translate
"value"
("arg", NumberValue 12.5 numberOptions{style = Currency "EUR", currencyDisplay = Name})
enGB
resource
`shouldBe` Right "12.50 euros"
describe "unit" do
it "narrow and long names" do
translate
"value"
("arg", NumberValue 5 numberOptions{style = Unit "litre", unitDisplay = Narrow})
enGB
resource
`shouldBe` Right "5l"
translate
"value"
("arg", NumberValue 5 numberOptions{style = Unit "litre", unitDisplay = Long})
enGB
resource
`shouldBe` Right "5 litres"
it "the locale's own name"
$ translate
"value"
("arg", NumberValue 5 numberOptions{style = Unit "litre", unitDisplay = Long})
deCH
resource
`shouldBe` Right "5 Liter"
it "the locale's own format"
$ translate
"value"
("arg", NumberValue 1234.5 numberOptions{minimumFractionDigits = 1})
deCH
resource
`shouldBe` Right "1'234.5"
describe "Plural" do
it "en-GB" do
translate "counted" ("arg", NumberValue 1 numberOptions) enGB resource
`shouldBe` Right "one"
translate "counted" ("arg", NumberValue 2 numberOptions) enGB resource
`shouldBe` Right "other"
it "ja" do
translate "counted" ("arg", NumberValue 1 numberOptions) ja resource
`shouldBe` Right "other"
translate "counted" ("arg", NumberValue 2 numberOptions) ja resource
`shouldBe` Right "other"
translate "counted" ("arg", NumberValue 5 numberOptions) ja resource
`shouldBe` Right "other"
it "ordinals" do
translate "counted" ("arg", NumberValue 1 ordinal) enGB resource
`shouldBe` Right "one"
translate "counted" ("arg", NumberValue 2 ordinal) enGB resource
`shouldBe` Right "two"
translate "counted" ("arg", NumberValue 3 ordinal) enGB resource
`shouldBe` Right "few"
translate "counted" ("arg", NumberValue 4 ordinal) enGB resource
`shouldBe` Right "other"
describe "Time" do
it "a date of numbers"
$ translate "value" ("arg", TimeValue afternoon timeOptions) enGB resource
`shouldBe` Right "10/08/2026"
it "the locale's own order"
$ translate "value" ("arg", TimeValue afternoon timeOptions) deCH resource
`shouldBe` Right "10.8.2026"
describe "month" do
it "a long name"
$ translate
"value"
("arg", TimeValue afternoon dated{month = Just (Right Long)})
enGB
resource
`shouldBe` Right "10 August 2026"
it "a long name on its own"
$ translate
"value"
("arg", TimeValue afternoon timeOptions{month = Just (Right Long)})
enGB
resource
`shouldBe` Right "August"
it "short and narrow names" do
translate "value" ("arg", TimeValue afternoon dated{month = Just (Right Short)}) enGB resource
`shouldBe` Right "10 Aug 2026"
translate
"value"
("arg", TimeValue afternoon timeOptions{month = Just (Right Narrow)})
enGB
resource
`shouldBe` Right "A"
it "weekday names" do
translate "value" ("arg", TimeValue afternoon timeOptions{weekday = Just Long}) enGB resource
`shouldBe` Right "Monday"
translate "value" ("arg", TimeValue afternoon timeOptions{weekday = Just Short}) enGB resource
`shouldBe` Right "Mon"
translate "value" ("arg", TimeValue afternoon timeOptions{weekday = Just Narrow}) enGB resource
`shouldBe` Right "M"
it "a two-digit year and day" do
translate "value" ("arg", TimeValue afternoon timeOptions{year = Just TwoDigit}) enGB resource
`shouldBe` Right "26"
translate "value" ("arg", TimeValue afternoon timeOptions{day = Just TwoDigit}) enGB resource
`shouldBe` Right "10"
it "an era"
$ translate
"value"
("arg", TimeValue afternoon timeOptions{era = Just Short, year = Just Numeric})
enGB
resource
`shouldBe` Right "2026 AD"
describe "clock" do
it "an hour and a minute"
$ translate "value" ("arg", TimeValue afternoon clock{hour12 = Just False}) enGB resource
`shouldBe` Right "12:34"
it "seconds"
$ translate
"value"
("arg", TimeValue afternoon clock{second = Just TwoDigit, hour12 = Just False})
enGB
resource
`shouldBe` Right "12:34:56"
it "the locale's own clock"
$ translate "value" ("arg", TimeValue afternoon clock) enGB resource
`shouldBe` Right "12:34"
it "a twelve-hour clock"
$ translate "value" ("arg", TimeValue afternoon clock{hour12 = Just True}) enGB resource
`shouldBe` Right "12:34 pm"
describe "time zone" do
it "short and long names" do
translate
"value"
("arg", TimeValue afternoon clock{timeZoneName = Just Short, hour12 = Just False})
enGB
resource
`shouldBe` Right "12:34 UTC"
translate
"value"
("arg", TimeValue afternoon clock{timeZoneName = Just Long, hour12 = Just False})
enGB
resource
`shouldBe` Right "12:34 Coordinated Universal Time"
it "another zone"
$ translate "value" ("arg", TimeValue afternoon berlin) enGB resource
`shouldBe` Right "14:34"
it "another zone in winter"
$ translate "value" ("arg", TimeValue midwinter berlin) enGB resource
`shouldBe` Right "13:34"
describe "Translations" do
it "MisoString"
$ translate "hello" (bundle (pure enGB) $ pure resource)
`shouldBe` ("Hello" :: MisoString)
it "View" $ (translate "hello" :: View (Bundle Locale) () () ()) `seq` pure @IO ()
shouldFail :: (HasCallStack) => Either String MisoString -> Expectation
shouldFail actual = actual `shouldSatisfy` isLeft
resource :: Resource
resource =
[fluent|
hello = Hello
value = { $arg }
counted = { $arg ->
[one] one
[two] two
[few] few
[many] many
*[other] other
}
|]
instance (r ~ Either String MisoString) => Translate (Locale -> Resource -> r) where
translate reference locale = translate reference . bundle (pure locale) . pure
enGB, deCH, ja :: Locale
enGB = Locale "en-GB"
deCH = Locale "de-CH"
ja = Locale "ja"
ordinal :: NumberOptions
ordinal = numberOptions{form = Ordinal}
dated :: TimeOptions
dated = timeOptions{day = Just Numeric, year = Just Numeric}
clock :: TimeOptions
clock = timeOptions{hour = Just Numeric, minute = Just TwoDigit}
berlin :: TimeOptions
berlin = clock{timeZone = "Europe/Berlin", hour12 = Just False}
afternoon :: UTCTime
afternoon = UTCTime (fromGregorian 2026 8 10) . secondsToDiffTime $ 12 * 3600 + 34 * 60 + 56
midwinter :: UTCTime
midwinter = UTCTime (fromGregorian 2026 1 10) . secondsToDiffTime $ 12 * 3600 + 34 * 60 + 56