mirror of
https://github.com/facebook/docusaurus.git
synced 2025-08-02 00:09:48 +02:00
feat(v2): Add Interpolate / interpolate APIs + complete theme translations (#4295)
* WIP: refactor team profile cards * Add Interpolate / interpolate APIs * Add interpolate snapshot test * comments * fix Interpolate TS types * Interpolate should handle numbers and other JS types * translate BlogPostItem * interpolate translate() fn + add translations for blog post tag header * localize the LastUpdated component * translate DocVersionSuggestions * fix test * add some new translations * Add node script to easily update the theme default translations * fix translation extractor bug due to translate() dynamic values * use ICU placeholder syntax * refactor month key * order * team page translation improvements * Add interpolation doc + improve i18n doc
This commit is contained in:
parent
cdcd0f05d4
commit
1734975f2f
29 changed files with 1468 additions and 482 deletions
|
@ -84,16 +84,63 @@ declare module '@docusaurus/Link' {
|
|||
export default Link;
|
||||
}
|
||||
|
||||
declare module '@docusaurus/Translate' {
|
||||
type TranslateProps = {children: string; id?: string; description?: string};
|
||||
const Translate: (props: TranslateProps) => JSX.Element;
|
||||
export default Translate;
|
||||
declare module '@docusaurus/Interpolate' {
|
||||
import type {ReactNode} from 'react';
|
||||
|
||||
export function translate(param: {
|
||||
message: string;
|
||||
// TODO use TS template literal feature to make values typesafe!
|
||||
// (requires upgrading TS first)
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
export type ExtractInterpolatePlaceholders<Str extends string> = string;
|
||||
|
||||
export type InterpolateValues<
|
||||
Str extends string,
|
||||
Value extends ReactNode
|
||||
> = Record<ExtractInterpolatePlaceholders<Str>, Value>;
|
||||
|
||||
// TS function overload: if all the values are plain strings, then interpolate returns a simple string
|
||||
export function interpolate<Str extends string>(
|
||||
text: Str,
|
||||
values?: InterpolateValues<Str, string | number>,
|
||||
): string;
|
||||
|
||||
// If values contain any ReactNode, then the return is a ReactNode
|
||||
export function interpolate<Str extends string, Value extends ReactNode>(
|
||||
text: Str,
|
||||
values?: InterpolateValues<Str, Value>,
|
||||
): ReactNode;
|
||||
|
||||
export type InterpolateProps<Str extends string> = {
|
||||
children: Str;
|
||||
values?: InterpolateValues<Str, ReactNode>;
|
||||
};
|
||||
|
||||
export default function Interpolate<Str extends string>(
|
||||
props: InterpolateProps<Str>,
|
||||
): JSX.Element;
|
||||
}
|
||||
|
||||
declare module '@docusaurus/Translate' {
|
||||
import type {
|
||||
InterpolateProps,
|
||||
InterpolateValues,
|
||||
} from '@docusaurus/Interpolate';
|
||||
|
||||
type TranslateProps<Str extends string> = InterpolateProps<Str> & {
|
||||
id?: string;
|
||||
description?: string;
|
||||
}): string;
|
||||
};
|
||||
export default function Translate<Str extends string>(
|
||||
props: TranslateProps<Str>,
|
||||
): JSX.Element;
|
||||
|
||||
export function translate<Str extends string>(
|
||||
param: {
|
||||
message: Str;
|
||||
id?: string;
|
||||
description?: string;
|
||||
},
|
||||
values?: InterpolateValues<Str, string | number>,
|
||||
): string;
|
||||
}
|
||||
|
||||
declare module '@docusaurus/router' {
|
||||
|
|
|
@ -1,44 +1,68 @@
|
|||
{
|
||||
"theme.NotFound.title": "Page Not Found",
|
||||
"theme.AnnouncementBar.closeButtonAriaLabel": "Close",
|
||||
"theme.CodeBlock.copied": "Copied",
|
||||
"theme.CodeBlock.copy": "Copy",
|
||||
"theme.CodeBlock.copyButtonAriaLabel": "Copy code to clipboard",
|
||||
"theme.NotFound.p1": "We could not find what you were looking for.",
|
||||
"theme.NotFound.p2": "Please contact the owner of the site that linked you to the original URL and let them know their link is broken.",
|
||||
"theme.AnnouncementBar.closeButtonAriaLabel": "Close",
|
||||
"theme.NotFound.title": "Page Not Found",
|
||||
"theme.Playground.liveEditor": "Live Editor",
|
||||
"theme.Playground.result": "Result",
|
||||
"theme.PwaReloadPopup.closeButtonAriaLabel": "Close",
|
||||
"theme.PwaReloadPopup.info": "New version available",
|
||||
"theme.PwaReloadPopup.refreshButtonText": "Refresh",
|
||||
"theme.SearchBar.label": "Search",
|
||||
"theme.SearchPage.algoliaLabel": "Search by Algolia",
|
||||
"theme.SearchPage.emptyResultsTitle": "Search the documentation",
|
||||
"theme.SearchPage.existingResultsTitle": "Search results for",
|
||||
"theme.SearchPage.fetchingNewResults": "Fetching new results...",
|
||||
"theme.SearchPage.inputLabel": "Search",
|
||||
"theme.SearchPage.inputPlaceholder": "Type your search here",
|
||||
"theme.SearchPage.noResultsText": "No results were found",
|
||||
"theme.blog.paginator.navAriaLabel": "Blog list page navigation",
|
||||
"theme.blog.paginator.newerEntries": "Newer Entries",
|
||||
"theme.blog.paginator.olderEntries": "Older Entries",
|
||||
"theme.blog.post.date": "{month} {day}, {year}",
|
||||
"theme.blog.post.nPosts": "{count} posts",
|
||||
"theme.blog.post.onePost": "One post",
|
||||
"theme.blog.post.paginator.navAriaLabel": "Blog post page navigation",
|
||||
"theme.blog.post.paginator.newerPost": "Newer Post",
|
||||
"theme.blog.post.paginator.olderPost": "Older Post",
|
||||
"theme.blog.post.readMore": "Read More",
|
||||
"theme.tags.tagsPageLink": "View All Tags",
|
||||
"theme.tags.tagsPageTitle": "Tags",
|
||||
"theme.tags.tagsListLabel": "Tags:",
|
||||
"theme.CodeBlock.copyButtonAriaLabel": "Copy code to clipboard",
|
||||
"theme.CodeBlock.copied": "Copied",
|
||||
"theme.CodeBlock.copy": "Copy",
|
||||
"theme.docs.paginator.navAriaLabel": "Docs pages navigation",
|
||||
"theme.docs.paginator.previous": "Previous",
|
||||
"theme.docs.paginator.next": "Next",
|
||||
"theme.docs.sidebar.expandButtonTitle": "Expand sidebar",
|
||||
"theme.docs.sidebar.expandButtonAriaLabel": "Expand sidebar",
|
||||
"theme.docs.sidebar.responsiveCloseButtonLabel": "Close menu",
|
||||
"theme.docs.sidebar.responsiveOpenButtonLabel": "Open menu",
|
||||
"theme.docs.sidebar.collapseButtonTitle": "Collapse sidebar",
|
||||
"theme.docs.sidebar.collapseButtonAriaLabel": "Collapse sidebar",
|
||||
"theme.blog.post.readingTime": "{readingTime} min read",
|
||||
"theme.blog.tagTitle": "{nPosts} tagged with \"{tagName}\"",
|
||||
"theme.common.editThisPage": "Edit this page",
|
||||
"theme.common.headingLinkTitle": "Direct link to heading",
|
||||
"theme.common.month.april": "April",
|
||||
"theme.common.month.august": "August",
|
||||
"theme.common.month.december": "December",
|
||||
"theme.common.month.february": "February",
|
||||
"theme.common.month.january": "January",
|
||||
"theme.common.month.july": "July",
|
||||
"theme.common.month.june": "June",
|
||||
"theme.common.month.march": "March",
|
||||
"theme.common.month.may": "May",
|
||||
"theme.common.month.november": "November",
|
||||
"theme.common.month.october": "October",
|
||||
"theme.common.month.september": "September",
|
||||
"theme.common.skipToMainContent": "Skip to main content",
|
||||
"theme.SearchPage.existingResultsTitle": "Search results for",
|
||||
"theme.SearchPage.emptyResultsTitle": "Search the documentation",
|
||||
"theme.SearchPage.inputPlaceholder": "Type your search here",
|
||||
"theme.SearchPage.inputLabel": "Search",
|
||||
"theme.SearchPage.algoliaLabel": "Search by Algolia",
|
||||
"theme.SearchPage.noResultsText": "No results were found",
|
||||
"theme.SearchPage.fetchingNewResults": "Fetching new results...",
|
||||
"theme.SearchBar.label": "Search",
|
||||
"theme.PwaReloadPopup.info": "New version available",
|
||||
"theme.PwaReloadPopup.refreshButtonText": "Refresh",
|
||||
"theme.PwaReloadPopup.closeButtonAriaLabel": "Close",
|
||||
"theme.Playground.liveEditor": "Live Editor",
|
||||
"theme.Playground.result": "Result"
|
||||
}
|
||||
"theme.docs.paginator.navAriaLabel": "Docs pages navigation",
|
||||
"theme.docs.paginator.next": "Next",
|
||||
"theme.docs.paginator.previous": "Previous",
|
||||
"theme.docs.sidebar.collapseButtonAriaLabel": "Collapse sidebar",
|
||||
"theme.docs.sidebar.collapseButtonTitle": "Collapse sidebar",
|
||||
"theme.docs.sidebar.expandButtonAriaLabel": "Expand sidebar",
|
||||
"theme.docs.sidebar.expandButtonTitle": "Expand sidebar",
|
||||
"theme.docs.sidebar.responsiveCloseButtonLabel": "Close menu",
|
||||
"theme.docs.sidebar.responsiveOpenButtonLabel": "Open menu",
|
||||
"theme.docs.versions.latestVersionLinkLabel": "latest version",
|
||||
"theme.docs.versions.latestVersionSuggestionLabel": "For up-to-date documentation, see the {latestVersionLink} ({versionLabel}).",
|
||||
"theme.docs.versions.unmaintainedVersionLabel": "This is documentation for {siteTitle} {versionLabel}, which is no longer actively maintained.",
|
||||
"theme.docs.versions.unreleasedVersionLabel": "This is unreleased documentation for {siteTitle} {versionLabel} version.",
|
||||
"theme.lastUpdated.atDate": "on {date}",
|
||||
"theme.lastUpdated.byUser": "by {user}",
|
||||
"theme.lastUpdated.lastUpdatedAtBy": "Last updated{atDate}{byUser}",
|
||||
"theme.tags.tagsListLabel": "Tags:",
|
||||
"theme.tags.tagsPageLink": "View All Tags",
|
||||
"theme.tags.tagsPageTitle": "Tags"
|
||||
}
|
|
@ -1,44 +1,68 @@
|
|||
{
|
||||
"theme.NotFound.title": "Seite nicht gefunden",
|
||||
"theme.NotFound.p1": "Wir konnten nicht finden, wonach Sie gesucht haben.",
|
||||
"theme.NotFound.p2": "Bitte kontaktieren Sie den Besitzer der Seite, die Sie mit der ursprünglichen URL verlinkt hat, und teilen Sie ihm mit, dass der Link nicht mehr funktioniert.",
|
||||
"theme.AnnouncementBar.closeButtonAriaLabel": "Schließen",
|
||||
"theme.blog.paginator.navAriaLabel": "Navigation der Blog-Listenseite",
|
||||
"theme.blog.paginator.newerEntries": "Neuere Einträge",
|
||||
"theme.blog.paginator.olderEntries": "Ältere Einträge",
|
||||
"theme.blog.post.paginator.navAriaLabel": "Blog Post Seiten Navigation",
|
||||
"theme.blog.post.paginator.newerPost": "Neuer Post",
|
||||
"theme.blog.post.paginator.olderPost": "Älterer Post",
|
||||
"theme.blog.post.readMore": "Mehr lesen",
|
||||
"theme.tags.tagsPageLink": "Alle Tags anzeigen",
|
||||
"theme.tags.tagsPageTitle": "Tags",
|
||||
"theme.tags.tagsListLabel": "Tags:",
|
||||
"theme.CodeBlock.copyButtonAriaLabel": "In die Zwischenablage kopieren",
|
||||
"theme.CodeBlock.copied": "Kopiert",
|
||||
"theme.CodeBlock.copy": "Kopieren",
|
||||
"theme.docs.paginator.navAriaLabel": "Dokumentation Seiten Navigation",
|
||||
"theme.docs.paginator.previous": "Zurück",
|
||||
"theme.docs.paginator.next": "Weiter",
|
||||
"theme.docs.sidebar.expandButtonTitle": "Seitenleiste ausklappen",
|
||||
"theme.docs.sidebar.expandButtonAriaLabel": "Seitenleiste ausklappen",
|
||||
"theme.docs.sidebar.responsiveCloseButtonLabel": "Menü schließen",
|
||||
"theme.docs.sidebar.responsiveOpenButtonLabel": "Menü öffenen",
|
||||
"theme.docs.sidebar.collapseButtonTitle": "Seitenleiste einklappen",
|
||||
"theme.docs.sidebar.collapseButtonAriaLabel": "Seitenleiste einklappen",
|
||||
"theme.common.editThisPage": "Diese Seite bearbeiten",
|
||||
"theme.common.headingLinkTitle": "Direkter Link zur Überschrift",
|
||||
"theme.common.skipToMainContent": "Zum Hauptinhalt springen",
|
||||
"theme.SearchPage.existingResultsTitle": "Suchergebnisse für",
|
||||
"theme.SearchPage.emptyResultsTitle": "Suche in der Dokumentation",
|
||||
"theme.SearchPage.inputPlaceholder": "Geben Sie hier Ihre Suche ein",
|
||||
"theme.SearchPage.inputLabel": "Suche",
|
||||
"theme.SearchPage.algoliaLabel": "Suche von Algolia",
|
||||
"theme.SearchPage.noResultsText": "Es wurden keine Ergebnisse gefunden",
|
||||
"theme.SearchPage.fetchingNewResults": "Neue Ergebnisse abrufen...",
|
||||
"theme.SearchBar.label": "Suche",
|
||||
"theme.PwaReloadPopup.info": "Neue Version verfügbar",
|
||||
"theme.PwaReloadPopup.refreshButtonText": "Aktualisieren",
|
||||
"theme.PwaReloadPopup.closeButtonAriaLabel": "Schließen",
|
||||
"theme.Playground.liveEditor": "Live Editor",
|
||||
"theme.Playground.result": "Ergebnisse"
|
||||
}
|
||||
"theme.AnnouncementBar.closeButtonAriaLabel": "Schließen",
|
||||
"theme.CodeBlock.copied": "Kopiert",
|
||||
"theme.CodeBlock.copy": "Kopieren",
|
||||
"theme.CodeBlock.copyButtonAriaLabel": "In die Zwischenablage kopieren",
|
||||
"theme.NotFound.p1": "Wir konnten nicht finden, wonach Sie gesucht haben.",
|
||||
"theme.NotFound.p2": "Bitte kontaktieren Sie den Besitzer der Seite, die Sie mit der ursprünglichen URL verlinkt hat, und teilen Sie ihm mit, dass der Link nicht mehr funktioniert.",
|
||||
"theme.NotFound.title": "Seite nicht gefunden",
|
||||
"theme.Playground.liveEditor": "Live Editor",
|
||||
"theme.Playground.result": "Ergebnisse",
|
||||
"theme.PwaReloadPopup.closeButtonAriaLabel": "Schließen",
|
||||
"theme.PwaReloadPopup.info": "Neue Version verfügbar",
|
||||
"theme.PwaReloadPopup.refreshButtonText": "Aktualisieren",
|
||||
"theme.SearchBar.label": "Suche",
|
||||
"theme.SearchPage.algoliaLabel": "Suche von Algolia",
|
||||
"theme.SearchPage.emptyResultsTitle": "Suche in der Dokumentation",
|
||||
"theme.SearchPage.existingResultsTitle": "Suchergebnisse für",
|
||||
"theme.SearchPage.fetchingNewResults": "Neue Ergebnisse abrufen...",
|
||||
"theme.SearchPage.inputLabel": "Suche",
|
||||
"theme.SearchPage.inputPlaceholder": "Geben Sie hier Ihre Suche ein",
|
||||
"theme.SearchPage.noResultsText": "Es wurden keine Ergebnisse gefunden",
|
||||
"theme.blog.paginator.navAriaLabel": "Navigation der Blog-Listenseite",
|
||||
"theme.blog.paginator.newerEntries": "Neuere Einträge",
|
||||
"theme.blog.paginator.olderEntries": "Ältere Einträge",
|
||||
"theme.blog.post.date": "{month} {day}, {year}",
|
||||
"theme.blog.post.nPosts": "{count} posts",
|
||||
"theme.blog.post.onePost": "One post",
|
||||
"theme.blog.post.paginator.navAriaLabel": "Blog Post Seiten Navigation",
|
||||
"theme.blog.post.paginator.newerPost": "Neuer Post",
|
||||
"theme.blog.post.paginator.olderPost": "Älterer Post",
|
||||
"theme.blog.post.readMore": "Mehr lesen",
|
||||
"theme.blog.post.readingTime": "{readingTime} min read",
|
||||
"theme.blog.tagTitle": "{nPosts} tagged with \"{tagName}\"",
|
||||
"theme.common.editThisPage": "Diese Seite bearbeiten",
|
||||
"theme.common.headingLinkTitle": "Direkter Link zur Überschrift",
|
||||
"theme.common.month.april": "April",
|
||||
"theme.common.month.august": "August",
|
||||
"theme.common.month.december": "December",
|
||||
"theme.common.month.february": "February",
|
||||
"theme.common.month.january": "January",
|
||||
"theme.common.month.july": "July",
|
||||
"theme.common.month.june": "June",
|
||||
"theme.common.month.march": "March",
|
||||
"theme.common.month.may": "May",
|
||||
"theme.common.month.november": "November",
|
||||
"theme.common.month.october": "October",
|
||||
"theme.common.month.september": "September",
|
||||
"theme.common.skipToMainContent": "Zum Hauptinhalt springen",
|
||||
"theme.docs.paginator.navAriaLabel": "Dokumentation Seiten Navigation",
|
||||
"theme.docs.paginator.next": "Weiter",
|
||||
"theme.docs.paginator.previous": "Zurück",
|
||||
"theme.docs.sidebar.collapseButtonAriaLabel": "Seitenleiste einklappen",
|
||||
"theme.docs.sidebar.collapseButtonTitle": "Seitenleiste einklappen",
|
||||
"theme.docs.sidebar.expandButtonAriaLabel": "Seitenleiste ausklappen",
|
||||
"theme.docs.sidebar.expandButtonTitle": "Seitenleiste ausklappen",
|
||||
"theme.docs.sidebar.responsiveCloseButtonLabel": "Menü schließen",
|
||||
"theme.docs.sidebar.responsiveOpenButtonLabel": "Menü öffenen",
|
||||
"theme.docs.versions.latestVersionLinkLabel": "latest version",
|
||||
"theme.docs.versions.latestVersionSuggestionLabel": "For up-to-date documentation, see the {latestVersionLink} ({versionLabel}).",
|
||||
"theme.docs.versions.unmaintainedVersionLabel": "This is documentation for {siteTitle} {versionLabel}, which is no longer actively maintained.",
|
||||
"theme.docs.versions.unreleasedVersionLabel": "This is unreleased documentation for {siteTitle} {versionLabel} version.",
|
||||
"theme.lastUpdated.atDate": "on {date}",
|
||||
"theme.lastUpdated.byUser": "by {user}",
|
||||
"theme.lastUpdated.lastUpdatedAtBy": "Last updated{atDate}{byUser}",
|
||||
"theme.tags.tagsListLabel": "Tags:",
|
||||
"theme.tags.tagsPageLink": "Alle Tags anzeigen",
|
||||
"theme.tags.tagsPageTitle": "Tags"
|
||||
}
|
|
@ -1,44 +1,68 @@
|
|||
{
|
||||
"theme.NotFound.title": "صفحهای که دنبال آن بودید پیدا نشد!",
|
||||
"theme.AnnouncementBar.closeButtonAriaLabel": "بستن",
|
||||
"theme.CodeBlock.copied": "کپی شد",
|
||||
"theme.CodeBlock.copy": "کپی کردن",
|
||||
"theme.CodeBlock.copyButtonAriaLabel": "کپی کردن کد به کلیپ بورد",
|
||||
"theme.NotFound.p1": "متاسفانه نتوانستیم مطلب مورد نظر شما را پیدا کنیم.",
|
||||
"theme.NotFound.p2": "لطفا با صاحب وبسایت تماس بگیرید و ایشان را از مشکل پیش آمده مطلع کنید.",
|
||||
"theme.AnnouncementBar.closeButtonAriaLabel": "بستن",
|
||||
"theme.NotFound.title": "صفحهای که دنبال آن بودید پیدا نشد!",
|
||||
"theme.Playground.liveEditor": "ویرایشگر زنده",
|
||||
"theme.Playground.result": "نتایج",
|
||||
"theme.PwaReloadPopup.closeButtonAriaLabel": "بستن",
|
||||
"theme.PwaReloadPopup.info": "نسخه جدیدی منتشر شده است",
|
||||
"theme.PwaReloadPopup.refreshButtonText": "بروزرسانی",
|
||||
"theme.SearchBar.label": "جستجو",
|
||||
"theme.SearchPage.algoliaLabel": "جستجو با Algolia",
|
||||
"theme.SearchPage.emptyResultsTitle": "جستجو در متن",
|
||||
"theme.SearchPage.existingResultsTitle": "جستجو برای عبارت",
|
||||
"theme.SearchPage.fetchingNewResults": "در حال دریافت نتایج...",
|
||||
"theme.SearchPage.inputLabel": "جستجو",
|
||||
"theme.SearchPage.inputPlaceholder": "عبارت مورد نظر را اینجا بنویسید",
|
||||
"theme.SearchPage.noResultsText": "هیچ نتیجه ای پیدا نشد",
|
||||
"theme.blog.paginator.navAriaLabel": "کنترل لیست صفحه وبسایت",
|
||||
"theme.blog.paginator.newerEntries": "مطالب جدیدتر",
|
||||
"theme.blog.paginator.olderEntries": "مطالب قدیمی تر",
|
||||
"theme.blog.post.date": "{month} {day}, {year}",
|
||||
"theme.blog.post.nPosts": "{count} posts",
|
||||
"theme.blog.post.onePost": "One post",
|
||||
"theme.blog.post.paginator.navAriaLabel": "کنترل پست های صفحه وبلاگ",
|
||||
"theme.blog.post.paginator.newerPost": "پست های جدید تر",
|
||||
"theme.blog.post.paginator.olderPost": "پست های قدیمی تر",
|
||||
"theme.blog.post.readMore": "ادامه مطلب",
|
||||
"theme.tags.tagsPageLink": "مشاهده تمام برچسب ها",
|
||||
"theme.tags.tagsPageTitle": "برچسب ها",
|
||||
"theme.tags.tagsListLabel": ":برچسب ها",
|
||||
"theme.CodeBlock.copyButtonAriaLabel": "کپی کردن کد به کلیپ بورد",
|
||||
"theme.CodeBlock.copied": "کپی شد",
|
||||
"theme.CodeBlock.copy": "کپی کردن",
|
||||
"theme.docs.paginator.navAriaLabel": "کنترل صفحه اسناد",
|
||||
"theme.docs.paginator.previous": "قبلی",
|
||||
"theme.docs.paginator.next": "بعدی",
|
||||
"theme.docs.sidebar.expandButtonTitle": "بزرگ کردن نوار کناری",
|
||||
"theme.docs.sidebar.expandButtonAriaLabel": "بزرگ کردن نوار کناری",
|
||||
"theme.docs.sidebar.responsiveCloseButtonLabel": "بستن منو",
|
||||
"theme.docs.sidebar.responsiveOpenButtonLabel": "باز کردن منو",
|
||||
"theme.docs.sidebar.collapseButtonTitle": "بستن نوار کناری",
|
||||
"theme.docs.sidebar.collapseButtonAriaLabel": "بستن نوار کناری",
|
||||
"theme.blog.post.readingTime": "{readingTime} min read",
|
||||
"theme.blog.tagTitle": "{nPosts} tagged with \"{tagName}\"",
|
||||
"theme.common.editThisPage": "ویرایش صفحه",
|
||||
"theme.common.headingLinkTitle": "لینک مستقیم به عنوان",
|
||||
"theme.common.month.april": "April",
|
||||
"theme.common.month.august": "August",
|
||||
"theme.common.month.december": "December",
|
||||
"theme.common.month.february": "February",
|
||||
"theme.common.month.january": "January",
|
||||
"theme.common.month.july": "July",
|
||||
"theme.common.month.june": "June",
|
||||
"theme.common.month.march": "March",
|
||||
"theme.common.month.may": "May",
|
||||
"theme.common.month.november": "November",
|
||||
"theme.common.month.october": "October",
|
||||
"theme.common.month.september": "September",
|
||||
"theme.common.skipToMainContent": "رفتن به مطلب اصلی",
|
||||
"theme.SearchPage.existingResultsTitle": "جستجو برای عبارت",
|
||||
"theme.SearchPage.emptyResultsTitle": "جستجو در متن",
|
||||
"theme.SearchPage.inputPlaceholder": "عبارت مورد نظر را اینجا بنویسید",
|
||||
"theme.SearchPage.inputLabel": "جستجو",
|
||||
"theme.SearchPage.algoliaLabel": "جستجو با Algolia",
|
||||
"theme.SearchPage.noResultsText": "هیچ نتیجه ای پیدا نشد",
|
||||
"theme.SearchPage.fetchingNewResults": "در حال دریافت نتایج...",
|
||||
"theme.SearchBar.label": "جستجو",
|
||||
"theme.PwaReloadPopup.info": "نسخه جدیدی منتشر شده است",
|
||||
"theme.PwaReloadPopup.refreshButtonText": "بروزرسانی",
|
||||
"theme.PwaReloadPopup.closeButtonAriaLabel": "بستن",
|
||||
"theme.Playground.liveEditor": "ویرایشگر زنده",
|
||||
"theme.Playground.result": "نتایج"
|
||||
}
|
||||
"theme.docs.paginator.navAriaLabel": "کنترل صفحه اسناد",
|
||||
"theme.docs.paginator.next": "بعدی",
|
||||
"theme.docs.paginator.previous": "قبلی",
|
||||
"theme.docs.sidebar.collapseButtonAriaLabel": "بستن نوار کناری",
|
||||
"theme.docs.sidebar.collapseButtonTitle": "بستن نوار کناری",
|
||||
"theme.docs.sidebar.expandButtonAriaLabel": "بزرگ کردن نوار کناری",
|
||||
"theme.docs.sidebar.expandButtonTitle": "بزرگ کردن نوار کناری",
|
||||
"theme.docs.sidebar.responsiveCloseButtonLabel": "بستن منو",
|
||||
"theme.docs.sidebar.responsiveOpenButtonLabel": "باز کردن منو",
|
||||
"theme.docs.versions.latestVersionLinkLabel": "latest version",
|
||||
"theme.docs.versions.latestVersionSuggestionLabel": "For up-to-date documentation, see the {latestVersionLink} ({versionLabel}).",
|
||||
"theme.docs.versions.unmaintainedVersionLabel": "This is documentation for {siteTitle} {versionLabel}, which is no longer actively maintained.",
|
||||
"theme.docs.versions.unreleasedVersionLabel": "This is unreleased documentation for {siteTitle} {versionLabel} version.",
|
||||
"theme.lastUpdated.atDate": "on {date}",
|
||||
"theme.lastUpdated.byUser": "by {user}",
|
||||
"theme.lastUpdated.lastUpdatedAtBy": "Last updated{atDate}{byUser}",
|
||||
"theme.tags.tagsListLabel": ":برچسب ها",
|
||||
"theme.tags.tagsPageLink": "مشاهده تمام برچسب ها",
|
||||
"theme.tags.tagsPageTitle": "برچسب ها"
|
||||
}
|
|
@ -1,44 +1,68 @@
|
|||
{
|
||||
"theme.NotFound.title": "Page introuvable",
|
||||
"theme.AnnouncementBar.closeButtonAriaLabel": "Fermer",
|
||||
"theme.CodeBlock.copied": "Copié",
|
||||
"theme.CodeBlock.copy": "Copier",
|
||||
"theme.CodeBlock.copyButtonAriaLabel": "Copier le code",
|
||||
"theme.NotFound.p1": "Nous n'avons pas trouvé ce que vous recherchez.",
|
||||
"theme.NotFound.p2": "Veuillez contacter le propriétaire du site qui vous a lié à l'URL d'origine et leur faire savoir que leur lien est cassé.",
|
||||
"theme.AnnouncementBar.closeButtonAriaLabel": "Fermer",
|
||||
"theme.NotFound.title": "Page introuvable",
|
||||
"theme.Playground.liveEditor": "Éditeur en direct",
|
||||
"theme.Playground.result": "Résultat",
|
||||
"theme.PwaReloadPopup.closeButtonAriaLabel": "Fermer",
|
||||
"theme.PwaReloadPopup.info": "Nouvelle version disponible",
|
||||
"theme.PwaReloadPopup.refreshButtonText": "Rafraichir",
|
||||
"theme.SearchBar.label": "Chercher",
|
||||
"theme.SearchPage.algoliaLabel": "Recharche Algolia",
|
||||
"theme.SearchPage.emptyResultsTitle": "Rechercher dans la documentation",
|
||||
"theme.SearchPage.existingResultsTitle": "Rechercher des résultats pour",
|
||||
"theme.SearchPage.fetchingNewResults": "Chargement de nouveaux résultats...",
|
||||
"theme.SearchPage.inputLabel": "Chercher",
|
||||
"theme.SearchPage.inputPlaceholder": "Tapez vôtre recherche ici",
|
||||
"theme.SearchPage.noResultsText": "Aucun résultat trouvé",
|
||||
"theme.blog.paginator.navAriaLabel": "Pagination de la liste des posts du blog",
|
||||
"theme.blog.paginator.newerEntries": "Nouvelles entrées",
|
||||
"theme.blog.paginator.olderEntries": "Anciennes entrées",
|
||||
"theme.blog.post.date": "{day} {month} {year}",
|
||||
"theme.blog.post.nPosts": "{count} articles",
|
||||
"theme.blog.post.onePost": "Un article",
|
||||
"theme.blog.post.paginator.navAriaLabel": "Pagination des blog posts",
|
||||
"theme.blog.post.paginator.newerPost": "Article plus récent",
|
||||
"theme.blog.post.paginator.olderPost": "Article plus ancien",
|
||||
"theme.blog.post.readMore": "Lire plus",
|
||||
"theme.tags.tagsPageLink": "Voir tous les tags",
|
||||
"theme.tags.tagsPageTitle": "Tags",
|
||||
"theme.tags.tagsListLabel": "Tags:",
|
||||
"theme.CodeBlock.copyButtonAriaLabel": "Copier le code",
|
||||
"theme.CodeBlock.copied": "Copié",
|
||||
"theme.CodeBlock.copy": "Copier",
|
||||
"theme.docs.paginator.navAriaLabel": "Pagination des documents",
|
||||
"theme.docs.paginator.previous": "Précédent",
|
||||
"theme.docs.paginator.next": "Suivant",
|
||||
"theme.docs.sidebar.expandButtonTitle": "Déplier le menu latéral",
|
||||
"theme.docs.sidebar.expandButtonAriaLabel": "Déplier le menu latéral",
|
||||
"theme.docs.sidebar.responsiveCloseButtonLabel": "Fermer le menu latéral",
|
||||
"theme.docs.sidebar.responsiveOpenButtonLabel": "Ouvrir le menu latéral",
|
||||
"theme.docs.sidebar.collapseButtonTitle": "Réduire le menu latéral",
|
||||
"theme.docs.sidebar.collapseButtonAriaLabel": "Réduire le menu latéral",
|
||||
"theme.blog.post.readingTime": "{readingTime} min de lecture",
|
||||
"theme.blog.tagTitle": "{nPosts} taggés avec \"{tagName}\"",
|
||||
"theme.common.editThisPage": "Éditer cette page",
|
||||
"theme.common.headingLinkTitle": "Lien direct vers le titre",
|
||||
"theme.common.month.april": "Avril",
|
||||
"theme.common.month.august": "Août",
|
||||
"theme.common.month.december": "Décembre",
|
||||
"theme.common.month.february": "Février",
|
||||
"theme.common.month.january": "Janvier",
|
||||
"theme.common.month.july": "Juillet",
|
||||
"theme.common.month.june": "Juin",
|
||||
"theme.common.month.march": "Mars",
|
||||
"theme.common.month.may": "Mai",
|
||||
"theme.common.month.november": "Novembre",
|
||||
"theme.common.month.october": "Octobre",
|
||||
"theme.common.month.september": "Septembre",
|
||||
"theme.common.skipToMainContent": "Aller au contenu principal",
|
||||
"theme.SearchPage.existingResultsTitle": "Rechercher des résultats pour",
|
||||
"theme.SearchPage.emptyResultsTitle": "Rechercher dans la documentation",
|
||||
"theme.SearchPage.inputPlaceholder": "Tapez vôtre recherche ici",
|
||||
"theme.SearchPage.inputLabel": "Chercher",
|
||||
"theme.SearchPage.algoliaLabel": "Recharche Algolia",
|
||||
"theme.SearchPage.noResultsText": "Aucun résultat trouvé",
|
||||
"theme.SearchPage.fetchingNewResults": "Chargement de nouveaux résultats...",
|
||||
"theme.SearchBar.label": "Chercher",
|
||||
"theme.PwaReloadPopup.info": "Nouvelle version disponible",
|
||||
"theme.PwaReloadPopup.refreshButtonText": "Rafraichir",
|
||||
"theme.PwaReloadPopup.closeButtonAriaLabel": "Fermer",
|
||||
"theme.Playground.liveEditor": "Éditeur en direct",
|
||||
"theme.Playground.result": "Résultat"
|
||||
}
|
||||
"theme.docs.paginator.navAriaLabel": "Pagination des documents",
|
||||
"theme.docs.paginator.next": "Suivant",
|
||||
"theme.docs.paginator.previous": "Précédent",
|
||||
"theme.docs.sidebar.collapseButtonAriaLabel": "Réduire le menu latéral",
|
||||
"theme.docs.sidebar.collapseButtonTitle": "Réduire le menu latéral",
|
||||
"theme.docs.sidebar.expandButtonAriaLabel": "Déplier le menu latéral",
|
||||
"theme.docs.sidebar.expandButtonTitle": "Déplier le menu latéral",
|
||||
"theme.docs.sidebar.responsiveCloseButtonLabel": "Fermer le menu latéral",
|
||||
"theme.docs.sidebar.responsiveOpenButtonLabel": "Ouvrir le menu latéral",
|
||||
"theme.docs.versions.latestVersionLinkLabel": "dernière version",
|
||||
"theme.docs.versions.latestVersionSuggestionLabel": "Pour une documentation à jour, consultez la {latestVersionLink} ({versionLabel}).",
|
||||
"theme.docs.versions.unmaintainedVersionLabel": "Ceci est la documentation de {siteTitle} {versionLabel}, qui n'est plus activement maintenue.",
|
||||
"theme.docs.versions.unreleasedVersionLabel": "Ceci est la documentation de la prochaine version {versionLabel} de {siteTitle}.",
|
||||
"theme.lastUpdated.atDate": "le {date}",
|
||||
"theme.lastUpdated.byUser": "par {user}",
|
||||
"theme.lastUpdated.lastUpdatedAtBy": "Dernière mise à jour{atDate}{byUser}",
|
||||
"theme.tags.tagsListLabel": "Tags:",
|
||||
"theme.tags.tagsPageLink": "Voir tous les tags",
|
||||
"theme.tags.tagsPageTitle": "Tags"
|
||||
}
|
|
@ -1,44 +1,68 @@
|
|||
{
|
||||
"theme.NotFound.title": "Страница не найдена",
|
||||
"theme.AnnouncementBar.closeButtonAriaLabel": "Закрыть",
|
||||
"theme.CodeBlock.copied": "Скопировано",
|
||||
"theme.CodeBlock.copy": "Скопировать",
|
||||
"theme.CodeBlock.copyButtonAriaLabel": "Скопировать в буфер обмена",
|
||||
"theme.NotFound.p1": "К сожалению, мы не смогли найти запрашиваемую вами страницу.",
|
||||
"theme.NotFound.p2": "Пожалуйста, обратитесь к владельцу сайта, с которого вы перешли на эту ссылку, чтобы сообщить ему ссылка не работает.",
|
||||
"theme.AnnouncementBar.closeButtonAriaLabel": "Закрыть",
|
||||
"theme.NotFound.title": "Страница не найдена",
|
||||
"theme.Playground.liveEditor": "Интерактивный редактор",
|
||||
"theme.Playground.result": "Результат",
|
||||
"theme.PwaReloadPopup.closeButtonAriaLabel": "Закрыть",
|
||||
"theme.PwaReloadPopup.info": "Доступна новая версия",
|
||||
"theme.PwaReloadPopup.refreshButtonText": "Обновить",
|
||||
"theme.SearchBar.label": "Поиск",
|
||||
"theme.SearchPage.algoliaLabel": "Поиск предоставлен Algolia",
|
||||
"theme.SearchPage.emptyResultsTitle": "Поиск по сайту",
|
||||
"theme.SearchPage.existingResultsTitle": "Результаты поиска по запросу",
|
||||
"theme.SearchPage.fetchingNewResults": "Загрузка новых результатов поиска...",
|
||||
"theme.SearchPage.inputLabel": "Поиск",
|
||||
"theme.SearchPage.inputPlaceholder": "Введите фразу для поиска",
|
||||
"theme.SearchPage.noResultsText": "По запросу ничего не найдено",
|
||||
"theme.blog.paginator.navAriaLabel": "Навигация по странице списка блогов",
|
||||
"theme.blog.paginator.newerEntries": "Следующие записи",
|
||||
"theme.blog.paginator.olderEntries": "Предыдущие записи",
|
||||
"theme.blog.post.date": "{month} {day}, {year}",
|
||||
"theme.blog.post.nPosts": "{count} posts",
|
||||
"theme.blog.post.onePost": "One post",
|
||||
"theme.blog.post.paginator.navAriaLabel": "Навигация по странице поста блога",
|
||||
"theme.blog.post.paginator.newerPost": "Следующий пост",
|
||||
"theme.blog.post.paginator.olderPost": "Предыдущий пост",
|
||||
"theme.blog.post.readMore": "Читать дальше",
|
||||
"theme.tags.tagsPageLink": "Посмотреть все теги",
|
||||
"theme.tags.tagsPageTitle": "Теги",
|
||||
"theme.tags.tagsListLabel": "Теги:",
|
||||
"theme.CodeBlock.copyButtonAriaLabel": "Скопировать в буфер обмена",
|
||||
"theme.CodeBlock.copied": "Скопировано",
|
||||
"theme.CodeBlock.copy": "Скопировать",
|
||||
"theme.docs.paginator.navAriaLabel": "Навигация по странице документации",
|
||||
"theme.docs.paginator.previous": "Предыдущая страница",
|
||||
"theme.docs.paginator.next": "Следующая страница",
|
||||
"theme.docs.sidebar.expandButtonTitle": "Развернуть сайдбар",
|
||||
"theme.docs.sidebar.expandButtonAriaLabel": "Развернуть сайдбар",
|
||||
"theme.docs.sidebar.responsiveCloseButtonLabel": "Закрыть меню",
|
||||
"theme.docs.sidebar.responsiveOpenButtonLabel": "Открыть меню",
|
||||
"theme.docs.sidebar.collapseButtonTitle": "Свернуть сайдбар",
|
||||
"theme.docs.sidebar.collapseButtonAriaLabel": "Свернуть сайдбар",
|
||||
"theme.blog.post.readingTime": "{readingTime} min read",
|
||||
"theme.blog.tagTitle": "{nPosts} tagged with \"{tagName}\"",
|
||||
"theme.common.editThisPage": "Отредактировать эту страницу",
|
||||
"theme.common.headingLinkTitle": "Прямая ссылка на этот заголовок",
|
||||
"theme.common.month.april": "April",
|
||||
"theme.common.month.august": "August",
|
||||
"theme.common.month.december": "December",
|
||||
"theme.common.month.february": "February",
|
||||
"theme.common.month.january": "January",
|
||||
"theme.common.month.july": "July",
|
||||
"theme.common.month.june": "June",
|
||||
"theme.common.month.march": "March",
|
||||
"theme.common.month.may": "May",
|
||||
"theme.common.month.november": "November",
|
||||
"theme.common.month.october": "October",
|
||||
"theme.common.month.september": "September",
|
||||
"theme.common.skipToMainContent": "Перейти к основному содержимому",
|
||||
"theme.SearchPage.existingResultsTitle": "Результаты поиска по запросу",
|
||||
"theme.SearchPage.emptyResultsTitle": "Поиск по сайту",
|
||||
"theme.SearchPage.inputPlaceholder": "Введите фразу для поиска",
|
||||
"theme.SearchPage.inputLabel": "Поиск",
|
||||
"theme.SearchPage.algoliaLabel": "Поиск предоставлен Algolia",
|
||||
"theme.SearchPage.noResultsText": "По запросу ничего не найдено",
|
||||
"theme.SearchPage.fetchingNewResults": "Загрузка новых результатов поиска...",
|
||||
"theme.SearchBar.label": "Поиск",
|
||||
"theme.PwaReloadPopup.info": "Доступна новая версия",
|
||||
"theme.PwaReloadPopup.refreshButtonText": "Обновить",
|
||||
"theme.PwaReloadPopup.closeButtonAriaLabel": "Закрыть",
|
||||
"theme.Playground.liveEditor": "Интерактивный редактор",
|
||||
"theme.Playground.result": "Результат"
|
||||
}
|
||||
"theme.docs.paginator.navAriaLabel": "Навигация по странице документации",
|
||||
"theme.docs.paginator.next": "Следующая страница",
|
||||
"theme.docs.paginator.previous": "Предыдущая страница",
|
||||
"theme.docs.sidebar.collapseButtonAriaLabel": "Свернуть сайдбар",
|
||||
"theme.docs.sidebar.collapseButtonTitle": "Свернуть сайдбар",
|
||||
"theme.docs.sidebar.expandButtonAriaLabel": "Развернуть сайдбар",
|
||||
"theme.docs.sidebar.expandButtonTitle": "Развернуть сайдбар",
|
||||
"theme.docs.sidebar.responsiveCloseButtonLabel": "Закрыть меню",
|
||||
"theme.docs.sidebar.responsiveOpenButtonLabel": "Открыть меню",
|
||||
"theme.docs.versions.latestVersionLinkLabel": "latest version",
|
||||
"theme.docs.versions.latestVersionSuggestionLabel": "For up-to-date documentation, see the {latestVersionLink} ({versionLabel}).",
|
||||
"theme.docs.versions.unmaintainedVersionLabel": "This is documentation for {siteTitle} {versionLabel}, which is no longer actively maintained.",
|
||||
"theme.docs.versions.unreleasedVersionLabel": "This is unreleased documentation for {siteTitle} {versionLabel} version.",
|
||||
"theme.lastUpdated.atDate": "on {date}",
|
||||
"theme.lastUpdated.byUser": "by {user}",
|
||||
"theme.lastUpdated.lastUpdatedAtBy": "Last updated{atDate}{byUser}",
|
||||
"theme.tags.tagsListLabel": "Теги:",
|
||||
"theme.tags.tagsPageLink": "Посмотреть все теги",
|
||||
"theme.tags.tagsPageTitle": "Теги"
|
||||
}
|
|
@ -19,7 +19,8 @@
|
|||
"babel:lib": "cross-env BABEL_ENV=lib babel src -d lib --extensions \".tsx,.ts\" --ignore \"**/*.d.ts\" --copy-files",
|
||||
"babel:lib-next": "cross-env BABEL_ENV=lib-next babel src -d lib-next --extensions \".tsx,.ts\" --ignore \"**/*.d.ts\" --copy-files",
|
||||
"prettier": "prettier --config ../../.prettierrc --ignore-path ../../.prettierignore --write \"**/*.{js,ts,jsx,tsc}\"",
|
||||
"prettier:lib-next": "prettier --config ../../.prettierrc --write \"lib-next/**/*.{js,ts,jsx,tsc}\""
|
||||
"prettier:lib-next": "prettier --config ../../.prettierrc --write \"lib-next/**/*.{js,ts,jsx,tsc}\"",
|
||||
"update-code-translations": "node update-code-translations.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@docusaurus/core": "2.0.0-alpha.70",
|
||||
|
@ -45,7 +46,10 @@
|
|||
"prop-types": "^15.7.2",
|
||||
"react-router-dom": "^5.2.0",
|
||||
"react-toggle": "^4.1.1",
|
||||
"rtlcss": "^2.6.2"
|
||||
"rtlcss": "^2.6.2",
|
||||
"chalk": "^4.1.0",
|
||||
"fs-extra": "^9.1.0",
|
||||
"globby": "^11.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@docusaurus/module-type-aliases": "2.0.0-alpha.70"
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
import React from 'react';
|
||||
import clsx from 'clsx';
|
||||
import {MDXProvider} from '@mdx-js/react';
|
||||
import Translate from '@docusaurus/Translate';
|
||||
import Translate, {translate} from '@docusaurus/Translate';
|
||||
import Link from '@docusaurus/Link';
|
||||
import MDXComponents from '@theme/MDXComponents';
|
||||
import Seo from '@theme/Seo';
|
||||
|
@ -17,18 +17,66 @@ import type {Props} from '@theme/BlogPostItem';
|
|||
import styles from './styles.module.css';
|
||||
|
||||
const MONTHS = [
|
||||
'January',
|
||||
'February',
|
||||
'March',
|
||||
'April',
|
||||
'May',
|
||||
'June',
|
||||
'July',
|
||||
'August',
|
||||
'September',
|
||||
'October',
|
||||
'November',
|
||||
'December',
|
||||
translate({
|
||||
id: 'theme.common.month.january',
|
||||
description: 'January month translation',
|
||||
message: 'January',
|
||||
}),
|
||||
translate({
|
||||
id: 'theme.common.month.february',
|
||||
description: 'February month translation',
|
||||
message: 'February',
|
||||
}),
|
||||
translate({
|
||||
id: 'theme.common.month.march',
|
||||
description: 'March month translation',
|
||||
message: 'March',
|
||||
}),
|
||||
translate({
|
||||
id: 'theme.common.month.april',
|
||||
description: 'April month translation',
|
||||
message: 'April',
|
||||
}),
|
||||
translate({
|
||||
id: 'theme.common.month.may',
|
||||
description: 'May month translation',
|
||||
message: 'May',
|
||||
}),
|
||||
translate({
|
||||
id: 'theme.common.month.june',
|
||||
description: 'June month translation',
|
||||
message: 'June',
|
||||
}),
|
||||
translate({
|
||||
id: 'theme.common.month.july',
|
||||
description: 'July month translation',
|
||||
message: 'July',
|
||||
}),
|
||||
translate({
|
||||
id: 'theme.common.month.august',
|
||||
description: 'August month translation',
|
||||
message: 'August',
|
||||
}),
|
||||
translate({
|
||||
id: 'theme.common.month.september',
|
||||
description: 'September month translation',
|
||||
message: 'September',
|
||||
}),
|
||||
translate({
|
||||
id: 'theme.common.month.october',
|
||||
description: 'October month translation',
|
||||
message: 'October',
|
||||
}),
|
||||
translate({
|
||||
id: 'theme.common.month.november',
|
||||
description: 'November month translation',
|
||||
message: 'November',
|
||||
}),
|
||||
translate({
|
||||
id: 'theme.common.month.december',
|
||||
description: 'December month translation',
|
||||
message: 'December',
|
||||
}),
|
||||
];
|
||||
|
||||
function BlogPostItem(props: Props): JSX.Element {
|
||||
|
@ -62,8 +110,25 @@ function BlogPostItem(props: Props): JSX.Element {
|
|||
</TitleHeading>
|
||||
<div className="margin-vert--md">
|
||||
<time dateTime={date} className={styles.blogPostDate}>
|
||||
{month} {day}, {year}{' '}
|
||||
{readingTime && <> · {Math.ceil(readingTime)} min read</>}
|
||||
<Translate
|
||||
id="theme.blog.post.date"
|
||||
description="The label to display the blog post date"
|
||||
values={{day, month, year}}>
|
||||
{'{month} {day}, {year}'}
|
||||
</Translate>{' '}
|
||||
{readingTime && (
|
||||
<>
|
||||
{' · '}
|
||||
<Translate
|
||||
id="theme.blog.post.readingTime"
|
||||
description="The label to display reading time of the blog post"
|
||||
values={{
|
||||
readingTime: Math.ceil(readingTime),
|
||||
}}>
|
||||
{'{readingTime} min read'}
|
||||
</Translate>
|
||||
</>
|
||||
)}
|
||||
</time>
|
||||
</div>
|
||||
<div className="avatar margin-vert--md">
|
||||
|
|
|
@ -50,7 +50,6 @@ function BlogTagsListPage(props: Props): JSX.Element {
|
|||
))
|
||||
.filter((item) => item != null);
|
||||
|
||||
// TODO soon: translate hardcoded labels, but factorize them (blog + docs will both have tags)
|
||||
return (
|
||||
<Layout
|
||||
title="Tags"
|
||||
|
|
|
@ -12,17 +12,33 @@ import BlogPostItem from '@theme/BlogPostItem';
|
|||
import Link from '@docusaurus/Link';
|
||||
import type {Props} from '@theme/BlogTagsPostsPage';
|
||||
import BlogSidebar from '@theme/BlogSidebar';
|
||||
import Translate from '@docusaurus/Translate';
|
||||
import Translate, {translate} from '@docusaurus/Translate';
|
||||
|
||||
function pluralize(count: number, word: string) {
|
||||
return count > 1 ? `${word}s` : word;
|
||||
// Very simple pluralization: probably good enough for now
|
||||
function pluralizePosts(count: number): string {
|
||||
return count === 1
|
||||
? translate(
|
||||
{
|
||||
id: 'theme.blog.post.onePost',
|
||||
description: 'Label to describe one blog post',
|
||||
message: 'One post',
|
||||
},
|
||||
{count},
|
||||
)
|
||||
: translate(
|
||||
{
|
||||
id: 'theme.blog.post.nPosts',
|
||||
description: 'Label to describe multiple blog posts',
|
||||
message: '{count} posts',
|
||||
},
|
||||
{count},
|
||||
);
|
||||
}
|
||||
|
||||
function BlogTagsPostPage(props: Props): JSX.Element {
|
||||
const {metadata, items, sidebar} = props;
|
||||
const {allTagsPath, name: tagName, count} = metadata;
|
||||
|
||||
// TODO soon: translate hardcoded labels, but factorize them (blog + docs will both have tags)
|
||||
return (
|
||||
<Layout
|
||||
title={`Posts tagged "${tagName}"`}
|
||||
|
@ -35,8 +51,12 @@ function BlogTagsPostPage(props: Props): JSX.Element {
|
|||
</div>
|
||||
<main className="col col--8">
|
||||
<h1>
|
||||
{count} {pluralize(count, 'post')} tagged with "{tagName}
|
||||
"
|
||||
<Translate
|
||||
id="theme.blog.tagTitle"
|
||||
description="The title of the page for a blog tag"
|
||||
values={{nPosts: pluralizePosts(count), tagName}}>
|
||||
{'{nPosts} tagged with "{tagName}"'}
|
||||
</Translate>
|
||||
</h1>
|
||||
<Link href={allTagsPath}>
|
||||
<Translate
|
||||
|
|
|
@ -9,6 +9,7 @@ import React from 'react';
|
|||
import DocPaginator from '@theme/DocPaginator';
|
||||
import DocVersionSuggestions from '@theme/DocVersionSuggestions';
|
||||
import Seo from '@theme/Seo';
|
||||
import LastUpdated from '@theme/LastUpdated';
|
||||
import type {Props} from '@theme/DocItem';
|
||||
import TOC from '@theme/TOC';
|
||||
import EditThisPage from '@theme/EditThisPage';
|
||||
|
@ -78,42 +79,10 @@ function DocItem(props: Props): JSX.Element {
|
|||
{editUrl && <EditThisPage editUrl={editUrl} />}
|
||||
</div>
|
||||
{(lastUpdatedAt || lastUpdatedBy) && (
|
||||
<div className="col text--right">
|
||||
<em>
|
||||
<small>
|
||||
{/* TODO: wait for using interpolation in translation function */}
|
||||
Last updated{' '}
|
||||
{lastUpdatedAt && (
|
||||
<>
|
||||
on{' '}
|
||||
<time
|
||||
dateTime={new Date(
|
||||
lastUpdatedAt * 1000,
|
||||
).toISOString()}
|
||||
className={styles.docLastUpdatedAt}>
|
||||
{new Date(
|
||||
lastUpdatedAt * 1000,
|
||||
).toLocaleDateString()}
|
||||
</time>
|
||||
{lastUpdatedBy && ' '}
|
||||
</>
|
||||
)}
|
||||
{lastUpdatedBy && (
|
||||
<>
|
||||
by <strong>{lastUpdatedBy}</strong>
|
||||
</>
|
||||
)}
|
||||
{process.env.NODE_ENV === 'development' && (
|
||||
<div>
|
||||
<small>
|
||||
{' '}
|
||||
(Simulated during dev for better perf)
|
||||
</small>
|
||||
</div>
|
||||
)}
|
||||
</small>
|
||||
</em>
|
||||
</div>
|
||||
<LastUpdated
|
||||
lastUpdatedAt={lastUpdatedAt}
|
||||
lastUpdatedBy={lastUpdatedBy}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -26,7 +26,3 @@
|
|||
padding: 0 0.3rem;
|
||||
}
|
||||
}
|
||||
|
||||
.docLastUpdatedAt {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
import React from 'react';
|
||||
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
|
||||
import Link from '@docusaurus/Link';
|
||||
import Translate from '@docusaurus/Translate';
|
||||
import {
|
||||
useActivePlugin,
|
||||
useActiveVersion,
|
||||
|
@ -15,6 +16,84 @@ import {
|
|||
} from '@theme/hooks/useDocs';
|
||||
import {useDocsPreferredVersion} from '@docusaurus/theme-common';
|
||||
|
||||
function UnreleasedVersionLabel({
|
||||
siteTitle,
|
||||
versionLabel,
|
||||
}: {
|
||||
siteTitle: string;
|
||||
versionLabel: string;
|
||||
}) {
|
||||
return (
|
||||
<Translate
|
||||
id="theme.docs.versions.unreleasedVersionLabel"
|
||||
description="The label used to tell the user that he's browsing an unreleased doc version"
|
||||
values={{
|
||||
siteTitle,
|
||||
versionLabel: <strong>{versionLabel}</strong>,
|
||||
}}>
|
||||
{
|
||||
'This is unreleased documentation for {siteTitle} {versionLabel} version.'
|
||||
}
|
||||
</Translate>
|
||||
);
|
||||
}
|
||||
|
||||
function UnmaintainedVersionLabel({
|
||||
siteTitle,
|
||||
versionLabel,
|
||||
}: {
|
||||
siteTitle: string;
|
||||
versionLabel: string;
|
||||
}) {
|
||||
return (
|
||||
<Translate
|
||||
id="theme.docs.versions.unmaintainedVersionLabel"
|
||||
description="The label used to tell the user that he's browsing an unmaintained doc version"
|
||||
values={{
|
||||
siteTitle,
|
||||
versionLabel: <strong>{versionLabel}</strong>,
|
||||
}}>
|
||||
{
|
||||
'This is documentation for {siteTitle} {versionLabel}, which is no longer actively maintained.'
|
||||
}
|
||||
</Translate>
|
||||
);
|
||||
}
|
||||
|
||||
function LatestVersionSuggestionLabel({
|
||||
versionLabel,
|
||||
to,
|
||||
onClick,
|
||||
}: {
|
||||
to: string;
|
||||
onClick: () => void;
|
||||
versionLabel: string;
|
||||
}) {
|
||||
return (
|
||||
<Translate
|
||||
id="theme.docs.versions.latestVersionSuggestionLabel"
|
||||
description="The label userd to tell the user that he's browsing an unmaintained doc version"
|
||||
values={{
|
||||
versionLabel,
|
||||
latestVersionLink: (
|
||||
<strong>
|
||||
<Link to={to} onClick={onClick}>
|
||||
<Translate
|
||||
id="theme.docs.versions.latestVersionLinkLabel"
|
||||
description="The label used for the latest version suggestion link label">
|
||||
latest version
|
||||
</Translate>
|
||||
</Link>
|
||||
</strong>
|
||||
),
|
||||
}}>
|
||||
{
|
||||
'For up-to-date documentation, see the {latestVersionLink} ({versionLabel}).'
|
||||
}
|
||||
</Translate>
|
||||
);
|
||||
}
|
||||
|
||||
const getVersionMainDoc = (version) =>
|
||||
version.docs.find((doc) => doc.id === version.mainDocId);
|
||||
|
||||
|
@ -44,34 +123,25 @@ function DocVersionSuggestions(): JSX.Element {
|
|||
|
||||
return (
|
||||
<div className="alert alert--warning margin-bottom--md" role="alert">
|
||||
{
|
||||
// TODO need refactoring
|
||||
// TODO need translate after interpolation appears
|
||||
activeVersion.name === 'current' ? (
|
||||
<div>
|
||||
This is unreleased documentation for {siteTitle}{' '}
|
||||
<strong>{activeVersion.label}</strong> version.
|
||||
</div>
|
||||
<div>
|
||||
{activeVersion.name === 'current' ? (
|
||||
<UnreleasedVersionLabel
|
||||
siteTitle={siteTitle}
|
||||
versionLabel={activeVersion.label}
|
||||
/>
|
||||
) : (
|
||||
<div>
|
||||
This is documentation for {siteTitle}{' '}
|
||||
<strong>{activeVersion.label}</strong>, which is no longer actively
|
||||
maintained.
|
||||
</div>
|
||||
)
|
||||
}
|
||||
<UnmaintainedVersionLabel
|
||||
siteTitle={siteTitle}
|
||||
versionLabel={activeVersion.label}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="margin-top--md">
|
||||
For up-to-date documentation, see the{' '}
|
||||
<strong>
|
||||
<Link
|
||||
to={latestVersionSuggestedDoc.path}
|
||||
onClick={() =>
|
||||
savePreferredVersionName(latestVersionSuggestion.name)
|
||||
}>
|
||||
latest version
|
||||
</Link>
|
||||
</strong>{' '}
|
||||
({latestVersionSuggestion.label}).
|
||||
<LatestVersionSuggestionLabel
|
||||
versionLabel={latestVersionSuggestion.label}
|
||||
to={latestVersionSuggestedDoc.path}
|
||||
onClick={() => savePreferredVersionName(latestVersionSuggestion.name)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -0,0 +1,91 @@
|
|||
/**
|
||||
* 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 styles from './styles.module.css';
|
||||
import Translate from '@docusaurus/Translate';
|
||||
|
||||
function LastUpdatedAtDate({lastUpdatedAt}: {lastUpdatedAt: number}) {
|
||||
return (
|
||||
<Translate
|
||||
id="theme.lastUpdated.atDate"
|
||||
description="The words used to describe on which date a page has been last updated"
|
||||
values={{
|
||||
// TODO localize this date
|
||||
// If it's the only place we need this, we'd rather keep it simple
|
||||
// Day.js may be a good lightweight option?
|
||||
// https://www.skypack.dev/blog/2021/02/the-best-javascript-date-libraries/
|
||||
date: (
|
||||
<time
|
||||
dateTime={new Date(lastUpdatedAt * 1000).toISOString()}
|
||||
className={styles.lastUpdatedDate}>
|
||||
{new Date(lastUpdatedAt * 1000).toLocaleDateString()}
|
||||
</time>
|
||||
),
|
||||
}}>
|
||||
{'on {date}'}
|
||||
</Translate>
|
||||
);
|
||||
}
|
||||
|
||||
function LastUpdatedByUser({lastUpdatedBy}: {lastUpdatedBy: string}) {
|
||||
return (
|
||||
<Translate
|
||||
id="theme.lastUpdated.byUser"
|
||||
description="The words used to describe by who the page has been last updated"
|
||||
values={{
|
||||
user: <strong>{lastUpdatedBy}</strong>,
|
||||
}}>
|
||||
{'by {user}'}
|
||||
</Translate>
|
||||
);
|
||||
}
|
||||
|
||||
export default function LastUpdated({
|
||||
lastUpdatedAt,
|
||||
lastUpdatedBy,
|
||||
}: {
|
||||
lastUpdatedAt: number | undefined;
|
||||
lastUpdatedBy: string | undefined;
|
||||
}) {
|
||||
return (
|
||||
<div className="col text--right">
|
||||
<em>
|
||||
<small>
|
||||
<Translate
|
||||
id="theme.lastUpdated.lastUpdatedAtBy"
|
||||
description="The sentence used to display when a page has been last updated, and by who"
|
||||
values={{
|
||||
atDate: lastUpdatedAt ? (
|
||||
<>
|
||||
{' '}
|
||||
<LastUpdatedAtDate lastUpdatedAt={lastUpdatedAt} />
|
||||
</>
|
||||
) : (
|
||||
''
|
||||
),
|
||||
byUser: lastUpdatedBy ? (
|
||||
<>
|
||||
{' '}
|
||||
<LastUpdatedByUser lastUpdatedBy={lastUpdatedBy} />
|
||||
</>
|
||||
) : (
|
||||
''
|
||||
),
|
||||
}}>
|
||||
{'Last updated{atDate}{byUser}'}
|
||||
</Translate>
|
||||
{process.env.NODE_ENV === 'development' && (
|
||||
<div>
|
||||
<small> (Simulated during dev for better perf)</small>
|
||||
</div>
|
||||
)}
|
||||
</small>
|
||||
</em>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
.lastUpdatedDate {
|
||||
font-weight: bold;
|
||||
}
|
189
packages/docusaurus-theme-classic/update-code-translations.js
Normal file
189
packages/docusaurus-theme-classic/update-code-translations.js
Normal file
|
@ -0,0 +1,189 @@
|
|||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
const chalk = require('chalk');
|
||||
const path = require('path');
|
||||
const fs = require('fs-extra');
|
||||
const globby = require('globby');
|
||||
const {mapValues, difference} = require('lodash');
|
||||
|
||||
function sortObjectKeys(obj) {
|
||||
const keys = Object.keys(obj);
|
||||
keys.sort();
|
||||
return keys.reduce((acc, key) => {
|
||||
acc[key] = obj[key];
|
||||
return acc;
|
||||
}, {});
|
||||
}
|
||||
|
||||
function logSection(title) {
|
||||
console.log(``);
|
||||
console.log(``);
|
||||
console.log(`##############################`);
|
||||
console.log(`## ${chalk.blue(title)}`);
|
||||
}
|
||||
|
||||
function logKeys(keys) {
|
||||
return `Keys:\n- ${keys.join('\n- ')}\``;
|
||||
}
|
||||
|
||||
async function extractThemeCodeMessages() {
|
||||
// Unsafe import, should we create a package for the translationsExtractor ?
|
||||
const {
|
||||
globSourceCodeFilePaths,
|
||||
extractAllSourceCodeFileTranslations,
|
||||
} = require('@docusaurus/core/lib/server/translations/translationsExtractor');
|
||||
|
||||
const codeDirPaths = [path.join(__dirname, 'lib-next')];
|
||||
const filePaths = (
|
||||
await globSourceCodeFilePaths(codeDirPaths)
|
||||
).filter((filePath) => ['.js', '.jsx'].includes(path.extname(filePath)));
|
||||
|
||||
const filesExtractedTranslations = await extractAllSourceCodeFileTranslations(
|
||||
filePaths,
|
||||
{
|
||||
presets: [require.resolve('@docusaurus/core/lib/babel/preset')],
|
||||
},
|
||||
);
|
||||
|
||||
filesExtractedTranslations.forEach((fileExtractedTranslations) => {
|
||||
fileExtractedTranslations.warnings.forEach((warning) => {
|
||||
console.warn(chalk.yellow(warning));
|
||||
});
|
||||
});
|
||||
|
||||
const translations = filesExtractedTranslations.reduce(
|
||||
(acc, extractedTranslations) => {
|
||||
return {...acc, ...extractedTranslations.translations};
|
||||
},
|
||||
{},
|
||||
);
|
||||
|
||||
const translationMessages = mapValues(
|
||||
translations,
|
||||
(translation) => translation.message,
|
||||
);
|
||||
|
||||
return translationMessages;
|
||||
}
|
||||
|
||||
async function readMessagesFile(filePath) {
|
||||
return JSON.parse(await fs.readFile(filePath));
|
||||
}
|
||||
|
||||
async function writeMessagesFile(filePath, messages) {
|
||||
const sortedMessages = sortObjectKeys(messages);
|
||||
await fs.writeFile(filePath, JSON.stringify(sortedMessages, null, 2));
|
||||
console.log(
|
||||
`${path.basename(filePath)} updated (${
|
||||
Object.keys(sortedMessages).length
|
||||
} messages)`,
|
||||
);
|
||||
}
|
||||
|
||||
async function getCodeTranslationFiles() {
|
||||
const codeTranslationsDir = path.join(__dirname, 'codeTranslations');
|
||||
const baseFile = path.join(codeTranslationsDir, 'base.json');
|
||||
const localesFiles = (await globby(codeTranslationsDir)).filter(
|
||||
(filepath) =>
|
||||
path.extname(filepath) === '.json' && !filepath.endsWith('base.json'),
|
||||
);
|
||||
return {baseFile, localesFiles};
|
||||
}
|
||||
|
||||
async function updateBaseFile(baseFile) {
|
||||
const baseMessages = await readMessagesFile(baseFile);
|
||||
|
||||
const codeMessages = await extractThemeCodeMessages();
|
||||
|
||||
const unknownMessages = difference(
|
||||
Object.keys(baseMessages),
|
||||
Object.keys(codeMessages),
|
||||
);
|
||||
|
||||
if (unknownMessages.length) {
|
||||
console.log(
|
||||
chalk.red(`Some messages exist in base.json but were not found by the code extractor!
|
||||
They won't be removed automatically, so do the cleanup manually if necessary!
|
||||
${logKeys(unknownMessages)}`),
|
||||
);
|
||||
}
|
||||
|
||||
const newBaseMessages = {
|
||||
...baseMessages, // Ensure we don't automatically remove unknown messages
|
||||
...codeMessages,
|
||||
};
|
||||
|
||||
await writeMessagesFile(baseFile, newBaseMessages);
|
||||
|
||||
return newBaseMessages;
|
||||
}
|
||||
|
||||
async function updateLocaleCodeTranslations(localeFile, baseFileMessages) {
|
||||
const localeFileMessages = await readMessagesFile(localeFile);
|
||||
|
||||
const unknownMessages = difference(
|
||||
Object.keys(localeFileMessages),
|
||||
Object.keys(baseFileMessages),
|
||||
);
|
||||
|
||||
if (unknownMessages.length) {
|
||||
console.log(
|
||||
chalk.red(`Some localized messages do not exist in base.json!
|
||||
You may want to delete these!
|
||||
${logKeys(unknownMessages)}`),
|
||||
);
|
||||
}
|
||||
|
||||
const newLocaleFileMessages = {
|
||||
...baseFileMessages,
|
||||
...localeFileMessages,
|
||||
};
|
||||
|
||||
const untranslatedKeys = Object.entries(newLocaleFileMessages)
|
||||
.filter(([key, value]) => {
|
||||
return value === baseFileMessages[key];
|
||||
})
|
||||
.map(([key]) => key);
|
||||
|
||||
if (untranslatedKeys.length) {
|
||||
console.warn(
|
||||
chalk.yellow(`Some messages do not seem to be translated!
|
||||
${logKeys(untranslatedKeys)}`),
|
||||
);
|
||||
}
|
||||
|
||||
await writeMessagesFile(localeFile, newLocaleFileMessages);
|
||||
}
|
||||
|
||||
async function updateCodeTranslations() {
|
||||
logSection('Will update base file');
|
||||
const {baseFile, localesFiles} = await getCodeTranslationFiles();
|
||||
const baseFileMessages = await updateBaseFile(baseFile);
|
||||
|
||||
for (const localeFile of localesFiles) {
|
||||
logSection(`Will update ${path.basename(localeFile)}`);
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await updateLocaleCodeTranslations(localeFile, baseFileMessages);
|
||||
}
|
||||
}
|
||||
|
||||
updateCodeTranslations().then(
|
||||
() => {
|
||||
console.log('');
|
||||
console.log(chalk.green('updateCodeTranslations end'));
|
||||
console.log('');
|
||||
},
|
||||
(e) => {
|
||||
console.log('');
|
||||
console.error(chalk.red(`updateCodeTranslations failure: ${e.message}`));
|
||||
console.log('');
|
||||
console.error(e.stack);
|
||||
console.log('');
|
||||
process.exit(1);
|
||||
},
|
||||
);
|
106
packages/docusaurus/src/client/exports/Interpolate.tsx
Normal file
106
packages/docusaurus/src/client/exports/Interpolate.tsx
Normal file
|
@ -0,0 +1,106 @@
|
|||
/**
|
||||
* 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, {ReactNode} from 'react';
|
||||
|
||||
/*
|
||||
Minimal implementation of a React interpolate component.
|
||||
We don't ship a markdown parser nor a feature-complete i18n library on purpose.
|
||||
More details here: https://github.com/facebook/docusaurus/pull/4295
|
||||
*/
|
||||
|
||||
const ValueRegexp = /{\w+}/g;
|
||||
const ValueFoundMarker = '{}'; // does not care much
|
||||
|
||||
// TODO use TS template literal feature to make values typesafe!
|
||||
// (requires upgrading TS first)
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
type ExtractInterpolatePlaceholders<Str extends string> = string;
|
||||
|
||||
type InterpolateValues<Str extends string, Value extends ReactNode> = Record<
|
||||
ExtractInterpolatePlaceholders<Str>,
|
||||
Value
|
||||
>;
|
||||
|
||||
// TS function overload: if all the values are plain strings, then interpolate returns a simple string
|
||||
export function interpolate<Str extends string>(
|
||||
text: Str,
|
||||
values?: InterpolateValues<Str, string | number>,
|
||||
): string;
|
||||
|
||||
// If values contain any ReactNode, then the return is a ReactNode
|
||||
export function interpolate<Str extends string, Value extends ReactNode>(
|
||||
text: Str,
|
||||
values?: InterpolateValues<Str, Value>,
|
||||
): ReactNode;
|
||||
|
||||
export function interpolate<Str extends string, Value extends ReactNode>(
|
||||
text: Str,
|
||||
values?: InterpolateValues<Str, Value>,
|
||||
): ReactNode {
|
||||
const elements: (Value | string)[] = [];
|
||||
|
||||
const processedText = text.replace(ValueRegexp, (match: string) => {
|
||||
// remove {{ and }} around the placeholder
|
||||
const key = match.substr(
|
||||
1,
|
||||
match.length - 2,
|
||||
) as ExtractInterpolatePlaceholders<Str>;
|
||||
|
||||
const value = values?.[key];
|
||||
|
||||
if (value) {
|
||||
const element = React.isValidElement(value)
|
||||
? value
|
||||
: // For non-React elements: basic primitive->string conversion
|
||||
String(value);
|
||||
elements.push(element);
|
||||
return ValueFoundMarker;
|
||||
} else {
|
||||
return match; // no match? add warning?
|
||||
}
|
||||
});
|
||||
|
||||
// No interpolation to be done: just return the text
|
||||
if (elements.length === 0) {
|
||||
return text;
|
||||
}
|
||||
// Basic string interpolation: returns interpolated string
|
||||
else if (elements.every((el) => typeof el === 'string')) {
|
||||
return processedText
|
||||
.split(ValueFoundMarker)
|
||||
.reduce<string>((str, value, index) => {
|
||||
return str.concat(value).concat((elements[index] as string) ?? '');
|
||||
}, '');
|
||||
}
|
||||
// JSX interpolation: returns ReactNode
|
||||
else {
|
||||
return processedText
|
||||
.split(ValueFoundMarker)
|
||||
.reduce<ReactNode[]>((array, value, index) => {
|
||||
return [
|
||||
...array,
|
||||
<React.Fragment key={index}>
|
||||
{value}
|
||||
{elements[index]}
|
||||
</React.Fragment>,
|
||||
];
|
||||
}, []);
|
||||
}
|
||||
}
|
||||
|
||||
export type InterpolateProps<Str extends string> = {
|
||||
children: Str;
|
||||
values?: InterpolateValues<Str, ReactNode>;
|
||||
};
|
||||
|
||||
export default function Interpolate<Str extends string>({
|
||||
children,
|
||||
values,
|
||||
}: InterpolateProps<Str>) {
|
||||
return interpolate(children, values);
|
||||
}
|
|
@ -6,6 +6,11 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import Interpolate, {
|
||||
interpolate,
|
||||
InterpolateProps,
|
||||
InterpolateValues,
|
||||
} from '@docusaurus/Interpolate';
|
||||
|
||||
// Can't read it from context, due to exposing imperative API
|
||||
import codeTranslations from '@generated/codeTranslations';
|
||||
|
@ -20,29 +25,37 @@ function getLocalizedMessage({
|
|||
return codeTranslations[id ?? message] ?? message;
|
||||
}
|
||||
|
||||
export type TranslateParam = {
|
||||
message: string;
|
||||
export type TranslateParam<Str extends string> = {
|
||||
message: Str;
|
||||
id?: string;
|
||||
description?: string;
|
||||
values?: InterpolateValues<Str, string | number>;
|
||||
};
|
||||
// Imperative translation API is useful for some edge-cases:
|
||||
// - translating page titles (meta)
|
||||
// - translating string props (input placeholders, image alt, aria labels...)
|
||||
export function translate({message, id}: TranslateParam): string {
|
||||
const localizedMessage = getLocalizedMessage({message, id});
|
||||
return localizedMessage ?? message;
|
||||
export function translate<Str extends string>(
|
||||
{message, id}: TranslateParam<Str>,
|
||||
values?: InterpolateValues<Str, string | number>,
|
||||
): string {
|
||||
const localizedMessage = getLocalizedMessage({message, id}) ?? message;
|
||||
return interpolate(localizedMessage, values);
|
||||
}
|
||||
|
||||
export type TranslateProps = {
|
||||
children: string;
|
||||
export type TranslateProps<Str extends string> = InterpolateProps<Str> & {
|
||||
id?: string;
|
||||
description?: string;
|
||||
};
|
||||
|
||||
// Maybe we'll want to improve this component with additional features
|
||||
// Like toggling a translation mode that adds a little translation button near the text?
|
||||
export default function Translate({children, id}: TranslateProps): JSX.Element {
|
||||
export default function Translate<Str extends string>({
|
||||
children,
|
||||
id,
|
||||
values,
|
||||
}: TranslateProps<Str>): JSX.Element {
|
||||
const localizedMessage: string =
|
||||
getLocalizedMessage({message: children, id}) ?? children;
|
||||
return <>{localizedMessage}</>;
|
||||
|
||||
return <Interpolate values={values}>{localizedMessage}</Interpolate>;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,77 @@
|
|||
/**
|
||||
* 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 {interpolate} from '../Interpolate';
|
||||
|
||||
describe('Interpolate', () => {
|
||||
test('without placeholders', () => {
|
||||
const text = 'Hello how are you?';
|
||||
expect(interpolate(text)).toEqual(text);
|
||||
});
|
||||
|
||||
test('placeholders with string values', () => {
|
||||
const text = 'Hello {name} how are you {day}?';
|
||||
const values = {name: 'Sébastien', day: 'today'};
|
||||
expect(interpolate(text, values)).toMatchInlineSnapshot(
|
||||
`"Hello Sébastien how are you today?"`,
|
||||
);
|
||||
});
|
||||
|
||||
test('placeholders with string values', () => {
|
||||
const text = '{number} {string} {object} {array}';
|
||||
const values = {
|
||||
number: 42,
|
||||
string: 'Hello',
|
||||
object: {hello: 'world'},
|
||||
array: ['Hello'],
|
||||
};
|
||||
// Do we need to improve the JS type -> String conversion logic here?
|
||||
expect(interpolate(text, values)).toMatchInlineSnapshot(
|
||||
`"42 Hello [object Object] Hello"`,
|
||||
);
|
||||
});
|
||||
|
||||
test('placeholders with string values mismatch', () => {
|
||||
// Should we emit warnings in such case?
|
||||
const text = 'Hello {name} how are you {unprovidedValue}?';
|
||||
const values = {name: 'Sébastien', extraValue: 'today'};
|
||||
expect(interpolate(text, values)).toMatchInlineSnapshot(
|
||||
`"Hello Sébastien how are you {unprovidedValue}?"`,
|
||||
);
|
||||
});
|
||||
|
||||
test('placeholders with values not provided', () => {
|
||||
// Should we emit warnings in such case?
|
||||
const text = 'Hello {name} how are you {day}?';
|
||||
expect(interpolate(text)).toEqual(text);
|
||||
expect(interpolate(text, {})).toEqual(text);
|
||||
});
|
||||
|
||||
test('placeholders with JSX values', () => {
|
||||
const text = 'Hello {name} how are you {day}?';
|
||||
const values = {name: <b>Sébastien</b>, day: <span>today</span>};
|
||||
expect(interpolate(text, values)).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('placeholders with mixed vales', () => {
|
||||
const text = 'Hello {name} how are you {day}?';
|
||||
const values = {name: 'Sébastien', day: <span>today</span>};
|
||||
expect(interpolate(text, values)).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('acceptance test', () => {
|
||||
const text = 'Hello {name} how are you {day}? Another {unprovidedValue}!';
|
||||
const values = {
|
||||
name: 'Sébastien',
|
||||
day: <span>today</span>,
|
||||
extraUselessValue1: <div>test</div>,
|
||||
extraUselessValue2: 'hi',
|
||||
};
|
||||
expect(interpolate(text, values)).toMatchSnapshot();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,57 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Interpolate acceptance test 1`] = `
|
||||
Array [
|
||||
<React.Fragment>
|
||||
Hello
|
||||
Sébastien
|
||||
</React.Fragment>,
|
||||
<React.Fragment>
|
||||
how are you
|
||||
<span>
|
||||
today
|
||||
</span>
|
||||
</React.Fragment>,
|
||||
<React.Fragment>
|
||||
? Another {unprovidedValue}!
|
||||
</React.Fragment>,
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`Interpolate placeholders with JSX values 1`] = `
|
||||
Array [
|
||||
<React.Fragment>
|
||||
Hello
|
||||
<b>
|
||||
Sébastien
|
||||
</b>
|
||||
</React.Fragment>,
|
||||
<React.Fragment>
|
||||
how are you
|
||||
<span>
|
||||
today
|
||||
</span>
|
||||
</React.Fragment>,
|
||||
<React.Fragment>
|
||||
?
|
||||
</React.Fragment>,
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`Interpolate placeholders with mixed vales 1`] = `
|
||||
Array [
|
||||
<React.Fragment>
|
||||
Hello
|
||||
Sébastien
|
||||
</React.Fragment>,
|
||||
<React.Fragment>
|
||||
how are you
|
||||
<span>
|
||||
today
|
||||
</span>
|
||||
</React.Fragment>,
|
||||
<React.Fragment>
|
||||
?
|
||||
</React.Fragment>,
|
||||
]
|
||||
`;
|
|
@ -255,7 +255,11 @@ describe('extractPluginsSourceCodeTranslations', () => {
|
|||
export default function MyComponent() {
|
||||
return (
|
||||
<div>
|
||||
<input text={translate({id: 'plugin1Id1',message: 'plugin1 message 1',description: 'plugin1 description 1'})}/>
|
||||
<input
|
||||
text={translate(
|
||||
{id: 'plugin1Id1',message: 'plugin1 message 1',description: 'plugin1 description 1'},
|
||||
{someDynamicValue: 42}
|
||||
)}/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -46,6 +46,19 @@ function getPluginSourceCodeFilePaths(plugin: InitPlugin): string[] {
|
|||
return codePaths;
|
||||
}
|
||||
|
||||
export async function globSourceCodeFilePaths(
|
||||
dirPaths: string[],
|
||||
): Promise<string[]> {
|
||||
// Required for Windows support, as paths using \ should not be used by globby
|
||||
// (also using the windows hard drive prefix like c: is not a good idea)
|
||||
const globPaths = dirPaths.map((dirPath) =>
|
||||
posixPath(nodePath.relative(process.cwd(), dirPath)),
|
||||
);
|
||||
|
||||
const filePaths = await globby(globPaths);
|
||||
return filePaths.filter(isTranslatableSourceCodePath);
|
||||
}
|
||||
|
||||
async function getSourceCodeFilePaths(
|
||||
plugins: InitPlugin[],
|
||||
): Promise<string[]> {
|
||||
|
@ -54,15 +67,7 @@ async function getSourceCodeFilePaths(
|
|||
// Hacky/implicit, but do we want to introduce a new lifecycle method for that???
|
||||
const allPathsToWatch = flatten(plugins.map(getPluginSourceCodeFilePaths));
|
||||
|
||||
// Required for Windows support, as paths using \ should not be used by globby
|
||||
// (also using the windows hard drive prefix like c: is not a good idea)
|
||||
const allRelativePosixPathsToWatch = allPathsToWatch.map((path) =>
|
||||
posixPath(nodePath.relative(process.cwd(), path)),
|
||||
);
|
||||
|
||||
const filePaths = await globby(allRelativePosixPathsToWatch);
|
||||
|
||||
return filePaths.filter(isTranslatableSourceCodePath);
|
||||
return globSourceCodeFilePaths(allPathsToWatch);
|
||||
}
|
||||
|
||||
export async function extractPluginsSourceCodeTranslations(
|
||||
|
@ -109,7 +114,7 @@ type SourceCodeFileTranslations = {
|
|||
warnings: string[];
|
||||
};
|
||||
|
||||
async function extractAllSourceCodeFileTranslations(
|
||||
export async function extractAllSourceCodeFileTranslations(
|
||||
sourceCodeFilePaths: string[],
|
||||
babelOptions: TransformOptions,
|
||||
): Promise<SourceCodeFileTranslations[]> {
|
||||
|
@ -265,7 +270,10 @@ function extractSourceCodeAstTranslations(
|
|||
path.node.callee.name === 'translate'
|
||||
) {
|
||||
// console.log('CallExpression', path.node);
|
||||
if (path.node.arguments.length === 1) {
|
||||
if (
|
||||
path.node.arguments.length === 1 ||
|
||||
path.node.arguments.length === 2
|
||||
) {
|
||||
const firstArgPath = path.get('arguments.0') as NodePath;
|
||||
|
||||
// evaluation allows translate("x" + "y"); to be considered as translate("xy");
|
||||
|
@ -291,7 +299,7 @@ function extractSourceCodeAstTranslations(
|
|||
}
|
||||
} else {
|
||||
warnings.push(
|
||||
`translate() function only takes 1 arg\n${sourceFileWarningPart(
|
||||
`translate() function only takes 1 or 2 args\n${sourceFileWarningPart(
|
||||
path.node,
|
||||
)}\n${generateCode(path.node)}`,
|
||||
);
|
||||
|
|
|
@ -6,6 +6,7 @@ Object {
|
|||
"@docusaurus/ComponentCreator": "../../client/exports/ComponentCreator.tsx",
|
||||
"@docusaurus/ExecutionEnvironment": "../../client/exports/ExecutionEnvironment.ts",
|
||||
"@docusaurus/Head": "../../client/exports/Head.tsx",
|
||||
"@docusaurus/Interpolate": "../../client/exports/Interpolate.tsx",
|
||||
"@docusaurus/Link": "../../client/exports/Link.tsx",
|
||||
"@docusaurus/Noop": "../../client/exports/Noop.ts",
|
||||
"@docusaurus/Translate": "../../client/exports/Translate.tsx",
|
||||
|
|
|
@ -4,13 +4,11 @@ title: Team
|
|||
slug: /team
|
||||
---
|
||||
|
||||
import TeamProfileCard from '@site/src/components/TeamProfileCard';
|
||||
|
||||
export function TeamProfileCardCol(props) {
|
||||
return (
|
||||
<TeamProfileCard {...props} className={'col col--6 margin-bottom--lg'} />
|
||||
);
|
||||
}
|
||||
import {
|
||||
ActiveTeamRow,
|
||||
HonoraryAlumniTeamRow,
|
||||
StudentFellowsTeamRow,
|
||||
} from '@site/src/components/TeamProfileCards';
|
||||
|
||||
## Active Team
|
||||
|
||||
|
@ -18,129 +16,19 @@ The Docusaurus team works on the core functionality, plugins for the classic the
|
|||
|
||||
Current members of the Docusaurus team are listed in alphabetical order below.
|
||||
|
||||
<div className="row">
|
||||
<TeamProfileCardCol
|
||||
name="Alexey Pyltsyn"
|
||||
githubUrl="https://github.com/lex111">
|
||||
Obsessed open-source enthusiast 👋 Eternal amateur at everything 🤷♂️
|
||||
Maintainer of Russian docs on PHP, React, Kubernetes and much more 🧐
|
||||
</TeamProfileCardCol>
|
||||
<TeamProfileCardCol
|
||||
name="Joel Marcey"
|
||||
githubUrl="https://github.com/JoelMarcey"
|
||||
twitterUrl="https://twitter.com/joelmarcey">
|
||||
Docusaurus founder and now ever grateful Docusaurus cheerleader to those who
|
||||
actually write code for it.
|
||||
</TeamProfileCardCol>
|
||||
<TeamProfileCardCol
|
||||
name="Sébastien Lorber"
|
||||
githubUrl="https://github.com/slorber"
|
||||
twitterUrl="https://twitter.com/sebastienlorber">
|
||||
React lover since 2014. Freelance, helping Facebook ship Docusaurus v2. He
|
||||
writes regularly, on his{' '}
|
||||
<a href="https://sebastienlorber.com/" target="_blank">
|
||||
website
|
||||
</a>{' '}
|
||||
and{' '}
|
||||
<a href="https://dev.to/sebastienlorber" target="_blank">
|
||||
Dev.to
|
||||
</a>
|
||||
.
|
||||
</TeamProfileCardCol>
|
||||
<TeamProfileCardCol
|
||||
name="Yangshun Tay"
|
||||
githubUrl="https://github.com/yangshun"
|
||||
twitterUrl="https://twitter.com/yangshunz">
|
||||
Full Front End Stack developer who likes working on the Jamstack. Working on
|
||||
Docusaurus made him Facebook's unofficial part-time Open Source webmaster,
|
||||
which is an awesome role to be in.
|
||||
</TeamProfileCardCol>
|
||||
</div>
|
||||
<ActiveTeamRow />
|
||||
|
||||
## Honorary Alumni
|
||||
|
||||
Docusaurus would never be what it is today without the huge contributions from these folks who have moved on to bigger and greater things.
|
||||
|
||||
<div className="row">
|
||||
<TeamProfileCardCol
|
||||
name="Endilie Yacop Sucipto"
|
||||
githubUrl="https://github.com/endiliey"
|
||||
twitterUrl="https://twitter.com/endiliey">
|
||||
Maintainer @docusaurus · 🔥🔥🔥
|
||||
</TeamProfileCardCol>
|
||||
<TeamProfileCardCol
|
||||
name="Wei Gao"
|
||||
githubUrl="https://github.com/wgao19"
|
||||
twitterUrl="https://twitter.com/wgao19">
|
||||
🏻🌾 Work in progress React developer, maintains Docusaurus, writes docs
|
||||
and spams this world with many websites.
|
||||
</TeamProfileCardCol>
|
||||
</div>
|
||||
<HonoraryAlumniTeamRow />
|
||||
|
||||
## Student Fellows
|
||||
|
||||
A handful of students have also worked on Docusaurus as part of their school term/internship and the [Major League Hacking Fellowship program](https://fellowship.mlh.io/), contributing amazing features such as plugin options validation, migration tooling, and a Bootstrap theme.
|
||||
|
||||
<div className="row">
|
||||
<TeamProfileCardCol
|
||||
name="Anshul Goyal"
|
||||
githubUrl="https://github.com/anshulrgoyal"
|
||||
twitterUrl="https://twitter.com/ar_goyal">
|
||||
Fullstack developer who loves to code and try new technologies. In his free
|
||||
time, he contributes to open source, writes blog posts on his{' '}
|
||||
<a href="https://anshulgoyal.dev/" target="_blank">
|
||||
website
|
||||
</a>{' '}
|
||||
and watches Anime.
|
||||
</TeamProfileCardCol>
|
||||
<TeamProfileCardCol
|
||||
name="Drew Alexander"
|
||||
githubUrl="https://github.com/drewbi">
|
||||
Developer and Creative, trying to gain the skills to build whatever he can
|
||||
think of.
|
||||
</TeamProfileCardCol>
|
||||
<TeamProfileCardCol
|
||||
name="Fanny Vieira"
|
||||
githubUrl="https://github.com/fanny"
|
||||
twitterUrl="https://twitter.com/fannyvieiira">
|
||||
Fanny got started with web development in high school, building a project
|
||||
for the school kitchen. In her free time she loves contributing to Open
|
||||
Source, occasionally writing on{' '}
|
||||
<a href="https://dev.to/fannyvieira" target="_blank">
|
||||
her blog
|
||||
</a>{' '}
|
||||
about her experiences, cooking, and creating{' '}
|
||||
<a href="https://open.spotify.com/user/anotherfanny" target="_blank">
|
||||
Spotify playlists
|
||||
</a>
|
||||
.
|
||||
</TeamProfileCardCol>
|
||||
<TeamProfileCardCol
|
||||
name="Sam Zhou"
|
||||
githubUrl="https://github.com/SamChou19815"
|
||||
twitterUrl="https://twitter.com/SamChou19815">
|
||||
Sam started programming in 2011 and built his{' '}
|
||||
<a href="https://developersam.com">website</a> in 2015. He is interested in
|
||||
programming languages, dev infra and web development, and has built his own{' '}
|
||||
<a href="https://samlang.developersam.com/">programming language</a> and{' '}
|
||||
<a href="https://github.com/SamChou19815/mini-react">mini React</a>.
|
||||
</TeamProfileCardCol>
|
||||
<TeamProfileCardCol
|
||||
name="Tan Teik Jun"
|
||||
githubUrl="https://github.com/teikjun"
|
||||
twitterUrl="https://twitter.com/teik_jun">
|
||||
Open-source enthusiast who aims to become as awesome as the other humans on
|
||||
this page. Working on Docusaurus brought him closer to his goal. 🌱
|
||||
</TeamProfileCardCol>
|
||||
<TeamProfileCardCol
|
||||
name="Nisarag Bhatt"
|
||||
githubUrl="https://github.com/FocalChord"
|
||||
twitterUrl="https://twitter.com/focalchord_">
|
||||
Fullstack web developer who loves learning new technologies and applying
|
||||
them! Loves contributing to open source as well as writing content articles
|
||||
and tutorials.
|
||||
</TeamProfileCardCol>
|
||||
</div>
|
||||
<StudentFellowsTeamRow />
|
||||
|
||||
## Acknowledgements
|
||||
|
||||
|
|
|
@ -125,9 +125,44 @@ const MyComponent = () => {
|
|||
};
|
||||
```
|
||||
|
||||
### `<Interpolate/>`
|
||||
|
||||
A simple interpolation component for text containing dynamic placeholders.
|
||||
|
||||
The placeholders will be replaced with the provided dynamic values and JSX elements of your choice (strings, links, styled elements...).
|
||||
|
||||
#### Props
|
||||
|
||||
- `children`: text containing interpolation placeholders like `{placeholderName}`
|
||||
- `values`: object containing interpolation placeholder values
|
||||
|
||||
```jsx
|
||||
import React from 'react';
|
||||
import Link from '@docusaurus/Link';
|
||||
import Interpolate from '@docusaurus/Interpolate';
|
||||
|
||||
export default function VisitMyWebsiteMessage() {
|
||||
return (
|
||||
// highlight-start
|
||||
<Interpolate
|
||||
values={{
|
||||
firstName: 'Sébastien',
|
||||
website: (
|
||||
<Link to="https://docusaurus.io" className="my-website-class">
|
||||
website
|
||||
</Link>
|
||||
),
|
||||
}}>
|
||||
{'Hello, {firstName}! How are you? Take a look at my {website}'}
|
||||
</Interpolate>
|
||||
// highlight-end
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### `<Translate/>`
|
||||
|
||||
When [localizing your site](./i18n/i18n-introduction.md), the `<Translate/>` component will allow providing **translation support to React components**, such as your homepage.
|
||||
When [localizing your site](./i18n/i18n-introduction.md), the `<Translate/>` component will allow providing **translation support to React components**, such as your homepage. The `<Translate>` component supports [interpolation](#interpolate).
|
||||
|
||||
The translation strings will be extracted from your code with the [`docusaurus write-translations`](./cli.md#docusaurus-write-translations) CLI and create a `code.json` translation file in `website/i18n/<locale>`.
|
||||
|
||||
|
@ -135,15 +170,16 @@ The translation strings will be extracted from your code with the [`docusaurus w
|
|||
|
||||
The `<Translate/>` props **must be hardcoded strings**.
|
||||
|
||||
It is **not possible to use variables**, or the extraction wouldn't work.
|
||||
Apart the `values` prop used for interpolation, it is **not possible to use variables**, or the static extraction wouldn't work.
|
||||
|
||||
:::
|
||||
|
||||
#### Props
|
||||
|
||||
- `children`: untranslated string in the default site locale`
|
||||
- `children`: untranslated string in the default site locale (can contain [interpolation placeholders](#interpolate))
|
||||
- `id`: optional value to use as key in JSON translation files
|
||||
- `description`: optional text to help the translator
|
||||
- `values`: optional object containing interpolation placeholder values
|
||||
|
||||
#### Example
|
||||
|
||||
|
@ -169,7 +205,9 @@ export default function Home() {
|
|||
</h1>
|
||||
<main>
|
||||
{/* highlight-start */}
|
||||
<Translate>My website content</Translate>
|
||||
<Translate values={{firstName: 'Sébastien'}}>
|
||||
{'Welcome, {firstName}! How are you?'}
|
||||
</Translate>
|
||||
{/* highlight-end */}
|
||||
</main>
|
||||
</Layout>
|
||||
|
@ -378,19 +416,58 @@ const MyComponent = () => {
|
|||
|
||||
## Functions
|
||||
|
||||
### `interpolate`
|
||||
|
||||
The imperative counterpart of the [`<Interpolate>`](#interpolate) component.
|
||||
|
||||
#### Signature
|
||||
|
||||
```ts
|
||||
// Simple string interpolation
|
||||
function interpolate(text: string, values: Record<string, string>): string;
|
||||
|
||||
// JSX interpolation
|
||||
function interpolate(
|
||||
text: string,
|
||||
values: Record<string, ReactNode>,
|
||||
): ReactNode;
|
||||
```
|
||||
|
||||
#### Example
|
||||
|
||||
```jsx
|
||||
// highlight-start
|
||||
import {interpolate} from '@docusaurus/Interpolate';
|
||||
// highlight-end
|
||||
|
||||
const message = interpolate('Welcome {firstName}', {firstName: 'Sébastien'});
|
||||
```
|
||||
|
||||
### `translate`
|
||||
|
||||
The imperative counterpart of the [`<Translate>`](#translate) component.
|
||||
The imperative counterpart of the [`<Translate>`](#translate) component. Also supporting [placeholders interpolation](#interpolate).
|
||||
|
||||
:::tip
|
||||
|
||||
Use the imperative API for the **rare cases** when a **component cannot be used**, such as:
|
||||
Use the imperative API for the **rare cases** where a **component cannot be used**, such as:
|
||||
|
||||
- the `placeholder` props of form input
|
||||
- the page `title` metadata
|
||||
- the `placeholder` props of form inputs
|
||||
- the `aria-label` props for accessibility
|
||||
|
||||
:::
|
||||
|
||||
#### Signature
|
||||
|
||||
```ts
|
||||
function translate(
|
||||
translation: {message: string; id?: string; description?: string},
|
||||
values: Record<string, string>,
|
||||
): string;
|
||||
```
|
||||
|
||||
#### Example
|
||||
|
||||
```jsx title="src/index.js"
|
||||
import React from 'react';
|
||||
import Layout from '@theme/Layout';
|
||||
|
@ -406,16 +483,19 @@ export default function Home() {
|
|||
title={translate({message: 'My page meta title'})}
|
||||
// highlight-end
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
placeholder={
|
||||
<img
|
||||
src={'https://docusaurus.io/logo.png'}
|
||||
aria-label={
|
||||
// highlight-start
|
||||
translate({
|
||||
message: 'Some input placeholder',
|
||||
// Optional
|
||||
id: 'homepage.input.placeholder',
|
||||
description: 'The homepage input placeholder',
|
||||
})
|
||||
translate(
|
||||
{
|
||||
message: 'The logo of site {siteName}',
|
||||
// Optional
|
||||
id: 'homepage.logo.ariaLabel',
|
||||
description: 'The home page logo aria label',
|
||||
},
|
||||
{siteName: 'Docusaurus'},
|
||||
)
|
||||
// highlight-end
|
||||
}
|
||||
/>
|
||||
|
|
|
@ -5,7 +5,7 @@ sidebar_label: Introduction
|
|||
slug: /i18n/introduction
|
||||
---
|
||||
|
||||
It is possible to translate a Docusaurus website through its internationalization support (abbreviated as [i18n](https://en.wikipedia.org/wiki/Internationalization_and_localization)).
|
||||
It is **easy to translate a Docusaurus website** with its internationalization ([i18n](https://en.wikipedia.org/wiki/Internationalization_and_localization)) support.
|
||||
|
||||
:::caution
|
||||
|
||||
|
@ -15,7 +15,7 @@ i18n is a new feature (released early 2021), please report any bug you find.
|
|||
|
||||
## Goals
|
||||
|
||||
This section should help you understand the design decisions behind the Docusaurus i18n support.
|
||||
It is important to understand the **design decisions** behind the Docusaurus i18n support.
|
||||
|
||||
For more context, you can read the initial [RFC](https://github.com/facebook/docusaurus/issues/3317) and [PR](https://github.com/facebook/docusaurus/pull/3325).
|
||||
|
||||
|
@ -27,13 +27,14 @@ The goals of the Docusaurus i18n system are:
|
|||
- **Flexible translation workflows**: based on Git (monorepo, forks or submodules), SaaS software, FTP...
|
||||
- **Flexible deployment options**: single or multiple domains.
|
||||
- **Modular**: allow plugin author to provide i18n support.
|
||||
- **Low-overhead runtime**: static json/markdown content does not require a heavy i18n JS library.
|
||||
- **Low-overhead runtime**: documentation is mostly static and does not require a heavy JS library or polyfills.
|
||||
- **Acceptable build-times**: allow building and deploying localized sites independently.
|
||||
- **Localize assets**: an image of your site might contain text that should be translated.
|
||||
- **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.
|
||||
- **Default translations**: theme labels are translated for you in [many languages](https://github.com/facebook/docusaurus/tree/master/packages/docusaurus-theme-classic/codeTranslations).
|
||||
|
||||
### i18n goals (TODO)
|
||||
|
||||
|
|
|
@ -65,7 +65,17 @@ Start your localized site in dev mode, using the locale of your choice:
|
|||
npm run start -- --locale fr
|
||||
```
|
||||
|
||||
Your site is accessible at **`http://localhost:3000/fr/`**, but **falls back to untranslated content**.
|
||||
Your site is accessible at **`http://localhost:3000/fr/`**
|
||||
|
||||
We haven't provided any translation, and the site is **mostly untranslated**.
|
||||
|
||||
:::tip
|
||||
|
||||
Docusaurus provides **default translations** for generic theme labels, such as "Next" and "Previous" for the pagination.
|
||||
|
||||
Please help us complete those **[default translations](https://github.com/facebook/docusaurus/tree/master/packages/docusaurus-theme-classic/codeTranslations)**.
|
||||
|
||||
:::
|
||||
|
||||
:::caution
|
||||
|
||||
|
@ -94,6 +104,8 @@ Open the homepage, and use the [translation APIs](../docusaurus-core.md#translat
|
|||
```jsx title="src/index.js"
|
||||
import React from 'react';
|
||||
import Layout from '@theme/Layout';
|
||||
import Link from '@docusaurus/Link';
|
||||
|
||||
// highlight-start
|
||||
import Translate, {translate} from '@docusaurus/Translate';
|
||||
// highlight-end
|
||||
|
@ -103,13 +115,19 @@ export default function Home() {
|
|||
<Layout>
|
||||
<h1>
|
||||
{/* highlight-start */}
|
||||
<Translate description="The homepage welcome message">
|
||||
Welcome to my website
|
||||
</Translate>
|
||||
<Translate>Welcome to my website</Translate>
|
||||
{/* highlight-end */}
|
||||
</h1>
|
||||
<main>
|
||||
{/* highlight-start */}
|
||||
<Translate
|
||||
id="homepage.visitMyBlog"
|
||||
description="The homepage message to ask the user to visit my blog"
|
||||
values={{blog: <Link to="https://docusaurus.io/blog">blog</Link>}}>
|
||||
{'You can also visit my {blog}'}
|
||||
</Translate>
|
||||
{/* highlight-end */}
|
||||
|
||||
<div>
|
||||
<input
|
||||
type="text"
|
||||
placeholder={
|
||||
|
@ -121,7 +139,7 @@ export default function Home() {
|
|||
// highlight-end
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</main>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
@ -129,7 +147,9 @@ export default function Home() {
|
|||
|
||||
:::caution
|
||||
|
||||
Docusaurus provides a **very simple and lightweight translation runtime**: documentation websites generally don't need advanced i18n features.
|
||||
Docusaurus provides a **very small and lightweight translation runtime** on purpose, and only supports basic [placeholders interpolation](../docusaurus-core.md#interpolate), using a subset of the [ICU Message Format](https://formatjs.io/docs/core-concepts/icu-syntax/).
|
||||
|
||||
Most documentation websites are generally **static** and don't need advanced i18n features (**plurals**, **genders**...). Use a library like [react-intl](https://www.npmjs.com/package/react-intl) for more advanced use-cases.
|
||||
|
||||
:::
|
||||
|
||||
|
|
|
@ -1,50 +0,0 @@
|
|||
/**
|
||||
* 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';
|
||||
|
||||
export default function TeamProfileCard({
|
||||
className,
|
||||
name,
|
||||
children,
|
||||
githubUrl,
|
||||
twitterUrl,
|
||||
}) {
|
||||
return (
|
||||
<div className={className}>
|
||||
<div className="card card--full-height">
|
||||
<div className="card__header">
|
||||
<div className="avatar avatar--vertical">
|
||||
<img
|
||||
className="avatar__photo avatar__photo--xl"
|
||||
src={githubUrl + '.png'}
|
||||
alt={`${name}'s avatar`}
|
||||
/>
|
||||
<div className="avatar__intro">
|
||||
<h3 className="avatar__name">{name}</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="card__body">{children}</div>
|
||||
<div className="card__footer">
|
||||
<div className="button-group button-group--block">
|
||||
{githubUrl && (
|
||||
<a className="button button--secondary" href={githubUrl}>
|
||||
GitHub
|
||||
</a>
|
||||
)}
|
||||
{twitterUrl && (
|
||||
<a className="button button--secondary" href={twitterUrl}>
|
||||
Twitter
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
201
website/src/components/TeamProfileCards/index.js
Normal file
201
website/src/components/TeamProfileCards/index.js
Normal file
|
@ -0,0 +1,201 @@
|
|||
/**
|
||||
* 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 Translate from '@docusaurus/Translate';
|
||||
import Link from '@docusaurus/Link';
|
||||
|
||||
function WebsiteLink({to, children}) {
|
||||
return (
|
||||
<Link to={to}>
|
||||
{children || (
|
||||
<Translate id="team.profile.websiteLinkLabel">website</Translate>
|
||||
)}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
function TeamProfileCard({className, name, children, githubUrl, twitterUrl}) {
|
||||
return (
|
||||
<div className={className}>
|
||||
<div className="card card--full-height">
|
||||
<div className="card__header">
|
||||
<div className="avatar avatar--vertical">
|
||||
<img
|
||||
className="avatar__photo avatar__photo--xl"
|
||||
src={githubUrl + '.png'}
|
||||
alt={`${name}'s avatar`}
|
||||
/>
|
||||
<div className="avatar__intro">
|
||||
<h3 className="avatar__name">{name}</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="card__body">{children}</div>
|
||||
<div className="card__footer">
|
||||
<div className="button-group button-group--block">
|
||||
{githubUrl && (
|
||||
<a className="button button--secondary" href={githubUrl}>
|
||||
GitHub
|
||||
</a>
|
||||
)}
|
||||
{twitterUrl && (
|
||||
<a className="button button--secondary" href={twitterUrl}>
|
||||
Twitter
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function TeamProfileCardCol(props) {
|
||||
return (
|
||||
<TeamProfileCard {...props} className={'col col--6 margin-bottom--lg'} />
|
||||
);
|
||||
}
|
||||
|
||||
export function ActiveTeamRow() {
|
||||
return (
|
||||
<div className="row">
|
||||
<TeamProfileCardCol
|
||||
name="Alexey Pyltsyn"
|
||||
githubUrl="https://github.com/lex111">
|
||||
<Translate id="team.profile.Alexey Pyltsyn.body">
|
||||
Obsessed open-source enthusiast 👋 Eternal amateur at everything 🤷♂️
|
||||
Maintainer of Russian docs on PHP, React, Kubernetes and much more 🧐
|
||||
</Translate>
|
||||
</TeamProfileCardCol>
|
||||
<TeamProfileCardCol
|
||||
name="Joel Marcey"
|
||||
githubUrl="https://github.com/JoelMarcey"
|
||||
twitterUrl="https://twitter.com/joelmarcey">
|
||||
<Translate id="team.profile.Joel Marcey.body">
|
||||
Docusaurus founder and now ever grateful Docusaurus cheerleader to
|
||||
those who actually write code for it.
|
||||
</Translate>
|
||||
</TeamProfileCardCol>
|
||||
<TeamProfileCardCol
|
||||
name="Sébastien Lorber"
|
||||
githubUrl="https://github.com/slorber"
|
||||
twitterUrl="https://twitter.com/sebastienlorber">
|
||||
<Translate
|
||||
id="team.profile.Sebastien Lorber.body"
|
||||
values={{
|
||||
website: <WebsiteLink to="https://sebastienlorber.com/" />,
|
||||
devto: <Link to="https://dev.to/sebastienlorber">Dev.to</Link>,
|
||||
}}>
|
||||
{
|
||||
'React lover since 2014. Freelance, helping Facebook ship Docusaurus v2. He writes regularly, on his {website} and {devto}.'
|
||||
}
|
||||
</Translate>
|
||||
</TeamProfileCardCol>
|
||||
<TeamProfileCardCol
|
||||
name="Yangshun Tay"
|
||||
githubUrl="https://github.com/yangshun"
|
||||
twitterUrl="https://twitter.com/yangshunz">
|
||||
<Translate id="team.profile.Yangshun Tay.body">
|
||||
Full Front End Stack developer who likes working on the Jamstack.
|
||||
Working on Docusaurus made him Facebook's unofficial part-time Open
|
||||
Source webmaster, which is an awesome role to be in.
|
||||
</Translate>
|
||||
</TeamProfileCardCol>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function HonoraryAlumniTeamRow() {
|
||||
return (
|
||||
<div className="row">
|
||||
<TeamProfileCardCol
|
||||
name="Endilie Yacop Sucipto"
|
||||
githubUrl="https://github.com/endiliey"
|
||||
twitterUrl="https://twitter.com/endiliey">
|
||||
<Translate id="team.profile.Endilie Yacop Sucipto.body">
|
||||
Maintainer @docusaurus · 🔥🔥🔥
|
||||
</Translate>
|
||||
</TeamProfileCardCol>
|
||||
<TeamProfileCardCol
|
||||
name="Wei Gao"
|
||||
githubUrl="https://github.com/wgao19"
|
||||
twitterUrl="https://twitter.com/wgao19">
|
||||
<Translate id="team.profile.Wei Gao.body">
|
||||
🏻🌾 Work in progress React developer, maintains Docusaurus, writes
|
||||
docs and spams this world with many websites.
|
||||
</Translate>
|
||||
</TeamProfileCardCol>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function StudentFellowsTeamRow() {
|
||||
return (
|
||||
<div className="row">
|
||||
<TeamProfileCardCol
|
||||
name="Anshul Goyal"
|
||||
githubUrl="https://github.com/anshulrgoyal"
|
||||
twitterUrl="https://twitter.com/ar_goyal">
|
||||
Fullstack developer who loves to code and try new technologies. In his
|
||||
free time, he contributes to open source, writes blog posts on his{' '}
|
||||
<a href="https://anshulgoyal.dev/" target="_blank">
|
||||
website
|
||||
</a>{' '}
|
||||
and watches Anime.
|
||||
</TeamProfileCardCol>
|
||||
<TeamProfileCardCol
|
||||
name="Drew Alexander"
|
||||
githubUrl="https://github.com/drewbi">
|
||||
Developer and Creative, trying to gain the skills to build whatever he
|
||||
can think of.
|
||||
</TeamProfileCardCol>
|
||||
<TeamProfileCardCol
|
||||
name="Fanny Vieira"
|
||||
githubUrl="https://github.com/fanny"
|
||||
twitterUrl="https://twitter.com/fannyvieiira">
|
||||
Fanny got started with web development in high school, building a
|
||||
project for the school kitchen. In her free time she loves contributing
|
||||
to Open Source, occasionally writing on{' '}
|
||||
<a href="https://dev.to/fannyvieira" target="_blank">
|
||||
her blog
|
||||
</a>{' '}
|
||||
about her experiences, cooking, and creating{' '}
|
||||
<a href="https://open.spotify.com/user/anotherfanny" target="_blank">
|
||||
Spotify playlists
|
||||
</a>
|
||||
.
|
||||
</TeamProfileCardCol>
|
||||
<TeamProfileCardCol
|
||||
name="Sam Zhou"
|
||||
githubUrl="https://github.com/SamChou19815"
|
||||
twitterUrl="https://twitter.com/SamChou19815">
|
||||
Sam started programming in 2011 and built his{' '}
|
||||
<a href="https://developersam.com">website</a> in 2015. He is interested
|
||||
in programming languages, dev infra and web development, and has built
|
||||
his own{' '}
|
||||
<a href="https://samlang.developersam.com/">programming language</a> and{' '}
|
||||
<a href="https://github.com/SamChou19815/mini-react">mini React</a>.
|
||||
</TeamProfileCardCol>
|
||||
<TeamProfileCardCol
|
||||
name="Tan Teik Jun"
|
||||
githubUrl="https://github.com/teikjun"
|
||||
twitterUrl="https://twitter.com/teik_jun">
|
||||
Open-source enthusiast who aims to become as awesome as the other humans
|
||||
on this page. Working on Docusaurus brought him closer to his goal. 🌱
|
||||
</TeamProfileCardCol>
|
||||
<TeamProfileCardCol
|
||||
name="Nisarag Bhatt"
|
||||
githubUrl="https://github.com/FocalChord"
|
||||
twitterUrl="https://twitter.com/focalchord_">
|
||||
Fullstack web developer who loves learning new technologies and applying
|
||||
them! Loves contributing to open source as well as writing content
|
||||
articles and tutorials.
|
||||
</TeamProfileCardCol>
|
||||
</div>
|
||||
);
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue