Hopefully last commit before 0.2.

Khaibit [07-07-14 - 04:00]
Hopefully last commit before 0.2.
Cleaned up localization.
Switched to using scene manager for mail/guild store screens.
Various tweaks/cleanup.
(One more commit if patch Jul. 7 breaks anything before final push)
Filename
Shopkeeper.lua
Shopkeeper.txt
Shopkeeper.xml
Shopkeeper_Namespace_Init.lua
bindings.xml
i18n/DE.lua
i18n/EN.lua
i18n/FR.lua
license
readme
diff --git a/Shopkeeper.lua b/Shopkeeper.lua
index 4cb25d0..b254415 100644
--- a/Shopkeeper.lua
+++ b/Shopkeeper.lua
@@ -1,8 +1,8 @@
 -- Shopkeeper Main Addon File
--- Last Updated July 3, 2014
+-- Last Updated July 7, 2014
 -- Written July 2014 by Dan Stone (@khaibit) - dankitymao@gmail.com
--- Released under terms in license.txt accompanying this file.
--- Distribution without license.txt is prohibited!
+-- Released under terms in license accompanying this file.
+-- Distribution without license is prohibited!

 -- Workaround because GetDisplayName() is broken
 function Shopkeeper.GetAccountName()
@@ -29,16 +29,22 @@ function Shopkeeper.translate(stringName)
 end

 -- Create a textual representation of a time interval
-function Shopkeeper.textTimeSince(theTime)
+-- (X and Y) or Z in LUA is the equivalent of C-style
+-- ternary syntax X ? Y : Z so long as Y is not false or nil
+function Shopkeeper.textTimeSince(theTime, useLowercase)
   local secsSince = GetTimeStamp() - theTime
   if secsSince < 60 then
-    return zo_strformat(Shopkeeper.translate('timeSecondsAgo'), secsSince)
+    return ((useLowercase and zo_strformat(Shopkeeper.translate('timeSecondsAgoLC'), secsSince)) or
+             zo_strformat(Shopkeeper.translate('timeSecondsAgo'), secsSince))
   elseif secsSince < 3600 then
-    return zo_strformat(Shopkeeper.translate('timeMinutesAgo'), math.floor(secsSince / 60.0))
+    return ((useLowercase and zo_strformat(Shopkeeper.translate('timeMinutesAgoLC'), math.floor(secsSince / 60.0))) or
+             zo_strformat(Shopkeeper.translate('timeMinutesAgo'), math.floor(secsSince / 60.0)))
   elseif secsSince < 86400 then
-    return zo_strformat(Shopkeeper.translate('timeHoursAgo'), math.floor(secsSince / 3600.0))
+    return ((useLowercase and zo_strformat(Shopkeeper.translate('timeHoursAgoLC'), math.floor(secsSince / 3600.0))) or
+             zo_strformat(Shopkeeper.translate('timeHoursAgo'), math.floor(secsSince / 3600.0)))
   else
-    return zo_strformat(Shopkeeper.translate('timeDaysAgo'), math.floor(secsSince / 86400.0))
+    return ((useLowercase and zo_strformat(Shopkeeper.translate('timeDaysAgoLC'), math.floor(secsSince / 86400.0))) or
+             zo_strformat(Shopkeeper.translate('timeDaysAgo'), math.floor(secsSince / 86400.0)))
   end
 end

@@ -226,8 +232,9 @@ function Shopkeeper.SetDataRow(index, buyer, guild, itemName, icon, quantity, se

   -- Insert thousands separators for the price
   local stringPrice = dispPrice
+  local subString = "%1" .. Shopkeeper.translate("thousandsSep") .."%2"
   while true do
-    stringPrice, k = string.gsub(stringPrice, "^(-?%d+)(%d%d%d)", '%1,%2')
+    stringPrice, k = string.gsub(stringPrice, "^(-?%d+)(%d%d%d)", subString)
     if (k == 0) then break end
   end

@@ -333,7 +340,7 @@ function Shopkeeper.DisplayRows()
     else
       local scanResult = Shopkeeper.SearchTable[rowIndex]

-      Shopkeeper.SetDataRow(i, scanResult[1], scanResult[2], scanResult[3], scanResult[4], scanResult[5], Shopkeeper.textTimeSince(scanResult[6]), scanResult[7], scanResult[8])
+      Shopkeeper.SetDataRow(i, scanResult[1], scanResult[2], scanResult[3], scanResult[4], scanResult[5], Shopkeeper.textTimeSince(scanResult[6], false), scanResult[7], scanResult[8])
     end
   end

@@ -405,14 +412,34 @@ function Shopkeeper:LibAddonInit()
           name = Shopkeeper.translate('openMailName'),
           tooltip = Shopkeeper.translate('openMailTip'),
           getFunc = function() return Shopkeeper.savedVariables.openWithMail end,
-          setFunc = function(value) Shopkeeper.savedVariables.openWithMail = value end,
+          setFunc = function(value)
+            Shopkeeper.savedVariables.openWithMail = value
+            if value then
+              -- Register for the mail scenes
+              MAIL_INBOX_SCENE:AddFragment(Shopkeeper.uiFragment)
+              MAIL_SEND_SCENE:AddFragment(Shopkeeper.uiFragment)
+            else
+              -- Unregister for the mail scenes
+              MAIL_INBOX_SCENE:RemoveFragment(Shopkeeper.uiFragment)
+              MAIL_SEND_SCENE:RemoveFragment(Shopkeeper.uiFragment)
+            end
+          end,
         },
         [3] = {
           type = "checkbox",
           name = Shopkeeper.translate('openStoreName'),
           tooltip = Shopkeeper.translate('openStoreTip'),
           getFunc = function() return Shopkeeper.savedVariables.openWithStore end,
-          setFunc = function(value) Shopkeeper.savedVariables.openWithStore = value end,
+          setFunc = function(value)
+            Shopkeeper.savedVariables.openWithStore = value
+            if value then
+              -- Register for the store scene
+              TRADING_HOUSE_SCENE:AddFragment(Shopkeeper.uiFragment)
+            else
+              -- Unregister for the store scene
+              TRADING_HOUSE_SCENE:RemoveFragment(Shopkeeper.uiFragment)
+            end
+          end,
         },
         [4] = {
           type = "checkbox",
@@ -537,19 +564,6 @@ function Shopkeeper.SwitchViewMode()
   Shopkeeper.DoSearch(ShopkeeperWindowSearchBox:GetText())
 end

--- Handlers for opening mailbox and guild store windows
-function Shopkeeper:OpenMail()
-  if Shopkeeper.savedVariables.openWithMail then
-    ShopkeeperWindow:SetHidden(false)
-  end
-end
-
-function Shopkeeper:OpenStore()
-  if Shopkeeper.savedVariables.openWithStore then
-    ShopkeeperWindow:SetHidden(false)
-  end
-end
-
 -- Actually carries out of the scan of a specific guild store's sales history.
 -- If checkOlder is true, will request older events first if there are any.
 -- Inserts all events that occurred after timeStamp into the ScanResults table.
@@ -640,8 +654,9 @@ function Shopkeeper:PostScan(timeStamp, doAlert)
       if Shopkeeper.savedVariables.showMultiple or numAlerts == 1 then
         -- Insert thousands separators for the price
         local stringPrice = dispPrice
+        local subString = "%1" .. Shopkeeper.translate("thousandsSep") .."%2"
         while true do
-          stringPrice, k = string.gsub(stringPrice, "^(-?%d+)(%d%d%d)", '%1,%2')
+          stringPrice, k = string.gsub(stringPrice, "^(-?%d+)(%d%d%d)", subString)
           if (k == 0) then break end
         end

@@ -654,15 +669,22 @@ function Shopkeeper:PostScan(timeStamp, doAlert)

         -- On-screen alert
         if Shopkeeper.savedVariables.showAnnounceAlerts then
-          CENTER_SCREEN_ANNOUNCE:AddMessage(EVENT_SKILL_RANK_UPDATE, CSA_EVENT_SMALL_TEXT, alertSound,
-            string.format(Shopkeeper.translate('salesAlertColor'), theEvent.itemName,
-                          theEvent.quant, stringPrice, theEvent.guild, Shopkeeper.textTimeSince(theEvent.saleTime)))
+          -- German word order differs so argument order also needs to be changed
+          if Shopkeeper.locale == "de" then
+            CENTER_SCREEN_ANNOUNCE:AddMessage(EVENT_SKILL_RANK_UPDATE, CSA_EVENT_SMALL_TEXT, alertSound,
+              string.format(Shopkeeper.translate('salesAlertColor'), theEvent.quant, theEvent.itemName,
+                            stringPrice, theEvent.guild, Shopkeeper.textTimeSince(theEvent.saleTime, true)))
+          else
+            CENTER_SCREEN_ANNOUNCE:AddMessage(EVENT_SKILL_RANK_UPDATE, CSA_EVENT_SMALL_TEXT, alertSound,
+              string.format(Shopkeeper.translate('salesAlertColor'), theEvent.itemName,
+                            theEvent.quant, stringPrice, theEvent.guild, Shopkeeper.textTimeSince(theEvent.saleTime, true)))
+          end
         end

         -- Chat alert
         if Shopkeeper.savedVariables.showChatAlerts then
           CHAT_SYSTEM:AddMessage(string.format("[Shopkeeper] " .. Shopkeeper.translate('salesAlert'),
-                                 theEvent.itemName, theEvent.quant, stringPrice, theEvent.guild, Shopkeeper.textTimeSince(theEvent.saleTime)))
+                                 theEvent.itemName, theEvent.quant, stringPrice, theEvent.guild, Shopkeeper.textTimeSince(theEvent.saleTime, true)))
         end
       end
     end
@@ -671,8 +693,9 @@ function Shopkeeper:PostScan(timeStamp, doAlert)
     if not Shopkeeper.savedVariables.showMultiple and numAlerts > 1 then
       -- Insert thousands separators for the price
       local stringPrice = totalGold
+      local subString = "%1" .. Shopkeeper.translate("thousandsSep") .."%2"
       while true do
-        stringPrice, k = string.gsub(stringPrice, "^(-?%d+)(%d%d%d)", '%1,%2')
+        stringPrice, k = string.gsub(stringPrice, "^(-?%d+)(%d%d%d)", substring)
         if (k == 0) then break end
       end

