module Language.Fluent.Locale.English where
import Data.Char qualified as Char
import Data.HashMap.Strict (HashMap)
import Data.HashMap.Strict qualified as HashMap
import Data.List qualified as List
import Data.Maybe qualified as Maybe
import Data.Scientific qualified as Scientific
import Data.Text (Text)
import Data.Text qualified as Text
import Data.Time qualified as Time
import Language.Fluent.Locale (Locale (..))
import Language.Fluent.Number (NumberOptions (..))
import Language.Fluent.Number qualified as NumberOptions
import Language.Fluent.Plural qualified as Plural
import Language.Fluent.Time (TimeOptions (..))
import Language.Fluent.Time qualified as TimeOptions
import Language.Fluent.Width (Width (..))
import Prelude
data English = English
instance Locale English where
fromCode :: Text -> Maybe English
fromCode Text
code
| Text -> Text
Text.toLower ((Char -> Bool) -> Text -> Text
Text.takeWhile Char -> Bool
Char.isAlpha Text
code) Text -> Text -> Bool
forall a. Eq a => a -> a -> Bool
== Text
"en" = English -> Maybe English
forall a. a -> Maybe a
Just English
English
| Bool
otherwise = Maybe English
forall a. Maybe a
Nothing
toCode :: English -> Text
toCode English
English = Text
"en"
displayLanguage :: English -> English -> Maybe Text
displayLanguage English
English English
English = Text -> Maybe Text
forall a. a -> Maybe a
Just Text
"English"
capitalise :: English -> Text -> Text
capitalise English
English (Int -> Text -> (Text, Text)
Text.splitAt Int
1 -> (Text
initial, Text
rest)) = Text -> Text
Text.toTitle Text
initial Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Text
rest
pluralCategory :: English -> Scientific -> NumberOptions -> Category
pluralCategory English
English Scientific
value NumberOptions
options = case NumberOptions
options.form of
Form
Plural.Cardinal
| Scientific -> Scientific
forall a. Num a => a -> a
abs Scientific
value Scientific -> Scientific -> Bool
forall a. Eq a => a -> a -> Bool
== Scientific
1 Bool -> Bool -> Bool
&& (Int, Int) -> Int
forall a b. (a, b) -> a
fst (NumberOptions -> (Int, Int)
NumberOptions.fractionDigits NumberOptions
options) Int -> Int -> Bool
forall a. Eq a => a -> a -> Bool
== Int
0 -> Category
Plural.One
| Bool
otherwise -> Category
Plural.Other
Form
Plural.Ordinal -> case forall i. (Integral i, Bounded i) => Scientific -> Maybe i
Scientific.toBoundedInteger @Int (Scientific -> Scientific
forall a. Num a => a -> a
abs Scientific
value) of
Just Int
n | Int
n Int -> Int -> Int
forall a. Integral a => a -> a -> a
`mod` Int
100 Int -> [Int] -> Bool
forall (t :: * -> *) a. (Foldable t, Eq a) => a -> t a -> Bool
`notElem` [Int
11 .. Int
13] -> case Int
n Int -> Int -> Int
forall a. Integral a => a -> a -> a
`mod` Int
10 of
Int
1 -> Category
Plural.One
Int
2 -> Category
Plural.Two
Int
3 -> Category
Plural.Few
Int
_ -> Category
Plural.Other
Maybe Int
_ -> Category
Plural.Other
formatNumber :: NonEmpty English
-> Scientific -> NumberOptions -> Either String Text
formatNumber NonEmpty English
_ Scientific
value NumberOptions
options = case NumberOptions
options.style of
Style
NumberOptions.Decimal -> Text -> Text -> Either String Text
format Text
"" Text
""
Style
NumberOptions.Percent -> Text -> Text -> Either String Text
format Text
"" Text
"\xA0%"
NumberOptions.Currency (Text -> Text
Text.toUpper -> Text
code)
| CurrencyDisplay
NumberOptions.Symbol <- NumberOptions
options.currencyDisplay
, Just Text
symbol <- Text -> HashMap Text Text -> Maybe Text
forall k v. (Eq k, Hashable k) => k -> HashMap k v -> Maybe v
HashMap.lookup Text
code HashMap Text Text
currencies ->
Text -> Text -> Either String Text
format Text
symbol Text
""
| Bool
otherwise -> Text -> Text -> Either String Text
format (Text
code Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Text
"\xA0") Text
""
NumberOptions.Unit (Text -> Text
NumberOptions.unitIdentifier -> Text
unit) ->
case Text -> HashMap Text Text -> Maybe Text
forall k v. (Eq k, Hashable k) => k -> HashMap k v -> Maybe v
HashMap.lookup Text
unit HashMap Text Text
units of
Maybe Text
Nothing -> String -> Either String Text
forall a b. a -> Either a b
Left (String -> Either String Text) -> String -> Either String Text
forall a b. (a -> b) -> a -> b
$ String
"NUMBER: unknown unit: " String -> String -> String
forall a. Semigroup a => a -> a -> a
<> Text -> String
Text.unpack Text
unit
Just Text
symbol
| Text
unit Text -> [Text] -> Bool
forall a. Eq a => a -> [a] -> Bool
forall (t :: * -> *) a. (Foldable t, Eq a) => a -> t a -> Bool
`elem` [Text
"day", Text
"week", Text
"month", Text
"year"]
, English -> Scientific -> NumberOptions -> Category
forall l. Locale l => l -> Scientific -> NumberOptions -> Category
pluralCategory English
English Scientific
value NumberOptions
options Category -> Category -> Bool
forall a. Eq a => a -> a -> Bool
/= Category
Plural.One ->
Text -> Text -> Either String Text
format Text
"" (Text
"\xA0" Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Text
symbol Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Text
"s")
| Bool
otherwise -> Text -> Text -> Either String Text
format Text
"" (Text
"\xA0" Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Text
symbol)
where
format :: Text -> Text -> Either String Text
format Text
before Text
after = Text -> Either String Text
forall a b. b -> Either a b
Right (Text -> Either String Text) -> Text -> Either String Text
forall a b. (a -> b) -> a -> b
$ Text
sign Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Text
before Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Text
digits Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Text
after
sign :: Text
sign = if Scientific
value Scientific -> Scientific -> Bool
forall a. Ord a => a -> a -> Bool
< Scientific
0 then Text
"-" else Text
""
amount :: Scientific
amount
| NumberOptions
options.style Style -> Style -> Bool
forall a. Eq a => a -> a -> Bool
== Style
NumberOptions.Percent = Scientific -> Scientific
forall a. Num a => a -> a
abs Scientific
value Scientific -> Scientific -> Scientific
forall a. Num a => a -> a -> a
* Scientific
100
| Bool
otherwise = Scientific -> Scientific
forall a. Num a => a -> a
abs Scientific
value
(Int
atLeast, Int
atMost) = case NumberOptions
options.style of
NumberOptions.Currency (Text -> Text
Text.toUpper -> Text
code)
| NumberOptions
options.minimumFractionDigits Int -> Int -> Bool
forall a. Eq a => a -> a -> Bool
== Int
0
, Maybe Int
Nothing <- NumberOptions
options.maximumFractionDigits
, let places :: Int
places = Text -> Int
currencyDigits Text
code ->
(Int
places, Int
places)
Style
_ -> NumberOptions -> (Int, Int)
NumberOptions.fractionDigits NumberOptions
options
(Integer
whole, Integer
fractional) =
forall a b. (RealFrac a, Integral b) => a -> b
floor @_ @Integer (Scientific -> Rational
forall a. Real a => a -> Rational
toRational Scientific
amount Rational -> Rational -> Rational
forall a. Num a => a -> a -> a
* Rational
10 Rational -> Int -> Rational
forall a b. (Num a, Integral b) => a -> b -> a
^ Int
atMost Rational -> Rational -> Rational
forall a. Num a => a -> a -> a
+ Rational
1 Rational -> Rational -> Rational
forall a. Fractional a => a -> a -> a
/ Rational
2) Integer -> Integer -> (Integer, Integer)
forall a. Integral a => a -> a -> (a, a)
`divMod` (Integer
10 Integer -> Int -> Integer
forall a b. (Num a, Integral b) => a -> b -> a
^ Int
atMost)
padded :: Text
padded = Int -> Char -> Text -> Text
Text.justifyRight (NumberOptions -> Int
NumberOptions.integerDigits NumberOptions
options) Char
'0' (Text -> Text) -> (Integer -> Text) -> Integer -> Text
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Integer -> Text
forall a. Show a => a -> Text
Text.show (Integer -> Text) -> Integer -> Text
forall a b. (a -> b) -> a -> b
$ Integer
whole
integer :: Text
integer
| NumberOptions
options.useGrouping =
Text -> Text
Text.reverse (Text -> Text) -> (Text -> Text) -> Text -> Text
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Text -> [Text] -> Text
Text.intercalate Text
"," ([Text] -> Text) -> (Text -> [Text]) -> Text -> Text
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Int -> Text -> [Text]
Text.chunksOf Int
3 (Text -> [Text]) -> (Text -> Text) -> Text -> [Text]
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Text -> Text
Text.reverse (Text -> Text) -> Text -> Text
forall a b. (a -> b) -> a -> b
$ Text
padded
| Bool
otherwise = Text
padded
fraction :: Text
fraction =
Int -> Char -> Text -> Text
Text.justifyLeft Int
atLeast Char
'0'
(Text -> Text) -> (Integer -> Text) -> Integer -> Text
forall b c a. (b -> c) -> (a -> b) -> a -> c
. (Char -> Bool) -> Text -> Text
Text.dropWhileEnd (Char -> Char -> Bool
forall a. Eq a => a -> a -> Bool
== Char
'0')
(Text -> Text) -> (Integer -> Text) -> Integer -> Text
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Int -> Char -> Text -> Text
Text.justifyRight Int
atMost Char
'0'
(Text -> Text) -> (Integer -> Text) -> Integer -> Text
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Integer -> Text
forall a. Show a => a -> Text
Text.show
(Integer -> Text) -> Integer -> Text
forall a b. (a -> b) -> a -> b
$ Integer
fractional
digits :: Text
digits
| Text -> Bool
Text.null Text
fraction = Text
integer
| Bool
otherwise = Text
integer Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Text
"." Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Text
fraction
formatTime :: NonEmpty English -> UTCTime -> TimeOptions -> Either String Text
formatTime NonEmpty English
_ UTCTime
when TimeOptions
options
| TimeOptions
options.timeZone Text -> Text -> Bool
forall a. Eq a => a -> a -> Bool
/= Text
"UTC" =
String -> Either String Text
forall a b. a -> Either a b
Left (String -> Either String Text) -> String -> Either String Text
forall a b. (a -> b) -> a -> b
$ String
"DATETIME: unsupported time zone: " String -> String -> String
forall a. Semigroup a => a -> a -> a
<> Text -> String
Text.unpack TimeOptions
options.timeZone
| Bool
otherwise = Text -> Either String Text
forall a b. b -> Either a b
Right (Text -> Either String Text)
-> (String -> Text) -> String -> Either String Text
forall b c a. (b -> c) -> (a -> b) -> a -> c
. String -> Text
Text.pack (String -> Either String Text) -> String -> Either String Text
forall a b. (a -> b) -> a -> b
$ TimeLocale -> String -> UTCTime -> String
forall t. FormatTime t => TimeLocale -> String -> t -> String
Time.formatTime TimeLocale
Time.defaultTimeLocale String
layout UTCTime
when
where
fields :: [(Char, Either Digits Width)]
fields = TimeOptions -> [(Char, Either Digits Width)]
TimeOptions.fields TimeOptions
options
textualMonth :: Bool
textualMonth = case Char
-> [(Char, Either Digits Width)] -> Maybe (Either Digits Width)
forall a b. Eq a => a -> [(a, b)] -> Maybe b
lookup Char
'M' [(Char, Either Digits Width)]
fields of
Just (Right Width
_) -> Bool
True
Maybe (Either Digits Width)
_ -> Bool
False
layout :: String
layout =
String -> [String] -> String
forall a. [a] -> [[a]] -> [a]
List.intercalate String
", " ([String] -> String)
-> ([String] -> [String]) -> [String] -> String
forall b c a. (b -> c) -> (a -> b) -> a -> c
. (String -> Bool) -> [String] -> [String]
forall a. (a -> Bool) -> [a] -> [a]
filter (Bool -> Bool
not (Bool -> Bool) -> (String -> Bool) -> String -> Bool
forall b c a. (b -> c) -> (a -> b) -> a -> c
. String -> Bool
forall a. [a] -> Bool
forall (t :: * -> *) a. Foldable t => t a -> Bool
null) ([String] -> String) -> [String] -> String
forall a b. (a -> b) -> a -> b
$
[ String -> String -> String
part String
"" String
"E"
, String -> String -> String
part (if Bool
textualMonth then String
" " else String
"/") String
"dMy"
, String -> String -> String
part String
":" String
"hHjms" String -> String -> String
forall a. Semigroup a => a -> a -> a
<> String
meridiem
]
meridiem :: String
meridiem = case Char
-> [(Char, Either Digits Width)] -> Maybe (Either Digits Width)
forall a b. Eq a => a -> [(a, b)] -> Maybe b
lookup Char
'h' [(Char, Either Digits Width)]
fields of
Just Either Digits Width
_ -> String
" %p"
Maybe (Either Digits Width)
Nothing -> String
""
part :: String -> String -> String
part String
separator String
symbols =
String -> [String] -> String
forall a. [a] -> [[a]] -> [a]
List.intercalate String
separator ([String] -> String) -> [String] -> String
forall a b. (a -> b) -> a -> b
$
(Char -> Maybe String) -> String -> [String]
forall a b. (a -> Maybe b) -> [a] -> [b]
Maybe.mapMaybe (\Char
symbol -> Char -> Either Digits Width -> String
render Char
symbol (Either Digits Width -> String)
-> Maybe (Either Digits Width) -> Maybe String
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> Char
-> [(Char, Either Digits Width)] -> Maybe (Either Digits Width)
forall a b. Eq a => a -> [(a, b)] -> Maybe b
lookup Char
symbol [(Char, Either Digits Width)]
fields) String
symbols
render :: Char -> Either Digits Width -> String
render Char
'E' (Right Width
Long) = String
"%A"
render Char
'E' (Right Width
_) = String
"%a"
render Char
'M' (Right Width
Long) = String
"%B"
render Char
'M' (Right Width
_) = String
"%b"
render Char
'M' (Left Digits
_) = String
"%m"
render Char
'd' Either Digits Width
_ = if Bool
textualMonth then String
"%-d" else String
"%d"
render Char
'y' Either Digits Width
_ = String
"%Y"
render Char
'h' Either Digits Width
_ = String
"%-I"
render Char
'H' Either Digits Width
_ = String
"%H"
render Char
'j' Either Digits Width
_ = String
"%H"
render Char
'm' Either Digits Width
_ = String
"%M"
render Char
's' Either Digits Width
_ = String
"%S"
render Char
_ Either Digits Width
_ = String
""
units :: HashMap Text Text
units :: HashMap Text Text
units =
[(Text, Text)] -> HashMap Text Text
forall k v. (Eq k, Hashable k) => [(k, v)] -> HashMap k v
HashMap.fromList ([(Text, Text)] -> HashMap Text Text)
-> (Text -> [(Text, Text)]) -> Text -> HashMap Text Text
forall b c a. (b -> c) -> (a -> b) -> a -> c
. (Text -> (Text, Text)) -> [Text] -> [(Text, Text)]
forall a b. (a -> b) -> [a] -> [b]
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
fmap ((Text -> Text) -> (Text, Text) -> (Text, Text)
forall a b. (a -> b) -> (Text, a) -> (Text, b)
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
fmap (Int -> Text -> Text
Text.drop Int
1) ((Text, Text) -> (Text, Text))
-> (Text -> (Text, Text)) -> Text -> (Text, Text)
forall b c a. (b -> c) -> (a -> b) -> a -> c
. HasCallStack => Text -> Text -> (Text, Text)
Text -> Text -> (Text, Text)
Text.breakOn Text
",") ([Text] -> [(Text, Text)])
-> (Text -> [Text]) -> Text -> [(Text, Text)]
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Text -> [Text]
Text.lines (Text -> HashMap Text Text) -> Text -> HashMap Text Text
forall a b. (a -> b) -> a -> b
$
Text
"millimeter,mm\n\
\centimeter,cm\n\
\meter,m\n\
\kilometer,km\n\
\inch,in\n\
\foot,ft\n\
\yard,yd\n\
\mile,mi\n\
\square-meter,m²\n\
\square-kilometer,km²\n\
\square-foot,sq ft\n\
\square-mile,sq mi\n\
\hectare,ha\n\
\acre,ac\n\
\milliliter,mL\n\
\liter,L\n\
\gallon,gal\n\
\fluid-ounce,fl oz\n\
\pint,pt\n\
\quart,qt\n\
\tablespoon,tbsp\n\
\teaspoon,tsp\n\
\milligram,mg\n\
\gram,g\n\
\kilogram,kg\n\
\ounce,oz\n\
\pound,lb\n\
\stone,st\n\
\metric-ton,t\n\
\ton,tn\n\
\millisecond,ms\n\
\second,s\n\
\minute,min\n\
\hour,hr\n\
\day,day\n\
\week,wk\n\
\month,mth\n\
\year,yr\n\
\kilometer-per-hour,km/h\n\
\mile-per-hour,mph\n\
\meter-per-second,m/s\n\
\bit,bit\n\
\byte,byte\n\
\kilobyte,kB\n\
\megabyte,MB\n\
\gigabyte,GB\n\
\terabyte,TB\n\
\petabyte,PB\n\
\watt,W\n\
\kilowatt,kW\n\
\megawatt,MW\n\
\kilowatt-hour,kWh\n\
\celsius,°C\n\
\fahrenheit,°F\n\
\kelvin,K\n\
\volt,V\n\
\ampere,A\n\
\hertz,Hz\n\
\kilohertz,kHz\n\
\megahertz,MHz\n\
\gigahertz,GHz"
currencies :: HashMap Text Text
currencies :: HashMap Text Text
currencies =
[(Text, Text)] -> HashMap Text Text
forall k v. (Eq k, Hashable k) => [(k, v)] -> HashMap k v
HashMap.fromList ([(Text, Text)] -> HashMap Text Text)
-> (Text -> [(Text, Text)]) -> Text -> HashMap Text Text
forall b c a. (b -> c) -> (a -> b) -> a -> c
. (Text -> (Text, Text)) -> [Text] -> [(Text, Text)]
forall a b. (a -> b) -> [a] -> [b]
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
fmap ((Text -> Text) -> (Text, Text) -> (Text, Text)
forall a b. (a -> b) -> (Text, a) -> (Text, b)
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
fmap (Int -> Text -> Text
Text.drop Int
1) ((Text, Text) -> (Text, Text))
-> (Text -> (Text, Text)) -> Text -> (Text, Text)
forall b c a. (b -> c) -> (a -> b) -> a -> c
. HasCallStack => Text -> Text -> (Text, Text)
Text -> Text -> (Text, Text)
Text.breakOn Text
",") ([Text] -> [(Text, Text)])
-> (Text -> [Text]) -> Text -> [(Text, Text)]
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Text -> [Text]
Text.lines (Text -> HashMap Text Text) -> Text -> HashMap Text Text
forall a b. (a -> b) -> a -> b
$
Text
"GBP,£\n\
\EUR,€\n\
\USD,$\n\
\AUD,A$\n\
\CAD,CA$\n\
\JPY,¥\n\
\CNY,CN¥\n\
\KRW,₩\n\
\INR,₹\n\
\HKD,HK$\n\
\NZD,NZ$\n\
\BRL,R$\n\
\MXN,MX$"
currencyDigits :: Text -> Int
currencyDigits :: Text -> Int
currencyDigits Text
code
| Text
code Text -> [Text] -> Bool
forall a. Eq a => a -> [a] -> Bool
forall (t :: * -> *) a. (Foldable t, Eq a) => a -> t a -> Bool
`elem` Text -> [Text]
Text.words Text
"BHD JOD KWD LYD OMR TND" = Int
3
| Text
code Text -> [Text] -> Bool
forall a. Eq a => a -> [a] -> Bool
forall (t :: * -> *) a. (Foldable t, Eq a) => a -> t a -> Bool
`elem` Text -> [Text]
Text.words Text
"CLF UYW" = Int
4
| Text
code Text -> [Text] -> Bool
forall a. Eq a => a -> [a] -> Bool
forall (t :: * -> *) a. (Foldable t, Eq a) => a -> t a -> Bool
`elem` Text -> [Text]
Text.words Text
zeroDigits = Int
0
| Bool
otherwise = Int
2
where
zeroDigits :: Text
zeroDigits :: Text
zeroDigits =
Text
"ADP AFN ALL BIF BYR CLP COP DJF ESP GNF HUF IDR IQD IRR ISK ITL JPY KMF KPW KRW LAK LBP \
\LUF MGA MGF MMK MRO PKR PYG RWF SLL SOS STD SYP TMM TRL UGX UYI VND VUV XAF XOF XPF YER \
\ZMK ZWD"