From 746ad28545b3473cc17dd598dfcde2639edffca5 Mon Sep 17 00:00:00 2001
From: Erik Thuning <boooink@gmail.com>
Date: Mon, 26 Feb 2024 13:10:13 +0100
Subject: [PATCH] Implemented internationalization. Let's see how many strings
 have been missed.

---
 config.php.example           |   6 +
 html/en/base.html            |  83 ++++
 html/en/fragments.html       | 843 +++++++++++++++++++++++++++++++++++
 html/{ => sv}/base.html      |   0
 html/{ => sv}/fragments.html |   0
 include/HistoryPage.php      |  16 +-
 include/Page.php             | 117 ++---
 include/ProductPage.php      |  16 +-
 include/Responder.php        |   3 +-
 include/UserPage.php         |   6 +-
 include/functions.php        |   7 +
 include/translations.php     | 232 ++++++++++
 12 files changed, 1251 insertions(+), 78 deletions(-)
 create mode 100644 html/en/base.html
 create mode 100644 html/en/fragments.html
 rename html/{ => sv}/base.html (100%)
 rename html/{ => sv}/fragments.html (100%)
 create mode 100644 include/translations.php

diff --git a/config.php.example b/config.php.example
index 82ee321..9f5aa15 100644
--- a/config.php.example
+++ b/config.php.example
@@ -6,6 +6,12 @@ $db_user = 'dbname';
 $db_pass = 'dbpassword';
 $db_name = 'dbuser';
 
+# Application language
+$language = 'en';
+
+# Site name
+$name = 'My product tracker';
+
 # Email subject prefix
 # Will be prepended without change, so should probably end with a space
 $email_subject_prefix = "System name: ";
