packages feed

bcp47-0.3.0.0: library/Data/BCP47.hs

{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}

-- | /Human beings on our planet have, past and present, used a number of/
-- /languages. There are many reasons why one would want to identify the/
-- /language used when presenting or requesting information./
--
-- /The language of an information item or a user's language preferences often/
-- /need to be identified so that appropriate processing can be applied. For/
-- /example, the user's language preferences in a Web browser can be used to/
-- /select Web pages appropriately. Language information can also be used to/
-- /select among tools (such as dictionaries) to assist in the processing or/
-- /understanding of content in different languages.  Knowledge about the/
-- /particular language used by some piece of information content might be useful/
-- /or even required by some types of processing, for example, spell-checking,/
-- /computer-synthesized speech, Braille transcription, or high-quality print/
-- /renderings./
--
-- / - /<https://tools.ietf.org/html/bcp47>
module Data.BCP47
  ( BCP47
  , inits

    -- * Construction
  , mkLanguage
  , mkLocalized
  , fromText
  , parser

    -- * Serialization
  , toText

    -- * Subtags

    -- | A language tag is composed from a sequence of one or more "subtags",
    -- each of which refines or narrows the range of language identified by
    -- the overall tag. Subtags, in turn, are a sequence of alphanumeric characters
    -- (letters and digits), distinguished and separated from other subtags in a tag
    -- by a hyphen ("-", [Unicode] U+002D).
  , toSubtags

    -- ** Language
  , ISO639_1
  , language
  , languageToText
  , languageFromText

    -- ** Language Extension
  , LanguageExtension
  , extendedLanguageSubtags
  , languageExtensionToText
  , languageExtensionFromText

    -- ** Language Script
  , Script
  , script
  , scriptToText
  , scriptFromText

    -- ** Region
  , Country
  , region
  , regionToText
  , regionFromText

    -- ** Variant
  , Variant
  , variants
  , variantToText
  , variantFromText

    -- ** Extension
  , Extension
  , extensions
  , extensionToText
  , extensionFromText

    -- ** Private Use
  , PrivateUse
  , privateUse
  , privateUseToText
  , privateUseFromText

    -- * For testing
  , en
  , es
  , sw
  , enGB
  , enUS
  )
where

import Control.Applicative ((<|>))
import Control.Monad (MonadPlus)
import Country.Identifier
  ( unitedKingdomOfGreatBritainAndNorthernIreland
  , unitedStatesOfAmerica
  )
import Data.BCP47.Internal.Arbitrary
  ( Arbitrary
  , arbitrary
  , choose
  , elements
  , listOf
  , vectorOf
  )
import Data.BCP47.Internal.Extension
import Data.BCP47.Internal.Language
import Data.BCP47.Internal.LanguageExtension
import Data.BCP47.Internal.PrivateUse
import Data.BCP47.Internal.Region
import Data.BCP47.Internal.Script
import Data.BCP47.Internal.Subtags
import Data.BCP47.Internal.Variant
import Data.Bifunctor (first)
import Data.Foldable (toList)
import Data.LanguageCodes (ISO639_1 (EN, ES, SW))
import qualified Data.List as List
import Data.Maybe (mapMaybe)
import Data.Set (Set)
import qualified Data.Set as Set
import Data.Text (Text, pack)
import qualified Data.Text as T
import Data.Void (Void)
import Text.Megaparsec (Parsec, eof, hidden, many, optional, parse, try)
import Text.Megaparsec.Char (char)
import Text.Megaparsec.Error (errorBundlePretty)

-- | A language tag
--
-- Language tags are used to help identify languages, whether spoken, written,
-- signed, or otherwise signaled, for the purpose of communication. This
-- includes constructed and artificial languages but excludes languages not
-- intended primarily for human communication, such as programming languages.
data BCP47 = BCP47
  { language :: ISO639_1
  -- ^ The language subtag
  , subtags :: Set Subtags
  }
  deriving stock (Eq, Ord)

instance Arbitrary BCP47 where
  arbitrary = BCP47 <$> elements [EN, ES] <*> specs
   where
    oneOrNone f = choose (0, 1) >>= (`vectorOf` (f <$> arbitrary))
    manyOf f = listOf (f <$> arbitrary)
    regions = [minBound .. maxBound]
    specs =
      Set.fromList . mconcat
        <$> sequenceA
          [ manyOf SpecifyLanguageExtension
          , oneOrNone SpecifyScript
          , choose (0, 1) >>= (`vectorOf` (elements $ SpecifyRegion <$> regions))
          , manyOf SpecifyVariant
          , manyOf SpecifyExtension
          , oneOrNone SpecifyPrivateUse
          ]

instance Show BCP47 where
  show = T.unpack . toText

instance Read BCP47 where
  readsPrec _ s = case fromText $ T.pack s of
    Left _ -> []
    Right b -> [(b, "")]

-- | Serialize @'BCP47'@ to @'Text'@
--
-- Subtags are serialized in the order described in the BCP 47 specification.
-- Private-use subtags only appear at the end prefixed with an x.
toText :: BCP47 -> Text
toText b =
  T.intercalate "-" $
    mconcat
      [ [languageToText $ language b]
      , mapMaybe fromSubtags . Set.toList $ subtags b
      , if Set.null (privateUse b) then [] else ["x"]
      , map privateUseToText . Set.toList $ privateUse b
      ]
 where
  fromSubtags = \case
    SpecifyLanguageExtension x -> Just $ languageExtensionToText x
    SpecifyScript x -> Just $ scriptToText x
    SpecifyRegion x -> Just $ regionToText x
    SpecifyVariant x -> Just $ variantToText x
    SpecifyExtension x -> Just $ extensionToText x
    SpecifyPrivateUse _ -> Nothing

