diff --git a/README.md b/README.md index 2122dff..08936ea 100644 --- a/README.md +++ b/README.md @@ -91,6 +91,8 @@ BULMASWATCH=default UPDATE_CHECK=true # Set to false to disable the profile pictures feature PFP=true +# Language of the interface, options listed in `languages` directory +LANGUAGE=en-US ## Wishlist Settings # Set to true to not allow users to have their own lists. You may want this for a birthday or wedding. diff --git a/config/index.js b/config/index.js index 00b2029..7f3f4e4 100644 --- a/config/index.js +++ b/config/index.js @@ -14,5 +14,6 @@ module.exports = { base: (process.env.ROOT_PATH || '/').endsWith('/') ? (process.env.ROOT_PATH || '/') : `${process.env.ROOT_PATH}/`, trustProxy: process.env.TRUST_PROXY === 'true' ? true : process.env.TRUST_PROXY || 'loopback', bulmaswatch: (process.env.BULMASWATCH || 'default').toLowerCase(), - pfp: process.env.PFP !== 'false' + pfp: process.env.PFP !== 'false', + language: process.env.LANGUAGE?.toLowerCase() || 'en-us' } diff --git a/index.js b/index.js index 3166a00..8e39333 100644 --- a/index.js +++ b/index.js @@ -12,10 +12,34 @@ const fetch = require('node-fetch') const express = require('express') _CC._ = require('lodash') +_CC.moment = require('moment/min/moment-with-locales') const config = require('./config') _CC.config = config +;(() => { + let language + try { + language = require(`./languages/${config.language}`) + } catch (error) { + if (error.message.startsWith('Cannot find module')) console.error(`Language ${config.language} is not supported. If you know this language and would like to translate Christmas Community, please ask for help doing so here: https://github.com/Wingysam/Christmas-Community/issues/new`) + else console.error(`Failed to load language ${config.language} because of ${error}`) + process.exit(1) + } + + if (_CC.moment.locale(language.momentLocale) !== language.momentLocale) { + console.error(`${_CC.moment.locale()} Failed to load language ${config.language}, moment locale missing. Valid locales: ${_CC.moment.locales().join(', ')}`) + process.exit(1) + } + + _CC.lang = (key, ...args) => { + const lang = language.strings[key] + if (!lang) return language.strings._NOT_LOCALIZED(key) + if (typeof lang === 'function') return lang(...args) + return lang + } +})() + if (!config.dbPrefix.startsWith('http')) { const mkdirp = require('mkdirp').sync mkdirp(config.dbPrefix) diff --git a/languages/en-us.js b/languages/en-us.js new file mode 100644 index 0000000..4e2d180 --- /dev/null +++ b/languages/en-us.js @@ -0,0 +1,181 @@ +module.exports.momentLocale = 'en' + +module.exports.strings = { + _NOT_LOCALIZED: key => `${key} hasn't been translated to English yet.`, + ADMIN_CLEAR_WISHLISTS_BUTTON: 'Clear all wishlists', + ADMIN_CLEAR_WISHLISTS_DESCRIPTION: 'This will instantly irreversibly delete all wishlists! Consider making a backup of the database before using this.', + ADMIN_CLEAR_WISHLISTS_HEADER: 'Wishlist Deletion', + ADMIN_SETTINGS_CLEARDB_BUTTON: 'Clear Wishlists', + ADMIN_SETTINGS_CLEARDB_DESCRIPTION: 'Warning: These options destroy data! You may want to back up the database before using these options.', + ADMIN_SETTINGS_CLEARDB_HEADER: 'Data Destruction', + ADMIN_SETTINGS_CLEARDB_SUCCESS: 'Cleared all wishlists.', + ADMIN_SETTINGS_HEADER: 'Admin Settings', + ADMIN_SETTINGS_USERS_ADD_BUTTON: 'Add User', + ADMIN_SETTINGS_USERS_ADD_HEADER: 'Add user', + ADMIN_SETTINGS_USERS_ADD_PLACEHOLDER: 'john', + ADMIN_SETTINGS_USERS_ADD_USERNAME: 'Username', + ADMIN_SETTINGS_USERS_EDIT_DELETE_FAIL_ADMIN: 'Failed to remove: user is admin.', + ADMIN_SETTINGS_USERS_EDIT_DELETE_SUCCESS: name => `Successfully removed user ${name}`, + ADMIN_SETTINGS_USERS_EDIT_DEMOTE_NOT_ADMIN: 'user is not an admin', + ADMIN_SETTINGS_USERS_EDIT_DEMOTE_SELF: 'You cannot demote yourself.', + ADMIN_SETTINGS_USERS_EDIT_DEMOTE_SUCCESS: name => `${name} is no longer an admin.`, + ADMIN_SETTINGS_USERS_EDIT_IMPERSONATE_SUCCESS: name => `You are now ${name}.`, + ADMIN_SETTINGS_USERS_EDIT_NO_USERNAME_PROVIDED: 'No username provided', + ADMIN_SETTINGS_USERS_EDIT_PROMOTE_ALREADY_ADMIN: 'user is already admin', + ADMIN_SETTINGS_USERS_EDIT_PROMOTE_DEMOTE_NOT_FOUND: 'User not found.', + ADMIN_SETTINGS_USERS_EDIT_PROMOTE_SUCCESS: name => `${name} is now an admin.`, + ADMIN_SETTINGS_USERS_EDIT_RENAMED_USER: 'Renamed user!', + ADMIN_SETTINGS_USERS_EDIT_SAME_NAME: 'Username is same as new username.', + ADMIN_SETTINGS_USERS_EDIT: 'Edit', + ADMIN_SETTINGS_USERS_HEADER: 'Users', + ADMIN_SETTINGS_VERSION_INFO: 'Version Info', + ADMIN_USER_EDIT_ACCOUNT_UNCONFIRMED: "This account hasn't been confirmed.", + ADMIN_USER_EDIT_ADMIN_ISADMIN: name => `${name} is an admin.`, + ADMIN_USER_EDIT_ADMIN_NOTADMIN: name => `${name} is not an admin.`, + ADMIN_USER_EDIT_ADMIN: 'Admin', + ADMIN_USER_EDIT_CHANGE_NAME: 'Change Name', + ADMIN_USER_EDIT_CHANGE_USERNAME: 'Change Username', + ADMIN_USER_EDIT_CONFIRMATION_LINK: 'Confirmation Link', + ADMIN_USER_EDIT_DELETE_ADMIN: 'User is admin', + ADMIN_USER_EDIT_DELETE_HEADER: 'Irreversible Deletion', + ADMIN_USER_EDIT_DELETE_USER: name => `Remove user ${name}`, + ADMIN_USER_EDIT_DEMOTE_SELF: 'You cannot demote yourself', + ADMIN_USER_EDIT_DEMOTE: name => `Demote ${name}`, + ADMIN_USER_EDIT_EDITING_USER: name => `Editing user "${name}"`, + ADMIN_USER_EDIT_GENERATE_NEW_LINK: 'Generate New Link', + ADMIN_USER_EDIT_IMPERSONATE_BUTTON: name => `Log in as ${name}`, + ADMIN_USER_EDIT_IMPERSONATE_HEADER: 'Impersonate', + ADMIN_USER_EDIT_LINK_EXPIRY_FUTURE: fromNow => `The following link expires ${fromNow}`, // fromNow is localized by moment + ADMIN_USER_EDIT_LINK_EXPIRY_PAST: fromNow => `The following link expired ${fromNow}`, + ADMIN_USER_EDIT_PROMOTE: name => `Promote ${name}`, + ADMIN_USER_EDIT_RESET_PASSWORD_HASLINK_EXPIRY_FUTURE: fromNow => `It expires ${fromNow}`, + ADMIN_USER_EDIT_RESET_PASSWORD_HASLINK_EXPIRY_PAST: fromNow => `It expired ${fromNow}`, + ADMIN_USER_EDIT_RESET_PASSWORD_HASLINK: 'There is a reset password link for this user.', + ADMIN_USER_EDIT_RESET_PASSWORD_HEADER: 'Reset Password', + ADMIN_USER_EDIT_RESET_PASSWORD_LINK_CANCEL: 'Cancel Password Reset Link', + ADMIN_USER_EDIT_RESET_PASSWORD_LINK_CREATE: 'Create Password Reset Link', + ADMIN_USER_EDIT_RESET_PASSWORD_LINK_REFRESH: 'Refresh Password Reset Link', + ADMIN_USER_EDIT_USERNAME: 'Username', + BACK_BUTTON: 'Back', + CONFIRM_ACCOUNT_EXPIRED: 'Your confirmation link has expired. Please ask for a new one.', + CONFIRM_ACCOUNT_HEADER_INVALID: `${_CC.config.siteTitle} | Confirmation Link Invalid`, + CONFIRM_ACCOUNT_HEADER_VALID: `${_CC.config.siteTitle} | Confirm Account`, + CONFIRM_ACCOUNT_INVALID: "This confirmation link isn't valid, perhaps the account was deleted or some characters at the end got cut off?", + CONFIRM_ACCOUNT_SET_PW_BUTTON: `Join ${_CC.config.siteTitle}`, + CONFIRM_ACCOUNT_SET_PW_PLACEHOLDER: 'pa$$word!', + CONFIRM_ACCOUNT_SET_PW_TEXT: name => `Hello ${name}! Please set your password here.`, + CONFIRM_ACCOUNT_SUCCESS: `Welcome to ${_CC.config.siteTitle}!`, + LOGIN_BUTTON: 'Log In', + LOGIN_PASSWORD_PLACEHOLDER: 'pa$$word!', + LOGIN_PASSWORD: 'Password', + LOGIN_USERNAME_PLACEHOLDER: 'john', + LOGIN_USERNAME: 'Username', + LOGOUT_BUTTON: 'Log Out', + NAVBAR_ADMIN: 'Admin Settings', + NAVBAR_LOGIN: 'Log In', + NAVBAR_LOGOUT: 'Log Out', + NAVBAR_PROFILE: 'Profile', + NAVBAR_WISHLIST: 'My Wishlist', + NOTE_BACK: name => `Back to ${name}'s wishlist`, + NOTE_GET_PRODUCT_DATA: 'Get Product Data', + NOTE_GUARD: 'Invalid user', + NOTE_IMAGE_URL: 'Image URL', + NOTE_MISSING_PROP: prop => `Missing property ${prop}`, // not really possible to localize this unfortunately + NOTE_NAME: 'Name', + NOTE_NOTE: 'Note', + NOTE_PRICE: 'Price', + NOTE_REFRESH_DATA: 'Refresh Data', + NOTE_REMOVE_GUARD: 'Invalid user', + NOTE_REMOVE_MISSING: 'Has no note', + NOTE_REMOVE_SUCCESS: 'Successfully removed note', + NOTE_SAVE_BUTTON: 'Save Item', + NOTE_SUCCESS: 'Successfully saved!', + NOTE_URL: 'URL', + PROFILE_HEADER: 'Profile', + PROFILE_PASSWORD_BUTTON: 'Save', + PROFILE_PASSWORD_NEW: 'New Password', + PROFILE_PASSWORD_OLD_MISMATCH: 'Incorrect old password', + PROFILE_PASSWORD_OLD: 'Old Password', + PROFILE_PASSWORD_PLACEHOLDER: 'pa$$word!', + PROFILE_PASSWORD_REQUIRED_NEW: 'New Password is required', + PROFILE_PASSWORD_REQUIRED_OLD: 'Old Password is required', + PROFILE_PASSWORD_SUCCESS: 'Changed saved successfully!', + PROFILE_PASSWORD_TITLE: name => `Profile Settings - Password - ${name}`, + PROFILE_PFP_IMAGE_URL: 'Image URL', + PROFILE_SAVE_PFP_DISABLED: 'Profile pictures are disabled.', + PROFILE_SAVE_PFP_SUCCESS: 'Saved profile picture!', + PROFILE_SECURITY_CHANGE_PASSWORD: 'Change Password', + PROFILE_SECURITY: 'Security', + PROFILE_TITLE: name => `Profile Settings - ${name}`, + RESET_PASSWORD_BUTTON: 'Reset Password', + RESET_PASSWORD_GREETING_EXPIRED: 'Your reset link has expired. Please ask for a new one.', + RESET_PASSWORD_GREETING_INVALID: "This reset link isn't valid, perhaps the link was canceled or some characters at the end got cut off?", + RESET_PASSWORD_GREETING_VALID: name => `Hello ${name}! Please set your password here.`, + RESET_PASSWORD_HEADER_INVALID: `${_CC.config.siteTitle} | Reset Link Invalid`, + RESET_PASSWORD_HEADER_VALID: `${_CC.config.siteTitle} | Reset Password`, + RESET_PASSWORD_PASSWORD_PLACEHOLDER: 'pa$$word!', + RESET_PASSWORD_PASSWORD: 'Password', + RESET_PASSWORD_SUCCESS: 'Password reset successfully!', + SETUP_ADMIN_USER: 'Admin User', + SETUP_BUTTON: 'Set up!', + SETUP_HEADER: 'Setup', + SETUP_PASSWORD_PLACEHOLDER: 'pa$$word!', + SETUP_PASSWORD: 'Password', + SETUP_USERNAME_PLACEHOLDER: 'john', + SETUP_USERNAME: 'Username', + SUPPORTED_SITES_HEADER: 'Supported Sites', + SUPPORTED_SITES_TEXT: 'Is a site missing or broken? Open an issue here! :)', + UPDATE_NOTICE: (current, latest) => ` + + Christmas Community is out of date. There may be new features or bug fixes. Consider updating! :) + +
+ (you can turn this off with UPDATE_CHECK=false) +

