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/prefer-nullish-coalescing": 0,
"@typescript-eslint/restrict-template-expressions": 0, "@typescript-eslint/restrict-template-expressions": 0,
"@typescript-eslint/promise-function-async": 0, "@typescript-eslint/promise-function-async": 0,
"@typescript-eslint/consistent-type-definitions": 0,
"@typescript-eslint/no-misused-promises": ["error", {"checksConditionals": false}], "@typescript-eslint/no-misused-promises": ["error", {"checksConditionals": false}],
"@typescript-eslint/no-redeclare": 0, "@typescript-eslint/no-redeclare": 0,
"@typescript-eslint/no-non-null-assertion": 0, "@typescript-eslint/no-non-null-assertion": 0,

View file

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

18
Rules
View file

@ -1,5 +1,6 @@
#!/usr/bin/env ruby #!/usr/bin/env ruby
# frozen_string_literal: true # frozen_string_literal: true
require "ryo" require "ryo"
require "nanoc-gunzip" require "nanoc-gunzip"
require "nanoc-webpack" 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/redirect"
require_rules "rules/pages/surah/id_redirect", {locales:} 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 # Defaults
compile("/**/*") { write(nil) } 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| 1.upto(114) do |id|
locales.each do |locale| 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) filter(:erb)
write("/#{locale}/#{id}/index.html") write("/#{locale}/#{id}/index.html")
end end

View file

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

View file

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

View file

@ -13,6 +13,8 @@
</div> </div>
</div> </div>
<div class="root" data-locale="<%= locale %>"></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> <script src="/js/pages/surah/index/loader.js"></script>
</body> </body>
</html> </html>

View file

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

View file

@ -1,6 +1,6 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import * as Quran from 'lib/Quran'; import * as Quran from 'lib/Quran';
import { numberToDecimal } from 'lib/i18n'; import { formatNumber } from 'lib/i18n';
interface Props { interface Props {
surah: Quran.Surah surah: Quran.Surah
@ -25,7 +25,7 @@ export function Timer ({ surah, stream, setStream, locale, isPaused }: Props) {
}, [ms, isPaused]); }, [ms, isPaused]);
return ( return (
<div className='timer'> <div className='timer'>
{numberToDecimal(ms / 1000, locale)} {formatNumber(ms / 1000, locale)}
</div> </div>
); );
} }

View file

