diff --git a/lib/core/Doc.js b/lib/core/Doc.js index 9c15e9542b6c..a3e960c7352c 100644 --- a/lib/core/Doc.js +++ b/lib/core/Doc.js @@ -10,18 +10,18 @@ const MarkdownBlock = require('./MarkdownBlock.js'); const translate = require('../server/translate.js').translate; -const editThisDoc = translate( - 'Edit this Doc|recruitment message asking to edit the doc source' -); -const translateThisDoc = translate( - 'Translate this Doc|recruitment message asking to translate the docs' -); - // inner doc component for article itself class Doc extends React.Component { render() { let docSource = this.props.source; + const editThisDoc = translate( + 'Edit this Doc|recruitment message asking to edit the doc source' + ); + const translateThisDoc = translate( + 'Translate this Doc|recruitment message asking to translate the docs' + ); + if (this.props.version && this.props.version !== 'next') { // If versioning is enabled and the current version is not next, we need to trim out "version-*" from the source if we want a valid edit link. docSource = docSource.match(new RegExp(/version-.*\/(.*\.md)/, 'i'))[1]; diff --git a/lib/core/DocsLayout.js b/lib/core/DocsLayout.js index 7078eef068d3..8591aeed2124 100644 --- a/lib/core/DocsLayout.js +++ b/lib/core/DocsLayout.js @@ -17,7 +17,7 @@ const OnPageNav = require('./nav/OnPageNav.js'); const Site = require('./Site.js'); const translation = require('../server/translation.js'); const docs = require('../server/docs.js'); -const {idx, getGitLastUpdated} = require('./utils.js'); +const {getGitLastUpdated} = require('./utils.js'); // component used to generate whole webpage for docs, including sidebar/header/footer class DocsLayout extends React.Component { @@ -37,10 +37,10 @@ class DocsLayout extends React.Component { render() { const metadata = this.props.metadata; const content = this.props.children; - const i18n = translation[metadata.language]; const id = metadata.localized_id; const defaultTitle = metadata.title; let DocComponent = Doc; + if (this.props.Doc) { DocComponent = this.props.Doc; } @@ -50,19 +50,18 @@ class DocsLayout extends React.Component { updateTime = getGitLastUpdated(filepath); } - const title = - idx(i18n, ['localized-strings', 'docs', id, 'title']) || defaultTitle; + const title = translation.t(['docs', id, 'title']) || defaultTitle; const hasOnPageNav = this.props.config.onPageNav === 'separate'; const previousTitle = - idx(i18n, ['localized-strings', metadata.previous_id]) || - idx(i18n, ['localized-strings', 'previous']) || + translation.t(metadata.previous_id) || metadata.previous_title || + translation.t('previous') || 'Previous'; const nextTitle = - idx(i18n, ['localized-strings', metadata.next_id]) || - idx(i18n, ['localized-strings', 'next']) || + translation.t(metadata.next_id) || metadata.next_title || + translation.t('next') || 'Next'; return ( diff --git a/lib/core/Redirect.js b/lib/core/Redirect.js index 2a8af6f73074..3c20cd3014fc 100644 --- a/lib/core/Redirect.js +++ b/lib/core/Redirect.js @@ -8,14 +8,11 @@ const React = require('react'); const Head = require('./Head.js'); const translation = require('../server/translation.js'); -const {idx} = require('./utils.js'); // Component used to provide same head, header, footer, other scripts to all pages class Redirect extends React.Component { render() { - const tagline = - idx(translation, [this.props.language, 'localized-strings', 'tagline']) || - this.props.config.tagline; + const tagline = translation.t('tagline') || this.props.config.tagline; const title = this.props.title ? `${this.props.title} · ${this.props.config.title}` : (!this.props.config.disableTitleTagline && diff --git a/lib/core/Site.js b/lib/core/Site.js index ad45ac4647a4..df4c4bb99fb9 100644 --- a/lib/core/Site.js +++ b/lib/core/Site.js @@ -8,22 +8,19 @@ const React = require('react'); const fs = require('fs'); +const CWD = process.cwd(); + const HeaderNav = require('./nav/HeaderNav.js'); const Head = require('./Head.js'); -const Footer = require(`${process.cwd()}/core/Footer.js`); +const Footer = require(`${CWD}/core/Footer.js`); const translation = require('../server/translation.js'); const constants = require('./constants'); -const {idx} = require('./utils.js'); - -const CWD = process.cwd(); // Component used to provide same head, header, footer, other scripts to all pages class Site extends React.Component { render() { - const tagline = - idx(translation, [this.props.language, 'localized-strings', 'tagline']) || - this.props.config.tagline; + const tagline = translation.t('tagline') || this.props.config.tagline; const title = this.props.title ? `${this.props.title} · ${this.props.config.title}` : (!this.props.config.disableTitleTagline && diff --git a/lib/core/nav/HeaderNav.js b/lib/core/nav/HeaderNav.js index 893a67205fcd..97c7821ac281 100644 --- a/lib/core/nav/HeaderNav.js +++ b/lib/core/nav/HeaderNav.js @@ -12,29 +12,25 @@ const fs = require('fs'); const classNames = require('classnames'); const siteConfig = require(`${CWD}/siteConfig.js`); +const {versioning} = require('../../server/env.js'); const translation = require('../../server/translation.js'); -const env = require('../../server/env.js'); - -const translate = require('../../server/translate.js').translate; -const setLanguage = require('../../server/translate.js').setLanguage; - +const {translate} = require('../../server/translate.js'); const readMetadata = require('../../server/readMetadata.js'); readMetadata.generateMetadataDocs(); const Metadata = require('../metadata.js'); -const {idx, getPath} = require('../utils.js'); +const {getPath} = require('../utils.js'); const extension = siteConfig.cleanUrl ? '' : '.html'; // language dropdown nav item for when translations are enabled class LanguageDropDown extends React.Component { render() { - setLanguage(this.props.language || 'en'); const helpTranslateString = translate( 'Help Translate|recruit community translators for your project' ); // add all enabled languages to dropdown - const enabledLanguages = env.translation + const enabledLanguages = translation .enabledLanguages() .filter(lang => lang.tag !== this.props.language) .map(lang => { @@ -66,7 +62,7 @@ class LanguageDropDown extends React.Component { } // Get the current language full name for display in the header nav - const currentLanguage = env.translation + const currentLanguage = translation .enabledLanguages() .filter(lang => lang.tag === this.props.language) .map(lang => lang.name); @@ -143,10 +139,7 @@ class HeaderNav extends React.Component { ); } if (link.languages) { - if ( - env.translation.enabled && - env.translation.enabledLanguages().length > 1 - ) { + if (translation.enabled && translation.enabledLanguages().length > 1) { return ( - {idx(i18n, ['localized-strings', 'links', link.label]) || link.label} + {translation.t(['links', link.label]) || link.label} ); @@ -292,7 +285,7 @@ class HeaderNav extends React.Component { : 'headerTitle'; const versionsLink = this.props.baseUrl + - (env.translation.enabled + (translation.enabled ? `${this.props.language}/versions${extension}` : `versions${extension}`); return ( @@ -302,7 +295,7 @@ class HeaderNav extends React.Component { {siteConfig.headerIcon && ( {this.props.title} )} - {env.versioning.enabled && ( + {versioning.enabled && ( -

