Merge pull request #100 from ReflectsLight/i18n

Add a simpler, more organized i18n.ts implementation
This commit is contained in:
Robert 2023-03-11 09:35:29 -03:00 committed by GitHub
commit f75a1764d4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 96 additions and 68 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,

View file

@ -1,4 +1,5 @@
# frozen_string_literal: true
source "https://rubygems.org"
##

18
Rules
View file

@ -1,5 +1,6 @@
#!/usr/bin/env ruby
# frozen_string_literal: true
require "ryo"
require "nanoc-gunzip"
require "nanoc-webpack"
@ -36,6 +37,23 @@ require_rules "rules/pages/surah/index", {locales:}
require_rules "rules/pages/surah/redirect"
require_rules "rules/pages/surah/id_redirect", {locales:}
##
# Inline JSON rules
compile "/i18n.json" do
filter(:minify_json)
write(nil)
end
compile "/surahs.json" do
filter(:minify_json)
write(nil)
end
compile "/slugs.json" do
filter(:minify_json)
write(nil)
end
##
# Defaults
compile("/**/*") { write(nil) }

11
lib/helper.rb Normal file
View file

@ -0,0 +1,11 @@
# frozen_string_literal: true
module Helper
def inline_json(path)
class_name = File.basename(path, File.extname(path))
"<script class='json #{class_name}' type='application/json'>" \
"#{items[path].compiled_content}" \
"</script>"
end
end
use_helper Helper

View file

@ -6,7 +6,7 @@
1.upto(114) do |id|
locales.each do |locale|
compile "/html/pages/surah/id_redirect.html.erb", rep: "redirect_id/#{id}" do
compile "/html/pages/surah/id_redirect.html.erb", rep: "redirect_id/#{locale}/#{id}" do
filter(:erb)
write("/#{locale}/#{id}/index.html")
end

View file

@ -30,8 +30,3 @@ compile "/css/pages/surah/index.scss" do
filter :rainpress
write("/css/pages/surah/index.css")
end
compile "/surahs.json" do
filter(:minify_json)
write "/surahs.json"
end

View file

@ -4,9 +4,7 @@
<title></title>
</head>
<body>
<script class="surah-id-to-slug" type="application/json">
<%= File.read(File.join(Dir.getwd, "src", "slugs.json")) %>
</script>
<%= inline_json('/slugs.json') %>
<script src="/js/pages/surah/id_redirect.js"></script>
</body>
</html>

View file

@ -13,6 +13,8 @@
</div>
</div>
<div class="root" data-locale="<%= locale %>"></div>
<%= inline_json("/i18n.json") %>
<%= inline_json("/surahs.json") %>
<script src="/js/pages/surah/index/loader.js"></script>
</body>
</html>

View file

@ -13,6 +13,7 @@
</div>
</div>
<div class="root" data-locale="<%= locale %>" data-surah-id="<%= surah_id %>"></div>
<%= inline_json("/i18n.json") %>
<script src="/js/pages/surah/stream/loader.js"></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,6 +1,6 @@
import React, { useEffect, useState } from 'react';
import * as Quran from 'lib/Quran';
import { numberToDecimal } from 'lib/i18n';
import { formatNumber } from 'lib/i18n';
interface Props {
surah: Quran.Surah
@ -25,7 +25,7 @@ export function Timer ({ surah, stream, setStream, locale, isPaused }: Props) {
}, [ms, isPaused]);
return (
<div className='timer'>
{numberToDecimal(ms / 1000, locale)}
{formatNumber(ms / 1000, locale)}
</div>
);
}

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 formatNumber(number: number, locale: Quran.Locale): string {
return number.toLocaleString(locale, { maximumFractionDigits: 1 })
.split(/([^\d])/)
.join(' ');

View file

@ -3,7 +3,7 @@
.split('/')
.filter(function (s) { return s.length; })
.slice(-2);
const el: HTMLElement = document.querySelector('.surah-id-to-slug')!;
const el: HTMLElement = document.querySelector('.json.slugs')!;
const slugs = JSON.parse(el.innerText);
const path = ['', locale, slugs[surahId]].join('/');
location.replace([path, location.search].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>
);
@ -62,7 +62,8 @@ function SurahIndex({ locale, surahs }: Props) {
(function() {
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 script: HTMLScriptElement = document.querySelector('.json.surahs')!;
const t = i18n(document.querySelector<HTMLElement>('.json.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>('.json.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}
/>
);
})();