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();
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));

View File

@ -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}`);

View File

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

View File

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

View File

@ -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) {
return cached;
if (cached != []) {
return cached;
} else {
console.log("Empty cached species list, retrieving again");
}
} else {
return eBird.product.spplist.in(regionCode)()
.then(species => {

View File

@ -1,4 +1,5 @@
{
"Game": "Spiel",
"About": "About"
"Game": "Spiel",
"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",
"About": "Acerca de"
"Home": "Inicio",
"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",
"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",
"Correct!": "Correct!",
"Wrong!": "Incorrect!",
"It was a": "C'était un"
"Game": "Jeu",
"About": "À propos",
"Contact": "Contact",
"Author": "Auteur",
"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",
"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 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');

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,
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;
}
/*

View File

@ -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
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
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.') }

View File

@ -5,18 +5,26 @@
i(data-feather="map-pin")
button.button.start-button
i(data-feather="play")
.game-quizz-step.none
ul.proposals
audio(controls).none
.game-results-step.none
p.message
.species.answer
p #{ __('It was a') }
span.com
span.sci
.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
.game-results-step.none
p.message
.species.answer
p #{ __('It was a') }
span.com
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")

View File

@ -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 &copy; 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")