feat(v2): Add localeDropdown navbar item type + i18n localeConfigs field (#3916)

* Add localeDropdown navbar item type

* fix type + add localeConfigs test
This commit is contained in:
Sébastien Lorber 2020-12-14 18:28:39 +01:00 committed by GitHub
parent 3570aa06c8
commit aff656182c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 189 additions and 4 deletions

View file

@ -52,6 +52,7 @@ declare module '@generated/i18n' {
defaultLocale: string; defaultLocale: string;
locales: [string, ...string[]]; locales: [string, ...string[]];
currentLocale: string; currentLocale: string;
localeConfigs: Record<string, {label: string}>;
}; };
export default i18n; export default i18n;
} }

View file

@ -0,0 +1,74 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import React from 'react';
import DefaultNavbarItem from './DefaultNavbarItem';
import type {Props} from '@theme/NavbarItem/LocaleDropdownNavbarItem';
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
import {useLocation} from '@docusaurus/router';
export default function LocaleDropdownNavbarItem({
mobile,
...props
}: Props): JSX.Element {
const {
siteConfig: {baseUrl},
i18n: {defaultLocale, currentLocale, locales, localeConfigs},
} = useDocusaurusContext();
const {pathname} = useLocation();
function getLocaleLabel(locale) {
return localeConfigs[locale].label;
}
// TODO Docusaurus could offer some APIs to we should probably
const baseUrlUnlocalized =
currentLocale === defaultLocale
? baseUrl
: baseUrl.replace(`/${currentLocale}/`, '/');
const pathnameSuffix = pathname.replace(baseUrl, '');
function getLocalizedBaseUrl(locale) {
return locale === defaultLocale
? `${baseUrlUnlocalized}`
: `${baseUrlUnlocalized}${locale}/`;
}
const items = locales.map((locale) => {
const to = `${getLocalizedBaseUrl(locale)}${pathnameSuffix}`;
console.log({
locale,
to,
pathname,
baseUrl,
baseUrlUnlocalized,
pathnameSuffix,
});
return {
isNavLink: true,
label: getLocaleLabel(locale),
to: `pathname://${to}`,
target: '_self',
autoAddBaseUrl: false,
className: locale === currentLocale ? 'dropdown__link--active' : '',
};
});
// Mobile is handled a bit differently
const dropdownLabel = mobile ? 'Languages' : getLocaleLabel(currentLocale);
return (
<DefaultNavbarItem
{...props}
mobile={mobile}
label={dropdownLabel}
items={items}
/>
);
}

View file

