Merge pull request #119 from Flow-Launcher/add_filecontent_search

Add file content search for Explorer plugin
This commit is contained in:
Jeremy Wu 2020-08-17 19:36:45 +10:00 committed by GitHub
commit 87ae60d2e8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 367 additions and 36 deletions

View file

@ -15,6 +15,16 @@ namespace Flow.Launcher.Infrastructure.UserSettings
if (Plugins.ContainsKey(metadata.ID))
{
var settings = Plugins[metadata.ID];
// TODO: Remove. This is one off for 1.2.0 release.
// Introduced a new action keyword in Explorer, so need to update plugin setting in the UserData folder.
// This kind of plugin meta update should be handled by a dedicated method trigger by version bump.
if (metadata.ID == "572be03c74c642baae319fc283e561a8" && metadata.ActionKeywords.Count != settings.ActionKeywords.Count)
settings.ActionKeywords = metadata.ActionKeywords;
if (string.IsNullOrEmpty(settings.Version))
settings.Version = metadata.Version;
if (settings.ActionKeywords?.Count > 0)
{
metadata.ActionKeywords = settings.ActionKeywords;
@ -28,6 +38,7 @@ namespace Flow.Launcher.Infrastructure.UserSettings
{
ID = metadata.ID,
Name = metadata.Name,
Version = metadata.Version,
ActionKeywords = metadata.ActionKeywords,
Disabled = metadata.Disabled
};
@ -39,6 +50,7 @@ namespace Flow.Launcher.Infrastructure.UserSettings
{
public string ID { get; set; }
public string Name { get; set; }
public string Version { get; set; }
public List<string> ActionKeywords { get; set; } // a reference of the action keywords from plugin manager
/// <summary>

View file

@ -184,6 +184,55 @@ namespace Flow.Launcher.Test.Plugins
$"Actual number of results is {results.Count} {Environment.NewLine}");
}
[TestCase(@"some words", @"FREETEXT('some words')")]
public void GivenWindowsIndexSearch_WhenQueryWhereRestrictionsIsForFileContentSearch_ThenShouldReturnFreeTextString(
string querySearchString, string expectedString)
{
// Given
var queryConstructor = new QueryConstructor(new Settings());
//When
var resultString = queryConstructor.QueryWhereRestrictionsForFileContentSearch(querySearchString);
// Then
Assert.IsTrue(resultString == expectedString,
$"Expected QueryWhereRestrictions string: {expectedString}{Environment.NewLine} " +
$"Actual string was: {resultString}{Environment.NewLine}");
}
[TestCase("some words", "SELECT TOP 100 System.FileName, System.ItemPathDisplay, System.ItemType " +
"FROM SystemIndex WHERE FREETEXT('some words') AND scope='file:'")]
public void GivenWindowsIndexSearch_WhenSearchForFileContent_ThenQueryShouldUseExpectedString(
string userSearchString, string expectedString)
{
// Given
var queryConstructor = new QueryConstructor(new Settings());
//When
var resultString = queryConstructor.QueryForFileContentSearch(userSearchString);
// Then
Assert.IsTrue(resultString == expectedString,
$"Expected query string: {expectedString}{Environment.NewLine} " +
$"Actual string was: {resultString}{Environment.NewLine}");
}
public void GivenQuery_WhenActionKeywordForFileContentSearchExists_ThenFileContentSearchRequiredShouldReturnTrue()
{
// Given
var query = new Query { ActionKeyword = "doc:", Search = "search term" };
var searchManager = new SearchManager(new Settings(), new PluginInitContext());
// When
var result = searchManager.IsFileContentSearch(query.ActionKeyword);
// Then
Assert.IsTrue(result,
$"Expected True for file content search. {Environment.NewLine} " +
$"Actual result was: {result}{Environment.NewLine}");
}
[TestCase(@"c:\\", false)]
[TestCase(@"i:\", true)]
[TestCase(@"\c:\", false)]

View file

@ -111,7 +111,7 @@
<system:String x:Key="done">Done</system:String>
<system:String x:Key="cannotFindSpecifiedPlugin">Can't find specified plugin</system:String>
<system:String x:Key="newActionKeywordsCannotBeEmpty">New Action Keyword can't be empty</system:String>
<system:String x:Key="newActionKeywordsHasBeenAssigned">New Action Keywords have been assigned to another plugin, please assign other new action keyword</system:String>
<system:String x:Key="newActionKeywordsHasBeenAssigned">This new Action Keyword is already assigned to another plugin, please choose a different one</system:String>
<system:String x:Key="success">Success</system:String>
<system:String x:Key="actionkeyword_tips">Use * if you don't want to specify an action keyword</system:String>

View file

@ -104,6 +104,7 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\Flow.Launcher.Core\Flow.Launcher.Core.csproj" />
<ProjectReference Include="..\..\Flow.Launcher.Infrastructure\Flow.Launcher.Infrastructure.csproj" />
<ProjectReference Include="..\..\Flow.Launcher.Plugin\Flow.Launcher.Plugin.csproj" />
</ItemGroup>

View file

@ -1,8 +1,9 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:system="clr-namespace:System;assembly=mscorlib">
<!--Dialogues-->
<system:String x:Key="plugin_explorer_make_selection_warning">Please make a selection first</system:String>
<system:String x:Key="plugin_explorer_select_folder_link_warning">Please select a folder link</system:String>
<system:String x:Key="plugin_explorer_delete_folder_link">Are you sure you want to delete {0}?</system:String>
<system:String x:Key="plugin_explorer_deletefilefolderconfirm">Are you sure you want to permanently delete this {0}?</system:String>
@ -13,10 +14,12 @@
<system:String x:Key="plugin_explorer_delete">Delete</system:String>
<system:String x:Key="plugin_explorer_edit">Edit</system:String>
<system:String x:Key="plugin_explorer_add">Add</system:String>
<system:String x:Key="plugin_explorer_manageactionkeywords_header">Customise Action Keywords</system:String>
<system:String x:Key="plugin_explorer_quickfolderaccess_header">Quick Folder Access Paths</system:String>
<system:String x:Key="plugin_explorer_indexsearchexcludedpaths_header">Index Search Excluded Paths</system:String>
<system:String x:Key="plugin_explorer_manageindexoptions">Indexing Options</system:String>
<system:String x:Key="plugin_explorer_actionkeywordview_search">Search Activation:</system:String>
<system:String x:Key="plugin_explorer_actionkeywordview_filecontentsearch">File Content Search:</system:String>
<!--Plugin Infos-->
<system:String x:Key="plugin_explorer_plugin_name">Explorer</system:String>

View file

@ -36,12 +36,15 @@ namespace Flow.Launcher.Plugin.Explorer.Search
var quickFolderLinks = quickFolderAccess.FolderList(query, settings.QuickFolderAccessLinks, context);
if (quickFolderLinks.Count > 0)
if (quickFolderLinks.Count > 0 && query.ActionKeyword == settings.SearchActionKeyword)
return quickFolderLinks;
if (string.IsNullOrEmpty(querySearch))
return results;
if (IsFileContentSearch(query.ActionKeyword))
return WindowsIndexFileContentSearch(query, querySearch);
var isEnvironmentVariable = EnvironmentVariables.IsEnvironmentVariableSearch(querySearch);
if (isEnvironmentVariable)
@ -74,6 +77,24 @@ namespace Flow.Launcher.Plugin.Explorer.Search
return results;
}
private List<Result> WindowsIndexFileContentSearch(Query query, string querySearchString)
{
var queryConstructor = new QueryConstructor(settings);
if (string.IsNullOrEmpty(querySearchString))
return new List<Result>();
return indexSearch.WindowsIndexSearch(querySearchString,
queryConstructor.CreateQueryHelper().ConnectionString,
queryConstructor.QueryForFileContentSearch,
query);
}
public bool IsFileContentSearch(string actionKeyword)
{
return actionKeyword == settings.FileContentSearchActionKeyword;
}
private List<Result> DirectoryInfoClassSearch(Query query, string querySearch)
{
var directoryInfoSearch = new DirectoryInfoSearch(context);

View file

@ -117,5 +117,23 @@ namespace Flow.Launcher.Plugin.Explorer.Search.WindowsIndex
{
return $"scope='file:'";
}
///<summary>
/// Search will be performed on all indexed file contents for the specified search keywords.
///</summary>
public string QueryForFileContentSearch(string userSearchString)
{
string query = "SELECT TOP " + settings.MaxResult + $" {CreateBaseQuery().QuerySelectColumns} FROM {SystemIndex} WHERE ";
return query + QueryWhereRestrictionsForFileContentSearch(userSearchString) + " AND " + QueryWhereRestrictionsForAllFilesAndFoldersSearch();
}
///<summary>
/// Set the required WHERE clause restriction to search within file content.
///</summary>
public string QueryWhereRestrictionsForFileContentSearch(string searchQuery)
{
return $"FREETEXT('{searchQuery}')";
}
}
}

View file

@ -17,5 +17,11 @@ namespace Flow.Launcher.Plugin.Explorer
[JsonProperty]
public List<FolderLink> IndexSearchExcludedSubdirectoryPaths { get; set; } = new List<FolderLink>();
[JsonProperty]
public string SearchActionKeyword { get; set; } = Query.GlobalPluginWildcardSign;
[JsonProperty]
public string FileContentSearchActionKeyword { get; set; } = "doc:";
}
}

View file

@ -1,4 +1,5 @@
using Flow.Launcher.Infrastructure.Storage;
using Flow.Launcher.Core.Plugin;
using Flow.Launcher.Infrastructure.Storage;
using Flow.Launcher.Plugin.Explorer.Search;
using Flow.Launcher.Plugin.Explorer.Search.FolderLinks;
using System.Diagnostics;
@ -40,5 +41,18 @@ namespace Flow.Launcher.Plugin.Explorer.ViewModels
Process.Start(psi);
}
internal void UpdateActionKeyword(string newActionKeyword, string oldActionKeyword)
{
PluginManager.ReplaceActionKeyword(Context.CurrentPluginMetadata.ID, oldActionKeyword, newActionKeyword);
if (Settings.FileContentSearchActionKeyword == oldActionKeyword)
Settings.FileContentSearchActionKeyword = newActionKeyword;
if (Settings.SearchActionKeyword == oldActionKeyword)
Settings.SearchActionKeyword = newActionKeyword;
}
internal bool IsActionKeywordAlreadyAssigned(string newActionKeyword) => PluginManager.ActionKeywordRegistered(newActionKeyword);
}
}

