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
This commit is contained in:
Laxman 2018-08-28 21:34:02 +05:30 committed by Endilie Yacop Sucipto
parent d18b09954b
commit cfabaedc99
12 changed files with 94 additions and 66 deletions

View file

@ -100,8 +100,16 @@ If you want to add additional custom translation strings, or override any of the
```json ```json
{ {
"localized-strings": { "localized-strings": {
"id": "string", "docs": {
"id2": "string2" "id": {
"title": "string1",
"sidebar_label": "string2"
},
"version-0.0.1-id": {
"title": "string3",
"sidebar_label": "string4"
}
}
}, },
"pages-strings" : { "pages-strings" : {
"id3": "string3", "id3": "string3",

View file

@ -16,6 +16,7 @@ const DocsSidebar = require('./DocsSidebar.js');
const OnPageNav = require('./nav/OnPageNav.js'); const OnPageNav = require('./nav/OnPageNav.js');
const Site = require('./Site.js'); const Site = require('./Site.js');
const translation = require('../server/translation.js'); const translation = require('../server/translation.js');
const {idx} = require('./utils.js');
// component used to generate whole webpage for docs, including sidebar/header/footer // component used to generate whole webpage for docs, including sidebar/header/footer
class DocsLayout extends React.Component { class DocsLayout extends React.Component {
@ -35,16 +36,15 @@ class DocsLayout extends React.Component {
render() { render() {
const metadata = this.props.metadata; const metadata = this.props.metadata;
const content = this.props.children; 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; let DocComponent = Doc;
if (this.props.Doc) { if (this.props.Doc) {
DocComponent = this.props.Doc; DocComponent = this.props.Doc;
} }
const title = i18n const title =
? translation[this.props.metadata.language]['localized-strings'][ idx(i18n, ['localized-strings', 'docs', id, 'title']) || defaultTitle;
this.props.metadata.localized_id
] || this.props.metadata.title
: this.props.metadata.title;
const hasOnPageNav = this.props.config.onPageNav === 'separate'; const hasOnPageNav = this.props.config.onPageNav === 'separate';
return ( return (
<Site <Site
@ -65,7 +65,7 @@ class DocsLayout extends React.Component {
content={content} content={content}
config={this.props.config} config={this.props.config}
source={metadata.source} source={metadata.source}
hideTitle={this.props.metadata.hide_title} hideTitle={metadata.hide_title}
title={title} title={title}
version={metadata.version} version={metadata.version}
language={metadata.language} language={metadata.language}
@ -79,15 +79,10 @@ class DocsLayout extends React.Component {
metadata.previous_id metadata.previous_id
)}> )}>
{' '} {' '}
{i18n {idx(i18n, ['localized-strings', metadata.previous_id]) ||
? translation[this.props.metadata.language][ idx(i18n, ['localized-strings', 'previous']) ||
'localized-strings' metadata.previous_title ||
][metadata.previous_id] || 'Previous'}
translation[this.props.metadata.language][
'localized-strings'
].previous ||
'Previous'
: metadata.previous_title || 'Previous'}
</a> </a>
)} )}
{metadata.next_id && ( {metadata.next_id && (
@ -97,15 +92,10 @@ class DocsLayout extends React.Component {
metadata.localized_id, metadata.localized_id,
metadata.next_id metadata.next_id
)}> )}>
{i18n {idx(i18n, ['localized-strings', metadata.next_id]) ||
? translation[this.props.metadata.language][ idx(i18n, ['localized-strings', 'next']) ||
'localized-strings' metadata.next_title ||
][metadata.next_id] || 'Next'}{' '}
translation[this.props.metadata.language][
'localized-strings'
].next ||
'Next'
: metadata.next_title || 'Next'}{' '}
</a> </a>
)} )}
@ -113,7 +103,7 @@ class DocsLayout extends React.Component {
</Container> </Container>
{hasOnPageNav && ( {hasOnPageNav && (
<nav className="onPageNav"> <nav className="onPageNav">
<OnPageNav rawContent={this.props.children} /> <OnPageNav rawContent={content} />
</nav> </nav>
)} )}
</div> </div>

View file

@ -8,13 +8,14 @@
const React = require('react'); const React = require('react');
const Head = require('./Head.js'); const Head = require('./Head.js');
const translation = require('../server/translation.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 // Component used to provide same head, header, footer, other scripts to all pages
class Redirect extends React.Component { class Redirect extends React.Component {
render() { render() {
const tagline = translation[this.props.language] const tagline =
? translation[this.props.language]['localized-strings'].tagline idx(translation, [this.props.language, 'localized-strings', 'tagline']) ||
: this.props.config.tagline; this.props.config.tagline;
const title = this.props.title const title = this.props.title
? `${this.props.title} · ${this.props.config.title}` ? `${this.props.title} · ${this.props.config.title}`
: (!this.props.config.disableTitleTagline && : (!this.props.config.disableTitleTagline &&

View file

@ -14,15 +14,16 @@ const Head = require('./Head.js');
const Footer = require(`${process.cwd()}/core/Footer.js`); const Footer = require(`${process.cwd()}/core/Footer.js`);
const translation = require('../server/translation.js'); const translation = require('../server/translation.js');
const constants = require('./constants'); const constants = require('./constants');
const {idx} = require('./utils.js');
const CWD = process.cwd(); const CWD = process.cwd();
// Component used to provide same head, header, footer, other scripts to all pages // Component used to provide same head, header, footer, other scripts to all pages
class Site extends React.Component { class Site extends React.Component {
render() { render() {
const tagline = translation[this.props.language] const tagline =
? translation[this.props.language]['localized-strings'].tagline idx(translation, [this.props.language, 'localized-strings', 'tagline']) ||
: this.props.config.tagline; this.props.config.tagline;
const title = this.props.title const title = this.props.title
? `${this.props.title} · ${this.props.config.title}` ? `${this.props.title} · ${this.props.config.title}`
: (!this.props.config.disableTitleTagline && : (!this.props.config.disableTitleTagline &&

View file

@ -22,7 +22,7 @@ const readMetadata = require('../../server/readMetadata.js');
readMetadata.generateMetadataDocs(); readMetadata.generateMetadataDocs();
const Metadata = require('../metadata.js'); const Metadata = require('../metadata.js');
const utils = require('../utils.js'); const {idx, getPath} = require('../utils.js');
const extension = siteConfig.cleanUrl ? '' : '.html'; const extension = siteConfig.cleanUrl ? '' : '.html';
@ -56,7 +56,7 @@ class LanguageDropDown extends React.Component {
} }
return ( return (
<li key={lang.tag}> <li key={lang.tag}>
<a href={utils.getPath(href, this.props.cleanUrl)}>{lang.name}</a> <a href={getPath(href, this.props.cleanUrl)}>{lang.name}</a>
</li> </li>
); );
}); });
@ -188,7 +188,7 @@ class HeaderNav extends React.Component {
} }
href = href =
this.props.config.baseUrl + 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; const {id: currentID, sidebar} = this.props.current;
docItemActive = currentID && currentID === id; docItemActive = currentID && currentID === id;
@ -220,12 +220,11 @@ class HeaderNav extends React.Component {
(link.blog && this.props.current.blogListing) || (link.blog && this.props.current.blogListing) ||
(link.page && link.page === this.props.current.id), (link.page && link.page === this.props.current.id),
}); });
const i18n = translation[this.props.language];
return ( return (
<li key={`${link.label}page`} className={itemClasses}> <li key={`${link.label}page`} className={itemClasses}>
<a href={href} target={link.external ? '_blank' : '_self'}> <a href={href} target={link.external ? '_blank' : '_self'}>
{translation[this.props.language] {idx(i18n, ['localized-strings', 'links', link.label]) || link.label}
? translation[this.props.language]['localized-strings'][link.label]
: link.label}
</a> </a>
</li> </li>
); );

View file

@ -10,15 +10,18 @@ const classNames = require('classnames');
const siteConfig = require(`${process.cwd()}/siteConfig.js`); const siteConfig = require(`${process.cwd()}/siteConfig.js`);
const translation = require('../../server/translation.js'); const translation = require('../../server/translation.js');
const utils = require('../utils.js'); const {getPath, idx} = require('../utils.js');
class SideNav extends React.Component { class SideNav extends React.Component {
// return appropriately translated category string // return appropriately translated category string
getLocalizedCategoryString(category) { getLocalizedCategoryString(category) {
const categoryString = translation[this.props.language] const categoryString =
? translation[this.props.language]['localized-strings'][category] || idx(translation, [
category this.props.language,
: category; 'localized-strings',
'categories',
category,
]) || category;
return categoryString; return categoryString;
} }
@ -26,17 +29,16 @@ class SideNav extends React.Component {
getLocalizedString(metadata) { getLocalizedString(metadata) {
let localizedString; let localizedString;
const i18n = translation[this.props.language]; const i18n = translation[this.props.language];
const id = metadata.localized_id;
const sbTitle = metadata.sidebar_label; const sbTitle = metadata.sidebar_label;
if (sbTitle) { if (sbTitle) {
localizedString = i18n localizedString =
? i18n['localized-strings'][sbTitle] || sbTitle idx(i18n, ['localized-strings', 'docs', id, 'sidebar_label']) ||
: sbTitle; sbTitle;
} else { } else {
const id = metadata.original_id || metadata.localized_id; localizedString =
localizedString = i18n idx(i18n, ['localized-strings', 'docs', id, 'title']) || metadata.title;
? i18n['localized-strings'][id] || metadata.title
: metadata.title;
} }
return localizedString; return localizedString;
} }
@ -44,14 +46,14 @@ class SideNav extends React.Component {
// return link to doc in sidebar // return link to doc in sidebar
getLink(metadata) { getLink(metadata) {
if (metadata.permalink) { if (metadata.permalink) {
const targetLink = utils.getPath(metadata.permalink, siteConfig.cleanUrl); const targetLink = getPath(metadata.permalink, siteConfig.cleanUrl);
if (targetLink.match(/^https?:/)) { if (targetLink.match(/^https?:/)) {
return targetLink; return targetLink;
} }
return siteConfig.baseUrl + targetLink; return siteConfig.baseUrl + targetLink;
} }
if (metadata.path) { if (metadata.path) {
return `${siteConfig.baseUrl}blog/${utils.getPath( return `${siteConfig.baseUrl}blog/${getPath(
metadata.path, metadata.path,
siteConfig.cleanUrl siteConfig.cleanUrl
)}`; )}`;

View file

@ -28,9 +28,14 @@ function getPath(pathStr, cleanUrl = false) {
: removeExtension(pathStr); : removeExtension(pathStr);
} }
function idx(target, path) {
return path.reduce((obj, key) => obj && obj[key], target);
}
module.exports = { module.exports = {
blogPostHasTruncateMarker, blogPostHasTruncateMarker,
extractBlogPostBeforeTruncate, extractBlogPostBeforeTruncate,
getPath, getPath,
removeExtension, removeExtension,
idx,
}; };

View file

@ -180,7 +180,7 @@ function generateMetadataDocs() {
// metadata for english files // metadata for english files
const docsDir = path.join(CWD, '../', getDocsPath()); const docsDir = path.join(CWD, '../', getDocsPath());
let files = glob.sync(`${CWD}/../${getDocsPath()}/**`); let files = glob.sync(`${docsDir}/**`);
files.forEach(file => { files.forEach(file => {
const extension = path.extname(file); const extension = path.extname(file);

View file

@ -26,6 +26,7 @@ const fs = require('fs-extra');
const glob = require('glob'); const glob = require('glob');
const mkdirp = require('mkdirp'); const mkdirp = require('mkdirp');
const nodePath = require('path'); const nodePath = require('path');
const deepmerge = require('deepmerge');
const readMetadata = require('./server/readMetadata.js'); const readMetadata = require('./server/readMetadata.js');
@ -34,12 +35,19 @@ const siteConfig = require(`${CWD}/siteConfig.js`);
const sidebars = require(`${CWD}/sidebars.json`); const sidebars = require(`${CWD}/sidebars.json`);
let customTranslations = { let customTranslations = {
'localized-strings': {}, 'localized-strings': {
docs: {},
links: {},
categories: {},
},
'pages-strings': {}, 'pages-strings': {},
}; };
if (fs.existsSync(`${CWD}/data/custom-translation-strings.json`)) { if (fs.existsSync(`${CWD}/data/custom-translation-strings.json`)) {
customTranslations = JSON.parse( customTranslations = deepmerge(
JSON.parse(
fs.readFileSync(`${CWD}/data/custom-translation-strings.json`, 'utf8') fs.readFileSync(`${CWD}/data/custom-translation-strings.json`, 'utf8')
),
customTranslations
); );
} }
@ -54,13 +62,20 @@ function execute() {
next: 'Next', next: 'Next',
previous: 'Previous', previous: 'Previous',
tagline: siteConfig.tagline, tagline: siteConfig.tagline,
docs: {},
links: {},
categories: {},
}, },
'pages-strings': {}, 'pages-strings': {},
}; };
// look through markdown headers of docs for titles and categories to translate // look through markdown headers of docs for titles and categories to translate
const docsDir = nodePath.join(CWD, '../', readMetadata.getDocsPath()); 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 => { files.forEach(file => {
const extension = nodePath.extname(file); const extension = nodePath.extname(file);
if (extension === '.md' || extension === '.markdown') { if (extension === '.md' || extension === '.markdown') {
@ -75,11 +90,13 @@ function execute() {
return; return;
} }
const metadata = res.metadata; 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) { if (metadata.sidebar_label) {
translations['localized-strings'][metadata.sidebar_label] = translations['localized-strings'].docs[id].sidebar_label =
metadata.sidebar_label; metadata.sidebar_label;
} }
} }
@ -87,7 +104,7 @@ function execute() {
// look through header links for text to translate // look through header links for text to translate
siteConfig.headerLinks.forEach(link => { siteConfig.headerLinks.forEach(link => {
if (link.label) { 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 => { Object.keys(sidebars).forEach(sb => {
const categories = sidebars[sb]; const categories = sidebars[sb];
Object.keys(categories).forEach(category => { 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 => { Object.keys(sidebarContent).forEach(sb => {
const categories = sidebarContent[sb]; const categories = sidebarContent[sb];
Object.keys(categories).forEach(category => { 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'], translations['pages-strings'],
customTranslations['pages-strings'] customTranslations['pages-strings']
); );
translations['localized-strings'] = Object.assign( translations['localized-strings'] = deepmerge(
translations['localized-strings'], translations['localized-strings'],
customTranslations['localized-strings'] customTranslations['localized-strings']
); );

View file

@ -70,6 +70,7 @@
"commander": "^2.16.0", "commander": "^2.16.0",
"crowdin-cli": "^0.3.0", "crowdin-cli": "^0.3.0",
"cssnano": "^3.10.0", "cssnano": "^3.10.0",
"deepmerge": "^2.1.1",
"escape-string-regexp": "^1.0.5", "escape-string-regexp": "^1.0.5",
"express": "^4.15.3", "express": "^4.15.3",
"feed": "^1.1.0", "feed": "^1.1.0",

View file

@ -1969,6 +1969,10 @@ deep-is@~0.1.3:
version "0.1.3" version "0.1.3"
resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34" 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: default-require-extensions@^2.0.0:
version "2.0.0" version "2.0.0"
resolved "https://registry.yarnpkg.com/default-require-extensions/-/default-require-extensions-2.0.0.tgz#f5f8fbb18a7d6d50b21f641f649ebb522cfe24f7" resolved "https://registry.yarnpkg.com/default-require-extensions/-/default-require-extensions-2.0.0.tgz#f5f8fbb18a7d6d50b21f641f649ebb522cfe24f7"