Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 2 commits into from
Dec 14, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/docusaurus-module-type-aliases/src/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ declare module '@generated/i18n' {
defaultLocale: string;
locales: [string, ...string[]];
currentLocale: string;
localeConfigs: Record<string, {label: string}>;
};
export default i18n;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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}
/>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,12 @@

import React from 'react';
import DefaultNavbarItem from '@theme/NavbarItem/DefaultNavbarItem';
import LocaleDropdownNavbarItem from '@theme/NavbarItem/LocaleDropdownNavbarItem';
import type {Props} from '@theme/NavbarItem';

const NavbarItemComponents = {
default: () => DefaultNavbarItem,
localeDropdown: () => LocaleDropdownNavbarItem,

// 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
Expand Down
9 changes: 9 additions & 0 deletions packages/docusaurus-theme-classic/src/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,15 @@ declare module '@theme/NavbarItem/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' {
import type {Props as DefaultNavbarItemProps} from '@theme/NavbarItem/DefaultNavbarItem';
import type {NavLinkProps} from '@theme/NavbarItem/DefaultNavbarItem';
Expand Down
11 changes: 10 additions & 1 deletion packages/docusaurus-theme-classic/src/validateThemeConfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,11 @@ const DocItemSchema = Joi.object({
activeSidebarClassName: Joi.string().default('navbar__link--active'),
});

const LocaleDropdownNavbarItemSchema = Joi.object({
type: Joi.string().equal('localeDropdown').required(),
position: NavbarItemPosition,
});

// Can this be made easier? :/
const isOfType = (type) => {
let typeSchema = Joi.string().required();
Expand Down Expand Up @@ -124,10 +129,14 @@ const NavbarItemSchema = Joi.object().when({
is: isOfType('doc'),
then: DocItemSchema,
},
{
is: isOfType('localeDropdown'),
then: LocaleDropdownNavbarItemSchema,
},
{
is: isOfType(undefined),
then: Joi.forbidden().messages({
'any.unknown': 'Bad nav item type {.type}',
'any.unknown': 'Bad navbar item type {.type}',
}),
},
],
Expand Down
5 changes: 5 additions & 0 deletions packages/docusaurus-types/src/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,9 +92,14 @@ export type TranslationFileContent = Record<string, TranslationMessage>;
export type TranslationFile = {path: string; content: TranslationFileContent};
export type TranslationFiles = TranslationFile[];

export type I18nLocaleConfig = {
label: string;
};

export type I18nConfig = {
defaultLocale: string;
locales: [string, ...string[]];
localeConfigs: Record<string, I18nLocaleConfig>;
};

export type I18n = I18nConfig & {
Expand Down
6 changes: 5 additions & 1 deletion packages/docusaurus/src/client/exports/Link.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ interface Props {
readonly activeClassName?: string;
readonly children?: ReactNode;
readonly isActive?: () => boolean;
readonly autoAddBaseUrl?: boolean;

// escape hatch in case broken links check is annoying for a specific link
readonly 'data-noBrokenLinkCheck'?: boolean;
Expand All @@ -45,6 +46,7 @@ function Link({
activeClassName,
isActive,
'data-noBrokenLinkCheck': noBrokenLinkCheck,
autoAddBaseUrl = true,
...props
}: Props): JSX.Element {
const {withBaseUrl} = useBaseUrlUtils();
Expand All @@ -57,7 +59,9 @@ function Link({
const targetLinkUnprefixed = to || href;

function maybeAddBaseUrl(str: string) {
return shouldAddBaseUrlAutomatically(str) ? withBaseUrl(str) : str;
return autoAddBaseUrl && shouldAddBaseUrlAutomatically(str)
? withBaseUrl(str)
: str;
}

const isInternal = isInternalUrl(targetLinkUnprefixed);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ Object {
"favicon": "img/docusaurus.ico",
"i18n": Object {
"defaultLocale": "en",
"localeConfigs": Object {},
"locales": Array [
"en",
],
Expand Down
46 changes: 45 additions & 1 deletion packages/docusaurus/src/server/__tests__/i18n.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,14 @@
* 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 path from 'path';
import {chain, identity} from 'lodash';

function testLocaleConfigsFor(locales: string[]) {
return chain(locales).keyBy(identity).mapValues(defaultLocaleConfig).value();
}

describe('loadI18n', () => {
test('should load I18n for default config', async () => {
Expand All @@ -22,6 +27,7 @@ describe('loadI18n', () => {
defaultLocale: 'en',
locales: ['en'],
currentLocale: 'en',
localeConfigs: testLocaleConfigsFor(['en']),
});
});

Expand All @@ -33,13 +39,15 @@ describe('loadI18n', () => {
i18n: {
defaultLocale: 'fr',
locales: ['en', 'fr', 'de'],
localeConfigs: {},
},
},
),
).resolves.toEqual({
defaultLocale: 'fr',
locales: ['en', 'fr', 'de'],
currentLocale: 'fr',
localeConfigs: testLocaleConfigsFor(['en', 'fr', 'de']),
});
});

Expand All @@ -51,6 +59,31 @@ describe('loadI18n', () => {
i18n: {
defaultLocale: 'fr',
locales: ['en', 'fr', 'de'],
localeConfigs: {},
},
},
{locale: 'de'},
),
).resolves.toEqual({
defaultLocale: 'fr',
locales: ['en', 'fr', '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'},
Expand All @@ -59,6 +92,11 @@ describe('loadI18n', () => {
defaultLocale: 'fr',
locales: ['en', 'fr', 'de'],
currentLocale: 'de',
localeConfigs: {
fr: {label: 'Français'},
en: defaultLocaleConfig('en'),
de: defaultLocaleConfig('de'),
},
});
});

Expand All @@ -70,6 +108,7 @@ describe('loadI18n', () => {
i18n: {
defaultLocale: 'fr',
locales: ['en', 'fr', 'de'],
localeConfigs: {},
},
},
{locale: 'it'},
Expand All @@ -88,6 +127,7 @@ describe('localizePath', () => {
defaultLocale: 'en',
locales: ['en', 'fr'],
currentLocale: 'fr',
localeConfigs: {},
},
options: {localizePath: true},
}),
Expand All @@ -103,6 +143,7 @@ describe('localizePath', () => {
defaultLocale: 'en',
locales: ['en', 'fr'],
currentLocale: 'fr',
localeConfigs: {},
},
options: {localizePath: true},
}),
Expand All @@ -118,6 +159,7 @@ describe('localizePath', () => {
defaultLocale: 'en',
locales: ['en', 'fr'],
currentLocale: 'en',
localeConfigs: {},
},
options: {localizePath: true},
}),
Expand All @@ -133,6 +175,7 @@ describe('localizePath', () => {
defaultLocale: 'en',
locales: ['en', 'fr'],
currentLocale: 'en',
localeConfigs: {},
},
// options: {localizePath: true},
}),
Expand All @@ -148,6 +191,7 @@ describe('localizePath', () => {
defaultLocale: 'en',
locales: ['en', 'fr'],
currentLocale: 'en',
localeConfigs: {},
},
// options: {localizePath: true},
}),
Expand Down
8 changes: 8 additions & 0 deletions packages/docusaurus/src/server/configValidation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ const DEFAULT_I18N_LOCALE = 'en';
export const DEFAULT_I18N_CONFIG: I18nConfig = {
defaultLocale: DEFAULT_I18N_LOCALE,
locales: [DEFAULT_I18N_LOCALE],
localeConfigs: {},
};

export const DEFAULT_CONFIG: Pick<
Expand Down Expand Up @@ -66,9 +67,16 @@ const PresetSchema = Joi.alternatives().try(
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>({
defaultLocale: Joi.string().required(),
locales: Joi.array().items().min(1).items(Joi.string().required()).required(),
localeConfigs: Joi.object()
.pattern(/.*/, LocaleConfigSchema)
.default(DEFAULT_I18N_CONFIG.localeConfigs),
})
.optional()
.default(DEFAULT_I18N_CONFIG);
Expand Down
21 changes: 20 additions & 1 deletion packages/docusaurus/src/server/i18n.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,16 @@
* This source code is licensed under the MIT license found in the
* 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 {normalizeUrl} from '@docusaurus/utils';

export function defaultLocaleConfig(locale: string): I18nLocaleConfig {
return {
label: locale,
};
}

export async function loadI18n(
config: DocusaurusConfig,
options: {locale?: string} = {},
Expand All @@ -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 {
...i18nConfig,
currentLocale,
localeConfigs,
};
}

Expand Down
Loading