Commit 4dd5a74f authored by S Anand's avatar S Anand

ENH: implement $.translate. Fixes #106 @som.sahoo

parent 5ea30332
Pipeline #73430 passed with stage
in 2 minutes and 13 seconds
......@@ -29,6 +29,7 @@ To use all features, add this to your HTML:
- [$.formhandler](docs/formhandler.md) renders a HTML table from a [FormHandler URL](https://learn.gramener.com/guide/formhandler/)
- [$.template](docs/template.md) renders lodash templates. Requires [lodash](https://lodash.com/)
- [$.dropdown](docs/dropdown.md) renders a Bootstrap dropdown. Requires [Bootstrap](https://getbootstrap.com/docs/4.2/)
- [$.translate](docs/translate.md) translates text to other languages using [Gramex translate](https://learn.gramener.com/guide/translate/)
- [g1.sanddance](docs/sanddance.md) moves DOM elements smoothly based on data
- [g1.mapviewer](docs/mapviewer.md) renders leaflet maps and simplifies adding layers from data.
- Note: Mapviewer is not included in [g1.min.js](dist/g1.min.js). Include [mapviewer.min.js](dist/mapviewer.min.js)
......@@ -49,18 +50,19 @@ To use all features, add this to your HTML:
You can import either [g1.min.js](dist/g1.min.js) -- which has all of these functions --
or one of the individual libraries below:
- [urlfilter.min.js](dist/urlfilter.min.js)
- [urlchange.min.js](dist/urlchange.min.js)
- [highlight.min.js](dist/highlight.min.js)
- [search.min.js](dist/search.min.js)
- [ajax.min.js](dist/ajax.min.js)
- [datafilter.min.js](dist/datafilter.min.js)
- [event.min.js](dist/event.min.js)
- [formhandler.min.js](dist/formhandler.min.js)
- [highlight.min.js](dist/highlight.min.js)
- [leaflet.min.js](dist/leaflet.min.js)
- [sanddance.min.js](dist/sanddance.min.js)
- [search.min.js](dist/search.min.js)
- [template.min.js](dist/template.min.js)
- [ajax.min.js](dist/ajax.min.js)
- [leaflet.min.js](dist/leaflet.min.js)
- [event.min.js](dist/event.min.js)
- [datafilter.min.js](dist/datafilter.min.js)
- [translate.min.js](dist/translate.min.js)
- [types.min.js](dist/types.min.js)
- [urlchange.min.js](dist/urlchange.min.js)
- [urlfilter.min.js](dist/urlfilter.min.js)
[mapviewer.min.js](dist/mapviewer.min.js) is not part of [g1.min.js](dist/g1.min.js).
......
# $.translate
`$.translate()` translates text across languages using an API compatible with
[Gramex translate](https://learn.gramener.com/guide/translate/), which is
based on [Google Translate](https://cloud.google.com/translate/docs/reference/rest).
```html
<div lang="en" lang-target="nl">
<h1>Hello <em>world</em></h1>
<p>This is some <em>English</em> text</p>
</div>
<script>
// Translate text nodes under element with lang-target=
$('[lang-target]').translate({
url: './translate', // The Gramex translate URL endpoint
})
</script>
```
`$.translate()` translates child text nodes under the selector. For example:
```html
<body>
<h1>This will be translated</h1>
<h2>This will also be translated</h2>
</body>
<script>
$('body').translate({target: 'nl', url: '...'}) // Translates h1, h2 to Dutch (nl)
$('h1').translate({target: 'de', url: '...'}) // Translates only h1 to German (de)
</script>
```
Each selected element can have only one source and target language. But
different selected elements may have different source and target languages. For
example:
```html
<div lang="en" lang-target="nl">Translate from English to Dutch</div>
<div lang-target="hi">Translate from auto-detected source language to Hindi</div>
<script>
$('div').translate({url: '...'}) // Translates as per the description above
</script>
```
## $.translate attributes
Translated nodes can have these attributes:
- `lang-target=` specifies the target language
- `lang=` specifies the source language. Defaults to auto-detect
- `lang-url` specifies the translation URL endpoint
For example:
```html
<!-- Translates from English to Hindi using ./translate?... as the endpoint -->
<p lang="en" lang-target="hi" lang-url="./translate">...</p>
<script>
$('[lang-target]').translate()
</script>
```
This is the same as
```js
$('p').translate({
source: 'en', // same as lang=
target: 'hi', // same as lang-target=
url: './translate' // same as lang-url=
})
```
## $.translate events
Once each translation is done, it fires a `translate` event on each DOM node
that was translated. The event has these attributes:
- `translate`: an array of objects, each with these keys:
- `q`: the source text
- `t`: the target text
- `source`: the source language
- `target`: the target language
- `node`: the DOM text node that was translated
For example:
```js
$('[lang-to]').translate()
.on('translate', function(e) { // Triggered on each [lang-to] node
// e.target is the translated node
// e.translate[0].q is the first source text
// e.translate[0].t is the first translated text
// etc.
})
.translate()
```
If the translation results in a HTTP error, it fires an `error` event on each
DOM node that resulted in an error. The event has these attributes:
- `xhr`: the XMLHTTPRequest object
- `request`: the query parameters sent as part of the request
export { version } from './src/package.js'
import { translate } from './src/translate.js'
if (typeof jQuery != 'undefined') {
jQuery.extend(jQuery.fn, {
translate: translate
})
}
......@@ -18,3 +18,4 @@ import './index-leaflet.js'
import './index-search.js'
import './index-template.js'
import './index-urlchange.js'
import './index-translate.js'
......@@ -67,6 +67,11 @@ export default [
output: { file: "dist/template.min.js", format: "umd", name: "g1" },
plugins: [uglify()]
},
{
input: "index-translate",
output: { file: "dist/translate.min.js", format: "umd", name: "g1" },
plugins: [uglify()]
},
{
input: "index-event",
output: { file: "dist/event.min.js", format: "umd", name: "g1" },
......
export function translate(options) {
options = options || {}
var self = this
self.each(function () {
var $this = $(this)
var source = $this.attr('lang') || options.source || ''
var target = $this.attr('lang-target') || options.target
if (!target)
throw new Error('$.translate has no target')
if (target == source)
return
var nodes = [], original = [], q = [], node
var walk = document.createTreeWalker(this, NodeFilter.SHOW_TEXT, null, false)
while (node = walk.nextNode()) {
var text = node.textContent
var trimmed = $.trim(text)
if (trimmed) {
nodes.push(node)
original.push(text)
q.push(trimmed)
}
}
var request = { q: q, source: source, target: target }
$.ajax({
url: $this.attr('lang-url') || options.url || 'translate',
data: request,
traditional: true
}).done(function (response) {
response.forEach(function (d, i) {
d.node = nodes[i]
d.node.textContent = original[i].replace(d.q, d.t)
})
$this.trigger({ type: 'translate', translate: response })
}).fail(function (xhr, testStatus, error) {
$this.trigger({ type: 'error', request: request, xhr: xhr })
// eslint-disable-next-line no-console
console.warn('$.translate: error', error)
})
})
return this
}
......@@ -17,10 +17,21 @@ app.route('/formhandler-data').get(function(req, res) {
res.send(g1.datafilter(data, g1.url.parse(url).searchList, 'fhname13'))
})
app.route('/data-list').get(function(req, res) {
app.route('/data-list').get(function (req, res) {
res.send(['bengaluru "', 'hyderabad', 'mumbai', 'delhi'])
})
app.route('/translate').get(function (req, res) {
var args = g1.url.parse(req.url).searchList
var source = args.source ? args.source[0] : ''
var target = args.target ? args.target[0] : ''
res.setHeader('Content-Type', 'application/json')
res.send(JSON.stringify(args.q.map(function (q) {
var t = q.replace(/^\s+/, '').replace(/\s$/, '')
return { q: q, t: source + '-' + target + '-' + t, source: source, target: target }
})))
})
async function run_puppeteer() {
const puppeteer = require('puppeteer')
const browser = await puppeteer.launch({
......
<!DOCTYPE html>
<html>
<head>
<title>translate tests</title>
<script src="../node_modules/jquery/dist/jquery.min.js"></script>
<script src="../node_modules/lodash/lodash.min.js"></script>
<script src="../dist/translate.min.js"></script>
<script src="tape.js"></script>
<script src="tape-stream.js"></script>
</head>
<body>
<script>
function text(selector) {
var walk = document.createTreeWalker($(selector).get(0), NodeFilter.SHOW_TEXT, null, false)
var node, result = []
while (node = walk.nextNode()) {
if ($.trim(node.textContent))
result.push(node.textContent)
}
return result
}
</script>
<section class="test1">
<div lang="en" lang-target="nl">
<h1>Hello <em>world</em></h1>
<p>Some <em>English</em> text</p>
</div>
<script>
tape('$.translate translates all child nodes under selector based on lang and lang-target', function(t) {
$('.test1 [lang-target]').on('translate', function () {
t.deepEqual(text('.test1 [lang-target]'), [
'en-nl-Hello ',
'en-nl-world',
'en-nl-Some ',
'en-nl-English',
' en-nl-text'
])
t.end()
}).translate({
url: '../translate'
})
})
</script>
</section>
<section class="test2">
<div>
<h1> x </h1>
<h2> y </h2>
</div>
<script>
tape('$.translate applies only to selected node', function(t) {
$('.test2 div').on('translate', function (e) {
t.deepEqual(text('.test2 div'), [' -nl-x ', ' -nl-y '])
// Ensure that events have all attributes
t.deepEqual(_.map(e.translate, 'q'), ['x', 'y'])
t.deepEqual(_.map(e.translate, 't'), ['-nl-x', '-nl-y'])
t.deepEqual(_.map(e.translate, 'source'), ['', ''])
t.deepEqual(_.map(e.translate, 'target'), ['nl', 'nl'])
t.deepEqual(_.map(e.translate, function(v) { return v.node.textContent }), [' -nl-x ', ' -nl-y '])
t.end()
}).translate({ target: 'nl', url: '../translate' })
})
</script>
</section>
<section class="test3">
<div>
<h1>This will be translated</h1>
<h2>This will not be translated</h2>
</div>
<script>
tape('$.translate applies only to selected node', function (t) {
$('.test3 h1').on('translate', function () {
t.deepEqual(text('.test3 div'), [
'-de-This will be translated',
'This will not be translated'
])
t.end()
}).translate({ target: 'de', url: '../translate' })
})
</script>
</section>
<section class="test4">
<div class="a" lang="en" lang-target="nl">English to Dutch</div>
<div class="b" lang-target="hi">Auto to Hindi</div>
<script>
tape('$.translate allows different selected elements to have different targets', function (t) {
t.plan(2)
$('.test4 div').on('translate', function (e) {
if ($(e.target).is('.a'))
t.deepEqual(text('.test4 .a'), ['en-nl-English to Dutch'])
else if ($(e.target).is('.b'))
t.deepEqual(text('.test4 .b'), ['-hi-Auto to Hindi'])
}).translate({ url: '../translate' })
})
</script>
</section>
<section class="test5">
<p lang="en" lang-target="hi" lang-url="../translate">...</p>
<script>
tape('$.translate attributes lang, lang-target, lang-url work as data attributes', function (t) {
$('.test5 p').on('translate', function () {
t.deepEqual(text('.test5 p'), ['en-hi-...'])
t.end()
}).translate()
})
</script>
</section>
<section class="test6">
<p lang="en" lang-target="hi" lang-url="nonexistent-url">...</p>
<script>
tape('$.translate fires error event', function (t) {
$('.test6 p').on('error', function (e) {
t.equal(e.xhr.status, 404)
t.deepEqual(e.request, {q: ['...'], source: 'en', target: 'hi'})
t.end()
}).translate()
})
</script>
</section>
</body>
</html>
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment