From 0f9757f2c210e07886b536fe10c6e92fcba456b1 Mon Sep 17 00:00:00 2001 From: Robbie Antenesse Date: Sun, 27 Oct 2019 12:48:28 -0600 Subject: [PATCH] Put i18n on server so it can free up frontend memory --- package.json | 1 + server/i18n/index.js | 50 +++++++++++ server/i18n/locales/en/pages/about.md | 11 +++ server/i18n/locales/en/pages/community.md | 5 ++ server/i18n/locales/en/ui.json | 100 ++++++++++++++++++++++ server/i18n/routes.js | 31 +++++++ server/index.js | 2 + yarn.lock | 5 ++ 8 files changed, 205 insertions(+) create mode 100644 server/i18n/index.js create mode 100644 server/i18n/locales/en/pages/about.md create mode 100644 server/i18n/locales/en/pages/community.md create mode 100644 server/i18n/locales/en/ui.json create mode 100644 server/i18n/routes.js diff --git a/package.json b/package.json index b3a2b98..dd7bb1d 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/server/i18n/index.js b/server/i18n/index.js new file mode 100644 index 0000000..8313de6 --- /dev/null +++ b/server/i18n/index.js @@ -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; \ No newline at end of file diff --git a/server/i18n/locales/en/pages/about.md b/server/i18n/locales/en/pages/about.md new file mode 100644 index 0000000..62a6102 --- /dev/null +++ b/server/i18n/locales/en/pages/about.md @@ -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! diff --git a/server/i18n/locales/en/pages/community.md b/server/i18n/locales/en/pages/community.md new file mode 100644 index 0000000..432708f --- /dev/null +++ b/server/i18n/locales/en/pages/community.md @@ -0,0 +1,5 @@ +# Community Policy + +This Readlebee hive has the following community policy: + +Something good. \ No newline at end of file diff --git a/server/i18n/locales/en/ui.json b/server/i18n/locales/en/ui.json new file mode 100644 index 0000000..0fed5d7 --- /dev/null +++ b/server/i18n/locales/en/ui.json @@ -0,0 +1,100 @@ +{ + "name": "English", + "locale": "en", + "global": { + "menu_search": "Search for Books", + "menu_about": "About", + "menu_login": "Log In / Create Account", + "menu_account": "My Profile", + "menu_logout": "Log Out", + "footer_repo": "Repo", + "footer_chat": "Chat", + "change_language": "Change Language" + }, + "home": { + "logged_out_subtitle": "All the Book Buzz in Once Place", + "logged_out_track_books": "Keep track of books you've read, want to read, and are currently reading.", + "logged_out_share_friends": "Share your thoughts about what you're reading and see what your friends think of their books.", + "logged_out_read_rate": "Rate, review, and recommmend books or something. I dunno. It's early days, my friends!", + "logged_out_community_header": "A Look Inside the Hive", + "logged_out_recent_reviews": "Recent Reviews", + "logged_out_recent_updates": "Recent Updates", + "logged_out_join_now": "Join Now!", + "logged_in_subtitle": "Welcome!", + "logged_in_updates": "Updates", + "logged_in_interactions": "Interactions" + }, + "404": { + "header": "Oops!", + "subheader": "It looks like the page you requested doesn't exist. Please try a different one!" + }, + "login": { + "log_in": "Log In", + "email": "Email", + "password": "Password", + "login_button": "Log In!", + "create_account": "Create a New Account", + "confirm_password": "Confirm Password", + "username": "Username", + "display_name": "Display Name", + "create_account_button": "Create Account!", + "login_required_field_blank": "You must enter both a valid email address and password.", + "create_required_field_blank": "You must complete all required fields.", + "create_password_confirm_mismatch": "Both password fields must match." + }, + "search": { + "header": "Search", + "placeholder": "Search for Books", + "button_text": "Search", + "search_source_label": "Search Source", + "search_source_help_button": "What's This?", + "search_source_help_header": "What does \"Search Source\" mean?", + "search_source_help_text": "This refers to where the search tries to look for data. Each source can be easily contributed to if you want to add missing books or correct errors.", + "search_source_help_inventaire": "Sources and extends data from WikiData, the service structure that powers Wikipedia and the like. Offers as many language options as possible.", + "search_source_help_openLibrary": "Sources data from Internet Archive and may provide a digital copy to read. Only offers English unless a work is in another language.", + "search_by_label": "Search By", + "search_by_title": "Title", + "search_by_author": "Author", + "loading": "Loading...", + "no_results": "None Found", + "no_results_suggestion": "If you're expecting book data, go and help fill out the Inventaire database!", + "people_header": "People", + "series_header": "Series", + "books_header": "Books", + "see_interaction_details": "See All Interactions", + "see_book_details": "See Book Details" + }, + "interaction": { + "required": "Required", + "reload": "Reload", + "heart": "Like", + "add": "Add to Shelf", + "average_rating": "Average Rating", + "reviews_written": "Total Reviews Written" + }, + "api": { + "account_already_logged_in": "You are already logged in! You cannot create an account or log in again.", + "account_create_required_data_missing": "Could not create account because required data is missing.", + "account_create_invalid_email": "The email address entered is not valid.", + "account_create_invalid_username": "The username entered is not valid. Usernames must be at least 2 characters long and can only contain letters a–z, numbers 0–9, and underscores", + "account_email_exists": "The email address entered is already in use.", + "account_username_exists": "The username entered is already in use.", + "account_email_send_fail": "Your account was created successfully, but we were unable to send the confirmation email!", + "account_confirm_email": "A confirmation email has been sent to the address you specified. Please confirm your account using the link provided.", + "account_create_success": "Account created successfully! You may now log in using the email address and password you provided.", + "account_confirm_required_data_missing": "Could not confirm account because required data is missing.", + "account_confirm_invalid_code": "The specified confirmation code is not valid.", + "account_confirm_update_fail": "Something went wrong and we couldn't confirm your account. Please try again later!", + "account_confirm_email_send_fail": "Your account has been confirmed, but we were unable to send the email notification about it. You can log in anyway.", + "account_confirm_success_email": "Your account has been confirmed, and an email notification has been sent! You may now log in using your email address and password.", + "account_confirm_success": "Your account has been confirmed! You may now log in using your email address and password.", + "account_login_required_data_missing": "Could not attempt login because required data is missing.", + "account_login_invalid_email": "The email address specified does not have an associated account.", + "account_login_not_confirmed": "The specified account has not been confirmed. Please use the link you received to confirm your email address.", + "account_login_invalid_password": "The password specified is not correct.", + "account_login_success": "You have been successfully logged in! You will now be redirected to the home screen.", + "account_validate_missing_token": "User not logged in: There is no login token to validate.", + "account_validate_invalid_token": "User not logged in: The stored token is not a valid token.", + "account_validate_renewed_token": "User logged in, and the token has been renewed." + } +} \ No newline at end of file diff --git a/server/i18n/routes.js b/server/i18n/routes.js new file mode 100644 index 0000000..4591b8c --- /dev/null +++ b/server/i18n/routes.js @@ -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; \ No newline at end of file diff --git a/server/index.js b/server/index.js index c0f7a34..904243f 100644 --- a/server/index.js +++ b/server/index.js @@ -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')); diff --git a/yarn.lock b/yarn.lock index c49d3a0..c836c21 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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"