Compare commits

68 Commits

Author SHA1 Message Date
9b030b24f4 feat: added parent context 2025-04-16 16:42:21 +02:00
44b651cace feat: move meta values to article 2025-04-16 16:39:06 +02:00
eb2edc3710 feat: removed card templates 2025-04-16 16:17:57 +02:00
f63497090b Revert "feat: removed redundand templates"
This reverts commit 215142dee4.
2025-04-16 16:17:13 +02:00
215142dee4 feat: removed redundand templates 2025-04-16 16:09:18 +02:00
e391f64cc5 feat: removed redundand methods 2025-04-16 16:08:25 +02:00
4216d153fd feat: removed redundand methods 2025-04-16 16:08:11 +02:00
9241ecfee8 feat: added dynamic children cards in templates 2025-04-16 16:07:20 +02:00
4bc7a6d980 feat: added article translation templating 2025-04-16 16:01:38 +02:00
b50833f19f feat: dynamic translation cards 2025-04-16 15:57:17 +02:00
f5cccc30dc feat: dynamic translation cards 2025-04-16 15:56:34 +02:00
8a8f02c5bd feat: implemented code to use with jinja 2025-04-16 15:45:06 +02:00
292d71edc5 feat: added proper content 2025-04-16 15:26:43 +02:00
c9fb8fda93 feat: building recursive context structures 2025-04-16 14:26:50 +02:00
1fae03e70b feat: properly recursively building context 2025-04-16 14:20:51 +02:00
6ed94db8cf feat: added translations 2025-04-16 14:17:45 +02:00
02a7c29dba feat: add flat context to article 2025-04-16 14:05:16 +02:00
cf8e2955c2 feat: initializing location 2025-04-16 13:23:30 +02:00
7aa06b7f83 feat: added meta context 2025-04-16 13:15:27 +02:00
f7a690405b feat: add language context 2025-04-16 13:09:46 +02:00
93ea11cd0e feat: removed weird article cards 2025-04-16 12:30:54 +02:00
263281df3c feat: renamed ArticleOverview to article 2025-04-16 12:18:44 +02:00
amnesia
b953933e6f added jinja to deps 2025-04-15 23:41:03 +02:00
7db84492b5 feat: hastly implementation of article cards 2025-04-15 17:48:21 +02:00
c7cd5e0601 feat: renamed overview_card to translation_card 2025-04-15 17:31:06 +02:00
c05f8fad35 feat: renamed overview_card to translation_card 2025-04-15 17:30:57 +02:00
b9c7535812 feat: added article card template 2025-04-15 17:29:25 +02:00
cdcd3bce1f fix: temlate typo 2025-04-15 17:28:12 +02:00
6eb02cc95f feat: added possibility for child pages cards 2025-04-15 17:26:09 +02:00
2454d26d6b feat: better timestamps 2025-04-15 17:15:55 +02:00
8054a4a983 fix: preview html fuckup 2025-04-15 17:13:04 +02:00
afefce0a11 feat: added datetime 2025-04-15 17:08:37 +02:00
334c82d098 feat: added date to terminal 2025-04-15 17:04:57 +02:00
ddd536c958 feat: added date to values 2025-04-15 16:54:01 +02:00
ae5ba0d044 feat: writing article config values to class 2025-04-15 16:48:00 +02:00
83133218a4 feat: writing article config values to class 2025-04-15 16:47:15 +02:00
d412d983bd feat: cleaned the logs and copy static 2025-04-15 16:34:43 +02:00
9859229101 feat: added nice config functionality 2025-04-15 16:21:05 +02:00
dc8f9d91e7 feat: added toml files 2025-04-15 15:26:48 +02:00
f3a86b8070 feat: added toml dependency 2025-04-15 15:20:39 +02:00
b814434c48 feat: added toml dependency 2025-04-15 15:13:08 +02:00
ba05ecdd94 feat: renamed pages 2025-04-15 13:12:36 +02:00
1751158cbd feat: added logger 2025-04-15 13:11:59 +02:00
c45cebe497 feat: properly building additional keywords 2025-04-15 13:00:34 +02:00
dc61a7ee92 fix: location position copy instead of reference 2025-04-15 12:47:56 +02:00
d70a0a8630 feat: slight rewrite 2025-04-15 12:43:09 +02:00
4f37283e68 fix: stupid ahh typo bug 2025-04-15 12:21:49 +02:00
0e948e7f55 feat: small refactoring for building of urls 2025-04-15 12:19:45 +02:00
79c95a9ddb feat: small refactoring for building of urls 2025-04-15 12:16:37 +02:00
0ca0fb90d6 feat: added type hints 2025-04-15 12:10:21 +02:00
603a7f5942 feat: filling an article lake 2025-04-15 12:09:28 +02:00
63f8541a82 feat: refactor type dict 2025-04-15 12:01:10 +02:00
dc5af8da28 feat: refactor type dict 2025-04-15 11:59:04 +02:00
3fdbf13d95 Merge branch 'main' of ssh://gitea.elara.ws:2222/Hazel/STSG 2025-04-15 11:55:57 +02:00
7e4640e7d9 feat: added inline type dict 2025-04-15 11:55:43 +02:00
amnesia
8239b3ea6d added src 2025-04-15 11:50:56 +02:00
amnesia
3f0049dfdb feat: recursive pages 2025-04-14 21:06:52 +02:00
amnesia
a113a13318 fix: added bs4 to dep 2025-04-14 19:29:39 +02:00
3926277f8f feat: move relevant card to the top 2025-04-14 17:35:59 +02:00
69b3efd78f feat: move relevant card to the top 2025-04-14 17:35:11 +02:00
2b80c90a19 feat: added lang attribute to reduce barriers 2025-04-14 16:59:18 +02:00
37d28a3542 feat: added priority to languages 2025-04-14 16:46:12 +02:00
3e692dd74f feat: added priority to languages 2025-04-14 16:45:16 +02:00
3e6884ad8a feat: added priority to config 2025-04-14 16:11:12 +02:00
8e57af5a78 feat: added article title 2025-04-14 16:07:25 +02:00
37974c418b feat: added article home overview 2025-04-14 16:04:40 +02:00
2dc1dfef98 feat: added article home overview 2025-04-14 15:58:56 +02:00
c36deab71a feat: added kurdish placeholder 2025-04-14 15:50:16 +02:00
19 changed files with 1316 additions and 1111 deletions

1
.gitignore vendored
View File

@@ -174,3 +174,4 @@ cython_debug/
.pypirc
dist
context.json

View File

@@ -2,7 +2,10 @@
name = "stsg"
dependencies = [
"watchdog~=6.0.0",
"markdown~=3.3.6"
"markdown~=3.3.6",
"bs4~=0.0.2",
"toml~0.10.2",
"jinja2~=3.1.6",
]
dynamic = []
authors = []

View File

@@ -1,38 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>STSG</title>
<link rel="stylesheet" href="/static/bulma.min.css" />
</head>
<body>
<!-- Header (Navbar) -->
<nav
class="navbar is-primary"
role="navigation"
aria-label="main navigation"
>
<div class="navbar-brand">
<a class="navbar-item" href="#">
<strong>Static Translated Site Generator</strong>
</a>
</div>
</nav>
<section class="section">
<div class="container">
<div class="column is-half is-offset-one-quarter">
{overview_cards}
</div>
</div>
</section>
<!-- Footer -->
<footer class="footer">
<div class="content has-text-centered">
<p><strong>STSG</strong> by Hazel. &copy; 2025</p>
</div>
</footer>
</body>
</html>

View File

@@ -1,8 +0,0 @@
<div class="card mb-4">
<a href="{article_href}" class="card mb-4" style="color: inherit; text-decoration: none;">
<div class="card-content" href="{article_href}">
<p class="title">{article_language_flag} {article_language_name}</p>
<p class="content">{article_preview}</p>
</div>
</a>
</div>

View File

