-
Notifications
You must be signed in to change notification settings - Fork 26
5.2. Tutorial 2 Country flags
In the second tutorial of this series, we are going to develop a new IA that will display the flag of a given country with the keyword "flag".
Example:
flag France
We're going to start off by generating our new IA:
$ new
IA-Core v3.1.1 - IA Generator
-----------HELP SECTION-----------
Name: Name of your Instant Answer
Description: Description of your Instant Answer
Keyword: WHAT keyword should trigger the IA ? ex: "weather Paris", weather is the keyword
Trigger: WHERE should the keyword be expected in a query ? ex: "weather Paris" = start
Modifiers: Regex modifier(s), see https://www.w3schools.com/jsref/jsref_obj_regexp.asp
Javascript script: Front script, used to interact with how your IA displays, can be modified later
Timeout: Time before your response is considered as canceled (in milliseconds)
Cache: Duration of the cached data (in seconds)
Order: Order in the IA hierarchy (0 = first, 1 = second, ... no order = added at the end, alphabetically)
? Name Country flags
? Description Displays the flag of a given country for the query "flag [country]"
? Keyword flag
? Trigger start
? Modifiers
? Javascript script No
? Timeout (in milliseconds) 3600
? Cache duration (in seconds) 10800
? Order in the hierarchy (integer or hit enter for no order)
Generating "Country flags" IA...
src/modules/country_flags created
src/modules/country_flags/country_flags.js created
src/modules/country_flags/public created
src/modules/country_flags/public/country_flags.dot created
src/modules/country_flags/public/css created
src/modules/country_flags/public/css/country_flags.scss created
src/modules/country_flags/lang_src created.
src/modules/country_flags/lang_src/fr.po created.
src/modules/country_flags/lang_src/de.po created.
src/modules/country_flags/lang_src/en.po created.
src/modules/country_flags/lang_src/co.po created.
src/modules/country_flags/lang_src/br.po created.
src/modules/country_flags/lang_src/eu.po created.
src/modules/country_flags/lang_src/ca.po created.
src/modules/country_flags/lang_src/es.po created.
src/modules/country_flags/lang_src/it.po created.
src/modules/country_flags/lang_src/nl.po created.
src/modules/country_flags/lang_src/ru.po created.
src/modules/country_flags/lang_src/pl.po created.
src/modules/country_flags/lang_src/pt.po created.
IA "Country flags" generated.
As you can see, we have already decided of a few things:
- Our IA will be named Country flags and its folder will be country_flags
- The keyword that will call our IA is flag for now, even though we expect every translation of flag to work as well. We'll see how to do that later.
- The trigger has been set to start because we expect the keyword to be at the start of our query ("flag [country]")
- We don't need a front javascript script because we don't need to interact with how things are displayed or work for that IA
Now that created the IA, we can try querying the server. You can hit the server directly from your browser. Queries should be passed through the ?q= argument, and optionals ?locale= parameters can be passed as well:
http://localhost:3002/?q=flag France
This will return a json object containing your answer or an error. Right now this query should timeout, as our app is not processing it yet.
{
"error": 200,
"status": "error",
"message": "operation timed out"
}
To display the flag of a country... we need a set of every flags!
There are several free data sets containing world flags available online. For this tutorial, we will use flagpedia's dataset.
Let's extract this archive in the public folder of our IA, under the img/flags folder (if it doesn't exist, create it):
$ cd app/src/modules/country_flags/public/img/flags/
$ tree
.
|-- ad.png
|-- ae.png
|-- af.png
|-- ag.png
|-- al.png
|-- am.png
|-- ao.png
|-- ar.png
|-- at.png
|-- au.png
|-- az.png
|-- ba.png
.....
0 directories, 196 files
As you can see, the flags are named according to the ISO 3166-1 standard, which uses two-letters country codes.
We will need to bind these files to their actual countries, so we need to know which code belongs to which country, to map the queries accordingly and display the right flag. We will use Open Knowledge International's country list dataset.
We are going to rename this file "countries.json" and place it in our module's root directory (app/src/modules/country_flags/):
$ cat countries.json
[{"Name":"Afghanistan","Code":"AF"},{"Name":"Åland Islands","Code":"AX"},{"Name":"Albania","Code":"AL"},{"Name":"Algeria","Code":"DZ"},{"Name":"American Samoa","Code":"AS"},{"Name":"Andorra","Code":"AD"},{"Name":"Angola","Code":"AO"},{"Name":"Anguilla","Code":"AI"},{"Name":"Antarctica","Code":"AQ"},{"Name":"Antigua and Barbuda"[...]
Our IA has been generated and every file we will need has been created. Now that we have our data, and our images, we need to open our project in our preferred IDE so we can start coding.
The first file we are going to edit is the main file of our module. It should contain everything to already run properly, but the result is not yet calculated. We need to process the query and send the right flag as a result.
The getData() function gets the query as an array of values. This is where we'll determine which flag to use:
getData: function (values, proxyURL, language, i18n) {
const _ = i18n._;
return new Promise(function (resolve, reject) {
// do something with the data (values)
});
},
We're going to start by checking that the query is complete and that we do have a country to search a flag for:
getData: function (values, proxyURL, language, i18n) {
const _ = i18n._;
return new Promise(function (resolve, reject) {
if (values[2]) {
// for "flag Sierra Leone", values[2] == "Sierra Leone"
} else {
reject("Country not found.")
}
});
},
Then, we are going to make a function that will look for the 2-letter code of a given country by parsing the json file we downloaded earlier. Inside your country_flags.js file, outside of the module.exports scope, create this function:
var path = require('path');
var countriesFile = path.join(__dirname, "countries.json");
var countries = require(countriesFile);
function getCountry (country) {
for (var key in countries) {
if (countries[key].Name.toLowerCase() === country.toLowerCase()) {
return {name: countries[key].Name, code: countries[key].Code.toLowerCase()};
}
}
return null;
}
Now that we are getting the 2-letter country code, we can send that data to the front.
getData: function (values, proxyURL, language, i18n) {
const _ = i18n._;
return new Promise(function (resolve, reject) {
if (values[2]) {
// We're calling the getCountry function to get the data: country.name and country.code
var country = getCountry(values[2]);
if (country) {
// We send the data so the front can take over and display it
resolve({
country_name: country.name,
country_code: country.code
});
} else {
reject("Flag not found.")
}
} else {
reject("Country not found.")
}
});
},
Now that we have our data, we have to display it. We need to edit the template file and write some HTML:
<link rel="stylesheet" href="css/country_flags.css" type="text/css">
<div class="ia__country_flags">
<div class="ia__country_flags__container">
<span class="ia__country_flags__result">
<img src="{{= it.images_path }}/country_flags/flags/{{= it.data.country_code }}.png" alt="{{= it.data.country_name }}" /> {{= it.data.country_name }}
</span>
</div>
</div>
As you can see, we have created some HTML elements that will dictate, alongside the .css file, how the IA looks like.
This file being a template, you can see there are some variables in it that are not yet defined:
- it.images_path
- it.data.country_code
- it.data.country_name
Those are going to be served to the front, and thus to the compiled template, through the AI API. You can already guess that it.data corresponds to what the getData() function returns.
Styling your IA is important, because it has to look good for every user, no matter what their browser or device might be.
We have written Styling Guidelines that should help you achieve that.
For the Country flags IA, the styling is pretty straight forward and shouldn't require much explanation:
.ia__country_flags {
padding: 21px 25px 0;
background: white;
border-top: 1px solid rgba(122,124,126,0.16);
}
.ia__country_flags__container {
display: inline-block;
width: -webkit-calc(100% - 110px);
width: -moz-calc(100% - 110px);
width: calc(100% - 110px);
margin: 14px 0;
vertical-align: middle;
word-wrap: break-word;
}
.ia__country_flags__result {
font-size: 14px;
font-family: 'Segoe UI', 'Helvetica', 'Droid Sans', Arial, sans-serif;
}
Qwant is a plurilangual website, which means that our IA could be displayed to someone who doesn't speak English. That could be problematic for some IAs, so we are going to see how to make that work!
Our system relies on .po files, called catalogs, which can be edited with Poedit, a gettext translation tool. You can also edit them manually by opening them in an editor, but we don't recommend that.
Those .po files are going to be compiled and our app will be able to display the right language, depending on the User's settings, by fetching the right key with the gettext (_()) function.
For example, in our main JS file we can see that the name of our IA is already calling for a translation. If it doesn't find any matching translation, it will print the actual string, without translation.
getName: function (i18n) {
const _ = i18n._;
return _("Country flags", "country_flags");
},
Here, our app is going to try to find the string "Country flags" for the context "country_flags". We use a context to differentiate same strings that shouldn't have the same translation in some languages.
To have that name translated, we need to add a translation key with Poedit. This tutorial won't cover how Poedit works, but here's how the fr.po file should look like in the end:
msgctxt "country_flags"
msgid "Country flags"
msgstr "Drapeaux nationaux"
Every translation should look like this, with the context, the string to translate and the translated string.
In order to allow non-English speaking users to use the country flags IA, we need to translate the keyword as well. For example, here is our french catalog:
msgctxt "country_flags"
msgid "flag"
msgstr "drapeau"
French users will be able to query a flag by using the French word: "drapeau".
In our main app file, we need to tell the app to look not only for "flag", but also for every key that matches "flag" in our catalogs:
getKeyword: function (i18n) {
const _ = i18n._;
return _("flag", "country_flags");
},
This function should already exist in your generated code. Right now, it is overwritten by the "keyword" option. We are going to comment that option so that the getKeyword function takes over:
//keyword: "flag",
Now, we can open our browser to test if our IA works:
{
"runtime": "nodejs",
"template_name": "country_flags",
"display_name": "Country flags",
"images_path": "ia/img/",
"data": {
"country_name": "France",
"country_code": "fr"
},
"query": "flag France",
"status": "success",
"cacheExpirationTime": 10800,
"files": [
{
"url": "ia/template/en_gb/country_flags.js?0bd2d5524c0203f1338dadeb62e1922820893bfe",
"type": "template"
}
]
}
And to test if the localization works...:
{
"runtime": "nodejs",
"template_name": "country_flags",
"display_name": "Country flags",
"images_path": "ia/img/",
"data": {
"country_name": "France",
"country_code": "fr"
},
"query": "drapeau France",
"status": "success",
"cacheExpirationTime": 10800,
"files": [
{
"url": "ia/template/fr_fr/country_flags.js?0bd2d5524c0203f1338dadeb62e1922820893bfe",
"type": "template"
}
]
}
If we try "drapeau France" without specifying a lang, the query won't return anything as the default language is English:
{
"error": 500,
"status": "error",
"message": "No module match the query: drapeau France"
}
The localization works! Our IA works!
Now that we made sure that our data is correct, we're going to test how our IA looks like. For that purpose, simply navigate to http://localhost:3030?q=flag France
in your browser:
Make sure that:
- Your IA is triggered correctly,
- Your IA blends in correctly with Qwant's layout
- Your IA actually gives the right result and works the way you intended (type "flag Canada"!)