From 51ece235f8f2707c4c34c1f8c5686c0f99fc8e2a Mon Sep 17 00:00:00 2001 From: Elio Struyf Date: Mon, 19 Feb 2024 16:04:41 +0100 Subject: [PATCH] #756 - Language filter + card actions + submenu --- l10n/bundle.l10n.json | 15 ++++ package.json | 8 ++ package.nls.json | 5 ++ src/commands/i18n.ts | 88 ++++++++++++------- src/components/shadcn/Dropdown.tsx | 57 +----------- src/constants/GeneralCommands.ts | 1 + .../components/Contents/ContentActions.tsx | 87 ++++++++++++++++-- .../components/Contents/I18nLabel.tsx | 49 +---------- .../components/Contents/Item.tsx | 8 +- .../components/Filters/LanguageFilter.tsx | 66 ++++++++++++++ .../components/Header/ClearFilters.tsx | 12 ++- .../components/Header/Filter.tsx | 2 +- .../components/Header/Filters.tsx | 4 +- .../components/Media/ItemMenu.tsx | 5 +- .../components/Menu/MenuItem.tsx | 4 +- .../components/Menu/QuickAction.tsx | 5 +- src/dashboardWebView/hooks/usePages.tsx | 50 +++++++++-- src/dashboardWebView/state/atom/LocaleAtom.ts | 8 ++ .../state/atom/LocalesAtom.ts | 7 ++ src/dashboardWebView/state/atom/index.ts | 2 + src/listeners/general/BaseListener.ts | 8 +- src/localization/localization.enum.ts | 52 +++++++++++ 22 files changed, 384 insertions(+), 159 deletions(-) create mode 100644 src/dashboardWebView/components/Filters/LanguageFilter.tsx create mode 100644 src/dashboardWebView/state/atom/LocaleAtom.ts create mode 100644 src/dashboardWebView/state/atom/LocalesAtom.ts diff --git a/l10n/bundle.l10n.json b/l10n/bundle.l10n.json index af9668d8..e20ccd28 100644 --- a/l10n/bundle.l10n.json +++ b/l10n/bundle.l10n.json @@ -90,6 +90,8 @@ "dashboard.contents.contentActions.menuItem.view": "View", "dashboard.contents.contentActions.alert.title": "Delete: {0}", "dashboard.contents.contentActions.alert.description": "Are you sure you want to delete the \"{0}\" content?", + "dashboard.contents.contentActions.translations.create": "Create translation", + "dashboard.contents.contentActions.translations.menu": "Translations", "dashboard.contents.item.invalidTitle": "", "dashboard.contents.item.invalidDescription": "", @@ -128,6 +130,9 @@ "dashboard.errorView.description": "Please close the dashboard and try again.", + "dashboard.filters.languageFilter.label": "Locale", + "dashboard.filters.languageFilter.all": "All", + "dashboard.header.breadcrumb.home": "Home", "dashboard.header.clearFilters.title": "Clear filters, grouping, and sorting", @@ -523,6 +528,16 @@ "commands.folders.get.notificationError.remove.action": "Remove folder", "commands.folders.get.notificationError.create.action": "Create folder", + "commands.i18n.create.warning.noFileSelected": "No file selected.", + "commands.i18n.create.warning.noFile": "The file could not be retrieved.", + "commands.i18n.create.warning.noContentType": "Content type could not be retrieved for the current file.", + "commands.i18n.create.warning.noConfig": "No i18n configuration found.", + "commands.i18n.create.warning.notDefaultLocale": "The current file cannot be used for i18n content creation.", + "commands.i18n.create.error.fileExists": "The i18n translation already exists.", + "commands.i18n.create.success.created": "Created \"{0}\" i18n content file.", + "commands.i18n.create.quickPick.title": "Create content for locale", + "commands.i18n.create.quickPick.placeHolder": "To which locale do you want to create a new content?", + "commands.preview.panel.title": "Preview: {0}", "commands.preview.askUserToPickFolder.title": "Select the folder of the article to preview", diff --git a/package.json b/package.json index 1498910c..2e628580 100644 --- a/package.json +++ b/package.json @@ -307,6 +307,13 @@ "defaultLocale": { "type": "string", "description": "%setting.frontMatter.content.pageFolders.items.properties.defaultLocale.description%" + }, + "locales": { + "type": "array", + "description": "%setting.frontMatter.content.pageFolders.items.properties.locales.description%", + "items": { + "$ref": "#i18n" + } } }, "additionalProperties": false, @@ -322,6 +329,7 @@ "default": [], "markdownDescription": "%setting.frontMatter.content.i18n.markdownDescription%", "items": { + "$id": "#i18n", "type": "object", "properties": { "title": { diff --git a/package.nls.json b/package.nls.json index 736ca210..597529d7 100644 --- a/package.nls.json +++ b/package.nls.json @@ -77,6 +77,11 @@ "setting.frontMatter.content.pageFolders.items.properties.contentTypes.description": "Defines which content types can be used for the current location. If not defined, all content types will be available.", "setting.frontMatter.content.pageFolders.items.properties.disableCreation.description": "Disable the creation of new content in the folder.", "setting.frontMatter.content.pageFolders.items.properties.defaultLocale.description": "Set the page folder as a default locale for the content. All content from this folder is translatable to the languages defined in the `frontMatter.content.i18n` setting.", + "setting.frontMatter.content.pageFolders.items.properties.locales.description": "Define the locales for the page folder. This will be used for the translation of the content.", + "setting.frontMatter.content.i18n.markdownDescription": "Specify the locales you want to use for your website. This setting can be overwritten on page folder level. [Check in the docs](https://frontmatter.codes/docs/settings/overview#frontmatter.content.i18n)", + "setting.frontMatter.content.i18n.items.properties.title.description": "Title of the locale", + "setting.frontMatter.content.i18n.items.properties.locale.description": "Locale code", + "setting.frontMatter.content.i18n.items.properties.path.description": "Relative path of the locale folder", "setting.frontMatter.content.placeholders.markdownDescription": "This array of placeholders defines the placeholders that you can use in your content types and templates for automatically populating your content its front matter. [Check in the docs](https://frontmatter.codes/docs/settings/overview#frontmatter.content.placeholders)", "setting.frontMatter.content.placeholders.items.properties.id.description": "ID of the placeholder, in your content type or template, use it as follows: {{placeholder}}", "setting.frontMatter.content.placeholders.items.properties.value.description": "The placeholder its value", diff --git a/src/commands/i18n.ts b/src/commands/i18n.ts index e897bb1f..ebd488bd 100644 --- a/src/commands/i18n.ts +++ b/src/commands/i18n.ts @@ -15,16 +15,22 @@ import { join, parse } from 'path'; import { existsAsync } from '../utils'; import { Folders } from '.'; import { ParsedFrontMatter } from '../parsers'; +import { PagesListener } from '../listeners/dashboard'; +import * as l10n from '@vscode/l10n'; +import { LocalizationKey } from '../localization'; // TODO: // Allow sponsors to automatically translate the content // Support page bundles -// Filter on locale +// Filter on locale ✅ // Locale settings on the page folder level and global level -// Show the i18n content -> if default locale is in subfolder, the other content is not found -// Update the page folder setting to include the locales property (use #ref) -// Update the default card item when the translation is removed -// Add action to create new translation +// Show the i18n content -> if default locale is in subfolder, the other content is not found ✅ +// Update the page folder setting to include the locales property (use #ref) ✅ +// Update the default card item when the translation is removed ✅ +// Add action to create new translation ✅ +// Trigger page update when translation is created ✅ +// Add translations to the menu ✅ +// Localization of the React components ✅ export class i18n { /** @@ -146,34 +152,37 @@ export class i18n { * @param filePath - The path of the file for which translations are requested. * @returns A promise that resolves to an object containing translations for each locale, or undefined if i18n settings are not available. */ - public static async getTranslations( - filePath: string - ): Promise<{ [locale: string]: { - locale: I18nConfig; - path: string; - } } | undefined> { + public static async getTranslations(filePath: string): Promise< + | { + [locale: string]: { + locale: I18nConfig; + path: string; + }; + } + | undefined + > { const i18nSettings = await i18n.getSettings(filePath); if (!i18nSettings) { return; } - const translations: { [locale: string]: { - locale: I18nConfig; - path: string; - } } = {}; + const translations: { + [locale: string]: { + locale: I18nConfig; + path: string; + }; + } = {}; const pageFolder = Folders.getPageFolderByFilePath(filePath); const fileName = parse(filePath).base; if (pageFolder && pageFolder.defaultLocale) { for (const i18n of i18nSettings) { - if (i18n.path) { - const translation = join(pageFolder.path, i18n.path, fileName); - if (await existsAsync(translation)) { - translations[i18n.locale] = { - locale: i18n, - path: translation - }; - } + const translation = join(pageFolder.path, i18n.path || '', fileName); + if (await existsAsync(translation)) { + translations[i18n.locale] = { + locale: i18n, + path: translation + }; } } return translations; @@ -217,34 +226,38 @@ export class i18n { * If no file path is provided, the active file in the editor will be used. * @param filePath The path of the file where the new content file should be created. */ - private static async create(fileUri?: Uri) { + private static async create(fileUri?: Uri | string) { if (!fileUri) { const filePath = ArticleHelper.getActiveFile(); fileUri = filePath ? Uri.file(filePath) : undefined; } if (!fileUri) { - Notifications.warning('No file selected'); + Notifications.warning(l10n.t(LocalizationKey.commandsI18nCreateWarningNoFileSelected)); return; } + if (typeof fileUri === 'string') { + fileUri = Uri.file(fileUri); + } + const i18nSettings = await i18n.getSettings(fileUri.fsPath); if (!i18nSettings) { - Notifications.warning('No i18n configuration found'); + Notifications.warning(l10n.t(LocalizationKey.commandsI18nCreateWarningNoConfig)); return; } const isDefaultLanguage = await i18n.isDefaultLanguage(fileUri.fsPath); if (!isDefaultLanguage) { - Notifications.warning('The current file cannot be used for i18n content creation'); + Notifications.warning(l10n.t(LocalizationKey.commandsI18nCreateWarningNotDefaultLocale)); return; } const locale = await window.showQuickPick( i18nSettings.filter((i18n) => i18n.path).map((i18n) => i18n.title || i18n.locale), { - title: 'Create content for locale', - placeHolder: 'To which locale do you want to create a new content?', + title: l10n.t(LocalizationKey.commandsI18nCreateQuickPickTitle), + placeHolder: l10n.t(LocalizationKey.commandsI18nCreateQuickPickPlaceHolder), ignoreFocusOut: true } ); @@ -257,19 +270,19 @@ export class i18n { (i18n) => i18n.title === locale || i18n.locale === locale ); if (!selectedI18n || !selectedI18n.path) { - Notifications.warning('No i18n configuration found'); + Notifications.warning(l10n.t(LocalizationKey.commandsI18nCreateWarningNoConfig)); return; } let article = await ArticleHelper.getFrontMatterByPath(fileUri.fsPath); if (!article) { - Notifications.warning('No content found'); + Notifications.warning(l10n.t(LocalizationKey.commandsI18nCreateWarningNoFile)); return; } const contentType = ArticleHelper.getContentType(article); if (!contentType) { - Notifications.warning('No content type found'); + Notifications.warning(l10n.t(LocalizationKey.commandsI18nCreateWarningNoContentType)); return; } @@ -291,7 +304,7 @@ export class i18n { const newFilePath = join(i18nDir, fileInfo.base); if (await existsAsync(newFilePath)) { - Notifications.warning('File already exists'); + Notifications.error(l10n.t(LocalizationKey.commandsI18nCreateErrorFileExists)); return; } @@ -303,7 +316,14 @@ export class i18n { await openFileInEditor(newFilePath); - Notifications.info(`Created "${selectedI18n.title || selectedI18n.locale}" i18n content file`); + PagesListener.refresh(); + + Notifications.info( + l10n.t( + LocalizationKey.commandsI18nCreateSuccessCreated, + selectedI18n.title || selectedI18n.locale + ) + ); } /** diff --git a/src/components/shadcn/Dropdown.tsx b/src/components/shadcn/Dropdown.tsx index 20cc43d7..9f6132c2 100644 --- a/src/components/shadcn/Dropdown.tsx +++ b/src/components/shadcn/Dropdown.tsx @@ -16,8 +16,6 @@ const DropdownMenuPortal = DropdownMenuPrimitive.Portal const DropdownMenuSub = DropdownMenuPrimitive.Sub -const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup - const DropdownMenuSubTrigger = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef & { @@ -27,7 +25,7 @@ const DropdownMenuSubTrigger = React.forwardRef< , - React.ComponentPropsWithoutRef ->(({ className, children, checked, ...props }, ref) => ( - - - - - - - {children} - -)) -DropdownMenuCheckboxItem.displayName = - DropdownMenuPrimitive.CheckboxItem.displayName - -const DropdownMenuRadioItem = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, children, ...props }, ref) => ( - - - - - - - {children} - -)) -DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName - const DropdownMenuLabel = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef & { @@ -186,8 +138,6 @@ export { DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem, - DropdownMenuCheckboxItem, - DropdownMenuRadioItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuShortcut, @@ -196,5 +146,4 @@ export { DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, - DropdownMenuRadioGroup, } \ No newline at end of file diff --git a/src/constants/GeneralCommands.ts b/src/constants/GeneralCommands.ts index e761c335..89a060cd 100644 --- a/src/constants/GeneralCommands.ts +++ b/src/constants/GeneralCommands.ts @@ -17,6 +17,7 @@ export const GeneralCommands = { getBranch: 'getBranch', selectBranch: 'gitSelectBranch' }, + runCommand: 'runCommand', getLocalization: 'getLocalization', openOnWebsite: 'openOnWebsite' } diff --git a/src/dashboardWebView/components/Contents/ContentActions.tsx b/src/dashboardWebView/components/Contents/ContentActions.tsx index 5b2d1b2c..84a3c49e 100644 --- a/src/dashboardWebView/components/Contents/ContentActions.tsx +++ b/src/dashboardWebView/components/Contents/ContentActions.tsx @@ -1,7 +1,7 @@ import { Messenger, messageHandler } from '@estruyf/vscode/dist/client'; -import { EyeIcon, GlobeEuropeAfricaIcon, CommandLineIcon, TrashIcon, EllipsisVerticalIcon } from '@heroicons/react/24/outline'; +import { EyeIcon, GlobeEuropeAfricaIcon, CommandLineIcon, TrashIcon, EllipsisVerticalIcon, LanguageIcon } from '@heroicons/react/24/outline'; import * as React from 'react'; -import { CustomScript, ScriptType } from '../../../models'; +import { CustomScript, I18nConfig, ScriptType } from '../../../models'; import { DashboardMessage } from '../../DashboardMessage'; import { QuickAction } from '../Menu'; import { Alert } from '../Modals/Alert'; @@ -9,10 +9,10 @@ import * as l10n from '@vscode/l10n'; import { LocalizationKey } from '../../../localization'; import { useRecoilState, useRecoilValue } from 'recoil'; import { SettingsSelector } from '../../state'; -import { GeneralCommands } from '../../../constants'; +import { COMMAND_NAME, GeneralCommands } from '../../../constants'; import { PinIcon } from '../Icons/PinIcon'; import { PinnedItemsAtom } from '../../state/atom/PinnedItems'; -import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '../../../components/shadcn/Dropdown'; +import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuPortal, DropdownMenuSeparator, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger } from '../../../components/shadcn/Dropdown'; export interface IContentActionsProps { title: string; @@ -20,6 +20,14 @@ export interface IContentActionsProps { relPath: string; scripts: CustomScript[] | undefined; listView?: boolean; + locale?: I18nConfig; + isDefaultLocale?: boolean; + translations?: { + [locale: string]: { + locale: I18nConfig; + path: string; + }; + }; onOpen: () => void; } @@ -29,7 +37,10 @@ export const ContentActions: React.FunctionComponent = ({ relPath, scripts, onOpen, - listView + listView, + isDefaultLocale, + translations, + locale }: React.PropsWithChildren) => { const [pinnedItems, setPinnedItems] = useRecoilState(PinnedItemsAtom); const [showDeletionAlert, setShowDeletionAlert] = React.useState(false); @@ -52,6 +63,10 @@ export const ContentActions: React.FunctionComponent = ({ setShowDeletionAlert(false); }; + const onOpenFile = (filePath: string) => { + messageHandler.send(DashboardMessage.openFile, filePath); + } + const openOnWebsite = React.useCallback((e: React.MouseEvent) => { e.stopPropagation(); if (settings?.websiteUrl && path) { @@ -84,6 +99,13 @@ export const ContentActions: React.FunctionComponent = ({ [path] ); + const runCommand = React.useCallback((commandId: string) => { + messageHandler.send(GeneralCommands.toVSCode.runCommand, { + command: commandId, + args: path + }) + }, [path]); + const isPinned = React.useMemo(() => { return pinnedItems.includes(relPath); }, [pinnedItems, relPath]); @@ -104,6 +126,45 @@ export const ContentActions: React.FunctionComponent = ({ )); }, [scripts]); + const translationsMenu = React.useMemo(() => { + if (!locale || !translations || Object.keys(translations).length === 0) { + return null; + } + + const crntLocale = translations[locale.locale]; + const otherLocales = Object.entries(translations).filter(([key]) => key !== locale.locale); + + return ( + + + + {l10n.t(LocalizationKey.dashboardContentsContentActionsTranslationsMenu)} + + + + + onOpenFile(crntLocale.path)}> + {crntLocale.locale.title || crntLocale.locale.locale} + + + + + { + otherLocales.map(([key, value]) => ( + onOpenFile(value.path)} + > + {value.locale.title || value.locale.locale} + + )) + } + + + + ); + }, [translations, locale, isDefaultLocale]); + return ( <>
= ({ ) } - +
@@ -161,6 +225,17 @@ export const ContentActions: React.FunctionComponent = ({ ) } + { + locale && isDefaultLocale && ( + runCommand(COMMAND_NAME.i18n.create)}> + + {l10n.t(LocalizationKey.dashboardContentsContentActionsTranslationsCreate)} + + ) + } + + {translationsMenu} + {customScriptActions} diff --git a/src/dashboardWebView/components/Contents/I18nLabel.tsx b/src/dashboardWebView/components/Contents/I18nLabel.tsx index 255315a9..0e63814a 100644 --- a/src/dashboardWebView/components/Contents/I18nLabel.tsx +++ b/src/dashboardWebView/components/Contents/I18nLabel.tsx @@ -13,59 +13,14 @@ export interface II18nLabelProps { export const I18nLabel: React.FunctionComponent = ({ page }: React.PropsWithChildren) => { - - const openFile = (filePath: string) => { - messageHandler.send(DashboardMessage.openFile, filePath); - } - - const dropdown = React.useMemo(() => { - console.log(page) - if (!page.fmLocale || !page.fmTranslations || Object.keys(page.fmTranslations).length < 1) { - return null; - } - - return ( - - - - - - openFile(value)} /> - - - - { - Object.entries(page.fmTranslations).map(([key, value]) => { - return ( - openFile(value)} /> - ); - }) - } - - - ) - }, [page]) - if (!page.fmLocale) { return null; } return (
- {/* - {page.fmLocale.title || page.fmLocale.locale} */} - - {dropdown} + + {page.fmLocale.title || page.fmLocale.locale}
); }; \ No newline at end of file diff --git a/src/dashboardWebView/components/Contents/Item.tsx b/src/dashboardWebView/components/Contents/Item.tsx index 8cbc34f6..ba1edd16 100644 --- a/src/dashboardWebView/components/Contents/Item.tsx +++ b/src/dashboardWebView/components/Contents/Item.tsx @@ -79,7 +79,7 @@ export const Item: React.FunctionComponent = ({ statusHtml ? (
) : ( - cardFields?.state && draftField && draftField.name && pageData[draftField.name] ? : null + cardFields?.state && draftField && draftField.name && typeof pageData[draftField.name] !== "undefined" ? : null ) ) }, [statusHtml, cardFields?.state, draftField, pageData]); @@ -111,8 +111,7 @@ export const Item: React.FunctionComponent = ({
diff --git a/src/dashboardWebView/components/Menu/MenuItem.tsx b/src/dashboardWebView/components/Menu/MenuItem.tsx index a6020ab9..948c9e85 100644 --- a/src/dashboardWebView/components/Menu/MenuItem.tsx +++ b/src/dashboardWebView/components/Menu/MenuItem.tsx @@ -5,6 +5,7 @@ export interface IMenuItemProps { title: JSX.Element | string; value?: any; isCurrent?: boolean; + className?: string; disabled?: boolean; onClick: (value: any, e: React.MouseEvent) => void; } @@ -13,12 +14,13 @@ export const MenuItem: React.FunctionComponent = ({ title, value, isCurrent, + className, disabled, onClick }: React.PropsWithChildren) => { return ( onClick(value, e)} > diff --git a/src/dashboardWebView/components/Menu/QuickAction.tsx b/src/dashboardWebView/components/Menu/QuickAction.tsx index fecb7be9..60ff2076 100644 --- a/src/dashboardWebView/components/Menu/QuickAction.tsx +++ b/src/dashboardWebView/components/Menu/QuickAction.tsx @@ -1,12 +1,15 @@ import * as React from 'react'; +import { cn } from '../../../utils/cn'; export interface IQuickActionProps { title: string; + className?: string; onClick: (e: React.MouseEvent) => void; } export const QuickAction: React.FunctionComponent = ({ title, + className, onClick, children }: React.PropsWithChildren) => { @@ -15,7 +18,7 @@ export const QuickAction: React.FunctionComponent = ({ type="button" title={title} onClick={onClick} - className={`px-2 group inline-flex justify-center text-sm font-medium text-[var(--vscode-foreground)] hover:text-[var(--frontmatter-button-hoverBackground)]`} + className={cn(`px-2 group inline-flex justify-center text-sm font-medium text-[var(--vscode-foreground)] hover:text-[var(--frontmatter-button-hoverBackground)]`, className)} > {children} {title} diff --git a/src/dashboardWebView/hooks/usePages.tsx b/src/dashboardWebView/hooks/usePages.tsx index b255baf5..d6ef66b8 100644 --- a/src/dashboardWebView/hooks/usePages.tsx +++ b/src/dashboardWebView/hooks/usePages.tsx @@ -8,6 +8,8 @@ import { FilterValuesAtom, FiltersAtom, FolderSelector, + LocaleAtom, + LocalesAtom, SearchSelector, SettingsSelector, SortingAtom, @@ -22,20 +24,25 @@ import { parseWinPath } from '../../helpers/parseWinPath'; import { sortPages } from '../../utils/sortPages'; import { ExtensionState } from '../../constants'; import { SortingOption } from '../models'; +import { I18nConfig } from '../../models'; +import { usePrevious } from '../../panelWebView/hooks/usePrevious'; export default function usePages(pages: Page[]) { - const [pageItems, setPageItems] = useRecoilState(AllPagesAtom); const [sortedPages, setSortedPages] = useState([]); + const [pageItems, setPageItems] = useRecoilState(AllPagesAtom); const [sorting, setSorting] = useRecoilState(SortingAtom); const [tabInfo, setTabInfo] = useRecoilState(TabInfoAtom); + const [locales, setLocales] = useRecoilState(LocalesAtom); const [, setFilterValues] = useRecoilState(FilterValuesAtom); const settings = useRecoilValue(SettingsSelector); const tab = useRecoilValue(TabSelector); const folder = useRecoilValue(FolderSelector); const search = useRecoilValue(SearchSelector); const tag = useRecoilValue(TagSelector); + const locale = useRecoilValue(LocaleAtom); const category = useRecoilValue(CategorySelector); const filters = useRecoilValue(FiltersAtom); + const tabPrevious = usePrevious(tab); /** * Process all the pages by applying the sorting, filtering and searching. @@ -90,6 +97,11 @@ export default function usePages(pages: Page[]) { ); } + // If filtered by locale + if (locale) { + pagesSorted = pagesSorted.filter((page) => page.fmLocale && page.fmLocale.locale === locale); + } + const filterNames = Object.keys(filters); if (filterNames.length > 0) { for (const filter of filterNames) { @@ -102,7 +114,7 @@ export default function usePages(pages: Page[]) { setSortedPages(pagesSorted); }, - [settings, tab, folder, search, tag, category, sorting, tabInfo, filters] + [settings, tab, folder, search, tag, category, locale, sorting, tabInfo, filters] ); /** @@ -114,8 +126,23 @@ export default function usePages(pages: Page[]) { let crntPages: Page[] = Object.assign([], pages); - // Filter out translations - crntPages = crntPages.filter((page) => !page.fmLocale || (page.fmLocale && page.fmDefaultLocale)) + // Update the translations of pages + crntPages = crntPages.map((page) => { + if (page.fmTranslations) { + const translations = Object.assign({}, page.fmTranslations); + + for (const [key, value] of Object.entries(translations)) { + const translatedPage = crntPages.find((p) => parseWinPath(p.fmFilePath).toLowerCase() === parseWinPath(value.path).toLowerCase()); + if (!translatedPage) { + delete translations[key]; + } + } + + return { ...page, fmTranslations: translations }; + } + + return page; + }); // Process the tab data const draftTypes = Object.assign({}, tabInfo); @@ -193,10 +220,21 @@ export default function usePages(pages: Page[]) { } } + if (tabPrevious !== tab || !locales || locales.length === 0) { + // Store the locale information + const config: I18nConfig[] = []; + crntPages.forEach((page) => { + if (page.fmLocale && !config.some(locale => locale.locale === page.fmLocale?.locale)) { + config.push(page.fmLocale); + } + }); + setLocales(config); + } + // Set the pages setPageItems(crntPages); }, - [tab, tabInfo, settings, filters] + [tab, tabInfo, settings, filters, locales, tabPrevious] ); /** @@ -238,7 +276,7 @@ export default function usePages(pages: Page[]) { } else { startPageProcessing(); } - }, [settings?.draftField, pages, sorting, search, tag, category, filters, folder]); + }, [settings?.draftField, pages, sorting, search, tag, category, locale, filters, folder]); useEffect(() => { processByTab(sortedPages); diff --git a/src/dashboardWebView/state/atom/LocaleAtom.ts b/src/dashboardWebView/state/atom/LocaleAtom.ts new file mode 100644 index 00000000..fc52f909 --- /dev/null +++ b/src/dashboardWebView/state/atom/LocaleAtom.ts @@ -0,0 +1,8 @@ +import { atom } from 'recoil'; + +export const DEFAULT_LOCALE_STATE = ''; + +export const LocaleAtom = atom({ + key: 'LocaleAtom', + default: DEFAULT_LOCALE_STATE +}); diff --git a/src/dashboardWebView/state/atom/LocalesAtom.ts b/src/dashboardWebView/state/atom/LocalesAtom.ts new file mode 100644 index 00000000..911f45f6 --- /dev/null +++ b/src/dashboardWebView/state/atom/LocalesAtom.ts @@ -0,0 +1,7 @@ +import { atom } from 'recoil'; +import { I18nConfig } from '../../../models'; + +export const LocalesAtom = atom({ + key: 'LocalesAtom', + default: undefined +}); diff --git a/src/dashboardWebView/state/atom/index.ts b/src/dashboardWebView/state/atom/index.ts index caad6272..de507e13 100644 --- a/src/dashboardWebView/state/atom/index.ts +++ b/src/dashboardWebView/state/atom/index.ts @@ -9,6 +9,8 @@ export * from './FolderAtom'; export * from './GroupingAtom'; export * from './LightboxAtom'; export * from './LoadingAtom'; +export * from './LocaleAtom'; +export * from './LocalesAtom'; export * from './MediaFoldersAtom'; export * from './MediaTotalAtom'; export * from './ModeAtom'; diff --git a/src/listeners/general/BaseListener.ts b/src/listeners/general/BaseListener.ts index a2564fd5..3340dac6 100644 --- a/src/listeners/general/BaseListener.ts +++ b/src/listeners/general/BaseListener.ts @@ -3,7 +3,7 @@ import { Dashboard } from '../../commands/Dashboard'; import { PanelProvider } from '../../panelWebView/PanelProvider'; import { ArticleHelper, Extension } from '../../helpers'; import { Logger } from '../../helpers/Logger'; -import { commands, Uri, window } from 'vscode'; +import { commands, Uri, window, workspace } from 'vscode'; import { PostMessageData } from '../../models'; import { Preview } from '../../commands'; import { urlJoin } from 'url-join-ts'; @@ -19,6 +19,12 @@ export abstract class BaseListener { case GeneralCommands.toVSCode.openOnWebsite: this.openOnWebsite(msg.payload); break; + case GeneralCommands.toVSCode.runCommand: + if (msg.payload) { + const { command, args } = msg.payload; + commands.executeCommand(command, args); + } + break; } } diff --git a/src/localization/localization.enum.ts b/src/localization/localization.enum.ts index 446b80c0..d123bf3a 100644 --- a/src/localization/localization.enum.ts +++ b/src/localization/localization.enum.ts @@ -315,6 +315,14 @@ export enum LocalizationKey { * Are you sure you want to delete the "{0}" content? */ dashboardContentsContentActionsAlertDescription = 'dashboard.contents.contentActions.alert.description', + /** + * Create translation + */ + dashboardContentsContentActionsTranslationsCreate = 'dashboard.contents.contentActions.translations.create', + /** + * Translations + */ + dashboardContentsContentActionsTranslationsMenu = 'dashboard.contents.contentActions.translations.menu', /** * */ @@ -427,6 +435,14 @@ export enum LocalizationKey { * Please close the dashboard and try again. */ dashboardErrorViewDescription = 'dashboard.errorView.description', + /** + * Locale + */ + dashboardFiltersLanguageFilterLabel = 'dashboard.filters.languageFilter.label', + /** + * All + */ + dashboardFiltersLanguageFilterAll = 'dashboard.filters.languageFilter.all', /** * Home */ @@ -1672,6 +1688,42 @@ export enum LocalizationKey { * Create folder */ commandsFoldersGetNotificationErrorCreateAction = 'commands.folders.get.notificationError.create.action', + /** + * No file selected. + */ + commandsI18nCreateWarningNoFileSelected = 'commands.i18n.create.warning.noFileSelected', + /** + * The file could not be retrieved. + */ + commandsI18nCreateWarningNoFile = 'commands.i18n.create.warning.noFile', + /** + * Content type could not be retrieved for the current file. + */ + commandsI18nCreateWarningNoContentType = 'commands.i18n.create.warning.noContentType', + /** + * No i18n configuration found. + */ + commandsI18nCreateWarningNoConfig = 'commands.i18n.create.warning.noConfig', + /** + * The current file cannot be used for i18n content creation. + */ + commandsI18nCreateWarningNotDefaultLocale = 'commands.i18n.create.warning.notDefaultLocale', + /** + * The i18n translation already exists. + */ + commandsI18nCreateErrorFileExists = 'commands.i18n.create.error.fileExists', + /** + * Created "{0}" i18n content file. + */ + commandsI18nCreateSuccessCreated = 'commands.i18n.create.success.created', + /** + * Create content for locale + */ + commandsI18nCreateQuickPickTitle = 'commands.i18n.create.quickPick.title', + /** + * To which locale do you want to create a new content? + */ + commandsI18nCreateQuickPickPlaceHolder = 'commands.i18n.create.quickPick.placeHolder', /** * Preview: {0} */