@@ -0,0 +1,75 @@
# Populumque avidosque Sparte quoque auctore equidem
## Sunt aevis
Lorem markdownum turbavere prisca Aeacidae morando esse. Quam Styga spectata,
pariter Iove iunctis exercere Solis? Atlantis possit succurrere quam!
if (stationRecord < ctp(rup, columnBase, dtd)) {
impact_qbe_symbolic(bank_c(exploit_gnutella_social), inbox);
marketing = telnetWebmasterFpu;
circuitSoapDns(dac, ieee);
} else {
ebook.spider(wildcard_publishing_memory.tcpDisk(encoding, 48149), -1 +
copyright_flash_icmp, superscalar_cluster + kofficeIsp);
reimage.mac = dslWebmail;
kilobyteVariable.margin_keylogger = dvd + microcomputer;
}
font_switch(servlet, file(1, protocol) * skinTouchscreen,
wheel_station_computer);
var startRom = bit(php_touchscreen_icio);
var unicode_hover_tiger = command(scan_install_cd(ruby(mail_chipset, web,
clientMemeSoftware), optical, wirelessMegahertz), oasis);
## Est nec locumque anxia et
Maligno puppes potuit petit. **Ipse regnat venit** tangit mitti opibus est unus
spectacula erat! Bacche qui dedit in ardet Phrygiis Liternum ipso ille Orphei
Canentem *ut*, parenti terrae! Frondibus deus sine leoni frustraque lentus. Est
deos cum corripuit erat sibi concussit simul; suus tantum.
Camenis Lucifer ex geniti sitis quem. Styga si Ceae nova media remugis: haerens
ridet, nam [Pindo](http://www.murra.com/argolicaemaximus) est tritis flamma
[dixit una licebit](http://www.sedare-mopsus.net/) Pelia perdite! Aura *aurea
mecum* una mirabantur mansit domum simul de Euboica altis vincula tenentis vires
sub, *Scythicae*. Mora sitis pocula. Ultimus idem triplices inquit.
## Et atque ministris imagine fas tenuit fornacibus
Adeunda suffundit ille: Bacchi moribundo et quam **cacumina videre Tamasenum**.
Gauderet in [non arbitrium caelo](http://www.madidusperiuraque.io/).
## In corpora in micat Phoebus corque transitus
Mihi in macies, ab avoque malorum decusque. Appellant expellam unus colore
exiguo, maior ara loqui sit vires.
var uat = cardPrinterLocalhost.pppoe(display, operating_row_fsb(
scalable_lion, compilerHeuristicTweet - 22));
var toslink_software = tokenPciPrompt.source_x_firmware(drive,
boxSdkDlc.servicesIcsDrive(certificate_cycle_illegal(resolution)),
outboxTftMap + rssTextZebibyte.flashImpactDisk(3, eup_ad));
var compression = programWebPort;
if (intellectual_left_system) {
favorites(lossyDramDay + 7, upsSliTruncate, 2);
laptop.android = ocr_piracy(clean, flatbedRte - -1);
directx_file_cable = -1;
} else {
point_sink_controller(flash(filenameDataAccess, vleSoftwareListserv),
point_rate_cmos(control_soa_restore));
javascript *= nic_format;
address = nameDcim(touchscreenLink.port_port(diskHeat, vaporware));
}
## Enim nunc solvi
Est pudet citharae, corpus? Modo in armentaque pennisque videri aquarum, est
equos ne in vulnere domus; maris. Quodque quoque orbe omnes metus, sol
[putas](http://www.uno.com/deorumvolumine) marmore fuit secreta haut vobis,
faciendo, oro.
Quid sequenti, supersunt quoque, sortite; in in tenetur vecta horriferamque
amabat. Vos nudae anni amor chelydri [Picus candentibus
et](http://pugnare.io/angulus.html) sonum, et sentibus geminato volucrem
mercibus bracchia, cum *posito* delubraque. Templa extrahit in totidem altera
Nioben, honoris sui, fibris!

View File

@@ -0,0 +1,35 @@
# Navê Gotarê example
Ev gotar, mînakek placeholder ya bi zimanê Kurdî ye. Nivîs, weşan û rûpelên nûçe di vê belgeyê de têne pêşniyar kirin. Ev metn ji bo testkirina layout an jî demo-yê hatiye çêkirin û çend bingehên cihanî yên naveroka gotarê nîşan dide.
## Destpêk
Di destpêka gotarê de, dikarin rêvekên bingehîn û armancên mijarê vebêjin. Tu dikarî vê metnê bikar bînin ji bo şexsî an jî projeyên xwe:
Lorem ipsum ji bo kurdî:
"Rojên nû û şevên hêja, ev nivîs têne çêkirin ku bi awayek nîşanbideke cîhan û hunermendî were afirandin."
## Lêkolîn û Naverok
Di vê beşê de hinek îzahiyên naverokî yên placeholder hatine peyda kirin. Hûn dikarin bi rêza li jêr anînên mifteyî yên gotarê şop bikin:
- **Mijar:** Di vê gotarê de, mijarên bi zor û hêja têne nîşandan.
- **Rêbaz:** Ew kesên ku ji ber vê gotarê şîrove û daxuyanî dikin, çavkaniyên xwe diguhezînin.
- **Agahî:** Di vê derbarê de, agahîyên cihanî, dîtin û têgihiştin hatine berhev kirin.
Di bin vê şexsiyeta, hertişt di dema xwe de têgihiştin û anîn pêşiyê dayîn.
## Şirove û Kod
Di vê paragrafa kêm-kêm de, em dikarin bingehên şiroveyê yên gotarê di kodê de jî şop bikin:
```python
def peyama_kurdî():
mesaj = "Her bijî Kurdistan! Rojên baş û serkeftin!"
return mesaj
print(peyama_kurdî())
```
Kodê li ser vê koda simplesa gotinê ye û dikare weşana nûçe an demo-yê projeyan bixebite.
# Encama Gotarê
Di encama gotarê de, hûn dikarin her çend beşên bingehîn yên nûçe, daxuyanî û şirove yên navekî bifikirin.
Bê guman, ev placeholder bi awayek qelew li ser çalakiya te ya malper, blog an jî her sedema dijîtal tê de karîger e.

2
src/articles/index.toml Normal file
View File

@@ -0,0 +1,2 @@
name="index_page"
datetime="2024-04-15 13:45:12.123456"

38
src/articles/ku.md Normal file
View File

@@ -0,0 +1,38 @@
# Navê Gotarê
[{article_title:example}]({article_url:example})
Ev gotar, mînakek placeholder ya bi zimanê Kurdî ye. Nivîs, weşan û rûpelên nûçe di vê belgeyê de têne pêşniyar kirin. Ev metn ji bo testkirina layout an jî demo-yê hatiye çêkirin û çend bingehên cihanî yên naveroka gotarê nîşan dide.
## Destpêk
Di destpêka gotarê de, dikarin rêvekên bingehîn û armancên mijarê vebêjin. Tu dikarî vê metnê bikar bînin ji bo şexsî an jî projeyên xwe:
Lorem ipsum ji bo kurdî:
"Rojên nû û şevên hêja, ev nivîs têne çêkirin ku bi awayek nîşanbideke cîhan û hunermendî were afirandin."
## Lêkolîn û Naverok
Di vê beşê de hinek îzahiyên naverokî yên placeholder hatine peyda kirin. Hûn dikarin bi rêza li jêr anînên mifteyî yên gotarê şop bikin:
- **Mijar:** Di vê gotarê de, mijarên bi zor û hêja têne nîşandan.
- **Rêbaz:** Ew kesên ku ji ber vê gotarê şîrove û daxuyanî dikin, çavkaniyên xwe diguhezînin.
- **Agahî:** Di vê derbarê de, agahîyên cihanî, dîtin û têgihiştin hatine berhev kirin.
Di bin vê şexsiyeta, hertişt di dema xwe de têgihiştin û anîn pêşiyê dayîn.
## Şirove û Kod
Di vê paragrafa kêm-kêm de, em dikarin bingehên şiroveyê yên gotarê di kodê de jî şop bikin:
```python
def peyama_kurdî():
mesaj = "Her bijî Kurdistan! Rojên baş û serkeftin!"
return mesaj
print(peyama_kurdî())
```
Kodê li ser vê koda simplesa gotinê ye û dikare weşana nûçe an demo-yê projeyan bixebite.
# Encama Gotarê
Di encama gotarê de, hûn dikarin her çend beşên bingehîn yên nûçe, daxuyanî û şirove yên navekî bifikirin.
Bê guman, ev placeholder bi awayek qelew li ser çalakiya te ya malper, blog an jî her sedema dijîtal tê de karîger e.

View File

@@ -1,57 +0,0 @@
meow
# Abstrahere reddita celebrare in ossa
## Usque de celebrabant puer
Lorem markdownum, nec ora et vero me nec natae suadent. Nec damno ignorat
propiore aliquid temptata decipienda habetur. Vulnera lacrimis aequoreo madidos,
copia uvae, herbosaque quoque, per harenas, canos fui monstro Peleus.
Et fuge cum liquidum puer Herculis arentis, tantum caudaque et generi vilior, in
rubore. Caeli modo palmis, suo tria accipe visus non similis qui remittat
retentus porrigit fluxit ubi testis.
> Sulphura et color reliquit dextera: quid summa continuere obductos egesto
> moriens **fluentia vult iunctamque**, mihi patres spiro. Est Iovis **imperat
> quem est** putavi annis *omnis*; flumina, leporem constabat grave pelagi,
> insiluit, igne invito? Auctor circuit quod.
## Rhoetus gravis
Parari modo in sustulerat: ora sic verba meruit, uti. Pignora citus facto
amplectimur cupido amentes foedantem multoque datura.
[Quid](http://www.prima.net/se) ex tanti armaque exhibita descenderat!
1. Alis ima
2. Mirantur nive sit
3. Maris ut adlabimur humana fine quam vultusque
## A idonea miserum
Montes tibi deorum igitur. Poterat nunc porrigitur perdidimus *sidereos animi
praesepia* nihil praeferri functa, in **Pleias removete** oculos sollemni
Tatius: **modo**. Inde dedit! Atque in matrem spinae foret ponti quam dixit,
aras. Gladii addendum fiducia magno, se quo fata humo esse **tellure corpora
discederet** sucis manibus, parentem ante, Iovem.
Laqueique honore sequentia tyranni Harpalos, paelex, foedat tempestiva nomen.
Sit ter indiciumque requirit utrumque in nil et *suspectus*, quaerite patriam
nec facta [securi](http://heuqua.io/adhaut). Confessa ut per sit nostro futura
metaque oblitae fameque exit adspiciens. Morte flectere invidiam certe cum
vixque *nubes clamor viderunt* praeceps infamis collo percussis axem plena saxum
urbes ferre undae. Totoque utuntur ore lupus inplet sibila ullam, qui corpore
*contermina aera*?
Convocat ipse abeunt sententia concolor a Auguste epops solent iubet qui
aequora. In illi, solvente resecare violentam nescio accipit multarum [aureus
exspectatus](http://oris-tibi.io/nasci) ergo ora. Positis **inquit**, sit iamque
hederae ulterius, pontus linguae matutina terra sic isse Graium passosque
sanguinis secus petit!
Adfore dei volucrum Lydas; [hoc](http://similemnescitve.net/ipsamquee) quam
[superosque](http://www.quoqueabit.com/cumqueiam.html) caligine vulnera quoque
corpus foedaque mentis qui nectare, fatendo sensit! Rursus nulli miraris nuda
*Acrisio*. Cum modo, satis dissuadet luce; cum *freta* ab et diu, labor *tenuata
ieiunia*? Caesosque thalamique precor, dedit nulla loca [arce
et](http://viscera-rerum.com/mixta) tinguit aera.

View File

@@ -1,51 +0,0 @@
# Annos postquam premeret docendam parabam
## Sunt luctus
Lorem markdownum [matrique](http://etanimae.org/), urit cerae pendens concipit
vitiatas dammis clausura. Labori femineis dominam nostris umerus non contiguas
apta sollicito lactis, [quam](http://acceptus-tumulo.net/) acie inplerant et.
1. Vires videbor praesens vidisse agros choreas
2. Illa mercede
3. In acies folioque lignum nitidis quid tellus
4. Multoque ira liber tamen rapiam cremabit crinalem
## Extremum pugnae muneris factum sed lecto Propoetidas
Praeferri iste. Rex primus gigantas Tantalis, **extulerat**, mediamque ardua,
per natae, in et, prius Alpheos.
> Nostro recentia evolvere quae quam: caesas spectans sic forte laesae fessos.
> Leti flammis. Hebes Atreus refluitque maximus necetur circumdet videt mare
> abit arbor sed, per Circe, violentus Niobe viri hominum. Cernis illum, sucos,
> pudorem, numen quoque, si tibique *euntem*, o cruento sitim. Illo tollere
> secutus sopore arma me ova mox noscere mundi est.
Herbas rauco altae patitur steterat domus; hoc pudor alatur falsamque precor. O
**timido** a haerebat, *atque servaturis naturale* tantaque; submersum
[poplite](http://purpureis.com/ferebat) tanta. Patiens latis inlato hinc tantae,
a Tydidae amissum; his! Clamor Finis.
## Fuit nomine mixtos pretium relinquam forma habitabilis
Adolentur sanguine **Penthea** adsum tutus potens adit luctu membra tibi quoque.
Sua siccaeque caesae quoque vosne forma cadit Threicio qua locis Lernae non;
inplent tecta. Cervicibus mare! Visa est locutus, mox amore exitium matrem ora
Hector vultus Minervae.
1. Rigavit mirabile
2. Bacis sua ut tellus florent ait nollet
3. Carmine prius
4. Tendit candida et colle corpore miscent
Fidem **et pondera** latronis uva neu hanc rerum divesque percussae caput ardor
contemptoremque effugies. Et multis, *hoc* sim in dispositam cornua, spatium; in
pelle fecerat [subiecta](http://cultu.io/amphimedon.php) propago miserae
lacerto. Tolle sui mea Lycabasque nocuit. Nereide dum furor genibus clementia
hastam?
Aere satis, Verque finitimi dique tot *carmina* fluminaque iubet resurgebant
nubibus, et suos versantes? Illa Phineus divesque tectis nec, aeno Semiramis
iuvencos desinite ergo, Minoide **calentes**, longos, virgineo. Putaret heros,
et quatit aerias, tibi contigit armatur documenta.

View File

@@ -0,0 +1,98 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>{{slug}}</title>
<link rel="stylesheet" href="/static/bulma.min.css" />
</head>
<body>
<!-- Header (Navbar) -->
<nav
class="navbar is-primary"
role="navigation"
aria-label="main navigation"
>
<div class="navbar-brand">
<a class="navbar-item" href="#">
<strong>Static Translated Site Generator</strong>
</a>
</div>
</nav>
<section class="section">
{% if translations|length %}
<div class="container content">
<div class="column is-half is-offset-one-quarter">
<h1>Translations</h1>
</div>
<div class="column is-half is-offset-one-quarter">
{% for t in translations %}
<div class="card mb-4" lang="{{t.language.code}}">
<a href="{{t.url}}" hreflang="{{t.language.code}}" class="card mb-4" style="color: inherit; text-decoration: none;">
<div class="card-content">
<p class="title">{{t.language.flag}} {{t.title}} </p>
<p class="content">
{{t.preview}}
<br />
<time datetime="{{iso_date}}">{{date}}</time>
</p>
</div>
</a>
</div>
{% endfor %}
</div>
</div>
{% endif %}
{% if children|length %}
<div class="container content">
<div class="column is-half is-offset-one-quarter">
<h1>Child Articles</h1>
</div>
<div class="column is-half is-offset-one-quarter">
{% for c in children %}
<div class="card mb-4" >
<a href="{{c.url}}" class="card mb-4" style="color: inherit; text-decoration: none;">
<div class="card-content">
<p class="title">{{c.slug}} </p>
<p class="content">
<time datetime="{{iso_date}}">{{date}}</time>
</p>
</div>
</a>
</div>
{% endfor %}
</div>
</div>
{% endif %}
</section>
<!-- Footer -->
<footer class="footer">
<div class="content has-text-centered">
<p><strong>STSG</strong> by Hazel. &copy; 2025</p>
</div>
</footer>
</body>
<script>
document.addEventListener("DOMContentLoaded", function () {
const userLang = navigator.language || navigator.userLanguage;
// Normalize and check if the language is not English or German
if (!["en", "de", "de-DE"].includes(userLang)) {
// Try to find a matching card by language attribute
const cardToMove =
document.querySelector(`.card[lang^="${userLang.replace("_", "-").toLowerCase()}"]`) ||
document.querySelector(`.card[lang^="${userLang.split("-")[0]}"]`);
if (cardToMove) {
const container = cardToMove.parentNode;
container.insertBefore(cardToMove, container.firstChild);
}
}
});
</script>
</html>

View File

@@ -1,9 +1,9 @@
<!DOCTYPE html>
<html>
<html lang="{article_language_code}">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>STSG</title>
<title>{{language.flag}} {{title}}</title>
<link rel="stylesheet" href="/static/bulma.min.css" />
</head>
<body>
@@ -14,8 +14,9 @@
aria-label="main navigation"
>
<div class="navbar-brand">
<a class="navbar-item" href="#">
<strong>Static Translated Site Generator</strong>
<a class="navbar-item" href="{{article_url}}">
<strong>{{language.flag}} {{title}}</strong>
<time datetime="{{meta.iso_date}}">{{meta.date}}</time>
</a>
</div>
</nav>
@@ -23,10 +24,7 @@
<!-- Main Content -->
<section class="section">
<div class="content">
<a href="../">Go Back</a>
</div>
<div class="content">
{article_content}
{{content}}
</div>
</section>

25
stsg.toml Normal file
View File

@@ -0,0 +1,25 @@
[setup]
source_directory = "src"
dist_directory = "dist"
[formatting]
article_preview_length = 400
datetime_format = "%d. %B %Y"
default_language = "de"
[languages]
[languages.de]
native_name = "Schland"
priority = 100
[languages.en]
priority = 90
[languages.ar_sy]
priority = 50
[languages.ku]
priority = 49
[languages.tr]
priority = 60

View File

@@ -0,0 +1,793 @@
class config:
class setup:
source_directory = "src"
dist_directory = "dist"
class formatting:
article_preview_length = 200
datetime_format = "%d. %B %Y"
fallback_language = "en"
languages = {
"af": {
"flag": "🇿🇦",
"name": "Afrikaans",
"native_name": "Afrikaans"
},
"am": {
"flag": "🇪🇹",
"name": "Amharic",
"native_name": "አማርኛ"
},
"an": {
"flag": "🇪🇸",
"name": "Aragonese",
"native_name": "aragonés"
},
"ar": {
"flag": "🇸🇦",
"name": "Arabic",
"native_name": "العربية"
},
"ar_ae": {
"flag": "🇦🇪",
"name": "Arabic (UAE)",
"native_name": "العربية (الإمارات)"
},
"ar_bh": {
"flag": "🇧🇭",
"name": "Arabic (Bahrain)",
"native_name": "العربية (البحرين)"
},
"ar_dz": {
"flag": "🇩🇿",
"name": "Arabic (Algeria)",
"native_name": "العربية (الجزائر)"
},
"ar_eg": {
"flag": "🇪🇬",
"name": "Arabic (Egypt)",
"native_name": "العربية (مصر)"
},
"ar_iq": {
"flag": "🇮🇶",
"name": "Arabic (Iraq)",
"native_name": "العربية (العراق)"
},
"ar_jo": {
"flag": "🇯🇴",
"name": "Arabic (Jordan)",
"native_name": "العربية (الأردن)"
},
"ar_kw": {
"flag": "🇰🇼",
"name": "Arabic (Kuwait)",
"native_name": "العربية (الكويت)"
},
"ar_lb": {
"flag": "🇱🇧",
"name": "Arabic (Lebanon)",
"native_name": "العربية (لبنان)"
},
"ar_ly": {
"flag": "🇱🇾",
"name": "Arabic (Libya)",
"native_name": "العربية (ليبيا)"
},
"ar_ma": {
"flag": "🇲🇦",
"name": "Arabic (Morocco)",
"native_name": "العربية (المغرب)"
},
"ar_om": {
"flag": "🇴🇲",
"name": "Arabic (Oman)",
"native_name": "العربية (عُمان)"
},
"ar_qa": {
"flag": "🇶🇦",
"name": "Arabic (Qatar)",
"native_name": "العربية (قطر)"
},
"ar_sa": {
"flag": "🇸🇦",
"name": "Arabic (Saudi Arabia)",
"native_name": "العربية (السعودية)"
},
"ar_sd": {
"flag": "🇸🇩",
"name": "Arabic (Sudan)",
"native_name": "العربية (السودان)"
},
"ar_sy": {
"flag": "🇸🇾",
"name": "Arabic (Syria)",
"native_name": "العربية (سوريا)",
},
"ar_tn": {
"flag": "🇹🇳",
"name": "Arabic (Tunisia)",
"native_name": "العربية (تونس)"
},
"ar_ye": {
"flag": "🇾🇪",
"name": "Arabic (Yemen)",
"native_name": "العربية (اليمن)"
},
"ars_ae": {
"flag": "🇦🇪",
"name": "Najdi Arabic (UAE)",
"native_name": "نَجْدِيّ"
},
"ars_arab_sa": {
"flag": "🇸🇦",
"name": "Najdi Arabic (Saudi Arabia, Arabic Script)",
"native_name": "نَجْدِيّ"
},
"ars_sa": {
"flag": "🇸🇦",
"name": "Najdi Arabic (Saudi Arabia)",
"native_name": "نَجْدِيّ"
},
"as": {
"flag": "🇮🇳",
"name": "Assamese",
"native_name": "অসমীয়া"
},
"az": {
"flag": "🇦🇿",
"name": "Azerbaijani",
"native_name": "Azərbaycan"
},
"be": {
"flag": "🇧🇾",
"name": "Belarusian",
"native_name": "Беларуская"
},
"bg": {
"flag": "🇧🇬",
"name": "Bulgarian",
"native_name": "Български"
},
"bm": {
"flag": "🇲🇱",
"name": "Bambara",
"native_name": "bamanankan"
},
"bn": {
"flag": "🇧🇩",
"name": "Bengali",
"native_name": "বাংলা"
},
"bn_in": {
"flag": "🇮🇳",
"name": "Bengali (India)",
"native_name": "বাংলা (ভারত)"
},
"br": {
"flag": "🏴",
"name": "Breton",
"native_name": "brezhoneg"
},
"bs": {
"flag": "🇧🇦",
"name": "Bosnian",
"native_name": "Bosanski"
},
"ca": {
"flag": "🇪🇸",
"name": "Catalan",
"native_name": "Català"
},
"crh": {
"flag": "🇺🇦",
"name": "Crimean Tatar",
"native_name": "qırımtatarca"
},
"cs": {
"flag": "🇨🇿",
"name": "Czech",
"native_name": "Čeština"
},
"cv": {
"flag": "🇷🇺",
"name": "Chuvash",
"native_name": "чӑваш чӗлхи"
},
"cy": {
"flag": "🏴",
"name": "Welsh",
"native_name": "Cymraeg"
},
"da": {
"flag": "🇩🇰",
"name": "Danish",
"native_name": "Dansk"
},
"de": {
"flag": "🇩🇪",
"name": "German",
"native_name": "Deutsch",
},
"de_at": {
"flag": "🇦🇹",
"name": "German (Austria)",
"native_name": "Deutsch (Österreich)"
},
"de_be": {
"flag": "🇧🇪",
"name": "German (Belgium)",
"native_name": "Deutsch (Belgien)"
},
"de_ch": {
"flag": "🇨🇭",
"name": "German (Switzerland)",
"native_name": "Deutsch (Schweiz)"
},
"dv": {
"flag": "🇲🇻",
"name": "Dhivehi",
"native_name": "ދިވެހި"
},
"dz": {
"flag": "🇧🇹",
"name": "Dzongkha",
"native_name": "རྫོང་ཁ"
},
"el": {
"flag": "🇬🇷",
"name": "Greek",
"native_name": "Ελληνικά"
},
"en": {
"flag": "🇺🇸",
"name": "English",
"native_name": "English",
},
"en_au": {
"flag": "🇦🇺",
"name": "English (Australia)",
"native_name": "English (Australia)"
},
"en_ca": {
"flag": "🇨🇦",
"name": "English (Canada)",
"native_name": "English (Canada)"
},
"en_gb": {
"flag": "🇬🇧",
"name": "English (UK)",
"native_name": "English (UK)",
},
"en_ie": {
"flag": "🇮🇪",
"name": "English (Ireland)",
"native_name": "English (Ireland)"
},
"en_in": {
"flag": "🇮🇳",
"name": "English (India)",
"native_name": "English (India)"
},
"en_nz": {
"flag": "🇳🇿",
"name": "English (New Zealand)",
"native_name": "English (New Zealand)"
},
"en_us": {
"flag": "🇺🇸",
"name": "English (US)",
"native_name": "English (US)",
},
"es": {
"flag": "🇪🇸",
"name": "Spanish",
"native_name": "Español"
},
"es_ar": {
"flag": "🇦🇷",
"name": "Spanish (Argentina)",
"native_name": "Español (Argentina)"
},
"es_mx": {
"flag": "🇲🇽",
"name": "Spanish (Mexico)",
"native_name": "Español (México)"
},
"et": {
"flag": "🇪🇪",
"name": "Estonian",
"native_name": "Eesti"
},
"fa": {
"flag": "🇮🇷",
"name": "Persian",
"native_name": "فارسی"
},
"ff": {
"flag": "🌍",
"name": "Fula",
"native_name": "Fulfulde"
},
"fi": {
"flag": "🇫🇮",
"name": "Finnish",
"native_name": "Suomi"
},
"fo": {
"flag": "🇫🇴",
"name": "Faroese",
"native_name": "føroyskt"
},
"fr": {
"flag": "🇫🇷",
"name": "French",
"native_name": "Français"
},
"fr_ca": {
"flag": "🇨🇦",
"name": "French (Canada)",
"native_name": "Français (Canada)"
},
"fr_ch": {
"flag": "🇨🇭",
"name": "French (Switzerland)",
"native_name": "Français (Suisse)"
},
"ga": {
"flag": "🇮🇪",
"name": "Irish",
"native_name": "Gaeilge"
},
"gl": {
"flag": "🇪🇸",
"name": "Galician",
"native_name": "Galego"
},
"gn": {
"flag": "🇵🇾",
"name": "Guarani",
"native_name": "Avañe'"
},
"gu": {
"flag": "🇮🇳",
"name": "Gujarati",
"native_name": "ગુજરાતી"
},
"ha": {
"flag": "🇳🇬",
"name": "Hausa",
"native_name": "هَوُسَ"
},
"he": {
"flag": "🇮🇱",
"name": "Hebrew",
"native_name": "עברית"
},
"hi": {
"flag": "🇮🇳",
"name": "Hindi",
"native_name": "हिन्दी"
},
"hr": {
"flag": "🇭🇷",
"name": "Croatian",
"native_name": "Hrvatski"
},
"ht": {
"flag": "🇭🇹",
"name": "Haitian Creole",
"native_name": "Kreyòl ayisyen"
},
"hu": {
"flag": "🇭🇺",
"name": "Hungarian",
"native_name": "Magyar"
},
"id": {
"flag": "🇮🇩",
"name": "Indonesian",
"native_name": "Bahasa Indonesia"
},
"io": {
"flag": "🌍",
"name": "Ido",
"native_name": "Ido"
},
"is": {
"flag": "🇮🇸",
"name": "Icelandic",
"native_name": "Íslenska"
},
"it": {
"flag": "🇮🇹",
"name": "Italian",
"native_name": "Italiano"
},
"ja": {
"flag": "🇯🇵",
"name": "Japanese",
"native_name": "日本語"
},
"jv_id": {
"flag": "🇮🇩",
"name": "Javanese (Indonesia)",
"native_name": "basa jawa"
},
"ka": {
"flag": "🇬🇪",
"name": "Georgian",
"native_name": "ქართული"
},
"kg": {
"flag": "🇨🇬",
"name": "Kongo",
"native_name": "KiKongo"
},
"kj": {
"flag": "🇳🇦",
"name": "Kuanyama",
"native_name": "Oshikwanyama"
},
"kk": {
"flag": "🇰🇿",
"name": "Kazakh",
"native_name": "Қазақ"
},
"kl": {
"flag": "🇬🇱",
"name": "Kalaallisut",
"native_name": "kalaallisut"
},
"km": {
"flag": "🇰🇭",
"name": "Khmer",
"native_name": "ខ្មែរ"
},
"ko": {
"flag": "🇰🇷",
"name": "Korean",
"native_name": "한국어"
},
"ks": {
"flag": "🇮🇳",
"name": "Kashmiri",
"native_name": "کٲشُر"
},
"ku": {
"flag": "🇮🇶",
"name": "Kurdish",
"native_name": "Kurdî",
},
"lo": {
"flag": "🇱🇦",
"name": "Lao",
"native_name": "ລາວ"
},
"lt": {
"flag": "🇱🇹",
"name": "Lithuanian",
"native_name": "Lietuvių"
},
"lv": {
"flag": "🇱🇻",
"name": "Latvian",
"native_name": "Latviešu"
},
"mg": {
"flag": "🇲🇬",
"name": "Malagasy",
"native_name": "Malagasy"
},
"mg_mg": {
"flag": "🇲🇬",
"name": "Malagasy (Madagascar)",
"native_name": "malagasy"
},
"mh": {
"flag": "🇲🇭",
"name": "Marshallese",
"native_name": "Kajin M̧ajeļ"
},
"mk": {
"flag": "🇲🇰",
"name": "Macedonian",
"native_name": "Македонски"
},
"mn_mn": {
"flag": "🇲🇳",
"name": "Mongolian (Mongolia)",
"native_name": "Монгол хэл"
},
"mr_in": {
"flag": "🇮🇳",
"name": "Marathi (India)",
"native_name": "मराठी"
},
"ms": {
"flag": "🇲🇾",
"name": "Malay",
"native_name": "Bahasa Melayu"
},
"my": {
"flag": "🇲🇲",
"name": "Burmese",
"native_name": "မြန်မာဘာသာ"
},
"na": {
"flag": "🇳🇷",
"name": "Nauruan",
"native_name": "Dorerin Naoero"
},
"nb": {
"flag": "🇳🇴",
"name": "Norwegian Bokmål",
"native_name": "Norsk Bokmål"
},
"ng": {
"flag": "🇳🇦",
"name": "Ndonga",
"native_name": "Oshindonga"
},
"nl": {
"flag": "🇳🇱",
"name": "Dutch",
"native_name": "Nederlands"
},
"om": {
"flag": "🇪🇹",
"name": "Oromo",
"native_name": "Afaan Oromoo"
},
"os": {
"flag": "🇷🇺",
"name": "Ossetian",
"native_name": "ирон æвзаг"
},
"pl": {
"flag": "🇵🇱",
"name": "Polish",
"native_name": "Polski"
},
"pt": {
"flag": "🇵🇹",
"name": "Portuguese",
"native_name": "Português"
},
"pt_br": {
"flag": "🇧🇷",
"name": "Portuguese (Brazil)",
"native_name": "Português (Brasil)"
},
"qu": {
"flag": "🇵🇪",
"name": "Quechua",
"native_name": "Runa Simi"
},
"ro": {
"flag": "🇷🇴",
"name": "Romanian",
"native_name": "Română"
},
"ru": {
"flag": "🇷🇺",
"name": "Russian",
"native_name": "Русский"
},
"rw": {
"flag": "🇷🇼",
"name": "Kinyarwanda",
"native_name": "Ikinyarwanda"
},
"sc": {
"flag": "🇮🇹",
"name": "Sardinian",
"native_name": "sardu"
},
"sg": {
"flag": "🇨🇫",
"name": "Sango",
"native_name": "yângâ tî sängö"
},
"sk": {
"flag": "🇸🇰",
"name": "Slovak",
"native_name": "Slovenčina"
},
"sl": {
"flag": "🇸🇮",
"name": "Slovenian",
"native_name": "Slovenščina"
},
"sm": {
"flag": "🇼🇸",
"name": "Samoan",
"native_name": "Gagana Samoa"
},
"sn": {
"flag": "🇿🇼",
"name": "Shona",
"native_name": "chiShona"
},
"so": {
"flag": "🇸🇴",
"name": "Somali",
"native_name": "Soomaaliga"
},
"sr": {
"flag": "🇷🇸",
"name": "Serbian",
"native_name": "Српски"
},
"ss": {
"flag": "🇸🇿",
"name": "Swati",
"native_name": "SiSwati"
},
"st": {
"flag": "🇱🇸",
"name": "Southern Sotho",
"native_name": "Sesotho"
},
"su_id": {
"flag": "🇮🇩",
"name": "Sundanese (Indonesia)",
"native_name": "basa sunda"
},
"sv": {
"flag": "🇸🇪",
"name": "Swedish",
"native_name": "Svenska"
},
"th": {
"flag": "🇹🇭",
"name": "Thai",
"native_name": "ไทย"
},
"tk": {
"flag": "🇹🇲",
"name": "Turkmen",
"native_name": "Türkmen"
},
"tn": {
"flag": "🇧🇼",
"name": "Tswana",
"native_name": "Setswana"
},
"to": {
"flag": "🇹🇴",
"name": "Tongan",
"native_name": "faka-Tonga"
},
"tr": {
"flag": "🇹🇷",
"name": "Turkish",
"native_name": "Türkçe",
},
"ts": {
"flag": "🇿🇦",
"name": "Tsonga",
"native_name": "Xitsonga"
},
"ts_zw": {
"flag": "🇿🇼",
"name": "Tsonga (Zimbabwe)",
"native_name": "xitsonga"
},
"ty": {
"flag": "🇵🇫",
"name": "Tahitian",
"native_name": "Reo Tahiti"
},
"uk": {
"flag": "🇺🇦",
"name": "Ukrainian",
"native_name": "Українська"
},
"ur": {
"flag": "🇵🇰",
"name": "Urdu",
"native_name": "اردو"
},
"uz": {
"flag": "🇺🇿",
"name": "Uzbek",
"native_name": "oʻzbek"
},
"ve": {
"flag": "🇿🇦",
"name": "Venda",
"native_name": "Tshivenda"
},
"vi": {
"flag": "🇻🇳",
"name": "Vietnamese",
"native_name": "Tiếng Việt"
},
"vo": {
"flag": "🌍",
"name": "Volapük",
"native_name": "Volapük"
},
"wa": {
"flag": "🇧🇪",
"name": "Walloon",
"native_name": "walon"
},
"xh": {
"flag": "🇿🇦",
"name": "Xhosa",
"native_name": "isiXhosa"
},
"yi": {
"flag": "🌍",
"name": "Yiddish",
"native_name": "ייִדיש"
},
"yo": {
"flag": "🇳🇬",
"name": "Yoruba",
"native_name": "Yorùbá"
},
"zh": {
"flag": "🇨🇳",
"name": "Chinese",
"native_name": "中文"
},
"zh_hk": {
"flag": "🇭🇰",
"name": "Chinese (Hong Kong)",
"native_name": "中文(香港)"
},
"zh_tw": {
"flag": "🇹🇼",
"name": "Chinese (Taiwan)",
"native_name": "中文(台灣)"
},
"zu": {
"flag": "🇿🇦",
"name": "Zulu",
"native_name": "isiZulu"
}
}
if True:
import toml
from pathlib import Path
def process_data_dict(data: dict, existing: dict):
for key, value in data.items():
if key not in existing:
existing[key] = value
continue
if isinstance(value, dict):
if isinstance(existing[key], dict):
process_data_dict(value, existing[key])
continue
existing[key] = value
def process_data(data: dict, obj):
for key, value in data.items():
if not hasattr(obj, key):
continue
existing = getattr(obj, key)
if isinstance(existing, dict):
process_data_dict(value, existing)
continue
if isinstance(value, dict):
process_data(value, getattr(obj, key))
continue
setattr(obj, key, value)
config_file = Path("stsg.toml")
if config_file.exists() and config_file.is_file():
with config_file.open("r") as f:
process_data(toml.loads(f.read()), config)

View File

@@ -5,9 +5,14 @@ import time
import os
import subprocess
from .config import SOURCE_DIRECTORY, CODE_DIRECTORY
from . import config
from .build import build as complete_build
CODE_DIRECTORY = "stsg"
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("stsg")
@@ -69,7 +74,7 @@ def build_on_change():
build()
observer = Observer()
observer.schedule(MarkdownChangeHandler(), path=SOURCE_DIRECTORY, recursive=True)
observer.schedule(MarkdownChangeHandler(), path=config.setup.source_directory, recursive=True)
observer.start()
try:
@@ -97,3 +102,4 @@ def hot_reload():
observer.stop()
observer.join()

View File

@@ -4,240 +4,277 @@ import shutil
from pathlib import Path
import os
import markdown
from typing import Optional, Union, Dict, Generator, List
from typing import Optional, Union, Dict, Generator, List, DefaultDict, Any
from bs4 import BeautifulSoup
from collections import defaultdict
import toml
from datetime import datetime
import jinja2
from .config import SOURCE_DIRECTORY, DIST_DIRECTORY, LANGUAGE_INFORMATION, ARTICLE_PREVIEW_LENGTH
from . import config
def replace_values(template: str, values: Dict[str, str]) -> str:
for key, value in values.items():
template = template.replace("{" + key + "}", value)
return template
def get_first_header_content(content, fallback: str = ""):
soup = BeautifulSoup(content, 'html.parser')
for level in range(1, 7):
header = soup.find(f'h{level}')
if header:
return header.get_text(strip=True)
return fallback
def shorten_text_and_clean(html_string, max_length=config.formatting.article_preview_length):
soup = BeautifulSoup(html_string, 'html.parser')
# Keep track of total characters added
total_chars = 0
finished = False
# Function to recursively trim and clean text
def process_element(element):
nonlocal total_chars, finished
for child in list(element.children):
if finished:
child.extract()
continue
if isinstance(child, str):
remaining = max_length - total_chars
if remaining <= 0:
child.extract()
finished = True
elif len(child) > remaining:
child.replace_with(child[:remaining] + '...')
total_chars = max_length
finished = True
else:
total_chars += len(child)
elif hasattr(child, 'children'):
process_element(child)
# Remove empty tags
if not child.text.strip():
child.decompose()
process_element(soup)
return str(soup)
def stem_to_language_code(stem: str) -> str:
language_code = stem.lower().replace("-", "_")
if language_code in config.languages:
return language_code
language_code = language_code.split("_")[0]
if language_code in config.languages:
return language_code
logger.error("Didn't recognize %s as a valid language code, add it to the config, or fix your structure.", stem)
exit(1)
class TemplateDict(dict):
def __init__(self, folder: Path):
self.folder = folder
super().__init__()
def __missing__(self, name: str) -> jinja2.Template:
f = self.folder / (name + ".html")
if not f.exists():
logger.error("no template with the name %s exists", name)
exit(1)
t = jinja2.Template(f.read_text())
self[name] = t
return t
TEMPLATE: Dict[str, jinja2.Template] = TemplateDict(Path(config.setup.source_directory, "templates"))
class LanguageDict(dict):
def __missing__(self, key: str):
if key not in config.languages:
raise KeyError(key)
lang_dict = config.languages[key]
lang_dict["priority"] = lang_dict.get("priority", 0)
elements = key.split("_")
if len(elements) > 1:
elements[-1] = elements[-1].upper()
lang_dict["code"] = "-".join(elements)
return lang_dict
LANGUAGES = LanguageDict()
class ArticleTranslation:
def __init__(self, file: Path, article: Article):
self.file = file
self.article = article
self.context: Dict[str, Any] = {}
# initializing the location of the article translation
self.language_code = stem_to_language_code(self.file.stem)
self.location_in_tree = [self.language_code, *self.article.location_in_tree]
self.url = "/" + "/".join(self.location_in_tree)
self.dist_path = Path(config.setup.dist_directory, *self.location_in_tree)
self.priority = LANGUAGES[self.language_code]["priority"]
self.real_language_code = LANGUAGES[self.language_code]["code"]
# TODO remove
self.article_content = self.file.read_text()
self.article_preview = self.article_content[:config.formatting.article_preview_length] + "..."
if self.file.suffix == ".md":
self.article_content = markdown.markdown(self.article_content)
self.article_preview = markdown.markdown(self.article_preview)
self.title = get_first_header_content(self.article_content, fallback="")
def __init_context__(self):
self.context["meta"] = self.article.context_shared
self.context["url"] = self.url
self.context["language"] = LANGUAGES[self.language_code]
self.context["article_url"] = self.article.url
html_content = self.file.read_text()
if self.file.suffix == ".md":
html_content = markdown.markdown(html_content)
self.context["title"] = get_first_header_content(html_content, fallback=LANGUAGES[self.language_code]["native_name"])
self.context["content"] = html_content
self.context["preview"] = shorten_text_and_clean(html_string=html_content)
def build(self):
self.dist_path.mkdir(parents=True, exist_ok=True)
with Path(self.dist_path, "index.html").open("w") as f:
f.write(TEMPLATE["article_translation"].render(self.context))
class Article:
def __init__(self, directory: Path, location_in_tree: Optional[List[str]] = None, is_root: bool = False, parent: Optional[Article] = None):
self.directory = directory
self.context: Dict[str, Any] = {}
self.context_shared: Dict[str, Any] = {}
if parent is not None:
self.context["parent"] = parent.context_shared
# initializing the config values of the article
config_file = self.directory / "index.toml"
self.config = toml.load(config_file) if config_file.exists() else {}
# initializing the location and slug of the article
self.slug = self.config.get("name", self.directory.name)
if self.slug in ARTICLE_LAKE:
logger.error("two articles have the same name at %s and %r", ARTICLE_LAKE[self.slug].directory, self.directory)
exit(1)
ARTICLE_LAKE[self.slug] = self
self.location_in_tree: List[str] = location_in_tree or []
if not is_root:
self.location_in_tree.append(self.slug)
self.url = "/" + "/".join(self.location_in_tree)
self.dist_path = Path(config.setup.dist_directory, *self.location_in_tree)
# build the tree
self.child_articles: List[Article] = []
self.article_translations_list: List[ArticleTranslation] = []
self.article_translations_map: Dict[str, ArticleTranslation] = {}
for c in self.directory.iterdir():
if c.name == "index.toml":
continue
if c.is_file():
at = ArticleTranslation(c, self)
self.article_translations_list.append(at)
self.article_translations_map[at.language_code] = at
elif c.is_dir():
self.child_articles.append(Article(
directory=c,
location_in_tree=self.location_in_tree.copy(),
parent=self,
))
self.article_translations_list.sort(key=lambda a: a.priority, reverse=True)
logger.info("found %s at %s with the translations %s", self.slug, ".".join(list(self.location_in_tree)), ",".join(self.article_translations_map.keys()))
def __init_context__(self):
self.context_shared["url"] = self.url
self.context_shared["slug"] = self.slug
modified_at = datetime.fromisoformat(self.config["datetime"]) if "datetime" in self.config else datetime.fromtimestamp(self.directory.stat().st_mtime)
self.context_shared["date"] = modified_at.strftime(config.formatting.datetime_format)
self.context_shared["iso_date"] = modified_at.isoformat()
self.context.update(self.context_shared)
# recursive context structures
translation_list = self.context["translations"] = []
child_article_list = self.context["children"] = []
for article_translation in self.article_translations_list:
self.context[article_translation.real_language_code] = article_translation.context
translation_list.append(article_translation.context)
for child_article in self.child_articles:
child_article_list.append(child_article.context)
# recursively build context
for at in self.article_translations_list:
at.__init_context__()
for a in self.child_articles:
a.__init_context__()
def build(self):
self.dist_path.mkdir(parents=True, exist_ok=True)
with Path(self.dist_path, "index.html").open("w") as f:
f.write(TEMPLATE["article"].render(self.context))
for at in self.article_translations_list:
at.build()
for ac in self.child_articles:
ac.build()
# GLOBALS
logger = logging.getLogger("stsg.build")
class CustomPath:
def __init__(self, path: Path):
self.path = path
def __repr__(self) -> str:
return str(self.path)
@property
def source_path(self) -> Path:
return Path(SOURCE_DIRECTORY, self.path)
@property
def dist_path(self) -> Path:
return Path(DIST_DIRECTORY, self.path)
@property
def name(self) -> str:
return Path(self.path).name
@property
def parent(self) -> CustomPath:
return CustomPath(Path(self.path).parent)
@property
def stem(self) -> str:
return Path(self.path).stem
def iterdir(self) -> Generator[CustomPath, None, None]:
for p in self.source_path.iterdir():
yield CustomPath(Path(self.path, p.name))
def get_child(self, name: str, force_directory: bool = False, force_file: bool = False) -> Optional[CustomPath]:
child = Path(self.source_path, name)
if not child.exists():
return None
if force_directory and not child.is_dir():
return None
if force_file and not child.is_file():
return None
return CustomPath(Path(self.path, name))
def read_text(self) -> str:
return self.source_path.read_text()
def copy_static(path: CustomPath):
src = path.source_path
dst = path.dist_path
if not src.exists():
logger.warning("The static folder '%s' wasn't defined.", src)
return
logger.info("Copying static files from '%s' to '%s'", src, dst)
dst.mkdir(parents=True, exist_ok=True)
for root, dirs, files in os.walk(src):
root_path = Path(root)
if any(part.startswith(".") for part in root_path.parts):
continue
rel_path = root_path.relative_to(src)
dest_dir = dst / rel_path
dest_dir.mkdir(parents=True, exist_ok=True)
for file in files:
if file.startswith("."):
continue
src_file = root_path / file
dest_file = dest_dir / file
shutil.copy2(src_file, dest_file)
class CustomLanguageCode:
def __init__(self, language_code: str):
self.language_code = language_code
def __repr__(self) -> str:
return f"{self.language_code}"
def _get_additional_data(self) -> dict:
parsed_language_code = self.language_code.lower().replace("-", "_")
if parsed_language_code in LANGUAGE_INFORMATION:
return LANGUAGE_INFORMATION[parsed_language_code]
parsed_language_code = parsed_language_code.split("_")[0]
if parsed_language_code in LANGUAGE_INFORMATION:
return LANGUAGE_INFORMATION[parsed_language_code]
return {}
@property
def flag(self) -> str:
return self._get_additional_data()["flag"]
@property
def native_name(self) -> str:
return self._get_additional_data()["native_name"]
class Article(CustomPath):
def __init__(self, path: CustomPath):
super().__init__(path.path)
def get_content(self) -> str:
if self.name.endswith(".md"):
return markdown.markdown(self.read_text())
return self.read_text()
@property
def article_directory(self) -> Path:
return self.dist_path.parent / self.stem
@property
def language_code(self) -> CustomLanguageCode:
return CustomLanguageCode(self.stem)
def get_article_keys(self) -> Dict[str, str]:
article_content = self.get_content()
return {
"article_content": article_content,
"article_preview": article_content[:ARTICLE_PREVIEW_LENGTH],
"article_href": "/" + str(self.path.parent / self.stem),
"article_language_name": self.language_code.native_name,
"article_language_code": self.language_code.language_code,
"article_language_flag": self.language_code.flag,
}
class Template:
def __init__(self, root: CustomPath):
self.root = root
self.articles: List[Article] = []
def copy(self):
return Template(root=self.root)
def _replace_keywords(self, text: str, **placeholder_values: str) -> str:
for key, value in placeholder_values.items():
text = text.replace("{" + key + "}", value)
return text
def get_article_template(self) -> str:
article = self.root.get_child("article.html", force_file=True)
return article.source_path.read_text() if article else "{article_content}"
def build_article(self, path: Article):
logger.info("converting %s", path)
article_text = self._replace_keywords(
self.get_article_template(),
**path.get_article_keys(),
)
path.article_directory.mkdir(parents=True, exist_ok=True)
with Path(path.article_directory, "index.html").open("w") as f:
f.write(article_text)
self.articles.append(path)
def get_overview_card_template(self) -> str:
overview_card = self.root.get_child("overview_card.html", force_file=True)
return overview_card.source_path.read_text() if overview_card else "<a href=\"{article_href}\"> {article_language_flag} {article_language_name} </a>"
def get_overview_template(self) -> str:
overview = self.root.get_child("overview.html", force_file=True)
return overview.source_path.read_text() if overview else "{overview_cards}"
def build_overview(self, root: CustomPath):
if not len(self.articles):
return
overview_card_template = self.get_overview_card_template()
overview_cards = "\n".join(self._replace_keywords(
overview_card_template,
**a.get_article_keys(),
) for a in self.articles)
overview_text = self._replace_keywords(
self.get_overview_template(),
overview_cards = overview_cards,
)
with Path(root.dist_path, "index.html").open("w") as f:
f.write(overview_text)
def walk_directory(root: CustomPath, template: Optional[Template] = None):
template_dir = root.get_child("_templates", force_directory=True)
if template_dir is not None:
template = Template(template_dir)
if template is None:
logger.error("Didn't find template for %d", root)
return
for current_path in root.iterdir():
if current_path.name.startswith("_") or current_path.name == "static":
continue
if current_path.source_path.is_file():
template.build_article(Article(current_path))
continue
if current_path.source_path.is_dir():
walk_directory(current_path, template=template.copy())
template.build_overview(root=root)
static_dir = root.get_child("static", force_directory=True)
if static_dir:
copy_static(static_dir)
ARTICLE_LAKE: Dict[str, Article] = {}
ARTICLE_REFERENCE_VALUES: DefaultDict[str, Dict[str, str]] = defaultdict(dict)
def build():
logger.info("building static page")
walk_directory(CustomPath(Path()))
logger.info("starting build process...")
logger.info("copying static folder...")
shutil.copytree(Path(config.setup.source_directory, "static"), Path(config.setup.dist_directory, "static"), dirs_exist_ok=True)
logger.info("building page tree...")
tree = Article(directory=Path(config.setup.source_directory, "articles"), is_root=True)
logger.info("compiling tree context...")
tree.__init_context__()
import json
with Path("context.json").open("w") as f:
json.dump(tree.context, f, indent=4)
logger.info("dumping page tree...")
tree.build()

View File

@@ -1,752 +0,0 @@
SOURCE_DIRECTORY = "src"
DIST_DIRECTORY = "dist"
# config template stuff
ARTICLE_PREVIEW_LENGTH = 200
# FOR DEVELOPMENT
CODE_DIRECTORY = "stsg"
# LANGUAGE INFORMATION
LANGUAGE_INFORMATION = {
"af": {
"flag": "🇿🇦",
"name": "Afrikaans",
"native_name": "Afrikaans"
},
"am": {
"flag": "🇪🇹",
"name": "Amharic",
"native_name": "አማርኛ"
},
"an": {
"flag": "🇪🇸",
"name": "Aragonese",
"native_name": "aragonés"
},
"ar": {
"flag": "🇸🇦",
"name": "Arabic",
"native_name": "العربية"
},
"ar_ae": {
"flag": "🇦🇪",
"name": "Arabic (UAE)",
"native_name": "العربية (الإمارات)"
},
"ar_bh": {
"flag": "🇧🇭",
"name": "Arabic (Bahrain)",
"native_name": "العربية (البحرين)"
},
"ar_dz": {
"flag": "🇩🇿",
"name": "Arabic (Algeria)",
"native_name": "العربية (الجزائر)"
},
"ar_eg": {
"flag": "🇪🇬",
"name": "Arabic (Egypt)",
"native_name": "العربية (مصر)"
},
"ar_iq": {
"flag": "🇮🇶",
"name": "Arabic (Iraq)",
"native_name": "العربية (العراق)"
},
"ar_jo": {
"flag": "🇯🇴",
"name": "Arabic (Jordan)",
"native_name": "العربية (الأردن)"
},
"ar_kw": {
"flag": "🇰🇼",
"name": "Arabic (Kuwait)",
"native_name": "العربية (الكويت)"
},
"ar_lb": {
"flag": "🇱🇧",
"name": "Arabic (Lebanon)",
"native_name": "العربية (لبنان)"
},
"ar_ly": {
"flag": "🇱🇾",
"name": "Arabic (Libya)",
"native_name": "العربية (ليبيا)"
},
"ar_ma": {
"flag": "🇲🇦",
"name": "Arabic (Morocco)",
"native_name": "العربية (المغرب)"
},
"ar_om": {
"flag": "🇴🇲",
"name": "Arabic (Oman)",
"native_name": "العربية (عُمان)"
},
"ar_qa": {
"flag": "🇶🇦",
"name": "Arabic (Qatar)",
"native_name": "العربية (قطر)"
},
"ar_sa": {
"flag": "🇸🇦",
"name": "Arabic (Saudi Arabia)",
"native_name": "العربية (السعودية)"
},
"ar_sd": {
"flag": "🇸🇩",
"name": "Arabic (Sudan)",
"native_name": "العربية (السودان)"
},
"ar_sy": {
"flag": "🇸🇾",
"name": "Arabic (Syria)",
"native_name": "العربية (سوريا)"
},
"ar_tn": {
"flag": "🇹🇳",
"name": "Arabic (Tunisia)",
"native_name": "العربية (تونس)"
},
"ar_ye": {
"flag": "🇾🇪",
"name": "Arabic (Yemen)",
"native_name": "العربية (اليمن)"
},
"ars_ae": {
"flag": "🇦🇪",
"name": "Najdi Arabic (UAE)",
"native_name": "نَجْدِيّ"
},
"ars_arab_sa": {
"flag": "🇸🇦",
"name": "Najdi Arabic (Saudi Arabia, Arabic Script)",
"native_name": "نَجْدِيّ"
},
"ars_sa": {
"flag": "🇸🇦",
"name": "Najdi Arabic (Saudi Arabia)",
"native_name": "نَجْدِيّ"
},
"as": {
"flag": "🇮🇳",
"name": "Assamese",
"native_name": "অসমীয়া"
},
"az": {
"flag": "🇦🇿",
"name": "Azerbaijani",
"native_name": "Azərbaycan"
},
"be": {
"flag": "🇧🇾",
"name": "Belarusian",
"native_name": "Беларуская"
},
"bg": {
"flag": "🇧🇬",
"name": "Bulgarian",
"native_name": "Български"
},
"bm": {
"flag": "🇲🇱",
"name": "Bambara",
"native_name": "bamanankan"
},
"bn": {
"flag": "🇧🇩",
"name": "Bengali",
"native_name": "বাংলা"
},
"bn_in": {
"flag": "🇮🇳",
"name": "Bengali (India)",
"native_name": "বাংলা (ভারত)"
},
"br": {
"flag": "🏴",
"name": "Breton",
"native_name": "brezhoneg"
},
"bs": {
"flag": "🇧🇦",
"name": "Bosnian",
"native_name": "Bosanski"
},
"ca": {
"flag": "🇪🇸",
"name": "Catalan",
"native_name": "Català"
},
"crh": {
"flag": "🇺🇦",
"name": "Crimean Tatar",
"native_name": "qırımtatarca"
},
"cs": {
"flag": "🇨🇿",
"name": "Czech",
"native_name": "Čeština"
},
"cv": {
"flag": "🇷🇺",
"name": "Chuvash",
"native_name": "чӑваш чӗлхи"
},
"cy": {
"flag": "🏴",
"name": "Welsh",
"native_name": "Cymraeg"
},
"da": {
"flag": "🇩🇰",
"name": "Danish",
"native_name": "Dansk"
},
"de": {
"flag": "🇩🇪",
"name": "German",
"native_name": "Deutsch"
},
"de_at": {
"flag": "🇦🇹",
"name": "German (Austria)",
"native_name": "Deutsch (Österreich)"
},
"de_be": {
"flag": "🇧🇪",
"name": "German (Belgium)",
"native_name": "Deutsch (Belgien)"
},
"de_ch": {
"flag": "🇨🇭",
"name": "German (Switzerland)",
"native_name": "Deutsch (Schweiz)"
},
"dv": {
"flag": "🇲🇻",
"name": "Dhivehi",
"native_name": "ދިވެހި"
},
"dz": {
"flag": "🇧🇹",
"name": "Dzongkha",
"native_name": "རྫོང་ཁ"
},
"el": {
"flag": "🇬🇷",
"name": "Greek",
"native_name": "Ελληνικά"
},
"en": {
"flag": "🇺🇸",
"name": "English",
"native_name": "English"
},
"en_au": {
"flag": "🇦🇺",
"name": "English (Australia)",
"native_name": "English (Australia)"
},
"en_ca": {
"flag": "🇨🇦",
"name": "English (Canada)",
"native_name": "English (Canada)"
},
"en_gb": {
"flag": "🇬🇧",
"name": "English (UK)",
"native_name": "English (UK)"
},
"en_ie": {
"flag": "🇮🇪",
"name": "English (Ireland)",
"native_name": "English (Ireland)"
},
"en_in": {
"flag": "🇮🇳",
"name": "English (India)",
"native_name": "English (India)"
},
"en_nz": {
"flag": "🇳🇿",
"name": "English (New Zealand)",
"native_name": "English (New Zealand)"
},
"en_us": {
"flag": "🇺🇸",
"name": "English (US)",
"native_name": "English (US)"
},
"es": {
"flag": "🇪🇸",
"name": "Spanish",
"native_name": "Español"
},
"es_ar": {
"flag": "🇦🇷",
"name": "Spanish (Argentina)",
"native_name": "Español (Argentina)"
},
"es_mx": {
"flag": "🇲🇽",
"name": "Spanish (Mexico)",
"native_name": "Español (México)"
},
"et": {
"flag": "🇪🇪",
"name": "Estonian",
"native_name": "Eesti"
},
"fa": {
"flag": "🇮🇷",
"name": "Persian",
"native_name": "فارسی"
},
"ff": {
"flag": "🌍",
"name": "Fula",
"native_name": "Fulfulde"
},
"fi": {
"flag": "🇫🇮",
"name": "Finnish",
"native_name": "Suomi"
},
"fo": {
"flag": "🇫🇴",
"name": "Faroese",
"native_name": "føroyskt"
},
"fr": {
"flag": "🇫🇷",
"name": "French",
"native_name": "Français"
},
"fr_ca": {
"flag": "🇨🇦",
"name": "French (Canada)",
"native_name": "Français (Canada)"
},
"fr_ch": {
"flag": "🇨🇭",
"name": "French (Switzerland)",
"native_name": "Français (Suisse)"
},
"ga": {
"flag": "🇮🇪",
"name": "Irish",
"native_name": "Gaeilge"
},
"gl": {
"flag": "🇪🇸",
"name": "Galician",
"native_name": "Galego"
},
"gn": {
"flag": "🇵🇾",
"name": "Guarani",
"native_name": "Avañe'"
},
"gu": {
"flag": "🇮🇳",
"name": "Gujarati",
"native_name": "ગુજરાતી"
},
"ha": {
"flag": "🇳🇬",
"name": "Hausa",
"native_name": "هَوُسَ"
},
"he": {
"flag": "🇮🇱",
"name": "Hebrew",
"native_name": "עברית"
},
"hi": {
"flag": "🇮🇳",
"name": "Hindi",
"native_name": "हिन्दी"
},
"hr": {
"flag": "🇭🇷",
"name": "Croatian",
"native_name": "Hrvatski"
},
"ht": {
"flag": "🇭🇹",
"name": "Haitian Creole",
"native_name": "Kreyòl ayisyen"
},
"hu": {
"flag": "🇭🇺",
"name": "Hungarian",
"native_name": "Magyar"
},
"id": {
"flag": "🇮🇩",
"name": "Indonesian",
"native_name": "Bahasa Indonesia"
},
"io": {
"flag": "🌍",
"name": "Ido",
"native_name": "Ido"
},
"is": {
"flag": "🇮🇸",
"name": "Icelandic",
"native_name": "Íslenska"
},
"it": {
"flag": "🇮🇹",
"name": "Italian",
"native_name": "Italiano"
},
"ja": {
"flag": "🇯🇵",
"name": "Japanese",
"native_name": "日本語"
},
"jv_id": {
"flag": "🇮🇩",
"name": "Javanese (Indonesia)",
"native_name": "basa jawa"
},
"ka": {
"flag": "🇬🇪",
"name": "Georgian",
"native_name": "ქართული"
},
"kg": {
"flag": "🇨🇬",
"name": "Kongo",
"native_name": "KiKongo"
},
"kj": {
"flag": "🇳🇦",
"name": "Kuanyama",
"native_name": "Oshikwanyama"
},
"kk": {
"flag": "🇰🇿",
"name": "Kazakh",
"native_name": "Қазақ"
},
"kl": {
"flag": "🇬🇱",
"name": "Kalaallisut",
"native_name": "kalaallisut"
},
"km": {
"flag": "🇰🇭",
"name": "Khmer",
"native_name": "ខ្មែរ"
},
"ko": {
"flag": "🇰🇷",
"name": "Korean",
"native_name": "한국어"
},
"ks": {
"flag": "🇮🇳",
"name": "Kashmiri",
"native_name": "کٲشُر"
},
"ku": {
"flag": "🇮🇶",
"name": "Kurdish",
"native_name": "Kurdî"
},
"lo": {
"flag": "🇱🇦",
"name": "Lao",
"native_name": "ລາວ"
},
"lt": {
"flag": "🇱🇹",
"name": "Lithuanian",
"native_name": "Lietuvių"
},
"lv": {
"flag": "🇱🇻",
"name": "Latvian",
"native_name": "Latviešu"
},
"mg": {
"flag": "🇲🇬",
"name": "Malagasy",
"native_name": "Malagasy"
},
"mg_mg": {
"flag": "🇲🇬",
"name": "Malagasy (Madagascar)",
"native_name": "malagasy"
},
"mh": {
"flag": "🇲🇭",
"name": "Marshallese",
"native_name": "Kajin M̧ajeļ"
},
"mk": {
"flag": "🇲🇰",
"name": "Macedonian",
"native_name": "Македонски"
},
"mn_mn": {
"flag": "🇲🇳",
"name": "Mongolian (Mongolia)",
"native_name": "Монгол хэл"
},
"mr_in": {
"flag": "🇮🇳",
"name": "Marathi (India)",
"native_name": "मराठी"
},
"ms": {
"flag": "🇲🇾",
"name": "Malay",
"native_name": "Bahasa Melayu"
},
"my": {
"flag": "🇲🇲",
"name": "Burmese",
"native_name": "မြန်မာဘာသာ"
},
"na": {
"flag": "🇳🇷",
"name": "Nauruan",
"native_name": "Dorerin Naoero"
},
"nb": {
"flag": "🇳🇴",
"name": "Norwegian Bokmål",
"native_name": "Norsk Bokmål"
},
"ng": {
"flag": "🇳🇦",
"name": "Ndonga",
"native_name": "Oshindonga"
},
"nl": {
"flag": "🇳🇱",
"name": "Dutch",
"native_name": "Nederlands"
},
"om": {
"flag": "🇪🇹",
"name": "Oromo",
"native_name": "Afaan Oromoo"
},
"os": {
"flag": "🇷🇺",
"name": "Ossetian",
"native_name": "ирон æвзаг"
},
"pl": {
"flag": "🇵🇱",
"name": "Polish",
"native_name": "Polski"
},
"pt": {
"flag": "🇵🇹",
"name": "Portuguese",
"native_name": "Português"
},
"pt_br": {
"flag": "🇧🇷",
"name": "Portuguese (Brazil)",
"native_name": "Português (Brasil)"
},
"qu": {
"flag": "🇵🇪",
"name": "Quechua",
"native_name": "Runa Simi"
},
"ro": {
"flag": "🇷🇴",
"name": "Romanian",
"native_name": "Română"
},
"ru": {
"flag": "🇷🇺",
"name": "Russian",
"native_name": "Русский"
},
"rw": {
"flag": "🇷🇼",
"name": "Kinyarwanda",
"native_name": "Ikinyarwanda"
},
"sc": {
"flag": "🇮🇹",
"name": "Sardinian",
"native_name": "sardu"
},
"sg": {
"flag": "🇨🇫",
"name": "Sango",
"native_name": "yângâ tî sängö"
},
"sk": {
"flag": "🇸🇰",
"name": "Slovak",
"native_name": "Slovenčina"
},
"sl": {
"flag": "🇸🇮",
"name": "Slovenian",
"native_name": "Slovenščina"
},
"sm": {
"flag": "🇼🇸",
"name": "Samoan",
"native_name": "Gagana Samoa"
},
"sn": {
"flag": "🇿🇼",
"name": "Shona",
"native_name": "chiShona"
},
"so": {
"flag": "🇸🇴",
"name": "Somali",
"native_name": "Soomaaliga"
},
"sr": {
"flag": "🇷🇸",
"name": "Serbian",
"native_name": "Српски"
},
"ss": {
"flag": "🇸🇿",
"name": "Swati",
"native_name": "SiSwati"
},
"st": {
"flag": "🇱🇸",
"name": "Southern Sotho",
"native_name": "Sesotho"
},
"su_id": {
"flag": "🇮🇩",
"name": "Sundanese (Indonesia)",
"native_name": "basa sunda"
},
"sv": {
"flag": "🇸🇪",
"name": "Swedish",
"native_name": "Svenska"
},
"th": {
"flag": "🇹🇭",
"name": "Thai",
"native_name": "ไทย"
},
"tk": {
"flag": "🇹🇲",
"name": "Turkmen",
"native_name": "Türkmen"
},
"tn": {
"flag": "🇧🇼",
"name": "Tswana",
"native_name": "Setswana"
},
"to": {
"flag": "🇹🇴",
"name": "Tongan",
"native_name": "faka-Tonga"
},
"tr": {
"flag": "🇹🇷",
"name": "Turkish",
"native_name": "Türkçe"
},
"ts": {
"flag": "🇿🇦",
"name": "Tsonga",
"native_name": "Xitsonga"
},
"ts_zw": {
"flag": "🇿🇼",
"name": "Tsonga (Zimbabwe)",
"native_name": "xitsonga"
},
"ty": {
"flag": "🇵🇫",
"name": "Tahitian",
"native_name": "Reo Tahiti"
},
"uk": {
"flag": "🇺🇦",
"name": "Ukrainian",
"native_name": "Українська"
},
"ur": {
"flag": "🇵🇰",
"name": "Urdu",
"native_name": "اردو"
},
"uz": {
"flag": "🇺🇿",
"name": "Uzbek",
"native_name": "oʻzbek"
},
"ve": {
"flag": "🇿🇦",
"name": "Venda",
"native_name": "Tshivenda"
},
"vi": {
"flag": "🇻🇳",
"name": "Vietnamese",
"native_name": "Tiếng Việt"
},
"vo": {
"flag": "🌍",
"name": "Volapük",
"native_name": "Volapük"
},
"wa": {
"flag": "🇧🇪",
"name": "Walloon",
"native_name": "walon"
},
"xh": {
"flag": "🇿🇦",
"name": "Xhosa",
"native_name": "isiXhosa"
},
"yi": {
"flag": "🌍",
"name": "Yiddish",
"native_name": "ייִדיש"
},
"yo": {
"flag": "🇳🇬",
"name": "Yoruba",
"native_name": "Yorùbá"
},
"zh": {
"flag": "🇨🇳",
"name": "Chinese",
"native_name": "中文"
},
"zh_hk": {
"flag": "🇭🇰",
"name": "Chinese (Hong Kong)",
"native_name": "中文(香港)"
},
"zh_tw": {
"flag": "🇹🇼",
"name": "Chinese (Taiwan)",
"native_name": "中文(台灣)"
},
"zu": {
"flag": "🇿🇦",
"name": "Zulu",
"native_name": "isiZulu"
}
}