4.3 KiB, 1 run
created by r2519314175:881, which is this file's identity for as long as the history lasts, whatever it is later renamed to
download · who wrote it · its history
| 1 | // What the extension says, in the eight languages the app speaks. |
| 2 | // |
| 3 | // The strings live in `_locales/<dir>/messages.json`, which is Chrome's own |
| 4 | // format for exactly one reason: it is the only thing that can translate the |
| 5 | // manifest. `name`, `description` and `action.default_title` are read by the |
| 6 | // browser before any code of ours runs, and `__MSG_*__` in the manifest is the |
| 7 | // sole way to reach them. Having paid for that file layout, everything else |
| 8 | // uses it too, rather than carrying a second table. |
| 9 | // |
| 10 | // Which language, though, is not Chrome's business. Chrome would dress these |
| 11 | // windows in the BROWSER's UI language; the user chose a language in the app, |
| 12 | // and the grant window is a sentence the app started. So the lookup is: |
| 13 | // |
| 14 | // 1. the locale the user chose in the app, mirrored here by announce.js; |
| 15 | // 2. chrome.i18n, i.e. the browser's UI language; |
| 16 | // 3. `default_locale`, which is English. |
| 17 | // |
| 18 | // Each step falls through to the next, so a missing file, a language we do not |
| 19 | // ship, or a fresh profile that has never seen the app all end in plain English |
| 20 | // rather than in a blank window. |
| 21 | |
| 22 | 'use strict'; |
| 23 | |
| 24 | (function () { |
| 25 | |
| 26 | /// App locale code -> Chrome `_locales` directory name. Chrome's directories |
| 27 | /// use an underscore and its own region spellings, so `pt-BR` is `pt_BR` and |
| 28 | /// `zh-Hans` is `zh_CN` -- the app's script-based tag has no `_locales` form. |
| 29 | const DIRS = { |
| 30 | 'en': 'en', |
| 31 | 'de': 'de', |
| 32 | 'es': 'es', |
| 33 | 'fr': 'fr', |
| 34 | 'ja': 'ja', |
| 35 | 'ko': 'ko', |
| 36 | 'pt-BR': 'pt_BR', |
| 37 | 'zh-Hans': 'zh_CN', |
| 38 | }; |
| 39 | |
| 40 | /// The chosen locale's table, or null while none is chosen or loaded. |
| 41 | let table = null; |
| 42 | /// The app locale code in force, for `document.documentElement.lang`. |
| 43 | let code = ''; |
| 44 | let readyP = null; |
| 45 | |
| 46 | /// Expand `$NAME$` from the entry's own placeholder map, then `$1`..`$9` |
| 47 | /// from the arguments -- the same two steps `chrome.i18n.getMessage` takes, |
| 48 | /// so a message reads identically whichever path served it. |
| 49 | function expand(entry, subs) { |
| 50 | const ph = entry.placeholders || {}; |
| 51 | const s = String(entry.message).replace(/\$([A-Za-z0-9_@]+)\$/g, (whole, name) => { |
| 52 | const p = ph[name] || ph[name.toLowerCase()]; |
| 53 | return p && p.content != null ? String(p.content) : whole; |
| 54 | }); |
| 55 | return s.replace(/\$([1-9])/g, (whole, n) => { |
| 56 | const v = subs[Number(n) - 1]; |
| 57 | return v == null ? '' : String(v); |
| 58 | }); |
| 59 | } |
| 60 | |
| 61 | /// The string for `key`, with any `$1`.. substitutions. |
| 62 | /// |
| 63 | /// An unknown key comes back as the key itself rather than as a blank, so a |
| 64 | /// mistake is visible on the surface instead of leaving a window with a gap |
| 65 | /// in it. |
| 66 | function t(key) { |
| 67 | const subs = Array.prototype.slice.call(arguments, 1); |
| 68 | const entry = table && table[key]; |
| 69 | if (entry && typeof entry.message === 'string') return expand(entry, subs); |
| 70 | try { |
| 71 | const m = chrome.i18n.getMessage(key, subs.map(String)); |
| 72 | if (m) return m; |
| 73 | } catch (e) { /* no i18n here; the key stands */ } |
| 74 | return key; |
| 75 | } |
| 76 | |
| 77 | /// The locale the app is speaking, as announce.js last saw it. |
| 78 | async function chosen() { |
| 79 | try { |
| 80 | const got = await chrome.storage.local.get('locale'); |
| 81 | const c = got && got.locale; |
| 82 | return DIRS[c] ? c : ''; |
| 83 | } catch (e) { |
| 84 | return ''; |
| 85 | } |
| 86 | } |
| 87 | |
| 88 | /// Load the chosen locale's table, once. Resolves whatever happens: a |
| 89 | /// failure here simply leaves `chrome.i18n` in charge. |
| 90 | function ready() { |
| 91 | if (!readyP) readyP = (async () => { |
| 92 | code = await chosen(); |
| 93 | if (!code) return; |
| 94 | try { |
| 95 | const res = await fetch(chrome.runtime.getURL('_locales/' + DIRS[code] + '/messages.json')); |
| 96 | if (res.ok) table = await res.json(); |
| 97 | } catch (e) { /* the browser's own language stands */ } |
| 98 | })(); |
| 99 | return readyP; |
| 100 | } |
| 101 | |
| 102 | /// Set every marked node in `root` from the table. Attributes carry their |
| 103 | /// own marks, because a button often needs a label and a title both. |
| 104 | function paint(root) { |
| 105 | const q = root || document; |
| 106 | q.querySelectorAll('[data-i18n]').forEach((n) => { n.textContent = t(n.getAttribute('data-i18n')); }); |
| 107 | q.querySelectorAll('[data-i18n-title]').forEach((n) => { n.title = t(n.getAttribute('data-i18n-title')); }); |
| 108 | // Say what language this window is in, so a screen reader pronounces it |
| 109 | // and the browser does not offer to translate what is already translated. |
| 110 | if (!root && code) { |
| 111 | try { document.documentElement.lang = code; } catch (e) {} |
| 112 | } |
| 113 | } |
| 114 | |
| 115 | globalThis.DaimondExtI18n = { t, ready, paint, dirs: DIRS, locale: () => code }; |
| 116 | |
| 117 | })(); |