Compare commits

...

3 Commits

Author SHA1 Message Date
baa4b9bd1a
Fix lazy loading of detail tabs
All checks were successful
Pull Request / build (pull_request) Successful in 3m59s
2024-11-03 13:34:09 +01:00
64ecdd84aa
Add "Search on Web" context menu for entries and sentences 2024-11-03 11:16:20 +01:00
b8194f3ca6
Add "Copy sentence" context menu for example sentences 2024-11-03 11:01:55 +01:00
12 changed files with 131 additions and 25 deletions

View File

@ -34,6 +34,11 @@ class Model(
private val internalIsFindingCharacters: BooleanProperty = SimpleBooleanProperty(false) private val internalIsFindingCharacters: BooleanProperty = SimpleBooleanProperty(false)
private val internalIsFindingSentences: BooleanProperty = SimpleBooleanProperty(false) private val internalIsFindingSentences: BooleanProperty = SimpleBooleanProperty(false)
private val internalFinishedFindingWordsBeginning: BooleanProperty = SimpleBooleanProperty(false)
private val internalFinishedFindingWordsContaining: BooleanProperty = SimpleBooleanProperty(false)
private val internalFinishedFindingCharacters: BooleanProperty = SimpleBooleanProperty(false)
private val internalFinishedFindingSentences: BooleanProperty = SimpleBooleanProperty(false)
val selectedEntry: ReadOnlyObjectProperty<DictionaryEntryFx> = internalSelectedEntry val selectedEntry: ReadOnlyObjectProperty<DictionaryEntryFx> = internalSelectedEntry
val searchResults: ObservableList<DictionaryEntryFx> = val searchResults: ObservableList<DictionaryEntryFx> =
@ -53,6 +58,11 @@ class Model(
val isFindingCharacters: ReadOnlyBooleanProperty = internalIsFindingCharacters val isFindingCharacters: ReadOnlyBooleanProperty = internalIsFindingCharacters
val isFindingSentences: ReadOnlyBooleanProperty = internalIsFindingSentences val isFindingSentences: ReadOnlyBooleanProperty = internalIsFindingSentences
val finishedFindingWordsBeginning: ReadOnlyBooleanProperty = internalFinishedFindingWordsBeginning
val finishedFindingWordsContaining: ReadOnlyBooleanProperty = internalFinishedFindingWordsContaining
val finishedFindingCharacters: ReadOnlyBooleanProperty = internalFinishedFindingCharacters
val finishedFindingSentences: ReadOnlyBooleanProperty = internalFinishedFindingSentences
private val coroutineScope = MainScope() private val coroutineScope = MainScope()
fun search(query: String, searchMode: SearchMode) { fun search(query: String, searchMode: SearchMode) {
@ -72,6 +82,7 @@ class Model(
.map { it.toFx() } .map { it.toFx() }
) )
internalIsFindingWordsBeginning.value = false internalIsFindingWordsBeginning.value = false
internalFinishedFindingWordsBeginning.value = true
} }
} }
@ -84,6 +95,7 @@ class Model(
.map { it.toFx() } .map { it.toFx() }
) )
internalIsFindingWordsContaining.value = false internalIsFindingWordsContaining.value = false
internalFinishedFindingWordsContaining.value = true
} }
} }
@ -96,6 +108,7 @@ class Model(
.map { it.toFx() } .map { it.toFx() }
) )
internalIsFindingCharacters.value = false internalIsFindingCharacters.value = false
internalFinishedFindingCharacters.value = true
} }
} }
@ -108,6 +121,7 @@ class Model(
.map { it.toFx() } .map { it.toFx() }
) )
internalIsFindingSentences.value = false internalIsFindingSentences.value = false
internalFinishedFindingSentences.value = true
} }
} }
@ -116,6 +130,12 @@ class Model(
internalWordsContaining.setAll(emptyList()) internalWordsContaining.setAll(emptyList())
internalCharacters.setAll(emptyList()) internalCharacters.setAll(emptyList())
internalSentences.setAll(emptyList()) internalSentences.setAll(emptyList())
internalFinishedFindingCharacters.value = false
internalFinishedFindingWordsBeginning.value = false
internalFinishedFindingWordsContaining.value = false
internalFinishedFindingSentences.value = false
internalSelectedEntry.value = entry internalSelectedEntry.value = entry
} }

View File

