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:
Sébastien Lorber 2021-02-26 13:19:51 +01:00 committed by GitHub
parent cdcd0f05d4
commit 1734975f2f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
29 changed files with 1468 additions and 482 deletions

View file

@ -84,16 +84,63 @@ declare module '@docusaurus/Link' {
export default Link; export default Link;
} }
declare module '@docusaurus/Translate' { declare module '@docusaurus/Interpolate' {
type TranslateProps = {children: string; id?: string; description?: string}; import type {ReactNode} from 'react';
const Translate: (props: TranslateProps) => JSX.Element;
export default Translate;
export function translate(param: { // TODO use TS template literal feature to make values typesafe!
message: string; // (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; id?: string;
description?: 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' { declare module '@docusaurus/router' {

View file

@ -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.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.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.navAriaLabel": "Blog list page navigation",
"theme.blog.paginator.newerEntries": "Newer Entries", "theme.blog.paginator.newerEntries": "Newer Entries",
"theme.blog.paginator.olderEntries": "Older 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.navAriaLabel": "Blog post page navigation",
"theme.blog.post.paginator.newerPost": "Newer Post", "theme.blog.post.paginator.newerPost": "Newer Post",
"theme.blog.post.paginator.olderPost": "Older Post", "theme.blog.post.paginator.olderPost": "Older Post",
"theme.blog.post.readMore": "Read More", "theme.blog.post.readMore": "Read More",
"theme.tags.tagsPageLink": "View All Tags", "theme.blog.post.readingTime": "{readingTime} min read",
"theme.tags.tagsPageTitle": "Tags", "theme.blog.tagTitle": "{nPosts} tagged with \"{tagName}\"",
"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.common.editThisPage": "Edit this page", "theme.common.editThisPage": "Edit this page",
"theme.common.headingLinkTitle": "Direct link to heading", "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.common.skipToMainContent": "Skip to main content",
"theme.SearchPage.existingResultsTitle": "Search results for", "theme.docs.paginator.navAriaLabel": "Docs pages navigation",
"theme.SearchPage.emptyResultsTitle": "Search the documentation", "theme.docs.paginator.next": "Next",
"theme.SearchPage.inputPlaceholder": "Type your search here", "theme.docs.paginator.previous": "Previous",
"theme.SearchPage.inputLabel": "Search", "theme.docs.sidebar.collapseButtonAriaLabel": "Collapse sidebar",
"theme.SearchPage.algoliaLabel": "Search by Algolia", "theme.docs.sidebar.collapseButtonTitle": "Collapse sidebar",
"theme.SearchPage.noResultsText": "No results were found", "theme.docs.sidebar.expandButtonAriaLabel": "Expand sidebar",
"theme.SearchPage.fetchingNewResults": "Fetching new results...", "theme.docs.sidebar.expandButtonTitle": "Expand sidebar",
"theme.SearchBar.label": "Search", "theme.docs.sidebar.responsiveCloseButtonLabel": "Close menu",
"theme.PwaReloadPopup.info": "New version available", "theme.docs.sidebar.responsiveOpenButtonLabel": "Open menu",
"theme.PwaReloadPopup.refreshButtonText": "Refresh", "theme.docs.versions.latestVersionLinkLabel": "latest version",
"theme.PwaReloadPopup.closeButtonAriaLabel": "Close", "theme.docs.versions.latestVersionSuggestionLabel": "For up-to-date documentation, see the {latestVersionLink} ({versionLabel}).",
"theme.Playground.liveEditor": "Live Editor", "theme.docs.versions.unmaintainedVersionLabel": "This is documentation for {siteTitle} {versionLabel}, which is no longer actively maintained.",
"theme.Playground.result": "Result" "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"
}

View file

@ -1,44 +1,68 @@
{ {
"theme.NotFound.title": "Seite nicht gefunden", "theme.AnnouncementBar.closeButtonAriaLabel": "Schließen",
"theme.NotFound.p1": "Wir konnten nicht finden, wonach Sie gesucht haben.", "theme.CodeBlock.copied": "Kopiert",
"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.CodeBlock.copy": "Kopieren",
"theme.AnnouncementBar.closeButtonAriaLabel": "Schließen", "theme.CodeBlock.copyButtonAriaLabel": "In die Zwischenablage kopieren",
"theme.blog.paginator.navAriaLabel": "Navigation der Blog-Listenseite", "theme.NotFound.p1": "Wir konnten nicht finden, wonach Sie gesucht haben.",
"theme.blog.paginator.newerEntries": "Neuere Einträge", "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.blog.paginator.olderEntries": "Ältere Einträge", "theme.NotFound.title": "Seite nicht gefunden",
"theme.blog.post.paginator.navAriaLabel": "Blog Post Seiten Navigation", "theme.Playground.liveEditor": "Live Editor",
"theme.blog.post.paginator.newerPost": "Neuer Post", "theme.Playground.result": "Ergebnisse",
"theme.blog.post.paginator.olderPost": "Älterer Post", "theme.PwaReloadPopup.closeButtonAriaLabel": "Schließen",
"theme.blog.post.readMore": "Mehr lesen", "theme.PwaReloadPopup.info": "Neue Version verfügbar",
"theme.tags.tagsPageLink": "Alle Tags anzeigen", "theme.PwaReloadPopup.refreshButtonText": "Aktualisieren",
"theme.tags.tagsPageTitle": "Tags", "theme.SearchBar.label": "Suche",
"theme.tags.tagsListLabel": "Tags:", "theme.SearchPage.algoliaLabel": "Suche von Algolia",
"theme.CodeBlock.copyButtonAriaLabel": "In die Zwischenablage kopieren", "theme.SearchPage.emptyResultsTitle": "Suche in der Dokumentation",
"theme.CodeBlock.copied": "Kopiert", "theme.SearchPage.existingResultsTitle": "Suchergebnisse für",
"theme.CodeBlock.copy": "Kopieren", "theme.SearchPage.fetchingNewResults": "Neue Ergebnisse abrufen...",
"theme.docs.paginator.navAriaLabel": "Dokumentation Seiten Navigation", "theme.SearchPage.inputLabel": "Suche",
"theme.docs.paginator.previous": "Zurück", "theme.SearchPage.inputPlaceholder": "Geben Sie hier Ihre Suche ein",
"theme.docs.paginator.next": "Weiter", "theme.SearchPage.noResultsText": "Es wurden keine Ergebnisse gefunden",
"theme.docs.sidebar.expandButtonTitle": "Seitenleiste ausklappen", "theme.blog.paginator.navAriaLabel": "Navigation der Blog-Listenseite",
"theme.docs.sidebar.expandButtonAriaLabel": "Seitenleiste ausklappen", "theme.blog.paginator.newerEntries": "Neuere Einträge",
"theme.docs.sidebar.responsiveCloseButtonLabel": "Menü schließen", "theme.blog.paginator.olderEntries": "Ältere Einträge",
"theme.docs.sidebar.responsiveOpenButtonLabel": "Menü öffenen", "theme.blog.post.date": "{month} {day}, {year}",
"theme.docs.sidebar.collapseButtonTitle": "Seitenleiste einklappen", "theme.blog.post.nPosts": "{count} posts",
"theme.docs.sidebar.collapseButtonAriaLabel": "Seitenleiste einklappen", "theme.blog.post.onePost": "One post",
"theme.common.editThisPage": "Diese Seite bearbeiten", "theme.blog.post.paginator.navAriaLabel": "Blog Post Seiten Navigation",
"theme.common.headingLinkTitle": "Direkter Link zur Überschrift", "theme.blog.post.paginator.newerPost": "Neuer Post",
"theme.common.skipToMainContent": "Zum Hauptinhalt springen", "theme.blog.post.paginator.olderPost": "Älterer Post",
"theme.SearchPage.existingResultsTitle": "Suchergebnisse für", "theme.blog.post.readMore": "Mehr lesen",
"theme.SearchPage.emptyResultsTitle": "Suche in der Dokumentation", "theme.blog.post.readingTime": "{readingTime} min read",
"theme.SearchPage.inputPlaceholder": "Geben Sie hier Ihre Suche ein", "theme.blog.tagTitle": "{nPosts} tagged with \"{tagName}\"",
"theme.SearchPage.inputLabel": "Suche", "theme.common.editThisPage": "Diese Seite bearbeiten",
"theme.SearchPage.algoliaLabel": "Suche von Algolia", "theme.common.headingLinkTitle": "Direkter Link zur Überschrift",
"theme.SearchPage.noResultsText": "Es wurden keine Ergebnisse gefunden", "theme.common.month.april": "April",
"theme.SearchPage.fetchingNewResults": "Neue Ergebnisse abrufen...", "theme.common.month.august": "August",
"theme.SearchBar.label": "Suche", "theme.common.month.december": "December",
"theme.PwaReloadPopup.info": "Neue Version verfügbar", "theme.common.month.february": "February",
"theme.PwaReloadPopup.refreshButtonText": "Aktualisieren", "theme.common.month.january": "January",
"theme.PwaReloadPopup.closeButtonAriaLabel": "Schließen", "theme.common.month.july": "July",
"theme.Playground.liveEditor": "Live Editor", "theme.common.month.june": "June",
"theme.Playground.result": "Ergebnisse" "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"
}

View file

@ -1,44 +1,68 @@
{ {
"theme.NotFound.title": "صفحه‌ای که دنبال آن بودید پیدا نشد!", "theme.AnnouncementBar.closeButtonAriaLabel": "بستن",
"theme.CodeBlock.copied": "کپی شد",
"theme.CodeBlock.copy": "کپی کردن",
"theme.CodeBlock.copyButtonAriaLabel": "کپی کردن کد به کلیپ بورد",
"theme.NotFound.p1": "متاسفانه نتوانستیم مطلب مورد نظر شما را پیدا کنیم.", "theme.NotFound.p1": "متاسفانه نتوانستیم مطلب مورد نظر شما را پیدا کنیم.",
"theme.NotFound.p2": "لطفا با صاحب وبسایت تماس بگیرید و ایشان را از مشکل پیش آمده مطلع کنید.", "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.navAriaLabel": "کنترل لیست صفحه وبسایت",
"theme.blog.paginator.newerEntries": "مطالب جدیدتر", "theme.blog.paginator.newerEntries": "مطالب جدیدتر",
"theme.blog.paginator.olderEntries": "مطالب قدیمی تر", "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.navAriaLabel": "کنترل پست های صفحه وبلاگ",
"theme.blog.post.paginator.newerPost": "پست های جدید تر", "theme.blog.post.paginator.newerPost": "پست های جدید تر",
"theme.blog.post.paginator.olderPost": "پست های قدیمی تر", "theme.blog.post.paginator.olderPost": "پست های قدیمی تر",
"theme.blog.post.readMore": "ادامه مطلب", "theme.blog.post.readMore": "ادامه مطلب",
"theme.tags.tagsPageLink": "مشاهده تمام برچسب ها", "theme.blog.post.readingTime": "{readingTime} min read",
"theme.tags.tagsPageTitle": "برچسب ها", "theme.blog.tagTitle": "{nPosts} tagged with \"{tagName}\"",
"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.common.editThisPage": "ویرایش صفحه", "theme.common.editThisPage": "ویرایش صفحه",
"theme.common.headingLinkTitle": "لینک مستقیم به عنوان", "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.common.skipToMainContent": "رفتن به مطلب اصلی",
"theme.SearchPage.existingResultsTitle": "جستجو برای عبارت", "theme.docs.paginator.navAriaLabel": "کنترل صفحه اسناد",
"theme.SearchPage.emptyResultsTitle": "جستجو در متن", "theme.docs.paginator.next": "بعدی",
"theme.SearchPage.inputPlaceholder": "عبارت مورد نظر را اینجا بنویسید", "theme.docs.paginator.previous": "قبلی",
"theme.SearchPage.inputLabel": "جستجو", "theme.docs.sidebar.collapseButtonAriaLabel": "بستن نوار کناری",
"theme.SearchPage.algoliaLabel": "جستجو با Algolia", "theme.docs.sidebar.collapseButtonTitle": "بستن نوار کناری",
"theme.SearchPage.noResultsText": "هیچ نتیجه ای پیدا نشد", "theme.docs.sidebar.expandButtonAriaLabel": "بزرگ کردن نوار کناری",
"theme.SearchPage.fetchingNewResults": "در حال دریافت نتایج...", "theme.docs.sidebar.expandButtonTitle": "بزرگ کردن نوار کناری",
"theme.SearchBar.label": "جستجو", "theme.docs.sidebar.responsiveCloseButtonLabel": "بستن منو",
"theme.PwaReloadPopup.info": "نسخه جدیدی منتشر شده است", "theme.docs.sidebar.responsiveOpenButtonLabel": "باز کردن منو",
"theme.PwaReloadPopup.refreshButtonText": "بروزرسانی", "theme.docs.versions.latestVersionLinkLabel": "latest version",
"theme.PwaReloadPopup.closeButtonAriaLabel": "بستن", "theme.docs.versions.latestVersionSuggestionLabel": "For up-to-date documentation, see the {latestVersionLink} ({versionLabel}).",
"theme.Playground.liveEditor": "ویرایشگر زنده", "theme.docs.versions.unmaintainedVersionLabel": "This is documentation for {siteTitle} {versionLabel}, which is no longer actively maintained.",
"theme.Playground.result": "نتایج" "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": "برچسب ها"
}

View file

@ -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.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.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.navAriaLabel": "Pagination de la liste des posts du blog",
"theme.blog.paginator.newerEntries": "Nouvelles entrées", "theme.blog.paginator.newerEntries": "Nouvelles entrées",
"theme.blog.paginator.olderEntries": "Anciennes 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.navAriaLabel": "Pagination des blog posts",
"theme.blog.post.paginator.newerPost": "Article plus récent", "theme.blog.post.paginator.newerPost": "Article plus récent",
"theme.blog.post.paginator.olderPost": "Article plus ancien", "theme.blog.post.paginator.olderPost": "Article plus ancien",
"theme.blog.post.readMore": "Lire plus", "theme.blog.post.readMore": "Lire plus",
"theme.tags.tagsPageLink": "Voir tous les tags", "theme.blog.post.readingTime": "{readingTime} min de lecture",
"theme.tags.tagsPageTitle": "Tags", "theme.blog.tagTitle": "{nPosts} taggés avec \"{tagName}\"",
"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.common.editThisPage": "Éditer cette page", "theme.common.editThisPage": "Éditer cette page",
"theme.common.headingLinkTitle": "Lien direct vers le titre", "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.common.skipToMainContent": "Aller au contenu principal",
"theme.SearchPage.existingResultsTitle": "Rechercher des résultats pour", "theme.docs.paginator.navAriaLabel": "Pagination des documents",
"theme.SearchPage.emptyResultsTitle": "Rechercher dans la documentation", "theme.docs.paginator.next": "Suivant",
"theme.SearchPage.inputPlaceholder": "Tapez vôtre recherche ici", "theme.docs.paginator.previous": "Précédent",
"theme.SearchPage.inputLabel": "Chercher", "theme.docs.sidebar.collapseButtonAriaLabel": "Réduire le menu latéral",
"theme.SearchPage.algoliaLabel": "Recharche Algolia", "theme.docs.sidebar.collapseButtonTitle": "Réduire le menu latéral",
"theme.SearchPage.noResultsText": "Aucun résultat trouvé", "theme.docs.sidebar.expandButtonAriaLabel": "Déplier le menu latéral",
"theme.SearchPage.fetchingNewResults": "Chargement de nouveaux résultats...", "theme.docs.sidebar.expandButtonTitle": "Déplier le menu latéral",
"theme.SearchBar.label": "Chercher", "theme.docs.sidebar.responsiveCloseButtonLabel": "Fermer le menu latéral",
"theme.PwaReloadPopup.info": "Nouvelle version disponible", "theme.docs.sidebar.responsiveOpenButtonLabel": "Ouvrir le menu latéral",
"theme.PwaReloadPopup.refreshButtonText": "Rafraichir", "theme.docs.versions.latestVersionLinkLabel": "dernière version",
"theme.PwaReloadPopup.closeButtonAriaLabel": "Fermer", "theme.docs.versions.latestVersionSuggestionLabel": "Pour une documentation à jour, consultez la {latestVersionLink} ({versionLabel}).",
"theme.Playground.liveEditor": "Éditeur en direct", "theme.docs.versions.unmaintainedVersionLabel": "Ceci est la documentation de {siteTitle} {versionLabel}, qui n'est plus activement maintenue.",
"theme.Playground.result": "Résultat" "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"
}

View file

@ -1,44 +1,68 @@
{ {
"theme.NotFound.title": "Страница не найдена", "theme.AnnouncementBar.closeButtonAriaLabel": "Закрыть",
"theme.CodeBlock.copied": "Скопировано",
"theme.CodeBlock.copy": "Скопировать",
"theme.CodeBlock.copyButtonAriaLabel": "Скопировать в буфер обмена",
"theme.NotFound.p1": "К сожалению, мы не смогли найти запрашиваемую вами страницу.", "theme.NotFound.p1": "К сожалению, мы не смогли найти запрашиваемую вами страницу.",
"theme.NotFound.p2": "Пожалуйста, обратитесь к владельцу сайта, с которого вы перешли на эту ссылку, чтобы сообщить ему ссылка не работает.", "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.navAriaLabel": "Навигация по странице списка блогов",
"theme.blog.paginator.newerEntries": "Следующие записи", "theme.blog.paginator.newerEntries": "Следующие записи",
"theme.blog.paginator.olderEntries": "Предыдущие записи", "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.navAriaLabel": "Навигация по странице поста блога",
"theme.blog.post.paginator.newerPost": "Следующий пост", "theme.blog.post.paginator.newerPost": "Следующий пост",
"theme.blog.post.paginator.olderPost": "Предыдущий пост", "theme.blog.post.paginator.olderPost": "Предыдущий пост",
"theme.blog.post.readMore": "Читать дальше", "theme.blog.post.readMore": "Читать дальше",
"theme.tags.tagsPageLink": "Посмотреть все теги", "theme.blog.post.readingTime": "{readingTime} min read",
"theme.tags.tagsPageTitle": "Теги", "theme.blog.tagTitle": "{nPosts} tagged with \"{tagName}\"",
"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.common.editThisPage": "Отредактировать эту страницу", "theme.common.editThisPage": "Отредактировать эту страницу",
"theme.common.headingLinkTitle": "Прямая ссылка на этот заголовок", "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.common.skipToMainContent": "Перейти к основному содержимому",
"theme.SearchPage.existingResultsTitle": "Результаты поиска по запросу", "theme.docs.paginator.navAriaLabel": "Навигация по странице документации",
"theme.SearchPage.emptyResultsTitle": "Поиск по сайту", "theme.docs.paginator.next": "Следующая страница",
"theme.SearchPage.inputPlaceholder": "Введите фразу для поиска", "theme.docs.paginator.previous": "Предыдущая страница",
"theme.SearchPage.inputLabel": "Поиск", "theme.docs.sidebar.collapseButtonAriaLabel": "Свернуть сайдбар",
"theme.SearchPage.algoliaLabel": "Поиск предоставлен Algolia", "theme.docs.sidebar.collapseButtonTitle": "Свернуть сайдбар",
"theme.SearchPage.noResultsText": "По запросу ничего не найдено", "theme.docs.sidebar.expandButtonAriaLabel": "Развернуть сайдбар",
"theme.SearchPage.fetchingNewResults": "Загрузка новых результатов поиска...", "theme.docs.sidebar.expandButtonTitle": "Развернуть сайдбар",
"theme.SearchBar.label": "Поиск", "theme.docs.sidebar.responsiveCloseButtonLabel": "Закрыть меню",
"theme.PwaReloadPopup.info": "Доступна новая версия", "theme.docs.sidebar.responsiveOpenButtonLabel": "Открыть меню",
"theme.PwaReloadPopup.refreshButtonText": "Обновить", "theme.docs.versions.latestVersionLinkLabel": "latest version",
"theme.PwaReloadPopup.closeButtonAriaLabel": "Закрыть", "theme.docs.versions.latestVersionSuggestionLabel": "For up-to-date documentation, see the {latestVersionLink} ({versionLabel}).",
"theme.Playground.liveEditor": "Интерактивный редактор", "theme.docs.versions.unmaintainedVersionLabel": "This is documentation for {siteTitle} {versionLabel}, which is no longer actively maintained.",
"theme.Playground.result": "Результат" "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": "Теги"
}

View file

@ -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": "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", "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": "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": { "dependencies": {
"@docusaurus/core": "2.0.0-alpha.70", "@docusaurus/core": "2.0.0-alpha.70",
@ -45,7 +46,10 @@
"prop-types": "^15.7.2", "prop-types": "^15.7.2",
"react-router-dom": "^5.2.0", "react-router-dom": "^5.2.0",
"react-toggle": "^4.1.1", "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": { "devDependencies": {
"@docusaurus/module-type-aliases": "2.0.0-alpha.70" "@docusaurus/module-type-aliases": "2.0.0-alpha.70"

View file

@ -8,7 +8,7 @@
import React from 'react'; import React from 'react';
import clsx from 'clsx'; import clsx from 'clsx';
import {MDXProvider} from '@mdx-js/react'; import {MDXProvider} from '@mdx-js/react';
import Translate from '@docusaurus/Translate'; import Translate, {translate} from '@docusaurus/Translate';
import Link from '@docusaurus/Link'; import Link from '@docusaurus/Link';
import MDXComponents from '@theme/MDXComponents'; import MDXComponents from '@theme/MDXComponents';
import Seo from '@theme/Seo'; import Seo from '@theme/Seo';
@ -17,18 +17,66 @@ import type {Props} from '@theme/BlogPostItem';
import styles from './styles.module.css'; import styles from './styles.module.css';
const MONTHS = [ const MONTHS = [
'January', translate({
'February', id: 'theme.common.month.january',
'March', description: 'January month translation',
'April', message: 'January',
'May', }),
'June', translate({
'July', id: 'theme.common.month.february',
'August', description: 'February month translation',
'September', message: 'February',
'October', }),
'November', translate({
'December', 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 { function BlogPostItem(props: Props): JSX.Element {
@ -62,8 +110,25 @@ function BlogPostItem(props: Props): JSX.Element {
</TitleHeading> </TitleHeading>
<div className="margin-vert--md"> <div className="margin-vert--md">
<time dateTime={date} className={styles.blogPostDate}> <time dateTime={date} className={styles.blogPostDate}>
{month} {day}, {year}{' '} <Translate
{readingTime && <> · {Math.ceil(readingTime)} min read</>} 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> </time>
</div> </div>
<div className="avatar margin-vert--md"> <div className="avatar margin-vert--md">

View file

@ -50,7 +50,6 @@ function BlogTagsListPage(props: Props): JSX.Element {
)) ))
.filter((item) => item != null); .filter((item) => item != null);
// TODO soon: translate hardcoded labels, but factorize them (blog + docs will both have tags)
return ( return (
<Layout <Layout
title="Tags" title="Tags"

View file

@ -12,17 +12,33 @@ import BlogPostItem from '@theme/BlogPostItem';
import Link from '@docusaurus/Link'; import Link from '@docusaurus/Link';
import type {Props} from '@theme/BlogTagsPostsPage'; import type {Props} from '@theme/BlogTagsPostsPage';
import BlogSidebar from '@theme/BlogSidebar'; import BlogSidebar from '@theme/BlogSidebar';
import Translate from '@docusaurus/Translate'; import Translate, {translate} from '@docusaurus/Translate';
function pluralize(count: number, word: string) { // Very simple pluralization: probably good enough for now
return count > 1 ? `${word}s` : word; 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 { function BlogTagsPostPage(props: Props): JSX.Element {
const {metadata, items, sidebar} = props; const {metadata, items, sidebar} = props;
const {allTagsPath, name: tagName, count} = metadata; const {allTagsPath, name: tagName, count} = metadata;
// TODO soon: translate hardcoded labels, but factorize them (blog + docs will both have tags)
return ( return (
<Layout <Layout
title={`Posts tagged "${tagName}"`} title={`Posts tagged "${tagName}"`}
@ -35,8 +51,12 @@ function BlogTagsPostPage(props: Props): JSX.Element {
</div> </div>
<main className="col col--8"> <main className="col col--8">
<h1> <h1>
{count} {pluralize(count, 'post')} tagged with &quot;{tagName} <Translate
&quot; 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> </h1>
<Link href={allTagsPath}> <Link href={allTagsPath}>
<Translate <Translate

View file

@ -9,6 +9,7 @@ import React from 'react';
import DocPaginator from '@theme/DocPaginator'; import DocPaginator from '@theme/DocPaginator';
import DocVersionSuggestions from '@theme/DocVersionSuggestions'; import DocVersionSuggestions from '@theme/DocVersionSuggestions';
import Seo from '@theme/Seo'; import Seo from '@theme/Seo';
import LastUpdated from '@theme/LastUpdated';
import type {Props} from '@theme/DocItem'; import type {Props} from '@theme/DocItem';
import TOC from '@theme/TOC'; import TOC from '@theme/TOC';
import EditThisPage from '@theme/EditThisPage'; import EditThisPage from '@theme/EditThisPage';
@ -78,42 +79,10 @@ function DocItem(props: Props): JSX.Element {
{editUrl && <EditThisPage editUrl={editUrl} />} {editUrl && <EditThisPage editUrl={editUrl} />}
</div> </div>
{(lastUpdatedAt || lastUpdatedBy) && ( {(lastUpdatedAt || lastUpdatedBy) && (
<div className="col text--right"> <LastUpdated
<em> lastUpdatedAt={lastUpdatedAt}
<small> lastUpdatedBy={lastUpdatedBy}
{/* 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>
)} )}
</div> </div>
</div> </div>

View file

@ -26,7 +26,3 @@
padding: 0 0.3rem; padding: 0 0.3rem;
} }
} }
.docLastUpdatedAt {
font-weight: bold;
}

View file

@ -8,6 +8,7 @@
import React from 'react'; import React from 'react';
import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
import Link from '@docusaurus/Link'; import Link from '@docusaurus/Link';
import Translate from '@docusaurus/Translate';
import { import {
useActivePlugin, useActivePlugin,
useActiveVersion, useActiveVersion,
@ -15,6 +16,84 @@ import {
} from '@theme/hooks/useDocs'; } from '@theme/hooks/useDocs';
import {useDocsPreferredVersion} from '@docusaurus/theme-common'; 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) => const getVersionMainDoc = (version) =>
version.docs.find((doc) => doc.id === version.mainDocId); version.docs.find((doc) => doc.id === version.mainDocId);
@ -44,34 +123,25 @@ function DocVersionSuggestions(): JSX.Element {
return ( return (
<div className="alert alert--warning margin-bottom--md" role="alert"> <div className="alert alert--warning margin-bottom--md" role="alert">
{ <div>
// TODO need refactoring {activeVersion.name === 'current' ? (
// TODO need translate after interpolation appears <UnreleasedVersionLabel
activeVersion.name === 'current' ? ( siteTitle={siteTitle}
<div> versionLabel={activeVersion.label}
This is unreleased documentation for {siteTitle}{' '} />
<strong>{activeVersion.label}</strong> version.
</div>
) : ( ) : (
<div> <UnmaintainedVersionLabel
This is documentation for {siteTitle}{' '} siteTitle={siteTitle}
<strong>{activeVersion.label}</strong>, which is no longer actively versionLabel={activeVersion.label}
maintained. />
</div> )}
) </div>
}
<div className="margin-top--md"> <div className="margin-top--md">
For up-to-date documentation, see the{' '} <LatestVersionSuggestionLabel
<strong> versionLabel={latestVersionSuggestion.label}
<Link to={latestVersionSuggestedDoc.path}
to={latestVersionSuggestedDoc.path} onClick={() => savePreferredVersionName(latestVersionSuggestion.name)}
onClick={() => />
savePreferredVersionName(latestVersionSuggestion.name)
}>
latest version
</Link>
</strong>{' '}
({latestVersionSuggestion.label}).
</div> </div>
</div> </div>
); );

View file

@ -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>
);
}

View file

@ -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;
}

View 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);
},
);

View 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);
}

View file

@ -6,6 +6,11 @@
*/ */
import React from 'react'; import React from 'react';
import Interpolate, {
interpolate,
InterpolateProps,
InterpolateValues,
} from '@docusaurus/Interpolate';
// Can't read it from context, due to exposing imperative API // Can't read it from context, due to exposing imperative API
import codeTranslations from '@generated/codeTranslations'; import codeTranslations from '@generated/codeTranslations';
@ -20,29 +25,37 @@ function getLocalizedMessage({
return codeTranslations[id ?? message] ?? message; return codeTranslations[id ?? message] ?? message;
} }
export type TranslateParam = { export type TranslateParam<Str extends string> = {
message: string; message: Str;
id?: string; id?: string;
description?: string; description?: string;
values?: InterpolateValues<Str, string | number>;
}; };
// Imperative translation API is useful for some edge-cases: // Imperative translation API is useful for some edge-cases:
// - translating page titles (meta) // - translating page titles (meta)
// - translating string props (input placeholders, image alt, aria labels...) // - translating string props (input placeholders, image alt, aria labels...)
export function translate({message, id}: TranslateParam): string { export function translate<Str extends string>(
const localizedMessage = getLocalizedMessage({message, id}); {message, id}: TranslateParam<Str>,
return localizedMessage ?? message; values?: InterpolateValues<Str, string | number>,
): string {
const localizedMessage = getLocalizedMessage({message, id}) ?? message;
return interpolate(localizedMessage, values);
} }
export type TranslateProps = { export type TranslateProps<Str extends string> = InterpolateProps<Str> & {
children: string;
id?: string; id?: string;
description?: string; description?: string;
}; };
// Maybe we'll want to improve this component with additional features // 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? // 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 = const localizedMessage: string =
getLocalizedMessage({message: children, id}) ?? children; getLocalizedMessage({message: children, id}) ?? children;
return <>{localizedMessage}</>;
return <Interpolate values={values}>{localizedMessage}</Interpolate>;
} }

View file

@ -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();
});
});

View file

@ -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>,
]
`;

View file

@ -255,7 +255,11 @@ describe('extractPluginsSourceCodeTranslations', () => {
export default function MyComponent() { export default function MyComponent() {
return ( return (
<div> <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> </div>
); );
} }

View file

@ -46,6 +46,19 @@ function getPluginSourceCodeFilePaths(plugin: InitPlugin): string[] {
return codePaths; 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( async function getSourceCodeFilePaths(
plugins: InitPlugin[], plugins: InitPlugin[],
): Promise<string[]> { ): Promise<string[]> {
@ -54,15 +67,7 @@ async function getSourceCodeFilePaths(
// Hacky/implicit, but do we want to introduce a new lifecycle method for that??? // Hacky/implicit, but do we want to introduce a new lifecycle method for that???
const allPathsToWatch = flatten(plugins.map(getPluginSourceCodeFilePaths)); const allPathsToWatch = flatten(plugins.map(getPluginSourceCodeFilePaths));
// Required for Windows support, as paths using \ should not be used by globby return globSourceCodeFilePaths(allPathsToWatch);
// (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);
} }
export async function extractPluginsSourceCodeTranslations( export async function extractPluginsSourceCodeTranslations(
@ -109,7 +114,7 @@ type SourceCodeFileTranslations = {
warnings: string[]; warnings: string[];
}; };
async function extractAllSourceCodeFileTranslations( export async function extractAllSourceCodeFileTranslations(
sourceCodeFilePaths: string[], sourceCodeFilePaths: string[],
babelOptions: TransformOptions, babelOptions: TransformOptions,
): Promise<SourceCodeFileTranslations[]> { ): Promise<SourceCodeFileTranslations[]> {
@ -265,7 +270,10 @@ function extractSourceCodeAstTranslations(
path.node.callee.name === 'translate' path.node.callee.name === 'translate'
) { ) {
// console.log('CallExpression', path.node); // 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; const firstArgPath = path.get('arguments.0') as NodePath;
// evaluation allows translate("x" + "y"); to be considered as translate("xy"); // evaluation allows translate("x" + "y"); to be considered as translate("xy");
@ -291,7 +299,7 @@ function extractSourceCodeAstTranslations(
} }
} else { } else {
warnings.push( warnings.push(
`translate() function only takes 1 arg\n${sourceFileWarningPart( `translate() function only takes 1 or 2 args\n${sourceFileWarningPart(
path.node, path.node,
)}\n${generateCode(path.node)}`, )}\n${generateCode(path.node)}`,
); );

View file

@ -6,6 +6,7 @@ Object {
"@docusaurus/ComponentCreator": "../../client/exports/ComponentCreator.tsx", "@docusaurus/ComponentCreator": "../../client/exports/ComponentCreator.tsx",
"@docusaurus/ExecutionEnvironment": "../../client/exports/ExecutionEnvironment.ts", "@docusaurus/ExecutionEnvironment": "../../client/exports/ExecutionEnvironment.ts",
"@docusaurus/Head": "../../client/exports/Head.tsx", "@docusaurus/Head": "../../client/exports/Head.tsx",
"@docusaurus/Interpolate": "../../client/exports/Interpolate.tsx",
"@docusaurus/Link": "../../client/exports/Link.tsx", "@docusaurus/Link": "../../client/exports/Link.tsx",
"@docusaurus/Noop": "../../client/exports/Noop.ts", "@docusaurus/Noop": "../../client/exports/Noop.ts",
"@docusaurus/Translate": "../../client/exports/Translate.tsx", "@docusaurus/Translate": "../../client/exports/Translate.tsx",

View file

@ -4,13 +4,11 @@ title: Team
slug: /team slug: /team
--- ---
import TeamProfileCard from '@site/src/components/TeamProfileCard'; import {
ActiveTeamRow,
export function TeamProfileCardCol(props) { HonoraryAlumniTeamRow,
return ( StudentFellowsTeamRow,
<TeamProfileCard {...props} className={'col col--6 margin-bottom--lg'} /> } from '@site/src/components/TeamProfileCards';
);
}
## Active Team ## 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. Current members of the Docusaurus team are listed in alphabetical order below.
<div className="row"> <ActiveTeamRow />
<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>
## Honorary Alumni ## 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. 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"> <HonoraryAlumniTeamRow />
<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>
## Student Fellows ## 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. 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"> <StudentFellowsTeamRow />
<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>
## Acknowledgements ## Acknowledgements

View file

@ -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/>` ### `<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>`. 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**. 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 #### 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 - `id`: optional value to use as key in JSON translation files
- `description`: optional text to help the translator - `description`: optional text to help the translator
- `values`: optional object containing interpolation placeholder values
#### Example #### Example
@ -169,7 +205,9 @@ export default function Home() {
</h1> </h1>
<main> <main>
{/* highlight-start */} {/* highlight-start */}
<Translate>My website content</Translate> <Translate values={{firstName: 'Sébastien'}}>
{'Welcome, {firstName}! How are you?'}
</Translate>
{/* highlight-end */} {/* highlight-end */}
</main> </main>
</Layout> </Layout>
@ -378,19 +416,58 @@ const MyComponent = () => {
## Functions ## 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` ### `translate`
The imperative counterpart of the [`<Translate>`](#translate) component. The imperative counterpart of the [`<Translate>`](#translate) component. Also supporting [placeholders interpolation](#interpolate).
:::tip :::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 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" ```jsx title="src/index.js"
import React from 'react'; import React from 'react';
import Layout from '@theme/Layout'; import Layout from '@theme/Layout';
@ -406,16 +483,19 @@ export default function Home() {
title={translate({message: 'My page meta title'})} title={translate({message: 'My page meta title'})}
// highlight-end // highlight-end
> >
<input <img
type="text" src={'https://docusaurus.io/logo.png'}
placeholder={ aria-label={
// highlight-start // highlight-start
translate({ translate(
message: 'Some input placeholder', {
// Optional message: 'The logo of site {siteName}',
id: 'homepage.input.placeholder', // Optional
description: 'The homepage input placeholder', id: 'homepage.logo.ariaLabel',
}) description: 'The home page logo aria label',
},
{siteName: 'Docusaurus'},
)
// highlight-end // highlight-end
} }
/> />

View file

@ -5,7 +5,7 @@ sidebar_label: Introduction
slug: /i18n/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 :::caution
@ -15,7 +15,7 @@ i18n is a new feature (released early 2021), please report any bug you find.
## Goals ## 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). 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 translation workflows**: based on Git (monorepo, forks or submodules), SaaS software, FTP...
- **Flexible deployment options**: single or multiple domains. - **Flexible deployment options**: single or multiple domains.
- **Modular**: allow plugin author to provide i18n support. - **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. - **Acceptable build-times**: allow building and deploying localized sites independently.
- **Localize assets**: an image of your site might contain text that should be translated. - **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. - **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. - **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. - **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. - **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) ### i18n goals (TODO)

View file

@ -65,7 +65,17 @@ Start your localized site in dev mode, using the locale of your choice:
npm run start -- --locale fr 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 :::caution
@ -94,6 +104,8 @@ Open the homepage, and use the [translation APIs](../docusaurus-core.md#translat
```jsx title="src/index.js" ```jsx title="src/index.js"
import React from 'react'; import React from 'react';
import Layout from '@theme/Layout'; import Layout from '@theme/Layout';
import Link from '@docusaurus/Link';
// highlight-start // highlight-start
import Translate, {translate} from '@docusaurus/Translate'; import Translate, {translate} from '@docusaurus/Translate';
// highlight-end // highlight-end
@ -103,13 +115,19 @@ export default function Home() {
<Layout> <Layout>
<h1> <h1>
{/* highlight-start */} {/* highlight-start */}
<Translate description="The homepage welcome message"> <Translate>Welcome to my website</Translate>
Welcome to my website
</Translate>
{/* highlight-end */} {/* highlight-end */}
</h1> </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 <input
type="text" type="text"
placeholder={ placeholder={
@ -121,7 +139,7 @@ export default function Home() {
// highlight-end // highlight-end
} }
/> />
</div> </main>
</Layout> </Layout>
); );
} }
@ -129,7 +147,9 @@ export default function Home() {
:::caution :::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.
::: :::

View file

@ -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>
);
}

View 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>
);
}