@@ -711,7 +734,7 @@ function Shopkeeper:ScanStores(checkOlder, doAlert)
       -- I need a better way to space out these checks than callbacks
       -- It works but feels ugly
       if RequestGuildHistoryCategoryNewest(guildID, GUILD_HISTORY_SALES) then
-        zo_callLater(function() Shopkeeper:DoScan(guildID, timeStamp, checkOlder) end, (((j - 1) * 2000) + 500))
+        zo_callLater(function() Shopkeeper:DoScan(guildID, timeStamp, checkOlder) end, (((j - 1) * 2000) + 1000))
       end
     end
     -- Once scans are done, wait a few seconds and do some cleanup
@@ -781,6 +804,9 @@ function Shopkeeper:SetupShopkeeperWindow()
   ShopkeeperWindow:GetNamedChild('Price'):SetText(Shopkeeper.translate('priceColumnName'))
   ShopkeeperWindow:GetNamedChild('SearchLabel'):SetText(Shopkeeper.translate('searchBoxName'))

+  -- Set second half of window title from translation
+  ShopkeeperWindow:GetNamedChild('Title'):SetText("Shopkeeper - " .. Shopkeeper.translate('yourSalesTitle'))
+
   -- Set up some helpful tooltips for the Buyer and Item column headers
   ShopkeeperWindowBuyer:SetHandler("OnMouseEnter", function(self)
     ZO_Tooltips_ShowTextTooltip(self, TOP, Shopkeeper.translate('buyerTooltip'))
@@ -816,14 +842,14 @@ function Shopkeeper:SetupShopkeeperWindow()
   -- Refresh button
   local refreshButton = CreateControlFromVirtual("ShopkeeperRefreshButton", ShopkeeperWindow, "ZO_DefaultButton")
   refreshButton:SetAnchor(BOTTOMRIGHT, ShopkeeperWindow, BOTTOMRIGHT, -20, -5)
-  refreshButton:SetWidth(100)
+  refreshButton:SetWidth(150)
   refreshButton:SetText(Shopkeeper.translate('refreshLabel'))
   refreshButton:SetHandler("OnClicked", Shopkeeper.DoRefresh)

   -- Reset button
   local resetButton = CreateControlFromVirtual("ShopkeeperResetButton", ShopkeeperWindow, "ZO_DefaultButton")
-  resetButton:SetAnchor(BOTTOMRIGHT, ShopkeeperWindow, BOTTOMRIGHT, -120, -5)
-  resetButton:SetWidth(100)
+  resetButton:SetAnchor(BOTTOMRIGHT, ShopkeeperWindow, BOTTOMRIGHT, -170, -5)
+  resetButton:SetWidth(150)
   resetButton:SetText(Shopkeeper.translate('resetLabel'))
   resetButton:SetHandler("OnClicked", Shopkeeper.DoReset)

@@ -902,6 +928,13 @@ function Shopkeeper:Initialize()
   self:RestoreWindowPosition()
   self.SortByTime("desc")

+  -- We'll grab their locale now, it's really only used for a couple things as
+  -- most localization is handled by the i18n/$(language).lua files
+  Shopkeeper.locale = GetCVar('Language.2')
+  if Shopkeeper.locale ~= "en" and Shopkeeper.locale ~= "de" and Shopkeeper.locale ~= "fr" then
+    Shopkeeper.locale = "en"
+  end
+
   -- Rather than constantly managing the length of the history, we'll just
   -- truncate it once at init-time since we now have it sorted.  As a result
   -- it will fluctuate in size depending on how active guild stores are and
@@ -917,16 +950,34 @@ function Shopkeeper:Initialize()
   -- Populate the search table
   self.DoSearch(nil)

-  -- Hook into the mail and guild store open/close events to open/close Shopkeeper also
-  EVENT_MANAGER:RegisterForEvent(Shopkeeper.name, EVENT_MAIL_OPEN_MAILBOX, Shopkeeper.OpenMail)
-  EVENT_MANAGER:RegisterForEvent(Shopkeeper.name, EVENT_MAIL_CLOSE_MAILBOX, function() ShopkeeperWindow:SetHidden(true) end)
-  EVENT_MANAGER:RegisterForEvent(Shopkeeper.name, EVENT_OPEN_TRADING_HOUSE, Shopkeeper.OpenStore)
-  EVENT_MANAGER:RegisterForEvent(Shopkeeper.name, EVENT_CLOSE_TRADING_HOUSE, function() ShopkeeperWindow:SetHidden(true) end)
+  -- Add the shopkeeper window to the mail and trading house scenes if the
+  -- player's settings indicate they want that behavior
+  Shopkeeper.uiFragment = ZO_FadeSceneFragment:New(ShopkeeperWindow)
+  if self.savedVariables.openWithMail then
+    MAIL_INBOX_SCENE:AddFragment(Shopkeeper.uiFragment)
+    MAIL_SEND_SCENE:AddFragment(Shopkeeper.uiFragment)
+  end
+
+  if self.savedVariables.openWithStore then
+    TRADING_HOUSE_SCENE:AddFragment(Shopkeeper.uiFragment)
+  end

+  -- Because we allow manual toggling of the Shopkeeper window in those scenes (without
+  -- making that setting permanent), we also have to hide the window on closing them
+  -- if they're not part of the scene.
+  EVENT_MANAGER:RegisterForEvent(Shopkeeper.name, EVENT_MAIL_CLOSE_MAILBOX, function()
+    if not Shopkeeper.savedVariables.openWithMail then ShopkeeperWindow:SetHidden(true) end
+  end)
+  EVENT_MANAGER:RegisterForEvent(Shopkeeper.name, EVENT_CLOSE_TRADING_HOUSE, function()
+    if not Shopkeeper.savedVariables.openWithStore then ShopkeeperWindow:SetHidden(true) end
+  end)
+
+  -- We also want to make sure the Shopkeeper window is hidden in the game menu
+  ZO_PreHookHandler(ZO_GameMenu_InGame, "OnShow", function() ShopkeeperWindow:SetHidden(true) end)
+
   -- Update fonts after each UI load
   EVENT_MANAGER:RegisterForEvent(Shopkeeper.name, EVENT_PLAYER_ACTIVATED, Shopkeeper.PlayerActive)

-
   -- RegisterForUpdate lets us scan at a given interval (in ms), so we'll use that to
   -- keep the sales history updated
   local scanInterval = self.savedVariables.scanFreq * 1000
diff --git a/Shopkeeper.txt b/Shopkeeper.txt
index c99782e..753c283 100644
--- a/Shopkeeper.txt
+++ b/Shopkeeper.txt
@@ -3,8 +3,8 @@
 ## Description: Notifies you when you've sold something in a guild store and presents guild sales info in a convenient table.
 ## Author: Dan Stone (@khaibit) - dankitymao@gmail.com
 ## Version: 0.2
-## License: See license.txt - distribution without license.txt is prohibited!
-## LastUpdated: July 3, 2014
+## License: See license - distribution without license is prohibited!
+## LastUpdated: July 7, 2014
 ## SavedVariables: ShopkeeperSavedVars
 ## OptionalDependsOn: LibAddonMenu-2.0, LibMediaProvider-1.0, LibStub

diff --git a/Shopkeeper.xml b/Shopkeeper.xml
index bdf6b63..de36a25 100644
--- a/Shopkeeper.xml
+++ b/Shopkeeper.xml
@@ -1,9 +1,9 @@
 <!--
       Shopkeeper UI Layout File
-      Last Updated July 3, 2014
+      Last Updated July 7, 2014
       Written July 2014 by Dan Stone (@khaibit) - dankitymao@gmail.com
-      Released under terms in license.txt accompanying this file.
-      Distribution without license.txt is prohibited!
+      Released under terms in license accompanying this file.
+      Distribution without license is prohibited!
 -->
 <GuiXml>
   <Controls>
@@ -14,11 +14,11 @@
       </OnMoveStop>
       <Controls>
         <Backdrop name="$(parent)BG" inherits="ZO_ThinBackdrop" />
-        <Label name="$(parent)SearchLabel" font="ZoFontGame" height="25" inheritAlpha="true" color="FFFFFF" verticalAlignment="TOP" horizontalAlignment="CENTER" text="Search: ">
-          <Anchor point="TOPLEFT" relativeTo="$(parent)" relativePoint="TOPLEFT" offsetX="10" offsetY="10" />
+        <Label name="$(parent)SearchLabel" font="ZoFontGame" height="25" inheritAlpha="true" color="FFFFFF" verticalAlignment="CENTER" horizontalAlignment="RIGHT" text="Search: ">
+          <Anchor point="TOPLEFT" relativeTo="$(parent)" relativePoint="TOPLEFT" offsetX="15" offsetY="10" />
         </Label>
         <EditBox name="$(parent)SearchBox" mouseEnabled="true" editEnabled="true" font="ZoFontGame" textType="TEXT_TYPE_ALL" multiLine="false" newLineEnabled="false">
-          <Anchor point="TOPLEFT" relativeTo="$(parent)" relativePoint="TOPLEFT" offsetX="75" offsetY="9" />
+          <Anchor point="TOPLEFT" relativeTo="$(parent)" relativePoint="TOPLEFT" offsetX="65" offsetY="9" />
           <Dimensions x="200" y="25" />
           <Controls>
             <Backdrop name="$(parent)TextBG" centerColor="000000" edgeColor="AAAAAA">
@@ -52,14 +52,11 @@
           <Anchor point="TOPLEFT" relativeTo="$(parent)" relativePoint="TOPLEFT" offsetX="143" offsetY="44" />
         </Label>
         <Button name="$(parent)ItemName" font="ZoFontGame" inheritAlpha="true" color="FFFFFF" verticalAlignment="TOP" horizontalAlignment="LEFT" text="Item">
-          <Dimensions x="100" y="25" />
-          <Anchor point="TOPLEFT" relativeTo="$(parent)" relativePoint="TOPLEFT" offsetX="353" offsetY="44" />
+          <Dimensions x="200" y="25" />
+          <Anchor point="TOPLEFT" relativeTo="$(parent)" relativePoint="TOPLEFT" offsetX="352" offsetY="44" />
         </Button>
-        <Label name="$(parent)Quantity" font="ZoFontGame" width="50" height="25" inheritAlpha="true" color="FFFFFF" verticalAlignment="TOP" horizontalAlignment="LEFT" text="">
-          <Anchor point="TOPLEFT" relativeTo="$(parent)" relativePoint="TOPLEFT" offsetX="525" offsetY="44" />
-        </Label>
         <Button name="$(parent)SellTime" font="ZoFontGame" inheritAlpha="true" color="FFFFFF" verticalAlignment="TOP" horizontalAlignment="LEFT" text="Sale Time">
-          <Anchor point="TOPLEFT" relativeTo="$(parent)" relativePoint="TOPLEFT" offsetX="650" offsetY="44" />
+          <Anchor point="TOPLEFT" relativeTo="$(parent)" relativePoint="TOPLEFT" offsetX="649" offsetY="44" />
           <Dimensions x="95" y="25" />
         </Button>
         <Texture name="$(parent)SortTime" textureFile="/esoui/art/miscellaneous/list_sortheader_icon_sortdown.dds" alpha="1">
diff --git a/Shopkeeper_Namespace_Init.lua b/Shopkeeper_Namespace_Init.lua
index 7af43f7..6feed1d 100644
--- a/Shopkeeper_Namespace_Init.lua
+++ b/Shopkeeper_Namespace_Init.lua
@@ -1,12 +1,13 @@
 -- Shopkeeper Namespace Setup
--- Last Updated July 3, 2014
+-- Last Updated July 7, 2014
 -- Written July 2014 by Dan Stone (@khaibit) - dankitymao@gmail.com
--- Released under terms in license.txt accompanying this file.
--- Distribution without license.txt is prohibited!
+-- Released under terms in license accompanying this file.
+-- Distribution without license is prohibited!

 Shopkeeper = {}
 Shopkeeper.name = "Shopkeeper"
 Shopkeeper.version = "0.1"
+Shopkeeper.locale = "en"
 Shopkeeper.viewMode = "self"
 Shopkeeper.isScanning = false
 Shopkeeper.i18n = {}
@@ -20,12 +21,13 @@ Shopkeeper.alertQueue = {}
 Shopkeeper.shopSlider = {}
 Shopkeeper.itemToolTip = {}
 Shopkeeper.curSort = {"time", "desc"}
+Shopkeeper.uiFragment = {}

 -- Sound table for mapping readable names to sound names
 Shopkeeper.alertSounds = {
   [1] = {name = "None", sound = "No_Sound"},
-  [2] = {name = "Ability Not Ready", sound = "Ability_NotReady"},
-  [3] = {name = "Add Guild Member", sound = "GuildRoster_Added"},
+  [2] = {name = "Add Guild Member", sound = "GuildRoster_Added"},
+  [3] = {name = "Armor Glyph", sound = "Enchanting_ArmorGlyph_Placed"},
   [4] = {name = "Book Acquired", sound = "Book_Acquired"},
   [5] = {name = "Book Collection Completed", sound = "Book_Collection_Completed"},
   [6] = {name = "Boss Killed", sound = "SkillXP_BossKilled"},
@@ -33,27 +35,27 @@ Shopkeeper.alertSounds = {
   [8] = {name = "Completed Event", sound = "ScriptedEvent_Completion"},
   [9] = {name = "Dark Fissure Closed", sound = "SkillXP_DarkFissureClosed"},
   [10] = {name = "Emperor Coronated", sound = "Emperor_Coronated_Ebonheart"},
-  [11] = {name = "Enchanting Rune Removed", sound = "Enchanting_PotencyRune_Removed"},
-  [12] = {name = "Gate Closed", sound = "AvA_Gate_Closed"},
-  [13] = {name = "Lockpicking Stress", sound = "Lockpicking_chamber_stress"},
-  [14] = {name = "Mail Attachment", sound = "Mail_ItemSelected"},
-  [15] = {name = "Mail Sent", sound = "Mail_Sent"},
-  [16] = {name = "Mark As Not Junk", sound = "InventoryItem_NotJunk"},
-  [17] = {name = "Money", sound = "Money_Transact"},
-  [18] = {name = "Morph Ability", sound = "Ability_MorphPurchased"},
-  [19] = {name = "Not Enough Gold", sound = "PlayerAction_NotEnoughMoney"},
+  [11] = {name = "Gate Closed", sound = "AvA_Gate_Closed"},
+  [12] = {name = "Lockpicking Stress", sound = "Lockpicking_chamber_stress"},
+  [13] = {name = "Mail Attachment", sound = "Mail_ItemSelected"},
+  [14] = {name = "Mail Sent", sound = "Mail_Sent"},
+  [15] = {name = "Money", sound = "Money_Transact"},
+  [16] = {name = "Morph Ability", sound = "Ability_MorphPurchased"},
+  [17] = {name = "Not Enough Gold", sound = "PlayerAction_NotEnoughMoney"},
+  [18] = {name = "Not Junk", sound = "InventoryItem_NotJunk"},
+  [19] = {name = "Not Ready", sound = "Ability_NotReady"},
   [20] = {name = "Objective Complete", sound = "Objective_Complete"},
   [21] = {name = "Open System Menu", sound = "System_Open"},
-  [22] = {name = "Placed Armor Glyph", sound = "Enchanting_ArmorGlyph_Placed"},
-  [23] = {name = "Quest Abandoned", sound = "Quest_Abandon"},
-  [24] = {name = "Quest Complete", sound = "Quest_Complete"},
-  [25] = {name = "Quickslot Empty", sound = "Quickslot_Use_Empty"},
-  [26] = {name = "Quickslot Open", sound = "Quickslot_Open"},
-  [27] = {name = "Raid Life Shown", sound = "Raid_Life_Display_Shown"},
-  [28] = {name = "Remove Guild Member", sound = "GuildRoster_Removed"},
-  [29] = {name = "Repair Item", sound = "InventoryItem_Repair"},
-  [30] = {name = "Skill Line Added", sound = "SkillLine_Added"},
-  [31] = {name = "Skill Line Leveled", sound = "SkillLine_Leveled"},
+  [22] = {name = "Quest Abandoned", sound = "Quest_Abandon"},
+  [23] = {name = "Quest Complete", sound = "Quest_Complete"},
+  [24] = {name = "Quickslot Empty", sound = "Quickslot_Use_Empty"},
+  [25] = {name = "Quickslot Open", sound = "Quickslot_Open"},
+  [26] = {name = "Raid Life", sound = "Raid_Life_Display_Shown"},
+  [27] = {name = "Remove Guild Member", sound = "GuildRoster_Removed"},
+  [28] = {name = "Repair Item", sound = "InventoryItem_Repair"},
+  [29] = {name = "Rune Removed", sound = "Enchanting_PotencyRune_Removed"},
+  [30] = {name = "Skill Added", sound = "SkillLine_Added"},
+  [31] = {name = "Skill Leveled", sound = "SkillLine_Leveled"},
   [32] = {name = "Stat Purchase", sound = "Stats_Purchase"},
   [33] = {name = "Synergy Ready", sound = "Ability_Synergy_Ready_Sound"},
 }
\ No newline at end of file
diff --git a/bindings.xml b/bindings.xml
index aee863a..9368e96 100644
--- a/bindings.xml
+++ b/bindings.xml
@@ -1,6 +1,6 @@
 <!--
       Shopkeeper Key Bindings File
-      Last Updated July 3, 2014
+      Last Updated July 7, 2014
       Written July 2014 by Dan Stone (@khaibit) - dankitymao@gmail.com
       Released under terms in license accompanying this file.
       Distribution without license is prohibited!
diff --git a/i18n/DE.lua b/i18n/DE.lua
index c20f04a..247ca01 100644
--- a/i18n/DE.lua
+++ b/i18n/DE.lua
@@ -1,17 +1,16 @@
 -- Shopkeeper German Localization File
--- Last Updated July 3, 2014
+-- Last Updated July 7, 2014
 -- Written July 2014 by Dan Stone (@khaibit) - dankitymao@gmail.com
 -- Released under terms in license.txt accompanying this file.
--- Distribution without license.txt is prohibited!
--- Translation provided by (anonymous)
+-- Distribution without license is prohibited!

 Shopkeeper.i18n.localized = {
-  saleAlertAnnounceName = "Benachrichtigungen",
-  saleAlertAnnounceTip = "Zeige Nachrichten auf dem Bildschirm.",
+  saleAlertAnnounceName = "Bildschirmanzeige von Benachrichtigungen",
+  saleAlertAnnounceTip = "Zeige Benachrichtigungen auf dem Bildschirm.",
   multAlertName = "Zeige mehrere Benachrichtigungen",
-  multAlertTip = "Zeige eine Benachrichtigung pro verkauftem Gegenstand, anstatt einer zusammenfassenden Banachrichtigung bei mehreren Gegenstnden.",
-  openMailName = "ffne mit der Mailbox",
-  openMailTip = "ffne die Verkausbersicht zusammen mit der Mailbox.",
+  multAlertTip = "Zeige eine Benachrichtigung pro verkauftem Gegenstand, anstatt einer zusammenfassenden Benachrichtigung bei mehreren Gegenstnden.",
+  openMailName = "ffne mit den Nachrichten",
+  openMailTip = "ffne die Verkausbersicht zusammen mit den Nachrichten.",
   openStoreName = "ffne mit den Laden",
   openStoreTip = "ffne Verkaufsbersicht zusammen mit dem Gildenladen.",
   fullSaleName = "Zeige vollen Verkaufspreis",
@@ -29,9 +28,9 @@ Shopkeeper.i18n.localized = {
   buyerColumnName = "Kufer",
   guildColumnName = "Gilde",
   itemColumnName = "Verkaufte Gegenstnde",
-  timeColumnName = "Verkaufszeitpunkt",
+  timeColumnName = "Zeitpunkt",
   priceColumnName = "Preis",
-  searchBoxName = "Suche: ",
+  searchBoxName = "Suche:",
   itemTooltip = "Klicke doppelt auf einen Gegenstand zur Verlinkung im Chat.",
   buyerTooltip = "Klicke doppelt auf einen Verkufer um ihn zu kontaktieren.",
   sortTimeTip = "Klicken um nach Verkaufszeit zu sortieren.",
@@ -40,14 +39,18 @@ Shopkeeper.i18n.localized = {
   timeMinutesAgo = "<<1[Vor 1 Minute/Vor %d Minuten]>>",
   timeHoursAgo = "<<1[Vor 1 Stunde/Vor %d Stunden]>>",
   timeDaysAgo = "<<1[Gestern/Vor %d Tagen]>>",
+  timeSecondsAgoLC = "<<1[soeben/vor %d Sekunden]>>",
+  timeMinutesAgoLC = "<<1[vor 1 Minute/vor %d Minuten]>>",
+  timeHoursAgoLC = "<<1[vor 1 Stunde/vor %d Stunden]>>",
+  timeDaysAgoLC = "<<1[gestern/vor %d Tagen]>>",
   refreshLabel = "Aktualisierung",
   refreshStart = "Starte Aktualisierung.",
   refreshDone = "Aktualisierung abgeschlossen.",
   refreshWait = "Bitte warte eine Minute oder so zwischen den Aktualisierungen.",
   resetLabel = "Reset",
   resetDone = "Reset der gespeicherten Daten.",
-  salesAlert = "Du hast %sx%d fr %sG ber die Gilde %s %s verkauft.",
-  salesAlertColor = "|cFFFFFFDu hast %sx%d fr |cD5B526%sG |cFFFFFFber die Gilde %s %s verkauft.",
+  salesAlert = "Du hast %d mal %s fr %sG ber die Gilde %s %s verkauft.",
+  salesAlertColor = "|cFFFFFFDu hast %d mal %s fr |cD5B526%sG |cFFFFFFber die Gilde %s %s verkauft.",
   salesGroupAlert = "Du hast %d Gegenstnde verkauft fr einen Gesammtwert von %sG.",
   salesGroupAlertColor = "|cFFFFFFDu hast %d Gegenstnde verkauft fr einen Gesammtwert von |cD5B526%sG|cFFFFFF.",
   alertOptionsName = "Sales Alert Options",
@@ -56,6 +59,7 @@ Shopkeeper.i18n.localized = {
   saleAlertChatTip = "Show sales alerts in your chat box.",
   alertTypeName = "Alert Sound",
   alertTypeTip = "The sound to play when you sell an item, if any.",
+  thousandsSep = ".",
 }

 ZO_CreateStringId("SI_BINDING_NAME_SHOPKEEPER_TOGGLE", "Show/Hide Shopkeeper Window")
\ No newline at end of file
diff --git a/i18n/EN.lua b/i18n/EN.lua
index e1fe336..4705ace 100644
--- a/i18n/EN.lua
+++ b/i18n/EN.lua
@@ -1,8 +1,8 @@
 -- Shopkeeper English Localization File
--- Last Updated July 3, 2014
+-- Last Updated July 7, 2014
 -- Written July 2014 by Dan Stone (@khaibit) - dankitymao@gmail.com
--- Released under terms in license.txt accompanying this file.
--- Distribution without license.txt is prohibited!
+-- Released under terms in license accompanying this file.
+-- Distribution without license is prohibited!

 Shopkeeper.i18n.localized = {
   saleAlertAnnounceName = "On-Screen Alerts",
@@ -39,6 +39,10 @@ Shopkeeper.i18n.localized = {
   timeMinutesAgo = "<<1[%d minute ago/%d minutes ago]>>",
   timeHoursAgo = "<<1[%d hour ago/%d hours ago]>>",
   timeDaysAgo = "<<1[Yesterday/%d days ago]>>",
+  timeSecondsAgoLC = "<<1[just now/%d seconds ago]>>",
+  timeMinutesAgoLC = "<<1[%d minute ago/%d minutes ago]>>",
+  timeHoursAgoLC = "<<1[%d hour ago/%d hours ago]>>",
+  timeDaysAgoLC = "<<1[yesterday/%d days ago]>>",
   refreshLabel = "Refresh",
   refreshStart = "Starting refresh.",
   refreshDone = "Refresh complete.",
@@ -55,6 +59,7 @@ Shopkeeper.i18n.localized = {
   alertTypeTip = "The sound to play when you sell an item, if any.",
   saleAlertChatName = "Chat Alerts",
   saleAlertChatTip = "Show sales alerts in your chat box.",
+  thousandsSep = ",",
 }

 ZO_CreateStringId("SI_BINDING_NAME_SHOPKEEPER_TOGGLE", "Show/Hide Shopkeeper Window")
\ No newline at end of file
diff --git a/i18n/FR.lua b/i18n/FR.lua
index 091c3db..4705ace 100644
--- a/i18n/FR.lua
+++ b/i18n/FR.lua
@@ -1,8 +1,8 @@
--- Shopkeeper French Localization File
--- Last Updated July 3, 2014
+-- Shopkeeper English Localization File
+-- Last Updated July 7, 2014
 -- Written July 2014 by Dan Stone (@khaibit) - dankitymao@gmail.com
--- Released under terms in license.txt accompanying this file.
--- Distribution without license.txt is prohibited!
+-- Released under terms in license accompanying this file.
+-- Distribution without license is prohibited!

 Shopkeeper.i18n.localized = {
   saleAlertAnnounceName = "On-Screen Alerts",
@@ -18,19 +18,19 @@ Shopkeeper.i18n.localized = {
   scanFreqName = "Scan Frequency",
   scanFreqTip = "How long to wait (in seconds) between checks of guild store sales.",
   historyDepthName = "Sales History Size",
-  historyDepthTip = "How many sales events to store.  Lowering this may reduce the performance impact of this addon.",
+  historyDepthTip = "How many sales events to store.  Lowering this may reduce the performance impact of this addon.",
   windowFontName = "Window Font",
   windowFontTip = "The font to use for the Shopkeeper window.",
-  viewModeAllName = "Show All Sales",
-  viewModeYourName = "Show Your Sales",
+  viewModeAllName = "Show All Sales",
+  viewModeYourName = "Show Your Sales",
   allSalesTitle = "All Sales",
   yourSalesTitle = "Your Sales",
-  buyerColumnName = "Buyer",
-  guildColumnName = "Guild",
-  itemColumnName = "Item Sold",
-  timeColumnName = "Sale Time",
-  priceColumnName = "Price",
-  searchBoxName = "Search: ",
+  buyerColumnName = "Buyer",
+  guildColumnName = "Guild",
+  itemColumnName = "Item Sold",
+  timeColumnName = "Sale Time",
+  priceColumnName = "Price",
+  searchBoxName = "Search: ",
   itemTooltip = "Double-click on an item to link it in chat.",
   buyerTooltip = "Double-click on a buyer to contact them.",
   sortTimeTip = "Click to sort by sale time.",
@@ -39,6 +39,10 @@ Shopkeeper.i18n.localized = {
   timeMinutesAgo = "<<1[%d minute ago/%d minutes ago]>>",
   timeHoursAgo = "<<1[%d hour ago/%d hours ago]>>",
   timeDaysAgo = "<<1[Yesterday/%d days ago]>>",
+  timeSecondsAgoLC = "<<1[just now/%d seconds ago]>>",
+  timeMinutesAgoLC = "<<1[%d minute ago/%d minutes ago]>>",
+  timeHoursAgoLC = "<<1[%d hour ago/%d hours ago]>>",
+  timeDaysAgoLC = "<<1[yesterday/%d days ago]>>",
   refreshLabel = "Refresh",
   refreshStart = "Starting refresh.",
   refreshDone = "Refresh complete.",
@@ -55,6 +59,7 @@ Shopkeeper.i18n.localized = {
   alertTypeTip = "The sound to play when you sell an item, if any.",
   saleAlertChatName = "Chat Alerts",
   saleAlertChatTip = "Show sales alerts in your chat box.",
+  thousandsSep = ",",
 }

 ZO_CreateStringId("SI_BINDING_NAME_SHOPKEEPER_TOGGLE", "Show/Hide Shopkeeper Window")
\ No newline at end of file
diff --git a/license b/license
index ea113c5..b784447 100644
--- a/license
+++ b/license
@@ -8,9 +8,9 @@ modification, are permitted provided that the following conditions are met:
     * Redistributions in binary form must reproduce the above copyright
       notice, this list of conditions and the following disclaimer in the
       documentation and/or other materials provided with the distribution.
-    * Neither the name of the addon author nor the
-      names of its contributors may be used to endorse or promote products
-      derived from this software without specific prior written permission.
+    * Neither the name of the addon author nor the names of its contributors
+      may be used to endorse or promote products derived from this software
+      without specific prior written permission.

 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
diff --git a/readme b/readme
index 12bbcce..ebd3ec6 100644
--- a/readme
+++ b/readme
@@ -4,28 +4,29 @@ trademarks or trademarks of ZeniMax Media Inc. in the United States and/or
 other countries. All rights reserved.


-Known Issues / Todo July 03,2014:
+Known Issues / Todo July 07,2014:

  - Due to patch 1.2.3 changing the format for item links, it is not possible to
    directly parse the name of an item from the link anymore.  As such, until I
    or someone else can figure out a way to do so, searching item names will
    not be possible.  See http://www.esoui.com/forums/showthread.php?t=1892 for
    a discussion on the matter or if you'd like to help out!
-
- - This is a FIRST RELEASE.  It will likely have bugs.  It probably won't eat
-   your dog or run off with your wife, but I can't guarantee that either.
-   I'll make you a deal: you report bugs, I'll fix them as best I can.
-   Right, deal!
-
+
  - I need translators!  If you speak French, and would like to help
    with translation, send me a message either on esoui.com here or in-game
    (@khaibit on the NA server).  Or, if you'd like to dive right in, take a
    look at i18n\FR.lua in the addon's directory and start
    translating each of those strings!

-
-
 Changelog for 0.2:
   German localization is complete!
   Fixed missing localizations on Reset/Refresh buttons.
-  Fixed a minor license issue.
\ No newline at end of file
+  Fixed a minor license issue.
+  Sound options added for alerts.
+  On-screen and chat alert options separated.
+  Shopkeeper button on guild store screen moved down slightly.
+  Fixed alert swarm after resetting listings.
+  Main window now has X to close button and a hotkey binding.
+  Main window now closes when you open the game menu.
+  Eliminated cases where slider could get confused as to number of items in the list.
+  LibAddonMenu updated to version 2.0r9 (thanks Seerah!)
\ No newline at end of file