+ Current: ${current} +
+ Latest: ${latest} + This message is only visible to admins`, + WISHLIST_ADD: 'Add item to wishlist', + WISHLIST_ADDED_BY_USER: addedBy => `Added by: ${addedBy}`, + WISHLIST_ADDED_BY: 'Added By', + WISHLIST_CONFLICT: 'Items are being added too quickly. Please try again.', + WISHLIST_DELETE: 'Delete', + WISHLIST_EDIT_ITEM: 'Edit Item', + WISHLIST_IMAGE: 'Image', + WISHLIST_MOVE_DOWN: 'Move Down', + WISHLIST_MOVE_GUARD: 'Not correct user', + WISHLIST_MOVE_INVALID: 'Invalid move', + WISHLIST_MOVE_ITEM_DOWN: 'Move Item Down', + WISHLIST_MOVE_ITEM_TOP: 'Move Item Top', + WISHLIST_MOVE_ITEM_UP: 'Move Item Up', + WISHLIST_MOVE_SUCCESS: 'Successfully moved item!', + WISHLIST_MOVE_TOP: 'Move Top', + WISHLIST_MOVE_UP: 'Move Up', + WISHLIST_NAME: 'Name', + WISHLIST_NOTE: 'Note', + WISHLIST_OPTIONAL: 'Optional', + WISHLIST_PLEDGE_DUPLICATE: 'Item already pledged for', + WISHLIST_PLEDGE_ITEM: 'Pledge item', + WISHLIST_PLEDGE_SUCCESS: 'Successfully pledged for item!', + WISHLIST_PLEDGE: 'Pledge', + WISHLIST_PLEDGED: pledgedBy => `Pledged for by ${pledgedBy}`, + WISHLIST_PRICE: 'Price', + WISHLIST_REFRESH_GUARD: 'Invalid user', + WISHLIST_REFRESH_NO_URL: 'Item has no URL.', + WISHLIST_REFRESH_SUCCESS: 'Successfully refreshed data!', + WISHLIST_REMOVE_GUARD: 'Not correct user', + WISHLIST_REMOVE_MISSING: 'Failed to find item', + WISHLIST_REMOVE_SUCCESS: 'Successfully removed from wishlist', + WISHLIST_TITLE: name => `${_CC.config.siteTitle} - Wishlist - ${name}`, + WISHLIST_UNPLEDGE_GUARD: 'You did not pledge for this', // should never happen unless someone makes their own http requests + WISHLIST_UNPLEDGE_MISSING: 'Failed to find item', + WISHLIST_UNPLEDGE_SUCCESS: 'Successfully unpledged for item!', + WISHLIST_UNPLEDGE: 'Unpledge', + WISHLIST_URL_LABEL: 'Item URL or Name (Supported Sites)', + WISHLIST_URL_PLACEHOLDER: 'https://www.amazon.com/dp/B00ZV9RDKK', + WISHLIST_URL_REQUIRED: 'Item URL or Name is required', + WISHLISTS_COUNTS_SELF: name => `${name}: ???/???`, + WISHLISTS_COUNTS: (name, pledged, total) => `${name}: ${pledged}/${total}`, + WISHLISTS_TITLE: `${_CC.config.siteTitle} - Wishlists` +} diff --git a/middlewares/locals.js b/middlewares/locals.js index 834ed3b..9ec9d95 100644 --- a/middlewares/locals.js +++ b/middlewares/locals.js @@ -1,6 +1,8 @@ const config = require('../config') + module.exports = (req, res, next) => { res.locals.config = config res.locals.req = req + res.locals.lang = _CC.lang next() } diff --git a/routes/adminSettings/index.js b/routes/adminSettings/index.js index f2bcbc0..73a1199 100644 --- a/routes/adminSettings/index.js +++ b/routes/adminSettings/index.js @@ -18,7 +18,7 @@ module.exports = ({ db, ensurePfp }) => { if (!req.user.admin) return res.redirect('/') db.allDocs({ include_docs: true }) .then(docs => { - res.render('adminSettings', { title: 'Admin Settings', users: docs.rows }) + res.render('adminSettings', { title: _CC.lang('ADMIN_SETTINGS_HEADER'), users: docs.rows }) }) .catch(err => { throw err }) }) @@ -75,11 +75,11 @@ module.exports = ({ db, ensurePfp }) => { router.post('/edit/rename/:userToRename', verifyAuth(), async (req, res) => { if (!req.user.admin && req.user._id !== req.params.userToRename) return res.redirect('/') if (!req.body.newUsername) { - req.flash('error', 'No username provided') + req.flash('error', _CC.lang('ADMIN_SETTINGS_USERS_EDIT_NO_USERNAME_PROVIDED')) return res.redirect(`/admin-settings/edit/${req.params.userToRename}`) } if (req.body.newUsername === req.params.userToRename) { - req.flash('error', 'Username is same as new username.') + req.flash('error', _CC.lang('ADMIN_SETTINGS_USERS_EDIT_SAME_NAME')) return res.redirect(`/admin-settings/edit/${req.params.userToRename}`) } @@ -105,7 +105,7 @@ module.exports = ({ db, ensurePfp }) => { await db.bulkDocs(usersBulk) await db.remove(await db.get(oldName)) - await req.flash('success', 'Renamed user!') + req.flash('success', _CC.lang('ADMIN_SETTINGS_USERS_EDIT_RENAMED_USER')) return res.redirect(`/wishlist/${newName}`) } catch (error) { console.log(error, error.stack) @@ -125,7 +125,7 @@ module.exports = ({ db, ensurePfp }) => { req.flash('error', err.message) return res.redirect(`/admin-settings/edit/${req.params.userToEdit}`) } - req.flash('success', `You are now ${req.params.userToEdit}.`) + req.flash('success', _CC.lang('ADMIN_SETTINGS_USERS_EDIT_IMPERSONATE_SUCCESS', req.params.userToEdit)) res.redirect('/') }) }) @@ -134,43 +134,43 @@ module.exports = ({ db, ensurePfp }) => { if (!req.user.admin) return res.redirect('/') const user = await db.get(req.params.userToPromote) if (!user) { - req.flash('error', 'User not found.') + req.flash('error', _CC.lang('ADMIN_SETTINGS_USERS_EDIT_PROMOTE_DEMOTE_NOT_FOUND')) return res.redirect(`/admin-settings/edit/${req.params.userToPromote}`) } if (user.admin) { - req.flash('error', 'user is already admin') + req.flash('error', _CC.lang('ADMIN_SETTINGS_USERS_EDIT_PROMOTE_ALREADY_ADMIN')) return res.redirect(`/admin-settings/edit/${req.params.userToPromote}`) } user.admin = true await db.put(user) - req.flash('success', `${user._id} is now an admin.`) + req.flash('success', _CC.lang('ADMIN_SETTINGS_USERS_EDIT_PROMOTE_SUCCESS', user._id)) return res.redirect(`/admin-settings/edit/${req.params.userToPromote}`) }) router.post('/edit/demote/:userToDemote', verifyAuth(), async (req, res) => { if (!req.user.admin) return res.redirect('/') if (req.user._id === req.params.userToDemote) { - req.flash('error', 'You cannot demote yourself.') + req.flash('error', _CC.lang('ADMIN_SETTINGS_USERS_EDIT_DEMOTE_SELF')) return res.redirect(`/admin-settings/edit/${req.params.userToDemote}`) } const user = await db.get(req.params.userToDemote) if (!user) { - req.flash('error', 'User not found.') + req.flash('error', _CC.lang('ADMIN_SETTINGS_USERS_EDIT_PROMOTE_DEMOTE_NOT_FOUND')) return res.redirect(`/admin-settings/edit/${req.params.userToDemote}`) } if (!user.admin) { - req.flash('error', 'user is not an admin') + req.flash('error', _CC.lang('ADMIN_SETTINGS_USERS_EDIT_DEMOTE_NOT_ADMIN')) return res.redirect(`/admin-settings/edit/${req.params.userToDemote}`) } user.admin = false await db.put(user) - req.flash('success', `${user._id} is no longer an admin.`) + req.flash('success', _CC.lang('ADMIN_SETTINGS_USERS_EDIT_DEMOTE_SUCCESS', user._id)) return res.redirect(`/admin-settings/edit/${req.params.userToDemote}`) }) @@ -178,7 +178,7 @@ module.exports = ({ db, ensurePfp }) => { if (!req.user.admin) return res.redirect('/') const doc = await db.get(req.params.userToRemove) if (doc.admin) { - req.flash('error', 'Failed to remove: user is admin.') + req.flash('error', _CC.lang('ADMIN_SETTINGS_USERS_EDIT_DELETE_FAIL_ADMIN')) return res.redirect('/admin-settings') } await db.remove(doc) @@ -192,7 +192,7 @@ module.exports = ({ db, ensurePfp }) => { } } } - req.flash('success', `Successfully removed user ${req.params.userToRemove}`) + req.flash('success', _CC.lang('ADMIN_SETTINGS_USERS_EDIT_DELETE_SUCCESS', req.params.userToRemove)) res.redirect('/admin-settings') }) @@ -208,7 +208,7 @@ module.exports = ({ db, ensurePfp }) => { row.doc.wishlist = [] await db.put(row.doc) } - req.flash('success', 'Cleared all wishlists.') + req.flash('success', _CC.lang('ADMIN_SETTINGS_CLEARDB_SUCCESS')) res.redirect('/admin-settings') }) diff --git a/routes/confirm-account/index.js b/routes/confirm-account/index.js index 0f5c464..209c5e0 100644 --- a/routes/confirm-account/index.js +++ b/routes/confirm-account/index.js @@ -34,7 +34,7 @@ module.exports = (db) => { req.flash('error', err.message) return res.redirect('/') } - req.flash('success', `Welcome to ${_CC.config.siteTitle}!`) + req.flash('success', _CC.lang('CONFIRM_ACCOUNT_SUCCESS')) res.redirect('/') }) }) diff --git a/routes/profile/index.js b/routes/profile/index.js index 0695b04..b7e7a96 100644 --- a/routes/profile/index.js +++ b/routes/profile/index.js @@ -7,7 +7,7 @@ module.exports = ({ db, config, ensurePfp }) => { router.get('/', verifyAuth(), async (req, res) => { await ensurePfp(req.user._id) - res.render('profile', { title: `Profile Settings - ${req.user._id}` }) + res.render('profile', { title: _CC.lang('PROFILE_TITLE', req.user._id) }) }) router.post('/pfp', verifyAuth(), async (req, res) => { @@ -15,24 +15,24 @@ module.exports = ({ db, config, ensurePfp }) => { req.user.pfp = req.body.image await db.put(req.user) if (!req.user.pfp) await ensurePfp(req.user._id) - req.flash('success', 'Saved profile picture!') + req.flash('success', _CC.lang('PROFILE_SAVE_PFP_SUCCESS')) } else { - req.flash('error', 'Profile pictures are disabled.') + req.flash('error', _CC.lang('PROFILE_SAVE_PFP_DISABLED')) } res.redirect(`${_CC.config.base}profile`) }) router.get('/password', verifyAuth(), async (req, res) => { await ensurePfp(req.user._id) - res.render('profile-password', { title: `Profile Settings - Password - ${req.user._id}` }) + res.render('profile-password', { title: _CC.lang('PROFILE_PASSWORD_TITLE', req.user._id) }) }) router.post('/password', verifyAuth(), (req, res) => { if (!req.body.oldPassword) { - req.flash('error', 'Old Password is required') + req.flash('error', _CC.lang('PROFILE_PASSWORD_REQUIRED_OLD')) return res.redirect('/profile/password') } if (!req.body.newPassword) { - req.flash('error', 'New Password is required') + req.flash('error', _CC.lang('PROFILE_PASSWORD_REQUIRED_NEW')) return res.redirect('/profile/password') } bcrypt.compare(req.body.oldPassword, req.user.password, (err, correct) => { @@ -45,7 +45,7 @@ module.exports = ({ db, config, ensurePfp }) => { doc.password = hash db.put(doc) .then(() => { - req.flash('success', 'Changes saved successfully!') + req.flash('success', _CC.lang('PROFILE_PASSWORD_SUCCESS')) res.redirect('/profile/password') }) .catch(err => { throw err }) @@ -53,7 +53,7 @@ module.exports = ({ db, config, ensurePfp }) => { .catch(err => { throw err }) }) } else { - req.flash('error', 'Incorrect old password') + req.flash('error', _CC.lang('PROFILE_PASSWORD_OLD_MISMATCH')) res.redirect('/profile/password') } }) diff --git a/routes/resetpw/index.js b/routes/resetpw/index.js index 28e5d80..0ca8e4c 100644 --- a/routes/resetpw/index.js +++ b/routes/resetpw/index.js @@ -34,7 +34,7 @@ module.exports = (db) => { req.flash('error', err.message) return res.redirect('/') } - req.flash('success', `Welcome to ${_CC.config.siteTitle}!`) + req.flash('success', _CC.lang('RESET_PASSWORD_SUCCESS')) res.redirect('/') }) }) diff --git a/routes/setup/index.js b/routes/setup/index.js index debadc7..423fcc6 100644 --- a/routes/setup/index.js +++ b/routes/setup/index.js @@ -8,7 +8,7 @@ module.exports = (db) => { async (req, res) => { const dbInfo = await db.info() if (dbInfo.doc_count === 0) { - res.render('setup', { title: 'Setup' }) + res.render('setup', { title: _CC.lang('SETUP_HEADER') }) } else { res.redirect('/') } diff --git a/routes/supported-sites/index.js b/routes/supported-sites/index.js index 04acc6a..9f3c314 100644 --- a/routes/supported-sites/index.js +++ b/routes/supported-sites/index.js @@ -4,7 +4,7 @@ module.exports = () => { const router = express.Router() router.get('/', async (req, res) => { - res.render('supported-sites', { title: 'Supported Sites' }) + res.render('supported-sites', { title: _CC.lang('SUPPORTED_SITES_HEADER') }) }) return router diff --git a/routes/wishlist/index.js b/routes/wishlist/index.js index 99833c3..a54d6db 100644 --- a/routes/wishlist/index.js +++ b/routes/wishlist/index.js @@ -44,7 +44,7 @@ module.exports = (db) => { if (row.doc.admin) return res.redirect(`/wishlist/${row.doc._id}`) } } - res.render('wishlists', { title: 'Wishlists', users: docs.rows, totals }) + res.render('wishlists', { title: _CC.lang('WISHLISTS_TITLE'), users: docs.rows, totals }) }) router.get('/:user', publicRoute(), async (req, res) => { @@ -66,7 +66,8 @@ module.exports = (db) => { if (global._CC.config.wishlist.note.markdown) item.note = DOMPurify.sanitize(marked(item.note)) } res.render('wishlist', { - title: `Wishlist - ${dbUser._id}`, + title: _CC.lang('WISHLIST_TITLE', dbUser._id), + name: dbUser._id, wishlist: [ ...dbUser.wishlist.filter(item => item.addedBy === req.params.user), ...dbUser.wishlist.filter(item => item.addedBy !== req.params.user) @@ -82,7 +83,7 @@ module.exports = (db) => { router.post('/:user', verifyAuth(), async (req, res) => { if (!req.body.itemUrlOrName) { - req.flash('error', 'Item URL or Name is required') + req.flash('error', _CC.lang('WISHLIST_URL_REQUIRED')) return res.redirect(`/wishlist/${req.params.user}`) } const potentialUrl = req.body.itemUrlOrName.split(' ').pop() @@ -108,7 +109,7 @@ module.exports = (db) => { try { await db.put(doc) } catch { - req.flash('error', 'Items are being added too quickly. Please try again.') + req.flash('error', _CC.lang('WISHLIST_CONFLICT')) return res.redirect(`/wishlist/${req.params.user}`) } req.flash( @@ -128,12 +129,12 @@ module.exports = (db) => { for (let j = 0; j < docs.rows[i].doc.wishlist.length; j++) { if (docs.rows[i].doc.wishlist[j].id === req.params.itemId) { if (docs.rows[i].doc.wishlist[j].pledgedBy !== undefined) { - req.flash('error', 'Item already pledged for') + req.flash('error', _CC.lang('WISHLIST_PLEDGE_DUPLICATE')) return res.redirect(`/wishlist/${req.params.user}`) } docs.rows[i].doc.wishlist[j].pledgedBy = req.user._id await db.put(docs.rows[i].doc) - req.flash('success', 'Successfully pledged for item!') + req.flash('success', _CC.lang('WISHLIST_PLEDGE_SUCCESS')) return res.redirect(`/wishlist/${req.params.user}`) } } @@ -145,24 +146,24 @@ module.exports = (db) => { for (let j = 0; j < docs.rows[i].doc.wishlist.length; j++) { if (docs.rows[i].doc.wishlist[j].id === req.params.itemId) { if (docs.rows[i].doc.wishlist[j].pledgedBy !== req.user._id) { - req.flash('error', 'You did not pledge for this') + req.flash('error', _CC.lang('WISHLIST_UNPLEDGE_GUARD')) return res.redirect(`/wishlist/${req.params.user}`) } docs.rows[i].doc.wishlist[j].pledgedBy = undefined if (docs.rows[i].doc.wishlist[j].addedBy === req.user._id) docs.rows[i].doc.wishlist.splice(j, 1) await db.put(docs.rows[i].doc) - req.flash('success', 'Successfully unpledged for item') + req.flash('success', _CC.lang('WISHLIST_UNPLEDGE_SUCCESS')) return res.redirect(`/wishlist/${req.params.user}`) } } } - req.flash('error', 'Failed to find item') + req.flash('error', _CC.lang('WISHLIST_UNPLEDGE_MISSING')) return res.redirect(`/wishlist/${req.params.user}`) }) router.post('/:user/remove/:itemId', verifyAuth(), async (req, res) => { if (req.user._id !== req.params.user) { - req.flash('error', 'Not correct user') + req.flash('error', _CC.lang('WISHLIST_REMOVE_GUARD')) return res.redirect(`/wishlist/${req.params.user}`) } const doc = await db.get(req.user._id) @@ -170,17 +171,17 @@ module.exports = (db) => { if (doc.wishlist[i].id === req.params.itemId) { doc.wishlist.splice(i, 1) await db.put(doc) - req.flash('success', 'Successfully removed from wishlist') + req.flash('success', _CC.lang('WISHLIST_REMOVE_SUCCESS')) return res.redirect(`/wishlist/${req.params.user}`) } } - req.flash('error', 'Failed to find item') + req.flash('error', _CC.lang('WISHLIST_REMOVE_MISSING')) return res.redirect(`/wishlist/${req.params.user}`) }) router.post('/:user/move/:direction/:itemId', verifyAuth(), async (req, res) => { if (req.user._id !== req.params.user) { - req.flash('error', 'Not correct user') + req.flash('error', _CC.lang('WISHLIST_MOVE_GUARD')) return res.redirect(`/wishlist/${req.params.user}`) } const doc = await db.get(req.user._id) @@ -197,7 +198,7 @@ module.exports = (db) => { }) const moveToIndex = wishlist.findIndex(wish => (wishlist.indexOf(wish) > moveFromIndex && wish.addedBy === req.user._id)) if (moveToIndex < 0 || moveToIndex > wishlist.length) { - req.flash('error', 'Invalid move') + req.flash('error', _CC.lang('WISHLIST_MOVE_INVALID')) return res.redirect(`/wishlist/${req.params.user}`) } [wishlist[moveFromIndex], wishlist[moveToIndex]] = [wishlist[moveToIndex], wishlist[moveFromIndex]] @@ -206,7 +207,7 @@ module.exports = (db) => { doc.wishlist = wishlist await db.put(doc) - req.flash('success', 'Successfully moved item!') + req.flash('success', _CC.lang('WISHLIST_MOVE_SUCCESS')) return res.redirect(`/wishlist/${req.params.user}`) }) @@ -222,14 +223,14 @@ module.exports = (db) => { const wishlistItem = wishlist[i] if (wishlistItem.id !== req.params.id) continue if (req.user._id !== req.params.user && req.user._id !== wishlistItem.addedBy) { - req.flash('error', 'Invalid user') + req.flash('error', _CC.lang('NOTE_GUARD')) return res.redirect(`/wishlist/${req.params.user}`) } for (const type of [ 'name', 'note', 'url', 'price', 'image' ]) { if (!Object.prototype.hasOwnProperty.call(req.body, type)) { - req.flash('error', `Missing property ${type}`) + req.flash('error', _CC.lang('NOTE_MISSING_PROP', type)) return res.redirect(`/wishlist/${req.params.user}/note/${req.params.id}`) } wishlistItem[type] = req.body[type] @@ -238,7 +239,7 @@ module.exports = (db) => { } doc.wishlist = wishlist await db.put(doc) - req.flash('success', 'Successfully saved note!') + req.flash('success', _CC.lang('NOTE_SUCCESS')) return res.redirect(`/wishlist/${req.params.user}`) }) router.post('/:user/refresh/:id', verifyAuth(), async (req, res) => { @@ -248,12 +249,12 @@ module.exports = (db) => { const wishlistItem = wishlist[i] if (wishlistItem.id !== req.params.id) continue if (req.user._id !== req.params.user && req.user._id !== wishlistItem.addedBy) { - req.flash('error', 'Invalid user') + req.flash('error', _CC.lang('WISHLIST_REFRESH_GUARD')) return res.redirect(`/wishlist/${req.params.user}`) } if (!wishlistItem.url) { - req.flash('error', 'Item has no URL.') + req.flash('error', _CC.lang('WISHLIST_REFRESH_NO_URL')) return res.redirect(`/wishlist/${req.params.user}/note/${req.params.id}`) } @@ -266,7 +267,7 @@ module.exports = (db) => { } doc.wishlist = wishlist await db.put(doc) - req.flash('success', 'Successfully refreshed data!') + req.flash('success', _CC.lang('WISHLIST_REFRESH_SUCCESS')) return res.redirect(`/wishlist/${req.params.user}/note/${req.params.id}`) }) router.post('/:user/note/remove/:id', verifyAuth(), async (req, res) => { @@ -276,20 +277,20 @@ module.exports = (db) => { const wishlistItem = wishlist[i] if (wishlistItem.id !== req.params.id) continue if (req.user._id !== req.params.user && req.user._id !== wishlistItem.addedBy) { - req.flash('error', 'Invalid user') + req.flash('error', _CC.lang('NOTE_REMOVE_GUARD')) return res.redirect(`/wishlist/${req.params.user}`) } if (wishlistItem.note) { wishlistItem.note = undefined wishlist[i] = wishlistItem } else { - req.flash('error', 'Has no note') + req.flash('error', _CC.lang('NOTE_REMOVE_MISSING')) return res.redirect(`/wishlist/${req.params.user}`) } } doc.wishlist = wishlist await db.put(doc) - req.flash('success', 'Successfully removed note') + req.flash('success', _CC.lang('NOTE_REMOVE_SUCCESS')) return res.redirect(`/wishlist/${req.params.user}`) }) return router diff --git a/views/admin-clear-wishlists.pug b/views/admin-clear-wishlists.pug index 2fcc01f..353b072 100644 --- a/views/admin-clear-wishlists.pug +++ b/views/admin-clear-wishlists.pug @@ -1,9 +1,9 @@ extends layout.pug block content - h2 Wishlist Deletion - p This will instantly irreversibly delete all wishlists! Consider making a backup of the database before using this. + h2= lang('ADMIN_CLEAR_WISHLISTS_HEADER') + p!= lang('ADMIN_CLEAR_WISHLISTS_DESCRIPTION') form(method='POST', action=`${_CC.config.base}admin-settings/clear-wishlists`) .field .control - input.button.is-danger(type='submit' value=`Clear all wishlists`) \ No newline at end of file + input.button.is-danger(type='submit' value=lang('ADMIN_CLEAR_WISHLISTS_BUTTON')) \ No newline at end of file diff --git a/views/admin-user-edit.pug b/views/admin-user-edit.pug index 7e35b5a..c786933 100644 --- a/views/admin-user-edit.pug +++ b/views/admin-user-edit.pug @@ -4,7 +4,7 @@ block title h1(style="margin-bottom: 0;") a(href='..') < | #{config.siteTitle} - p Editing user "#{user._id}" + p= lang('ADMIN_USER_EDIT_EDITING_USER', user._id) block content .columns @@ -14,94 +14,94 @@ block content .box(style='overflow: hidden;') .columns(style='margin-bottom: 0;') .column.is-narrow(style='padding-bottom: 0;') - h2 Confirmation Link + h2= lang('ADMIN_USER_EDIT_CONFIRMATION_LINK') .column(style='padding-bottom: 0;') p - span This account hasn't been confirmed. + span= lang('ADMIN_USER_EDIT_ACCOUNT_UNCONFIRMED') br if user.expiry > new Date().getTime() - span= `The following link expires ${_CC.require('moment')(user.expiry).fromNow()}` + span= lang('ADMIN_USER_EDIT_LINK_EXPIRY_FUTURE', _CC.moment(user.expiry).fromNow()) else - span.has-text-weight-bold(style='color: red;')= `The following link expired ${_CC.require('moment')(user.expiry).fromNow()}` + span.has-text-weight-bold(style='color: red;')= lang('ADMIN_USER_EDIT_LINK_EXPIRY_PAST', _CC.moment(user.expiry).fromNow()) h3(style='margin-bottom: 0; margin-top: 0;') .level .level-left .level-item form(method='POST', action=`${_CC.config.base}admin-settings/edit/refresh-signup-token/${user._id}`) - input.button.is-rounded(type='submit', value='Generate New Link') + input.button.is-rounded(type='submit', value=lang('ADMIN_USER_EDIT_GENERATE_NEW_LINK')) .level-item a(href=signupLink, style='font-family: monospaced; word-break: break-all;')= signupLink .columns .column.is-narrow - h2 Change Name + h2= lang('ADMIN_USER_EDIT_CHANGE_NAME') form(action=`${_CC.config.base}admin-settings/edit/rename/${user._id}`, method='POST') .field - label.label Username + label.label= lang('ADMIN_USER_EDIT_USERNAME') .control.has-icons-left input.input(type='text', name='newUsername', placeholder=user._id, value=user._id) span.icon.is-small.is-left i.fas.fa-user .field .control - input.button.is-primary(type='submit' value='Change Username') + input.button.is-primary(type='submit' value=lang('ADMIN_USER_EDIT_CHANGE_USERNAME')) .column.is-narrow - h2 Admin + h2= lang('ADMIN_USER_EDIT_ADMIN') //- Yes, ternary exists, but I think the code is cleaner with a more "naive" style :) //- p.is-marginless #{user._id} is #{user.admin ? '' : 'not '}an admin. //- vs. if user.admin - p.is-marginless #{user._id} is an admin. + p.is-marginless= lang('ADMIN_USER_EDIT_ADMIN_ISADMIN', user._id) form(action=`${_CC.config.base}admin-settings/edit/demote/${user._id}`, method='POST') .field .control if user._id === req.user._id - input.input.button(disabled, type='submit', value=`You cannot demote yourself`, style='margin-top: 1em;') + input.input.button(disabled, type='submit', value=lang('ADMIN_USER_EDIT_DEMOTE_SELF'), style='margin-top: 1em;') else - input.input.button(type='submit', value=`Demote ${user._id}`, style='margin-top: 1em;') + input.input.button(type='submit', value=lang('ADMIN_USER_EDIT_DEMOTE', user._id), style='margin-top: 1em;') else - p.is-marginless #{user._id} is not an admin. + p.is-marginless= lang('ADMIN_USER_EDIT_ADMIN_NOTADMIN', user._id) form(action=`${_CC.config.base}admin-settings/edit/promote/${user._id}`, method='POST') .field .control - input.input.button(type='submit', value=`Promote ${user._id}`, style='margin-top: 1em;') + input.input.button(type='submit', value=lang('ADMIN_USER_EDIT_PROMOTE', user._id), style='margin-top: 1em;') if user._id !== req.user._id .column.is-narrow - h2 Impersonate + h2= lang('ADMIN_USER_EDIT_IMPERSONATE_HEADER') form(action=`${_CC.config.base}admin-settings/edit/impersonate/${user._id}`, method='POST') .field .control - input.input.button.is-warning(type='submit', value=`Log in as ${user._id}`, style='margin-top: 1em;') - h2(style='margin-bottom: 1em;') Reset Password + input.input.button.is-warning(type='submit', value=lang('ADMIN_USER_EDIT_IMPERSONATE_BUTTON', user._id), style='margin-top: 1em;') + h2(style='margin-bottom: 1em;')= lang('ADMIN_USER_EDIT_RESET_PASSWORD_HEADER') if user.pwToken - const resetLink = `${_CC.config.base}resetpw/${user.pwToken}` - p There is a reset password link for this user. + p= lang('ADMIN_USER_EDIT_RESET_PASSWORD_HASLINK') if user.pwExpiry > new Date().getTime() - span It expires #{_CC.require('moment')(user.pwExpiry).fromNow()} + span= lang('ADMIN_USER_EDIT_RESET_PASSWORD_HASLINK_EXPIRY_FUTURE', _CC.moment(user.pwExpiry).fromNow()) else - span.has-text-weight-bold.has-text-danger It expired #{_CC.require('moment')(user.pwExpiry).fromNow()} + span.has-text-weight-bold.has-text-danger= lang('ADMIN_USER_EDIT_RESET_PASSWORD_HASLINK_EXPIRY_PAST', _CC.moment(user.pwExpiry).fromNow()) a(href=resetLink)= resetLink .columns .column.is-narrow form(method='POST', action=`${_CC.config.base}admin-settings/edit/resetpw/${user._id}`) .field .control - input.button.is-primary(type='submit' value='Refresh Password Reset Link') + input.button.is-primary(type='submit' value=lang('ADMIN_USER_EDIT_RESET_PASSWORD_LINK_REFRESH')) .column.is-narrow form(method='POST', action=`${_CC.config.base}admin-settings/edit/cancelresetpw/${user._id}`) .field .control - input.button.is-info(type='submit' value='Cancel Password Reset Link') + input.button.is-info(type='submit' value=lang('ADMIN_USER_EDIT_RESET_PASSWORD_LINK_CANCEL')) else form(method='POST', action=`${_CC.config.base}admin-settings/edit/resetpw/${user._id}`) .field .control - input.button.is-danger(type='submit' value='Create Password Reset Link') + input.button.is-danger(type='submit' value=lang('ADMIN_USER_EDIT_RESET_PASSWORD_LINK_CREATE')) .column.is-narrow - h2 Irreversible Deletion + h2= lang('ADMIN_USER_EDIT_DELETE_HEADER') form(method='POST', action=`${_CC.config.base}admin-settings/edit/remove/${user._id}`) .field .control if user.admin - input.button.is-danger(disabled, type='submit' value=`User is admin`) + input.button.is-danger(disabled, type='submit' value=lang('ADMIN_USER_EDIT_DELETE_ADMIN')) else - input.button.is-danger(type='submit' value=`Remove user ${user._id}`) \ No newline at end of file + input.button.is-danger(type='submit' value=lang('ADMIN_USER_EDIT_DELETE_USER', user._id)) \ No newline at end of file diff --git a/views/adminSettings.pug b/views/adminSettings.pug index 6930387..d8a9683 100644 --- a/views/adminSettings.pug +++ b/views/adminSettings.pug @@ -1,30 +1,30 @@ extends layout.pug block content - h2 Users + h2= lang('ADMIN_SETTINGS_USERS_HEADER') each user in users span.is-size-6.inline= user.id a(href=`${_CC.config.base}admin-settings/edit/${user.id}`) span.is-size-7.icon.has-text-info i.fas.fa-edit span.is-sr-only - | Edit + = lang('ADMIN_SETTINGS_USERS_EDIT') br - h3 Add user + h3= lang('ADMIN_SETTINGS_USERS_ADD_HEADER') form(action=`${_CC.config.base}admin-settings/add`, method='POST') .field - label.label Username + label.label= lang('ADMIN_SETTINGS_USERS_ADD_USERNAME') .control.has-icons-left - input.input(type='text', name='newUserUsername', placeholder='john') + input.input(type='text', name='newUserUsername', placeholder=lang('ADMIN_SETTINGS_USERS_ADD_PLACEHOLDER')) span.icon.is-small.is-left i.fas.fa-user .field .control - input.button.is-primary(type='submit' value='Add User') - h3 Data Destruction - p Warning: These options destroy data! You may want to back up the database before using these options. - a.button.is-danger(href=`${_CC.config.base}admin-settings/clear-wishlists`) Clear Wishlists - h3 Version Info + input.button.is-primary(type='submit' value=lang('ADMIN_SETTINGS_USERS_ADD_BUTTON')) + h3= lang('ADMIN_SETTINGS_CLEARDB_HEADER') + p!= lang('ADMIN_SETTINGS_CLEARDB_DESCRIPTION') + a.button.is-danger(href=`${_CC.config.base}admin-settings/clear-wishlists`)= lang('ADMIN_SETTINGS_CLEARDB_BUTTON') + h3= lang('ADMIN_SETTINGS_VERSION_INFO') p Christmas Community: v#{_CC.package.version} p Get Product Data: v#{_CC.require('get-product-name/package.json').version} p Node: #{process.version} diff --git a/views/confirm-account.pug b/views/confirm-account.pug index bb6e381..9d458bd 100644 --- a/views/confirm-account.pug +++ b/views/confirm-account.pug @@ -9,26 +9,26 @@ mixin icon(c, text) block title if doc - h1 #{config.siteTitle} | Confirm Account + h1= lang('CONFIRM_ACCOUNT_HEADER_VALID') else - h1 #{config.siteTitle} | Confirmation Link Invalid + h1= lang('CONFIRM_ACCOUNT_HEADER_INVALID') block content if doc if doc.expiry > new Date().getTime() - +icon('fas fa-smile-beam', `Hello ${doc._id}! Please set your password here.`) + +icon('fas fa-smile-beam', lang('CONFIRM_ACCOUNT_SET_PW_TEXT', doc._id)) form(method='POST') .field label.label Password .control.has-icons-left - input.input(type='password', name='password', placeholder='pa$$word!') + input.input(type='password', name='password', placeholder=lang('CONFIRM_ACCOUNT_SET_PW_PLACEHOLDER')) span.icon.is-small.is-left i.fas.fa-lock .field .control - input.button.is-primary(type='submit' value=`Join ${_CC.config.siteTitle}`) + input.button.is-primary(type='submit' value=lang('CONFIRM_ACCOUNT_SET_PW_BUTTON')) else - +icon('fas fa-frown-open', 'Your confirmation link has expired. Please ask for a new one.') + +icon('fas fa-frown-open', lang('CONFIRM_ACCOUNT_EXPIRED')) else - +icon('fas fa-frown-open', "This confirmation link isn't valid, perhaps the account was deleted or some characters at the end got cut off?") + +icon('fas fa-frown-open', lang('CONFIRM_ACCOUNT_INVALID')) diff --git a/views/includes/navbar.pug b/views/includes/navbar.pug index a7afe8a..7a4a670 100644 --- a/views/includes/navbar.pug +++ b/views/includes/navbar.pug @@ -3,6 +3,7 @@ mixin navBarLink(href, title) a.is-active.navbar-item(href=href)= title else a.navbar-item(href=href)= title + nav.navbar.is-fixed-top(role='navigation', aria-label='main navigation',style='box-shadow: 0px 0px 7px rgb(14, 15, 17);') .navbar-brand if _CC.config.base === req.path @@ -22,17 +23,17 @@ nav.navbar.is-fixed-top(role='navigation', aria-label='main navigation',style='b if req.isAuthenticated() if req.user._id === 'Unknown' .navbar-item - a.button.is-primary(href='/login') Log In + a.button.is-primary(href='/login')= lang('NAVBAR_') else .navbar-item.has-dropdown.is-hoverable a.navbar-link= req.user._id .navbar-dropdown - +navBarLink(`${_CC.config.base}wishlist/${req.user._id}`, 'My Wishlist') - +navBarLink(`${_CC.config.base}profile`, 'Profile') + +navBarLink(`${_CC.config.base}wishlist/${req.user._id}`, lang('NAVBAR_WISHLIST')) + +navBarLink(`${_CC.config.base}profile`, lang('NAVBAR_PROFILE')) if req.user.admin - +navBarLink(`${_CC.config.base}admin-settings`, 'Admin settings') + +navBarLink(`${_CC.config.base}admin-settings`, lang('NAVBAR_ADMIN')) hr.navbar-divider .navbar-item form#logoutForm(action=`${_CC.config.base}logout`, method='POST') - button.button.is-warning(type='submit') Log Out + button.button.is-warning(type='submit')= lang('NAVBAR_LOGOUT') script(src=`${_CC.config.base}js/nav.js`) diff --git a/views/includes/updateNotice.pug b/views/includes/updateNotice.pug index b162cda..9b9d087 100644 --- a/views/includes/updateNotice.pug +++ b/views/includes/updateNotice.pug @@ -2,13 +2,4 @@ if req.user && req.user.admin && _CC.updateNotice .columns .column .column.is-narrow - .box - span.has-text-danger.is-size-4.has-text-weight-bold Christmas Community is out of date. There may be new features or bug fixes. Consider updating! :) - br - span (you can turn this off with UPDATE_CHECK=false) - br - br - span Current: #{_CC.updateNotice.current} - br - span Latest: #{_CC.updateNotice.latest} - span.has-text-info(style='float: right;') This message is only visible to admins \ No newline at end of file + .box!= lang('UPDATE_NOTICE', _CC.updateNotice.current, _CC.updateNotice.latest) \ No newline at end of file diff --git a/views/layout.pug b/views/layout.pug index 21911cf..daaa23b 100644 --- a/views/layout.pug +++ b/views/layout.pug @@ -36,7 +36,7 @@ html(lang='en') div.container.is-marginless.fullwidth block title if title - h1= config.siteTitle + ' - ' + title + h1= title else if title !== false h1 #{config.siteTitle} include includes/messages.pug diff --git a/views/login.pug b/views/login.pug index 0f109f1..5c3fc5b 100644 --- a/views/login.pug +++ b/views/login.pug @@ -3,18 +3,18 @@ extends layout.pug block content form(method='POST') .field - label.label Username + label.label= lang('LOGIN_USERNAME') .control.has-icons-left - input.input(type='text', name='username', placeholder='john') + input.input(type='text', name='username', placeholder=lang('LOGIN_USERNAME_PLACEHOLDER')) span.icon.is-small.is-left i.fas.fa-user .field - label.label Password + label.label= lang('LOGIN_PASSWORD') .control.has-icons-left - input.input(type='password', name='password', placeholder='pa$$word!') + input.input(type='password', name='password', placeholder=lang('LOGIN_PASSWORD_PLACEHOLDER')) span.icon.is-small.is-left i.fas.fa-lock .field .control - input.button.is-primary(type='submit' value='Log In') + input.button.is-primary(type='submit' value=lang('LOGIN_BUTTON')) diff --git a/views/logout.pug b/views/logout.pug index 7d94770..d1aea2a 100644 --- a/views/logout.pug +++ b/views/logout.pug @@ -4,4 +4,4 @@ block content form(method='POST') .field .control - input.button.is-primary(type='submit' value='Log Out') \ No newline at end of file + input.button.is-primary(type='submit' value=lang('LOGOUT_BUTTON')) \ No newline at end of file diff --git a/views/note.pug b/views/note.pug index e9879e5..0b9d6b5 100644 --- a/views/note.pug +++ b/views/note.pug @@ -1,40 +1,40 @@ extends layout.pug block content - a(href='..') Back to #{req.params.user}'s wishlist + a(href='..') #{lang('NOTE_BACK', req.params.user)} form(id='refreshform', method='POST', action=`${_CC.config.base}wishlist/${req.params.user}/refresh/${req.params.id}`) form.inline(method='POST', action=`${_CC.config.base}wishlist/${req.params.user}/note/${req.params.id}`) .columns .column .field - label.label Name + label.label= lang('NOTE_NAME') .control input.input(name='name', value=item.name) .column .field - label.label URL + label.label= lang('NOTE_URL') .control input.input(name='url', value=item.url) .column .field - label.label Price + label.label= lang('NOTE_PRICE') .control input.input(name='price', value=item.price) .column .field - label.label Image URL + label.label= lang('NOTE_IMAGE_URL') .control input.input(name='image', value=item.image) .column.is-narrow .field - label.label Get Product Data - input.is-ghost.button(type='submit', value='Refresh Data' style='width: 100%;', form='refreshform') + label.label= lang('NOTE_GET_PRODUCT_DATA') + input.is-ghost.button(type='submit', value=lang('NOTE_REFRESH_DATA') style='width: 100%;', form='refreshform') .field - label.label Note + label.label= lang('NOTE_NOTE') .control textarea.textarea( name='note' )= item.note .field.inline .control.inline - input.button.is-primary(type='submit' value='Save Item') \ No newline at end of file + input.button.is-primary(type='submit' value=lang('NOTE_SAVE_BUTTON')) \ No newline at end of file diff --git a/views/profile-password.pug b/views/profile-password.pug index afdc657..e620df4 100644 --- a/views/profile-password.pug +++ b/views/profile-password.pug @@ -1,21 +1,21 @@ extends layout.pug block content - a.button(href=`${_CC.config.base}profile`) Back + a.button(href=`${_CC.config.base}profile`)= lang('BACK_BUTTON') form(method='POST') .field - label.label Old Password (Required if changing password) + label.label= lang('PROFILE_PASSWORD_OLD') .control.has-icons-left - input.input(type='password', name='oldPassword', placeholder='pa$$word!') + input.input(type='password', name='oldPassword', placeholder=lang('PROFILE_PASSWORD_PLACEHOLDER')) span.icon.is-small.is-left i.fas.fa-lock .field - label.label New Password (Leave blank if not changing password) + label.label= lang('PROFILE_PASSWORD_NEW') .control.has-icons-left - input.input(type='password', name='newPassword', placeholder='pa$$word!') + input.input(type='password', name='newPassword', placeholder=lang('PROFILE_PASSWORD_PLACEHOLDER')) span.icon.is-small.is-left i.fas.fa-lock .field .control - input.button.is-primary(type='submit' value='Save') + input.button.is-primary(type='submit' value=lang('PROFILE_PASSWORD_BUTTON')) diff --git a/views/profile.pug b/views/profile.pug index 19892e3..532e8e0 100644 --- a/views/profile.pug +++ b/views/profile.pug @@ -2,7 +2,7 @@ extends layout.pug block content if config.pfp - h2 Profile + h2= lang('PROFILE_HEADER') div(style='margin-top: 1em;') .columns(style='margin-top: 1em;') .column.is-narrow @@ -17,7 +17,7 @@ block content br br form(action=`${_CC.config.base}profile/pfp`, method='POST') - label.label Image URL + label.label= lang('PROFILE_PFP_IMAGE_URL') .field.has-addons .control input.input(name='image', value=req.user.pfp) @@ -25,8 +25,8 @@ block content button.button.is-primary(type='submit') span.icon i.fas.fa-save - h2 Security + h2= lang('PROFILE_SECURITY') a.button.is-primary(href=`${_CC.config.base}profile/password`) span.icon i.fas.fa-shield-alt - span Change Password + span= lang('PROFILE_SECURITY_CHANGE_PASSWORD') diff --git a/views/resetpw.pug b/views/resetpw.pug index da2eb63..61d716e 100644 --- a/views/resetpw.pug +++ b/views/resetpw.pug @@ -9,26 +9,26 @@ mixin icon(c, text) block title if doc - h1 #{config.siteTitle} | Reset Password + h1= lang('RESET_PASSWORD_HEADER_VALID') else - h1 #{config.siteTitle} | Reset Link Invalid + h1= lang('RESET_PASSWORD_HEADER_INVALID') block content if doc if doc.pwExpiry > new Date().getTime() - +icon('fas fa-smile-beam', `Hello ${doc._id}! Please set your password here.`) + +icon('fas fa-smile-beam', lang('RESET_PASSWORD_GREETING_VALID', doc._id)) form(method='POST') .field - label.label Password + label.label= lang('RESET_PASSWORD_PASSWORD') .control.has-icons-left - input.input(type='password', name='password', placeholder='pa$$word!') + input.input(type='password', name='password', placeholder=lang('RESET_PASSWORD_PASSWORD_PLACEHOLDER')) span.icon.is-small.is-left i.fas.fa-lock .field .control input.button.is-primary(type='submit' value=`Reset Password`) else - +icon('fas fa-frown-open', 'Your reset link has expired. Please ask for a new one.') + +icon('fas fa-frown-open', lang('RESET_PASSWORD_GREETING_EXPIRED')) else - +icon('fas fa-frown-open', "This reset link isn't valid, perhaps the link was canceled or some characters at the end got cut off?") + +icon('fas fa-frown-open', lang('RESET_PASSWORD_INVALID')) diff --git a/views/setup.pug b/views/setup.pug index 178c1f5..31c7841 100644 --- a/views/setup.pug +++ b/views/setup.pug @@ -1,20 +1,20 @@ extends layout.pug block content - h2 Admin User + h2= lang('SETUP_ADMIN_USER') form(action=`${_CC.config.base}setup`, method='POST') .field - label.label Username + label.label= lang('SETUP_USERNAME') .control.has-icons-left - input.input(type='text', name='adminUsername', placeholder='john') + input.input(type='text', name='adminUsername', placeholder=lang('SETUP_USERNAME_PLACEHOLDER')) span.icon.is-small.is-left i.fas.fa-user .field - label.label Password + label.label= lang('SETUP_PASSWORD') .control.has-icons-left - input.input(type='password', name='adminPassword', placeholder='pa$$word!') + input.input(type='password', name='adminPassword', placeholder=lang('SETUP_PASSWORD_PLACEHOLDER')) span.icon.is-small.is-left i.fas.fa-lock .field .control - input.button.is-primary(type='submit' value='Set up!') \ No newline at end of file + input.button.is-primary(type='submit' value=lang('SETUP_BUTTON')) \ No newline at end of file diff --git a/views/supported-sites.pug b/views/supported-sites.pug index 99c4c0c..dc92221 100644 --- a/views/supported-sites.pug +++ b/views/supported-sites.pug @@ -3,8 +3,8 @@ extends layout.pug block content if req.headers.referer span.is-size-3(style='margin-top: 0; padding-top: 0;') - a(href=req.headers.referer) < Back + a(href=req.headers.referer) < #{lang('BACK_BUTTON')} ul for site of _CC.require('get-product-name').sites li= site.name - p Is a site missing or broken? Open an issue here! :) \ No newline at end of file + p!= lang('SUPPORTED_SITES_TEXT') \ No newline at end of file diff --git a/views/wishlist.pug b/views/wishlist.pug index 5fff90e..96d47e6 100644 --- a/views/wishlist.pug +++ b/views/wishlist.pug @@ -9,7 +9,7 @@ block title span.icon i.fas.fa-sync-alt .level-item - span #{config.siteTitle} - #{title} + span= lang('WISHLIST_TITLE', name) block content script(type='data/user_id')= req.user._id @@ -18,19 +18,19 @@ block content table.table.has-mobile-cards thead th # - th(style='width: 15%;') Image - th(style='width: 25%;') Name - th(style='width: 50%;') Note - th(style='width: 10%;') Price - th Edit Item - th Added By + th(style='width: 15%;')= lang('WISHLIST_IMAGE') + th(style='width: 25%;')= lang('WISHLIST_NAME') + th(style='width: 50%;')= lang('WISHLIST_NOTE') + th(style='width: 10%;')= lang('WISHLIST_PRICE') + th= lang('WISHLIST_EDIT_ITEM') + th= lang('WISHLIST_ADDED_BY') if req.params.user === req.user._id - th Move Top - th Move Up - th Move Down + th= lang('WISHLIST_MOVE_TOP') + th= lang('WISHLIST_MOVE_UP') + th= lang('WISHLIST_MOVE_DOWN') else - th Pledge - th Delete + th= lang('WISHLIST_PLEDGE') + th= lang('WIDHLIST_DELETE') tbody each item, index in wishlist if req.user._id === item.addedBy || req.params.user !== req.user._id @@ -47,14 +47,14 @@ block content target='_blank' )= (item.name ? item.name : item.url) else - td.ugc(data-label='Name')= item.name + td.ugc(data-label=lang('WISHLIST_NAME'))= item.name if _CC.config.wishlist.note.markdown - td.ugc(data-label='Note') + td.ugc(data-label=lang('WISHLIST_NOTE')) div!= item.note else - td.ugc(data-label='Note')= item.note - td.ugc(data-label='Price')= item.price - td(data-label='Edit Item') + td.ugc(data-label=lang('WISHLIST_NOTE'))= item.note + td.ugc(data-label=lang('WISHLIST_PRICE'))= item.price + td(data-label=lang('WISHLIST_EDIT_ITEM')) form.inline(method='GET', action=`${_CC.config.base}wishlist/${req.params.user}/note/${item.id}`) .field.inline .control.inline @@ -65,9 +65,9 @@ block content ) span.icon i.far.fa-edit - td.ugc(data-label='Added By')= item.addedBy + td.ugc(data-label=lang('WISHLIST_ADDED_BY'))= item.addedBy if req.params.user === req.user._id - td(data-label='Move Item Top') + td(data-label=lang('WISHLIST_MOVE_ITEM_TOP')) form.topForm.inline(method='POST', action=`${_CC.config.base}wishlist/${req.params.user}/move/top/${item.id}`) .field.inline .control.inline @@ -78,7 +78,7 @@ block content ) span.icon i.fas.fa-angle-double-up - td(data-label='Move Item Up') + td(data-label=lang('WISHLIST_MOVE_ITEM_UP')) form.upForm.inline(method='POST', action=`${_CC.config.base}wishlist/${req.params.user}/move/up/${item.id}`) .field.inline .control.inline @@ -89,7 +89,7 @@ block content ) span.icon i.fas.fa-arrow-up - td(data-label='Move Item Down') + td(data-label=lang('WISHLIST_MOVE_ITEM_DOWN')) form.downForm.inline(method='POST', action=`${_CC.config.base}wishlist/${req.params.user}/move/down/${item.id}`) .field.inline .control.inline @@ -101,7 +101,7 @@ block content span.icon i.fas.fa-arrow-down else - td(data-label='Pledge') + td(data-label=lang('WISHLIST_PLEDGE')) if req.params.user !== req.user._id && !item.pledgedBy form.inline(method='POST', action=`${_CC.config.base}wishlist/${req.params.user}/pledge/${item.id}`) .field.inline @@ -113,7 +113,7 @@ block content .control.inline input.inline.button(type='submit' value='Unpledge') if item.pledgedBy && item.pledgedBy !== req.user._id - span.ugc Pledged for by #{item.pledgedBy} + span.ugc=lang('WISHLIST_PLEDGED', item.pledgedBy) td(data-label='Delete Item') form.inline( method='POST', @@ -161,60 +161,60 @@ block content style='height: 100%;' readonly )= item.note - span.overflowWrap Added by: #{item.addedBy} + span.overflowWrap=lang('WISHLIST_ADDED_BY', item.addedBy) hr div if req.params.user !== req.user._id && !item.pledgedBy form.inline(method='POST', action=`${_CC.config.base}wishlist/${req.params.user}/pledge/${item.id}`) .field.inline .control.inline - input.inline.button.is-primary(type='submit' value='Pledge') + input.inline.button.is-primary(type='submit' value=lang('WISHLIST_PLEDGE')) if item.pledgedBy === req.user._id form.inline(method='POST', action=`${_CC.config.base}wishlist/${req.params.user}/unpledge/${item.id}`) .field.inline .control.inline - input.inline.button(type='submit' value='Unpledge') + input.inline.button(type='submit' value=lang('WISHLIST_UNPLEDGE')) if req.user._id === req.params.user form.inline(method='POST', action=`${_CC.config.base}wishlist/${req.params.user}/remove/${item.id}`) .field.inline .control.inline - input.inline.button.is-warning(type='submit' value='Remove') + input.inline.button.is-warning(type='submit' value=lang('WISHLIST_DELETE')) if req.user._id === req.params.user || req.user._id === item.addedBy form.inline(method='GET', action=`${_CC.config.base}wishlist/${req.params.user}/note/${item.id}`) .field.inline .control.inline - input.inline.button(type='submit', value='Edit item') + input.inline.button(type='submit', value=lang('WISHLIST_EDIT_ITEM')) if index !== firstCanSee && req.user._id === req.params.user form.inline(method='POST', action=`${_CC.config.base}wishlist/${req.params.user}/move/up/${item.id}`) .field.inline .control.inline - input.inline.button(type='submit' value='Move item up') + input.inline.button(type='submit' value=lang('WISHLIST_MOVE_ITEM_UP')) if index !== lastCanSee && req.user._id === req.params.user form.inline(method='POST', action=`${_CC.config.base}wishlist/${req.params.user}/move/down/${item.id}`) .field.inline .control.inline - input.inline.button(type='submit' value='Move item down') + input.inline.button(type='submit' value=lang('WISHLIST_MOVE_ITEM_DOWN')) form(method='POST') .field - label.label Item URL or Name (Supported Sites) + label.label!=lang('WISHLIST_URL_LABEL') .control.has-icons-left input.input( type='text', name='itemUrlOrName', - placeholder='https://www.amazon.com/dp/B00ZV9RDKK' + placeholder=lang('WISHLIST_URL_PLACEHOLDER') ) span.icon.is-small.is-left i.fas.fa-gift .field - label.label Note + label.label= lang('WISHLIST_NOTE') .control textarea.textarea( name='note', - placeholder='Optional' + placeholder=lang('WISHLIST_OPTIONAL') ) .field .control - input.button(type='submit' value=(req.user._id === req.params.user ? 'Add item to wishlist' : 'Pledge item')) + input.button(type='submit' value=(req.user._id === req.params.user ? lang('WISHLIST_ADD') : lang('WISHLIST_PLEDGE_ITEM'))) script(src=`${_CC.config.base}js/wishlist.js`) block print diff --git a/views/wishlists.pug b/views/wishlists.pug index 1dae881..de246b6 100644 --- a/views/wishlists.pug +++ b/views/wishlists.pug @@ -12,8 +12,8 @@ block content figure.image.is-square.is-fullwidth.is-marginless(style='display: inline-block;') img.is-rounded.is-fullwidth(src=req.user.pfp, style='object-fit: cover;') .column - span=req.user._id - span : ???/??? + span + span=lang('WISHLISTS_COUNTS_SELF', req.user._id) progress.progress(value=1, max=1) each user in users if req.user._id !== user.id @@ -26,6 +26,5 @@ block content figure.image.is-square.is-fullwidth.is-marginless(style='display: inline-block;') img.is-rounded.is-fullwidth(src=user.doc.pfp, style='object-fit: cover;') .column - span= user.id - span : #{totals(user.doc.wishlist).pledged}/#{user.doc.wishlist.length} + span= lang('WISHLISTS_COUNTS', user.id, totals(user.doc.wishlist).pledged, user.doc.wishlist.length) progress.progress.is-info(value=totals(user.doc.wishlist).pledged, max=user.doc.wishlist.length) \ No newline at end of file