diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,13 @@
 # Revision history for xdg-desktop-entry
 
+## 0.1.1.6 -- 2026-09-04
+
+* Replace the `ini` based parser with one that follows the desktop entry
+  specification. Localised keys such as `Name[de]` and `GenericName[ar]` no
+  longer make `readDesktopEntry` fail, so nearly every real-world desktop file
+  now parses. Files are decoded as UTF-8 regardless of the process locale.
+* Drop the `ini` and `unordered-containers` dependencies.
+
 ## 0.1.1.5 -- 2026-05-13
 
 * Clean up parser and test warnings for newer GHC releases.
diff --git a/src/System/Environment/XDG/DesktopEntry.hs b/src/System/Environment/XDG/DesktopEntry.hs
--- a/src/System/Environment/XDG/DesktopEntry.hs
+++ b/src/System/Environment/XDG/DesktopEntry.hs
@@ -39,16 +39,16 @@
 import Control.Monad
 import Control.Monad.IO.Class
 import Control.Monad.Trans.Except
-import Data.Bifunctor (bimap)
+import qualified Data.ByteString as BS
 import Data.Char
 import Data.Either
 import Data.Either.Combinators
-import qualified Data.HashMap.Strict as HM
-import qualified Data.Ini as Ini
 import Data.List
 import Data.Maybe
 import qualified Data.MultiMap as MM
-import Data.Text (pack, unpack)
+import Data.Text (unpack)
+import Data.Text.Encoding (decodeUtf8With)
+import Data.Text.Encoding.Error (lenientDecode)
 import Safe
 import System.Directory
 import System.FilePath.Posix
@@ -193,19 +193,46 @@
 -- | Read a desktop entry from a file.
 readDesktopEntry :: FilePath -> IO (Either String DesktopEntry)
 readDesktopEntry filePath = runExceptT $ do
-  -- let foo1 = join . fmap except . liftIO $ Ini.readIniFile filePath
-  -- let bar :: ExceptT String IO (HM.HashMap Text [(Text, Text)]) = map Ini.iniSections . liftIO $ Ini.readIniFile filePath
-  -- sections <- fmap Ini.iniSections . join . fmap except . liftIO $ Ini.readIniFile filePath
-  sections <- liftIO (Ini.readIniFile filePath) >>= fmap Ini.iniSections . except
+  contents <- liftIO $ unpack . decodeUtf8With lenientDecode <$> BS.readFile filePath
+  groups <- except $ parseDesktopEntryGroups contents
   result <-
-    maybe (throwE "Section [Desktop Entry] not found") (pure . fmap (bimap unpack unpack)) $
-      HM.lookup (pack "Desktop Entry") sections
+    maybe (throwE "Section [Desktop Entry] not found") pure $
+      lookup "Desktop Entry" groups
   return
     DesktopEntry
       { deType = fromMaybe Application $ lookup "Type" result >>= readMaybe,
         deFilename = filePath,
         deAttributes = result
       }
+
+-- | Parse the groups of a desktop entry file into association lists, in file
+-- order. Keys keep their locale suffix (e.g. @Name[de]@), whitespace around
+-- @=@ is ignored, and blank and @#@ comment lines are skipped.
+parseDesktopEntryGroups :: String -> Either String [(String, [(String, String)])]
+parseDesktopEntryGroups = go Nothing [] . zip [1 :: Int ..] . lines
+  where
+    go current done [] = Right $ reverse $ finish current done
+    go current done ((lineNo, rawLine) : rest)
+      | null line || "#" `isPrefixOf` line = go current done rest
+      | "[" `isPrefixOf` line && "]" `isSuffixOf` line =
+          go (Just (takeWhile (/= ']') $ drop 1 line, [])) (finish current done) rest
+      | otherwise =
+          case break (== '=') line of
+            (rawKey, '=' : rawValue)
+              | not (null key) ->
+                  case current of
+                    Nothing ->
+                      Left $ printf "line %d: entry before any group header" lineNo
+                    Just (name, entries) ->
+                      go (Just (name, (key, trim rawValue) : entries)) done rest
+              where
+                key = trim rawKey
+            _ -> Left $ printf "line %d: expected a group header or key=value" lineNo
+      where
+        line = trim rawLine
+    finish Nothing done = done
+    finish (Just (name, entries)) done = (name, reverse entries) : done
+    trim = dropWhileEnd isSpace . dropWhile isSpace
 
 -- | Construct a 'MM.Multimap' where each 'DesktopEntry' in the provided
 -- foldable is indexed by the keys returned from the provided indexing function.