@ -8,6 +8,7 @@ import com.marvinelsen.willow.ui.controllers.MenuController
import com.marvinelsen.willow.ui.controllers.SearchController import com.marvinelsen.willow.ui.controllers.SearchController
import com.marvinelsen.willow.ui.controllers.SearchResultsController import com.marvinelsen.willow.ui.controllers.SearchResultsController
import javafx.application.Application import javafx.application.Application
import javafx.application.HostServices
import javafx.fxml.FXMLLoader import javafx.fxml.FXMLLoader
import javafx.scene.Scene import javafx.scene.Scene
import javafx.scene.image.Image import javafx.scene.image.Image
@ -46,15 +47,17 @@ class WillowApplication : Application() {
val config = Config() val config = Config()
config.load() config.load()
val hostServices: HostServices = hostServices
val fxmlLoader = FXMLLoader() val fxmlLoader = FXMLLoader()
fxmlLoader.resources = ResourceBundle.getBundle("i18n/willow", config.locale.value) fxmlLoader.resources = ResourceBundle.getBundle("i18n/willow", config.locale.value)
fxmlLoader.controllerFactory = Callback { type -> fxmlLoader.controllerFactory = Callback { type ->
when (type) { when (type) {
MainController::class.java -> MainController(model) MainController::class.java -> MainController(model)
MenuController::class.java -> MenuController(model, config) MenuController::class.java -> MenuController(model, config)
DetailsController::class.java -> DetailsController(model, config) DetailsController::class.java -> DetailsController(model, config, hostServices)
SearchController::class.java -> SearchController(model) SearchController::class.java -> SearchController(model)
SearchResultsController::class.java -> SearchResultsController(model, config) SearchResultsController::class.java -> SearchResultsController(model, config, hostServices)
else -> error("Trying to instantiate unknown controller type $type") else -> error("Trying to instantiate unknown controller type $type")
} }
} }

View File

@ -5,6 +5,7 @@ import com.marvinelsen.willow.config.Pronunciation
import com.marvinelsen.willow.config.Script import com.marvinelsen.willow.config.Script
import com.marvinelsen.willow.ui.DictionaryEntryFx import com.marvinelsen.willow.ui.DictionaryEntryFx
import com.marvinelsen.willow.ui.util.createContextMenuForEntry import com.marvinelsen.willow.ui.util.createContextMenuForEntry
import javafx.application.HostServices
import javafx.beans.binding.Bindings import javafx.beans.binding.Bindings
import javafx.geometry.VPos import javafx.geometry.VPos
import javafx.scene.control.Label import javafx.scene.control.Label
@ -15,10 +16,14 @@ import javafx.scene.layout.VBox
import javafx.util.Callback import javafx.util.Callback
import java.util.ResourceBundle import java.util.ResourceBundle
class DictionaryEntryCellFactory(private val resources: ResourceBundle, private val config: Config) : class DictionaryEntryCellFactory(
Callback<ListView<DictionaryEntryFx?>, ListCell<DictionaryEntryFx?>> { private val resources: ResourceBundle,
private val config: Config,
private val hostServices: HostServices,
) : Callback<ListView<DictionaryEntryFx?>, ListCell<DictionaryEntryFx?>> {
override fun call(listView: ListView<DictionaryEntryFx?>): ListCell<DictionaryEntryFx?> { override fun call(listView: ListView<DictionaryEntryFx?>): ListCell<DictionaryEntryFx?> {
val entryCell = EntryCell(resources, config) val entryCell = EntryCell(resources, config, hostServices)
entryCell.prefWidthProperty().bind(listView.widthProperty().subtract(CELL_PADDING)) entryCell.prefWidthProperty().bind(listView.widthProperty().subtract(CELL_PADDING))
return entryCell return entryCell
} }
@ -28,8 +33,12 @@ class DictionaryEntryCellFactory(private val resources: ResourceBundle, private
} }
} }
private class EntryCell(private val resources: ResourceBundle, private val config: Config) : private class EntryCell(
ListCell<DictionaryEntryFx?>() { private val resources: ResourceBundle,
private val config: Config,
private val hostServices: HostServices,
) : ListCell<DictionaryEntryFx?>() {
private val labelHeadword = Label().apply { private val labelHeadword = Label().apply {
styleClass.add("headword") styleClass.add("headword")
styleProperty().bind( styleProperty().bind(
@ -118,14 +127,16 @@ private class EntryCell(private val resources: ResourceBundle, private val confi
entry.crossStraitsDefinitions.isNotEmpty() -> entry.crossStraitsDefinitions.joinToString( entry.crossStraitsDefinitions.isNotEmpty() -> entry.crossStraitsDefinitions.joinToString(
separator = " / " separator = " / "
) { it.definition } ) { it.definition }
entry.moedictDefinitions.isNotEmpty() -> entry.moedictDefinitions.joinToString( entry.moedictDefinitions.isNotEmpty() -> entry.moedictDefinitions.joinToString(
separator = " / " separator = " / "
) { it.definition } ) { it.definition }
else -> error("No definition for entry") else -> error("No definition for entry")
} }
labelDefinition.text = definition labelDefinition.text = definition
contextMenu = createContextMenuForEntry(entry, resources) contextMenu = createContextMenuForEntry(entry, resources, hostServices)
graphic = root graphic = root
} }
} }

View File

@ -3,17 +3,24 @@ package com.marvinelsen.willow.ui.cells
import com.marvinelsen.willow.config.Config import com.marvinelsen.willow.config.Config
import com.marvinelsen.willow.config.Script import com.marvinelsen.willow.config.Script
import com.marvinelsen.willow.ui.SentenceFx import com.marvinelsen.willow.ui.SentenceFx
import com.marvinelsen.willow.ui.util.createContextMenuForSentence
import javafx.application.HostServices
import javafx.beans.binding.Bindings import javafx.beans.binding.Bindings
import javafx.scene.control.Label import javafx.scene.control.Label
import javafx.scene.control.ListCell import javafx.scene.control.ListCell
import javafx.scene.control.ListView import javafx.scene.control.ListView
import javafx.scene.layout.VBox import javafx.scene.layout.VBox
import javafx.util.Callback import javafx.util.Callback
import java.util.ResourceBundle
class SentenceCellFactory(private val config: Config) : Callback<ListView<SentenceFx?>, ListCell<SentenceFx?>> { class SentenceCellFactory(
private val resources: ResourceBundle,
private val config: Config,
private val hostServices: HostServices,
) : Callback<ListView<SentenceFx?>, ListCell<SentenceFx?>> {
override fun call(listView: ListView<SentenceFx?>): ListCell<SentenceFx?> { override fun call(listView: ListView<SentenceFx?>): ListCell<SentenceFx?> {
val sentenceCell = SentenceCell(config) val sentenceCell = SentenceCell(resources, config, hostServices)
sentenceCell.prefWidthProperty().bind(listView.widthProperty().subtract(CELL_PADDING)) sentenceCell.prefWidthProperty().bind(listView.widthProperty().subtract(CELL_PADDING))
return sentenceCell return sentenceCell
} }
@ -23,7 +30,12 @@ class SentenceCellFactory(private val config: Config) : Callback<ListView<Senten
} }
} }
private class SentenceCell(private val config: Config) : ListCell<SentenceFx?>() { private class SentenceCell(
private val resources: ResourceBundle,
private val config: Config,
private val hostServices: HostServices,
) : ListCell<SentenceFx?>() {
private val labelSentence = Label().apply { private val labelSentence = Label().apply {
styleClass.add("sentence") styleClass.add("sentence")
isWrapText = true isWrapText = true
@ -51,6 +63,7 @@ private class SentenceCell(private val config: Config) : ListCell<SentenceFx?>()
) )
) )
contextMenu = createContextMenuForSentence(sentence, resources, hostServices)
graphic = root graphic = root
} }
} }

