Compare commits

...

31 Commits

Author SHA1 Message Date
HF
45fc4b4d9c final commit v1.0 2023-12-21 19:01:52 +01:00
HF
18d9828b2b silently fail when pixellog file doesn't exist
fix #76
2023-12-13 12:15:17 +01:00
HF
9c5596d86a only extract langs from server when full build 2023-12-13 11:25:24 +01:00
HF
8d41c6533d don't require server restart on client changes,
by watching the asset files and reloading if changed
2023-12-13 11:11:25 +01:00
HF
058290aa16 do minifyCss right after cleaning assets
Use hashes in css filenames again
2023-12-13 11:10:07 +01:00
HF
0a4199e633 make example ecosystem.yml watch the server script for changes 2023-12-13 10:58:13 +01:00
HF
05298604b1 fix image link in mail 2023-12-13 10:57:12 +01:00
HF
260f6a6210 fix typo 2023-12-13 04:05:16 +01:00
HF
fb87a18bd7 update deployment scripts 2023-12-13 04:01:34 +01:00
HF
1304dfb4ba change all logger prefixes 2023-12-13 03:45:11 +01:00
HF
eb6c76c1b6 add shard names to logfile names 2023-12-13 03:18:14 +01:00
HF
6ecffafd00 remove assets.json from deployement scripts 2023-12-13 02:07:46 +01:00
HF
d8042552d8 move langage-code-to-country-code mapping into ssr filename 2023-12-13 02:04:55 +01:00
HF
3895bf292a only extract languages on full builds 2023-12-13 01:41:32 +01:00
hf
dddc0d0823 Merge pull request 'Add ge translation' (#91) from sallbet/pixelplanet-fork:translations into master
Reviewed-on: ppfun/pixelplanet#91
2023-12-13 00:36:09 +00:00
HF
b5ccda251c use multiple processes while building
extract langs on production builds not on dev
update webpack
2023-12-13 01:30:02 +01:00
HF
ab840c6f23 default to english only production build in webpack config 2023-12-12 20:33:05 +01:00
HF
3aad00dca7 allow building of only specfic languages,
english is not a required locale anymore
2023-12-12 20:30:39 +01:00
HF
872c3a5659 clean webpack cache on full build
add option to clena assets
2023-12-12 18:58:53 +01:00
HF
997d887cfd dont generate styleassets.json 2023-12-12 18:58:34 +01:00
HF
b48821e92e no-cache on http requests depending on cookies
(etag reevaluation still works)
2023-12-12 18:57:40 +01:00
HF
a3edd2cb44 use sting 'en' for english translation instead of 'default' 2023-12-12 17:58:59 +01:00
HF
6d3b6edd8a cache all language builds in the same folder
write custom webpack loader to invalidate language specific files
choose most recent asset based on mtime rather than birthtime
2023-12-12 17:58:06 +01:00
HF
22b9cf2612 make asset detection smarter 2023-12-11 22:26:16 +01:00
HF
b5d48be01f renaming entry points fixes different three.js compilations weirdly 2023-12-11 22:11:55 +01:00
HF
af97cf7ec8 don't clean dist directory before build 2023-12-11 22:11:39 +01:00
HF
bcc489eff9 fix etag caching when run on localhost
(languages were not possible to be selected if run locally, cause of
aggressive caching)
2023-12-11 20:56:26 +01:00
HF
6b0404b66d THREE.js doesn't like the timestamp name,
cause every language bundle builds its own three.js
change order of script insertion (vendor before client)
2023-12-11 20:54:52 +01:00
HF
d2fa61f3c6 use port 5000 on example ecosystem.yml 2023-12-11 16:49:27 +01:00
HF
fe8541bcbf save cookie by hostname instead of host, because cookies don't like
ports
2023-12-11 16:48:47 +01:00
HF
8544c42e7b deprecate assets.json by reading the assets directory ourselves,
use build timestamp instead of hash in filename
fix #92
2023-12-11 16:48:26 +01:00
39 changed files with 5025 additions and 4305 deletions

1
.gitignore vendored
View File

@ -10,6 +10,7 @@ logs
*.log *.log
npm-debug.log* npm-debug.log*
*.tmp *.tmp
records.json
pids pids
*.pid *.pid

View File

@ -1,3 +1,5 @@
> **Currently pixelplanet is being developed in a another private repository. This development version diverted and is incompatible with the stable version here. Until it is ready, this public repository will be frozen.**
# PixelPlanet.fun # PixelPlanet.fun
@ -7,7 +9,7 @@ Official repository of [pixelplanet.fun](http://www.pixelplanet.fun).
![videothumb](promotion/videothumb.gif) ![videothumb](promotion/videothumb.gif)
> **TRANSLATORS NEEDED** If you want to help us translate pixelplanet.fun, look into [i18n](./i18n) > If you want to help us translate pixelplanet.fun, look into [i18n](./i18n)
To the 2nd anniversary of r/space, pixelplanet takes pixelgames to a new level. Place pixels, create pixelart and fight faction wars on pixelplanet.fun. To the 2nd anniversary of r/space, pixelplanet takes pixelgames to a new level. Place pixels, create pixelart and fight faction wars on pixelplanet.fun.
Pixelplanet is a 65k x 65k large canvas that is a map of the world and can also be seen as 3d globe, you can place pixels where ever you want, build an island, take over another country with a flag or just create pixelart. Pixelplanet is a 65k x 65k large canvas that is a map of the world and can also be seen as 3d globe, you can place pixels where ever you want, build an island, take over another country with a flag or just create pixelart.
@ -256,11 +258,19 @@ If v is set and true for a canvas in the canvas.json, it will be a 3D voxel canv
Run `npm run lint:src` to check for code errors and warnings or `npm run lint -- ./your/file.js` to check a single file. Run `npm run lint:src` to check for code errors and warnings or `npm run lint -- ./your/file.js` to check a single file.
[ttag](https://github.com/ttag-org/ttag/) is used for handling translations. For server-side rendering the `Accept-Language` header gets checked and the first locale used and on-the-fly translated (`src/core/ttag.js` provides the functions for it). On the client-side a seperate bundle for every language gets provided. Compile with source-maps and debug options (but only english language) with
The language definitions in `i18n/template.pot` and `i18n/template-ssr.pot` get updated when doing a dev build with
``` ```
npm run build:dev npm run build:dev
``` ```
which also only builds the default local in a development environment for debugging.
[ttag](https://github.com/ttag-org/ttag/) is used for handling translations. For server-side rendering the `Accept-Language` header gets checked and the first locale used and on-the-fly translated (`src/core/ttag.js` provides the functions for it). On the client-side a seperate bundle for every language gets provided.
The language definitions in `i18n/template.pot` and `i18n/template-ssr.pot` get updated when doing a full production build with all languages (`npm run build`)-
To build only specific languages, you can define them with the `--langs` flag:
```
npm run build -- --langs de,gr
```
You can use `npm run babel-node ./utils/script.js` to execute a script with local babel (path always relative to the root directory). You can use `npm run babel-node ./utils/script.js` to execute a script with local babel (path always relative to the root directory).

View File

@ -1,9 +1,11 @@
apps: apps:
- script : ./server.js - script : ./server.js
name : 'ppfun-server' name : 'ppfun'
node_args: --nouse-idle-notification --expose-gc node_args : --nouse-idle-notification --expose-gc
watch : [ 'server.js' ]
watch_delay: 5000
env: env:
PORT: 80 PORT: 5000
HOST: "localhost" HOST: "localhost"
REDIS_URL: 'redis://localhost:6379' REDIS_URL: 'redis://localhost:6379'
MYSQL_HOST: "localhost" MYSQL_HOST: "localhost"

View File

@ -1,15 +1,13 @@
#!/bin/bash #!/bin/bash
# This hook builds pixelplanet after a push, and deploys it, it should be ron post-receive # This hook builds pixelplanet after a push to a development branch,
# If it is the master branch, it will deploy it on the life system, and other branch will get deployed to the dev-canvas (a second canvas that is running on the server) # and starts the dev-canvas
# #
# To set up a server to use this, you have to go through the building steps manually first. # To set up a server to use this, you have to go through the building steps manually first.
# #
#folder for building the canvas (the git repository will get checkout there and the canvas will get buil thtere) #folder for building the canvas (the git repository will get checkout there and the canvas will get built thtere)
BUILDDIR="/home/pixelpla/pixelplanet-build" BUILDDIR="/home/pixelpla/pixelplanet-build"
#folder for dev canvas #folder for dev canvas
DEVFOLDER="/home/pixelpla/pixelplanet-dev" DEVFOLDER="/home/pixelpla/pixelplanet-dev"
#folder for production canvas
PFOLDER="/home/pixelpla/pixelplanet"
should_reinstall () { should_reinstall () {
local TMPFILE="${BUILDDIR}/package.json.${1}.tmp" local TMPFILE="${BUILDDIR}/package.json.${1}.tmp"
@ -33,14 +31,12 @@ npm_reinstall () {
copy () { copy () {
local TARGETDIR="${1}" local TARGETDIR="${1}"
local REINSTALL="${2}" local REINSTALL="${2}"
cp -r dist/*.js "${TARGETDIR}/" cp -r "${BUILDDIR}"/dist/*.js "${TARGETDIR}/"
cp -r dist/workers "${TARGETDIR}/" cp -r "${BUILDDIR}"/dist/workers "${TARGETDIR}/"
rm -rf "${TARGETDIR}/public/assets" rm -rf "${TARGETDIR}/public/assets"
cp -r dist/public "${TARGETDIR}/" cp -r "${BUILDDIR}"/dist/public "${TARGETDIR}/"
cp -r dist/captchaFonts "${TARGETDIR}/" cp -r "${BUILDDIR}"/dist/captchaFonts "${TARGETDIR}/"
cp -r dist/package.json "${TARGETDIR}/" cp -r "${BUILDDIR}"/dist/package.json "${TARGETDIR}/"
cp -r dist/assets.json "${TARGETDIR}/"
cp -r dist/styleassets.json "${TARGETDIR}/"
mkdir -p "${TARGETDIR}/log" mkdir -p "${TARGETDIR}/log"
cd "${TARGETDIR}" cd "${TARGETDIR}"
[ $REINSTALL -eq 0 ] && npm_reinstall [ $REINSTALL -eq 0 ] && npm_reinstall
@ -53,28 +49,10 @@ do
GIT_WORK_TREE="$BUILDDIR" GIT_DIR="${BUILDDIR}/.git" git fetch --all GIT_WORK_TREE="$BUILDDIR" GIT_DIR="${BUILDDIR}/.git" git fetch --all
cd "$BUILDDIR" cd "$BUILDDIR"
branch=$(git rev-parse --symbolic --abbrev-ref $refname) branch=$(git rev-parse --symbolic --abbrev-ref $refname)
if [ "master" == "$branch" ]; then if [ "test" == "$branch" ] || [ "devel" == "$branch" ]; then
echo "---UPDATING REPO ON PRODUCTION SERVER---"
GIT_WORK_TREE="$BUILDDIR" GIT_DIR="${BUILDDIR}/.git" git reset --hard "origin/$branch"
COMMITS=`git log --pretty=format:'- %s%b' $newrev ^$oldrev`
COMMITS=`echo "$COMMITS" | sed ':a;N;$!ba;s/\n/\\\n/g'`
echo "---BUILDING pixelplanet---"
should_reinstall master
DO_REINSTALL=$?
[ $DO_REINSTALL -eq 0 ] && npm_reinstall
npm run build
echo "---RESTARTING CANVAS---"
pm2 stop ppfun-server
pm2 stop ppfun-backups
copy "${PFOLDER}" "${DO_REINSTALL}"
cd "$PFOLDER"
pm2 start ecosystem-backup.yml
else
echo "---UPDATING REPO ON DEV SERVER---" echo "---UPDATING REPO ON DEV SERVER---"
pm2 stop ppfun-server-dev pm2 stop ppfun-server-dev
GIT_WORK_TREE="$BUILDDIR" GIT_DIR="${BUILDDIR}/.git" git reset --hard "origin/$branch" GIT_WORK_TREE="$BUILDDIR" GIT_DIR="${BUILDDIR}/.git" git reset --hard "origin/$branch"
COMMITS=`git log --pretty=format:'- %s%b' $newrev ^$oldrev`
COMMITS=`echo "$COMMITS" | sed ':a;N;$!ba;s/\n/\\\n/g'`
echo "---BUILDING pixelplanet---" echo "---BUILDING pixelplanet---"
should_reinstall dev should_reinstall dev
DO_REINSTALL=$? DO_REINSTALL=$?

View File

@ -1,18 +1,10 @@
#!/bin/bash #!/bin/bash
# Rebuild from master branch and restart pixelplanet # Rebuild and Restert pixelplanet
#folder for building the canvas (the git repository will get checkout there and the canvas will get buil thtere) #folder for building the canvas (the git repository will get checkout there and the canvas will get buil thtere)
BUILDDIR="/home/pixelpla/pixelplanet-build" BUILDDIR="/home/pixelpla/pixelplanet-build"
#folder for dev canvas #folder for dev canvas
DEVFOLDER="/home/pixelpla/pixelplanet-dev" DEVFOLDER="/home/pixelpla/pixelplanet-dev"
#folder for shards
SCBFOLDER="/home/pixelpla/pixelplanet-scb"
SCCFOLDER="/home/pixelpla/pixelplanet-scc"
SCDFOLDER="/home/pixelpla/pixelplanet-scd"
SCEFOLDER="/home/pixelpla/pixelplanet-sce"
SCFFOLDER="/home/pixelpla/pixelplanet-scf"
SCGFOLDER="/home/pixelpla/pixelplanet-scg"
SCHFOLDER="/home/pixelpla/pixelplanet-sch"
#folder for production canvas #folder for production canvas
PFOLDER="/home/pixelpla/pixelplanet" PFOLDER="/home/pixelpla/pixelplanet"
#which branch to use #which branch to use
@ -40,23 +32,20 @@ npm_reinstall () {
copy () { copy () {
local TARGETDIR="${1}" local TARGETDIR="${1}"
local REINSTALL="${2}" local REINSTALL="${2}"
cp -r dist/*.js "${TARGETDIR}/" cp -r "${BUILDDIR}"/dist/*.js "${TARGETDIR}/"
cp -r dist/workers "${TARGETDIR}/" cp -r "${BUILDDIR}"/dist/workers "${TARGETDIR}/"
rm -rf "${TARGETDIR}/public/assets" rm -rf "${TARGETDIR}/public/assets"
cp -r dist/public "${TARGETDIR}/" cp -r "${BUILDDIR}"/dist/public "${TARGETDIR}/"
cp -r dist/captchaFonts "${TARGETDIR}/" cp -r "${BUILDDIR}"/dist/captchaFonts "${TARGETDIR}/"
cp -r dist/package.json "${TARGETDIR}/" cp -r "${BUILDDIR}"/dist/package.json "${TARGETDIR}/"
cp -r dist/assets.json "${TARGETDIR}/"
cp -r dist/styleassets.json "${TARGETDIR}/"
mkdir -p "${TARGETDIR}/log" mkdir -p "${TARGETDIR}/log"
cd "${TARGETDIR}" cd "${TARGETDIR}"
[ $REINSTALL -eq 0 ] && npm_reinstall [ ${REINSTALL} -eq 0 ] && npm_reinstall
pm2 start ecosystem.yml
cd - cd -
} }
GIT_WORK_TREE="$BUILDDIR" GIT_DIR="${BUILDDIR}/.git" git fetch --all
cd "$BUILDDIR" cd "$BUILDDIR"
GIT_WORK_TREE="$BUILDDIR" GIT_DIR="${BUILDDIR}/.git" git fetch --all
echo "---UPDATING REPO ON PRODUCTION SERVER---" echo "---UPDATING REPO ON PRODUCTION SERVER---"
GIT_WORK_TREE="$BUILDDIR" GIT_DIR="${BUILDDIR}/.git" git reset --hard "origin/${BRANCH}" GIT_WORK_TREE="$BUILDDIR" GIT_DIR="${BUILDDIR}/.git" git reset --hard "origin/${BRANCH}"
echo "---BUILDING pixelplanet---" echo "---BUILDING pixelplanet---"
@ -65,22 +54,10 @@ DO_REINSTALL=$?
[ $DO_REINSTALL -eq 0 ] && npm_reinstall [ $DO_REINSTALL -eq 0 ] && npm_reinstall
npm run build npm run build
echo "---RESTARTING CANVAS---" echo "---RESTARTING CANVAS---"
pm2 stop ppfun-server cp dist/canvases.json ~/
pm2 stop ppfun-backups
pm2 stop ppfun-scb
pm2 stop ppfun-scc
pm2 stop ppfun-scd
pm2 stop ppfun-sce
pm2 stop ppfun-scf
pm2 stop ppfun-scg
pm2 stop ppfun-sch
copy "${PFOLDER}" "${DO_REINSTALL}"
copy "${SCBFOLDER}" 1
copy "${SCCFOLDER}" 1
copy "${SCDFOLDER}" 1
copy "${SCEFOLDER}" 1
copy "${SCFFOLDER}" 1
copy "${SCGFOLDER}" 1
copy "${SCHFOLDER}" 1
cd "$PFOLDER" cd "$PFOLDER"
pm2 stop ppfun-backups
pm2 stop ecosystem.config.js
copy "${PFOLDER}" "${DO_REINSTALL}"
pm2 start ecosystem-backup.yml pm2 start ecosystem-backup.yml
pm2 start ecosystem.config.js

View File

@ -6,6 +6,8 @@ Translation files can be created out of the templates [template.pot](https://git
All translated languages get an own chat channel that just people who use this language can access. All translated languages get an own chat channel that just people who use this language can access.
If a language code differs from the country code of a wanted flag, it can be defined in the ssr filename, like `ssr-en-gb.po` would be the english language, with the flag of Great Britain.
## With poedit ## With poedit
### Create new translation ### Create new translation

View File

@ -29,6 +29,7 @@ msgid "You are muted for another ${ timeMin } minutes"
msgstr "" msgstr ""
#: src/core/ChatProvider.js:450 #: src/core/ChatProvider.js:450
#, javascript-format
msgid "You are muted for another ${ ttl } seconds" msgid "You are muted for another ${ ttl } seconds"
msgstr "" msgstr ""
@ -81,35 +82,35 @@ msgstr ""
msgid "Invalid url :( Please check your mail again." msgid "Invalid url :( Please check your mail again."
msgstr "" msgstr ""
#: src/ssr/Globe.jsx:32 #: src/ssr/Globe.jsx:37
msgid "PixelPlanet.Fun 3DGlobe" msgid "PixelPlanet.Fun 3DGlobe"
msgstr "" msgstr ""
#: src/ssr/Globe.jsx:33 #: src/ssr/Globe.jsx:38
msgid "A 3D globe of our whole map" msgid "A 3D globe of our whole map"
msgstr "" msgstr ""
#: src/ssr/Globe.jsx:46 #: src/ssr/Globe.jsx:51
msgid "Double click on globe to go back." msgid "Double click on globe to go back."
msgstr "" msgstr ""
#: src/ssr/Globe.jsx:47 #: src/ssr/Globe.jsx:52
msgid "Loading..." msgid "Loading..."
msgstr "" msgstr ""
#: src/ssr/PopUp.jsx:58 #: src/ssr/PopUp.jsx:55
msgid "ppfun" msgid "ppfun"
msgstr "" msgstr ""
#: src/ssr/PopUp.jsx:59 #: src/ssr/PopUp.jsx:56
msgid "PixelPlanet.Fun PopUp" msgid "PixelPlanet.Fun PopUp"
msgstr "" msgstr ""
#: src/ssr/Main.jsx:67 #: src/ssr/Main.jsx:64
msgid "PixelPlanet.Fun" msgid "PixelPlanet.Fun"
msgstr "" msgstr ""
#: src/ssr/Main.jsx:68 #: src/ssr/Main.jsx:65
msgid "Place color pixels on an map styled canvas with other players online" msgid "Place color pixels on an map styled canvas with other players online"
msgstr "" msgstr ""
@ -219,31 +220,7 @@ msgstr ""
msgid "You are not banned" msgid "You are not banned"
msgstr "" msgstr ""
#: src/routes/api/auth/logout.js:11 #: src/routes/api/auth/change_mail.js:22
msgid "You are not even logged in."
msgstr ""
#: src/routes/api/auth/delete_account.js:55
#: src/routes/api/auth/logout.js:20
msgid "Server error when logging out."
msgstr ""
#: src/routes/api/auth/verify.js:26
#: src/routes/api/auth/verify.js:35
msgid "Mail verification"
msgstr ""
#: src/routes/api/auth/verify.js:27
msgid "You are now verified :)"
msgstr ""
#: src/routes/api/auth/verify.js:35
msgid ""
"Your mail verification code is invalid or already expired :(, please "
"request a new one."
msgstr ""
#: src/routes/api/auth/change_mail.js:21
#: src/routes/api/auth/register.js:24 #: src/routes/api/auth/register.js:24
msgid "This email provider is not allowed" msgid "This email provider is not allowed"
msgstr "" msgstr ""
@ -280,18 +257,50 @@ msgstr ""
msgid "Failed to establish session after register :(" msgid "Failed to establish session after register :("
msgstr "" msgstr ""
#: src/routes/api/auth/change_mail.js:43 #: src/routes/api/auth/verify.js:26
#: src/routes/api/auth/verify.js:35
msgid "Mail verification"
msgstr ""
#: src/routes/api/auth/verify.js:27
msgid "You are now verified :)"
msgstr ""
#: src/routes/api/auth/verify.js:35
msgid ""
"Your mail verification code is invalid or already expired :(, please "
"request a new one."
msgstr ""
#: src/routes/api/auth/logout.js:11
msgid "You are not even logged in."
msgstr ""
#: src/routes/api/auth/delete_account.js:65
#: src/routes/api/auth/logout.js:20
msgid "Server error when logging out."
msgstr ""
#: src/routes/api/auth/change_mail.js:44
#: src/routes/api/auth/change_passwd.js:34 #: src/routes/api/auth/change_passwd.js:34
#: src/routes/api/auth/delete_account.js:35 #: src/routes/api/auth/delete_account.js:36
msgid "You are not authenticated." msgid "You are not authenticated."
msgstr "" msgstr ""
#: src/routes/api/auth/change_mail.js:52 #: src/routes/api/auth/change_mail.js:53
#: src/routes/api/auth/change_passwd.js:43 #: src/routes/api/auth/change_passwd.js:43
#: src/routes/api/auth/delete_account.js:45 #: src/routes/api/auth/delete_account.js:55
msgid "Incorrect password!" msgid "Incorrect password!"
msgstr "" msgstr ""
#: src/routes/api/auth/delete_account.js:46
msgid "Muted users can not delete their account."
msgstr ""
#: src/routes/api/auth/change_mail.js:62
msgid "Muted users can not do this."
msgstr ""
#: src/ssr/RedirectionPage.jsx:19 #: src/ssr/RedirectionPage.jsx:19
msgid "PixelPlanet.fun Accounts" msgid "PixelPlanet.fun Accounts"
msgstr "" msgstr ""
@ -337,48 +346,59 @@ msgstr ""
msgid "Top10" msgid "Top10"
msgstr "" msgstr ""
#: src/canvasesDesc.js:29 #: src/canvasesDesc.js:27
msgid "Our main canvas, a huge map of the world. Place everywhere you like" msgid "Thoia"
msgstr "" msgstr ""
#: src/canvasesDesc.js:30 #: src/canvasesDesc.js:30
msgid "Our main canvas, a huge map of the world. Place everywhere you like"
msgstr ""
#: src/canvasesDesc.js:31
msgid "" msgid ""
"Moon canvas. Safe space for art. No flags or large text (unless part of " "Moon canvas. Safe space for art. No flags or large text (unless part of "
"art) or art larger than 1.5k x 1.5k pixels." "art) or art larger than 1.5k x 1.5k pixels."
msgstr "" msgstr ""
#: src/canvasesDesc.js:31 #: src/canvasesDesc.js:32
msgid "Place Voxels on a 3D canvas with others" msgid "Place Voxels on a 3D canvas with others"
msgstr "" msgstr ""
#: src/canvasesDesc.js:32 #: src/canvasesDesc.js:33
msgid "Special canvas to spread awareness of SARS-CoV2" msgid "Special canvas to spread awareness of SARS-CoV2"
msgstr "" msgstr ""
#: src/canvasesDesc.js:33 #: src/canvasesDesc.js:34
msgid "Mirror of PixelZone" msgid "Mirror of PixelZone"
msgstr "" msgstr ""
#: src/canvasesDesc.js:34 #: src/canvasesDesc.js:35
msgid "Mirror of PixelCanvas" msgid "Mirror of PixelCanvas"
msgstr "" msgstr ""
#: src/canvasesDesc.js:35 #: src/canvasesDesc.js:36
msgid "Black and White canvas" msgid "Black and White canvas"
msgstr "" msgstr ""
#: src/canvasesDesc.js:36 #: src/canvasesDesc.js:37
msgid "" msgid ""
"A canvas for the most active players from the the previous day. Daily " "A canvas for the most active players from the the previous day. Daily "
"ranking updates at 00:00 UTC." "ranking updates at 00:00 UTC."
msgstr "" msgstr ""
#: src/canvasesDesc.js:38
msgid ""
"Thoia World Canvas. Advanced fictional worldbuilding and arts. Abandon the "
"old world and all it entails."
msgstr ""
#: src/core/MailProvider.js:66 #: src/core/MailProvider.js:66
#, javascript-format #, javascript-format
msgid "Welcome ${ name } to PixelPlanet, please verify your mail" msgid "Welcome ${ name } to PixelPlanet, please verify your mail"
msgstr "" msgstr ""
#: src/core/MailProvider.js:67 #: src/core/MailProvider.js:67
#, javascript-format
msgid "Hello ${ name }" msgid "Hello ${ name }"
msgstr "" msgstr ""
@ -446,4 +466,4 @@ msgstr ""
#: src/core/MailProvider.js:130 #: src/core/MailProvider.js:130
msgid "Couldn't find this mail in our database" msgid "Couldn't find this mail in our database"
msgstr "" msgstr ""

View File

@ -194,6 +194,7 @@ msgid "try again after ${ ti }min"
msgstr "" msgstr ""
#: src/store/actions/fetch.js:70 #: src/store/actions/fetch.js:70
#, javascript-format
msgid "Connection error ${ code } :(" msgid "Connection error ${ code } :("
msgstr "" msgstr ""
@ -230,12 +231,6 @@ msgstr ""
msgid "You have new messages in chat" msgid "You have new messages in chat"
msgstr "" msgstr ""
#: src/components/Converter.jsx:561
#: src/components/CoordinatesBox.jsx:31
#: src/components/ModWatchtools.jsx:371
msgid "Copy to Clipboard"
msgstr ""
#: src/components/OnlineBox.jsx:40 #: src/components/OnlineBox.jsx:40
msgid "Online Users on Canvas" msgid "Online Users on Canvas"
msgstr "" msgstr ""
@ -248,6 +243,13 @@ msgstr ""
msgid "Pixels placed" msgid "Pixels placed"
msgstr "" msgstr ""
#: src/components/Converter.jsx:565
#: src/components/CoordinatesBox.jsx:31
#: src/components/ModWatchtools.jsx:378
#: src/components/ModWatchtools.jsx:396
msgid "Copy to Clipboard"
msgstr ""
#: src/components/buttons/CanvasSwitchButton.jsx:20 #: src/components/buttons/CanvasSwitchButton.jsx:20
#: src/components/windows/index.js:19 #: src/components/windows/index.js:19
msgid "Canvas Selection" msgid "Canvas Selection"
@ -279,7 +281,7 @@ msgstr ""
#: src/components/Admintools.jsx:103 #: src/components/Admintools.jsx:103
#: src/components/ModCanvastools.jsx:222 #: src/components/ModCanvastools.jsx:222
#: src/components/ModWatchtools.jsx:118 #: src/components/ModWatchtools.jsx:120
#: src/components/Window.jsx:157 #: src/components/Window.jsx:157
#: src/components/Window.jsx:260 #: src/components/Window.jsx:260
#: src/components/contextmenus/ChannelContextMenu.jsx:59 #: src/components/contextmenus/ChannelContextMenu.jsx:59
@ -310,6 +312,26 @@ msgstr ""
msgid "Resize" msgid "Resize"
msgstr "" msgstr ""
#: src/components/BanInfo.jsx:75
#: src/components/buttons/HelpButton.jsx:20
#: src/components/windows/index.js:13
msgid "Help"
msgstr ""
#: src/components/buttons/SettingsButton.jsx:21
#: src/components/windows/index.js:14
msgid "Settings"
msgstr ""
#: src/components/buttons/LogInButton.jsx:20
#: src/components/windows/index.js:15
msgid "User Area"
msgstr ""
#: src/components/buttons/DownloadButton.jsx:36
msgid "Make Screenshot"
msgstr ""
#: src/components/buttons/GlobeButton.jsx:34 #: src/components/buttons/GlobeButton.jsx:34
msgid "Globe View" msgid "Globe View"
msgstr "" msgstr ""
@ -322,26 +344,6 @@ msgstr ""
msgid "Open Palette" msgid "Open Palette"
msgstr "" msgstr ""
#: src/components/buttons/SettingsButton.jsx:21
#: src/components/windows/index.js:14
msgid "Settings"
msgstr ""
#: src/components/BanInfo.jsx:75
#: src/components/buttons/HelpButton.jsx:20
#: src/components/windows/index.js:13
msgid "Help"
msgstr ""
#: src/components/buttons/LogInButton.jsx:20
#: src/components/windows/index.js:15
msgid "User Area"
msgstr ""
#: src/components/buttons/DownloadButton.jsx:36
msgid "Make Screenshot"
msgstr ""
#: src/components/windows/index.js:16 #: src/components/windows/index.js:16
msgid "Registration" msgid "Registration"
msgstr "" msgstr ""
@ -447,99 +449,6 @@ msgstr ""
msgid "Why?" msgid "Why?"
msgstr "" msgstr ""
#: src/components/windows/Settings.jsx:86
msgid "Show Grid"
msgstr ""
#: src/components/windows/Settings.jsx:91
msgid "Turn on grid to highlight pixel borders."
msgstr ""
#: src/components/windows/Settings.jsx:94
msgid "Show Pixel Activity"
msgstr ""
#: src/components/windows/Settings.jsx:99
msgid "Show circles where pixels are placed."
msgstr ""
#: src/components/windows/Settings.jsx:102
msgid "Disable Game Sounds"
msgstr ""
#: src/components/windows/Settings.jsx:108
msgid "All sound effects will be disabled."
msgstr ""
#: src/components/windows/Settings.jsx:112
msgid ""
"Your Browser doesn't allow us to use AudioContext to play sounds. Do you "
"have some privacy feature blocking us?"
msgstr ""
#: src/components/windows/Settings.jsx:118
msgid "Enable chat notifications"
msgstr ""
#: src/components/windows/Settings.jsx:122
msgid "Play a sound when new chat messages arrive"
msgstr ""
#: src/components/windows/Settings.jsx:125
msgid "Auto Zoom In"
msgstr ""
#: src/components/windows/Settings.jsx:130
msgid ""
"Zoom in instead of placing a pixel when you tap the canvas and your zoom is "
"small."
msgstr ""
#: src/components/windows/Settings.jsx:133
msgid "Compact Palette"
msgstr ""
#: src/components/windows/Settings.jsx:138
msgid "Display Palette in a compact form that takes less screen space."
msgstr ""
#: src/components/windows/Settings.jsx:141
msgid "Potato Mode"
msgstr ""
#: src/components/windows/Settings.jsx:145
msgid "For when you are playing on a potato."
msgstr ""
#: src/components/Converter.jsx:376
#: src/components/windows/Settings.jsx:148
msgid "Light Grid"
msgstr ""
#: src/components/windows/Settings.jsx:152
msgid "Show Grid in white instead of black."
msgstr ""
#: src/components/windows/Settings.jsx:156
msgid "Historical View"
msgstr ""
#: src/components/windows/Settings.jsx:161
msgid "Check out past versions of the canvas."
msgstr ""
#: src/components/windows/Settings.jsx:166
msgid "Themes"
msgstr ""
#: src/components/windows/Settings.jsx:171
msgid "How pixelplanet should look like."
msgstr ""
#: src/components/windows/Settings.jsx:178
msgid "Select Language"
msgstr ""
#: src/components/windows/Help.jsx:42 #: src/components/windows/Help.jsx:42
msgid "Place color pixels on a large canvas with other players online!" msgid "Place color pixels on a large canvas with other players online!"
msgstr "" msgstr ""
@ -609,6 +518,7 @@ msgid "Press ${ bindG } to toggle grid"
msgstr "" msgstr ""
#: src/components/windows/Help.jsx:64 #: src/components/windows/Help.jsx:64
#, javascript-format
msgid "Press ${ bindX } to toggle showing of pixel activity" msgid "Press ${ bindX } to toggle showing of pixel activity"
msgstr "" msgstr ""
@ -618,6 +528,7 @@ msgid "Press ${ bindH } to toggle historical view"
msgstr "" msgstr ""
#: src/components/windows/Help.jsx:66 #: src/components/windows/Help.jsx:66
#, javascript-format
msgid "Press ${ bindR } to copy coordinates" msgid "Press ${ bindR } to copy coordinates"
msgstr "" msgstr ""
@ -639,6 +550,7 @@ msgid "Press ${ bindAUp }, ${ bindALeft }, ${ bindADown }, ${ bindARight } to mo
msgstr "" msgstr ""
#: src/components/windows/Help.jsx:70 #: src/components/windows/Help.jsx:70
#, javascript-format
msgid "Drag ${ mouseSymbol } mouse or ${ touchSymbol } pan to move" msgid "Drag ${ mouseSymbol } mouse or ${ touchSymbol } pan to move"
msgstr "" msgstr ""
@ -648,6 +560,7 @@ msgid "Scroll ${ mouseSymbol } mouse wheel or ${ touchSymbol } pinch to zoom"
msgstr "" msgstr ""
#: src/components/windows/Help.jsx:72 #: src/components/windows/Help.jsx:72
#, javascript-format
msgid "Hold left ${ bindShift } for placing while moving mouse" msgid "Hold left ${ bindShift } for placing while moving mouse"
msgstr "" msgstr ""
@ -666,12 +579,14 @@ msgstr ""
#: src/components/windows/Help.jsx:75 #: src/components/windows/Help.jsx:75
#: src/components/windows/Help.jsx:87 #: src/components/windows/Help.jsx:87
#, javascript-format
msgid "" msgid ""
"Click ${ mouseSymbol } middle mouse button or ${ touchSymbol } long-tap to " "Click ${ mouseSymbol } middle mouse button or ${ touchSymbol } long-tap to "
"select current hovering color" "select current hovering color"
msgstr "" msgstr ""
#: src/components/windows/Help.jsx:81 #: src/components/windows/Help.jsx:81
#, javascript-format
msgid "Press ${ bindE } and ${ bindC } to fly up and down" msgid "Press ${ bindE } and ${ bindC } to fly up and down"
msgstr "" msgstr ""
@ -688,6 +603,7 @@ msgid ""
msgstr "" msgstr ""
#: src/components/windows/Help.jsx:84 #: src/components/windows/Help.jsx:84
#, javascript-format
msgid "${ mouseSymbol } Right click and drag mouse to pan" msgid "${ mouseSymbol } Right click and drag mouse to pan"
msgstr "" msgstr ""
@ -710,9 +626,103 @@ msgid "Credit for the Palette of the Moon goes to ${ starhouseLink }."
msgstr "" msgstr ""
#: src/components/windows/Help.jsx:97 #: src/components/windows/Help.jsx:97
#, javascript-format
msgid "Credit for the Palette of the Top10 canvas goes to ${ vinikLink }." msgid "Credit for the Palette of the Top10 canvas goes to ${ vinikLink }."
msgstr "" msgstr ""
#: src/components/windows/Settings.jsx:86
msgid "Show Grid"
msgstr ""
#: src/components/windows/Settings.jsx:91
msgid "Turn on grid to highlight pixel borders."
msgstr ""
#: src/components/windows/Settings.jsx:94
msgid "Show Pixel Activity"
msgstr ""
#: src/components/windows/Settings.jsx:99
msgid "Show circles where pixels are placed."
msgstr ""
#: src/components/windows/Settings.jsx:102
msgid "Disable Game Sounds"
msgstr ""
#: src/components/windows/Settings.jsx:108
msgid "All sound effects will be disabled."
msgstr ""
#: src/components/windows/Settings.jsx:112
msgid ""
"Your Browser doesn't allow us to use AudioContext to play sounds. Do you "
"have some privacy feature blocking us?"
msgstr ""
#: src/components/windows/Settings.jsx:118
msgid "Enable chat notifications"
msgstr ""
#: src/components/windows/Settings.jsx:122
msgid "Play a sound when new chat messages arrive"
msgstr ""
#: src/components/windows/Settings.jsx:125
msgid "Auto Zoom In"
msgstr ""
#: src/components/windows/Settings.jsx:130
msgid ""
"Zoom in instead of placing a pixel when you tap the canvas and your zoom is "
"small."
msgstr ""
#: src/components/windows/Settings.jsx:133
msgid "Compact Palette"
msgstr ""
#: src/components/windows/Settings.jsx:138
msgid "Display Palette in a compact form that takes less screen space."
msgstr ""
#: src/components/windows/Settings.jsx:141
msgid "Potato Mode"
msgstr ""
#: src/components/windows/Settings.jsx:145
msgid "For when you are playing on a potato."
msgstr ""
#: src/components/Converter.jsx:380
#: src/components/windows/Settings.jsx:148
msgid "Light Grid"
msgstr ""
#: src/components/windows/Settings.jsx:152
msgid "Show Grid in white instead of black."
msgstr ""
#: src/components/windows/Settings.jsx:156
msgid "Historical View"
msgstr ""
#: src/components/windows/Settings.jsx:161
msgid "Check out past versions of the canvas."
msgstr ""
#: src/components/windows/Settings.jsx:166
msgid "Themes"
msgstr ""
#: src/components/windows/Settings.jsx:171
msgid "How pixelplanet should look like."
msgstr ""
#: src/components/windows/Settings.jsx:178
msgid "Select Language"
msgstr ""
#: src/components/windows/UserArea.jsx:36 #: src/components/windows/UserArea.jsx:36
#: src/components/windows/UserArea.jsx:56 #: src/components/windows/UserArea.jsx:56
msgid "Profile" msgid "Profile"
@ -826,14 +836,6 @@ msgid ""
"how the canvas was at that time." "how the canvas was at that time."
msgstr "" msgstr ""
#: src/components/windows/ForgotPassword.jsx:58
msgid "Sent you a mail with instructions to reset your password."
msgstr ""
#: src/components/windows/ForgotPassword.jsx:69
msgid "Enter your mail address and we will send you a new password:"
msgstr ""
#: src/components/windows/Chat.jsx:180 #: src/components/windows/Chat.jsx:180
msgid "Start chatting here" msgid "Start chatting here"
msgstr "" msgstr ""
@ -850,6 +852,14 @@ msgstr ""
msgid "Channel settings" msgid "Channel settings"
msgstr "" msgstr ""
#: src/components/windows/ForgotPassword.jsx:58
msgid "Sent you a mail with instructions to reset your password."
msgstr ""
#: src/components/windows/ForgotPassword.jsx:69
msgid "Enter your mail address and we will send you a new password:"
msgstr ""
#: src/components/Captcha.jsx:51 #: src/components/Captcha.jsx:51
#: src/components/Captcha.jsx:105 #: src/components/Captcha.jsx:105
msgid "Could not load captcha" msgid "Could not load captcha"
@ -931,13 +941,6 @@ msgstr ""
msgid "Password must be shorter than 60 characters." msgid "Password must be shorter than 60 characters."
msgstr "" msgstr ""
#: src/components/ChangeMail.jsx:91
#: src/components/ChangeName.jsx:68
#: src/components/ChangePassword.jsx:109
#: src/components/LanguageSelect.jsx:80
msgid "Save"
msgstr ""
#: src/components/GetIID.jsx:44 #: src/components/GetIID.jsx:44
msgid "Get IID" msgid "Get IID"
msgstr "" msgstr ""
@ -946,6 +949,13 @@ msgstr ""
msgid "Copy" msgid "Copy"
msgstr "" msgstr ""
#: src/components/ChangeMail.jsx:91
#: src/components/ChangeName.jsx:68
#: src/components/ChangePassword.jsx:109
#: src/components/LanguageSelect.jsx:80
msgid "Save"
msgstr ""
#: src/components/LogInArea.jsx:19 #: src/components/LogInArea.jsx:19
msgid "Login to access more features and stats." msgid "Login to access more features and stats."
msgstr "" msgstr ""
@ -1020,77 +1030,77 @@ msgstr ""
msgid "Choose Canvas" msgid "Choose Canvas"
msgstr "" msgstr ""
#: src/components/Converter.jsx:216 #: src/components/Converter.jsx:217
msgid "Palette Download" msgid "Palette Download"
msgstr "" msgstr ""
#: src/components/Converter.jsx:218 #: src/components/Converter.jsx:219
#, javascript-format #, javascript-format
msgid "Palette for ${ gimpLink }" msgid "Palette for ${ gimpLink }"
msgstr "" msgstr ""
#: src/components/Converter.jsx:236 #: src/components/Converter.jsx:240
msgid "Image Converter" msgid "Image Converter"
msgstr "" msgstr ""
#: src/components/Converter.jsx:237 #: src/components/Converter.jsx:241
msgid "Convert an image to canvas colors" msgid "Convert an image to canvas colors"
msgstr "" msgstr ""
#: src/components/Converter.jsx:256 #: src/components/Converter.jsx:260
msgid "Choose Strategy" msgid "Choose Strategy"
msgstr "" msgstr ""
#: src/components/Converter.jsx:294 #: src/components/Converter.jsx:298
msgid "Serpentine" msgid "Serpentine"
msgstr "" msgstr ""
#: src/components/Converter.jsx:296 #: src/components/Converter.jsx:300
msgid "Minimum Color Distance" msgid "Minimum Color Distance"
msgstr "" msgstr ""
#: src/components/Converter.jsx:323 #: src/components/Converter.jsx:327
msgid "Calculate like GIMP" msgid "Calculate like GIMP"
msgstr "" msgstr ""
#: src/components/Converter.jsx:327 #: src/components/Converter.jsx:331
msgid "Choose Color Mode" msgid "Choose Color Mode"
msgstr "" msgstr ""
#: src/components/Converter.jsx:356 #: src/components/Converter.jsx:360
msgid "Add Grid (uncheck if you need a 1:1 template)" msgid "Add Grid (uncheck if you need a 1:1 template)"
msgstr "" msgstr ""
#: src/components/Converter.jsx:378 #: src/components/Converter.jsx:382
#: src/components/Converter.jsx:394 #: src/components/Converter.jsx:398
msgid "Offset" msgid "Offset"
msgstr "" msgstr ""
#: src/components/Converter.jsx:423 #: src/components/Converter.jsx:427
msgid "Scale Image" msgid "Scale Image"
msgstr "" msgstr ""
#: src/components/Converter.jsx:434 #: src/components/Converter.jsx:438
msgid "Width" msgid "Width"
msgstr "" msgstr ""
#: src/components/Converter.jsx:465 #: src/components/Converter.jsx:469
msgid "Height" msgid "Height"
msgstr "" msgstr ""
#: src/components/Converter.jsx:504 #: src/components/Converter.jsx:508
msgid "Keep Ratio" msgid "Keep Ratio"
msgstr "" msgstr ""
#: src/components/Converter.jsx:517 #: src/components/Converter.jsx:521
msgid "Anti Aliasing" msgid "Anti Aliasing"
msgstr "" msgstr ""
#: src/components/Converter.jsx:531 #: src/components/Converter.jsx:535
msgid "Reset" msgid "Reset"
msgstr "" msgstr ""
#: src/components/Converter.jsx:549 #: src/components/Converter.jsx:553
msgid "Download Template" msgid "Download Template"
msgstr "" msgstr ""
@ -1189,7 +1199,7 @@ msgstr ""
msgid "Countries by Pixels Today" msgid "Countries by Pixels Today"
msgstr "" msgstr ""
#: src/core/chartSettings.js:352 #: src/core/chartSettings.js:351
msgid "Total Pixels placed per day" msgid "Total Pixels placed per day"
msgstr "" msgstr ""
@ -1215,10 +1225,6 @@ msgstr ""
msgid "Click here to request a new verification mail." msgid "Click here to request a new verification mail."
msgstr "" msgstr ""
#: src/components/ChangeName.jsx:64
msgid "New Username"
msgstr ""
#: src/components/ChangePassword.jsx:21 #: src/components/ChangePassword.jsx:21
msgid "Passwords do not match." msgid "Passwords do not match."
msgstr "" msgstr ""
@ -1239,6 +1245,10 @@ msgstr ""
msgid "Confirm New Password" msgid "Confirm New Password"
msgstr "" msgstr ""
#: src/components/ChangeName.jsx:64
msgid "New Username"
msgstr ""
#: src/components/ChangeMail.jsx:59 #: src/components/ChangeMail.jsx:59
msgid "" msgid ""
"Changed Mail successfully. We sent you a verification mail, " "Changed Mail successfully. We sent you a verification mail, "
@ -1339,14 +1349,14 @@ msgstr ""
#: src/components/ModCanvastools.jsx:330 #: src/components/ModCanvastools.jsx:330
#: src/components/ModCanvastools.jsx:403 #: src/components/ModCanvastools.jsx:403
#: src/components/ModCanvastools.jsx:486 #: src/components/ModCanvastools.jsx:486
#: src/components/ModWatchtools.jsx:174 #: src/components/ModWatchtools.jsx:176
msgid "Top-left corner" msgid "Top-left corner"
msgstr "" msgstr ""
#: src/components/ModCanvastools.jsx:347 #: src/components/ModCanvastools.jsx:347
#: src/components/ModCanvastools.jsx:420 #: src/components/ModCanvastools.jsx:420
#: src/components/ModCanvastools.jsx:503 #: src/components/ModCanvastools.jsx:503
#: src/components/ModWatchtools.jsx:191 #: src/components/ModWatchtools.jsx:193
msgid "Bottom-right corner" msgid "Bottom-right corner"
msgstr "" msgstr ""
@ -1370,54 +1380,6 @@ msgstr ""
msgid "Stop Cleaner" msgid "Stop Cleaner"
msgstr "" msgstr ""
#: src/components/ModWatchtools.jsx:48
msgid "Interval is invalid"
msgstr ""
#: src/components/ModWatchtools.jsx:122
msgid "Check who placed in an area"
msgstr ""
#: src/components/ModWatchtools.jsx:123
msgid "Canvas"
msgstr ""
#: src/components/ModWatchtools.jsx:142
msgid "Interval"
msgstr ""
#: src/components/ModWatchtools.jsx:157
msgid "IID (optional)"
msgstr ""
#: src/components/ModWatchtools.jsx:236
msgid "Get Pixels"
msgstr ""
#: src/components/ModWatchtools.jsx:267
msgid "Get Users"
msgstr ""
#: src/components/ModIIDtools.jsx:20
msgid "You must enter a duration"
msgstr ""
#: src/components/ModIIDtools.jsx:24
msgid "You must enter an IID"
msgstr ""
#: src/components/ModIIDtools.jsx:53
msgid "IID Actions"
msgstr ""
#: src/components/ModIIDtools.jsx:80
msgid "Enter Reason"
msgstr ""
#: src/components/ModIIDtools.jsx:97
msgid "(0 = infinite)"
msgstr ""
#: src/components/Admintools.jsx:109 #: src/components/Admintools.jsx:109
msgid "IP Actions" msgid "IP Actions"
msgstr "" msgstr ""
@ -1450,8 +1412,52 @@ msgstr ""
msgid "User Name" msgid "User Name"
msgstr "" msgstr ""
#: src/components/contextmenus/ChannelContextMenu.jsx:46 #: src/components/ModWatchtools.jsx:48
msgid "Mute" msgid "Interval is invalid"
msgstr ""
#: src/components/ModWatchtools.jsx:124
msgid "Check who placed in an area"
msgstr ""
#: src/components/ModWatchtools.jsx:125
msgid "Canvas"
msgstr ""
#: src/components/ModWatchtools.jsx:144
msgid "Interval"
msgstr ""
#: src/components/ModWatchtools.jsx:159
msgid "IID (optional)"
msgstr ""
#: src/components/ModWatchtools.jsx:239
msgid "Get Pixels"
msgstr ""
#: src/components/ModWatchtools.jsx:271
msgid "Get Users"
msgstr ""
#: src/components/ModIIDtools.jsx:20
msgid "You must enter a duration"
msgstr ""
#: src/components/ModIIDtools.jsx:24
msgid "You must enter an IID"
msgstr ""
#: src/components/ModIIDtools.jsx:53
msgid "IID Actions"
msgstr ""
#: src/components/ModIIDtools.jsx:80
msgid "Enter Reason"
msgstr ""
#: src/components/ModIIDtools.jsx:97
msgid "(0 = infinite)"
msgstr "" msgstr ""
#: src/components/contextmenus/UserContextMenu.jsx:49 #: src/components/contextmenus/UserContextMenu.jsx:49
@ -1466,6 +1472,10 @@ msgstr ""
msgid "Block" msgid "Block"
msgstr "" msgstr ""
#: src/components/contextmenus/ChannelContextMenu.jsx:46
msgid "Mute"
msgstr ""
#: src/components/windows/Help.jsx:15 #: src/components/windows/Help.jsx:15
#: src/components/windows/Settings.jsx:87 #: src/components/windows/Settings.jsx:87
msgctxt "keybinds" msgctxt "keybinds"
@ -1478,11 +1488,6 @@ msgctxt "keybinds"
msgid "X" msgid "X"
msgstr "" msgstr ""
#: src/components/windows/Settings.jsx:103
msgctxt "keybinds"
msgid "M"
msgstr ""
#: src/components/windows/Help.jsx:17 #: src/components/windows/Help.jsx:17
#: src/components/windows/Settings.jsx:158 #: src/components/windows/Settings.jsx:158
msgctxt "keybinds" msgctxt "keybinds"
@ -1532,4 +1537,9 @@ msgstr ""
#: src/components/windows/Help.jsx:32 #: src/components/windows/Help.jsx:32
msgctxt "keybinds" msgctxt "keybinds"
msgid "C" msgid "C"
msgstr "" msgstr ""
#: src/components/windows/Settings.jsx:103
msgctxt "keybinds"
msgid "M"
msgstr ""

7649
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -8,8 +8,11 @@
"description": "Unlimited planet canvas for placing pixels", "description": "Unlimited planet canvas for placing pixels",
"main": "server.js", "main": "server.js",
"scripts": { "scripts": {
"build": "node scripts/build.js && npm run minify-css", "build": "node scripts/build.js",
"build:dev": "webpack --env extract --config ./webpack.config.server.js && webpack --env extract --env development --config ./webpack.config.client.js && npm run minify-css", "build:server": "webpack --config ./webpack.config.server.js",
"build:client": "node scripts/build.js --client",
"build:dev": "webpack --config ./webpack.config.server.js && webpack --env development --config ./webpack.config.client.js && npm run minify-css",
"update-browserlist": "browserslist --update-db",
"deploy": "ssh pixelplanet /home/pixelpla/rebuild.sh", "deploy": "ssh pixelplanet /home/pixelpla/rebuild.sh",
"minify-css": "node scripts/minifyCss.js", "minify-css": "node scripts/minifyCss.js",
"babel-node": "babel-node", "babel-node": "babel-node",
@ -23,11 +26,11 @@
"not IE_Mob 11" "not IE_Mob 11"
], ],
"dependencies": { "dependencies": {
"bcrypt": "^5.0.1", "bcrypt": "^5.1.1",
"chart.js": "^3.9.1", "chart.js": "^3.9.1",
"compression": "^1.7.3", "compression": "^1.7.3",
"cookie": "^0.5.0", "cookie": "^0.5.0",
"core-js": "^3.23.4", "core-js": "^3.34.0",
"etag": "^1.8.1", "etag": "^1.8.1",
"express": "^4.17.2", "express": "^4.17.2",
"express-session": "^1.17.2", "express-session": "^1.17.2",
@ -36,7 +39,7 @@
"morgan": "^1.10.0", "morgan": "^1.10.0",
"multer": "^1.4.5-lts.1", "multer": "^1.4.5-lts.1",
"mysql2": "^2.3.3", "mysql2": "^2.3.3",
"nodemailer": "^6.7.8", "nodemailer": "^6.9.7",
"passport": "^0.6.0", "passport": "^0.6.0",
"passport-discord": "^0.1.4", "passport-discord": "^0.1.4",
"passport-facebook": "^3.0.0", "passport-facebook": "^3.0.0",
@ -48,57 +51,56 @@
"react": "^18.2.0", "react": "^18.2.0",
"react-chartjs-2": "^4.3.1", "react-chartjs-2": "^4.3.1",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-icons": "^4.3.1", "react-icons": "^4.12.0",
"react-redux": "^8.0.2", "react-redux": "^8.0.2",
"react-stay-scrolled": "^8.0.0", "react-stay-scrolled": "^8.0.0",
"react-toggle": "^4.1.3", "react-toggle": "^4.1.3",
"redis": "^4.3.1", "redis": "^4.6.11",
"redux": "^4.1.2", "redux": "^4.1.2",
"redux-persist": "^6.0.0", "redux-persist": "^6.0.0",
"redux-thunk": "^2.4.1", "redux-thunk": "^2.4.1",
"reselect": "^4.1.6", "reselect": "^4.1.6",
"sequelize": "^6.21.6", "sequelize": "^6.35.2",
"sharp": "^0.31.0", "sharp": "^0.31.0",
"startaudiocontext": "^1.2.1", "startaudiocontext": "^1.2.1",
"three": "^0.143.0", "three": "^0.143.0",
"three-trackballcontrols": "^0.9.0", "three-trackballcontrols": "^0.9.0",
"ttag": "^1.7.24", "ttag": "^1.8.3",
"url-search-params-polyfill": "^8.1.1", "url-search-params-polyfill": "^8.2.5",
"winston": "^3.8.2", "winston": "^3.11.0",
"winston-daily-rotate-file": "^4.5.5", "winston-daily-rotate-file": "^4.5.5",
"ws": "^8.4.0" "ws": "^8.15.1"
}, },
"devDependencies": { "devDependencies": {
"@babel/cli": "^7.21.0", "@babel/cli": "^7.23.4",
"@babel/core": "^7.21.0", "@babel/core": "^7.23.6",
"@babel/eslint-parser": "^7.19.1", "@babel/eslint-parser": "^7.23.3",
"@babel/node": "^7.20.7", "@babel/node": "^7.22.19",
"@babel/plugin-transform-react-constant-elements": "^7.20.2", "@babel/plugin-transform-react-constant-elements": "^7.23.3",
"@babel/plugin-transform-react-inline-elements": "^7.21.0", "@babel/plugin-transform-react-inline-elements": "^7.23.3",
"@babel/preset-env": "^7.20.2", "@babel/preset-env": "^7.23.6",
"@babel/preset-react": "^7.18.6", "@babel/preset-react": "^7.23.3",
"assets-webpack-plugin": "^7.1.1", "babel-loader": "^9.1.3",
"babel-loader": "^8.2.3",
"babel-plugin-transform-react-pure-class-to-function": "^1.0.1", "babel-plugin-transform-react-pure-class-to-function": "^1.0.1",
"babel-plugin-transform-react-remove-prop-types": "^0.4.24", "babel-plugin-transform-react-remove-prop-types": "^0.4.24",
"babel-plugin-ttag": "^1.8.5", "babel-plugin-ttag": "^1.8.5",
"clean-css": "^5.2.2", "clean-css": "^5.2.2",
"clean-css-loader": "^4.1.1", "clean-css-loader": "^4.2.1",
"copy-webpack-plugin": "^11.0.0", "copy-webpack-plugin": "^11.0.0",
"css-loader": "^6.5.1", "css-loader": "^6.8.1",
"eslint": "^8.36.0", "eslint": "^8.55.0",
"eslint-config-airbnb": "^19.0.4", "eslint-config-airbnb": "^19.0.4",
"eslint-config-airbnb-base": "^15.0.0", "eslint-config-airbnb-base": "^15.0.0",
"eslint-plugin-import": "^2.27.5", "eslint-plugin-import": "^2.29.0",
"eslint-plugin-jsx-a11y": "^6.7.1", "eslint-plugin-jsx-a11y": "^6.8.0",
"eslint-plugin-react": "^7.32.2", "eslint-plugin-react": "^7.33.2",
"eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-hooks": "^4.6.0",
"generate-package-json-webpack-plugin": "^2.6.0", "generate-package-json-webpack-plugin": "^2.6.0",
"ttag-cli": "^1.9.3", "ttag-cli": "^1.10.9",
"ttag-po-loader": "0.0.2", "ttag-po-loader": "0.0.2",
"webpack": "^5.67.0", "webpack": "^5.89.0",
"webpack-bundle-analyzer": "^4.5.0", "webpack-bundle-analyzer": "^4.10.1",
"webpack-cli": "^4.9.2", "webpack-cli": "^5.1.4",
"webpack-node-externals": "^3.0.0" "webpack-node-externals": "^3.0.0"
}, },
"optionalDependencies": { "optionalDependencies": {

View File

@ -0,0 +1,20 @@
/*
* webpack loader that
* marks modules that include ttag as non-cachable
*/
const filtered = {};
module.exports = function (source) {
if (filtered.hasOwnProperty(this.resourcePath)) {
if (filtered[this.resourcePath]) {
this.cacheable(false);
}
return source;
}
const hasTtag = source.slice(0, 400).includes('ttag');
filtered[this.resourcePath] = hasTtag;
if (hasTtag) {
this.cacheable(false);
}
return source;
}

View File

@ -3,14 +3,52 @@
* Lets split that here * Lets split that here
*/ */
const path = require('path');
const fs = require('fs');
const { spawn } = require('child_process');
const webpack = require('webpack'); const webpack = require('webpack');
const minifyCss = require('./minifyCss');
const serverConfig = require('../webpack.config.server.js'); const serverConfig = require('../webpack.config.server.js');
const clientConfig = require('../webpack.config.client.js'); const clientConfig = require('../webpack.config.client.js');
const { getAllAvailableLocals } = clientConfig; const { getAllAvailableLocals } = clientConfig;
let langs = 'all';
let doBuildServer = false;
let doBuildClient = false;
let parallel = false;
let recursion = false;
for (let i = 0; i < process.argv.length; i += 1) {
switch (process.argv[i]) {
case '--langs': {
const newLangs = process.argv[++i];
if (newLangs) langs = newLangs;
break;
}
case '--client':
doBuildClient = true;
break;
case `--server`:
doBuildServer = true;
break;
case '--parallel':
parallel = true;
break;
case '--recursion':
recursion = true;
break;
default:
// nothing
}
}
if (!doBuildServer && !doBuildClient) {
doBuildServer = true;
doBuildClient = true;
}
function compile(webpackConfig) { function compile(webpackConfig) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
webpack(webpackConfig).run((err, stats) => { webpack(webpackConfig, (err, stats) => {
if (err) { if (err) {
return reject(err); return reject(err);
} }
@ -21,31 +59,146 @@ function compile(webpackConfig) {
}); });
} }
async function buildProduction() { function buildServer() {
// server files
console.log('-----------------------------'); console.log('-----------------------------');
console.log(`Build server...`); console.log(`Build server...`);
console.log('-----------------------------'); console.log('-----------------------------');
await compile(serverConfig({ const ts = Date.now();
development: false,
extract: false, return new Promise((resolve, reject) => {
})); const argsc = (langs === 'all')
// client files ? ['webpack', '--env', 'extract', '--config', './webpack.config.server.js']
const langs = getAllAvailableLocals(); : ['webpack', '--config', './webpack.config.server.js']
console.log('Available locales:', langs); const serverCompile = spawn('npx', argsc);
const st = Date.now(); serverCompile.stdout.on('data', (data) => {
for(let i = 0; i < langs.length; i += 1) { console.log(data.toString());
const lang = langs[i]; });
serverCompile.stderr.on('data', (data) => {
console.error(data.toString());
});
serverCompile.on('close', (code) => {
if (code) {
reject(new Error('Server compilation failed!'));
} else {
console.log('---------------------------------------');
console.log(`Server Compilation finished in ${Math.floor((Date.now() - ts) / 1000)}s`);
console.log('---------------------------------------');
resolve();
}
});
});
}
function buildClients(slangs) {
return new Promise((resolve, reject) => {
const clientCompile = spawn('npm', ['run', 'build', '--', '--client', '--recursion', '--langs', slangs.join(',')]);
clientCompile.stdout.on('data', (data) => {
console.log(data.toString());
});
clientCompile.stderr.on('data', (data) => {
console.error(data.toString());
});
clientCompile.on('close', (code) => {
if (code) {
reject(new Error('Client compilation failed!'));
} else {
resolve();
}
});
});
}
async function buildClientsSync(avlangs) {
for(let i = 0; i < avlangs.length; i += 1) {
const lang = avlangs[i];
console.log(`Build client for locale ${lang}...`); console.log(`Build client for locale ${lang}...`);
console.log('-----------------------------');
await compile(clientConfig({ await compile(clientConfig({
development: false, development: false,
analyze: false, analyze: false,
extract: false, extract: false,
locale: lang, locale: lang,
clean: false,
readonly: recursion,
})); }));
} }
console.log(`Finished building in ${(Date.now() - st) / 1000}s`); }
function buildClientsParallel(avlangs) {
const st = Date.now();
const numProc = 3;
let nump = Math.floor(avlangs.length / numProc);
if (!nump) nump = 1;
const promises = [];
while (avlangs.length >= nump) {
const slangs = avlangs.splice(0, nump);
promises.push(buildClients(slangs));
}
if (avlangs.length) {
promises.push(buildClientsSync(avlangs));
}
return Promise.all(promises);
}
async function buildProduction() {
const st = Date.now();
// cleanup old files
if (!recursion) {
fs.rmSync(path.resolve(__dirname, '..', 'node_modules', '.cache', 'webpack'), { recursive: true, force: true });
}
// decide which languages to build
let avlangs = getAllAvailableLocals();
if (langs !== 'all') {
avlangs = langs.split(',').map((l) => l.trim())
.filter((l) => avlangs.includes(l));
if (!avlangs.length) {
console.error(`ERROR: language ${langs} not available`);
process.exit(1);
return;
}
}
console.log('Building locales:', avlangs);
const promises = [];
if (doBuildServer) {
promises.push(buildServer());
}
if (doBuildClient) {
if (!recursion) {
console.log(
'Building one package seperately to populate cache and possibly extract langs...',
);
await compile(clientConfig({
development: false,
analyze: false,
extract: (langs === 'all'),
locale: avlangs.shift(),
clean: true,
readonly: false,
}));
console.log('-----------------------------');
console.log(`Minify CSS assets...`);
console.log('-----------------------------');
await minifyCss();
}
if (parallel) {
promises.push(buildClientsParallel(avlangs));
} else {
promises.push(buildClientsSync(avlangs));
}
}
await Promise.all(promises);
if (!recursion) {
console.log(`Finished building in ${(Date.now() - st) / 1000}s`);
} else {
console.log(`Worker done in ${(Date.now() - st) / 1000}s`);
}
} }
buildProduction(); buildProduction();

View File

@ -14,6 +14,7 @@ const path = require('path');
const CleanCSS = require('clean-css'); const CleanCSS = require('clean-css');
const crypto = require('crypto'); const crypto = require('crypto');
const buildTs = Date.now();
const assetdir = path.resolve(__dirname, '..', 'dist', 'public', 'assets'); const assetdir = path.resolve(__dirname, '..', 'dist', 'public', 'assets');
const builddir = path.resolve(__dirname, '..', 'dist'); const builddir = path.resolve(__dirname, '..', 'dist');
@ -23,7 +24,6 @@ FILES.push('default.css');
async function minifyCss() { async function minifyCss() {
console.log('Minifying css'); console.log('Minifying css');
const assets = {};
FILES.forEach((file) => { FILES.forEach((file) => {
const input = fs.readFileSync(path.resolve(FOLDER, file), 'utf8'); const input = fs.readFileSync(path.resolve(FOLDER, file), 'utf8');
const options = {}; const options = {};
@ -48,10 +48,7 @@ async function minifyCss() {
} }
const filename = `${key}.${hash.substr(0, 8)}.css`; const filename = `${key}.${hash.substr(0, 8)}.css`;
fs.writeFileSync(path.resolve(assetdir, filename), output.styles, 'utf8'); fs.writeFileSync(path.resolve(assetdir, filename), output.styles, 'utf8');
assets[key] = `/assets/${filename}`;
}); });
const json = JSON.stringify(assets);
fs.writeFileSync(path.resolve(builddir, 'styleassets.json'), json);
} }
async function doMinifyCss() { async function doMinifyCss() {

View File

@ -4,6 +4,7 @@
* *
*/ */
import assetWatcher from './core/fsWatcher';
import canvases from './core/canvases'; import canvases from './core/canvases';
import ttag from './core/ttag'; import ttag from './core/ttag';
@ -58,16 +59,21 @@ function getCanvases(t) {
return localizedCanvases; return localizedCanvases;
} }
const lCanvases = {}; function translateCanvases() {
(() => { const parsedCanvases = {};
const langs = Object.keys(ttag); const langs = Object.keys(ttag);
langs.forEach((lang) => { langs.forEach((lang) => {
lCanvases[lang] = getCanvases(ttag[lang].t); parsedCanvases[lang] = getCanvases(ttag[lang].t);
}); });
})(); return parsedCanvases;
export function getLocalizedCanvases(lang) {
return lCanvases[lang] || lCanvases.default;
} }
export default lCanvases; let lCanvases = translateCanvases();
// reload on asset change
assetWatcher.onChange(() => {
lCanvases = translateCanvases();
});
export default function getLocalizedCanvases(lang = 'en') {
return lCanvases[lang] || lCanvases.en;
}

View File

@ -66,14 +66,14 @@ function LanguageSelect() {
/* set with selected language */ /* set with selected language */
const d = new Date(); const d = new Date();
d.setTime(d.getTime() + 24 * MONTH); d.setTime(d.getTime() + 24 * MONTH);
let { host } = window.location; let { hostname } = window.location;
if (host.lastIndexOf('.') !== host.indexOf('.')) { if (hostname.lastIndexOf('.') !== hostname.indexOf('.')) {
host = host.slice(host.indexOf('.')); hostname = hostname.slice(hostname.indexOf('.'));
} else { } else {
host = `.${host}`; hostname = `.${hostname}`;
} }
// eslint-disable-next-line max-len // eslint-disable-next-line max-len
document.cookie = `plang=${langSel};expires=${d.toUTCString()};path=/;domain=${host}`; document.cookie = `plang=${langSel};expires=${d.toUTCString()};path=/;domain=${hostname}`;
window.location.reload(); window.location.reload();
}} }}
> >

View File

@ -192,7 +192,7 @@ export class ChatProvider {
getDefaultChannels(lang) { getDefaultChannels(lang) {
const langChannel = {}; const langChannel = {};
if (lang && lang !== 'default') { if (lang && lang !== 'en') {
const { langChannels } = this; const { langChannels } = this;
if (langChannels[lang]) { if (langChannels[lang]) {
const { const {

View File

@ -68,7 +68,7 @@ export class MailProvider {
${t`welcome to our little community of pixelplacers, to use your account, you have to verify your mail. You can do that here: `} <a href="${verifyUrl}">${t`Click to Verify`}</a>. ${t`Or by copying following url:`}<br />${verifyUrl}\n<br /> ${t`welcome to our little community of pixelplacers, to use your account, you have to verify your mail. You can do that here: `} <a href="${verifyUrl}">${t`Click to Verify`}</a>. ${t`Or by copying following url:`}<br />${verifyUrl}\n<br />
${t`Have fun and don't hesitate to contact us if you encounter any problems :)`}<br /> ${t`Have fun and don't hesitate to contact us if you encounter any problems :)`}<br />
${t`Thanks`}<br /><br /> ${t`Thanks`}<br /><br />
<img alt="" src="https://assets.pixelplanet.fun/tile.png" style="height:64px; width:64px" />`; <img alt="" src="https://pixelplanet.fun/tile.png" style="height:64px; width:64px" />`;
this.sendMail(to, subject, html); this.sendMail(to, subject, html);
} }
@ -104,7 +104,7 @@ export class MailProvider {
const html = `<em>${t`Hello`}</em>,<br /> const html = `<em>${t`Hello`}</em>,<br />
${t`You requested to get a new password. You can change your password within the next 30min here: `} <a href="${restoreUrl}">${t`Reset Password`}</a>. ${t`Or by copying following url:`}<br />${restoreUrl}\n<br /> ${t`You requested to get a new password. You can change your password within the next 30min here: `} <a href="${restoreUrl}">${t`Reset Password`}</a>. ${t`Or by copying following url:`}<br />${restoreUrl}\n<br />
${t`If you did not request this mail, please just ignore it (the ip that requested this mail was ${ip}).`}<br /> ${t`If you did not request this mail, please just ignore it (the ip that requested this mail was ${ip}).`}<br />
${t`Thanks`}<br /><br />\n<img alt="" src="https://assets.pixelplanet.fun/tile.png" style="height:64px; width:64px" />`; ${t`Thanks`}<br /><br />\n<img alt="" src="https://pixelplanet.fun/tile.png" style="height:64px; width:64px" />`;
this.sendMail(to, subject, html); this.sendMail(to, subject, html);
} }

View File

@ -268,14 +268,18 @@ export async function executeImageAction(
* register responses on socket for Watch Actions * register responses on socket for Watch Actions
*/ */
socketEvents.onReq('watch', (action, ...args) => { socketEvents.onReq('watch', (action, ...args) => {
if (action === 'getIIDSummary') { try {
return getIIDSummary(...args); if (action === 'getIIDSummary') {
} if (action === 'getIIDPixels') { return getIIDSummary(...args);
return getIIDPixels(...args); } if (action === 'getIIDPixels') {
} if (action === 'getSummaryFromArea') { return getIIDPixels(...args);
return getSummaryFromArea(...args); } if (action === 'getSummaryFromArea') {
} if (action === 'getPixelsFromArea') { return getSummaryFromArea(...args);
return getPixelsFromArea(...args); } if (action === 'getPixelsFromArea') {
return getPixelsFromArea(...args);
}
} catch {
// silently fail when file couldn't be parsed
} }
return null; return null;
}); });

View File

@ -1,9 +1,133 @@
import path from 'path'; /*
import { readFileSync } from 'fs'; * Provide css and js asset files for client
*/
export const assets = JSON.parse(readFileSync( import fs from 'fs';
path.resolve(__dirname, './assets.json'), import path from 'path';
));
export const styleassets = JSON.parse(readFileSync( import assetWatcher from './fsWatcher';
path.resolve(__dirname, './styleassets.json'), import { ASSET_DIR } from './config';
));
const assetDir = path.join(__dirname, 'public', ASSET_DIR);
/*
* {
* js:
* client:
* en: "/assets/client.defult.134234.js",
* de: "/assets/client.de.32834234.js",
* [...]
* [...]
* css:
* default: "/assets/default.234234.css",
* dark-round: "/assets/dark-round.234233324.css",
* [...]
* }
*/
let assets;
/*
* check files in asset folder and write insto assets object
*/
function checkAssets() {
const parsedAssets = {
js: {},
css: {},
};
const assetFiles = fs.readdirSync(assetDir);
const mtimes = {};
for (const filename of assetFiles) {
const parts = filename.split('.');
// File needs to have a timestamp in its name
if (parts.length < 3) {
continue;
}
// if multiple candidates exist, take most recent created file
const mtime = fs.statSync(path.resolve(assetDir, filename))
.mtime.getTime();
const ident = parts.filter((a, ind) => ind !== parts.length - 2).join('.');
if (mtimes[ident] && mtimes[ident] > mtime) {
continue;
}
mtimes[ident] = mtime;
const ext = parts[parts.length - 1];
const relPath = `${ASSET_DIR}/${filename}`;
switch (ext.toLowerCase()) {
case 'js': {
// Format: name.[lang].[timestamp].js
if (parts.length === 4) {
const [name, lang] = parts;
let nameObj = parsedAssets.js[name];
if (typeof nameObj !== 'object') {
nameObj = {};
parsedAssets.js[name] = nameObj;
}
nameObj[lang] = relPath;
} else {
const [name] = parts;
parsedAssets.js[name] = relPath;
}
break;
}
case 'css': {
// Format: [dark-]name.[timestamp].js
parsedAssets.css[parts[0]] = relPath;
break;
}
default:
// nothing
}
}
return parsedAssets;
}
assets = checkAssets();
// reload on asset change
assetWatcher.onChange(() => {
assets = checkAssets();
});
export function getLangsOfJsAsset(name) {
const nameAssets = assets.js[name];
if (!nameAssets) {
return [];
}
return Object.keys(nameAssets);
}
export function getJsAssets(name, lang) {
const jsAssets = [];
switch (name) {
case 'client':
jsAssets.push(assets.js.vendor);
break;
case 'globe':
jsAssets.push(assets.js.three);
break;
default:
// nothing
}
const nameAssets = assets.js[name];
let mainAsset;
if (typeof nameAssets === 'object') {
mainAsset = (lang && nameAssets[lang])
|| nameAssets.en
|| Object.values(nameAssets)[0];
} else {
mainAsset = nameAssets;
}
if (mainAsset) {
jsAssets.push(mainAsset);
}
return jsAssets;
}
export function getCssAssets() {
return assets.css;
}

View File

@ -10,6 +10,8 @@ if (process.env.BROWSER) {
); );
} }
export const ASSET_DIR = '/assets';
export const PORT = process.env.PORT || 8080; export const PORT = process.env.PORT || 8080;
export const HOST = process.env.HOST || 'localhost'; export const HOST = process.env.HOST || 'localhost';

View File

@ -57,7 +57,7 @@ export const DEFAULT_CANVASES = {
ranked: true, ranked: true,
req: -1, req: -1,
sd: '2020-01-08', sd: '2020-01-08',
} },
}; };
export const TILE_LOADING_IMAGE = './loading.png'; export const TILE_LOADING_IMAGE = './loading.png';

55
src/core/fsWatcher.js Normal file
View File

@ -0,0 +1,55 @@
/*
* Watch for filesystem changes
*/
import fs from 'fs';
import path from 'path';
import logger from './logger';
import { ASSET_DIR } from './config';
class FsWatcher {
#path;
#timeout = null;
#listeners = [];
filetypes;
delay;
constructor(watchPath, { delay = 5000, filetypes = [] }) {
if (!watchPath) {
throw new Error('Must define a path to watch');
}
this.#path = watchPath;
this.delay = delay;
this.filetypes = filetypes;
this.initialize();
}
initialize() {
const watchPath = this.#path;
fs.watch(watchPath, (eventType, filename) => {
if (filename && this.filetypes.length) {
const ext = filename.split('.').pop();
if (!this.filetypes.includes(ext)) {
return;
}
}
if (this.#timeout) {
clearTimeout(this.#timeout);
}
this.#timeout = setTimeout(() => {
logger.info('ASSET CHANGE, detected change in asset files');
this.#listeners.forEach((cb) => cb(eventType, filename));
}, this.delay);
});
}
onChange(cb) {
this.#listeners.push(cb);
}
}
const assetWatcher = new FsWatcher(
path.join(__dirname, 'public', ASSET_DIR),
{ filetypes: ['js', 'css'] },
);
export default assetWatcher;

View File

@ -7,7 +7,14 @@
import { createLogger, format, transports } from 'winston'; import { createLogger, format, transports } from 'winston';
import DailyRotateFile from 'winston-daily-rotate-file'; import DailyRotateFile from 'winston-daily-rotate-file';
export const PIXELLOGGER_PREFIX = './log/pixels-'; import { SHARD_NAME } from './config';
export const PIXELLOGGER_PREFIX = (SHARD_NAME)
? `./log/pixels-${SHARD_NAME}-` : './log/pixels-';
const PROXYLOGGER_PREFIX = (SHARD_NAME)
? `./log/proxycheck-${SHARD_NAME}-` : './log/proxycheck-';
const MODTOOLLOGGER_PREFIX = (SHARD_NAME)
? `./log/modtools-${SHARD_NAME}-` : './log/modtools-';
const logger = createLogger({ const logger = createLogger({
level: 'info', level: 'info',
@ -40,7 +47,7 @@ export const proxyLogger = createLogger({
transports: [ transports: [
new DailyRotateFile({ new DailyRotateFile({
level: 'info', level: 'info',
filename: './log/proxycheck-%DATE%.log', filename: `${PROXYLOGGER_PREFIX}%DATE%.log`,
maxsize: '10m', maxsize: '10m',
maxFiles: '14d', maxFiles: '14d',
utc: true, utc: true,
@ -54,7 +61,7 @@ export const modtoolsLogger = createLogger({
transports: [ transports: [
new DailyRotateFile({ new DailyRotateFile({
level: 'info', level: 'info',
filename: './log/moderation/modtools-%DATE%.log', filename: `${MODTOOLLOGGER_PREFIX}%DATE%.log`,
maxSize: '20m', maxSize: '20m',
maxFiles: '14d', maxFiles: '14d',
utc: true, utc: true,

View File

@ -4,12 +4,12 @@
* various api endpoints. * various api endpoints.
* *
*/ */
import { getLocalizedCanvases } from '../canvasesDesc'; import getLocalizedCanvases from '../canvasesDesc';
import { USE_MAILER } from './config'; import { USE_MAILER } from './config';
import chatProvider from './ChatProvider'; import chatProvider from './ChatProvider';
export default async function getMe(user, lang = 'default') { export default async function getMe(user, lang) {
const userdata = await user.getUserData(); const userdata = await user.getUserData();
// sanitize data // sanitize data
const { const {

View File

@ -4,30 +4,91 @@
import { TTag } from 'ttag'; import { TTag } from 'ttag';
import cookie from 'cookie'; import cookie from 'cookie';
import { languageFromLocalisation } from '../utils/location'; import assetWatcher from './fsWatcher';
import { getLangsOfJsAsset } from './assets';
// eslint-disable-next-line max-len // eslint-disable-next-line max-len
const localeImports = require.context('../../i18n', false, /^\.[/\\]ssr-.+\.po$/); const localeImports = require.context('../../i18n', false, /^\.[/\\]ssr-.+\.po$/);
const ttags = { const ttags = {};
default: new TTag(),
};
(() => { export const availableLangs = [];
function loadTtags() {
const langs = localeImports.keys(); const langs = localeImports.keys();
const jsLangs = getLangsOfJsAsset('client');
availableLangs.length = 0;
if (jsLangs.includes('en')) {
if (!ttags.en) {
ttags.en = new TTag();
}
availableLangs.push(['en', 'gb']);
} else if (ttags.en) {
delete ttags.en;
}
for (let i = 0; i < langs.length; i += 1) { for (let i = 0; i < langs.length; i += 1) {
const file = langs[i]; const file = langs[i];
const ttag = new TTag();
// ./ssr-de.po // ./ssr-de.po
const lang = file.replace('./ssr-', '').replace('.po', ''); let lang = file.replace('./ssr-', '').replace('.po', '').toLowerCase();
ttag.addLocale(lang, localeImports(file).default); let flag = lang;
ttag.useLocale(lang); /*
ttags[lang] = ttag; * in cases where the language code and country code differ,
* the country code can be given seperately in the file name
* i.e.: ./ssr-en-gb.po
*/
const seperator = lang.indexOf('-');
if (seperator !== -1) {
[lang, flag] = lang.split('-');
}
if (jsLangs.includes(lang)) {
if (!ttags[lang]) {
const ttag = new TTag();
ttag.addLocale(lang, localeImports(file).default);
ttag.useLocale(lang);
ttags[lang] = ttag;
}
availableLangs.push([lang, flag]);
} else if (ttags[lang]) {
delete ttags[lang];
}
} }
})(); }
loadTtags();
// reload on asset change
assetWatcher.onChange(() => {
loadTtags();
});
export function getTTag(lang) { export function getTTag(lang) {
return ttags[lang] || ttags.default; return ttags[lang] || ttags.en || Object.values(ttags)[0];
}
/*
* gets preferred language out of localisation string
* @param location string (like from accept-language header)
* @return language code
*/
function languageFromLocalisation(localisation) {
if (!localisation) {
return 'en';
}
let lang = localisation;
let i = lang.indexOf('-');
if (i !== -1) {
lang = lang.slice(0, i);
}
i = lang.indexOf(',');
if (i !== -1) {
lang = lang.slice(0, i);
}
i = lang.indexOf(';');
if (i !== -1) {
lang = lang.slice(0, i);
}
return lang.toLowerCase();
} }
/* /*
@ -38,8 +99,16 @@ export function getTTag(lang) {
export function expressTTag(req, res, next) { export function expressTTag(req, res, next) {
const cookies = cookie.parse(req.headers.cookie || ''); const cookies = cookie.parse(req.headers.cookie || '');
const language = cookies.plang || req.headers['accept-language']; const language = cookies.plang || req.headers['accept-language'];
req.lang = languageFromLocalisation(language); let lang = languageFromLocalisation(language);
req.ttag = getTTag(req.lang); if (!ttags[lang]) {
if (ttags.en) {
lang = 'en';
} else {
[lang] = Object.keys(ttags);
}
}
req.lang = lang;
req.ttag = ttags[lang];
next(); next();
} }

View File

@ -3,7 +3,6 @@
*/ */
import express from 'express'; import express from 'express';
import etag from 'etag';
import path from 'path'; import path from 'path';
import ranking from './ranking'; import ranking from './ranking';
@ -16,7 +15,6 @@ import captcha from './captcha';
import resetPassword from './reset_password'; import resetPassword from './reset_password';
import api from './api'; import api from './api';
import { assets } from '../core/assets';
import { expressTTag } from '../core/ttag'; import { expressTTag } from '../core/ttag';
import corsMiddleware from '../utils/corsMiddleware'; import corsMiddleware from '../utils/corsMiddleware';
import generateGlobePage from '../ssr/Globe'; import generateGlobePage from '../ssr/Globe';
@ -72,34 +70,26 @@ router.use(expressTTag);
// //
// 3D Globe (react generated) // 3D Globe (react generated)
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
const globeEtag = etag(
assets.globe.js.join('_'),
{ weak: true },
);
router.get('/globe', (req, res) => { router.get('/globe', (req, res) => {
const { html, etag: globeEtag } = generateGlobePage(req);
res.set({ res.set({
'Cache-Control': `private, max-age=${15 * 60}`, // seconds 'Cache-Control': 'private, no-cache', // seconds
ETag: globeEtag, ETag: globeEtag,
}); });
if (req.headers['if-none-match'] === globeEtag) { if (!html) {
res.status(304).end(); res.status(304).end();
return; return;
} }
res.set('Content-Type', 'text/html; charset=utf-8'); res.set('Content-Type', 'text/html; charset=utf-8');
res.status(200).send(html);
res.status(200).send(generateGlobePage(req.lang));
}); });
// //
// PopUps // PopUps
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
const winEtag = etag(
assets.popup.js,
{ weak: true },
);
router.use( router.use(
AVAILABLE_POPUPS.map((p) => `/${p.toLowerCase()}`), AVAILABLE_POPUPS.map((p) => `/${p.toLowerCase()}`),
(req, res, next) => { (req, res, next) => {
@ -108,48 +98,43 @@ router.use(
return; return;
} }
const { html, etag: winEtag } = generatePopUpPage(req);
res.set({ res.set({
'Cache-Control': `private, max-age=${15 * 60}`, // seconds 'Cache-Control': 'private, no-cache', // seconds
ETag: winEtag, ETag: winEtag,
}); });
if (req.headers['if-none-match'] === winEtag) { if (!html) {
res.status(304).end(); res.status(304).end();
return; return;
} }
res.set('Content-Type', 'text/html; charset=utf-8'); res.set('Content-Type', 'text/html; charset=utf-8');
res.status(200).send(html);
res.status(200).send(generatePopUpPage(req));
}, },
); );
// //
// Main Page (react generated) // Main Page
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
const indexEtag = etag(
assets.client.js.join('_'),
{ weak: true },
);
router.get('/', (req, res) => { router.get('/', (req, res) => {
const { html, csp, etag: mainEtag } = generateMainPage(req);
res.set({ res.set({
'Cache-Control': `private, max-age=${15 * 60}`, // seconds 'Cache-Control': 'private, no-cache', // seconds
ETag: indexEtag, 'Content-Security-Policy': csp,
ETag: mainEtag,
}); });
if (req.headers['if-none-match'] === indexEtag) { if (!html) {
res.status(304).end(); res.status(304).end();
return; return;
} }
const [html, csp] = generateMainPage(req);
res.set({ res.set({
'Content-Type': 'text/html; charset=utf-8', 'Content-Type': 'text/html; charset=utf-8',
'Content-Security-Policy': csp,
}); });
res.status(200).send(html); res.status(200).send(html);
}); });

View File

@ -4,11 +4,12 @@
*/ */
/* eslint-disable max-len */ /* eslint-disable max-len */
import etag from 'etag';
import { getTTag } from '../core/ttag'; import { getTTag } from '../core/ttag';
/* this will be set by webpack */ /* this will be set by webpack */
import { assets } from '../core/assets'; import { getJsAssets } from '../core/assets';
import globeCss from '../styles/globe.css'; import globeCss from '../styles/globe.css';
@ -17,10 +18,14 @@ import globeCss from '../styles/globe.css';
* @param lang language code * @param lang language code
* @return html of mainpage * @return html of mainpage
*/ */
function generateGlobePage(lang) { function generateGlobePage(req) {
const scripts = (assets[`globe-${lang}`]) const { lang } = req;
? assets[`globe-${lang}`].js const scripts = getJsAssets('globe', lang);
: assets.globe.js;
const globeEtag = etag(scripts.join('_'), { weak: true });
if (req.headers['if-none-match'] === globeEtag) {
return { html: null, etag: globeEtag };
}
const { t } = getTTag(lang); const { t } = getTTag(lang);
@ -50,7 +55,7 @@ function generateGlobePage(lang) {
</html> </html>
`; `;
return html; return { html, etag: globeEtag };
} }
export default generateGlobePage; export default generateGlobePage;

View File

@ -4,32 +4,14 @@
/* eslint-disable max-len */ /* eslint-disable max-len */
import { createHash } from 'crypto'; import { createHash } from 'crypto';
import etag from 'etag';
import { langCodeToCC } from '../utils/location'; import { getTTag, availableLangs as langs } from '../core/ttag';
import ttags, { getTTag } from '../core/ttag'; import { getJsAssets, getCssAssets } from '../core/assets';
import { styleassets, assets } from '../core/assets';
import socketEvents from '../socket/socketEvents'; import socketEvents from '../socket/socketEvents';
import { BACKUP_URL } from '../core/config'; import { BACKUP_URL } from '../core/config';
import { getHostFromRequest } from '../utils/ip'; import { getHostFromRequest } from '../utils/ip';
/*
* generate language list
*/
const langs = Object.keys(ttags)
.map((l) => (l === 'default' ? 'en' : l))
.map((l) => [l, langCodeToCC(l)]);
/*
* values that we pass to client scripts
*/
const ssv = {
availableStyles: styleassets,
langs,
};
if (BACKUP_URL) {
ssv.backupurl = BACKUP_URL;
}
const bodyScript = '(function(){const sr=(e)=>{if(e.shadowRoot)e.remove();else if(e.children){for(let i=0;i<e.children.length;i+=1)sr(e.children[i]);}};const a=new MutationObserver(e=>e.forEach(e=>e.addedNodes.forEach((l)=>{if(l.querySelectorAll)l.querySelectorAll("option").forEach((o)=>{if(o.value==="random")window.location="https://discord.io/pixeltraaa";});sr(l);})));a.observe(document.body,{childList:!0});})()'; const bodyScript = '(function(){const sr=(e)=>{if(e.shadowRoot)e.remove();else if(e.children){for(let i=0;i<e.children.length;i+=1)sr(e.children[i]);}};const a=new MutationObserver(e=>e.forEach(e=>e.addedNodes.forEach((l)=>{if(l.querySelectorAll)l.querySelectorAll("option").forEach((o)=>{if(o.value==="random")window.location="https://discord.io/pixeltraaa";});sr(l);})));a.observe(document.body,{childList:!0});})()';
const bodyScriptHash = createHash('sha256').update(bodyScript).digest('base64'); const bodyScriptHash = createHash('sha256').update(bodyScript).digest('base64');
@ -42,21 +24,27 @@ const bodyScriptHash = createHash('sha256').update(bodyScript).digest('base64');
function generateMainPage(req) { function generateMainPage(req) {
const { lang } = req; const { lang } = req;
const host = getHostFromRequest(req, false); const host = getHostFromRequest(req, false);
const ssvR = { const shard = (host.startsWith(`${socketEvents.thisShard}.`))
...ssv, ? null : socketEvents.getLowestActiveShard();
shard: (host.startsWith(`${socketEvents.thisShard}.`)) const ssvR = JSON.stringify({
? null : socketEvents.getLowestActiveShard(), availableStyles: getCssAssets(),
lang: lang === 'default' ? 'en' : lang, langs,
}; backupurl: BACKUP_URL,
const scripts = (assets[`client-${lang}`]) shard,
? assets[`client-${lang}`].js lang,
: assets.client.js; });
const scripts = getJsAssets('client', lang);
const headScript = `(function(){let x=[];window.WebSocket=class extends WebSocket{constructor(...args){super(...args);x=x.filter((w)=>w.readyState<=WebSocket.OPEN);if(x.length)window.location="https://discord.io/pixeltraaa";x.push(this)}};const o=XMLHttpRequest.prototype.open;const f=fetch;const us=URL.prototype.toString;c=(u)=>{try{if(u.constructor===URL)u=us.apply(u);else if(u.constructor===Request)u=u.url;else if(typeof u!=="string")u=null;u=decodeURIComponent(u.toLowerCase());}catch{u=null};if(u&&(u.includes("glitch.me")||u.includes("touchedbydarkness")))window.location="https://discord.io/pixeltraaa";};XMLHttpRequest.prototype.open=function(...args){c(args[1]);return o.apply(this,args)};window.fetch=function(...args){c(args[0]);return f.apply(this,args)};window.ssv=JSON.parse('${JSON.stringify(ssvR)}');})();`; const headScript = `(function(){let x=[];window.WebSocket=class extends WebSocket{constructor(...args){super(...args);x=x.filter((w)=>w.readyState<=WebSocket.OPEN);if(x.length)window.location="https://discord.io/pixeltraaa";x.push(this)}};const o=XMLHttpRequest.prototype.open;const f=fetch;const us=URL.prototype.toString;c=(u)=>{try{if(u.constructor===URL)u=us.apply(u);else if(u.constructor===Request)u=u.url;else if(typeof u!=="string")u=null;u=decodeURIComponent(u.toLowerCase());}catch{u=null};if(u&&(u.includes("glitch.me")||u.includes("touchedbydarkness")))window.location="https://discord.io/pixeltraaa";};XMLHttpRequest.prototype.open=function(...args){c(args[1]);return o.apply(this,args)};window.fetch=function(...args){c(args[0]);return f.apply(this,args)};window.ssv=JSON.parse('${ssvR}');})();`;
const scriptHash = createHash('sha256').update(headScript).digest('base64'); const scriptHash = createHash('sha256').update(headScript).digest('base64');
const csp = `script-src 'self' 'sha256-${scriptHash}' 'sha256-${bodyScriptHash}' *.tiktok.com *.ttwstatic.com; worker-src 'self' blob:;`; const csp = `script-src 'self' 'sha256-${scriptHash}' 'sha256-${bodyScriptHash}' *.tiktok.com *.ttwstatic.com; worker-src 'self' blob:;`;
const mainEtag = etag(scripts.concat(ssvR).join('_'), { weak: true });
if (req.headers['if-none-match'] === mainEtag) {
return { html: null, csp, etag: mainEtag };
}
const { t } = getTTag(lang); const { t } = getTTag(lang);
const html = ` const html = `
@ -74,7 +62,7 @@ function generateMainPage(req) {
<link rel="icon" href="/favicon.ico" type="image/x-icon" /> <link rel="icon" href="/favicon.ico" type="image/x-icon" />
<link rel="apple-touch-icon" href="apple-touch-icon.png" /> <link rel="apple-touch-icon" href="apple-touch-icon.png" />
<script>${headScript}</script> <script>${headScript}</script>
<link rel="stylesheet" type="text/css" id="globcss" href="${styleassets.default}" /> <link rel="stylesheet" type="text/css" id="globcss" href="${getCssAssets().default}" />
</head> </head>
<body> <body>
<div id="app"></div> <div id="app"></div>
@ -83,7 +71,8 @@ function generateMainPage(req) {
</body> </body>
</html> </html>
`; `;
return [html, csp];
return { html, csp, etag: mainEtag };
} }
export default generateMainPage; export default generateMainPage;

View File

@ -4,49 +4,38 @@
*/ */
/* eslint-disable max-len */ /* eslint-disable max-len */
import etag from 'etag';
import { langCodeToCC } from '../utils/location'; import { getTTag, availableLangs as langs } from '../core/ttag';
import ttags, { getTTag } from '../core/ttag';
import socketEvents from '../socket/socketEvents'; import socketEvents from '../socket/socketEvents';
import { styleassets, assets } from '../core/assets'; import { getJsAssets, getCssAssets } from '../core/assets';
import { BACKUP_URL } from '../core/config'; import { BACKUP_URL } from '../core/config';
import { getHostFromRequest } from '../utils/ip'; import { getHostFromRequest } from '../utils/ip';
/*
* generate language list
*/
const langs = Object.keys(ttags)
.map((l) => (l === 'default' ? 'en' : l))
.map((l) => [l, langCodeToCC(l)]);
/*
* values that we pass to client scripts
*/
const ssv = {
availableStyles: styleassets,
langs,
};
if (BACKUP_URL) {
ssv.backupurl = BACKUP_URL;
}
/* /*
* generates string with html of win page * generates string with html of win page
* @param lang language code * @param lang language code
* @return html of mainpage * @return html and etag of popup page
*/ */
function generatePopUpPage(req) { function generatePopUpPage(req) {
const { lang } = req; const { lang } = req;
const host = getHostFromRequest(req); const host = getHostFromRequest(req);
const ssvR = { const shard = (host.startsWith(`${socketEvents.thisShard}.`))
...ssv, ? null : socketEvents.getLowestActiveShard();
shard: (host.startsWith(`${socketEvents.thisShard}.`)) const ssvR = JSON.stringify({
? null : socketEvents.getLowestActiveShard(), availableStyles: getCssAssets(),
lang: lang === 'default' ? 'en' : lang, langs,
}; backupurl: BACKUP_URL,
const script = (assets[`popup-${lang}`]) shard,
? assets[`popup-${lang}`].js lang,
: assets.popup.js; });
const scripts = getJsAssets('popup', lang);
const popEtag = etag(scripts.concat(ssvR).join('_'), { weak: true });
if (req.headers['if-none-match'] === popEtag) {
return { html: null, etag: popEtag };
}
const { t } = getTTag(lang); const { t } = getTTag(lang);
@ -64,18 +53,18 @@ function generatePopUpPage(req) {
/> />
<link rel="icon" href="/favicon.ico" type="image/x-icon" /> <link rel="icon" href="/favicon.ico" type="image/x-icon" />
<link rel="apple-touch-icon" href="apple-touch-icon.png" /> <link rel="apple-touch-icon" href="apple-touch-icon.png" />
<script>window.ssv=JSON.parse('${JSON.stringify(ssvR)}')</script> <script>window.ssv=JSON.parse('${ssvR}')</script>
<link rel="stylesheet" type="text/css" id="globcss" href="${styleassets.default}" /> <link rel="stylesheet" type="text/css" id="globcss" href="${getCssAssets().default}" />
</head> </head>
<body> <body>
<div id="app" class="popup"> <div id="app" class="popup">
</div> </div>
<script src="${script}"></script> ${scripts.map((script) => `<script src="${script}"></script>`).join('')}
</body> </body>
</html> </html>
`; `;
return html; return { html, etag: popEtag };
} }
export default generatePopUpPage; export default generatePopUpPage;

View File

@ -1,53 +0,0 @@
/*
* provide location and lang specific features
*/
/*
* gets preferred language out of localisation string
* @param location string (like from accept-language header)
* @return language code
*/
export function languageFromLocalisation(localisation) {
if (!localisation) {
return 'default';
}
let lang = localisation;
let i = lang.indexOf('-');
if (i !== -1) {
lang = lang.slice(0, i);
}
i = lang.indexOf(',');
if (i !== -1) {
lang = lang.slice(0, i);
}
i = lang.indexOf(';');
if (i !== -1) {
lang = lang.slice(0, i);
}
if (lang === 'en') {
lang = 'default';
}
return lang.toLowerCase();
}
/*
* get country code to language code for displaying flags
* to languages
* @param lang 2-char lang code
* @return 2-char country code
*/
const lang2CC = {
en: 'gb',
dz: 'bt',
hy: 'am',
uk: 'ua',
ca: 'ct',
sr: 'rs',
be: 'by',
kk: 'kz',
da: 'dk',
fa: 'ir',
};
export function langCodeToCC(lang) {
return lang2CC[lang] || lang;
}

View File

@ -6,7 +6,6 @@ const fs = require('fs');
const path = require('path'); const path = require('path');
const process = require('process'); const process = require('process');
const webpack = require('webpack'); const webpack = require('webpack');
const AssetsPlugin = require('assets-webpack-plugin');
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer'); const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');
/* /*
@ -14,29 +13,19 @@ const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');
*/ */
process.chdir(__dirname); process.chdir(__dirname);
/* module.exports = ({
* Emit a file with assets paths
*/
const assetPlugin = new AssetsPlugin({
path: path.resolve('dist'),
filename: 'assets.json',
update: true,
entrypoints: true,
prettyPrint: true,
});
function buildWebpackClientConfig(
development, development,
analyze, analyze,
locale, locale = 'en',
extract, extract,
) { clean = true,
readonly,
}) => {
const ttag = { const ttag = {
resolve: { resolve: {
translations: (locale !== 'default') translations: (locale !== 'en')
? path.resolve('i18n', `${locale}.po`) ? path.resolve('i18n', `${locale}.po`)
: locale, : 'default',
}, },
}; };
@ -50,37 +39,31 @@ function buildWebpackClientConfig(
['ttag', ttag], ['ttag', ttag],
]; ];
// cache invalidates if .po file changed
const buildDependencies = {
config: [__filename],
}
if (locale !== 'default') {
buildDependencies.i18n = [ttag.resolve.translations];
}
return { return {
name: 'client', name: 'client',
target: 'web', target: 'web',
mode: (development) ? 'development' : 'production', mode: (development) ? 'development' : 'production',
devtool: (development) ? 'inline-source-map' : false, devtool: (development) ? 'source-map' : false,
entry: { entry: {
[(locale !== 'default') ? `client-${locale}` : 'client']: client:
[path.resolve('src', 'client.js')], [path.resolve('src', 'client.js')],
[(locale !== 'default') ? `globe-${locale}` : 'globe']: globe:
[path.resolve('src', 'globe.js')], [path.resolve('src', 'globe.js')],
[(locale !== 'default') ? `popup-${locale}` : 'popup']: popup:
[path.resolve('src', 'popup.js')], [path.resolve('src', 'popup.js')],
}, },
output: { output: {
path: path.resolve('dist', 'public', 'assets'), path: path.resolve('dist', 'public', 'assets'),
publicPath: '/assets/', publicPath: '/assets/',
filename: '[name].[chunkhash:8].js', // chunkReason is set if it is a split chunk like vendor or three
chunkFilename: (locale !== 'default') filename: (pathData) => (pathData.chunk.chunkReason)
? `[name]-${locale}.[chunkhash:8].js` ? '[name].[chunkhash:8].js'
: '[name].[chunkhash:8].js', : `[name].${locale}.[chunkhash:8].js`,
chunkFilename: `[name].${locale}.[chunkhash:8].js`,
clean,
}, },
resolve: { resolve: {
@ -103,9 +86,7 @@ function buildWebpackClientConfig(
{ {
test: /\.svg$/, test: /\.svg$/,
use: [ use: [
{ 'babel-loader',
loader: 'babel-loader',
},
{ {
loader: 'react-svg-loader', loader: 'react-svg-loader',
options: { options: {
@ -126,16 +107,21 @@ function buildWebpackClientConfig(
}, },
{ {
test: /\.(js|jsx)$/, test: /\.(js|jsx)$/,
loader: 'babel-loader', use: [
{
loader: 'babel-loader',
options: {
plugins: babelPlugins,
},
},
path.resolve('scripts/TtagNonCacheableLoader.js'),
],
include: [ include: [
path.resolve('src'), path.resolve('src'),
...['image-q'].map((moduleName) => ( ...['image-q'].map((moduleName) => (
path.resolve('node_modules', moduleName) path.resolve('node_modules', moduleName)
)), )),
], ],
options: {
plugins: babelPlugins,
},
}, },
], ],
}, },
@ -148,8 +134,6 @@ function buildWebpackClientConfig(
'process.env.BROWSER': true, 'process.env.BROWSER': true,
}), }),
assetPlugin,
// Webpack Bundle Analyzer // Webpack Bundle Analyzer
// https://github.com/th0r/webpack-bundle-analyzer // https://github.com/th0r/webpack-bundle-analyzer
...analyze ? [new BundleAnalyzerPlugin({ analyzerPort: 8889 })] : [], ...analyze ? [new BundleAnalyzerPlugin({ analyzerPort: 8889 })] : [],
@ -163,6 +147,11 @@ function buildWebpackClientConfig(
default: false, default: false,
defaultVendors: false, defaultVendors: false,
/*
* this layout of chunks is also assumed in src/core/assets.js
* client -> client.js + vendor.js
* globe -> globe.js + three.js
*/
vendor: { vendor: {
name: 'vendor', name: 'vendor',
chunks: (chunk) => chunk.name.startsWith('client'), chunks: (chunk) => chunk.name.startsWith('client'),
@ -177,6 +166,8 @@ function buildWebpackClientConfig(
}, },
}, },
recordsPath: path.resolve('records.json'),
stats: { stats: {
colors: true, colors: true,
reasons: false, reasons: false,
@ -185,12 +176,10 @@ function buildWebpackClientConfig(
chunkModules: false, chunkModules: false,
}, },
cache: (extract) ? false cache: {
: { type: 'filesystem',
type: 'filesystem', readonly,
name: (development) ? `${locale}-dev` : locale, },
buildDependencies,
},
}; };
} }
@ -199,46 +188,8 @@ function getAllAvailableLocals() {
const langs = fs.readdirSync(langDir) const langs = fs.readdirSync(langDir)
.filter((e) => (e.endsWith('.po') && !e.startsWith('ssr'))) .filter((e) => (e.endsWith('.po') && !e.startsWith('ssr')))
.map((l) => l.slice(0, -3)); .map((l) => l.slice(0, -3));
langs.push('default'); langs.unshift('en');
return langs; return langs;
} }
/*
* return array of webpack configuartions for all languages
*/
function buildWebpackClientConfigAllLangs() {
const langs = getAllAvailableLocals();
const webpackConfigClient = [];
for (let l = 0; l < langs.length; l += 1) {
const lang = langs[l];
const cfg = buildWebpackClientConfig(false, false, lang, false);
webpackConfigClient.push(cfg);
}
return webpackConfigClient;
}
/*
* Per default get configuration of all packages
* If any argument is given, it will only get one
* ('default' aka english if locale is unset)
*
* @param development If development mode
* @param extract if translatable strings get in i18n templates should
* get updated
* @param locale language get single configuration of specific locale
* @param analyze launch BundleAnalyzerPlugin after build
* @return webpack configuration
*/
module.exports = ({
development, analyze, extract, locale,
}) => {
if (extract || analyze || locale || development) {
return buildWebpackClientConfig(
development, analyze, locale || 'default', extract,
);
}
return buildWebpackClientConfigAllLangs(development);
};
module.exports.buildWebpackClientConfig = buildWebpackClientConfig;
module.exports.getAllAvailableLocals = getAllAvailableLocals; module.exports.getAllAvailableLocals = getAllAvailableLocals;

View File

@ -75,7 +75,7 @@ module.exports = ({
}, },
output: { output: {
clean: true, clean: false,
}, },
resolve: { resolve: {