Merge remote-tracking branch 'origin/main' into feature/akkoma-local-only

# Conflicts:
#	src/locales/en.po
This commit is contained in:
Stefano Pigozzi 2024-09-05 15:46:56 +02:00
commit 7f06187d75
No known key found for this signature in database
GPG key ID: 5ADA3868646C3FC0
74 changed files with 28158 additions and 14722 deletions

4
.github/release.yml vendored Normal file
View file

@ -0,0 +1,4 @@
changelog:
exclude:
labels:
- 'i18n'

View file

@ -3,6 +3,8 @@ name: i18n PR auto-merge
on:
pull_request:
types: [opened, synchronize, reopened, labeled]
branches:
- main
jobs:
run-and-merge:
@ -10,14 +12,21 @@ jobs:
github.event.pull_request.base.ref == 'main' &&
github.event.pull_request.head.ref == 'l10n_main'
runs-on: ubuntu-latest
# concurrency:
# group: ${{ github.workflow }}-${{ github.ref }}
# cancel-in-progress: true
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- run: sleep 15
- name: Check if the branch is dirty
run: |
git fetch origin ${{ github.event.pull_request.head.ref }}
if [ $(git rev-parse HEAD) != $(git rev-parse origin/${{ github.event.pull_request.head.ref }}) ]; then
echo "Branch is dirty. Exiting..."
exit 0
fi
- name: Check auto-merge conditions
run: |
BASE_SHA="${{ github.event.pull_request.base.sha }}"
@ -47,8 +56,16 @@ jobs:
exit 0
else
echo "More than 50 lines have been changed. Merging pull request."
# List of locales changed
LOCALES_CHANGED=$(git diff --name-only $BASE_SHA $HEAD_SHA | grep '\.po$' | awk -F '/' '{print $NF}' | sed 's/\.po$//' | tr '\n' ',' | sed 's/,$//')
# Better subject
# "i18n updates ([LOCALES_CHANGED])"
SUBJECT="i18n updates ($LOCALES_CHANGED)"
PR_NUMBER=$(echo ${{ github.event.pull_request.number }})
gh pr merge $PR_NUMBER --auto --squash || true
gh pr merge $PR_NUMBER --author "github-actions[bot]@users.noreply.github.com" --squash --subject "$SUBJECT" || true
fi
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View file

@ -0,0 +1,34 @@
name: Update README with list of i18n volunteers
on:
schedule:
# Every week
- cron: '0 0 * * 0'
workflow_dispatch:
jobs:
update-readme:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm ci
- run: |
npm run fetch-i18n-volunteers
npm run readme:i18n-volunteers
# Commit & push if there are changes
if git diff --quiet README.md; then
echo "No changes to README.md"
else
echo "Changes to README.md"
git config --global user.email "github-actions[bot]@users.noreply.github.com"
git config --global user.name "github-actions[bot]"
git add README.md
git commit -m "Update README.md"
git push
fi
env:
CROWDIN_ACCESS_TOKEN: ${{ secrets.CROWDIN_ACCESS_TOKEN }}

View file

@ -1,17 +1,18 @@
name: Update catalogs
name: Update Catalogs
on:
pull_request_target:
types:
- closed
push:
branches:
- l10n_main
workflow_dispatch:
jobs:
update-catalogs:
if: github.event.pull_request.merged == true
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
ref: l10n_main
- uses: actions/setup-node@v4
with:
node-version: 20
@ -27,5 +28,5 @@ jobs:
git config --global user.name "github-actions[bot]"
git add src/data/catalogs.json
git commit -m "Update catalogs.json"
git push origin HEAD:main
git push origin HEAD:l10n_main || true
fi

View file

