Archived
Template
1
0

Merge pull request #16 from uberswe/multilanguage

Added multilanguage support
This commit is contained in:
Markus Tenghamn
2022-01-29 10:27:19 +01:00
committed by GitHub
49 changed files with 2402 additions and 154 deletions

1
.gitignore vendored
View File

@ -1,3 +1,4 @@
.idea .idea
node_modules node_modules
docker-compose.local.yml docker-compose.local.yml
translate.*.toml

View File

@ -12,6 +12,9 @@ RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o "main" -ldflags="-w -s" ./
FROM scratch FROM scratch
WORKDIR /app
COPY --from=builder /app/active.en.toml /app/active.en.toml
COPY --from=builder /app/active.sv.toml /app/active.sv.toml
COPY --from=builder /app/main /usr/bin/ COPY --from=builder /app/main /usr/bin/
COPY --from=builder /etc/ssl/certs/ /etc/ssl/certs/ COPY --from=builder /etc/ssl/certs/ /etc/ssl/certs/

View File

@ -164,6 +164,12 @@ There is a workflow to deploy to my personal server whenever there is a merge to
I use [supervisor](http://supervisord.org/) with [docker-compose](https://docs.docker.com/compose/production/) to run my containers. [Caddy](https://caddyserver.com/) handles the SSL configuration and routing. I use [Ansible](https://docs.ansible.com/ansible/latest/user_guide/playbooks.html) to manage my configurations. I use [supervisor](http://supervisord.org/) with [docker-compose](https://docs.docker.com/compose/production/) to run my containers. [Caddy](https://caddyserver.com/) handles the SSL configuration and routing. I use [Ansible](https://docs.ansible.com/ansible/latest/user_guide/playbooks.html) to manage my configurations.
## Translations
This project uses [go-i18n](https://github.com/nicksnyder/go-i18n) to handle translations. Only English and Swedish is currently supported, but I would gladly add more languages if someone would like to contribute.
To update languages first run `goi18n extract` to update `active.en.toml`. Then run `goi18n merge active.*.toml` to generate `translate.*.toml` which can then be translated. Finally, run `goi18n merge active.*.toml translate.*.toml` to merge the translated files into the active files.
## Documentation ## Documentation
See [GoDoc](https://godoc.org/github.com/uberswe/golang-base-project) for further documentation. See [GoDoc](https://godoc.org/github.com/uberswe/golang-base-project) for further documentation.

51
active.en.toml Normal file
View File

@ -0,0 +1,51 @@
404_message_1 = "The page you're looking for could not be found."
404_message_2 = "to return to the main page."
404_not_found = "404 Not Found"
activate = "Activate"
activation_success = "Account activated. You may now proceed to login to your account."
activation_validation_token = "Please provide a valid activation token"
admin = "Admin"
admin_dashboard = "Admin Dashboard"
click_here = "Click here"
created_by = "Created by"
dashboard_message = "You now have an authenticated session, feel free to log out using the link in the navbar above."
email_address = "Email address"
footer_message_1 = "Fork this project on"
forgot_password = "Forgot password?"
forgot_password_message = "Use the form below to reset your password. If we have an account with your email you will receive instructions on how to reset your password."
forgot_password_success = "An email with instructions describing how to reset your password has been sent."
home = "Home"
index_message_1 = "A simple website with user login and registration."
index_message_2 = "The frontend uses"
index_message_3 = "and the backend is written in"
index_message_4 = "Read more about this project on"
lang_key = "en"
login = "Login"
login_activated_error = "Account is not activated yet."
login_error = "Could not login, please make sure that you have typed in the correct email and password. If you have forgotten your password, please click the forgot password link below."
login_terms = "By pressing the button below to login you agree to the use of cookies on this website."
logout = "Logout"
no_results_found = "No results found"
password = "Password"
password_error = "Your password must be 8 characters in length or longer"
password_reset = "Password Reset"
password_reset_email = "Use the following link to reset your password. If this was not requested by you, please ignore this email.\n%s"
password_reset_success = "Your password has successfully been reset."
register = "Register"
register_error = "Could not register, please make sure the details you have provided are correct and that you do not already have an existing account."
register_success = "Thank you for registering. An activation email has been sent with steps describing how to activate your account."
request_activation_email = "Request activation email"
request_new_activation_email = "Request a new activation email"
request_reset_email = "Request reset email"
resend_activation_email = "Resend Activation Email"
resend_activation_email_message = "If you have already registered but never activated your account you can use the form below to request a new activation email."
resend_activation_email_subject = "Resend Activation Email"
resend_activation_email_success = "A new activation email has been sent if the account exists and is not already activated. Please remember to check your spam inbox in case the email is not showing in your inbox."
reset_password = "Reset Password"
reset_password_error = "Could not reset password, please try again"
reset_password_message = "Please enter a new password."
search = "Search"
search_results = "Search Results"
site_name = "Golang Base Project"
user_activation = "User Activation"
user_activation_email = "Use the following link to activate your account. If this was not requested by you, please ignore this email.\n%s"

203
active.sv.toml Normal file
View File

@ -0,0 +1,203 @@
[404_message_1]
hash = "sha1-bcadd04af7cf1fa64f31f38899045f82685d55a7"
other = "Sidan du letar efter kunde inte hittas."
[404_message_2]
hash = "sha1-5805972e75bdf639b44d9deb0460971aa4b3525b"
other = "för att återgå till huvudsidan."
[404_not_found]
hash = "sha1-5b65037351caeb0e5a48d963d7ffa88d0271d546"
other = "404 Sidan Finns Inte"
[activate]
hash = "sha1-92ef08325a4813563a3110359906076374683282"
other = "Aktivera"
[activation_success]
hash = "sha1-97cd06296a78166b20d8fab740f0b699c3a90b5e"
other = "Kontot aktiverat. Du kan nu fortsätta att logga in på ditt konto."
[activation_validation_token]
hash = "sha1-1f11e2f3e762728845f9adccc50758470bca3418"
other = "Ange en giltig aktiveringstoken"
[admin]
hash = "sha1-4e7afebcfbae000b22c7c85e5560f89a2a0280b4"
other = "Admin"
[admin_dashboard]
hash = "sha1-9f1362cde54e66a589837b63e41769eeeca76388"
other = "Admin Dashboard"
[click_here]
hash = "sha1-0049f8894e41937ebb9111cd3def6749049fb50f"
other = "Klicka här"
[created_by]
hash = "sha1-5d73cc30510c739ed68c572c5199e106d325b648"
other = "Skapad av"
[dashboard_message]
hash = "sha1-cd2bf2ee8212e8af2ba8d2b47153c7ca383adf80"
other = "Du har nu en autentiserad session, du kan logga ut med länken i navigeringsfältet ovan."
[email_address]
hash = "sha1-c94d3175a6560565410511df2cebab9cda96027e"
other = "E-postadress"
[footer_message_1]
hash = "sha1-14d277545460f1796542547a5cf2151fc433f917"
other = "Skapa en fork av detta projekt på"
[forgot_password]
hash = "sha1-4c29f7f0335807c2524d8c36d531496aee23f473"
other = "Glömt ditt lösenord?"
[forgot_password_message]
hash = "sha1-e9ae7548f4477dc0f04e78ac836393242f0682a6"
other = "Använd formuläret nedan för att återställa ditt lösenord. Om vi har ett konto med din e-post kommer du att få instruktioner om hur du återställer ditt lösenord."
[forgot_password_success]
hash = "sha1-d25d119c050b6ac501c231415759e5ec3a72de9b"
other = "Ett e-postmeddelande med instruktioner som beskriver hur du återställer ditt lösenord har skickats."
[home]
hash = "sha1-70f8bb9a8a5393ef080507a89e4b98d139000d65"
other = "Hem"
[index_message_1]
hash = "sha1-dc5bb22d1389141a7db9916410bc6d88db00c339"
other = "En enkel webbplats med användarinloggning och registrering."
[index_message_2]
hash = "sha1-506b20db7cd3410ef335d47d623d3b868333570b"
other = "Till vår frontend används"
[index_message_3]
hash = "sha1-35b4b7fe61e07add7f32d4d636120ba28a107da3"
other = "och vår backend använder"
[index_message_4]
hash = "sha1-ca5f9ad5b945d7e0e9b4554e5647d8d8038fdb21"
other = "Läs mer om detta projekt på"
[lang_key]
hash = "sha1-094b0fe0e302854af1311afab85b5203ba457a3b"
other = "sv"
[login]
hash = "sha1-4e5a2893bdcc7d239c1db72e4c4ffbe4bea73174"
other = "Logga in"
[login_activated_error]
hash = "sha1-25e7e4d45cbacbbf3ca6dbef0c4541d1e5a29a94"
other = "Kontot är inte aktiverat ännu."
[login_error]
hash = "sha1-63818d94ab9bded7e8c2f4785e50a7f5893f142e"
other = "Kunde inte logga in, se till att du har skrivit in rätt e-postadress och lösenord. Om du har glömt ditt lösenord, klicka på länken 'glömt ditt lösenord?' nedan."
[login_terms]
hash = "sha1-ea0e769c166cd14f9bca7d9c7acfb6b3821e05bc"
other = "Genom att trycka på knappen nedan för att logga in godkänner du användningen av cookies på denna webbplats."
[logout]
hash = "sha1-e43d612e11f1568f2373e719d4c4b08dcecdc7cc"
other = "Logga ut"
[no_results_found]
hash = "sha1-658e79f9dc7fca34dc164cbb79e1c0be3cdebf23"
other = "Inga resultat hittades"
[password]
hash = "sha1-8be3c943b1609fffbfc51aad666d0a04adf83c9d"
other = "Lösenord"
[password_error]
hash = "sha1-1ee13120caefba4ef187011d65cff5a61da5e5df"
other = "Ditt lösenord måste vara 8 tecken långt eller längre"
[password_reset]
hash = "sha1-79167df1dd0bc2f673932f531fce1d7b36b8be21"
other = "Lösenordsåterställning"
[password_reset_email]
hash = "sha1-844372e0b366c2d90720b680e27aee05d4f1ea3b"
other = "Använd följande länk för att återställa ditt lösenord. Om detta inte begärdes av dig, ignorera detta e-postmeddelande.\n%s"
[password_reset_success]
hash = "sha1-e9d5c887a57a274b7b839b8109625c324f3d6536"
other = "Ditt lösenord har återställts."
[register]
hash = "sha1-d672995a14650d0e018026b64f297663d8c71c8d"
other = "Registrera"
[register_error]
hash = "sha1-cb20bd219572313ae8b6b2a03b816a13f43c907c"
other = "Det gick inte att genomföra registreringen, se till att uppgifterna du har angett är korrekta och att du inte redan har ett befintligt konto."
[register_success]
hash = "sha1-300d4e738bd6bf5c14a303c6c84f36a1bbf2132f"
other = "Tack för din registrering. Ett aktiveringsmail har skickats med steg som beskriver hur du aktiverar ditt konto."
[request_activation_email]
hash = "sha1-8c63a65400fb703de388b9b17b308b62f7477b16"
other = "Begär aktiveringsmail"
[request_new_activation_email]
hash = "sha1-fc2463c91df0c8d36687a82096815486578eb3e4"
other = "Begär ett nytt aktiveringsmail"
[request_reset_email]
hash = "sha1-2a2c3609527fa9a048ae9f66b12597c7fa10379c"
other = "Begär återställningsmail"
[resend_activation_email]
hash = "sha1-2a765254a16b8d77c89125cabc42996f28dac3e6"
other = "Skicka aktiveringsmail igen"
[resend_activation_email_message]
hash = "sha1-1b1134c510f756f97e23dfb6c2eef4ec1cfdbcc6"
other = "Om du redan har registrerat dig men aldrig aktiverat ditt konto kan du använda formuläret nedan för att begära ett nytt aktiveringsmail."
[resend_activation_email_subject]
hash = "sha1-2a765254a16b8d77c89125cabc42996f28dac3e6"
other = "Skicka aktiveringsmail igen"
[resend_activation_email_success]
hash = "sha1-53b66e75cf183ff99c2253a23c47dc3501dc2669"
other = "Ett nytt aktiveringsmail har skickats om kontot finns och inte redan är aktiverat. Kom ihåg att kontrollera din skräppost om e-postmeddelandet inte visas i din inkorg."
[reset_password]
hash = "sha1-3fb75e3bfe4de94eb5198656fa9de95352dab915"
other = "Återställ lösenord"
[reset_password_error]
hash = "sha1-5affbbb3d71cd9502970b1422824b63c46d35ac9"
other = "Det gick inte att återställa lösenordet, försök igen"
[reset_password_message]
hash = "sha1-9dcea7196a4837caabeec6ff42187ac2e06ecfe0"
other = "Vänligen ange ett nytt lösenord."
[search]
hash = "sha1-bce06414177f72ab70e6387b6af9f8ceef0d6049"
other = "Sök"
[search_results]
hash = "sha1-5e054413dacd642ddacc8f8ec146442bea000086"
other = "Sökresultat"
[site_name]
hash = "sha1-ffe1d232b4c4a3aaa1070a9c1fb4bf5cf0ea650d"
other = "Golang Base Project"
[user_activation]
hash = "sha1-065b4495daa8deaa8b7faad2c855f786bdb9e8ee"
other = "Användaraktivering"
[user_activation_email]
hash = "sha1-66f145eb134e23445d22ae815c3415a1849baf3e"
other = "Använd följande länk för att aktivera ditt konto. Om detta inte begärdes av dig, ignorera detta e-postmeddelande.\n%s"

View File

@ -2,8 +2,8 @@
<main class="flex-shrink-0"> <main class="flex-shrink-0">
<div class="container"> <div class="container">
<h1>404 Not Found</h1> <h1>{{ call .Trans "404 Not Found" }}</h1>
<p>The page you're looking for could not be found. <a href="/">Click here</a> to return to the main page.</p> <p>{{ call .Trans "The page you're looking for could not be found." }} <a href="/">{{ call .Trans "Click here" }}</a> {{ call .Trans "to return to the main page." }}</p>
</div> </div>
</main> </main>

View File

@ -2,8 +2,8 @@
<main class="flex-shrink-0"> <main class="flex-shrink-0">
<div class="container"> <div class="container">
<h1 class="mt-5">Admin Dashboard</h1> <h1 class="mt-5">{{ call .Trans "Admin Dashboard" }}</h1>
<p>You now have an authenticated session, feel free to log out using the link in the navbar above.</p> <p>{{ call .Trans "You now have an authenticated session, feel free to log out using the link in the navbar above." }}</p>
</div> </div>
</main> </main>

View File

@ -1,6 +1,6 @@
<footer class="footer mt-auto py-3 bg-light"> <footer class="footer mt-auto py-3 bg-light">
<div class="container"> <div class="container">
<span class="text-muted">Fork this project on <a href="https://github.com/uberswe/golang-base-project">GitHub</a> | Created by <a href="https://github.com/uberswe">Markus Tenghamn</a></span> <span class="text-muted">{{ call .Trans "Fork this project on" }} <a href="https://github.com/uberswe/golang-base-project">GitHub</a> | {{ call .Trans "Created by" }} <a href="https://github.com/uberswe">Markus Tenghamn</a></span>
</div> </div>
</footer> </footer>
<script src="/assets/js/main.js?c={{ .CacheParameter }}"></script> <script src="/assets/js/main.js?c={{ .CacheParameter }}"></script>

View File

@ -1,18 +1,18 @@
{{ template "header.html" . }} {{ template "header.html" . }}
<main class="form-signin"> <main class="form-signin">
<form method="post" action="/user/password/forgot"> <form method="post" action="/user/password/forgot">
<h1 class="h3 mb-3 fw-normal">Forgot password?</h1> <h1 class="h3 mb-3 fw-normal">{{ call .Trans "Forgot password?" }}</h1>
{{ template "messages.html" . }} {{ template "messages.html" . }}
<p>Use the form below to reset your password. If we have an account with your email you will receive instructions on how to reset your passsword.</p> <p>{{ call .Trans "Use the form below to reset your password. If we have an account with your email you will receive instructions on how to reset your passsword." }}</p>
<div class="form-floating"> <div class="form-floating">
<input name="email" type="email" class="form-control" id="floatingInput" placeholder="name@example.com"> <input name="email" type="email" class="form-control" id="floatingInput" placeholder="name@example.com">
<label for="floatingInput">Email address</label> <label for="floatingInput">{{ call .Trans "Email address" }}</label>
</div> </div>
<button class="w-100 btn btn-lg btn-primary" type="submit">Request reset email</button> <button class="w-100 btn btn-lg btn-primary" type="submit">{{ call .Trans "Request reset email" }}</button>
</form> </form>
</main> </main>
{{ template "footer.html" . }} {{ template "footer.html" . }}

View File

@ -1,11 +1,11 @@
<!doctype html> <!doctype html>
<html lang="en" class="h-100"> <html lang="{{ call .Trans "en" }}" class="h-100">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content=""> <meta name="description" content="">
<meta name="author" content="Markus Tenghamn"> <meta name="author" content="Markus Tenghamn">
<title>{{ .Title }} - Golang Base Project</title> <title>{{ .Title }} - {{ call .Trans "site_name" }}</title>
<link rel="canonical" href="https://golangbase.com"> <link rel="canonical" href="https://golangbase.com">

View File

@ -3,7 +3,7 @@
<header> <header>
<nav class="navbar navbar-expand-md navbar-dark fixed-top bg-dark"> <nav class="navbar navbar-expand-md navbar-dark fixed-top bg-dark">
<div class="container-fluid"> <div class="container-fluid">
<a class="navbar-brand" href="/">Golang Base Project</a> <a class="navbar-brand" href="/">{{ call .Trans "site_name" }}</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarCollapse" <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarCollapse"
aria-controls="navbarCollapse" aria-expanded="false" aria-label="Toggle navigation"> aria-controls="navbarCollapse" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span> <span class="navbar-toggler-icon"></span>
@ -12,31 +12,31 @@
{{ if .IsAuthenticated }} {{ if .IsAuthenticated }}
<ul class="navbar-nav me-auto mb-2 mb-md-0"> <ul class="navbar-nav me-auto mb-2 mb-md-0">
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" href="/">Home</a> <a class="nav-link" href="/">{{ call .Trans "Home" }}</a>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" href="/admin">Admin</a> <a class="nav-link" href="/admin">{{ call .Trans "Admin" }}</a>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" href="/logout">Logout</a> <a class="nav-link" href="/logout">{{ call .Trans "Logout" }}</a>
</li> </li>
</ul> </ul>
{{ else }} {{ else }}
<ul class="navbar-nav me-auto mb-2 mb-md-0"> <ul class="navbar-nav me-auto mb-2 mb-md-0">
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" href="/">Home</a> <a class="nav-link" href="/">{{ call .Trans "Home" }}</a>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" href="/login">Login</a> <a class="nav-link" href="/login">{{ call .Trans "Login" }}</a>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" href="/register">Register</a> <a class="nav-link" href="/register">{{ call .Trans "Register" }}</a>
</li> </li>
</ul> </ul>
{{ end }} {{ end }}
<form method="post" action="/search" class="d-flex"> <form method="post" action="/search" class="d-flex">
<input name="search" class="form-control me-2" type="search" placeholder="Search" aria-label="Search"> <input name="search" class="form-control me-2" type="search" placeholder="{{ call .Trans "Search" }}" aria-label="Search">
<button class="btn btn-outline-success" type="submit">Search</button> <button class="btn btn-outline-success" type="submit">{{ call .Trans "Search" }}</button>
</form> </form>
</div> </div>
</div> </div>

View File

@ -2,9 +2,9 @@
<main class="flex-shrink-0"> <main class="flex-shrink-0">
<div class="container"> <div class="container">
<h1 class="mt-5">Golang Base Project</h1> <h1 class="mt-5">{{ call .Trans "site_name" }}</h1>
<p class="lead">A simple website with user login and registration. The frontend uses <a href="https://getbootstrap.com/docs/5.0/getting-started/introduction/">Bootstrap 5</a> and the backend is written in <a href="https://go.dev/">Go</a>.</p> <p class="lead">{{ call .Trans "A simple website with user login and registration." }} {{ call .Trans "The frontend uses" }} <a href="https://getbootstrap.com/docs/5.0/getting-started/introduction/">Bootstrap 5</a> {{ call .Trans "and the backend is written in" }} <a href="https://go.dev/">Go</a>.</p>
<p>Read more about this project on <a href="https://github.com/uberswe/golang-base-project">GitHub</a>.</p> <p>{{ call .Trans "Read more about this project on" }} <a href="https://github.com/uberswe/golang-base-project">GitHub</a>.</p>
</div> </div>
</main> </main>

View File

@ -1,24 +1,24 @@
{{ template "header.html" . }} {{ template "header.html" . }}
<main class="form-signin"> <main class="form-signin">
<form method="post" action="/login"> <form method="post" action="/login">
<h1 class="h3 mb-3 fw-normal">Login</h1> <h1 class="h3 mb-3 fw-normal">{{ call .Trans "Login" }}</h1>
{{ template "messages.html" . }} {{ template "messages.html" . }}
<div class="form-floating"> <div class="form-floating">
<input name="email" type="email" class="form-control" id="floatingInput" placeholder="name@example.com"> <input name="email" type="email" class="form-control" id="floatingInput" placeholder="name@example.com">
<label for="floatingInput">Email address</label> <label for="floatingInput">{{ call .Trans "Email address" }}</label>
</div> </div>
<div class="form-floating"> <div class="form-floating">
<input name="password" type="password" class="form-control" id="floatingPassword" placeholder="Password"> <input name="password" type="password" class="form-control" id="floatingPassword" placeholder="{{ call .Trans "Password" }}">
<label for="floatingPassword">Password</label> <label for="floatingPassword">{{ call .Trans "Password" }}</label>
</div> </div>
<div class="mb-3"> <div class="mb-3">
<p>By pressing the button below to login you agree to the use of cookies on this website.</p> <p>{{ call .Trans "By pressing the button below to login you agree to the use of cookies on this website." }}</p>
</div> </div>
<button class="w-100 btn btn-lg btn-primary" type="submit">Login</button> <button class="w-100 btn btn-lg btn-primary" type="submit">{{ call .Trans "Login" }}</button>
<p class="mt-5 mb-3 text-muted"><a href="/user/password/forgot">Forgot password?</a></p> <p class="mt-5 mb-3 text-muted"><a href="/user/password/forgot">{{ call .Trans "Forgot password?" }}</a></p>
</form> </form>
</main> </main>
{{ template "footer.html" . }} {{ template "footer.html" . }}

View File

@ -1,22 +1,22 @@
{{ template "header.html" . }} {{ template "header.html" . }}
<main class="form-signin"> <main class="form-signin">
<form method="post" action="/register"> <form method="post" action="/register">
<h1 class="h3 mb-3 fw-normal">Register</h1> <h1 class="h3 mb-3 fw-normal">{{ call .Trans "Register" }}</h1>
{{ template "messages.html" . }} {{ template "messages.html" . }}
<div class="form-floating"> <div class="form-floating">
<input name="email" type="email" class="form-control" id="floatingInput" placeholder="name@example.com"> <input name="email" type="email" class="form-control" id="floatingInput" placeholder="name@example.com">
<label for="floatingInput">Email address</label> <label for="floatingInput">{{ call .Trans "Email address" }}</label>
</div> </div>
<div class="form-floating"> <div class="form-floating">
<input name="password" type="password" class="form-control" id="floatingPassword" placeholder="Password"> <input name="password" type="password" class="form-control" id="floatingPassword" placeholder="{{ call .Trans "Password" }}">
<label for="floatingPassword">Password</label> <label for="floatingPassword">{{ call .Trans "Password" }}</label>
</div> </div>
<button class="w-100 btn btn-lg btn-primary" type="submit">Register</button> <button class="w-100 btn btn-lg btn-primary" type="submit">{{ call .Trans "Register" }}</button>
<p class="mt-5 mb-3 text-muted"><a href="/activate/resend">Request a new activation email</a></p> <p class="mt-5 mb-3 text-muted"><a href="/activate/resend">{{ call .Trans "Request a new activation email" }}</a></p>
</form> </form>
</main> </main>
{{ template "footer.html" . }} {{ template "footer.html" . }}

View File

@ -1,18 +1,18 @@
{{ template "header.html" . }} {{ template "header.html" . }}
<main class="form-signin"> <main class="form-signin">
<form method="post" action="/activate/resend"> <form method="post" action="/activate/resend">
<h1 class="h3 mb-3 fw-normal">Resend Activation Email</h1> <h1 class="h3 mb-3 fw-normal">{{ call .Trans "Resend Activation Email" }}</h1>
{{ template "messages.html" . }} {{ template "messages.html" . }}
<p>If you have already registered but never activated your account you can use the form below to request a new activation email.</p> <p>{{ call .Trans "If you have already registered but never activated your account you can use the form below to request a new activation email." }}</p>
<div class="form-floating"> <div class="form-floating">
<input type="email" class="form-control" id="floatingInput" placeholder="name@example.com"> <input type="email" class="form-control" id="floatingInput" placeholder="name@example.com">
<label for="floatingInput">Email address</label> <label for="floatingInput">{{ call .Trans "Email address" }}</label>
</div> </div>
<button class="w-100 btn btn-lg btn-primary" type="submit">Request activation email</button> <button class="w-100 btn btn-lg btn-primary" type="submit">{{ call .Trans "Request activation email" }}</button>
</form> </form>
</main> </main>
{{ template "footer.html" . }} {{ template "footer.html" . }}

View File

@ -1,18 +1,18 @@
{{ template "header.html" . }} {{ template "header.html" . }}
<main class="form-signin"> <main class="form-signin">
<form method="post" action="/user/password/reset/{{ .Token }}"> <form method="post" action="/user/password/reset/{{ .Token }}">
<h1 class="h3 mb-3 fw-normal">Reset password</h1> <h1 class="h3 mb-3 fw-normal">{{ call .Trans "Reset password" }}</h1>
{{ template "messages.html" . }} {{ template "messages.html" . }}
<p>Please enter a new password.</p> <p>{{ call .Trans "Please enter a new password." }}</p>
<div class="form-floating"> <div class="form-floating">
<input name="password" type="password" class="form-control" id="floatingPassword" placeholder="Password"> <input name="password" type="password" class="form-control" id="floatingPassword" placeholder="{{ call .Trans "Password" }}">
<label for="floatingPassword">Password</label> <label for="floatingPassword">{{ call .Trans "Password" }}</label>
</div> </div>
<button class="w-100 btn btn-lg btn-primary" type="submit">Reset password</button> <button class="w-100 btn btn-lg btn-primary" type="submit">{{ call .Trans "Reset password" }}</button>
</form> </form>
</main> </main>
{{ template "footer.html" . }} {{ template "footer.html" . }}

View File

@ -2,7 +2,7 @@
<main class="flex-shrink-0"> <main class="flex-shrink-0">
<div class="container"> <div class="container">
<h1 class="mt-5">Search Results</h1> <h1 class="mt-5">{{ call .Trans "Search Results" }}</h1>
{{ template "messages.html" . }} {{ template "messages.html" . }}
{{ range $result := .Results }} {{ range $result := .Results }}
<div class="search-result"> <div class="search-result">

3
go.mod
View File

@ -3,15 +3,18 @@ module github.com/uberswe/golang-base-project
go 1.16 go 1.16
require ( require (
github.com/BurntSushi/toml v0.3.1
github.com/gin-contrib/sessions v0.0.4 github.com/gin-contrib/sessions v0.0.4
github.com/gin-gonic/gin v1.7.7 github.com/gin-gonic/gin v1.7.7
github.com/go-playground/validator/v10 v10.4.1 github.com/go-playground/validator/v10 v10.4.1
github.com/gorilla/securecookie v1.1.1 github.com/gorilla/securecookie v1.1.1
github.com/kr/text v0.2.0 // indirect github.com/kr/text v0.2.0 // indirect
github.com/nicksnyder/go-i18n/v2 v2.1.2
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect
github.com/oklog/ulid/v2 v2.0.2 github.com/oklog/ulid/v2 v2.0.2
github.com/ulule/limiter/v3 v3.9.0 github.com/ulule/limiter/v3 v3.9.0
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 golang.org/x/crypto v0.0.0-20210921155107-089bfa567519
golang.org/x/text v0.3.7
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 // indirect gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 // indirect
gorm.io/driver/mysql v1.2.1 gorm.io/driver/mysql v1.2.1

3
go.sum
View File

@ -1,3 +1,4 @@
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030IGemrRc= github.com/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030IGemrRc=
github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
@ -161,6 +162,8 @@ github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OH
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/nicksnyder/go-i18n/v2 v2.1.2 h1:QHYxcUJnGHBaq7XbvgunmZ2Pn0focXFqTD61CkH146c=
github.com/nicksnyder/go-i18n/v2 v2.1.2/go.mod h1:d++QJC9ZVf7pa48qrsRWhMJ5pSHIPmS3OLqK1niyLxs=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=

39
lang/main.go Normal file
View File

@ -0,0 +1,39 @@
package lang
import (
"github.com/gin-gonic/gin"
"github.com/nicksnyder/go-i18n/v2/i18n"
)
type Service struct {
bundle *i18n.Bundle
ctx *gin.Context
localizer *i18n.Localizer
}
func New(ctx *gin.Context, bundle *i18n.Bundle) Service {
localizer := i18n.NewLocalizer(bundle, ctx.Request.Header.Get("Accept-Language"), "en")
return Service{
bundle: bundle,
ctx: ctx,
localizer: localizer,
}
}
func (s *Service) Trans(str string) string {
// TODO, modify this to handle plural and more types of phrases
for _, m := range translationMessages {
if m.ID == str {
localizedString, _ := s.localizer.Localize(&i18n.LocalizeConfig{
DefaultMessage: &m,
})
return localizedString
} else if m.Other == str {
localizedString, _ := s.localizer.Localize(&i18n.LocalizeConfig{
DefaultMessage: &m,
})
return localizedString
}
}
return str
}

236
lang/messages.go Normal file
View File

@ -0,0 +1,236 @@
package lang
import (
"github.com/nicksnyder/go-i18n/v2/i18n"
)
var translationMessages = []i18n.Message{
{
ID: "site_name",
Other: "Golang Base Project",
},
{
ID: "home",
Other: "Home",
},
{
ID: "activation_validation_token",
Other: "Please provide a valid activation token",
},
{
ID: "activation_success",
Other: "Account activated. You may now proceed to login to your account.",
},
{
ID: "activate",
Other: "Activate",
},
{
ID: "admin",
Other: "Admin",
},
{
ID: "forgot_password",
Other: "Forgot Password",
},
{
ID: "forgot_password_success",
Other: "An email with instructions describing how to reset your password has been sent.",
},
{
ID: "password_reset",
Other: "Password Reset",
},
{
ID: "password_reset_email",
Other: "Use the following link to reset your password. If this was not requested by you, please ignore this email.\n%s",
},
{
ID: "login",
Other: "Login",
},
{
ID: "login_error",
Other: "Could not login, please make sure that you have typed in the correct email and password. If you have forgotten your password, please click the forgot password link below.",
},
{
ID: "login_activated_error",
Other: "Account is not activated yet.",
},
{
ID: "404_not_found",
Other: "404 Not Found",
},
{
ID: "register",
Other: "Register",
},
{
ID: "password_error",
Other: "Your password must be 8 characters in length or longer",
},
{
ID: "register_error",
Other: "Could not register, please make sure the details you have provided are correct and that you do not already have an existing account.",
},
{
ID: "register_success",
Other: "Thank you for registering. An activation email has been sent with steps describing how to activate your account.",
},
{
ID: "user_activation",
Other: "User Activation",
},
{
ID: "user_activation_email",
Other: "Use the following link to activate your account. If this was not requested by you, please ignore this email.\n%s",
},
{
ID: "resend_activation_email_subject",
Other: "Resend Activation Email",
},
{
ID: "resend_activation_email_success",
Other: "A new activation email has been sent if the account exists and is not already activated. Please remember to check your spam inbox in case the email is not showing in your inbox.",
},
{
ID: "reset_password",
Other: "Reset Password",
},
{
ID: "reset_password_error",
Other: "Could not reset password, please try again",
},
{
ID: "password_reset_success",
Other: "Your password has successfully been reset.",
},
{
ID: "search",
Other: "Search",
},
{
ID: "search_results",
Other: "Search Results",
},
{
ID: "no_results_found",
Other: "No results found",
},
{
ID: "404_message_1",
Other: "The page you're looking for could not be found.",
},
{
ID: "click_here",
Other: "Click here",
},
{
ID: "404_message_2",
Other: "to return to the main page.",
},
{
ID: "admin_dashboard",
Other: "Admin Dashboard",
},
{
ID: "dashboard_message",
Other: "You now have an authenticated session, feel free to log out using the link in the navbar above.",
},
{
ID: "footer_message_1",
Other: "Fork this project on",
},
{
ID: "created_by",
Other: "Created by",
},
{
ID: "forgot_password",
Other: "Forgot password?",
},
{
ID: "forgot_password_message",
Other: "Use the form below to reset your password. If we have an account with your email you will receive instructions on how to reset your password.",
},
{
ID: "email_address",
Other: "Email address",
},
{
ID: "request_reset_email",
Other: "Request reset email",
},
{
ID: "lang_key",
Other: "en",
},
{
ID: "home",
Other: "Home",
},
{
ID: "admin",
Other: "Admin",
},
{
ID: "logout",
Other: "Logout",
},
{
ID: "login",
Other: "Login",
},
{
ID: "register",
Other: "Register",
},
{
ID: "search",
Other: "Search",
},
{
ID: "index_message_1",
Other: "A simple website with user login and registration.",
},
{
ID: "index_message_2",
Other: "The frontend uses",
},
{
ID: "index_message_3",
Other: "and the backend is written in",
},
{
ID: "index_message_4",
Other: "Read more about this project on",
},
{
ID: "password",
Other: "Password",
},
{
ID: "login_terms",
Other: "By pressing the button below to login you agree to the use of cookies on this website.",
},
{
ID: "request_new_activation_email",
Other: "Request a new activation email",
},
{
ID: "resend_activation_email",
Other: "Resend Activation Email",
},
{
ID: "resend_activation_email_message",
Other: "If you have already registered but never activated your account you can use the form below to request a new activation email.",
},
{
ID: "request_activation_email",
Other: "Request activation email",
},
{
ID: "reset_password_message",
Other: "Please enter a new password.",
},
}

20
main.go
View File

@ -2,11 +2,15 @@ package baseproject
import ( import (
"embed" "embed"
"fmt"
"github.com/BurntSushi/toml"
"github.com/gin-contrib/sessions" "github.com/gin-contrib/sessions"
"github.com/gin-contrib/sessions/cookie" "github.com/gin-contrib/sessions/cookie"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/nicksnyder/go-i18n/v2/i18n"
"github.com/uberswe/golang-base-project/middleware" "github.com/uberswe/golang-base-project/middleware"
"github.com/uberswe/golang-base-project/routes" "github.com/uberswe/golang-base-project/routes"
"golang.org/x/text/language"
"html/template" "html/template"
"io/fs" "io/fs"
"log" "log"
@ -27,6 +31,20 @@ func Run() {
// We load environment variables, these are only read when the application launches // We load environment variables, these are only read when the application launches
conf := loadEnvVariables() conf := loadEnvVariables()
// Translations
bundle := i18n.NewBundle(language.English)
bundle.RegisterUnmarshalFunc("toml", toml.Unmarshal)
languages := []string{
"en",
"sv",
}
for _, l := range languages {
_, err := bundle.LoadMessageFile(fmt.Sprintf("active.%s.toml", l))
if err != nil {
log.Fatalln(err)
}
}
// We connect to the database using the configuration generated from the environment variables. // We connect to the database using the configuration generated from the environment variables.
db, err := connectToDatabase(conf) db, err := connectToDatabase(conf)
if err != nil { if err != nil {
@ -81,7 +99,7 @@ func Run() {
r.Use(middleware.General()) r.Use(middleware.General())
// A new instance of the routes controller is created // A new instance of the routes controller is created
controller := routes.New(db, conf) controller := routes.New(db, conf, bundle)
// Any request to / will call controller.Index // Any request to / will call controller.Index
r.GET("/", controller.Index) r.GET("/", controller.Index)

View File

@ -10,13 +10,10 @@ import (
// Activate handles requests used to activate a users account // Activate handles requests used to activate a users account
func (controller Controller) Activate(c *gin.Context) { func (controller Controller) Activate(c *gin.Context) {
activationError := "Please provide a valid activation token" pd := controller.DefaultPageData(c)
activationSuccess := "Account activated. You may now proceed to login to your account." activationError := pd.Trans("Please provide a valid activation token")
pd := PageData{ activationSuccess := pd.Trans("Account activated. You may now proceed to login to your account.")
Title: "Activate", pd.Title = pd.Trans("Activate")
IsAuthenticated: isAuthenticated(c),
CacheParameter: controller.config.CacheParameter,
}
token := c.Param("token") token := c.Param("token")
activationToken := models.Token{ activationToken := models.Token{
Value: token, Value: token,

View File

@ -7,10 +7,7 @@ import (
// Admin renders the admin dashboard // Admin renders the admin dashboard
func (controller Controller) Admin(c *gin.Context) { func (controller Controller) Admin(c *gin.Context) {
pd := PageData{ pd := controller.DefaultPageData(c)
Title: "Admin", pd.Title = pd.Trans("Admin")
IsAuthenticated: isAuthenticated(c),
CacheParameter: controller.config.CacheParameter,
}
c.HTML(http.StatusOK, "admin.html", pd) c.HTML(http.StatusOK, "admin.html", pd)
} }

View File

@ -16,38 +16,32 @@ import (
// ForgotPassword renders the HTML page where a password request can be initiated // ForgotPassword renders the HTML page where a password request can be initiated
func (controller Controller) ForgotPassword(c *gin.Context) { func (controller Controller) ForgotPassword(c *gin.Context) {
pd := PageData{ pd := controller.DefaultPageData(c)
Title: "Forgot Password", pd.Title = pd.Trans("Forgot Password")
IsAuthenticated: isAuthenticated(c),
CacheParameter: controller.config.CacheParameter,
}
c.HTML(http.StatusOK, "forgotpassword.html", pd) c.HTML(http.StatusOK, "forgotpassword.html", pd)
} }
// ForgotPasswordPost handles the POST request which requests a password reset and then renders the HTML page with the appropriate message // ForgotPasswordPost handles the POST request which requests a password reset and then renders the HTML page with the appropriate message
func (controller Controller) ForgotPasswordPost(c *gin.Context) { func (controller Controller) ForgotPasswordPost(c *gin.Context) {
pd := PageData{ pd := controller.DefaultPageData(c)
Title: "Forgot Password", pd.Title = pd.Trans("Forgot Password")
IsAuthenticated: isAuthenticated(c),
CacheParameter: controller.config.CacheParameter,
}
email := c.PostForm("email") email := c.PostForm("email")
user := models.User{Email: email} user := models.User{Email: email}
res := controller.db.Where(&user).First(&user) res := controller.db.Where(&user).First(&user)
if res.Error == nil && user.ActivatedAt != nil { if res.Error == nil && user.ActivatedAt != nil {
go controller.forgotPasswordEmailHandler(user.ID, email) go controller.forgotPasswordEmailHandler(user.ID, email, pd.Trans)
} }
pd.Messages = append(pd.Messages, Message{ pd.Messages = append(pd.Messages, Message{
Type: "success", Type: "success",
Content: "An email with instructions describing how to reset your password has been sent.", Content: pd.Trans("An email with instructions describing how to reset your password has been sent."),
}) })
// We always return a positive response here to prevent user enumeration // We always return a positive response here to prevent user enumeration
c.HTML(http.StatusOK, "forgotpassword.html", pd) c.HTML(http.StatusOK, "forgotpassword.html", pd)
} }
func (controller Controller) forgotPasswordEmailHandler(userID uint, email string) { func (controller Controller) forgotPasswordEmailHandler(userID uint, email string, trans func(string) string) {
forgotPasswordToken := models.Token{ forgotPasswordToken := models.Token{
Value: ulid.Generate(), Value: ulid.Generate(),
Type: models.TokenPasswordReset, Type: models.TokenPasswordReset,
@ -56,7 +50,7 @@ func (controller Controller) forgotPasswordEmailHandler(userID uint, email strin
res := controller.db.Where(&forgotPasswordToken).First(&forgotPasswordToken) res := controller.db.Where(&forgotPasswordToken).First(&forgotPasswordToken)
if (res.Error != nil && res.Error != gorm.ErrRecordNotFound) || res.RowsAffected > 0 { if (res.Error != nil && res.Error != gorm.ErrRecordNotFound) || res.RowsAffected > 0 {
// If the forgot password token already exists we try to generate it again // If the forgot password token already exists we try to generate it again
controller.forgotPasswordEmailHandler(userID, email) controller.forgotPasswordEmailHandler(userID, email, trans)
return return
} }
@ -70,10 +64,10 @@ func (controller Controller) forgotPasswordEmailHandler(userID uint, email strin
log.Println(res.Error) log.Println(res.Error)
return return
} }
controller.sendForgotPasswordEmail(forgotPasswordToken.Value, email) controller.sendForgotPasswordEmail(forgotPasswordToken.Value, email, trans)
} }
func (controller Controller) sendForgotPasswordEmail(token string, email string) { func (controller Controller) sendForgotPasswordEmail(token string, email string, trans func(string) string) {
u, err := url.Parse(controller.config.BaseURL) u, err := url.Parse(controller.config.BaseURL)
if err != nil { if err != nil {
log.Println(err) log.Println(err)
@ -86,5 +80,5 @@ func (controller Controller) sendForgotPasswordEmail(token string, email string)
emailService := email2.New(controller.config) emailService := email2.New(controller.config)
emailService.Send(email, "Password Reset", fmt.Sprintf("Use the following link to reset your password. If this was not requested by you, please ignore this email.\n%s", resetPasswordURL)) emailService.Send(email, trans("Password Reset"), fmt.Sprintf(trans("Use the following link to reset your password. If this was not requested by you, please ignore this email.\n%s"), resetPasswordURL))
} }

View File

@ -7,10 +7,7 @@ import (
// Index renders the HTML of the index page // Index renders the HTML of the index page
func (controller Controller) Index(c *gin.Context) { func (controller Controller) Index(c *gin.Context) {
pd := PageData{ pd := controller.DefaultPageData(c)
Title: "Home", pd.Title = pd.Trans("Home")
IsAuthenticated: isAuthenticated(c),
CacheParameter: controller.config.CacheParameter,
}
c.HTML(http.StatusOK, "index.html", pd) c.HTML(http.StatusOK, "index.html", pd)
} }

View File

@ -14,22 +14,16 @@ import (
// Login renders the HTML of the login page // Login renders the HTML of the login page
func (controller Controller) Login(c *gin.Context) { func (controller Controller) Login(c *gin.Context) {
pd := PageData{ pd := controller.DefaultPageData(c)
Title: "Login", pd.Title = pd.Trans("Login")
IsAuthenticated: isAuthenticated(c),
CacheParameter: controller.config.CacheParameter,
}
c.HTML(http.StatusOK, "login.html", pd) c.HTML(http.StatusOK, "login.html", pd)
} }
// LoginPost handles login requests and returns the appropriate HTML and messages // LoginPost handles login requests and returns the appropriate HTML and messages
func (controller Controller) LoginPost(c *gin.Context) { func (controller Controller) LoginPost(c *gin.Context) {
loginError := "Could not login, please make sure that you have typed in the correct email and password. If you have forgotten your password, please click the forgot password link below." pd := controller.DefaultPageData(c)
pd := PageData{ loginError := pd.Trans("Could not login, please make sure that you have typed in the correct email and password. If you have forgotten your password, please click the forgot password link below.")
Title: "Login", pd.Title = pd.Trans("Login")
IsAuthenticated: isAuthenticated(c),
CacheParameter: controller.config.CacheParameter,
}
email := c.PostForm("email") email := c.PostForm("email")
user := models.User{Email: email} user := models.User{Email: email}
@ -56,7 +50,7 @@ func (controller Controller) LoginPost(c *gin.Context) {
if user.ActivatedAt == nil { if user.ActivatedAt == nil {
pd.Messages = append(pd.Messages, Message{ pd.Messages = append(pd.Messages, Message{
Type: "error", Type: "error",
Content: "Account is not activated yet.", Content: pd.Trans("Account is not activated yet."),
}) })
c.HTML(http.StatusBadRequest, "login.html", pd) c.HTML(http.StatusBadRequest, "login.html", pd)
return return

View File

@ -13,7 +13,9 @@ func (controller Controller) Logout(c *gin.Context) {
session := sessions.Default(c) session := sessions.Default(c)
session.Delete(middleware.SessionIDKey) session.Delete(middleware.SessionIDKey)
err := session.Save() err := session.Save()
log.Println(err) if err != nil {
log.Println(err)
}
c.Redirect(http.StatusTemporaryRedirect, "/") c.Redirect(http.StatusTemporaryRedirect, "/")
} }

View File

@ -3,7 +3,9 @@ package routes
import ( import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/nicksnyder/go-i18n/v2/i18n"
"github.com/uberswe/golang-base-project/config" "github.com/uberswe/golang-base-project/config"
"github.com/uberswe/golang-base-project/lang"
"github.com/uberswe/golang-base-project/middleware" "github.com/uberswe/golang-base-project/middleware"
"gorm.io/gorm" "gorm.io/gorm"
) )
@ -12,13 +14,15 @@ import (
type Controller struct { type Controller struct {
db *gorm.DB db *gorm.DB
config config.Config config config.Config
bundle *i18n.Bundle
} }
// New creates a new instance of the routes.Controller // New creates a new instance of the routes.Controller
func New(db *gorm.DB, c config.Config) Controller { func New(db *gorm.DB, c config.Config, bundle *i18n.Bundle) Controller {
return Controller{ return Controller{
db: db, db: db,
config: c, config: c,
bundle: bundle,
} }
} }
@ -28,6 +32,7 @@ type PageData struct {
Messages []Message Messages []Message
IsAuthenticated bool IsAuthenticated bool
CacheParameter string CacheParameter string
Trans func(s string) string
} }
// Message holds a message which can be rendered as responses on HTML pages // Message holds a message which can be rendered as responses on HTML pages
@ -41,3 +46,14 @@ func isAuthenticated(c *gin.Context) bool {
_, exists := c.Get(middleware.UserIDKey) _, exists := c.Get(middleware.UserIDKey)
return exists return exists
} }
func (controller Controller) DefaultPageData(c *gin.Context) PageData {
langService := lang.New(c, controller.bundle)
return PageData{
Title: "Home",
Messages: nil,
IsAuthenticated: isAuthenticated(c),
CacheParameter: controller.config.CacheParameter,
Trans: langService.Trans,
}
}

View File

@ -7,10 +7,7 @@ import (
// NoRoute handles rendering of the 404 page // NoRoute handles rendering of the 404 page
func (controller Controller) NoRoute(c *gin.Context) { func (controller Controller) NoRoute(c *gin.Context) {
pd := PageData{ pd := controller.DefaultPageData(c)
Title: "404 Not Found", pd.Title = pd.Trans("404 Not Found")
IsAuthenticated: isAuthenticated(c),
CacheParameter: controller.config.CacheParameter,
}
c.HTML(http.StatusOK, "404.html", pd) c.HTML(http.StatusOK, "404.html", pd)
} }

View File

@ -18,24 +18,18 @@ import (
// Register renders the HTML content of the register page // Register renders the HTML content of the register page
func (controller Controller) Register(c *gin.Context) { func (controller Controller) Register(c *gin.Context) {
pd := PageData{ pd := controller.DefaultPageData(c)
Title: "Register", pd.Title = pd.Trans("Register")
IsAuthenticated: isAuthenticated(c),
CacheParameter: controller.config.CacheParameter,
}
c.HTML(http.StatusOK, "register.html", pd) c.HTML(http.StatusOK, "register.html", pd)
} }
// RegisterPost handles requests to register users and returns appropriate messages as HTML content // RegisterPost handles requests to register users and returns appropriate messages as HTML content
func (controller Controller) RegisterPost(c *gin.Context) { func (controller Controller) RegisterPost(c *gin.Context) {
passwordError := "Your password must be 8 characters in length or longer" pd := controller.DefaultPageData(c)
registerError := "Could not register, please make sure the details you have provided are correct and that you do not already have an existing account." passwordError := pd.Trans("Your password must be 8 characters in length or longer")
registerSuccess := "Thank you for registering. An activation email has been sent with steps describing how to activate your account." registerError := pd.Trans("Could not register, please make sure the details you have provided are correct and that you do not already have an existing account.")
pd := PageData{ registerSuccess := pd.Trans("Thank you for registering. An activation email has been sent with steps describing how to activate your account.")
Title: "Register", pd.Title = pd.Trans("Register")
IsAuthenticated: isAuthenticated(c),
CacheParameter: controller.config.CacheParameter,
}
password := c.PostForm("password") password := c.PostForm("password")
if len(password) < 8 { if len(password) < 8 {
pd.Messages = append(pd.Messages, Message{ pd.Messages = append(pd.Messages, Message{
@ -111,7 +105,7 @@ func (controller Controller) RegisterPost(c *gin.Context) {
} }
// Generate activation token and send activation email // Generate activation token and send activation email
go controller.activationEmailHandler(user.ID, email) go controller.activationEmailHandler(user.ID, email, pd.Trans)
pd.Messages = append(pd.Messages, Message{ pd.Messages = append(pd.Messages, Message{
Type: "success", Type: "success",
@ -121,7 +115,7 @@ func (controller Controller) RegisterPost(c *gin.Context) {
c.HTML(http.StatusOK, "register.html", pd) c.HTML(http.StatusOK, "register.html", pd)
} }
func (controller Controller) activationEmailHandler(userID uint, email string) { func (controller Controller) activationEmailHandler(userID uint, email string, trans func(string) string) {
activationToken := models.Token{ activationToken := models.Token{
Value: ulid.Generate(), Value: ulid.Generate(),
Type: models.TokenUserActivation, Type: models.TokenUserActivation,
@ -130,7 +124,7 @@ func (controller Controller) activationEmailHandler(userID uint, email string) {
res := controller.db.Where(&activationToken).First(&activationToken) res := controller.db.Where(&activationToken).First(&activationToken)
if (res.Error != nil && res.Error != gorm.ErrRecordNotFound) || res.RowsAffected > 0 { if (res.Error != nil && res.Error != gorm.ErrRecordNotFound) || res.RowsAffected > 0 {
// If the activation token already exists we try to generate it again // If the activation token already exists we try to generate it again
controller.activationEmailHandler(userID, email) controller.activationEmailHandler(userID, email, trans)
return return
} }
@ -143,10 +137,10 @@ func (controller Controller) activationEmailHandler(userID uint, email string) {
log.Println(res.Error) log.Println(res.Error)
return return
} }
controller.sendActivationEmail(activationToken.Value, email) controller.sendActivationEmail(activationToken.Value, email, trans)
} }
func (controller Controller) sendActivationEmail(token string, email string) { func (controller Controller) sendActivationEmail(token string, email string, trans func(string) string) {
u, err := url.Parse(controller.config.BaseURL) u, err := url.Parse(controller.config.BaseURL)
if err != nil { if err != nil {
log.Println(err) log.Println(err)
@ -159,5 +153,5 @@ func (controller Controller) sendActivationEmail(token string, email string) {
emailService := email2.New(controller.config) emailService := email2.New(controller.config)
emailService.Send(email, "User Activation", fmt.Sprintf("Use the following link to activate your account. If this was not requested by you, please ignore this email.\n%s", activationURL)) emailService.Send(email, trans("User Activation"), fmt.Sprintf(trans("Use the following link to activate your account. If this was not requested by you, please ignore this email.\n%s"), activationURL))
} }

View File

@ -9,21 +9,15 @@ import (
// ResendActivation renders the HTML page used to request a new activation email // ResendActivation renders the HTML page used to request a new activation email
func (controller Controller) ResendActivation(c *gin.Context) { func (controller Controller) ResendActivation(c *gin.Context) {
pd := PageData{ pd := controller.DefaultPageData(c)
Title: "Resend Activation Email", pd.Title = pd.Trans("Resend Activation Email")
IsAuthenticated: isAuthenticated(c),
CacheParameter: controller.config.CacheParameter,
}
c.HTML(http.StatusOK, "resendactivation.html", pd) c.HTML(http.StatusOK, "resendactivation.html", pd)
} }
// ResendActivationPost handles the post request for requesting a new activation email // ResendActivationPost handles the post request for requesting a new activation email
func (controller Controller) ResendActivationPost(c *gin.Context) { func (controller Controller) ResendActivationPost(c *gin.Context) {
pd := PageData{ pd := controller.DefaultPageData(c)
Title: "Resend Activation Email", pd.Title = pd.Trans("Resend Activation Email")
IsAuthenticated: isAuthenticated(c),
CacheParameter: controller.config.CacheParameter,
}
email := c.PostForm("email") email := c.PostForm("email")
user := models.User{Email: email} user := models.User{Email: email}
res := controller.db.Where(&user).First(&user) res := controller.db.Where(&user).First(&user)
@ -36,10 +30,10 @@ func (controller Controller) ResendActivationPost(c *gin.Context) {
res = controller.db.Where(&activationToken).First(&activationToken) res = controller.db.Where(&activationToken).First(&activationToken)
if res.Error == nil { if res.Error == nil {
// If the activation token exists we simply send an email // If the activation token exists we simply send an email
go controller.sendActivationEmail(activationToken.Value, user.Email) go controller.sendActivationEmail(activationToken.Value, user.Email, pd.Trans)
} else { } else {
// If there is no token then we need to generate a new token // If there is no token then we need to generate a new token
go controller.activationEmailHandler(user.ID, user.Email) go controller.activationEmailHandler(user.ID, user.Email, pd.Trans)
} }
} else { } else {
log.Println(res.Error) log.Println(res.Error)
@ -48,7 +42,7 @@ func (controller Controller) ResendActivationPost(c *gin.Context) {
// We always return a positive response here to prevent user enumeration and other attacks // We always return a positive response here to prevent user enumeration and other attacks
pd.Messages = append(pd.Messages, Message{ pd.Messages = append(pd.Messages, Message{
Type: "success", Type: "success",
Content: "A new activation email has been sent if the account exists and is not already activated. Please remember to check your spam inbox in case the email is not showing in your inbox.", Content: pd.Trans("A new activation email has been sent if the account exists and is not already activated. Please remember to check your spam inbox in case the email is not showing in your inbox."),
}) })
c.HTML(http.StatusOK, "resendactivation.html", pd) c.HTML(http.StatusOK, "resendactivation.html", pd)
} }

View File

@ -17,30 +17,26 @@ type ResetPasswordPageData struct {
// ResetPassword renders the HTML page for resetting the users password // ResetPassword renders the HTML page for resetting the users password
func (controller Controller) ResetPassword(c *gin.Context) { func (controller Controller) ResetPassword(c *gin.Context) {
token := c.Param("token") token := c.Param("token")
pdPre := controller.DefaultPageData(c)
pdPre.Title = pdPre.Trans("Reset Password")
pd := ResetPasswordPageData{ pd := ResetPasswordPageData{
PageData: PageData{ PageData: pdPre,
Title: "Reset Password", Token: token,
IsAuthenticated: isAuthenticated(c),
CacheParameter: controller.config.CacheParameter,
},
Token: token,
} }
c.HTML(http.StatusOK, "resetpassword.html", pd) c.HTML(http.StatusOK, "resetpassword.html", pd)
} }
// ResetPasswordPost handles post request used to reset users passwords // ResetPasswordPost handles post request used to reset users passwords
func (controller Controller) ResetPasswordPost(c *gin.Context) { func (controller Controller) ResetPasswordPost(c *gin.Context) {
passwordError := "Your password must be 8 characters in length or longer" pdPre := controller.DefaultPageData(c)
resetError := "Could not reset password, please try again" passwordError := pdPre.Trans("Your password must be 8 characters in length or longer")
resetError := pdPre.Trans("Could not reset password, please try again")
token := c.Param("token") token := c.Param("token")
pdPre.Title = pdPre.Trans("Reset Password")
pd := ResetPasswordPageData{ pd := ResetPasswordPageData{
PageData: PageData{ PageData: pdPre,
Title: "Reset Password", Token: token,
IsAuthenticated: isAuthenticated(c),
CacheParameter: controller.config.CacheParameter,
},
Token: token,
} }
password := c.PostForm("password") password := c.PostForm("password")
@ -125,7 +121,7 @@ func (controller Controller) ResetPasswordPost(c *gin.Context) {
pd.Messages = append(pd.Messages, Message{ pd.Messages = append(pd.Messages, Message{
Type: "success", Type: "success",
Content: "Your password has successfully been reset.", Content: pdPre.Trans("Your password has successfully been reset."),
}) })
c.HTML(http.StatusOK, "resetpassword.html", pd) c.HTML(http.StatusOK, "resetpassword.html", pd)

View File

@ -16,12 +16,10 @@ type SearchData struct {
// Search renders the search HTML page and any search results // Search renders the search HTML page and any search results
func (controller Controller) Search(c *gin.Context) { func (controller Controller) Search(c *gin.Context) {
pdS := controller.DefaultPageData(c)
pdS.Title = pdS.Trans("Search")
pd := SearchData{ pd := SearchData{
PageData: PageData{ PageData: pdS,
Title: "Search",
IsAuthenticated: isAuthenticated(c),
CacheParameter: controller.config.CacheParameter,
},
} }
search := c.PostForm("search") search := c.PostForm("search")
@ -36,7 +34,7 @@ func (controller Controller) Search(c *gin.Context) {
if res.Error != nil || len(results) == 0 { if res.Error != nil || len(results) == 0 {
pd.Messages = append(pd.Messages, Message{ pd.Messages = append(pd.Messages, Message{
Type: "error", Type: "error",
Content: "No results found", Content: pdS.Trans("No results found"),
}) })
log.Println(res.Error) log.Println(res.Error)
c.HTML(http.StatusOK, "search.html", pd) c.HTML(http.StatusOK, "search.html", pd)

19
vendor/github.com/nicksnyder/go-i18n/v2/LICENSE generated vendored Normal file
View File

@ -0,0 +1,19 @@
Copyright (c) 2014 Nick Snyder https://github.com/nicksnyder
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

144
vendor/github.com/nicksnyder/go-i18n/v2/i18n/bundle.go generated vendored Normal file
View File

@ -0,0 +1,144 @@
package i18n
import (
"fmt"
"io/ioutil"
"github.com/nicksnyder/go-i18n/v2/internal/plural"
"golang.org/x/text/language"
)
// UnmarshalFunc unmarshals data into v.
type UnmarshalFunc func(data []byte, v interface{}) error
// Bundle stores a set of messages and pluralization rules.
// Most applications only need a single bundle
// that is initialized early in the application's lifecycle.
// It is not goroutine safe to modify the bundle while Localizers
// are reading from it.
type Bundle struct {
defaultLanguage language.Tag
unmarshalFuncs map[string]UnmarshalFunc
messageTemplates map[language.Tag]map[string]*MessageTemplate
pluralRules plural.Rules
tags []language.Tag
matcher language.Matcher
}
// artTag is the language tag used for artificial languages
// https://en.wikipedia.org/wiki/Codes_for_constructed_languages
var artTag = language.MustParse("art")
// NewBundle returns a bundle with a default language and a default set of plural rules.
func NewBundle(defaultLanguage language.Tag) *Bundle {
b := &Bundle{
defaultLanguage: defaultLanguage,
pluralRules: plural.DefaultRules(),
}
b.pluralRules[artTag] = b.pluralRules.Rule(language.English)
b.addTag(defaultLanguage)
return b
}
// RegisterUnmarshalFunc registers an UnmarshalFunc for format.
func (b *Bundle) RegisterUnmarshalFunc(format string, unmarshalFunc UnmarshalFunc) {
if b.unmarshalFuncs == nil {
b.unmarshalFuncs = make(map[string]UnmarshalFunc)
}
b.unmarshalFuncs[format] = unmarshalFunc
}
// LoadMessageFile loads the bytes from path
// and then calls ParseMessageFileBytes.
func (b *Bundle) LoadMessageFile(path string) (*MessageFile, error) {
buf, err := ioutil.ReadFile(path)
if err != nil {
return nil, err
}
return b.ParseMessageFileBytes(buf, path)
}
// MustLoadMessageFile is similar to LoadTranslationFile
// except it panics if an error happens.
func (b *Bundle) MustLoadMessageFile(path string) {
if _, err := b.LoadMessageFile(path); err != nil {
panic(err)
}
}
// ParseMessageFileBytes parses the bytes in buf to add translations to the bundle.
//
// The format of the file is everything after the last ".".
//
// The language tag of the file is everything after the second to last "." or after the last path separator, but before the format.
func (b *Bundle) ParseMessageFileBytes(buf []byte, path string) (*MessageFile, error) {
messageFile, err := ParseMessageFileBytes(buf, path, b.unmarshalFuncs)
if err != nil {
return nil, err
}
if err := b.AddMessages(messageFile.Tag, messageFile.Messages...); err != nil {
return nil, err
}
return messageFile, nil
}
// MustParseMessageFileBytes is similar to ParseMessageFileBytes
// except it panics if an error happens.
func (b *Bundle) MustParseMessageFileBytes(buf []byte, path string) {
if _, err := b.ParseMessageFileBytes(buf, path); err != nil {
panic(err)
}
}
// AddMessages adds messages for a language.
// It is useful if your messages are in a format not supported by ParseMessageFileBytes.
func (b *Bundle) AddMessages(tag language.Tag, messages ...*Message) error {
pluralRule := b.pluralRules.Rule(tag)
if pluralRule == nil {
return fmt.Errorf("no plural rule registered for %s", tag)
}
if b.messageTemplates == nil {
b.messageTemplates = map[language.Tag]map[string]*MessageTemplate{}
}
if b.messageTemplates[tag] == nil {
b.messageTemplates[tag] = map[string]*MessageTemplate{}
b.addTag(tag)
}
for _, m := range messages {
b.messageTemplates[tag][m.ID] = NewMessageTemplate(m)
}
return nil
}
// MustAddMessages is similar to AddMessages except it panics if an error happens.
func (b *Bundle) MustAddMessages(tag language.Tag, messages ...*Message) {
if err := b.AddMessages(tag, messages...); err != nil {
panic(err)
}
}
func (b *Bundle) addTag(tag language.Tag) {
for _, t := range b.tags {
if t == tag {
// Tag already exists
return
}
}
b.tags = append(b.tags, tag)
b.matcher = language.NewMatcher(b.tags)
}
// LanguageTags returns the list of language tags
// of all the translations loaded into the bundle
func (b *Bundle) LanguageTags() []language.Tag {
return b.tags
}
func (b *Bundle) getMessageTemplate(tag language.Tag, id string) *MessageTemplate {
templates := b.messageTemplates[tag]
if templates == nil {
return nil
}
return templates[id]
}

24
vendor/github.com/nicksnyder/go-i18n/v2/i18n/doc.go generated vendored Normal file
View File

@ -0,0 +1,24 @@
// Package i18n provides support for looking up messages
// according to a set of locale preferences.
//
// Create a Bundle to use for the lifetime of your application.
// bundle := i18n.NewBundle(language.English)
//
// Load translations into your bundle during initialization.
// bundle.LoadMessageFile("en-US.yaml")
//
// Create a Localizer to use for a set of language preferences.
// func(w http.ResponseWriter, r *http.Request) {
// lang := r.FormValue("lang")
// accept := r.Header.Get("Accept-Language")
// localizer := i18n.NewLocalizer(bundle, lang, accept)
// }
//
// Use the Localizer to lookup messages.
// localizer.MustLocalize(&i18n.LocalizeConfig{
// DefaultMessage: &i18n.Message{
// ID: "HelloWorld",
// Other: "Hello World!",
// },
// })
package i18n

View File

@ -0,0 +1,214 @@
package i18n
import (
"fmt"
"text/template"
"github.com/nicksnyder/go-i18n/v2/internal/plural"
"golang.org/x/text/language"
)
// Localizer provides Localize and MustLocalize methods that return localized messages.
type Localizer struct {
// bundle contains the messages that can be returned by the Localizer.
bundle *Bundle
// tags is the list of language tags that the Localizer checks
// in order when localizing a message.
tags []language.Tag
}
// NewLocalizer returns a new Localizer that looks up messages
// in the bundle according to the language preferences in langs.
// It can parse Accept-Language headers as defined in http://www.ietf.org/rfc/rfc2616.txt.
func NewLocalizer(bundle *Bundle, langs ...string) *Localizer {
return &Localizer{
bundle: bundle,
tags: parseTags(langs),
}
}
func parseTags(langs []string) []language.Tag {
tags := []language.Tag{}
for _, lang := range langs {
t, _, err := language.ParseAcceptLanguage(lang)
if err != nil {
continue
}
tags = append(tags, t...)
}
return tags
}
// LocalizeConfig configures a call to the Localize method on Localizer.
type LocalizeConfig struct {
// MessageID is the id of the message to lookup.
// This field is ignored if DefaultMessage is set.
MessageID string
// TemplateData is the data passed when executing the message's template.
// If TemplateData is nil and PluralCount is not nil, then the message template
// will be executed with data that contains the plural count.
TemplateData interface{}
// PluralCount determines which plural form of the message is used.
PluralCount interface{}
// DefaultMessage is used if the message is not found in any message files.
DefaultMessage *Message
// Funcs is used to extend the Go template engine's built in functions
Funcs template.FuncMap
}
type invalidPluralCountErr struct {
messageID string
pluralCount interface{}
err error
}
func (e *invalidPluralCountErr) Error() string {
return fmt.Sprintf("invalid plural count %#v for message id %q: %s", e.pluralCount, e.messageID, e.err)
}
// MessageNotFoundErr is returned from Localize when a message could not be found.
type MessageNotFoundErr struct {
tag language.Tag
messageID string
}
func (e *MessageNotFoundErr) Error() string {
return fmt.Sprintf("message %q not found in language %q", e.messageID, e.tag)
}
type pluralizeErr struct {
messageID string
tag language.Tag
}
func (e *pluralizeErr) Error() string {
return fmt.Sprintf("unable to pluralize %q because there no plural rule for %q", e.messageID, e.tag)
}
type messageIDMismatchErr struct {
messageID string
defaultMessageID string
}
func (e *messageIDMismatchErr) Error() string {
return fmt.Sprintf("message id %q does not match default message id %q", e.messageID, e.defaultMessageID)
}
// Localize returns a localized message.
func (l *Localizer) Localize(lc *LocalizeConfig) (string, error) {
msg, _, err := l.LocalizeWithTag(lc)
return msg, err
}
// Localize returns a localized message.
func (l *Localizer) LocalizeMessage(msg *Message) (string, error) {
return l.Localize(&LocalizeConfig{
DefaultMessage: msg,
})
}
// TODO: uncomment this (and the test) when extract has been updated to extract these call sites too.
// Localize returns a localized message.
// func (l *Localizer) LocalizeMessageID(messageID string) (string, error) {
// return l.Localize(&LocalizeConfig{
// MessageID: messageID,
// })
// }
// LocalizeWithTag returns a localized message and the language tag.
// It may return a best effort localized message even if an error happens.
func (l *Localizer) LocalizeWithTag(lc *LocalizeConfig) (string, language.Tag, error) {
messageID := lc.MessageID
if lc.DefaultMessage != nil {
if messageID != "" && messageID != lc.DefaultMessage.ID {
return "", language.Und, &messageIDMismatchErr{messageID: messageID, defaultMessageID: lc.DefaultMessage.ID}
}
messageID = lc.DefaultMessage.ID
}
var operands *plural.Operands
templateData := lc.TemplateData
if lc.PluralCount != nil {
var err error
operands, err = plural.NewOperands(lc.PluralCount)
if err != nil {
return "", language.Und, &invalidPluralCountErr{messageID: messageID, pluralCount: lc.PluralCount, err: err}
}
if templateData == nil {
templateData = map[string]interface{}{
"PluralCount": lc.PluralCount,
}
}
}
tag, template, err := l.getMessageTemplate(messageID, lc.DefaultMessage)
if template == nil {
return "", language.Und, err
}
pluralForm := l.pluralForm(tag, operands)
msg, err2 := template.Execute(pluralForm, templateData, lc.Funcs)
if err2 != nil {
if err == nil {
err = err2
}
// Attempt to fallback to "Other" pluralization in case translations are incomplete.
if pluralForm != plural.Other {
msg2, err3 := template.Execute(plural.Other, templateData, lc.Funcs)
if err3 == nil {
msg = msg2
}
}
}
return msg, tag, err
}
func (l *Localizer) getMessageTemplate(id string, defaultMessage *Message) (language.Tag, *MessageTemplate, error) {
_, i, _ := l.bundle.matcher.Match(l.tags...)
tag := l.bundle.tags[i]
mt := l.bundle.getMessageTemplate(tag, id)
if mt != nil {
return tag, mt, nil
}
if tag == l.bundle.defaultLanguage {
if defaultMessage == nil {
return language.Und, nil, &MessageNotFoundErr{tag: tag, messageID: id}
}
return tag, NewMessageTemplate(defaultMessage), nil
}
// Fallback to default language in bundle.
mt = l.bundle.getMessageTemplate(l.bundle.defaultLanguage, id)
if mt != nil {
return l.bundle.defaultLanguage, mt, &MessageNotFoundErr{tag: tag, messageID: id}
}
// Fallback to default message.
if defaultMessage == nil {
return language.Und, nil, &MessageNotFoundErr{tag: tag, messageID: id}
}
return l.bundle.defaultLanguage, NewMessageTemplate(defaultMessage), &MessageNotFoundErr{tag: tag, messageID: id}
}
func (l *Localizer) pluralForm(tag language.Tag, operands *plural.Operands) plural.Form {
if operands == nil {
return plural.Other
}
return l.bundle.pluralRules.Rule(tag).PluralFormFunc(operands)
}
// MustLocalize is similar to Localize, except it panics if an error happens.
func (l *Localizer) MustLocalize(lc *LocalizeConfig) string {
localized, err := l.Localize(lc)
if err != nil {
panic(err)
}
return localized
}

221
vendor/github.com/nicksnyder/go-i18n/v2/i18n/message.go generated vendored Normal file
View File

@ -0,0 +1,221 @@
package i18n
import (
"fmt"
"strings"
)
// Message is a string that can be localized.
type Message struct {
// ID uniquely identifies the message.
ID string
// Hash uniquely identifies the content of the message
// that this message was translated from.
Hash string
// Description describes the message to give additional
// context to translators that may be relevant for translation.
Description string
// LeftDelim is the left Go template delimiter.
LeftDelim string
// RightDelim is the right Go template delimiter.``
RightDelim string
// Zero is the content of the message for the CLDR plural form "zero".
Zero string
// One is the content of the message for the CLDR plural form "one".
One string
// Two is the content of the message for the CLDR plural form "two".
Two string
// Few is the content of the message for the CLDR plural form "few".
Few string
// Many is the content of the message for the CLDR plural form "many".
Many string
// Other is the content of the message for the CLDR plural form "other".
Other string
}
// NewMessage parses data and returns a new message.
func NewMessage(data interface{}) (*Message, error) {
m := &Message{}
if err := m.unmarshalInterface(data); err != nil {
return nil, err
}
return m, nil
}
// MustNewMessage is similar to NewMessage except it panics if an error happens.
func MustNewMessage(data interface{}) *Message {
m, err := NewMessage(data)
if err != nil {
panic(err)
}
return m
}
// unmarshalInterface unmarshals a message from data.
func (m *Message) unmarshalInterface(v interface{}) error {
strdata, err := stringMap(v)
if err != nil {
return err
}
for k, v := range strdata {
switch strings.ToLower(k) {
case "id":
m.ID = v
case "description":
m.Description = v
case "hash":
m.Hash = v
case "leftdelim":
m.LeftDelim = v
case "rightdelim":
m.RightDelim = v
case "zero":
m.Zero = v
case "one":
m.One = v
case "two":
m.Two = v
case "few":
m.Few = v
case "many":
m.Many = v
case "other":
m.Other = v
}
}
return nil
}
type keyTypeErr struct {
key interface{}
}
func (err *keyTypeErr) Error() string {
return fmt.Sprintf("expected key to be a string but got %#v", err.key)
}
type valueTypeErr struct {
value interface{}
}
func (err *valueTypeErr) Error() string {
return fmt.Sprintf("unsupported type %#v", err.value)
}
func stringMap(v interface{}) (map[string]string, error) {
switch value := v.(type) {
case string:
return map[string]string{
"other": value,
}, nil
case map[string]string:
return value, nil
case map[string]interface{}:
strdata := make(map[string]string, len(value))
for k, v := range value {
err := stringSubmap(k, v, strdata)
if err != nil {
return nil, err
}
}
return strdata, nil
case map[interface{}]interface{}:
strdata := make(map[string]string, len(value))
for k, v := range value {
kstr, ok := k.(string)
if !ok {
return nil, &keyTypeErr{key: k}
}
err := stringSubmap(kstr, v, strdata)
if err != nil {
return nil, err
}
}
return strdata, nil
default:
return nil, &valueTypeErr{value: value}
}
}
func stringSubmap(k string, v interface{}, strdata map[string]string) error {
if k == "translation" {
switch vt := v.(type) {
case string:
strdata["other"] = vt
default:
v1Message, err := stringMap(v)
if err != nil {
return err
}
for kk, vv := range v1Message {
strdata[kk] = vv
}
}
return nil
}
switch vt := v.(type) {
case string:
strdata[k] = vt
return nil
case nil:
return nil
default:
return fmt.Errorf("expected value for key %q be a string but got %#v", k, v)
}
}
// isMessage tells whether the given data is a message, or a map containing
// nested messages.
// A map is assumed to be a message if it contains any of the "reserved" keys:
// "id", "description", "hash", "leftdelim", "rightdelim", "zero", "one", "two", "few", "many", "other"
// with a string value.
// e.g.,
// - {"message": {"description": "world"}} is a message
// - {"message": {"description": "world", "foo": "bar"}} is a message ("foo" key is ignored)
// - {"notmessage": {"description": {"hello": "world"}}} is not
// - {"notmessage": {"foo": "bar"}} is not
func isMessage(v interface{}) bool {
reservedKeys := []string{"id", "description", "hash", "leftdelim", "rightdelim", "zero", "one", "two", "few", "many", "other"}
switch data := v.(type) {
case string:
return true
case map[string]interface{}:
for _, key := range reservedKeys {
val, ok := data[key]
if !ok {
continue
}
_, ok = val.(string)
if !ok {
continue
}
// v is a message if it contains a "reserved" key holding a string value
return true
}
case map[interface{}]interface{}:
for _, key := range reservedKeys {
val, ok := data[key]
if !ok {
continue
}
_, ok = val.(string)
if !ok {
continue
}
// v is a message if it contains a "reserved" key holding a string value
return true
}
}
return false
}

View File

@ -0,0 +1,65 @@
package i18n
import (
"fmt"
"text/template"
"github.com/nicksnyder/go-i18n/v2/internal"
"github.com/nicksnyder/go-i18n/v2/internal/plural"
)
// MessageTemplate is an executable template for a message.
type MessageTemplate struct {
*Message
PluralTemplates map[plural.Form]*internal.Template
}
// NewMessageTemplate returns a new message template.
func NewMessageTemplate(m *Message) *MessageTemplate {
pluralTemplates := map[plural.Form]*internal.Template{}
setPluralTemplate(pluralTemplates, plural.Zero, m.Zero, m.LeftDelim, m.RightDelim)
setPluralTemplate(pluralTemplates, plural.One, m.One, m.LeftDelim, m.RightDelim)
setPluralTemplate(pluralTemplates, plural.Two, m.Two, m.LeftDelim, m.RightDelim)
setPluralTemplate(pluralTemplates, plural.Few, m.Few, m.LeftDelim, m.RightDelim)
setPluralTemplate(pluralTemplates, plural.Many, m.Many, m.LeftDelim, m.RightDelim)
setPluralTemplate(pluralTemplates, plural.Other, m.Other, m.LeftDelim, m.RightDelim)
if len(pluralTemplates) == 0 {
return nil
}
return &MessageTemplate{
Message: m,
PluralTemplates: pluralTemplates,
}
}
func setPluralTemplate(pluralTemplates map[plural.Form]*internal.Template, pluralForm plural.Form, src, leftDelim, rightDelim string) {
if src != "" {
pluralTemplates[pluralForm] = &internal.Template{
Src: src,
LeftDelim: leftDelim,
RightDelim: rightDelim,
}
}
}
type pluralFormNotFoundError struct {
pluralForm plural.Form
messageID string
}
func (e pluralFormNotFoundError) Error() string {
return fmt.Sprintf("message %q has no plural form %q", e.messageID, e.pluralForm)
}
// Execute executes the template for the plural form and template data.
func (mt *MessageTemplate) Execute(pluralForm plural.Form, data interface{}, funcs template.FuncMap) (string, error) {
t := mt.PluralTemplates[pluralForm]
if t == nil {
return "", pluralFormNotFoundError{
pluralForm: pluralForm,
messageID: mt.Message.ID,
}
}
return t.Execute(funcs, data)
}

166
vendor/github.com/nicksnyder/go-i18n/v2/i18n/parse.go generated vendored Normal file
View File

@ -0,0 +1,166 @@
package i18n
import (
"encoding/json"
"errors"
"fmt"
"os"
"golang.org/x/text/language"
)
// MessageFile represents a parsed message file.
type MessageFile struct {
Path string
Tag language.Tag
Format string
Messages []*Message
}
// ParseMessageFileBytes returns the messages parsed from file.
func ParseMessageFileBytes(buf []byte, path string, unmarshalFuncs map[string]UnmarshalFunc) (*MessageFile, error) {
lang, format := parsePath(path)
tag := language.Make(lang)
messageFile := &MessageFile{
Path: path,
Tag: tag,
Format: format,
}
if len(buf) == 0 {
return messageFile, nil
}
unmarshalFunc := unmarshalFuncs[messageFile.Format]
if unmarshalFunc == nil {
if messageFile.Format == "json" {
unmarshalFunc = json.Unmarshal
} else {
return nil, fmt.Errorf("no unmarshaler registered for %s", messageFile.Format)
}
}
var err error
var raw interface{}
if err = unmarshalFunc(buf, &raw); err != nil {
return nil, err
}
if messageFile.Messages, err = recGetMessages(raw, isMessage(raw), true); err != nil {
return nil, err
}
return messageFile, nil
}
const nestedSeparator = "."
var errInvalidTranslationFile = errors.New("invalid translation file, expected key-values, got a single value")
// recGetMessages looks for translation messages inside "raw" parameter,
// scanning nested maps using recursion.
func recGetMessages(raw interface{}, isMapMessage, isInitialCall bool) ([]*Message, error) {
var messages []*Message
var err error
switch data := raw.(type) {
case string:
if isInitialCall {
return nil, errInvalidTranslationFile
}
m, err := NewMessage(data)
return []*Message{m}, err
case map[string]interface{}:
if isMapMessage {
m, err := NewMessage(data)
return []*Message{m}, err
}
messages = make([]*Message, 0, len(data))
for id, data := range data {
// recursively scan map items
messages, err = addChildMessages(id, data, messages)
if err != nil {
return nil, err
}
}
case map[interface{}]interface{}:
if isMapMessage {
m, err := NewMessage(data)
return []*Message{m}, err
}
messages = make([]*Message, 0, len(data))
for id, data := range data {
strid, ok := id.(string)
if !ok {
return nil, fmt.Errorf("expected key to be string but got %#v", id)
}
// recursively scan map items
messages, err = addChildMessages(strid, data, messages)
if err != nil {
return nil, err
}
}
case []interface{}:
// Backward compatibility for v1 file format.
messages = make([]*Message, 0, len(data))
for _, data := range data {
// recursively scan slice items
childMessages, err := recGetMessages(data, isMessage(data), false)
if err != nil {
return nil, err
}
messages = append(messages, childMessages...)
}
default:
return nil, fmt.Errorf("unsupported file format %T", raw)
}
return messages, nil
}
func addChildMessages(id string, data interface{}, messages []*Message) ([]*Message, error) {
isChildMessage := isMessage(data)
childMessages, err := recGetMessages(data, isChildMessage, false)
if err != nil {
return nil, err
}
for _, m := range childMessages {
if isChildMessage {
if m.ID == "" {
m.ID = id // start with innermost key
}
} else {
m.ID = id + nestedSeparator + m.ID // update ID with each nested key on the way
}
messages = append(messages, m)
}
return messages, nil
}
func parsePath(path string) (langTag, format string) {
formatStartIdx := -1
for i := len(path) - 1; i >= 0; i-- {
c := path[i]
if os.IsPathSeparator(c) {
if formatStartIdx != -1 {
langTag = path[i+1 : formatStartIdx]
}
return
}
if path[i] == '.' {
if formatStartIdx != -1 {
langTag = path[i+1 : formatStartIdx]
return
}
if formatStartIdx == -1 {
format = path[i+1:]
formatStartIdx = i
}
}
}
if formatStartIdx != -1 {
langTag = path[:formatStartIdx]
}
return
}

View File

@ -0,0 +1,3 @@
// Package plural provides support for pluralizing messages
// according to CLDR rules http://cldr.unicode.org/index/cldr-spec/plural-rules
package plural

View File

@ -0,0 +1,16 @@
package plural
// Form represents a language pluralization form as defined here:
// http://cldr.unicode.org/index/cldr-spec/plural-rules
type Form string
// All defined plural forms.
const (
Invalid Form = ""
Zero Form = "zero"
One Form = "one"
Two Form = "two"
Few Form = "few"
Many Form = "many"
Other Form = "other"
)

View File

@ -0,0 +1,120 @@
package plural
import (
"fmt"
"strconv"
"strings"
)
// Operands is a representation of http://unicode.org/reports/tr35/tr35-numbers.html#Operands
type Operands struct {
N float64 // absolute value of the source number (integer and decimals)
I int64 // integer digits of n
V int64 // number of visible fraction digits in n, with trailing zeros
W int64 // number of visible fraction digits in n, without trailing zeros
F int64 // visible fractional digits in n, with trailing zeros
T int64 // visible fractional digits in n, without trailing zeros
}
// NEqualsAny returns true if o represents an integer equal to any of the arguments.
func (o *Operands) NEqualsAny(any ...int64) bool {
for _, i := range any {
if o.I == i && o.T == 0 {
return true
}
}
return false
}
// NModEqualsAny returns true if o represents an integer equal to any of the arguments modulo mod.
func (o *Operands) NModEqualsAny(mod int64, any ...int64) bool {
modI := o.I % mod
for _, i := range any {
if modI == i && o.T == 0 {
return true
}
}
return false
}
// NInRange returns true if o represents an integer in the closed interval [from, to].
func (o *Operands) NInRange(from, to int64) bool {
return o.T == 0 && from <= o.I && o.I <= to
}
// NModInRange returns true if o represents an integer in the closed interval [from, to] modulo mod.
func (o *Operands) NModInRange(mod, from, to int64) bool {
modI := o.I % mod
return o.T == 0 && from <= modI && modI <= to
}
// NewOperands returns the operands for number.
func NewOperands(number interface{}) (*Operands, error) {
switch number := number.(type) {
case int:
return newOperandsInt64(int64(number)), nil
case int8:
return newOperandsInt64(int64(number)), nil
case int16:
return newOperandsInt64(int64(number)), nil
case int32:
return newOperandsInt64(int64(number)), nil
case int64:
return newOperandsInt64(number), nil
case string:
return newOperandsString(number)
case float32, float64:
return nil, fmt.Errorf("floats should be formatted into a string")
default:
return nil, fmt.Errorf("invalid type %T; expected integer or string", number)
}
}
func newOperandsInt64(i int64) *Operands {
if i < 0 {
i = -i
}
return &Operands{float64(i), i, 0, 0, 0, 0}
}
func newOperandsString(s string) (*Operands, error) {
if s[0] == '-' {
s = s[1:]
}
n, err := strconv.ParseFloat(s, 64)
if err != nil {
return nil, err
}
ops := &Operands{N: n}
parts := strings.SplitN(s, ".", 2)
ops.I, err = strconv.ParseInt(parts[0], 10, 64)
if err != nil {
return nil, err
}
if len(parts) == 1 {
return ops, nil
}
fraction := parts[1]
ops.V = int64(len(fraction))
for i := ops.V - 1; i >= 0; i-- {
if fraction[i] != '0' {
ops.W = i + 1
break
}
}
if ops.V > 0 {
f, err := strconv.ParseInt(fraction, 10, 0)
if err != nil {
return nil, err
}
ops.F = f
}
if ops.W > 0 {
t, err := strconv.ParseInt(fraction[:ops.W], 10, 0)
if err != nil {
return nil, err
}
ops.T = t
}
return ops, nil
}

View File

@ -0,0 +1,44 @@
package plural
import (
"golang.org/x/text/language"
)
// Rule defines the CLDR plural rules for a language.
// http://www.unicode.org/cldr/charts/latest/supplemental/language_plural_rules.html
// http://unicode.org/reports/tr35/tr35-numbers.html#Operands
type Rule struct {
PluralForms map[Form]struct{}
PluralFormFunc func(*Operands) Form
}
func addPluralRules(rules Rules, ids []string, ps *Rule) {
for _, id := range ids {
if id == "root" {
continue
}
tag := language.MustParse(id)
rules[tag] = ps
}
}
func newPluralFormSet(pluralForms ...Form) map[Form]struct{} {
set := make(map[Form]struct{}, len(pluralForms))
for _, plural := range pluralForms {
set[plural] = struct{}{}
}
return set
}
func intInRange(i, from, to int64) bool {
return from <= i && i <= to
}
func intEqualsAny(i int64, any ...int64) bool {
for _, a := range any {
if i == a {
return true
}
}
return false
}

View File

@ -0,0 +1,589 @@
// This file is generated by i18n/plural/codegen/generate.sh; DO NOT EDIT
package plural
// DefaultRules returns a map of Rules generated from CLDR language data.
func DefaultRules() Rules {
rules := Rules{}
addPluralRules(rules, []string{"bm", "bo", "dz", "id", "ig", "ii", "in", "ja", "jbo", "jv", "jw", "kde", "kea", "km", "ko", "lkt", "lo", "ms", "my", "nqo", "osa", "root", "sah", "ses", "sg", "su", "th", "to", "vi", "wo", "yo", "yue", "zh"}, &Rule{
PluralForms: newPluralFormSet(Other),
PluralFormFunc: func(ops *Operands) Form {
return Other
},
})
addPluralRules(rules, []string{"am", "as", "bn", "fa", "gu", "hi", "kn", "pcm", "zu"}, &Rule{
PluralForms: newPluralFormSet(One, Other),
PluralFormFunc: func(ops *Operands) Form {
// i = 0 or n = 1
if intEqualsAny(ops.I, 0) ||
ops.NEqualsAny(1) {
return One
}
return Other
},
})
addPluralRules(rules, []string{"ff", "fr", "hy", "kab"}, &Rule{
PluralForms: newPluralFormSet(One, Other),
PluralFormFunc: func(ops *Operands) Form {
// i = 0,1
if intEqualsAny(ops.I, 0, 1) {
return One
}
return Other
},
})
addPluralRules(rules, []string{"pt"}, &Rule{
PluralForms: newPluralFormSet(One, Other),
PluralFormFunc: func(ops *Operands) Form {
// i = 0..1
if intInRange(ops.I, 0, 1) {
return One
}
return Other
},
})
addPluralRules(rules, []string{"ast", "ca", "de", "en", "et", "fi", "fy", "gl", "ia", "io", "it", "ji", "nl", "pt_PT", "sc", "scn", "sv", "sw", "ur", "yi"}, &Rule{
PluralForms: newPluralFormSet(One, Other),
PluralFormFunc: func(ops *Operands) Form {
// i = 1 and v = 0
if intEqualsAny(ops.I, 1) && intEqualsAny(ops.V, 0) {
return One
}
return Other
},
})
addPluralRules(rules, []string{"si"}, &Rule{
PluralForms: newPluralFormSet(One, Other),
PluralFormFunc: func(ops *Operands) Form {
// n = 0,1 or i = 0 and f = 1
if ops.NEqualsAny(0, 1) ||
intEqualsAny(ops.I, 0) && intEqualsAny(ops.F, 1) {
return One
}
return Other
},
})
addPluralRules(rules, []string{"ak", "bho", "guw", "ln", "mg", "nso", "pa", "ti", "wa"}, &Rule{
PluralForms: newPluralFormSet(One, Other),
PluralFormFunc: func(ops *Operands) Form {
// n = 0..1
if ops.NInRange(0, 1) {
return One
}
return Other
},
})
addPluralRules(rules, []string{"tzm"}, &Rule{
PluralForms: newPluralFormSet(One, Other),
PluralFormFunc: func(ops *Operands) Form {
// n = 0..1 or n = 11..99
if ops.NInRange(0, 1) ||
ops.NInRange(11, 99) {
return One
}
return Other
},
})
addPluralRules(rules, []string{"af", "an", "asa", "az", "bem", "bez", "bg", "brx", "ce", "cgg", "chr", "ckb", "dv", "ee", "el", "eo", "es", "eu", "fo", "fur", "gsw", "ha", "haw", "hu", "jgo", "jmc", "ka", "kaj", "kcg", "kk", "kkj", "kl", "ks", "ksb", "ku", "ky", "lb", "lg", "mas", "mgo", "ml", "mn", "mr", "nah", "nb", "nd", "ne", "nn", "nnh", "no", "nr", "ny", "nyn", "om", "or", "os", "pap", "ps", "rm", "rof", "rwk", "saq", "sd", "sdh", "seh", "sn", "so", "sq", "ss", "ssy", "st", "syr", "ta", "te", "teo", "tig", "tk", "tn", "tr", "ts", "ug", "uz", "ve", "vo", "vun", "wae", "xh", "xog"}, &Rule{
PluralForms: newPluralFormSet(One, Other),
PluralFormFunc: func(ops *Operands) Form {
// n = 1
if ops.NEqualsAny(1) {
return One
}
return Other
},
})
addPluralRules(rules, []string{"da"}, &Rule{
PluralForms: newPluralFormSet(One, Other),
PluralFormFunc: func(ops *Operands) Form {
// n = 1 or t != 0 and i = 0,1
if ops.NEqualsAny(1) ||
!intEqualsAny(ops.T, 0) && intEqualsAny(ops.I, 0, 1) {
return One
}
return Other
},
})
addPluralRules(rules, []string{"is"}, &Rule{
PluralForms: newPluralFormSet(One, Other),
PluralFormFunc: func(ops *Operands) Form {
// t = 0 and i % 10 = 1 and i % 100 != 11 or t != 0
if intEqualsAny(ops.T, 0) && intEqualsAny(ops.I%10, 1) && !intEqualsAny(ops.I%100, 11) ||
!intEqualsAny(ops.T, 0) {
return One
}
return Other
},
})
addPluralRules(rules, []string{"mk"}, &Rule{
PluralForms: newPluralFormSet(One, Other),
PluralFormFunc: func(ops *Operands) Form {
// v = 0 and i % 10 = 1 and i % 100 != 11 or f % 10 = 1 and f % 100 != 11
if intEqualsAny(ops.V, 0) && intEqualsAny(ops.I%10, 1) && !intEqualsAny(ops.I%100, 11) ||
intEqualsAny(ops.F%10, 1) && !intEqualsAny(ops.F%100, 11) {
return One
}
return Other
},
})
addPluralRules(rules, []string{"ceb", "fil", "tl"}, &Rule{
PluralForms: newPluralFormSet(One, Other),
PluralFormFunc: func(ops *Operands) Form {
// v = 0 and i = 1,2,3 or v = 0 and i % 10 != 4,6,9 or v != 0 and f % 10 != 4,6,9
if intEqualsAny(ops.V, 0) && intEqualsAny(ops.I, 1, 2, 3) ||
intEqualsAny(ops.V, 0) && !intEqualsAny(ops.I%10, 4, 6, 9) ||
!intEqualsAny(ops.V, 0) && !intEqualsAny(ops.F%10, 4, 6, 9) {
return One
}
return Other
},
})
addPluralRules(rules, []string{"lv", "prg"}, &Rule{
PluralForms: newPluralFormSet(Zero, One, Other),
PluralFormFunc: func(ops *Operands) Form {
// n % 10 = 0 or n % 100 = 11..19 or v = 2 and f % 100 = 11..19
if ops.NModEqualsAny(10, 0) ||
ops.NModInRange(100, 11, 19) ||
intEqualsAny(ops.V, 2) && intInRange(ops.F%100, 11, 19) {
return Zero
}
// n % 10 = 1 and n % 100 != 11 or v = 2 and f % 10 = 1 and f % 100 != 11 or v != 2 and f % 10 = 1
if ops.NModEqualsAny(10, 1) && !ops.NModEqualsAny(100, 11) ||
intEqualsAny(ops.V, 2) && intEqualsAny(ops.F%10, 1) && !intEqualsAny(ops.F%100, 11) ||
!intEqualsAny(ops.V, 2) && intEqualsAny(ops.F%10, 1) {
return One
}
return Other
},
})
addPluralRules(rules, []string{"lag"}, &Rule{
PluralForms: newPluralFormSet(Zero, One, Other),
PluralFormFunc: func(ops *Operands) Form {
// n = 0
if ops.NEqualsAny(0) {
return Zero
}
// i = 0,1 and n != 0
if intEqualsAny(ops.I, 0, 1) && !ops.NEqualsAny(0) {
return One
}
return Other
},
})
addPluralRules(rules, []string{"ksh"}, &Rule{
PluralForms: newPluralFormSet(Zero, One, Other),
PluralFormFunc: func(ops *Operands) Form {
// n = 0
if ops.NEqualsAny(0) {
return Zero
}
// n = 1
if ops.NEqualsAny(1) {
return One
}
return Other
},
})
addPluralRules(rules, []string{"iu", "naq", "sat", "se", "sma", "smi", "smj", "smn", "sms"}, &Rule{
PluralForms: newPluralFormSet(One, Two, Other),
PluralFormFunc: func(ops *Operands) Form {
// n = 1
if ops.NEqualsAny(1) {
return One
}
// n = 2
if ops.NEqualsAny(2) {
return Two
}
return Other
},
})
addPluralRules(rules, []string{"shi"}, &Rule{
PluralForms: newPluralFormSet(One, Few, Other),
PluralFormFunc: func(ops *Operands) Form {
// i = 0 or n = 1
if intEqualsAny(ops.I, 0) ||
ops.NEqualsAny(1) {
return One
}
// n = 2..10
if ops.NInRange(2, 10) {
return Few
}
return Other
},
})
addPluralRules(rules, []string{"mo", "ro"}, &Rule{
PluralForms: newPluralFormSet(One, Few, Other),
PluralFormFunc: func(ops *Operands) Form {
// i = 1 and v = 0
if intEqualsAny(ops.I, 1) && intEqualsAny(ops.V, 0) {
return One
}
// v != 0 or n = 0 or n % 100 = 2..19
if !intEqualsAny(ops.V, 0) ||
ops.NEqualsAny(0) ||
ops.NModInRange(100, 2, 19) {
return Few
}
return Other
},
})
addPluralRules(rules, []string{"bs", "hr", "sh", "sr"}, &Rule{
PluralForms: newPluralFormSet(One, Few, Other),
PluralFormFunc: func(ops *Operands) Form {
// v = 0 and i % 10 = 1 and i % 100 != 11 or f % 10 = 1 and f % 100 != 11
if intEqualsAny(ops.V, 0) && intEqualsAny(ops.I%10, 1) && !intEqualsAny(ops.I%100, 11) ||
intEqualsAny(ops.F%10, 1) && !intEqualsAny(ops.F%100, 11) {
return One
}
// v = 0 and i % 10 = 2..4 and i % 100 != 12..14 or f % 10 = 2..4 and f % 100 != 12..14
if intEqualsAny(ops.V, 0) && intInRange(ops.I%10, 2, 4) && !intInRange(ops.I%100, 12, 14) ||
intInRange(ops.F%10, 2, 4) && !intInRange(ops.F%100, 12, 14) {
return Few
}
return Other
},
})
addPluralRules(rules, []string{"gd"}, &Rule{
PluralForms: newPluralFormSet(One, Two, Few, Other),
PluralFormFunc: func(ops *Operands) Form {
// n = 1,11
if ops.NEqualsAny(1, 11) {
return One
}
// n = 2,12
if ops.NEqualsAny(2, 12) {
return Two
}
// n = 3..10,13..19
if ops.NInRange(3, 10) || ops.NInRange(13, 19) {
return Few
}
return Other
},
})
addPluralRules(rules, []string{"sl"}, &Rule{
PluralForms: newPluralFormSet(One, Two, Few, Other),
PluralFormFunc: func(ops *Operands) Form {
// v = 0 and i % 100 = 1
if intEqualsAny(ops.V, 0) && intEqualsAny(ops.I%100, 1) {
return One
}
// v = 0 and i % 100 = 2
if intEqualsAny(ops.V, 0) && intEqualsAny(ops.I%100, 2) {
return Two
}
// v = 0 and i % 100 = 3..4 or v != 0
if intEqualsAny(ops.V, 0) && intInRange(ops.I%100, 3, 4) ||
!intEqualsAny(ops.V, 0) {
return Few
}
return Other
},
})
addPluralRules(rules, []string{"dsb", "hsb"}, &Rule{
PluralForms: newPluralFormSet(One, Two, Few, Other),
PluralFormFunc: func(ops *Operands) Form {
// v = 0 and i % 100 = 1 or f % 100 = 1
if intEqualsAny(ops.V, 0) && intEqualsAny(ops.I%100, 1) ||
intEqualsAny(ops.F%100, 1) {
return One
}
// v = 0 and i % 100 = 2 or f % 100 = 2
if intEqualsAny(ops.V, 0) && intEqualsAny(ops.I%100, 2) ||
intEqualsAny(ops.F%100, 2) {
return Two
}
// v = 0 and i % 100 = 3..4 or f % 100 = 3..4
if intEqualsAny(ops.V, 0) && intInRange(ops.I%100, 3, 4) ||
intInRange(ops.F%100, 3, 4) {
return Few
}
return Other
},
})
addPluralRules(rules, []string{"he", "iw"}, &Rule{
PluralForms: newPluralFormSet(One, Two, Many, Other),
PluralFormFunc: func(ops *Operands) Form {
// i = 1 and v = 0
if intEqualsAny(ops.I, 1) && intEqualsAny(ops.V, 0) {
return One
}
// i = 2 and v = 0
if intEqualsAny(ops.I, 2) && intEqualsAny(ops.V, 0) {
return Two
}
// v = 0 and n != 0..10 and n % 10 = 0
if intEqualsAny(ops.V, 0) && !ops.NInRange(0, 10) && ops.NModEqualsAny(10, 0) {
return Many
}
return Other
},
})
addPluralRules(rules, []string{"cs", "sk"}, &Rule{
PluralForms: newPluralFormSet(One, Few, Many, Other),
PluralFormFunc: func(ops *Operands) Form {
// i = 1 and v = 0
if intEqualsAny(ops.I, 1) && intEqualsAny(ops.V, 0) {
return One
}
// i = 2..4 and v = 0
if intInRange(ops.I, 2, 4) && intEqualsAny(ops.V, 0) {
return Few
}
// v != 0
if !intEqualsAny(ops.V, 0) {
return Many
}
return Other
},
})
addPluralRules(rules, []string{"pl"}, &Rule{
PluralForms: newPluralFormSet(One, Few, Many, Other),
PluralFormFunc: func(ops *Operands) Form {
// i = 1 and v = 0
if intEqualsAny(ops.I, 1) && intEqualsAny(ops.V, 0) {
return One
}
// v = 0 and i % 10 = 2..4 and i % 100 != 12..14
if intEqualsAny(ops.V, 0) && intInRange(ops.I%10, 2, 4) && !intInRange(ops.I%100, 12, 14) {
return Few
}
// v = 0 and i != 1 and i % 10 = 0..1 or v = 0 and i % 10 = 5..9 or v = 0 and i % 100 = 12..14
if intEqualsAny(ops.V, 0) && !intEqualsAny(ops.I, 1) && intInRange(ops.I%10, 0, 1) ||
intEqualsAny(ops.V, 0) && intInRange(ops.I%10, 5, 9) ||
intEqualsAny(ops.V, 0) && intInRange(ops.I%100, 12, 14) {
return Many
}
return Other
},
})
addPluralRules(rules, []string{"be"}, &Rule{
PluralForms: newPluralFormSet(One, Few, Many, Other),
PluralFormFunc: func(ops *Operands) Form {
// n % 10 = 1 and n % 100 != 11
if ops.NModEqualsAny(10, 1) && !ops.NModEqualsAny(100, 11) {
return One
}
// n % 10 = 2..4 and n % 100 != 12..14
if ops.NModInRange(10, 2, 4) && !ops.NModInRange(100, 12, 14) {
return Few
}
// n % 10 = 0 or n % 10 = 5..9 or n % 100 = 11..14
if ops.NModEqualsAny(10, 0) ||
ops.NModInRange(10, 5, 9) ||
ops.NModInRange(100, 11, 14) {
return Many
}
return Other
},
})
addPluralRules(rules, []string{"lt"}, &Rule{
PluralForms: newPluralFormSet(One, Few, Many, Other),
PluralFormFunc: func(ops *Operands) Form {
// n % 10 = 1 and n % 100 != 11..19
if ops.NModEqualsAny(10, 1) && !ops.NModInRange(100, 11, 19) {
return One
}
// n % 10 = 2..9 and n % 100 != 11..19
if ops.NModInRange(10, 2, 9) && !ops.NModInRange(100, 11, 19) {
return Few
}
// f != 0
if !intEqualsAny(ops.F, 0) {
return Many
}
return Other
},
})
addPluralRules(rules, []string{"mt"}, &Rule{
PluralForms: newPluralFormSet(One, Few, Many, Other),
PluralFormFunc: func(ops *Operands) Form {
// n = 1
if ops.NEqualsAny(1) {
return One
}
// n = 0 or n % 100 = 2..10
if ops.NEqualsAny(0) ||
ops.NModInRange(100, 2, 10) {
return Few
}
// n % 100 = 11..19
if ops.NModInRange(100, 11, 19) {
return Many
}
return Other
},
})
addPluralRules(rules, []string{"ru", "uk"}, &Rule{
PluralForms: newPluralFormSet(One, Few, Many, Other),
PluralFormFunc: func(ops *Operands) Form {
// v = 0 and i % 10 = 1 and i % 100 != 11
if intEqualsAny(ops.V, 0) && intEqualsAny(ops.I%10, 1) && !intEqualsAny(ops.I%100, 11) {
return One
}
// v = 0 and i % 10 = 2..4 and i % 100 != 12..14
if intEqualsAny(ops.V, 0) && intInRange(ops.I%10, 2, 4) && !intInRange(ops.I%100, 12, 14) {
return Few
}
// v = 0 and i % 10 = 0 or v = 0 and i % 10 = 5..9 or v = 0 and i % 100 = 11..14
if intEqualsAny(ops.V, 0) && intEqualsAny(ops.I%10, 0) ||
intEqualsAny(ops.V, 0) && intInRange(ops.I%10, 5, 9) ||
intEqualsAny(ops.V, 0) && intInRange(ops.I%100, 11, 14) {
return Many
}
return Other
},
})
addPluralRules(rules, []string{"br"}, &Rule{
PluralForms: newPluralFormSet(One, Two, Few, Many, Other),
PluralFormFunc: func(ops *Operands) Form {
// n % 10 = 1 and n % 100 != 11,71,91
if ops.NModEqualsAny(10, 1) && !ops.NModEqualsAny(100, 11, 71, 91) {
return One
}
// n % 10 = 2 and n % 100 != 12,72,92
if ops.NModEqualsAny(10, 2) && !ops.NModEqualsAny(100, 12, 72, 92) {
return Two
}
// n % 10 = 3..4,9 and n % 100 != 10..19,70..79,90..99
if (ops.NModInRange(10, 3, 4) || ops.NModEqualsAny(10, 9)) && !(ops.NModInRange(100, 10, 19) || ops.NModInRange(100, 70, 79) || ops.NModInRange(100, 90, 99)) {
return Few
}
// n != 0 and n % 1000000 = 0
if !ops.NEqualsAny(0) && ops.NModEqualsAny(1000000, 0) {
return Many
}
return Other
},
})
addPluralRules(rules, []string{"ga"}, &Rule{
PluralForms: newPluralFormSet(One, Two, Few, Many, Other),
PluralFormFunc: func(ops *Operands) Form {
// n = 1
if ops.NEqualsAny(1) {
return One
}
// n = 2
if ops.NEqualsAny(2) {
return Two
}
// n = 3..6
if ops.NInRange(3, 6) {
return Few
}
// n = 7..10
if ops.NInRange(7, 10) {
return Many
}
return Other
},
})
addPluralRules(rules, []string{"gv"}, &Rule{
PluralForms: newPluralFormSet(One, Two, Few, Many, Other),
PluralFormFunc: func(ops *Operands) Form {
// v = 0 and i % 10 = 1
if intEqualsAny(ops.V, 0) && intEqualsAny(ops.I%10, 1) {
return One
}
// v = 0 and i % 10 = 2
if intEqualsAny(ops.V, 0) && intEqualsAny(ops.I%10, 2) {
return Two
}
// v = 0 and i % 100 = 0,20,40,60,80
if intEqualsAny(ops.V, 0) && intEqualsAny(ops.I%100, 0, 20, 40, 60, 80) {
return Few
}
// v != 0
if !intEqualsAny(ops.V, 0) {
return Many
}
return Other
},
})
addPluralRules(rules, []string{"kw"}, &Rule{
PluralForms: newPluralFormSet(Zero, One, Two, Few, Many, Other),
PluralFormFunc: func(ops *Operands) Form {
// n = 0
if ops.NEqualsAny(0) {
return Zero
}
// n = 1
if ops.NEqualsAny(1) {
return One
}
// n % 100 = 2,22,42,62,82 or n % 1000 = 0 and n % 100000 = 1000..20000,40000,60000,80000 or n != 0 and n % 1000000 = 100000
if ops.NModEqualsAny(100, 2, 22, 42, 62, 82) ||
ops.NModEqualsAny(1000, 0) && (ops.NModInRange(100000, 1000, 20000) || ops.NModEqualsAny(100000, 40000, 60000, 80000)) ||
!ops.NEqualsAny(0) && ops.NModEqualsAny(1000000, 100000) {
return Two
}
// n % 100 = 3,23,43,63,83
if ops.NModEqualsAny(100, 3, 23, 43, 63, 83) {
return Few
}
// n != 1 and n % 100 = 1,21,41,61,81
if !ops.NEqualsAny(1) && ops.NModEqualsAny(100, 1, 21, 41, 61, 81) {
return Many
}
return Other
},
})
addPluralRules(rules, []string{"ar", "ars"}, &Rule{
PluralForms: newPluralFormSet(Zero, One, Two, Few, Many, Other),
PluralFormFunc: func(ops *Operands) Form {
// n = 0
if ops.NEqualsAny(0) {
return Zero
}
// n = 1
if ops.NEqualsAny(1) {
return One
}
// n = 2
if ops.NEqualsAny(2) {
return Two
}
// n % 100 = 3..10
if ops.NModInRange(100, 3, 10) {
return Few
}
// n % 100 = 11..99
if ops.NModInRange(100, 11, 99) {
return Many
}
return Other
},
})
addPluralRules(rules, []string{"cy"}, &Rule{
PluralForms: newPluralFormSet(Zero, One, Two, Few, Many, Other),
PluralFormFunc: func(ops *Operands) Form {
// n = 0
if ops.NEqualsAny(0) {
return Zero
}
// n = 1
if ops.NEqualsAny(1) {
return One
}
// n = 2
if ops.NEqualsAny(2) {
return Two
}
// n = 3
if ops.NEqualsAny(3) {
return Few
}
// n = 6
if ops.NEqualsAny(6) {
return Many
}
return Other
},
})
return rules
}

View File

@ -0,0 +1,24 @@
package plural
import "golang.org/x/text/language"
// Rules is a set of plural rules by language tag.
type Rules map[language.Tag]*Rule
// Rule returns the closest matching plural rule for the language tag
// or nil if no rule could be found.
func (r Rules) Rule(tag language.Tag) *Rule {
t := tag
for {
if rule := r[t]; rule != nil {
return rule
}
t = t.Parent()
if t.IsRoot() {
break
}
}
base, _ := tag.Base()
baseTag, _ := language.Parse(base.String())
return r[baseTag]
}

View File

@ -0,0 +1,51 @@
package internal
import (
"bytes"
"strings"
"sync"
gotemplate "text/template"
)
// Template stores the template for a string.
type Template struct {
Src string
LeftDelim string
RightDelim string
parseOnce sync.Once
parsedTemplate *gotemplate.Template
parseError error
}
func (t *Template) Execute(funcs gotemplate.FuncMap, data interface{}) (string, error) {
leftDelim := t.LeftDelim
if leftDelim == "" {
leftDelim = "{{"
}
if !strings.Contains(t.Src, leftDelim) {
// Fast path to avoid parsing a template that has no actions.
return t.Src, nil
}
var gt *gotemplate.Template
var err error
if funcs == nil {
t.parseOnce.Do(func() {
// If funcs is nil, then we only need to parse this template once.
t.parsedTemplate, t.parseError = gotemplate.New("").Delims(t.LeftDelim, t.RightDelim).Parse(t.Src)
})
gt, err = t.parsedTemplate, t.parseError
} else {
gt, err = gotemplate.New("").Delims(t.LeftDelim, t.RightDelim).Funcs(funcs).Parse(t.Src)
}
if err != nil {
return "", err
}
var buf bytes.Buffer
if err := gt.Execute(&buf, data); err != nil {
return "", err
}
return buf.String(), nil
}

9
vendor/modules.txt vendored
View File

@ -1,3 +1,6 @@
# github.com/BurntSushi/toml v0.3.1
## explicit
github.com/BurntSushi/toml
# github.com/gin-contrib/sessions v0.0.4 # github.com/gin-contrib/sessions v0.0.4
## explicit ## explicit
github.com/gin-contrib/sessions github.com/gin-contrib/sessions
@ -68,6 +71,11 @@ github.com/mattn/go-sqlite3
github.com/modern-go/concurrent github.com/modern-go/concurrent
# github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 # github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742
github.com/modern-go/reflect2 github.com/modern-go/reflect2
# github.com/nicksnyder/go-i18n/v2 v2.1.2
## explicit
github.com/nicksnyder/go-i18n/v2/i18n
github.com/nicksnyder/go-i18n/v2/internal
github.com/nicksnyder/go-i18n/v2/internal/plural
# github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e # github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e
## explicit ## explicit
# github.com/oklog/ulid/v2 v2.0.2 # github.com/oklog/ulid/v2 v2.0.2
@ -95,6 +103,7 @@ golang.org/x/sys/cpu
golang.org/x/sys/internal/unsafeheader golang.org/x/sys/internal/unsafeheader
golang.org/x/sys/unix golang.org/x/sys/unix
# golang.org/x/text v0.3.7 # golang.org/x/text v0.3.7
## explicit
golang.org/x/text/cases golang.org/x/text/cases
golang.org/x/text/internal golang.org/x/text/internal
golang.org/x/text/internal/language golang.org/x/text/internal/language