diff --git a/html/en/base.html b/html/en/base.html
new file mode 100644
index 0000000..0e1081f
--- /dev/null
+++ b/html/en/base.html
@@ -0,0 +1,83 @@
+¤¤ head ¤¤
+<!DOCTYPE html>
+<html>
+  <head>
+    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1" />
+    <link rel="shortcut icon" href="images/favicon.ico" type="image/x-icon">
+    <link rel="stylesheet" type="text/css" href="./template.css" />
+    <link rel="stylesheet" type="text/css" href="./style.css" />
+    <script type="text/javascript" src="./script.js"></script>
+    <link rel="stylesheet" type="text/css" href="./calendar/dhtmlxcalendar.css" />
+    <script type="text/javascript" src="./calendar/dhtmlxcalendar.js"></script>
+    <title>¤title¤</title>
+  </head>
+  <body>
+    <div id="container">
+      <a class="accessibility-link"
+         accesskey="s"
+         href="#contents"
+         title="Skip navigation"></a>
+      <div id="header">
+        <a id="header-su-responsive"
+           href="https://www.su.se/english/"
+           title="To the Stockholm Universiy website">
+          <img src="images/su_logo_responsive_en.png"
+               alt="Stockholm University" />
+        </a>
+
+        <a id="header-dsv"
+           href="https://dsv.su.se/english/"
+           title="To DSV's website"
+           accesskey="1">
+          <img src="images/dsv_logo_en.png"
+               alt="Department of Computer and Systems Sciences" />
+        </a>
+
+        <a id="header-su"
+           href="https://www.su.se/english/"
+           title="To the Stockholm Universiy website">
+          <img src="images/su_logo_en.gif"
+               alt="Stockholm University" />
+        </a>
+        <div class="clear">
+        </div>
+      </div>
+      <div id="menu">
+        ¤menu¤
+        <div class="clear">
+        </div>
+      </div>
+      <div id="contents">
+        ¤¤ foot ¤¤
+        <div class="clear">
+        </div>
+      </div>
+
+      <div id="footer">
+        <div id="footer-name">
+          <div id="footer-dsv">
+            Department of Computer and Systems Sciences
+          </div>
+          <div id="footer-su">
+            Stockholm University
+          </div>
+        </div>
+        <div id="footer-contact">
+          <a id="footer-contact-link"
+             href="https://www.su.se/department-of-computer-and-systems-sciences/about-the-department/contact"
+             accesskey="7">
+            Contact
+          </a>
+        </div>
+        <div class="clear">
+        </div>
+      </div>
+    </div>
+  </body>
+</html>
+¤¤ menuitem ¤¤
+<a class="item ¤align¤ ¤active¤"
+   href="?page=¤page¤">
+  ¤title¤
+</a>
diff --git a/html/en/fragments.html b/html/en/fragments.html
new file mode 100644
index 0000000..78d2b3b
--- /dev/null
+++ b/html/en/fragments.html
@@ -0,0 +1,843 @@
+¤¤ title ¤¤
+<h1>¤title¤</h1>
+
+¤¤ subtitle ¤¤
+<h2>¤title¤</h2>
+
+¤¤ item_link ¤¤
+<a href="./?page=¤page¤&action=show&id=¤id¤">¤name¤</a>
+
+¤¤ message ¤¤
+<div onClick="JavaScript:hideMessage()"
+     id="message"
+     class="¤type¤">
+  ¤message¤
+</div>
+
+¤¤ user_table ¤¤
+<form id="newloan"
+      class="hidden"
+      method="GET">
+  <input type="hidden"
+         name="action"
+         value="checkout" />
+</form>
+<table id="user-table">
+  <thead>
+    <tr>
+      <th>
+        Name
+      </th>
+      <th>
+        Username
+      </th>
+      <th>
+        Loan
+      </th>
+      <th>
+      </th>
+    </tr>
+  </thead>
+  <tbody>
+    ¤rows¤
+  </tbody>
+</table>
+
+¤¤ user_row ¤¤
+<tr>
+  <td>
+    ¤item_link¤<span title="¤notes¤">¤has_notes¤</span>
+  </td>
+  <td>
+    ¤name¤
+  </td>
+  <td>
+    ¤loan¤
+  </td>
+  <td>
+    <button form="newloan"
+            name="user"
+            value="¤name¤">
+      New loan
+    </button>
+  </td>
+</tr>
+
+¤¤ product_page ¤¤
+<div id="product-table">
+    ¤product_table¤
+</div>
+
+¤¤ product_table ¤¤
+<table>
+  <thead>
+    <tr>
+      <th class="status">
+      </th>
+      <th>
+        Name
+      </th>
+      <th>
+        Serial number
+      </th>
+      <th>
+        Status
+      </th>
+    </tr>
+  </thead>
+  <tbody class="¤type¤">
+    ¤rows¤
+  </tbody>
+</table>
+
+¤¤ product_row ¤¤
+<tr>
+  <td class="status ¤status¤">
+  </td>
+  <td>
+    ¤item_link¤
+  </td>
+  <td>
+    ¤serial¤
+  </td>
+  <td>
+    ¤note¤
+  </td>
+</tr>
+
+¤¤ product_detail_row ¤¤
+<tr>
+  <td class="status ¤status¤">
+  </td>
+  <td colspan="3">
+    <dl>
+      ¤details¤
+    </dl>
+  </td>
+</tr>
+
+¤¤ product_detail ¤¤
+<dt>
+  ¤name¤:
+</dt>
+<dd>
+  ¤value¤
+</dd>
+
+¤¤ template_management ¤¤
+<div>
+  <h2>Templates</h2>
+  <form class="dark templates"
+        onSubmit="JavaScript:loadTemplate(event)">
+    <datalist id="template_suggest"></datalist>
+    <input type="hidden"
+           name="page"
+           value="new" />
+    <input onFocus="JavaScript:suggest(this, 'template')"
+           list="template_suggest"
+           autocomplete="off"
+           type="text"
+           name="template"
+           value="¤template¤"
+           placeholder="Name" />
+    <button>
+      Load
+    </button>
+    <button onClick="JavaScript:saveTemplate(event)">
+      Save
+    </button>
+    <button onClick="JavaScript:deleteTemplate(event)">
+      Delete
+    </button>
+  </form>
+</div>
+
+¤¤ product_form ¤¤
+<div id="product-details">
+  <h2>Product details</h2>
+  <form id="product-data"
+        onSubmit="JavaScript:saveProduct(event)"
+        class="data">
+    <input type="hidden"
+           name="id"
+           value="¤id¤" />
+    <datalist id="field_suggest"></datalist>
+    <datalist id="tag_suggest"></datalist>
+    <table>
+      <tfoot>
+        <tr>
+          <td>
+          </td>
+          <td>
+            <button id="save"
+                    class="right">
+              Save
+            </button>
+            <button id="reset"
+                    class="right">
+              Reset
+            </button>
+          </td>
+        </tr>
+      </tfoot>
+      <tbody>
+        <tr>
+          <td>
+            Name:
+          </td>
+          <td>
+            <input type="text"
+                   name="name"
+                   value="¤name¤"
+                   onFocus="JavaScript:suggestContent(this)"
+                   list="name_suggest"
+                   autocomplete="off" />
+            <datalist id="name_suggest"></datalist>
+          </td>
+        </tr>
+        <tr>
+          <td>
+            Manufacturer:
+          </td>
+          <td>
+            <input type="text"
+                   name="brand"
+                   value="¤brand¤"
+                   onFocus="JavaScript:suggestContent(this)"
+                   list="brand_suggest"
+                   autocomplete="off" />
+            <datalist id="brand_suggest"></datalist>
+          </td>
+        </tr>
+        <tr>
+          <td>
+            Invoice number:
+          </td>
+          <td>
+            <input type="text"
+                   name="invoice"
+                   value="¤invoice¤" />
+          </td>
+        </tr>
+        <tr id="before_info">
+          <td>
+            Serial number:
+          </td>
+          <td>
+            <input type="text"
+                   name="serial"
+                   value="¤serial¤" />
+          </td>
+        </tr>
+        ¤info¤
+        <tr>
+          <td>
+            <input onKeyPress="JavaScript:addField(event)"
+                   onFocus="JavaScript:suggest(this, 'field')"
+                   list="field_suggest"
+                   autocomplete="off"
+                   class="newfield"
+                   type="text"
+                   name="new_key"
+                   placeholder="New field" />
+            <button class="minibutton"
+                    onClick="addField(event)">
+              +
+            </button>
+          </td>
+          <td>
+          </td>
+        </tr>
+        <tr>
+          <td>
+            Tags:
+          </td>
+          <td id="tags">
+            ¤tags¤
+            <input onKeyPress="JavaScript:addTag(event)"
+                   onFocus="JavaScript:suggest(this, 'tag')"
+                   list="tag_suggest"
+                   autocomplete="off"
+                   class="newtag"
+                   type="text"
+                   name="new_tag"
+                   placeholder="New tag" />
+            <button class="minibutton"
+                    onClick="JavaScript:addTag(event)">
+              +
+            </button>
+          </td>
+        </tr>
+      </tbody>
+    </table>
+  </form>
+  <form id="product-actions"
+        class="¤hidden¤">
+    <input type="hidden"
+           name="id"
+           value="¤id¤" />
+    <button onClick="JavaScript:discardProduct(event)">
+      Discard product
+    </button>
+    <button onClick="JavaScript:toggleService(event)">
+      ¤service¤
+    </button>
+  </form>
+</div>
+<div id="product-attachments"
+     class="¤hidden¤">
+  <h2>Attachments</h2>
+  ¤attachments¤
+  <form id="attachment_upload"
+        onSubmit="JavaScript:uploadAttachment(event)">
+    <input type="hidden"
+           name="id"
+           value="¤id¤" />
+    <input id="uploadfile"
+           name="uploadfile"
+           type="file"
+           onchange="showFile(event)"/>
+    <input id="filename"
+           name="filename"
+           type="text"
+           placeholder="Choose a file..."
+           onclick="selectFile(event)"
+           readonly />
+    <button>Upload</button>
+  </form>
+</div>
+
+¤¤ product_meta ¤¤
+<div id="product-history">
+  <h2>Product history</h2>
+  ¤history¤
+</div>
+<div id="product-label"
+     class="¤label_hidden¤">
+  <h2>Label</h2>
+  ¤label¤
+</div>
+<div id="product-direct-checkout" class="¤checkout_hidden¤">
+  <h2>Check out</h2>
+  <form class="light"
+        onSubmit="JavaScript:checkoutProduct(event)">
+    <datalist id="user_suggest"></datalist>
+    <input type="hidden"
+           name="page"
+           value="checkout" />
+    <label for="user">Username:</label>
+    <input onFocus="JavaScript:suggest(this, 'user')"
+           type="text"
+           name="user"
+           list="user_suggest"
+           autocomplete="off"
+           placeholder="Username"
+           required />
+    <input type="hidden"
+           id="product"
+           name="product"
+           value="¤serial¤" />
+    <button>
+      Check out
+    </button>
+    <br/>
+    <label>Loan length:</label>
+    <button onClick="JavaScript:loanLength(event, 7, 'day')">1 week</button>
+    <button onClick="JavaScript:loanLength(event, 1, 'year')">1 year</button>
+    <button onClick="JavaScript:loanLength(event, 3, 'year')">3 years</button>
+    <br/>
+    <label for="end">End date:</label>
+    <input type="text"
+           id="end"
+           onClick="JavaScript:calendar(event)"
+           name="end"
+           value="¤end¤" />
+  </form>
+</div>
+
+¤¤ attachment_list ¤¤
+<ul class="attachment-list">
+  ¤attachments¤
+</ul>
+
+¤¤ attachment ¤¤
+<li>
+  <strong>¤name¤</strong> (¤date¤): <a href="./?page=dl&id=¤id¤">Download</a>
+  <br/>
+  <form onSubmit="JavaScript:deleteAttachment(event)">
+    <input type="hidden"
+           name="id"
+           value="¤id¤" />
+    <input type="hidden"
+           name="name"
+           value="¤name¤" />
+    <button>Delete</button>
+  </form>
+</li>
+
+¤¤ product_label ¤¤
+<div class="qr">
+  <a href="./?page=print&id=¤id¤"
+     title="Print">
+    <span>¤name¤</span>
+    <img src="./?page=qr&id=¤id¤">
+    <span>¤serial¤</span>
+  </a>
+</div>
+
+¤¤ label_page ¤¤
+<!DOCTYPE html>
+<html>
+  <head>
+    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1" />
+    <link rel="shortcut icon" href="images/favicon.ico" type="image/x-icon">
+    <link rel="stylesheet" type="text/css" href="./template.css" />
+    <link rel="stylesheet" type="text/css" href="./style.css" />
+    <title>¤title¤</title>
+  </head>
+  <body>
+    ¤label¤
+  </body>
+</html>
+
+¤¤ info_item ¤¤
+<tr>
+  <td>
+    ¤name¤:
+  </td>
+  <td>
+    <input type="text"
+           class="info_item"
+           name="¤key¤"
+           value="¤value¤"
+           onFocus="JavaScript:suggestContent(this)"
+           list="¤key¤_suggest"
+           autocomplete="off" />
+    <datalist id="¤key¤_suggest"></datalist>
+  </td>
+</tr>
+
+¤¤ tag ¤¤
+<p>
+  <span class="tag">
+    <input type="hidden"
+           name="tag[]"
+           value="¤tag¤" />
+    ¤tag¤
+    <a class="tagremove"
+       onClick="JavaScript:removeTag(event)">
+      x
+    </a>
+  </span>
+</p>
+
+¤¤ user_details ¤¤
+<div id="user-details">
+  <form onSubmit="JavaScript:updateUser(event)">
+    <input type="hidden"
+           name="id"
+           value="¤id¤" />
+    <table>
+      <tfoot>
+        <tr>
+          <td>
+          </td>
+          <td>
+            <button class="right"
+                    id="save">
+              Save
+            </button>
+            <button class="right"
+                    id="reset">
+              Reset
+            </button>
+          </td>
+        </tr>
+      </tfoot>
+      <tbody>
+        <tr>
+          <td>
+            Name:
+          </td>
+          <td>
+          <input type="text"
+                 value="¤displayname¤"
+                 disabled />
+          </td>
+        </tr>
+        <tr>
+          <td>
+            Username:
+          </td>
+          <td>
+            <input type="text"
+                   name="name"
+                   value="¤name¤" />
+          </td>
+        </tr>
+        <tr>
+          <td>
+            Notes:
+          </td>
+          <td>
+            <textarea name="notes">¤notes¤</textarea>
+          </td>
+        </tr>
+      </tbody>
+    </table>
+  </form>
+  <form method="GET">
+    <input type="hidden"
+           name="action"
+           value="checkout" />
+    <button name="user"
+            value="¤name¤">
+      New loan
+    </button>
+  </form>
+</div>
+<div id="active-loans">
+  <h2>Current loans</h2>
+  ¤active_loans¤
+</div>
+<div id="inactive-loans">
+  <h2>Old loans</h2>
+  ¤inactive_loans¤
+</div>
+
+¤¤ history_table ¤¤
+<table class="history">
+  <thead>
+    <tr>
+      <th class="status">
+      </th>
+      <th>
+        ¤item¤
+      </th>
+      <th>
+        Start date
+      </th>
+      <th>
+        End date
+      </th>
+      <th>
+        Misc
+      </th>
+    </tr>
+  </thead>
+  <tbody>
+    ¤rows¤
+  </tbody>
+</table>
+
+¤¤ history_row ¤¤
+<tr>
+  <td class="status ¤status¤">
+  </td>
+  <td>
+    ¤item_link¤
+  </td>
+  <td>
+    ¤start_date¤
+  </td>
+  <td>
+    ¤end_date¤
+  </td>
+  <td>
+    ¤note¤
+  </td>
+</tr>
+
+¤¤ loan_extend_form ¤¤
+<button onClick="JavaScript:showExtend(event)">
+  Extend
+</button>
+<form class="renew_confirm hidden"
+      onSubmit="JavaScript:extendLoan(event)">
+  <input type="hidden"
+         name="product"
+         value="¤id¤" />
+  <input onClick="JavaScript:calendar(event)"
+         id="¤id¤_date"
+         class="narrow"
+         type="text"
+         name="end"
+         value="¤end_new¤" />
+  <button>
+    Save
+  </button>
+</form>
+
+
+¤¤ checkout_page ¤¤
+<div id="user-select">
+  <h2>Choose borrower</h2>
+  <form class="dark"
+        action="./"
+        method="GET">
+    <datalist id="user_suggest"></datalist>
+    <input type="hidden"
+           name="page"
+           value="checkout" />
+    <div>
+      <label for="user">
+        Username:
+      </label>
+      <input onFocus="JavaScript:suggest(this, 'user')"
+             type="text"
+             name="user"
+             id="user"
+             list="user_suggest"
+             autocomplete="off"
+             placeholder="Username"
+             value="¤user¤" />
+      <button type="submit" >
+        Find
+      </button>
+    </div>
+    <div>
+      <label for="email">
+        E-mail:
+      </label>
+      <input type="text"
+             name="email"
+             id="email"
+             autocomplete="off"
+             placeholder="E-mail"
+             value="¤email¤" />
+    </div>
+    <div>
+      <label for="displayname">
+        Name:
+      </label>
+      <input type="text"
+             name="displayname"
+             id="displayname"
+             value="¤displayname¤"
+             disabled />
+    </div>
+    <div>
+      <label for="notes">
+        Notes:
+      </label>
+      <textarea name="notes"
+                id="notes"
+                disabled>¤notes¤</textarea>
+    </div>
+  </form>
+</div>
+<div id="product-checkout">
+  <h2>Check out product</h2>
+  <form class="light"
+        onSubmit="JavaScript:checkoutProduct(event)">
+    <input type="hidden"
+           name="page"
+           value="checkout" />
+    <input type="hidden"
+           name="user"
+           value="¤user¤">
+    <label for="product">Product:</label>
+    <input type="text"
+           id="product"
+           name="product"
+           placeholder="Serial number"
+           required
+           ¤disabled¤ />
+    <button>
+      Check out
+    </button>
+    <br/>
+    <label>Loan length:</label>
+    <button onClick="JavaScript:loanLength(event, 7, 'day')">1 week</button>
+    <button onClick="JavaScript:loanLength(event, 1, 'year')">1 year</button>
+    <button onClick="JavaScript:loanLength(event, 3, 'year')">3 years</button>
+    <br/>
+    <label for="end">End date:</label>
+    <input type="text"
+           id="end"
+           onClick="JavaScript:calendar(event)"
+           name="end"
+           value="¤end¤"
+           ¤disabled¤ />
+  </form>
+  ¤subtitle¤
+  ¤loan_table¤
+</div>
+
+¤¤ inventory_start ¤¤
+<form class="dark"
+      onSubmit="JavaScript:startInventory(event)">
+  <button name="start">
+    Start inventory
+  </button>
+</form>
+
+¤¤ inventory_do ¤¤
+
+<div id="inventory-overview"
+     class="dark">
+  <span class="label">
+    Start date:
+  </span>
+  ¤start_date¤
+  <br/>
+  <span class="label">
+    Total number of products:
+  </span>
+  ¤total_count¤
+  <br/>
+  <span class="label">
+    Number of registered products:
+  </span>
+  <span id="seen_count">
+    ¤seen_count¤
+  </span>
+  <form class="dark ¤hide¤"
+        onSubmit="JavaScript:endInventory(event)">
+    <button name="end">
+      End inventory
+    </button>
+  </form>
+</div>
+<form id="inventory-register"
+      class="light ¤hide¤"
+      onSubmit="JavaScript:inventoryProduct(event)">
+  <label for="serial">
+    Product:
+  </label>
+  <input type="text"
+         name="serial"
+         id="serial"
+         placeholder="Serial number" />
+  <button>
+    Register
+  </button>
+</form>
+<div id="unseen-products">
+  <h2>¤unseen_title¤</h2>
+  ¤unseen¤
+</div>
+<div id="seen-products">
+  <h2>Registered products</h2>
+  ¤seen¤
+</div>
+
+¤¤ return_page ¤¤
+<form class="dark"
+      onSubmit="JavaScript:returnProduct(event)">
+  <label for="serial">
+    Product:
+  </label>
+  <input type="text"
+         name="serial"
+         id="serial"
+         placeholder="Serial number"
+         required />
+  <button>
+    Return
+  </button>
+</form>
+
+¤¤ inventory_table ¤¤
+<table id="inventory-history">
+  <thead>
+    <tr>
+      <th>
+      </th>
+      <th>
+        ¤item¤
+      </th>
+      <th>
+        Start date
+      </th>
+      <th>
+        End date
+      </th>
+      <th>
+        Registered products
+      </th>
+      <th>
+        Missing products
+      </th>
+    </tr>
+  </thead>
+  <tbody>
+    ¤rows¤
+  </tbody>
+</table>
+
+¤¤ inventory_row ¤¤
+<tr>
+  <td>
+  </td>
+  <td>
+    ¤item_link¤
+  </td>
+  <td>
+    ¤start_date¤
+  </td>
+  <td>
+    ¤end_date¤
+  </td>
+  <td>
+    ¤num_seen¤
+  </td>
+  <td>
+    ¤num_unseen¤
+  </td>
+</tr>
+
+¤¤ search_form ¤¤
+<form onSubmit="JavaScript:doSearch(event)"
+      id="search"
+      class="dark">
+  <p>
+    <label for="q">Search term:</label>
+    <input type="hidden"
+           name="page"
+           value="search" />
+    <input type="text"
+           onKeyPress="JavaScript:searchInput(event)"
+           name="q"
+           id="q"
+           placeholder="What are you looking for?"
+           value=""
+           autofocus />
+    <button type="submit">
+      Search
+    </button>
+  </p>
+  <div id="terms">
+    ¤terms¤
+  </div>
+  <div class="clear"></div>
+</form>
+<p id="hints">
+  Advanced searches are possible by prefixing a search term with a keyword ending with a colon, for example: <strong>serial:123456</strong> or <strong>tag:computer</tag>
+</p>
+<div id="found-products"
+     class="¤hidden¤">
+  <h2>Products</h2>
+  ¤product_results¤
+</div>
+<div id="found-users"
+     class="¤hidden¤">
+  <h2>Users</h2>
+  ¤user_results¤
+</div>
+
+¤¤ search_term ¤¤
+
+<p class="left">
+  <span class="term">
+    <input type="hidden"
+           name="¤key¤"
+           value="¤value¤" />
+    ¤term¤
+    <a class="termremove"
+       onClick="JavaScript:removeTerm(event)">
+      x
+    </a>
+  </span>
+</p>
diff --git a/html/base.html b/html/sv/base.html
similarity index 100%
rename from html/base.html
rename to html/sv/base.html
diff --git a/html/fragments.html b/html/sv/fragments.html
similarity index 100%
rename from html/fragments.html
rename to html/sv/fragments.html
diff --git a/include/HistoryPage.php b/include/HistoryPage.php
index 7d0f830..6761c2c 100644
--- a/include/HistoryPage.php
+++ b/include/HistoryPage.php
@@ -14,15 +14,15 @@ class HistoryPage extends Page {
             } catch(Exception $e) {
                 $this->inventory = null;
                 $this->action = 'list';
-                $this->error = 'Det finns ingen inventering med det ID-numret.';
+                $this->error = i18n('There is no inventory with that ID.');
             }
         }
         switch($this->action) {
             case 'show':
-                $this->subtitle = 'Inventeringsdetaljer';
+                $this->subtitle = i18n('Inventory details');
                 break;
             case 'list':
-                $this->subtitle = 'Historik';
+                $this->subtitle = i18n('History');
                 break;
         }
     }
