feat: Add new i18n
This commit is contained in:
parent
40079e1145
commit
3e28cb35e9
14
app.js
14
app.js
@ -1,4 +1,8 @@
|
||||
require('dotenv').config();
|
||||
|
||||
const redisHost = process.env.REDIS_HOST ? process.env.REDIS_HOST : 'localhost';
|
||||
const redisPort = process.env.REDIS_PORT ? process.env.REDIS_PORT : 6379;
|
||||
|
||||
const createError = require('http-errors');
|
||||
const express = require('express');
|
||||
const session = require('express-session');
|
||||
@ -34,11 +38,11 @@ const sess = {
|
||||
cookie: { secure: false }
|
||||
}
|
||||
|
||||
if (app.get('env') === 'production') {
|
||||
app.set('trust proxy', 1); // trust first proxy
|
||||
sess.cookie.secure = true; // serve secure cookies
|
||||
sess.store = new RedisStore({ client: redisClient });
|
||||
}
|
||||
// if (app.get('env') === 'production') {
|
||||
// app.set('trust proxy', 1); // trust first proxy
|
||||
// sess.cookie.secure = true; // serve secure cookies
|
||||
// sess.store = new RedisStore({ host: redisHost, port: redisPort, client: redisClient });
|
||||
// }
|
||||
|
||||
app.use(session(sess));
|
||||
|
||||
|
@ -9,7 +9,7 @@ const QUIZZ_SIZE = process.env.QUIZZ_SIZE ? process.env.QUIZZ_SIZE : 5;
|
||||
|
||||
function getHome(req, res) {
|
||||
res.render('api', {
|
||||
title: "SoundBirder api",
|
||||
title: "SoundBirder API",
|
||||
version: 0
|
||||
});
|
||||
}
|
||||
@ -24,7 +24,7 @@ async function region(req, res) {
|
||||
})
|
||||
}
|
||||
|
||||
function quizz(req, res) {
|
||||
async function quizz(req, res) {
|
||||
debug('Generating quizz');
|
||||
const { region } = req.query;
|
||||
// debug(`Coordinates: ${lat}, ${lng}`);
|
||||
|
@ -8,7 +8,7 @@ function indexPage(req, res) {
|
||||
|
||||
function loginPage(req, res) {
|
||||
res.render('auth/login', {
|
||||
title: req.i18n.__("SoundBirder - Login"),
|
||||
title: req.i18n.__('SoundBirder - Login'),
|
||||
locale: req.i18n.locale
|
||||
});
|
||||
}
|
||||
@ -23,7 +23,7 @@ function logout(req, res) {
|
||||
|
||||
function registerPage(req, res) {
|
||||
res.render('auth/register', {
|
||||
title: req.i18n.__("SoundBirder - Register"),
|
||||
title: req.i18n.__('SoundBirder - Register'),
|
||||
locale: req.i18n.locale
|
||||
});
|
||||
}
|
||||
|
@ -9,7 +9,7 @@ function getIndex(req, res) {
|
||||
|
||||
function getAbout(req, res) {
|
||||
res.render('about', {
|
||||
title: 'About SoundBirder',
|
||||
title: req.i18n.__('About SoundBirder'),
|
||||
locale: req.i18n.locale
|
||||
});
|
||||
}
|
||||
|
@ -46,9 +46,6 @@ async function generateQuizz(region, locale, size) {
|
||||
let answer;
|
||||
do {
|
||||
answer = choice(speciesSelectionLocalized);
|
||||
if (answer === undefined) {
|
||||
debug('Species', speciesSelectionLocalized);
|
||||
}
|
||||
quizz.answer = answer;
|
||||
quizz.audio = await getAudio(answer.sciName);
|
||||
if (quizz.audio === undefined) {
|
||||
@ -89,8 +86,9 @@ async function getLocalizedNames(speciesCodes, locale) {
|
||||
console.error(error);
|
||||
}
|
||||
if (!flag) {
|
||||
let names;
|
||||
try {
|
||||
const names = { speciesCode, sciName, comName } = await getLocalizedName(code, locale);
|
||||
names = { speciesCode, sciName, comName } = await getLocalizedName(code, locale);
|
||||
cache.cacheResponse(`${code}-${locale}`, names);
|
||||
allNames.push(names);
|
||||
} catch (error) {
|
||||
@ -121,7 +119,11 @@ function getLocalizedName(speciesCode, locale) {
|
||||
async function getSpeciesList(regionCode) {
|
||||
const cached = await cache.getCached(`spplist-${regionCode}`);
|
||||
if (cached) {
|
||||
if (cached != []) {
|
||||
return cached;
|
||||
} else {
|
||||
console.log("Empty cached species list, retrieving again");
|
||||
}
|
||||
} else {
|
||||
return eBird.product.spplist.in(regionCode)()
|
||||
.then(species => {
|
||||
|
@ -1,4 +1,5 @@
|
||||
{
|
||||
"Game": "Spiel",
|
||||
"About": "About"
|
||||
"About": "About",
|
||||
"It was a": "__It was a"
|
||||
}
|
16
locales/en.json
Executable file → Normal file
16
locales/en.json
Executable file → Normal file
@ -1,15 +1 @@
|
||||
{
|
||||
"Home": "Home",
|
||||
"About": "About",
|
||||
"Game": "Game",
|
||||
"Contact": "Contact",
|
||||
"SoundBirder is an open-source web application to learn bird song identification. It is based on bird records from Xeno-Canto and data from eBird": "SoundBirder is an open-source web application to learn bird song identification. It is based on bird records from Xeno-Canto and data from eBird",
|
||||
"Author": "Author",
|
||||
"The project is made with ♥ by Samuel ORTION": "The project is made with ♥ by Samuel ORTION",
|
||||
"SoundBirder is an open-source web application to learn bird song identification. It is based on bird records from Xeno-Canto and data from eBird.": "SoundBirder is an open-source web application to learn bird song identification. It is based on bird records from Xeno-Canto and data from eBird.",
|
||||
"The project is made with ♥ by Samuel ORTION.": "The project is made with ♥ by Samuel ORTION.",
|
||||
"Welcome to SoundBirder' API": "Welcome to SoundBirder' API",
|
||||
"Wrong!": "Wrong!",
|
||||
"Correct!": "Correct!",
|
||||
"It was a": "It was a"
|
||||
}
|
||||
{}
|
@ -1,4 +1,9 @@
|
||||
{
|
||||
"Home": "Inicio",
|
||||
"About": "Acerca de"
|
||||
"About": "Acerca de",
|
||||
"SoundBirder is an open-source web application to learn bird song identification. It is based on bird records from Xeno-Canto and data from eBird.": "__SoundBirder is an open-source web application to learn bird song identification. It is based on bird records from Xeno-Canto and data from eBird.",
|
||||
"Author": "__Author",
|
||||
"The project is made with ♥ by Samuel Ortion.": "__The project is made with ♥ by Samuel Ortion.",
|
||||
"It was a": "__It was a",
|
||||
"Game": "__Game"
|
||||
}
|
7
locales/fr.json
Executable file → Normal file
7
locales/fr.json
Executable file → Normal file
@ -2,10 +2,11 @@
|
||||
"Game": "Jeu",
|
||||
"About": "À propos",
|
||||
"Contact": "Contact",
|
||||
"SoundBirder is an open-source web application to learn bird song identification. It is based on bird records from Xeno-Canto and data from eBird": "SoundBirder est une application web open-source qui permet d'apprendre à reconnaitre les chants d'oiseaux. Elle se base sur des enregistrements audios de Xeno-Canto et sur les données de répartitions d'eBird",
|
||||
"Author": "Auteur",
|
||||
"The project is made with ♥ by Samuel ORTION": "Ce projet est fait avec ♥ par Samuel ORTION",
|
||||
"The project is made with ♥ by Samuel Ortion": "Ce projet est fait avec ♥ par Samuel Ortion",
|
||||
"Correct!": "Correct !",
|
||||
"Wrong!": "Incorrect !",
|
||||
"It was a": "C'était un"
|
||||
"It was a": "C'était un",
|
||||
"SoundBirder is an open-source web application to learn bird song identification. It is based on bird records from Xeno-Canto and data from eBird.": "SoundBirder est une application open-source pour apprendre les chant d'oiseaux. Elle est basée sur les enregistrements de Xeno-Canto et sur les données d'eBird",
|
||||
"The project is made with ♥ by Samuel Ortion.": "Ce projet est fait avec ♥ par Samuel Ortion."
|
||||
}
|
@ -5,18 +5,24 @@ let map = document.getElementById('map');
|
||||
let startButton = document.querySelector('.game .start-button');
|
||||
let gameMapStep = document.querySelector('.game-map-step');
|
||||
let gameQuizzStep = document.querySelector('.game-quizz-step');
|
||||
let gameQuizz = document.querySelector('.game-quizz');
|
||||
let gameResultStep = document.querySelector('.game-results-step');
|
||||
let gameLoading = document.querySelector('.game-loading');
|
||||
let audio = document.querySelector('.game-quizz-step audio');
|
||||
let proposals = document.querySelector('.game-quizz-step .proposals');
|
||||
let result = document.querySelector('.game-results-step .message');
|
||||
let restartButton = document.querySelector('.game-results-step .restart-button');
|
||||
let restartButton = document.querySelector('.game-quizz .restart-button');
|
||||
let mapButton = document.querySelector('.game-quizz .map-button');
|
||||
let resultComName = document.querySelector('.game-results-step .species .com');
|
||||
let resultSciName = document.querySelector('.game-results-step .species .sci');
|
||||
|
||||
const API_VERSION = "0";
|
||||
|
||||
let region = 'FR';
|
||||
|
||||
function returnToMap() {
|
||||
gameMapStep.classList.remove("none");
|
||||
gameQuizz.classList.add("none");
|
||||
}
|
||||
|
||||
function geolocationStep() {
|
||||
if (map != undefined)
|
||||
geolocationHandler();
|
||||
@ -37,8 +43,11 @@ function regionCoder() {
|
||||
}
|
||||
|
||||
function quizzStep() {
|
||||
gameQuizz.classList.remove("none");
|
||||
gameLoading.classList.remove("none");
|
||||
client.getQuizz(region)
|
||||
.then(quizz => {
|
||||
gameLoading.classList.add("none");
|
||||
displayQuizz(quizz);
|
||||
}).catch(error => {
|
||||
console.log(error);
|
||||
@ -47,6 +56,10 @@ function quizzStep() {
|
||||
|
||||
|
||||
function displayQuizz(quizz) {
|
||||
if (quizz.audio == undefined) {
|
||||
quizzStep();
|
||||
return;
|
||||
}
|
||||
audio.src = quizz.audio;
|
||||
audio.classList.remove("none"); // Display the audio controls
|
||||
gameQuizzStep.classList.remove("none");
|
||||
@ -82,6 +95,7 @@ function verifyAnswer(event) {
|
||||
});
|
||||
}
|
||||
restartButton.addEventListener('click', restart);
|
||||
mapButton.addEventListener('click', returnToMap);
|
||||
|
||||
function displayResult(message_class, message, species) {
|
||||
gameQuizzStep.classList.toggle('none');
|
||||
|
101
public/stylesheets/spinner.css
Normal file
101
public/stylesheets/spinner.css
Normal file
@ -0,0 +1,101 @@
|
||||
/* html, body {
|
||||
height: 100%;
|
||||
}
|
||||
*/
|
||||
/* body {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
} */
|
||||
|
||||
.spinner {
|
||||
-webkit-animation: rotator 1.4s linear infinite;
|
||||
animation: rotator 1.4s linear infinite;
|
||||
}
|
||||
|
||||
@-webkit-keyframes rotator {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(270deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes rotator {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(270deg);
|
||||
}
|
||||
}
|
||||
.path {
|
||||
stroke-dasharray: 187;
|
||||
stroke-dashoffset: 0;
|
||||
transform-origin: center;
|
||||
-webkit-animation: dash 1.4s ease-in-out infinite, colors 5.6s ease-in-out infinite;
|
||||
animation: dash 1.4s ease-in-out infinite, colors 5.6s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@-webkit-keyframes colors {
|
||||
0% {
|
||||
stroke: #4285F4;
|
||||
}
|
||||
25% {
|
||||
stroke: #DE3E35;
|
||||
}
|
||||
50% {
|
||||
stroke: #F7C223;
|
||||
}
|
||||
75% {
|
||||
stroke: #1B9A59;
|
||||
}
|
||||
100% {
|
||||
stroke: #4285F4;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes colors {
|
||||
0% {
|
||||
stroke: #4285F4;
|
||||
}
|
||||
25% {
|
||||
stroke: #DE3E35;
|
||||
}
|
||||
50% {
|
||||
stroke: #F7C223;
|
||||
}
|
||||
75% {
|
||||
stroke: #1B9A59;
|
||||
}
|
||||
100% {
|
||||
stroke: #4285F4;
|
||||
}
|
||||
}
|
||||
@-webkit-keyframes dash {
|
||||
0% {
|
||||
stroke-dashoffset: 187;
|
||||
}
|
||||
50% {
|
||||
stroke-dashoffset: 46.75;
|
||||
transform: rotate(135deg);
|
||||
}
|
||||
100% {
|
||||
stroke-dashoffset: 187;
|
||||
transform: rotate(450deg);
|
||||
}
|
||||
}
|
||||
@keyframes dash {
|
||||
0% {
|
||||
stroke-dashoffset: 187;
|
||||
}
|
||||
50% {
|
||||
stroke-dashoffset: 46.75;
|
||||
transform: rotate(135deg);
|
||||
}
|
||||
100% {
|
||||
stroke-dashoffset: 187;
|
||||
transform: rotate(450deg);
|
||||
}
|
||||
}
|
@ -166,15 +166,15 @@ input,
|
||||
optgroup,
|
||||
select,
|
||||
textarea {
|
||||
font-family: inherit; /* 1 */
|
||||
font-feature-settings: inherit; /* 1 */
|
||||
font-variation-settings: inherit; /* 1 */
|
||||
font-size: 100%; /* 1 */
|
||||
font-weight: inherit; /* 1 */
|
||||
line-height: inherit; /* 1 */
|
||||
color: inherit; /* 1 */
|
||||
margin: 0; /* 2 */
|
||||
padding: 0; /* 3 */
|
||||
font-family: inherit;
|
||||
font-feature-settings: inherit;
|
||||
font-variation-settings: inherit;
|
||||
font-size: 100%;
|
||||
font-weight: inherit;
|
||||
line-height: inherit;
|
||||
color: inherit;
|
||||
margin: 1em;
|
||||
padding: 1em;
|
||||
}
|
||||
|
||||
/*
|
||||
|
3
redis.js
3
redis.js
@ -2,6 +2,7 @@ const redis = require('redis');
|
||||
|
||||
const redisHost = process.env.REDIS_HOST ? process.env.REDIS_HOST : 'localhost';
|
||||
const redisPort = process.env.REDIS_PORT ? process.env.REDIS_PORT : 6379;
|
||||
|
||||
const url = `redis://${redisHost}:${redisPort}`;
|
||||
const redisClient = redis.createClient({
|
||||
url,
|
||||
@ -9,7 +10,7 @@ const redisClient = redis.createClient({
|
||||
});
|
||||
|
||||
(async () => {
|
||||
redisClient.connect()
|
||||
redisClient.connect().catch(console.error)
|
||||
})();
|
||||
|
||||
module.exports = {
|
||||
|
71
utils/extract-i18n.js
Normal file
71
utils/extract-i18n.js
Normal file
@ -0,0 +1,71 @@
|
||||
#!/usr/bin/env node
|
||||
const fs = require('fs');
|
||||
|
||||
const path = require('path');
|
||||
|
||||
const messagePattern = /\_\_\('([^']+)'\)/g;
|
||||
|
||||
function getSourceFiles(dirs) {
|
||||
let sourceFiles = [];
|
||||
for (let dir of dirs) {
|
||||
let files = fs.readdirSync(dir);
|
||||
let sourceFilesDir = files.filter(file => file.endsWith('.pug') || file.endsWith('.js'));
|
||||
sourceFilesDir = sourceFilesDir.map(file => path.join(dir, file));
|
||||
sourceFiles = [...sourceFiles, ...sourceFilesDir];
|
||||
}
|
||||
return sourceFiles;
|
||||
}
|
||||
|
||||
function extractAllMessages(files) {
|
||||
let allMessages = [];
|
||||
files.forEach(file => {
|
||||
const content = fs.readFileSync(file, 'utf8');
|
||||
messages = extractMessages(content);
|
||||
allMessages = allMessages.concat(messages);
|
||||
});
|
||||
return allMessages;
|
||||
}
|
||||
|
||||
function extractMessages(content) {
|
||||
let messages = [];
|
||||
let match;
|
||||
|
||||
while (match = messagePattern.exec(content)) {
|
||||
messages.push(match[1]);
|
||||
}
|
||||
return messages;
|
||||
}
|
||||
|
||||
function dumps(dir, locales, messages) {
|
||||
const uniqueMessages = [...new Set(messages)];
|
||||
locales.forEach(lang => {
|
||||
const file = path.join(dir, 'locales', lang + '.json');
|
||||
let content = JSON.parse(fs.readFileSync(file, 'utf8'));
|
||||
uniqueMessages.forEach(message => {
|
||||
if (!content[message]) {
|
||||
content[message] = `__${message}`;
|
||||
}
|
||||
});
|
||||
fs.writeFileSync(file, JSON.stringify(content, null, 2));
|
||||
});
|
||||
}
|
||||
|
||||
const extractor = {
|
||||
run(dirs) {
|
||||
const files = getSourceFiles(dirs);
|
||||
const messages = extractAllMessages(files);
|
||||
return messages;
|
||||
},
|
||||
|
||||
dumps
|
||||
};
|
||||
|
||||
|
||||
const [,, ...args] = process.argv
|
||||
|
||||
const DIRS = ["controllers", "views", "views/auth"];
|
||||
|
||||
const LOCALES = args.slice(args.length) || ['en'];
|
||||
|
||||
const keys = extractor.run(DIRS);
|
||||
extractor.dumps('.', LOCALES, keys);
|
2
utils/translate.sh
Normal file
2
utils/translate.sh
Normal file
@ -0,0 +1,2 @@
|
||||
#!/usr/bin/env bash
|
||||
node ./utils/extract-i18n.js views fr de es
|
@ -1,8 +1,8 @@
|
||||
extends layout.pug
|
||||
|
||||
block content
|
||||
p #{ __("SoundBirder is an open-source web application to learn bird song identification. It is based on bird records from Xeno-Canto and data from eBird.") }
|
||||
p #{ __('SoundBirder is an open-source web application to learn bird song identification. It is based on bird records from Xeno-Canto and data from eBird.') }
|
||||
|
||||
h2 #{ __("Author") }
|
||||
p #{ __("The project is made with ♥ by Samuel ORTION.") }
|
||||
h2 #{ __('Author') }
|
||||
p #{ __('The project is made with ♥ by Samuel Ortion.') }
|
||||
|
||||
|
@ -5,6 +5,11 @@
|
||||
i(data-feather="map-pin")
|
||||
button.button.start-button
|
||||
i(data-feather="play")
|
||||
.game-quizz.none
|
||||
.game-loading
|
||||
<svg class="spinner" width="65px" height="65px" viewBox="0 0 66 66" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle class="path" fill="none" stroke-width="6" stroke-linecap="round" cx="33" cy="33" r="30"></circle>
|
||||
</svg>
|
||||
.game-quizz-step.none
|
||||
ul.proposals
|
||||
audio(controls).none
|
||||
@ -16,7 +21,10 @@
|
||||
span.sci
|
||||
button.button.restart-button
|
||||
i(data-feather="repeat")
|
||||
button.button.map-button
|
||||
i(data-feather="map")
|
||||
link(rel="stylesheet" href="/dist/leaflet/leaflet.css")
|
||||
link(rel="stylesheet" href="/stylesheets/spinner.css")
|
||||
script(src="/dist/leaflet/leaflet.js")
|
||||
script(src="/dist/axios/axios.min.js")
|
||||
script(src="/javascripts/game.js" type="module")
|
@ -14,17 +14,19 @@ html
|
||||
- var i18n_prefix = locale ? '/' + locale : ''
|
||||
ul.flex.flex-row.text-center.justify-evenly
|
||||
li
|
||||
a(href=`${i18n_prefix}/`) #{ __("Game") }
|
||||
a(href=`${i18n_prefix}/`) #{ __('Game') }
|
||||
li
|
||||
a(href=`${i18n_prefix}/about`) #{ __("About") }
|
||||
a(href=`${i18n_prefix}/about`) #{ __('About') }
|
||||
header
|
||||
h1= title
|
||||
main
|
||||
block content
|
||||
footer.w-full.bg-black.text-white.text-center.p-5
|
||||
.description
|
||||
.copyright Copyright © 2022 -
|
||||
.copyright
|
||||
a(href="https://fsf.org/") 🄯
|
||||
| 2022 -
|
||||
span.author
|
||||
a(href="https://samuel.ortion.fr" class="link") Samuel ORTION
|
||||
a(href="https://samuel.ortion.fr" class="link") Samuel Ortion
|
||||
script(src="/javascripts/app.js" type="module")
|
||||
script(src="/dist/feather/feather.min.js")
|
Loading…
Reference in New Issue
Block a user