diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,1 @@
+Public Domain
diff --git a/Setup.hs b/Setup.hs
new file mode 100644
--- /dev/null
+++ b/Setup.hs
@@ -0,0 +1,2 @@
+import Distribution.Simple
+main = defaultMain
diff --git a/cndict.cabal b/cndict.cabal
new file mode 100644
--- /dev/null
+++ b/cndict.cabal
@@ -0,0 +1,38 @@
+-- Initial cndict.cabal generated by cabal init.  For further
+-- documentation, see http://haskell.org/cabal/users-guide/
+
+name:                cndict
+version:             0.1.0
+synopsis:            Chinese/Mandarin <-> English dictionary, Chinese lexer.
+-- description:
+license:             PublicDomain
+license-file:        LICENSE
+author:              Lemmih <lemmih@gmail.com>
+maintainer:          Lemmih <lemmih@gmail.com>
+homepage:            https://github.com/Lemmih/cndict
+-- copyright:
+category:            Natural Language Processing
+build-type:          Simple
+cabal-version:       >=1.8
+
+data-files:
+  data/SUBTLEX_CH_131210_CE.utf8
+  data/cedict_1_0_ts_utf-8_mdbg.txt
+
+source-repository head
+    type: git
+    location: git://github.com/Lemmih/cndict.git
+
+library
+  exposed-modules:     Data.Chinese.CCDict, Data.Chinese.Pinyin, Data.Chinese.Frequency
+  -- other-modules:
+  build-depends:       base       == 4.6.*,
+                       text       >= 0.11.0.0,
+                       file-embed >= 0.0.4.9,
+                       containers >= 0.5.0.0,
+                       vector     >= 0.10.0.0,
+                       bytestring >= 0.9.0.0,
+                       cassava    >= 0.2.0.0
+  hs-source-dirs:      src
+  ghc-options:         -Wall
+
diff --git a/data/SUBTLEX_CH_131210_CE.utf8 b/data/SUBTLEX_CH_131210_CE.utf8
new file mode 100644
# file too large to diff: data/SUBTLEX_CH_131210_CE.utf8
diff --git a/data/cedict_1_0_ts_utf-8_mdbg.txt b/data/cedict_1_0_ts_utf-8_mdbg.txt
new file mode 100644
# file too large to diff: data/cedict_1_0_ts_utf-8_mdbg.txt
diff --git a/src/Data/Chinese/CCDict.hs b/src/Data/Chinese/CCDict.hs
new file mode 100644
--- /dev/null
+++ b/src/Data/Chinese/CCDict.hs
@@ -0,0 +1,140 @@
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE TemplateHaskell   #-}
+-- | Simplified Chinese <-> English dictionary with pinyin phonetics.
+module Data.Chinese.CCDict
+  ( CCDict
+  , Entry(..)
+  , load
+  , parse
+  , lookup
+  , ccDict
+  , Token(..)
+  , tokenizer
+  ) where
+
+import           Data.Char
+import           Data.FileEmbed
+import           Data.List           (foldl', nub)
+import           Data.Map            (Map)
+import qualified Data.Map            as M
+import           Data.Maybe
+import           Data.Text           (Text)
+import qualified Data.Text           as T
+import qualified Data.Text.Encoding  as T
+import qualified Data.Text.IO        as T
+
+import           Prelude             hiding (lookup)
+
+import           Data.Chinese.Pinyin
+
+--------------------------------------------------
+-- Dictionary
+
+
+-- | Dictionary entry
+data Entry = Entry
+  { entryChinese    :: Text
+  , entryPinyin     :: [Text]
+  , entryDefinition :: [Text]
+  } deriving ( Read, Show, Eq, Ord )
+
+type CCDict = Map Char CCTrieEntry
+data CCTrieEntry = CCTrieEntry (Maybe Entry) CCDict
+
+-- | Load dictionary from file.
+load :: FilePath -> IO CCDict
+load path = parse `fmap` T.readFile path
+
+-- | Load dictionary from unicode text.
+parse :: Text -> CCDict
+parse txt = fromList [ entry | Just entry <- map parseLine (T.lines txt) ]
+
+-- | O(n). Lookup dictionary entry for a string of simplified chinese.
+lookup :: Text -> CCDict -> Maybe Entry
+lookup key trie =
+    case T.unpack key of
+      [] -> Nothing
+      (x:xs) -> go xs =<< M.lookup x trie
+  where
+    go [] (CCTrieEntry es _) = es
+    go (x:xs) (CCTrieEntry es m) = maybe es (go xs) (M.lookup x m)
+
+
+--------------------------------------------------
+-- Tokenizer
+
+data Token = KnownWord Entry | UnknownWord Text
+  deriving ( Read, Show, Eq, Ord )
+
+-- | Break a string of simplified chinese down to a list of tokens.
+tokenizer :: CCDict -> Text -> [Token]
+tokenizer trie inp = filter isValid $ go 0 inp inp
+  where
+    isValid (UnknownWord txt) = not (T.null txt)
+    isValid _ = True
+    go n unrecognied txt
+      | T.null txt = [ unknown ]
+      | otherwise =
+          case lookup txt trie of
+            Nothing -> go (n+1) unrecognied (T.drop 1 txt)
+            Just es ->
+              let rest = T.drop (T.length (entryChinese es)) txt in
+              unknown : KnownWord es : go 0 rest rest
+      where
+        unknown = UnknownWord $ T.take n unrecognied
+
+
+
+--------------------------------------------------
+-- Dictionary trie
+
+union :: CCDict -> CCDict -> CCDict
+union = M.unionWith join
+  where
+    join (CCTrieEntry e1 t1) (CCTrieEntry e2 t2) =
+      CCTrieEntry (joinEntry e1 e2) (M.unionWith join t1 t2)
+
+joinEntry :: Maybe Entry -> Maybe Entry -> Maybe Entry
+joinEntry Nothing Nothing     = Nothing
+joinEntry Nothing (Just e)    = Just e
+joinEntry (Just e) Nothing    = Just e
+joinEntry (Just e1) (Just e2) = Just Entry
+  { entryChinese = entryChinese e1
+  , entryPinyin     = nub $ entryPinyin e1 ++ entryPinyin e2
+  , entryDefinition = nub $ entryDefinition e1 ++ entryDefinition e2 }
+
+unions :: [CCDict] -> CCDict
+unions = foldl' union M.empty
+
+fromList :: [Entry] -> CCDict
+fromList = unions . map singleton
+
+singleton :: Entry -> CCDict
+singleton entry = go (T.unpack (entryChinese entry))
+  where
+    go []     = error "singleton: Invalid entry."
+    go [x]    = M.singleton x (CCTrieEntry (Just entry) M.empty)
+    go (x:xs) = M.singleton x (CCTrieEntry Nothing (go xs))
+
+parseLine :: Text -> Maybe Entry
+parseLine line | "#" `T.isPrefixOf` line = Nothing
+parseLine line =
+    Just Entry
+    { entryChinese = chinese
+    , entryPinyin     = map toToneMarks $ T.words $ T.tail $ T.init $ T.unwords (pinyin ++ [pin])
+    , entryDefinition = [T.unwords english] }
+  where
+    (_traditional : chinese : rest) = T.words line
+    (pinyin, (pin : english)) = break (\word -> T.count "]" word > 0) rest
+
+
+
+--------------------------------------------------
+-- Embedded dictionary
+
+-- | Embedded dictionary.
+ccDict :: CCDict
+ccDict = parse $ T.decodeUtf8 raw
+  where
+    raw = $(embedFile "data/cedict_1_0_ts_utf-8_mdbg.txt")
+
diff --git a/src/Data/Chinese/Frequency.hs b/src/Data/Chinese/Frequency.hs
new file mode 100644
--- /dev/null
+++ b/src/Data/Chinese/Frequency.hs
@@ -0,0 +1,63 @@
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE TemplateHaskell   #-}
+module Data.Chinese.Frequency
+  ( SubtlexMap
+  , SubtlexEntry(..)
+  , subtlex
+  ) where
+
+import           Control.Applicative
+import qualified Data.ByteString.Lazy as L
+import           Data.Csv             as Csv
+import           Data.FileEmbed
+import           Data.Map             (Map)
+import qualified Data.Map             as M
+import           Data.Text            (Text)
+import qualified Data.Text            as T
+import           Data.Vector          (Vector)
+import qualified Data.Vector          as V
+
+import           Data.Chinese.Pinyin
+
+type SubtlexMap = Map Text SubtlexEntry
+
+data SubtlexEntry = SubtlexEntry
+  { subtlexWord     :: T.Text
+  , subtlexPinyin   :: [T.Text]
+  , subtlexWCount   :: Int
+  , subtlexWMillion :: Double
+  , subtlexEnglish  :: T.Text
+  } deriving ( Show )
+
+instance FromRecord SubtlexEntry where
+  parseRecord rec = SubtlexEntry
+    <$> index rec 0
+    <*> fmap (map toToneMarks . T.splitOn "/") (index rec 2)
+    <*> index rec 4
+    <*> index rec 5 <*> index rec 14
+
+loadSubtlexEntries :: FilePath -> IO (Vector SubtlexEntry)
+loadSubtlexEntries path = do
+  inp <- L.readFile path
+  case Csv.decodeWith (Csv.DecodeOptions 9) True inp of
+    Left msg   -> error msg
+    Right rows -> return rows
+
+mkSubtlexMap :: Vector SubtlexEntry -> SubtlexMap
+mkSubtlexMap rows = M.fromList [ (subtlexWord row, row) | row <- V.toList rows ]
+
+
+
+
+
+
+------------------------------------------------------------
+-- Embedded files
+
+subtlex :: SubtlexMap
+subtlex = mkSubtlexMap $
+  case Csv.decodeWith (Csv.DecodeOptions 9) True inp of
+    Left msg -> error msg
+    Right rows -> rows
+  where
+    inp = L.fromStrict $(embedFile "data/SUBTLEX_CH_131210_CE.utf8")
diff --git a/src/Data/Chinese/Pinyin.hs b/src/Data/Chinese/Pinyin.hs
new file mode 100644
--- /dev/null
+++ b/src/Data/Chinese/Pinyin.hs
@@ -0,0 +1,58 @@
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE PatternGuards     #-}
+module Data.Chinese.Pinyin
+  ( toToneMarks
+  , fromToneMarks
+  ) where
+
+import           Data.Char
+import           Data.List
+import           Data.Maybe
+import           Data.Text  (Text)
+import qualified Data.Text  as T
+
+toToneMarks :: Text -> Text
+toToneMarks = modToneNumber toTonal
+
+fromToneMarks :: Text -> Text
+fromToneMarks = undefined
+
+modToneNumber :: (Int -> Char -> Char) -> Text -> Text
+modToneNumber fn txt
+  | T.null txt || not (isDigit (T.last txt)) = txt
+  | Just n <- T.findIndex (`elem` "ae") txt' = modify n
+  | Just n <- findStrIndex "ou" txt'         = modify n
+  | Just n <- findSecondVowel txt'           = modify n
+  | Just n <- T.findIndex (`elem` "aoeiu") txt' = modify n
+  | otherwise = txt
+  where
+    tone = digitToInt (T.last txt)
+    modify n = T.pack [ if n==i then fn tone c else c | (i,c) <- zip [0..] (T.unpack txt') ]
+    txt' = T.init txt
+
+findSecondVowel :: Text -> Maybe Int
+findSecondVowel = listToMaybe . drop 1 . findIndices isVowel . T.unpack
+  where
+    isVowel = (`elem` "aoeiu")
+
+findStrIndex :: Text -> Text -> Maybe Int
+findStrIndex key = worker 0
+  where
+    worker n line
+      | T.null line             = Nothing
+      | key `T.isPrefixOf` line = Just n
+      | otherwise               = worker (n+1) (T.drop 1 line)
+
+toTonal :: Int -> Char -> Char
+toTonal n key =
+    case Prelude.lookup key lst of
+      Nothing    -> key
+      Just tones -> tones !! (n-1)
+  where
+    lst =
+      [ ('a', "āáǎàa")
+      , ('o', "ōóǒòo")
+      , ('e', "ēéěèe")
+      , ('i', "īíǐìi")
+      , ('u', "ūúǔùu") ]
+
