Compare commits

...

32 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
878f2cd269
Add ge translation 2023-12-01 17:51:32 +03:00
41 changed files with 7077 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

1575
i18n/ge.po Normal file

File diff suppressed because it is too large Load Diff

477
i18n/ssr-ge.po Normal file
View File

@ -0,0 +1,477 @@
msgid ""
msgstr ""
"Project-Id-Version: \n"
"POT-Creation-Date: \n"
"PO-Revision-Date: \n"
"Last-Translator: \n"
"Language-Team: \n"
"Language: ka\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Generator: Poedit 3.4.1\n"
#: src/core/ChatProvider.js:434
msgid "You can not send chat messages with proxy"
msgstr "ვერ გაგზავნი მესიჯებს პროქსით"
#: src/core/ChatProvider.js:436
msgid "Your country is temporary muted from this chat channel"
msgstr "თქვენი ქვეყანა დამუდებული არის ამ ჩატიდან"
#: src/core/ChatProvider.js:439
msgid "You are permanently muted, join our guilded to apppeal the mute"
msgstr ""
"თქვენ სამუდამოდ დადუმებული ხართ, შემოუერთდით ჩვენს გილდიას, რათა გაასაჩივროთ "
"მუნჯი"
#: src/core/ChatProvider.js:441
msgid "You are banned"
msgstr "აკრძალული ხარ. თქვენ ფიქრობთ, რომ ეს გაუმართლებელია? შეამოწმეთ"
#: src/core/ChatProvider.js:443
msgid "Your Internet Provider is banned"
msgstr "თქვენს ინტერნეტ პროვაიდერს აეკრძალა ამ თამაშის თამაში"
#: src/core/ChatProvider.js:448
#, javascript-format
msgid "You are muted for another ${ timeMin } minutes"
msgstr "თქვენ დადუმებული ხართ კიდევ $ {timeMin } წუთით"
#: src/core/ChatProvider.js:450
msgid "You are muted for another ${ ttl } seconds"
msgstr "თქვენ დადუმებული ხართ კიდევ ${ ttl } წამით"
#: src/core/ChatProvider.js:467
#, javascript-format
msgid "You are sending messages too fast, you have to wait ${ waitTime }s :("
msgstr ""
"თქვენ ძალიან სწრაფად აგზავნით შეტყობინებებს, უნდა დაელოდოთ ${ waitTime }s :("
#: src/core/ChatProvider.js:471
msgid "You don't have access to this channel"
msgstr "თქვენ არ გაქვთ წვდომა ამ არხზე"
#: src/core/ChatProvider.js:488
msgid "Your mail has to be verified in order to chat"
msgstr "თქვენი ფოსტა უნდა დადასტურდეს ჩეთისთვის"
#: src/core/ChatProvider.js:498
msgid "You can't send a message this long :("
msgstr "ამდენ ხანს მესიჯს ვერ გაუგზავნი :("
#: src/core/ChatProvider.js:502
msgid "Please use int channel"
msgstr "გთხოვთ გამოიყენოთ int არხი"
#: src/core/ChatProvider.js:510
msgid "Stop flooding."
msgstr "შეაჩერე ჩატის გასპამვა."
#: src/routes/reset_password.js:39
msgid "You sent an empty password or invalid data :("
msgstr "თქვენ გაგზავნეთ ცარიელი პაროლი ან არასწორი მონაცემები :("
#: src/routes/reset_password.js:51
msgid "This password-reset link isn't valid anymore :("
msgstr "ეს პაროლის აღდგენის ბმული აღარ არის მოქმედი :("
#: src/routes/reset_password.js:62
msgid "Your passwords do not match :("
msgstr "Თქვენი პაროლები არ ემთხვევა :("
#: src/routes/reset_password.js:77
msgid "User doesn't exist in our database :("
msgstr "მომხმარებელი არ არსებობს ჩვენს მონაცემთა ბაზაში :("
#: src/routes/reset_password.js:89
msgid "Passowrd successfully changed."
msgstr "პაროლი წარმატებით შეიცვალა."
#: src/routes/reset_password.js:108
msgid "Invalid url :( Please check your mail again."
msgstr "არასწორი url :( გთხოვთ, ხელახლა შეამოწმოთ თქვენი ფოსტა."
#: src/ssr/Globe.jsx:32
msgid "PixelPlanet.Fun 3DGlobe"
msgstr "PixelPlanet.Fun 3DGlobe"
#: src/ssr/Globe.jsx:33
msgid "A 3D globe of our whole map"
msgstr "ჩვენი მთელი რუკის 3D გლობუსი"
#: src/ssr/Globe.jsx:46
msgid "Double click on globe to go back."
msgstr "ორჯერ დააწკაპუნეთ გლობუსზე უკან დასაბრუნებლად."
#: src/ssr/Globe.jsx:47
msgid "Loading..."
msgstr "Ჩატვირთვა..."
#: src/ssr/PopUp.jsx:58
msgid "ppfun"
msgstr "ppfun"
#: src/ssr/PopUp.jsx:59
msgid "PixelPlanet.Fun PopUp"
msgstr "PixelPlanet.Fun PopUp"
#: src/ssr/Main.jsx:67
msgid "PixelPlanet.Fun"
msgstr "PixelPlanet.Fun"
#: src/ssr/Main.jsx:68
msgid "Place color pixels on an map styled canvas with other players online"
msgstr ""
"განათავსეთ ფერადი პიქსელები რუკის კანვასზე ტილოზე სხვა მოთამაშეებთან ერთად "
"ონლაინ რეჟიმში"
#: src/utils/validation.js:17
msgid "Email can't be empty."
msgstr "ელფოსტა არ შეიძლება იყოს ცარიელი."
#: src/utils/validation.js:18
msgid "Email should be at least 5 characters long."
msgstr "ელფოსტა უნდა იყოს მინიმუმ 5 სიმბოლო."
#: src/utils/validation.js:19
msgid "Email can't be longer than 40 characters."
msgstr "ელფოსტა არ შეიძლება იყოს 40 სიმბოლოზე მეტი."
#: src/utils/validation.js:20
msgid "Email should at least contain a dot"
msgstr "ელფოსტა მინიმუმ წერტილს უნდა შეიცავდეს"
#: src/utils/validation.js:22
msgid "Email should contain a @"
msgstr "ელფოსტა უნდა შეიცავდეს @"
#: src/utils/validation.js:29
msgid "Name can't be empty."
msgstr "სახელი არ შეიძლება იყოს ცარიელი."
#: src/utils/validation.js:30
msgid "Name must be at least 2 characters long"
msgstr "სახელი უნდა იყოს მინიმუმ 2 სიმბოლო"
#: src/utils/validation.js:31
msgid "Name must be shorter than 26 characters"
msgstr "სახელი უნდა იყოს 26 სიმბოლოზე მოკლე"
#: src/utils/validation.js:38
msgid "Name contains invalid character like @, /, \\ or #"
msgstr "სახელი შეიცავს არასწორ სიმბოლოს, როგორიცაა @, /, \\ ან #"
#: src/utils/validation.js:53
msgid "No password given."
msgstr "პაროლი არ არის მოცემული."
#: src/utils/validation.js:56
msgid "Password must be at least 6 characters long."
msgstr "პაროლი უნდა იყოს მინიმუმ 6 სიმბოლო."
#: src/utils/validation.js:59
msgid "Password must be shorter than 60 characters."
msgstr "პაროლი უნდა იყოს 60 სიმბოლოზე ნაკლები."
#: src/ssr/PasswordReset.jsx:20 src/ssr/PasswordReset.jsx:40
msgid "PixelPlanet.fun Password Reset"
msgstr "PixelPlanet.fun პაროლის აღდგენა"
#: src/ssr/PasswordReset.jsx:21 src/ssr/PasswordReset.jsx:41
msgid "Reset your password here"
msgstr "გადააყენეთ თქვენი პაროლი აქ"
#: src/core/MailProvider.js:105 src/ssr/PasswordReset.jsx:28
#: src/ssr/PasswordReset.jsx:49
msgid "Reset Password"
msgstr "პაროლის აღდგენა"
#: src/ssr/PasswordReset.jsx:30 src/ssr/RedirectionPage.jsx:12
msgid "Click here"
msgstr "Დააკლიკე აქ"
#: src/ssr/PasswordReset.jsx:30
msgid "to go back to pixelplanet"
msgstr "პიქსელ პლანეტაზე დასაბრუნებლად"
#: src/ssr/PasswordReset.jsx:50
#, javascript-format
msgid "Hello ${ name }, you can set your new password here:"
msgstr "გამარჯობა ${ name }, შეგიძლიათ დააყენოთ თქვენი ახალი პაროლი აქ:"
#: src/ssr/PasswordReset.jsx:54
msgid "New Password"
msgstr "ახალი პაროლი"
#: src/ssr/PasswordReset.jsx:60
msgid "Confirm New Password"
msgstr "Დაადასტურეთ ახალი პაროლი"
#: src/ssr/PasswordReset.jsx:65
msgid "Submit"
msgstr "გაგზავნა"
#: src/routes/api/modtools.js:53
msgid "You are not logged in"
msgstr "თქვენ არ ხართ შესული"
#: src/routes/api/modtools.js:65
msgid "You are not allowed to access this page"
msgstr "თქვენ არ გაქვთ უფლება შეხვიდეთ ამ გვერდზე"
#: src/routes/api/modtools.js:207
msgid "Just admins can do that"
msgstr "ამის გაკეთება მხოლოდ ადმინებს შეუძლიათ"
#: src/routes/api/baninfo.js:32
msgid "You are not banned"
msgstr "არ ხარ აკრძალული"
#: src/routes/api/auth/change_mail.js:21 src/routes/api/auth/register.js:24
msgid "This email provider is not allowed"
msgstr "ელფოსტის ეს პროვაიდერი დაუშვებელია"
#: src/routes/api/auth/register.js:31
msgid "No Captcha given"
msgstr "კაპჩა არ არის მოცემული"
#: src/routes/api/auth/register.js:34
msgid "E-Mail already in use."
msgstr "Ელექტრონული ფოსტა უკვე გამოყენებულია."
#: src/routes/api/auth/register.js:36
msgid "Username already in use."
msgstr "Მომხმარებლის სახელი უკვე გამოიყენება."
#: src/routes/api/auth/register.js:59
msgid "You took too long, try again."
msgstr "ძალიან დიდი დრო დაგჭირდათ, ისევ სცადეთ."
#: src/routes/api/auth/register.js:62
msgid "You failed your captcha"
msgstr "თქვენ ვერ მოახერხეთ თქვენი კაპტჩა"
#: src/routes/api/auth/register.js:65
msgid "Unknown Captcha Error"
msgstr "უცნობი კაპჩტას შეცდომა"
#: src/routes/api/auth/register.js:89
msgid "Failed to create new user :("
msgstr "ახალი მომხმარებლის შექმნა ვერ მოხერხდა :("
#: src/routes/api/auth/register.js:105
msgid "Failed to establish session after register :("
msgstr "რეგისტრაციის შემდეგ სესიის დამყარება ვერ მოხერხდა :("
#: src/routes/api/auth/logout.js:11
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/change_mail.js:43
#: src/routes/api/auth/change_passwd.js:34
#: src/routes/api/auth/delete_account.js:35
msgid "You are not authenticated."
msgstr "თქვენ არ ხართ დამოწმებული."
#: src/routes/api/auth/change_mail.js:52
#: src/routes/api/auth/change_passwd.js:43
#: src/routes/api/auth/delete_account.js:45
msgid "Incorrect password!"
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/ssr/RedirectionPage.jsx:19
msgid "PixelPlanet.fun Accounts"
msgstr "PixelPlanet.fun ანგარიშები"
#: src/ssr/RedirectionPage.jsx:29
msgid "You will be automatically redirected after 15s"
msgstr "თქვენ ავტომატურად გადამისამართდებით 15 წამის შემდეგ"
#: src/ssr/RedirectionPage.jsx:30
#, javascript-format
msgid "Or ${ clickHere } to go back to pixelplanet"
msgstr "ან ${ clickHere } pixelplanet-ზე დასაბრუნებლად"
#: src/canvasesDesc.js:19
msgid "Earth"
msgstr "დედამიწა"
#: src/canvasesDesc.js:20
msgid "Moon"
msgstr "მთვარე"
#: src/canvasesDesc.js:21
msgid "3D Canvas"
msgstr "3დ კანვასი"
#: src/canvasesDesc.js:22
msgid "Coronavirus"
msgstr "კორონავირუსი"
#: src/canvasesDesc.js:23
msgid "PixelZone"
msgstr "პიქსელზონა"
#: src/canvasesDesc.js:24
msgid "PixelCanvas"
msgstr "PixelCanvas"
#: src/canvasesDesc.js:25
msgid "1bit"
msgstr "1 ბიტი"
#: src/canvasesDesc.js:26
msgid "Top10"
msgstr "10 საუკეთესო"
#: src/canvasesDesc.js:29
msgid "Our main canvas, a huge map of the world. Place everywhere you like"
msgstr ""
"ჩვენი მთავარი კანვასი, მსოფლიოს უზარმაზარი რუკა. განათავსეთ ყველგან, სადაც "
"გსურთ"
#: src/canvasesDesc.js:30
msgid ""
"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."
msgstr ""
"მთვარის კანვასი. უსაფრთხო სივრცე ხელოვნებისთვის. არ არის დროშები ან დიდი "
"ტექსტი (თუ არ არის ხელოვნების ნაწილი) ან 1,5 კ x 1,5 კ პიქსელზე მეტი "
"ნამუშევარი."
#: src/canvasesDesc.js:31
msgid "Place Voxels on a 3D canvas with others"
msgstr "მოათავსეთ ვოქსელები 3D ტილოზე სხვებთან ერთად"
#: src/canvasesDesc.js:32
msgid "Special canvas to spread awareness of SARS-CoV2"
msgstr "სპეციალური ტილო SARS-CoV2-ის შესახებ ცნობადობის გასავრცელებლად"
#: src/canvasesDesc.js:33
msgid "Mirror of PixelZone"
msgstr "PixelZone-ის სარკე"
#: src/canvasesDesc.js:34
msgid "Mirror of PixelCanvas"
msgstr "PixelCanvas-ის სარკე"
#: src/canvasesDesc.js:35
msgid "Black and White canvas"
msgstr "შავ-თეთრი ტილო"
#: src/canvasesDesc.js:36
msgid ""
"A canvas for the most active players from the the previous day. Daily "
"ranking updates at 00:00 UTC."
msgstr ""
"ტილო წინა დღის ყველაზე აქტიური მოთამაშეებისთვის. ყოველდღიური რეიტინგის "
"განახლებები 00:00 UTC."
#: src/core/MailProvider.js:66
#, javascript-format
msgid "Welcome ${ name } to PixelPlanet, plese verify your mail"
msgstr ""
"მოგესალმებით ${ name } PixelPlanet-ში, გთხოვთ, გადაამოწმოთ თქვენი ფოსტა"
#: src/core/MailProvider.js:67
msgid "Hello ${ name }"
msgstr "გამარჯობა ${ name }"
#: src/core/MailProvider.js:68
msgid ""
"welcome to our little community of pixelplacers, to use your account, you "
"have to verify your mail. You can do that here: "
msgstr ""
"კეთილი იყოს თქვენი მობრძანება pixelplacers-ის ჩვენს პატარა საზოგადოებაში, "
"თქვენი ანგარიშის გამოსაყენებლად, თქვენ უნდა გადაამოწმოთ თქვენი ფოსტა. ამის "
"გაკეთება შეგიძლიათ აქ:"
#: src/core/MailProvider.js:68
msgid "Click to Verify"
msgstr "დააწკაპუნეთ დასადასტურებლად"
#: src/core/MailProvider.js:68 src/core/MailProvider.js:105
msgid "Or by copying following url:"
msgstr "ან შემდეგი url-ის კოპირებით:"
#: src/core/MailProvider.js:69
msgid ""
"Have fun and don't hesitate to contact us if you encouter any problems :)"
msgstr ""
"გაერთეთ და ნუ მოგერიდებათ დაგვიკავშირდეთ თუ რაიმე პრობლემა შეგექმნათ :)"
#: src/core/MailProvider.js:70 src/core/MailProvider.js:107
msgid "Thanks"
msgstr "მადლობა"
#: src/core/MailProvider.js:87
#, javascript-format
msgid ""
"We already sent you a verification mail, you can request another one in "
"${ minLeft } minutes."
msgstr ""
"ჩვენ უკვე გამოგიგზავნეთ დამადასტურებელი წერილი, შეგიძლიათ მოითხოვოთ სხვა "
"${ minLeft } წუთში."
#: src/core/MailProvider.js:103
msgid "You forgot your password for PixelPlanet? Get a new one here"
msgstr "დაგავიწყდათ თქვენი პაროლი PixelPlanet-ისთვის? მიიღეთ ახალი აქ"
#: src/core/MailProvider.js:104
msgid "Hello"
msgstr "გამარჯობა"
#: src/core/MailProvider.js:105
msgid ""
"You requested to get a new password. You can change your password within the "
"next 30min here: "
msgstr ""
"თქვენ მოითხოვეთ ახალი პაროლის მიღება. თქვენ შეგიძლიათ შეცვალოთ თქვენი პაროლი "
"შემდეგი 30 წუთის განმავლობაში აქ:"
#: src/core/MailProvider.js:106
#, javascript-format
msgid ""
"If you did not request this mail, please just ignore it (the ip that "
"requested this mail was ${ ip })."
msgstr ""
"თუ თქვენ არ მოითხოვეთ ეს წერილი, გთხოვთ, უბრალოდ უგულებელყოთ იგი (IP, "
"რომელმაც მოითხოვა ეს წერილი, იყო ${ ip })."
#: src/core/MailProvider.js:114
msgid "Mail is not configured on the server"
msgstr "ფოსტა არ არის კონფიგურირებული სერვერზე"
#: src/core/MailProvider.js:122
msgid ""
"We already sent you a mail with instructions. Please wait before requesting "
"another mail."
msgstr ""
"ჩვენ უკვე გამოგიგზავნეთ მეილი ინსტრუქციებით. გთხოვთ, დაელოდოთ სხვა ფოსტის "
"მოთხოვნამდე."
#: src/core/MailProvider.js:130
msgid "Couldn't find this mail in our database"
msgstr "ვერ ვიპოვე ეს წერილი ჩვენს მონაცემთა ბაზაში"

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: {