diff --git a/app.js b/app.js index 6f4a0d7..3a53093 100644 --- a/app.js +++ b/app.js @@ -11,6 +11,7 @@ const helmet = require('helmet') require('dotenv').config({ path: 'variables.env' }) const helpers = require('./helpers') +const { translate, initializeTranslations } = require('./i18n/i18n') const breadcrumb = require('./lib/breadcrumb') const routes = require('./routes/index') const { initClients, getSpace } = require('./services/contentful') @@ -30,7 +31,6 @@ app.use(bodyParser.json()) app.use(bodyParser.urlencoded({ extended: false })) app.use(cookieParser()) app.use(express.static(path.join(__dirname, 'public'))) -app.use(breadcrumb()) // Force all requests on production to be served over https app.use(function (req, res, next) { @@ -84,11 +84,11 @@ app.use(async function (request, response, next) { const apis = [ { id: 'cda', - label: 'Delivery (published content)' + label: 'Delivery' }, { id: 'cpa', - label: 'Preview (draft content)' + label: 'Preview' } ] @@ -111,6 +111,10 @@ app.use(async function (request, response, next) { response.locals.currentLocale = defaultLocale } + // Initialize translations and include them on templates + initializeTranslations() + response.locals.translate = translate + // Inject custom helpers response.locals.helpers = helpers @@ -123,6 +127,8 @@ app.use(async function (request, response, next) { next() }) +app.use(breadcrumb()) + // Initialize the route handling // Check ./routes/index.js to get a list of all implemented routes app.use('/', routes) diff --git a/helpers.js b/helpers.js index 7e4427d..72acd75 100644 --- a/helpers.js +++ b/helpers.js @@ -1,4 +1,5 @@ const marked = require('marked') +const { translate } = require('./i18n/i18n') // Parse markdown text module.exports.markdown = (content = '') => { @@ -11,11 +12,11 @@ module.exports.markdown = (content = '') => { // A handy debugging function we can use to sort of "console.log" our data module.exports.dump = (obj) => JSON.stringify(obj, null, 2) -module.exports.formatMetaTitle = (title) => { +module.exports.formatMetaTitle = (title, localeCode = 'en-US') => { if (!title) { - return 'The Example App' + return translate('defaultTitle', localeCode) } - return `${title.charAt(0).toUpperCase()}${title.slice(1)} — The Example App` + return `${title.charAt(0).toUpperCase()}${title.slice(1)} — ${translate('defaultTitle', localeCode)}` } /** diff --git a/i18n/i18n.js b/i18n/i18n.js new file mode 100644 index 0000000..2454e73 --- /dev/null +++ b/i18n/i18n.js @@ -0,0 +1,46 @@ +const fs = require('fs') +const path = require('path') + +let translations = null +// Initializes translation dictionary with contents from /public/locales +module.exports.initializeTranslations = () => { + if (translations) { + return + } + + translations = {} + + const localesPath = path.join(__dirname, '..', 'public', 'locales') + + try { + const files = fs.readdirSync(localesPath) + + files.forEach((file) => { + const localeDict = require(path.join(localesPath, file)) + translations[file.replace('.json', '')] = localeDict + }) + } catch (error) { + console.error('Error loading localization files:') + console.error(error) + } +} + +/** + * Translate a static string + * @param symbol string Identifier for static text + * @param locale string Locale code + * + * @returns string + */ +module.exports.translate = (symbol, locale = 'en-US') => { + const localeDict = translations[locale] + if (!localeDict) { + return `Localization file for ${locale} is not available` + } + const translatedValue = localeDict[symbol] + if (!translatedValue) { + return `Translation not found for ${symbol} in ${locale}` + } + + return translatedValue +} diff --git a/lib/breadcrumb.js b/lib/breadcrumb.js index 59e6e82..d39f51b 100644 --- a/lib/breadcrumb.js +++ b/lib/breadcrumb.js @@ -1,4 +1,5 @@ const url = require('url') +const { translate } = require('../i18n/i18n') module.exports = (modifier) => { return (request, response, next) => { @@ -6,7 +7,10 @@ module.exports = (modifier) => { const urlComponents = url.parse(request.url).pathname.split('/').filter(Boolean) let breadcrumbs = [] - breadcrumbs.push({ label: 'Home', url: baseUrl }) + breadcrumbs.push({ + label: translate('homeLabel', response.locals.currentLocale.code), + url: baseUrl + }) // Map components of the path to breadcrumbs with resolvable URLs let mappedComponents = urlComponents.map((component, i, array) => { const path = array.slice(0, i + 1).join('/') diff --git a/public/locales/de-DE.json b/public/locales/de-DE.json new file mode 100644 index 0000000..9875605 --- /dev/null +++ b/public/locales/de-DE.json @@ -0,0 +1,77 @@ +{ + "defaultTitle": "Die Beispielanwendung", + "whatIsThisApp": "Was ist die Beispielanwendung?", + "metaDescription": "Dies ist die Beispielanwendung, eine Anwendung die Ihnen hilft Ihre eigene Anwendung mit Contentful zu bauen.", + "metaTwitterCard": "Dies ist die Beispielanwendung, eine Anwendung die Ihnen hilft Ihre eigene Anwendung mit Contentful zu bauen.", + "metaImageAlt": "Dies ist die Beispielanwendung, eine Anwendung die Ihnen hilft Ihre eigene Anwendung mit Contentful zu bauen.", + "metaImageDescription": "Dies ist die Beispielanwendung, eine Anwendung die Ihnen hilft Ihre eigene Anwendung mit Contentful zu bauen.", + "viewOnGithub": "Auf GitHub ansehen", + "apiSwitcherHelp": "Ansehen des veröffentlichten und unveröffentlichten Inhalts durch Wechsel von Delivery und Preview APIs.", + "apiLabelHelpcda": "(veröffentlichter Inhalt)", + "apiLabelHelpcpa": "(unveröffentlichter Inhalt)", + "locale": "Sprache", + "localeQuestion": "Sie arbeiten mit verschiedenen Sprachen? Dann können Sie die Sprache für Anfragen an die Content Delivery API definieren.", + "settingsLabel": "Einstellungen", + "logoAlt": "Die Beispielanwendung für Contentful", + "homeLabel": "Home", + "coursesLabel": "Kurse", + "footerDisclaimer": "Powered by Contentful. Diese Website und deren Materialien existieren nur für Demonstrationszwecken. Sie können diese benutzen, um den Inhalt ihres Contentful Kontos anzusehen.", + "imprintLabel": "Impressum", + "contactUsLabel": "Uns Kontaktieren", + "modalTitle": "Ein referenzierbares Beispiel für Entwickler, die Contentful benutzen.", + "modalIntro": "Dies ist die Beispielanwendung, eine Anwendung die Ihnen hilft Ihre eigene Anwendung mit Contentful zu bauen. Sie stellt eine Plattform fürs online Lernen dar, die mithilfe von Contentful gebaut wurde. (sehr meta!)", + "modalCodeIntro": "Wenn Sie es bevorzugen, sich die Hände schmutzig zu machen, sehen sie sich die Anwendung hier an", + "modalCTALabel": "Gut, verstanden.", + "editorialFeaturesHint": "Bearbeiten Sie diesen Eintrag in unserer Web App. Sie müssen sich eingelogged haben und Zugang zum Space haben, um diese Funktion nutzen zu können.", + "draftLabel": "Entwurf", + "pendingChangesLabel": "Änderungen vorbehalten", + "lessonModuleErrorTitle": "⚠️ Ungültiges Lektionsmodul", + "lessonModuleErrorBody": "Konnte den Typ nicht erkennen: ", + "nextLessonLabel": "Nächste Lektion ansehen", + "imageErrorTitle": "⚠️ Bild fehlt", + "viewCourseLabel": "Kurs ansehen", + "categoriesWelcomeLabel": "Willkommen zur folgenden Kategorie: ", + "sitemapWelcomeLabel": "Willkommen zur folgenden Kategorie: ", + "tableOfContentsLabel": "Inhalt", + "courseOverviewLabel": "Kurs Übersicht", + "overviewLabel": "Übersicht", + "durationLabel": "Dauer", + "minutesLabel": "min", + "skillLevelLabel": "Komplexität", + "startCourseLabel": "Kurs beginnen", + "categoriesLabel": "Kategorien", + "allCoursesLabel": "Alle Kurse", + "somethingWentWrongLabel": "Hmm, etwas ging schief.", + "tryLabel": "Versuchen Sie", + "contentModelChangedErrorLabel": "Überprüfen Sie, ob das Content Model verändert wurde.", + "draftOrPublishedErrorLabel": "Überprüfen Sie, ob es nicht veröffentlichte Änderungen gibt.", + "localeContentErrorLabel": "Überprüfen Sie, ob es Inhalt für diese Sprache gibt.", + "verifyCredentialsErrorLabel": "Überprüfen Sie, ob die Zugangsdaten richtig und nicht abgelaufen sind.", + "stackTraceErrorLabel": "Schauen Sie sich den folgenden Stack Trace an", + "errorLabel": "Fehler", + "stackTraceLabel": "Stack Trace", + "companyLabel": "Firma", + "officeLabel": "Büro in Berlin", + "germanyLabel": "Deutschland", + "registrationCourtLabel": "Amtsgericht", + "managingDirectorLabel": "Verwalter", + "vatNumberLabel": "Steuernummer", + "settingsIntroLabel": "Um Inhalt von unseren APIs zu bekommen, müssen Anwendungen von Kunden sich authentifizieren, sowohl mit der Space ID als auch dem Access Token.", + "changesSavedLabel": "Änderungen erfolgreich gespeichert!", + "errorOcurredTitleLabel": "Fehler aufgetreten", + "errorOcurredMessageLabel": "Einige Fehler sind aufgetreten. Bitte schauen Sie sich die Fehlermeldungen neben den Feldern an.", + "connectedToSpaceLabel": "Mit einem Space verbinden.", + "spaceIdLabel": "Space ID", + "spaceIdHelpText": "Die Space Id ist eine eindeutige Identifizierung für Ihren Space.", + "accessTokenLabel": "Access Token", + "contentDeliveryApiHelpText": "Schauen Sie sich veröffentlichten Inhalt mit dieser API an.", + "contentPreviewApiHelpText": "Schauen Sie sich unveröffentlichten Inhalt mit dieser API an. (z.B. Inhalt im Zustand “Entwurf”).", + "enableEditorialFeaturesLabel": "Editoriale Funktionen aktivieren.", + "enableEditorialFeaturesHelpText": "Aktivieren, um Bearbeitung und weitere kontextabhängige Helferlein zu aktivieren. Damit dies funktioniert, müssen sie Zugang zu dem Space haben.", + "saveSettingsButtonLabel": "Einstellungen Speichern", + "fieldIsRequiredLabel": "Diese Feld ist notwendig.", + "deliveryKeyInvalidLabel": "Ihr Delivery API Zugangsschlüssel ist ungültig.", + "spaceOrTokenInvalid": "Dieser Space existiert nicht, oder Ihr Access Token kommt nicht von diesem Space.", + "somethingWentWrongLabel": "Irgendetwas lief falsch.", + "previewKeyInvalidLabel": "Ihr Preview API Zugangsschlüssel ist ungültig." +} diff --git a/public/locales/en-US.json b/public/locales/en-US.json new file mode 100644 index 0000000..1932523 --- /dev/null +++ b/public/locales/en-US.json @@ -0,0 +1,77 @@ +{ + "defaultTitle": "The Example App", + "whatIsThisApp": "What is this example app?", + "metaDescription": "This is The Example App, an application built to serve you as a reference while building your own applications using Contentful.", + "metaTwitterCard": "This is The Example App, an application built to serve you as a reference while building your own applications using Contentful.", + "metaImageAlt": "This is The Example App, an application built to serve you as a reference while building your own applications using Contentful.", + "metaImageDescription": "This is The Example App, an application built to serve you as a reference while building your own applications using Contentful.", + "viewOnGithub": "View on Github", + "apiSwitcherHelp": "View the published or draft content by simply switching between the Deliver and Preview APIs.", + "apiLabelHelpcda": "(published content)", + "apiLabelHelpcpa": "(draft content)", + "locale": "Locale", + "localeQuestion": "Working with multiple languages? You can query the Content Delivery API for a specific locale.", + "settingsLabel": "Settings", + "logoAlt": "Contentful Example App", + "homeLabel": "Home", + "coursesLabel": "Courses", + "footerDisclaimer": "Powered by Contentful. This website and the materials found on it are for demo purposes. You can use this to preview the content created on your Contentful account.", + "imprintLabel": "Imprint", + "contactUsLabel": "Contact us", + "modalTitle": "A referenceable example for developers using Contentful", + "modalIntro": "This is The Example App, an application built to serve you as a reference while building your own applications using Contentful. This app is an online learning platform which teaches you how Contentful was used to build this app (so meta)!", + "modalCodeIntro": "If you prefer to start by getting your hands dirty with code, check out this app on", + "modalCTALabel": "Ok, got it.", + "editorialFeaturesHint": "Edit this entry in our web app. You have to be logged in and have access to the connected space to use this feature.", + "draftLabel": "draft", + "pendingChangesLabel": "pending changes", + "lessonModuleErrorTitle": "⚠️ Invalid lesson module", + "lessonModuleErrorBody": "Could not determine type of", + "nextLessonLabel": "View next lesson", + "imageErrorTitle": "⚠️ Image missing", + "viewCourseLabel": "view course", + "categoriesWelcomeLabel": "Welcome to", + "sitemapWelcomeLabel": "Welcome to", + "tableOfContentsLabel": "Table of contents", + "courseOverviewLabel": "Cource overview", + "overviewLabel": "Overview", + "durationLabel": "Duration", + "minutesLabel": "min", + "skillLevelLabel": "Skill level", + "startCourseLabel": "Start course", + "categoriesLabel": "Categories", + "allCoursesLabel": "All courses", + "somethingWentWrongLabel": "Oops Something went wrong", + "tryLabel": "Try", + "contentModelChangedErrorLabel": "Check if the content model has changed", + "draftOrPublishedErrorLabel": "Check the selection has content in draft or published state (for Preview or Delivery)", + "localeContentErrorLabel": "Check if there's any content for this locale", + "verifyCredentialsErrorLabel": "Verify credentials are correct and up to date", + "stackTraceErrorLabel": "Check the stack trace below", + "errorLabel": "Error", + "stackTraceLabel": "Stack trace", + "companyLabel": "Company", + "officeLabel": "Office Berlin", + "germanyLabel": "Germany", + "registrationCourtLabel": "Registration Court", + "managingDirectorLabel": "Managing Director", + "vatNumberLabel": "VAT Number", + "settingsIntroLabel": "To query and get content using the APIs, client applications need to authenticate with both the Space ID and an access token.", + "changesSavedLabel": "Changes saved successfully!", + "errorOcurredTitleLabel": "Error occurred", + "errorOcurredMessageLabel": "Some errors occurred. Please check the error messages next to the fields.", + "connectedToSpaceLabel": "Connected to space", + "spaceIdLabel": "Space ID", + "spaceIdHelpText": "The Space ID is a unique identifier for your space.", + "accessTokenLabel": "access token", + "contentDeliveryApiHelpText": "View published content using this API.", + "contentPreviewApiHelpText": "Preview unpublished content using this API (i.e. content with “Draft” status).", + "enableEditorialFeaturesLabel": "Enable editorial features", + "enableEditorialFeaturesHelpText": "Enable to display an edit link and other contextual helpers for authors. You need to have access to the connected space to make this work.", + "saveSettingsButtonLabel": "Save settings", + "fieldIsRequiredLabel": "This field is required", + "deliveryKeyInvalidLabel": "Your Delivery API key is invalid.", + "spaceOrTokenInvalid": "This space does not exist or your access token is not associated with your space.", + "somethingWentWrongLabel": "Something went wrong", + "previewKeyInvalidLabel": "Your Preview API key is invalid." +} diff --git a/routes/courses.js b/routes/courses.js index 5123d3a..4bf059a 100644 --- a/routes/courses.js +++ b/routes/courses.js @@ -11,6 +11,7 @@ const { const attachEntryState = require('../lib/entry-state') const { updateCookie } = require('../lib/cookies') +const { translate } = require('../i18n/i18n') /** * Renders courses list when `/courses` route is requested @@ -33,7 +34,11 @@ module.exports.getCourses = async (request, response, next) => { } categories = await getCategories(response.locals.currentLocale.code, response.locals.currentApi.id) - response.render('courses', { title: `All Courses (${courses.length})`, categories, courses }) + response.render('courses', { + title: `${translate('allCoursesLabel', response.locals.currentLocale.code)} (${courses.length})`, + categories, + courses + }) } /** diff --git a/routes/imprint.js b/routes/imprint.js index 74b3d16..9449087 100644 --- a/routes/imprint.js +++ b/routes/imprint.js @@ -1,3 +1,4 @@ +const { translate } = require('../i18n/i18n') /** * Renders imprint page when `/imprint` route is requested * @param request - Object - Express request @@ -6,6 +7,7 @@ * @returns {undefined} */ module.exports.getImprint = (request, response, next) => { - response.render('imprint', { title: 'Imprint' }) + response.render('imprint', { + title: translate('imprintLabel', response.locals.currentLocale.code) + }) } - diff --git a/routes/landingPage.js b/routes/landingPage.js index fc23f10..9a1a408 100644 --- a/routes/landingPage.js +++ b/routes/landingPage.js @@ -33,4 +33,3 @@ module.exports.getLandingPage = async (request, response, next) => { response.render('landingPage', { title: pathname, landingPage }) } - diff --git a/routes/settings.js b/routes/settings.js index e552725..ff2e3a8 100644 --- a/routes/settings.js +++ b/routes/settings.js @@ -5,6 +5,7 @@ const { createClient } = require('contentful') const { initClients, getSpace } = require('./../services/contentful') const { updateCookie } = require('../lib/cookies') +const { translate } = require('../i18n/i18n') const SETTINGS_NAME = 'theExampleAppSettings' @@ -20,7 +21,7 @@ async function renderSettings (response, opts) { } response.render('settings', { - title: 'Settings', + title: translate('settingsLabel', response.locals.currentLocale.code), errors: {}, hasErrors: false, success: false, @@ -56,6 +57,7 @@ module.exports.getSettings = async (request, response, next) => { * @returns {undefined} */ module.exports.postSettings = async (request, response, next) => { + const currentLocale = response.locals.currentLocale const errorList = [] const { spaceId, deliveryToken, previewToken, editorialFeatures } = request.body const settings = { @@ -69,21 +71,21 @@ module.exports.postSettings = async (request, response, next) => { if (!spaceId) { errorList.push({ field: 'spaceId', - message: 'This field is required' + message: translate('fieldIsRequiredLabel', currentLocale.code) }) } if (!deliveryToken) { errorList.push({ field: 'deliveryToken', - message: 'This field is required' + message: translate('fieldIsRequiredLabel', currentLocale.code) }) } if (!previewToken) { errorList.push({ field: 'previewToken', - message: 'This field is required' + message: translate('fieldIsRequiredLabel', currentLocale.code) }) } @@ -98,17 +100,17 @@ module.exports.postSettings = async (request, response, next) => { if (err.response.status === 401) { errorList.push({ field: 'deliveryToken', - message: 'Your Delivery API key is invalid.' + message: translate('deliveryKeyInvalidLabel', currentLocale.code) }) } else if (err.response.status === 404) { errorList.push({ field: 'spaceId', - message: 'This space does not exist or your access token is not associated with your space.' + message: translate('spaceOrTokenInvalid', currentLocale.code) }) } else { errorList.push({ field: 'deliveryToken', - message: `Something went wrong: ${err.response.data.message}` + message: `${translate('somethingWentWrongLabel', currentLocale.code)}: ${err.response.data.message}` }) } } @@ -126,17 +128,17 @@ module.exports.postSettings = async (request, response, next) => { if (err.response.status === 401) { errorList.push({ field: 'previewToken', - message: 'Your Preview API key is invalid.' + message: translate('previewKeyInvalidLabel', currentLocale.code) }) } else if (err.response.status === 404) { errorList.push({ field: 'spaceId', - message: 'This space does not exist or your delivery token is not associated with your space.' + message: translate('spaceOrTokenInvalid', currentLocale.code) }) } else { errorList.push({ field: 'previewToken', - message: `Something went wrong: ${err.response.data.message}` + message: `${translate('somethingWentWrongLabel', currentLocale.code)}: ${err.response.data.message}` }) } } @@ -171,4 +173,3 @@ module.exports.postSettings = async (request, response, next) => { success: errorList.length === 0 }) } - diff --git a/test/integration/courses.test.js b/test/integration/courses.test.js index afe2e81..2f323e6 100644 --- a/test/integration/courses.test.js +++ b/test/integration/courses.test.js @@ -7,7 +7,7 @@ describe('courses', () => { return request(app).get('/courses') .expect(200) .then((response) => { - expect(response.text.match(/