View file

@ -0,0 +1,36 @@
<Window x:Class="Flow.Launcher.Plugin.Explorer.Views.ActionKeywordSetting"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:Flow.Launcher.Plugin.Explorer.Views"
mc:Ignorable="d"
ResizeMode="NoResize"
WindowStartupLocation="CenterScreen"
Title="Action Keyword Setting" Height="200" Width="500">
<Grid>
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="180" />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<TextBlock Margin="20 10 10 10" FontSize="14" Grid.Row="0" Grid.Column="0" VerticalAlignment="Center"
HorizontalAlignment="Left" Text="Current Action Keyword:" />
<TextBox Name="txtCurrentActionKeyword"
Margin="10" Grid.Row="0" Width="105" Grid.Column="1"
VerticalAlignment="Center"
HorizontalAlignment="Left" />
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right" Grid.Row="1" Grid.Column="1">
<Button Click="OnConfirmButtonClick"
Margin="10 0 10 0" Width="80" Height="35"
Content="OK" />
<Button Click="OnCancelButtonClick"
Margin="10 0 10 0" Width="80" Height="35"
Content="Cancel" />
</StackPanel>
</Grid>
</Window>

View file

@ -0,0 +1,76 @@
using Flow.Launcher.Plugin.Explorer.ViewModels;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Shapes;
namespace Flow.Launcher.Plugin.Explorer.Views
{
/// <summary>
/// Interaction logic for ActionKeywordSetting.xaml
/// </summary>
public partial class ActionKeywordSetting : Window
{
private SettingsViewModel settingsViewModel;
private ActionKeywordView currentActionKeyword;
private List<ActionKeywordView> actionKeywordListView;
public ActionKeywordSetting(SettingsViewModel settingsViewModel, List<ActionKeywordView> actionKeywordListView, ActionKeywordView selectedActionKeyword)
{
InitializeComponent();
this.settingsViewModel = settingsViewModel;
currentActionKeyword = selectedActionKeyword;
txtCurrentActionKeyword.Text = selectedActionKeyword.Keyword;
this.actionKeywordListView = actionKeywordListView;
}
private void OnConfirmButtonClick(object sender, RoutedEventArgs e)
{
var newActionKeyword = txtCurrentActionKeyword.Text;
if (string.IsNullOrEmpty(newActionKeyword))
return;
if (newActionKeyword == currentActionKeyword.Keyword)
{
Close();
return;
}
if(!settingsViewModel.IsActionKeywordAlreadyAssigned(newActionKeyword))
{
settingsViewModel.UpdateActionKeyword(newActionKeyword, currentActionKeyword.Keyword);
actionKeywordListView.Where(x => x.Description == currentActionKeyword.Description).FirstOrDefault().Keyword = newActionKeyword;
Close();
return;
}
MessageBox.Show(settingsViewModel.Context.API.GetTranslation("newActionKeywordsHasBeenAssigned"));
}
private void OnCancelButtonClick(object sender, RoutedEventArgs e)
{
Close();
return;
}
}
}

View file

@ -1,4 +1,4 @@
<UserControl x:Class="Flow.Launcher.Plugin.Explorer.Views.ExplorerSettings"
<UserControl x:Class="Flow.Launcher.Plugin.Explorer.Views.ExplorerSettings"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
@ -17,6 +17,16 @@
Text="{Binding Nickname, Mode=OneTime}"
Margin="0,5,0,5" />
</DataTemplate>
<DataTemplate x:Key="ListViewActionKeywords">
<Grid>
<TextBlock
Text="{Binding Description, Mode=OneTime}"
Margin="0,5,0,0" />
<TextBlock
Text="{Binding Keyword, Mode=OneTime}"
Margin="150,5,0,0" />
</Grid>
</DataTemplate>
</UserControl.Resources>
<Grid Margin="10">
<Grid.RowDefinitions>
@ -25,8 +35,14 @@
</Grid.RowDefinitions>
<ScrollViewer Margin="20 35 0 0" HorizontalScrollBarVisibility="Hidden" Grid.Row="0" VerticalScrollBarVisibility="Auto">
<StackPanel>
<Expander Name="expActionKeywords" Header="{DynamicResource plugin_explorer_manageactionkeywords_header}"
Expanded="expActionKeywords_Click" Collapsed="expActionKeywords_Collapsed">
<ListView x:Name="lbxActionKeywords"
ItemTemplate="{StaticResource ListViewActionKeywords}"/>
</Expander>
<Expander Name="expFolderLinks" Header="{DynamicResource plugin_explorer_quickfolderaccess_header}"
Expanded="expFolderLinks_Click" Collapsed="expFolderLinks_Click">
Expanded="expFolderLinks_Click" Collapsed="expFolderLinks_Collapsed"
Margin="0 10 0 0">
<ListView
x:Name="lbxFolderLinks" AllowDrop="True"
Drop="lbxFolders_Drop"
@ -34,7 +50,7 @@
ItemTemplate="{StaticResource ListViewTemplateFolderLinks}"/>
</Expander>
<Expander x:Name="expExcludedPaths" Header="{DynamicResource plugin_explorer_indexsearchexcludedpaths_header}"
Expanded="expExcludedPaths_Click" Collapsed="expExcludedPaths_Click"
Expanded="expExcludedPaths_Click"
Margin="0 10 0 0">
<ListView
x:Name="lbxExcludedPaths" AllowDrop="True"

