Skip to content

Commit

Permalink
Remember translation target language
Browse files Browse the repository at this point in the history
Also fixes #1270, and it's now possible to search the languages.

Fixes #1159
  • Loading branch information
johnfactotum committed Sep 25, 2024
1 parent 4fbeecb commit 02c5727
Show file tree
Hide file tree
Showing 11 changed files with 491 additions and 270 deletions.
3 changes: 3 additions & 0 deletions data/com.github.johnfactotum.Foliate.gschema.xml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@
<key name="highlight-color" type="s">
<default>'yellow'</default>
</key>
<key name="translate-target-language" type="s">
<default>''</default>
</key>
</schema>

<schema id="com.github.johnfactotum.Foliate.viewer.view"
Expand Down
Binary file modified data/gschemas.compiled
Binary file not shown.
File renamed without changes.
6 changes: 5 additions & 1 deletion src/gresource.xml
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,11 @@
<file>foliate-js/vendor/pdfjs/pdf.worker.js</file>
<file>opds/main.js</file>
<file>opds/main.html</file>
<file>opds/widgets.js</file>
<file>selection-tools/common.css</file>
<file>selection-tools/translate.html</file>
<file>selection-tools/wikipedia.html</file>
<file>selection-tools/wiktionary.html</file>
<file>common/widgets.js</file>
<file>reader/reader.html</file>
<file>reader/reader.js</file>
<file>reader/markup.js</file>
Expand Down
2 changes: 1 addition & 1 deletion src/opds/main.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import './widgets.js'
import '../common/widgets.js'
import { REL, SYMBOL, isOPDSCatalog, getPublication, getFeed, getSearch, getOpenSearch } from '../foliate-js/opds.js'

const emit = x => globalThis.webkit.messageHandlers.opds
Expand Down
316 changes: 49 additions & 267 deletions src/selection-tools.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,284 +25,60 @@ const getGoogleTranslateLanguages = utils.memoize(() => {
const displayName = new Intl.DisplayNames(locales, { type: 'language' })
const langs = ['af', 'sq', 'am', 'ar', 'hy', 'as', 'ay', 'az', 'bm', 'eu', 'be', 'bn', 'bho', 'bs', 'bg', 'ca', 'ceb', 'zh-CN', 'zh-TW', 'co', 'hr', 'cs', 'da', 'dv', 'doi', 'nl', 'en', 'eo', 'et', 'ee', 'fil', 'fi', 'fr', 'fy', 'gl', 'ka', 'de', 'el', 'gn', 'gu', 'ht', 'ha', 'haw', 'he', 'hi', 'hmn', 'hu', 'is', 'ig', 'ilo', 'id', 'ga', 'it', 'ja', 'jv', 'kn', 'kk', 'km', 'rw', 'gom', 'ko', 'kri', 'ku', 'ckb', 'ky', 'lo', 'la', 'lv', 'ln', 'lt', 'lg', 'lb', 'mk', 'mai', 'mg', 'ms', 'ml', 'mt', 'mi', 'mr', 'mni-Mtei', 'lus', 'mn', 'my', 'ne', 'no', 'ny', 'or', 'om', 'ps', 'fa', 'pl', 'pt', 'pa', 'qu', 'ro', 'ru', 'sm', 'sa', 'gd', 'nso', 'sr', 'st', 'sn', 'sd', 'si', 'sk', 'sl', 'so', 'es', 'su', 'sw', 'sv', 'tl', 'tg', 'ta', 'tt', 'te', 'th', 'ti', 'ts', 'tr', 'tk', 'ak', 'uk', 'ur', 'ug', 'uz', 'vi', 'cy', 'xh', 'yi', 'yo', 'zu']
const defaultLang = matchLocales(langs)[0] ?? 'en'
return JSON.stringify([langs.map(lang => [lang, displayName.of(lang)]), defaultLang])
return [langs.map(lang => [lang, displayName.of(lang)]), defaultLang]
})

