You've already forked golang-base-project
Merge pull request #16 from uberswe/multilanguage
Added multilanguage support
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@ -1,3 +1,4 @@
|
||||
.idea
|
||||
node_modules
|
||||
docker-compose.local.yml
|
||||
docker-compose.local.yml
|
||||
translate.*.toml
|
@ -12,6 +12,9 @@ RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o "main" -ldflags="-w -s" ./
|
||||
|
||||
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 /etc/ssl/certs/ /etc/ssl/certs/
|
||||
|
||||
|
@ -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.
|
||||
|
||||
## 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
|
||||
|
||||
See [GoDoc](https://godoc.org/github.com/uberswe/golang-base-project) for further documentation.
|
||||
|
51
active.en.toml
Normal file
51
active.en.toml
Normal 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
203
active.sv.toml
Normal 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"
|
4
dist/templates/404.html
vendored
4
dist/templates/404.html
vendored
@ -2,8 +2,8 @@
|
||||
|
||||
<main class="flex-shrink-0">
|
||||
<div class="container">
|
||||
<h1>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>
|
||||
<h1>{{ call .Trans "404 Not Found" }}</h1>
|
||||
<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>
|
||||
</main>
|
||||
|
||||
|
4
dist/templates/admin.html
vendored
4
dist/templates/admin.html
vendored
@ -2,8 +2,8 @@
|
||||
|
||||
<main class="flex-shrink-0">
|
||||
<div class="container">
|
||||
<h1 class="mt-5">Admin Dashboard</h1>
|
||||
<p>You now have an authenticated session, feel free to log out using the link in the navbar above.</p>
|
||||
<h1 class="mt-5">{{ call .Trans "Admin Dashboard" }}</h1>
|
||||
<p>{{ call .Trans "You now have an authenticated session, feel free to log out using the link in the navbar above." }}</p>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
|
2
dist/templates/footer.html
vendored
2
dist/templates/footer.html
vendored
@ -1,6 +1,6 @@
|
||||
<footer class="footer mt-auto py-3 bg-light">
|
||||
<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>
|
||||
</footer>
|
||||
<script src="/assets/js/main.js?c={{ .CacheParameter }}"></script>
|
||||
|
8
dist/templates/forgotpassword.html
vendored
8
dist/templates/forgotpassword.html
vendored
@ -1,18 +1,18 @@
|
||||
{{ template "header.html" . }}
|
||||
<main class="form-signin">
|
||||
<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" . }}
|
||||
|
||||
<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">
|
||||
<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>
|
||||
|
||||
<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>
|
||||
</main>
|
||||
{{ template "footer.html" . }}
|
4
dist/templates/head.html
vendored
4
dist/templates/head.html
vendored
@ -1,11 +1,11 @@
|
||||
<!doctype html>
|
||||
<html lang="en" class="h-100">
|
||||
<html lang="{{ call .Trans "en" }}" class="h-100">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="description" content="">
|
||||
<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">
|
||||
|
||||
|
18
dist/templates/header.html
vendored
18
dist/templates/header.html
vendored
@ -3,7 +3,7 @@
|
||||
<header>
|
||||
<nav class="navbar navbar-expand-md navbar-dark fixed-top bg-dark">
|
||||
<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"
|
||||
aria-controls="navbarCollapse" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
@ -12,31 +12,31 @@
|
||||
{{ if .IsAuthenticated }}
|
||||
<ul class="navbar-nav me-auto mb-2 mb-md-0">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/">Home</a>
|
||||
<a class="nav-link" href="/">{{ call .Trans "Home" }}</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/admin">Admin</a>
|
||||
<a class="nav-link" href="/admin">{{ call .Trans "Admin" }}</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/logout">Logout</a>
|
||||
<a class="nav-link" href="/logout">{{ call .Trans "Logout" }}</a>
|
||||
</li>
|
||||
</ul>
|
||||
{{ else }}
|
||||
<ul class="navbar-nav me-auto mb-2 mb-md-0">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/">Home</a>
|
||||
<a class="nav-link" href="/">{{ call .Trans "Home" }}</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/login">Login</a>
|
||||
<a class="nav-link" href="/login">{{ call .Trans "Login" }}</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/register">Register</a>
|
||||
<a class="nav-link" href="/register">{{ call .Trans "Register" }}</a>
|
||||
</li>
|
||||
</ul>
|
||||
{{ end }}
|
||||
<form method="post" action="/search" class="d-flex">
|
||||
<input name="search" class="form-control me-2" type="search" placeholder="Search" aria-label="Search">
|
||||
<button class="btn btn-outline-success" type="submit">Search</button>
|
||||
<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">{{ call .Trans "Search" }}</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
6
dist/templates/index.html
vendored
6
dist/templates/index.html
vendored
@ -2,9 +2,9 @@
|
||||
|
||||
<main class="flex-shrink-0">
|
||||
<div class="container">
|
||||
<h1 class="mt-5">Golang Base Project</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>Read more about this project on <a href="https://github.com/uberswe/golang-base-project">GitHub</a>.</p>
|
||||
<h1 class="mt-5">{{ call .Trans "site_name" }}</h1>
|
||||
<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>{{ call .Trans "Read more about this project on" }} <a href="https://github.com/uberswe/golang-base-project">GitHub</a>.</p>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
|
14
dist/templates/login.html
vendored
14
dist/templates/login.html
vendored
@ -1,24 +1,24 @@
|
||||
{{ template "header.html" . }}
|
||||
<main class="form-signin">
|
||||
<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" . }}
|
||||
|
||||
<div class="form-floating">
|
||||
<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 class="form-floating">
|
||||
<input name="password" type="password" class="form-control" id="floatingPassword" placeholder="Password">
|
||||
<label for="floatingPassword">Password</label>
|
||||
<input name="password" type="password" class="form-control" id="floatingPassword" placeholder="{{ call .Trans "Password" }}">
|
||||
<label for="floatingPassword">{{ call .Trans "Password" }}</label>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
<button class="w-100 btn btn-lg btn-primary" type="submit">Login</button>
|
||||
<p class="mt-5 mb-3 text-muted"><a href="/user/password/forgot">Forgot password?</a></p>
|
||||
<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">{{ call .Trans "Forgot password?" }}</a></p>
|
||||
</form>
|
||||
</main>
|
||||
{{ template "footer.html" . }}
|
12
dist/templates/register.html
vendored
12
dist/templates/register.html
vendored
@ -1,22 +1,22 @@
|
||||
{{ template "header.html" . }}
|
||||
<main class="form-signin">
|
||||
<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" . }}
|
||||
|
||||
<div class="form-floating">
|
||||
<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 class="form-floating">
|
||||
<input name="password" type="password" class="form-control" id="floatingPassword" placeholder="Password">
|
||||
<label for="floatingPassword">Password</label>
|
||||
<input name="password" type="password" class="form-control" id="floatingPassword" placeholder="{{ call .Trans "Password" }}">
|
||||
<label for="floatingPassword">{{ call .Trans "Password" }}</label>
|
||||
</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>
|
||||
</main>
|
||||
{{ template "footer.html" . }}
|
8
dist/templates/resendactivation.html
vendored
8
dist/templates/resendactivation.html
vendored
@ -1,18 +1,18 @@
|
||||
{{ template "header.html" . }}
|
||||
<main class="form-signin">
|
||||
<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" . }}
|
||||
|
||||
<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">
|
||||
<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>
|
||||
|
||||
<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>
|
||||
</main>
|
||||
{{ template "footer.html" . }}
|
10
dist/templates/resetpassword.html
vendored
10
dist/templates/resetpassword.html
vendored
@ -1,18 +1,18 @@
|
||||
{{ template "header.html" . }}
|
||||
<main class="form-signin">
|
||||
<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" . }}
|
||||
|
||||
<p>Please enter a new password.</p>
|
||||
<p>{{ call .Trans "Please enter a new password." }}</p>
|
||||
|
||||
<div class="form-floating">
|
||||
<input name="password" type="password" class="form-control" id="floatingPassword" placeholder="Password">
|
||||
<label for="floatingPassword">Password</label>
|
||||
<input name="password" type="password" class="form-control" id="floatingPassword" placeholder="{{ call .Trans "Password" }}">
|
||||
<label for="floatingPassword">{{ call .Trans "Password" }}</label>
|
||||
</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>
|
||||
</main>
|
||||
{{ template "footer.html" . }}
|
2
dist/templates/search.html
vendored
2
dist/templates/search.html
vendored
@ -2,7 +2,7 @@
|
||||
|
||||
<main class="flex-shrink-0">
|
||||
<div class="container">
|
||||
<h1 class="mt-5">Search Results</h1>
|
||||
<h1 class="mt-5">{{ call .Trans "Search Results" }}</h1>
|
||||
{{ template "messages.html" . }}
|
||||
{{ range $result := .Results }}
|
||||
<div class="search-result">
|
||||
|
3
go.mod
3
go.mod
@ -3,15 +3,18 @@ module github.com/uberswe/golang-base-project
|
||||
go 1.16
|
||||
|
||||
require (
|
||||
github.com/BurntSushi/toml v0.3.1
|
||||
github.com/gin-contrib/sessions v0.0.4
|
||||
github.com/gin-gonic/gin v1.7.7
|
||||
github.com/go-playground/validator/v10 v10.4.1
|
||||
github.com/gorilla/securecookie v1.1.1
|
||||
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/oklog/ulid/v2 v2.0.2
|
||||
github.com/ulule/limiter/v3 v3.9.0
|
||||
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/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 // indirect
|
||||
gorm.io/driver/mysql v1.2.1
|
||||
|
3
go.sum
3
go.sum
@ -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/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030IGemrRc=
|
||||
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/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/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/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
|
||||
|
39
lang/main.go
Normal file
39
lang/main.go
Normal 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
236
lang/messages.go
Normal 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
20
main.go
@ -2,11 +2,15 @@ package baseproject
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"fmt"
|
||||
"github.com/BurntSushi/toml"
|
||||
"github.com/gin-contrib/sessions"
|
||||
"github.com/gin-contrib/sessions/cookie"
|
||||
"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/routes"
|
||||
"golang.org/x/text/language"
|
||||
"html/template"
|
||||
"io/fs"
|
||||
"log"
|
||||
@ -27,6 +31,20 @@ func Run() {
|
||||
// We load environment variables, these are only read when the application launches
|
||||
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.
|
||||
db, err := connectToDatabase(conf)
|
||||
if err != nil {
|
||||
@ -81,7 +99,7 @@ func Run() {
|
||||
r.Use(middleware.General())
|
||||
|
||||
// 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
|
||||
r.GET("/", controller.Index)
|
||||
|
@ -10,13 +10,10 @@ import (
|
||||
|
||||
// Activate handles requests used to activate a users account
|
||||
func (controller Controller) Activate(c *gin.Context) {
|
||||
activationError := "Please provide a valid activation token"
|
||||
activationSuccess := "Account activated. You may now proceed to login to your account."
|
||||
pd := PageData{
|
||||
Title: "Activate",
|
||||
IsAuthenticated: isAuthenticated(c),
|
||||
CacheParameter: controller.config.CacheParameter,
|
||||
}
|
||||
pd := controller.DefaultPageData(c)
|
||||
activationError := pd.Trans("Please provide a valid activation token")
|
||||
activationSuccess := pd.Trans("Account activated. You may now proceed to login to your account.")
|
||||
pd.Title = pd.Trans("Activate")
|
||||
token := c.Param("token")
|
||||
activationToken := models.Token{
|
||||
Value: token,
|
||||
|
@ -7,10 +7,7 @@ import (
|
||||
|
||||
// Admin renders the admin dashboard
|
||||
func (controller Controller) Admin(c *gin.Context) {
|
||||
pd := PageData{
|
||||
Title: "Admin",
|
||||
IsAuthenticated: isAuthenticated(c),
|
||||
CacheParameter: controller.config.CacheParameter,
|
||||
}
|
||||
pd := controller.DefaultPageData(c)
|
||||
pd.Title = pd.Trans("Admin")
|
||||
c.HTML(http.StatusOK, "admin.html", pd)
|
||||
}
|
||||
|
@ -16,38 +16,32 @@ import (
|
||||
|
||||
// ForgotPassword renders the HTML page where a password request can be initiated
|
||||
func (controller Controller) ForgotPassword(c *gin.Context) {
|
||||
pd := PageData{
|
||||
Title: "Forgot Password",
|
||||
IsAuthenticated: isAuthenticated(c),
|
||||
CacheParameter: controller.config.CacheParameter,
|
||||
}
|
||||
pd := controller.DefaultPageData(c)
|
||||
pd.Title = pd.Trans("Forgot Password")
|
||||
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
|
||||
func (controller Controller) ForgotPasswordPost(c *gin.Context) {
|
||||
pd := PageData{
|
||||
Title: "Forgot Password",
|
||||
IsAuthenticated: isAuthenticated(c),
|
||||
CacheParameter: controller.config.CacheParameter,
|
||||
}
|
||||
pd := controller.DefaultPageData(c)
|
||||
pd.Title = pd.Trans("Forgot Password")
|
||||
email := c.PostForm("email")
|
||||
user := models.User{Email: email}
|
||||
res := controller.db.Where(&user).First(&user)
|
||||
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{
|
||||
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
|
||||
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{
|
||||
Value: ulid.Generate(),
|
||||
Type: models.TokenPasswordReset,
|
||||
@ -56,7 +50,7 @@ func (controller Controller) forgotPasswordEmailHandler(userID uint, email strin
|
||||
res := controller.db.Where(&forgotPasswordToken).First(&forgotPasswordToken)
|
||||
if (res.Error != nil && res.Error != gorm.ErrRecordNotFound) || res.RowsAffected > 0 {
|
||||
// If the forgot password token already exists we try to generate it again
|
||||
controller.forgotPasswordEmailHandler(userID, email)
|
||||
controller.forgotPasswordEmailHandler(userID, email, trans)
|
||||
return
|
||||
}
|
||||
|
||||
@ -70,10 +64,10 @@ func (controller Controller) forgotPasswordEmailHandler(userID uint, email strin
|
||||
log.Println(res.Error)
|
||||
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)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
@ -86,5 +80,5 @@ func (controller Controller) sendForgotPasswordEmail(token string, email string)
|
||||
|
||||
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))
|
||||
}
|
||||
|
@ -7,10 +7,7 @@ import (
|
||||
|
||||
// Index renders the HTML of the index page
|
||||
func (controller Controller) Index(c *gin.Context) {
|
||||
pd := PageData{
|
||||
Title: "Home",
|
||||
IsAuthenticated: isAuthenticated(c),
|
||||
CacheParameter: controller.config.CacheParameter,
|
||||
}
|
||||
pd := controller.DefaultPageData(c)
|
||||
pd.Title = pd.Trans("Home")
|
||||
c.HTML(http.StatusOK, "index.html", pd)
|
||||
}
|
||||
|
@ -14,22 +14,16 @@ import (
|
||||
|
||||
// Login renders the HTML of the login page
|
||||
func (controller Controller) Login(c *gin.Context) {
|
||||
pd := PageData{
|
||||
Title: "Login",
|
||||
IsAuthenticated: isAuthenticated(c),
|
||||
CacheParameter: controller.config.CacheParameter,
|
||||
}
|
||||
pd := controller.DefaultPageData(c)
|
||||
pd.Title = pd.Trans("Login")
|
||||
c.HTML(http.StatusOK, "login.html", pd)
|
||||
}
|
||||
|
||||
// LoginPost handles login requests and returns the appropriate HTML and messages
|
||||
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 := PageData{
|
||||
Title: "Login",
|
||||
IsAuthenticated: isAuthenticated(c),
|
||||
CacheParameter: controller.config.CacheParameter,
|
||||
}
|
||||
pd := controller.DefaultPageData(c)
|
||||
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.")
|
||||
pd.Title = pd.Trans("Login")
|
||||
email := c.PostForm("email")
|
||||
user := models.User{Email: email}
|
||||
|
||||
@ -56,7 +50,7 @@ func (controller Controller) LoginPost(c *gin.Context) {
|
||||
if user.ActivatedAt == nil {
|
||||
pd.Messages = append(pd.Messages, Message{
|
||||
Type: "error",
|
||||
Content: "Account is not activated yet.",
|
||||
Content: pd.Trans("Account is not activated yet."),
|
||||
})
|
||||
c.HTML(http.StatusBadRequest, "login.html", pd)
|
||||
return
|
||||
|
@ -13,7 +13,9 @@ func (controller Controller) Logout(c *gin.Context) {
|
||||
session := sessions.Default(c)
|
||||
session.Delete(middleware.SessionIDKey)
|
||||
err := session.Save()
|
||||
log.Println(err)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
|
||||
c.Redirect(http.StatusTemporaryRedirect, "/")
|
||||
}
|
||||
|
@ -3,7 +3,9 @@ package routes
|
||||
|
||||
import (
|
||||
"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/lang"
|
||||
"github.com/uberswe/golang-base-project/middleware"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
@ -12,13 +14,15 @@ import (
|
||||
type Controller struct {
|
||||
db *gorm.DB
|
||||
config config.Config
|
||||
bundle *i18n.Bundle
|
||||
}
|
||||
|
||||
// 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{
|
||||
db: db,
|
||||
config: c,
|
||||
bundle: bundle,
|
||||
}
|
||||
}
|
||||
|
||||
@ -28,6 +32,7 @@ type PageData struct {
|
||||
Messages []Message
|
||||
IsAuthenticated bool
|
||||
CacheParameter string
|
||||
Trans func(s string) string
|
||||
}
|
||||
|
||||
// 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)
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
@ -7,10 +7,7 @@ import (
|
||||
|
||||
// NoRoute handles rendering of the 404 page
|
||||
func (controller Controller) NoRoute(c *gin.Context) {
|
||||
pd := PageData{
|
||||
Title: "404 Not Found",
|
||||
IsAuthenticated: isAuthenticated(c),
|
||||
CacheParameter: controller.config.CacheParameter,
|
||||
}
|
||||
pd := controller.DefaultPageData(c)
|
||||
pd.Title = pd.Trans("404 Not Found")
|
||||
c.HTML(http.StatusOK, "404.html", pd)
|
||||
}
|
||||
|
@ -18,24 +18,18 @@ import (
|
||||
|
||||
// Register renders the HTML content of the register page
|
||||
func (controller Controller) Register(c *gin.Context) {
|
||||
pd := PageData{
|
||||
Title: "Register",
|
||||
IsAuthenticated: isAuthenticated(c),
|
||||
CacheParameter: controller.config.CacheParameter,
|
||||
}
|
||||
pd := controller.DefaultPageData(c)
|
||||
pd.Title = pd.Trans("Register")
|
||||
c.HTML(http.StatusOK, "register.html", pd)
|
||||
}
|
||||
|
||||
// RegisterPost handles requests to register users and returns appropriate messages as HTML content
|
||||
func (controller Controller) RegisterPost(c *gin.Context) {
|
||||
passwordError := "Your password must be 8 characters in length or longer"
|
||||
registerError := "Could not register, please make sure the details you have provided are correct and that you do not already have an existing account."
|
||||
registerSuccess := "Thank you for registering. An activation email has been sent with steps describing how to activate your account."
|
||||
pd := PageData{
|
||||
Title: "Register",
|
||||
IsAuthenticated: isAuthenticated(c),
|
||||
CacheParameter: controller.config.CacheParameter,
|
||||
}
|
||||
pd := controller.DefaultPageData(c)
|
||||
passwordError := pd.Trans("Your password must be 8 characters in length or longer")
|
||||
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.")
|
||||
registerSuccess := pd.Trans("Thank you for registering. An activation email has been sent with steps describing how to activate your account.")
|
||||
pd.Title = pd.Trans("Register")
|
||||
password := c.PostForm("password")
|
||||
if len(password) < 8 {
|
||||
pd.Messages = append(pd.Messages, Message{
|
||||
@ -111,7 +105,7 @@ func (controller Controller) RegisterPost(c *gin.Context) {
|
||||
}
|
||||
|
||||
// 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{
|
||||
Type: "success",
|
||||
@ -121,7 +115,7 @@ func (controller Controller) RegisterPost(c *gin.Context) {
|
||||
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{
|
||||
Value: ulid.Generate(),
|
||||
Type: models.TokenUserActivation,
|
||||
@ -130,7 +124,7 @@ func (controller Controller) activationEmailHandler(userID uint, email string) {
|
||||
res := controller.db.Where(&activationToken).First(&activationToken)
|
||||
if (res.Error != nil && res.Error != gorm.ErrRecordNotFound) || res.RowsAffected > 0 {
|
||||
// If the activation token already exists we try to generate it again
|
||||
controller.activationEmailHandler(userID, email)
|
||||
controller.activationEmailHandler(userID, email, trans)
|
||||
return
|
||||
}
|
||||
|
||||
@ -143,10 +137,10 @@ func (controller Controller) activationEmailHandler(userID uint, email string) {
|
||||
log.Println(res.Error)
|
||||
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)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
@ -159,5 +153,5 @@ func (controller Controller) sendActivationEmail(token string, email string) {
|
||||
|
||||
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))
|
||||
}
|
||||
|
@ -9,21 +9,15 @@ import (
|
||||
|
||||
// ResendActivation renders the HTML page used to request a new activation email
|
||||
func (controller Controller) ResendActivation(c *gin.Context) {
|
||||
pd := PageData{
|
||||
Title: "Resend Activation Email",
|
||||
IsAuthenticated: isAuthenticated(c),
|
||||
CacheParameter: controller.config.CacheParameter,
|
||||
}
|
||||
pd := controller.DefaultPageData(c)
|
||||
pd.Title = pd.Trans("Resend Activation Email")
|
||||
c.HTML(http.StatusOK, "resendactivation.html", pd)
|
||||
}
|
||||
|
||||
// ResendActivationPost handles the post request for requesting a new activation email
|
||||
func (controller Controller) ResendActivationPost(c *gin.Context) {
|
||||
pd := PageData{
|
||||
Title: "Resend Activation Email",
|
||||
IsAuthenticated: isAuthenticated(c),
|
||||
CacheParameter: controller.config.CacheParameter,
|
||||
}
|
||||
pd := controller.DefaultPageData(c)
|
||||
pd.Title = pd.Trans("Resend Activation Email")
|
||||
email := c.PostForm("email")
|
||||
user := models.User{Email: email}
|
||||
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)
|
||||
if res.Error == nil {
|
||||
// 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 {
|
||||
// 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 {
|
||||
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
|
||||
pd.Messages = append(pd.Messages, Message{
|
||||
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)
|
||||
}
|
||||
|
@ -17,30 +17,26 @@ type ResetPasswordPageData struct {
|
||||
// ResetPassword renders the HTML page for resetting the users password
|
||||
func (controller Controller) ResetPassword(c *gin.Context) {
|
||||
token := c.Param("token")
|
||||
pdPre := controller.DefaultPageData(c)
|
||||
pdPre.Title = pdPre.Trans("Reset Password")
|
||||
pd := ResetPasswordPageData{
|
||||
PageData: PageData{
|
||||
Title: "Reset Password",
|
||||
IsAuthenticated: isAuthenticated(c),
|
||||
CacheParameter: controller.config.CacheParameter,
|
||||
},
|
||||
Token: token,
|
||||
PageData: pdPre,
|
||||
Token: token,
|
||||
}
|
||||
c.HTML(http.StatusOK, "resetpassword.html", pd)
|
||||
}
|
||||
|
||||
// ResetPasswordPost handles post request used to reset users passwords
|
||||
func (controller Controller) ResetPasswordPost(c *gin.Context) {
|
||||
passwordError := "Your password must be 8 characters in length or longer"
|
||||
resetError := "Could not reset password, please try again"
|
||||
pdPre := controller.DefaultPageData(c)
|
||||
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")
|
||||
pdPre.Title = pdPre.Trans("Reset Password")
|
||||
pd := ResetPasswordPageData{
|
||||
PageData: PageData{
|
||||
Title: "Reset Password",
|
||||
IsAuthenticated: isAuthenticated(c),
|
||||
CacheParameter: controller.config.CacheParameter,
|
||||
},
|
||||
Token: token,
|
||||
PageData: pdPre,
|
||||
Token: token,
|
||||
}
|
||||
password := c.PostForm("password")
|
||||
|
||||
@ -125,7 +121,7 @@ func (controller Controller) ResetPasswordPost(c *gin.Context) {
|
||||
|
||||
pd.Messages = append(pd.Messages, Message{
|
||||
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)
|
||||
|
@ -16,12 +16,10 @@ type SearchData struct {
|
||||
|
||||
// Search renders the search HTML page and any search results
|
||||
func (controller Controller) Search(c *gin.Context) {
|
||||
pdS := controller.DefaultPageData(c)
|
||||
pdS.Title = pdS.Trans("Search")
|
||||
pd := SearchData{
|
||||
PageData: PageData{
|
||||
Title: "Search",
|
||||
IsAuthenticated: isAuthenticated(c),
|
||||
CacheParameter: controller.config.CacheParameter,
|
||||
},
|
||||
PageData: pdS,
|
||||
}
|
||||
search := c.PostForm("search")
|
||||
|
||||
@ -36,7 +34,7 @@ func (controller Controller) Search(c *gin.Context) {
|
||||
if res.Error != nil || len(results) == 0 {
|
||||
pd.Messages = append(pd.Messages, Message{
|
||||
Type: "error",
|
||||
Content: "No results found",
|
||||
Content: pdS.Trans("No results found"),
|
||||
})
|
||||
log.Println(res.Error)
|
||||
c.HTML(http.StatusOK, "search.html", pd)
|
||||
|
19
vendor/github.com/nicksnyder/go-i18n/v2/LICENSE
generated
vendored
Normal file
19
vendor/github.com/nicksnyder/go-i18n/v2/LICENSE
generated
vendored
Normal 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
144
vendor/github.com/nicksnyder/go-i18n/v2/i18n/bundle.go
generated
vendored
Normal 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
24
vendor/github.com/nicksnyder/go-i18n/v2/i18n/doc.go
generated
vendored
Normal 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
|
214
vendor/github.com/nicksnyder/go-i18n/v2/i18n/localizer.go
generated
vendored
Normal file
214
vendor/github.com/nicksnyder/go-i18n/v2/i18n/localizer.go
generated
vendored
Normal 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
221
vendor/github.com/nicksnyder/go-i18n/v2/i18n/message.go
generated
vendored
Normal 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
|
||||
}
|
65
vendor/github.com/nicksnyder/go-i18n/v2/i18n/message_template.go
generated
vendored
Normal file
65
vendor/github.com/nicksnyder/go-i18n/v2/i18n/message_template.go
generated
vendored
Normal 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
166
vendor/github.com/nicksnyder/go-i18n/v2/i18n/parse.go
generated
vendored
Normal 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
|
||||
}
|
3
vendor/github.com/nicksnyder/go-i18n/v2/internal/plural/doc.go
generated
vendored
Normal file
3
vendor/github.com/nicksnyder/go-i18n/v2/internal/plural/doc.go
generated
vendored
Normal 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
|
16
vendor/github.com/nicksnyder/go-i18n/v2/internal/plural/form.go
generated
vendored
Normal file
16
vendor/github.com/nicksnyder/go-i18n/v2/internal/plural/form.go
generated
vendored
Normal 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"
|
||||
)
|
120
vendor/github.com/nicksnyder/go-i18n/v2/internal/plural/operands.go
generated
vendored
Normal file
120
vendor/github.com/nicksnyder/go-i18n/v2/internal/plural/operands.go
generated
vendored
Normal 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
|
||||
}
|
44
vendor/github.com/nicksnyder/go-i18n/v2/internal/plural/rule.go
generated
vendored
Normal file
44
vendor/github.com/nicksnyder/go-i18n/v2/internal/plural/rule.go
generated
vendored
Normal 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
|
||||
}
|
589
vendor/github.com/nicksnyder/go-i18n/v2/internal/plural/rule_gen.go
generated
vendored
Normal file
589
vendor/github.com/nicksnyder/go-i18n/v2/internal/plural/rule_gen.go
generated
vendored
Normal 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
|
||||
}
|
24
vendor/github.com/nicksnyder/go-i18n/v2/internal/plural/rules.go
generated
vendored
Normal file
24
vendor/github.com/nicksnyder/go-i18n/v2/internal/plural/rules.go
generated
vendored
Normal 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]
|
||||
}
|
51
vendor/github.com/nicksnyder/go-i18n/v2/internal/template.go
generated
vendored
Normal file
51
vendor/github.com/nicksnyder/go-i18n/v2/internal/template.go
generated
vendored
Normal 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
9
vendor/modules.txt
vendored
@ -1,3 +1,6 @@
|
||||
# github.com/BurntSushi/toml v0.3.1
|
||||
## explicit
|
||||
github.com/BurntSushi/toml
|
||||
# github.com/gin-contrib/sessions v0.0.4
|
||||
## explicit
|
||||
github.com/gin-contrib/sessions
|
||||
@ -68,6 +71,11 @@ github.com/mattn/go-sqlite3
|
||||
github.com/modern-go/concurrent
|
||||
# github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742
|
||||
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
|
||||
## explicit
|
||||
# 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/unix
|
||||
# golang.org/x/text v0.3.7
|
||||
## explicit
|
||||
golang.org/x/text/cases
|
||||
golang.org/x/text/internal
|
||||
golang.org/x/text/internal/language
|
||||
|
Reference in New Issue
Block a user