Add a simpler, more organized i18n.ts implementation

This commit is contained in:
0x1eef 2023-03-10 16:36:52 -03:00
parent e96795f077
commit 914c918eaa
9 changed files with 68 additions and 55 deletions

View file

@ -13,6 +13,7 @@ module.exports = {
"@typescript-eslint/prefer-nullish-coalescing": 0,
"@typescript-eslint/restrict-template-expressions": 0,
"@typescript-eslint/promise-function-async": 0,
"@typescript-eslint/consistent-type-definitions": 0,
"@typescript-eslint/no-misused-promises": ["error", {"checksConditionals": false}],
"@typescript-eslint/no-redeclare": 0,
"@typescript-eslint/no-non-null-assertion": 0,

5
Rules
View file

@ -36,6 +36,11 @@ require_rules "rules/pages/surah/index", {locales:}
require_rules "rules/pages/surah/redirect"
require_rules "rules/pages/surah/id_redirect", {locales:}
compile "/i18n.json" do
filter(:minify_json)
write("/i18n.json")
end
##
# Defaults
compile("/**/*") { write(nil) }

View file

@ -14,5 +14,8 @@
</div>
<div class="root" data-locale="<%= locale %>"></div>
<script src="/js/pages/surah/index/loader.js"></script>
<script class="i18n" type="application/json">
<%= items["/i18n.json"].compiled_content %>
</script>
</body>
</html>

View file

@ -14,5 +14,8 @@
</div>
<div class="root" data-locale="<%= locale %>" data-surah-id="<%= surah_id %>"></div>
<script src="/js/pages/surah/stream/loader.js"></script>
<script class="i18n" type="application/json">
<%= items["/i18n.json"].compiled_content %>
</script>
</body>
</html>

16
src/i18n.json Normal file
View file

@ -0,0 +1,16 @@
{
"en": {
"TheNobleQuran": "The Noble Quran",
"ChooseRandomChapter": "Choose a random chapter",
"surah": "Surah",
"ayah": "Ayah",
"comma": ","
},
"ar": {
"TheNobleQuran": "القرآن الكريم",
"ChooseRandomChapter": "اختر سورة عشوائية",
"surah": "سورة",
"ayah": "آية",
"comma": "،"
}
}

View file

@ -1,6 +1,6 @@
import * as Quran from 'lib/Quran';
import React, { useEffect } from 'react';
import { numbers, strings } from 'lib/i18n';
import { TFunction } from 'lib/i18n';
import { Slice } from 'lib/Quran/Slice';
import classNames from 'classnames';
@ -11,21 +11,20 @@ interface Props {
slice: Slice
endOfStream: boolean
isPaused: boolean
t: TFunction
}
export function Stream({ surah, stream, locale, slice, endOfStream, isPaused }: Props) {
const n = numbers(locale);
const s = strings(locale);
export function Stream({ surah, stream, locale, slice, endOfStream, isPaused, t }: Props) {
const className = classNames('body', 'stream', { 'scroll-y': endOfStream || isPaused });
const ayat = stream.map((ayah: Quran.Ayah) => {
return (
<li key={ayah.id} className="ayah fade">
<span className="surah-id ayah-id">
{s('surah')}{' '}
{n(surah.id)}
{s('comma')}{' '}
{s('ayah')}{' '}
{n(ayah.id)}
{t(locale, 'surah')}{' '}
{surah.id.toLocaleString(locale)}
{t(locale, 'comma')}{' '}
{t(locale, 'ayah')}{' '}
{ayah.id.toLocaleString(locale)}
</span>
<p>{ayah.text}</p>
</li>

View file

@ -1,25 +1,4 @@
import { Locale } from 'lib/Quran';
type Strings = 'decimal' | 'surah' | 'ayah' | 'comma' |
'TheNobleQuran' | 'ChooseRandomChapter';
const sTable: Record<Locale, Record<Strings, string>> = {
en: {
TheNobleQuran: 'The Noble Quran',
ChooseRandomChapter: 'Choose a random chapter',
decimal: '.',
surah: 'Surah',
ayah: 'Ayah',
comma: ','
},
ar: {
TheNobleQuran: '\u{627}\u{644}\u{642}\u{631}\u{622}\u{646}\u{20}\u{627}\u{644}\u{643}\u{631}\u{64a}\u{645}',
ChooseRandomChapter: '\u{627}\u{62e}\u{62a}\u{631}\u{20}\u{633}\u{648}\u{631}\u{629}\u{20}\u{639}\u{634}\u{648}\u{627}\u{626}\u{64a}\u{629}',
decimal: '\u{066B}',
surah: '\u{633}\u{648}\u{631}\u{629}',
ayah: '\u{622}\u{64a}\u{629}',
comma: '\u{60c}'
}
};
import * as Quran from 'lib/Quran';
/**
* The read time baseline - as a number milliseconds -
@ -31,25 +10,30 @@ export const DelayBaseLine = 2000;
* The read time for each word in an Ayah,
* relative to the active locale.
*/
export const DelayPerWord: Record<Locale, number> = {
export const DelayPerWord: Record<Quran.Locale, number> = {
en: 500,
ar: 750
};
export function numbers (locale: Locale) {
return function(number: number): string {
return Number(number).toLocaleString(locale);
type PhraseMap<T> = {
[key: string]: undefined | string | PhraseMap<T>
};
export type TFunction = (locale: Quran.Locale, key: string) => string;
export function i18n(json: string): TFunction {
const phrases: PhraseMap<string> = JSON.parse(json);
return function (locale: Quran.Locale, key: string) {
const path = key.split('.');
const phrase = path.reduce(
(o, k) => typeof(o) === 'object' ? o[k] : o,
phrases[locale]
);
return typeof phrase === 'string' ? phrase : key;
};
}
export function strings (locale: Locale) {
return function(key: Strings): string {
const table = sTable[locale];
return table[key];
};
}
export function numberToDecimal(number: number, locale: Locale): string {
export function numberToDecimal(number: number, locale: Quran.Locale): string {
return number.toLocaleString(locale, { maximumFractionDigits: 1 })
.split(/([^\d])/)
.join(' ');

View file

@ -6,16 +6,16 @@ import * as Quran from 'lib/Quran';
import { SelectOption } from 'components/Select';
import { ThemeSelect } from 'components/ThemeSelect';
import { LanguageSelect } from 'components/LanguageSelect';
import { strings } from 'lib/i18n';
import { i18n, TFunction } from 'lib/i18n';
interface Props {
locale: Quran.Locale
surahs: Quran.Surah[]
t: TFunction
}
function SurahIndex({ locale, surahs }: Props) {
function SurahIndex({ locale, surahs, t }: Props) {
const [theme, setTheme] = useState(getCookie('theme') || 'moon');
const s = strings(locale);
const onLanguageChange = (o: SelectOption) => {
document.location.replace(`/${o.value}/`);
};
@ -29,7 +29,7 @@ function SurahIndex({ locale, surahs }: Props) {
<div className="header">
<a href={'/' + locale} className="image" />
</div>
<div className="row title">{s('TheNobleQuran')}</div>
<div className="row title">{t(locale, 'TheNobleQuran')}</div>
<div className="row dropdown-row">
<ThemeSelect theme={theme} setTheme={setTheme} />
<LanguageSelect locale={locale} onChange={onLanguageChange} />
@ -52,7 +52,7 @@ function SurahIndex({ locale, surahs }: Props) {
))}
</ul>
<a href={`/${locale}/random`} className="row surah choose-random">
{s('ChooseRandomChapter')}
{t(locale, 'ChooseRandomChapter')}
</a>
</div>
);
@ -63,6 +63,7 @@ function SurahIndex({ locale, surahs }: Props) {
const root: HTMLElement = document.querySelector('.root')!;
const locale = root.getAttribute('data-locale') as Quran.Locale;
const script: HTMLScriptElement = document.querySelector('script[src="/surahs.json"]')!;
const t = i18n(document.querySelector<HTMLElement>('.i18n')!.innerText);
const surahs: Quran.Surah[] = JSON.parse(script.innerText)
.map((el: Quran.JSON.Surah) => {
return Quran.Surah.fromJSON(locale, el);
@ -71,6 +72,6 @@ function SurahIndex({ locale, surahs }: Props) {
ReactDOM
.createRoot(root)
.render(
<SurahIndex locale={locale} surahs={surahs} />
<SurahIndex locale={locale} surahs={surahs} t={t} />
);
})();

View file

@ -10,23 +10,22 @@ import { LanguageSelect } from 'components/LanguageSelect';
import { PlayShape, PauseShape } from 'components/Shape';
import * as Quran from 'lib/Quran';
import { Slice } from 'lib/Quran/Slice';
import { strings } from 'lib/i18n';
import { i18n, TFunction } from 'lib/i18n';
interface Props {
node: HTMLScriptElement
locale: Quran.Locale
surahId: number
slice: Slice
paused: boolean
t: TFunction
}
function SurahStream({ node, locale, surahId, slice, paused }: Props) {
function SurahStream({ node, locale, slice, paused, t }: Props) {
const [stream, setStream] = useState<Quran.Ayat>([]);
const [isPaused, setIsPaused] = useState<boolean>(paused);
const [theme, setTheme] = useState(getCookie('theme') || 'moon');
const [surah] = useState<Quran.Surah>(Quran.Surah.fromDOMNode(locale, node));
const readyToRender = stream.length > 0;
const s = strings(locale);
const onLanguageChange = (o: SelectOption) => {
const locale = o.value;
const params = [
@ -66,7 +65,7 @@ function SurahStream({ node, locale, surahId, slice, paused }: Props) {
</div>
{readyToRender && (
<>
<div className="row title">{s('TheNobleQuran')}</div>
<div className="row title">{t(locale, 'TheNobleQuran')}</div>
<div className="row dropdown-row">
<ThemeSelect theme={theme} setTheme={setTheme} />
<LanguageSelect locale={locale} onChange={onLanguageChange} />
@ -87,6 +86,7 @@ function SurahStream({ node, locale, surahId, slice, paused }: Props) {
locale={locale}
endOfStream={endOfStream}
isPaused={isPaused}
t={t}
/>
}
<div className="row">
@ -118,6 +118,7 @@ function SurahStream({ node, locale, surahId, slice, paused }: Props) {
const params = new URLSearchParams(location.search);
const slice = Slice.fromParam(params.get('ayah'));
const paused = toBoolean(params.get('paused'));
const t = i18n(document.querySelector<HTMLElement>('.i18n')!.innerText);
ReactDOM
.createRoot(root)
@ -125,9 +126,9 @@ function SurahStream({ node, locale, surahId, slice, paused }: Props) {
<SurahStream
node={node}
locale={locale}
surahId={surahId}
slice={slice}
paused={paused}
t={t}
/>
);
})();