@ -7,10 +7,12 @@
import React from 'react'; import React from 'react';
import DefaultNavbarItem from '@theme/NavbarItem/DefaultNavbarItem'; import DefaultNavbarItem from '@theme/NavbarItem/DefaultNavbarItem';
import LocaleDropdownNavbarItem from '@theme/NavbarItem/LocaleDropdownNavbarItem';
import type {Props} from '@theme/NavbarItem'; import type {Props} from '@theme/NavbarItem';
const NavbarItemComponents = { const NavbarItemComponents = {
default: () => DefaultNavbarItem, default: () => DefaultNavbarItem,
localeDropdown: () => LocaleDropdownNavbarItem,
// Need to lazy load these items as we don't know for sure the docs plugin is loaded // Need to lazy load these items as we don't know for sure the docs plugin is loaded
// See https://github.com/facebook/docusaurus/issues/3360 // See https://github.com/facebook/docusaurus/issues/3360

View file

@ -304,6 +304,15 @@ declare module '@theme/NavbarItem/DefaultNavbarItem' {
export default DefaultNavbarItem; export default DefaultNavbarItem;
} }
declare module '@theme/NavbarItem/LocaleDropdownNavbarItem' {
import type {Props as DefaultNavbarItemProps} from '@theme/NavbarItem/DefaultNavbarItem';
export type Props = DefaultNavbarItemProps;
const LocaleDropdownNavbarItem: (props: Props) => JSX.Element;
export default LocaleDropdownNavbarItem;
}
declare module '@theme/NavbarItem/DocsVersionDropdownNavbarItem' { declare module '@theme/NavbarItem/DocsVersionDropdownNavbarItem' {
import type {Props as DefaultNavbarItemProps} from '@theme/NavbarItem/DefaultNavbarItem'; import type {Props as DefaultNavbarItemProps} from '@theme/NavbarItem/DefaultNavbarItem';
import type {NavLinkProps} from '@theme/NavbarItem/DefaultNavbarItem'; import type {NavLinkProps} from '@theme/NavbarItem/DefaultNavbarItem';

View file

@ -96,6 +96,11 @@ const DocItemSchema = Joi.object({
activeSidebarClassName: Joi.string().default('navbar__link--active'), activeSidebarClassName: Joi.string().default('navbar__link--active'),
}); });
const LocaleDropdownNavbarItemSchema = Joi.object({
type: Joi.string().equal('localeDropdown').required(),
position: NavbarItemPosition,
});
// Can this be made easier? :/ // Can this be made easier? :/
const isOfType = (type) => { const isOfType = (type) => {
let typeSchema = Joi.string().required(); let typeSchema = Joi.string().required();
@ -124,10 +129,14 @@ const NavbarItemSchema = Joi.object().when({
is: isOfType('doc'), is: isOfType('doc'),
then: DocItemSchema, then: DocItemSchema,
}, },
{
is: isOfType('localeDropdown'),
then: LocaleDropdownNavbarItemSchema,
},
{ {
is: isOfType(undefined), is: isOfType(undefined),
then: Joi.forbidden().messages({ then: Joi.forbidden().messages({
'any.unknown': 'Bad nav item type {.type}', 'any.unknown': 'Bad navbar item type {.type}',
}), }),
}, },
], ],

View file

@ -92,9 +92,14 @@ export type TranslationFileContent = Record<string, TranslationMessage>;
export type TranslationFile = {path: string; content: TranslationFileContent}; export type TranslationFile = {path: string; content: TranslationFileContent};
export type TranslationFiles = TranslationFile[]; export type TranslationFiles = TranslationFile[];
export type I18nLocaleConfig = {
label: string;
};
export type I18nConfig = { export type I18nConfig = {
defaultLocale: string; defaultLocale: string;
locales: [string, ...string[]]; locales: [string, ...string[]];
localeConfigs: Record<string, I18nLocaleConfig>;
}; };
export type I18n = I18nConfig & { export type I18n = I18nConfig & {

View file

@ -26,6 +26,7 @@ interface Props {
readonly activeClassName?: string; readonly activeClassName?: string;
readonly children?: ReactNode; readonly children?: ReactNode;
readonly isActive?: () => boolean; readonly isActive?: () => boolean;
readonly autoAddBaseUrl?: boolean;
// escape hatch in case broken links check is annoying for a specific link // escape hatch in case broken links check is annoying for a specific link
readonly 'data-noBrokenLinkCheck'?: boolean; readonly 'data-noBrokenLinkCheck'?: boolean;
@ -45,6 +46,7 @@ function Link({
activeClassName, activeClassName,
isActive, isActive,
'data-noBrokenLinkCheck': noBrokenLinkCheck, 'data-noBrokenLinkCheck': noBrokenLinkCheck,
autoAddBaseUrl = true,
...props ...props
}: Props): JSX.Element { }: Props): JSX.Element {
const {withBaseUrl} = useBaseUrlUtils(); const {withBaseUrl} = useBaseUrlUtils();
@ -57,7 +59,9 @@ function Link({
const targetLinkUnprefixed = to || href; const targetLinkUnprefixed = to || href;
function maybeAddBaseUrl(str: string) { function maybeAddBaseUrl(str: string) {
return shouldAddBaseUrlAutomatically(str) ? withBaseUrl(str) : str; return autoAddBaseUrl && shouldAddBaseUrlAutomatically(str)
? withBaseUrl(str)
: str;
} }
const isInternal = isInternalUrl(targetLinkUnprefixed); const isInternal = isInternalUrl(targetLinkUnprefixed);

View file

@ -23,6 +23,7 @@ Object {
"favicon": "img/docusaurus.ico", "favicon": "img/docusaurus.ico",
"i18n": Object { "i18n": Object {
"defaultLocale": "en", "defaultLocale": "en",
"localeConfigs": Object {},
"locales": Array [ "locales": Array [
"en", "en",
], ],

View file

@ -5,9 +5,14 @@
* LICENSE file in the root directory of this source tree. * LICENSE file in the root directory of this source tree.
*/ */
import {loadI18n, localizePath} from '../i18n'; import {loadI18n, localizePath, defaultLocaleConfig} from '../i18n';
import {DEFAULT_I18N_CONFIG} from '../configValidation'; import {DEFAULT_I18N_CONFIG} from '../configValidation';
import path from 'path'; import path from 'path';
import {chain, identity} from 'lodash';
function testLocaleConfigsFor(locales: string[]) {
return chain(locales).keyBy(identity).mapValues(defaultLocaleConfig).value();
}
describe('loadI18n', () => { describe('loadI18n', () => {
test('should load I18n for default config', async () => { test('should load I18n for default config', async () => {
@ -22,6 +27,7 @@ describe('loadI18n', () => {
defaultLocale: 'en', defaultLocale: 'en',
locales: ['en'], locales: ['en'],
currentLocale: 'en', currentLocale: 'en',
localeConfigs: testLocaleConfigsFor(['en']),
}); });
}); });
@ -33,6 +39,7 @@ describe('loadI18n', () => {
i18n: { i18n: {
defaultLocale: 'fr', defaultLocale: 'fr',
locales: ['en', 'fr', 'de'], locales: ['en', 'fr', 'de'],
localeConfigs: {},
}, },
}, },
), ),
@ -40,6 +47,7 @@ describe('loadI18n', () => {
defaultLocale: 'fr', defaultLocale: 'fr',
locales: ['en', 'fr', 'de'], locales: ['en', 'fr', 'de'],
currentLocale: 'fr', currentLocale: 'fr',
localeConfigs: testLocaleConfigsFor(['en', 'fr', 'de']),
}); });
}); });
@ -51,6 +59,7 @@ describe('loadI18n', () => {
i18n: { i18n: {
defaultLocale: 'fr', defaultLocale: 'fr',
locales: ['en', 'fr', 'de'], locales: ['en', 'fr', 'de'],
localeConfigs: {},
}, },
}, },
{locale: 'de'}, {locale: 'de'},
@ -59,6 +68,35 @@ describe('loadI18n', () => {
defaultLocale: 'fr', defaultLocale: 'fr',
locales: ['en', 'fr', 'de'], locales: ['en', 'fr', 'de'],
currentLocale: 'de', currentLocale: 'de',
localeConfigs: testLocaleConfigsFor(['en', 'fr', 'de']),
});
});
test('should load I18n for multi-locale config with some xcustom locale configs', async () => {
await expect(
loadI18n(
{
i18n: {
defaultLocale: 'fr',
locales: ['en', 'fr', 'de'],
localeConfigs: {
fr: {label: 'Français'},
// @ts-expect-error: empty on purpose
en: {},
},
},
},
{locale: 'de'},
),
).resolves.toEqual({
defaultLocale: 'fr',
locales: ['en', 'fr', 'de'],
currentLocale: 'de',
localeConfigs: {
fr: {label: 'Français'},
en: defaultLocaleConfig('en'),
de: defaultLocaleConfig('de'),
},
}); });
}); });
@ -70,6 +108,7 @@ describe('loadI18n', () => {
i18n: { i18n: {
defaultLocale: 'fr', defaultLocale: 'fr',
locales: ['en', 'fr', 'de'], locales: ['en', 'fr', 'de'],
localeConfigs: {},
}, },
}, },
{locale: 'it'}, {locale: 'it'},
@ -88,6 +127,7 @@ describe('localizePath', () => {
defaultLocale: 'en', defaultLocale: 'en',
locales: ['en', 'fr'], locales: ['en', 'fr'],
currentLocale: 'fr', currentLocale: 'fr',
localeConfigs: {},
}, },
options: {localizePath: true}, options: {localizePath: true},
}), }),
@ -103,6 +143,7 @@ describe('localizePath', () => {
defaultLocale: 'en', defaultLocale: 'en',
locales: ['en', 'fr'], locales: ['en', 'fr'],
currentLocale: 'fr', currentLocale: 'fr',
localeConfigs: {},
}, },
options: {localizePath: true}, options: {localizePath: true},
}), }),
@ -118,6 +159,7 @@ describe('localizePath', () => {
defaultLocale: 'en', defaultLocale: 'en',
locales: ['en', 'fr'], locales: ['en', 'fr'],
currentLocale: 'en', currentLocale: 'en',
localeConfigs: {},
}, },
options: {localizePath: true}, options: {localizePath: true},
}), }),
@ -133,6 +175,7 @@ describe('localizePath', () => {
defaultLocale: 'en', defaultLocale: 'en',
locales: ['en', 'fr'], locales: ['en', 'fr'],
currentLocale: 'en', currentLocale: 'en',
localeConfigs: {},
}, },
// options: {localizePath: true}, // options: {localizePath: true},
}), }),
@ -148,6 +191,7 @@ describe('localizePath', () => {
defaultLocale: 'en', defaultLocale: 'en',
locales: ['en', 'fr'], locales: ['en', 'fr'],
currentLocale: 'en', currentLocale: 'en',
localeConfigs: {},
}, },
// options: {localizePath: true}, // options: {localizePath: true},
}), }),

View file

@ -19,6 +19,7 @@ const DEFAULT_I18N_LOCALE = 'en';
export const DEFAULT_I18N_CONFIG: I18nConfig = { export const DEFAULT_I18N_CONFIG: I18nConfig = {
defaultLocale: DEFAULT_I18N_LOCALE, defaultLocale: DEFAULT_I18N_LOCALE,
locales: [DEFAULT_I18N_LOCALE], locales: [DEFAULT_I18N_LOCALE],
localeConfigs: {},
}; };
export const DEFAULT_CONFIG: Pick< export const DEFAULT_CONFIG: Pick<
@ -66,9 +67,16 @@ const PresetSchema = Joi.alternatives().try(
Joi.array().items(Joi.string().required(), Joi.object().required()).length(2), Joi.array().items(Joi.string().required(), Joi.object().required()).length(2),
); );
const LocaleConfigSchema = Joi.object({
label: Joi.string(),
});
const I18N_CONFIG_SCHEMA = Joi.object<I18nConfig>({ const I18N_CONFIG_SCHEMA = Joi.object<I18nConfig>({
defaultLocale: Joi.string().required(), defaultLocale: Joi.string().required(),
locales: Joi.array().items().min(1).items(Joi.string().required()).required(), locales: Joi.array().items().min(1).items(Joi.string().required()).required(),
localeConfigs: Joi.object()
.pattern(/.*/, LocaleConfigSchema)
.default(DEFAULT_I18N_CONFIG.localeConfigs),
}) })
.optional() .optional()
.default(DEFAULT_I18N_CONFIG); .default(DEFAULT_I18N_CONFIG);

