From 19b857637bb7476bc30b14d7e5124187d95df1f9 Mon Sep 17 00:00:00 2001 From: Clement Hoang Date: Fri, 10 Nov 2017 10:46:59 +0000 Subject: [PATCH 01/37] Add translation download script --- .gitignore | 1 + crowdin/config.js | 4 ++ crowdin/download.js | 6 +++ package.json | 1 + yarn.lock | 104 +++++++++++++++++++++++++++++++++++++++++--- 5 files changed, 110 insertions(+), 6 deletions(-) create mode 100644 crowdin/config.js create mode 100644 crowdin/download.js diff --git a/.gitignore b/.gitignore index dbe72d17694..0741ceee83a 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ .idea node_modules public +crowdin/translations diff --git a/crowdin/config.js b/crowdin/config.js new file mode 100644 index 00000000000..a7fb0743ca1 --- /dev/null +++ b/crowdin/config.js @@ -0,0 +1,4 @@ +module.exports = { + key: process.env.CROWDIN_API_KEY, + url: 'https://api.crowdin.com/api/project/react' +} diff --git a/crowdin/download.js b/crowdin/download.js new file mode 100644 index 00000000000..8535d45ac6b --- /dev/null +++ b/crowdin/download.js @@ -0,0 +1,6 @@ +var Crowdin = require('crowdin'); +var config = require('./config'); + +var crowdin = new Crowdin({ apiKey: config.key, endpointUrl: config.url }); + +crowdin.downloadToPath('./translations'); diff --git a/package.json b/package.json index e888cb43f8d..f80494c7bda 100644 --- a/package.json +++ b/package.json @@ -84,6 +84,7 @@ "reset": "rimraf ./.cache" }, "devDependencies": { + "crowdin": "^1.0.0", "eslint-config-prettier": "^2.6.0", "lz-string": "^1.4.4", "recursive-readdir": "^2.2.1", diff --git a/yarn.lock b/yarn.lock index abe90f6bf4f..293587e7bbe 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1470,6 +1470,13 @@ binary-extensions@^1.0.0: version "1.10.0" resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.10.0.tgz#9aeb9a6c5e88638aad171e167f5900abe24835d0" +"binary@>= 0.3.0 < 1": + version "0.3.0" + resolved "https://registry.yarnpkg.com/binary/-/binary-0.3.0.tgz#9f60553bc5ce8c3386f3b553cff47462adecaa79" + dependencies: + buffers "~0.1.1" + chainsaw "~0.1.0" + bl@^1.0.0: version "1.2.1" resolved "https://registry.yarnpkg.com/bl/-/bl-1.2.1.tgz#cac328f7bee45730d404b692203fcb590e172d5e" @@ -1486,6 +1493,10 @@ block-stream@*: dependencies: inherits "~2.0.0" +bluebird@^2.9.21: + version "2.11.0" + resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-2.11.0.tgz#534b9033c022c9579c56ba3b3e5a5caafbb650e1" + bluebird@^3.0.5, bluebird@^3.3.4, bluebird@^3.5.0: version "3.5.1" resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.1.tgz#d9551f9de98f1fcda1e683d17ee91a0602ee2eb9" @@ -1704,6 +1715,10 @@ buffer@^4.3.0, buffer@^4.9.0: ieee754 "^1.1.4" isarray "^1.0.0" +buffers@~0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/buffers/-/buffers-0.1.1.tgz#b24579c3bed4d6d396aeee6d9a8ae7f5482ab7bb" + builtin-modules@^1.0.0: version "1.1.1" resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-1.1.1.tgz#270f076c5a72c02f5b65a47df94c5fe3a278892f" @@ -1844,6 +1859,12 @@ center-align@^0.1.1: align-text "^0.1.3" lazy-cache "^1.0.3" +chainsaw@~0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/chainsaw/-/chainsaw-0.1.0.tgz#5eab50b28afe58074d0d58291388828b5e5fbc98" + dependencies: + traverse ">=0.3.0 <0.4" + chalk@1.1.3, chalk@^1.0.0, chalk@^1.1.1, chalk@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98" @@ -2359,6 +2380,17 @@ cross-spawn@^5.0.1, cross-spawn@^5.1.0: shebang-command "^1.2.0" which "^1.2.9" +crowdin@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/crowdin/-/crowdin-1.0.0.tgz#7e6331c90cf65c460db1c38483edfef211a28015" + dependencies: + bluebird "^2.9.21" + graceful-fs "^3.0.6" + js-yaml "^3.2.7" + lodash "^3.6.0" + request "^2.54.0" + unzip "^0.1.11" + crypt@~0.0.1: version "0.0.2" resolved "https://registry.yarnpkg.com/crypt/-/crypt-0.0.2.tgz#88d7ff7ec0dfb86f713dc87bbb42d044d3e6c41b" @@ -3834,6 +3866,15 @@ fstream-ignore@^1.0.5: inherits "2" minimatch "^3.0.0" +"fstream@>= 0.1.30 < 1": + version "0.1.31" + resolved "https://registry.yarnpkg.com/fstream/-/fstream-0.1.31.tgz#7337f058fbbbbefa8c9f561a28cab0849202c988" + dependencies: + graceful-fs "~3.0.2" + inherits "~2.0.0" + mkdirp "0.5" + rimraf "2" + fstream@^1.0.0, fstream@^1.0.10, fstream@^1.0.2: version "1.0.11" resolved "https://registry.yarnpkg.com/fstream/-/fstream-1.0.11.tgz#5c1fb1f117477114f0632a0eb4b71b3cb0fd3171" @@ -4467,6 +4508,12 @@ got@^7.1.0: url-parse-lax "^1.0.0" url-to-options "^1.0.1" +graceful-fs@^3.0.6, graceful-fs@~3.0.2: + version "3.0.11" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-3.0.11.tgz#7613c778a1afea62f25c630a086d7f3acbbdd818" + dependencies: + natives "^1.1.0" + graceful-fs@^4.0.0, graceful-fs@^4.1.2, graceful-fs@^4.1.3, graceful-fs@^4.1.4, graceful-fs@^4.1.6: version "4.1.11" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.11.tgz#0e8bdfe4d1ddb8854d64e04ea7c00e2a026e5658" @@ -5505,7 +5552,7 @@ js-tokens@^3.0.0, js-tokens@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b" -js-yaml@^3.4.6, js-yaml@^3.5.2, js-yaml@^3.8.1, js-yaml@^3.9.1: +js-yaml@^3.2.7, js-yaml@^3.4.6, js-yaml@^3.5.2, js-yaml@^3.8.1, js-yaml@^3.9.1: version "3.10.0" resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.10.0.tgz#2e78441646bd4682e963f22b6e92823c309c62dc" dependencies: @@ -6003,7 +6050,7 @@ lodash.uniq@^4.5.0: version "4.5.0" resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773" -lodash@3.10.1: +lodash@3.10.1, lodash@^3.6.0: version "3.10.1" resolved "https://registry.yarnpkg.com/lodash/-/lodash-3.10.1.tgz#5bf45e8e49ba4189e17d482789dfd15bd140b7b6" @@ -6134,6 +6181,13 @@ markdown-table@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/markdown-table/-/markdown-table-1.1.0.tgz#1f5ae61659ced8808d882554c32e8b3f38dd1143" +"match-stream@>= 0.0.2 < 1": + version "0.0.2" + resolved "https://registry.yarnpkg.com/match-stream/-/match-stream-0.0.2.tgz#99eb050093b34dffade421b9ac0b410a9cfa17cf" + dependencies: + buffers "~0.1.1" + readable-stream "~1.0.0" + math-expression-evaluator@^1.2.14: version "1.2.17" resolved "https://registry.yarnpkg.com/math-expression-evaluator/-/math-expression-evaluator-1.2.17.tgz#de819fdbcd84dccd8fae59c6aeb79615b9d266ac" @@ -6400,7 +6454,7 @@ mixin-deep@^1.2.0: for-in "^1.0.2" is-extendable "^0.1.1" -"mkdirp@>=0.5 0", mkdirp@^0.5, mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@~0.5.0, mkdirp@~0.5.1: +mkdirp@0.5, "mkdirp@>=0.5 0", mkdirp@^0.5, mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@~0.5.0, mkdirp@~0.5.1: version "0.5.1" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" dependencies: @@ -6448,6 +6502,10 @@ nanomatch@^1.2.5: snapdragon "^0.8.1" to-regex "^3.0.1" +natives@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/natives/-/natives-1.1.0.tgz#e9ff841418a6b2ec7a495e939984f78f163e6e31" + natural-compare@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" @@ -6848,6 +6906,10 @@ output-file-sync@^1.1.2: mkdirp "^0.5.1" object-assign "^4.1.0" +"over@>= 0.0.5 < 1": + version "0.0.5" + resolved "https://registry.yarnpkg.com/over/-/over-0.0.5.tgz#f29852e70fd7e25f360e013a8ec44c82aedb5708" + p-cancelable@^0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-0.3.0.tgz#b9e123800bcebb7ac13a479be195b507b98d30fa" @@ -7783,6 +7845,15 @@ public-encrypt@^4.0.0: parse-asn1 "^5.0.0" randombytes "^2.0.1" +"pullstream@>= 0.4.1 < 1": + version "0.4.1" + resolved "https://registry.yarnpkg.com/pullstream/-/pullstream-0.4.1.tgz#d6fb3bf5aed697e831150eb1002c25a3f8ae1314" + dependencies: + over ">= 0.0.5 < 1" + readable-stream "~1.0.31" + setimmediate ">= 1.0.2 < 2" + slice-stream ">= 1.0.0 < 2" + pump@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/pump/-/pump-1.0.2.tgz#3b3ee6512f94f0e575538c17995f9f16990a5d51" @@ -8011,7 +8082,7 @@ read@^1.0.7: dependencies: mute-stream "~0.0.4" -readable-stream@1.0, "readable-stream@>=1.0.33-1 <1.1.0-0", readable-stream@~1.0.31: +readable-stream@1.0, "readable-stream@>=1.0.33-1 <1.1.0-0", readable-stream@~1.0.0, readable-stream@~1.0.31: version "1.0.34" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.0.34.tgz#125820e34bc842d2f2aaafafe4c2916ee32c157c" dependencies: @@ -8371,7 +8442,7 @@ request@2.81.0: tunnel-agent "^0.6.0" uuid "^3.0.0" -request@^2.58.0, request@^2.67.0, request@^2.74.0: +request@^2.54.0, request@^2.58.0, request@^2.67.0, request@^2.74.0: version "2.83.0" resolved "https://registry.yarnpkg.com/request/-/request-2.83.0.tgz#ca0b65da02ed62935887808e6f510381034e3356" dependencies: @@ -8729,7 +8800,7 @@ set-value@^2.0.0: is-plain-object "^2.0.3" split-string "^3.0.1" -setimmediate@^1.0.4, setimmediate@^1.0.5: +"setimmediate@>= 1.0.1 < 2", "setimmediate@>= 1.0.2 < 2", setimmediate@^1.0.4, setimmediate@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285" @@ -8823,6 +8894,12 @@ slice-ansi@1.0.0: dependencies: is-fullwidth-code-point "^2.0.0" +"slice-stream@>= 1.0.0 < 2": + version "1.0.0" + resolved "https://registry.yarnpkg.com/slice-stream/-/slice-stream-1.0.0.tgz#5b33bd66f013b1a7f86460b03d463dec39ad3ea0" + dependencies: + readable-stream "~1.0.31" + slugify@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/slugify/-/slugify-1.2.1.tgz#2118f4b0fbcfd79d7b2c451c22b724e5c1991330" @@ -9593,6 +9670,10 @@ traceur@0.0.105: semver "^4.3.3" source-map-support "~0.2.8" +"traverse@>=0.3.0 <0.4": + version "0.3.9" + resolved "https://registry.yarnpkg.com/traverse/-/traverse-0.3.9.tgz#717b8f220cc0bb7b44e40514c22b2e8bbc70d8b9" + trim-lines@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/trim-lines/-/trim-lines-1.1.0.tgz#9926d03ede13ba18f7d42222631fb04c79ff26fe" @@ -9871,6 +9952,17 @@ unzip-response@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/unzip-response/-/unzip-response-2.0.1.tgz#d2f0f737d16b0615e72a6935ed04214572d56f97" +unzip@^0.1.11: + version "0.1.11" + resolved "https://registry.yarnpkg.com/unzip/-/unzip-0.1.11.tgz#89749c63b058d7d90d619f86b98aa1535d3b97f0" + dependencies: + binary ">= 0.3.0 < 1" + fstream ">= 0.1.30 < 1" + match-stream ">= 0.0.2 < 1" + pullstream ">= 0.4.1 < 1" + readable-stream "~1.0.31" + setimmediate ">= 1.0.1 < 2" + urix@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/urix/-/urix-0.1.0.tgz#da937f7a62e21fec1fd18d49b35c2935067a6c72" From 7f72e0ded831171242dd829ba949a54f0eae72a1 Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Fri, 10 Nov 2017 22:48:43 +0000 Subject: [PATCH 02/37] Initial iteration on localization stuff --- gatsby-config.js | 19 +++++- gatsby/createPages.js | 68 ++++++++++++++++++---- plugins/gatsby-plugin-crowdin/index.js | 37 ++++++++++++ plugins/gatsby-plugin-crowdin/package.json | 4 ++ 4 files changed, 116 insertions(+), 12 deletions(-) create mode 100644 plugins/gatsby-plugin-crowdin/index.js create mode 100644 plugins/gatsby-plugin-crowdin/package.json diff --git a/gatsby-config.js b/gatsby-config.js index 6c65bdf2958..ab65ced2f50 100644 --- a/gatsby-config.js +++ b/gatsby-config.js @@ -33,17 +33,24 @@ module.exports = { { resolve: 'gatsby-source-filesystem', options: { - path: `${__dirname}/src/pages`, name: 'pages', + path: `${__dirname}/src/pages`, }, }, { resolve: 'gatsby-source-filesystem', options: { - name: 'packages', + name: 'english', path: `${__dirname}/content/`, }, }, + { + resolve: 'gatsby-source-filesystem', + options: { + name: 'translations', + path: `${__dirname}/crowdin/translations/docs/`, + }, + }, { resolve: 'gatsby-transformer-remark', options: { @@ -69,6 +76,14 @@ module.exports = { target: '_blank', }, }, + { + resolve: 'gatsby-plugin-crowdin', + options: { + defaultLanguageCode: 'en', + defaultSourceName: 'english', + translationsSourceName: 'translations', + }, + }, 'gatsby-remark-use-jsx', { resolve: 'gatsby-remark-prismjs', diff --git a/gatsby/createPages.js b/gatsby/createPages.js index 8dd2922c4c4..e8f108b1327 100644 --- a/gatsby/createPages.js +++ b/gatsby/createPages.js @@ -8,6 +8,21 @@ const {resolve} = require('path'); +const DEFAULT_LANGUAGE_CODE = 'en' + +// TODO Copy this helper method into a shared util +const getLanguageCodeFromPath = path => { + const match = path.match(/^([a-z]{2}|[a-z]{2}-[A-Z]+)\//); + + return match ? match[1] : null; +} + +// TODO Create Remark parser that adds language prefix to markdown links + +// TODO Decide how we handle Gatsby website links? +// Maybe attach a language code field to GraphQL so we can pass it to templates, +// So it can be evaluated dynamically at build time to generate links that are static at runtime? + module.exports = async ({graphql, boundActionCreators}) => { const {createPage, createRedirect} = boundActionCreators; @@ -20,12 +35,16 @@ module.exports = async ({graphql, boundActionCreators}) => { const tutorialTemplate = resolve(__dirname, '../src/templates/tutorial.js'); // Redirect /index.html to root. + // TODO Setup redirects for each language code too (eg '/zn-CH/index.html' => '/zn-CH/'). createRedirect({ fromPath: '/index.html', redirectInBrowser: true, toPath: '/', }); + // TODO Maybe redirect naked root / to a page that checks browser language + // And auto-redirects to user's specific //index.html + const allMarkdown = await graphql( ` { @@ -33,6 +52,7 @@ module.exports = async ({graphql, boundActionCreators}) => { edges { node { fields { + path redirect slug } @@ -50,7 +70,12 @@ module.exports = async ({graphql, boundActionCreators}) => { } allMarkdown.data.allMarkdownRemark.edges.forEach(edge => { - const slug = edge.node.fields.slug; + const {fields} = edge.node; + const {path, slug} = fields; + + // Parse language code from path for Crowdin content. + // Fall back to English as the default. + const languageCode = getLanguageCodeFromPath(path) || DEFAULT_LANGUAGE_CODE; if (slug === 'docs/error-decoder.html') { // No-op so far as markdown templates go. @@ -79,26 +104,52 @@ module.exports = async ({graphql, boundActionCreators}) => { template = tutorialTemplate; } - const createArticlePage = path => + const createArticlePage = path => { + path = path.replace(/^\//, ''); + + const localizedPath = `/${languageCode}/${path}`; + createPage({ - path: path, + path: localizedPath, component: template, context: { slug, }, }); + // TODO Create these redirects in a format that allows Netlify to insert language code. + // Daniel mentioned that should be possible in a comment on #82. + // We only need to create redirect once. + + // Create redirect without locale if languageCode is default. + // This allows us to support backwards compatible links (pre-localization) + if (languageCode === DEFAULT_LANGUAGE_CODE) { + createRedirect({ + fromPath: `/${path}`, + redirectInBrowser: true, + toPath: localizedPath, + }); + } + } + // Register primary URL. createArticlePage(slug); // Register redirects as well if the markdown specifies them. - if (edge.node.fields.redirect) { + if (fields.redirect) { let redirect = JSON.parse(edge.node.fields.redirect); if (!Array.isArray(redirect)) { redirect = [redirect]; } redirect.forEach(fromPath => { + fromPath = `${languageCode}/${fromPath}`; + + // A leading "/" is required for redirects to work, + // But multiple leading "/" will break redirects. + // For more context see github.com/reactjs/reactjs.org/pull/194 + const toPath = `/${languageCode}/${slug.replace(/^\//, '')}`; + if (redirectToSlugMap[fromPath] != null) { console.error( `Duplicate redirect detected from "${fromPath}" to:\n` + @@ -108,14 +159,10 @@ module.exports = async ({graphql, boundActionCreators}) => { process.exit(1); } - // A leading "/" is required for redirects to work, - // But multiple leading "/" will break redirects. - // For more context see github.com/reactjs/reactjs.org/pull/194 - const toPath = slug.startsWith('/') ? slug : `/${slug}`; + redirectToSlugMap[fromPath] = toPath; - redirectToSlugMap[fromPath] = slug; createRedirect({ - fromPath: `/${fromPath}`, + fromPath, redirectInBrowser: true, toPath, }); @@ -146,6 +193,7 @@ module.exports = async ({graphql, boundActionCreators}) => { const newestBlogNode = newestBlogEntry.data.allMarkdownRemark.edges[0].node; // Blog landing page should always show the most recent blog entry. + // TODO Setup redirets for each language code too. createRedirect({ fromPath: '/blog/', redirectInBrowser: true, diff --git a/plugins/gatsby-plugin-crowdin/index.js b/plugins/gatsby-plugin-crowdin/index.js new file mode 100644 index 00000000000..b1e9b2b908e --- /dev/null +++ b/plugins/gatsby-plugin-crowdin/index.js @@ -0,0 +1,37 @@ +const visit = require('unist-util-visit'); + +// TODO Copy this helper method into a shared util +const getLanguageCodeFromPath = path => { + const match = path.match(/^([a-z]{2}|[a-z]{2}-[A-Z]+)\//); + + return match ? match[1] : null; +}; + +module.exports = ( + {markdownAST, markdownNode, getNode}, + {defaultLanguageCode, defaultSourceName, translationsSourceName}, +) => { + const parentNode = getNode(markdownNode.parent); + const {relativePath, sourceInstanceName} = parentNode; + + let languageCode = null; + + switch (sourceInstanceName) { + case defaultSourceName: + languageCode = defaultLanguageCode; + break; + case translationsSourceName: + languageCode = getLanguageCodeFromPath(relativePath); + break; + } + + if (languageCode !== null) { + // Prepand language code before links with absolute URLs, + // eg '/path/to/file.html' => '/en/path/to/file.js' + visit(markdownAST, `link`, node => { + if (node.url.startsWith('/')) { + node.url = `/${languageCode}${node.url}`; + } + }); + } +}; diff --git a/plugins/gatsby-plugin-crowdin/package.json b/plugins/gatsby-plugin-crowdin/package.json new file mode 100644 index 00000000000..8d0b600607d --- /dev/null +++ b/plugins/gatsby-plugin-crowdin/package.json @@ -0,0 +1,4 @@ +{ + "name": "gatsby-plugin-crowdin", + "version": "0.0.1" +} \ No newline at end of file From c97a8cb0251560069ff62fb4245550ed74569487 Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Sat, 11 Nov 2017 22:31:24 +0000 Subject: [PATCH 03/37] Iterated more on Crowdin plug-in. --- gatsby-config.js | 16 ++-- gatsby/createPages.js | 76 +++++++------------ gatsby/modifyWebpackConfig.js | 6 +- plugins/gatsby-plugin-crowdin/Link.js | 36 +++++++++ plugins/gatsby-plugin-crowdin/createPage.js | 45 +++++++++++ .../gatsby-plugin-crowdin/createRedirect.js | 21 +++++ plugins/gatsby-plugin-crowdin/index.js | 23 +----- plugins/gatsby-plugin-crowdin/package.json | 6 +- plugins/gatsby-plugin-crowdin/utils.js | 9 +++ src/utils/createLink.js | 24 ++++-- 10 files changed, 177 insertions(+), 85 deletions(-) create mode 100644 plugins/gatsby-plugin-crowdin/Link.js create mode 100644 plugins/gatsby-plugin-crowdin/createPage.js create mode 100644 plugins/gatsby-plugin-crowdin/createRedirect.js create mode 100644 plugins/gatsby-plugin-crowdin/utils.js diff --git a/gatsby-config.js b/gatsby-config.js index ab65ced2f50..e668607c502 100644 --- a/gatsby-config.js +++ b/gatsby-config.js @@ -6,6 +6,8 @@ 'use strict'; +const {readdirSync} = require('fs'); + module.exports = { siteMetadata: { title: 'React: A JavaScript library for building user interfaces', @@ -37,18 +39,11 @@ module.exports = { path: `${__dirname}/src/pages`, }, }, - { - resolve: 'gatsby-source-filesystem', - options: { - name: 'english', - path: `${__dirname}/content/`, - }, - }, { resolve: 'gatsby-source-filesystem', options: { name: 'translations', - path: `${__dirname}/crowdin/translations/docs/`, + path: `${__dirname}/crowdin/translations/`, }, }, { @@ -80,8 +75,9 @@ module.exports = { resolve: 'gatsby-plugin-crowdin', options: { defaultLanguageCode: 'en', - defaultSourceName: 'english', - translationsSourceName: 'translations', + languageCodes: readdirSync( + `${__dirname}/crowdin/translations`, + ).filter(dir => !dir.startsWith('.')), }, }, 'gatsby-remark-use-jsx', diff --git a/gatsby/createPages.js b/gatsby/createPages.js index e8f108b1327..f2b9e19800c 100644 --- a/gatsby/createPages.js +++ b/gatsby/createPages.js @@ -7,24 +7,18 @@ 'use strict'; const {resolve} = require('path'); +const {getLanguageCodeFromPath} = require('../plugins/gatsby-plugin-crowdin/utils'); -const DEFAULT_LANGUAGE_CODE = 'en' +// TODO Read these values from the gatbsy-config rather than duplicating them +const DEFAULT_LANGUAGE_CODE = 'en'; -// TODO Copy this helper method into a shared util -const getLanguageCodeFromPath = path => { - const match = path.match(/^([a-z]{2}|[a-z]{2}-[A-Z]+)\//); +module.exports = async (params) => { + const {graphql, boundActionCreators} = params; + const {createRedirect} = boundActionCreators; // TODO? - return match ? match[1] : null; -} - -// TODO Create Remark parser that adds language prefix to markdown links - -// TODO Decide how we handle Gatsby website links? -// Maybe attach a language code field to GraphQL so we can pass it to templates, -// So it can be evaluated dynamically at build time to generate links that are static at runtime? - -module.exports = async ({graphql, boundActionCreators}) => { - const {createPage, createRedirect} = boundActionCreators; + const pluginConfig = {defaultLanguageCode: DEFAULT_LANGUAGE_CODE}; + const createPage = require('../plugins/gatsby-plugin-crowdin/createPage')(params, pluginConfig); + //const createRedirect = require('../plugins/gatsby-plugin-crowdin/createRedirect')(params, pluginConfig); // Used to detect and prevent duplicate redirects const redirectToSlugMap = {}; @@ -35,15 +29,14 @@ module.exports = async ({graphql, boundActionCreators}) => { const tutorialTemplate = resolve(__dirname, '../src/templates/tutorial.js'); // Redirect /index.html to root. - // TODO Setup redirects for each language code too (eg '/zn-CH/index.html' => '/zn-CH/'). createRedirect({ fromPath: '/index.html', redirectInBrowser: true, toPath: '/', }); - // TODO Maybe redirect naked root / to a page that checks browser language - // And auto-redirects to user's specific //index.html + // TODO Create localized pages/index for each language + // TODO Netlify language redirect for home page const allMarkdown = await graphql( ` @@ -105,32 +98,14 @@ module.exports = async ({graphql, boundActionCreators}) => { } const createArticlePage = path => { - path = path.replace(/^\//, ''); - - const localizedPath = `/${languageCode}/${path}`; - createPage({ - path: localizedPath, + path, component: template, context: { slug, }, }); - - // TODO Create these redirects in a format that allows Netlify to insert language code. - // Daniel mentioned that should be possible in a comment on #82. - // We only need to create redirect once. - - // Create redirect without locale if languageCode is default. - // This allows us to support backwards compatible links (pre-localization) - if (languageCode === DEFAULT_LANGUAGE_CODE) { - createRedirect({ - fromPath: `/${path}`, - redirectInBrowser: true, - toPath: localizedPath, - }); - } - } + }; // Register primary URL. createArticlePage(slug); @@ -142,29 +117,34 @@ module.exports = async ({graphql, boundActionCreators}) => { redirect = [redirect]; } - redirect.forEach(fromPath => { - fromPath = `${languageCode}/${fromPath}`; + if (redirectToSlugMap[languageCode] === undefined) { + redirectToSlugMap[languageCode] = {}; + } - // A leading "/" is required for redirects to work, - // But multiple leading "/" will break redirects. - // For more context see github.com/reactjs/reactjs.org/pull/194 - const toPath = `/${languageCode}/${slug.replace(/^\//, '')}`; + // Extract language code (eg "zn") from language & region code strings (eg "zn-CH"). + // TODO Use helper function in gatsby-plugin-crowdin + const language = languageCode.indexOf('-') + ? languageCode.split('-')[0] + : languageCode; - if (redirectToSlugMap[fromPath] != null) { + redirect.forEach(fromPath => { + if (redirectToSlugMap[languageCode][fromPath] != null) { console.error( `Duplicate redirect detected from "${fromPath}" to:\n` + - `* ${redirectToSlugMap[fromPath]}\n` + + `* ${redirectToSlugMap[languageCode][fromPath]}\n` + `* ${slug}\n`, ); process.exit(1); } - redirectToSlugMap[fromPath] = toPath; + redirectToSlugMap[languageCode][fromPath] = slug; + // Create language-aware redirect createRedirect({ fromPath, + toPath: `/${languageCode}/${slug}`, redirectInBrowser: true, - toPath, + Language: language, }); }); } diff --git a/gatsby/modifyWebpackConfig.js b/gatsby/modifyWebpackConfig.js index d163571e6cf..1d2f978b776 100644 --- a/gatsby/modifyWebpackConfig.js +++ b/gatsby/modifyWebpackConfig.js @@ -6,7 +6,7 @@ 'use strict'; -const {resolve} = require('path'); +const {join, resolve} = require('path'); const webpack = require('webpack'); module.exports = ({config, stage}) => { @@ -17,6 +17,10 @@ module.exports = ({config, stage}) => { resolve: { root: resolve(__dirname, '../src'), extensions: ['', '.js', '.jsx', '.json'], + alias: { + // TODO Remove this alias (and the one below) after plug-in release. + 'gatsby-plugin-crowdin': join(__dirname, '../plugins/gatsby-plugin-crowdin'), + }, }, }); return config; diff --git a/plugins/gatsby-plugin-crowdin/Link.js b/plugins/gatsby-plugin-crowdin/Link.js new file mode 100644 index 00000000000..bfff02e580d --- /dev/null +++ b/plugins/gatsby-plugin-crowdin/Link.js @@ -0,0 +1,36 @@ +/** + * Copyright (c) 2013-present, Facebook, Inc. + * + * @emails react-core + */ + +'use strict'; + +import Link from 'gatsby-link'; +import PropTypes from 'prop-types'; +import React from 'react'; +import {getLanguageCodeFromPath} from './utils'; + +// TODO THis is a hack :( Pass this down via context or some other way? +const DEFAULT_LANGUAGE = 'en'; + +const DecoratedLink = ({location, to, ...rest}, ...other) => { + if (to.startsWith('/')) { + const languageCode = + getLanguageCodeFromPath(location.pathname.substr(1)) || DEFAULT_LANGUAGE; + + to = `/${languageCode}${to}`; + } + + return React.createElement(Link, { + to, + ...rest, + }); +}; + +DecoratedLink.propTypes = { + location: PropTypes.object.isRequired, + to: PropTypes.string.isRequired, +}; + +export default DecoratedLink; diff --git a/plugins/gatsby-plugin-crowdin/createPage.js b/plugins/gatsby-plugin-crowdin/createPage.js new file mode 100644 index 00000000000..70fa45beebc --- /dev/null +++ b/plugins/gatsby-plugin-crowdin/createPage.js @@ -0,0 +1,45 @@ +'use strict'; + +const {getLanguageCodeFromPath} = require('./utils'); + +module.exports = ( + {boundActionCreators}, + {defaultLanguageCode, languageCodes}, +) => { + const {createPage, createRedirect} = boundActionCreators; + + return ({path, ...rest}) => { + // Gatsby paths always start with a "/". + // Our code below is easier to read if we strip it though. + path = path.substr(1); + + const languageCode = getLanguageCodeFromPath(path); + const localizedPath = `/${languageCode}/${path}`; + + createPage({ + path: localizedPath, + ...rest, + }); + + // Re-route non-language-specified URLs based on language code. + // (See github.com/gatsbyjs/gatsby/pull/2890) + // This provides backwards compatibility for links that existed before i18n, + // And ensures that users will get served the most appropriate content by default. + // We only need to do this once per "page" so just do it when we detect the default language. + if (languageCode === defaultLanguageCode) { + for (let languageCode in languageCodes) { + // Extract language code (eg "zn") from language & region code strings (eg "zn-CH"). + const language = languageCode.indexOf('-') + ? languageCode.split('-')[0] + : languageCode; + + createRedirect({ + fromPath: `/${path}`, + toPath: `/${languageCode}/${path}`, + redirectInBrowser: true, + Language: language, + }); + } + } + }; +}; diff --git a/plugins/gatsby-plugin-crowdin/createRedirect.js b/plugins/gatsby-plugin-crowdin/createRedirect.js new file mode 100644 index 00000000000..300ca56faf4 --- /dev/null +++ b/plugins/gatsby-plugin-crowdin/createRedirect.js @@ -0,0 +1,21 @@ +'use strict'; + +const {getLanguageCodeFromPath} = require('./utils'); + +module.exports = ({boundActionCreators}, {defaultLanguageCode}) => { + const {createRedirect} = boundActionCreators; + + return ({fromPath, toPath, ...rest}) => { + toPath = toPath.replace(/^\//, ''); + + const languageCode = getLanguageCodeFromPath(toPath); + + fromPath = `/${languageCode}/${fromPath}`; + + createRedirect({ + fromPath, + toPath, + ...rest, + }); + }; +}; diff --git a/plugins/gatsby-plugin-crowdin/index.js b/plugins/gatsby-plugin-crowdin/index.js index b1e9b2b908e..441ca6ae63f 100644 --- a/plugins/gatsby-plugin-crowdin/index.js +++ b/plugins/gatsby-plugin-crowdin/index.js @@ -1,29 +1,14 @@ const visit = require('unist-util-visit'); - -// TODO Copy this helper method into a shared util -const getLanguageCodeFromPath = path => { - const match = path.match(/^([a-z]{2}|[a-z]{2}-[A-Z]+)\//); - - return match ? match[1] : null; -}; +const {getLanguageCodeFromPath} = require('./utils'); module.exports = ( {markdownAST, markdownNode, getNode}, - {defaultLanguageCode, defaultSourceName, translationsSourceName}, + {defaultLanguageCode}, ) => { const parentNode = getNode(markdownNode.parent); - const {relativePath, sourceInstanceName} = parentNode; + const {relativePath} = parentNode; - let languageCode = null; - - switch (sourceInstanceName) { - case defaultSourceName: - languageCode = defaultLanguageCode; - break; - case translationsSourceName: - languageCode = getLanguageCodeFromPath(relativePath); - break; - } + let languageCode = getLanguageCodeFromPath(relativePath); if (languageCode !== null) { // Prepand language code before links with absolute URLs, diff --git a/plugins/gatsby-plugin-crowdin/package.json b/plugins/gatsby-plugin-crowdin/package.json index 8d0b600607d..83fe1676a4b 100644 --- a/plugins/gatsby-plugin-crowdin/package.json +++ b/plugins/gatsby-plugin-crowdin/package.json @@ -1,4 +1,8 @@ { "name": "gatsby-plugin-crowdin", - "version": "0.0.1" + "version": "0.0.1", + "dependencies": { + "gatsby-link": "^1.6.9", + "prop-types": "^15.6.0" + } } \ No newline at end of file diff --git a/plugins/gatsby-plugin-crowdin/utils.js b/plugins/gatsby-plugin-crowdin/utils.js new file mode 100644 index 00000000000..18cfea0caf9 --- /dev/null +++ b/plugins/gatsby-plugin-crowdin/utils.js @@ -0,0 +1,9 @@ +'use strict'; + +// Parses language code (eg en, zh-CH) from a path (eg en/path/to/file.js) +// Returns null if path doesn't contain a language code. +exports.getLanguageCodeFromPath = path => { + const match = path.match(/^([a-z]{2}|[a-z]{2}-[A-Z]+)\//); + + return match ? match[1] : null; +}; diff --git a/src/utils/createLink.js b/src/utils/createLink.js index 98ee4039212..04aad642280 100644 --- a/src/utils/createLink.js +++ b/src/utils/createLink.js @@ -6,22 +6,25 @@ 'use strict'; -import Link from 'gatsby-link'; +import Link from 'gatsby-plugin-crowdin/Link'; import React from 'react'; import ExternalLinkSvg from 'templates/components/ExternalLinkSvg'; import slugify from 'utils/slugify'; import {colors, media} from 'theme'; -const createLinkBlog = ({isActive, item, section}) => { +const createLinkBlog = ({isActive, item, location, section}) => { return ( - + {isActive && } {item.title} ); }; -const createLinkCommunity = ({isActive, item, section}) => { +const createLinkCommunity = ({isActive, item, location, section}) => { if (item.href) { return ( @@ -40,14 +43,16 @@ const createLinkCommunity = ({isActive, item, section}) => { return createLinkDocs({ isActive, item, + location, section, }); }; -const createLinkDocs = ({isActive, item, section}) => { +const createLinkDocs = ({isActive, item, location, section}) => { return ( {isActive && } {item.title} @@ -55,10 +60,17 @@ const createLinkDocs = ({isActive, item, section}) => { ); }; -const createLinkTutorial = ({isActive, item, onLinkClick, section}) => { +const createLinkTutorial = ({ + isActive, + item, + location, + onLinkClick, + section, +}) => { return ( {isActive && } From ed5e32660f39a850f792baf8bef77468ab8a615b Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Sun, 12 Nov 2017 19:42:54 +0000 Subject: [PATCH 04/37] Iterating on Gatsby+Crowdin plug-in a bit more --- .eslintignore | 1 + crowdin/config.js | 4 +- gatsby-config.js | 11 ++- gatsby/createPages.js | 56 +++++------- gatsby/onCreateNode.js | 15 ++++ plugins/gatsby-plugin-crowdin/createPage.js | 45 ---------- .../gatsby-plugin-crowdin/createRedirect.js | 21 ----- plugins/gatsby-plugin-crowdin/gatsby-node.js | 4 + plugins/gatsby-plugin-crowdin/index.js | 16 ++-- plugins/gatsby-plugin-crowdin/onCreateNode.js | 44 +++++++++ plugins/gatsby-plugin-crowdin/onCreatePage.js | 89 +++++++++++++++++++ plugins/gatsby-plugin-crowdin/utils.js | 7 ++ src/templates/blog.js | 10 +-- src/templates/community.js | 6 +- src/templates/docs.js | 6 +- src/templates/tutorial.js | 6 +- 16 files changed, 215 insertions(+), 126 deletions(-) delete mode 100644 plugins/gatsby-plugin-crowdin/createPage.js delete mode 100644 plugins/gatsby-plugin-crowdin/createRedirect.js create mode 100644 plugins/gatsby-plugin-crowdin/gatsby-node.js create mode 100644 plugins/gatsby-plugin-crowdin/onCreateNode.js create mode 100644 plugins/gatsby-plugin-crowdin/onCreatePage.js diff --git a/.eslintignore b/.eslintignore index 9425417154d..fde4479aa63 100644 --- a/.eslintignore +++ b/.eslintignore @@ -2,6 +2,7 @@ node_modules/* # Ignore markdown files and examples content/* +crowdin/translations/* # Ignore built files public/* diff --git a/crowdin/config.js b/crowdin/config.js index a7fb0743ca1..16a155ffcbc 100644 --- a/crowdin/config.js +++ b/crowdin/config.js @@ -1,4 +1,4 @@ module.exports = { key: process.env.CROWDIN_API_KEY, - url: 'https://api.crowdin.com/api/project/react' -} + url: 'https://api.crowdin.com/api/project/react', +}; \ No newline at end of file diff --git a/gatsby-config.js b/gatsby-config.js index e668607c502..0d6909155a1 100644 --- a/gatsby-config.js +++ b/gatsby-config.js @@ -6,8 +6,6 @@ 'use strict'; -const {readdirSync} = require('fs'); - module.exports = { siteMetadata: { title: 'React: A JavaScript library for building user interfaces', @@ -25,6 +23,11 @@ module.exports = { 'gatsby-plugin-netlify', 'gatsby-plugin-glamor', 'gatsby-plugin-react-next', + { + resolve: 'gatsby-plugin-crowdin', + options: { + }, + }, 'gatsby-plugin-twitter', { resolve: 'gatsby-plugin-nprogress', @@ -74,10 +77,6 @@ module.exports = { { resolve: 'gatsby-plugin-crowdin', options: { - defaultLanguageCode: 'en', - languageCodes: readdirSync( - `${__dirname}/crowdin/translations`, - ).filter(dir => !dir.startsWith('.')), }, }, 'gatsby-remark-use-jsx', diff --git a/gatsby/createPages.js b/gatsby/createPages.js index f2b9e19800c..6b50254214f 100644 --- a/gatsby/createPages.js +++ b/gatsby/createPages.js @@ -7,18 +7,10 @@ 'use strict'; const {resolve} = require('path'); -const {getLanguageCodeFromPath} = require('../plugins/gatsby-plugin-crowdin/utils'); - -// TODO Read these values from the gatbsy-config rather than duplicating them -const DEFAULT_LANGUAGE_CODE = 'en'; module.exports = async (params) => { const {graphql, boundActionCreators} = params; - const {createRedirect} = boundActionCreators; // TODO? - - const pluginConfig = {defaultLanguageCode: DEFAULT_LANGUAGE_CODE}; - const createPage = require('../plugins/gatsby-plugin-crowdin/createPage')(params, pluginConfig); - //const createRedirect = require('../plugins/gatsby-plugin-crowdin/createRedirect')(params, pluginConfig); + const {createPage, createRedirect} = boundActionCreators; // Used to detect and prevent duplicate redirects const redirectToSlugMap = {}; @@ -35,8 +27,7 @@ module.exports = async (params) => { toPath: '/', }); - // TODO Create localized pages/index for each language - // TODO Netlify language redirect for home page + // TODO Create localized root redirects for each language const allMarkdown = await graphql( ` @@ -45,6 +36,9 @@ module.exports = async (params) => { edges { node { fields { + id + language + languageCode path redirect slug @@ -64,11 +58,7 @@ module.exports = async (params) => { allMarkdown.data.allMarkdownRemark.edges.forEach(edge => { const {fields} = edge.node; - const {path, slug} = fields; - - // Parse language code from path for Crowdin content. - // Fall back to English as the default. - const languageCode = getLanguageCodeFromPath(path) || DEFAULT_LANGUAGE_CODE; + let {id, language, languageCode, slug} = fields; if (slug === 'docs/error-decoder.html') { // No-op so far as markdown templates go. @@ -98,11 +88,17 @@ module.exports = async (params) => { } const createArticlePage = path => { + if (path.startsWith('/')) { + path = `/${languageCode}/${path.substr(1)}`; + } + createPage({ path, component: template, context: { - slug, + id, + language, + languageCode, }, }); }; @@ -117,32 +113,28 @@ module.exports = async (params) => { redirect = [redirect]; } - if (redirectToSlugMap[languageCode] === undefined) { - redirectToSlugMap[languageCode] = {}; - } + redirect.forEach(fromPath => { + if (fromPath.startsWith('/')) { + fromPath = fromPath.substr(1); + } - // Extract language code (eg "zn") from language & region code strings (eg "zn-CH"). - // TODO Use helper function in gatsby-plugin-crowdin - const language = languageCode.indexOf('-') - ? languageCode.split('-')[0] - : languageCode; + const localizedFromPath = `/${languageCode}/${fromPath}`; - redirect.forEach(fromPath => { - if (redirectToSlugMap[languageCode][fromPath] != null) { + if (redirectToSlugMap[localizedFromPath] != null) { console.error( `Duplicate redirect detected from "${fromPath}" to:\n` + - `* ${redirectToSlugMap[languageCode][fromPath]}\n` + + `* ${redirectToSlugMap[localizedFromPath]}\n` + `* ${slug}\n`, ); process.exit(1); } - redirectToSlugMap[languageCode][fromPath] = slug; + redirectToSlugMap[localizedFromPath] = slug; // Create language-aware redirect createRedirect({ - fromPath, - toPath: `/${languageCode}/${slug}`, + fromPath: localizedFromPath, + toPath: slug, redirectInBrowser: true, Language: language, }); @@ -173,7 +165,7 @@ module.exports = async (params) => { const newestBlogNode = newestBlogEntry.data.allMarkdownRemark.edges[0].node; // Blog landing page should always show the most recent blog entry. - // TODO Setup redirets for each language code too. + // Note that blog content is not localized. createRedirect({ fromPath: '/blog/', redirectInBrowser: true, diff --git a/gatsby/onCreateNode.js b/gatsby/onCreateNode.js index c9d71859924..689a04d7185 100644 --- a/gatsby/onCreateNode.js +++ b/gatsby/onCreateNode.js @@ -9,6 +9,8 @@ // Parse date information out of blog post filename. const BLOG_POST_FILENAME_REGEX = /([0-9]+)\-([0-9]+)\-([0-9]+)\-(.+)\.md$/; +let id = 0; + // Add custom fields to MarkdownRemark nodes. module.exports = exports.onCreateNode = ({node, boundActionCreators, getNode}) => { const {createNodeField} = boundActionCreators; @@ -54,6 +56,19 @@ module.exports = exports.onCreateNode = ({node, boundActionCreators, getNode}) = ); } + // Slugs are easier to process elsewhere if we ensure they always start with "/" + if (!slug.startsWith('/')) { + slug = `/${slug}`; + } + + // Unique ID for template GraphQL queries; + // This avoids potential duplicate slugs between translated content. + createNodeField({ + node, + name: 'id', + value: (++id).toString(), + }); + // Used to generate URL to view this content. createNodeField({ node, diff --git a/plugins/gatsby-plugin-crowdin/createPage.js b/plugins/gatsby-plugin-crowdin/createPage.js deleted file mode 100644 index 70fa45beebc..00000000000 --- a/plugins/gatsby-plugin-crowdin/createPage.js +++ /dev/null @@ -1,45 +0,0 @@ -'use strict'; - -const {getLanguageCodeFromPath} = require('./utils'); - -module.exports = ( - {boundActionCreators}, - {defaultLanguageCode, languageCodes}, -) => { - const {createPage, createRedirect} = boundActionCreators; - - return ({path, ...rest}) => { - // Gatsby paths always start with a "/". - // Our code below is easier to read if we strip it though. - path = path.substr(1); - - const languageCode = getLanguageCodeFromPath(path); - const localizedPath = `/${languageCode}/${path}`; - - createPage({ - path: localizedPath, - ...rest, - }); - - // Re-route non-language-specified URLs based on language code. - // (See github.com/gatsbyjs/gatsby/pull/2890) - // This provides backwards compatibility for links that existed before i18n, - // And ensures that users will get served the most appropriate content by default. - // We only need to do this once per "page" so just do it when we detect the default language. - if (languageCode === defaultLanguageCode) { - for (let languageCode in languageCodes) { - // Extract language code (eg "zn") from language & region code strings (eg "zn-CH"). - const language = languageCode.indexOf('-') - ? languageCode.split('-')[0] - : languageCode; - - createRedirect({ - fromPath: `/${path}`, - toPath: `/${languageCode}/${path}`, - redirectInBrowser: true, - Language: language, - }); - } - } - }; -}; diff --git a/plugins/gatsby-plugin-crowdin/createRedirect.js b/plugins/gatsby-plugin-crowdin/createRedirect.js deleted file mode 100644 index 300ca56faf4..00000000000 --- a/plugins/gatsby-plugin-crowdin/createRedirect.js +++ /dev/null @@ -1,21 +0,0 @@ -'use strict'; - -const {getLanguageCodeFromPath} = require('./utils'); - -module.exports = ({boundActionCreators}, {defaultLanguageCode}) => { - const {createRedirect} = boundActionCreators; - - return ({fromPath, toPath, ...rest}) => { - toPath = toPath.replace(/^\//, ''); - - const languageCode = getLanguageCodeFromPath(toPath); - - fromPath = `/${languageCode}/${fromPath}`; - - createRedirect({ - fromPath, - toPath, - ...rest, - }); - }; -}; diff --git a/plugins/gatsby-plugin-crowdin/gatsby-node.js b/plugins/gatsby-plugin-crowdin/gatsby-node.js new file mode 100644 index 00000000000..886c352e487 --- /dev/null +++ b/plugins/gatsby-plugin-crowdin/gatsby-node.js @@ -0,0 +1,4 @@ +'use strict'; + +exports.onCreateNode = require('./onCreateNode'); +exports.onCreatePage = require('./onCreatePage'); diff --git a/plugins/gatsby-plugin-crowdin/index.js b/plugins/gatsby-plugin-crowdin/index.js index 441ca6ae63f..25a549999e1 100644 --- a/plugins/gatsby-plugin-crowdin/index.js +++ b/plugins/gatsby-plugin-crowdin/index.js @@ -1,19 +1,23 @@ const visit = require('unist-util-visit'); const {getLanguageCodeFromPath} = require('./utils'); -module.exports = ( - {markdownAST, markdownNode, getNode}, - {defaultLanguageCode}, -) => { +// This file "localizes" static markdown links during build-time +// eg /path/to/file.html => /zh-CN/path/to/file.html +// This is so Gatbsy will prefetch the correct language content +module.exports = ({markdownAST, markdownNode, getNode}, pluginOptions) => { const parentNode = getNode(markdownNode.parent); const {relativePath} = parentNode; let languageCode = getLanguageCodeFromPath(relativePath); + // Only convert links for pages that contain language codes. + // TODO Does this upport linking from a Markdown page to a JavaScript page? + // Or will it incorrectly try to localize the static page? if (languageCode !== null) { - // Prepand language code before links with absolute URLs, - // eg '/path/to/file.html' => '/en/path/to/file.js' visit(markdownAST, `link`, node => { + // Only prepand language code before root URLs (eg /path/to/file.html) + // Ignore relative links (eg file.html) + // And links to other domains (eg www.google.com) if (node.url.startsWith('/')) { node.url = `/${languageCode}${node.url}`; } diff --git a/plugins/gatsby-plugin-crowdin/onCreateNode.js b/plugins/gatsby-plugin-crowdin/onCreateNode.js new file mode 100644 index 00000000000..861fbbf26e2 --- /dev/null +++ b/plugins/gatsby-plugin-crowdin/onCreateNode.js @@ -0,0 +1,44 @@ +/** + * Copyright (c) 2013-present, Facebook, Inc. + * + * @emails react-core + */ + +'use strict'; + +const { + getLanguageCodeFromPath, + getLanguageFromLanguageAndRegion, +} = require('./utils'); + +module.exports = exports.onCreateNode = ({ + node, + boundActionCreators, + getNode, +}) => { + const {createNodeField} = boundActionCreators; + + switch (node.internal.type) { + case 'MarkdownRemark': + const {relativePath} = getNode(node.parent); + + const languageCode = getLanguageCodeFromPath(relativePath); + + if (languageCode !== null) { + const language = getLanguageFromLanguageAndRegion(languageCode); + + createNodeField({ + node, + name: 'language', + value: language, + }); + + createNodeField({ + node, + name: 'languageCode', + value: languageCode, + }); + } + return; + } +}; diff --git a/plugins/gatsby-plugin-crowdin/onCreatePage.js b/plugins/gatsby-plugin-crowdin/onCreatePage.js new file mode 100644 index 00000000000..bf0affa4170 --- /dev/null +++ b/plugins/gatsby-plugin-crowdin/onCreatePage.js @@ -0,0 +1,89 @@ +'use strict'; + +/** Params +{ page: + { layout: 'index', + jsonName: 'dev-404-page.json', + internalComponentName: 'ComponentDev404Page', + path: '/dev-404-page/', + matchPath: undefined, + component: '/Users/bvaughn/Documents/git/reactjs.org/.cache/dev-404-page.js', + componentChunkName: 'component---cache-dev-404-page-js', + context: {}, + updatedAt: 1510471947417, + pluginCreator___NODE: 'Plugin dev-404-page', + pluginCreatorId: 'Plugin dev-404-page', + componentPath: '/Users/bvaughn/Documents/git/reactjs.org/.cache/dev-404-page.js' }, + traceId: 'initial-createPages', + pathPrefix: '', + boundActionCreators: + { deletePage: [Function], + createPage: [Function], + deleteLayout: [Function], + createLayout: [Function], + deleteNode: [Function], + deleteNodes: [Function], + createNode: [Function], + touchNode: [Function], + createNodeField: [Function], + createParentChildLink: [Function], + createPageDependency: [Function], + deleteComponentsDependencies: [Function], + replaceComponentQuery: [Function], + createJob: [Function], + setJob: [Function], + endJob: [Function], + setPluginStatus: [Function], + createRedirect: [Function] }, + loadNodeContent: [Function], + store: + { dispatch: [Function: dispatch], + subscribe: [Function: subscribe], + getState: [Function: getState], + replaceReducer: [Function: replaceReducer], + [Symbol(observable)]: [Function: observable] }, + getNodes: [Function], + getNode: [Function: getNode], + hasNodeChanged: [Function], + reporter: undefined, + getNodeAndSavePathDependency: [Function], + cache: + { initCache: [Function], + get: [Function], + set: [Function] } } +{ plugins: [], + ...pluginOptions } +*/ + +// Gatsby has 2 types of pages: +// (1) Pages generated from Markdown content (src/templates) +// (2) Pages defined in JavaScript (src/pages) +// This plug-in makes no attempt to localize pages, only markdown content. +// One way to differnetiate between the 2 is to check for 'context.languageCode' +// This field gets inserted during markdown template creation. +module.exports = ({page, boundActionCreators}, pluginOptions) => { + const {createRedirect} = boundActionCreators; + + const { + context: { + language, // eg zn + languageCode, // eg zn-CH + }, + path, // eg /zn-CH/path/to/template.html, /path/to/page.html + } = page; + + if (!languageCode) { + return; // No-op for JavaScript pages (eg src/pages) + } + + const nonLocalizedPath = path.substr( + path.indexOf(languageCode) + languageCode.length, + ); + + createRedirect({ + fromPath: nonLocalizedPath, + toPath: path, + redirectInBrowser: true, + Language: language, + }); +}; diff --git a/plugins/gatsby-plugin-crowdin/utils.js b/plugins/gatsby-plugin-crowdin/utils.js index 18cfea0caf9..dc855bb481c 100644 --- a/plugins/gatsby-plugin-crowdin/utils.js +++ b/plugins/gatsby-plugin-crowdin/utils.js @@ -7,3 +7,10 @@ exports.getLanguageCodeFromPath = path => { return match ? match[1] : null; }; + +// Parses a language (eg en, zn) from a langauge and region string (eg en-GB, zh-CH). +// If the specified param doesn't contain a region (eg en) then it is returned as-is. +exports.getLanguageFromLanguageAndRegion = languageAndRegion => + languageAndRegion.indexOf('-') + ? languageAndRegion.split('-')[0] + : languageAndRegion; diff --git a/src/templates/blog.js b/src/templates/blog.js index e520f71828a..45229066ce0 100644 --- a/src/templates/blog.js +++ b/src/templates/blog.js @@ -16,7 +16,7 @@ const toSectionList = allMarkdownRemark => [ title: 'Recent Posts', items: allMarkdownRemark.edges .map(({node}) => ({ - id: node.fields.slug, + id: node.fields.id, title: node.frontmatter.title, })) .concat({ @@ -45,8 +45,8 @@ Blog.propTypes = { // eslint-disable-next-line no-undef export const pageQuery = graphql` - query TemplateBlogMarkdown($slug: String!) { - markdownRemark(fields: {slug: {eq: $slug}}) { + query TemplateBlogMarkdown($id: String!) { + markdownRemark(fields: {id: {eq: $id}}) { html excerpt(pruneLength: 500) frontmatter { @@ -63,7 +63,7 @@ export const pageQuery = graphql` fields { date(formatString: "MMMM DD, YYYY") path - slug + id } } allMarkdownRemark( @@ -77,7 +77,7 @@ export const pageQuery = graphql` title } fields { - slug + id } } } diff --git a/src/templates/community.js b/src/templates/community.js index 72f7a222d73..3030416c9f1 100644 --- a/src/templates/community.js +++ b/src/templates/community.js @@ -28,8 +28,8 @@ Community.propTypes = { // eslint-disable-next-line no-undef export const pageQuery = graphql` - query TemplateCommunityMarkdown($slug: String!) { - markdownRemark(fields: {slug: {eq: $slug}}) { + query TemplateCommunityMarkdown($id: String!) { + markdownRemark(fields: {id: {eq: $id}}) { html frontmatter { title @@ -38,7 +38,7 @@ export const pageQuery = graphql` } fields { path - slug + id } } } diff --git a/src/templates/docs.js b/src/templates/docs.js index b087d6d0add..1e3728173f9 100644 --- a/src/templates/docs.js +++ b/src/templates/docs.js @@ -28,8 +28,8 @@ Docs.propTypes = { // eslint-disable-next-line no-undef export const pageQuery = graphql` - query TemplateDocsMarkdown($slug: String!) { - markdownRemark(fields: {slug: {eq: $slug}}) { + query TemplateDocsMarkdown($id: String!) { + markdownRemark(fields: {id: {eq: $id}}) { html frontmatter { title @@ -38,7 +38,7 @@ export const pageQuery = graphql` } fields { path - slug + id } } } diff --git a/src/templates/tutorial.js b/src/templates/tutorial.js index 33efed979c4..11d26317925 100644 --- a/src/templates/tutorial.js +++ b/src/templates/tutorial.js @@ -40,8 +40,8 @@ Tutorial.propTypes = { // eslint-disable-next-line no-undef export const pageQuery = graphql` - query TemplateTutorialMarkdown($slug: String!) { - markdownRemark(fields: {slug: {eq: $slug}}) { + query TemplateTutorialMarkdown($id: String!) { + markdownRemark(fields: {id: {eq: $id}}) { html frontmatter { title @@ -50,7 +50,7 @@ export const pageQuery = graphql` } fields { path - slug + id } } } From d426e71e9afd1534aa441cf74b4f6d9d58f65390 Mon Sep 17 00:00:00 2001 From: Clement Hoang Date: Fri, 10 Nov 2017 16:04:07 +0000 Subject: [PATCH 05/37] Update script to filter locales with certain progress thresholds and symlink them --- .gitignore | 1 - crowdin/.gitignore | 1 + crowdin/config.js | 3 ++- crowdin/download.js | 21 +++++++++++++++++++-- crowdin/translations/.gitignore | 1 + crowdin/translations/en-US | 1 + package.json | 4 ++-- yarn.lock | 4 ++-- 8 files changed, 28 insertions(+), 8 deletions(-) create mode 100644 crowdin/.gitignore create mode 100644 crowdin/translations/.gitignore create mode 120000 crowdin/translations/en-US diff --git a/.gitignore b/.gitignore index 0741ceee83a..dbe72d17694 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,3 @@ .idea node_modules public -crowdin/translations diff --git a/crowdin/.gitignore b/crowdin/.gitignore new file mode 100644 index 00000000000..eac63ba561b --- /dev/null +++ b/crowdin/.gitignore @@ -0,0 +1 @@ +__translations/ diff --git a/crowdin/config.js b/crowdin/config.js index 16a155ffcbc..a184e157be1 100644 --- a/crowdin/config.js +++ b/crowdin/config.js @@ -1,4 +1,5 @@ module.exports = { key: process.env.CROWDIN_API_KEY, url: 'https://api.crowdin.com/api/project/react', -}; \ No newline at end of file + translation_threshold: 30, +}; diff --git a/crowdin/download.js b/crowdin/download.js index 8535d45ac6b..5a03b7afe16 100644 --- a/crowdin/download.js +++ b/crowdin/download.js @@ -1,6 +1,23 @@ -var Crowdin = require('crowdin'); +var Crowdin = require('crowdin-node'); var config = require('./config'); +var path = require('path'); var crowdin = new Crowdin({ apiKey: config.key, endpointUrl: config.url }); +process.chdir(path.resolve(__dirname, 'translations')); -crowdin.downloadToPath('./translations'); +crowdin.downloadToPath(path.resolve(__dirname, '__translations')); + +crowdin.getTranslationStatus().then(locales => { + const usableLocales = locales.filter(locale => locale.translated_progress > config.translation_threshold); + + usableLocales.forEach(locale => { + createSymLink(locale.code); + }); +}); + +function createSymLink(localeName) { + fs.symlink('../../content/' + localeName, localeName, (err) => { + console.error(err); + process.exit(1); + }); +} diff --git a/crowdin/translations/.gitignore b/crowdin/translations/.gitignore new file mode 100644 index 00000000000..dceefb7eaef --- /dev/null +++ b/crowdin/translations/.gitignore @@ -0,0 +1 @@ +!en-US/ diff --git a/crowdin/translations/en-US b/crowdin/translations/en-US new file mode 120000 index 00000000000..b181ab91b05 --- /dev/null +++ b/crowdin/translations/en-US @@ -0,0 +1 @@ +../../content \ No newline at end of file diff --git a/package.json b/package.json index f6c7fd0e92b..1b7acb56686 100644 --- a/package.json +++ b/package.json @@ -13,9 +13,9 @@ "eslint-plugin-relay/graphql": "0.10.5" }, "dependencies": { - "react-live": "1.8.0-0", "array-from": "^2.1.1", "babel-eslint": "^8.0.1", + "crowdin-node": "^1.0.0", "eslint": "^4.8.0", "eslint-config-fbjs": "^2.0.0", "eslint-config-react": "^1.1.7", @@ -52,6 +52,7 @@ "glamor": "^2.20.40", "hex2rgba": "^0.0.1", "prettier": "^1.7.4", + "react-live": "1.8.0-0", "remarkable": "^1.7.1", "request-promise": "^4.2.2", "rimraf": "^2.6.1", @@ -85,7 +86,6 @@ "reset": "rimraf ./.cache" }, "devDependencies": { - "crowdin": "^1.0.0", "eslint-config-prettier": "^2.6.0", "lz-string": "^1.4.4", "recursive-readdir": "^2.2.1", diff --git a/yarn.lock b/yarn.lock index b0a6b74ad76..ee163e616e1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2366,9 +2366,9 @@ cross-spawn@5.1.0, cross-spawn@^5.0.1, cross-spawn@^5.1.0: shebang-command "^1.2.0" which "^1.2.9" -crowdin@^1.0.0: +crowdin-node@^1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/crowdin/-/crowdin-1.0.0.tgz#7e6331c90cf65c460db1c38483edfef211a28015" + resolved "https://registry.yarnpkg.com/crowdin-node/-/crowdin-node-1.0.0.tgz#17c734026607e24e6eb26eee36df4d7527b4a21c" dependencies: bluebird "^2.9.21" graceful-fs "^3.0.6" From 0ac23878c62aeb1cb3c4539ac4d61dccda6452af Mon Sep 17 00:00:00 2001 From: Clement Hoang Date: Sat, 11 Nov 2017 23:47:50 +0000 Subject: [PATCH 06/37] Update download script to symlink downloaded translations --- crowdin/download.js | 107 +++++++++++++++++++++++++++++++++++++------- 1 file changed, 92 insertions(+), 15 deletions(-) diff --git a/crowdin/download.js b/crowdin/download.js index 5a03b7afe16..c0a5fb07d03 100644 --- a/crowdin/download.js +++ b/crowdin/download.js @@ -1,23 +1,100 @@ -var Crowdin = require('crowdin-node'); -var config = require('./config'); -var path = require('path'); +const Crowdin = require('crowdin-node'); +const config = require('./config'); +const path = require('path'); +const {symlink, lstatSync, readdirSync} = require('fs'); -var crowdin = new Crowdin({ apiKey: config.key, endpointUrl: config.url }); -process.chdir(path.resolve(__dirname, 'translations')); +const SYMLINKED_TRANSLATIONS_PATH = path.resolve(__dirname, 'translations'); +const DOWNLOADED_TRANSLATIONS_PATH = path.resolve(__dirname, '__translations'); +const DOWNLOADED_TRANSLATIONS_DOCS_PATH = path.resolve( + __dirname, + '__translations', + 'docs', +); -crowdin.downloadToPath(path.resolve(__dirname, '__translations')); +function main() { + const crowdin = new Crowdin({apiKey: config.key, endpointUrl: config.url}); + process.chdir(SYMLINKED_TRANSLATIONS_PATH); -crowdin.getTranslationStatus().then(locales => { - const usableLocales = locales.filter(locale => locale.translated_progress > config.translation_threshold); + crowdin + .downloadToPath(DOWNLOADED_TRANSLATIONS_PATH) + .then(() => { + return crowdin.getTranslationStatus(); + }) + .then(locales => { + const usableLocales = locales + .filter( + locale => locale.translated_progress > config.translation_threshold, + ) + .map(local => local.code); - usableLocales.forEach(locale => { - createSymLink(locale.code); + const localeDirectories = getDirectories( + DOWNLOADED_TRANSLATIONS_DOCS_PATH, + ); + + const localeToFolderMap = createLocaleToFolderMap(localeDirectories); + + usableLocales.forEach(locale => { + createSymLink(localeToFolderMap.get(locale)); + }); + }); +} + +// Creates a relative symlink from a downloaded translation in the current working directory +// Note that the current working directory of this node process should be where the symlink is created +// or else the relative paths would be incorrect +function createSymLink(folder) { + symlink(`../__translations/docs/${folder}`, folder, err => { + if (!err) { + console.log(`Created symlink for ${folder}.`); + return; + } + + if (err.code === 'EEXIST') { + console.log( + `Skipped creating symlink for ${folder}. A symlink already exists.`, + ); + } else { + console.error(err); + process.exit(1); + } }); -}); +} -function createSymLink(localeName) { - fs.symlink('../../content/' + localeName, localeName, (err) => { - console.error(err); - process.exit(1); +// When we run getTranslationStatus(), it gives us 2-ALPHA locale codes unless necessary +// However, the folder structure of downloaded translations always has 4-ALPHA locale codes +// This function creates a map from a locale code to its corresponding folder name +function createLocaleToFolderMap(directories) { + const twoAlphaLocale = locale => locale.substring(0, 2); + const localeToFolders = new Map(); + const localeToFolder = new Map(); + + for (let locale of directories) { + localeToFolders.set( + twoAlphaLocale(locale), + localeToFolders.has(twoAlphaLocale(locale)) + ? localeToFolders.get(twoAlphaLocale(locale)).concat(locale) + : [locale], + ); + } + + localeToFolders.forEach((folders, locale) => { + if (folders.length === 1) { + localeToFolder.set(locale, folders[0]); + } else { + for (let folder of folders) { + localeToFolder.set(folder, folder); + } + } }); + + return localeToFolder; } + +function getDirectories(source) { + return readdirSync(source).filter( + name => + lstatSync(path.join(source, name)).isDirectory() && name !== '_data', + ); +} + +main(); From 3801c2b9245a03ff181cc79782fcec2cf4149ca8 Mon Sep 17 00:00:00 2001 From: Clement Hoang Date: Sat, 11 Nov 2017 23:58:51 +0000 Subject: [PATCH 07/37] Update cleanup script --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 1b7acb56686..e8c83c2b51a 100644 --- a/package.json +++ b/package.json @@ -83,7 +83,7 @@ "netlify": "yarn install && yarn build", "prettier": "prettier --config .prettierrc --write \"{gatsby-*.js,{flow-typed,plugins,src}/**/*.js}\"", "prettier:diff": "prettier --config .prettierrc --list-different \"{gatsby-*.js,{flow-typed,plugins,src}/**/*.js}\"", - "reset": "rimraf ./.cache" + "reset": "rimraf ./.cache && rimraf ./crowdin/__translations && find crowdin/translations -type l -not -name '*en-US' -delete" }, "devDependencies": { "eslint-config-prettier": "^2.6.0", From 5efa663a69f7acb55ecb898fd088e713007f4eb8 Mon Sep 17 00:00:00 2001 From: Clement Hoang Date: Sun, 12 Nov 2017 00:22:32 +0000 Subject: [PATCH 08/37] Refactor crowdin download script --- crowdin/download.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/crowdin/download.js b/crowdin/download.js index c0a5fb07d03..a569d1ff47b 100644 --- a/crowdin/download.js +++ b/crowdin/download.js @@ -16,10 +16,9 @@ function main() { process.chdir(SYMLINKED_TRANSLATIONS_PATH); crowdin - .downloadToPath(DOWNLOADED_TRANSLATIONS_PATH) - .then(() => { - return crowdin.getTranslationStatus(); - }) + // .export() // Not sure if this should be called in the script since it could be very slow + .then(() => crowdin.downloadToPath(DOWNLOADED_TRANSLATIONS_PATH)) + .then(() => crowdin.getTranslationStatus()) .then(locales => { const usableLocales = locales .filter( From aee9bdc6ead0347377e7330bd533ce66e4ffbd01 Mon Sep 17 00:00:00 2001 From: Clement Hoang Date: Mon, 13 Nov 2017 09:54:12 -0800 Subject: [PATCH 09/37] Make script runnable again --- crowdin/download.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crowdin/download.js b/crowdin/download.js index a569d1ff47b..4b4168ba8d1 100644 --- a/crowdin/download.js +++ b/crowdin/download.js @@ -17,7 +17,8 @@ function main() { crowdin // .export() // Not sure if this should be called in the script since it could be very slow - .then(() => crowdin.downloadToPath(DOWNLOADED_TRANSLATIONS_PATH)) + // .then(() => crowdin.downloadToPath(DOWNLOADED_TRANSLATIONS_PATH)) + .downloadToPath(DOWNLOADED_TRANSLATIONS_PATH) .then(() => crowdin.getTranslationStatus()) .then(locales => { const usableLocales = locales From fc1d65cd919f24ac0a25d2cc953cf4e8812013c7 Mon Sep 17 00:00:00 2001 From: Clement Hoang Date: Mon, 13 Nov 2017 15:58:35 -0800 Subject: [PATCH 10/37] Change allowable language threshold to 50% --- crowdin/config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crowdin/config.js b/crowdin/config.js index a184e157be1..b0474077d82 100644 --- a/crowdin/config.js +++ b/crowdin/config.js @@ -1,5 +1,5 @@ module.exports = { key: process.env.CROWDIN_API_KEY, url: 'https://api.crowdin.com/api/project/react', - translation_threshold: 30, + translation_threshold: 50, }; From 06f68ce88ff1d0e5e13696ebd51a1b64c2204519 Mon Sep 17 00:00:00 2001 From: Clement Hoang Date: Fri, 17 Nov 2017 08:39:42 -0800 Subject: [PATCH 11/37] Add yarn script to download/symlink translations --- package.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index e8c83c2b51a..535043e25f2 100644 --- a/package.json +++ b/package.json @@ -80,10 +80,11 @@ "ci-check": "yarn prettier:diff && yarn lint && yarn flow", "dev": "gatsby develop -H 0.0.0.0", "lint": "eslint .", - "netlify": "yarn install && yarn build", + "netlify": "yarn install && yarn translations && yarn build", "prettier": "prettier --config .prettierrc --write \"{gatsby-*.js,{flow-typed,plugins,src}/**/*.js}\"", "prettier:diff": "prettier --config .prettierrc --list-different \"{gatsby-*.js,{flow-typed,plugins,src}/**/*.js}\"", - "reset": "rimraf ./.cache && rimraf ./crowdin/__translations && find crowdin/translations -type l -not -name '*en-US' -delete" + "reset": "rimraf ./.cache && rimraf ./crowdin/__translations && find crowdin/translations -type l -not -name '*en-US' -delete", + "translations": "node ./crowdin/download" }, "devDependencies": { "eslint-config-prettier": "^2.6.0", From 4b6e332e176150c7a52da4fcfc4e698f141380bf Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Thu, 10 May 2018 10:11:17 -0700 Subject: [PATCH 12/37] Prettier --- gatsby-config.js | 8 +++----- plugins/gatsby-source-react-error-codes/gatsby-node.js | 4 +++- src/components/CodeEditor/CodeEditor.js | 3 ++- src/components/MarkdownPage/MarkdownPage.js | 5 +++-- src/pages/acknowledgements.html.js | 8 ++------ src/theme.js | 10 ++++++---- 6 files changed, 19 insertions(+), 19 deletions(-) diff --git a/gatsby-config.js b/gatsby-config.js index 0d6909155a1..7d56b749d74 100644 --- a/gatsby-config.js +++ b/gatsby-config.js @@ -2,7 +2,7 @@ * Copyright (c) 2013-present, Facebook, Inc. * * @emails react-core -*/ + */ 'use strict'; @@ -25,8 +25,7 @@ module.exports = { 'gatsby-plugin-react-next', { resolve: 'gatsby-plugin-crowdin', - options: { - }, + options: {}, }, 'gatsby-plugin-twitter', { @@ -76,8 +75,7 @@ module.exports = { }, { resolve: 'gatsby-plugin-crowdin', - options: { - }, + options: {}, }, 'gatsby-remark-use-jsx', { diff --git a/plugins/gatsby-source-react-error-codes/gatsby-node.js b/plugins/gatsby-source-react-error-codes/gatsby-node.js index eef00fbe35d..cde9ef59626 100644 --- a/plugins/gatsby-source-react-error-codes/gatsby-node.js +++ b/plugins/gatsby-source-react-error-codes/gatsby-node.js @@ -20,7 +20,9 @@ exports.sourceNodes = async ({boundActionCreators}) => { }); } catch (error) { console.error( - `The gatsby-source-react-error-codes plugin has failed:\n${error.message}`, + `The gatsby-source-react-error-codes plugin has failed:\n${ + error.message + }`, ); process.exit(1); diff --git a/src/components/CodeEditor/CodeEditor.js b/src/components/CodeEditor/CodeEditor.js index a843de82010..64de54b8b5a 100644 --- a/src/components/CodeEditor/CodeEditor.js +++ b/src/components/CodeEditor/CodeEditor.js @@ -148,7 +148,8 @@ class CodeEditor extends Component { - this.setState({showJSX: event.target.checked})} + this.setState({showJSX: event.target.checked}) + } type="checkbox" />{' '} JSX? diff --git a/src/components/MarkdownPage/MarkdownPage.js b/src/components/MarkdownPage/MarkdownPage.js index b43b8e09047..6a9d4f69dd0 100644 --- a/src/components/MarkdownPage/MarkdownPage.js +++ b/src/components/MarkdownPage/MarkdownPage.js @@ -99,8 +99,9 @@ const MarkdownPage = ({ diff --git a/src/pages/acknowledgements.html.js b/src/pages/acknowledgements.html.js index 962f6e2f321..638c462586e 100644 --- a/src/pages/acknowledgements.html.js +++ b/src/pages/acknowledgements.html.js @@ -54,9 +54,7 @@ const Acknowlegements = ({data, location}) => (
  • Christopher Aue for - letting us use the - reactjs.com - {' '} + letting us use the reactjs.com{' '} domain name and the{' '} @reactjs username on Twitter. @@ -72,9 +70,7 @@ const Acknowlegements = ({data, location}) => ( react org on GitHub.
  • - - Dmitri Voronianski - {' '} + Dmitri Voronianski{' '} for letting us use the{' '} Oceanic Next diff --git a/src/theme.js b/src/theme.js index ab304ef4dd3..d29998800e1 100644 --- a/src/theme.js +++ b/src/theme.js @@ -47,14 +47,16 @@ type Size = $Keys; const media = { between(smallKey: Size, largeKey: Size, excludeLarge: boolean = false) { if (excludeLarge) { - return `@media (min-width: ${SIZES[smallKey] - .min}px) and (max-width: ${SIZES[largeKey].min - 1}px)`; + return `@media (min-width: ${ + SIZES[smallKey].min + }px) and (max-width: ${SIZES[largeKey].min - 1}px)`; } else { if (SIZES[largeKey].max === Infinity) { return `@media (min-width: ${SIZES[smallKey].min}px)`; } else { - return `@media (min-width: ${SIZES[smallKey] - .min}px) and (max-width: ${SIZES[largeKey].max}px)`; + return `@media (min-width: ${SIZES[smallKey].min}px) and (max-width: ${ + SIZES[largeKey].max + }px)`; } } }, From 70b9bd5e5573c23d6a71ba1fb1e9141f07c346ed Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Fri, 11 May 2018 10:58:39 -0700 Subject: [PATCH 13/37] Added config validation and fixed language <> locale mappin --- crowdin/.gitignore | 1 + crowdin/config.js | 3 +- crowdin/download.js | 86 +++++++++++++++++++++++++++++---------------- package.json | 6 ++-- 4 files changed, 62 insertions(+), 34 deletions(-) diff --git a/crowdin/.gitignore b/crowdin/.gitignore index eac63ba561b..91de876516c 100644 --- a/crowdin/.gitignore +++ b/crowdin/.gitignore @@ -1 +1,2 @@ __translations/ +translations/ \ No newline at end of file diff --git a/crowdin/config.js b/crowdin/config.js index b0474077d82..8296498138f 100644 --- a/crowdin/config.js +++ b/crowdin/config.js @@ -1,5 +1,6 @@ module.exports = { key: process.env.CROWDIN_API_KEY, url: 'https://api.crowdin.com/api/project/react', - translation_threshold: 50, + threshold: 50, + downloadedRootDirectory: 'test-17', }; diff --git a/crowdin/download.js b/crowdin/download.js index 4b4168ba8d1..ba556f8077a 100644 --- a/crowdin/download.js +++ b/crowdin/download.js @@ -5,16 +5,41 @@ const {symlink, lstatSync, readdirSync} = require('fs'); const SYMLINKED_TRANSLATIONS_PATH = path.resolve(__dirname, 'translations'); const DOWNLOADED_TRANSLATIONS_PATH = path.resolve(__dirname, '__translations'); -const DOWNLOADED_TRANSLATIONS_DOCS_PATH = path.resolve( + +// Path to the "docs" folder within the downloaded Crowdin translations bundle. +const downloadedDocsPath = path.resolve( __dirname, '__translations', + config.downloadedRootDirectory, 'docs', ); -function main() { - const crowdin = new Crowdin({apiKey: config.key, endpointUrl: config.url}); - process.chdir(SYMLINKED_TRANSLATIONS_PATH); +// Sanity check (local) Crowdin config file for expected values. +const validateCrowdinConfig = () => { + const errors = []; + if (!config.key) { + errors.push('key: No process.env.CROWDIN_API_KEY value defined.'); + } + if (!Number.isInteger(config.threshold)) { + errors.push(`threshold: Invalid translation threshold defined.`); + } + if (!config.downloadedRootDirectory) { + errors.push('downloadedRootDirectory: No root directory defined for the downloaded translations bundle.'); + } + if (!config.url) { + errors.push('url: No Crowdin project URL defined.'); + } + if (errors.length > 0) { + console.error('Invalid Crowdin config values for:\n• ' + errors.join('\n• ')); + throw Error('Invalid Crowdin config'); + } +}; +// Download Crowdin translations (into DOWNLOADED_TRANSLATIONS_PATH), +// Filter languages that have been sufficiently translated (based on config.threshold), +// And setup symlinks for them (in SYMLINKED_TRANSLATIONS_PATH) for Gatsby to read. +const downloadAndSymlink = () => { + const crowdin = new Crowdin({apiKey: config.key, endpointUrl: config.url}); crowdin // .export() // Not sure if this should be called in the script since it could be very slow // .then(() => crowdin.downloadToPath(DOWNLOADED_TRANSLATIONS_PATH)) @@ -23,56 +48,56 @@ function main() { .then(locales => { const usableLocales = locales .filter( - locale => locale.translated_progress > config.translation_threshold, + locale => locale.translated_progress > config.threshold, ) .map(local => local.code); - const localeDirectories = getDirectories( - DOWNLOADED_TRANSLATIONS_DOCS_PATH, - ); - + const localeDirectories = getLanguageDirectories(downloadedDocsPath); const localeToFolderMap = createLocaleToFolderMap(localeDirectories); usableLocales.forEach(locale => { createSymLink(localeToFolderMap.get(locale)); }); }); -} + +}; // Creates a relative symlink from a downloaded translation in the current working directory // Note that the current working directory of this node process should be where the symlink is created // or else the relative paths would be incorrect -function createSymLink(folder) { - symlink(`../__translations/docs/${folder}`, folder, err => { +const createSymLink = (folder) => { + const from = path.resolve(downloadedDocsPath, folder); + const to = path.resolve(SYMLINKED_TRANSLATIONS_PATH, folder); + symlink(from, to, err => { if (!err) { - console.log(`Created symlink for ${folder}.`); return; } if (err.code === 'EEXIST') { - console.log( - `Skipped creating symlink for ${folder}. A symlink already exists.`, - ); + // eslint-disable-next-line no-console + console.info(`Symlink already exists for ${folder}`); } else { console.error(err); process.exit(1); } }); -} +}; -// When we run getTranslationStatus(), it gives us 2-ALPHA locale codes unless necessary -// However, the folder structure of downloaded translations always has 4-ALPHA locale codes -// This function creates a map from a locale code to its corresponding folder name -function createLocaleToFolderMap(directories) { - const twoAlphaLocale = locale => locale.substring(0, 2); +// Crowdin.getTranslationStatus() provides ISO 639-1 (e.g. "fr" for French) or 639-3 (e.g. "fil" for Filipino) language codes, +// But the folder structure of downloaded translations uses locale codes (e.g. "fr-FR" for French, "fil-PH" for the Philippines). +// This function creates a map between language and locale code. +const createLocaleToFolderMap = (directories) => { + const localeToLanguageCode = locale => locale.includes('-') ? locale.substr(0, locale.indexOf('-')) : locale; const localeToFolders = new Map(); const localeToFolder = new Map(); for (let locale of directories) { + const languageCode = localeToLanguageCode(locale); + localeToFolders.set( - twoAlphaLocale(locale), - localeToFolders.has(twoAlphaLocale(locale)) - ? localeToFolders.get(twoAlphaLocale(locale)).concat(locale) + languageCode, + localeToFolders.has(languageCode) + ? localeToFolders.get(languageCode).concat(locale) : [locale], ); } @@ -88,13 +113,14 @@ function createLocaleToFolderMap(directories) { }); return localeToFolder; -} +}; -function getDirectories(source) { - return readdirSync(source).filter( +// Parse downloaded translation folder to determine which langauges it contains. +const getLanguageDirectories = source => + readdirSync(source).filter( name => lstatSync(path.join(source, name)).isDirectory() && name !== '_data', ); -} -main(); +validateCrowdinConfig(config); +downloadAndSymlink(); \ No newline at end of file diff --git a/package.json b/package.json index 23c5be2ae8b..75d13c9bc28 100644 --- a/package.json +++ b/package.json @@ -79,20 +79,20 @@ "build": "gatsby build", "check-all": "npm-run-all prettier --parallel lint flow", "ci-check": "npm-run-all prettier:diff --parallel lint flow", + "crowdin:download": "node ./crowdin/download", "dev": "gatsby develop -H 0.0.0.0", "flow": "flow", "format:source": "prettier --config .prettierrc --write \"{gatsby-*.js,{flow-typed,plugins,src}/**/*.js}\"", "format:examples": "prettier --config .prettierrc.examples --write \"examples/**/*.js\"", "lint": "eslint .", - "netlify": "yarn install && yarn translations && yarn build", + "netlify": "yarn install && yarn crowdin:download && yarn build", "nit:source": "prettier --config .prettierrc --list-different \"{gatsby-*.js,{flow-typed,plugins,src}/**/*.js}\"", "nit:examples": "prettier --config .prettierrc.examples --list-different \"examples/**/*.js\"", "prettier": "yarn format:source && yarn format:examples", "prettier:diff": "yarn nit:source && yarn nit:examples", "reset": "yarn reset:cache && yarn reset:translations", "reset:cache": "rimraf ./.cache", - "reset:translations": "rimraf ./crowdin/__translations && find crowdin/translations -type l -not -name '*en-US' -delete", - "translations": "node ./crowdin/download" + "reset:translations": "rimraf ./crowdin/__translations && find crowdin/translations -type l -not -name '*en-US' -delete" }, "devDependencies": { "eslint-config-prettier": "^2.6.0", From 31381a813071a58a84bd257d4d7f0d63983b01a0 Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Fri, 11 May 2018 14:56:35 -0700 Subject: [PATCH 14/37] Iterating on Crowdin scripts, Link, pages, etc --- .gitignore | 1 + crowdin/.gitignore | 2 +- crowdin/README.md | 48 +++++++++++++++++++ crowdin/config.js | 5 +- crowdin/download.js | 6 +-- package.json | 5 +- plugins/gatsby-plugin-crowdin/Link.js | 7 +-- src/pages/blog/all.html.js | 11 +++-- src/templates/blog.js | 4 +- .../NavigationFooter/NavigationFooter.js | 2 +- src/templates/components/Sidebar/Section.js | 2 + src/utils/createLink.js | 9 ++++ 12 files changed, 85 insertions(+), 17 deletions(-) create mode 100644 crowdin/README.md diff --git a/.gitignore b/.gitignore index dbe72d17694..afbe8974e9b 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ .idea node_modules public +yarn-error.log diff --git a/crowdin/.gitignore b/crowdin/.gitignore index 91de876516c..f23bf1c266b 100644 --- a/crowdin/.gitignore +++ b/crowdin/.gitignore @@ -1,2 +1,2 @@ -__translations/ +__exports/ translations/ \ No newline at end of file diff --git a/crowdin/README.md b/crowdin/README.md new file mode 100644 index 00000000000..c8776133477 --- /dev/null +++ b/crowdin/README.md @@ -0,0 +1,48 @@ +## How does this work? + +### Downloading content from Crowdin + +This subdirectory contains some JavaScript files as well as a symlink for the default language (English) that points to [the `content` directory](https://github.com/reactjs/reactjs.org/tree/master/content): +```sh +. +└── crowdin +   ├── config.js # Crowdin configuration settings +   ├── download.js # Node Download script +   └── translations +   └── en-US -> ../../content +``` + +Translations can be downloaded locally with `yarn crowdin:download`. This uses the Crowdin API to download data into an `__exports` subdirectory: +```sh +. +└── crowdin +   ├── config.js # Crowdin configuration settings +   ├── download.js # Node Download script +   ├── translations +   │ └── en-US -> ../../content +   └── __exports +       └── ... # Downloaded translations go here +``` + +Next the task identifies which languages have been translated past a certain threshold (specified by `crowdin/config.js`). For these languages, the script creates symlinks in the `translations` subdirectory: +```sh +. +└── crowdin +   ├── config.js # Crowdin configuration settings +   ├── download.js # Node Download script +   ├── translations +   │ ├── en-US -> ../../content +   │ ├── es-ES -> ../__exports/.../es-ES +   │ ├── zh-CN -> ../__exports/.../zh-CN +   │ └── ... # Other symlinks go here +   └── __exports +       └── ... # Downloaded translations go here +``` + +### Gatsby integration + +A new (local) `gatsby-plugin-crowdin` plugin has been created that knows how to create localized links to certain sections of the website. **For now, only content from [the `content/docs` directory](https://github.com/reactjs/reactjs.org/tree/master/content/docs) is localized. All other sections/pages remain English only.** + +The `gatsby-source-filesystem` plugin has also been reconfigured to read all content from the `crowdin/translations/*` (symlinked) directories rather than `content`. This way it consumes translated content when available. (Crowdin provides default language content for sections that have not yet been translated for any given locale.) + +Because of the default symlink (`crowdin/translations/en-US` -> `content`) Gatsby will serve English content when run locally, even if the Crowdin script has not been run. This should enable fast iteration and creation of new content. \ No newline at end of file diff --git a/crowdin/config.js b/crowdin/config.js index 8296498138f..dfa72180a00 100644 --- a/crowdin/config.js +++ b/crowdin/config.js @@ -1,6 +1,9 @@ +const path = require('path'); + +// Also relates to the crowdin.yaml file in the root directory module.exports = { key: process.env.CROWDIN_API_KEY, url: 'https://api.crowdin.com/api/project/react', threshold: 50, - downloadedRootDirectory: 'test-17', + downloadedRootDirectory: path.join('test-17', 'docs'), }; diff --git a/crowdin/download.js b/crowdin/download.js index ba556f8077a..4e68275ba63 100644 --- a/crowdin/download.js +++ b/crowdin/download.js @@ -4,14 +4,12 @@ const path = require('path'); const {symlink, lstatSync, readdirSync} = require('fs'); const SYMLINKED_TRANSLATIONS_PATH = path.resolve(__dirname, 'translations'); -const DOWNLOADED_TRANSLATIONS_PATH = path.resolve(__dirname, '__translations'); +const DOWNLOADED_TRANSLATIONS_PATH = path.resolve(__dirname, '__exports'); // Path to the "docs" folder within the downloaded Crowdin translations bundle. const downloadedDocsPath = path.resolve( - __dirname, - '__translations', + DOWNLOADED_TRANSLATIONS_PATH, config.downloadedRootDirectory, - 'docs', ); // Sanity check (local) Crowdin config file for expected values. diff --git a/package.json b/package.json index 75d13c9bc28..47cda16e301 100644 --- a/package.json +++ b/package.json @@ -80,7 +80,7 @@ "check-all": "npm-run-all prettier --parallel lint flow", "ci-check": "npm-run-all prettier:diff --parallel lint flow", "crowdin:download": "node ./crowdin/download", - "dev": "gatsby develop -H 0.0.0.0", + "dev": "yarn start", "flow": "flow", "format:source": "prettier --config .prettierrc --write \"{gatsby-*.js,{flow-typed,plugins,src}/**/*.js}\"", "format:examples": "prettier --config .prettierrc.examples --write \"examples/**/*.js\"", @@ -92,7 +92,8 @@ "prettier:diff": "yarn nit:source && yarn nit:examples", "reset": "yarn reset:cache && yarn reset:translations", "reset:cache": "rimraf ./.cache", - "reset:translations": "rimraf ./crowdin/__translations && find crowdin/translations -type l -not -name '*en-US' -delete" + "reset:translations": "rimraf ./crowdin/__translations && find crowdin/translations -type l -not -name '*en-US' -delete", + "start": "gatsby develop -H 0.0.0.0" }, "devDependencies": { "eslint-config-prettier": "^2.6.0", diff --git a/plugins/gatsby-plugin-crowdin/Link.js b/plugins/gatsby-plugin-crowdin/Link.js index bfff02e580d..caaaa41c2f5 100644 --- a/plugins/gatsby-plugin-crowdin/Link.js +++ b/plugins/gatsby-plugin-crowdin/Link.js @@ -12,10 +12,10 @@ import React from 'react'; import {getLanguageCodeFromPath} from './utils'; // TODO THis is a hack :( Pass this down via context or some other way? -const DEFAULT_LANGUAGE = 'en'; +const DEFAULT_LANGUAGE = 'en-US'; -const DecoratedLink = ({location, to, ...rest}, ...other) => { - if (to.startsWith('/')) { +const DecoratedLink = ({isLocalized, location, to, ...rest}, ...other) => { + if (isLocalized !== false && to.startsWith('/')) { const languageCode = getLanguageCodeFromPath(location.pathname.substr(1)) || DEFAULT_LANGUAGE; @@ -29,6 +29,7 @@ const DecoratedLink = ({location, to, ...rest}, ...other) => { }; DecoratedLink.propTypes = { + isLocalized: PropTypes.bool, location: PropTypes.object.isRequired, to: PropTypes.string.isRequired, }; diff --git a/src/pages/blog/all.html.js b/src/pages/blog/all.html.js index e28c8c09dc0..b0cfe8761f1 100644 --- a/src/pages/blog/all.html.js +++ b/src/pages/blog/all.html.js @@ -5,7 +5,7 @@ * @flow */ -import Link from 'gatsby-link'; +import Link from 'gatsby-plugin-crowdin/Link'; import Container from 'components/Container'; import Header from 'components/Header'; import TitleAndMetaTags from 'components/TitleAndMetaTags'; @@ -19,9 +19,10 @@ import type {allMarkdownRemarkData} from 'types'; type Props = { data: allMarkdownRemarkData, + location: Location, }; -const AllBlogPosts = ({data}: Props) => ( +const AllBlogPosts = ({data, location}: Props) => (
    @@ -53,7 +54,7 @@ const AllBlogPosts = ({data}: Props) => ( width: '33.33%', }, }} - key={node.fields.slug}> + key={node.fields.id}>

    ( borderBottomColor: colors.black, }, }} - key={node.fields.slug} + key={node.fields.id} + location={location} to={node.fields.slug}> {node.frontmatter.title} @@ -116,6 +118,7 @@ export const pageQuery = graphql` } fields { date(formatString: "MMMM DD, YYYY") + id slug } } diff --git a/src/templates/blog.js b/src/templates/blog.js index 4754b854779..477c621265a 100644 --- a/src/templates/blog.js +++ b/src/templates/blog.js @@ -14,10 +14,11 @@ const toSectionList = allMarkdownRemark => [ title: 'Recent Posts', items: allMarkdownRemark.edges .map(({node}) => ({ - id: node.fields.id, + id: node.fields.slug, title: node.frontmatter.title, })) .concat({ + isLocalized: false, id: '/blog/all.html', title: 'All posts ...', }), @@ -76,6 +77,7 @@ export const pageQuery = graphql` } fields { id + slug } } } diff --git a/src/templates/components/NavigationFooter/NavigationFooter.js b/src/templates/components/NavigationFooter/NavigationFooter.js index f5ce55aba32..96064bcebe9 100644 --- a/src/templates/components/NavigationFooter/NavigationFooter.js +++ b/src/templates/components/NavigationFooter/NavigationFooter.js @@ -11,7 +11,7 @@ import PropTypes from 'prop-types'; import React from 'react'; import {colors, fonts, media} from 'theme'; -const NavigationFooter = ({next, prev, location}) => { +const NavigationFooter = ({location, next, prev}) => { return (
    {isActive && } @@ -39,6 +42,7 @@ const createLinkBlog = ({ const createLinkCommunity = ({ isActive, + isLocalized, item, location, section, @@ -60,6 +64,7 @@ const createLinkCommunity = ({ } return createLinkDocs({ isActive, + isLocalized, item, location, section, @@ -68,6 +73,7 @@ const createLinkCommunity = ({ const createLinkDocs = ({ isActive, + isLocalized, item, location, section, @@ -75,6 +81,7 @@ const createLinkDocs = ({ return ( {isActive && } @@ -89,6 +96,7 @@ type CreateLinkTutorialProps = { const createLinkTutorial = ({ isActive, + isLocalized, item, location, onLinkClick, @@ -97,6 +105,7 @@ const createLinkTutorial = ({ return ( From 40d6768e7cd45d22cba20a1a7489b24dbe67c8cc Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Fri, 11 May 2018 15:08:56 -0700 Subject: [PATCH 15/37] Reorganized Crowdin folders for readability --- crowdin/.gitignore | 4 ++-- crowdin/README.md | 24 +++++++++---------- .../{translations => __filtered__}/.gitignore | 0 crowdin/{translations => __filtered__}/en-US | 0 crowdin/download.js | 4 ++-- 5 files changed, 16 insertions(+), 16 deletions(-) rename crowdin/{translations => __filtered__}/.gitignore (100%) rename crowdin/{translations => __filtered__}/en-US (100%) diff --git a/crowdin/.gitignore b/crowdin/.gitignore index f23bf1c266b..2f5a1b14438 100644 --- a/crowdin/.gitignore +++ b/crowdin/.gitignore @@ -1,2 +1,2 @@ -__exports/ -translations/ \ No newline at end of file +__exported__/ +__filtered__/ \ No newline at end of file diff --git a/crowdin/README.md b/crowdin/README.md index c8776133477..65e0083c1e5 100644 --- a/crowdin/README.md +++ b/crowdin/README.md @@ -8,41 +8,41 @@ This subdirectory contains some JavaScript files as well as a symlink for the de └── crowdin    ├── config.js # Crowdin configuration settings    ├── download.js # Node Download script -   └── translations +   └── __filtered__    └── en-US -> ../../content ``` -Translations can be downloaded locally with `yarn crowdin:download`. This uses the Crowdin API to download data into an `__exports` subdirectory: +Translations can be downloaded locally with `yarn crowdin:download`. This uses the Crowdin API to download data into an `__exported__` subdirectory: ```sh . └── crowdin    ├── config.js # Crowdin configuration settings    ├── download.js # Node Download script -   ├── translations +   ├── __filtered__    │ └── en-US -> ../../content -   └── __exports +   └── __exported__       └── ... # Downloaded translations go here ``` -Next the task identifies which languages have been translated past a certain threshold (specified by `crowdin/config.js`). For these languages, the script creates symlinks in the `translations` subdirectory: +Next the task identifies which languages have been translated past a certain threshold (specified by `crowdin/config.js`). For these languages, the script creates symlinks in the `__filtered__` subdirectory: ```sh . └── crowdin    ├── config.js # Crowdin configuration settings    ├── download.js # Node Download script -   ├── translations +   ├── __filtered__    │ ├── en-US -> ../../content -   │ ├── es-ES -> ../__exports/.../es-ES -   │ ├── zh-CN -> ../__exports/.../zh-CN +   │ ├── es-ES -> ../__exported__/.../es-ES +   │ ├── zh-CN -> ../__exported__/.../zh-CN    │ └── ... # Other symlinks go here -   └── __exports +   └── __exported__       └── ... # Downloaded translations go here ``` ### Gatsby integration -A new (local) `gatsby-plugin-crowdin` plugin has been created that knows how to create localized links to certain sections of the website. **For now, only content from [the `content/docs` directory](https://github.com/reactjs/reactjs.org/tree/master/content/docs) is localized. All other sections/pages remain English only.** +A (local) `gatsby-plugin-crowdin` plugin has been created that knows how to create localized links to certain sections of the website. **For now, only content from [the `content/docs` directory](https://github.com/reactjs/reactjs.org/tree/master/content/docs) is localized. All other sections/pages remain English only.** -The `gatsby-source-filesystem` plugin has also been reconfigured to read all content from the `crowdin/translations/*` (symlinked) directories rather than `content`. This way it consumes translated content when available. (Crowdin provides default language content for sections that have not yet been translated for any given locale.) +The `gatsby-source-filesystem` plugin has also been reconfigured to read all content from the `crowdin/__filtered__/*` (symlinked) directories rather than `content`. This way it consumes translated content when available. (Crowdin provides default language content for sections that have not yet been translated for any given locale.) -Because of the default symlink (`crowdin/translations/en-US` -> `content`) Gatsby will serve English content when run locally, even if the Crowdin script has not been run. This should enable fast iteration and creation of new content. \ No newline at end of file +Because of the default symlink (`crowdin/__filtered__/en-US` -> `content`) Gatsby will serve English content when run locally, even if the Crowdin script has not been run. This should enable fast iteration and creation of new content. \ No newline at end of file diff --git a/crowdin/translations/.gitignore b/crowdin/__filtered__/.gitignore similarity index 100% rename from crowdin/translations/.gitignore rename to crowdin/__filtered__/.gitignore diff --git a/crowdin/translations/en-US b/crowdin/__filtered__/en-US similarity index 100% rename from crowdin/translations/en-US rename to crowdin/__filtered__/en-US diff --git a/crowdin/download.js b/crowdin/download.js index 4e68275ba63..a86dd38b9b6 100644 --- a/crowdin/download.js +++ b/crowdin/download.js @@ -3,8 +3,8 @@ const config = require('./config'); const path = require('path'); const {symlink, lstatSync, readdirSync} = require('fs'); -const SYMLINKED_TRANSLATIONS_PATH = path.resolve(__dirname, 'translations'); -const DOWNLOADED_TRANSLATIONS_PATH = path.resolve(__dirname, '__exports'); +const SYMLINKED_TRANSLATIONS_PATH = path.resolve(__dirname, '__filtered__'); +const DOWNLOADED_TRANSLATIONS_PATH = path.resolve(__dirname, '__exported__'); // Path to the "docs" folder within the downloaded Crowdin translations bundle. const downloadedDocsPath = path.resolve( From 52a0f6aa11aca88c81b2887f6bbc55c3c2aaf63c Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Tue, 15 May 2018 07:02:23 -0700 Subject: [PATCH 16/37] Changing Crowdin symlink directory structure. --- crowdin/.gitignore | 2 +- crowdin/__filtered__/.gitignore | 1 - crowdin/__filtered__/en-US | 1 - crowdin/__untranslated__/404.md | 1 + crowdin/__untranslated__/acknowledgements.yml | 1 + crowdin/__untranslated__/authors.yml | 1 + crowdin/__untranslated__/blog | 1 + crowdin/__untranslated__/community | 1 + crowdin/__untranslated__/home | 1 + crowdin/__untranslated__/images | 1 + crowdin/__untranslated__/tutorial | 1 + crowdin/__untranslated__/versions.yml | 1 + crowdin/__untranslated__/warnings | 1 + crowdin/config.js | 5 +- crowdin/download.js | 56 +++++---- gatsby-config.js | 11 +- plugins/gatsby-plugin-crowdin/onCreateNode.js | 22 ++++ plugins/gatsby-plugin-crowdin/onCreatePage.js | 107 ++++++++++-------- src/templates/docs.js | 7 ++ src/utils/createLink.js | 7 +- 20 files changed, 152 insertions(+), 77 deletions(-) delete mode 100644 crowdin/__filtered__/.gitignore delete mode 120000 crowdin/__filtered__/en-US create mode 120000 crowdin/__untranslated__/404.md create mode 120000 crowdin/__untranslated__/acknowledgements.yml create mode 120000 crowdin/__untranslated__/authors.yml create mode 120000 crowdin/__untranslated__/blog create mode 120000 crowdin/__untranslated__/community create mode 120000 crowdin/__untranslated__/home create mode 120000 crowdin/__untranslated__/images create mode 120000 crowdin/__untranslated__/tutorial create mode 120000 crowdin/__untranslated__/versions.yml create mode 120000 crowdin/__untranslated__/warnings diff --git a/crowdin/.gitignore b/crowdin/.gitignore index 2f5a1b14438..fa3f0ea49bd 100644 --- a/crowdin/.gitignore +++ b/crowdin/.gitignore @@ -1,2 +1,2 @@ __exported__/ -__filtered__/ \ No newline at end of file +__translated__/ \ No newline at end of file diff --git a/crowdin/__filtered__/.gitignore b/crowdin/__filtered__/.gitignore deleted file mode 100644 index dceefb7eaef..00000000000 --- a/crowdin/__filtered__/.gitignore +++ /dev/null @@ -1 +0,0 @@ -!en-US/ diff --git a/crowdin/__filtered__/en-US b/crowdin/__filtered__/en-US deleted file mode 120000 index b181ab91b05..00000000000 --- a/crowdin/__filtered__/en-US +++ /dev/null @@ -1 +0,0 @@ -../../content \ No newline at end of file diff --git a/crowdin/__untranslated__/404.md b/crowdin/__untranslated__/404.md new file mode 120000 index 00000000000..ea9377974f3 --- /dev/null +++ b/crowdin/__untranslated__/404.md @@ -0,0 +1 @@ +../../content/404.md \ No newline at end of file diff --git a/crowdin/__untranslated__/acknowledgements.yml b/crowdin/__untranslated__/acknowledgements.yml new file mode 120000 index 00000000000..8a080ff1898 --- /dev/null +++ b/crowdin/__untranslated__/acknowledgements.yml @@ -0,0 +1 @@ +../../content/acknowledgements.yml \ No newline at end of file diff --git a/crowdin/__untranslated__/authors.yml b/crowdin/__untranslated__/authors.yml new file mode 120000 index 00000000000..41bd15345e1 --- /dev/null +++ b/crowdin/__untranslated__/authors.yml @@ -0,0 +1 @@ +../../content/authors.yml \ No newline at end of file diff --git a/crowdin/__untranslated__/blog b/crowdin/__untranslated__/blog new file mode 120000 index 00000000000..b88eff338de --- /dev/null +++ b/crowdin/__untranslated__/blog @@ -0,0 +1 @@ +../../content/blog \ No newline at end of file diff --git a/crowdin/__untranslated__/community b/crowdin/__untranslated__/community new file mode 120000 index 00000000000..43f0a9bbf45 --- /dev/null +++ b/crowdin/__untranslated__/community @@ -0,0 +1 @@ +../../content/community \ No newline at end of file diff --git a/crowdin/__untranslated__/home b/crowdin/__untranslated__/home new file mode 120000 index 00000000000..419e1cca147 --- /dev/null +++ b/crowdin/__untranslated__/home @@ -0,0 +1 @@ +../../content/home \ No newline at end of file diff --git a/crowdin/__untranslated__/images b/crowdin/__untranslated__/images new file mode 120000 index 00000000000..2bf9fd8e63b --- /dev/null +++ b/crowdin/__untranslated__/images @@ -0,0 +1 @@ +../../content/images \ No newline at end of file diff --git a/crowdin/__untranslated__/tutorial b/crowdin/__untranslated__/tutorial new file mode 120000 index 00000000000..9699b9f4b20 --- /dev/null +++ b/crowdin/__untranslated__/tutorial @@ -0,0 +1 @@ +../../content/tutorial \ No newline at end of file diff --git a/crowdin/__untranslated__/versions.yml b/crowdin/__untranslated__/versions.yml new file mode 120000 index 00000000000..783cccf8cd3 --- /dev/null +++ b/crowdin/__untranslated__/versions.yml @@ -0,0 +1 @@ +../../content/versions.yml \ No newline at end of file diff --git a/crowdin/__untranslated__/warnings b/crowdin/__untranslated__/warnings new file mode 120000 index 00000000000..9ef7a1dc98e --- /dev/null +++ b/crowdin/__untranslated__/warnings @@ -0,0 +1 @@ +../../content/warnings \ No newline at end of file diff --git a/crowdin/config.js b/crowdin/config.js index dfa72180a00..23d03305cd6 100644 --- a/crowdin/config.js +++ b/crowdin/config.js @@ -2,8 +2,9 @@ const path = require('path'); // Also relates to the crowdin.yaml file in the root directory module.exports = { + downloadedRootDirectory: path.join('test-17', 'docs'), key: process.env.CROWDIN_API_KEY, - url: 'https://api.crowdin.com/api/project/react', threshold: 50, - downloadedRootDirectory: path.join('test-17', 'docs'), + url: 'https://api.crowdin.com/api/project/react', + whitelist: ['docs'], }; diff --git a/crowdin/download.js b/crowdin/download.js index a86dd38b9b6..e3c03a425e5 100644 --- a/crowdin/download.js +++ b/crowdin/download.js @@ -1,30 +1,31 @@ const Crowdin = require('crowdin-node'); -const config = require('./config'); +const {downloadedRootDirectory, key, threshold, url, whitelist} = require('./config'); +const fs = require('fs'); const path = require('path'); const {symlink, lstatSync, readdirSync} = require('fs'); -const SYMLINKED_TRANSLATIONS_PATH = path.resolve(__dirname, '__filtered__'); -const DOWNLOADED_TRANSLATIONS_PATH = path.resolve(__dirname, '__exported__'); +const TRANSLATED_PATH = path.resolve(__dirname, '__translated__'); +const EXPORTED_PATH = path.resolve(__dirname, '__exported__'); // Path to the "docs" folder within the downloaded Crowdin translations bundle. const downloadedDocsPath = path.resolve( - DOWNLOADED_TRANSLATIONS_PATH, - config.downloadedRootDirectory, + EXPORTED_PATH, + downloadedRootDirectory, ); // Sanity check (local) Crowdin config file for expected values. const validateCrowdinConfig = () => { const errors = []; - if (!config.key) { + if (!key) { errors.push('key: No process.env.CROWDIN_API_KEY value defined.'); } - if (!Number.isInteger(config.threshold)) { + if (!Number.isInteger(threshold)) { errors.push(`threshold: Invalid translation threshold defined.`); } - if (!config.downloadedRootDirectory) { + if (!downloadedRootDirectory) { errors.push('downloadedRootDirectory: No root directory defined for the downloaded translations bundle.'); } - if (!config.url) { + if (!url) { errors.push('url: No Crowdin project URL defined.'); } if (errors.length > 0) { @@ -33,20 +34,20 @@ const validateCrowdinConfig = () => { } }; -// Download Crowdin translations (into DOWNLOADED_TRANSLATIONS_PATH), +// Download Crowdin translations (into EXPORTED_PATH), // Filter languages that have been sufficiently translated (based on config.threshold), -// And setup symlinks for them (in SYMLINKED_TRANSLATIONS_PATH) for Gatsby to read. +// And setup symlinks for them (in TRANSLATED_PATH) for Gatsby to read. const downloadAndSymlink = () => { - const crowdin = new Crowdin({apiKey: config.key, endpointUrl: config.url}); + const crowdin = new Crowdin({apiKey: key, endpointUrl: url}); crowdin // .export() // Not sure if this should be called in the script since it could be very slow - // .then(() => crowdin.downloadToPath(DOWNLOADED_TRANSLATIONS_PATH)) - .downloadToPath(DOWNLOADED_TRANSLATIONS_PATH) + // .then(() => crowdin.downloadToPath(EXPORTED_PATH)) + .downloadToPath(EXPORTED_PATH) .then(() => crowdin.getTranslationStatus()) .then(locales => { const usableLocales = locales .filter( - locale => locale.translated_progress > config.threshold, + locale => locale.translated_progress > threshold, ) .map(local => local.code); @@ -54,7 +55,22 @@ const downloadAndSymlink = () => { const localeToFolderMap = createLocaleToFolderMap(localeDirectories); usableLocales.forEach(locale => { - createSymLink(localeToFolderMap.get(locale)); + const languageCode = localeToFolderMap.get(locale); + const rootLanguageFolder = path.resolve(TRANSLATED_PATH, languageCode); + + if (Array.isArray(whitelist)) { + if (!fs.existsSync(rootLanguageFolder)) { + fs.mkdirSync(rootLanguageFolder); + } + + // Symlink only the whitelisted subdirectories + whitelist.forEach(subdirectory => { + createSymLink(path.join(languageCode, subdirectory)); + }); + } else { + // Otherwise symlink the entire language export + createSymLink(languageCode); + } }); }); @@ -63,9 +79,9 @@ const downloadAndSymlink = () => { // Creates a relative symlink from a downloaded translation in the current working directory // Note that the current working directory of this node process should be where the symlink is created // or else the relative paths would be incorrect -const createSymLink = (folder) => { - const from = path.resolve(downloadedDocsPath, folder); - const to = path.resolve(SYMLINKED_TRANSLATIONS_PATH, folder); +const createSymLink = (relativePath) => { + const from = path.resolve(downloadedDocsPath, relativePath); + const to = path.resolve(TRANSLATED_PATH, relativePath); symlink(from, to, err => { if (!err) { return; @@ -120,5 +136,5 @@ const getLanguageDirectories = source => lstatSync(path.join(source, name)).isDirectory() && name !== '_data', ); -validateCrowdinConfig(config); +validateCrowdinConfig(); downloadAndSymlink(); \ No newline at end of file diff --git a/gatsby-config.js b/gatsby-config.js index b020a2d325f..6f604ed3c8e 100644 --- a/gatsby-config.js +++ b/gatsby-config.js @@ -45,8 +45,15 @@ module.exports = { { resolve: 'gatsby-source-filesystem', options: { - name: 'translations', - path: `${__dirname}/crowdin/translations/`, + name: 'untranslated', + path: `${__dirname}/crowdin/__untranslated__/`, + }, + }, + { + resolve: 'gatsby-source-filesystem', + options: { + name: 'translated', + path: `${__dirname}/crowdin/__translated__/`, }, }, { diff --git a/plugins/gatsby-plugin-crowdin/onCreateNode.js b/plugins/gatsby-plugin-crowdin/onCreateNode.js index 861fbbf26e2..7972ea5b9a0 100644 --- a/plugins/gatsby-plugin-crowdin/onCreateNode.js +++ b/plugins/gatsby-plugin-crowdin/onCreateNode.js @@ -11,6 +11,27 @@ const { getLanguageFromLanguageAndRegion, } = require('./utils'); +/** Params +{ + node: { + id: "...", + children: [], + parent: "...", + internal: { + content: "...", + contentDigest: "0351d452c1fabfe0eaec3faa9a60cde3", + type: "MarkdownRemark", + owner: "gatsby-transformer-remark" + }, + frontmatter: { + title: "...", + order: 1, + parent: "/path/to/parent/file/file.md" + }, + fileAbsolutePath: "/path/to/file.md" + } +} + */ module.exports = exports.onCreateNode = ({ node, boundActionCreators, @@ -24,6 +45,7 @@ module.exports = exports.onCreateNode = ({ const languageCode = getLanguageCodeFromPath(relativePath); + // TODO: Only do this for `gatsby-source-filesystem` name=translated sources? if (languageCode !== null) { const language = getLanguageFromLanguageAndRegion(languageCode); diff --git a/plugins/gatsby-plugin-crowdin/onCreatePage.js b/plugins/gatsby-plugin-crowdin/onCreatePage.js index bf0affa4170..ddd955bec58 100644 --- a/plugins/gatsby-plugin-crowdin/onCreatePage.js +++ b/plugins/gatsby-plugin-crowdin/onCreatePage.js @@ -1,58 +1,70 @@ +/** + * Copyright (c) 2013-present, Facebook, Inc. + * + * @emails react-core + */ + 'use strict'; /** Params -{ page: - { layout: 'index', - jsonName: 'dev-404-page.json', - internalComponentName: 'ComponentDev404Page', - path: '/dev-404-page/', - matchPath: undefined, - component: '/Users/bvaughn/Documents/git/reactjs.org/.cache/dev-404-page.js', - componentChunkName: 'component---cache-dev-404-page-js', - context: {}, - updatedAt: 1510471947417, - pluginCreator___NODE: 'Plugin dev-404-page', - pluginCreatorId: 'Plugin dev-404-page', - componentPath: '/Users/bvaughn/Documents/git/reactjs.org/.cache/dev-404-page.js' }, - traceId: 'initial-createPages', - pathPrefix: '', - boundActionCreators: - { deletePage: [Function], - createPage: [Function], - deleteLayout: [Function], - createLayout: [Function], - deleteNode: [Function], - deleteNodes: [Function], - createNode: [Function], - touchNode: [Function], - createNodeField: [Function], - createParentChildLink: [Function], - createPageDependency: [Function], - deleteComponentsDependencies: [Function], - replaceComponentQuery: [Function], - createJob: [Function], - setJob: [Function], - endJob: [Function], - setPluginStatus: [Function], - createRedirect: [Function] }, +{ + page: { + layout: "index", + jsonName: "dev-404-page.json", + internalComponentName: "ComponentDev404Page", + path: "/dev-404-page/", + matchPath: undefined, + component: + "/Users/bvaughn/Documents/git/reactjs.org/.cache/dev-404-page.js", + componentChunkName: "component---cache-dev-404-page-js", + context: {}, + updatedAt: 1510471947417, + pluginCreator___NODE: "Plugin dev-404-page", + pluginCreatorId: "Plugin dev-404-page", + componentPath: + "/Users/bvaughn/Documents/git/reactjs.org/.cache/dev-404-page.js" + }, + traceId: "initial-createPages", + pathPrefix: "", + boundActionCreators: { + deletePage: [Function], + createPage: [Function], + deleteLayout: [Function], + createLayout: [Function], + deleteNode: [Function], + deleteNodes: [Function], + createNode: [Function], + touchNode: [Function], + createNodeField: [Function], + createParentChildLink: [Function], + createPageDependency: [Function], + deleteComponentsDependencies: [Function], + replaceComponentQuery: [Function], + createJob: [Function], + setJob: [Function], + endJob: [Function], + setPluginStatus: [Function], + createRedirect: [Function] + }, loadNodeContent: [Function], - store: - { dispatch: [Function: dispatch], - subscribe: [Function: subscribe], - getState: [Function: getState], - replaceReducer: [Function: replaceReducer], - [Symbol(observable)]: [Function: observable] }, + store: { + dispatch: [(Function: dispatch)], + subscribe: [(Function: subscribe)], + getState: [(Function: getState)], + replaceReducer: [(Function: replaceReducer)], + [Symbol(observable)]: [(Function: observable)] + }, getNodes: [Function], - getNode: [Function: getNode], + getNode: [(Function: getNode)], hasNodeChanged: [Function], reporter: undefined, getNodeAndSavePathDependency: [Function], - cache: - { initCache: [Function], - get: [Function], - set: [Function] } } -{ plugins: [], - ...pluginOptions } + cache: { + initCache: [Function], + get: [Function], + set: [Function] + } +} */ // Gatsby has 2 types of pages: @@ -72,6 +84,7 @@ module.exports = ({page, boundActionCreators}, pluginOptions) => { path, // eg /zn-CH/path/to/template.html, /path/to/page.html } = page; + // TODO: Only do this for `gatsby-source-filesystem` name=translated sources? if (!languageCode) { return; // No-op for JavaScript pages (eg src/pages) } diff --git a/src/templates/docs.js b/src/templates/docs.js index 2fcb9fd0e22..568b4e716d3 100644 --- a/src/templates/docs.js +++ b/src/templates/docs.js @@ -24,6 +24,13 @@ Docs.propTypes = { data: PropTypes.object.isRequired, }; +// allFile(filter: {internal: {mediaType: {eq: "text/markdown"}}, sourceInstanceName: {eq: "projects"}}) { +// allMarkdownRemark( +// filter: { fileAbsolutePath: {regex : "\/posts/"} }, +// sort: {fields: [frontmatter___date], order: DESC}, +// ) { +// https://github.com/gatsbyjs/gatsby/issues/1634 + // eslint-disable-next-line no-undef export const pageQuery = graphql` query TemplateDocsMarkdown($id: String!) { diff --git a/src/utils/createLink.js b/src/utils/createLink.js index 39e7577548c..df69e0c5ab7 100644 --- a/src/utils/createLink.js +++ b/src/utils/createLink.js @@ -5,7 +5,8 @@ * @flow */ -import Link from 'gatsby-plugin-crowdin/Link'; +import LocalizedLink from 'gatsby-plugin-crowdin/Link'; +import Link from 'gatsby-link'; import React from 'react'; import ExternalLinkSvg from 'templates/components/ExternalLinkSvg'; import slugify from 'utils/slugify'; @@ -79,14 +80,14 @@ const createLinkDocs = ({ section, }: CreateLinkBaseProps): Node => { return ( - {isActive && } {item.title} - + ); }; From 1fb5ffe653dcaf10437130a58166a6157c6add80 Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Tue, 15 May 2018 07:32:02 -0700 Subject: [PATCH 17/37] Improved some conditionals --- gatsby/createPages.js | 3 ++- plugins/gatsby-plugin-crowdin/onCreateNode.js | 10 +++++----- src/pages/blog/all.html.js | 6 ++---- 3 files changed, 9 insertions(+), 10 deletions(-) diff --git a/gatsby/createPages.js b/gatsby/createPages.js index 6b50254214f..3e12b34c64f 100644 --- a/gatsby/createPages.js +++ b/gatsby/createPages.js @@ -88,7 +88,8 @@ module.exports = async (params) => { } const createArticlePage = path => { - if (path.startsWith('/')) { + // TODO This should be a utility exposed by gatsby-plugin-crowdin + if (languageCode != null && path.startsWith('/')) { path = `/${languageCode}/${path.substr(1)}`; } diff --git a/plugins/gatsby-plugin-crowdin/onCreateNode.js b/plugins/gatsby-plugin-crowdin/onCreateNode.js index 7972ea5b9a0..88125d94160 100644 --- a/plugins/gatsby-plugin-crowdin/onCreateNode.js +++ b/plugins/gatsby-plugin-crowdin/onCreateNode.js @@ -41,12 +41,12 @@ module.exports = exports.onCreateNode = ({ switch (node.internal.type) { case 'MarkdownRemark': - const {relativePath} = getNode(node.parent); + // Parent node is owned by the 'gatsby-source-filesystem' plug-in + const {relativePath, sourceInstanceName} = getNode(node.parent); - const languageCode = getLanguageCodeFromPath(relativePath); - - // TODO: Only do this for `gatsby-source-filesystem` name=translated sources? - if (languageCode !== null) { + // We only need to attribute language metadata for "translated" sources + if (sourceInstanceName === 'translated') { + const languageCode = getLanguageCodeFromPath(relativePath); const language = getLanguageFromLanguageAndRegion(languageCode); createNodeField({ diff --git a/src/pages/blog/all.html.js b/src/pages/blog/all.html.js index b0cfe8761f1..24e8d114849 100644 --- a/src/pages/blog/all.html.js +++ b/src/pages/blog/all.html.js @@ -5,7 +5,7 @@ * @flow */ -import Link from 'gatsby-plugin-crowdin/Link'; +import Link from 'gatsby-link'; import Container from 'components/Container'; import Header from 'components/Header'; import TitleAndMetaTags from 'components/TitleAndMetaTags'; @@ -19,10 +19,9 @@ import type {allMarkdownRemarkData} from 'types'; type Props = { data: allMarkdownRemarkData, - location: Location, }; -const AllBlogPosts = ({data, location}: Props) => ( +const AllBlogPosts = ({data}: Props) => (
    @@ -70,7 +69,6 @@ const AllBlogPosts = ({data, location}: Props) => ( }, }} key={node.fields.id} - location={location} to={node.fields.slug}> {node.frontmatter.title} From ca3fd115b308c2eeb6da47dc624c998664f91cc4 Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Tue, 15 May 2018 07:39:31 -0700 Subject: [PATCH 18/37] Added en-US default symlink --- crowdin/__translated__/.gitignore | 1 + crowdin/__translated__/en-US/docs | 1 + 2 files changed, 2 insertions(+) create mode 100644 crowdin/__translated__/.gitignore create mode 120000 crowdin/__translated__/en-US/docs diff --git a/crowdin/__translated__/.gitignore b/crowdin/__translated__/.gitignore new file mode 100644 index 00000000000..557915cbb4f --- /dev/null +++ b/crowdin/__translated__/.gitignore @@ -0,0 +1 @@ +!en-US \ No newline at end of file diff --git a/crowdin/__translated__/en-US/docs b/crowdin/__translated__/en-US/docs new file mode 120000 index 00000000000..532209e8bf3 --- /dev/null +++ b/crowdin/__translated__/en-US/docs @@ -0,0 +1 @@ +../../../content/docs \ No newline at end of file From a0f5689d6c3a82eb69ddba776eb35764f175f5f1 Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Tue, 15 May 2018 07:41:13 -0700 Subject: [PATCH 19/37] Variable typo --- crowdin/download.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crowdin/download.js b/crowdin/download.js index e3c03a425e5..3ebd6dd4996 100644 --- a/crowdin/download.js +++ b/crowdin/download.js @@ -89,7 +89,7 @@ const createSymLink = (relativePath) => { if (err.code === 'EEXIST') { // eslint-disable-next-line no-console - console.info(`Symlink already exists for ${folder}`); + console.info(`Symlink already exists for ${to}`); } else { console.error(err); process.exit(1); From 3737457e8bf8af7feb3e097c33ec56d4e75c0c1c Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Tue, 15 May 2018 07:46:38 -0700 Subject: [PATCH 20/37] Made language code parsing more robust --- plugins/gatsby-plugin-crowdin/utils.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/gatsby-plugin-crowdin/utils.js b/plugins/gatsby-plugin-crowdin/utils.js index dc855bb481c..c9bd33047ab 100644 --- a/plugins/gatsby-plugin-crowdin/utils.js +++ b/plugins/gatsby-plugin-crowdin/utils.js @@ -1,9 +1,9 @@ 'use strict'; -// Parses language code (eg en, zh-CH) from a path (eg en/path/to/file.js) +// Parses language code (e.g. en, zh-CH, fil-PH) from a path (eg en/path/to/file.js) // Returns null if path doesn't contain a language code. exports.getLanguageCodeFromPath = path => { - const match = path.match(/^([a-z]{2}|[a-z]{2}-[A-Z]+)\//); + const match = path.match(/^([a-z]{2}|[a-z]{2,}-[A-Z]+)\//); return match ? match[1] : null; }; From 425af192c93cfde031167117994e567102b9f276 Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Tue, 15 May 2018 07:53:33 -0700 Subject: [PATCH 21/37] Updated crowdin/README.md --- crowdin/README.md | 81 ++++++++++++++++++++++++++++++++--------------- 1 file changed, 55 insertions(+), 26 deletions(-) diff --git a/crowdin/README.md b/crowdin/README.md index 65e0083c1e5..28be1ed2869 100644 --- a/crowdin/README.md +++ b/crowdin/README.md @@ -1,48 +1,77 @@ -## How does this work? +## How does it work? + +**Only content from [the `content/docs` directory](https://github.com/reactjs/reactjs.org/tree/master/content/docs) is localized. All other sections/pages remain English only.** ### Downloading content from Crowdin -This subdirectory contains some JavaScript files as well as a symlink for the default language (English) that points to [the `content` directory](https://github.com/reactjs/reactjs.org/tree/master/content): +This directory contains some JavaScript files as well as a symlink for the default language (English) that points to [the `content/docs` directory](https://github.com/reactjs/reactjs.org/tree/master/content/docs): ```sh . └── crowdin -   ├── config.js # Crowdin configuration settings -   ├── download.js # Node Download script -   └── __filtered__ -   └── en-US -> ../../content +   ├── __translated__ #----------------------- Initially empty, except for English + │   └── en-US + │   └── docs -> ../../../content/docs +   ├── __untranslated__ #--------------------- Contains symlinks to untranslated content + │   ├── blog -> ../../content/blog + │   └── # ... +   ├── config.js #---------------------------- Crowdin configuration settings +   └── download.js #-------------------------- Node Download script ``` -Translations can be downloaded locally with `yarn crowdin:download`. This uses the Crowdin API to download data into an `__exported__` subdirectory: +To retrieve translations using the Crowdin API, use the Yarn task `yarn crowdin:download`. This will download data into an `__exported__` subdirectory: ```sh . └── crowdin -   ├── config.js # Crowdin configuration settings -   ├── download.js # Node Download script -   ├── __filtered__ -   │ └── en-US -> ../../content -   └── __exported__ -       └── ... # Downloaded translations go here +   ├── __exported__ +   │   └── # Crowdin expoert goes here ... + ├── __translated__ +   │   └── # ... + ├── __untranslated__ +   │   └── # ... +   └── # ... ``` -Next the task identifies which languages have been translated past a certain threshold (specified by `crowdin/config.js`). For these languages, the script creates symlinks in the `__filtered__` subdirectory: +Next the task identifies which languages have been translated past a certain threshold (specified by `config.js`). For these languages, the script creates symlinks in the `__translated__` subdirectory: ```sh . └── crowdin -   ├── config.js # Crowdin configuration settings -   ├── download.js # Node Download script -   ├── __filtered__ -   │ ├── en-US -> ../../content -   │ ├── es-ES -> ../__exported__/.../es-ES -   │ ├── zh-CN -> ../__exported__/.../zh-CN -   │ └── ... # Other symlinks go here -   └── __exported__ -       └── ... # Downloaded translations go here + ├── __exported__ +   │   └── # ... + ├── __translated__ + │   ├── en-US + │   │   └── docs -> ../../../content/docs + │   ├── es-ES + │   │   └── docs -> ../../__exported__/path/to/docs/es-ES/docs + │   └── # Other languages that pass the threshold ... + ├── __untranslated__ +   │   └── # ... +   └── # ... ``` ### Gatsby integration -A (local) `gatsby-plugin-crowdin` plugin has been created that knows how to create localized links to certain sections of the website. **For now, only content from [the `content/docs` directory](https://github.com/reactjs/reactjs.org/tree/master/content/docs) is localized. All other sections/pages remain English only.** +A new (local) `gatsby-plugin-crowdin` plugin has been created that knows how to create localized links to certain sections of the website (e.g. things within the translated "/docs" directory). + +The `gatsby-source-filesystem` plugin has been configured to read all content from the `crowdin/__translated__/` and `crowdin/__untranslated__/` (symlinked) directories rather than `content`. This way it consumes translated content when available. (Crowdin provides default language fallbacks for pages/sections that have not yet been translated for any given locale.) + +This configuration is done via `gatsby-config.js`: +```js +{ + resolve: 'gatsby-source-filesystem', + options: { + name: 'untranslated', + path: `${__dirname}/crowdin/__untranslated__/`, + }, +}, +{ + resolve: 'gatsby-source-filesystem', + options: { + name: 'translated', + path: `${__dirname}/crowdin/__translated__/`, + }, +}, +``` -The `gatsby-source-filesystem` plugin has also been reconfigured to read all content from the `crowdin/__filtered__/*` (symlinked) directories rather than `content`. This way it consumes translated content when available. (Crowdin provides default language content for sections that have not yet been translated for any given locale.) +Because of the default initial symlink (`crowdin/__translated__/en-US/docs` -> `content/docs`) Gatsby will still serve English content when run locally, even if the Crowdin script has not been run. This should enable fast iteration and creation of new content. -Because of the default symlink (`crowdin/__filtered__/en-US` -> `content`) Gatsby will serve English content when run locally, even if the Crowdin script has not been run. This should enable fast iteration and creation of new content. \ No newline at end of file +Translations can be updated by running `yarn crowdin:download` (or automatically as part of CI deployment). \ No newline at end of file From 926500ec0c951fd3d7124fb85c831692f5d95e9d Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Tue, 15 May 2018 08:14:38 -0700 Subject: [PATCH 22/37] Fixed docs permalink redirects to be language-aware --- gatsby/createPages.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/gatsby/createPages.js b/gatsby/createPages.js index 3e12b34c64f..69b68342bee 100644 --- a/gatsby/createPages.js +++ b/gatsby/createPages.js @@ -130,12 +130,14 @@ module.exports = async (params) => { process.exit(1); } - redirectToSlugMap[localizedFromPath] = slug; + const localizedToPath = `/${languageCode}/${slug.startsWith('/') ? slug.substr(1) : slug}`; + + redirectToSlugMap[localizedFromPath] = localizedToPath; // Create language-aware redirect createRedirect({ fromPath: localizedFromPath, - toPath: slug, + toPath: localizedToPath, redirectInBrowser: true, Language: language, }); From b0bd040e8d0f961674df3bedf9281871455bbbe2 Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Tue, 15 May 2018 13:29:49 -0700 Subject: [PATCH 23/37] Added Node debug command to package.json for convenience --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 47cda16e301..fb74ac44ab4 100644 --- a/package.json +++ b/package.json @@ -80,6 +80,7 @@ "check-all": "npm-run-all prettier --parallel lint flow", "ci-check": "npm-run-all prettier:diff --parallel lint flow", "crowdin:download": "node ./crowdin/download", + "debug": "node --inspect-brk ./node_modules/.bin/gatsby develop -H 0.0.0.0", "dev": "yarn start", "flow": "flow", "format:source": "prettier --config .prettierrc --write \"{gatsby-*.js,{flow-typed,plugins,src}/**/*.js}\"", From 86f51260997731a6511c88cb2f01827789916068 Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Tue, 15 May 2018 13:34:08 -0700 Subject: [PATCH 24/37] Fixed redirects (localized and not) --- crowdin/config.js | 1 + gatsby/createPages.js | 52 ++++++++++++++++++++++++------------------- 2 files changed, 30 insertions(+), 23 deletions(-) diff --git a/crowdin/config.js b/crowdin/config.js index 23d03305cd6..47ffabba541 100644 --- a/crowdin/config.js +++ b/crowdin/config.js @@ -2,6 +2,7 @@ const path = require('path'); // Also relates to the crowdin.yaml file in the root directory module.exports = { + defaultLanguage: 'en', downloadedRootDirectory: path.join('test-17', 'docs'), key: process.env.CROWDIN_API_KEY, threshold: 50, diff --git a/gatsby/createPages.js b/gatsby/createPages.js index 69b68342bee..797f94eff5d 100644 --- a/gatsby/createPages.js +++ b/gatsby/createPages.js @@ -7,6 +7,7 @@ 'use strict'; const {resolve} = require('path'); +const {defaultLanguage} = require('../crowdin/config.js'); module.exports = async (params) => { const {graphql, boundActionCreators} = params; @@ -27,8 +28,6 @@ module.exports = async (params) => { toPath: '/', }); - // TODO Create localized root redirects for each language - const allMarkdown = await graphql( ` { @@ -87,25 +86,28 @@ module.exports = async (params) => { template = tutorialTemplate; } - const createArticlePage = path => { - // TODO This should be a utility exposed by gatsby-plugin-crowdin - if (languageCode != null && path.startsWith('/')) { - path = `/${languageCode}/${path.substr(1)}`; - } + const prependSlash = path => path.startsWith('/') ? path : `/${path}`; - createPage({ - path, - component: template, - context: { - id, - language, - languageCode, - }, - }); + // TODO This should be a utility exposed by gatsby-plugin-crowdin + const localizePath = path => { + path = prependSlash(path); + if (languageCode != null) { + return `/${languageCode}${path}`; + } else { + return path; + } }; // Register primary URL. - createArticlePage(slug); + createPage({ + path: localizePath(slug), + component: template, + context: { + id, + language, + languageCode, + }, + }); // Register redirects as well if the markdown specifies them. if (fields.redirect) { @@ -115,11 +117,7 @@ module.exports = async (params) => { } redirect.forEach(fromPath => { - if (fromPath.startsWith('/')) { - fromPath = fromPath.substr(1); - } - - const localizedFromPath = `/${languageCode}/${fromPath}`; + const localizedFromPath = localizePath(fromPath); if (redirectToSlugMap[localizedFromPath] != null) { console.error( @@ -130,10 +128,18 @@ module.exports = async (params) => { process.exit(1); } - const localizedToPath = `/${languageCode}/${slug.startsWith('/') ? slug.substr(1) : slug}`; + const localizedToPath = localizePath(slug); redirectToSlugMap[localizedFromPath] = localizedToPath; + if (language === defaultLanguage) { + createRedirect({ + fromPath: prependSlash(fromPath), + toPath: localizedToPath, + redirectInBrowser: true, + }); + } + // Create language-aware redirect createRedirect({ fromPath: localizedFromPath, From 2c9d7df11c7c576b459adffd464f666a26b8f262 Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Tue, 15 May 2018 13:53:31 -0700 Subject: [PATCH 25/37] Default redirect pages for English only --- plugins/gatsby-plugin-crowdin/onCreatePage.js | 31 ++++++++++--------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/plugins/gatsby-plugin-crowdin/onCreatePage.js b/plugins/gatsby-plugin-crowdin/onCreatePage.js index ddd955bec58..27dd84682ca 100644 --- a/plugins/gatsby-plugin-crowdin/onCreatePage.js +++ b/plugins/gatsby-plugin-crowdin/onCreatePage.js @@ -67,6 +67,9 @@ } */ +// TODO THis is a hack :( Read this from pluginOptions? +const DEFAULT_LANGUAGE_CODE = 'en-US'; + // Gatsby has 2 types of pages: // (1) Pages generated from Markdown content (src/templates) // (2) Pages defined in JavaScript (src/pages) @@ -84,19 +87,19 @@ module.exports = ({page, boundActionCreators}, pluginOptions) => { path, // eg /zn-CH/path/to/template.html, /path/to/page.html } = page; - // TODO: Only do this for `gatsby-source-filesystem` name=translated sources? - if (!languageCode) { - return; // No-op for JavaScript pages (eg src/pages) - } - - const nonLocalizedPath = path.substr( - path.indexOf(languageCode) + languageCode.length, - ); + // If we're creating a localized page, + // Create a non-localized redriect for the default language. + // TODO Maybe make this behavior configurable as well via pluginOptions + if (languageCode === DEFAULT_LANGUAGE_CODE) { + const nonLocalizedPath = path.substr( + path.indexOf(languageCode) + languageCode.length, + ); - createRedirect({ - fromPath: nonLocalizedPath, - toPath: path, - redirectInBrowser: true, - Language: language, - }); + createRedirect({ + fromPath: nonLocalizedPath, + toPath: path, + redirectInBrowser: true, + Language: language, + }); + } }; From 8e77586e3319f508dfadcaeb264e54f89e1e88ce Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Tue, 15 May 2018 16:34:02 -0700 Subject: [PATCH 26/37] Initial idea for gather language data for locale switcher UI --- crowdin/.gitignore | 3 ++- crowdin/update-languages.js | 20 ++++++++++++++++++++ package.json | 3 +++ src/components/LayoutHeader/Header.js | 3 +++ 4 files changed, 28 insertions(+), 1 deletion(-) create mode 100644 crowdin/update-languages.js diff --git a/crowdin/.gitignore b/crowdin/.gitignore index fa3f0ea49bd..017422d75ad 100644 --- a/crowdin/.gitignore +++ b/crowdin/.gitignore @@ -1,2 +1,3 @@ __exported__/ -__translated__/ \ No newline at end of file +__translated__/ +languages.json \ No newline at end of file diff --git a/crowdin/update-languages.js b/crowdin/update-languages.js new file mode 100644 index 00000000000..5a362e8b3bf --- /dev/null +++ b/crowdin/update-languages.js @@ -0,0 +1,20 @@ +const fs = require('fs'); +const path = require('path'); + +const TRANSLATED_LANGUAGES_JSON_PATH = path.resolve(__dirname, 'languages.json'); +const TRANSLATED_PATH = path.resolve(__dirname, '__translated__'); + +// Determine which languages we have translations downloaded for... +const languages = []; +fs.readdirSync(TRANSLATED_PATH).forEach(entry => { + if (fs.statSync(path.join(TRANSLATED_PATH, entry)).isDirectory()) { + languages.push(entry); + } +}); + +// Update the languages JSON config file. +// This file is used to display the localization toggle UI. +fs.writeFileSync( + TRANSLATED_LANGUAGES_JSON_PATH, + JSON.stringify(languages), +); diff --git a/package.json b/package.json index fb74ac44ab4..0114c79e69d 100644 --- a/package.json +++ b/package.json @@ -80,6 +80,7 @@ "check-all": "npm-run-all prettier --parallel lint flow", "ci-check": "npm-run-all prettier:diff --parallel lint flow", "crowdin:download": "node ./crowdin/download", + "crowdin:update-languages": "node ./crowdin/update-languages", "debug": "node --inspect-brk ./node_modules/.bin/gatsby develop -H 0.0.0.0", "dev": "yarn start", "flow": "flow", @@ -89,6 +90,8 @@ "netlify": "yarn install && yarn crowdin:download && yarn build", "nit:source": "prettier --config .prettierrc --list-different \"{gatsby-*.js,{flow-typed,plugins,src}/**/*.js}\"", "nit:examples": "prettier --config .prettierrc.examples --list-different \"examples/**/*.js\"", + "prebuild": "yarn crowdin:update-languages", + "prestart": "yarn crowdin:update-languages", "prettier": "yarn format:source && yarn format:examples", "prettier:diff": "yarn nit:source && yarn nit:examples", "reset": "yarn reset:cache && yarn reset:translations", diff --git a/src/components/LayoutHeader/Header.js b/src/components/LayoutHeader/Header.js index 35e6c55cf0c..f546237d261 100644 --- a/src/components/LayoutHeader/Header.js +++ b/src/components/LayoutHeader/Header.js @@ -15,6 +15,9 @@ import ExternalLinkSvg from 'templates/components/ExternalLinkSvg'; import DocSearch from './DocSearch'; import logoSvg from 'icons/logo.svg'; +import languages from '../../../crowdin/languages.json'; + +// TODO Use languages array to power locale drop-down const Header = ({location}: {location: Location}) => (
    Date: Wed, 16 May 2018 09:52:28 -0700 Subject: [PATCH 27/37] Added stub translations page --- src/components/LayoutHeader/Header.js | 32 ++++++++++++++--- src/pages/translations.js | 51 +++++++++++++++++++++++++++ 2 files changed, 79 insertions(+), 4 deletions(-) create mode 100644 src/pages/translations.js diff --git a/src/components/LayoutHeader/Header.js b/src/components/LayoutHeader/Header.js index f546237d261..d43e99cb044 100644 --- a/src/components/LayoutHeader/Header.js +++ b/src/components/LayoutHeader/Header.js @@ -15,9 +15,6 @@ import ExternalLinkSvg from 'templates/components/ExternalLinkSvg'; import DocSearch from './DocSearch'; import logoSvg from 'icons/logo.svg'; -import languages from '../../../crowdin/languages.json'; - -// TODO Use languages array to power locale drop-down const Header = ({location}: {location: Location}) => (
    (
    ( ( href="/versions"> v{version} + + + + + + ( + + + +); + +export default Translations; From 4086ea8e2caa7c2697b855fb039684dad4ecf676 Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Wed, 16 May 2018 09:57:04 -0700 Subject: [PATCH 28/37] Prettier and Flow fix --- src/components/LayoutHeader/Header.js | 5 ++++- src/pages/translations.js | 15 +++++++++------ src/types.js | 1 + 3 files changed, 14 insertions(+), 7 deletions(-) diff --git a/src/components/LayoutHeader/Header.js b/src/components/LayoutHeader/Header.js index d43e99cb044..ce00f15dd3c 100644 --- a/src/components/LayoutHeader/Header.js +++ b/src/components/LayoutHeader/Header.js @@ -189,7 +189,10 @@ const Header = ({location}: {location: Location}) => ( href="/translations"> - + (
    diff --git a/src/types.js b/src/types.js index 782a5b6f6bd..4f70677941f 100644 --- a/src/types.js +++ b/src/types.js @@ -14,6 +14,7 @@ export type Node = { excerpt: string, fields: { date?: string, + id: string, path: string, redirect: string, slug: string, From 2cf08de0cb957d09f921c5ac6bc0724cca90d8c4 Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Wed, 16 May 2018 10:04:35 -0700 Subject: [PATCH 29/37] Updated ESLint ignore file --- .eslintignore | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.eslintignore b/.eslintignore index fde4479aa63..1cc7ab1e189 100644 --- a/.eslintignore +++ b/.eslintignore @@ -2,7 +2,9 @@ node_modules/* # Ignore markdown files and examples content/* -crowdin/translations/* +crowdin/__exported__/* +crowdin/__translated__/* +crowdin/__untranslated__/* # Ignore built files public/* From 3e0073cdc96ae6d82d3f88ac86f97d76f87aa631 Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Wed, 16 May 2018 10:13:18 -0700 Subject: [PATCH 30/37] Tidied up reqiure statements for Crowdin scripts --- crowdin/download.js | 24 ++++++++++++------------ crowdin/update-languages.js | 14 +++++++------- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/crowdin/download.js b/crowdin/download.js index 3ebd6dd4996..f10e9565084 100644 --- a/crowdin/download.js +++ b/crowdin/download.js @@ -1,14 +1,14 @@ const Crowdin = require('crowdin-node'); const {downloadedRootDirectory, key, threshold, url, whitelist} = require('./config'); -const fs = require('fs'); -const path = require('path'); +const {existsSync, mkdirSync} = require('fs'); +const {join, resolve} = require('path'); const {symlink, lstatSync, readdirSync} = require('fs'); -const TRANSLATED_PATH = path.resolve(__dirname, '__translated__'); -const EXPORTED_PATH = path.resolve(__dirname, '__exported__'); +const TRANSLATED_PATH = resolve(__dirname, '__translated__'); +const EXPORTED_PATH = resolve(__dirname, '__exported__'); // Path to the "docs" folder within the downloaded Crowdin translations bundle. -const downloadedDocsPath = path.resolve( +const downloadedDocsPath = resolve( EXPORTED_PATH, downloadedRootDirectory, ); @@ -56,16 +56,16 @@ const downloadAndSymlink = () => { usableLocales.forEach(locale => { const languageCode = localeToFolderMap.get(locale); - const rootLanguageFolder = path.resolve(TRANSLATED_PATH, languageCode); + const rootLanguageFolder = resolve(TRANSLATED_PATH, languageCode); if (Array.isArray(whitelist)) { - if (!fs.existsSync(rootLanguageFolder)) { - fs.mkdirSync(rootLanguageFolder); + if (!existsSync(rootLanguageFolder)) { + mkdirSync(rootLanguageFolder); } // Symlink only the whitelisted subdirectories whitelist.forEach(subdirectory => { - createSymLink(path.join(languageCode, subdirectory)); + createSymLink(join(languageCode, subdirectory)); }); } else { // Otherwise symlink the entire language export @@ -80,8 +80,8 @@ const downloadAndSymlink = () => { // Note that the current working directory of this node process should be where the symlink is created // or else the relative paths would be incorrect const createSymLink = (relativePath) => { - const from = path.resolve(downloadedDocsPath, relativePath); - const to = path.resolve(TRANSLATED_PATH, relativePath); + const from = resolve(downloadedDocsPath, relativePath); + const to = resolve(TRANSLATED_PATH, relativePath); symlink(from, to, err => { if (!err) { return; @@ -133,7 +133,7 @@ const createLocaleToFolderMap = (directories) => { const getLanguageDirectories = source => readdirSync(source).filter( name => - lstatSync(path.join(source, name)).isDirectory() && name !== '_data', + lstatSync(join(source, name)).isDirectory() && name !== '_data', ); validateCrowdinConfig(); diff --git a/crowdin/update-languages.js b/crowdin/update-languages.js index 5a362e8b3bf..c867b86007b 100644 --- a/crowdin/update-languages.js +++ b/crowdin/update-languages.js @@ -1,20 +1,20 @@ -const fs = require('fs'); -const path = require('path'); +const {readdirSync, statSync, writeFileSync} = require('fs'); +const {join, resolve} = require('path'); -const TRANSLATED_LANGUAGES_JSON_PATH = path.resolve(__dirname, 'languages.json'); -const TRANSLATED_PATH = path.resolve(__dirname, '__translated__'); +const TRANSLATED_LANGUAGES_JSON_PATH = resolve(__dirname, 'languages.json'); +const TRANSLATED_PATH = resolve(__dirname, '__translated__'); // Determine which languages we have translations downloaded for... const languages = []; -fs.readdirSync(TRANSLATED_PATH).forEach(entry => { - if (fs.statSync(path.join(TRANSLATED_PATH, entry)).isDirectory()) { +readdirSync(TRANSLATED_PATH).forEach(entry => { + if (statSync(join(TRANSLATED_PATH, entry)).isDirectory()) { languages.push(entry); } }); // Update the languages JSON config file. // This file is used to display the localization toggle UI. -fs.writeFileSync( +writeFileSync( TRANSLATED_LANGUAGES_JSON_PATH, JSON.stringify(languages), ); From 753d2c73f5fb64c2cc725cf9e73775f29c4a79f5 Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Wed, 16 May 2018 10:54:14 -0700 Subject: [PATCH 31/37] Fixed broken error decoder route --- gatsby/createPages.js | 2 +- gatsby/onCreatePage.js | 6 +++--- src/components/ErrorDecoder/ErrorDecoder.js | 12 +++++++----- src/pages/docs/error-decoder.html.js | 4 ++-- 4 files changed, 13 insertions(+), 11 deletions(-) diff --git a/gatsby/createPages.js b/gatsby/createPages.js index 797f94eff5d..b5d938c17ac 100644 --- a/gatsby/createPages.js +++ b/gatsby/createPages.js @@ -59,7 +59,7 @@ module.exports = async (params) => { const {fields} = edge.node; let {id, language, languageCode, slug} = fields; - if (slug === 'docs/error-decoder.html') { + if (slug === '/docs/error-decoder.html') { // No-op so far as markdown templates go. // Error codes are managed by a page in src/pages // (which gets created by Gatsby during a separate phase). diff --git a/gatsby/onCreatePage.js b/gatsby/onCreatePage.js index a6da8a80fe1..9a85f9b9ec3 100644 --- a/gatsby/onCreatePage.js +++ b/gatsby/onCreatePage.js @@ -12,9 +12,9 @@ module.exports = async ({page, boundActionCreators}) => { return new Promise(resolvePromise => { // page.matchPath is a special key that's used for matching pages only on the client. // Explicitly wire up all error code wildcard matches to redirect to the error code page. - if (page.path.includes('docs/error-decoder.html')) { - page.matchPath = 'docs/error-decoder:path?'; - page.context.slug = 'docs/error-decoder.html'; + if (page.path.includes('/docs/error-decoder.html')) { + page.matchPath = '/docs/error-decoder:path?'; + page.context.slug = '/docs/error-decoder.html'; createPage(page); } diff --git a/src/components/ErrorDecoder/ErrorDecoder.js b/src/components/ErrorDecoder/ErrorDecoder.js index e25531bce59..7dac99a8331 100644 --- a/src/components/ErrorDecoder/ErrorDecoder.js +++ b/src/components/ErrorDecoder/ErrorDecoder.js @@ -39,9 +39,9 @@ function urlify(str: string): Node { // `?invariant=123&args[]=foo&args[]=bar` // or `// ?invariant=123&args[0]=foo&args[1]=bar` function parseQueryString( - search: string, + search: ?string, ): ?{|code: string, args: Array|} { - const rawQueryString = search.substring(1); + const rawQueryString = search ? search.substring(1) : null; if (!rawQueryString) { return null; } @@ -89,13 +89,15 @@ function ErrorResult(props: {|code: ?string, msg: string|}) { function ErrorDecoder(props: {| errorCodesString: string, - location: {search: string}, + location: {search: ?string}, |}) { let code = null; let msg = ''; - const errorCodes = JSON.parse(props.errorCodesString); - const parseResult = parseQueryString(props.location.search); + const {errorCodesString, location} = props; + + const errorCodes = JSON.parse(errorCodesString); + const parseResult = parseQueryString(location.search); if (parseResult != null) { code = parseResult.code; msg = replaceArgs(errorCodes[code], parseResult.args); diff --git a/src/pages/docs/error-decoder.html.js b/src/pages/docs/error-decoder.html.js index 4f97caf3008..d29dec68acf 100644 --- a/src/pages/docs/error-decoder.html.js +++ b/src/pages/docs/error-decoder.html.js @@ -100,8 +100,8 @@ const ErrorPage = ({data, location}: Props) => ( // eslint-disable-next-line no-undef export const pageQuery = graphql` - query ErrorPageMarkdown($slug: String!) { - markdownRemark(fields: {slug: {eq: $slug}}) { + query ErrorPageMarkdown { + markdownRemark(fields: {slug: {eq: "/docs/error-decoder.html"}}) { html fields { path From d5211dc87723d471b198ec35ac6370bb4366e1ce Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Wed, 16 May 2018 11:34:14 -0700 Subject: [PATCH 32/37] Updated Crowdin README --- crowdin/README.md | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/crowdin/README.md b/crowdin/README.md index 28be1ed2869..13a4638f058 100644 --- a/crowdin/README.md +++ b/crowdin/README.md @@ -74,4 +74,23 @@ This configuration is done via `gatsby-config.js`: Because of the default initial symlink (`crowdin/__translated__/en-US/docs` -> `content/docs`) Gatsby will still serve English content when run locally, even if the Crowdin script has not been run. This should enable fast iteration and creation of new content. -Translations can be updated by running `yarn crowdin:download` (or automatically as part of CI deployment). \ No newline at end of file +Translations can be updated by running `yarn crowdin:download` (or automatically as part of CI deployment). + +### Language selector + +The Yarn task `crowdin:update-languages` determines which translated languages have been downloaded. (This task is automatically run before `yarn dev` or `yarn build` in order to just-in-time update the list.) The task writes a list of locales to a local JSON file, `languages.json`: + +```sh +. +└── crowdin +   ├── __exported__ +   │   └── # ... + ├── __translated__ +   │   └── # ... + ├── __untranslated__ +   │   └── # ... + ├── translated-languages.json # This is the list of local translations +   └── # ... +``` + +This `languages.json` list is imported into a translations page (`pages/translations.js`) and used to create a list of links to translated docs. \ No newline at end of file From dec99ee20dfb0e0e5d105203cfb02cee44abdf6a Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Wed, 16 May 2018 13:32:22 -0700 Subject: [PATCH 33/37] JIT choose default docs language and persist user selections with localStorage --- plugins/gatsby-plugin-crowdin/onCreatePage.js | 2 +- src/pages/docs-language-redirect.js | 23 ++++++++++ src/pages/translations.js | 6 +-- src/templates/docs.js | 28 +++++++---- src/utils/createLink.js | 7 +-- src/utils/languageUtils.js | 46 +++++++++++++++++++ 6 files changed, 92 insertions(+), 20 deletions(-) create mode 100644 src/pages/docs-language-redirect.js create mode 100644 src/utils/languageUtils.js diff --git a/plugins/gatsby-plugin-crowdin/onCreatePage.js b/plugins/gatsby-plugin-crowdin/onCreatePage.js index 27dd84682ca..18e219cb91b 100644 --- a/plugins/gatsby-plugin-crowdin/onCreatePage.js +++ b/plugins/gatsby-plugin-crowdin/onCreatePage.js @@ -97,7 +97,7 @@ module.exports = ({page, boundActionCreators}, pluginOptions) => { createRedirect({ fromPath: nonLocalizedPath, - toPath: path, + toPath: `/docs-language-redirect/?${nonLocalizedPath}`, redirectInBrowser: true, Language: language, }); diff --git a/src/pages/docs-language-redirect.js b/src/pages/docs-language-redirect.js new file mode 100644 index 00000000000..f3d4326d708 --- /dev/null +++ b/src/pages/docs-language-redirect.js @@ -0,0 +1,23 @@ +/** + * Copyright (c) 2013-present, Facebook, Inc. + * + * @emails react-core + */ + +import React from 'react'; +import PropTypes from 'prop-types'; +import {Redirect} from 'react-router-dom'; +import {getSelectedLanguage} from 'utils/languageUtils'; + +const DocsRedirect = ({location}) => { + // Redirect the user to their most recent locale, or English as a fallback. + const language = getSelectedLanguage(); + + return ; +}; + +DocsRedirect.propTypes = { + location: PropTypes.object.isRequired, +}; + +export default DocsRedirect; diff --git a/src/pages/translations.js b/src/pages/translations.js index 6c7f121ae93..451734c61bf 100644 --- a/src/pages/translations.js +++ b/src/pages/translations.js @@ -11,9 +11,7 @@ import TitleAndMetaTags from 'components/TitleAndMetaTags'; import Link from 'gatsby-link'; import React from 'react'; import {sharedStyles} from 'theme'; - -// $FlowFixMe This is a valid path -import languages from '../../crowdin/languages.json'; +import {getTranslatedLanguages} from 'utils/languageUtils'; const Translations = () => ( @@ -27,7 +25,7 @@ const Translations = () => ( languages:

      - {languages.map(language => ( + {getTranslatedLanguages().map(language => (
    • {language} diff --git a/src/templates/docs.js b/src/templates/docs.js index 568b4e716d3..fc805a30e28 100644 --- a/src/templates/docs.js +++ b/src/templates/docs.js @@ -9,16 +9,26 @@ import PropTypes from 'prop-types'; import React from 'react'; import {createLinkDocs} from 'utils/createLink'; import {sectionListDocs} from 'utils/sectionList'; +import {setSelectedLanguage} from 'utils/languageUtils'; -const Docs = ({data, location}) => ( - -); +const Docs = ({data, location}) => { + // Store the user's most recent locale based on the current URL. + // We'll restore this language when they visit a new (unlocalized) URL. + const matches = location.pathname.substr(1).split('/'); + if (matches.length > 1) { + setSelectedLanguage(matches[0]); + } + + return ( + + ); +}; Docs.propTypes = { data: PropTypes.object.isRequired, diff --git a/src/utils/createLink.js b/src/utils/createLink.js index df69e0c5ab7..e15d7689998 100644 --- a/src/utils/createLink.js +++ b/src/utils/createLink.js @@ -24,7 +24,6 @@ type CreateLinkBaseProps = { const createLinkBlog = ({ isActive, - isLocalized, item, location, section, @@ -32,7 +31,6 @@ const createLinkBlog = ({ return ( {isActive && } @@ -43,7 +41,6 @@ const createLinkBlog = ({ const createLinkCommunity = ({ isActive, - isLocalized, item, location, section, @@ -65,7 +62,7 @@ const createLinkCommunity = ({ } return createLinkDocs({ isActive, - isLocalized, + isLocalized: false, item, location, section, @@ -97,7 +94,6 @@ type CreateLinkTutorialProps = { const createLinkTutorial = ({ isActive, - isLocalized, item, location, onLinkClick, @@ -106,7 +102,6 @@ const createLinkTutorial = ({ return ( diff --git a/src/utils/languageUtils.js b/src/utils/languageUtils.js new file mode 100644 index 00000000000..fbfac00173b --- /dev/null +++ b/src/utils/languageUtils.js @@ -0,0 +1,46 @@ +/** + * Copyright (c) 2013-present, Facebook, Inc. + * + * @emails react-core + * @flow + */ + +// $FlowFixMe This is a valid path +import languagesArray from '../../crowdin/languages.json'; + +const DEFAULT_LANGUAGE = 'en-US'; + +const languagesMap = languagesArray.reduce((map: Object, language: string) => { + map[language] = true; + return map; +}, Object.create(null)); + +export function getTranslatedLanguages(): Array { + return languagesArray; +} + +export function getSelectedLanguage(): string { + let language = localStorage.getItem('selectedLanguage'); + if (languagesMap[language]) { + return ((language: any): string); + } else { + const {languages} = navigator; + for (let i = 0; i < languages.length; i++) { + language = languages[i]; + if (languagesMap[language]) { + return language; + } + } + } + return DEFAULT_LANGUAGE; +} + +export function setSelectedLanguage(language: string): void { + if (languagesMap[language]) { + localStorage.setItem('selectedLanguage', language); + } else if (process.env.NODE_ENV !== 'production') { + console.warn( + `Specified language "${language}" is not a valid translation.`, + ); + } +} From e7595524c0e1c8813edfe5b0b841341530cfbd93 Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Wed, 16 May 2018 13:35:21 -0700 Subject: [PATCH 34/37] Crowdin README update --- crowdin/README.md | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/crowdin/README.md b/crowdin/README.md index 13a4638f058..715a572dbca 100644 --- a/crowdin/README.md +++ b/crowdin/README.md @@ -93,4 +93,13 @@ The Yarn task `crowdin:update-languages` determines which translated languages h    └── # ... ``` -This `languages.json` list is imported into a translations page (`pages/translations.js`) and used to create a list of links to translated docs. \ No newline at end of file +This `languages.json` list is imported into a translations page (`pages/translations.js`) and used to create a list of links to translated docs. + +### Locale persistence + +By default, legacy links to docs pages (e.g. `/docs/hello-world.html`) are re-routed to a new page (`docs-language-redirect.js`) that determines which locale to redirect to (e.g. `/en-US/docs/hello-world.html`). This is done as follows: +* First it checks `localStorage` for the user's selected language. If one is found, it is used. +* Next it checks the user's preferred languages (using `navigator.languages`). If any have been translated, it is used. +* Lastly it falls back to English. + +Each time a user visits a localized docs path, the website updates their currently selected language (in `localStorage`) so that subsequent visits (within this session or a new session) will restore their selected language. \ No newline at end of file From 8fb1b0d7961efbae8921543462d189068d5a0627 Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Wed, 16 May 2018 14:15:03 -0700 Subject: [PATCH 35/37] Added versions and translations links to the footer (for mobile) --- src/components/LayoutFooter/Footer.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/components/LayoutFooter/Footer.js b/src/components/LayoutFooter/Footer.js index 0b9402c8ef0..c20f1d650db 100644 --- a/src/components/LayoutFooter/Footer.js +++ b/src/components/LayoutFooter/Footer.js @@ -134,6 +134,8 @@ const Footer = ({layoutHasSidebar = false}: {layoutHasSidebar: boolean}) => ( rel="noopener"> React Native + Versions + Translations
    Date: Wed, 16 May 2018 14:30:03 -0700 Subject: [PATCH 36/37] Fixed sidebar detection hack that broke with localized routes --- src/layouts/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/layouts/index.js b/src/layouts/index.js index 4da990bdbd3..e28ffa051b4 100644 --- a/src/layouts/index.js +++ b/src/layouts/index.js @@ -35,7 +35,7 @@ class Template extends Component { let layoutHasSidebar = false; if ( location.pathname.match( - /^\/(docs|tutorial|community|blog|contributing|warnings)/, + /\/(docs|tutorial|community|blog|contributing|warnings)\//, ) ) { layoutHasSidebar = true; From a8ad83e799fa41628fa2e622cd2b98ef6a8338cb Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Mon, 21 May 2018 08:51:39 -0700 Subject: [PATCH 37/37] Added TODO --- crowdin/update-languages.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/crowdin/update-languages.js b/crowdin/update-languages.js index c867b86007b..53ebda1cc6c 100644 --- a/crowdin/update-languages.js +++ b/crowdin/update-languages.js @@ -4,6 +4,8 @@ const {join, resolve} = require('path'); const TRANSLATED_LANGUAGES_JSON_PATH = resolve(__dirname, 'languages.json'); const TRANSLATED_PATH = resolve(__dirname, '__translated__'); +// TODO Use crowdin.yaml + // Determine which languages we have translations downloaded for... const languages = []; readdirSync(TRANSLATED_PATH).forEach(entry => {