diff options
| author | rxliuli <rxliuli@gmail.com> | 2025-11-04 05:03:50 +0800 |
|---|---|---|
| committer | rxliuli <rxliuli@gmail.com> | 2025-11-04 05:03:50 +0800 |
| commit | bce557cc2dc767628bed6aac87301a1be7c5431b (patch) | |
| tree | b51a051228d01fe3306cd7626d4a96768aadb944 /shared/localization | |
init commit
Diffstat (limited to 'shared/localization')
| -rw-r--r-- | shared/localization/node_modules/make-plural/cardinals.mjs | 458 | ||||
| -rw-r--r-- | shared/localization/src/getLocAttributes.ts | 78 | ||||
| -rw-r--r-- | shared/localization/src/getPageDir.ts | 40 | ||||
| -rw-r--r-- | shared/localization/src/i18n.ts | 104 | ||||
| -rw-r--r-- | shared/localization/src/setHTMLAttributes.ts | 15 | ||||
| -rw-r--r-- | shared/localization/src/translator.ts | 174 |
6 files changed, 869 insertions, 0 deletions
diff --git a/shared/localization/node_modules/make-plural/cardinals.mjs b/shared/localization/node_modules/make-plural/cardinals.mjs new file mode 100644 index 0000000..1484b3a --- /dev/null +++ b/shared/localization/node_modules/make-plural/cardinals.mjs @@ -0,0 +1,458 @@ +function a(n) { + return n == 1 ? 'one' : 'other'; +} +function b(n) { + return (n == 0 || n == 1) ? 'one' : 'other'; +} +function c(n) { + return n >= 0 && n <= 1 ? 'one' : 'other'; +} +function d(n) { + var s = String(n).split('.'), v0 = !s[1]; + return n == 1 && v0 ? 'one' : 'other'; +} +function e(n) { + return 'other'; +} +function f(n) { + return n == 1 ? 'one' + : n == 2 ? 'two' + : 'other'; +} + +export const _in = e; +export const af = a; +export const ak = b; +export const am = c; +export const an = a; +export function ar(n) { + var s = String(n).split('.'), t0 = Number(s[0]) == n, n100 = t0 && s[0].slice(-2); + return n == 0 ? 'zero' + : n == 1 ? 'one' + : n == 2 ? 'two' + : (n100 >= 3 && n100 <= 10) ? 'few' + : (n100 >= 11 && n100 <= 99) ? 'many' + : 'other'; +} +export function ars(n) { + var s = String(n).split('.'), t0 = Number(s[0]) == n, n100 = t0 && s[0].slice(-2); + return n == 0 ? 'zero' + : n == 1 ? 'one' + : n == 2 ? 'two' + : (n100 >= 3 && n100 <= 10) ? 'few' + : (n100 >= 11 && n100 <= 99) ? 'many' + : 'other'; +} +export const as = c; +export const asa = a; +export const ast = d; +export const az = a; +export function be(n) { + var s = String(n).split('.'), t0 = Number(s[0]) == n, n10 = t0 && s[0].slice(-1), n100 = t0 && s[0].slice(-2); + return n10 == 1 && n100 != 11 ? 'one' + : (n10 >= 2 && n10 <= 4) && (n100 < 12 || n100 > 14) ? 'few' + : t0 && n10 == 0 || (n10 >= 5 && n10 <= 9) || (n100 >= 11 && n100 <= 14) ? 'many' + : 'other'; +} +export const bem = a; +export const bez = a; +export const bg = a; +export const bho = b; +export const bm = e; +export const bn = c; +export const bo = e; +export function br(n) { + var s = String(n).split('.'), t0 = Number(s[0]) == n, n10 = t0 && s[0].slice(-1), n100 = t0 && s[0].slice(-2), n1000000 = t0 && s[0].slice(-6); + return n10 == 1 && n100 != 11 && n100 != 71 && n100 != 91 ? 'one' + : n10 == 2 && n100 != 12 && n100 != 72 && n100 != 92 ? 'two' + : ((n10 == 3 || n10 == 4) || n10 == 9) && (n100 < 10 || n100 > 19) && (n100 < 70 || n100 > 79) && (n100 < 90 || n100 > 99) ? 'few' + : n != 0 && t0 && n1000000 == 0 ? 'many' + : 'other'; +} +export const brx = a; +export function bs(n) { + var s = String(n).split('.'), i = s[0], f = s[1] || '', v0 = !s[1], i10 = i.slice(-1), i100 = i.slice(-2), f10 = f.slice(-1), f100 = f.slice(-2); + return v0 && i10 == 1 && i100 != 11 || f10 == 1 && f100 != 11 ? 'one' + : v0 && (i10 >= 2 && i10 <= 4) && (i100 < 12 || i100 > 14) || (f10 >= 2 && f10 <= 4) && (f100 < 12 || f100 > 14) ? 'few' + : 'other'; +} +export const ca = d; +export const ce = a; +export function ceb(n) { + var s = String(n).split('.'), i = s[0], f = s[1] || '', v0 = !s[1], i10 = i.slice(-1), f10 = f.slice(-1); + return v0 && (i == 1 || i == 2 || i == 3) || v0 && i10 != 4 && i10 != 6 && i10 != 9 || !v0 && f10 != 4 && f10 != 6 && f10 != 9 ? 'one' : 'other'; +} +export const cgg = a; +export const chr = a; +export const ckb = a; +export function cs(n) { + var s = String(n).split('.'), i = s[0], v0 = !s[1]; + return n == 1 && v0 ? 'one' + : (i >= 2 && i <= 4) && v0 ? 'few' + : !v0 ? 'many' + : 'other'; +} +export function cy(n) { + return n == 0 ? 'zero' + : n == 1 ? 'one' + : n == 2 ? 'two' + : n == 3 ? 'few' + : n == 6 ? 'many' + : 'other'; +} +export function da(n) { + var s = String(n).split('.'), i = s[0], t0 = Number(s[0]) == n; + return n == 1 || !t0 && (i == 0 || i == 1) ? 'one' : 'other'; +} +export const de = d; +export function dsb(n) { + var s = String(n).split('.'), i = s[0], f = s[1] || '', v0 = !s[1], i100 = i.slice(-2), f100 = f.slice(-2); + return v0 && i100 == 1 || f100 == 1 ? 'one' + : v0 && i100 == 2 || f100 == 2 ? 'two' + : v0 && (i100 == 3 || i100 == 4) || (f100 == 3 || f100 == 4) ? 'few' + : 'other'; +} +export const dv = a; +export const dz = e; +export const ee = a; +export const el = a; +export const en = d; +export const eo = a; +export const es = a; +export const et = d; +export const eu = a; +export const fa = c; +export function ff(n) { + return n >= 0 && n < 2 ? 'one' : 'other'; +} +export const fi = d; +export function fil(n) { + var s = String(n).split('.'), i = s[0], f = s[1] || '', v0 = !s[1], i10 = i.slice(-1), f10 = f.slice(-1); + return v0 && (i == 1 || i == 2 || i == 3) || v0 && i10 != 4 && i10 != 6 && i10 != 9 || !v0 && f10 != 4 && f10 != 6 && f10 != 9 ? 'one' : 'other'; +} +export const fo = a; +export function fr(n) { + return n >= 0 && n < 2 ? 'one' : 'other'; +} +export const fur = a; +export const fy = d; +export function ga(n) { + var s = String(n).split('.'), t0 = Number(s[0]) == n; + return n == 1 ? 'one' + : n == 2 ? 'two' + : (t0 && n >= 3 && n <= 6) ? 'few' + : (t0 && n >= 7 && n <= 10) ? 'many' + : 'other'; +} +export function gd(n) { + var s = String(n).split('.'), t0 = Number(s[0]) == n; + return (n == 1 || n == 11) ? 'one' + : (n == 2 || n == 12) ? 'two' + : ((t0 && n >= 3 && n <= 10) || (t0 && n >= 13 && n <= 19)) ? 'few' + : 'other'; +} +export const gl = d; +export const gsw = a; +export const gu = c; +export const guw = b; +export function gv(n) { + var s = String(n).split('.'), i = s[0], v0 = !s[1], i10 = i.slice(-1), i100 = i.slice(-2); + return v0 && i10 == 1 ? 'one' + : v0 && i10 == 2 ? 'two' + : v0 && (i100 == 0 || i100 == 20 || i100 == 40 || i100 == 60 || i100 == 80) ? 'few' + : !v0 ? 'many' + : 'other'; +} +export const ha = a; +export const haw = a; +export function he(n) { + var s = String(n).split('.'), i = s[0], v0 = !s[1], t0 = Number(s[0]) == n, n10 = t0 && s[0].slice(-1); + return n == 1 && v0 ? 'one' + : i == 2 && v0 ? 'two' + : v0 && (n < 0 || n > 10) && t0 && n10 == 0 ? 'many' + : 'other'; +} +export const hi = c; +export function hr(n) { + var s = String(n).split('.'), i = s[0], f = s[1] || '', v0 = !s[1], i10 = i.slice(-1), i100 = i.slice(-2), f10 = f.slice(-1), f100 = f.slice(-2); + return v0 && i10 == 1 && i100 != 11 || f10 == 1 && f100 != 11 ? 'one' + : v0 && (i10 >= 2 && i10 <= 4) && (i100 < 12 || i100 > 14) || (f10 >= 2 && f10 <= 4) && (f100 < 12 || f100 > 14) ? 'few' + : 'other'; +} +export function hsb(n) { + var s = String(n).split('.'), i = s[0], f = s[1] || '', v0 = !s[1], i100 = i.slice(-2), f100 = f.slice(-2); + return v0 && i100 == 1 || f100 == 1 ? 'one' + : v0 && i100 == 2 || f100 == 2 ? 'two' + : v0 && (i100 == 3 || i100 == 4) || (f100 == 3 || f100 == 4) ? 'few' + : 'other'; +} +export const hu = a; +export function hy(n) { + return n >= 0 && n < 2 ? 'one' : 'other'; +} +export const ia = d; +export const id = e; +export const ig = e; +export const ii = e; +export const io = d; +export function is(n) { + var s = String(n).split('.'), i = s[0], t0 = Number(s[0]) == n, i10 = i.slice(-1), i100 = i.slice(-2); + return t0 && i10 == 1 && i100 != 11 || !t0 ? 'one' : 'other'; +} +export const it = d; +export const iu = f; +export function iw(n) { + var s = String(n).split('.'), i = s[0], v0 = !s[1], t0 = Number(s[0]) == n, n10 = t0 && s[0].slice(-1); + return n == 1 && v0 ? 'one' + : i == 2 && v0 ? 'two' + : v0 && (n < 0 || n > 10) && t0 && n10 == 0 ? 'many' + : 'other'; +} +export const ja = e; +export const jbo = e; +export const jgo = a; +export const ji = d; +export const jmc = a; +export const jv = e; +export const jw = e; +export const ka = a; +export function kab(n) { + return n >= 0 && n < 2 ? 'one' : 'other'; +} +export const kaj = a; +export const kcg = a; +export const kde = e; +export const kea = e; +export const kk = a; +export const kkj = a; +export const kl = a; +export const km = e; +export const kn = c; +export const ko = e; +export const ks = a; +export const ksb = a; +export function ksh(n) { + return n == 0 ? 'zero' + : n == 1 ? 'one' + : 'other'; +} +export const ku = a; +export function kw(n) { + var s = String(n).split('.'), t0 = Number(s[0]) == n, n100 = t0 && s[0].slice(-2), n1000 = t0 && s[0].slice(-3), n100000 = t0 && s[0].slice(-5), n1000000 = t0 && s[0].slice(-6); + return n == 0 ? 'zero' + : n == 1 ? 'one' + : (n100 == 2 || n100 == 22 || n100 == 42 || n100 == 62 || n100 == 82) || t0 && n1000 == 0 && ((n100000 >= 1000 && n100000 <= 20000) || n100000 == 40000 || n100000 == 60000 || n100000 == 80000) || n != 0 && n1000000 == 100000 ? 'two' + : (n100 == 3 || n100 == 23 || n100 == 43 || n100 == 63 || n100 == 83) ? 'few' + : n != 1 && (n100 == 1 || n100 == 21 || n100 == 41 || n100 == 61 || n100 == 81) ? 'many' + : 'other'; +} +export const ky = a; +export function lag(n) { + var s = String(n).split('.'), i = s[0]; + return n == 0 ? 'zero' + : (i == 0 || i == 1) && n != 0 ? 'one' + : 'other'; +} +export const lb = a; +export const lg = a; +export const lkt = e; +export const ln = b; +export const lo = e; +export function lt(n) { + var s = String(n).split('.'), f = s[1] || '', t0 = Number(s[0]) == n, n10 = t0 && s[0].slice(-1), n100 = t0 && s[0].slice(-2); + return n10 == 1 && (n100 < 11 || n100 > 19) ? 'one' + : (n10 >= 2 && n10 <= 9) && (n100 < 11 || n100 > 19) ? 'few' + : f != 0 ? 'many' + : 'other'; +} +export function lv(n) { + var s = String(n).split('.'), f = s[1] || '', v = f.length, t0 = Number(s[0]) == n, n10 = t0 && s[0].slice(-1), n100 = t0 && s[0].slice(-2), f100 = f.slice(-2), f10 = f.slice(-1); + return t0 && n10 == 0 || (n100 >= 11 && n100 <= 19) || v == 2 && (f100 >= 11 && f100 <= 19) ? 'zero' + : n10 == 1 && n100 != 11 || v == 2 && f10 == 1 && f100 != 11 || v != 2 && f10 == 1 ? 'one' + : 'other'; +} +export const mas = a; +export const mg = b; +export const mgo = a; +export function mk(n) { + var s = String(n).split('.'), i = s[0], f = s[1] || '', v0 = !s[1], i10 = i.slice(-1), i100 = i.slice(-2), f10 = f.slice(-1), f100 = f.slice(-2); + return v0 && i10 == 1 && i100 != 11 || f10 == 1 && f100 != 11 ? 'one' : 'other'; +} +export const ml = a; +export const mn = a; +export function mo(n) { + var s = String(n).split('.'), v0 = !s[1], t0 = Number(s[0]) == n, n100 = t0 && s[0].slice(-2); + return n == 1 && v0 ? 'one' + : !v0 || n == 0 || (n100 >= 2 && n100 <= 19) ? 'few' + : 'other'; +} +export const mr = a; +export const ms = e; +export function mt(n) { + var s = String(n).split('.'), t0 = Number(s[0]) == n, n100 = t0 && s[0].slice(-2); + return n == 1 ? 'one' + : n == 0 || (n100 >= 2 && n100 <= 10) ? 'few' + : (n100 >= 11 && n100 <= 19) ? 'many' + : 'other'; +} +export const my = e; +export const nah = a; +export const naq = f; +export const nb = a; +export const nd = a; +export const ne = a; +export const nl = d; +export const nn = a; +export const nnh = a; +export const no = a; +export const nqo = e; +export const nr = a; +export const nso = b; +export const ny = a; +export const nyn = a; +export const om = a; +export const or = a; +export const os = a; +export const osa = e; +export const pa = b; +export const pap = a; +export function pl(n) { + var s = String(n).split('.'), i = s[0], v0 = !s[1], i10 = i.slice(-1), i100 = i.slice(-2); + return n == 1 && v0 ? 'one' + : v0 && (i10 >= 2 && i10 <= 4) && (i100 < 12 || i100 > 14) ? 'few' + : v0 && i != 1 && (i10 == 0 || i10 == 1) || v0 && (i10 >= 5 && i10 <= 9) || v0 && (i100 >= 12 && i100 <= 14) ? 'many' + : 'other'; +} +export function prg(n) { + var s = String(n).split('.'), f = s[1] || '', v = f.length, t0 = Number(s[0]) == n, n10 = t0 && s[0].slice(-1), n100 = t0 && s[0].slice(-2), f100 = f.slice(-2), f10 = f.slice(-1); + return t0 && n10 == 0 || (n100 >= 11 && n100 <= 19) || v == 2 && (f100 >= 11 && f100 <= 19) ? 'zero' + : n10 == 1 && n100 != 11 || v == 2 && f10 == 1 && f100 != 11 || v != 2 && f10 == 1 ? 'one' + : 'other'; +} +export const ps = a; +export function pt(n) { + var s = String(n).split('.'), i = s[0]; + return (i == 0 || i == 1) ? 'one' : 'other'; +} +export const pt_PT = d; +export const rm = a; +export function ro(n) { + var s = String(n).split('.'), v0 = !s[1], t0 = Number(s[0]) == n, n100 = t0 && s[0].slice(-2); + return n == 1 && v0 ? 'one' + : !v0 || n == 0 || (n100 >= 2 && n100 <= 19) ? 'few' + : 'other'; +} +export const rof = a; +export const root = e; +export function ru(n) { + var s = String(n).split('.'), i = s[0], v0 = !s[1], i10 = i.slice(-1), i100 = i.slice(-2); + return v0 && i10 == 1 && i100 != 11 ? 'one' + : v0 && (i10 >= 2 && i10 <= 4) && (i100 < 12 || i100 > 14) ? 'few' + : v0 && i10 == 0 || v0 && (i10 >= 5 && i10 <= 9) || v0 && (i100 >= 11 && i100 <= 14) ? 'many' + : 'other'; +} +export const rwk = a; +export const sah = e; +export const saq = a; +export const sc = d; +export const scn = d; +export const sd = a; +export const sdh = a; +export const se = f; +export const seh = a; +export const ses = e; +export const sg = e; +export function sh(n) { + var s = String(n).split('.'), i = s[0], f = s[1] || '', v0 = !s[1], i10 = i.slice(-1), i100 = i.slice(-2), f10 = f.slice(-1), f100 = f.slice(-2); + return v0 && i10 == 1 && i100 != 11 || f10 == 1 && f100 != 11 ? 'one' + : v0 && (i10 >= 2 && i10 <= 4) && (i100 < 12 || i100 > 14) || (f10 >= 2 && f10 <= 4) && (f100 < 12 || f100 > 14) ? 'few' + : 'other'; +} +export function shi(n) { + var s = String(n).split('.'), t0 = Number(s[0]) == n; + return n >= 0 && n <= 1 ? 'one' + : (t0 && n >= 2 && n <= 10) ? 'few' + : 'other'; +} +export function si(n) { + var s = String(n).split('.'), i = s[0], f = s[1] || ''; + return (n == 0 || n == 1) || i == 0 && f == 1 ? 'one' : 'other'; +} +export function sk(n) { + var s = String(n).split('.'), i = s[0], v0 = !s[1]; + return n == 1 && v0 ? 'one' + : (i >= 2 && i <= 4) && v0 ? 'few' + : !v0 ? 'many' + : 'other'; +} +export function sl(n) { + var s = String(n).split('.'), i = s[0], v0 = !s[1], i100 = i.slice(-2); + return v0 && i100 == 1 ? 'one' + : v0 && i100 == 2 ? 'two' + : v0 && (i100 == 3 || i100 == 4) || !v0 ? 'few' + : 'other'; +} +export const sma = f; +export const smi = f; +export const smj = f; +export const smn = f; +export const sms = f; +export const sn = a; +export const so = a; +export const sq = a; +export function sr(n) { + var s = String(n).split('.'), i = s[0], f = s[1] || '', v0 = !s[1], i10 = i.slice(-1), i100 = i.slice(-2), f10 = f.slice(-1), f100 = f.slice(-2); + return v0 && i10 == 1 && i100 != 11 || f10 == 1 && f100 != 11 ? 'one' + : v0 && (i10 >= 2 && i10 <= 4) && (i100 < 12 || i100 > 14) || (f10 >= 2 && f10 <= 4) && (f100 < 12 || f100 > 14) ? 'few' + : 'other'; +} +export const ss = a; +export const ssy = a; +export const st = a; +export const su = e; +export const sv = d; +export const sw = d; +export const syr = a; +export const ta = a; +export const te = a; +export const teo = a; +export const th = e; +export const ti = b; +export const tig = a; +export const tk = a; +export function tl(n) { + var s = String(n).split('.'), i = s[0], f = s[1] || '', v0 = !s[1], i10 = i.slice(-1), f10 = f.slice(-1); + return v0 && (i == 1 || i == 2 || i == 3) || v0 && i10 != 4 && i10 != 6 && i10 != 9 || !v0 && f10 != 4 && f10 != 6 && f10 != 9 ? 'one' : 'other'; +} +export const tn = a; +export const to = e; +export const tr = a; +export const ts = a; +export function tzm(n) { + var s = String(n).split('.'), t0 = Number(s[0]) == n; + return (n == 0 || n == 1) || (t0 && n >= 11 && n <= 99) ? 'one' : 'other'; +} +export const ug = a; +export function uk(n) { + var s = String(n).split('.'), i = s[0], v0 = !s[1], i10 = i.slice(-1), i100 = i.slice(-2); + return v0 && i10 == 1 && i100 != 11 ? 'one' + : v0 && (i10 >= 2 && i10 <= 4) && (i100 < 12 || i100 > 14) ? 'few' + : v0 && i10 == 0 || v0 && (i10 >= 5 && i10 <= 9) || v0 && (i100 >= 11 && i100 <= 14) ? 'many' + : 'other'; +} +export const ur = d; +export const uz = a; +export const ve = a; +export const vi = e; +export const vo = a; +export const vun = a; +export const wa = b; +export const wae = a; +export const wo = e; +export const xh = a; +export const xog = a; +export const yi = d; +export const yo = e; +export const yue = e; +export const zh = e; +export const zu = c; diff --git a/shared/localization/src/getLocAttributes.ts b/shared/localization/src/getLocAttributes.ts new file mode 100644 index 0000000..2f462db --- /dev/null +++ b/shared/localization/src/getLocAttributes.ts @@ -0,0 +1,78 @@ +import { getPageDir } from './getPageDir'; + +/** + * Checks if a string contains language script + * ex. "zh-Hant-HK", "zh-Hant-TW", "zh-Hans-CN" + * @param {string} locale + * @returns {boolean} + */ +const hasSupportedLanguageScript = (locale: string): boolean => { + const SUPPORTED_SCRIPTS = ['-hans-', '-hant-']; + + const formattedLocale = locale.toLowerCase(); + return SUPPORTED_SCRIPTS.some((item) => formattedLocale.includes(item)); +}; + +/** + * + * BCP47 https://www.w3.org/International/articles/language-tags/ + * + * @param {string} language https://en.wikipedia.org/wiki/ISO_639 + * @param {string} region https://en.wikipedia.org/wiki/ISO_3166-1 + * @param {string} script https://en.wikipedia.org/wiki/ISO_15924 + + */ +const buildBcp47String = ( + language: string, + region: string, + script?: string, +): string => { + let capitalizeScript: string | null = null; + if (script) { + capitalizeScript = + script[0].toUpperCase() + script.substring(1).toLowerCase(); + } + let bcp47Arr = [ + language.toLowerCase(), + capitalizeScript, + region.toUpperCase(), + ]; + + return bcp47Arr.filter((item) => item !== null).join('-'); +}; + +/** + * @description + * get values to be used in <html> tag lang and dir attributes. + * + * @param {string} locale + * @returns { { dir: 'rtl' | 'ltr', lang: string }} HTML dir + lang values + */ + +export function getLocAttributes(locale: string): { + dir: 'rtl' | 'ltr'; + lang: string; +} { + const pageDir = getPageDir(locale); + let bcp47 = locale; + + const localeStrings = locale.split('-'); + + // region index in array + const regionIndex = hasSupportedLanguageScript(locale) ? 2 : 1; + + const language = localeStrings[0]; + const script = hasSupportedLanguageScript(locale) + ? localeStrings[1] + : undefined; + const region = localeStrings[regionIndex]; + + if (language && region) { + bcp47 = buildBcp47String(language, region, script); + } + + return { + dir: pageDir, + lang: bcp47, + }; +} diff --git a/shared/localization/src/getPageDir.ts b/shared/localization/src/getPageDir.ts new file mode 100644 index 0000000..47b855d --- /dev/null +++ b/shared/localization/src/getPageDir.ts @@ -0,0 +1,40 @@ +/** + * TODO: rdar://73010072 (Make localization utils its own package) + * Copied from: + * https://github.pie.apple.com/amp-ui/desktop-music-app/blob/main/app/utils/page-dir.js + */ + +// these overrides were determined to always show page in RTL, even if the global elements dont contain +// an he_il entry +// <rdar://problem/49297213> LOC: IW-IL: RTL: Web Preview Pages: The Preview Pages are not RTL. +const RTL_LANG_CODES_OVERRIDE = [ + 'he', // hebrew +]; + +const RTL_LANG_CODES = [ + 'ar', // arabic + 'he', // hebrew + 'ku', // kurdish + 'ur', // urdu + 'ps', // pashto + 'yi', // yiddish +]; + +/** + * Determine the page-direction for a given locale + * + * @param {String} localeCode - A string containing a language code and region code separated by a hyphen. + * @param {String|undefined|null} langParam - A language code passed from the `l=` query param. + */ +export function getPageDir( + localeCode: string, + langParam: string | undefined | null = null, +) { + const twoLettersLangCode = localeCode.split('-')[0]; + const isRTLLang = RTL_LANG_CODES.includes(twoLettersLangCode); + const isRTLLangOverride = + typeof langParam === 'string' && + RTL_LANG_CODES_OVERRIDE.includes(langParam); + + return isRTLLang || isRTLLangOverride ? 'rtl' : 'ltr'; +} diff --git a/shared/localization/src/i18n.ts b/shared/localization/src/i18n.ts new file mode 100644 index 0000000..bcd5e28 --- /dev/null +++ b/shared/localization/src/i18n.ts @@ -0,0 +1,104 @@ +import Translator from './translator'; +import type { + Locale, + InterpolationOptions, + ILocaleJSON, + ITranslator, +} from './types'; +import type { Logger } from '@amp/web-apps-logger'; + +/** @internal */ +const formatOptions = ( + options: InterpolationOptions | number, +): InterpolationOptions => + typeof options === 'number' ? { count: options } : options; + +/** + * + * Adapter class to expose expected LOC interface + * @category Localization + */ +export class I18N { + private readonly log: Logger; + private readonly locale: Locale; + private readonly translator: ITranslator; + private readonly keys: ILocaleJSON; + private readonly alwaysShowScreamers: boolean; + + /** + * builds a new I18N class + * @param locale - the locale to use default:`'en-us'` + * @param translation - translation object default: `{}` + * @param alwaysShowScreamers - optional boolean that is set upstream + * by a FeatureKit feature flag. This makes it so the LOC keys themselves are + * printed to the DOM, rather than their translations, which is helpful for QA testing + */ + constructor( + log: Logger, + locale: Locale = 'en-us', + translation: ILocaleJSON = {}, + alwaysShowScreamers: boolean = false, + ) { + this.log = log; + this.locale = locale; + this.translator = new Translator(locale, translation, { + onMissingKeyFn: (key: string): string => { + log.warn('key missing:', key); + return `**${key}**`; + }, + onMissingInterpolationFn: (key: string, interpolation: string) => { + log.warn(`key ${key} missing interpolation:`, interpolation); + }, + }); + this.keys = translation; + this.alwaysShowScreamers = alwaysShowScreamers; + } + + get currentLocale(): Locale { + return this.locale; + } + + get currentKeys(): ILocaleJSON { + return this.keys; + } + + /** + * Gets non-interpolated string. + * @category Localization + * @param key key to lookup in the translation.json + * @returns an uninterpolated string value + */ + getUninterpolatedString(key: string): string { + if (this.alwaysShowScreamers) { + return key; + } else { + return this.translator.getUninterpolatedString(key); + } + } + + /** + * Method for fetching translation based on key. + * + * If alwaysShowScreamers is true, return the key itself for QA testing purposes + * (our app tends to call into this method within Svelte templates) + * + * @category Localization + * @param key key to lookup in the translation.json + * @param options options for translations + * @returns interpolated string + */ + t = (key: string, options: number | InterpolationOptions = {}): string => { + if (this.alwaysShowScreamers) { + return key; + } + + let internalOptions: InterpolationOptions = formatOptions(options); + if (typeof key !== 'string') { + this.log.warn('received non-string key:', key); + return ''; + } + return this.translator.translate(key, internalOptions); + }; +} + +export default I18N; diff --git a/shared/localization/src/setHTMLAttributes.ts b/shared/localization/src/setHTMLAttributes.ts new file mode 100644 index 0000000..3bc0725 --- /dev/null +++ b/shared/localization/src/setHTMLAttributes.ts @@ -0,0 +1,15 @@ +import { getLocAttributes } from './getLocAttributes'; + +/** + * sets Language attributes to HTML tag. + * @param {string} language + * @returns {void} + */ +export function setHTMLAttributes(language: string): void { + if (typeof window === 'undefined') return; + const attributes = getLocAttributes(language); + + for (let [attribute, value] of Object.entries(attributes)) { + window.document.documentElement.setAttribute(attribute, value); + } +} diff --git a/shared/localization/src/translator.ts b/shared/localization/src/translator.ts new file mode 100644 index 0000000..48b901f --- /dev/null +++ b/shared/localization/src/translator.ts @@ -0,0 +1,174 @@ +//TODO: rdar://73157363 (Limit loc plural functions to only use supported locales) +import * as cardinals from 'make-plural/cardinals'; +import type { + Locale, + ILocaleJSON, + InterpolationOptions, + TranslatorOptions, + ImissingInterpolationFn, + ImissingKeyFn, + ITranslator, +} from './types'; + +const DEFAULT_MISSING_FN: ImissingKeyFn = (key: string): string => `**${key}**`; +const DEFAULT_INTERPOLATION_REGEX: RegExp = /@@(.*?)@@/g; + +/** + * Interpolates string and returns result. + * @category Localization + * @param phrase phrase to be interpolated ex. ```"hello my name is @@name@@" ``` + * @param options object containing values to subsitute ex. ``` { name: "Joe" } ``` + * @param onMissingInterpolationFn callback to be called if options object does not contain a value for the interpolation schema + * + * @returns translated string ex ``` "hello my name is Joe" ``` + */ +export function interpolateString( + key: string, + phrase: string, + options: InterpolationOptions, + onMissingInterpolationFn: ImissingInterpolationFn | null, + locale: Locale, +): string { + const result = phrase.replace( + DEFAULT_INTERPOLATION_REGEX, + function (expression: string, argument: string) { + const optionHasProperty = options.hasOwnProperty(argument); + const optionType = typeof options[argument]; + const argumentIsUndefined = optionType === 'undefined'; + const argumentIsValid = + optionType === 'string' || optionType === 'number'; + let value: string = expression; + if (optionHasProperty && argumentIsValid) { + let validValue: string | number = options[argument]; + if ( + optionType === 'number' && + options.hasOwnProperty('count') + ) { + validValue = (validValue as number).toLocaleString([ + locale, + 'en-US', + ]); + } + value = validValue as string; + } else if (onMissingInterpolationFn && argumentIsUndefined) { + onMissingInterpolationFn(key, value); + } + return value; + }, + ); + + return result; +} + +type Cardinal = (n: number | string) => cardinals.PluralCategory; + +function getCardinal(selectedLang: string): Cardinal | undefined { + // @ts-ignore-error TypeScript does not allow us to index into a namespace dynamically + return cardinals[selectedLang]; +} + +/** + * TODO: rdar://73157363 (Limit loc plural functions to only use supported locales) + * Used to select the locale specific cardinal plural form key. + * @category Localization + * @param count number to determine the cardinal value + * @param key base key + * @param locale to lookup plural + * + * Reference: + * https://confluence.sd.apple.com/pages/viewpage.action?spaceKey=ASL&title=Pluralization+Rules + * + * @returns key + correct plural ex. ```[key].[ 'zero' | 'one' | 'two' | 'few' | 'many' | 'other'] ``` + */ + +export const getPlural = ( + count: number, + key: string, + locale: Locale, +): string => { + const lang = locale.split('-')[0]; + + // use english plural for dev strings + const selectedLang = lang === 'dev' ? 'en' : lang; + const cardinal = getCardinal(selectedLang); + + let plural: cardinals.PluralCategory | null = null; + if (cardinal) { + plural = cardinal(count); + // TODO: rdar://93665757 (JMOTW: investigate where to use 'few' and 'many' loc keys) + if (plural === 'few' || plural === 'many') plural = 'other'; + } + return plural ? `${key}.${plural}` : key; +}; + +/** + * Class that manages translations, plural rules, + * and interpolation for a single locale. + * @category Localization + */ +class Translator implements ITranslator { + private translationMap: Map<string, string>; + private locale: Locale; + private onMissingKeyFn: ImissingKeyFn; + private onMissingInterpolationFn: ImissingInterpolationFn | null; + constructor( + locale: Locale, + phrases: ILocaleJSON, + options: TranslatorOptions = {}, + ) { + const { + onMissingKeyFn = DEFAULT_MISSING_FN, + onMissingInterpolationFn = null, + } = options; + this.locale = locale; + this.translationMap = new Map(Object.entries(phrases)); + this.onMissingKeyFn = onMissingKeyFn; + this.onMissingInterpolationFn = onMissingInterpolationFn; + } + + /** + * Gets the correct value from the translation map. + * @category Localization + * @param key used to look up the value + */ + private getValue(key: string): string | null { + return this.translationMap.get(key) || null; + } + /** + * Gets an uniterpolated value of key. + * @category Localization + * @param key used to look up the value + */ + getUninterpolatedString(key: string) { + const keyValue = this.getValue(key); + return keyValue ? keyValue : this.onMissingKeyFn(key); + } + /** + * Translate string based on translation map, plural rules interpolates values. + * @category Localization + * @param key used to look up the value + * @param options used for interpolation + * @returns translated string + */ + translate(key: string, options: InterpolationOptions = {}): string { + let internalKey = key; + const { count } = options; + + if (count && !isNaN(count)) { + internalKey = getPlural(count, key, this.locale); + } + + const keyValue = this.getValue(internalKey); + return keyValue + ? interpolateString( + internalKey, + keyValue, + options, + this.onMissingInterpolationFn, + this.locale, + ) + : this.onMissingKeyFn(internalKey); + } +} + +export default Translator; |
