Mainly internationalisation improvements - not finished yet.

This commit is contained in:
Simon Brooke 2017-09-07 14:44:13 +01:00
parent a7409c25be
commit d4ed336827
21 changed files with 90 additions and 234 deletions

View file

@ -5,6 +5,8 @@ Smeagol is a simple Wiki engine inspired by [Gollum](https://github.com/gollum/g
So at this stage Smeagol is a Wiki engine written in Clojure which uses Markdown as its text format, which does have user authentication, and which uses Git as its versioning and backup system.
<a href="https://zenhub.com"><img src="https://raw.githubusercontent.com/ZenHubIO/support/master/zenhub-badge.png"></a>
## Status
Smeagol is now a fully working small Wiki engine, and meets my own immediate needs.

View file

@ -8,7 +8,7 @@
[org.clojure/core.memoize "0.5.9"]
[org.clojure/data.json "0.2.6"]
[org.clojure/tools.logging "0.4.0"]
[org.clojars.simon_brooke/internationalisation "1.0.0"]
[org.clojars.simon_brooke/internationalisation "1.0.1-SNAPSHOT"]
[clj-jgit "0.8.9"]
[clj-yaml "0.4.0"]
[com.cemerick/url "0.1.1"]

View file

@ -1,107 +0,0 @@
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;
;;;; Smeagol: a very simple Wiki engine.
;;;;
;;;; This program is free software; you can redistribute it and/or
;;;; modify it under the terms of the GNU General Public License
;;;; as published by the Free Software Foundation; either version 2
;;;; of the License, or (at your option) any later version.
;;;;
;;;; This program is distributed in the hope that it will be useful,
;;;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;;;; GNU General Public License for more details.
;;;;
;;;; You should have received a copy of the GNU General Public License
;;;; along with this program; if not, write to the Free Software
;;;; Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
;;;; USA.
;;;;
;;;; Copyright (C) 2017 Simon Brooke
;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;; en-GB.edn: English-language messages.
;;; This is essentially all the text in the chrome - that which isn't editable
;;; through the wiki itself
;; ; ; ; ; ; ; ; ; ;
{:add-user-label "Add new user" ;; label for the add user link on edit users page
:change-pass-label "Change password!"
;; text of the change password widget itself on the
;; change password page
:change-pass-link "Change password"
;; text of the change password link on the menu
:change-pass-prompt "To change your password"
;; text of the change password widget prompt on the
;; change password page
:change-col-hdr "Changes" ;; header for the changes column in history
:chpass-bad-match "Your proposed passwords don't match"
;; error text if proposed passwords don't match
:chpass-fail "Your password was not changed"
;; error text on fail other htan too short or bad match
:chpass-success "Your password was changed"
;; confirmation text on password change
:chpass-too-short "You proposed password wasn't long enough: eight characters required"
;; error text if proposed password is too short
:chpass-title-prefix "Change password for"
;; prefix for title of change password page
:cookies-about "About cookies" ;; about cookies text
:cookies-more "This website stores session information as a 'cookie' on your browser. This helps us show you the content you want to see. This cookie does not identify you, and cannot be read by other websites. It is deleted by your browser as soon as you leave this site. This website does not use any third party cookies, so your visit here cannot be tracked by other websites."
;; more about cookies text
:default-page-title "Introduction" ;; title of the default page in this wiki
:del-col-hdr "Delete" ;; header for delete column on edit users page
:del-user-fail "Could not delete user"
;; error message on failure to delete user
:del-user-success "Successfully deleted user"
;; confirmation message on deletion of user
:diff-title-prefix "Changes since version"
;; prefix for the header of the changes page
:edit-col-hdr "Edit" ;; header for edit column on edit users page
:edit-page-link "Edit this page"
;; text of the edit page link on the content frame
:edit-title-prefix "Edit" ;; prefix for title of edit content page
:edit-users-link "Edit users" ;; text of the edit users link on the menu
:edit-users-title "Select user to edit"
;; title of edit users page
:email-prompt "Email address" ;; text of the email widget prompt on edit user page
:file-upload-link-text "You may link to this file using a link of the form"
;; Text introducing the link to an uploaded file
:file-upload-prompt "File to upload" ;; prompt string for the file upload widget
:file-upload-title "Upload a file" ;; title for the file upload page
:is-admin-prompt "Is administrator?"
:home-link "Home" ;; text of the home link on the menu
:login-label "Log in!" ;; text of the login widget on the login page
:login-link "Log in" ;; text of the login link on the menu
:login-prompt "To edit this wiki"
;; text of the action widget prompt on the login page
:logout-label "Log out!" ;; text of the logout widget on the logout page
:logout-link "Log out" ;; text of the logout link on the menu
:logged-in-as "You are logged in as"
;; text of the 'logged in as' label on the menu
:history-link "History" ;; text of the history link on the content frame
:history-title-prefix "History of" ;; prefix of the title on the history page
:new-pass-prompt "New password" ;; text of the new password widget prompt on the change
;; password and edit user pages
:old-pass-prompt "Your password"
;; text of the old password widget prompt on the change
;; password page, and password widget on login page
:rpt-pass-prompt "And again" ;; text of the new password widget prompt on the change
;; password and edit user pages
:save-prompt "When you have finished editing"
;; text of the save widget label on edit content
;; and edit user page
:save-label "Save!" ;; text of the save widget itself
:save-user-fail "Failed to store user"
:save-user-success "Successfully stored user"
:username-prompt "Username" ;; text of the username widget prompt on edit user page
;; text of the is admin widget prompt on edit user page
:user-title-prefix "Edit user" ;; prefix for title of edit user page
:vers-col-hdr "Version" ;; header for the version column in history
:what-col-hdr "What" ;; header for the what column in history
:what-changed-prompt "What have you changed?"
;; text of the summary widget prompt on edit
;; content page
:when-col-hdr "When" ;; header for the when column in history
:your-uname-prompt "Your username" ;; text of the username widget prompt on the login page
}

View file

@ -1 +0,0 @@
en-GB.edn

View file

@ -1 +1 @@
{:admin {:admin true, :email "info@weft.scot", :password "admin"}}
{:admin {:admin true, :email "info@weft.scot", :password "admin"}, :simon {:email "simon@journeyman.cc", :admin true, :password "$s0$f0801$hIKgbtrGTP4Z7d0rQV7HyA==$PjSkX6m7qH1j3k5vI759KvApueEHMktBj9v/0sWiY0o="}}

View file

@ -1,55 +0,0 @@
## Prerequisites
You will need [Leiningen](https://github.com/technomancy/leiningen) 2.0 or above installed.
You will need [node](https://nodejs.org/en/) and [bower](https://bower.io/) installed.
## Running
To start a web server for the application during development, run:
lein bower install
lein ring server
To deploy Smeagol as a stand-alone application, compile it with:
lein bower install
lein uberjar
This will create a jar file in the `target` directory, named `smeagol-`*VERSION*`-standalone.jar`. You can run this file with:
java -jar smeagol-VERSION-standalone.jar
Test modification?
Alternatively, if you want to deploy to a servlet container (which I would strongly recommend), the simplest thing is to run:
lein bower install
lein ring uberwar
(a command which I'm sure Smeagol would entirely appreciate) and deploy the resulting war file.
## Experimental Docker image
You can now run Smeagol as a [Docker](http://www.docker.com) image. Read more about [[Using the Docker Image]].
To run my Docker image, use
docker run simonbrooke/smeagol
Smeagol will run, obviously, on the IP address of your Docker image, on port 8080. To find the IP address, start the image using the command above and then use
docker inspect --format '{{ .NetworkSettings.IPAddress }}' $(docker ps -q)
Suppose this prints '10.10.10.10', then the URL to browse to will be http://10.10.10.10:8080/smeagol/
This image is _experimental_, but it does seem to work fairly well. What it does **not** yet do, however, is push the git repository to a remote location, so when you tear the Docker image down your edits will be lost. My next objective for this image is for it to have a cammand line parameter being the git address of a repository from which it can initialise the Wiki content, and to which it will periodically push local changes to the Wiki content.
To build your own Docker image, run:
lein clean
lein bower install
lein ring uberwar
lein docker build
This will build a new Docker image locally; you can, obviously, push it to your own Docker repository if you wish.

View file

@ -6,21 +6,21 @@
<input type="hidden" name="redirect-to" value="{{redirect-to}}"/>
{% if user %}
<p class="widget">
<label for="submit">{{config.save-prompt}}</label>
<input name="action" id="action" type="submit" class="action-dangerous" value="{{config.logout-label}}"/>
<label for="submit">{% i18n save-prompt %}</label>
<input name="action" id="action" type="submit" class="action-dangerous" value="{% i18n logout-label %}"/>
</p>
{% else %}
<p class="widget">
<label for="username">{{config.your-uname-prompt}}</label>
<label for="username">{% i18n your-uname-prompt %}</label>
<input name="username" id="username" type="text" required/>
</p>
<p class="widget">
<label for="password">{{config.old-pass-prompt}}</label>
<label for="password">{% i18n old-pass-prompt %}</label>
<input name="password" id="password" type="password" required/>
</p>
<p class="widget">
<label for="submit">{{config.login-prompt}}</label>
<input name="action" id="action" type="submit" class="action" value="{{config.login-label}}"/>
<label for="submit">{% i18n login-prompt %}</label>
<input name="action" id="action" type="submit" class="action" value="{% i18n login-label %}"/>
</p>
{% endif %}
</form>

View file

@ -1,7 +1,7 @@
<!DOCTYPE html>
<html>
<head>
<title>{{config.site-title}}: {{title}}</title>
<title>{% i18n site-title %}: {{title}}</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
@ -15,19 +15,19 @@
<div id="nav">
<img id="nav-icon" src="{{servlet-context}}/img/three-lines.png" alt="Menu"/>
<menu id="nav-menu">
<li class="{{wiki-selected}}"><a href="{{servlet-context}}/">{{config.home-link}}</a></li>
<li class="{{wiki-selected}}"><a href="{{servlet-context}}/">{% i18n home-link %}</a></li>
{% if admin %}
<li class="{{edit-users-selected}}"><a href="{{servlet-context}}/edit-users">{{config.edit-users-link}}</a></li>
<li class="{{edit-users-selected}}"><a href="{{servlet-context}}/edit-users">{% i18n edit-users-link %}</a></li>
{% endif %}
{% if user %}
<li class="{{upload-selected}}"><a href="upload">{{config.file-upload-title}}</a></li>
<li class="{{passwd-selected}}"><a href="passwd">{{config.change-pass-link}}</a></li>
<li class="user" id="user">{{config.logged-in-as}} {{user}}</li>
<li class="{{upload-selected}}"><a href="upload">{% i18n file-upload-title %}</a></li>
<li class="{{passwd-selected}}"><a href="passwd">{% i18n change-pass-link %}</a></li>
<li class="user" id="user">{% i18n logged-in-as}} {{user}}</li>
<li class="{{auth-selected}}"><a href="{{servlet-context}}/auth">
{{config.logout-link}}</a></li>
{% i18n logout-link %}</a></li>
{% else %}
<li class="{{auth-selected}}"><a href="{{servlet-context}}/auth">
{{config.login-link}}</a></li>
{% i18n login-link %}</a></li>
{% endif %}
</menu>
<div id="phone-side-bar" class="wiki">
@ -45,7 +45,7 @@
</div>
</div>
</div>
<h1>{{config.site-title}}: {{title}}</h1>
<h1>{% i18n site-title %}: {{title}}</h1>
{{header|safe}}
{% if message %}
<div id="message">
@ -68,10 +68,10 @@
</div>
<div id="cookies">
<div id="more-about-cookies">
{{config.cookies-more}}
{% i18n cookies-more %}
</div>
<div id="about-cookies">
{{config.cookies-about}}
{% i18n cookies-about %}
</div>
</div>
<footer>

View file

@ -7,13 +7,13 @@
<input type="hidden" name="page" value="{{page}}"/>
<textarea name="src" id="src" rows="25" cols="80">{{content}}</textarea>
<p class="widget">
<label for="summary">{{config.what-changed-prompt}}</label>
<label for="summary">{% i18n what-changed-prompt %}</label>
<input name="summary" id="summary" type="text"
value="{%if exists%}{%else%}New file {{title}}{%endif%}" required/>
</p>
<p class="widget">
<label for="submit">{{config.save-prompt}}</label>
<input name="submit" id="submit" type="submit" class="action" value="{{config.save-label}}"/>
<label for="submit">{% i18n save-prompt %}</label>
<input name="submit" id="submit" type="submit" class="action" value="{% i18n save-label %}"/>
</p>
</form>
</div>

View file

@ -5,28 +5,28 @@
<form action="{{servlet-context}}/edit-user" method="POST">
{% csrf-field %}
<p class="widget">
<label for="target">{{config.username-prompt}}</label>
<label for="target">{% i18n username-prompt %}</label>
<input type="text" name="target" id="target" value="{{target}}" required {% ifunequal target "" %}disabled{% endifunequal %}/>
</p>
<p class="widget">
<label for="pass1">{{config.new-pass-prompt}}</label>
<label for="pass1">{% i18n new-pass-prompt %}</label>
<input name="pass1" id="pass1" type="password"/>
</p>
<p class="widget">
<label for="pass2">{{config.rpt-pass-prompt}}</label>
<label for="pass2">{% i18n rpt-pass-prompt %}</label>
<input name="pass2" id="pass2" type="password"/>
</p>
<p class="widget">
<label for="email">{{config.email-prompt}}</label>
<label for="email">{% i18n email-prompt %}</label>
<input name="email" id="email" type="text" value="{{details.email}}" required/>
</p>
<p class="widget">
<label for="admin">{{config.is-admin-prompt}}</label>
<label for="admin">{% i18n is-admin-prompt %}</label>
<input name="admin" id="admin" type="checkbox" {% if details.admin %}checked{% endif %}/>
</p>
<p class="widget">
<label for="submit">{{config.save-prompt}}</label>
<input name="submit" id="submit" type="submit" class="action" value="{{config.save-label}}"/>
<label for="submit">{% i18n save-prompt %}</label>
<input name="submit" id="submit" type="submit" class="action" value="{% i18n save-label}}"/>
</p>
</form>
</div>

View file

@ -4,17 +4,17 @@
<div id="content">
<table>
<tr>
<th/><th>{{config.edit-col-hdr}}</th><th>{{config.del-col-hdr}}</th>
<th/><th>{% i18n edit-col-hdr %}</th><th>{% i18n del-col-hdr %}</th>
</tr>
{% for user in users %}
<tr>
<td>{{user}}</td>
<td><a href="edit-user?target={{user}}">{{config.edit-col-hdr}} {{user}}</a></td>
<td><a href="delete-user?target={{user}}">{{config.del-col-hdr}} {{user}}</a></td>
<td><a href="edit-user?target={{user}}">{% i18n edit-col-hdr %} {{user}}</a></td>
<td><a href="delete-user?target={{user}}">{% i18n del-col-hdr %} {{user}}</a></td>
</tr>
{% endfor %}
<tr>
<td><a href="edit-user">{{config.add-user-label}}</a></td>
<td><a href="edit-user">{% i18n add-user-label %}</a></td>
<td></td>
<td></td>
</tr>

View file

@ -11,13 +11,13 @@
<input type="hidden" name="page" value="{{page}}"/>
<textarea name="src" id="src" rows="25" cols="80">{{content}}</textarea>
<p class="widget">
<label for="summary">{{config.what-changed-prompt}}</label>
<label for="summary">{% i18n what-changed-prompt %}</label>
<input name="summary" id="summary" type="text"
value="{%if exists%}{%else%}New file {{title}}{%endif%}" required/>
</p>
<p class="widget">
<label for="submit">{{config.save-prompt}}</label>
<input name="submit" id="submit" type="submit" class="action" value="{{config.save-label}}"/>
<label for="submit">{% i18n save-prompt %}</label>
<input name="submit" id="submit" type="submit" class="action" value="{% i18n save-label %}"/>
</p>
</form>
</div>

View file

@ -3,10 +3,10 @@
<div id="content" class="history">
<table class="music-ruled">
<tr>
<th>{{config.when-col-hdr}}</th>
<th>{{config.what-col-hdr}}</th>
<th>{{config.vers-col-hdr}}</th>
<th>{{config.change-col-hdr}}</th>
<th>{% i18n when-col-hdr %}</th>
<th>{% i18n what-col-hdr %}</th>
<th>{% i18n vers-col-hdr %}</th>
<th>{% i18n change-col-hdr %}</th>
</tr>
{% for entry in history %}
<tr>

View file

@ -4,20 +4,20 @@
<form action="{{servlet-context}}/passwd" method="POST">
{% csrf-field %}
<p class="widget">
<label for="oldpass">{{config.old-pass-prompt}}</label>
<label for="oldpass">{% i18n old-pass-prompt %}</label>
<input name="oldpass" id="oldpass" type="password" required/>
</p>
<p class="widget">
<label for="pass1">{{config.new-pass-prompt}}</label>
<label for="pass1">{% i18n new-pass-prompt %}</label>
<input name="pass1" id="pass1" type="password" required/>
</p>
<p class="widget">
<label for="pass2">{{config.rpt-pass-prompt}}</label>
<label for="pass2">{% i18n rpt-pass-prompt %}</label>
<input name="pass2" id="pass2" type="password" required/>
</p>
<p class="widget">
<label for="submit">{{config.change-pass-prompt}}</label>
<input name="action" id="action" type="submit" class="action" value="{{config.change-pass-link}}!"/>
<label for="submit">{% i18n change-pass-prompt %}</label>
<input name="action" id="action" type="submit" class="action" value="{% i18n change-pass-link %}!"/>
</p>
</form>
</div>

View file

@ -6,13 +6,13 @@
<img alt="Uploaded image" src="uploads/{{uploaded}}"/>
<p>
{{config.file-upload-link-text}}:
{% i18n file-upload-link-text %}:
<code>![Uploaded image](uploads/{{uploaded}})</code>
</p>
{% else %}
<p>
{{config.file-upload-link-text}}:
{% i18n file-upload-link-text %}:
<code>[Uploaded file](uploads/{{uploaded}})</code>
</p>
@ -21,12 +21,12 @@
<form action="{{servlet-context}}/upload" enctype="multipart/form-data" method="POST">
{% csrf-field %}
<p class="widget">
<label for="upload">{{config.file-upload-prompt}}</label>
<label for="upload">{% i18n file-upload-prompt %}</label>
<input name="upload" id="upload" type="file" required/>
</p>
<p class="widget">
<label for="submit">{{config.save-prompt}}</label>
<input name="submit" id="submit" type="submit" class="action" value="{{config.save-label}}"/>
<label for="submit">{% i18n save-prompt %}</label>
<input name="submit" id="submit" type="submit" class="action" value="{% i18n save-label %}"/>
</p>
</form>
{% endif %}

View file

@ -16,8 +16,8 @@
<div id="content" class="wiki">
{% if editable %}
<ul class="minor-controls">
<li><a href="{{servlet-context}}/edit?page={{title}}">{{config.edit-page-link}}</a></li>
<li><a href="history?page={{page}}">{{config.history-link}}</a></li>
<li><a href="{{servlet-context}}/edit?page={{title}}">{% i18n edit-page-link %}</a></li>
<li><a href="history?page={{page}}">{% i18n history-link %}</a></li>
</ul>
{% endif %}
{{content|safe}}

View file

@ -34,8 +34,8 @@
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; the relative path to the password file.
(def password-file-path (str (io/resource-path) "../passwd"))
;; (def password-file-path (str (io/resource-path) "../passwd"))
(def password-file-path (str (clojure.java.io/resource "passwd")))
(defn- get-users
"Get the whole content of the password file as a clojure map"

View file

@ -2,13 +2,14 @@
(ns ^{:doc "Render a page as HTML."
:author "Simon Brooke"}
smeagol.layout
(:require [selmer.parser :as parser]
[clojure.string :as s]
[ring.util.anti-forgery :refer [anti-forgery-field]]
[ring.util.response :refer [content-type response]]
(:require [clojure.string :as s]
[compojure.response :refer [Renderable]]
[environ.core :refer [env]]
[smeagol.util :as util]))
[ring.util.anti-forgery :refer [anti-forgery-field]]
[ring.util.response :refer [content-type response]]
[selmer.parser :as parser]
[smeagol.util :as util]
[taoensso.timbre :as timbre]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;
@ -37,6 +38,16 @@
(parser/add-tag! :csrf-field (fn [_ _] (anti-forgery-field)))
;; Attempt to do internationalisation more neatly
;; This tag takes two arguments, the first is a key, the (optional) second is a
;; default. The key is looked up in the i18n
(parser/add-tag! :i18n
(fn [args context-map]
(let [messages (:i18n context-map)
default (or (second args) (first args))]
(timbre/info (str "i18n: key is " (first args) " messages map is " messages))
(if (map? messages) (get messages (keyword (first args)) default) default))))
(deftype RenderableTemplate [template params]
Renderable
@ -44,7 +55,7 @@
(content-type
(->> (assoc params
(keyword (s/replace template #".html" "-selected")) "active"
:config (util/get-messages request)
:i18n (util/get-messages request)
:dev (env :dev)
:servlet-context
(if-let [context (:servlet-context request)]

View file

@ -70,15 +70,17 @@
message (if stored (str (:save-user-success (util/get-messages request)) " " target "."))
error (if (and (:email params) (not stored))
(str (:save-user-fail (util/get-messages request)) " " target "."))
page (if stored "edit-users.html" "edit-user.html")
details (auth/fetch-user-details target)]
(if message
(timbre/info message))
(if error
(timbre/warn error))
(layout/render "edit-user.html"
(layout/render page
(merge (util/standard-params request)
{:title (str (:edit-title-prefix (util/get-messages request)) " " target)
:message message
:error error
:target target
:details details}))))
:details details
:users (auth/list-users)}))))

View file

@ -114,7 +114,7 @@
"Render the markdown page specified in this `request`, if any. If none found, redirect to edit-page"
[request]
(let [params (keywordize-keys (:params request))
page (or (:page params) (util/get-message :default-page-title request))
page (or (:page params) (util/get-message :default-page-title "Introduction" request))
file-name (str page ".md")
file-path (cjio/file util/content-dir file-name)
exists? (.exists (clojure.java.io/as-file file-path))]

View file

@ -58,7 +58,7 @@
(merge
(i18n/get-messages
((:headers request) "accept-language")
(str (clojure.java.io/resource "i18n/"))
(cjio/file (io/resource-path) "i18n")
"en-GB")
config))
@ -67,10 +67,14 @@
(defn get-message
[message-key request]
"Return the message with this `message-key` from this `request`.
if not found, return this `default`, if provided; else return the
`message-key`."
([message-key request]
(get-message message-key message-key request))
([message-key default request]
(let [messages (get-messages request)]
(if
(map? messages)
(or (messages message-key) message-key)
message-key)))
(or (messages message-key) default)
default))))