All Courses /)).toBeTruthy() + expect(response.text.match(/

All courses /)).toBeTruthy() }) }) test('it should render a course', () => { diff --git a/test/unit/index.test.js b/test/unit/index.test.js index 1d44508..e634683 100644 --- a/test/unit/index.test.js +++ b/test/unit/index.test.js @@ -2,6 +2,7 @@ const { getCourses, getCourse, getCoursesByCategory, getLesson } = require('../../routes/courses') const { getSettings } = require('../../routes/settings') const { mockCourse, mockCategory } = require('./mocks/index') +const { translate, initializeTranslations } = require('../../i18n/i18n') jest.mock('../../services/contentful') const contentful = require('../../services/contentful') @@ -25,6 +26,8 @@ const response = { } beforeAll(() => { + initializeTranslations() + contentful.getCourses.mockImplementation(() => [mockCourse]) contentful.getCourse.mockImplementation(() => mockCourse) @@ -45,7 +48,7 @@ describe('Courses', () => { test('it should courses list once', async () => { await getCourses(request, response) expect(response.render.mock.calls[0][0]).toBe('courses') - expect(response.render.mock.calls[0][1].title).toBe('All Courses (1)') + expect(response.render.mock.calls[0][1].title).toBe('All courses (1)') expect(response.render.mock.calls[0][1].courses.length).toBe(1) expect(response.render.mock.calls.length).toBe(1) }) @@ -88,3 +91,17 @@ describe('Settings', () => { expect(response.render.mock.calls[0][1].settings).toBe(response.locals.settings) }) }) + +describe('i18n', () => { + test('It returns an error when locale file is not found', () => { + expect(translate('foo', 'bar-locale')).toBe('Localization file for bar-locale is not available') + }) + + test('It returns an error when symbol is not found on locale file', () => { + expect(translate('foo', 'en-US')).toBe('Translation not found for foo in en-US') + }) + + test('It returns the translated string when symbol is found on locale file', () => { + expect(translate('coursesLabel', 'en-US')).toBe('Courses') + }) +}) diff --git a/views/course.pug b/views/course.pug index 000cf6c..262a938 100644 --- a/views/course.pug +++ b/views/course.pug @@ -10,12 +10,12 @@ block content .layout-sidebar section.layout-sidebar__sidebar .layout-sidebar__sidebar-header - h2.layout-sidebar__sidebar-title Table of contents + h2.layout-sidebar__sidebar-title #{translate('tableOfContentsLabel', currentLocale.code)} .layout-sidebar__sidebar-content .table-of-contents .table-of-contents__list .table-of-contents__item - a.table-of-contents__link(href=`/courses/${course.fields.slug}${queryString}` class=(currentPath.endsWith(course.fields.slug) ? 'active' : '') class=(visitedLessons.includes(course.sys.id) ? 'visited' : '')) Course overview + a.table-of-contents__link(href=`/courses/${course.fields.slug}${queryString}` class=(currentPath.endsWith(course.fields.slug) ? 'active' : '') class=(visitedLessons.includes(course.sys.id) ? 'visited' : '')) #{translate('courseOverviewLabel', currentLocale.code)} each l in course.fields.lessons if l.fields .table-of-contents__item @@ -28,17 +28,17 @@ block content h1.course__title= course.fields.title +editorialFeatures(course) .course__overview - h3.course__overview-title Overview + h3.course__overview-title #{translate('overviewLabel', currentLocale.code)} if course.fields.duration .course__overview-item svg.course__overview-icon use(xlink:href='/icons/icons.svg#duration') - .course__overview-value Duration: #{course.fields.duration} min + .course__overview-value #{translate('durationLabel', currentLocale.code)}: #{course.fields.duration} #{translate('minutesLabel', currentLocale.code)} if course.fields.skillLevel .course__overview-item svg.course__overview-icon use(xlink:href='/icons/icons.svg#skill-level') - .course__overview-value Skill level: #{course.fields.skillLevel} + .course__overview-value #{translate('skillLevelLabel', currentLocale.code)}: #{course.fields.skillLevel} .course__overview-cta-wrapper - a.course__overview-cta.cta(href=`/courses/${course.fields.slug}/lessons/${course.fields.lessons[0].fields.slug}${queryString}`) Start course + a.course__overview-cta.cta(href=`/courses/${course.fields.slug}/lessons/${course.fields.lessons[0].fields.slug}${queryString}`) #{translate('startCourseLabel', currentLocale.code)} .course__description !{helpers.markdown(course.fields.description)} diff --git a/views/courses.pug b/views/courses.pug index 3dd2932..cfb44ca 100644 --- a/views/courses.pug +++ b/views/courses.pug @@ -9,12 +9,12 @@ block content .layout-sidebar section.layout-sidebar__sidebar .layout-sidebar__sidebar-header - h2.layout-sidebar__sidebar-title Categories + h2.layout-sidebar__sidebar-title #{translate('categoriesLabel', currentLocale.code)} .layout-sidebar__sidebar-content .sidebar-menu ul.sidebar-menu__list li.sidebar-menu__item - a.sidebar-menu__link(href=`/courses${queryString}` class=(currentPath.endsWith('/courses') ? 'active' : '')) All courses + a.sidebar-menu__link(href=`/courses${queryString}` class=(currentPath.endsWith('/courses') ? 'active' : '')) #{translate('allCoursesLabel', currentLocale.code)} each category in categories li.sidebar-menu__item a.sidebar-menu__link(href=`/courses/categories/${category.fields.slug}${queryString}` class=(currentPath.endsWith(category.fields.slug) ? 'active' : '')) #{category.fields.title} diff --git a/views/error.pug b/views/error.pug index 8a261cc..2b6088f 100644 --- a/views/error.pug +++ b/views/error.pug @@ -6,18 +6,18 @@ block content .layout-centered +breadcrumb .error - h1 Oops Something went wrong (#{error.status}) - h2 Try: + h1 #{translate('somethingWentWrongLabel', currentLocale.code)} (#{error.status}) + h2 #{translate('tryLabel', currentLocale.code)}: ul - li Check if the content model has changed - li Check the selection has content in draft or published state (for Preview or Delivery) - li Check if there's any content for this locale - li Verify credentials are correct and up to date - li Check the stack trace below + li #{translate('contentModelChangedErrorLabel', currentLocale.code)} + li #{translate('draftOrPublishedErrorLabel', currentLocale.code)} + li #{translate('localeContentErrorLabel', currentLocale.code)} + li #{translate('verifyCredentialsErrorLabel', currentLocale.code)} + li #{translate('stackTraceErrorLabel', currentLocale.code)} if error.response - h2 Error + h2 #{translate('errorLabel', currentLocale.code)} pre.error__stack-trace code.shell #{helpers.dump(error.response.data)} - h2 Stack trace + h2 #{translate('stackTraceLabel', currentLocale.code)} pre.error__stack-trace code.shell #{error.stack} diff --git a/views/imprint.pug b/views/imprint.pug index 2de7ec9..086e0b1 100644 --- a/views/imprint.pug +++ b/views/imprint.pug @@ -13,22 +13,22 @@ block content table tbody tr - th Company: + th #{translate('companyLabel', currentLocale.code)}: td Contentful GmbH tr - th Office Berlin: + th #{translate('officeLabel', currentLocale.code)}: td | Ritterstr. 12-14 br | 10969 Berlin br - | Germany + | #{translate('germanyLabel', currentLocale.code)} tr - th Registration Court: + th #{translate('registrationCourtLabel', currentLocale.code)}: td Berlin-Charlottenburg HRB 155607 B tr - th Managing Director: + th #{translate('managingDirectorLabel', currentLocale.code)}: td Sascha Konietzke tr - th VAT Number: + th #{translate('vatNumberLabel', currentLocale.code)}: td DE275148225 diff --git a/views/layout.pug b/views/layout.pug index 8ba793e..a99807f 100644 --- a/views/layout.pug +++ b/views/layout.pug @@ -1,20 +1,20 @@ doctype html html head - title= helpers.formatMetaTitle(title) + title= helpers.formatMetaTitle(title, currentLocale.code) link(rel='stylesheet', href='/stylesheets/style.css') meta(name='viewport' content='width=device-width, initial-scale=1') - meta(name='description' content='This is The Example App, an application built to serve you as a reference while building your own applications using Contentful.') - meta(name='twitter:card' content='This is The Example App, an application built to serve you as a reference while building your own applications using Contentful.') - meta(property='og:title' content=helpers.formatMetaTitle(title)) + meta(name='description' content=translate('metaDescription', currentLocale.code)) + meta(name='twitter:card' content=translate('metaTwitterCard', currentLocale.code)) + meta(property='og:title' content=helpers.formatMetaTitle(title, currentLocale.code)) meta(property='og:type' content='article') meta(property='og:url' content=`http://contentful-example-app.herokuapp.com${currentPath}`) meta(property='og:image' content='http://contentful-example-app.herokuapp.com/og-image.jpg') meta(property='og:image:type' content='image/jpeg') meta(property='og:image:width' content='1200') meta(property='og:image:height' content='1200') - meta(property='og:image:alt' content='This is The Example App, an application built to serve you as a reference while building your own applications using Contentful.') - meta(property='og:description' content='This is The Example App, an application built to serve you as a reference while building your own applications using Contentful.') + meta(property='og:image:alt' content=translate('metaImageAlt', currentLocale.code)) + meta(property='og:description' content=translate('metaImageDescription', currentLocale.code)) link(rel='apple-touch-icon' sizes='120x120' href='/apple-touch-icon.png') link(rel='icon' type='image/png' sizes='32x32' href='/favicon-32x32.png') link(rel='icon' type='image/png' sizes='16x16' href='/favicon-16x16.png') @@ -31,55 +31,55 @@ html a#about-this-modal-trigger(href='#').header__title svg.course__overview-icon use(xlink:href='/icons/icons.svg#info') - span What is this example app? + span #{translate('whatIsThisApp', currentLocale.code)} .header__upper-second .header__upper-copy a.header__upper-link(href='https://images.contentful.com/82t39nctsu20/1JOkYZnY8YG0w88ImweME2/c8aef71dfe1ea633e16e17d99379416c/Github-repo_2x__1_.png' target='_blank' rel='noopener') svg.header__upper-icon use(xlink:href='/icons/icons.svg#github') - | View on Github + | #{translate('viewOnGithub', currentLocale.code)} .header__controls .header__controls_group form(action='' method='get') - .header__controls_label API: #{currentApi.label} + .header__controls_label API: #{currentApi.label} #{translate(`apiLabelHelp${currentApi.id}`, currentLocale.code)} .header__controls_dropdown - .header__controls_help_text View the published or draft content by simply switching between the Deliver and Preview APIs. + .header__controls_help_text #{translate('apiSwitcherHelp', currentLocale.code)} button.header__controls_button( type='submit' name='api' value='cda' class=`${currentApi.id === 'cda' ? 'header__controls_button--active' : ''}` - ) Delivery (published content) + ) Delivery #{translate('apiLabelHelpcda', currentLocale.code)} button.header__controls_button( type='submit' name='api' value='cpa' class=`${currentApi.id === 'cpa' ? 'header__controls_button--active' : ''}` - ) Preview (draft content) + ) Preview #{translate('apiLabelHelpcpa', currentLocale.code)} input(type='hidden' name='locale' value=currentLocale.code) .header__controls_group form(action='' method='get') input(type='hidden' name='api' value=currentApi.id) - .header__controls_label Locale: #{currentLocale.name} + .header__controls_label #{translate('locale', currentLocale.code)}: #{currentLocale.name} .header__controls_dropdown - .header__controls_help_text Working with multiple languages? You can query the Content Delivery API for a specific locale. + .header__controls_help_text #{translate('localeQuestion', currentLocale.code)} each locale in locales button.header__controls_button(type='submit' name='locale' value=locale.code class=`${locale.code === currentLocale.code ? 'header__controls_button--active' : ''}`)= `${locale.name} (${locale.code})` .header__upper-menu - a(href=`/settings${queryString}` class=(currentPath.startsWith('/settings') ? 'active' : '')) Settings + a(href=`/settings${queryString}` class=(currentPath.startsWith('/settings') ? 'active' : '')) #{translate('settingsLabel', currentLocale.code)} .header__lower-wrapper .header__lower.layout-centered .header__logo a.header__logo-link(href=`/${queryString}`) - img(src='/images/logo-node.svg' alt='Contentful Example App') + img(src='/images/logo-node.svg' alt=translate('logoAlt', currentLocale.code)) nav.header__navigation.main-navigation ul li - a(href=`/${queryString}` class=(currentPath === '/'? 'active' : '')) Home + a(href=`/${queryString}` class=(currentPath === '/'? 'active' : '')) #{translate('homeLabel', currentLocale.code)} li - a(href=`/courses${queryString}` class=(currentPath.startsWith('/courses') ? 'active' : '') ) Courses + a(href=`/courses${queryString}` class=(currentPath.startsWith('/courses') ? 'active' : '') ) #{translate('coursesLabel', currentLocale.code)} .main__content block content @@ -90,9 +90,9 @@ html nav.footer__navigation.main-navigation ul li - a(href=`/${queryString}` class=(currentPath === '/'? 'active' : '')) Home + a(href=`/${queryString}` class=(currentPath === '/'? 'active' : '')) #{translate('homeLabel', currentLocale.code)} li - a(href=`/courses${queryString}` class=(currentPath.startsWith('/courses') ? 'active' : '') ) Courses + a(href=`/courses${queryString}` class=(currentPath.startsWith('/courses') ? 'active' : '') ) #{translate('coursesLabel', currentLocale.code)} .footer__apps //- a(href='#') //- img(src='/images/badge-app-store.svg') @@ -103,12 +103,12 @@ html a(href='https://www.contentful.com/' target='_blank' rel='noopener') img.footer__disclaimer-logo(src='/images/contentful-logo.svg') p.footer__disclaimer-text - | Powered by Contentful. This website and the materials found on it are for demo purposes. You can use this to preview the content created on your Contentful account.  - a(href='https://images.contentful.com/82t39nctsu20/1JOkYZnY8YG0w88ImweME2/c8aef71dfe1ea633e16e17d99379416c/Github-repo_2x__1_.png' target='_blank' rel='noopener') View on Github + | #{translate('footerDisclaimer', currentLocale.code)}  + a(href='https://images.contentful.com/82t39nctsu20/1JOkYZnY8YG0w88ImweME2/c8aef71dfe1ea633e16e17d99379416c/Github-repo_2x__1_.png' target='_blank' rel='noopener') #{translate('viewOnGithub', currentLocale.code)} | .  - a(href=`/imprint${queryString}` ) Imprint + a(href=`/imprint${queryString}` ) #{translate('imprintLabel', currentLocale.code)} | .  - a(href=`https://www.contentful.com/contact/` ) Contact us + a(href=`https://www.contentful.com/contact/` ) #{translate('contactUsLabel', currentLocale.code)} | . .footer__social p @@ -124,15 +124,15 @@ html section.modal#about-this-modal .modal__overlay.close .modal__wrapper - h1.modal__title A referenceable example for developers using Contentful + h1.modal__title #{translate('modalTitle', currentLocale.code)} .modal__content - p This is The Example App, an application built to serve you as a reference while building your own applications using Contentful. This app is an online learning platform which teaches you how Contentful was used to build this app (so meta)! + p #{translate('modalIntro', currentLocale.code)} p - | If you prefer to start by getting your hands dirty with code, check out this app on  + | #{translate('modalCodeIntro', currentLocale.code)}  a(href='https://images.contentful.com/82t39nctsu20/1JOkYZnY8YG0w88ImweME2/c8aef71dfe1ea633e16e17d99379416c/Github-repo_2x__1_.png' target='_blank' rel='noopener') Github | . .modal__cta-wrapper - a(href='#').modal__cta.close Ok, got it. + a(href='#').modal__cta.close #{translate('modalCTALabel', currentLocale.code)} .modal__close-wrapper a(href='#').modal__close-button.close svg diff --git a/views/mixins/_courseCard.pug b/views/mixins/_courseCard.pug index 9263318..50c8af5 100644 --- a/views/mixins/_courseCard.pug +++ b/views/mixins/_courseCard.pug @@ -13,5 +13,4 @@ mixin courseCard(course = {fields: {title: '', description: '', categories: [], +entryState(course) p.course-card__description= course.fields.shortDescription .course-card__link-wrapper - a.course-card__link(href=`/courses/${course.fields.slug}${queryString}`) view course - + a.course-card__link(href=`/courses/${course.fields.slug}${queryString}`) #{translate("viewCourseLabel", currentLocale.code)} diff --git a/views/mixins/_editorialFeatures.pug b/views/mixins/_editorialFeatures.pug index 62cba29..ed6a5d2 100644 --- a/views/mixins/_editorialFeatures.pug +++ b/views/mixins/_editorialFeatures.pug @@ -15,4 +15,4 @@ mixin editorialFeatures(entry) .editorial-features__hint-wrapper svg.editorial-features__hint-icon use(xlink:href='/icons/icons.svg#info') - .editorial-features__hint-message Edit this entry in our web app. You have to be logged in and have access to the connected space to use this feature. + .editorial-features__hint-message #{translate('editorialFeaturesHint', currentLocale.code)} diff --git a/views/mixins/_entryState.pug b/views/mixins/_entryState.pug index 54b67a5..611b6ce 100644 --- a/views/mixins/_entryState.pug +++ b/views/mixins/_entryState.pug @@ -1,5 +1,5 @@ mixin entryState(entry) if entry.draft - .pill.pill--draft draft + .pill.pill--draft #{translate('draftLabel', currentLocale.code)} if entry.pendingChanges - .pill.pill--pending-changes pending changes + .pill.pill--pending-changes #{translate('pendingChangesLabel', currentLocale.code)} diff --git a/views/mixins/_lesson.pug b/views/mixins/_lesson.pug index 702c6c8..8a0924c 100644 --- a/views/mixins/_lesson.pug +++ b/views/mixins/_lesson.pug @@ -20,10 +20,10 @@ mixin lesson(lesson, course, nextLesson) when 'lessonImage' +lessonModuleImage(module) else - h2 ️️⚠️ Invalid lesson module + h2 ️️#{translate('lessonModuleErrorTitle', currentLocale.code)} p - span Could not determine type of + span #{translate('lessonModuleErrorBody', currentLocale.code)} strong #{module.sys.id} if nextLesson - a.lesson__cta.cta(href=`/courses/${course.fields.slug}/lessons/${nextLesson.fields.slug}${queryString}`) View next lesson + a.lesson__cta.cta(href=`/courses/${course.fields.slug}/lessons/${nextLesson.fields.slug}${queryString}`) #{translate('nextLessonLabel', currentLocale.code)} diff --git a/views/mixins/_lessonModuleImage.pug b/views/mixins/_lessonModuleImage.pug index df18d03..fd69ad4 100644 --- a/views/mixins/_lessonModuleImage.pug +++ b/views/mixins/_lessonModuleImage.pug @@ -3,4 +3,4 @@ mixin lessonModuleImage(module) if module.fields.image && module.fields.image.fields.file && module.fields.image.fields.file.url img.lesson-module-image__image(src=module.fields.image.fields.file.url alt=module.fields.image.fields.title) else - h3 ⚠️ Image missing + h3 #{translate('imageErrorTitle', currentLocale.code)} diff --git a/views/mixins/_moduleHighlightedCourse.pug b/views/mixins/_moduleHighlightedCourse.pug index 70cec60..ca2b223 100644 --- a/views/mixins/_moduleHighlightedCourse.pug +++ b/views/mixins/_moduleHighlightedCourse.pug @@ -14,4 +14,4 @@ mixin moduleHighlightedCourse(module, course) .module-higlighted-course__description-wrapper p !{helpers.markdown(course.fields.shortDescription)} .module-higlighted-course__link-wrapper - a.module-higlighted-course__link(href=`/courses/${course.fields.slug}${queryString}`) view course + a.module-higlighted-course__link(href=`/courses/${course.fields.slug}${queryString}`) #{translate('viewCourseLabel', currentLocale.code)} diff --git a/views/settings.pug b/views/settings.pug index d70eafd..7227485 100644 --- a/views/settings.pug +++ b/views/settings.pug @@ -13,60 +13,60 @@ block content .layout-centered-small +breadcrumb h1= title - p To query and get content using the APIs, client applications need to authenticate with both the Space ID and an access token. + p #{translate('settingsIntroLabel', currentLocale.code)} if success .status-block.status-block--success svg.status-block__icon.status-block__icon--success use(xlink:href='/icons/icons.svg#success') .status-block__content - .status-block__title Changes saved successfully! + .status-block__title #{translate('changesSavedLabel', currentLocale.code)} if hasErrors .status-block.status-block--error svg.status-block__icon.status-block__icon--error use(xlink:href='/icons/icons.svg#error') .status-block__content - .status-block__title Error occurred - .status-block__message Some errors occurred. Please check the error messages next to the fields. + .status-block__title #{translate('errorOcurredTitleLabel', currentLocale.code)} + .status-block__message #{translate('errorOcurredMessageLabel', currentLocale.code)} if space && !hasErrors .status-block.status-block--info svg.status-block__icon.status-block__icon--info use(xlink:href='/icons/icons.svg#info') .status-block__content - .status-block__message Connected to space “#{space.name}” + .status-block__message #{translate('connectedToSpaceLabel', currentLocale.code)} “#{space.name}” form(action=`/settings` method="POST" class="form") .form-item - label(for="input-space-id") Space ID + label(for="input-space-id") #{translate('spaceIdLabel', currentLocale.code)} input(type="text" name="spaceId" id="input-space-id" value=settings.spaceId) if 'spaceId' in errors +renderErrors(errors.spaceId) - .form-item__help-text The Space ID is a unique identifier for your space. + .form-item__help-text #{translate('spaceIdHelpText', currentLocale.code)} .form-item - label(for="input-delivery-token") Content Delivery API - access token + label(for="input-delivery-token") Content Delivery API - #{translate('accessTokenLabel', currentLocale.code)} input(type="text" name="deliveryToken" id="input-delivery-token" value=settings.deliveryToken) if 'deliveryToken' in errors +renderErrors(errors.deliveryToken) .form-item__help-text - | View published content using this API.  + | #{translate('contentDeliveryApiHelpText', currentLocale.code)}  a(href='https://www.contentful.com/developers/docs/references/content-delivery-api/' target='_blank' rel='noopener') Content Delivery API. .form-item - label(for="input-preview-token") Content Preview API - access token + label(for="input-preview-token") Content Preview API - #{translate('accessTokenLabel', currentLocale.code)} input(type="text" name="previewToken" id="input-preview-token" value=settings.previewToken) if 'previewToken' in errors +renderErrors(errors.previewToken) .form-item__help-text - | Preview unpublished content using this API (i.e. content with “Draft” status).  + | #{translate('contentPreviewApiHelpText', currentLocale.code)}  a(href='https://www.contentful.com/developers/docs/references/content-preview-api/' target='_blank' rel='noopener') Content Preview API. .form-item input(type="checkbox" name="editorialFeatures" id="input-editorial-features" checked=settings.editorialFeatures) - label(for="input-editorial-features") Enable editorial features - .form-item__help-text Enable to display an edit link and other contextual helpers for authors. You need to have access to the connected space to make this work. + label(for="input-editorial-features") #{translate('enableEditorialFeaturesLabel', currentLocale.code)} + .form-item__help-text #{translate('enableEditorialFeaturesHelpText', currentLocale.code)} .form-item - input.cta(type="submit" value="Save settings") + input.cta(type="submit" value=translate('saveSettingsButtonLabel', currentLocale.code))