diff --git a/l10n/bundle.l10n.json b/l10n/bundle.l10n.json index 29a6412c..00bbf3b4 100644 --- a/l10n/bundle.l10n.json +++ b/l10n/bundle.l10n.json @@ -583,8 +583,13 @@ "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.i18n.createOrOpen.quickPick.title": "Open or create translation", + "commands.i18n.createOrOpen.quickPick.category.existing": "Existing translations", + "commands.i18n.createOrOpen.quickPick.action.open": "Open \"{0}\"", + "commands.i18n.createOrOpen.quickPick.category.new": "New translations", + "commands.i18n.createOrOpen.quickPick.action.create": "Create \"{0}\"", "commands.i18n.translate.progress.title": "Translating 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 e094473e..0e725729 100644 --- a/package.json +++ b/package.json @@ -2393,6 +2393,15 @@ "title": "%command.frontMatter.cache.clear%", "category": "Front Matter" }, + { + "command": "frontMatter.i18n.createOrOpen", + "title": "%command.frontMatter.i18n.createOrOpen%", + "category": "Front Matter", + "icon": { + "light": "assets/icons/i18n-light.svg", + "dark": "assets/icons/i18n-dark.svg" + } + }, { "command": "frontMatter.i18n.create", "title": "%command.frontMatter.i18n.create%", @@ -2448,7 +2457,7 @@ "when": "frontMatter:file:isValid == true" }, { - "command": "frontMatter.i18n.create", + "command": "frontMatter.i18n.createOrOpen", "group": "navigation@-127", "when": "frontMatter:file:isValid && frontMatter:i18n:enabled" }, diff --git a/package.nls.json b/package.nls.json index 7fc0537f..43953f35 100644 --- a/package.nls.json +++ b/package.nls.json @@ -50,6 +50,7 @@ "command.frontMatter.git.sync": "Sync", "command.frontMatter.cache.clear": "Clear cache", "command.frontMatter.i18n.create": "Create new translation", + "command.frontMatter.i18n.createOrOpen": "Create or open translation", "settings.configuration.title": "Front Matter: use frontmatter.json for shared team settings", "setting.frontMatter.projects.markdownDescription": "Specify the list of projects to load in the Front Matter CMS. [Local](https://file%2B.vscode-resource.vscode-cdn.net/Users/eliostruyf/nodejs/frontmatter-test-projects/astro-blog/test.html) - [Docs](https://frontmatter.codes/docs/settings/overview#frontmatter.projects) - [View in VS Code](vscode://simpleBrowser.show?%5B%22https://frontmatter.codes/docs/settings/overview%23frontmatter.projects%22%5D)", "setting.frontMatter.projects.items.properties.name.markdownDescription": "Specify the name of the project.", diff --git a/src/commands/i18n.ts b/src/commands/i18n.ts index 845040ac..aa9120da 100644 --- a/src/commands/i18n.ts +++ b/src/commands/i18n.ts @@ -1,4 +1,14 @@ -import { ProgressLocation, Uri, commands, window, workspace } from 'vscode'; +import { + ProgressLocation, + QuickPickItem, + QuickPickItemKind, + QuickPickOptions, + ThemeIcon, + Uri, + commands, + window, + workspace +} from 'vscode'; import { ArticleHelper, ContentType, @@ -32,6 +42,7 @@ export class i18n { const subscriptions = Extension.getInstance().subscriptions; subscriptions.push(commands.registerCommand(COMMAND_NAME.i18n.create, i18n.create)); + subscriptions.push(commands.registerCommand(COMMAND_NAME.i18n.createOrOpen, i18n.createOrOpen)); i18n.clearFiles(); } @@ -391,6 +402,181 @@ export class i18n { ); } + /** + * This method handles the process of creating a new translation file if it doesn't exist, + * or opening an existing translation file if it's already present. + * @param filePath The path of the file where the new content file should be created or being switched to. Behaves like `create` if not provided. + */ + private static async createOrOpen(fileUri?: Uri | string) { + if (!fileUri) { + const filePath = ArticleHelper.getActiveFile(); + fileUri = filePath ? Uri.file(filePath) : undefined; + } + + if (!fileUri) { + Notifications.warning(l10n.t(LocalizationKey.commandsI18nCreateWarningNoFileSelected)); + return; + } + + if (typeof fileUri === 'string') { + fileUri = Uri.file(fileUri); + } + + const pageFolder = await Folders.getPageFolderByFilePath(fileUri.fsPath); + if (!pageFolder || !pageFolder.localeSourcePath) { + Notifications.error(l10n.t(LocalizationKey.commandsI18nCreateErrorNoContentFolder)); + return; + } + + let article = await ArticleHelper.getFrontMatterByPath(fileUri.fsPath); + if (!article) { + Notifications.warning(l10n.t(LocalizationKey.commandsI18nCreateWarningNoFile)); + return; + } + + const contentType = await ArticleHelper.getContentType(article); + if (!contentType) { + Notifications.warning(l10n.t(LocalizationKey.commandsI18nCreateWarningNoContentType)); + return; + } + + const i18nSettings = await i18n.getSettings(fileUri.fsPath); + if (!i18nSettings) { + Notifications.warning(l10n.t(LocalizationKey.commandsI18nCreateWarningNoConfig)); + return; + } + + const sourceLocale = await i18n.getLocale(fileUri.fsPath); + if (!sourceLocale || !sourceLocale.locale) { + Notifications.warning(l10n.t(LocalizationKey.commandsI18nCreateErrorNoLocaleDefinition)); + return; + } + + // Determine translation file paths + const fileInfo = parse(fileUri.fsPath); + let pageBundleDir = ''; + if (await ArticleHelper.isPageBundle(fileUri.fsPath)) { + const dir = ArticleHelper.getPageFolderFromBundlePath(fileUri.fsPath); + pageBundleDir = fileUri.fsPath.replace(dir, ''); + pageBundleDir = join(parse(pageBundleDir).dir); + } + + // Gather target locales & metadata + const translations = (await i18n.getTranslations(fileUri.fsPath)) || {}; + const targetLocales = i18nSettings + .filter((i18n) => { + return i18n.path && i18n.locale !== sourceLocale.locale; + }) + .map((i18n) => { + return { + ...i18n, + dir: join(pageFolder.localeSourcePath!, i18n.path!, pageBundleDir), + absolutePath: join( + pageFolder.localeSourcePath!, + i18n.path!, + pageBundleDir, + fileInfo.base + ), + relativePath: join(i18n.path!, pageBundleDir, fileInfo.base) + }; + }) + .sort((a, b) => (a.title || a.locale).localeCompare(b.title || b.locale)); + + if (targetLocales.length === 0) { + Notifications.warning(l10n.t(LocalizationKey.commandsI18nCreateErrorNoLocales)); + return; + } + + // Configure quick pick items & options + const existingTargetLocales = targetLocales.filter((i18n) => translations[i18n.locale]); + const newTargetLocales = targetLocales.filter((i18n) => !translations[i18n.locale]); + const quickPickItems: QuickPickItem[] = [ + ...(existingTargetLocales.length + ? [ + { + label: l10n.t(LocalizationKey.commandsI18nCreateOrOpenQuickPickCategoryExisting), + kind: QuickPickItemKind.Separator + }, + ...existingTargetLocales.map((i18n) => ({ + label: i18n.title || i18n.locale, + detail: l10n.t(LocalizationKey.commandsI18nCreateOrOpenQuickPickActionOpen, i18n.relativePath) + })) + ] + : []), + ...(newTargetLocales.length + ? [ + { + label: l10n.t(LocalizationKey.commandsI18nCreateOrOpenQuickPickCategoryNew), + kind: QuickPickItemKind.Separator + }, + ...newTargetLocales.map((i18n) => ({ + label: i18n.title || i18n.locale, + detail: `$(file-add) ${l10n.t(LocalizationKey.commandsI18nCreateOrOpenQuickPickActionCreate, i18n.relativePath)}` + })) + ] + : []) + ]; + const quickPickOptions: QuickPickOptions = { + title: l10n.t(LocalizationKey.commandsI18nCreateOrOpenQuickPickTitle), + ignoreFocusOut: true, + matchOnDetail: true + }; + + const localeItem = await window.showQuickPick(quickPickItems, quickPickOptions); + const locale = localeItem?.label; + if (!locale) { + return; + } + + const targetLocale = targetLocales.find( + (i18n) => i18n.title === locale || i18n.locale === locale + ); + if (!targetLocale || !targetLocale.path) { + Notifications.warning(l10n.t(LocalizationKey.commandsI18nCreateWarningNoConfig)); + return; + } + + // If it exists, open the translation file + if (await existsAsync(targetLocale.absolutePath)) { + await openFileInEditor(targetLocale.absolutePath); + return; + } + + // If it doesn't exist, create the new translation file & update front matter + if (!(await existsAsync(targetLocale.dir))) { + await workspace.fs.createDirectory(Uri.file(targetLocale.dir)); + } + + article = await i18n.updateFrontMatter( + article, + fileUri.fsPath, + contentType, + sourceLocale, + targetLocale, + targetLocale.dir + ); + if (sourceLocale?.locale) { + article = await i18n.translate(article, sourceLocale, targetLocale); + } + + const newFileUri = Uri.file(targetLocale.absolutePath); + await workspace.fs.writeFile( + newFileUri, + Buffer.from(ArticleHelper.stringifyFrontMatter(article.content, article.data)) + ); + + await openFileInEditor(targetLocale.absolutePath); + + PagesListener.refresh(); + + Notifications.info( + l10n.t( + LocalizationKey.commandsI18nCreateSuccessCreated, + sourceLocale.title || sourceLocale.locale + ) + ); + } + /** * Translates the given article from the source locale to the target locale using DeepL translation service. * @param article - The article to be translated. diff --git a/src/constants/Extension.ts b/src/constants/Extension.ts index 4c1e01cc..47dec66e 100644 --- a/src/constants/Extension.ts +++ b/src/constants/Extension.ts @@ -70,7 +70,8 @@ export const COMMAND_NAME = { // i18n i18n: { - create: getCommandName('i18n.create') + create: getCommandName('i18n.create'), + createOrOpen: getCommandName('i18n.createOrOpen') }, // Project diff --git a/src/localization/localization.enum.ts b/src/localization/localization.enum.ts index 18e573ac..8387f727 100644 --- a/src/localization/localization.enum.ts +++ b/src/localization/localization.enum.ts @@ -1884,6 +1884,26 @@ export enum LocalizationKey { * To which locale do you want to create a new content? */ commandsI18nCreateQuickPickPlaceHolder = 'commands.i18n.create.quickPick.placeHolder', + /** + * Open or create translation + */ + commandsI18nCreateOrOpenQuickPickTitle = 'commands.i18n.createOrOpen.quickPick.title', + /** + * Existing translations + */ + commandsI18nCreateOrOpenQuickPickCategoryExisting = 'commands.i18n.createOrOpen.quickPick.category.existing', + /** + * Open "{0}" + */ + commandsI18nCreateOrOpenQuickPickActionOpen = 'commands.i18n.createOrOpen.quickPick.action.open', + /** + * New translations + */ + commandsI18nCreateOrOpenQuickPickCategoryNew = 'commands.i18n.createOrOpen.quickPick.category.new', + /** + * Create "{0}" + */ + commandsI18nCreateOrOpenQuickPickActionCreate = 'commands.i18n.createOrOpen.quickPick.action.create', /** * Translating content... */