diff --git a/packages/docusaurus-module-type-aliases/src/index.d.ts b/packages/docusaurus-module-type-aliases/src/index.d.ts index a40b0cdd7f..96203f53b8 100644 --- a/packages/docusaurus-module-type-aliases/src/index.d.ts +++ b/packages/docusaurus-module-type-aliases/src/index.d.ts @@ -52,7 +52,7 @@ declare module '@generated/i18n' { defaultLocale: string; locales: [string, ...string[]]; currentLocale: string; - localeConfigs: Record; + localeConfigs: Record; }; export default i18n; } diff --git a/packages/docusaurus-theme-classic/package.json b/packages/docusaurus-theme-classic/package.json index a2ff0171df..692047c131 100644 --- a/packages/docusaurus-theme-classic/package.json +++ b/packages/docusaurus-theme-classic/package.json @@ -34,15 +34,17 @@ "@types/react-toggle": "^4.0.2", "clsx": "^1.1.1", "copy-text-to-clipboard": "^2.2.0", - "infima": "0.2.0-alpha.18", + "infima": "0.2.0-alpha.19", "joi": "^17.2.1", "lodash": "^4.17.19", "parse-numeric-range": "^1.2.0", + "postcss": "^7.0.2", "prism-react-renderer": "^1.1.1", "prismjs": "^1.23.0", "prop-types": "^15.7.2", "react-router-dom": "^5.2.0", - "react-toggle": "^4.1.1" + "react-toggle": "^4.1.1", + "rtlcss": "^2.6.2" }, "devDependencies": { "@docusaurus/module-type-aliases": "2.0.0-alpha.70" diff --git a/packages/docusaurus-theme-classic/src/index.ts b/packages/docusaurus-theme-classic/src/index.ts index 6129cff1d7..961582d48d 100644 --- a/packages/docusaurus-theme-classic/src/index.ts +++ b/packages/docusaurus-theme-classic/src/index.ts @@ -9,6 +9,8 @@ import {Plugin} from '@docusaurus/types'; import {getTranslationFiles, translateThemeConfig} from './translations'; import path from 'path'; import Module from 'module'; +import postcss from 'postcss'; +import rtlcss from 'rtlcss'; const createRequire = Module.createRequire || Module.createRequireFromPath; const requireFromDocusaurusCore = createRequire( @@ -59,15 +61,23 @@ const noFlashColorMode = ({defaultMode, respectPrefersColorScheme}) => { })();`; }; +function getInfimaCSSFile(direction) { + return `infima/dist/css/default/default${ + direction === 'rtl' ? '-rtl' : '' + }.css`; +} + export default function docusaurusThemeClassic( context, options, ): Plugin { const { siteConfig: {themeConfig}, + i18n: {currentLocale, localeConfigs}, } = context; const {colorMode, prism: {additionalLanguages = []} = {}} = themeConfig || {}; const {customCss} = options || {}; + const {direction} = localeConfigs[currentLocale]; return { name: 'docusaurus-theme-classic', @@ -95,7 +105,7 @@ export default function docusaurusThemeClassic( getClientModules() { const modules = [ - require.resolve('infima/dist/css/default/default.css'), + require.resolve(getInfimaCSSFile(direction)), path.resolve(__dirname, './prism-include-languages'), ]; @@ -135,6 +145,37 @@ export default function docusaurusThemeClassic( }; }, + configurePostCss(postCssOptions) { + if (direction === 'rtl') { + postCssOptions.plugins.push( + postcss.plugin('RtlCssPlugin', () => { + function isInfimaCSSFile(file) { + return ( + file.endsWith(getInfimaCSSFile(direction)) || + // special case for our own monorepo using symlinks! + file.endsWith( + 'infima/packages/core/dist/css/default/default-rtl.css', + ) + ); + } + + return function (root: any) { + const file = root?.source.input.file; + + // Skip Infima as we are using the its RTL version. + if (isInfimaCSSFile(file)) { + return; + } + + rtlcss.process(root); + }; + }), + ); + } + + return postCssOptions; + }, + injectHtmlTags() { return { preBodyTags: [ diff --git a/packages/docusaurus-theme-classic/src/theme/DocPage/index.tsx b/packages/docusaurus-theme-classic/src/theme/DocPage/index.tsx index bff6aaaf7c..decf3c4ca3 100644 --- a/packages/docusaurus-theme-classic/src/theme/DocPage/index.tsx +++ b/packages/docusaurus-theme-classic/src/theme/DocPage/index.tsx @@ -99,7 +99,10 @@ function DocPageContent({ role="button" onKeyDown={toggleSidebar} onClick={toggleSidebar}> - + )} diff --git a/packages/docusaurus-theme-classic/src/theme/DocPage/styles.module.css b/packages/docusaurus-theme-classic/src/theme/DocPage/styles.module.css index 27237b5825..11b72a8a85 100644 --- a/packages/docusaurus-theme-classic/src/theme/DocPage/styles.module.css +++ b/packages/docusaurus-theme-classic/src/theme/DocPage/styles.module.css @@ -44,6 +44,13 @@ background-color: var(--ifm-color-emphasis-200); } + .expandSidebarButtonIcon { + transform: rotate(0); + } + html[dir='rtl'] .expandSidebarButtonIcon { + transform: rotate(180deg); + } + html[data-theme='dark'] .collapsedDocSidebar:hover, html[data-theme='dark'] .collapsedDocSidebar:focus { background-color: var(--collapse-button-bg-color-dark); diff --git a/packages/docusaurus-theme-classic/src/theme/DocSidebar/styles.module.css b/packages/docusaurus-theme-classic/src/theme/DocSidebar/styles.module.css index 95f72110c3..721e212c22 100644 --- a/packages/docusaurus-theme-classic/src/theme/DocSidebar/styles.module.css +++ b/packages/docusaurus-theme-classic/src/theme/DocSidebar/styles.module.css @@ -78,6 +78,9 @@ transform: rotate(180deg); margin-top: 4px; } + html[dir='rtl'] .collapseSidebarButtonIcon { + transform: rotate(0); + } html[data-theme='dark'] .collapseSidebarButton { background-color: var(--collapse-button-bg-color-dark); diff --git a/packages/docusaurus-theme-classic/src/theme/LayoutHead/index.tsx b/packages/docusaurus-theme-classic/src/theme/LayoutHead/index.tsx index 44f934ed63..62a1ffe776 100644 --- a/packages/docusaurus-theme-classic/src/theme/LayoutHead/index.tsx +++ b/packages/docusaurus-theme-classic/src/theme/LayoutHead/index.tsx @@ -83,7 +83,7 @@ function CanonicalUrlHeaders({permalink}: {permalink?: string}) { export default function LayoutHead(props: Props): JSX.Element { const { siteConfig, - i18n: {currentLocale}, + i18n: {currentLocale, localeConfigs}, } = useDocusaurusContext(); const { favicon, @@ -98,11 +98,12 @@ export default function LayoutHead(props: Props): JSX.Element { // See https://github.com/facebook/docusaurus/issues/3317#issuecomment-754661855 // const htmlLang = currentLocale.split('-')[0]; const htmlLang = currentLocale; // should we allow the user to override htmlLang with localeConfig? + const htmlDir = localeConfigs[currentLocale].direction; return ( <> - + {metaTitle && {metaTitle}} {metaTitle && } {favicon && } diff --git a/packages/docusaurus-types/src/index.d.ts b/packages/docusaurus-types/src/index.d.ts index 4602f2e999..0d784c7d0c 100644 --- a/packages/docusaurus-types/src/index.d.ts +++ b/packages/docusaurus-types/src/index.d.ts @@ -94,6 +94,7 @@ export type TranslationFiles = TranslationFile[]; export type I18nLocaleConfig = { label: string; + direction: string; }; export type I18nConfig = { diff --git a/packages/docusaurus/src/server/__tests__/i18n.test.ts b/packages/docusaurus/src/server/__tests__/i18n.test.ts index de094634ec..7cf62035b3 100644 --- a/packages/docusaurus/src/server/__tests__/i18n.test.ts +++ b/packages/docusaurus/src/server/__tests__/i18n.test.ts @@ -93,7 +93,7 @@ describe('loadI18n', () => { locales: ['en', 'fr', 'de'], currentLocale: 'de', localeConfigs: { - fr: {label: 'Français'}, + fr: {label: 'Français', direction: 'ltr'}, en: defaultLocaleConfig('en'), de: defaultLocaleConfig('de'), }, diff --git a/packages/docusaurus/src/server/configValidation.ts b/packages/docusaurus/src/server/configValidation.ts index 3ad949da0a..47bd3c83f5 100644 --- a/packages/docusaurus/src/server/configValidation.ts +++ b/packages/docusaurus/src/server/configValidation.ts @@ -71,6 +71,7 @@ const PresetSchema = Joi.alternatives().try( const LocaleConfigSchema = Joi.object({ label: Joi.string(), + direction: Joi.string().equal('ltr', 'rtl').default('ltr'), }); const I18N_CONFIG_SCHEMA = Joi.object({ diff --git a/packages/docusaurus/src/server/i18n.ts b/packages/docusaurus/src/server/i18n.ts index 2ee6bf1740..bec9250191 100644 --- a/packages/docusaurus/src/server/i18n.ts +++ b/packages/docusaurus/src/server/i18n.ts @@ -11,6 +11,7 @@ import {normalizeUrl} from '@docusaurus/utils'; export function defaultLocaleConfig(locale: string): I18nLocaleConfig { return { label: locale, + direction: 'ltr', }; } diff --git a/packages/docusaurus/src/webpack/utils.ts b/packages/docusaurus/src/webpack/utils.ts index be9e48a268..fe5bfdce3b 100644 --- a/packages/docusaurus/src/webpack/utils.ts +++ b/packages/docusaurus/src/webpack/utils.ts @@ -11,7 +11,6 @@ import merge from 'webpack-merge'; import webpack, { Configuration, Loader, - NewLoader, Plugin, RuleSetRule, Stats, @@ -182,21 +181,27 @@ export function applyConfigurePostCss( ): Configuration { type LocalPostCSSLoader = Loader & {options: {postcssOptions: any}}; + // TODO not ideal heuristic but good enough for our usecase? function isPostCssLoader(loader: Loader): loader is LocalPostCSSLoader { - // TODO not ideal heuristic but good enough for our usecase? return !!(loader as any)?.options?.postcssOptions; } // Does not handle all edge cases, but good enough for now - config.module?.rules.map((rule) => { - for (const loader of rule.use as NewLoader[]) { - if (isPostCssLoader(loader)) { - loader.options.postcssOptions = configurePostCss( - loader.options.postcssOptions, - ); - } + function overridePostCssOptions(entry) { + if (isPostCssLoader(entry)) { + entry.options.postcssOptions = configurePostCss( + entry.options.postcssOptions, + ); + } else if (Array.isArray(entry.oneOf)) { + entry.oneOf.forEach(overridePostCssOptions); + } else if (Array.isArray(entry.use)) { + entry.use + .filter((u) => typeof u === 'object') + .forEach(overridePostCssOptions); } - }); + } + + config.module?.rules.forEach(overridePostCssOptions); return config; } diff --git a/website/docs/api/docusaurus.config.js.md b/website/docs/api/docusaurus.config.js.md index 608d3949fe..2dbd9e3585 100644 --- a/website/docs/api/docusaurus.config.js.md +++ b/website/docs/api/docusaurus.config.js.md @@ -96,15 +96,20 @@ module.exports = { localeConfigs: { en: { label: 'English', + direction: 'ltr', }, fr: { label: 'Français', + direction: 'ltr', }, }, }, }; ``` +- `label`: the label to use for this locale +- `direction`: `ltr` (default) or `rtl` (for [right-to-left languages](https://developer.mozilla.org/en-US/docs/Glossary/rtl) like Araric, Hebrew...) + ### `noIndex` - Type: `boolean` diff --git a/website/docs/i18n/i18n-introduction.md b/website/docs/i18n/i18n-introduction.md index 9f1c5856a5..3c67091be1 100644 --- a/website/docs/i18n/i18n-introduction.md +++ b/website/docs/i18n/i18n-introduction.md @@ -33,12 +33,12 @@ The goals of the Docusaurus i18n system are: - **No coupling**: not forced to use any SaaS, yet the integration is possible. - **Easy to use with [Crowdin](http://crowdin.com/)**: multiple Docusaurus v1 sites use Crowdin, and should be able to migrate to v2. - **Good SEO defaults**: setting useful SEO headers like [`hreflang`](https://developers.google.com/search/docs/advanced/crawling/localized-versions) for you. +- **RTL support**: locales reading right-to-left (Arabic, Hebrew...) should be easy to use. ### i18n goals (TODO) Features that are **not yet implemented**: -- **RTL support**: one locale should not be harder to use than another. - **Contextual translations**: reduce friction to contribute to the translation effort. - **Anchor links**: linking should not break when you localize headings. - **Advanced configuration options**: customize route paths, file-system paths. diff --git a/yarn.lock b/yarn.lock index 1c95a84ae5..c09b91b8f9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1212,6 +1212,13 @@ resolved "https://registry.yarnpkg.com/@bugsnag/safe-json-stringify/-/safe-json-stringify-6.0.0.tgz#22abdcd83e008c369902976730c34c150148a758" integrity sha512-htzFO1Zc57S8kgdRK9mLcPVTW1BY2ijfH7Dk2CeZmspTWKdKqSo1iwmqrq2WtRjFlo8aRZYgLX0wFrDXF/9DLA== +"@choojs/findup@^0.2.1": + version "0.2.1" + resolved "https://registry.yarnpkg.com/@choojs/findup/-/findup-0.2.1.tgz#ac13c59ae7be6e1da64de0779a0a7f03d75615a3" + integrity sha512-YstAqNb0MCN8PjdLCDfRsBcGVRN41f3vgLvaI0IrIcBp4AqILRSS0DeWNGkicC+f/zRIPJLc+9RURVSepwvfBw== + dependencies: + commander "^2.15.1" + "@cnakazawa/watch@^1.0.3": version "1.0.3" resolved "https://registry.yarnpkg.com/@cnakazawa/watch/-/watch-1.0.3.tgz#099139eaec7ebf07a27c1786a3ff64f39464d2ef" @@ -6244,7 +6251,7 @@ comma-separated-tokens@^1.0.0: resolved "https://registry.yarnpkg.com/comma-separated-tokens/-/comma-separated-tokens-1.0.7.tgz#419cd7fb3258b1ed838dc0953167a25e152f5b59" integrity sha512-Jrx3xsP4pPv4AwJUDWY9wOXGtwPXARej6Xd99h4TUGotmf8APuquKMpK+dnD3UgyxK7OEWaisjZz+3b5jtL6xQ== -commander@^2.19.0, commander@^2.20.0, commander@^2.20.3, commander@^2.3.0, commander@^2.8.1: +commander@^2.15.1, commander@^2.19.0, commander@^2.20.0, commander@^2.20.3, commander@^2.3.0, commander@^2.8.1: version "2.20.3" resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== @@ -10777,10 +10784,10 @@ infer-owner@^1.0.3, infer-owner@^1.0.4: resolved "https://registry.yarnpkg.com/infer-owner/-/infer-owner-1.0.4.tgz#c4cefcaa8e51051c2a40ba2ce8a3d27295af9467" integrity sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A== -infima@0.2.0-alpha.18: - version "0.2.0-alpha.18" - resolved "https://registry.yarnpkg.com/infima/-/infima-0.2.0-alpha.18.tgz#8ac62711f13ef99b1f4a45b3ac14571722a2ccf6" - integrity sha512-ndSEffXzjgM/eiSm5jpLTX6ON9MmylzxqBnV2bTiC3kCSyDYdvzTs+bSwf+C4TWayuqnRTnBK1JUePo3m6Bnfg== +infima@0.2.0-alpha.19: + version "0.2.0-alpha.19" + resolved "https://registry.yarnpkg.com/infima/-/infima-0.2.0-alpha.19.tgz#037dbb0b03e3aff00fb9b5e39f360b22645da6d6" + integrity sha512-3ZWyljOy6xNbOsEXJ2wecj3yXtL2vno4PbAEekTQxsSt6fu4kIOnleZH8RSW9ZLbWnaAokZgoJsAZZk3dI4hcg== inflight@^1.0.4: version "1.0.6" @@ -16092,6 +16099,15 @@ postcss-zindex@^4.0.1: postcss "^7.0.0" uniqs "^2.0.0" +postcss@^6.0.23: + version "6.0.23" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-6.0.23.tgz#61c82cc328ac60e677645f979054eb98bc0e3324" + integrity sha512-soOk1h6J3VMTZtVeVpv15/Hpdl2cBLX3CAw4TAbkpTJiNPk9YP/zWcD1ND+xEtvyuuvKzbxliTOIyvkSeSJ6ag== + dependencies: + chalk "^2.4.1" + source-map "^0.6.1" + supports-color "^5.4.0" + postcss@^7.0.0, postcss@^7.0.1, postcss@^7.0.14, postcss@^7.0.17, postcss@^7.0.18, postcss@^7.0.2, postcss@^7.0.21, postcss@^7.0.23, postcss@^7.0.26, postcss@^7.0.27, postcss@^7.0.31, postcss@^7.0.32, postcss@^7.0.5, postcss@^7.0.6, postcss@^7.0.7: version "7.0.32" resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.32.tgz#4310d6ee347053da3433db2be492883d62cec59d" @@ -17680,6 +17696,17 @@ rsvp@^4.8.4: resolved "https://registry.yarnpkg.com/rsvp/-/rsvp-4.8.5.tgz#c8f155311d167f68f21e168df71ec5b083113734" integrity sha512-nfMOlASu9OnRJo1mbEk2cz0D56a1MBNrJ7orjRZQG10XDyuvwksKbuXNp6qa+kbn839HwjwhBzhFmdsaEAfauA== +rtlcss@^2.6.2: + version "2.6.2" + resolved "https://registry.yarnpkg.com/rtlcss/-/rtlcss-2.6.2.tgz#55b572b52c70015ba6e03d497e5c5cb8137104b4" + integrity sha512-06LFAr+GAPo+BvaynsXRfoYTJvSaWRyOhURCQ7aeI1MKph9meM222F+Zkt3bDamyHHJuGi3VPtiRkpyswmQbGA== + dependencies: + "@choojs/findup" "^0.2.1" + chalk "^2.4.2" + mkdirp "^0.5.1" + postcss "^6.0.23" + strip-json-comments "^2.0.0" + run-async@^2.2.0, run-async@^2.3.0, run-async@^2.4.0: version "2.4.1" resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.4.1.tgz#8440eccf99ea3e70bd409d49aab88e10c189a455" @@ -18843,16 +18870,16 @@ strip-indent@^3.0.0: dependencies: min-indent "^1.0.0" +strip-json-comments@^2.0.0, strip-json-comments@~2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" + integrity sha1-PFMZQukIwml8DsNEhYwobHygpgo= + strip-json-comments@^3.0.1, strip-json-comments@^3.1.0, strip-json-comments@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== -strip-json-comments@~2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" - integrity sha1-PFMZQukIwml8DsNEhYwobHygpgo= - strip-outer@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/strip-outer/-/strip-outer-1.0.1.tgz#b2fd2abf6604b9d1e6013057195df836b8a9d631"