feat: Add new i18n

This commit is contained in:
Samuel Ortion 2024-01-08 23:02:04 +01:00
parent 40079e1145
commit 3e28cb35e9
18 changed files with 270 additions and 72 deletions

14
app.js
View File

@ -1,4 +1,8 @@
require('dotenv').config(); 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 createError = require('http-errors');
const express = require('express'); const express = require('express');
const session = require('express-session'); const session = require('express-session');
@ -34,11 +38,11 @@ const sess = {
cookie: { secure: false } cookie: { secure: false }
} }
if (app.get('env') === 'production') { // if (app.get('env') === 'production') {
app.set('trust proxy', 1); // trust first proxy // app.set('trust proxy', 1); // trust first proxy
sess.cookie.secure = true; // serve secure cookies // sess.cookie.secure = true; // serve secure cookies
sess.store = new RedisStore({ client: redisClient }); // sess.store = new RedisStore({ host: redisHost, port: redisPort, client: redisClient });
} // }
app.use(session(sess)); app.use(session(sess));

View File

@ -9,7 +9,7 @@ const QUIZZ_SIZE = process.env.QUIZZ_SIZE ? process.env.QUIZZ_SIZE : 5;
function getHome(req, res) { function getHome(req, res) {
res.render('api', { res.render('api', {
title: "SoundBirder api", title: "SoundBirder API",
version: 0 version: 0
}); });
} }
@ -24,7 +24,7 @@ async function region(req, res) {
}) })
} }
function quizz(req, res) { async function quizz(req, res) {
debug('Generating quizz'); debug('Generating quizz');
const { region } = req.query; const { region } = req.query;
// debug(`Coordinates: ${lat}, ${lng}`); // debug(`Coordinates: ${lat}, ${lng}`);

View File

@ -8,7 +8,7 @@ function indexPage(req, res) {
function loginPage(req, res) { function loginPage(req, res) {
res.render('auth/login', { res.render('auth/login', {
title: req.i18n.__("SoundBirder - Login"), title: req.i18n.__('SoundBirder - Login'),
locale: req.i18n.locale locale: req.i18n.locale
}); });
} }
@ -23,7 +23,7 @@ function logout(req, res) {
function registerPage(req, res) { function registerPage(req, res) {
res.render('auth/register', { res.render('auth/register', {
title: req.i18n.__("SoundBirder - Register"), title: req.i18n.__('SoundBirder - Register'),
locale: req.i18n.locale locale: req.i18n.locale
}); });
} }

View File

@ -9,7 +9,7 @@ function getIndex(req, res) {
function getAbout(req, res) { function getAbout(req, res) {
res.render('about', { res.render('about', {
title: 'About SoundBirder', title: req.i18n.__('About SoundBirder'),
locale: req.i18n.locale locale: req.i18n.locale
}); });
} }

View File

@ -46,9 +46,6 @@ async function generateQuizz(region, locale, size) {
let answer; let answer;
do { do {
answer = choice(speciesSelectionLocalized); answer = choice(speciesSelectionLocalized);
if (answer === undefined) {
debug('Species', speciesSelectionLocalized);
}
quizz.answer = answer; quizz.answer = answer;
quizz.audio = await getAudio(answer.sciName); quizz.audio = await getAudio(answer.sciName);
if (quizz.audio === undefined) { if (quizz.audio === undefined) {
@ -89,8 +86,9 @@ async function getLocalizedNames(speciesCodes, locale) {
console.error(error); console.error(error);
} }
if (!flag) { if (!flag) {
let names;
try { try {
const names = { speciesCode, sciName, comName } = await getLocalizedName(code, locale); names = { speciesCode, sciName, comName } = await getLocalizedName(code, locale);
cache.cacheResponse(`${code}-${locale}`, names); cache.cacheResponse(`${code}-${locale}`, names);
allNames.push(names); allNames.push(names);
} catch (error) { } catch (error) {
@ -121,7 +119,11 @@ function getLocalizedName(speciesCode, locale) {
async function getSpeciesList(regionCode) { async function getSpeciesList(regionCode) {
const cached = await cache.getCached(`spplist-${regionCode}`); const cached = await cache.getCached(`spplist-${regionCode}`);
if (cached) { if (cached) {
return cached; if (cached != []) {
return cached;
} else {
console.log("Empty cached species list, retrieving again");
}
} else { } else {
return eBird.product.spplist.in(regionCode)() return eBird.product.spplist.in(regionCode)()
.then(species => { .then(species => {

View File

@ -1,4 +1,5 @@
{ {
"Game": "Spiel", "Game": "Spiel",
"About": "About" "About": "About",
"It was a": "__It was a"
} }

16
locales/en.json Executable file → Normal file
View 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"
}

View File

@ -1,4 +1,9 @@
{ {
"Home": "Inicio", "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"
} }

19
locales/fr.json Executable file → Normal file
View File

@ -1,11 +1,12 @@
{ {
"Game": "Jeu", "Game": "Jeu",
"About": "À propos", "About": "À propos",
"Contact": "Contact", "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",
"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 !",
"Correct!": "Correct!", "Wrong!": "Incorrect !",
"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."
} }

View File

@ -5,18 +5,24 @@ let map = document.getElementById('map');
let startButton = document.querySelector('.game .start-button'); let startButton = document.querySelector('.game .start-button');
let gameMapStep = document.querySelector('.game-map-step'); let gameMapStep = document.querySelector('.game-map-step');
let gameQuizzStep = document.querySelector('.game-quizz-step'); let gameQuizzStep = document.querySelector('.game-quizz-step');
let gameQuizz = document.querySelector('.game-quizz');
let gameResultStep = document.querySelector('.game-results-step'); let gameResultStep = document.querySelector('.game-results-step');
let gameLoading = document.querySelector('.game-loading');
let audio = document.querySelector('.game-quizz-step audio'); let audio = document.querySelector('.game-quizz-step audio');
let proposals = document.querySelector('.game-quizz-step .proposals'); let proposals = document.querySelector('.game-quizz-step .proposals');
let result = document.querySelector('.game-results-step .message'); 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 resultComName = document.querySelector('.game-results-step .species .com');
let resultSciName = document.querySelector('.game-results-step .species .sci'); let resultSciName = document.querySelector('.game-results-step .species .sci');
const API_VERSION = "0";
let region = 'FR'; let region = 'FR';
function returnToMap() {
gameMapStep.classList.remove("none");
gameQuizz.classList.add("none");
}
function geolocationStep() { function geolocationStep() {
if (map != undefined) if (map != undefined)
geolocationHandler(); geolocationHandler();
@ -37,8 +43,11 @@ function regionCoder() {
} }
function quizzStep() { function quizzStep() {
gameQuizz.classList.remove("none");
gameLoading.classList.remove("none");
client.getQuizz(region) client.getQuizz(region)
.then(quizz => { .then(quizz => {
gameLoading.classList.add("none");
displayQuizz(quizz); displayQuizz(quizz);
}).catch(error => { }).catch(error => {
console.log(error); console.log(error);
@ -47,6 +56,10 @@ function quizzStep() {
function displayQuizz(quizz) { function displayQuizz(quizz) {
if (quizz.audio == undefined) {
quizzStep();
return;
}
audio.src = quizz.audio; audio.src = quizz.audio;
audio.classList.remove("none"); // Display the audio controls audio.classList.remove("none"); // Display the audio controls
gameQuizzStep.classList.remove("none"); gameQuizzStep.classList.remove("none");
@ -82,6 +95,7 @@ function verifyAnswer(event) {
}); });
} }
restartButton.addEventListener('click', restart); restartButton.addEventListener('click', restart);
mapButton.addEventListener('click', returnToMap);
function displayResult(message_class, message, species) { function displayResult(message_class, message, species) {
gameQuizzStep.classList.toggle('none'); gameQuizzStep.classList.toggle('none');

View 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);
}
}

View File

@ -166,15 +166,15 @@ input,
optgroup, optgroup,
select, select,
textarea { textarea {
font-family: inherit; /* 1 */ font-family: inherit;
font-feature-settings: inherit; /* 1 */ font-feature-settings: inherit;
font-variation-settings: inherit; /* 1 */ font-variation-settings: inherit;
font-size: 100%; /* 1 */ font-size: 100%;
font-weight: inherit; /* 1 */ font-weight: inherit;
line-height: inherit; /* 1 */ line-height: inherit;
color: inherit; /* 1 */ color: inherit;
margin: 0; /* 2 */ margin: 1em;
padding: 0; /* 3 */ padding: 1em;
} }
/* /*

View File

@ -2,6 +2,7 @@ const redis = require('redis');
const redisHost = process.env.REDIS_HOST ? process.env.REDIS_HOST : 'localhost'; const redisHost = process.env.REDIS_HOST ? process.env.REDIS_HOST : 'localhost';
const redisPort = process.env.REDIS_PORT ? process.env.REDIS_PORT : 6379; const redisPort = process.env.REDIS_PORT ? process.env.REDIS_PORT : 6379;
const url = `redis://${redisHost}:${redisPort}`; const url = `redis://${redisHost}:${redisPort}`;
const redisClient = redis.createClient({ const redisClient = redis.createClient({
url, url,
@ -9,7 +10,7 @@ const redisClient = redis.createClient({
}); });
(async () => { (async () => {
redisClient.connect() redisClient.connect().catch(console.error)
})(); })();
module.exports = { module.exports = {

71
utils/extract-i18n.js Normal file
View 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
View File

@ -0,0 +1,2 @@
#!/usr/bin/env bash
node ./utils/extract-i18n.js views fr de es

View File

@ -1,8 +1,8 @@
extends layout.pug extends layout.pug
block content 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") } h2 #{ __('Author') }
p #{ __("The project is made with ♥ by Samuel ORTION.") } p #{ __('The project is made with ♥ by Samuel Ortion.') }

View File

@ -5,18 +5,26 @@
i(data-feather="map-pin") i(data-feather="map-pin")
button.button.start-button button.button.start-button
i(data-feather="play") i(data-feather="play")
.game-quizz-step.none .game-quizz.none
ul.proposals .game-loading
audio(controls).none <svg class="spinner" width="65px" height="65px" viewBox="0 0 66 66" xmlns="http://www.w3.org/2000/svg">
.game-results-step.none <circle class="path" fill="none" stroke-width="6" stroke-linecap="round" cx="33" cy="33" r="30"></circle>
p.message </svg>
.species.answer .game-quizz-step.none
p #{ __('It was a') } ul.proposals
span.com audio(controls).none
span.sci .game-results-step.none
p.message
.species.answer
p #{ __('It was a') }
span.com
span.sci
button.button.restart-button button.button.restart-button
i(data-feather="repeat") i(data-feather="repeat")
button.button.map-button
i(data-feather="map")
link(rel="stylesheet" href="/dist/leaflet/leaflet.css") link(rel="stylesheet" href="/dist/leaflet/leaflet.css")
link(rel="stylesheet" href="/stylesheets/spinner.css")
script(src="/dist/leaflet/leaflet.js") script(src="/dist/leaflet/leaflet.js")
script(src="/dist/axios/axios.min.js") script(src="/dist/axios/axios.min.js")
script(src="/javascripts/game.js" type="module") script(src="/javascripts/game.js" type="module")

View File

@ -14,17 +14,19 @@ html
- var i18n_prefix = locale ? '/' + locale : '' - var i18n_prefix = locale ? '/' + locale : ''
ul.flex.flex-row.text-center.justify-evenly ul.flex.flex-row.text-center.justify-evenly
li li
a(href=`${i18n_prefix}/`) #{ __("Game") } a(href=`${i18n_prefix}/`) #{ __('Game') }
li li
a(href=`${i18n_prefix}/about`) #{ __("About") } a(href=`${i18n_prefix}/about`) #{ __('About') }
header header
h1= title h1= title
main main
block content block content
footer.w-full.bg-black.text-white.text-center.p-5 footer.w-full.bg-black.text-white.text-center.p-5
.description .description
.copyright Copyright &copy; 2022 - .copyright
a(href="https://fsf.org/") 🄯
| 2022 -
span.author 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="/javascripts/app.js" type="module")
script(src="/dist/feather/feather.min.js") script(src="/dist/feather/feather.min.js")