Commit 0d3b4674 authored by lunasorcery's avatar lunasorcery
Browse files

first pass at preparing for localization support

parent 72381ce6
......@@ -2,7 +2,7 @@
<div class="content">
<div class="content-thing slim">
<div class="text">
<span class="question">What is Executable Graphics?</span>
<span class="question">{{i18n.about.what-is-exegfx}}</span>
<div class="answer">
<p>
In the purest sense, Executable Graphics is visual artwork through the medium of software.
......@@ -18,7 +18,7 @@
</p>
</div>
<br/>
<span class="question">What is executable.graphics?</span>
<span class="question">{{i18n.about.what-is-exe-dot-gfx}}</span>
<div class="answer">
<p>
This website presents a gallery of artworks from various artists in the Executable Graphics scene.
......
......@@ -19,6 +19,15 @@ def crc32_file(filename):
return f"{buf:08x}"
# WIP features
enableLanguageDropdown = False
enableDebugLanguage = False
with open('i18n.json') as file:
i18n = [lang for lang in json.load(file) if lang.get('enabled', True)]
# load prods
with open('prods.json') as file:
prods = [prod for prod in json.load(file) if not prod.get('hidden', False)]
......@@ -76,11 +85,11 @@ for idx,prod in enumerate(prods):
print(f"- {slug}")
if os.path.exists(dst_jpg):
prods[idx]['image_url'] = f"img/{slug}.jpg"
prods[idx]['image_url'] = f"/img/{slug}.jpg"
print(" already exists, skipping...")
continue
elif os.path.exists(dst_png):
prods[idx]['image_url'] = f"img/{slug}.png"
prods[idx]['image_url'] = f"/img/{slug}.png"
print(" already exists, skipping...")
continue
......@@ -106,18 +115,46 @@ for idx,prod in enumerate(prods):
# ...and use the smaller one
print(f" src_png: {os.path.getsize(src_png): 10,}b")
if os.path.getsize(dst_jpg) < os.path.getsize(src_png):
prods[idx]['image_url'] = f"img/{slug}.jpg"
prods[idx]['image_url'] = f"/img/{slug}.jpg"
print(f" dst_jpg: {os.path.getsize(dst_jpg): 10,}b")
else:
os.remove(dst_jpg)
shutil.copyfile(src_png, dst_png)
prods[idx]['image_url'] = f"img/{slug}.png"
prods[idx]['image_url'] = f"/img/{slug}.png"
print(f" dst_png: {os.path.getsize(dst_png): 10,}b")
else:
print(" missing raw image!")
raise hell
# add debug lang
if enableDebugLanguage:
devLang = {
'id': "i18n-debug",
'name': "Debug",
'enabled': False,
'data': {}
}
for key in i18n[0]['data'].keys():
devLang['data'][key] = {}
for key2 in i18n[0]['data'][key].keys():
devLang['data'][key][key2] = f"[[{key}.{key2}]]"
i18n.append(devLang)
# prepare language dropdown
langDropdown = []
for lang in i18n:
dropdownEntry = {}
dropdownEntry['lang-name'] = lang['name']
if lang['id'] == 'en':
dropdownEntry['lang-root'] = ""
else:
dropdownEntry['lang-root'] = f"/{lang['id']}"
langDropdown.append(dropdownEntry)
sharedTemplate = {
'meta-title': "executable.graphics",
'meta-image': prods[0]['image_url'],
......@@ -129,32 +166,44 @@ sharedTemplate = {
'hash-favicon-ico': crc32_file('gen/favicon.ico'),
'hash-apple-touch-icon-png': crc32_file('gen/apple-touch-icon.png'),
'hash-manifest-json': crc32_file('manifest.json'),
'svg-globe': open('globe.svg').read(),
'svg-moon': open('moon.svg').read(),
'enable-language-dropdown': enableLanguageDropdown,
'languages': langDropdown
}
print("applying templates...")
with open('index.mustache', 'r') as f:
with open('gen/index.html', 'w') as fout:
fout.write(chevron.render(f, sharedTemplate | {
'meta-description': "A curated gallery of 4K Executable Graphics works from the demoscene.",
'meta-twitter-card-type': "summary_large_image",
'page-gallery': True,
'entries': prods }))
with open('meteoriks.mustache', 'r') as f:
with open('gen/meteoriks.html', 'w') as fout:
fout.write(chevron.render(f, sharedTemplate | {
'meta-subtitle': "Meteoriks",
'meta-description': "Nominees and winners of the 'Best Executable Graphics' Meteorik award.",
'meta-twitter-card-type': "summary",
'meta-image': meteorikProds[0]['image_url'],
'page-meteoriks': True,
'entries': meteorikProds }))
with open('about.mustache', 'r') as f:
with open('gen/about.html', 'w') as fout:
fout.write(chevron.render(f, sharedTemplate | {
'meta-subtitle': "About",
'meta-description': "What is Executable Graphics?",
'meta-twitter-card-type': "summary",
'page-about': True }))
for lang in i18n:
outdir = 'gen'
if lang['id'] != 'en':
outdir += '/' + lang['id']
maybe_mkdir(outdir)
langTemplate = { 'i18n': lang['data'] }
with open('index.mustache', 'r') as f:
with open(f"{outdir}/index.html", 'w') as fout:
fout.write(chevron.render(f, sharedTemplate | langTemplate | {
'meta-description': "A curated gallery of 4K Executable Graphics works from the demoscene.",
'meta-twitter-card-type': "summary_large_image",
'page-gallery': True,
'entries': prods }))
with open('meteoriks.mustache', 'r') as f:
with open(f"{outdir}/meteoriks.html", 'w') as fout:
fout.write(chevron.render(f, sharedTemplate | langTemplate | {
'meta-subtitle': "Meteoriks",
'meta-description': "Nominees and winners of the 'Best Executable Graphics' Meteorik award.",
'meta-twitter-card-type': "summary",
'meta-image': meteorikProds[0]['image_url'],
'page-meteoriks': True,
'entries': meteorikProds }))
with open('about.mustache', 'r') as f:
with open(f"{outdir}/about.html", 'w') as fout:
fout.write(chevron.render(f, sharedTemplate | langTemplate | {
'meta-subtitle': "About",
'meta-description': "What is Executable Graphics?",
'meta-twitter-card-type': "summary",
'page-about': True }))
<svg width="28" height="28" xmlns="http://www.w3.org/2000/svg">
<defs>
<mask id="mask-globe">
<rect width="100%" height="100%" fill="black"/>
<circle r="50%" cx="50%" cy="50%" fill="white"/>
</mask>
</defs>
<g stroke="currentColor" stroke-width="1px" fill="none" >
<circle r="13.5px" cx="50%" cy="50%" />
<ellipse rx="6.75px" ry="13.5px" cx="14px" cy="14px" />
<line x1="0%" y1="50%" x2="100%" y2="50%" />
<line x1="50%" y1="0%" x2="50%" y2="100%" />
<circle r="100%" cx="50%" cy="-70%" mask="url(#mask-globe)" />
<circle r="100%" cx="50%" cy="170%" mask="url(#mask-globe)" />
</g>
</svg>
\ No newline at end of file
......@@ -9,42 +9,59 @@
<meta name="twitter:title" content="{{meta-title}}{{#meta-subtitle}} | {{meta-subtitle}}{{/meta-subtitle}}"/>
<meta name="apple-mobile-web-app-title" content="exe.gfx"/>
<link rel="manifest" href="manifest.json?cache={{hash-manifest-json}}"/>
<link rel="manifest" href="/manifest.json?cache={{hash-manifest-json}}"/>
<link rel="shortcut icon" href="favicon.ico?cache={{hash-favicon-ico}}"/>
<link rel="apple-touch-icon" href="apple-touch-icon.png?cache={{hash-apple-touch-icon-png}}"/>
<link rel="image_src" href="apple-touch-icon.png?cache={{hash-apple-touch-icon-png}}"/>
<link rel="shortcut icon" href="/favicon.ico?cache={{hash-favicon-ico}}"/>
<link rel="apple-touch-icon" href="/apple-touch-icon.png?cache={{hash-apple-touch-icon-png}}"/>
<link rel="image_src" href="/apple-touch-icon.png?cache={{hash-apple-touch-icon-png}}"/>
<meta name="description" content="{{meta-description}}"/>
<meta property="og:description" content="{{meta-description}}"/>
<meta name="twitter:description" content="{{meta-description}}"/>
<meta property="og:image" content="https://executable.graphics/{{meta-image}}"/>
<meta name="twitter:image" content="https://executable.graphics/{{meta-image}}"/>
<meta property="og:image" content="{{meta-image}}"/>
<meta name="twitter:image" content="{{meta-image}}"/>
<meta name="twitter:card" content="{{meta-twitter-card-type}}"/>
<meta name="twitter:creator" content="@lunasorcery"/>
<link href="fonts.css?cache={{hash-fonts-css}}" rel="stylesheet"/>
<link href="style.css?cache={{hash-style-css}}" rel="stylesheet"/>
<link href="/fonts.css?cache={{hash-fonts-css}}" rel="stylesheet"/>
<link href="/style.css?cache={{hash-style-css}}" rel="stylesheet"/>
</head>
<body>
<label class="theme-switch" for="checkbox">
<span>toggle theme</span>
<svg width="28" height="28" alt="Toggle Light/Dark Theme" xmlns="http://www.w3.org/2000/svg">
<defs>
<mask id="hole">
<rect width="100%" height="100%" fill="white"/>
<circle r="40%" cx="32%" cy="32%" fill="black"/>
</mask>
</defs>
<circle r="50%" cx="50%" cy="50%" mask="url(#hole)" fill="currentColor"/>
</svg>
<input type="checkbox" id="checkbox" />
</label>
<script src="script.js"></script>
{{#enable-language-dropdown}}
<div class="switch switch-left switch-lang" tabindex="0">
<label class="switch-label switch-label-left">
{{{svg-globe}}}
<span>{{i18n.global.toggle-lang}}</span>
</label>
<div class="mousetrap">
<ul class="dropdown">
{{#languages}}
{{#page-gallery}}
<a href="{{lang-root}}/" class="item" tabindex="0">{{lang-name}}</a>
{{/page-gallery}}
{{#page-meteoriks}}
<a href="{{lang-root}}/meteoriks.html" class="item" tabindex="0">{{lang-name}}</a>
{{/page-meteoriks}}
{{#page-about}}
<a href="{{lang-root}}/about.html" class="item" tabindex="0">{{lang-name}}</a>
{{/page-about}}
{{/languages}}
</ul>
</div>
</div>
{{/enable-language-dropdown}}
<div class="navbar">
<a href="/" class="entry{{#page-gallery}} active{{/page-gallery}}">Gallery</a>
<a href="meteoriks.html" class="entry{{#page-meteoriks}} active{{/page-meteoriks}}">Meteoriks</a>
<a href="about.html" class="entry{{#page-about}} active{{/page-about}}">About</a>
<a href="." class="entry{{#page-gallery}} active{{/page-gallery}}">{{i18n.nav.gallery}}</a>
<a href="meteoriks.html" class="entry{{#page-meteoriks}} active{{/page-meteoriks}}">{{i18n.nav.meteoriks}}</a>
<a href="about.html" class="entry{{#page-about}} active{{/page-about}}">{{i18n.nav.about}}</a>
</div>
<div class="switch switch-right switch-theme" tabindex="0">
<label class="switch-label switch-label-right" for="theme-checkbox">
<span>{{i18n.global.toggle-theme}}</span>
{{{svg-moon}}}
<input type="checkbox" id="theme-checkbox" />
</label>
</div>
<script src="/script.js"></script>
[
{
"id": "en",
"name": "English",
"enabled": true,
"data": {
"global": {
"toggle-theme": "Toggle Theme",
"toggle-lang": "Language"
},
"nav": {
"gallery": "Gallery",
"meteoriks": "Meteoriks",
"about": "About"
},
"meteoriks": {
"what-are-meteoriks": "What are the Meteoriks?"
},
"about": {
"what-is-exegfx": "What is Executable Graphics?",
"what-is-exe-dot-gfx": "What is executable.graphics?"
}
}
},
{
"id": "es",
"name": "Español",
"enabled": false,
"data": { }
},
{
"id": "jp",
"name": "日本語",
"enabled": false,
"data": { }
}
]
\ No newline at end of file
......@@ -2,7 +2,7 @@
<div class="content">
<div class="content-thing slim">
<div class="text">
<span class="question">What are the Meteoriks?</span>
<span class="question">{{i18n.meteoriks.what-are-meteoriks}}</span>
<div class="answer">
<p>
<a href="https://meteoriks.org/">The Meteoriks</a> are an award to honor the best productions that the demoscene has to offer, and are presented annually at the Revision demoparty over the Easter weekend.
......
<svg width="28" height="28" xmlns="http://www.w3.org/2000/svg">
<defs>
<mask id="mask-moon">
<rect width="100%" height="100%" fill="white"/>
<circle r="40%" cx="32%" cy="32%" fill="black"/>
</mask>
</defs>
<circle r="50%" cx="50%" cy="50%" mask="url(#mask-moon)" fill="currentColor"/>
</svg>
\ No newline at end of file
chevron>=0.14.0
\ No newline at end of file
#!/bin/bash
python3 -m http.server -d gen/
// adapted from https://dev.to/ananyaneogi/create-a-dark-light-mode-switch-with-css-variables-34l8
const toggleSwitch = document.querySelector('input[type="checkbox"]#checkbox');
const toggleSwitch = document.querySelector('input[type="checkbox"]#theme-checkbox');
const userDefaultTheme = (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light')
const currentTheme = localStorage?.getItem('theme') ?? userDefaultTheme;
......
......@@ -13,6 +13,14 @@
--darkmode-meteorik-nominee-bg-color: #555555;
--darkmode-meteorik-winner-bg-color: #645020;
--lightmode-dropdown-bg-color: #ffffff;
--lightmode-dropdown-divider-color: #d0d0d0;
--lightmode-dropdown-hover-color: #f0f0f0;
--darkmode-dropdown-bg-color: #404040;
--darkmode-dropdown-divider-color: #101010;
--darkmode-dropdown-hover-color: #383838;
--main-text-color: var(--lightmode-main-text-color);
--panel-bg-color: var(--lightmode-panel-bg-color);
--main-bg-color: var(--lightmode-main-bg-color);
......@@ -20,7 +28,14 @@
--meteorik-nominee-bg-color: var(--lightmode-meteorik-nominee-bg-color);
--meteorik-winner-bg-color: var(--lightmode-meteorik-winner-bg-color);
--dropdown-bg-color: var(--lightmode-dropdown-bg-color);
--dropdown-divider-color: var(--lightmode-dropdown-divider-color);
--dropdown-hover-color: var(--lightmode-dropdown-hover-color);
--image-margin: 0px;
--theme-transition-duration: 150ms;
--theme-transition-easing: ease-in-out;
}
[data-theme="dark"] {
......@@ -30,15 +45,14 @@
--meteorik-nominee-bg-color: var(--darkmode-meteorik-nominee-bg-color);
--meteorik-winner-bg-color: var(--darkmode-meteorik-winner-bg-color);
--dropdown-bg-color: var(--darkmode-dropdown-bg-color);
--dropdown-divider-color: var(--darkmode-dropdown-divider-color);
--dropdown-hover-color: var(--darkmode-dropdown-hover-color);
}
* {
box-sizing: border-box;
color: var(--main-text-color);
transition:
color 200ms ease-in-out,
background-color 200ms ease-in-out;
}
html {
......@@ -52,6 +66,7 @@ body {
background: var(--main-bg-color);
text-align: center;
padding: 12px;
transition: background-color var(--theme-transition-duration) var(--theme-transition-easing);
}
a {
......@@ -77,53 +92,118 @@ a {
.navbar .entry {
padding: 8px 16px;
color: var(--main-text-color);
transition: color var(--theme-transition-duration) var(--theme-transition-easing);
}
.navbar .entry.active {
font-weight: 600;
}
.navbar .entry.active,
.navbar .entry:hover {
.navbar .entry:hover,
.navbar .entry:focus {
border-bottom: 1px solid var(--main-text-color);
transition:
color var(--theme-transition-duration) var(--theme-transition-easing),
border-color var(--theme-transition-duration) var(--theme-transition-easing);
}
.theme-switch {
.switch {
position: absolute;
top: 12px;
right: 12px;
padding: 4px;
}
.switch-label {
/* vertically center text */
display: flex;
align-items: center;
cursor: pointer;
text-transform: lowercase;
/*align text*/
display: flex;
align-items: center;
color: var(--main-text-color);
transition: color var(--theme-transition-duration) var(--theme-transition-easing);
}
.theme-switch span {
padding-right: 8px;
.switch span {
opacity: 0;
transition: opacity 200ms ease-in-out;
}
.theme-switch:hover span {
.switch:hover span,
.switch:focus-within span,
.switch:focus span {
opacity: 0.75;
}
.theme-switch input {
.switch input {
display: none;
}
.switch-right { right: 12px; }
.switch-left { left: 12px; }
.switch-label-right span { padding-right: 8px; }
.switch-label-left span { padding-left: 8px; }
/* CSS-only dropdown */
/* loosely based on https://stackoverflow.com/a/20058341 */
/* with accessibility notes from https://css-tricks.com/solved-with-css-dropdown-menus/ */
.switch .mousetrap {
display: none;
position: absolute;
top: 0px;
left: 0px;
min-width: 100%;
padding-top: 36px;
padding-left: 8px;
}
.switch .mousetrap:hover,
.switch:active .mousetrap,
.switch:focus .mousetrap,
.switch:focus-within .mousetrap {
display: block;
}
.switch .dropdown {
margin: 0;
padding: 0;
border-radius: 8px;
overflow: hidden;
box-shadow:
0px 1px 2px rgba(0,0,0,0.3),
0px 2px 5px rgba(0,0,0,0.05),
0px 8px 12px rgba(0,0,0,0.15);
}
.switch .dropdown .item {
display: block;
background-color: var(--dropdown-bg-color);
list-style: none;
color: var(--main-text-color);
padding: 8px 12px;
border-bottom: 1px solid var(--dropdown-divider-color);
}
.switch .dropdown .item:last-child {
border: none;
}
.switch .dropdown .item:hover,
.switch .dropdown .item:focus {
background: var(--dropdown-hover-color);
outline: none;
}
.content-thing {
text-align: left;
font-size: 12pt;
margin: 0 auto 32px;
background: var(--panel-bg-color);
background-color: var(--panel-bg-color);
box-shadow:
0px 1px 2px rgba(0,0,0,0.3),
0px 2px 5px rgba(0,0,0,0.05);
width: fit-content;
width: -moz-fit-content;
max-width: 100%;
transition: background-color var(--theme-transition-duration) var(--theme-transition-easing);
}
.content-thing.slim {
max-width: 760px;
......@@ -144,6 +224,10 @@ a {
box-shadow:
0px 1px 2px rgba(0,0,0,0.3),
0px 2px 5px rgba(0,0,0,0.05);
color: var(--main-text-color);
transition:
color var(--theme-transition-duration) var(--theme-transition-easing),
background-color var(--theme-transition-duration) var(--theme-transition-easing);
}
.content-thing .meteorik-nominee {
background: var(--meteorik-nominee-bg-color);
......@@ -156,6 +240,12 @@ a {
padding: 12px;
}
.content-thing .text,
.content-thing .text a {
color: var(--main-text-color);
transition: color var(--theme-transition-duration) var(--theme-transition-easing);
}
.content-thing .text .author {
font-weight: 400;
}
......@@ -174,7 +264,8 @@ a {
.content-thing .text > *:last-child {
margin-bottom: 0;
}
.content-thing:hover .text .title {
.content-thing:hover .text .title,
.content-thing a:focus .text .title {
text-decoration: underline;
}
......@@ -201,12 +292,15 @@ a {
text-decoration-style: dotted;
text-decoration-thickness: from-font;
}
.content-thing .text p a[href]:hover {
.content-thing .text p a[href]:hover,
.content-thing .text p a[href]:focus {
text-decoration-style: solid;
}
.footer {
padding-bottom: 24px;
color: var(--main-text-color);
transition: color var(--theme-transition-duration) var(--theme-transition-easing);
}
@media only screen and (max-width: 600px) {
......@@ -216,7 +310,7 @@ a {
* {
transition: none;
}
.theme-switch span {
.switch-label span {
display: none;
}
......@@ -246,7 +340,7 @@ a {
/* disable animated effects for users who prefer reduced motion */
@media (prefers-reduced-motion) {
* {
transition: none;
transition: none !important;
}
}
......@@ -282,6 +376,9 @@ a {
--main-text-color: #ffffff;
--panel-bg-color: #446688;
--main-bg-color: #3a6ea5;
--dropdown-bg-color: #224488;
--dropdown-hover-color: #224488;
--dropdown-divider-color: #000000;
font-family: 'Tahoma', 'Verdana', 'Arial', 'Helvetica', sans-serif;
font-size: 9pt;
}
......@@ -289,9 +386,36 @@ a {
padding: 12px 0px;
background: var(--main-bg-color) url('https://content.pouet.net/styles/001/gfx/trumpet.svg');
}
[data-theme="pouet"] .theme-switch {
[data-theme="pouet"] .switch-theme {
display: none;
}
[data-theme="pouet"] .switch-lang svg {
display: none;
}
[data-theme="pouet"] .switch-lang span {
opacity: 1;
font-weight: bold;
text-transform: none;
display: inline-block !important;
}
[data-theme="pouet"] .switch .mousetrap {