const commonStyle = `
html, body {
color-scheme: light dark;
font: menu;
}
h1 {
font-size: larger;
}
h2 {
font-size: smaller;
}
a:any-link {
color: highlight;
}
ul, ol {
padding-inline-start: 2em;
}
footer {
font-size: smaller;
opacity: .6;
}
:is([data-state="loading"], [data-state="error"]) footer {
display: none;
}
[data-state="loaded"] footer {
display: block;
}
[data-state="error"] main {
display: flex;
position: absolute;
inset: 0;
width: 100%;
height: 100%;
text-align: center;
justify-content: center;
align-items: center;
}
`

const tools = {
'dictionary': {
label: _('Dictionary'),
run: ({ text, lang }) => {
const language = getLanguage(lang)
return `
<base href="https://en.wiktionary.org/wiki/Wiktionary:Main_Page">
<style>
${commonStyle}
ul {
margin: .5em 0;
font-style: italic;
opacity: .75;
font-size: smaller;
list-style: none;
}
h1 {
padding-inline-end: 1em;
display: inline;
}
hgroup p {
font-size: smaller;
display: inline;
}
</style>
<main></main>
<footer><p>${_('From <a id="link">Wiktionary</a>, released under the <a href="https://creativecommons.org/licenses/by-sa/4.0/">CC BY-SA License</a>.')}</footer>
<script>
const main = document.querySelector('main')
const footer = document.querySelector('footer')
const wiktionary = (word, language, languageName) => {
document.body.dataset.state = 'loading'
return fetch('https://en.wiktionary.org/api/rest_v1/page/definition/' + encodeURI(word))
.then(res => res.ok ? res.json() : Promise.reject(new Error()))
.then(json => {
const results = language ? json[language]
: languageName ? Object.values(json)
.find(x => x.some(x => x.language === languageName))
: json['en']
const hgroup = document.createElement('hgroup')
const h1 = document.createElement('h1')
h1.innerText = word
const p = document.createElement('p')
p.innerText = results[0].language
hgroup.append(h1, p)
main.append(hgroup)
for (const { partOfSpeech, definitions } of results) {
const h2 = document.createElement('h2')
h2.innerText = partOfSpeech
const ol = document.createElement('ol')
main.append(h2, ol)
for (const { definition, examples } of definitions) {
const li = document.createElement('li')
li.innerHTML = definition
ol.append(li)
const ul = document.createElement('ul')
li.append(ul)
if (examples) for (const example of examples) {
const li = document.createElement('li')
li.innerHTML = example
ul.append(li)
}
}
}
document.querySelector('#link').href = '/wiki/' + word
document.body.dataset.state = 'loaded'
})
.catch(e => {
console.error(e)
const lower = word.toLocaleLowerCase(language)
if (lower !== word) return wiktionary(lower, language)
else {
const div = document.createElement('div')
const h1 = document.createElement('h1')
h1.innerText = decodeURIComponent("${encodeURIComponent(_('No Definitions Found'))}")
const p = document.createElement('p')
p.innerHTML = \`<a href="https://en.wiktionary.org/w/index.php?search=${encodeURIComponent(text)}">${_('Search on Wiktionary')}</a>\`
div.append(h1, p)
main.append(div)
document.body.dataset.state = 'error'
}
})
}
// see https://en.wiktionary.org/wiki/Wiktionary:Namespace
const wikiNamespaces = [
'Media', 'Special', 'Talk', 'User', 'Wiktionary', 'File', 'MediaWiki',
'Template', 'Help', 'Category',
'Summary', 'Appendix', 'Concordance', 'Index', 'Rhymes', 'Transwiki',
'Thesaurus', 'Citations', 'Sign',
]
main.addEventListener('click', e => {
const { target } = e
if (target.tagName === 'A') {
const href = target.getAttribute('href')
if (href.startsWith('/wiki/')) {
const [word, languageName] = href.replace('/wiki/', '').split('#')
if (wikiNamespaces.every(namespace => !word.startsWith(namespace + ':')
&& !word.startsWith(namespace + '_talk:'))) {
e.preventDefault()
main.replaceChildren()
wiktionary(word.replaceAll('_', ' '), null, languageName)
}
}
}
})
wiktionary(decodeURIComponent("${encodeURIComponent(text)}"), "${language}")
</script>`
},
uri: 'foliate-selection-tool:///selection-tools/wiktionary.html',
run: (__, { text, lang }) => ({
msg: {
footer: _('From <a id="link">Wiktionary</a>, released under the <a href="https://creativecommons.org/licenses/by-sa/4.0/">CC BY-SA License</a>.'),
error: _('No Definitions Found'),
errorAction: _('Search on Wiktionary'),
},
text,
lang: getLanguage(lang),
}),
},
'wikipedia': {
label: _('Wikipedia'),
run: ({ text, lang }) => {
const language = getLanguage(lang)
return `<style>
${commonStyle}
hgroup {
color: #fff;
background-position: center center;
background-size: cover;
background-color: rgba(0, 0, 0, .4);
background-blend-mode: darken;
border-radius: 6px;
padding: 12px;
margin: -8px;
margin-bottom: 0;
min-height: 100px;
}
</style>
<main></main>
<footer><p>${_('From <a id="link">Wikipedia</a>, released under the <a href="https://en.wikipedia.org/wiki/Wikipedia:Text_of_the_Creative_Commons_Attribution-ShareAlike_4.0_International_License">CC BY-SA License</a>.')}</footer>
<script>
const main = document.querySelector('main')
document.body.dataset.state = 'loading'
const word = decodeURIComponent("${encodeURIComponent(text)}")
fetch('https://${language}.wikipedia.org/api/rest_v1/page/summary/' + word)
.then(res => res.ok ? res.json() : Promise.reject(new Error()))
.then(json => {
const hgroup = document.createElement('hgroup')
const h1 = document.createElement('h1')
h1.innerHTML = json.titles.display
hgroup.append(h1)
if (json.description) {
const p = document.createElement('p')
p.innerText = json.description
hgroup.append(p)
}
if (json.thumbnail)
hgroup.style.backgroundImage = 'url("' + json.thumbnail.source + '")'
const div = document.createElement('div')
div.innerHTML = json.extract_html
main.append(hgroup, div)
main.dir = json.dir
document.querySelector('#link').href = json.content_urls.desktop.page
document.body.dataset.state = 'loaded'
})
.catch(e => {
console.error(e)
const div = document.createElement('div')
const h1 = document.createElement('h1')
h1.innerText = decodeURIComponent("${encodeURIComponent(_('No Definitions Found'))}")
const p = document.createElement('p')
p.innerHTML = \`<a href="https://${language}.wikipedia.org/w/index.php?search=${encodeURIComponent(text)}">${_('Search on Wikipedia')}</a>\`
div.append(h1, p)
main.append(div)
document.body.dataset.state = 'error'
})
</script>`
},
uri: 'foliate-selection-tool:///selection-tools/wikipedia.html',
run: (__, { text, lang }) => ({
msg: {
footer: _('From <a id="link">Wikipedia</a>, released under the <a href="https://en.wikipedia.org/wiki/Wikipedia:Text_of_the_Creative_Commons_Attribution-ShareAlike_4.0_International_License">CC BY-SA License</a>.'),
error: _('No Definitions Found'),
errorAction: _('Search on Wikipedia'),
},
text,
lang: getLanguage(lang),
}),
},
'translate': {
label: _('Translate'),
run: ({ text }) => `<style>
${commonStyle}
html, body {
margin: 0;
height: 100%;
width: 100%;
}
body {
display: flex;
flex-direction: column;
}
select {
width: 100%;
}
main {
flex: 1;
overflow-y: auto;
margin: 8px 0;
}
#output {
padding: 0 8px;
}
</style>
<header></header>
<main><div id="output"></div></main>
<footer>${_('Translation by Google Translate')}</footer>
<script>
const googleTranslate = (text, lang = defaultLang) => {
fetch('https://translate.googleapis.com/translate_a/single?client=gtx'
+ '&ie=UTF-8&oe=UTF-&sl=auto&tl=' + lang
+ '&dt=t&q=' + text)
.then(res => res.ok ? res.json() : Promise.reject(new Error()))
.then(json => json[0].map(x => x[0]).join(''))
.then(result => document.querySelector('#output').innerText = result)
.catch(e => {
console.error(e)
document.querySelector('#output').innerText = decodeURIComponent("${encodeURIComponent(_('Cannot retrieve translation'))}")
})
}
const text = "${encodeURIComponent(text)}"
const select = document.createElement('select')
const [langs, defaultLang] = JSON.parse(decodeURI("${encodeURI(getGoogleTranslateLanguages())}"))
for (const [lang, label] of langs) {
const option = document.createElement('option')
option.value = lang
option.innerText = label
if (lang === defaultLang) option.selected = true
select.append(option)
}
document.querySelector('header').append(select)
select.onchange = () => googleTranslate(text, select.value)
googleTranslate(text)
</script>
`,
uri: 'foliate-selection-tool:///selection-tools/translate.html',
run: (popover, { text }) => {
const [langs, defaultLang] = getGoogleTranslateLanguages()
return {
msg: {
footer: _('Translation by Google Translate'),
error: _('Cannot retrieve translation'),
search: _('Search…'),
langs,
},
text,
lang: popover.translate_target_language || defaultLang,
}
},
},
}