@@ -30,16 +30,16 @@ class HistoryPage extends Page {
     protected function render_body() {
         switch($this->action) {
             case 'list':
-                print(replace(array('title' => 'Genomförda inventeringar'),
+                print(replace(array('title' => i18n('Past inventories')),
                               $this->fragments['subtitle']));
                 print($this->build_inventory_table());
-                print(replace(array('title' => 'Skrotade artiklar'),
+                print(replace(array('title' => i18n('Discarded products')),
                               $this->fragments['subtitle']));
                 $discards = get_items('product_discarded');
                 if($discards) {
                     print($this->build_product_table($discards));
                 } else {
-                    print('Inga artiklar skrotade.');
+                    print(i18n('No products discarded.'));
                 }
                 break;
             case 'show':
@@ -55,7 +55,7 @@ class HistoryPage extends Page {
     private function build_inventory_table() {
         $items = get_items('inventory_old');
         if(!$items) {
-            return 'Inga inventeringar gjorda.';
+            return i18n('No inventories have been performed.');
         }
         $rows = '';
         foreach($items as $inventory) {
@@ -75,7 +75,7 @@ class HistoryPage extends Page {
                                    'num_unseen' => $num_unseen),
                              $this->fragments['inventory_row']);
         }
-        return replace(array('item' => 'Tillfälle',
+        return replace(array('item' => i18n('Number'),
                              'rows' => $rows),
                        $this->fragments['inventory_table']);
     }
diff --git a/include/Page.php b/include/Page.php
index af01b01..e302222 100644
--- a/include/Page.php
+++ b/include/Page.php
@@ -3,22 +3,26 @@ abstract class Page extends Responder {
     protected abstract function render_body();
 
     protected $page = 'checkout';
-    protected $title = "DSV Utlåning";
+    protected $title = '';
     protected $subtitle = '';
     protected $error = null;
-    protected $menuitems = array('checkout' => 'Låna',
-                                 'return' => 'Lämna',
-                                 'products' => 'Artiklar',
-                                 'new' => 'Ny artikel',
-                                 'users' => 'Låntagare',
-                                 'inventory' => 'Inventera',
-                                 'history' => 'Historik',
-                                 'search' => 'Sök');
+    protected $menuitems = array();
     private $template_parts = array();
 
     public function __construct() {
+        global $language, $name;
         parent::__construct();
-        $this->template_parts = get_fragments('./html/base.html');
+
+        $this->title = $name;
+        $this->menuitems = array('checkout' => i18n('Check out'),
+                                 'return' => i18n('Return'),
+                                 'products' => i18n('Products'),
+                                 'new' => i18n('New product'),
+                                 'users' => i18n('Borrowers'),
+                                 'inventory' => i18n('Inventory'),
+                                 'history' => i18n('History'),
+                                 'search' => i18n('Search'));
+        $this->template_parts = get_fragments("./html/$language/base.html");
 
         if(isset($_GET['page'])) {
             $this->page = $_GET['page'];
@@ -118,7 +122,7 @@ abstract class Page extends Responder {
                     $loan_str = $product->get_name();
                     break;
                 default:
-                    $loan_str = $count .' artiklar';
+                    $loan_str = i18n('{count} products', $count);
                     break;
             }
             $replacements['loan'] = $loan_str;
@@ -143,35 +147,31 @@ abstract class Page extends Responder {
                                   'name' => $product->get_name(),
                                   'page' => 'products'),
                             $this->fragments['item_link']);
-        $note = 'Tillgänglig';
+        $note = i18n('Available');
         $status = $product->get_status();
         switch($status) {
-        case 'discarded':
-            $discarded = format_date($product->get_discardtime());
-            $note = 'Skrotad '.$discarded;
-            break;
-        case 'service':
-            $service = $product->get_active_service();
-            $note = 'På service sedan '
-                  .format_date($service->get_starttime());
-            break;
-        case 'on_loan':
-        case 'overdue':
-            $loan = $product->get_active_loan();
-            $user = $loan->get_user();
-            $replacements = array('name' => $user->get_displayname($this->ldap),
-                                  'id'   => $user->get_id(),
-                                  'page' => 'users');
-            $userlink = replace($replacements,
-                                $this->fragments['item_link']);
-            $note = 'Utlånad till '.$userlink;
-            if($loan->is_overdue()) {
-                $note .= ', försenad';
-            } else {
-                $note .= ', slutdatum '
-                      .format_date($loan->get_endtime());
-            }
-            break;
+            case 'discarded':
+                $discarded = format_date($product->get_discardtime());
+                $note = i18n('Discarded on {date}', $discarded);
+                break;
+            case 'service':
+                $service = $product->get_active_service();
+                $note = i18n('Being serviced since {date}',
+                             $service->get_starttime());
+                break;
+            case 'on_loan':
+            case 'overdue':
+                $loan = $product->get_active_loan();
+                $user = $loan->get_user();
+                $replacements = array(
+                    'name' => $user->get_displayname($this->ldap),
+                    'id'   => $user->get_id(),
+                    'page' => 'users'
+                );
+                $userlink = replace($replacements,
+                                    $this->fragments['item_link']);
+                $note = i18n('Borrowed by {user} {loan}', $userlink, $loan);
+                break;
         }
         $out = replace(array('status'    => $status,
                              'item_link' => $prodlink,
@@ -229,7 +229,7 @@ abstract class Page extends Responder {
                              $this->fragments['history_row']);
         }
         return replace(array('rows' => $rows,
-                             'item' => 'Artikel'),
+                             'item' => i18n('Product')),
                        $this->fragments['history_table']);
     }
 
@@ -249,34 +249,35 @@ abstract class Page extends Responder {
                 $discardtime = $product->get_discardtime();
                 if($discardtime && $discardtime < $regtime) {
                     $status = 'discarded';
-                    $note = 'Skrotad '.format_date($discardtime);
+                    $note = i18n('Discarded on {date}',
+                                 format_date($discardtime));
                 } else {
                     $status = 'available';
                 }
             } else if($event instanceof Service) {
                 $status = 'service';
-                $note = 'På service sedan '.format_date($event->get_starttime());
+                $starttime = $event->get_starttime();
                 $returntime = $event->get_returntime();
+                $note = i18n('Being serviced since {date}', $starttime);
                 if($returntime) {
-                    $note .= ', åter den '.format_date($returntime);
+                    $note = i18n("Serviced between {start} and {end}",
+                                 $starttime,
+                                 $returntime);
                 }
             } else if($event instanceof Loan) {
                 $user = $event->get_user();
-                $userlink = replace(array('name' => $user->get_displayname($this->ldap),
-                                          'id' => $user->get_id(),
-                                          'page' => 'users'),
-                                    $this->fragments['item_link']);
+                $userlink = replace(
+                    array('name' => $user->get_displayname($this->ldap),
+                          'id' => $user->get_id(),
+                          'page' => 'users'),
+                    $this->fragments['item_link']);
                 $status = 'on_loan';
-                $note = 'Utlånad till '.$userlink;
-                $returntime = $event->get_returntime();
+                $note = i18n('Borrowed by {user} {loan} {inventorytime}',
+                             $userlink,
+                             $event,
+                             $regtime);
                 if($event->get_endtime() < $regtime) {
                     $status = 'overdue';
-                    $note .= ', försenad';
-                } else {
-                    $note .= ', slutdatum '.format_date($event->get_endtime());
-                }
-                if($returntime) {
-                    $note .= ', återlämnad '.format_date($returntime);
                 }
             }
             $rows .= replace(array('status' => $status,
@@ -302,17 +303,17 @@ abstract class Page extends Responder {
                 $unseen[] = $product;
             }
         }
-        $missing = 'Saknade artiklar';
+        $missing = i18n('Missing products');
         $hidden = 'hidden';
         if($interactive) {
-            $missing = 'Kvarvarande artiklar';
+            $missing = i18n('Remaining products');
             $hidden = '';
         }
-        $unseen_table = 'Inga artiklar saknas.';
+        $unseen_table = i18n('No products are missing.');
         if($unseen) {
             $unseen_table = $this->build_product_table($unseen);
         }
-        $seen_table = 'Inga artiklar inventerade.';
+        $seen_table = i18n('No products registered');
         if($seen) {
             $seen_table = $this->build_seen_table($seen, $inventory);
         }
diff --git a/include/ProductPage.php b/include/ProductPage.php
index 8f00fa2..4edd51d 100644
--- a/include/ProductPage.php
+++ b/include/ProductPage.php
@@ -16,16 +16,16 @@ class ProductPage extends Page {
                 } catch(Exception $e) {
                     $this->action = 'list';
                     $this->product = null;
-                    $this->error = 'Det finns ingen artikel med det ID-numret.';
+                    $this->error = i18n('There is no product with that ID.');
                 }
             }
         }
         switch($this->action) {
             case 'show':
-                $this->subtitle = 'Artikeldetaljer';
+                $this->subtitle = i18n('Product details');
                 break;
             case 'list':
-                $this->subtitle = 'Artikellista';
+                $this->subtitle = i18n('Product list');
                 break;
         }
     }
@@ -70,7 +70,7 @@ class ProductPage extends Page {
                         'label_hidden' => 'hidden',
                         'checkout_hidden'      => 'hidden',
                         'hidden'      => '',
-                        'service'     => 'Starta service',
+                        'service'     => i18n('Start service'),
                         'history'     => $history,
                         'attachments' => $attachments,
                         'end'         => format_date(default_loan_end(time())));
@@ -81,7 +81,7 @@ class ProductPage extends Page {
         if(!$this->product->get_discardtime()) {
             $fields['label_hidden'] = '';
             if($this->product->get_status() == 'service') {
-                $fields['service'] = 'Avsluta service';
+                $fields['service'] = i18n('End service');
             }
             if($this->product->get_status() == 'available') {
                 $fields['checkout_hidden'] = '';
@@ -94,7 +94,7 @@ class ProductPage extends Page {
 
     private function build_history_table($history) {
         if(!$history) {
-            return 'Ingen historik att visa.';
+            return i18n('No history to display.');
         }
         $rows = '';
         foreach($history as $event) {
@@ -126,14 +126,14 @@ class ProductPage extends Page {
                              $this->fragments['history_row']);
         }
         return replace(array('rows' => $rows,
-                             'item' => 'Låntagare'),
+                             'item' => i18n('Borrower')),
                        $this->fragments['history_table']);
     }
 
 
     private function build_attachment_list($attachments) {
         if(!$attachments) {
-            return '<p>Inga bilagor.</p>';
+            return '<p>'.i18n('No attachments.').'</p>';
         }
         $items = '';
         foreach($attachments as $attachment) {
diff --git a/include/Responder.php b/include/Responder.php
index ff621cd..69864e0 100644
--- a/include/Responder.php
+++ b/include/Responder.php
@@ -4,7 +4,8 @@ abstract class Responder {
     protected $ldap = null;
     
     public function __construct() {
-        $this->fragments = get_fragments('./html/fragments.html');
+        global $language;
+        $this->fragments = get_fragments("./html/$language/fragments.html");
         $this->ldap = new Ldap();
     }
     
diff --git a/include/UserPage.php b/include/UserPage.php
index c5ff4e1..4473aa7 100644
--- a/include/UserPage.php
+++ b/include/UserPage.php
@@ -16,16 +16,16 @@ class UserPage extends Page {
                 } catch(Exception $e) {
                     $this->user = null;
                     $this->action = 'list';
-                    $this->error = 'Det finns ingen användare med det ID-numret.';
+                    $this->error = i18n('There is no user with that ID.');
                 }
             }
         }
         switch($this->action) {
             case 'show':
-                $this->subtitle = 'Låntagardetaljer';
+                $this->subtitle = i18n('Borrower details');
                 break;
             case 'list':
-                $this->subtitle = 'Låntagarlista';
+                $this->subtitle = i18n('Borrower list');
                 break;
         }
     }
diff --git a/include/functions.php b/include/functions.php
index f265439..84d4a4c 100644
--- a/include/functions.php
+++ b/include/functions.php
@@ -1,5 +1,7 @@
 <?php
 
+require_once('./include/translations.php');
+
 /*
    Takes an html file containing named fragments.
    Returns an associative array on the format array[name]=>fragment.
@@ -43,6 +45,11 @@ function get_fragments($infile) {
     return try_adding($name, $current_fragment, $out, $infile);
 }
 
+function i18n($string, ...$args) {
+    global $language, $i18n;
+    return $i18n[$string][$language](...$args);
+}
+
 function try_adding($key, $value, $array, $filename) {
     if(array_key_exists($key, $array)) {
         throw new Exception('There is already a fragment with that name in '.$filename);
diff --git a/include/translations.php b/include/translations.php
new file mode 100644
index 0000000..7ed6dd7
--- /dev/null
+++ b/include/translations.php
@@ -0,0 +1,232 @@
+<?php
+
+$i18n = array(
+    "Check out" => array(
+        "en" => function() { return "Check out"; },
+        "sv" => function() { return "Låna"; },
+    ),
+    "Return" => array(
+        "en" => function() { return "Return"; },
+        "sv" => function() { return "Lämna"; },
+    ),
+    "Products" => array(
+        "en" => function() { return "Products"; },
+        "sv" => function() { return "Artiklar"; },
+    ),
+    "Product" => array(
+        "en" => function() { return "Product"; },
+        "sv" => function() { return "Artikel"; },
+    ),
+    "New product" => array(
+        "en" => function() { return "New product"; },
+        "sv" => function() { return "Ny artikel"; },
+    ),
+    "Borrowers" => array(
+        "en" => function() { return "Borrowers"; },
+        "sv" => function() { return "Låntagare"; },
+    ),
+    "Borrower" => array(
+        "en" => function() { return "Borrower"; },
+        "sv" => function() { return "Låntagare"; },
+    ),
+    "Inventory" => array(
+        "en" => function() { return "Inventory"; },
+        "sv" => function() { return "Inventera"; },
+    ),
+    "History" => array(
+        "en" => function() { return "History"; },
+        "sv" => function() { return "Historik"; },
+    ),
+    "Search" => array(
+        "en" => function() { return "Search"; },
+        "sv" => function() { return "Sök"; },
+    ),
+    "{count} products" => array(
+        "en" => function($count) { return "$count products"; },
+        "sv" => function($count) { return "$count artiklar"; },
+    ),
+    "Available" => array(
+        "en" => function() { return "Available"; },
+        "sv" => function() { return "Tillgänglig"; },
+    ),
+    "Discarded on {date}" => array(
+        "en" => function($date) { return "Discarded on $date"; },
+        "sv" => function($date) { return "Skrotad $date"; },
+    ),
+    "Being serviced since {date}" => array(
+        "en" => function($date) {
+            $date = format_date($date);
+            return "Being serviced since $date";
+        },
+        "sv" => function($date) {
+            $date = format_date($date);
+            return "På service sedan $date";
+        },
+    ),
+    "Serviced between {start} and {end}" => array(
+        "en" => function($start, $end) {
+            $start = format_date($start);
+            $end = format_date($end);
+            return "Serviced between $start and $end";
+        },
+        "sv" => function($start, $end) {
+            $start = format_date($start);
+            $end = format_date($end);
+            return "På service mellan $start och $end";
+        },
+    ),
+    "Borrowed by {user} {loan}" => array(
+        "en" => function($user, $loan) {
+            $note = "Borrowed by $user";
+            if($loan->is_overdue()) {
+                $note .= ', overdue';
+            } else {
+                $note .= ', end date '.format_date($loan->get_endtime());
+            }
+            return $note;
+        },
+        "sv" => function($user, $loan) {
+            $note = "Utlånad till $user";
+            if($loan->is_overdue()) {
+                $note .= ', försenad';
+            } else {
+                $note .= ', slutdatum '.format_date($loan->get_endtime());
+            }
+            return $note;
+        },
+    ),
+    "Borrowed by {user} {loan} {inventorytime}" => array(
+        "en" => function($user, $loan, $inventorytime) {
+            $note = "Borrowed by $user";
+            $endtime = $loan->get_endtime();
+            $returntime = $loan->get_returntime();
+            if($endtime < $inventorytime) {
+                $note .= ', overdue';
+            } else {
+                $note .= ', end date '.format_date($endtime);
+            }
+            if($returntime) {
+                $note .= ', returned on '.format_date($returntime);
+            }
+            return $note;
+        },
+        "sv" => function($user, $loan, $inventorytime) {
+            $note = "Utlånad till $user";
+            $endtime = $loan->get_endtime();
+            $returntime = $loan->get_returntime();
+            if($endtime < $inventorytime) {
+                $note .= ', försenad';
+            } else {
+                $note .= ', slutdatum '.format_date($endtime);
+            }
+            if($returntime) {
+                $note .= ', återlämnad '.format_date($returntime);
+            }
+            return $note;
+        },
+    ),
+    "Missing products" => array(
+        "en" => function() { return "Missing products"; },
+        "sv" => function() { return "Saknade artiklar"; },
+    ),
+    "Remaining products" => array(
+        "en" => function() { return "Remaining products"; },
+        "sv" => function() { return "Kvarvarande artiklar"; },
+    ),
+    "No products are missing." => array(
+        "en" => function() { return "No products are missing."; },
+        "sv" => function() { return "Inga artiklar saknas."; },
+    ),
+    "No products registered" => array(
+        "en" => function() { return "No products registered"; },
+        "sv" => function() { return "Inga artiklar inventerade."; },
+    ),
+    "There is no product with that ID." => array(
+        "en" => function() {
+            return "There is no product with that ID.";
+        },
+        "sv" => function() {
+            return "Det finns ingen artikel med det ID-numret.";
+        },
+    ),
+    "Product details" => array(
+        "en" => function() { return "Product details"; },
+        "sv" => function() { return "Artikeldetaljer"; },
+    ),
+    "Product list" => array(
+        "en" => function() { return "Product list"; },
+        "sv" => function() { return "Artikellista"; },
+    ),
+    "Start service" => array(
+        "en" => function() { return "Start service"; },
+        "sv" => function() { return "Starta service"; },
+    ),
+    "End service" => array(
+        "en" => function() { return "End service"; },
+        "sv" => function() { return "Avsluta service"; },
+    ),
+    "No history to display." => array(
+        "en" => function() { return "No history to display."; },
+        "sv" => function() { return "Ingen historik att visa."; },
+    ),
+    "No attachments." => array(
+        "en" => function() { return "No attachments."; },
+        "sv" => function() { return "Inga bilagor."; },
+    ),
+    "Inventory details" => array(
+        "en" => function() { return "Inventory details"; },
+        "sv" => function() { return "Inventeringsdetaljer"; },
+    ),
+    "There is no inventory with that ID." => array(
+        "en" => function() {
+            return "There is no inventory with that ID.";
+        },
+        "sv" => function() {
+            return "Det finns ingen inventering med det ID-numret.";
+        },
+    ),
+    "Past inventories" => array(
+        "en" => function() { return "Past inventories"; },
+        "sv" => function() { return "Genomförda inventeringar"; },
+    ),
+    "Discarded products" => array(
+        "en" => function() { return "Discarded products"; },
+        "sv" => function() { return "Skrotade artiklar"; },
+    ),
+    "No products discarded." => array(
+        "en" => function() { return "No products discarded."; },
+        "sv" => function() { return "Inga artiklar skrotade."; },
+    ),
+    "No inventories have been performed." => array(
+        "en" => function() { return "No inventories have been performed."; },
+        "sv" => function() { return "Inga inventeringar gjorda."; },
+    ),
+    "Number" => array(
+        "en" => function() { return "Number"; },
+        "sv" => function() { return "Nummer"; },
+    ),
+    "There is no user with that ID." => array(
+        "en" => function() {
+            return "There is no user with that ID.";
+        },
+        "sv" => function() {
+            return "Det finns ingen användare med det ID-numret.";
+        },
+    ),
+    "Borrower list" => array(
+        "en" => function() { return "Borrower list"; },
+        "sv" => function() { return "Låntagarlista"; },
+    ),
+    "Borrower details" => array(
+        "en" => function() { return "Borrower details"; },
+        "sv" => function() { return "Låntagardetaljer"; },
+    ),
+    /*
+    "" => array(
+        "en" => function() { return ""; },
+        "sv" => function() { return ""; },
+    ),
+    */
+);
+
+?>