diff --git a/Bustle.hs b/Bustle.hs
--- a/Bustle.hs
+++ b/Bustle.hs
@@ -25,6 +25,7 @@
 import System.Exit (exitFailure)
 import Control.Monad (when)
 import Bustle.Noninteractive
+import Bustle.Translation
 import Bustle.UI
 
 usage :: Bool
@@ -49,6 +50,7 @@
 
 main :: IO ()
 main = do
+    initTranslation
     args <- getArgs
 
     case args of
diff --git a/Bustle/Diagram.hs b/Bustle/Diagram.hs
--- a/Bustle/Diagram.hs
+++ b/Bustle/Diagram.hs
@@ -341,8 +341,8 @@
                     in drawArc cx cy dx dy <$>
                           topx <*> topy <*> bottomx <*> bottomy <*> caption
           SignalArrow {} -> drawSignalArrow <$> epicentre
-                                            <*> Just . shapex1
-                                            <*> Just . shapex2
+                                            <*> (Just . shapex1)
+                                            <*> (Just . shapex2)
                                             <*> shapey
           DirectedSignalArrow { } -> drawDirectedSignalArrow <$> epicentre
                                                              <*> shapex
diff --git a/Bustle/Loader.hs b/Bustle/Loader.hs
--- a/Bustle/Loader.hs
+++ b/Bustle/Loader.hs
@@ -29,10 +29,13 @@
 import Control.Monad.Error
 import Control.Arrow ((***))
 
+import Text.Printf
+
 import qualified Bustle.Loader.OldSkool as Old
 import qualified Bustle.Loader.Pcap as Pcap
 import Bustle.Upgrade (upgrade)
 import Bustle.Types
+import Bustle.Translation (__)
 import Bustle.Util (io)
 
 data LoadError = LoadError FilePath String
@@ -58,7 +61,7 @@
             Right input -> do
                 let oldResult = fmap upgrade $ Old.readLog input
                 case oldResult of
-                    Left e  -> throwError $ LoadError f ("Parse error " ++ show e)
+                    Left e  -> throwError $ LoadError f (printf (__ "Parse error %s") (show e))
                     Right r -> return r
 
 isRelevant :: Event
diff --git a/Bustle/Noninteractive.hs b/Bustle/Noninteractive.hs
--- a/Bustle/Noninteractive.hs
+++ b/Bustle/Noninteractive.hs
@@ -33,6 +33,7 @@
 import Text.Printf
 
 import Bustle.Loader
+import Bustle.Translation (__)
 import Bustle.Types
 import Bustle.Stats
 
@@ -44,18 +45,14 @@
     ret <- runErrorT $ readLog filepath
     case ret of
         Left (LoadError _ err) -> do
-            warn $ concat [ "Couldn't parse "
-                          , filepath
-                          , ": "
-                          , err
-                          ]
+            warn $ printf (__ "Couldn't parse '%s': %s") filepath err
             exitFailure
         Right (warnings, log) -> do
             mapM warn warnings
             mapM_ (putStrLn . format) $ analyze log
 
 formatInterface :: Maybe InterfaceName -> String
-formatInterface = maybe "(no interface)" formatInterfaceName
+formatInterface = maybe (__ "(no interface)") formatInterfaceName
 
 runCount :: FilePath -> IO ()
 runCount filepath = process filepath frequencies format
diff --git a/Bustle/StatisticsPane.hs b/Bustle/StatisticsPane.hs
--- a/Bustle/StatisticsPane.hs
+++ b/Bustle/StatisticsPane.hs
@@ -28,6 +28,7 @@
 import Text.Printf
 import Graphics.UI.Gtk hiding (Markup)
 import Bustle.Stats
+import Bustle.Translation (__)
 import Bustle.Types (Log)
 import qualified Bustle.Markup as Markup
 import Bustle.Markup (Markup)
@@ -89,7 +90,7 @@
     cellLayoutPackStart col renderer expand
     set renderer [ cellTextSizePoints := 7 ]
     cellLayoutSetAttributes col renderer store $ \x ->
-        [ cellTextMarkup := Just . Markup.unMarkup $ f x ]
+        [ cellTextMarkup := (Just . Markup.unMarkup) $ f x ]
     return renderer
 
 addMemberRenderer :: TreeViewColumn
@@ -152,7 +153,7 @@
   set countView [ treeViewHeadersVisible := False ]
 
   nameColumn <- treeViewColumnNew
-  treeViewColumnSetTitle nameColumn "Name"
+  treeViewColumnSetTitle nameColumn (__ "Name")
   set nameColumn [ treeViewColumnResizable := True
                  , treeViewColumnExpand := True
                  ]
@@ -167,7 +168,7 @@
   treeViewAppendColumn countView nameColumn
 
   countColumn <- treeViewColumnNew
-  treeViewColumnSetTitle countColumn "Frequency"
+  treeViewColumnSetTitle countColumn (__ "Frequency")
   treeViewColumnSetMinWidth countColumn 120
 
   -- Using a progress bar here is not really ideal, but I CBA to do anything
@@ -177,7 +178,7 @@
   cellLayoutSetAttributes countColumn countBar countStore $
       \(FrequencyInfo {fiFrequency = count}) ->
       [ cellProgressValue :=> do
-          upperBound <- maximum . map fiFrequency <$>
+          upperBound <- (maximum . map fiFrequency) <$>
                         listStoreToList countStore
           -- ensure that we always show *something*
           return $ 2 + (count * 98 `div` upperBound)
@@ -196,7 +197,7 @@
   set timeView [ treeViewHeadersVisible := True ]
 
   nameColumn <- treeViewColumnNew
-  treeViewColumnSetTitle nameColumn "Method"
+  treeViewColumnSetTitle nameColumn (__ "Method")
   set nameColumn [ treeViewColumnResizable := True
                  , treeViewColumnExpand := True
                  ]
@@ -205,11 +206,11 @@
       Markup.formatMember (tiInterface ti) (tiMethodName ti)
   treeViewAppendColumn timeView nameColumn
 
-  addTextStatColumn timeView timeStore "Total"
-                (printf "%.1f ms" . tiTotalTime)
-  addTextStatColumn timeView timeStore "Calls" (show . tiCallFrequency)
-  addTextStatColumn timeView timeStore "Mean"
-                (printf "%.1f ms" . tiMeanCallTime)
+  addTextStatColumn timeView timeStore (__ "Total")
+                (printf (__ "%.1f ms") . tiTotalTime)
+  addTextStatColumn timeView timeStore (__ "Calls") (show . tiCallFrequency)
+  addTextStatColumn timeView timeStore (__ "Mean")
+                (printf (__ "%.1f ms") . tiMeanCallTime)
 
   return (timeStore, timeView)
 
@@ -224,9 +225,9 @@
 
 formatSize :: Int -> Markup
 formatSize s
-    | s < maxB = value 1 `mappend` units "B"
-    | s < maxKB = value 1024 `mappend` units "KB"
-    | otherwise = value (1024 * 1024) `mappend` units "MB"
+    | s < maxB = value 1 `mappend` units (__ "B")
+    | s < maxKB = value 1024 `mappend` units (__ "KB")
+    | otherwise = value (1024 * 1024) `mappend` units (__ "MB")
   where
     maxB = 10000
     maxKB = 10000 * 1024
@@ -245,7 +246,7 @@
   set sizeView [ treeViewHeadersVisible := True ]
 
   nameColumn <- treeViewColumnNew
-  treeViewColumnSetTitle nameColumn "Member"
+  treeViewColumnSetTitle nameColumn (__ "Member")
   set nameColumn [ treeViewColumnResizable := True
                  , treeViewColumnExpand := True
                  ]
@@ -258,8 +259,8 @@
   addMemberRenderer nameColumn sizeStore True formatSizeInfoMember
   treeViewAppendColumn sizeView nameColumn
 
-  addStatColumn sizeView sizeStore "Smallest" (formatSize . siMinSize)
-  addStatColumn sizeView sizeStore "Mean" (formatSize . siMeanSize)
-  addStatColumn sizeView sizeStore "Largest" (formatSize . siMaxSize)
+  addStatColumn sizeView sizeStore (__ "Smallest") (formatSize . siMinSize)
+  addStatColumn sizeView sizeStore (__ "Mean") (formatSize . siMeanSize)
+  addStatColumn sizeView sizeStore (__ "Largest") (formatSize . siMaxSize)
 
   return (sizeStore, sizeView)
diff --git a/Bustle/Translation.hs b/Bustle/Translation.hs
new file mode 100644
--- /dev/null
+++ b/Bustle/Translation.hs
@@ -0,0 +1,25 @@
+module Bustle.Translation
+    (
+      initTranslation
+    , __
+    )
+where
+
+import Text.I18N.GetText
+import System.Locale.SetLocale
+import System.IO.Unsafe
+
+import GetText_bustle
+
+initTranslation :: IO ()
+initTranslation = do
+    setLocale LC_ALL (Just "")
+    domain <- getMessageCatalogDomain
+    dir <- getMessageCatalogDir
+    bindTextDomain domain (Just dir)
+    textDomain (Just domain)
+    return ()
+
+-- FIXME: I do not like this unsafePerformIO one little bit.
+__ :: String -> String
+__ = unsafePerformIO . getText
diff --git a/Bustle/UI.hs b/Bustle/UI.hs
--- a/Bustle/UI.hs
+++ b/Bustle/UI.hs
@@ -31,6 +31,7 @@
 import Data.List (intercalate)
 import Data.Time
 import Data.Monoid (mempty)
+import Text.Printf
 
 import Paths_bustle
 import Bustle.Application.Monad
@@ -46,6 +47,7 @@
 import Bustle.UI.Recorder
 import Bustle.UI.Util (displayError)
 import Bustle.StatisticsPane
+import Bustle.Translation (__)
 import Bustle.Loader
 
 import qualified Control.Exception as C
@@ -205,7 +207,7 @@
 
     case ret of
       Left (LoadError f e) -> io $
-          displayError Nothing ("Could not read '" ++ f ++ "'") (Just e)
+          displayError Nothing (printf (__ "Could not read '%s'") f) (Just e)
       Right () -> return ()
 
 startRecording :: B ()
@@ -281,15 +283,15 @@
     case mdetails of
         Just (RecordedLog tempFilePath) -> do
             let tempFileName = takeFileName tempFilePath
-                title = "Save log “" ++ tempFileName ++ "” before closing?"
+                title = printf (__ "Save log '%s' before closing?") tempFileName
             prompt <- messageDialogNew (Just (wiWindow wi))
                                        [DialogModal]
                                        MessageWarning
                                        ButtonsNone
                                        title
             messageDialogSetSecondaryText prompt
-                "If you don’t save, this log will be lost forever."
-            dialogAddButton prompt "Close _without saving" ResponseClose
+                (__ "If you don't save, this log will be lost forever.")
+            dialogAddButton prompt (__ "Close _Without Saving") ResponseClose
             dialogAddButton prompt stockCancel ResponseCancel
             dialogAddButton prompt stockSave ResponseYes
 
@@ -462,7 +464,7 @@
                 -> IO ()
 wiSetLogDetails wi logDetails = do
     writeIORef (wiLogDetails wi) (Just logDetails)
-    windowSetTitle (wiWindow wi) (logWindowTitle logDetails ++ " — Bustle")
+    windowSetTitle (wiWindow wi) (printf (__ "%s - Bustle") (logWindowTitle logDetails))
 
 setPage :: MonadIO io
         => WindowInfo
diff --git a/Bustle/UI/AboutDialog.hs b/Bustle/UI/AboutDialog.hs
--- a/Bustle/UI/AboutDialog.hs
+++ b/Bustle/UI/AboutDialog.hs
@@ -30,6 +30,7 @@
 
 import Graphics.UI.Gtk
 
+import Bustle.Translation (__)
 import Bustle.Util
 import Paths_bustle
 
@@ -43,12 +44,12 @@
     license <- (Just `fmap` (readFile =<< getDataFileName "LICENSE"))
                `catch` (\e -> warn (show (e :: IOException)) >> return Nothing)
 
-    dialog `set` [ aboutDialogName := "Bustle"
+    dialog `set` [ aboutDialogName := __ "Bustle"
                  , aboutDialogVersion := showVersion version
-                 , aboutDialogComments := "Someone's favourite D-Bus profiler"
+                 , aboutDialogComments := __ "Someone's favourite D-Bus profiler"
                  , aboutDialogWebsite := "http://willthompson.co.uk/bustle"
                  , aboutDialogAuthors := authors
-                 , aboutDialogCopyright := "© 2008–2013 Will Thompson, Collabora Ltd. and contributors"
+                 , aboutDialogCopyright := "© 2008–2014 Will Thompson, Collabora Ltd. and contributors"
                  , aboutDialogLicense := license
                  ]
     dialog `afterResponse` \resp ->
@@ -67,4 +68,5 @@
           , "Cosimo Alfarano"
           , "Sergei Trofimovich"
           , "Alex Merry"
+          , "Philip Withnall"
           ]
diff --git a/Bustle/UI/DetailsView.hs b/Bustle/UI/DetailsView.hs
--- a/Bustle/UI/DetailsView.hs
+++ b/Bustle/UI/DetailsView.hs
@@ -29,6 +29,7 @@
 
 import qualified DBus as D
 
+import Bustle.Translation (__)
 import Bustle.Types
 import Bustle.Markup
 import Bustle.VariantFormatter
@@ -80,10 +81,10 @@
     miscSetAlignment title 0 0
     tableAttach table title 0 2 0 1 [Fill] [Fill] 0 0
 
-    pathLabel <- addField table "Path:" 1
-    memberLabel <- addField table "Member:" 2
+    pathLabel <- addField table (__ "Path:") 1
+    memberLabel <- addField table (__ "Member:") 2
 
-    addTitle table "Arguments:" 3
+    addTitle table (__ "Arguments:") 3
 
     view <- textViewNew
     textViewSetWrapMode view WrapWordChar
@@ -100,13 +101,13 @@
 
 pickTitle :: Detailed Message -> Markup
 pickTitle (Detailed _ m _) = case m of
-    MethodCall {} -> b (escape "Method call")
-    MethodReturn {} -> b (escape "Method return")
-    Error {} -> b (escape "Error")
+    MethodCall {} -> b (escape (__ "Method call"))
+    MethodReturn {} -> b (escape (__ "Method return"))
+    Error {} -> b (escape (__ "Error"))
     Signal { signalDestination = d } ->
         b . escape $ case d of
-            Nothing -> "Signal"
-            Just _  -> "Directed signal"
+            Nothing -> (__ "Signal")
+            Just _  -> (__ "Directed signal")
 
 getMemberMarkup :: Member -> String
 getMemberMarkup m =
@@ -123,8 +124,8 @@
 
 formatMessage :: Detailed Message -> String
 formatMessage (Detailed _ _ Nothing) =
-    "# No message body information is available. Please capture a fresh log\n\
-    \# using bustle-pcap if you need it!"
+    __ "No message body information is available. Please capture a fresh log \
+       \using a recent version of Bustle!"
 formatMessage (Detailed _ _ (Just (_size, rm))) =
     formatArgs $ D.receivedMessageBody rm
   where
@@ -140,6 +141,8 @@
     buf <- textViewGetBuffer $ detailsBodyView d
     let member_ = getMember m
     labelSetMarkup (detailsTitle d) (unMarkup $ pickTitle m)
-    labelSetText (detailsPath d) (maybe "Unknown" (D.formatObjectPath . path) member_)
-    labelSetMarkup (detailsMember d) (maybe "Unknown" getMemberMarkup member_)
+    labelSetText (detailsPath d) (maybe unknown (D.formatObjectPath . path) member_)
+    labelSetMarkup (detailsMember d) (maybe unknown getMemberMarkup member_)
     textBufferSetText buf $ formatMessage m
+  where
+    unknown = __ "<unknown>"
diff --git a/Bustle/UI/FilterDialog.hs b/Bustle/UI/FilterDialog.hs
--- a/Bustle/UI/FilterDialog.hs
+++ b/Bustle/UI/FilterDialog.hs
@@ -28,6 +28,7 @@
 
 import Graphics.UI.Gtk
 
+import Bustle.Translation (__)
 import Bustle.Types
 
 formatNames :: (UniqueName, Set OtherName)
@@ -101,10 +102,10 @@
     instructions <- labelNew Nothing
     widgetSetSizeRequest instructions 600 (-1)
     labelSetMarkup instructions
-        "Unticking a service hides its column in the diagram, \
+        (__ "Unticking a service hides its column in the diagram, \
         \and all messages it is involved in. That is, all methods it calls \
         \or are called on it, the corresponding returns, and all signals it \
-        \emits will be hidden."
+        \emits will be hidden.")
     labelSetLineWrap instructions True
     boxPackStart vbox instructions PackNatural 0
 
diff --git a/Bustle/UI/Recorder.hs b/Bustle/UI/Recorder.hs
--- a/Bustle/UI/Recorder.hs
+++ b/Bustle/UI/Recorder.hs
@@ -28,6 +28,7 @@
 import qualified Data.Map as Map
 import Data.Monoid
 import Control.Monad.State (runStateT)
+import Text.Printf
 
 import qualified Control.Exception as C
 import System.Glib.GError
@@ -37,6 +38,7 @@
 import Bustle.Loader (isRelevant)
 import Bustle.Monitor
 import Bustle.Renderer
+import Bustle.Translation (__)
 import Bustle.Types
 import Bustle.UI.Util (displayError)
 import Bustle.Util
@@ -76,7 +78,7 @@
                 i <- takeMVar n
                 let j = i + (length pending)
                 labelSetMarkup label $
-                    "Logged <b>" ++ show j ++ "</b> messages…"
+                    printf (__ "Logged <b>%u</b> messages…") j
                 putMVar n j
 
                 incoming rr'
@@ -96,7 +98,7 @@
     dialog `set` [ windowModal := True ]
 
     label <- labelNew Nothing
-    labelSetMarkup label "Logged <b>0</b> messages…"
+    labelSetMarkup label $ printf (__ "Logged <b>%u</b> messages…") (0 :: Int)
     loaderStateRef <- newMVar Map.empty
     pendingRef <- newMVar []
     let updateLabel µs body = do
diff --git a/Bustle/Util.hs b/Bustle/Util.hs
--- a/Bustle/Util.hs
+++ b/Bustle/Util.hs
@@ -41,6 +41,7 @@
 import Foreign.C.String
 import System.Directory
 import System.FilePath ((</>))
+import Bustle.Translation (__)
 
 -- Escape hatch to log a value from a non-IO monadic context.
 traceM :: (Show a, Monad m) => a -> m ()
@@ -49,7 +50,7 @@
 -- Log a warning which isn't worth showing to the user, but which might
 -- interest someone debugging the application.
 warn :: String -> IO ()
-warn = hPutStrLn stderr . ("Warning: " ++)
+warn = hPutStrLn stderr . ((__ "Warning: ") ++)
 
 -- Shorthand for liftIO.
 io :: MonadIO m => IO a -> m a
diff --git a/Makefile b/Makefile
--- a/Makefile
+++ b/Makefile
@@ -5,15 +5,18 @@
 DESTDIR =
 PREFIX = /usr/local
 BINDIR = $(DESTDIR)$(PREFIX)/bin
-MAN1DIR = $(DESTDIR)$(PREFIX)/share/man/man1
+DATADIR = $(DESTDIR)$(PREFIX)/share
+MAN1DIR = $(DATADIR)/man/man1
 
 BINARIES = \
 	dist/build/bustle-pcap \
 	$(NULL)
 
 MANPAGE = bustle-pcap.1
+DESKTOP_FILE = bustle.desktop
+APPDATA_FILE = bustle.appdata.xml
 
-all: $(BINARIES) $(MANPAGE)
+all: $(BINARIES) $(MANPAGE) $(DESKTOP_FILE) $(APPDATA_FILE)
 
 BUSTLE_PCAP_SOURCES = c-sources/pcap-monitor.c c-sources/bustle-pcap.c
 BUSTLE_PCAP_GENERATED_HEADERS = dist/build/autogen/version.h
@@ -22,6 +25,12 @@
 bustle-pcap.1: dist/build/bustle-pcap
 	-help2man --output=$@ --no-info --name='Generate D-Bus logs for bustle' $<
 
+bustle.desktop: data/bustle.desktop.in
+	LC_ALL=C intltool-merge -d -u po $< $@
+
+bustle.appdata.xml: data/bustle.appdata.xml.in
+	LC_ALL=C intltool-merge -x -u po $< $@
+
 dist/build/bustle-pcap: $(BUSTLE_PCAP_SOURCES) $(BUSTLE_PCAP_HEADERS)
 	@mkdir -p dist/build
 	$(CC) -Idist/build/autogen $(CFLAGS) $(CPPFLAGS) $(LDFLAGS) \
@@ -37,13 +46,20 @@
 	mkdir -p $(BINDIR)
 	cp $(BINARIES) $(BINDIR)
 	-mkdir -p $(MAN1DIR)
-	-cp bustle-pcap.1 $(MAN1DIR)
+	-cp $(MANPAGE) $(MAN1DIR)
+	mkdir -p $(DATADIR)/applications
+	cp $(DESKTOP_FILE) $(DATADIR)/applications
+	mkdir -p $(DATADIR)/appdata
+	cp $(APPDATA_FILE) $(DATADIR)/appdata
 
 uninstall:
-	rm -f $(notdir $(BINARIES))
+	rm -f $(BINDIR)/$(notdir $(BINARIES))
+	rm -f $(MAN1DIR)/$(MANPAGE)
+	rm -f $(DATADIR)/applications/$(DESKTOP_FILE)
+	rm -f $(DATADIR)/appdata/$(APPDATA_FILE)
 
 clean:
-	rm -f $(BINARIES) $(MANPAGE) $(BUSTLE_PCAP_GENERATED_HEADERS)
+	rm -f $(BINARIES) $(MANPAGE) $(BUSTLE_PCAP_GENERATED_HEADERS) $(DESKTOP_FILE) $(APPDATA_FILE)
 	if test -d ./$(TARBALL_DIR); then rm -r ./$(TARBALL_DIR); fi
 	rm -f ./$(TARBALL)
 
@@ -55,17 +71,18 @@
 TARBALL := $(TARBALL_DIR).tar.bz2
 maintainer-binary-tarball: all
 	mkdir -p $(TARBALL_FULL_DIR)
-	cabal-dev configure --prefix=$(TOP)/$(TARBALL_FULL_DIR) \
+	cabal-dev install --prefix=$(TOP)/$(TARBALL_FULL_DIR) \
 		--datadir=$(TOP)/$(TARBALL_FULL_DIR) --datasubdir=.
-	cabal-dev build
-	cabal-dev copy
 	cp bustle.sh README $(TARBALL_FULL_DIR)
 	perl -pi -e 's{^    bustle-pcap}{    ./bustle-pcap};' \
 		-e  's{^    bustle}     {    ./bustle.sh};' \
 		$(TARBALL_FULL_DIR)/README
-	cp $(BINARIES) $(MANPAGE) $(TARBALL_FULL_DIR)
+	cp $(BINARIES) $(MANPAGE) $(DESKTOP_FILE) $(APPDATA_FILE) $(TARBALL_FULL_DIR)
 	mkdir -p $(TARBALL_FULL_DIR)/lib
 	cp LICENSE.bundled-libraries $(TARBALL_FULL_DIR)/lib
 	./ldd-me-up.sh $(TARBALL_FULL_DIR)/bin/bustle \
 		| xargs -I XXX cp XXX $(TARBALL_FULL_DIR)/lib
 	cd $(TARBALL_PARENT_DIR) && tar cjf $(TARBALL) $(TARBALL_DIR)
+
+maintainer-update-messages-pot:
+	find Bustle -name '*.hs' -print0 | xargs -0 hgettext -k __ -o po/messages.pot
diff --git a/NEWS b/NEWS
--- a/NEWS
+++ b/NEWS
@@ -1,3 +1,14 @@
+Bustle 0.4.4 (2014-01-30)
+-------------------------
+
+Wow, I can't believe the first release was in 2008!
+
+* Bustle's now translatable. It only ships with an English translation,
+  but others are more than welcome! Thanks to Philip Withnall for
+  getting this started.
+* Add an AppData and .desktop file. (Philip Withnall)
+
+
 Bustle 0.4.3 (2013-12-05)
 -------------------------
 
diff --git a/Setup.hs b/Setup.hs
--- a/Setup.hs
+++ b/Setup.hs
@@ -1,2 +1,113 @@
+{-# OPTIONS_GHC -Wall #-}
+import Data.Maybe (fromMaybe)
+import System.FilePath ( (</>), (<.>) )
+
+import Distribution.PackageDescription
 import Distribution.Simple
-main = defaultMain
+import Distribution.Simple.BuildPaths ( autogenModulesDir )
+import Distribution.Simple.InstallDirs as I
+import Distribution.Simple.LocalBuildInfo
+import Distribution.Simple.Setup as S
+import Distribution.Simple.Utils
+import Distribution.Text ( display )
+
+import Distribution.ModuleName (ModuleName)
+import qualified Distribution.ModuleName as ModuleName
+
+import qualified Distribution.Simple.I18N.GetText as GetText
+
+main :: IO ()
+main = defaultMainWithHooks $ installBustleHooks simpleUserHooks
+
+-- Okay, so we want to use hgettext's install hook, but not the hook that
+-- miraculously runs all our code through CPP just to add a couple of
+-- constants. (cpp doesn't like multi-line Haskell strings, so this is not
+-- purely an academic preference.)
+--
+-- Instead, we generate GetText_bustle.hs which contains the constants, in the
+-- same way as Paths_bustle.hs gets generated by Cabal. Much neater.
+--
+-- TODO: upstream this to hgettext
+installBustleHooks :: UserHooks
+                   -> UserHooks
+installBustleHooks uh = uh
+  { postInst = postInst gtuh
+  , buildHook = \pkg lbi hooks flags -> do
+        writeGetTextConstantsFile pkg lbi flags
+        buildHook uh pkg lbi hooks flags
+  }
+  where
+    gtuh = GetText.installGetTextHooks uh
+
+
+writeGetTextConstantsFile :: PackageDescription -> LocalBuildInfo -> BuildFlags -> IO ()
+writeGetTextConstantsFile pkg lbi flags = do
+    let verbosity = fromFlag (buildVerbosity flags)
+
+    createDirectoryIfMissingVerbose verbosity True (autogenModulesDir lbi)
+
+    let pathsModulePath = autogenModulesDir lbi
+                      </> ModuleName.toFilePath (getTextConstantsModuleName pkg) <.> "hs"
+    rewriteFile pathsModulePath (generateModule pkg lbi)
+
+getTextConstantsModuleName :: PackageDescription -> ModuleName
+getTextConstantsModuleName pkg_descr =
+  ModuleName.fromString $
+    "GetText_" ++ fixedPackageName pkg_descr
+
+-- Cargo-culted from two separate places in Cabal!
+fixedPackageName :: PackageDescription -> String
+fixedPackageName = map fixchar . display . packageName
+  where fixchar '-' = '_'
+        fixchar c   = c
+
+generateModule :: PackageDescription -> LocalBuildInfo -> String
+generateModule pkg lbi =
+    header ++ body
+  where
+    moduleName = getTextConstantsModuleName pkg
+
+    header =
+        "module " ++ display moduleName ++ " (\n"++
+        "    getMessageCatalogDomain,\n" ++
+        "    getMessageCatalogDir\n" ++
+        ") where\n"++
+        "\n" ++
+        "import qualified Control.Exception as Exception\n" ++
+        "import System.Environment (getEnv)\n"
+
+    body =
+        "catchIO :: IO a -> (Exception.IOException -> IO a) -> IO a\n" ++
+        "catchIO = Exception.catch\n" ++
+        "\n" ++
+        "getMessageCatalogDomain :: IO String\n" ++
+        "getMessageCatalogDomain = return " ++ show dom ++ "\n" ++
+        "\n" ++
+        "messageCatalogDir :: String\n" ++
+        "messageCatalogDir = " ++ show tar ++ "\n" ++
+        "\n" ++
+        "getMessageCatalogDir :: IO FilePath\n" ++
+        "getMessageCatalogDir = catchIO (getEnv \"" ++ fixedPackageName pkg ++ "_localedir\") (\\_ -> return messageCatalogDir)\n"
+
+    sMap = customFieldsPD (localPkgDescr lbi)
+    dom = getDomainNameDefault sMap (getPackageName lbi)
+    tar = targetDataDir lbi
+
+-- Cargo-culted from hgettext
+findInParametersDefault :: [(String, String)] -> String -> String -> String
+findInParametersDefault al name def = (fromMaybe def . lookup name) al
+
+getPackageName :: LocalBuildInfo -> String
+getPackageName = fromPackageName . packageName . localPkgDescr
+    where fromPackageName (PackageName s) = s
+
+getDomainNameDefault :: [(String, String)] -> String -> String
+getDomainNameDefault al d = findInParametersDefault al "x-gettext-domain-name" d
+
+targetDataDir :: LocalBuildInfo -> FilePath
+targetDataDir l =
+    let dirTmpls = installDirTemplates l
+        prefix' = prefix dirTmpls
+        data' = datadir dirTmpls
+        dataEx = I.fromPathTemplate $ I.substPathTemplate [(PrefixVar, prefix')] data'
+    in dataEx ++ "/locale"
diff --git a/bustle.cabal b/bustle.cabal
--- a/bustle.cabal
+++ b/bustle.cabal
@@ -1,19 +1,19 @@
 Name:           bustle
 Category:       Network, Desktop
-Version:        0.4.3
+Version:        0.4.4
 Cabal-Version:  >= 1.8
 Synopsis:       Draw pretty sequence diagrams of D-Bus traffic
 Description:    Draw pretty sequence diagrams of D-Bus traffic
 License:        OtherLicense
 License-file:   LICENSE
-Author:         Will Thompson <will.thompson@collabora.co.uk>
-Maintainer:     Will Thompson <will.thompson@collabora.co.uk>
+Author:         Will Thompson <will@willthompson.co.uk>
+Maintainer:     Will Thompson <will@willthompson.co.uk>
 Data-files:     data/bustle.png,
                 data/dfeet-method.png,
                 data/dfeet-signal.png,
                 data/bustle.ui,
                 LICENSE
-Build-type:     Simple
+Build-type:     Custom
 Extra-source-files:
                   -- C bits
                     c-sources/bustle-pcap.c,
@@ -34,7 +34,13 @@
                   , ldd-me-up.sh
                   , LICENSE.bundled-libraries
 
+                  -- wow many translate
+                  , po/*.po
+                  , po/*.pot
 
+x-gettext-po-files:     po/*.po
+x-gettext-domain-name:  bustle
+
 Source-Repository head
   Type:           git
   Location:       git://anongit.freedesktop.org/bustle
@@ -61,6 +67,7 @@
                , Bustle.Renderer
                , Bustle.StatisticsPane
                , Bustle.Stats
+               , Bustle.Translation
                , Bustle.Types
                , Bustle.UI
                , Bustle.UI.AboutDialog
@@ -89,11 +96,13 @@
                , filepath
                , glib
                , gtk >= 0.12.4
+               , hgettext >= 0.1.5
                , mtl
                , pango
                , parsec
                , pcap
                , process
+               , setlocale
                , text
                , time
 
@@ -118,10 +127,12 @@
                , filepath
                , gtk > 0.12
                , glib
+               , hgettext
                , mtl
                , pango
                , parsec
                , pcap
+               , setlocale
                , text
 
 Executable dump-messages
diff --git a/bustle.sh b/bustle.sh
--- a/bustle.sh
+++ b/bustle.sh
@@ -6,6 +6,9 @@
 bustle_datadir="${root}"
 export bustle_datadir
 
+bustle_localedir="${root}/locale"
+export bustle_localedir
+
 LD_LIBRARY_PATH="${root}/lib:${LD_LIBRARY_PATH}"
 export LD_LIBRARY_PATH
 
diff --git a/c-sources/bustle-pcap.c b/c-sources/bustle-pcap.c
--- a/c-sources/bustle-pcap.c
+++ b/c-sources/bustle-pcap.c
@@ -127,9 +127,9 @@
   if (version)
     {
       fprintf (stdout, "bustle-pcap " BUSTLE_VERSION "\n\n");
-      fprintf (stdout, "Copyright 2011 Will Thompson <will.thompson@collabora.co.uk>\n");
+      fprintf (stdout, "Copyright 2011 Will Thompson <will@willthompson.co.uk>\n");
       fprintf (stdout, "This is free software; see the source for copying conditions.  There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n\n");
-      fprintf (stdout, "Written by Will Thompson <will.thompson@collabora.co.uk>\n");
+      fprintf (stdout, "Written by Will Thompson <will@willthompson.co.uk>\n");
       exit (0);
     }
   else if (session_specified && system_specified)
diff --git a/po/en.po b/po/en.po
new file mode 100644
--- /dev/null
+++ b/po/en.po
@@ -0,0 +1,164 @@
+# Translation file
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2009-01-13 06:05-0800\n"
+"PO-Revision-Date: 2014-01-12 14:19+0000\n"
+"Last-Translator: Will Thompson <will@willthompson.co.uk>\n"
+"Language-Team: English\n"
+"Language: en\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: Bustle/StatisticsPane.hs:210 Bustle/StatisticsPane.hs:213
+msgid "%.1f ms"
+msgstr "%.1f ms"
+
+#: Bustle/UI.hs:467
+msgid "%s - Bustle"
+msgstr "%s – Bustle"
+
+#: Bustle/Noninteractive.hs:55
+msgid "(no interface)"
+msgstr "(no interface)"
+
+#: Bustle/UI/DetailsView.hs:148
+msgid "<unknown>"
+msgstr "<unknown>"
+
+#: Bustle/UI/DetailsView.hs:87
+msgid "Arguments:"
+msgstr "Arguments:"
+
+#: Bustle/StatisticsPane.hs:228
+msgid "B"
+msgstr "B"
+
+#: Bustle/UI/AboutDialog.hs:47
+msgid "Bustle"
+msgstr "Bustle"
+
+#: Bustle/StatisticsPane.hs:211
+msgid "Calls"
+msgstr "Calls"
+
+#: Bustle/UI.hs:294
+msgid "Close _Without Saving"
+msgstr "Close _Without Saving"
+
+#: Bustle/UI.hs:210
+msgid "Could not read '%s'"
+msgstr "Could not read ‘%s’"
+
+#: Bustle/Noninteractive.hs:48
+msgid "Couldn't parse '%s': %s"
+msgstr "Couldn’t parse ‘%s’: %s"
+
+#: Bustle/UI/DetailsView.hs:110
+msgid "Directed signal"
+msgstr "Directed signal"
+
+#: Bustle/UI/DetailsView.hs:106
+msgid "Error"
+msgstr "Error"
+
+#: Bustle/StatisticsPane.hs:171
+msgid "Frequency"
+msgstr "Frequency"
+
+#: Bustle/UI.hs:293
+msgid "If you don't save, this log will be lost forever."
+msgstr "If you don’t save, this log will be lost forever."
+
+#: Bustle/StatisticsPane.hs:229
+msgid "KB"
+msgstr "KB"
+
+#: Bustle/StatisticsPane.hs:264
+msgid "Largest"
+msgstr "Largest"
+
+#: Bustle/StatisticsPane.hs:230
+msgid "MB"
+msgstr "MB"
+
+#: Bustle/StatisticsPane.hs:212 Bustle/StatisticsPane.hs:263
+msgid "Mean"
+msgstr "Mean"
+
+#: Bustle/StatisticsPane.hs:249
+msgid "Member"
+msgstr "Member"
+
+#: Bustle/UI/DetailsView.hs:85
+msgid "Member:"
+msgstr "Member:"
+
+#: Bustle/StatisticsPane.hs:200
+msgid "Method"
+msgstr "Method"
+
+#: Bustle/UI/DetailsView.hs:104
+msgid "Method call"
+msgstr "Method call"
+
+#: Bustle/UI/DetailsView.hs:105
+msgid "Method return"
+msgstr "Method return"
+
+#: Bustle/StatisticsPane.hs:156
+msgid "Name"
+msgstr "Name"
+
+#: Bustle/UI/DetailsView.hs:127
+msgid ""
+"No message body information is available. Please capture a fresh log using a "
+"recent version of Bustle!"
+msgstr ""
+"No message body information is available. Please capture a fresh log using a "
+"recent version of Bustle!"
+
+#: Bustle/Loader.hs:64
+msgid "Parse error %s"
+msgstr "Parse error %s"
+
+#: Bustle/UI/DetailsView.hs:84
+msgid "Path:"
+msgstr "Path:"
+
+#: Bustle/UI.hs:286
+msgid "Save log '%s' before closing?"
+msgstr "Save log ‘%s’ before closing?"
+
+#: Bustle/UI/DetailsView.hs:109
+msgid "Signal"
+msgstr "Signal"
+
+#: Bustle/StatisticsPane.hs:262
+msgid "Smallest"
+msgstr "Smallest"
+
+#: Bustle/UI/AboutDialog.hs:49
+msgid "Someone's favourite D-Bus profiler"
+msgstr "Someone's favourite D-Bus profiler"
+
+#: Bustle/StatisticsPane.hs:209
+msgid "Total"
+msgstr "Total"
+
+#: Bustle/UI/FilterDialog.hs:105
+msgid ""
+"Unticking a service hides its column in the diagram, and all messages it is "
+"involved in. That is, all methods it calls or are called on it, the "
+"corresponding returns, and all signals it emits will be hidden."
+msgstr ""
+"Unticking a service hides its column in the diagram, and all messages it is "
+"involved in. That is, all methods it calls or are called on it, the "
+"corresponding returns, and all signals it emits will be hidden."
+
+#: Bustle/Util.hs:53
+msgid "Warning: "
+msgstr "Warning: "
diff --git a/po/messages.pot b/po/messages.pot
new file mode 100644
--- /dev/null
+++ b/po/messages.pot
@@ -0,0 +1,157 @@
+# Translation file
+
+msgid ""
+msgstr ""
+
+"Project-Id-Version: PACKAGE VERSION\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2009-01-13 06:05-0800\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: LANGUAGE <LL@li.org>\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+
+#: Bustle/StatisticsPane.hs:210
+#: Bustle/StatisticsPane.hs:213
+msgid "%.1f ms"
+msgstr ""
+
+#: Bustle/UI.hs:467
+msgid "%s - Bustle"
+msgstr ""
+
+#: Bustle/Noninteractive.hs:55
+msgid "(no interface)"
+msgstr ""
+
+#: Bustle/UI/DetailsView.hs:148
+msgid "<unknown>"
+msgstr ""
+
+#: Bustle/UI/DetailsView.hs:87
+msgid "Arguments:"
+msgstr ""
+
+#: Bustle/StatisticsPane.hs:228
+msgid "B"
+msgstr ""
+
+#: Bustle/UI/AboutDialog.hs:47
+msgid "Bustle"
+msgstr ""
+
+#: Bustle/StatisticsPane.hs:211
+msgid "Calls"
+msgstr ""
+
+#: Bustle/UI.hs:294
+msgid "Close _Without Saving"
+msgstr ""
+
+#: Bustle/UI.hs:210
+msgid "Could not read '%s'"
+msgstr ""
+
+#: Bustle/Noninteractive.hs:48
+msgid "Couldn't parse '%s': %s"
+msgstr ""
+
+#: Bustle/UI/DetailsView.hs:110
+msgid "Directed signal"
+msgstr ""
+
+#: Bustle/UI/DetailsView.hs:106
+msgid "Error"
+msgstr ""
+
+#: Bustle/StatisticsPane.hs:171
+msgid "Frequency"
+msgstr ""
+
+#: Bustle/UI.hs:293
+msgid "If you don't save, this log will be lost forever."
+msgstr ""
+
+#: Bustle/StatisticsPane.hs:229
+msgid "KB"
+msgstr ""
+
+#: Bustle/StatisticsPane.hs:264
+msgid "Largest"
+msgstr ""
+
+#: Bustle/StatisticsPane.hs:230
+msgid "MB"
+msgstr ""
+
+#: Bustle/StatisticsPane.hs:212
+#: Bustle/StatisticsPane.hs:263
+msgid "Mean"
+msgstr ""
+
+#: Bustle/StatisticsPane.hs:249
+msgid "Member"
+msgstr ""
+
+#: Bustle/UI/DetailsView.hs:85
+msgid "Member:"
+msgstr ""
+
+#: Bustle/StatisticsPane.hs:200
+msgid "Method"
+msgstr ""
+
+#: Bustle/UI/DetailsView.hs:104
+msgid "Method call"
+msgstr ""
+
+#: Bustle/UI/DetailsView.hs:105
+msgid "Method return"
+msgstr ""
+
+#: Bustle/StatisticsPane.hs:156
+msgid "Name"
+msgstr ""
+
+#: Bustle/UI/DetailsView.hs:127
+msgid "No message body information is available. Please capture a fresh log using a recent version of Bustle!"
+msgstr ""
+
+#: Bustle/Loader.hs:64
+msgid "Parse error %s"
+msgstr ""
+
+#: Bustle/UI/DetailsView.hs:84
+msgid "Path:"
+msgstr ""
+
+#: Bustle/UI.hs:286
+msgid "Save log '%s' before closing?"
+msgstr ""
+
+#: Bustle/UI/DetailsView.hs:109
+msgid "Signal"
+msgstr ""
+
+#: Bustle/StatisticsPane.hs:262
+msgid "Smallest"
+msgstr ""
+
+#: Bustle/UI/AboutDialog.hs:49
+msgid "Someone's favourite D-Bus profiler"
+msgstr ""
+
+#: Bustle/StatisticsPane.hs:209
+msgid "Total"
+msgstr ""
+
+#: Bustle/UI/FilterDialog.hs:105
+msgid "Unticking a service hides its column in the diagram, and all messages it is involved in. That is, all methods it calls or are called on it, the corresponding returns, and all signals it emits will be hidden."
+msgstr ""
+
+#: Bustle/Util.hs:53
+msgid "Warning: "
+msgstr ""
+
