Oregami
Repositories/oxedyne/daimond

oxedyne/daimond/ext/i18n.js

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})();