-- | Look up all language extension subtags
extendedLanguageSubtags :: BCP47 -> Set LanguageExtension
extendedLanguageSubtags = asSet $ \case
  SpecifyLanguageExtension x -> Just x
  _otherwise -> Nothing

-- | Look up the script subtag
script :: BCP47 -> Maybe Script
script = headMay . mapMaybe f . Set.toList . subtags
 where
  f = \case
    SpecifyScript x -> Just x
    _otherwise -> Nothing

-- | Look up the region subtag
region :: BCP47 -> Maybe Country
region = headMay . mapMaybe f . Set.toList . subtags
 where
  f = \case
    SpecifyRegion x -> Just x
    _otherwise -> Nothing

-- | Look up all variant subtags
variants :: BCP47 -> Set Variant
variants = asSet $ \case
  SpecifyVariant x -> Just x
  _otherwise -> Nothing

-- | Look up all extension subtags
extensions :: BCP47 -> Set Extension
extensions = asSet $ \case
  SpecifyExtension x -> Just x
  _otherwise -> Nothing

-- | Look up all private use subtags
privateUse :: BCP47 -> Set PrivateUse
privateUse = asSet $ \case
  SpecifyPrivateUse x -> Just x
  _otherwise -> Nothing

asSet :: Ord a => (Subtags -> Maybe a) -> BCP47 -> Set a
asSet f = Set.fromList . mapMaybe f . Set.toList . subtags

headMay :: [x] -> Maybe x
headMay [] = Nothing
headMay (x : _) = Just x

-- | Convert tag to list of subtags
toSubtags :: BCP47 -> [Subtags]
toSubtags tag = toList $ subtags tag

-- | Produce a list of @(<= priority)@ language tags
--
-- >>> inits <$> fromText (pack "en-GB-t-jp")
-- Right [en,en-GB,en-GB-t-jp]
inits :: BCP47 -> [BCP47]
inits tag =
  map (BCP47 (language tag) . Set.fromList) . List.inits $ toSubtags tag

-- | Construct a simple language tag
mkLanguage :: ISO639_1 -> BCP47
mkLanguage lang = BCP47 lang mempty

-- | Construct a localized tag
mkLocalized :: ISO639_1 -> Country -> BCP47
mkLocalized lang locale = BCP47 lang . Set.singleton $ SpecifyRegion locale

-- | Parse a language tag from text
--
-- >>> fromText $ pack "en"
-- Right en
--
-- >>> fromText $ pack "de-CH"
-- Right de-CH
--
-- >>> fromText $ pack "ru-USR"
-- Left "fromText:1:3:\n  |\n1 | ru-USR\n  |   ^\nunexpected '-'\n"
--
-- >>> fromText $ pack "en-a-ccc-v-qqq-a-bbb"
-- Right en-a-bbb-a-ccc-v-qqq
--
-- >>> fromText $ pack "de-Latn-DE"
-- Right de-Latn-DE
--
-- >>> fromText $ pack "de-Latf-DE"
-- Right de-Latf-DE
--
-- >>> fromText $ pack "de-CH-1996"
-- Right de-CH-1996
--
-- >>> fromText $ pack "de-Deva"
-- Right de-Deva
--
-- >>> fromText $ pack "zh-Hant-CN-x-private1-private2"
-- Right zh-Hant-CN-x-private1-private2
--
-- >>> fromText $ pack "zh-Hant-CN-x-private1"
-- Right zh-Hant-CN-x-private1
--
-- >>> fromText $ pack "zh-Hant-CN"
-- Right zh-Hant-CN
--
-- >>> fromText $ pack "zh-Hant"
-- Right zh-Hant
--
-- >>> fromText $ pack "zh"
-- Right zh
fromText :: Text -> Either Text BCP47
fromText =
  first (pack . errorBundlePretty) . parse (parser <* hidden eof) "fromText"

-- |
--
-- >>> _example $ pack "en;"
-- Right (en,';')
_example :: Text -> Either Text (BCP47, Char)
_example = first (pack . errorBundlePretty) . parse p "example"
 where
  p = (,) <$> parser <*> char ';'

parser :: Parsec Void Text BCP47
parser = BCP47 <$> languageP <*> subtagsP
 where
  subtagsP =
    mconcat
      <$> sequenceA
        [ manyAsSet SpecifyLanguageExtension (try (char '-' *> languageExtensionP))
        , maybe mempty (Set.singleton . SpecifyScript)
            <$> (try (optional $ char '-' *> scriptP) <|> pure Nothing)
        , maybe mempty (Set.singleton . SpecifyRegion)
            <$> (try (optional $ char '-' *> regionP) <|> pure Nothing)
        , manyAsSet SpecifyVariant (try (char '-' *> variantP))
        , manyAsSet SpecifyExtension (try (char '-' *> extensionP))
        , Set.map SpecifyPrivateUse <$> (try (char '-' *> privateUseP) <|> mempty)
        ]

manyAsSet :: (Ord b, MonadPlus m) => (a -> b) -> m a -> m (Set b)
manyAsSet f p = Set.fromList . map f <$> many p

-- | Spanish
es :: BCP47
es = mkLanguage ES

-- | English
en :: BCP47
en = mkLanguage EN

-- | Swahili
sw :: BCP47
sw = mkLanguage SW

-- | British English
enGB :: BCP47
enGB = mkLocalized EN unitedKingdomOfGreatBritainAndNorthernIreland

-- | American English
enUS :: BCP47
enUS = mkLocalized EN unitedStatesOfAmerica