{this.props.version || env.versioning.defaultVersion}

+

{this.props.version || versioning.defaultVersion}

)} {this.renderResponsiveNav()} diff --git a/lib/core/nav/SideNav.js b/lib/core/nav/SideNav.js index 8cc150a64ea5..252d31e06eff 100644 --- a/lib/core/nav/SideNav.js +++ b/lib/core/nav/SideNav.js @@ -10,36 +10,28 @@ const classNames = require('classnames'); const siteConfig = require(`${process.cwd()}/siteConfig.js`); const translation = require('../../server/translation.js'); -const {getPath, idx} = require('../utils.js'); +const {getPath} = require('../utils.js'); class SideNav extends React.Component { // return appropriately translated category string getLocalizedCategoryString(category) { - const categoryString = - idx(translation, [ - this.props.language, - 'localized-strings', - 'categories', - category, - ]) || category; + const categoryString = translation.t(['categories', category]) || category; + return categoryString; } // return appropriately translated label to use for doc/blog in sidebar getLocalizedString(metadata) { let localizedString; - const i18n = translation[this.props.language]; const id = metadata.localized_id; const sbTitle = metadata.sidebar_label; if (sbTitle) { - localizedString = - idx(i18n, ['localized-strings', 'docs', id, 'sidebar_label']) || - sbTitle; + localizedString = translation.t(['docs', id, 'sidebar_label']) || sbTitle; } else { - localizedString = - idx(i18n, ['localized-strings', 'docs', id, 'title']) || metadata.title; + localizedString = translation.t(['docs', id, 'title']) || metadata.title; } + return localizedString; } diff --git a/lib/server/blog.js b/lib/server/blog.js index 0c218efc8034..475531de10d6 100644 --- a/lib/server/blog.js +++ b/lib/server/blog.js @@ -34,8 +34,12 @@ function fileToUrl(file) { function getPagesMarkup(numOfBlog, config) { const BlogPageLayout = require('../core/BlogPageLayout.js'); + const env = require('./env'); const blogPages = {}; const perPage = 10; + + env.translation.setLanguage('en'); + for (let page = 0; page < Math.ceil(numOfBlog / perPage); page++) { const metadata = {page, perPage}; const blogPageComp = ( @@ -65,10 +69,16 @@ function getMetadata(file) { function getPostMarkup(file, config) { const metadata = getMetadata(file); + if (!metadata) { return null; } + + const env = require('./env'); const BlogPostLayout = require('../core/BlogPostLayout.js'); + + env.translation.setLanguage('en'); + const blogPostComp = ( {metadata.content} diff --git a/lib/server/docs.js b/lib/server/docs.js index 387985198a05..7354890b7a6e 100644 --- a/lib/server/docs.js +++ b/lib/server/docs.js @@ -101,6 +101,8 @@ function getMarkup(rawContent, mdToHtml, metadata) { // replace any relative links to static assets (not in fenced code blocks) to absolute links content = replaceAssetsLink(content); + env.translation.setLanguage(metadata.language); + const DocsLayout = require('../core/DocsLayout.js'); return renderToStaticMarkupWithDoctype( } key + * @param {'default' | 'pages'} [category='default'] + * @param {Object} options + * @param {string} options.language + * + * @return {string} - translation or key + */ + t(key, category = 'default', options = {}) { + const language = options.language || this.getLanguage(); + + const categoryMap = { + default: 'localized-strings', + pages: 'pages-strings', + }; + + if (!categoryMap[category]) { + throw new Error(`Unknown category name: ${category}`); + } + + const message = idx( + this.translations, + [language, categoryMap[category]].concat(key) + ); + + if (message) { + if (category === 'pages' && options.fallbackFrom) { + console.error( + `Could not find a string translation in '${ + options.fallbackFrom + }' for string '${key}'. Using English version instead.` + ); + } + + return this.parseEscapeSequences(message); + } + + if (options.fallbackFrom) { + if (category === 'pages') { + // for pages we have more strict rules + throw new Error( + `Text that you've identified for translation ('${key}') hasn't been added to the global list in 'en.json'. To solve this problem run 'yarn write-translations'.` + ); + } + } else { + return this.t(key, category, { + language: DEFAULT_LANG, + fallbackFrom: language, + }); + } + + return null; } enabledLanguages = () => this.languages.filter(lang => lang.enabled); - getLanguage(file, refDir) { + /** + * Parse locale from doc file path + * + * @param {string} file + * @param {string} refDir + * + * @return {?string} + */ + getFileLanguage(file, refDir) { const separator = escapeStringRegexp(path.sep); const baseDir = escapeStringRegexp(path.basename(refDir)); const regexSubFolder = new RegExp( @@ -38,6 +114,7 @@ class Translation { const enabledLanguages = this.enabledLanguages().map( language => language.tag ); + if (enabledLanguages.indexOf(match[1]) !== -1) { return match[1]; } @@ -51,6 +128,61 @@ class Translation { this.enabled = true; this.languages = require(languagesFile); } + + this.loadTranslations(); + } + + loadTranslations() { + const translations = {languages: this.enabledLanguages()}; + + const files = glob.sync(`${CWD}/i18n/**`); + const langRegex = /\/i18n\/(.*)\.json$/; + let wasWarned = false; + + files.forEach(file => { + const extension = path.extname(file); + + if (extension === '.json') { + const match = langRegex.exec(file); + const language = match[1]; + + translations[language] = require(file); + + if (!Object.hasOwnProperty.call(this, language)) { + Object.defineProperty(this, language, { + get() { + if (!wasWarned) { + wasWarned = true; + + console.error( + "translation[language][category][key] api is deprecated. Please use translation.t(key, category = 'default')" + ); + } + + return translations[language]; + }, + set() { + // noop + }, + }); + } + } + }); + + this.translations = translations; + } + + /* handle escaped characters that get converted into json strings */ + parseEscapeSequences(str) { + return str + .replace(new RegExp('\\\\n', 'g'), '\n') + .replace(new RegExp('\\\\b', 'g'), '\b') + .replace(new RegExp('\\\\f', 'g'), '\f') + .replace(new RegExp('\\\\r', 'g'), '\r') + .replace(new RegExp('\\\\t', 'g'), '\t') + .replace(new RegExp("\\\\'", 'g'), "'") + .replace(new RegExp('\\\\"', 'g'), '"') + .replace(new RegExp('\\\\', 'g'), '\\'); } } diff --git a/lib/server/env/__tests__/Translation.test.js b/lib/server/env/__tests__/Translation.test.js index 0584244c8d2f..49b49c724ae2 100644 --- a/lib/server/env/__tests__/Translation.test.js +++ b/lib/server/env/__tests__/Translation.test.js @@ -27,7 +27,11 @@ beforeEach(() => { ]; }); -test('getLanguage', () => { +afterEach(() => { + jest.restoreAllMocks(); +}); + +test('#getFileLanguage()', () => { const testDocEnglish = path.join('translated_docs', 'en', 'test.md'); const testDocJapanese = path.join('translated_docs', 'ja', 'test.md'); const testDocJapaneseInSubfolder = path.join( @@ -39,13 +43,114 @@ test('getLanguage', () => { const testDocInSubfolder = path.join('docs', 'ro', 'test.md'); const testDocNoLanguage = path.join('docs', 'test.md'); - expect(translation.getLanguage(testDocEnglish, 'translated_docs')).toBe('en'); - expect(translation.getLanguage(testDocJapanese, 'translated_docs')).toBe( + expect(translation.getFileLanguage(testDocEnglish, 'translated_docs')).toBe( + 'en' + ); + expect(translation.getFileLanguage(testDocJapanese, 'translated_docs')).toBe( 'ja' ); expect( - translation.getLanguage(testDocJapaneseInSubfolder, 'translated_docs') + translation.getFileLanguage(testDocJapaneseInSubfolder, 'translated_docs') ).toBe('ja'); - expect(translation.getLanguage(testDocInSubfolder, 'docs')).toBeNull(); - expect(translation.getLanguage(testDocNoLanguage, 'docs')).toBeNull(); + expect(translation.getFileLanguage(testDocInSubfolder, 'docs')).toBeNull(); + expect(translation.getFileLanguage(testDocNoLanguage, 'docs')).toBeNull(); +}); + +describe('#t()', () => { + beforeEach(() => { + translation.translations = {}; + }); + + test('translate default category', () => { + translation.translations = { + en: { + 'localized-strings': { + foo: 'foo-en', + }, + }, + }; + + expect(translation.t('foo')).toBe('foo-en'); + }); + + test('key specified by an array', () => { + translation.translations = { + en: { + 'localized-strings': { + foo: { + bar: { + baz: 'baz-en', + }, + }, + }, + }, + }; + + expect(translation.t(['foo', 'bar', 'baz'])).toBe('baz-en'); + }); + + test('translate default category in other lang', () => { + translation.translations = { + fr: { + 'localized-strings': { + foo: 'foo-fr', + }, + }, + }; + + translation.setLanguage('fr'); + + expect(translation.t('foo')).toBe('foo-fr'); + }); + + test('fallback to en lang', () => { + jest.spyOn(console, 'error').mockImplementation(() => {}); + translation.translations = { + en: { + 'localized-strings': { + foo: 'foo-en', + }, + }, + }; + + translation.setLanguage('fr'); + + expect(translation.t('foo')).toBe('foo-en'); + expect(console.error).not.toHaveBeenCalled(); + }); + + test('returns null when no translation', () => { + expect(translation.t('foo')).toBe(null); + }); + + test('throws error when no translation for pages category', () => { + jest.spyOn(console, 'error').mockImplementation(() => {}); + translation.setLanguage('fr'); + + expect(() => + translation.t('foo', 'pages') + ).toThrowErrorMatchingInlineSnapshot( + `"Text that you've identified for translation ('foo') hasn't been added to the global list in 'en.json'. To solve this problem run 'yarn write-translations'."` + ); + expect(console.error).not.toHaveBeenCalled(); + }); + + test('warns when fallbacks to default lang for pages category', () => { + jest.spyOn(console, 'error').mockImplementation(() => {}); + + translation.translations = { + en: { + 'pages-strings': { + foo: 'foo-en', + }, + }, + }; + + translation.setLanguage('fr'); + + expect(translation.t('foo', 'pages')).toBe('foo-en'); + expect(console.error).toHaveBeenCalledWith( + "Could not find a string translation in 'fr' for string 'foo'. Using English version instead." + ); + }); }); diff --git a/lib/server/generate.js b/lib/server/generate.js index fd0710db0c4f..93ecb1e1ce9c 100644 --- a/lib/server/generate.js +++ b/lib/server/generate.js @@ -20,9 +20,8 @@ async function execute() { const glob = require('glob'); const chalk = require('chalk'); const Site = require('../core/Site.js'); - const env = require('./env.js'); const siteConfig = require(`${CWD}/siteConfig.js`); - const translate = require('./translate.js'); + const translation = require('./translation.js'); const feed = require('./feed.js'); const sitemap = require('./sitemap.js'); const join = path.join; @@ -281,9 +280,7 @@ async function execute() { fs.writeFileSync(mainCss, css); // compile/copy pages from user - const enabledLanguages = env.translation - .enabledLanguages() - .map(lang => lang.tag); + const enabledLanguages = translation.enabledLanguages().map(lang => lang.tag); files = glob.sync(join(CWD, 'pages', '**')); files.forEach(file => { // Why normalize? In case we are on Windows. @@ -326,7 +323,8 @@ async function execute() { normalizedFile.replace(`${sep}en${sep}`, sep + language + sep) ) ) { - translate.setLanguage(language); + translation.setLanguage(language); + const str = renderToStaticMarkupWithDoctype(
{ - if (!translation.getLanguage(file, translatedDir)) { + if (!translation.getFileLanguage(file, translatedDir)) { return; } @@ -149,16 +149,11 @@ class DocsMetadata { // Get the titles of the previous and next ids so that we can use them in // navigation buttons in DocsLayout.js Object.keys(metadatas).forEach(docId => { - const defaultTitles = { - next: 'Next', - previous: 'Previous', - }; - ['next', 'previous'].forEach(key => { const siblingId = metadatas[docId][key]; + const {title = null} = metadatas[siblingId] || {}; - metadatas[docId][`${key}_title`] = - (metadatas[siblingId] || {}).title || defaultTitles[siblingId]; + metadatas[docId][`${key}_title`] = title; }); }); @@ -182,7 +177,7 @@ class DocsMetadata { const sidebarIndex = this.sidebarIndex; const result = metadataUtils.extractMetadata(fs.readFileSync(file, 'utf8')); - const language = translation.getLanguage(file, refDir) || 'en'; + const language = translation.getFileLanguage(file, refDir) || 'en'; const metadata = {}; Object.keys(result.metadata).forEach(fieldName => { diff --git a/lib/server/metadata/__tests__/DocsMetadata.test.js b/lib/server/metadata/__tests__/DocsMetadata.test.js index b96437ea3110..84f6aa3d921c 100644 --- a/lib/server/metadata/__tests__/DocsMetadata.test.js +++ b/lib/server/metadata/__tests__/DocsMetadata.test.js @@ -30,7 +30,7 @@ test('metadata for simple docs', () => { }, translation: { enabled: false, - getLanguage() { + getFileLanguage() { return null; }, enabledLanguages() { @@ -70,7 +70,7 @@ test('wrong id format', () => { sidebars: {}, translation: { enabled: false, - getLanguage() { + getFileLanguage() { return null; }, enabledLanguages() { @@ -116,7 +116,7 @@ it('metadata for siteConfig.useEnglishUrl', () => { }, translation: { enabled: false, - getLanguage() { + getFileLanguage() { return null; }, enabledLanguages() { @@ -199,7 +199,7 @@ test('metadata with versioning', () => { }, translation: { enabled: false, - getLanguage() { + getFileLanguage() { return null; }, enabledLanguages() { @@ -244,7 +244,7 @@ test('metadata with translations', () => { }, translation: { enabled: true, - getLanguage(file) { + getFileLanguage(file) { return file.includes('/de/') ? 'de' : 'en'; }, enabledLanguages() { @@ -293,7 +293,7 @@ test('metadata with translations and versioning', () => { }, translation: { enabled: true, - getLanguage(file) { + getFileLanguage(file) { return file.includes('/de/') ? 'de' : 'en'; }, enabledLanguages() { diff --git a/lib/server/metadata/__tests__/__snapshots__/DocsMetadata.test.js.snap b/lib/server/metadata/__tests__/__snapshots__/DocsMetadata.test.js.snap index f9b9d5fabb0a..728cd1708dfb 100644 --- a/lib/server/metadata/__tests__/__snapshots__/DocsMetadata.test.js.snap +++ b/lib/server/metadata/__tests__/__snapshots__/DocsMetadata.test.js.snap @@ -11,7 +11,7 @@ Object { "next_id": "doc2", "next_title": "doc2", "permalink": "docs/doc1.html", - "previous_title": undefined, + "previous_title": null, "sidebar": "root", "source": "doc1.md", "title": "Document 1", @@ -21,7 +21,7 @@ Object { "id": "doc2", "language": "en", "localized_id": "doc2", - "next_title": undefined, + "next_title": null, "permalink": "docs/doc2.html", "previous": "doc1", "previous_id": "doc1", @@ -34,9 +34,9 @@ Object { "id": "doc3", "language": "en", "localized_id": "doc3", - "next_title": undefined, + "next_title": null, "permalink": "docs/doc3.html", - "previous_title": undefined, + "previous_title": null, "source": "doc3.md", "title": "doc3", }, @@ -44,9 +44,9 @@ Object { "id": "sub/doc1", "language": "en", "localized_id": "sub/doc1", - "next_title": undefined, + "next_title": null, "permalink": "docs/sub/doc1.html", - "previous_title": undefined, + "previous_title": null, "source": "sub/doc1.md", "title": "Document 1", }, @@ -64,7 +64,7 @@ Object { "next_id": "doc2", "next_title": "doc2", "permalink": "docs/en/doc1.html", - "previous_title": undefined, + "previous_title": null, "sidebar": "root", "source": "doc1.md", "title": "Document 1", @@ -74,7 +74,7 @@ Object { "id": "doc2", "language": "en", "localized_id": "doc2", - "next_title": undefined, + "next_title": null, "permalink": "docs/en/doc2.html", "previous": "doc1", "previous_id": "doc1", @@ -87,9 +87,9 @@ Object { "id": "doc3", "language": "en", "localized_id": "doc3", - "next_title": undefined, + "next_title": null, "permalink": "docs/en/doc3.html", - "previous_title": undefined, + "previous_title": null, "source": "doc3.md", "title": "doc3", }, @@ -97,9 +97,9 @@ Object { "id": "sub/doc1", "language": "en", "localized_id": "sub/doc1", - "next_title": undefined, + "next_title": null, "permalink": "docs/en/sub/doc1.html", - "previous_title": undefined, + "previous_title": null, "source": "sub/doc1.md", "title": "Document 1", }, @@ -112,9 +112,9 @@ Object { "id": "de-de/doc1", "language": "de", "localized_id": "de/doc1", - "next_title": undefined, + "next_title": null, "permalink": "docs/de/de/doc1.html", - "previous_title": undefined, + "previous_title": null, "source": "de/doc1.md", "title": "[DE] Document 1", }, @@ -122,9 +122,9 @@ Object { "id": "de-de/doc2", "language": "de", "localized_id": "de/doc2", - "next_title": undefined, + "next_title": null, "permalink": "docs/de/de/doc2.html", - "previous_title": undefined, + "previous_title": null, "source": "de/doc2.markdown", "title": "[DE] Document 2", }, @@ -137,7 +137,7 @@ Object { "next_id": "doc2", "next_title": "doc2", "permalink": "docs/de/doc1.html", - "previous_title": undefined, + "previous_title": null, "sidebar": "root", "source": "doc1.md", "title": "Document 1", @@ -147,7 +147,7 @@ Object { "id": "de-doc2", "language": "de", "localized_id": "doc2", - "next_title": undefined, + "next_title": null, "permalink": "docs/de/doc2.html", "previous": "de-doc1", "previous_id": "doc1", @@ -160,9 +160,9 @@ Object { "id": "de-doc3", "language": "de", "localized_id": "doc3", - "next_title": undefined, + "next_title": null, "permalink": "docs/de/doc3.html", - "previous_title": undefined, + "previous_title": null, "source": "doc3.md", "title": "doc3", }, @@ -170,9 +170,9 @@ Object { "id": "de-sub/doc1", "language": "de", "localized_id": "sub/doc1", - "next_title": undefined, + "next_title": null, "permalink": "docs/de/sub/doc1.html", - "previous_title": undefined, + "previous_title": null, "source": "sub/doc1.md", "title": "Document 1", }, @@ -185,7 +185,7 @@ Object { "next_id": "doc2", "next_title": "doc2", "permalink": "docs/en/doc1.html", - "previous_title": undefined, + "previous_title": null, "sidebar": "root", "source": "doc1.md", "title": "Document 1", @@ -195,7 +195,7 @@ Object { "id": "en-doc2", "language": "en", "localized_id": "doc2", - "next_title": undefined, + "next_title": null, "permalink": "docs/en/doc2.html", "previous": "en-doc1", "previous_id": "doc1", @@ -208,9 +208,9 @@ Object { "id": "en-doc3", "language": "en", "localized_id": "doc3", - "next_title": undefined, + "next_title": null, "permalink": "docs/en/doc3.html", - "previous_title": undefined, + "previous_title": null, "source": "doc3.md", "title": "doc3", }, @@ -218,9 +218,9 @@ Object { "id": "en-sub/doc1", "language": "en", "localized_id": "sub/doc1", - "next_title": undefined, + "next_title": null, "permalink": "docs/en/sub/doc1.html", - "previous_title": undefined, + "previous_title": null, "source": "sub/doc1.md", "title": "Document 1", }, @@ -233,9 +233,9 @@ Object { "id": "de-de/doc1", "language": "de", "localized_id": "de/doc1", - "next_title": undefined, + "next_title": null, "permalink": "docs/de/next/de/doc1.html", - "previous_title": undefined, + "previous_title": null, "source": "de/doc1.md", "title": "[DE] Document 1", "version": "next", @@ -244,9 +244,9 @@ Object { "id": "de-de/doc2", "language": "de", "localized_id": "de/doc2", - "next_title": undefined, + "next_title": null, "permalink": "docs/de/next/de/doc2.html", - "previous_title": undefined, + "previous_title": null, "source": "de/doc2.markdown", "title": "[DE] Document 2", "version": "next", @@ -260,7 +260,7 @@ Object { "next_id": "doc2", "next_title": "doc2", "permalink": "docs/de/next/doc1.html", - "previous_title": undefined, + "previous_title": null, "sidebar": "root", "source": "doc1.md", "title": "Document 1", @@ -271,7 +271,7 @@ Object { "id": "de-doc2", "language": "de", "localized_id": "doc2", - "next_title": undefined, + "next_title": null, "permalink": "docs/de/next/doc2.html", "previous": "de-doc1", "previous_id": "doc1", @@ -285,9 +285,9 @@ Object { "id": "de-doc3", "language": "de", "localized_id": "doc3", - "next_title": undefined, + "next_title": null, "permalink": "docs/de/next/doc3.html", - "previous_title": undefined, + "previous_title": null, "source": "doc3.md", "title": "doc3", "version": "next", @@ -296,9 +296,9 @@ Object { "id": "de-sub/doc1", "language": "de", "localized_id": "sub/doc1", - "next_title": undefined, + "next_title": null, "permalink": "docs/de/next/sub/doc1.html", - "previous_title": undefined, + "previous_title": null, "source": "sub/doc1.md", "title": "Document 1", "version": "next", @@ -312,7 +312,7 @@ Object { "next_id": "doc2", "next_title": "doc2", "permalink": "docs/en/next/doc1.html", - "previous_title": undefined, + "previous_title": null, "sidebar": "root", "source": "doc1.md", "title": "Document 1", @@ -323,7 +323,7 @@ Object { "id": "en-doc2", "language": "en", "localized_id": "doc2", - "next_title": undefined, + "next_title": null, "permalink": "docs/en/next/doc2.html", "previous": "en-doc1", "previous_id": "doc1", @@ -337,9 +337,9 @@ Object { "id": "en-doc3", "language": "en", "localized_id": "doc3", - "next_title": undefined, + "next_title": null, "permalink": "docs/en/next/doc3.html", - "previous_title": undefined, + "previous_title": null, "source": "doc3.md", "title": "doc3", "version": "next", @@ -348,9 +348,9 @@ Object { "id": "en-sub/doc1", "language": "en", "localized_id": "sub/doc1", - "next_title": undefined, + "next_title": null, "permalink": "docs/en/next/sub/doc1.html", - "previous_title": undefined, + "previous_title": null, "source": "sub/doc1.md", "title": "Document 1", "version": "next", @@ -369,7 +369,7 @@ Object { "next_id": "doc2", "next_title": "doc2", "permalink": "docs/next/doc1.html", - "previous_title": undefined, + "previous_title": null, "sidebar": "root", "source": "doc1.md", "title": "Document 1", @@ -380,7 +380,7 @@ Object { "id": "doc2", "language": "en", "localized_id": "doc2", - "next_title": undefined, + "next_title": null, "permalink": "docs/next/doc2.html", "previous": "doc1", "previous_id": "doc1", @@ -394,9 +394,9 @@ Object { "id": "doc3", "language": "en", "localized_id": "doc3", - "next_title": undefined, + "next_title": null, "permalink": "docs/next/doc3.html", - "previous_title": undefined, + "previous_title": null, "source": "doc3.md", "title": "doc3", "version": "next", @@ -405,10 +405,10 @@ Object { "id": "en-version-1.3.1-no-sidebar", "language": "en", "localized_id": "version-1.3.1-no-sidebar", - "next_title": undefined, + "next_title": null, "original_id": "no-sidebar", "permalink": "docs/en/1.3.1/no-sidebar.html", - "previous_title": undefined, + "previous_title": null, "source": "version-1.2.0/guides-no-sidebar.md", "title": "No Sidebar", "version": "1.3.1", @@ -420,12 +420,12 @@ Object { "localized_id": "version-1.3.1-translation", "next": "version-1.3.1-versioning", "next_id": "versioning", - "next_title": undefined, + "next_title": null, "original_id": "translation", "permalink": "docs/en/1.3.1/translation.html", "previous": "version-1.3.1-navigation", "previous_id": "navigation", - "previous_title": undefined, + "previous_title": null, "sidebar": "version-1.3.1-docs", "source": "version-1.2.1/guides-translation.md", "title": "Translations & Localization", @@ -436,12 +436,12 @@ Object { "id": "en-version-1.3.1-versioning", "language": "en", "localized_id": "version-1.3.1-versioning", - "next_title": undefined, + "next_title": null, "original_id": "versioning", "permalink": "docs/en/1.3.1/versioning.html", "previous": "version-1.3.1-translation", "previous_id": "translation", - "previous_title": undefined, + "previous_title": null, "sidebar": "version-1.3.1-docs", "source": "version-1.2.0/guides-versioning.md", "title": "Versioning", @@ -451,9 +451,9 @@ Object { "id": "sub/doc1", "language": "en", "localized_id": "sub/doc1", - "next_title": undefined, + "next_title": null, "permalink": "docs/next/sub/doc1.html", - "previous_title": undefined, + "previous_title": null, "source": "sub/doc1.md", "title": "Document 1", "version": "next", diff --git a/lib/server/readCategories.js b/lib/server/readCategories.js index 21c22427c5bb..7a6e4956cd2f 100644 --- a/lib/server/readCategories.js +++ b/lib/server/readCategories.js @@ -5,28 +5,13 @@ * LICENSE file in the root directory of this source tree. */ -const fs = require('fs'); - +const env = require('./env'); const Metadata = require('../core/metadata.js'); -const CWD = process.cwd(); -let languages; -if (fs.existsSync(`${CWD}/languages.js`)) { - languages = require(`${CWD}/languages.js`); -} else { - languages = [ - { - enabled: true, - name: 'English', - tag: 'en', - }, - ]; -} - // returns data broken up into categories for a sidebar function readCategories(sidebar) { - const enabledLanguages = languages - .filter(lang => lang.enabled) + const enabledLanguages = env.translation + .enabledLanguages() .map(lang => lang.tag); const allCategories = {}; diff --git a/lib/server/server.js b/lib/server/server.js index 87a59d9dc2c5..a6014aa60164 100644 --- a/lib/server/server.js +++ b/lib/server/server.js @@ -12,7 +12,6 @@ function execute(port, options) { const metadataUtils = require('./metadataUtils'); const blog = require('./blog'); const docs = require('./docs'); - const env = require('./env.js'); const express = require('express'); const React = require('react'); const request = require('request'); @@ -25,7 +24,7 @@ function execute(port, options) { const gaze = require('gaze'); const tinylr = require('tiny-lr'); const constants = require('../core/constants'); - const translate = require('./translate'); + const translation = require('./translation'); const {renderToStaticMarkupWithDoctype} = require('./renderUtils'); const feed = require('./feed'); const sitemap = require('./sitemap'); @@ -216,6 +215,9 @@ function execute(port, options) { if (siteConfig.wrapPagesHTML) { removeModuleAndChildrenFromCache(join('..', 'core', 'Site.js')); const Site = require(join('..', 'core', 'Site.js')); + + translation.setLanguage('en'); + const str = renderToStaticMarkupWithDoctype( lang.tag); @@ -287,7 +289,9 @@ function execute(port, options) { const ReactComp = require(tempFile); removeModuleAndChildrenFromCache(join('..', 'core', 'Site.js')); const Site = require(join('..', 'core', 'Site.js')); - translate.setLanguage(language); + + translation.setLanguage(language); + const str = renderToStaticMarkupWithDoctype( lang.enabled); - } + const enabledLanguages = env.translation.enabledLanguages(); // Create a url mapping to all the enabled languages files const urls = files.map(file => { diff --git a/lib/server/translate.js b/lib/server/translate.js index 68d929e8dc23..7ccce7d79bd1 100644 --- a/lib/server/translate.js +++ b/lib/server/translate.js @@ -5,61 +5,12 @@ * LICENSE file in the root directory of this source tree. */ -const translation = require('./translation.js'); - -let language = 'en'; - -/* handle escaped characters that get converted into json strings */ -function parseEscapeSequences(str) { - return str - .replace(new RegExp('\\\\n', 'g'), '\n') - .replace(new RegExp('\\\\b', 'g'), '\b') - .replace(new RegExp('\\\\f', 'g'), '\f') - .replace(new RegExp('\\\\r', 'g'), '\r') - .replace(new RegExp('\\\\t', 'g'), '\t') - .replace(new RegExp("\\\\'", 'g'), "'") - .replace(new RegExp('\\\\"', 'g'), '"') - .replace(new RegExp('\\\\', 'g'), '\\'); -} - -function setLanguage(lang) { - language = lang; -} - -function doesTranslationExist(str, lang) { - return ( - translation[lang] && - translation[lang]['pages-strings'] && - translation[lang]['pages-strings'][str] - ); -} +const {translation} = require('./env.js'); function translate(str) { - if (!language || language === '') { - // Check English, just in case; otherwise, just return the raw string back - if (doesTranslationExist(str, 'en')) { - return parseEscapeSequences(translation.en['pages-strings'][str]); - } - return str; - } - - if (!doesTranslationExist(str, language)) { - // if a translated string doesn't exist, but english does then fallback - if (doesTranslationExist(str, 'en')) { - console.error( - `Could not find a string translation in '${language}' for string '${str}'. Using English version instead.` - ); - - return parseEscapeSequences(translation.en['pages-strings'][str]); - } - throw new Error( - `Text that you've identified for translation ('${str}') hasn't been added to the global list in 'en.json'. To solve this problem run 'yarn write-translations'.` - ); - } - return parseEscapeSequences(translation[language]['pages-strings'][str]); + return translation.t(str, 'pages'); } module.exports = { - setLanguage, translate, }; diff --git a/lib/server/translation.js b/lib/server/translation.js index 885df363ae5f..df4aa40877b4 100644 --- a/lib/server/translation.js +++ b/lib/server/translation.js @@ -5,40 +5,6 @@ * LICENSE file in the root directory of this source tree. */ -// translation object contains all translations for each string in i18n/en.json - -const CWD = process.cwd(); -const fs = require('fs'); -const glob = require('glob'); -const path = require('path'); - -let languages; -if (fs.existsSync(`${CWD}/languages.js`)) { - languages = require(`${CWD}/languages.js`); -} else { - languages = [ - { - enabled: true, - name: 'English', - tag: 'en', - }, - ]; -} - -const enabledLanguages = languages.filter(lang => lang.enabled); - -const translation = {languages: enabledLanguages}; - -const files = glob.sync(`${CWD}/i18n/**`); -const langRegex = /\/i18n\/(.*)\.json$/; - -files.forEach(file => { - const extension = path.extname(file); - if (extension === '.json') { - const match = langRegex.exec(file); - const language = match[1]; - translation[language] = require(file); - } -}); +const {translation} = require('./env'); module.exports = translation; diff --git a/lib/server/utils.js b/lib/server/utils.js index 16a24432b7f9..4306d3c37285 100644 --- a/lib/server/utils.js +++ b/lib/server/utils.js @@ -15,12 +15,6 @@ function getSubDir(file, refDir) { return subDir !== '.' && !subDir.includes('..') ? subDir : null; } -function getLanguage(file, refDir) { - const env = require('./env.js'); - - return env.translation.getLanguage(file, refDir); -} - function isSeparateCss(file, separateDirs) { if (!separateDirs) { return false; @@ -52,7 +46,6 @@ function autoPrefixCss(cssContent) { module.exports = { getSubDir, - getLanguage, isSeparateCss, minifyCss, autoPrefixCss, diff --git a/lib/write-translations.js b/lib/write-translations.js index ce3f9aad8fbe..e1a7a6b79fd8 100755 --- a/lib/write-translations.js +++ b/lib/write-translations.js @@ -29,6 +29,7 @@ const nodePath = require('path'); const deepmerge = require('deepmerge'); const readMetadata = require('./server/readMetadata.js'); +const translation = require('./server/translation.js'); const CWD = process.cwd(); const siteConfig = require(`${CWD}/siteConfig.js`); @@ -191,6 +192,7 @@ function execute() { translations['localized-strings'], customTranslations['localized-strings'] ); + writeFileAndCreateFolder( `${CWD}/i18n/en.json`, `${JSON.stringify( @@ -204,6 +206,9 @@ function execute() { 2 )}\n` ); + + // re-read fresh translations + translation.loadTranslations(); } execute();