Compare commits
4 Commits
2ccee904da
...
b5ef4a9bb5
Author | SHA1 | Date |
---|---|---|
Robbie Antenesse | b5ef4a9bb5 | |
Robbie Antenesse | f76c715aba | |
Robbie Antenesse | 4670e046bb | |
Robbie Antenesse | 0f9757f2c2 |
|
@ -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
|
||||
});
|
||||
})
|
||||
});
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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>
|
|
@ -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,
|
||||
];
|
||||
}
|
|
@ -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>`;
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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;
|
|
@ -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!
|
|
@ -0,0 +1,5 @@
|
|||
# Community Policy
|
||||
|
||||
This Readlebee hive has the following community policy:
|
||||
|
||||
Something good.
|
|
@ -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;
|
|
@ -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'));
|
||||
|
|
|
@ -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"
|
||||
|
|
Loading…
Reference in New Issue