|
13 | 13 | * lang=fr Override the browser language (default: auto-detected) |
14 | 14 | * id=myDiv Insert the banner inside the element with this id |
15 | 15 | * (default: prepend to <body>) |
16 | | - * size=normal Banner size: "normal" (default) or "mini" |
| 16 | + * size=normal Banner size: "normal" (default), "mini" or "minimal" |
17 | 17 | * link=URL Make the banner text a link (default: https://keepandroidopen.org) |
18 | 18 | * Set link=none to disable the link |
19 | 19 | * hidebutton=on Show an X close button (default: on) |
20 | 20 | * Set hidebutton=off to hide the close button |
| 21 | + * animation=on Add animation to border of banner (default: on) |
| 22 | + * Set animation=off to disable |
21 | 23 | */ |
22 | 24 | (function () { |
23 | 25 | "use strict"; |
|
26 | 28 | var messages = { |
27 | 29 | fa: "اندروید، یک سکّوی بسته خواهد شد!", |
28 | 30 | ar: "سيصبح نظام أندرويد منصة مغلقة في", |
29 | | - en: "Android will become a locked-down platform", |
| 31 | + he: "אנדרואיד תהפוך לפלטפורמה נעולה בעוד", |
| 32 | + en: "Android will become a locked-down platform in", |
30 | 33 | ca: "Android es convertir\u00E0 en una plataforma tancada", |
31 | | - cs: "Android will become a locked-down platform in", |
| 34 | + cs: "Android se stane uzamčenou platformou za", |
32 | 35 | de: "Android wird eine geschlossene Plattform werden.", |
| 36 | + da: "Android vil blive en lukket platform om", |
| 37 | + nl: "Android zal een gesloten platform worden over", |
33 | 38 | el: "\u03A4\u03BF Android \u03B8\u03B1 \u03B3\u03AF\u03BD\u03B5\u03B9 \u03BC\u03AF\u03B1 \u03BA\u03BB\u03B5\u03B9\u03C3\u03C4\u03AE \u03C0\u03BB\u03B1\u03C4\u03C6\u03CC\u03C1\u03BC\u03B1", |
34 | 39 | es: "Android se convertir\u00E1 en una plataforma cerrada", |
35 | 40 | fr: "Android va devenir une plateforme ferm\u00E9e", |
|
44 | 49 | tr: "Android k\u0131s\u0131tl\u0131 bir platform haline gelecek.", |
45 | 50 | uk: "Android \u0441\u0442\u0430\u043D\u0435 \u0437\u0430\u043A\u0440\u0438\u0442\u043E\u044E \u043F\u043B\u0430\u0442\u0444\u043E\u0440\u043C\u043E\u044E", |
46 | 51 | "zh-CN": "\u5B89\u5353\u5C06\u6210\u4E3A\u4E00\u4E2A\u5C01\u95ED\u5E73\u53F0", |
47 | | - "zh-TW": "\u5012\u6578 Android \u5373\u5C07\u6DEA\u70BA\u756B\u5730\u70BA\u7262\u3001\u684E\u688F\u6EFF\u76C8\u7684\u5C01\u9589\u5E73\u81FA", |
48 | | - ja: "Androidは閉鎖的なプラットフォームになろうとしています" |
| 52 | + "zh-TW": "Android \u5C07\u6210\u70BA\u4E00\u500B\u5C01\u9589\u5E73\u53F0", |
| 53 | + ja: "Androidは閉鎖的なプラットフォームになろうとしています", |
| 54 | + fi: "Androidista tulee suljettu alusta", |
| 55 | + hu: "Az Android egy lezárt platform lesz", |
| 56 | + vi: "Android sẽ trở thành một hệ điều hành đóng", |
49 | 57 | }; |
50 | 58 |
|
51 | 59 | // ── Parse query parameters from the script's own src URL ────────────── |
|
70 | 78 | // ── Determine locale ────────────────────────────────────────────────── |
71 | 79 | function resolveLocale(tag) { |
72 | 80 | if (!tag) return "en"; |
| 81 | + // Exact match |
73 | 82 | if (messages[tag]) return tag; |
| 83 | + // Case-insensitive exact match (e.g. "pt-br" → "pt-BR") |
74 | 84 | var lower = tag.toLowerCase(); |
75 | 85 | for (var key in messages) { |
76 | 86 | if (key.toLowerCase() === lower) return key; |
77 | 87 | } |
78 | | - var base = tag.split("-")[0].toLowerCase(); |
79 | | - if (messages[base]) return base; |
| 88 | + // Fallback to base language (e.g. "de-CH" → "de", "zh-Hans" → "zh") |
| 89 | + var base = lower.split("-")[0]; |
80 | 90 | for (var key2 in messages) { |
81 | | - if (key2.toLowerCase().split("-")[0] === base) return key2; |
| 91 | + if (key2.toLowerCase() === base) return key2; |
| 92 | + } |
| 93 | + // Fallback to any regional variant of the base language (e.g. "pt" → "pt-BR") |
| 94 | + for (var key3 in messages) { |
| 95 | + if (key3.toLowerCase().split("-")[0] === base) return key3; |
82 | 96 | } |
83 | 97 | return "en"; |
84 | 98 | } |
|
91 | 105 | ); |
92 | 106 |
|
93 | 107 | // ── Size variant ────────────────────────────────────────────────────── |
94 | | - var size = params.size === "mini" ? "mini" : "normal"; |
| 108 | + var size = params.size === "mini" ? "mini" |
| 109 | + : params.size === "minimal" |
| 110 | + ? "minimal" |
| 111 | + : "normal"; |
95 | 112 |
|
96 | 113 | // ── Link ──────────────────────────────────────────────────────────── |
97 | 114 | var linkParam = params.link; |
98 | | - var linkUrl = linkParam === "none" ? null : (linkParam || "https://keepandroidopen.org"); |
| 115 | + var defaultLink = "https://keepandroidopen.org" + (locale === "en" ? "" : "/" + locale + "/"); |
| 116 | + var linkUrl = linkParam === "none" ? null : (linkParam || defaultLink); |
99 | 117 |
|
100 | 118 | // ── Close button ──────────────────────────────────────────────────── |
101 | 119 | var showClose = params.hidebutton !== "off"; |
|
122 | 140 | "0px 3px 0px #751111," + |
123 | 141 | "0px 4px 0px #5e0d0d," + |
124 | 142 | "0px 6px 10px rgba(0,0,0,0.5);" + |
125 | | - "animation:kao-pulse 2s infinite;" + |
126 | 143 | "padding:0.5rem 2.5rem;" + |
127 | 144 | "line-height:1.6;" + |
128 | 145 | "box-sizing:border-box;" + |
|
145 | 162 | "0px 1px 0px #9e1a1a," + |
146 | 163 | "0px 2px 0px #8a1515," + |
147 | 164 | "0px 3px 5px rgba(0,0,0,0.4);" + |
148 | | - "animation:kao-pulse 2s infinite;" + |
149 | 165 | "padding:0.25rem 1.5rem;" + |
150 | 166 | "line-height:1.4;" + |
151 | 167 | "box-sizing:border-box;" + |
152 | 168 | "}"; |
153 | 169 |
|
| 170 | + var cssMinimal = |
| 171 | + ".kao-banner{" + |
| 172 | + "position:relative;" + |
| 173 | + "font-variant-numeric:tabular-nums;" + |
| 174 | + "background:linear-gradient(180deg,#d32f2f 0%,#b71c1c 100%);" + |
| 175 | + "border-bottom:2px solid #801313;" + |
| 176 | + "color:#fff;" + |
| 177 | + "font-family:'Arial Black',sans-serif;" + |
| 178 | + "font-weight:900;" + |
| 179 | + "text-transform:uppercase;" + |
| 180 | + "letter-spacing:1px;" + |
| 181 | + "font-size:0.75rem;" + |
| 182 | + "text-align:center;" + |
| 183 | + "text-shadow:" + |
| 184 | + "0px 1px 0px #9e1a1a," + |
| 185 | + "0px 2px 0px #8a1515," + |
| 186 | + "0px 3px 5px rgba(0,0,0,0.4);" + |
| 187 | + "padding:0.25rem 1.5rem;" + |
| 188 | + "line-height:1.4;" + |
| 189 | + "box-sizing:border-box;" + |
| 190 | + "}"; |
| 191 | + |
154 | 192 | var cssCommon = |
155 | 193 | ".kao-banner a{color:#fff;text-decoration:none;}" + |
156 | 194 | ".kao-banner a:hover{text-decoration:underline;}" + |
|
169 | 207 | "line-height:1;" + |
170 | 208 | "text-shadow:none;" + |
171 | 209 | "}" + |
172 | | - ".kao-banner-close:hover{opacity:1;}" + |
| 210 | + ".kao-banner-close:hover{opacity:1;}"; |
| 211 | + |
| 212 | + var cssKaoPulse = |
| 213 | + ".kao-banner:not(.no-animation) { animation:kao-pulse 2s infinite; }" + |
173 | 214 | "@keyframes kao-pulse{" + |
174 | 215 | "0%{box-shadow:0 0 0 0 rgba(211,47,47,0.7)}" + |
175 | 216 | "70%{box-shadow:0 0 0 15px rgba(211,47,47,0)}" + |
176 | 217 | "100%{box-shadow:0 0 0 0 rgba(211,47,47,0)}" + |
177 | 218 | "}"; |
178 | 219 |
|
179 | 220 | var style = document.createElement("style"); |
180 | | - style.textContent = (size === "mini" ? cssMini : cssNormal) + cssCommon; |
| 221 | + style.textContent = (size === "mini" ? cssMini : size === "minimal" ? cssMinimal : cssNormal) |
| 222 | + + (params.animation === "off" ? "" : cssKaoPulse) |
| 223 | + + cssCommon; |
181 | 224 | document.head.appendChild(style); |
182 | 225 |
|
183 | 226 | // ── Check if previously dismissed (reappears after dismissDays) ───── |
|
194 | 237 |
|
195 | 238 | // ── Create banner DOM ───────────────────────────────────────────────── |
196 | 239 | var banner = document.createElement("div"); |
197 | | - banner.className = "kao-banner"; |
| 240 | + banner.className = params.animation === "off" ? "kao-banner no-animation" : "kao-banner"; |
198 | 241 |
|
199 | 242 | var messageText = messages[locale] || messages.en; |
200 | 243 |
|
|
209 | 252 | banner.appendChild(document.createTextNode(messageText)); |
210 | 253 | } |
211 | 254 |
|
212 | | - banner.appendChild(document.createElement("br")); |
| 255 | + if (params.size === "minimal") { |
| 256 | + banner.appendChild(document.createTextNode("\u00A0")); |
| 257 | + } else { |
| 258 | + banner.appendChild(document.createElement("br")); |
| 259 | + } |
213 | 260 |
|
214 | 261 | var countdownSpan = document.createElement("span"); |
215 | 262 | countdownSpan.textContent = "\u00A0"; |
|
244 | 291 | // ── Countdown logic ─────────────────────────────────────────────────── |
245 | 292 | var countDownDate = new Date("Sep 1, 2026 00:00:00").getTime(); |
246 | 293 |
|
247 | | - var formatter = new Intl.RelativeTimeFormat(locale, { style: "narrow" }); |
248 | | - |
249 | | - var pfx = new Array(4); |
250 | | - var sfx = new Array(4); |
251 | | - |
252 | | - function getOffset(unit) { |
253 | | - switch (unit) { |
254 | | - case "day": return 0; |
255 | | - case "hour": return 1; |
256 | | - case "minute": return 2; |
257 | | - case "second": return 3; |
258 | | - } |
259 | | - } |
260 | | - |
261 | | - function extractCommon(p, c, reverse) { |
262 | | - var s = 0; |
263 | | - var w = 0; |
264 | | - var i = reverse ? p.length - 1 : 0; |
265 | | - var j = reverse ? c.length - 1 : 0; |
266 | | - var pEnd = reverse ? 0 : p.length; |
267 | | - var cEnd = reverse ? 0 : c.length; |
268 | | - var chr; |
269 | | - while ( |
270 | | - (reverse ? i >= pEnd : i < pEnd) && |
271 | | - (reverse ? j >= cEnd : j < cEnd) && |
272 | | - (chr = p[reverse ? i-- : i++]) === c[reverse ? j-- : j++] |
273 | | - ) { |
274 | | - w = chr === " " ? w + 1 : 0; |
275 | | - s++; |
276 | | - } |
277 | | - return s - w; |
278 | | - } |
279 | | - |
280 | | - function cacheFormattingInfo(value, unit) { |
281 | | - var p = formatter.formatToParts(value, unit); |
282 | | - if (!p.length) return; |
283 | | - var c = formatter.formatToParts(-value, unit); |
284 | | - |
285 | | - var offset = getOffset(unit); |
286 | | - if (p[0].type === "literal" && (!c.length || c[0].type !== "literal" || !c[0].value.endsWith(p[0].value))) { |
287 | | - pfx[offset] = p[0].value.length; |
288 | | - } |
289 | | - if (p[p.length - 1].type === "literal") { |
290 | | - if (!c.length || c[c.length - 1].type !== "literal") { |
291 | | - sfx[offset] = p[p.length - 1].value.length; |
292 | | - } else if (!c[c.length - 1].value.startsWith(p[p.length - 1].value)) { |
293 | | - sfx[offset] = |
294 | | - p[p.length - 1].value.length - |
295 | | - extractCommon(p[p.length - 1].value, c[c.length - 1].value, false); |
296 | | - } |
297 | | - } |
298 | | - } |
| 294 | + var unitFormatters = { |
| 295 | + day: new Intl.NumberFormat(locale, { style: "unit", unit: "day", unitDisplay: "narrow" }), |
| 296 | + hour: new Intl.NumberFormat(locale, { style: "unit", unit: "hour", unitDisplay: "narrow" }), |
| 297 | + minute: new Intl.NumberFormat(locale, { style: "unit", unit: "minute", unitDisplay: "narrow" }), |
| 298 | + second: new Intl.NumberFormat(locale, { style: "unit", unit: "second", unitDisplay: "narrow" }) |
| 299 | + }; |
299 | 300 |
|
300 | | - cacheFormattingInfo(1, "day"); |
301 | | - cacheFormattingInfo(2, "hour"); |
302 | | - cacheFormattingInfo(3, "minute"); |
303 | | - cacheFormattingInfo(4, "second"); |
304 | | - |
305 | | - function getLocalizedUnit(value, unit, trimConjunction, trimSuffix) { |
306 | | - var offset = getOffset(unit); |
307 | | - var string = formatter.format(value, unit); |
308 | | - var p = pfx[offset]; |
309 | | - var s = sfx[offset]; |
310 | | - return string.slice( |
311 | | - trimConjunction && p || (p == 1 && string[0] === "+") ? pfx[offset] : 0, |
312 | | - trimSuffix && s ? -sfx[offset] : string.length |
313 | | - ); |
| 301 | + function formatUnit(value, unit) { |
| 302 | + return unitFormatters[unit].format(value); |
314 | 303 | } |
315 | 304 |
|
316 | 305 | var remaining = new Array(7); |
|
329 | 318 | var seconds = Math.floor((distance % (1000 * 60)) / 1000); |
330 | 319 |
|
331 | 320 | var parts = 0; |
332 | | - remaining[0] = days > 0 ? getLocalizedUnit(days, "day", parts++, true) : null; |
| 321 | + remaining[0] = days > 0 ? formatUnit(days, "day") : null; |
| 322 | + if (remaining[0]) parts++; |
333 | 323 | remaining[1] = parts ? separator : null; |
334 | 324 | remaining[2] = |
335 | 325 | parts || hours > 0 |
336 | | - ? getLocalizedUnit(hours, "hour", parts++, true) |
| 326 | + ? formatUnit(hours, "hour") |
337 | 327 | : null; |
| 328 | + if (remaining[2]) parts++; |
338 | 329 | remaining[3] = parts ? separator : null; |
339 | 330 | remaining[4] = |
340 | 331 | parts || minutes > 0 |
341 | | - ? getLocalizedUnit(minutes, "minute", parts++, true) |
| 332 | + ? formatUnit(minutes, "minute") |
342 | 333 | : null; |
| 334 | + if (remaining[4]) parts++; |
343 | 335 | remaining[5] = parts ? separator : null; |
344 | | - remaining[6] = getLocalizedUnit(seconds, "second", parts++, false); |
| 336 | + remaining[6] = formatUnit(seconds, "second"); |
345 | 337 |
|
346 | 338 | countdownSpan.textContent = remaining.join(""); |
347 | 339 |
|
|
0 commit comments