View file

@ -4,10 +4,16 @@
* This source code is licensed under the MIT license found in the * This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree. * LICENSE file in the root directory of this source tree.
*/ */
import {I18n, DocusaurusConfig} from '@docusaurus/types'; import {I18n, DocusaurusConfig, I18nLocaleConfig} from '@docusaurus/types';
import path from 'path'; import path from 'path';
import {normalizeUrl} from '@docusaurus/utils'; import {normalizeUrl} from '@docusaurus/utils';
export function defaultLocaleConfig(locale: string): I18nLocaleConfig {
return {
label: locale,
};
}
export async function loadI18n( export async function loadI18n(
config: DocusaurusConfig, config: DocusaurusConfig,
options: {locale?: string} = {}, options: {locale?: string} = {},
@ -25,9 +31,22 @@ Note: Docusaurus only support running one local at a time.`,
); );
} }
function getLocaleConfig(locale: string): I18nLocaleConfig {
// User provided values
const localeConfigOptions: Partial<I18nLocaleConfig> =
i18nConfig.localeConfigs[locale];
return {...defaultLocaleConfig(locale), ...localeConfigOptions};
}
const localeConfigs = i18nConfig.locales.reduce((acc, locale) => {
return {...acc, [locale]: getLocaleConfig(locale)};
}, {});
return { return {
...i18nConfig, ...i18nConfig,
currentLocale, currentLocale,
localeConfigs,
}; };
} }

View file

@ -52,6 +52,14 @@ module.exports = {
i18n: { i18n: {
defaultLocale: 'en', defaultLocale: 'en',
locales: ['en', 'fr'], locales: ['en', 'fr'],
localeConfigs: {
en: {
label: 'English',
},
fr: {
label: 'Français',
},
},
}, },
onBrokenLinks: 'throw', onBrokenLinks: 'throw',
onBrokenMarkdownLinks: 'warn', onBrokenMarkdownLinks: 'warn',
@ -303,6 +311,7 @@ module.exports = {
}, },
], ],
}, },
// {type: 'localeDropdown', position: 'right'},
{ {
href: 'https://github.com/facebook/docusaurus', href: 'https://github.com/facebook/docusaurus',
position: 'right', position: 'right',