const SelectionToolPopover = GObject.registerClass({
GTypeName: 'FoliateSelectionToolPopover',
Properties: utils.makeParams({
'translate-target-language': 'string',
}),
}, class extends Gtk.Popover {
#webView = utils.connect(new WebView({
settings: new WebKit.Settings({
Expand All @@ -318,7 +94,7 @@ const SelectionToolPopover = GObject.registerClass({
case WebKit.PolicyDecisionType.NAVIGATION_ACTION:
case WebKit.PolicyDecisionType.NEW_WINDOW_ACTION: {
const { uri } = decision.navigation_action.get_request()
if (!uri.startsWith('foliate:')) {
if (!uri.startsWith('foliate-selection-tool:')) {
decision.ignore()
new Gtk.UriLauncher({ uri }).launch(this.root, null, null)
return true
Expand All @@ -329,16 +105,22 @@ const SelectionToolPopover = GObject.registerClass({
})
constructor(params) {
super(params)
utils.bindSettings('viewer', this, ['translate-target-language'])
Object.assign(this, {
width_request: 300,
height_request: 300,
})
this.child = this.#webView
this.#webView.set_background_color(new Gdk.RGBA())
this.#webView.registerHandler('settings', payload => {
if (payload.key === 'translate-target-language')
this.translate_target_language = payload.value
})
}
load(html) {
this.#webView.loadHTML(html, 'foliate:selection-tool')
loadTool(tool, init) {
this.#webView.loadURI(tool.uri)
.then(() => this.#webView.opacity = 1)
.then(() => this.#webView.exec('init', init))
.catch(e => console.error(e))
}
})
Expand Down Expand Up @@ -366,8 +148,8 @@ export const SelectionPopover = GObject.registerClass({
const action = new Gio.SimpleAction({ name })
action.connect('activate', () => {
const popover = getSelectionToolPopover()
Promise.resolve(tool.run(this.emit('run-tool')))
.then(x => popover.load(x))
Promise.resolve(tool.run(popover, this.emit('run-tool')))
.then(x => popover.loadTool(tool, x))
.catch(e => console.error(e))
this.emit('show-popover', popover)
})
Expand Down
Loading

0 comments on commit 02c5727

Please sign in to comment.