├── README.md
├── style.css
├── manifest.json
├── .glitch-assets
├── sw.js
├── index.html
└── script.js
/README.md:
--------------------------------------------------------------------------------
1 | Road Trip
2 | =========
3 |
4 | An app that reads to you about the places you visit.
5 |
6 | Live instance https://roadtrip.glitch.me/
--------------------------------------------------------------------------------
/style.css:
--------------------------------------------------------------------------------
1 | /* CSS files add styling rules to your content */
2 |
3 | .page-content {
4 | padding: 16px;
5 | }
6 |
7 | #map {
8 | height: 300px
9 | }
--------------------------------------------------------------------------------
/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Road Trip",
3 | "short_name": "Road Trip",
4 | "start_url": "https://roadtrip.glitch.me/",
5 | "icons": [
6 | {
7 | "src": "https://cdn.glitch.com/93aac3ad-5e37-4ee6-8bf0-133d676ba436%2Flogo.png?1530855990742",
8 | "sizes": "512x512",
9 | "type": "image/png"
10 | }
11 | ],
12 | "theme_color": "#3f51b5",
13 | "background_color": "#ffffff",
14 | "display": "standalone"
15 | }
--------------------------------------------------------------------------------
/.glitch-assets:
--------------------------------------------------------------------------------
1 | {"name":"drag-in-files.svg","date":"2016-10-22T16:17:49.954Z","url":"https://cdn.hyperdev.com/drag-in-files.svg","type":"image/svg","size":7646,"imageWidth":276,"imageHeight":276,"thumbnail":"https://cdn.hyperdev.com/drag-in-files.svg","thumbnailWidth":276,"thumbnailHeight":276,"dominantColor":"rgb(102, 153, 205)","uuid":"adSBq97hhhpFNUna"}
2 | {"name":"click-me.svg","date":"2016-10-23T16:17:49.954Z","url":"https://cdn.hyperdev.com/click-me.svg","type":"image/svg","size":7116,"imageWidth":276,"imageHeight":276,"thumbnail":"https://cdn.hyperdev.com/click-me.svg","thumbnailWidth":276,"thumbnailHeight":276,"dominantColor":"rgb(243, 185, 186)","uuid":"adSBq97hhhpFNUnb"}
3 | {"name":"paste-me.svg","date":"2016-10-24T16:17:49.954Z","url":"https://cdn.hyperdev.com/paste-me.svg","type":"image/svg","size":7242,"imageWidth":276,"imageHeight":276,"thumbnail":"https://cdn.hyperdev.com/paste-me.svg","thumbnailWidth":276,"thumbnailHeight":276,"dominantColor":"rgb(42, 179, 185)","uuid":"adSBq97hhhpFNUnc"}
4 | {"uuid":"adSBq97hhhpFNUna","deleted":true}
5 | {"uuid":"adSBq97hhhpFNUnb","deleted":true}
6 | {"uuid":"adSBq97hhhpFNUnc","deleted":true}
7 | {"name":"logo.png","date":"2018-07-06T05:46:30.742Z","url":"https://cdn.glitch.com/93aac3ad-5e37-4ee6-8bf0-133d676ba436%2Flogo.png","type":"image/png","size":442308,"imageWidth":512,"imageHeight":512,"thumbnail":"https://cdn.glitch.com/93aac3ad-5e37-4ee6-8bf0-133d676ba436%2Fthumbnails%2Flogo.png","thumbnailWidth":330,"thumbnailHeight":330,"dominantColor":"rgb(206,173,154)","uuid":"JGjGnsHF4PAgHyKO"}
8 | {"name":"Wikipedia-logo.svg.png","date":"2018-07-14T21:15:43.752Z","url":"https://cdn.glitch.com/e2f643e1-ad10-4089-93d8-fc9e55b77d3b%2FWikipedia-logo.svg.png","type":"image/png","size":85149,"imageWidth":600,"imageHeight":600,"thumbnail":"https://cdn.glitch.com/e2f643e1-ad10-4089-93d8-fc9e55b77d3b%2Fthumbnails%2FWikipedia-logo.svg.png","thumbnailWidth":330,"thumbnailHeight":330,"dominantColor":null,"uuid":"V5Q8vIkJNV2V8N15"}
9 |
--------------------------------------------------------------------------------
/sw.js:
--------------------------------------------------------------------------------
1 | importScripts('https://storage.googleapis.com/workbox-cdn/releases/3.0.0-beta.0/workbox-sw.js');
2 |
3 | // Uncomment for debugging
4 | // workbox.core.setLogLevel(workbox.core.LOG_LEVELS.debug);
5 |
6 | workbox.routing.registerRoute(
7 | /^https:\/\/((\w+)\.googleapis\.com|www\.googletagmanager\.com|code\.getmdl\.io)\/.+$/,
8 | workbox.strategies.staleWhileRevalidate({
9 | cacheName: 'google-cache',
10 | plugins: [
11 | // Allow opaque responses
12 | new workbox.cacheableResponse.Plugin({
13 | statuses: [0, 200] // YOLO
14 | }),
15 | ],
16 | })
17 | );
18 |
19 | workbox.routing.registerRoute(
20 | // Cache immutable files forever
21 | /^https:\/\/roadtrip-api\./,
22 | // Use the cache if it's available
23 | workbox.strategies.cacheFirst({
24 | cacheName: 'api-cache',
25 | plugins: [
26 | new workbox.expiration.Plugin({
27 | // Cache for a maximum of 30 days
28 | maxAgeSeconds: 30 * 24 * 60 * 60,
29 | })
30 | ],
31 | })
32 | );
33 |
34 | workbox.routing.registerRoute(
35 | // Cache immutable files forever
36 | /^https:\/\/[^.]+\.wikipedia\.org\//,
37 | // Use the cache if it's available
38 | workbox.strategies.cacheFirst({
39 | cacheName: 'wiki-cache',
40 | plugins: [
41 | new workbox.expiration.Plugin({
42 | // Cache for a maximum of 2 days
43 | maxAgeSeconds: 2 * 24 * 60 * 60,
44 | })
45 | ],
46 | })
47 | );
48 |
49 | workbox.routing.registerRoute(
50 | // Prefer the network but if it doesn't respond within 1 seconds,
51 | // fallback to a doc if we have a cached version that is max
52 | // 10 days old.
53 | // Basically that means that the schedule should load offline for the
54 | // duration of the conference
55 | /(\/|\.html|\.js|\.css)$/,
56 | // Use the network unless things are slow
57 | workbox.strategies.networkFirst({
58 | cacheName: 'doc-cache',
59 | networkTimeoutSeconds: 1,
60 | plugins: [
61 | new workbox.expiration.Plugin({
62 | // Cache for a maximum of 10 days
63 | maxAgeSeconds: 10 * 24 * 60 * 60,
64 | })
65 | ],
66 | })
67 | );
68 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Road Trip
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
16 |
17 |
18 |
19 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
91 |
92 |
93 |
--------------------------------------------------------------------------------
/script.js:
--------------------------------------------------------------------------------
1 | let position;
2 | let manualPosition = false;
3 | let map;
4 | let locationData;
5 | let cancelSpeaking;
6 | let skipThisParagraph;
7 | const state = {
8 | playing: false,
9 | loading: false,
10 | paused: false,
11 | };
12 | const pollSeconds = 30;
13 |
14 | const seen = {};
15 |
16 | const interestingTypes = [
17 | 'point_of_interest',
18 | 'natural_feature',
19 | 'neighborhood',
20 | 'sublocality',
21 | 'locality',
22 | ];
23 |
24 | const languageMap = {
25 | 'English (US)': {wikiTag: 'en', speechTag: 'en-US', welcomeMsg: 'Welcome to your road trip.'},
26 | 'English (UK)': {wikiTag: 'en', speechTag: 'en-GB', welcomeMsg: 'Welcome to your road trip.'},
27 | 'Français': {wikiTag: 'fr', speechTag: 'fr-FR', welcomeMsg: 'Bienvenue dans votre voyage.'},
28 | 'Cebuano': {wikiTag: 'ceb', speechTag: 'ceb', welcomeMsg: 'Welcome sa imong pagbiyahe sa dalan.'},
29 | 'Svenska': {wikiTag: 'sv', speechTag: 'sv-SE', welcomeMsg: 'Välkommen till din vägresa.'},
30 | 'Deutsch': {wikiTag: 'de', speechTag: 'de-DE', welcomeMsg: 'Willkommen zu Ihrem Roadtrip.'},
31 | 'Nederlands ': {wikiTag: 'nl', speechTag: 'nl-NL', welcomeMsg: 'Welkom bij je roadtrip.'},
32 | 'русский': {wikiTag: 'ru', speechTag: 'ru-RU', welcomeMsg: 'Добро пожаловать в свою поездку.'},
33 | 'Italiano': {wikiTag: 'it', speechTag: 'it-IT', welcomeMsg: 'Benvenuto nel tuo viaggio.'},
34 | 'Español': {wikiTag: 'es', speechTag: 'es', welcomeMsg: 'Bienvenido a tu viaje.'},
35 | 'Polski': {wikiTag: 'pl', speechTag: 'pl-PL', welcomeMsg: 'Witamy w podróży.'},
36 | 'Tiếng Việt': {wikiTag: 'vi', speechTag: 'vi-VN', welcomeMsg: 'Chào mừng bạn đến với chuyến đi của bạn.'},
37 | '日本語': {wikiTag: 'ja', speechTag: 'ja-JP', welcomeMsg: 'あなたのロードトリップへようこそ。'},
38 | '中文': {wikiTag: 'ch', speechTag: 'zh-CN', welcomeMsg: '欢迎来到您的公路旅行。'},
39 | 'Português (PT)': {wikiTag: 'pt', speechTag: 'pt-PT', welcomeMsg: 'Bem-vindo à sua viagem.'},
40 | 'Português (BR)': {wikiTag: 'pt', speechTag: 'pt-BR', welcomeMsg: 'Bem-vindo à sua viagem.'},
41 | 'українська': {wikiTag: 'uk', speechTag: 'uk-UA', welcomeMsg: 'Ласкаво просимо до дорожньої подорожі.'},
42 | 'فارسی': {wikiTag: 'fa', speechTag: 'fa-IR', welcomeMsg: 'به سفر جاده ای شما خوش آمدید'},
43 | 'српски': {wikiTag: 'sr', speechTag: 'sr-BA', welcomeMsg: 'Добродошли на путовање.'},
44 | 'Català': {wikiTag: 'ca', speechTag: 'ca-ES', welcomeMsg: 'Benvingut al vostre viatge.'},
45 | 'العربية': {wikiTag: 'ar', speechTag: 'ar', welcomeMsg: 'مرحبا بك في رحلتك'},
46 | };
47 |
48 | function wikiUrl(path, api, mobile) {
49 | let url = 'https://' + state.lang.wikiTag;
50 | if (mobile) url += '.m';
51 | return url + '.wikipedia.org/' + (api ? 'w/api.php?' : 'wiki/') + path
52 | }
53 |
54 | function startWatchLocation() {
55 | return new Promise(resolve => {
56 | navigator.geolocation.watchPosition(pos => {
57 | if (!manualPosition) {
58 | console.info('Updated position', pos);
59 | position = pos;
60 | if (map) {
61 | map.setCenter({lat: pos.coords.latitude, lng: pos.coords.longitude});
62 | }
63 | }
64 | resolve();
65 | }, error => {
66 | console.error('WatchPosition error.', error.message, error);
67 | alert('Failed to find your current location.')
68 | }, {
69 | enableHighAccuracy: false,
70 | maximumAge: 15000,
71 | });
72 | });
73 | }
74 |
75 | async function geoCode(position) {
76 | const response = await fetchWithTimeout('https://roadtrip-api.glitch.me/geocode?latlng='
77 | + position.coords.latitude + ',' + position.coords.longitude);
78 | if (response.ok) {
79 | const json = await response.json();
80 | //console.info('Geo coding result: ' + JSON.stringify(json));
81 | if (json.status == 'OK') {
82 | return json.results;
83 | } else {
84 | throw new Error('Reverse geocoding failed: ' + JSON.stringify(json));
85 | }
86 | } else {
87 | const text = await response.text();
88 | throw new Error('Reverse geocoding failed: ' + text);
89 | }
90 | }
91 |
92 | function processResult(results) {
93 | return results.filter(result => {
94 | let found = false ;
95 | result.types.forEach(type => {
96 | let index = interestingTypes.indexOf(type);
97 | if (index == -1) {
98 | return;
99 | }
100 | if (!result.interestingness || result.interestingness > index) {
101 | result.interestingness = index;
102 | console.info('Location candidate', result.formatted_address, result.types);
103 | found = true;
104 | }
105 | });
106 | return found;
107 | }).sort((a, b) => a.interestingness - b.interestingness);
108 | }
109 |
110 | async function searchWikipedia(term) {
111 | const response = await fetchWithTimeout(wikiUrl('action=opensearch&format=json&origin=*&limit=1&search=' + encodeURIComponent(term), true));
112 | if (!response.ok) {
113 | console.error('Wikipedia call failed', response)
114 | throw new Error('Wikipedia search is down');
115 | }
116 | const json = await response.json();
117 | console.info('Search response', term, json);
118 | const title = json[1][0]
119 | if (title) {
120 | return title;
121 | }
122 | const parts = term.split(/\,\s+/);
123 | parts.pop();
124 | const newTerm = parts.join(', ');
125 | if (newTerm) {
126 | return searchWikipedia(newTerm);
127 | }
128 | return null;
129 | }
130 |
131 | async function getContent(title) {
132 | console.info('Getting content');
133 | const response = await fetchWithTimeout(wikiUrl('redirects=true&format=json&origin=*&action=query&prop=extracts|coordinates&titles=' + encodeURIComponent(title), true));
134 | if (!response.ok) {
135 | console.error('Wikipedia content call failed', response)
136 | throw new Error('Wikipedia content is down');
137 | }
138 | const json = await response.json();
139 | const page = Object.values(json.query.pages)[0];
140 | console.info('Page', page)
141 | seen[page.title] = true;
142 | return {
143 | url: wikiUrl(encodeURIComponent(page.title), false),
144 | title: page.title,
145 | label: page.title,
146 | content: simpleHtmlToText(page.extract.trim()),
147 | lang: state.lang.speechTag,
148 | coordinates: page.coordinates[0] ? {
149 | lat: page.coordinates[0].lat,
150 | lng: page.coordinates[0].lon,
151 | } : null,
152 | };
153 | }
154 |
155 | async function getArticleForLocation() {
156 | console.info('Geo coding');
157 | const locationResults = processResult(await geoCode(position));
158 | console.info('Location results retrieved', locationResults.length);
159 | let result;
160 | while (result = locationResults.find(r => !seen[r.formatted_address])) {
161 | seen[result.formatted_address] = true;
162 | console.info('Searching for', result.formatted_address);
163 | const title = await searchWikipedia(result.formatted_address);
164 | if (!title) {
165 | continue;
166 | }
167 | console.info('Title', title);
168 | return getContent(title);
169 | };
170 | return null;
171 | }
172 |
173 | async function getNearbyArticle() {
174 | console.info('Finding nearby article');
175 | const response = await fetchWithTimeout(wikiUrl('action=query&format=json&origin=*&generator=geosearch&ggsradius=10000&ggsnamespace=0&ggslimit=50&formatversion=2&ggscoord=' + encodeURIComponent(position.coords.latitude) + '%7C' + encodeURIComponent(position.coords.longitude), true, true));
176 | if (!response.ok) {
177 | console.error('Wikipedia nearby failed', response)
178 | throw new Error('Wikipedia nearby is down');
179 | }
180 | const json = await response.json();
181 | console.info('Nearby response', json);
182 | const pages = json.query.pages;
183 | for (let page of pages) {
184 | const title = page.title;
185 | if (seen[title]) {
186 | continue;
187 | }
188 | seen[title] = true;
189 | console.info('Title', title);
190 | return getContent(title);
191 | }
192 | return null;
193 | }
194 |
195 | async function speak(text, language) {
196 | // Mobile Chrome doesn't like long texts, so we just do one sentence at a time.
197 | // Make a sentence end a paragraph end. \w\w to not match e.g.
198 | const paras = text.split(/\n/).filter(p => p.trim());
199 | for (let p of paras) {
200 | p = p.replace(/(\w\w\.)/, '$1\n')
201 | const sentences = p.split(/\.\n/).filter(e => e.trim());
202 | for (let sentence of sentences) {
203 | await speakSentence(sentence, language);
204 | if (cancelSpeaking) {
205 | cancelSpeaking = false;
206 | console.info('Cancel speaking');
207 | return;
208 | }
209 | if (skipThisParagraph) {
210 | skipThisParagraph = false;
211 | console.info('Skipping paragraph');
212 | break; // Goes to next step in paras loop
213 | }
214 | }
215 | }
216 |
217 | }
218 |
219 | function speakSentence(text, language) {
220 | var utterance = new SpeechSynthesisUtterance();
221 | utterance.text = text + '.';
222 | utterance.lang = language;
223 | console.info('Start speaking', utterance.text);
224 | let interval;
225 | return new Promise(resolve => {
226 | let spoke = false;
227 | interval = setInterval(() => {
228 | if (window.speechSynthesis.speaking) {
229 | spoke = true;
230 | }
231 | if (spoke && !window.speechSynthesis.speaking) {
232 | if (state.paused) {
233 | return;
234 | }
235 | console.info('Detected end of speaking without end event', utterance.text);
236 | resolve();
237 | }
238 | }, 100);
239 | utterance.onend = () => {
240 | console.info('End speaking', utterance.text);
241 | resolve();
242 | };
243 | utterance.onerror = e => {
244 | console.error('Error speaking', e.error, e);
245 | resolve();
246 | };
247 | utterance.onpause = () => console.info('Pause');
248 | utterance.onresume = () => console.info('Resume');
249 | window.speechSynthesis.speak(utterance);
250 | }).then(() => clearInterval(interval))
251 | }
252 |
253 | async function talkAboutLocation(article) {
254 | state.status = `Reading about ${html(article.title)}`;
255 | render();
256 | gtag('config', 'UA-121987888-1', {
257 | 'page_title' : 'Article ' + article.title,
258 | 'page_path': '/article/' + encodeURIComponent(article.title),
259 | });
260 | if (article.coordinates && map) {
261 | article.marker = new google.maps.Marker({
262 | position: article.coordinates,
263 | map: map,
264 | title: article.title,
265 | //label: article.title,
266 | });
267 | }
268 | return speak(article.content, article.lang);
269 | }
270 |
271 | async function start() {
272 | state.playing = true;
273 | state.loading = true;
274 | state.lang = languageMap[$('language_inner_select').value];
275 | state.status = 'Finding your location.'
276 | render();
277 | const s = startWatchLocation();
278 | $('toast').MaterialSnackbar.showSnackbar({
279 | message: 'Please make sure your volume is turned up!',
280 | timeout: 10000,
281 | });
282 | await speak(state.lang.welcomeMsg + '\n\n', state.lang.speechTag);
283 | await s;
284 | next();
285 | }
286 |
287 | async function next() {
288 | console.info('Starting session');
289 | state.playing = true;
290 | state.loading = true;
291 | state.status = 'Finding something interesting to read. I\'ll keep checking as you move.'
292 | render();
293 | gtag('config', 'UA-121987888-1', {
294 | 'page_title' : 'Next',
295 | 'page_path': '/next/' + state.lang.speechTag,
296 | });
297 | try {
298 | console.info('Finding article.');
299 | let article = await getArticleForLocation();
300 | if (!article) {
301 | console.info('Did not find location article. Trying nearby.');
302 | article = await getNearbyArticle();
303 | }
304 | if (!article) {
305 | console.info('Did not find article');
306 | setTimeout(next, pollSeconds * 1000);
307 | return;
308 | }
309 | console.info('Retrieved article');
310 | state.loading = false;
311 | render();
312 | await talkAboutLocation(article);
313 | } catch (e) {
314 | console.error('Error :' + e, e);
315 | setTimeout(next, pollSeconds * 1000);
316 | return;
317 | }
318 | next();
319 | }
320 |
321 | function pause() {
322 | state.paused = true;
323 | render()
324 | window.speechSynthesis.pause();
325 | }
326 |
327 | function play() {
328 | state.paused = false;
329 | render()
330 | window.speechSynthesis.resume();
331 | }
332 |
333 | function forward() {
334 | cancelSpeaking = true;
335 | window.speechSynthesis.cancel();
336 | }
337 |
338 | function skipParagraph() {
339 | skipThisParagraph = true;
340 | window.speechSynthesis.cancel();
341 | }
342 |
343 | function render() {
344 | $('html_start').style.display = display(!state.playing);
345 | $('language_select').style.display = display(!state.playing);
346 | $('html_pause').style.display = display(!state.loading && state.playing && !state.paused);
347 | $('html_play').style.display = display(!state.loading && state.playing && state.paused);
348 | $('html_next').style.display = display(!state.loading && state.playing);
349 | $('html_skip').style.display = display(!state.loading && state.playing);
350 | $('html_spinner').style.display = display(state.loading);
351 | $('html_title').innerHTML = state.status;
352 | }
353 |
354 | function simpleHtmlToText(html) {
355 | const div = document.createElement('div');
356 | div.innerHTML = html;
357 | remove(div.querySelector('#References'));
358 | remove(div.querySelector('#See_also'));
359 | let text = div.textContent;
360 | text = text.replace(/(.)\n/g, '$1.\n');
361 | // Remove stuff in parantheses. Nobody wants to hear that stuff.
362 | // This isn't how you pass the Google interview.
363 | // But it is technically O(n)
364 | for (let i = 0; i < 10; i++) {
365 | text = text.replace(/\([^\)]+\)/g, '');
366 | }
367 | return text;
368 | }
369 |
370 | function remove(element) {
371 | if (!element) {
372 | return false;
373 | }
374 | return element.parentElement.removeChild(element);
375 | }
376 |
377 | function $(id) {
378 | return document.getElementById(id);
379 | }
380 |
381 | function display(bool) {
382 | return bool ? 'block' : 'none';
383 | }
384 |
385 | function html(text) {
386 | const div = document.createElement('div');
387 | div.textContent = text;
388 | return div.innerHTML;
389 | }
390 |
391 | function initMap() {
392 | const startLoc = {lat: -34.397, lng: 150.644};
393 | map = new google.maps.Map($('map'), {
394 | center: startLoc,
395 | zoom: 11
396 | });
397 | const centerMarker = new google.maps.Marker({
398 | position: startLoc,
399 | map: map,
400 | title: 'Current Location'
401 | });
402 | map.addListener('center_changed', e => {
403 | position = {
404 | coords: {
405 | longitude: map.getCenter().lng(),
406 | latitude: map.getCenter().lat(),
407 | }
408 | };
409 | centerMarker.setPosition({
410 | lat: map.getCenter().lat(),
411 | lng: map.getCenter().lng()
412 | });
413 | console.info('Map position', position);
414 | manualPosition = true;
415 | });
416 | }
417 |
418 | function initLanguageSelect(selected) {
419 | var fragment = document.createDocumentFragment();
420 |
421 | for (let language of Object.keys(languageMap)) {
422 | var opt = document.createElement('option');
423 | opt.innerHTML = language;
424 | opt.value = language;
425 | fragment.appendChild(opt);
426 | }
427 |
428 | $('language_inner_select').appendChild(fragment);
429 | $('language_inner_select').value = selected;
430 | }
431 |
432 | function selectLang(langTag) {
433 | let selected = null;
434 | for (var langValue in languageMap) {
435 | if (languageMap[langValue].speechTag === langTag || languageMap[langValue].wikiTag === langTag) {
436 | selected = langValue;
437 | break;
438 | }
439 | }
440 |
441 | if (!selected) {
442 | selected = 'Browser Language (' + langTag + ')';
443 | languageMap[selected] = {
444 | speechTag: langTag,
445 | wikiTag: langTag.split('-')[0],
446 | welcomeMsg: ''
447 | };
448 | }
449 | initLanguageSelect(selected);
450 | }
451 |
452 | function timeout(time, message) {
453 | return new Promise((resolve, reject) => {
454 | setTimeout(() => reject(new Error('Timeout: ' + message)), time);
455 | })
456 | }
457 |
458 | function fetchWithTimeout(url, paras) {
459 | return Promise.race([fetch(url, paras),
460 | timeout(15 * 1000, 'Fetch timed out for ' + url)]);
461 | }
462 |
463 | async function guessLang() {
464 | const langTag = navigator.language;
465 | const wikiTag = langTag.split('-')[0];
466 | const response = await fetchWithTimeout('https://' + wikiTag + '.wikipedia.org/w/api.php?redirects=true&format=json&origin=*&action=query&prop=extracts&titles=Main_Page');
467 | if (response.ok) {
468 | console.info('Set language to browser language', langTag);
469 | return langTag;
470 | }
471 | }
472 |
473 | function init() {
474 | onunload = function() {
475 | window.speechSynthesis.cancel();
476 | }
477 | navigator.serviceWorker.register("/sw.js");
478 |
479 | const l = new URLSearchParams(location.search).get('lang');
480 | if (l) {
481 | selectLang(encodeURIComponent(l));
482 | }
483 | else {
484 | guessLang().then(selectLang);
485 | }
486 | }
487 |
488 | init();
--------------------------------------------------------------------------------