summaryrefslogtreecommitdiff
path: root/shared/localization
diff options
context:
space:
mode:
authorrxliuli <rxliuli@gmail.com>2025-11-04 05:03:50 +0800
committerrxliuli <rxliuli@gmail.com>2025-11-04 05:03:50 +0800
commitbce557cc2dc767628bed6aac87301a1be7c5431b (patch)
treeb51a051228d01fe3306cd7626d4a96768aadb944 /shared/localization
init commit
Diffstat (limited to 'shared/localization')
-rw-r--r--shared/localization/node_modules/make-plural/cardinals.mjs458
-rw-r--r--shared/localization/src/getLocAttributes.ts78
-rw-r--r--shared/localization/src/getPageDir.ts40
-rw-r--r--shared/localization/src/i18n.ts104
-rw-r--r--shared/localization/src/setHTMLAttributes.ts15
-rw-r--r--shared/localization/src/translator.ts174
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;