Compare commits

...

4 Commits

15 changed files with 202 additions and 52 deletions

View File

@ -14,11 +14,15 @@ export const appListeners = (app, state, emitter) => {
emitter.on('set-language', newLanguage => {
app.setSettingsItem('lang', newLanguage);
state.language = newLanguage;
emitter.emit('render');
state.i18n.fetchLocaleUI().then(() => {
emitter.emit('render');
});
});
app.checkIfLoggedIn(state).then(isLoggedIn => {
emitter.emit('render'); // This should hopefully only run once after the DOM is loaded. It prevents routing issues where 'render' hasn't been defined yet
});
state.i18n.fetchLocaleUI().then(() => {
app.checkIfLoggedIn(state).then(isLoggedIn => {
emitter.emit('render'); // This should hopefully only run once after the DOM is loaded. It prevents routing issues where 'render' hasn't been defined yet
});
})
});
}

View File

@ -1,5 +1,6 @@
import { globalView } from './views/global';
import { homeView } from './views/home';
import { aboutView } from './views/about';
import { loginView } from './views/login';
import { searchView } from './views/search';
import { errorView } from './views/404';
@ -7,6 +8,8 @@ import { errorView } from './views/404';
export const appRoutes = (app) => {
app.route('/', (state, emit) => globalView(state, emit, homeView));
app.route('/about', (state, emit) => globalView(state, emit, aboutView));
app.route('/login', (state, emit) => globalView(state, emit, loginView));
app.route('/logout', () => window.location.reload()); // If Choo navigates here, refresh the page instead so the server can handle it and log out

59
app/i18n.js Normal file
View File

@ -0,0 +1,59 @@
export class I18n {
constructor(appState) {
this.appState = appState;
this.availableLanguages = null;
this.language = null;
this.default = null;
this.pages = {};
}
get needsFetch () {
return !this.availableLanguages && !this.language && !this.default;
}
fetchLocaleUI () {
return fetch(`/locales/${this.appState.language}/ui`).then(response => response.json()).then(response => {
this.availableLanguages = response.available;
this.default = response.default;
this.language = response.locale;
}).catch(error => {
console.error(error);
});
}
fetchLocalePage (page) {
return fetch(`/locales/${this.appState.language}/page/${page}`).then(response => response.text()).then(response => {
this.pages[page] = response;
}).catch(error => {
console.error(error);
});
}
translate (section, phrase) {
let result;
let language = this.default;
if (!this.needsFetch && this.appState.language !== this.language.locale) {
console.warn(`The target language (${this.appState.language}) does not exist. Defaulting to ${this.default.name} (${this.default.locale}).`);
} else if (typeof this.language[section] == 'undefined' || typeof this.language[section][phrase] == 'undefined') {
console.warn(`The translation for "${section}.${phrase}" is not set in the ${this.language.locale} locale. Using ${this.default.name} (${this.default.locale}) instead.`);
} else {
language = this.language;
}
if (typeof language[section] !== 'undefined' && typeof language[section][phrase] !== 'undefined') {
result = language[section][phrase];
} else {
console.error(`The translation for "${section}.${phrase}" is set up in neither the target nor default locale.`);
result = `${section}.${phrase}`;
}
return result;
}
__ (translation) {
const pieces = translation.split('.');
const result = this.translate(pieces[0], pieces[1]);
return result;
}
}

View File

@ -1,44 +0,0 @@
import en from './locales/en.json';
export class I18n {
constructor(appState) {
// Available languages should be kept up to date with the available locales.
this.availableLanguages = {
default: en,
en,
};
this.appState = appState;
}
get language () {
return this.appState.language;
}
translate (section, phrase) {
let result;
let language = this.availableLanguages.default;
if (typeof this.availableLanguages[this.language] == 'undefined') {
console.warn(`The target language (${this.language}) does not exist. Defaulting to ${this.availableLanguages.default.name} (${this.availableLanguages.default.locale}).`);
} else if (typeof this.availableLanguages[this.language][section] == 'undefined' || typeof this.availableLanguages[this.language][section][phrase] == 'undefined') {
console.warn(`The translation for "${section}.${phrase}" is not set in the ${this.language} locale. Using ${this.availableLanguages.default.name} (${this.availableLanguages.default.locale}) instead.`);
} else {
language = this.availableLanguages[this.language];
}
if (typeof language[section] !== 'undefined' && typeof language[section][phrase] !== 'undefined') {
result = language[section][phrase];
} else {
console.error(`The translation for "${section}.${phrase}" is set up in neither the target nor default locale.`);
result = `${section}.${phrase}`;
}
return result;
}
__ (translation) {
const pieces = translation.split('.');
const result = this.translate(pieces[0], pieces[1]);
return result;
}
}

View File

@ -21,6 +21,6 @@
</head>
<!-- Choo replaces the body tag with the app. -->
<body>Loading...</body>
<body><i class="icon-loading animate-spin"></i></body>
</html>

22
app/views/about.js Normal file
View File

@ -0,0 +1,22 @@
import html from 'choo/html';
export const aboutView = (state, emit, i18n) => {
const content = html`<section class="content"><i class="icon-loading animate-spin"></i></section>`;
const community = html`<section class="content"></section>`;
const promises = [];
if (typeof i18n.pages.about === 'undefined' || typeof i18n.pages.community === 'undefined') {
promises.push(i18n.fetchLocalePage('about'));
promises.push(i18n.fetchLocalePage('community'));
} else {
content.innerHTML = i18n.pages.about;
community.innerHTML = i18n.pages.community;
}
if (promises.length > 0) {
Promise.all(promises).then(fulfilled => emit('render'));
}
return [
content,
community,
];
}

View File

@ -4,6 +4,9 @@ import headerImage from '../../dev/images/header.png';
export const globalView = (state, emit, view) => {
const { i18n } = state;
if (i18n.needsFetch) {
return html`<body><i class="icon-loading animate-spin"></i></body>`;
}
// Create a wrapper for view content that includes global header/footer
return html`<body>
<header>
@ -55,9 +58,7 @@ export const globalView = (state, emit, view) => {
<label class="flex">
<span class="third">${i18n.__('global.change_language')}:</span>
<select class="two-third" onchange=${e => emit('set-language', e.target.value)}>
${Object.keys(i18n.availableLanguages).map(languageKey => {
if (languageKey === 'default') return null;
const language = i18n.availableLanguages[languageKey];
${i18n.availableLanguages.map(language => {
return html`<option value=${language.locale} ${state.language === language.locale ? 'selected' : null}>
${language.name}
</option>`;

View File

@ -41,6 +41,7 @@
"fastify-sequelize": "^1.0.4",
"fastify-static": "^2.5.0",
"make-promises-safe": "^5.0.0",
"marked": "^0.7.0",
"mysql2": "^1.7.0",
"node-fetch": "^2.6.0",
"pg": "^7.12.1",

50
server/i18n/index.js Normal file
View File

@ -0,0 +1,50 @@
const fp = require('fastify-plugin');
const fs = require('fs');
const path = require('path');
const marked = require('marked');
async function plugin (fastify, opts, done) {
const i18n = {
available: [],
pages: {},
};
try {
const locales = fs.readdirSync(path.resolve(__dirname, './locales'));
locales
.filter(file => !file.split().every(letter => letter === '.')) // Filter out relative folders
.forEach(locale => {
try {
const ui = fs.readFileSync(path.resolve(__dirname, `./locales/${locale}/ui.json`)),
about = fs.readFileSync(path.resolve(__dirname, `./locales/${locale}/pages/about.md`)),
community = fs.readFileSync(path.resolve(__dirname, `./locales/${locale}/pages/community.md`));
i18n[locale] = JSON.parse(ui.toString());
i18n.available.push({
name: i18n[locale].name,
locale: i18n[locale].locale,
});
i18n.pages[locale] = {
about: marked(about.toString()),
community: marked(community.toString()),
}
} catch (ex) {
console.error('Encountered a problem with locale.\n', ex);
}
});
// Set the default language to English after parsing locales because it has the most coverage.
i18n.default = i18n.en;
} catch (ex) {
console.error('Could not get locales folder.\n', ex);
}
fastify.decorate('i18n', i18n);
fastify.register(require(path.resolve(__dirname, './routes'))); // Self-register the routing for fetching locales
done();
}
module.exports = plugin;

View File

@ -0,0 +1,11 @@
# About Readlebee
Readlebee is a social network for people who read books!
You can:
- Keep track of progress on books that you are reading,
- Save arbitrary lists of books with Shelves,
- Give books ratings and reviews,
- Send and receive recommendations for books you think others would like,
- And interact with the reviews and updates of your friends!

View File

@ -0,0 +1,5 @@
# Community Policy
This Readlebee hive has the following community policy:
Something good.

31
server/i18n/routes.js Normal file
View File

@ -0,0 +1,31 @@
async function routes(fastify, options) {
fastify.get('/locales/:locale/ui', async (request, reply) => {
const response = {
available: fastify.i18n.available,
default: fastify.i18n.default,
};
if (typeof fastify.i18n[request.params.locale] == 'undefined') {
console.warn(`The target language (${request.params.locale}) does not exist. Defaulting to ${fastify.i18n.default.name} (${fastify.i18n.default.locale}).`);
response.locale = fastify.i18n.default;
} else {
response.locale = fastify.i18n[request.params.locale];
}
return response;
});
fastify.get('/locales/:locale/page/:page', async (request, reply) => {
if (typeof fastify.i18n.pages[request.params.locale] == 'undefined') {
console.warn(`The target language (${request.params.locale}) does not exist. Defaulting to ${fastify.i18n.default.name} (${fastify.i18n.default.locale}).`);
if (typeof fastify.i18n.pages[fastify.i18n.default.locale][request.params.page] == 'undefined') {
console.error(`The target page (${request.params.page}) does not exist. Returning blank.`);
return request.params.page;
}
return fastify.i18n.pages[fastify.i18n.default.locale][request.params.page];
}
return fastify.i18n.pages[request.params.locale][request.params.page];
});
}
module.exports = routes;

View File

@ -84,6 +84,8 @@ fastify.addHook('onRequest', async (request, reply) => {
}
});
// Store i18n files in fastify object and register locales routes
fastify.register(require('./i18n'));
// Routes
fastify.register(require('./routes/public'));

View File

@ -4165,6 +4165,11 @@ map-visit@^1.0.0:
dependencies:
object-visit "^1.0.0"
marked@^0.7.0:
version "0.7.0"
resolved "https://registry.yarnpkg.com/marked/-/marked-0.7.0.tgz#b64201f051d271b1edc10a04d1ae9b74bb8e5c0e"
integrity sha512-c+yYdCZJQrsRjTPhUx7VKkApw9bwDkNbHUKo1ovgcfDjb2kc8rLuRbIFyXL5WOEUwzSSKo3IXpph2K6DqB/KZg==
md5.js@^1.3.4:
version "1.3.5"
resolved "https://registry.yarnpkg.com/md5.js/-/md5.js-1.3.5.tgz#b5d07b8e3216e3e27cd728d72f70d1e6a342005f"