diff --git a/test/Main.hs b/test/Main.hs
--- a/test/Main.hs
+++ b/test/Main.hs
@@ -12,7 +12,27 @@
     "[Desktop Entry]\n\
     \icon=2",
     "[desktop entry]\n\
-    \Icon=3"
+    \Icon=3",
+    "# Localised keys, comments, padding around '=' and extra groups.\n\
+    \[Desktop Entry]\n\
+    \Version=1.0\n\
+    \Name=Google Chrome\n\
+    \# Only KDE 4 seems to use GenericName.\n\
+    \GenericName=Web Browser\n\
+    \GenericName[ar]=متصفح الشبكة\n\
+    \Name[de]=Google Chrome (de)\n\
+    \Icon = google-chrome\n\
+    \Exec=/usr/bin/google-chrome %U\n\
+    \Actions=new-window;\n\
+    \\n\
+    \[Desktop Action new-window]\n\
+    \Name=New Window\n\
+    \Exec=/usr/bin/google-chrome",
+    "Icon=before-any-group\n\
+    \[Desktop Entry]\n\
+    \Icon=4",
+    "[Desktop Entry]\n\
+    \this line has no equals sign"
   ]
 
 main :: IO ()
@@ -39,4 +59,24 @@
             deIcon deResult `shouldBe` Nothing
       it "content2 should not work" $ do
         deResultE <- readDesktopEntry $ filepath 2
+        isLeft deResultE `shouldBe` True
+    describe "readDesktopEntry" $ do
+      it "parses localised keys, comments and extra groups" $ do
+        deResultE <- readDesktopEntry $ filepath 3
+        case deResultE of
+          Left e -> expectationFailure $ show e
+          Right deResult -> do
+            deIcon deResult `shouldBe` Just "google-chrome"
+            deName [] deResult `shouldBe` "Google Chrome"
+            deName ["de"] deResult `shouldBe` "Google Chrome (de)"
+            deName ["ar"] deResult `shouldBe` "Google Chrome"
+            deCommand deResult `shouldBe` Just "/usr/bin/google-chrome"
+            lookup "GenericName[ar]" (deAttributes deResult)
+              `shouldBe` Just "متصفح الشبكة"
+            lookup "Name" (deAttributes deResult) `shouldBe` Just "Google Chrome"
+      it "rejects entries before any group header" $ do
+        deResultE <- readDesktopEntry $ filepath 4
+        isLeft deResultE `shouldBe` True
+      it "rejects lines that are neither headers nor key=value" $ do
+        deResultE <- readDesktopEntry $ filepath 5
         isLeft deResultE `shouldBe` True
diff --git a/xdg-desktop-entry.cabal b/xdg-desktop-entry.cabal
--- a/xdg-desktop-entry.cabal
+++ b/xdg-desktop-entry.cabal
@@ -1,6 +1,6 @@
 cabal-version:       2.4
 name:                xdg-desktop-entry
-version:             0.1.1.5
+version:             0.1.1.6
 synopsis:            Parse files conforming to the xdg desktop entry spec
 description:         Parse files conforming to the xdg desktop entry spec.
 bug-reports:         https://github.com/taffybar/taffybar/issues
@@ -22,16 +22,15 @@
 library
   exposed-modules:     System.Environment.XDG.DesktopEntry
   build-depends:       base >=4.13 && < 5,
+                       bytestring >= 0.10 && < 0.13,
                        directory >= 1.3.6 && < 1.4,
                        either >= 5.0.1.1 && < 5.1,
                        filepath >= 1.4.2 && < 1.6,
-                       ini >= 0.4.1 && < 0.6,
                        multimap >= 1.2.1 && < 1.3,
                        safe >= 0.3.19 && < 0.4,
                        text >= 1.2.4 && < 2.2,
                        transformers >= 0.5.6 && < 0.7,
-                       unix >= 2.7.2 && < 2.9,
-                       unordered-containers >= 0.2.10 && < 0.3
+                       unix >= 2.7.2 && < 2.9
   hs-source-dirs:      src
   default-language:    Haskell2010
   ghc-options:         -Wall
