From cfabaedc99397584461ae9d25508f03b863e4160 Mon Sep 17 00:00:00 2001 From: Laxman Date: Tue, 28 Aug 2018 21:34:02 +0530 Subject: [PATCH] Fix: conflicting strings issue in translations (#917) * Fix conflicting strings issue in translations * Preserve structure of `customTranslations` * Use `deepmerge` to merge whole of `localized-strings` * Simplify and make deep property access on an object safe * Fix deep property accessor and rename it to idx --- docs/guides-translation.md | 12 +++++- lib/core/DocsLayout.js | 42 ++++++++------------ lib/core/Redirect.js | 7 ++-- lib/core/Site.js | 7 ++-- lib/core/nav/HeaderNav.js | 11 +++-- lib/core/nav/SideNav.js | 30 +++++++------- lib/core/utils.js | 5 +++ lib/server/readMetadata.js | 2 +- lib/write-translations.js | 37 ++++++++++++----- package.json | 1 + website/data/custom-translation-strings.json | 2 +- yarn.lock | 4 ++ 12 files changed, 94 insertions(+), 66 deletions(-) diff --git a/docs/guides-translation.md b/docs/guides-translation.md index e1c249446b..14c57d45fa 100644 --- a/docs/guides-translation.md +++ b/docs/guides-translation.md @@ -100,8 +100,16 @@ If you want to add additional custom translation strings, or override any of the ```json { "localized-strings": { - "id": "string", - "id2": "string2" + "docs": { + "id": { + "title": "string1", + "sidebar_label": "string2" + }, + "version-0.0.1-id": { + "title": "string3", + "sidebar_label": "string4" + } + } }, "pages-strings" : { "id3": "string3", diff --git a/lib/core/DocsLayout.js b/lib/core/DocsLayout.js index decf445e4b..0f28006f16 100644 --- a/lib/core/DocsLayout.js +++ b/lib/core/DocsLayout.js @@ -16,6 +16,7 @@ const DocsSidebar = require('./DocsSidebar.js'); const OnPageNav = require('./nav/OnPageNav.js'); const Site = require('./Site.js'); const translation = require('../server/translation.js'); +const {idx} = require('./utils.js'); // component used to generate whole webpage for docs, including sidebar/header/footer class DocsLayout extends React.Component { @@ -35,16 +36,15 @@ class DocsLayout extends React.Component { render() { const metadata = this.props.metadata; const content = this.props.children; - const i18n = translation[this.props.metadata.language]; + 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; } - const title = i18n - ? translation[this.props.metadata.language]['localized-strings'][ - this.props.metadata.localized_id - ] || this.props.metadata.title - : this.props.metadata.title; + const title = + idx(i18n, ['localized-strings', 'docs', id, 'title']) || defaultTitle; const hasOnPageNav = this.props.config.onPageNav === 'separate'; return ( ←{' '} - {i18n - ? translation[this.props.metadata.language][ - 'localized-strings' - ][metadata.previous_id] || - translation[this.props.metadata.language][ - 'localized-strings' - ].previous || - 'Previous' - : metadata.previous_title || 'Previous'} + {idx(i18n, ['localized-strings', metadata.previous_id]) || + idx(i18n, ['localized-strings', 'previous']) || + metadata.previous_title || + 'Previous'} )} {metadata.next_id && ( @@ -97,15 +92,10 @@ class DocsLayout extends React.Component { metadata.localized_id, metadata.next_id )}> - {i18n - ? translation[this.props.metadata.language][ - 'localized-strings' - ][metadata.next_id] || - translation[this.props.metadata.language][ - 'localized-strings' - ].next || - 'Next' - : metadata.next_title || 'Next'}{' '} + {idx(i18n, ['localized-strings', metadata.next_id]) || + idx(i18n, ['localized-strings', 'next']) || + metadata.next_title || + 'Next'}{' '} → )} @@ -113,7 +103,7 @@ class DocsLayout extends React.Component { {hasOnPageNav && ( )} diff --git a/lib/core/Redirect.js b/lib/core/Redirect.js index 2b3014bbb9..2a8af6f730 100644 --- a/lib/core/Redirect.js +++ b/lib/core/Redirect.js @@ -8,13 +8,14 @@ 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 = translation[this.props.language] - ? translation[this.props.language]['localized-strings'].tagline - : this.props.config.tagline; + const tagline = + idx(translation, [this.props.language, 'localized-strings', '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 fa31cd36f6..ad45ac4647 100644 --- a/lib/core/Site.js +++ b/lib/core/Site.js @@ -14,15 +14,16 @@ const Head = require('./Head.js'); const Footer = require(`${process.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 = translation[this.props.language] - ? translation[this.props.language]['localized-strings'].tagline - : this.props.config.tagline; + const tagline = + idx(translation, [this.props.language, 'localized-strings', '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 f4be273ccc..145526ad00 100644 --- a/lib/core/nav/HeaderNav.js +++ b/lib/core/nav/HeaderNav.js @@ -22,7 +22,7 @@ const readMetadata = require('../../server/readMetadata.js'); readMetadata.generateMetadataDocs(); const Metadata = require('../metadata.js'); -const utils = require('../utils.js'); +const {idx, getPath} = require('../utils.js'); const extension = siteConfig.cleanUrl ? '' : '.html'; @@ -56,7 +56,7 @@ class LanguageDropDown extends React.Component { } return (
  • - {lang.name} + {lang.name}
  • ); }); @@ -188,7 +188,7 @@ class HeaderNav extends React.Component { } href = this.props.config.baseUrl + - utils.getPath(Metadata[id].permalink, this.props.config.cleanUrl); + getPath(Metadata[id].permalink, this.props.config.cleanUrl); const {id: currentID, sidebar} = this.props.current; docItemActive = currentID && currentID === id; @@ -220,12 +220,11 @@ class HeaderNav extends React.Component { (link.blog && this.props.current.blogListing) || (link.page && link.page === this.props.current.id), }); + const i18n = translation[this.props.language]; return (
  • - {translation[this.props.language] - ? translation[this.props.language]['localized-strings'][link.label] - : link.label} + {idx(i18n, ['localized-strings', 'links', link.label]) || link.label}
  • ); diff --git a/lib/core/nav/SideNav.js b/lib/core/nav/SideNav.js index 221b19009f..8cc150a64e 100644 --- a/lib/core/nav/SideNav.js +++ b/lib/core/nav/SideNav.js @@ -10,15 +10,18 @@ const classNames = require('classnames'); const siteConfig = require(`${process.cwd()}/siteConfig.js`); const translation = require('../../server/translation.js'); -const utils = require('../utils.js'); +const {getPath, idx} = require('../utils.js'); class SideNav extends React.Component { // return appropriately translated category string getLocalizedCategoryString(category) { - const categoryString = translation[this.props.language] - ? translation[this.props.language]['localized-strings'][category] || - category - : category; + const categoryString = + idx(translation, [ + this.props.language, + 'localized-strings', + 'categories', + category, + ]) || category; return categoryString; } @@ -26,17 +29,16 @@ class SideNav extends React.Component { getLocalizedString(metadata) { let localizedString; const i18n = translation[this.props.language]; + const id = metadata.localized_id; const sbTitle = metadata.sidebar_label; if (sbTitle) { - localizedString = i18n - ? i18n['localized-strings'][sbTitle] || sbTitle - : sbTitle; + localizedString = + idx(i18n, ['localized-strings', 'docs', id, 'sidebar_label']) || + sbTitle; } else { - const id = metadata.original_id || metadata.localized_id; - localizedString = i18n - ? i18n['localized-strings'][id] || metadata.title - : metadata.title; + localizedString = + idx(i18n, ['localized-strings', 'docs', id, 'title']) || metadata.title; } return localizedString; } @@ -44,14 +46,14 @@ class SideNav extends React.Component { // return link to doc in sidebar getLink(metadata) { if (metadata.permalink) { - const targetLink = utils.getPath(metadata.permalink, siteConfig.cleanUrl); + const targetLink = getPath(metadata.permalink, siteConfig.cleanUrl); if (targetLink.match(/^https?:/)) { return targetLink; } return siteConfig.baseUrl + targetLink; } if (metadata.path) { - return `${siteConfig.baseUrl}blog/${utils.getPath( + return `${siteConfig.baseUrl}blog/${getPath( metadata.path, siteConfig.cleanUrl )}`; diff --git a/lib/core/utils.js b/lib/core/utils.js index a01f992150..e8b81ea32e 100644 --- a/lib/core/utils.js +++ b/lib/core/utils.js @@ -28,9 +28,14 @@ function getPath(pathStr, cleanUrl = false) { : removeExtension(pathStr); } +function idx(target, path) { + return path.reduce((obj, key) => obj && obj[key], target); +} + module.exports = { blogPostHasTruncateMarker, extractBlogPostBeforeTruncate, getPath, removeExtension, + idx, }; diff --git a/lib/server/readMetadata.js b/lib/server/readMetadata.js index db6060bae2..74d73ba0b5 100644 --- a/lib/server/readMetadata.js +++ b/lib/server/readMetadata.js @@ -180,7 +180,7 @@ function generateMetadataDocs() { // metadata for english files const docsDir = path.join(CWD, '../', getDocsPath()); - let files = glob.sync(`${CWD}/../${getDocsPath()}/**`); + let files = glob.sync(`${docsDir}/**`); files.forEach(file => { const extension = path.extname(file); diff --git a/lib/write-translations.js b/lib/write-translations.js index c9c974b49f..ce3f9aad8f 100755 --- a/lib/write-translations.js +++ b/lib/write-translations.js @@ -26,6 +26,7 @@ const fs = require('fs-extra'); const glob = require('glob'); const mkdirp = require('mkdirp'); const nodePath = require('path'); +const deepmerge = require('deepmerge'); const readMetadata = require('./server/readMetadata.js'); @@ -34,12 +35,19 @@ const siteConfig = require(`${CWD}/siteConfig.js`); const sidebars = require(`${CWD}/sidebars.json`); let customTranslations = { - 'localized-strings': {}, + 'localized-strings': { + docs: {}, + links: {}, + categories: {}, + }, 'pages-strings': {}, }; if (fs.existsSync(`${CWD}/data/custom-translation-strings.json`)) { - customTranslations = JSON.parse( - fs.readFileSync(`${CWD}/data/custom-translation-strings.json`, 'utf8') + customTranslations = deepmerge( + JSON.parse( + fs.readFileSync(`${CWD}/data/custom-translation-strings.json`, 'utf8') + ), + customTranslations ); } @@ -54,13 +62,20 @@ function execute() { next: 'Next', previous: 'Previous', tagline: siteConfig.tagline, + docs: {}, + links: {}, + categories: {}, }, 'pages-strings': {}, }; // look through markdown headers of docs for titles and categories to translate const docsDir = nodePath.join(CWD, '../', readMetadata.getDocsPath()); - let files = glob.sync(`${CWD}/../${readMetadata.getDocsPath()}/**`); + const versionedDocsDir = nodePath.join(CWD, 'versioned_docs'); + let files = [ + ...glob.sync(`${docsDir}/**`), + ...glob.sync(`${versionedDocsDir}/**`), + ]; files.forEach(file => { const extension = nodePath.extname(file); if (extension === '.md' || extension === '.markdown') { @@ -75,11 +90,13 @@ function execute() { return; } const metadata = res.metadata; + const id = metadata.localized_id; - translations['localized-strings'][metadata.localized_id] = metadata.title; + translations['localized-strings'].docs[id] = {}; + translations['localized-strings'].docs[id].title = metadata.title; if (metadata.sidebar_label) { - translations['localized-strings'][metadata.sidebar_label] = + translations['localized-strings'].docs[id].sidebar_label = metadata.sidebar_label; } } @@ -87,7 +104,7 @@ function execute() { // look through header links for text to translate siteConfig.headerLinks.forEach(link => { if (link.label) { - translations['localized-strings'][link.label] = link.label; + translations['localized-strings'].links[link.label] = link.label; } }); @@ -95,7 +112,7 @@ function execute() { Object.keys(sidebars).forEach(sb => { const categories = sidebars[sb]; Object.keys(categories).forEach(category => { - translations['localized-strings'][category] = category; + translations['localized-strings'].categories[category] = category; }); }); @@ -120,7 +137,7 @@ function execute() { Object.keys(sidebarContent).forEach(sb => { const categories = sidebarContent[sb]; Object.keys(categories).forEach(category => { - translations['localized-strings'][category] = category; + translations['localized-strings'].categories[category] = category; }); }); }); @@ -170,7 +187,7 @@ function execute() { translations['pages-strings'], customTranslations['pages-strings'] ); - translations['localized-strings'] = Object.assign( + translations['localized-strings'] = deepmerge( translations['localized-strings'], customTranslations['localized-strings'] ); diff --git a/package.json b/package.json index 7f6705aa1d..6a6f23d01f 100644 --- a/package.json +++ b/package.json @@ -70,6 +70,7 @@ "commander": "^2.16.0", "crowdin-cli": "^0.3.0", "cssnano": "^3.10.0", + "deepmerge": "^2.1.1", "escape-string-regexp": "^1.0.5", "express": "^4.15.3", "feed": "^1.1.0", diff --git a/website/data/custom-translation-strings.json b/website/data/custom-translation-strings.json index 4f663211b7..c475739e04 100644 --- a/website/data/custom-translation-strings.json +++ b/website/data/custom-translation-strings.json @@ -4,6 +4,6 @@ "translation": "Translations and Localization" }, "pages-strings" : { - "Help Translate|recruit community translators for your project": "Help Us Translate" + "Help Translate|recruit community translators for your project": "Help Us Translate" } } diff --git a/yarn.lock b/yarn.lock index 9206246225..de2eb9237f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1969,6 +1969,10 @@ deep-is@~0.1.3: version "0.1.3" resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34" +deepmerge@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-2.1.1.tgz#e862b4e45ea0555072bf51e7fd0d9845170ae768" + default-require-extensions@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/default-require-extensions/-/default-require-extensions-2.0.0.tgz#f5f8fbb18a7d6d50b21f641f649ebb522cfe24f7"