@ -292,6 +292,59 @@ Costs involved in running and developing this web app:
[![Contributors](https://contrib.rocks/image?repo=cheeaun/phanpy)](https://github.com/cheeaun/phanpy/graphs/contributors)
### Translation volunteers
<!-- i18n volunteers start -->
- <img src="https://crowdin-static.downloads.crowdin.com/avatar/12571163/medium/9f3ea938f4243f5ffe2a43f814ddc9e8_default.png" alt="" width="16" height="16" /> alidsds11 (Arabic)
- <img src="https://crowdin-static.downloads.crowdin.com/avatar/13170041/medium/603136896af17fc005fd592ce3f48717_default.png" alt="" width="16" height="16" /> BoFFire (Arabic, French, Kabyle)
- <img src="https://crowdin-static.downloads.crowdin.com/avatar/12898464/medium/d3758a76b894bade4bf271c9b32ea69b.png" alt="" width="16" height="16" /> Brawaru (Russian)
- <img src="https://crowdin-static.downloads.crowdin.com/avatar/15460040/medium/1cfcfe5f5511b783b5d9f2b968bad819.png" alt="" width="16" height="16" /> cbasje (Dutch)
- <img src="https://crowdin-static.downloads.crowdin.com/avatar/15525631/medium/51293156034d0236f1a1020c10f7d539_default.png" alt="" width="16" height="16" /> cbo92 (French)
- <img src="https://crowdin-static.downloads.crowdin.com/avatar/15910131/medium/67fab7eeab5551853450e76e2ef19e59.jpeg" alt="" width="16" height="16" /> CDN (Chinese Simplified)
- <img src="https://crowdin-static.downloads.crowdin.com/avatar/16556801/medium/ed5e501ca1f3cc6525d2da28db646346.jpeg" alt="" width="16" height="16" /> dannypsnl (Chinese Traditional)
- <img src="https://crowdin-static.downloads.crowdin.com/avatar/3711/medium/d95ddd44e8dcb3a039f8a3463aed781d_default.png" alt="" width="16" height="16" /> databio (Catalan)
- <img src="https://crowdin-static.downloads.crowdin.com/avatar/12618120/medium/ccb11bd042bbf4c7189033f7af2dbd32_default.png" alt="" width="16" height="16" /> drydenwu (Chinese Traditional)
- <img src="https://crowdin-static.downloads.crowdin.com/avatar/13557465/medium/8feebf3677fa80c01e8c54c4fbe097e0_default.png" alt="" width="16" height="16" /> elissarc (French)
- <img src="https://crowdin-static.downloads.crowdin.com/avatar/16528627/medium/9036f6eced0257f4e1ea4c5bd499de2d_default.png" alt="" width="16" height="16" /> ElPamplina (Spanish)
- <img src="https://crowdin-static.downloads.crowdin.com/avatar/14277386/medium/29b30d2c73a214000e3941c9978f49e4_default.png" alt="" width="16" height="16" /> Fitik (Esperanto, Hebrew)
- <img src="https://crowdin-static.downloads.crowdin.com/avatar/14444512/medium/99d0e7a3076deccbdfe0aa0b0612308c.jpeg" alt="" width="16" height="16" /> Freeesia (Japanese)
- <img src="https://crowdin-static.downloads.crowdin.com/avatar/12617257/medium/a201650da44fed28890b0e0d8477a663.jpg" alt="" width="16" height="16" /> ghose (Galician)
- <img src="https://crowdin-static.downloads.crowdin.com/avatar/15248754/medium/0dac6334ea0f4e8d4194a605c0a5594a.jpeg" alt="" width="16" height="16" /> hongminhee (Korean)
- <img src="https://crowdin-static.downloads.crowdin.com/avatar/13454728/medium/1f78b7124b3c962bc4ae55e8d701fc91_default.png" alt="" width="16" height="16" /> isard (Catalan)
- <img src="https://crowdin-static.downloads.crowdin.com/avatar/16532403/medium/4cefb19623bcc44d7cdb9e25aebf5250.jpeg" alt="" width="16" height="16" /> karlafej (Czech)
- <img src="https://crowdin-static.downloads.crowdin.com/avatar/15791971/medium/88bdda3090339f16f6083390d32bb434_default.png" alt="" width="16" height="16" /> katullo11 (Italian)
- <img src="https://crowdin-static.downloads.crowdin.com/avatar/14677260/medium/e53420d200961f48602324e18c091bdc.png" alt="" width="16" height="16" /> Kytta (German)
- <img src="https://crowdin-static.downloads.crowdin.com/avatar/16529521/medium/ae6add93a901b0fefa2d9b1077920d73.png" alt="" width="16" height="16" /> llun (Thai)
- <img src="https://crowdin-static.downloads.crowdin.com/avatar/16291756/medium/e1c4210f15537394cc764b8bc2dffe37.jpg" alt="" width="16" height="16" /> lucasofchirst (Occitan, Portuguese, Portuguese, Brazilian)
- <img src="https://crowdin-static.downloads.crowdin.com/avatar/16537713/medium/825f0bf1a14fc545a76891a52839d86e_default.png" alt="" width="16" height="16" /> marcin.kozinski (Polish)
- <img src="https://crowdin-static.downloads.crowdin.com/avatar/12882812/medium/77744d8db46e9a3e09030e1a02b7a572.jpeg" alt="" width="16" height="16" /> mojosoeun (Korean)
- <img src="https://crowdin-static.downloads.crowdin.com/avatar/13613969/medium/c7834ddc0ada84a79671697a944bb274.png" alt="" width="16" height="16" /> moreal (Korean)
- <img src="https://crowdin-static.downloads.crowdin.com/avatar/14158861/medium/ba1ff31dc5743b067ea6685f735229a5_default.png" alt="" width="16" height="16" /> MrWillCom (Chinese Simplified)
- <img src="https://crowdin-static.downloads.crowdin.com/avatar/15652333/medium/7f36f289f9e2fe41d89ad534a1047f0e.png" alt="" width="16" height="16" /> nclm (French)
- <img src="https://crowdin-static.downloads.crowdin.com/avatar/16539461/medium/2f41b9f0b802c1d200a6ab62167a7229_default.png" alt="" width="16" height="16" /> pazpi (Italian)
- <img src="https://crowdin-static.downloads.crowdin.com/avatar/15106977/medium/54bf93b19af8bbfdee579ea51685bafa.jpeg" alt="" width="16" height="16" /> punkrockgirl (Basque)
- <img src="https://crowdin-static.downloads.crowdin.com/avatar/16536247/medium/f010c8e718a36229733a8b58f6bad2a4_default.png" alt="" width="16" height="16" /> radecos (French)
- <img src="https://crowdin-static.downloads.crowdin.com/avatar/16538917/medium/092ec03f56f9dd1cbce94379fa4d4d38.png" alt="" width="16" height="16" /> Razem (Czech)
- <img src="https://crowdin-static.downloads.crowdin.com/avatar/14345134/medium/89a299239890c79a1d791d08ec3951dc.png" alt="" width="16" height="16" /> realpixelcode (German)
- <img src="https://crowdin-static.downloads.crowdin.com/avatar/16527325/medium/37ebb27e7a50f7f85ae93beafc7028a2.jpg" alt="" width="16" height="16" /> rezahosseinzadeh (Persian)
- <img src="https://crowdin-static.downloads.crowdin.com/avatar/13422319/medium/66632a98d73d48e36753d94ebcec9d4f.png" alt="" width="16" height="16" /> rwmpelstilzchen (Esperanto, Hebrew)
- <img src="https://crowdin-static.downloads.crowdin.com/avatar/16538605/medium/bcdb6e3286b7d6237923f3a9383eed29.png" alt="" width="16" height="16" /> SadmL (Russian)
- <img src="https://crowdin-static.downloads.crowdin.com/avatar/14565190/medium/79100599131b7776e9803e4b696915a3_default.png" alt="" width="16" height="16" /> Sky_NiniKo (French)
- <img src="https://crowdin-static.downloads.crowdin.com/avatar/16532441/medium/1a47e8d80c95636e02d2260f6e233ca5.png" alt="" width="16" height="16" /> Su5hicz (Czech)
- <img src="https://crowdin-static.downloads.crowdin.com/avatar/16533843/medium/7314c15492ef90118c33a80a427e6c87_default.png" alt="" width="16" height="16" /> Talos00 (Italian)
- <img src="https://crowdin-static.downloads.crowdin.com/avatar/16530049/medium/683f3581620c6b4a5c753b416ed695a7.jpeg" alt="" width="16" height="16" /> tferrermo (Spanish)
- <img src="https://crowdin-static.downloads.crowdin.com/avatar/16527851/medium/649e5a9a8a8cc61ced670d89e9cca082.png" alt="" width="16" height="16" /> tux93 (German)
- <img src="https://crowdin-static.downloads.crowdin.com/avatar/16529833/medium/2991a65722acd721849656223014cd49.png" alt="" width="16" height="16" /> Urbestro (Esperanto, Spanish)
- <img src="https://crowdin-static.downloads.crowdin.com/avatar/16539171/medium/db6fb87481026c72b895adfb94e17d2c_default.png" alt="" width="16" height="16" /> UsualUsername (Russian)
- <img src="https://crowdin-static.downloads.crowdin.com/avatar/14427566/medium/ab733b5044c21867fc5a9d1b22cd2c03.png" alt="" width="16" height="16" /> Vac31. (Lithuanian)
- <img src="https://crowdin-static.downloads.crowdin.com/avatar/16026914/medium/e3ca187f354a298ef0c9d02a0ed17be7.jpg" alt="" width="16" height="16" /> valtlai (Finnish)
- <img src="https://crowdin-static.downloads.crowdin.com/avatar/15982109/medium/9c03062bdc1d3c6d384dbfead97c26ba.jpeg" alt="" width="16" height="16" /> xabi_itzultzaile (Basque)
- <img src="https://crowdin-static.downloads.crowdin.com/avatar/16556017/medium/216e0f7a0c35b079920366939a3aaca7_default.png" alt="" width="16" height="16" /> xen4n (Ukrainian)
- <img src="https://crowdin-static.downloads.crowdin.com/avatar/16532657/medium/f309f319266e1ff95f3070eab0c9a9d9_default.png" alt="" width="16" height="16" /> xqueralt (Catalan)
- <img src="https://crowdin-static.downloads.crowdin.com/avatar/14041603/medium/6ab77a0467b06aeb49927c6d9c409f89.jpg" alt="" width="16" height="16" /> ZiriSut (Kabyle)
- <img src="https://crowdin-static.downloads.crowdin.com/avatar/16530601/medium/e1b6d5c24953b6405405c1ab33c0fa46.jpeg" alt="" width="16" height="16" /> zkreml (Czech)
<!-- i18n volunteers end -->
## Backstory
I am one of the earliest users of Twitter. Twitter was launched on [15 July 2006](https://en.wikipedia.org/wiki/Twitter). I joined on December 2006 and my [first tweet](https://twitter.com/cheeaun/status/1298723) was posted on 18 December 2006.

345
i18n-volunteers.json Normal file
View file

@ -0,0 +1,345 @@
[
{
"avatarUrl": "https://crowdin-static.downloads.crowdin.com/avatar/12571163/medium/9f3ea938f4243f5ffe2a43f814ddc9e8_default.png",
"username": "alidsds11",
"languages": [
"Arabic"
]
},
{
"avatarUrl": "https://crowdin-static.downloads.crowdin.com/avatar/13170041/medium/603136896af17fc005fd592ce3f48717_default.png",
"username": "BoFFire",
"languages": [
"Arabic",
"French",
"Kabyle"
]
},
{
"avatarUrl": "https://crowdin-static.downloads.crowdin.com/avatar/12898464/medium/d3758a76b894bade4bf271c9b32ea69b.png",
"username": "Brawaru",
"languages": [
"Russian"
]
},
{
"avatarUrl": "https://crowdin-static.downloads.crowdin.com/avatar/15460040/medium/1cfcfe5f5511b783b5d9f2b968bad819.png",
"username": "cbasje",
"languages": [
"Dutch"
]
},
{
"avatarUrl": "https://crowdin-static.downloads.crowdin.com/avatar/15525631/medium/51293156034d0236f1a1020c10f7d539_default.png",
"username": "cbo92",
"languages": [
"French"
]
},
{
"avatarUrl": "https://crowdin-static.downloads.crowdin.com/avatar/15910131/medium/67fab7eeab5551853450e76e2ef19e59.jpeg",
"username": "CDN",
"languages": [
"Chinese Simplified"
]
},
{
"avatarUrl": "https://crowdin-static.downloads.crowdin.com/avatar/16556801/medium/ed5e501ca1f3cc6525d2da28db646346.jpeg",
"username": "dannypsnl",
"languages": [
"Chinese Traditional"
]
},
{
"avatarUrl": "https://crowdin-static.downloads.crowdin.com/avatar/3711/medium/d95ddd44e8dcb3a039f8a3463aed781d_default.png",
"username": "databio",
"languages": [
"Catalan"
]
},
{
"avatarUrl": "https://crowdin-static.downloads.crowdin.com/avatar/12618120/medium/ccb11bd042bbf4c7189033f7af2dbd32_default.png",
"username": "drydenwu",
"languages": [
"Chinese Traditional"
]
},
{
"avatarUrl": "https://crowdin-static.downloads.crowdin.com/avatar/13557465/medium/8feebf3677fa80c01e8c54c4fbe097e0_default.png",
"username": "elissarc",
"languages": [
"French"
]
},
{
"avatarUrl": "https://crowdin-static.downloads.crowdin.com/avatar/16528627/medium/9036f6eced0257f4e1ea4c5bd499de2d_default.png",
"username": "ElPamplina",
"languages": [
"Spanish"
]
},
{
"avatarUrl": "https://crowdin-static.downloads.crowdin.com/avatar/14277386/medium/29b30d2c73a214000e3941c9978f49e4_default.png",
"username": "Fitik",
"languages": [
"Esperanto",
"Hebrew"
]
},
{
"avatarUrl": "https://crowdin-static.downloads.crowdin.com/avatar/14444512/medium/99d0e7a3076deccbdfe0aa0b0612308c.jpeg",
"username": "Freeesia",
"languages": [
"Japanese"
]
},
{
"avatarUrl": "https://crowdin-static.downloads.crowdin.com/avatar/12617257/medium/a201650da44fed28890b0e0d8477a663.jpg",
"username": "ghose",
"languages": [
"Galician"
]
},
{
"avatarUrl": "https://crowdin-static.downloads.crowdin.com/avatar/15248754/medium/0dac6334ea0f4e8d4194a605c0a5594a.jpeg",
"username": "hongminhee",
"languages": [
"Korean"
]
},
{
"avatarUrl": "https://crowdin-static.downloads.crowdin.com/avatar/13454728/medium/1f78b7124b3c962bc4ae55e8d701fc91_default.png",
"username": "isard",
"languages": [
"Catalan"
]
},
{
"avatarUrl": "https://crowdin-static.downloads.crowdin.com/avatar/16532403/medium/4cefb19623bcc44d7cdb9e25aebf5250.jpeg",
"username": "karlafej",
"languages": [
"Czech"
]
},
{
"avatarUrl": "https://crowdin-static.downloads.crowdin.com/avatar/15791971/medium/88bdda3090339f16f6083390d32bb434_default.png",
"username": "katullo11",
"languages": [
"Italian"
]
},
{
"avatarUrl": "https://crowdin-static.downloads.crowdin.com/avatar/14677260/medium/e53420d200961f48602324e18c091bdc.png",
"username": "Kytta",
"languages": [
"German"
]
},
{
"avatarUrl": "https://crowdin-static.downloads.crowdin.com/avatar/16529521/medium/ae6add93a901b0fefa2d9b1077920d73.png",
"username": "llun",
"languages": [
"Thai"
]
},
{
"avatarUrl": "https://crowdin-static.downloads.crowdin.com/avatar/16291756/medium/e1c4210f15537394cc764b8bc2dffe37.jpg",
"username": "lucasofchirst",
"languages": [
"Occitan",
"Portuguese",
"Portuguese, Brazilian"
]
},
{
"avatarUrl": "https://crowdin-static.downloads.crowdin.com/avatar/16537713/medium/825f0bf1a14fc545a76891a52839d86e_default.png",
"username": "marcin.kozinski",
"languages": [
"Polish"
]
},
{
"avatarUrl": "https://crowdin-static.downloads.crowdin.com/avatar/12882812/medium/77744d8db46e9a3e09030e1a02b7a572.jpeg",
"username": "mojosoeun",
"languages": [
"Korean"
]
},
{
"avatarUrl": "https://crowdin-static.downloads.crowdin.com/avatar/13613969/medium/c7834ddc0ada84a79671697a944bb274.png",
"username": "moreal",
"languages": [
"Korean"
]
},
{
"avatarUrl": "https://crowdin-static.downloads.crowdin.com/avatar/14158861/medium/ba1ff31dc5743b067ea6685f735229a5_default.png",
"username": "MrWillCom",
"languages": [
"Chinese Simplified"
]
},
{
"avatarUrl": "https://crowdin-static.downloads.crowdin.com/avatar/15652333/medium/7f36f289f9e2fe41d89ad534a1047f0e.png",
"username": "nclm",
"languages": [
"French"
]
},
{
"avatarUrl": "https://crowdin-static.downloads.crowdin.com/avatar/16539461/medium/2f41b9f0b802c1d200a6ab62167a7229_default.png",
"username": "pazpi",
"languages": [
"Italian"
]
},
{
"avatarUrl": "https://crowdin-static.downloads.crowdin.com/avatar/15106977/medium/54bf93b19af8bbfdee579ea51685bafa.jpeg",
"username": "punkrockgirl",
"languages": [
"Basque"
]
},
{
"avatarUrl": "https://crowdin-static.downloads.crowdin.com/avatar/16536247/medium/f010c8e718a36229733a8b58f6bad2a4_default.png",
"username": "radecos",
"languages": [
"French"
]
},
{
"avatarUrl": "https://crowdin-static.downloads.crowdin.com/avatar/16538917/medium/092ec03f56f9dd1cbce94379fa4d4d38.png",
"username": "Razem",
"languages": [
"Czech"
]
},
{
"avatarUrl": "https://crowdin-static.downloads.crowdin.com/avatar/14345134/medium/89a299239890c79a1d791d08ec3951dc.png",
"username": "realpixelcode",
"languages": [
"German"
]
},
{
"avatarUrl": "https://crowdin-static.downloads.crowdin.com/avatar/16527325/medium/37ebb27e7a50f7f85ae93beafc7028a2.jpg",
"username": "rezahosseinzadeh",
"languages": [
"Persian"
]
},
{
"avatarUrl": "https://crowdin-static.downloads.crowdin.com/avatar/13422319/medium/66632a98d73d48e36753d94ebcec9d4f.png",
"username": "rwmpelstilzchen",
"languages": [
"Esperanto",
"Hebrew"
]
},
{
"avatarUrl": "https://crowdin-static.downloads.crowdin.com/avatar/16538605/medium/bcdb6e3286b7d6237923f3a9383eed29.png",
"username": "SadmL",
"languages": [
"Russian"
]
},
{
"avatarUrl": "https://crowdin-static.downloads.crowdin.com/avatar/14565190/medium/79100599131b7776e9803e4b696915a3_default.png",
"username": "Sky_NiniKo",
"languages": [
"French"
]
},
{
"avatarUrl": "https://crowdin-static.downloads.crowdin.com/avatar/16532441/medium/1a47e8d80c95636e02d2260f6e233ca5.png",
"username": "Su5hicz",
"languages": [
"Czech"
]
},
{
"avatarUrl": "https://crowdin-static.downloads.crowdin.com/avatar/16533843/medium/7314c15492ef90118c33a80a427e6c87_default.png",
"username": "Talos00",
"languages": [
"Italian"
]
},
{
"avatarUrl": "https://crowdin-static.downloads.crowdin.com/avatar/16530049/medium/683f3581620c6b4a5c753b416ed695a7.jpeg",
"username": "tferrermo",
"languages": [
"Spanish"
]
},
{
"avatarUrl": "https://crowdin-static.downloads.crowdin.com/avatar/16527851/medium/649e5a9a8a8cc61ced670d89e9cca082.png",
"username": "tux93",
"languages": [
"German"
]
},
{
"avatarUrl": "https://crowdin-static.downloads.crowdin.com/avatar/16529833/medium/2991a65722acd721849656223014cd49.png",
"username": "Urbestro",
"languages": [
"Esperanto",
"Spanish"
]
},
{
"avatarUrl": "https://crowdin-static.downloads.crowdin.com/avatar/16539171/medium/db6fb87481026c72b895adfb94e17d2c_default.png",
"username": "UsualUsername",
"languages": [
"Russian"
]
},
{
"avatarUrl": "https://crowdin-static.downloads.crowdin.com/avatar/14427566/medium/ab733b5044c21867fc5a9d1b22cd2c03.png",
"username": "Vac31.",
"languages": [
"Lithuanian"
]
},
{
"avatarUrl": "https://crowdin-static.downloads.crowdin.com/avatar/16026914/medium/e3ca187f354a298ef0c9d02a0ed17be7.jpg",
"username": "valtlai",
"languages": [
"Finnish"
]
},
{
"avatarUrl": "https://crowdin-static.downloads.crowdin.com/avatar/15982109/medium/9c03062bdc1d3c6d384dbfead97c26ba.jpeg",
"username": "xabi_itzultzaile",
"languages": [
"Basque"
]
},
{
"avatarUrl": "https://crowdin-static.downloads.crowdin.com/avatar/16556017/medium/216e0f7a0c35b079920366939a3aaca7_default.png",
"username": "xen4n",
"languages": [
"Ukrainian"
]
},
{
"avatarUrl": "https://crowdin-static.downloads.crowdin.com/avatar/16532657/medium/f309f319266e1ff95f3070eab0c9a9d9_default.png",
"username": "xqueralt",
"languages": [
"Catalan"
]
},
{
"avatarUrl": "https://crowdin-static.downloads.crowdin.com/avatar/14041603/medium/6ab77a0467b06aeb49927c6d9c409f89.jpg",
"username": "ZiriSut",
"languages": [
"Kabyle"
]
},
{
"avatarUrl": "https://crowdin-static.downloads.crowdin.com/avatar/16530601/medium/e1b6d5c24953b6405405c1ab33c0fa46.jpeg",
"username": "zkreml",
"languages": [
"Czech"
]
}
]

View file

@ -1,7 +1,8 @@
import { LOCALES } from './src/locales';
import { ALL_LOCALES } from './src/locales';
const config = {
locales: LOCALES,
locales: ALL_LOCALES,
sourceLocale: 'en',
pseudoLocale: 'pseudo-LOCALE',
fallbackLocales: {
default: 'en',

545
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -11,7 +11,9 @@
"bundle-visualizer": "npx vite-bundle-visualizer",
"messages:extract": "lingui extract",
"messages:extract:clean": "lingui extract --locale en --clean",
"messages:compile": "lingui compile"
"messages:compile": "lingui compile",
"fetch-i18n-volunteers": "env $(cat .env.local | grep -v \"#\" | xargs) node scripts/fetch-i18n-volunteers.js",
"readme:i18n-volunteers": "node scripts/update-i18n-volunteers-readme.js"
},
"dependencies": {
"@formatjs/intl-localematcher": "~0.5.4",
@ -20,9 +22,9 @@
"@github/text-expander-element": "~2.7.1",
"@iconify-icons/mingcute": "~1.2.9",
"@justinribeiro/lite-youtube": "~1.5.0",
"@lingui/detect-locale": "~4.11.3",
"@lingui/macro": "~4.11.3",
"@lingui/react": "~4.11.3",
"@lingui/detect-locale": "~4.11.4",
"@lingui/macro": "~4.11.4",
"@lingui/react": "~4.11.4",
"@szhsin/react-menu": "~4.2.2",
"compare-versions": "~6.1.1",
"fast-blurhash": "~1.1.4",
@ -31,6 +33,7 @@
"html-prettify": "~1.0.7",
"idb-keyval": "~6.2.1",
"intl-locale-textinfo-polyfill": "~2.1.1",
"js-cookie": "~3.0.5",
"just-debounce-it": "~3.2.0",
"lz-string": "~1.5.0",
"masto": "~6.8.0",
@ -39,7 +42,7 @@
"p-throttle": "~6.2.0",
"preact": "~10.23.2",
"punycode": "~2.3.1",
"react-hotkeys-hook": "~4.5.0",
"react-hotkeys-hook": "~4.5.1",
"react-intersection-observer": "~9.13.0",
"react-quick-pinch-zoom": "~5.1.0",
"react-router-dom": "6.6.2",
@ -48,25 +51,25 @@
"tinyld": "~1.3.4",
"toastify-js": "~1.12.0",
"uid": "~2.0.2",
"use-debounce": "~10.0.2",
"use-debounce": "~10.0.3",
"use-long-press": "~3.2.0",
"use-resize-observer": "~9.1.0",
"valtio": "1.13.2"
"valtio": "2.0.0"
},
"devDependencies": {
"@ianvs/prettier-plugin-sort-imports": "~4.3.1",
"@lingui/cli": "~4.11.3",
"@lingui/vite-plugin": "~4.11.3",
"@lingui/cli": "~4.11.4",
"@lingui/vite-plugin": "~4.11.4",
"@preact/preset-vite": "~2.9.0",
"babel-plugin-macros": "~3.1.0",
"postcss": "~8.4.41",
"postcss": "~8.4.45",
"postcss-dark-theme-class": "~1.3.0",
"postcss-preset-env": "~10.0.1",
"postcss-preset-env": "~10.0.2",
"twitter-text": "~3.1.0",
"vite": "~5.4.1",
"vite": "~5.4.3",
"vite-plugin-generate-file": "~0.2.0",
"vite-plugin-html-config": "~1.0.11",
"vite-plugin-pwa": "~0.20.1",
"vite-plugin-html-config": "~2.0.2",
"vite-plugin-pwa": "~0.20.3",
"vite-plugin-remove-console": "~2.2.0",
"vite-plugin-run": "~0.5.2",
"workbox-cacheable-response": "~7.1.0",

View file

@ -73,18 +73,21 @@ function IDN(inputCode, outputCode) {
return result;
}
// Sort by percentage
const sortedCatalogs = Object.entries(catalogs)
.sort((a, b) => b[1] - a[1])
const fullCatalogs = Object.entries(catalogs)
// sort by key
.sort((a, b) => a[0].localeCompare(b[0]))
.map(([code, completion]) => {
const nativeName = IDN(code, code);
const name = IDN('en', code);
// let names = {};
return { code, nativeName, name, completion };
});
// Sort by completion
const sortedCatalogs = [...fullCatalogs].sort(
(a, b) => b.completion - a.completion,
);
console.table(sortedCatalogs);
const path = 'src/data/catalogs.json';
fs.writeFileSync(path, JSON.stringify(sortedCatalogs, null, 2));
fs.writeFileSync(path, JSON.stringify(fullCatalogs, null, 2));
console.log('File written:', path);

View file

@ -0,0 +1,131 @@
import fs from 'fs';
const { CROWDIN_ACCESS_TOKEN } = process.env;
const PROJECT_ID = '703337';
if (!CROWDIN_ACCESS_TOKEN) {
throw new Error('CROWDIN_ACCESS_TOKEN is not set');
}
// Generate Report
let REPORT_ID = null;
{
const response = await fetch(
`https://api.crowdin.com/api/v2/projects/${PROJECT_ID}/reports`,
{
headers: {
Authorization: `Bearer ${CROWDIN_ACCESS_TOKEN}`,
'Content-Type': 'application/json',
},
method: 'POST',
body: JSON.stringify({
name: 'top-members',
schema: {
format: 'json',
},
}),
},
);
const json = await response.json();
console.log(`Report ID: ${json?.data?.identifier}`);
REPORT_ID = json?.data?.identifier;
}
if (!REPORT_ID) {
throw new Error('Report ID is not found');
}
// Check Report Generation Status
let finished = false;
{
let maxPolls = 10;
do {
maxPolls--;
if (maxPolls < 0) break;
// Wait for 1 second
await new Promise((resolve) => setTimeout(resolve, 1000));
const status = await fetch(
`https://api.crowdin.com/api/v2/projects/${PROJECT_ID}/reports/${REPORT_ID}`,
{
headers: {
Authorization: `Bearer ${CROWDIN_ACCESS_TOKEN}`,
'Content-Type': 'application/json',
},
},
);
const json = await status.json();
const progress = json?.data?.progress;
console.log(`Progress: ${progress}% (${maxPolls} retries left)`);
finished = json?.data?.status === 'finished';
} while (!finished);
}
if (!finished) {
throw new Error('Failed to generate report');
}
// Download Report
let reportURL = null;
{
const response = await fetch(
`https://api.crowdin.com/api/v2/projects/${PROJECT_ID}/reports/${REPORT_ID}/download`,
{
headers: {
Authorization: `Bearer ${CROWDIN_ACCESS_TOKEN}`,
'Content-Type': 'application/json',
},
},
);
const json = await response.json();
reportURL = json?.data?.url;
console.log(`Report URL: ${reportURL}`);
}
if (!reportURL) {
throw new Error('Report URL is not found');
}
// Actually download the report
let members = null;
{
const response = await fetch(reportURL);
const json = await response.json();
const { data } = json;
if (!data?.length) {
throw new Error('No data found');
}
// Sort by 'user.fullName'
data.sort((a, b) => a.user.username.localeCompare(b.user.username));
members = data
.filter((item) => {
const isMyself = item.user.username === 'cheeaun';
const translatedMoreThanZero = item.translated > 0;
return !isMyself && translatedMoreThanZero;
})
.map((item) => ({
avatarUrl: item.user.avatarUrl,
username: item.user.username,
languages: item.languages.map((lang) => lang.name),
}));
console.log(members);
if (members?.length) {
fs.writeFileSync(
'i18n-volunteers.json',
JSON.stringify(members, null, '\t'),
);
}
}
if (!members?.length) {
throw new Error('No members found');
}

View file

@ -0,0 +1,27 @@
// Find for <!-- i18n volunteers start --><!-- i18n volunteers end --> and inject list of i18n volunteers in between
import fs from 'fs';
const i18nVolunteers = JSON.parse(fs.readFileSync('i18n-volunteers.json'));
const readme = fs.readFileSync('README.md', 'utf8');
const i18nVolunteersStart = '<!-- i18n volunteers start -->';
const i18nVolunteersEnd = '<!-- i18n volunteers end -->';
const i18nVolunteersList = i18nVolunteers
.map((member) => {
return `- <img src="${member.avatarUrl}" alt="" width="16" height="16" /> ${
member.username
} (${member.languages.join(', ')})`;
})
.join('\n');
const readmeUpdated = readme.replace(
new RegExp(`${i18nVolunteersStart}.*${i18nVolunteersEnd}`, 's'),
`${i18nVolunteersStart}\n${i18nVolunteersList}\n${i18nVolunteersEnd}`,
);
fs.writeFileSync('README.md', readmeUpdated);
console.log('Updated README.md');

View file

@ -1810,16 +1810,17 @@ body > .szh-menu-container {
env(safe-area-inset-bottom) env(safe-area-inset-left);
}
.szh-menu {
padding: 8px 0;
padding: 4px 0;
margin: 0;
font-size: var(--text-size);
background-color: var(--bg-color);
border: 1px solid var(--outline-color);
border: 1px solid var(--outline-stronger-color);
border-radius: 8px;
box-shadow: 0 3px 16px -3px var(--drop-shadow-color);
box-shadow: 0 3px 8px var(--drop-shadow-color),
0 6px 32px -6px var(--drop-shadow-color);
text-align: start;
/* animation: appear-smooth 0.15s ease-in-out; */
width: 16em;
min-width: 16em;
max-width: 90vw;
/* overflow: hidden; */
}
@ -1874,13 +1875,16 @@ body > .szh-menu-container {
display: flex;
gap: 8px;
align-items: center;
line-height: 1.1;
line-height: 1.3;
padding: 8px 16px !important;
/* transition: all 0.1s ease-in-out; */
text-decoration: none;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
--menu-item-bg-inset: 0 4px;
--menu-item-bg-color: var(--button-bg-color);
}
.szh-menu .szh-menu__item--focusable {
background-color: transparent;
@ -1917,9 +1921,30 @@ body > .szh-menu-container {
.szh-menu__item:not(.szh-menu__item--disabled, .szh-menu__item--hover) {
color: var(--text-color);
}
.szh-menu .szh-menu__item:not(.menu-field) {
position: relative;
& > * {
/* z-index: 1; */
}
&:before {
content: '';
background-color: var(--menu-item-bg-color);
position: absolute;
inset: var(--menu-item-bg-inset);
border-radius: 4px;
z-index: -1;
opacity: 0;
}
}
.szh-menu .szh-menu__item--hover:not(.menu-field) {
color: var(--button-text-color);
background-color: var(--button-bg-color);
/* background-color: var(--button-bg-color); */
background-color: transparent;
&:before {
opacity: 1;
}
}
.szh-menu__divider {
background-color: var(--divider-color);
@ -1995,10 +2020,12 @@ body > .szh-menu-container {
}
.szh-menu
.szh-menu__item.danger:not(.szh-menu__item--disabled).szh-menu__item--hover {
background-color: var(--red-text-color);
/* background-color: var(--red-text-color); */
--menu-item-bg-color: var(--red-text-color);
@media (prefers-color-scheme: dark) {
background-color: var(--red-color);
/* background-color: var(--red-color); */
--menu-item-bg-color: var(--red-color);
}
}
.szh-menu
@ -2038,12 +2065,20 @@ body > .szh-menu-container {
);
}
}
&:before {
content: '';
}
}
.szh-menu__item--hover {
background-color: var(--menu-item-bg-color);
}
}
.menu-control-group-horizontal:first-child,
li[role='none'] + .menu-control-group-horizontal {
margin-top: -8px;
margin-top: -4px;
margin-bottom: -4px;
.szh-menu__item {
@ -2078,6 +2113,8 @@ body > .szh-menu-container {
}
.szh-menu .menu-wrap {
min-width: 16em;
width: min-content;
display: flex;
flex-wrap: wrap;
}
@ -2092,11 +2129,10 @@ body > .szh-menu-container {
background-color: var(--bg-blur-color);
backdrop-filter: blur(8px) saturate(3);
border: var(--hairline-width) solid var(--bg-color);
box-shadow: 0 3px 8px -1px var(--drop-shadow-color);
text-shadow: 0 var(--hairline-width) var(--bg-color), 0 0 8px var(--bg-color);
}
.glass-menu .szh-menu__item--hover {
background-color: var(--button-bg-blur-color);
/* background-color: var(--button-bg-blur-color); */
text-shadow: none;
}

View file

@ -56,7 +56,11 @@ import { getAccessToken } from './utils/auth';
import focusDeck from './utils/focus-deck';
import states, { initStates, statusKey } from './utils/states';
import store from './utils/store';
import { getCurrentAccount, setCurrentAccountID } from './utils/store-utils';
import {
getAccount,
getCurrentAccount,
setCurrentAccountID,
} from './utils/store-utils';
import './utils/toast-alert';
@ -317,9 +321,10 @@ function App() {
window.location.pathname || '/',
);
const clientID = store.session.get('clientID');
const clientSecret = store.session.get('clientSecret');
const vapidKey = store.session.get('vapidKey');
const clientID = store.sessionCookie.get('clientID');
const clientSecret = store.sessionCookie.get('clientSecret');
const vapidKey = store.sessionCookie.get('vapidKey');
const verifier = store.sessionCookie.get('codeVerifier');
(async () => {
setUIState('loading');
@ -328,8 +333,10 @@ function App() {
client_id: clientID,
client_secret: clientSecret,
code,
code_verifier: verifier || undefined,
});
if (accessToken) {
const client = initClient({ instance: instanceURL, accessToken });
await Promise.allSettled([
initPreferences(client),
@ -337,13 +344,35 @@ function App() {
initAccount(client, instanceURL, accessToken, vapidKey),
]);
initStates();
window.__IGNORE_GET_ACCOUNT_ERROR__ = true;
setIsLoggedIn(true);
setUIState('default');
} else {
setUIState('error');
}
})();
} else {
window.__IGNORE_GET_ACCOUNT_ERROR__ = true;
const account = getCurrentAccount();
const searchAccount = decodeURIComponent(
(window.location.search.match(/account=([^&]+)/) || [, ''])[1],
);
let account;
if (searchAccount) {
account = getAccount(searchAccount);
console.log('searchAccount', searchAccount, account);
if (account) {
setCurrentAccountID(account.info.id);
window.history.replaceState(
{},
document.title,
window.location.pathname || '/',
);
}
}
if (!account) {
account = getCurrentAccount();
}
if (account) {
setCurrentAccountID(account.info.id);
const { client } = api({ account });
@ -365,6 +394,11 @@ function App() {
setUIState('default');
}
}
// Cleanup
store.sessionCookie.del('clientID');
store.sessionCookie.del('clientSecret');
store.sessionCookie.del('codeVerifier');
}, []);
let location = useLocation();

View file

@ -63,7 +63,6 @@ const MUTE_DURATIONS_LABELS = {
259_200: i18nDuration(3, 'day'),
604_800: i18nDuration(1, 'week'),
};
console.log({ MUTE_DURATIONS_LABELS });
const LIMIT = 80;

View file

@ -16,7 +16,18 @@ export default memo(function BackgroundService({ isLoggedIn }) {
// Notifications service
// - WebSocket to receive notifications when page is visible
const [visible, setVisible] = useState(true);
usePageVisibility(setVisible);
const visibleTimeout = useRef();
usePageVisibility((visible) => {
clearTimeout(visibleTimeout.current);
if (visible) {
setVisible(true);
} else {
visibleTimeout.current = setTimeout(() => {
setVisible(false);
}, POLL_INTERVAL);
}
});
const checkLatestNotification = async (masto, instance, skipCheckMarkers) => {
if (states.notificationsLast) {
const notificationsIterator = masto.v1.notifications.list({

View file

@ -1154,7 +1154,7 @@ function Compose({
class={`toolbar-button ${
visibility !== 'public' && !sensitive ? 'show-field' : ''
} ${visibility !== 'public' ? 'highlight' : ''}`}
title={`Visibility: ${visibility}`}
title={visibility}
>
<Icon icon={visibilityIconsMap[visibility]} alt={visibility} />
<select
@ -1471,7 +1471,14 @@ function Compose({
class="large"
disabled={uiState === 'loading'}
>
{replyToStatus ? t`Reply` : editStatus ? t`Update` : t`Post`}
{replyToStatus
? t`Reply`
: editStatus
? t`Update`
: t({
message: 'Post',
context: 'Submit button in composer',
})}
</button>
</div>
</form>

View file

@ -1,13 +1,15 @@
import { useLingui } from '@lingui/react';
import { useMemo } from 'preact/hooks';
import { CATALOGS, DEFAULT_LANG, LOCALES } from '../locales';
import { CATALOGS, DEFAULT_LANG, DEV_LOCALES, LOCALES } from '../locales';
import { activateLang } from '../utils/lang';
import localeCode2Text from '../utils/localeCode2Text';
import store from '../utils/store';
const regionMaps = {
'zh-CN': 'zh-Hans',
'zh-TW': 'zh-Hant',
'pt-BR': 'pt-BR',
};
export default function LangSelector() {
@ -16,10 +18,6 @@ export default function LangSelector() {
// Sorted on render, so the order won't suddenly change based on current locale
const populatedLocales = useMemo(() => {
return LOCALES.map((lang) => {
if (lang === 'pseudo-LOCALE') {
return { code: lang, native: 'Pseudolocalization (test)' };
}
// Don't need regions for now, it makes text too noisy
// Wait till there's too many languages and there are regional clashes
const regionlessCode = regionMaps[lang] || lang.replace(/-[a-z]+$/i, '');
@ -45,9 +43,6 @@ export default function LangSelector() {
native,
};
}).sort((a, b) => {
// If pseudo-LOCALE, always put it at the bottom
if (a.code === 'pseudo-LOCALE') return 1;
if (b.code === 'pseudo-LOCALE') return -1;
// Sort by common name
const order = a._common.localeCompare(b._common, i18n.locale);
if (order !== 0) return order;
@ -65,21 +60,11 @@ export default function LangSelector() {
class="small"
value={i18n.locale || DEFAULT_LANG}
onChange={(e) => {
localStorage.setItem('lang', e.target.value);
store.local.set('lang', e.target.value);
activateLang(e.target.value);
}}
>
{populatedLocales.map(({ code, regionlessCode, native }) => {
if (code === 'pseudo-LOCALE') {
return (
<>
<hr />
<option value={code} key={code}>
{native}
</option>
</>
);
}
// Common name changes based on current locale
const common = localeCode2Text({
code: regionlessCode,
@ -97,6 +82,33 @@ export default function LangSelector() {
</option>
);
})}
{(import.meta.env.DEV || import.meta.env.PHANPY_SHOW_DEV_LOCALES) && (
<optgroup label="🚧 Development (<50% translated)">
{DEV_LOCALES.map((code) => {
if (code === 'pseudo-LOCALE') {
return (
<>
<hr />
<option value={code} key={code}>
Pseudolocalization (test)
</option>
</>
);
}
const nativeName = CATALOGS.find(
(c) => c.code === code,
)?.nativeName;
const completion = CATALOGS.find(
(c) => c.code === code,
)?.completion;
return (
<option value={code} key={code}>
{nativeName || code} &lrm;[{completion}%]
</option>
);
})}
</optgroup>
)}
</select>
</label>
);

View file

@ -1,7 +1,18 @@
.nav-menu section:last-child {
.nav-menu {
overflow: hidden;
section:last-child {
background-color: var(--bg-faded-color);
margin-bottom: -8px;
padding-bottom: 8px;
margin-bottom: -4px;
padding-bottom: 4px;
.szh-menu__item:before {
z-index: 0;
}
.szh-menu__item > * {
z-index: 1;
}
}
}
@media (min-width: 23em) {
@ -13,16 +24,16 @@
'top top'
'left right';
padding: 0;
width: 22em;
/* min-width: 22em; */
max-width: calc(100vw - 16px);
}
.nav-menu .top-menu {
grid-area: top;
padding-top: 8px;
margin-bottom: -8px;
padding-top: 4px;
margin-bottom: -4px;
}
.nav-menu section {
padding: 8px 0;
padding: 4px 0;
/* width: 50%; */
}
@keyframes phanpying {

View file

@ -1,4 +1,4 @@
import { Plural, t, Trans } from '@lingui/macro';
import { Plural, plural, t, Trans } from '@lingui/macro';
import { useState } from 'preact/hooks';
import shortenNumber from '../utils/shorten-number';
@ -113,9 +113,10 @@ export default function Poll({
</div>
<div
class="poll-option-votes"
title={`${optionVotesCount} vote${
optionVotesCount === 1 ? '' : 's'
}`}
title={plural(optionVotesCount, {
one: `# vote`,
other: `# votes`,
})}
>
{percentage}
</div>

View file

@ -16,7 +16,8 @@ function isValidDate(value) {
const resolvedLocale = new Intl.DateTimeFormat().resolvedOptions().locale;
const DTF = mem((locale, opts = {}) => {
const lang = localeMatch([locale], [resolvedLocale]);
const regionlessLocale = locale.replace(/-[a-z]+$/i, '');
const lang = localeMatch([regionlessLocale], [resolvedLocale], locale);
try {
return new Intl.DateTimeFormat(lang, opts);
} catch (e) {}

View file

@ -121,13 +121,31 @@
text-overflow: ellipsis;
overflow: hidden;
}
#shortcuts .tab-bar li a {
position: relative;
&:before {
content: '';
position: absolute;
inset: 4px 0;
border-radius: 8px;
background-color: var(--bg-color);
z-index: -1;
transform: scale(0.5);
opacity: 0;
transition: all 0.1s ease-in-out;
}
}
#shortcuts .tab-bar li a.is-active {
color: var(--link-color);
background-image: radial-gradient(
/* background-image: radial-gradient(
closest-side at 50% 50%,
var(--bg-color),
transparent
);
); */
&:before {
transform: scale(1);
opacity: 1;
}
}
#app:has(#home-page):not(:has(#home-page ~ .deck-container)):has(header[hidden])
#shortcuts

View file

@ -430,6 +430,7 @@
> span + span {
position: static;
width: auto;
&:empty {
display: none;
@ -1895,6 +1896,7 @@ a:focus-visible .card img {
.meta-container {
align-self: flex-start;
flex-grow: 0;
max-width: 100%;
}
.card .title {
line-height: 1.25;

View file

@ -1061,7 +1061,14 @@ function Status({
)}
<MenuItem href={url} target="_blank">
<Icon icon="external" />
<small class="menu-double-lines">{nicePostURL(url)}</small>
<small
class="menu-double-lines"
style={{
maxWidth: '16em',
}}
>
{nicePostURL(url)}
</small>
</MenuItem>
<div class="menu-horizontal">
<MenuItem
@ -2653,8 +2660,13 @@ function Card({ card, selfReferential, instance }) {
const imageData = ctx.createImageData(w, h);
imageData.data.set(blurhashPixels);
ctx.putImageData(imageData, 0, 0);
if (window.OffscreenCanvas) {
const blob = canvas.convertToBlob();
blurhashImage = URL.createObjectURL(blob);
} else {
blurhashImage = canvas.toDataURL();
}
}
const isPost = isCardPost(domain);

View file

@ -15,6 +15,7 @@ import { useSnapshot } from 'valtio';
import FilterContext from '../utils/filter-context';
import { filteredItems, isFiltered } from '../utils/filters';
import isRTL from '../utils/is-rtl';
import showToast from '../utils/show-toast';
import states, { statusKey } from '../utils/states';
import statusPeek from '../utils/status-peek';
import { isMediaFirstInstance } from '../utils/store-utils';
@ -121,6 +122,9 @@ function Timeline({
} catch (e) {
console.error(e);
setUIState('error');
if (firstLoad && !items.length && errorText) {
showToast(errorText);
}
} finally {
loadItems.cancel();
}

View file

@ -9,8 +9,10 @@ import { render } from 'preact';
import { useEffect, useState } from 'preact/hooks';
import ComposeSuspense from './components/compose-suspense';
import Loader from './components/loader';
import { initActivateLang } from './utils/lang';
import { initStates } from './utils/states';
import { getCurrentAccount, setCurrentAccountID } from './utils/store-utils';
import useTitle from './utils/useTitle';
initActivateLang();
@ -21,6 +23,7 @@ if (window.opener) {
function App() {
const [uiState, setUIState] = useState('default');
const [isLoggedIn, setIsLoggedIn] = useState(null);
const { editStatus, replyToStatus, draftStatus } = window.__COMPOSE__ || {};
@ -35,7 +38,11 @@ function App() {
);
useEffect(() => {
const account = getCurrentAccount();
setIsLoggedIn(!!account);
if (account) {
initStates();
}
}, []);
useEffect(() => {
@ -69,6 +76,25 @@ function App() {
console.debug('OPEN COMPOSE');
if (isLoggedIn === false) {
return (
<div class="box">
<h1>
<Trans>Error</Trans>
</h1>
<p>
<Trans>Login required.</Trans>
</p>
<p>
<a href="/">
<Trans>Go home</Trans>
</a>
</p>
</div>
);
}
if (isLoggedIn) {
return (
<ComposeSuspense
editStatus={editStatus}
@ -88,6 +114,13 @@ function App() {
}}
/>
);
}
return (
<div class="box">
<Loader />
</div>
);
}
render(

View file

@ -1,16 +1,52 @@
[
{
"code": "ar-SA",
"nativeName": "العربية",
"name": "Arabic",
"completion": 26
},
{
"code": "ca-ES",
"nativeName": "català",
"name": "Catalan",
"completion": 100
},
{
"code": "cs-CZ",
"nativeName": "čeština",
"name": "Czech",
"completion": 79
},
{
"code": "de-DE",
"nativeName": "Deutsch",
"name": "German",
"completion": 96
},
{
"code": "eo-UY",
"nativeName": "Esperanto",
"name": "Esperanto",
"completion": 30
},
{
"code": "es-ES",
"nativeName": "español",
"name": "Spanish",
"completion": 100
},
{
"code": "eu-ES",
"nativeName": "euskara",
"name": "Basque",
"completion": 100
},
{
"code": "fa-IR",
"nativeName": "فارسی",
"name": "Persian",
"completion": 73
},
{
"code": "fi-FI",
"nativeName": "suomi",
@ -18,58 +54,52 @@
"completion": 100
},
{
"code": "zh-CN",
"nativeName": "简体中文",
"name": "Simplified Chinese",
"completion": 100
},
{
"code": "de-DE",
"nativeName": "Deutsch",
"name": "German",
"completion": 98
},
{
"code": "eu-ES",
"nativeName": "euskara",
"name": "Basque",
"completion": 98
"code": "fr-FR",
"nativeName": "français",
"name": "French",
"completion": 99
},
{
"code": "gl-ES",
"nativeName": "galego",
"name": "Galician",
"completion": 98
"completion": 100
},
{
"code": "fr-FR",
"nativeName": "français",
"name": "French",
"completion": 97
"code": "he-IL",
"nativeName": "עברית",
"name": "Hebrew",
"completion": 12
},
{
"code": "ko-KR",
"nativeName": "한국어",
"name": "Korean",
"completion": 75
"code": "it-IT",
"nativeName": "italiano",
"name": "Italian",
"completion": 34
},
{
"code": "cs-CZ",
"nativeName": "čeština",
"name": "Czech",
"completion": 72
"code": "ja-JP",
"nativeName": "日本語",
"name": "Japanese",
"completion": 31
},
{
"code": "kab",
"nativeName": "Taqbaylit",
"name": "Kabyle",
"completion": 67
"completion": 100
},
{
"code": "fa-IR",
"nativeName": "فارسی",
"name": "Persian",
"completion": 62
"code": "ko-KR",
"nativeName": "한국어",
"name": "Korean",
"completion": 86
},
{
"code": "lt-LT",
"nativeName": "lietuvių",
"name": "Lithuanian",
"completion": 43
},
{
"code": "nl-NL",
@ -78,46 +108,28 @@
"completion": 48
},
{
"code": "ja-JP",
"nativeName": "日本語",
"name": "Japanese",
"completion": 32
"code": "pl-PL",
"nativeName": "polski",
"name": "Polish",
"completion": 1
},
{
"code": "lt-LT",
"nativeName": "lietuvių",
"name": "Lithuanian",
"completion": 28
"code": "pt-BR",
"nativeName": "português",
"name": "Portuguese",
"completion": 100
},
{
"code": "pt-PT",
"nativeName": "português",
"name": "Portuguese",
"completion": 100
},
{
"code": "ru-RU",
"nativeName": "русский",
"name": "Russian",
"completion": 23
},
{
"code": "ar-SA",
"nativeName": "العربية",
"name": "Arabic",
"completion": 22
},
{
"code": "it-IT",
"nativeName": "italiano",
"name": "Italian",
"completion": 20
},
{
"code": "eo-UY",
"nativeName": "Esperanto",
"name": "Esperanto",
"completion": 14
},
{
"code": "he-IL",
"nativeName": "עברית",
"name": "Hebrew",
"completion": 11
"completion": 100
},
{
"code": "th-TH",
@ -125,10 +137,22 @@
"name": "Thai",
"completion": 3
},
{
"code": "uk-UA",
"nativeName": "українська",
"name": "Ukrainian",
"completion": 26
},
{
"code": "zh-CN",
"nativeName": "简体中文",
"name": "Simplified Chinese",
"completion": 100
},
{
"code": "zh-TW",
"nativeName": "繁體中文",
"name": "Traditional Chinese",
"completion": 3
"completion": 14
}
]

View file

@ -84,6 +84,7 @@
var(--text-color) 60%
);
--outline-color: rgba(128, 128, 128, 0.2);
--outline-stronger-color: rgba(128, 128, 128, 0.4);
--outline-hover-color: rgba(128, 128, 128, 0.7);
--divider-color: rgba(0, 0, 0, 0.1);
--backdrop-color: rgba(0, 0, 0, 0.1);
@ -149,6 +150,11 @@
mediumslateblue 70%,
var(--text-color) 30%
);
--button-bg-color: color-mix(
in srgb,
var(--blue-color) 80%,
var(--bg-color) 20%
);
--reblog-faded-color: #b190f141;
--reply-to-text-color: var(--reply-to-color);
--reply-to-faded-color: #ffa60017;

View file

@ -12,7 +12,15 @@ const locales = [
.filter(({ completion }) => completion >= PERCENTAGE_THRESHOLD)
.map(({ code }) => code),
];
if (import.meta.env.DEV) {
locales.push('pseudo-LOCALE');
}
export const LOCALES = locales;
let devLocales = [];
if (import.meta.env?.DEV || import.meta.env?.PHANPY_SHOW_DEV_LOCALES) {
devLocales = catalogs
.filter(({ completion }) => completion < PERCENTAGE_THRESHOLD)
.map(({ code }) => code);
devLocales.push('pseudo-LOCALE');
}
export const DEV_LOCALES = devLocales;
export const ALL_LOCALES = [...locales, ...devLocales];

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

3732
src/locales/pl-PL.po Normal file

File diff suppressed because it is too large Load diff

3733
src/locales/pt-BR.po Normal file

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

3732
src/locales/uk-UA.po Normal file

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -9,6 +9,7 @@ import Avatar from '../components/avatar';
import Icon from '../components/icon';
import Link from '../components/link';
import MenuConfirm from '../components/menu-confirm';
import MenuLink from '../components/menu-link';
import Menu2 from '../components/menu2';
import NameText from '../components/name-text';
import { api } from '../utils/api';
@ -16,6 +17,8 @@ import states from '../utils/states';
import store from '../utils/store';
import { getCurrentAccountID, setCurrentAccountID } from '../utils/store-utils';
const isStandalone = window.matchMedia('(display-mode: standalone)').matches;
function Accounts({ onClose }) {
const { masto } = api();
// Accounts
@ -107,6 +110,32 @@ function Accounts({ onClose }) {
</button>
}
>
{moreThanOneAccount && (
<>
<MenuItem
disabled={isCurrent}
onClick={() => {
setCurrentAccountID(account.info.id);
location.reload();
}}
>
<Icon icon="transfer" />{' '}
<Trans>Switch to this account</Trans>
</MenuItem>
{!isStandalone && !isCurrent && (
<MenuLink
href={`./?account=${account.info.id}`}
target="_blank"
>
<Icon icon="external" />
<span>
<Trans>Switch in new tab/window</Trans>
</span>
</MenuLink>
)}
<MenuDivider />
</>
)}
<MenuItem
onClick={() => {
states.showAccount = `${account.info.username}@${account.instanceURL}`;

View file

@ -22,7 +22,7 @@ function Bookmarks() {
<Timeline
title={t`Bookmarks`}
id="bookmarks"
emptyText={`No bookmarks yet. Go bookmark something!`}
emptyText={t`No bookmarks yet. Go bookmark something!`}
errorText={t`Unable to load bookmarks.`}
instance={instance}
fetchItems={fetchBookmarks}

View file

@ -822,6 +822,22 @@ function Catchup() {
},
);
const handleArrowKeys = useCallback((e) => {
const activeElement = document.activeElement;
const isRadio =
activeElement?.tagName === 'INPUT' && activeElement.type === 'radio';
const isArrowKeys =
e.key === 'ArrowDown' ||
e.key === 'ArrowUp' ||
e.key === 'ArrowLeft' ||
e.key === 'ArrowRight';
if (isArrowKeys && isRadio) {
// Note: page scroll won't trigger on first arrow key press due to this. Subsequent presses will.
activeElement.blur();
return;
}
}, []);
return (
<div
ref={(node) => {
@ -883,7 +899,7 @@ function Catchup() {
</div>
</div>
</header>
<main>
<main onKeyDown={handleArrowKeys}>
{uiState === 'start' && (
<div class="catchup-start">
<h1>

View file

@ -22,7 +22,7 @@ function Favourites() {
<Timeline
title={t`Likes`}
id="favourites"
emptyText={`No likes yet. Go like something!`}
emptyText={t`No likes yet. Go like something!`}
errorText={t`Unable to load likes.`}
instance={instance}
fetchItems={fetchFavourites}

View file

@ -205,7 +205,7 @@ function Hashtags({ media: mediaView, columnMode, ...props }) {
<MenuConfirm
subMenu
confirm={info.following}
confirmLabel={`Unfollow #${hashtag}?`}
confirmLabel={t`Unfollow #${hashtag}?`}
disabled={followUIState === 'loading' || !authenticated}
onClick={() => {
setFollowUIState('loading');

View file

@ -11,7 +11,12 @@ import LangSelector from '../components/lang-selector';
import Link from '../components/link';
import Loader from '../components/loader';
import instancesListURL from '../data/instances.json?url';
import { getAuthorizationURL, registerApplication } from '../utils/auth';
import {
getAuthorizationURL,
getPKCEAuthorizationURL,
registerApplication,
} from '../utils/auth';
import { supportsPKCE } from '../utils/oauth-pkce';
import store from '../utils/store';
import useTitle from '../utils/useTitle';
@ -63,17 +68,36 @@ function Login() {
instanceURL,
});
const authPKCE = await supportsPKCE({ instanceURL });
console.log({ authPKCE });
if (authPKCE) {
if (client_id && client_secret) {
store.session.set('clientID', client_id);
store.session.set('clientSecret', client_secret);
store.session.set('vapidKey', vapid_key);
store.sessionCookie.set('clientID', client_id);
store.sessionCookie.set('clientSecret', client_secret);
store.sessionCookie.set('vapidKey', vapid_key);
const [url, verifier] = await getPKCEAuthorizationURL({
instanceURL,
client_id,
});
store.sessionCookie.set('codeVerifier', verifier);
location.href = url;
} else {
alert(t`Failed to register application`);
}
} else {
if (client_id && client_secret) {
store.sessionCookie.set('clientID', client_id);
store.sessionCookie.set('clientSecret', client_secret);
store.sessionCookie.set('vapidKey', vapid_key);
location.href = await getAuthorizationURL({
instanceURL,
client_id,
});
} else {
alert('Failed to register application');
alert(t`Failed to register application`);
}
}
setUIState('default');
} catch (e) {
@ -158,7 +182,7 @@ function Login() {
autocapitalize="off"
autocomplete="off"
spellCheck={false}
placeholder={`instance domain`}
placeholder={t`instance domain`}
onInput={(e) => {
setInstanceText(e.target.value);
}}

View file

@ -232,9 +232,21 @@ function Settings({ onClose }) {
</div>
</li>
<li>
<span>
<label>
<Trans>Display language</Trans>
</label>
<br />
<small>
<a
href="https://crowdin.com/project/phanpy"
target="_blank"
rel="noopener noreferrer"
>
<Trans>Volunteer translations</Trans>
</a>
</small>
</span>
<LangSelector />
</li>
</ul>

View file

@ -1,12 +1,32 @@
const { PHANPY_CLIENT_NAME: CLIENT_NAME, PHANPY_WEBSITE: WEBSITE } = import.meta
.env;
import { generateCodeChallenge, verifier } from './oauth-pkce';
const {
DEV,
PHANPY_CLIENT_NAME: CLIENT_NAME,
PHANPY_WEBSITE: WEBSITE,
} = import.meta.env;
const SCOPES = 'read write follow push';
/*
PHANPY_WEBSITE is set to the default official site.
It's used in pre-built releases, so there's no way to change it dynamically
without rebuilding.
Therefore, we can't use it as redirect_uri.
We only use PHANPY_WEBSITE if it's "same" as current location URL.
Very basic check based on location.hostname for now
*/
const sameSite = WEBSITE
? WEBSITE.toLowerCase().includes(location.hostname)
: false;
const currentLocation = location.origin + location.pathname;
const REDIRECT_URI = DEV || !sameSite ? currentLocation : WEBSITE;
export async function registerApplication({ instanceURL }) {
const registrationParams = new URLSearchParams({
client_name: CLIENT_NAME,
redirect_uris: location.origin + location.pathname,
redirect_uris: REDIRECT_URI,
scopes: SCOPES,
website: WEBSITE,
});
@ -25,11 +45,26 @@ export async function registerApplication({ instanceURL }) {
return registrationJSON;
}
export async function getPKCEAuthorizationURL({ instanceURL, client_id }) {
const codeVerifier = verifier();
const codeChallenge = await generateCodeChallenge(codeVerifier);
const params = new URLSearchParams({
client_id,
code_challenge_method: 'S256',
code_challenge: codeChallenge,
redirect_uri: REDIRECT_URI,
response_type: 'code',
scope: SCOPES,
});
const authorizationURL = `https://${instanceURL}/oauth/authorize?${params.toString()}`;
return [authorizationURL, codeVerifier];
}
export async function getAuthorizationURL({ instanceURL, client_id }) {
const authorizationParams = new URLSearchParams({
client_id,
scope: SCOPES,
redirect_uri: location.origin + location.pathname,
redirect_uri: REDIRECT_URI,
// redirect_uri: 'urn:ietf:wg:oauth:2.0:oob',
response_type: 'code',
});
@ -42,15 +77,23 @@ export async function getAccessToken({
client_id,
client_secret,
code,
code_verifier,
}) {
const params = new URLSearchParams({
client_id,
client_secret,
redirect_uri: location.origin + location.pathname,
redirect_uri: REDIRECT_URI,
grant_type: 'authorization_code',
code,
scope: SCOPES,
// client_secret,
// code_verifier,
});
if (client_secret) {
params.append('client_secret', client_secret);
}
if (code_verifier) {
params.append('code_verifier', code_verifier);
}
const tokenResponse = await fetch(`https://${instanceURL}/oauth/token`, {
method: 'POST',
headers: {

View file

@ -5,6 +5,7 @@ const statusPostRegexes = [
/^\/@[^@\/]+\/(?:statuses|posts)\/([^\/]+)/i, // GoToSocial, Takahe
/\/notes\/([^\/]+)/i, // Misskey, Firefish
/^\/(?:notice|objects)\/([a-z0-9-]+)/i, // Pleroma
/\/@[^@\/]+\/post\/([^\/]+)/i, // Threads
/\/@[^@\/]+@?[^\/]+?\/([^\/]+)/i, // Mastodon
/^\/p\/[^\/]+\/([^\/]+)/i, // Pixelfed
];

View file

@ -6,7 +6,7 @@ export default function isMastodonLinkMaybe(url) {
/^\/(@[^/]+|users\/[^/]+)\/(statuses|posts)\/\w+\/?$/i.test(pathname) || // GoToSocial, Takahe
/^\/notes\/[a-z0-9]+$/i.test(pathname) || // Misskey, Firefish
/^\/(notice|objects)\/[a-z0-9-]+$/i.test(pathname) || // Pleroma
/^\/@[^/]+\/post\/[a-z0-9]+$/i.test(pathname) || // Threads
/^\/@[^/]+\/post\/[a-z0-9\-_]+$/i.test(pathname) || // Threads
/^\/@[^/]+\/[a-z0-9]+[a-z0-9\-]+[a-z0-9]+$/i.test(pathname) || // Hollo
(hostname === 'fed.brid.gy' && pathname.startsWith('/r/http')) || // Bridgy Fed
/#\/[^\/]+\.[^\/]+\/s\/.+/i.test(hash) // Phanpy 🫣

View file

@ -7,7 +7,7 @@ import {
} from '@lingui/detect-locale';
import Locale from 'intl-locale-textinfo-polyfill';
import { DEFAULT_LANG, LOCALES } from '../locales';
import { ALL_LOCALES, DEFAULT_LANG } from '../locales';
import { messages } from '../locales/en.po';
import localeMatch from '../utils/locale-match';
@ -62,7 +62,7 @@ export function initActivateLang() {
DEFAULT_LANG,
);
const matchedLang =
LOCALES.find((l) => l === lang) || localeMatch(lang, LOCALES);
ALL_LOCALES.find((l) => l === lang) || localeMatch(lang, ALL_LOCALES);
activateLang(matchedLang);
// const yes = confirm(t`Reload to apply language setting?`);

View file

@ -1,12 +1,14 @@
import { i18n } from '@lingui/core';
import localeMatch from './locale-match';
import mem from './mem';
const defaultLocale = new Intl.DateTimeFormat().resolvedOptions().locale;
const _DateTimeFormat = (opts) => {
const { locale, dateYear, hideTime, formatOpts } = opts || {};
const loc = locale && !/pseudo/i.test(locale) ? locale : defaultLocale;
const regionlessLocale = locale.replace(/-[a-z]+$/i, '');
const loc = localeMatch([regionlessLocale], [defaultLocale], locale);
const currentYear = new Date().getFullYear();
const options = {
// Show year if not current year
@ -20,9 +22,11 @@ const _DateTimeFormat = (opts) => {
};
try {
return Intl.DateTimeFormat(loc, options);
} catch (e) {
} catch (e) {}
try {
return Intl.DateTimeFormat(locale, options);
} catch (e) {}
return Intl.DateTimeFormat(undefined, options);
}
};
const DateTimeFormat = mem(_DateTimeFormat);

46
src/utils/oauth-pkce.js Normal file
View file

@ -0,0 +1,46 @@
function dec2hex(dec) {
return ('0' + dec.toString(16)).slice(-2);
}
export function verifier() {
var array = new Uint32Array(56 / 2);
window.crypto.getRandomValues(array);
return Array.from(array, dec2hex).join('');
}
function sha256(plain) {
// returns promise ArrayBuffer
const encoder = new TextEncoder();
const data = encoder.encode(plain);
return window.crypto.subtle.digest('SHA-256', data);
}
function base64urlencode(a) {
let str = '';
const bytes = new Uint8Array(a);
const len = bytes.byteLength;
for (var i = 0; i < len; i++) {
str += String.fromCharCode(bytes[i]);
}
return btoa(str).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
}
export async function generateCodeChallenge(v) {
const hashed = await sha256(v);
return base64urlencode(hashed);
}
// If /.well-known/oauth-authorization-server exists and code_challenge_methods_supported includes "S256", means support PKCE
export async function supportsPKCE({ instanceURL }) {
if (!instanceURL) return false;
try {
const res = await fetch(
`https://${instanceURL}/.well-known/oauth-authorization-server`,
);
if (!res.ok || res.status !== 200) return false;
const json = await res.json();
if (json.code_challenge_methods_supported?.includes('S256')) return true;
return false;
} catch (e) {
return false;
}
}
// For debugging
window.__generateCodeChallenge = generateCodeChallenge;

View file

@ -1,6 +1,6 @@
// Utils for push notifications
import { api } from './api';
import { getCurrentAccount } from './store-utils';
import { getVapidKey } from './store-utils';
// Subscription is an object with the following structure:
// {
@ -113,7 +113,7 @@ export async function initSubscription() {
// Check if the subscription changed
if (backendSubscription && subscription) {
const sameEndpoint = backendSubscription.endpoint === subscription.endpoint;
const { vapidKey } = getCurrentAccount();
const vapidKey = getVapidKey();
const sameKey = backendSubscription.serverKey === vapidKey;
if (!sameEndpoint) {
throw new Error('Backend subscription endpoint changed');
@ -146,7 +146,7 @@ export async function initSubscription() {
if (subscription && !backendSubscription) {
// check if account's vapidKey is same as subscription's applicationServerKey
const { vapidKey } = getCurrentAccount();
const vapidKey = getVapidKey();
if (vapidKey) {
const { applicationServerKey } = subscription.options;
const vapidKeyStr = urlBase64ToUint8Array(vapidKey).toString();
@ -210,7 +210,7 @@ export async function updateSubscription({ data, policy }) {
}
} else {
// User is not subscribed
const { vapidKey } = getCurrentAccount();
const vapidKey = getVapidKey();
if (!vapidKey) throw new Error('No server key found');
subscription = await registration.pushManager.subscribe({
userVisibleOnly: true,

View file

@ -154,6 +154,13 @@ export function getCurrentInstanceConfiguration() {
return getInstanceConfiguration(instance);
}
export function getVapidKey() {
// Vapid key has moved from account to instance config
const config = getCurrentInstanceConfiguration();
const vapidKey = config?.vapid?.publicKey || config?.vapid?.public_key;
return vapidKey || getCurrentAccount()?.vapidKey;
}
export function isMediaFirstInstance() {
const instance = getCurrentInstance();
return /pixelfed/i.test(instance?.version);

View file

@ -1,5 +1,9 @@
import Cookies from 'js-cookie';
import { getCurrentAccountNS } from './store-utils';
const cookies = Cookies.withAttributes({ sameSite: 'strict', secure: true });
const local = {
get: (key) => {
try {
@ -86,6 +90,38 @@ const session = {
},
};
// Session secure cookie
const cookie = {
get: (key) => cookies.get(key),
set: (key, value) => cookies.set(key, value),
del: (key) => cookies.remove(key),
};
// Cookie with sessionStorage fallback
const sessionCookie = {
get: (key) => {
if (navigator.cookieEnabled) {
return cookie.get(key);
} else {
return session.get(key);
}
},
set: (key, value) => {
if (navigator.cookieEnabled) {
return cookie.set(key, value);
} else {
return session.set(key, value);
}
},
del: (key) => {
if (navigator.cookieEnabled) {
return cookie.del(key);
} else {
return session.del(key);
}
},
};
// Store with account namespace (id@domain.tld) <- uses id, not username
const account = {
get: (key) => {
@ -118,4 +154,4 @@ const account = {
},
};
export default { local, session, account };
export default { local, session, sessionCookie, cookie, account };

View file

@ -12,9 +12,12 @@ import { VitePWA } from 'vite-plugin-pwa';
import removeConsole from 'vite-plugin-remove-console';
import { run } from 'vite-plugin-run';
import { ALL_LOCALES } from './src/locales';
const allowedEnvPrefixes = ['VITE_', 'PHANPY_'];
const { NODE_ENV } = process.env;
const {
PHANPY_WEBSITE: WEBSITE,
PHANPY_CLIENT_NAME: CLIENT_NAME,
PHANPY_APP_ERROR_LOGGING: ERROR_LOGGING,
} = loadEnv('production', process.cwd(), allowedEnvPrefixes);
@ -70,6 +73,11 @@ export default defineConfig({
run: ['npm', 'run', 'messages:extract:clean'],
pattern: 'src/**/*.{js,jsx,ts,tsx}',
},
// {
// name: 'update-catalogs',
// run: ['node', 'scripts/catalogs.js'],
// pattern: 'src/locales/*.po',
// },
],
}),
splitVendorChunkPlugin(),
@ -78,6 +86,20 @@ export default defineConfig({
}),
htmlPlugin({
headScripts: ERROR_LOGGING ? [rollbarCode] : [],
links: [
...ALL_LOCALES.map((lang) => ({
rel: 'alternate',
hreflang: lang,
// *Fully-qualified* URLs
href: `${WEBSITE}/?lang=${lang}`,
})),
// https://developers.google.com/search/docs/specialty/international/localized-versions#xdefault
{
rel: 'alternate',
hreflang: 'x-default',
href: `${WEBSITE}`,
},
],
}),
generateFile([
{