View file

@ -20,6 +20,9 @@ namespace Flow.Launcher.Plugin.Explorer.Views
public partial class ExplorerSettings
{
private readonly SettingsViewModel viewModel;
private List<ActionKeywordView> actionKeywordsListView;
public ExplorerSettings(SettingsViewModel viewModel)
{
InitializeComponent();
@ -30,6 +33,22 @@ namespace Flow.Launcher.Plugin.Explorer.Views
lbxExcludedPaths.ItemsSource = this.viewModel.Settings.IndexSearchExcludedSubdirectoryPaths;
actionKeywordsListView = new List<ActionKeywordView>
{
new ActionKeywordView()
{
Description = viewModel.Context.API.GetTranslation("plugin_explorer_actionkeywordview_search"),
Keyword = this.viewModel.Settings.SearchActionKeyword
},
new ActionKeywordView()
{
Description = viewModel.Context.API.GetTranslation("plugin_explorer_actionkeywordview_filecontentsearch"),
Keyword = this.viewModel.Settings.FileContentSearchActionKeyword
}
};
lbxActionKeywords.ItemsSource = actionKeywordsListView;
RefreshView();
}
@ -43,9 +62,14 @@ namespace Flow.Launcher.Plugin.Explorer.Views
btnEdit.Visibility = Visibility.Hidden;
btnAdd.Visibility = Visibility.Hidden;
if (expFolderLinks.IsExpanded || expExcludedPaths.IsExpanded)
if (expFolderLinks.IsExpanded || expExcludedPaths.IsExpanded || expActionKeywords.IsExpanded)
{
btnAdd.Visibility = Visibility.Visible;
if (!expActionKeywords.IsExpanded)
btnAdd.Visibility = Visibility.Visible;
if (expActionKeywords.IsExpanded
&& btnEdit.Visibility == Visibility.Hidden)
btnEdit.Visibility = Visibility.Visible;
if ((lbxFolderLinks.Items.Count == 0 && lbxExcludedPaths.Items.Count == 0)
&& btnDelete.Visibility == Visibility.Visible
@ -77,22 +101,50 @@ namespace Flow.Launcher.Plugin.Explorer.Views
lbxFolderLinks.Items.Refresh();
lbxExcludedPaths.Items.Refresh();
lbxActionKeywords.Items.Refresh();
}
private void expActionKeywords_Click(object sender, RoutedEventArgs e)
{
if (expActionKeywords.IsExpanded)
expActionKeywords.Height = 215;
if (expExcludedPaths.IsExpanded)
expExcludedPaths.IsExpanded = false;
if (expFolderLinks.IsExpanded)
expFolderLinks.IsExpanded = false;
RefreshView();
}
private void expActionKeywords_Collapsed(object sender, RoutedEventArgs e)
{
if (!expActionKeywords.IsExpanded)
expActionKeywords.Height = Double.NaN;
}
private void expFolderLinks_Click(object sender, RoutedEventArgs e)
{
if (expFolderLinks.IsExpanded)
expFolderLinks.Height = 235;
if (!expFolderLinks.IsExpanded)
expFolderLinks.Height = Double.NaN;
expFolderLinks.Height = 215;
if (expExcludedPaths.IsExpanded)
expExcludedPaths.IsExpanded = false;
if (expActionKeywords.IsExpanded)
expActionKeywords.IsExpanded = false;
RefreshView();
}
private void expFolderLinks_Collapsed(object sender, RoutedEventArgs e)
{
if (!expFolderLinks.IsExpanded)
expFolderLinks.Height = Double.NaN;
}
private void expExcludedPaths_Click(object sender, RoutedEventArgs e)
{
if (expExcludedPaths.IsExpanded)
@ -101,6 +153,9 @@ namespace Flow.Launcher.Plugin.Explorer.Views
if (expFolderLinks.IsExpanded)
expFolderLinks.IsExpanded = false;
if (expActionKeywords.IsExpanded)
expActionKeywords.IsExpanded = false;
RefreshView();
}
@ -132,33 +187,46 @@ namespace Flow.Launcher.Plugin.Explorer.Views
private void btnEdit_Click(object sender, RoutedEventArgs e)
{
var selectedRow = lbxFolderLinks.SelectedItem as FolderLink ?? lbxExcludedPaths.SelectedItem as FolderLink;
if (selectedRow != null)
if (lbxActionKeywords.SelectedItem is ActionKeywordView)
{
var folderBrowserDialog = new FolderBrowserDialog();
folderBrowserDialog.SelectedPath = selectedRow.Path;
if (folderBrowserDialog.ShowDialog() == DialogResult.OK)
{
if (expFolderLinks.IsExpanded)
{
var link = viewModel.Settings.QuickFolderAccessLinks.First(x => x.Path == selectedRow.Path);
link.Path = folderBrowserDialog.SelectedPath;
}
var selectedActionKeyword = lbxActionKeywords.SelectedItem as ActionKeywordView;
if (expExcludedPaths.IsExpanded)
{
var link = viewModel.Settings.IndexSearchExcludedSubdirectoryPaths.First(x => x.Path == selectedRow.Path);
link.Path = folderBrowserDialog.SelectedPath;
}
}
var actionKeywordWindow = new ActionKeywordSetting(viewModel, actionKeywordsListView, selectedActionKeyword);
actionKeywordWindow.ShowDialog();
RefreshView();
}
else
{
string warning = viewModel.Context.API.GetTranslation("plugin_explorer_select_folder_link_warning");
MessageBox.Show(warning);
var selectedRow = lbxFolderLinks.SelectedItem as FolderLink ?? lbxExcludedPaths.SelectedItem as FolderLink;
if (selectedRow != null)
{
var folderBrowserDialog = new FolderBrowserDialog();
folderBrowserDialog.SelectedPath = selectedRow.Path;
if (folderBrowserDialog.ShowDialog() == DialogResult.OK)
{
if (expFolderLinks.IsExpanded)
{
var link = viewModel.Settings.QuickFolderAccessLinks.First(x => x.Path == selectedRow.Path);
link.Path = folderBrowserDialog.SelectedPath;
}
if (expExcludedPaths.IsExpanded)
{
var link = viewModel.Settings.IndexSearchExcludedSubdirectoryPaths.First(x => x.Path == selectedRow.Path);
link.Path = folderBrowserDialog.SelectedPath;
}
}
RefreshView();
}
else
{
string warning = viewModel.Context.API.GetTranslation("plugin_explorer_make_selection_warning");
MessageBox.Show(warning);
}
}
}
@ -242,4 +310,11 @@ namespace Flow.Launcher.Plugin.Explorer.Views
viewModel.OpenWindowsIndexingOptions();
}
}
public class ActionKeywordView
{
public string Description { get; set; }
public string Keyword { get; set; }
}
}

View file

@ -1,10 +1,13 @@
{
"ID": "572be03c74c642baae319fc283e561a8",
"ActionKeyword": "*",
"ActionKeywords": [
"*",
"doc:"
],
"Name": "Explorer",
"Description": "Search and manage files and folders. Explorer utilises Windows Index Search",
"Author": "Jeremy Wu",
"Version": "1.1.0",
"Version": "1.2.0",
"Language": "csharp",
"Website": "https://github.com/Flow-Launcher/Flow.Launcher",
"ExecuteFileName": "Flow.Launcher.Plugin.Explorer.dll",

View file

@ -16,6 +16,7 @@ Flow Launcher. Dedicated to make your workflow flow more seamlessly. Aimed at be
![The Flow](https://user-images.githubusercontent.com/26427004/82151677-fa9c7100-989f-11ea-9143-81de60aaf07d.gif)
- Search everything from applications, files, bookmarks, YouTube, Twitter and more. All from the comfort of your keyboard without ever touching the mouse.
- Search for file contents
- Support search using environment variable paths
- Run batch and PowerShell commands as Administrator or a different user.
- Support languages from Chinese to Italian and more.