View File

@ -9,6 +9,7 @@ import com.marvinelsen.willow.ui.SentenceFx
import com.marvinelsen.willow.ui.cells.DictionaryEntryCellFactory import com.marvinelsen.willow.ui.cells.DictionaryEntryCellFactory
import com.marvinelsen.willow.ui.cells.SentenceCellFactory import com.marvinelsen.willow.ui.cells.SentenceCellFactory
import com.marvinelsen.willow.ui.util.createContextMenuForEntry import com.marvinelsen.willow.ui.util.createContextMenuForEntry
import javafx.application.HostServices
import javafx.beans.binding.Bindings import javafx.beans.binding.Bindings
import javafx.fxml.FXML import javafx.fxml.FXML
import javafx.scene.control.Label import javafx.scene.control.Label
@ -31,7 +32,12 @@ import kotlinx.html.stream.createHTML
import java.util.ResourceBundle import java.util.ResourceBundle
@Suppress("UnusedPrivateMember", "TooManyFunctions") @Suppress("UnusedPrivateMember", "TooManyFunctions")
class DetailsController(private val model: Model, private val config: Config) { class DetailsController(
private val model: Model,
private val config: Config,
private val hostServices: HostServices,
) {
@FXML @FXML
private lateinit var resources: ResourceBundle private lateinit var resources: ResourceBundle
@ -192,7 +198,7 @@ class DetailsController(private val model: Model, private val config: Config) {
private fun initializeListViewSentences() { private fun initializeListViewSentences() {
listViewSentences.apply { listViewSentences.apply {
cellFactory = SentenceCellFactory(config) cellFactory = SentenceCellFactory(resources, config, hostServices)
items = model.sentences items = model.sentences
disableProperty().bind(Bindings.or(model.isFindingSentences, Bindings.isEmpty(model.sentences))) disableProperty().bind(Bindings.or(model.isFindingSentences, Bindings.isEmpty(model.sentences)))
@ -205,7 +211,7 @@ class DetailsController(private val model: Model, private val config: Config) {
private fun initializeListViewWordsContaining() { private fun initializeListViewWordsContaining() {
listViewWordsContaining.apply { listViewWordsContaining.apply {
cellFactory = DictionaryEntryCellFactory(resources, config) cellFactory = DictionaryEntryCellFactory(resources, config, hostServices)
items = model.wordsContaining items = model.wordsContaining
disableProperty().bind(Bindings.or(model.isFindingWordsContaining, Bindings.isEmpty(model.wordsContaining))) disableProperty().bind(Bindings.or(model.isFindingWordsContaining, Bindings.isEmpty(model.wordsContaining)))
@ -218,7 +224,7 @@ class DetailsController(private val model: Model, private val config: Config) {
private fun initializeListViewWordsBeginning() { private fun initializeListViewWordsBeginning() {
listViewWordsBeginning.apply { listViewWordsBeginning.apply {
cellFactory = DictionaryEntryCellFactory(resources, config) cellFactory = DictionaryEntryCellFactory(resources, config, hostServices)
items = model.wordsBeginning items = model.wordsBeginning
disableProperty().bind(Bindings.or(model.isFindingWordsBeginning, Bindings.isEmpty(model.wordsBeginning))) disableProperty().bind(Bindings.or(model.isFindingWordsBeginning, Bindings.isEmpty(model.wordsBeginning)))
@ -231,7 +237,7 @@ class DetailsController(private val model: Model, private val config: Config) {
private fun initializeListViewCharacters() { private fun initializeListViewCharacters() {
listViewCharacters.apply { listViewCharacters.apply {
cellFactory = DictionaryEntryCellFactory(resources, config) cellFactory = DictionaryEntryCellFactory(resources, config, hostServices)
items = model.characters items = model.characters
disableProperty().bind(Bindings.or(model.isFindingCharacters, Bindings.isEmpty(model.characters))) disableProperty().bind(Bindings.or(model.isFindingCharacters, Bindings.isEmpty(model.characters)))
@ -258,7 +264,7 @@ class DetailsController(private val model: Model, private val config: Config) {
private fun headerOnContextMenuRequested(contextMenuEvent: ContextMenuEvent) { private fun headerOnContextMenuRequested(contextMenuEvent: ContextMenuEvent) {
if (model.selectedEntry.value == null) return if (model.selectedEntry.value == null) return
createContextMenuForEntry(model.selectedEntry.value, resources).show( createContextMenuForEntry(model.selectedEntry.value, resources, hostServices).show(
flowPaneHeader.scene.window, flowPaneHeader.scene.window,
contextMenuEvent.screenX, contextMenuEvent.screenX,
contextMenuEvent.screenY contextMenuEvent.screenY
@ -269,22 +275,22 @@ class DetailsController(private val model: Model, private val config: Config) {
private fun lazyUpdateTabContent(selectedTabId: String?) { private fun lazyUpdateTabContent(selectedTabId: String?) {
when (selectedTabId) { when (selectedTabId) {
"tabWords" -> { "tabWords" -> {
if (model.wordsContaining.isEmpty()) { if (!model.finishedFindingWordsContaining.value) {
model.findWordsContaining() model.findWordsContaining()
} }
if (model.wordsBeginning.isEmpty()) { if (!model.finishedFindingWordsBeginning.value) {
model.findWordsBeginning() model.findWordsBeginning()
} }
} }
"tabCharacters" -> { "tabCharacters" -> {
if (model.characters.isNotEmpty()) return if (model.finishedFindingCharacters.value) return
model.findCharacters() model.findCharacters()
} }
"tabSentences" -> { "tabSentences" -> {
if (model.sentences.isNotEmpty()) return if (model.finishedFindingSentences.value) return
model.findSentences() model.findSentences()
} }

View File

@ -4,6 +4,7 @@ import com.marvinelsen.willow.Model
import com.marvinelsen.willow.config.Config import com.marvinelsen.willow.config.Config
import com.marvinelsen.willow.ui.DictionaryEntryFx import com.marvinelsen.willow.ui.DictionaryEntryFx
import com.marvinelsen.willow.ui.cells.DictionaryEntryCellFactory import com.marvinelsen.willow.ui.cells.DictionaryEntryCellFactory
import javafx.application.HostServices
import javafx.beans.binding.Bindings import javafx.beans.binding.Bindings
import javafx.fxml.FXML import javafx.fxml.FXML
import javafx.scene.control.Label import javafx.scene.control.Label
@ -11,7 +12,12 @@ import javafx.scene.control.ListView
import javafx.scene.control.ProgressIndicator import javafx.scene.control.ProgressIndicator
import java.util.ResourceBundle import java.util.ResourceBundle
class SearchResultsController(private val model: Model, private val config: Config) { class SearchResultsController(
private val model: Model,
private val config: Config,
private val hostServices: HostServices,
) {
@FXML @FXML
private lateinit var resources: ResourceBundle private lateinit var resources: ResourceBundle
@ -28,7 +34,7 @@ class SearchResultsController(private val model: Model, private val config: Conf
@FXML @FXML
@Suppress("UnusedPrivateMember") @Suppress("UnusedPrivateMember")
private fun initialize() { private fun initialize() {
listViewSearchResults.cellFactory = DictionaryEntryCellFactory(resources, config) listViewSearchResults.cellFactory = DictionaryEntryCellFactory(resources, config, hostServices)
listViewSearchResults.items = model.searchResults listViewSearchResults.items = model.searchResults
listViewSearchResults listViewSearchResults
.disableProperty() .disableProperty()

View File

@ -1,12 +1,20 @@
package com.marvinelsen.willow.ui.util package com.marvinelsen.willow.ui.util
import com.marvinelsen.willow.ui.DictionaryEntryFx import com.marvinelsen.willow.ui.DictionaryEntryFx
import com.marvinelsen.willow.ui.SentenceFx
import javafx.application.HostServices
import javafx.event.EventHandler import javafx.event.EventHandler
import javafx.scene.control.ContextMenu import javafx.scene.control.ContextMenu
import javafx.scene.control.MenuItem import javafx.scene.control.MenuItem
import java.net.URLEncoder
import java.nio.charset.StandardCharsets
import java.util.ResourceBundle import java.util.ResourceBundle
fun createContextMenuForEntry(entry: DictionaryEntryFx, resourceBundle: ResourceBundle) = ContextMenu().apply { fun createContextMenuForEntry(
entry: DictionaryEntryFx,
resourceBundle: ResourceBundle,
hostServices: HostServices,
) = ContextMenu().apply {
val menuItemCopyHeadword = val menuItemCopyHeadword =
MenuItem(resourceBundle.getString("menubar.edit.copy.headword")).apply { MenuItem(resourceBundle.getString("menubar.edit.copy.headword")).apply {
onAction = EventHandler { ClipboardHelper.copyString(entry.traditionalProperty.value) } onAction = EventHandler { ClipboardHelper.copyString(entry.traditionalProperty.value) }
@ -17,5 +25,34 @@ fun createContextMenuForEntry(entry: DictionaryEntryFx, resourceBundle: Resource
onAction = EventHandler { ClipboardHelper.copyString(entry.pinyinWithToneMarksProperty.value) } onAction = EventHandler { ClipboardHelper.copyString(entry.pinyinWithToneMarksProperty.value) }
} }
items.addAll(menuItemCopyHeadword, menuItemCopyPronunciation) val menuItemSearchOnWeb =
MenuItem(resourceBundle.getString("menubar.edit.search-web")).apply {
onAction = EventHandler {
val query = URLEncoder.encode(entry.traditionalProperty.value, StandardCharsets.UTF_8)
hostServices.showDocument("https://duckduckgo.com/?q=$query")
}
}
items.addAll(menuItemCopyHeadword, menuItemCopyPronunciation, menuItemSearchOnWeb)
}
fun createContextMenuForSentence(
sentence: SentenceFx,
resourceBundle: ResourceBundle,
hostServices: HostServices,
) = ContextMenu().apply {
val menuItemCopySentence =
MenuItem(resourceBundle.getString("menubar.edit.copy.sentence")).apply {
onAction = EventHandler { ClipboardHelper.copyString(sentence.traditionalProperty.value) }
}
val menuItemSearchOnWeb =
MenuItem(resourceBundle.getString("menubar.edit.search-web")).apply {
onAction = EventHandler {
val query = URLEncoder.encode(sentence.traditionalProperty.value, StandardCharsets.UTF_8)
hostServices.showDocument("https://duckduckgo.com/?q=$query")
}
}
items.addAll(menuItemCopySentence, menuItemSearchOnWeb)
} }

View File

@ -25,6 +25,8 @@ menubar.file.preferences=_Preferences…
menubar.edit=_Edit menubar.edit=_Edit
menubar.edit.copy.headword=Copy Headword menubar.edit.copy.headword=Copy Headword
menubar.edit.copy.pronunciation=Copy Pronunciation menubar.edit.copy.pronunciation=Copy Pronunciation
menubar.edit.copy.sentence=Copy Sentence
menubar.edit.search-web=Search on Web...
menubar.help=_Help menubar.help=_Help
menubar.help.about=_About… menubar.help.about=_About…

View File

@ -25,6 +25,8 @@ menubar.file.preferences=_Einstellungen…
menubar.edit=_Bearbeiten menubar.edit=_Bearbeiten
menubar.edit.copy.headword=Kopiere Wort menubar.edit.copy.headword=Kopiere Wort
menubar.edit.copy.pronunciation=Kopiere Aussprache menubar.edit.copy.pronunciation=Kopiere Aussprache
menubar.edit.copy.sentence=Copy Sentence
menubar.edit.search-web=Search on Web...
menubar.help=_Hilfe menubar.help=_Hilfe
menubar.help.about=_Über… menubar.help.about=_Über…

View File

@ -25,6 +25,8 @@ menubar.file.preferences=_Preferences…
menubar.edit=_Edit menubar.edit=_Edit
menubar.edit.copy.headword=Copy Headword menubar.edit.copy.headword=Copy Headword
menubar.edit.copy.pronunciation=Copy Pronunciation menubar.edit.copy.pronunciation=Copy Pronunciation
menubar.edit.copy.sentence=Copy Sentence
menubar.edit.search-web=Search on Web...
menubar.help=_Help menubar.help=_Help
menubar.help.about=_About… menubar.help.about=_About…

View File

@ -25,6 +25,8 @@ menubar.file.preferences=_設定…
menubar.edit=_編輯 menubar.edit=_編輯
menubar.edit.copy.headword=複製 Wort menubar.edit.copy.headword=複製 Wort
menubar.edit.copy.pronunciation=複製 Aussprache menubar.edit.copy.pronunciation=複製 Aussprache
menubar.edit.copy.sentence=Copy Sentence
menubar.edit.search-web=Search on Web...
menubar.help=_說明 menubar.help=_說明
menubar.help.about=_關於 Willow… menubar.help.about=_關於 Willow…

View File

@ -25,6 +25,8 @@ menubar.file.preferences=_設定…
menubar.edit=_編輯 menubar.edit=_編輯
menubar.edit.copy.headword=複製 Wort menubar.edit.copy.headword=複製 Wort
menubar.edit.copy.pronunciation=複製 Aussprache menubar.edit.copy.pronunciation=複製 Aussprache
menubar.edit.copy.sentence=Copy Sentence
menubar.edit.search-web=Search on Web...
menubar.help=_說明 menubar.help=_說明
menubar.help.about=_關於 Willow… menubar.help.about=_關於 Willow…