@ -1,25 +1,4 @@
import { Locale } from 'lib/Quran'; import * as Quran 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}'
}
};
/** /**
* The read time baseline - as a number milliseconds - * 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, * The read time for each word in an Ayah,
* relative to the active locale. * relative to the active locale.
*/ */
export const DelayPerWord: Record<Locale, number> = { export const DelayPerWord: Record<Quran.Locale, number> = {
en: 500, en: 500,
ar: 750 ar: 750
}; };
export function numbers (locale: Locale) { type PhraseMap<T> = {
return function(number: number): string { [key: string]: undefined | string | PhraseMap<T>
return Number(number).toLocaleString(locale); };
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) { export function formatNumber(number: number, locale: Quran.Locale): string {
return function(key: Strings): string {
const table = sTable[locale];
return table[key];
};
}
export function numberToDecimal(number: number, locale: Locale): string {
return number.toLocaleString(locale, { maximumFractionDigits: 1 }) return number.toLocaleString(locale, { maximumFractionDigits: 1 })
.split(/([^\d])/) .split(/([^\d])/)
.join(' '); .join(' ');

View file

@ -3,7 +3,7 @@
.split('/') .split('/')
.filter(function (s) { return s.length; }) .filter(function (s) { return s.length; })
.slice(-2); .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 slugs = JSON.parse(el.innerText);
const path = ['', locale, slugs[surahId]].join('/'); const path = ['', locale, slugs[surahId]].join('/');
location.replace([path, location.search].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 { SelectOption } from 'components/Select';
import { ThemeSelect } from 'components/ThemeSelect'; import { ThemeSelect } from 'components/ThemeSelect';
import { LanguageSelect } from 'components/LanguageSelect'; import { LanguageSelect } from 'components/LanguageSelect';
import { strings } from 'lib/i18n'; import { i18n, TFunction } from 'lib/i18n';
interface Props { interface Props {
locale: Quran.Locale locale: Quran.Locale
surahs: Quran.Surah[] surahs: Quran.Surah[]
t: TFunction
} }
function SurahIndex({ locale, surahs }: Props) { function SurahIndex({ locale, surahs, t }: Props) {
const [theme, setTheme] = useState(getCookie('theme') || 'moon'); const [theme, setTheme] = useState(getCookie('theme') || 'moon');
const s = strings(locale);
const onLanguageChange = (o: SelectOption) => { const onLanguageChange = (o: SelectOption) => {
document.location.replace(`/${o.value}/`); document.location.replace(`/${o.value}/`);
}; };
@ -29,7 +29,7 @@ function SurahIndex({ locale, surahs }: Props) {
<div className="header"> <div className="header">
<a href={'/' + locale} className="image" /> <a href={'/' + locale} className="image" />
</div> </div>
<div className="row title">{s('TheNobleQuran')}</div> <div className="row title">{t(locale, 'TheNobleQuran')}</div>
<div className="row dropdown-row"> <div className="row dropdown-row">
<ThemeSelect theme={theme} setTheme={setTheme} /> <ThemeSelect theme={theme} setTheme={setTheme} />
<LanguageSelect locale={locale} onChange={onLanguageChange} /> <LanguageSelect locale={locale} onChange={onLanguageChange} />
@ -52,7 +52,7 @@ function SurahIndex({ locale, surahs }: Props) {
))} ))}
</ul> </ul>
<a href={`/${locale}/random`} className="row surah choose-random"> <a href={`/${locale}/random`} className="row surah choose-random">
{s('ChooseRandomChapter')} {t(locale, 'ChooseRandomChapter')}
</a> </a>
</div> </div>
); );
@ -62,7 +62,8 @@ function SurahIndex({ locale, surahs }: Props) {
(function() { (function() {
const root: HTMLElement = document.querySelector('.root')!; const root: HTMLElement = document.querySelector('.root')!;
const locale = root.getAttribute('data-locale') as Quran.Locale; 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) const surahs: Quran.Surah[] = JSON.parse(script.innerText)
.map((el: Quran.JSON.Surah) => { .map((el: Quran.JSON.Surah) => {
return Quran.Surah.fromJSON(locale, el); return Quran.Surah.fromJSON(locale, el);
@ -71,6 +72,6 @@ function SurahIndex({ locale, surahs }: Props) {
ReactDOM ReactDOM
.createRoot(root) .createRoot(root)
.render( .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 { PlayShape, PauseShape } from 'components/Shape';
import * as Quran from 'lib/Quran'; import * as Quran from 'lib/Quran';
import { Slice } from 'lib/Quran/Slice'; import { Slice } from 'lib/Quran/Slice';
import { strings } from 'lib/i18n'; import { i18n, TFunction } from 'lib/i18n';
interface Props { interface Props {
node: HTMLScriptElement node: HTMLScriptElement
locale: Quran.Locale locale: Quran.Locale
surahId: number
slice: Slice slice: Slice
paused: boolean 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 [stream, setStream] = useState<Quran.Ayat>([]);
const [isPaused, setIsPaused] = useState<boolean>(paused); const [isPaused, setIsPaused] = useState<boolean>(paused);
const [theme, setTheme] = useState(getCookie('theme') || 'moon'); const [theme, setTheme] = useState(getCookie('theme') || 'moon');
const [surah] = useState<Quran.Surah>(Quran.Surah.fromDOMNode(locale, node)); const [surah] = useState<Quran.Surah>(Quran.Surah.fromDOMNode(locale, node));
const readyToRender = stream.length > 0; const readyToRender = stream.length > 0;
const s = strings(locale);
const onLanguageChange = (o: SelectOption) => { const onLanguageChange = (o: SelectOption) => {
const locale = o.value; const locale = o.value;
const params = [ const params = [
@ -66,7 +65,7 @@ function SurahStream({ node, locale, surahId, slice, paused }: Props) {
</div> </div>
{readyToRender && ( {readyToRender && (
<> <>
<div className="row title">{s('TheNobleQuran')}</div> <div className="row title">{t(locale, 'TheNobleQuran')}</div>
<div className="row dropdown-row"> <div className="row dropdown-row">
<ThemeSelect theme={theme} setTheme={setTheme} /> <ThemeSelect theme={theme} setTheme={setTheme} />
<LanguageSelect locale={locale} onChange={onLanguageChange} /> <LanguageSelect locale={locale} onChange={onLanguageChange} />
@ -87,6 +86,7 @@ function SurahStream({ node, locale, surahId, slice, paused }: Props) {
locale={locale} locale={locale}
endOfStream={endOfStream} endOfStream={endOfStream}
isPaused={isPaused} isPaused={isPaused}
t={t}
/> />
} }
<div className="row"> <div className="row">
@ -118,6 +118,7 @@ function SurahStream({ node, locale, surahId, slice, paused }: Props) {
const params = new URLSearchParams(location.search); const params = new URLSearchParams(location.search);
const slice = Slice.fromParam(params.get('ayah')); const slice = Slice.fromParam(params.get('ayah'));
const paused = toBoolean(params.get('paused')); const paused = toBoolean(params.get('paused'));
const t = i18n(document.querySelector<HTMLElement>('.json.i18n')!.innerText);
ReactDOM ReactDOM
.createRoot(root) .createRoot(root)
@ -125,9 +126,9 @@ function SurahStream({ node, locale, surahId, slice, paused }: Props) {
<SurahStream <SurahStream
node={node} node={node}
locale={locale} locale={locale}
surahId={surahId}
slice={slice} slice={slice}
paused={paused} paused={paused}
t={t}
/> />
); );
})(); })();