diff --git a/app/views/base/page.scala b/app/views/base/page.scala index a5588f7ed2e65..6addb667dcefc 100644 --- a/app/views/base/page.scala +++ b/app/views/base/page.scala @@ -9,7 +9,6 @@ import lila.api.SocketTest object page: val ui = lila.web.ui.layout(helpers, assetHelper)( - jsQuantity = lila.i18n.JsQuantity.apply, isRTL = lila.i18n.LangList.isRTL, popularAlternateLanguages = lila.i18n.LangList.popularAlternateLanguages, reportScoreThreshold = env.report.scoreThresholdsSetting.get, diff --git a/modules/i18n/src/main/JsQuantity.scala b/modules/i18n/src/main/JsQuantity.scala deleted file mode 100644 index 6f06d250c753b..0000000000000 --- a/modules/i18n/src/main/JsQuantity.scala +++ /dev/null @@ -1,145 +0,0 @@ -package lila.i18n - -import play.api.i18n.Lang - -object JsQuantity: - def apply(lang: Lang): String = - lang.language match - case "fr" | "ff" | "kab" | "co" | "ak" | "am" | "bh" | "fil" | "tl" | "guw" | "hi" | "ln" | "mg" | - "nso" | "ti" | "wa" => // french - """o=>o<=1?"one":"other"""" - case "cs" | "sk" => // czech - """o=>1==o?"one":o>=2&&o<=4?"few":"other"""" - case "hr" | "ru" | "sr" | "uk" | "be" | "bs" | "sh" | "ry" => // balkan - """o=>{const e=o%100,t=o%10;return 1==t&&11!=e?"one":t>=2&&t<=4&&!(e>=12&&e<=14)?"few":0==t||t>=5&&t<=9||e>=11&&e<=14?"many":"other"}""" - case "lv" => // latvian - """o=>0==o?"zero":o%10==1&&o%100!=11?"one":"other"""" - case "lt" => // lithuanian - """o=>{const e=o%100,t=o%10;return 1!=t||e>=11&&e<=19?t>=2&&t<=9&&!(e>=11&&e<=19)?"few":"other":"one"}""" - case "pl" => // polish - """o=>{const e=o%100,t=o%10;return 1==o?"one":t>=2&&t<=4&&!(e>=12&&e<=14)?"few":"other"}""" - case "ro" | "mo" => // romanian - """o=>{const e=o%100;return 1==o?"one":0==o||e>=1&&e<=19?"few":"other"}""" - case "sl" => // slovenian - """o=>{const e=o%100;return 1==e?"one":2==e?"two":e>=3&&e<=4?"few":"other"}""" - case "ar" => // arabic - """o=>{const e=o%100;return 0==o?"zero":1==o?"one":2==o?"two":e>=3&&e<=10?"few":e>=11&&e<=99?"many":"other"}""" - case "mk" => // macedonian - """o=>o%10==1&&11!=o?"one":"other"""" - case "cy" | "br" => // welsh - """o=>0==o?"zero":1==o?"one":2==o?"two":3==o?"few":6==o?"many":"other"""" - case "mt" => // maltese - """o=>{const e=o%100;return 1==o?"one":0==o||e>=2&&e<=10?"few":e>=11&&e<=19?"many":"other"}""" - case "ga" | "se" | "sma" | "smi" | "smj" | "smn" | "sms" => // two - """o=>1==o?"one":2==o?"two":"other"""" - case "az" | "bm" | "fa" | "ig" | "hu" | "ja" | "kde" | "kea" | "ko" | "my" | "ses" | "sg" | "to" | - "tr" | "vi" | "wo" | "yo" | "zh" | "bo" | "dz" | "id" | "jv" | "ka" | "km" | "kn" | "ms" | "th" | - "tp" | "io" | "ia" => // none - """o=>"other"""" - case _ => // other - """o=>1==o?"one":"other"""" - -/* - -// $ terser --mangle --compress --ecma 2018 --safari10 - -export const french = c => { - return c <= 1 ? 'one' : 'other'; -}; - -export const czech = c => { - if (c == 1) return 'one'; - else if (c >= 2 && c <= 4) return 'few'; - else return 'other'; -}; - -export const balkan = c => { - const rem100 = c % 100; - const rem10 = c % 10; - if (rem10 == 1 && rem100 != 11) return 'one'; - else if (rem10 >= 2 && rem10 <= 4 && !(rem100 >= 12 && rem100 <= 14)) return 'few'; - else if (rem10 == 0 || (rem10 >= 5 && rem10 <= 9) || (rem100 >= 11 && rem100 <= 14)) return 'many'; - else return 'other'; -}; - -export const latvian = c => { - if (c == 0) return 'zero'; - else if (c % 10 == 1 && c % 100 != 11) return 'one'; - else return 'other'; -}; - -export const lithuanian = c => { - const rem100 = c % 100; - const rem10 = c % 10; - if (rem10 == 1 && !(rem100 >= 11 && rem100 <= 19)) return 'one'; - else if (rem10 >= 2 && rem10 <= 9 && !(rem100 >= 11 && rem100 <= 19)) return 'few'; - else return 'other'; -}; - -export const polish = c => { - const rem100 = c % 100; - const rem10 = c % 10; - if (c == 1) return 'one'; - else if (rem10 >= 2 && rem10 <= 4 && !(rem100 >= 12 && rem100 <= 14)) return 'few'; - else return 'other'; -}; - -export const romanian = c => { - const rem100 = c % 100; - if (c == 1) return 'one'; - else if (c == 0 || (rem100 >= 1 && rem100 <= 19)) return 'few'; - else return 'other'; -}; - -export const slovenian = c => { - const rem100 = c % 100; - if (rem100 == 1) return 'one'; - else if (rem100 == 2) return 'two'; - else if (rem100 >= 3 && rem100 <= 4) return 'few'; - else return 'other'; -}; - -export const arabic = c => { - const rem100 = c % 100; - if (c == 0) return 'zero'; - else if (c == 1) return 'one'; - else if (c == 2) return 'two'; - else if (rem100 >= 3 && rem100 <= 10) return 'few'; - else if (rem100 >= 11 && rem100 <= 99) return 'many'; - else return 'other'; -}; - -export const macedonian = c => { - return c % 10 == 1 && c != 11 ? 'one' : 'other'; -}; - -export const welsh = c => { - if (c == 0) return 'zero'; - else if (c == 1) return 'one'; - else if (c == 2) return 'two'; - else if (c == 3) return 'few'; - else if (c == 6) return 'many'; - else return 'other'; -}; - -export const maltese = c => { - const rem100 = c % 100; - if (c == 1) return 'one'; - else if (c == 0 || (rem100 >= 2 && rem100 <= 10)) return 'few'; - else if (rem100 >= 11 && rem100 <= 19) return 'many'; - else return 'other'; -}; - -export const two = c => { - if (c == 1) return 'one'; - else if (c == 2) return 'two'; - else return 'other'; -}; - -export const none = _ => 'other'; - -export const other = c => { - return c == 1 ? 'one' : 'other'; -}; - - */ diff --git a/modules/web/src/main/ui/layout.scala b/modules/web/src/main/ui/layout.scala index 7fe3d7e96fa40..7c680dedc0735 100644 --- a/modules/web/src/main/ui/layout.scala +++ b/modules/web/src/main/ui/layout.scala @@ -10,7 +10,6 @@ import lila.ui.* import ScalatagsTemplate.{ *, given } final class layout(helpers: Helpers, assetHelper: lila.web.ui.AssetFullHelper)( - jsQuantity: Lang => String, isRTL: Lang => Boolean, popularAlternateLanguages: List[Language], reportScoreThreshold: () => ScoreThresholds, @@ -345,9 +344,6 @@ final class layout(helpers: Helpers, assetHelper: lila.web.ui.AssetFullHelper)( private def jsCode(using t: Translate) = cache.computeIfAbsent( t.lang, - _ => - "if (!window.site) window.site={};" + - """window.site.load=new Promise(r=>document.addEventListener("DOMContentLoaded",r));""" + - s"window.site.quantity=${jsQuantity(t.lang)};" + _ => """window.site={load:new Promise(r=>document.addEventListener("DOMContentLoaded",r))};""" ) end inlineJs diff --git a/ui/.build/src/i18n.ts b/ui/.build/src/i18n.ts index ae70e5a156812..f8fd0f3d860a2 100644 --- a/ui/.build/src/i18n.ts +++ b/ui/.build/src/i18n.ts @@ -16,53 +16,6 @@ let watchTimeout: NodeJS.Timeout | undefined; const i18nWatch: fs.FSWatcher[] = []; const isFormat = /%(?:[\d]\$)?s/; -const tsPrelude = `// Generated -interface I18nFormat { - (...args: (string | number)[]): string; // formatted - asArray: (...args: T[]) => (T | string)[]; // vdom -} -interface I18nPlural { - (quantity: number, ...args: (string | number)[]): string; // pluralSame - asArray: (quantity: number, ...args: T[]) => (T | string)[]; // vdomPlural / plural -} -interface I18n { - /** Global noarg key lookup (only if absolutely necessary). */ - (key: string): string;\n\n`; - -const jsPrelude = - '"use strict";(()=>{' + - ( - await transform( - // s(...) is the standard format function, p(...) is the plural format function. - // both have an asArray method for vdom. - `function p(t) { - let r = (n, ...e) => l(o(t, n), n, ...e).join(''); - return (r.asArray = (n, ...e) => l(o(t, n), ...e)), r; - } - function s(t) { - let r = (...n) => l(t, ...n).join(''); - return (r.asArray = (...n) => l(t, ...n)), r; - } - function o(t, n) { - return t[site.quantity(n)] || t.other || t.one || ''; - } - function l(t, ...r) { - let n = t.split(/(%(?:\\d\\$)?s)/); - if (r.length) { - let e = n.indexOf('%s'); - if (e !== -1) n[e] = r[0]; - else - for (let i = 0; i < r.length; i++) { - let s = n.indexOf('%' + (i + 1) + '$s'); - s !== -1 && (n[s] = r[i]); - } - } - return n; - }`, - { minify: true, loader: 'js' }, - ) - ).code; - export function stopI18n(): void { clearTimeout(watchTimeout); watchTimeout = undefined; @@ -177,15 +130,18 @@ async function writeJavascript(cat: string, locale?: string, xstat: fs.Stats | f .then(parseXml) : []), ]); + const lang = locale?.split('-')[0]; const jsInit = - cat === 'site' - ? 'window.i18n=function(k){for(let v of Object.values(window.i18n))if(v[k])return v[k];return k};' - : ''; + cat !== 'site' + ? '' + : siteInit + + 'window.i18n.quantity=' + + (jsQuantity.find(({ l }) => l.includes(lang ?? ''))?.q ?? `o=>o==1?'one':'other'`) + + ';'; const code = jsPrelude + jsInit + - `if(!window.i18n.${cat})window.i18n.${cat}={};` + - `let i=window.i18n.${cat};` + + `let i=window.i18n.${cat}={};` + [...translations] .map( ([k, v]) => @@ -244,3 +200,123 @@ function zip(arr1: T[], arr2: U[]): [T, U][] { } return result; } + +async function min(js: string): Promise { + return (await transform(js, { minify: true, loader: 'js' })).code; +} + +const tsPrelude = `// Generated +interface I18nFormat { + (...args: (string | number)[]): string; // formatted + asArray: (...args: T[]) => (T | string)[]; // vdom +} +interface I18nPlural { + (quantity: number, ...args: (string | number)[]): string; // pluralSame + asArray: (quantity: number, ...args: T[]) => (T | string)[]; // vdomPlural / plural +} +interface I18n { + /** Global noarg key lookup (only if absolutely necessary). */ + (key: string): string; + quantity: (count: number) => 'zero' | 'one' | 'two' | 'few' | 'many' | 'other';\n\n`; + +const jsPrelude = + '"use strict";(()=>{' + + (await min( + // s(...) is the standard format function, p(...) is the plural format function. + // both have an asArray method for vdom. + `function p(t) { + let r = (n, ...e) => l(o(t, n), n, ...e).join(''); + return (r.asArray = (n, ...e) => l(o(t, n), ...e)), r; + } + function s(t) { + let r = (...n) => l(t, ...n).join(''); + return (r.asArray = (...n) => l(t, ...n)), r; + } + function o(t, n) { + return t[i18n.quantity(n)] || t.other || t.one || ''; + } + function l(t, ...r) { + let n = t.split(/(%(?:\\d\\$)?s)/); + if (r.length) { + let e = n.indexOf('%s'); + if (e != -1) n[e] = r[0]; + else + for (let i = 0; i < r.length; i++) { + let s = n.indexOf('%' + (i + 1) + '$s'); + s != -1 && (n[s] = r[i]); + } + } + return n; + }`, + )); + +const siteInit = await min( + `window.i18n = function(k) { + for (let v of Object.values(window.i18n)) { + if (v[k]) return v[k]; + return k; + } + }`, +); + +const jsQuantity = [ + { + l: ['fr', 'ff', 'kab', 'co', 'ak', 'am', 'bh', 'fil', 'tl', 'guw', 'hi', 'ln', 'mg', 'nso', 'ti', 'wa'], + q: `o=>o<=1?"one":"other"`, // french + }, + { + l: ['cs', 'sk'], + q: `o=>1==o?"one":o>=2&&o<=4?"few":"other"`, // czech + }, + { + l: ['hr', 'ru', 'sr', 'uk', 'be', 'bs', 'sh', 'ry'], // balkan + q: `o=>{const e=o%100,t=o%10;return 1==t&&11!=e?"one":t>=2&&t<=4&&!(e>=12&&e<=14)?"few":0==t||t>=5&&t<=9||e>=11&&e<=14?"many":"other"}`, + }, + { + l: ['lv'], // latvian + q: `o=>0==o?"zero":o%10==1&&o%100!=11?"one":"other"`, + }, + { + l: ['lt'], // lithuanian + q: `o=>{const e=o%100,t=o%10;return 1!=t||e>=11&&e<=19?t>=2&&t<=9&&!(e>=11&&e<=19)?"few":"other":"one"}`, + }, + { + l: ['pl'], // polish + q: `o=>{const e=o%100,t=o%10;return 1==o?"one":t>=2&&t<=4&&!(e>=12&&e<=14)?"few":"other"}`, + }, + { + l: ['ro', 'mo'], // romanian + q: `o=>{const e=o%100;return 1==o?"one":0==o||e>=1&&e<=19?"few":"other"}`, + }, + { + l: ['sl'], // slovenian + q: `o=>{const e=o%100;return 1==e?"one":2==e?"two":e>=3&&e<=4?"few":"other"}`, + }, + { + l: ['ar'], // arabic + q: `o=>{const e=o%100;return 0==o?"zero":1==o?"one":2==o?"two":e>=3&&e<=10?"few":e>=11&&e<=99?"many":"other"}`, + }, + { + l: ['mk'], // macedonian + q: `o=>o%10==1&&11!=o?"one":"other"`, + }, + { + l: ['cy', 'br'], // welsh + q: `o=>0==o?"zero":1==o?"one":2==o?"two":3==o?"few":6==o?"many":"other"`, + }, + { + l: ['mt'], // maltese + q: `o=>{const e=o%100;return 1==o?"one":0==o||e>=2&&e<=10?"few":e>=11&&e<=19?"many":"other"}`, + }, + { + l: ['ga', 'se', 'sma', 'smi', 'smj', 'smn', 'sms'], + q: `o=>1==o?"one":2==o?"two":"other"`, + }, + { + l: [ + ...['az', 'bm', 'fa', 'ig', 'hu', 'ja', 'kde', 'kea', 'ko', 'my', 'ses', 'sg', 'to', 'tr', 'vi', 'wo'], + ...['yo', 'zh', 'bo', 'dz', 'id', 'jv', 'ka', 'km', 'kn', 'ms', 'th', 'tp', 'io', 'ia'], + ], + q: `o=>"other"`, + }, +]; diff --git a/ui/@types/lichess/i18n.d.ts b/ui/@types/lichess/i18n.d.ts index 118ad8e76e85e..df04a1f3fb761 100644 --- a/ui/@types/lichess/i18n.d.ts +++ b/ui/@types/lichess/i18n.d.ts @@ -10,6 +10,7 @@ interface I18nPlural { interface I18n { /** Global noarg key lookup (only if absolutely necessary). */ (key: string): string; + quantity: (count: number) => 'zero' | 'one' | 'two' | 'few' | 'many' | 'other'; activity: { /** Activity */ diff --git a/ui/site/src/site.ts b/ui/site/src/site.ts index 7bd5074742a14..79dac80afa1b8 100644 --- a/ui/site/src/site.ts +++ b/ui/site/src/site.ts @@ -11,10 +11,10 @@ import { pubsub } from 'common/pubsub'; const site = window.site; (site as any).pubsub = pubsub; // do not declare in index.d.ts. some extensions need this here -// site.load, site.quantity, site.siteI18n are initialized in layout.scala embedded script tags -// site.manifest is fetched immediately from the server +// site.load is initialized in layout.scala embedded script tags +// site.manifest is fetched // site.info, site.debug are populated by ui/build -// site.socket, site.quietMode, site.analysis are set elsewhere but available here +// site.socket, site.quietMode, site.analysis are set elsewhere site.sri = randomToken(); site.displayLocale = displayLocale; site.blindMode = document.body.classList.contains('blind-mode');