mirror of
https://github.com/cheeaun/phanpy.git
synced 2025-02-08 17:16:25 +01:00
Merge remote-tracking branch 'origin/main' into feature/akkoma-local-only
# Conflicts: # src/locales/en.po
This commit is contained in:
commit
7f06187d75
74 changed files with 28158 additions and 14722 deletions
4
.github/release.yml
vendored
Normal file
4
.github/release.yml
vendored
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
changelog:
|
||||||
|
exclude:
|
||||||
|
labels:
|
||||||
|
- 'i18n'
|
25
.github/workflows/i18n-automerge.yml
vendored
25
.github/workflows/i18n-automerge.yml
vendored
|
@ -3,6 +3,8 @@ name: i18n PR auto-merge
|
||||||
on:
|
on:
|
||||||
pull_request:
|
pull_request:
|
||||||
types: [opened, synchronize, reopened, labeled]
|
types: [opened, synchronize, reopened, labeled]
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
run-and-merge:
|
run-and-merge:
|
||||||
|
@ -10,14 +12,21 @@ jobs:
|
||||||
github.event.pull_request.base.ref == 'main' &&
|
github.event.pull_request.base.ref == 'main' &&
|
||||||
github.event.pull_request.head.ref == 'l10n_main'
|
github.event.pull_request.head.ref == 'l10n_main'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
# concurrency:
|
|
||||||
# group: ${{ github.workflow }}-${{ github.ref }}
|
|
||||||
# cancel-in-progress: true
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
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
|
- name: Check auto-merge conditions
|
||||||
run: |
|
run: |
|
||||||
BASE_SHA="${{ github.event.pull_request.base.sha }}"
|
BASE_SHA="${{ github.event.pull_request.base.sha }}"
|
||||||
|
@ -47,8 +56,16 @@ jobs:
|
||||||
exit 0
|
exit 0
|
||||||
else
|
else
|
||||||
echo "More than 50 lines have been changed. Merging pull request."
|
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 }})
|
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
|
fi
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
34
.github/workflows/i18n-update-readme.yml
vendored
Normal file
34
.github/workflows/i18n-update-readme.yml
vendored
Normal 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 }}
|
13
.github/workflows/update-catalogs.yml
vendored
13
.github/workflows/update-catalogs.yml
vendored
|
@ -1,17 +1,18 @@
|
||||||
name: Update catalogs
|
name: Update Catalogs
|
||||||
|
|
||||||
on:
|
on:
|
||||||
pull_request_target:
|
push:
|
||||||
types:
|
branches:
|
||||||
- closed
|
- l10n_main
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
update-catalogs:
|
update-catalogs:
|
||||||
if: github.event.pull_request.merged == true
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
ref: l10n_main
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 20
|
node-version: 20
|
||||||
|
@ -27,5 +28,5 @@ jobs:
|
||||||
git config --global user.name "github-actions[bot]"
|
git config --global user.name "github-actions[bot]"
|
||||||
git add src/data/catalogs.json
|
git add src/data/catalogs.json
|
||||||
git commit -m "Update catalogs.json"
|
git commit -m "Update catalogs.json"
|
||||||
git push origin HEAD:main
|
git push origin HEAD:l10n_main || true
|
||||||
fi
|
fi
|
||||||
|
|
53
README.md
53
README.md
|
@ -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)
|
[![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
|
## 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.
|
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
345
i18n-volunteers.json
Normal 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"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
|
@ -1,7 +1,8 @@
|
||||||
import { LOCALES } from './src/locales';
|
import { ALL_LOCALES } from './src/locales';
|
||||||
|
|
||||||
const config = {
|
const config = {
|
||||||
locales: LOCALES,
|
locales: ALL_LOCALES,
|
||||||
|
sourceLocale: 'en',
|
||||||
pseudoLocale: 'pseudo-LOCALE',
|
pseudoLocale: 'pseudo-LOCALE',
|
||||||
fallbackLocales: {
|
fallbackLocales: {
|
||||||
default: 'en',
|
default: 'en',
|
||||||
|
|
545
package-lock.json
generated
545
package-lock.json
generated
File diff suppressed because it is too large
Load diff
31
package.json
31
package.json
|
@ -11,7 +11,9 @@
|
||||||
"bundle-visualizer": "npx vite-bundle-visualizer",
|
"bundle-visualizer": "npx vite-bundle-visualizer",
|
||||||
"messages:extract": "lingui extract",
|
"messages:extract": "lingui extract",
|
||||||
"messages:extract:clean": "lingui extract --locale en --clean",
|
"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": {
|
"dependencies": {
|
||||||
"@formatjs/intl-localematcher": "~0.5.4",
|
"@formatjs/intl-localematcher": "~0.5.4",
|
||||||
|
@ -20,9 +22,9 @@
|
||||||
"@github/text-expander-element": "~2.7.1",
|
"@github/text-expander-element": "~2.7.1",
|
||||||
"@iconify-icons/mingcute": "~1.2.9",
|
"@iconify-icons/mingcute": "~1.2.9",
|
||||||
"@justinribeiro/lite-youtube": "~1.5.0",
|
"@justinribeiro/lite-youtube": "~1.5.0",
|
||||||
"@lingui/detect-locale": "~4.11.3",
|
"@lingui/detect-locale": "~4.11.4",
|
||||||
"@lingui/macro": "~4.11.3",
|
"@lingui/macro": "~4.11.4",
|
||||||
"@lingui/react": "~4.11.3",
|
"@lingui/react": "~4.11.4",
|
||||||
"@szhsin/react-menu": "~4.2.2",
|
"@szhsin/react-menu": "~4.2.2",
|
||||||
"compare-versions": "~6.1.1",
|
"compare-versions": "~6.1.1",
|
||||||
"fast-blurhash": "~1.1.4",
|
"fast-blurhash": "~1.1.4",
|
||||||
|
@ -31,6 +33,7 @@
|
||||||
"html-prettify": "~1.0.7",
|
"html-prettify": "~1.0.7",
|
||||||
"idb-keyval": "~6.2.1",
|
"idb-keyval": "~6.2.1",
|
||||||
"intl-locale-textinfo-polyfill": "~2.1.1",
|
"intl-locale-textinfo-polyfill": "~2.1.1",
|
||||||
|
"js-cookie": "~3.0.5",
|
||||||
"just-debounce-it": "~3.2.0",
|
"just-debounce-it": "~3.2.0",
|
||||||
"lz-string": "~1.5.0",
|
"lz-string": "~1.5.0",
|
||||||
"masto": "~6.8.0",
|
"masto": "~6.8.0",
|
||||||
|
@ -39,7 +42,7 @@
|
||||||
"p-throttle": "~6.2.0",
|
"p-throttle": "~6.2.0",
|
||||||
"preact": "~10.23.2",
|
"preact": "~10.23.2",
|
||||||
"punycode": "~2.3.1",
|
"punycode": "~2.3.1",
|
||||||
"react-hotkeys-hook": "~4.5.0",
|
"react-hotkeys-hook": "~4.5.1",
|
||||||
"react-intersection-observer": "~9.13.0",
|
"react-intersection-observer": "~9.13.0",
|
||||||
"react-quick-pinch-zoom": "~5.1.0",
|
"react-quick-pinch-zoom": "~5.1.0",
|
||||||
"react-router-dom": "6.6.2",
|
"react-router-dom": "6.6.2",
|
||||||
|
@ -48,25 +51,25 @@
|
||||||
"tinyld": "~1.3.4",
|
"tinyld": "~1.3.4",
|
||||||
"toastify-js": "~1.12.0",
|
"toastify-js": "~1.12.0",
|
||||||
"uid": "~2.0.2",
|
"uid": "~2.0.2",
|
||||||
"use-debounce": "~10.0.2",
|
"use-debounce": "~10.0.3",
|
||||||
"use-long-press": "~3.2.0",
|
"use-long-press": "~3.2.0",
|
||||||
"use-resize-observer": "~9.1.0",
|
"use-resize-observer": "~9.1.0",
|
||||||
"valtio": "1.13.2"
|
"valtio": "2.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@ianvs/prettier-plugin-sort-imports": "~4.3.1",
|
"@ianvs/prettier-plugin-sort-imports": "~4.3.1",
|
||||||
"@lingui/cli": "~4.11.3",
|
"@lingui/cli": "~4.11.4",
|
||||||
"@lingui/vite-plugin": "~4.11.3",
|
"@lingui/vite-plugin": "~4.11.4",
|
||||||
"@preact/preset-vite": "~2.9.0",
|
"@preact/preset-vite": "~2.9.0",
|
||||||
"babel-plugin-macros": "~3.1.0",
|
"babel-plugin-macros": "~3.1.0",
|
||||||
"postcss": "~8.4.41",
|
"postcss": "~8.4.45",
|
||||||
"postcss-dark-theme-class": "~1.3.0",
|
"postcss-dark-theme-class": "~1.3.0",
|
||||||
"postcss-preset-env": "~10.0.1",
|
"postcss-preset-env": "~10.0.2",
|
||||||
"twitter-text": "~3.1.0",
|
"twitter-text": "~3.1.0",
|
||||||
"vite": "~5.4.1",
|
"vite": "~5.4.3",
|
||||||
"vite-plugin-generate-file": "~0.2.0",
|
"vite-plugin-generate-file": "~0.2.0",
|
||||||
"vite-plugin-html-config": "~1.0.11",
|
"vite-plugin-html-config": "~2.0.2",
|
||||||
"vite-plugin-pwa": "~0.20.1",
|
"vite-plugin-pwa": "~0.20.3",
|
||||||
"vite-plugin-remove-console": "~2.2.0",
|
"vite-plugin-remove-console": "~2.2.0",
|
||||||
"vite-plugin-run": "~0.5.2",
|
"vite-plugin-run": "~0.5.2",
|
||||||
"workbox-cacheable-response": "~7.1.0",
|
"workbox-cacheable-response": "~7.1.0",
|
||||||
|
|
|
@ -73,18 +73,21 @@ function IDN(inputCode, outputCode) {
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sort by percentage
|
const fullCatalogs = Object.entries(catalogs)
|
||||||
const sortedCatalogs = Object.entries(catalogs)
|
// sort by key
|
||||||
.sort((a, b) => b[1] - a[1])
|
.sort((a, b) => a[0].localeCompare(b[0]))
|
||||||
.map(([code, completion]) => {
|
.map(([code, completion]) => {
|
||||||
const nativeName = IDN(code, code);
|
const nativeName = IDN(code, code);
|
||||||
const name = IDN('en', code);
|
const name = IDN('en', code);
|
||||||
// let names = {};
|
|
||||||
return { code, nativeName, name, completion };
|
return { code, nativeName, name, completion };
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Sort by completion
|
||||||
|
const sortedCatalogs = [...fullCatalogs].sort(
|
||||||
|
(a, b) => b.completion - a.completion,
|
||||||
|
);
|
||||||
console.table(sortedCatalogs);
|
console.table(sortedCatalogs);
|
||||||
|
|
||||||
const path = 'src/data/catalogs.json';
|
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);
|
console.log('File written:', path);
|
||||||
|
|
131
scripts/fetch-i18n-volunteers.js
Normal file
131
scripts/fetch-i18n-volunteers.js
Normal 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');
|
||||||
|
}
|
27
scripts/update-i18n-volunteers-readme.js
Normal file
27
scripts/update-i18n-volunteers-readme.js
Normal 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');
|
58
src/app.css
58
src/app.css
|
@ -1810,16 +1810,17 @@ body > .szh-menu-container {
|
||||||
env(safe-area-inset-bottom) env(safe-area-inset-left);
|
env(safe-area-inset-bottom) env(safe-area-inset-left);
|
||||||
}
|
}
|
||||||
.szh-menu {
|
.szh-menu {
|
||||||
padding: 8px 0;
|
padding: 4px 0;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: var(--text-size);
|
font-size: var(--text-size);
|
||||||
background-color: var(--bg-color);
|
background-color: var(--bg-color);
|
||||||
border: 1px solid var(--outline-color);
|
border: 1px solid var(--outline-stronger-color);
|
||||||
border-radius: 8px;
|
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;
|
text-align: start;
|
||||||
/* animation: appear-smooth 0.15s ease-in-out; */
|
/* animation: appear-smooth 0.15s ease-in-out; */
|
||||||
width: 16em;
|
min-width: 16em;
|
||||||
max-width: 90vw;
|
max-width: 90vw;
|
||||||
/* overflow: hidden; */
|
/* overflow: hidden; */
|
||||||
}
|
}
|
||||||
|
@ -1874,13 +1875,16 @@ body > .szh-menu-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
line-height: 1.1;
|
line-height: 1.3;
|
||||||
padding: 8px 16px !important;
|
padding: 8px 16px !important;
|
||||||
/* transition: all 0.1s ease-in-out; */
|
/* transition: all 0.1s ease-in-out; */
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
|
|
||||||
|
--menu-item-bg-inset: 0 4px;
|
||||||
|
--menu-item-bg-color: var(--button-bg-color);
|
||||||
}
|
}
|
||||||
.szh-menu .szh-menu__item--focusable {
|
.szh-menu .szh-menu__item--focusable {
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
|
@ -1917,9 +1921,30 @@ body > .szh-menu-container {
|
||||||
.szh-menu__item:not(.szh-menu__item--disabled, .szh-menu__item--hover) {
|
.szh-menu__item:not(.szh-menu__item--disabled, .szh-menu__item--hover) {
|
||||||
color: var(--text-color);
|
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) {
|
.szh-menu .szh-menu__item--hover:not(.menu-field) {
|
||||||
color: var(--button-text-color);
|
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 {
|
.szh-menu__divider {
|
||||||
background-color: var(--divider-color);
|
background-color: var(--divider-color);
|
||||||
|
@ -1995,10 +2020,12 @@ body > .szh-menu-container {
|
||||||
}
|
}
|
||||||
.szh-menu
|
.szh-menu
|
||||||
.szh-menu__item.danger:not(.szh-menu__item--disabled).szh-menu__item--hover {
|
.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) {
|
@media (prefers-color-scheme: dark) {
|
||||||
background-color: var(--red-color);
|
/* background-color: var(--red-color); */
|
||||||
|
--menu-item-bg-color: var(--red-color);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.szh-menu
|
.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,
|
.menu-control-group-horizontal:first-child,
|
||||||
li[role='none'] + .menu-control-group-horizontal {
|
li[role='none'] + .menu-control-group-horizontal {
|
||||||
margin-top: -8px;
|
margin-top: -4px;
|
||||||
margin-bottom: -4px;
|
margin-bottom: -4px;
|
||||||
|
|
||||||
.szh-menu__item {
|
.szh-menu__item {
|
||||||
|
@ -2078,6 +2113,8 @@ body > .szh-menu-container {
|
||||||
}
|
}
|
||||||
|
|
||||||
.szh-menu .menu-wrap {
|
.szh-menu .menu-wrap {
|
||||||
|
min-width: 16em;
|
||||||
|
width: min-content;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
@ -2092,11 +2129,10 @@ body > .szh-menu-container {
|
||||||
background-color: var(--bg-blur-color);
|
background-color: var(--bg-blur-color);
|
||||||
backdrop-filter: blur(8px) saturate(3);
|
backdrop-filter: blur(8px) saturate(3);
|
||||||
border: var(--hairline-width) solid var(--bg-color);
|
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);
|
text-shadow: 0 var(--hairline-width) var(--bg-color), 0 0 8px var(--bg-color);
|
||||||
}
|
}
|
||||||
.glass-menu .szh-menu__item--hover {
|
.glass-menu .szh-menu__item--hover {
|
||||||
background-color: var(--button-bg-blur-color);
|
/* background-color: var(--button-bg-blur-color); */
|
||||||
text-shadow: none;
|
text-shadow: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
44
src/app.jsx
44
src/app.jsx
|
@ -56,7 +56,11 @@ import { getAccessToken } from './utils/auth';
|
||||||
import focusDeck from './utils/focus-deck';
|
import focusDeck from './utils/focus-deck';
|
||||||
import states, { initStates, statusKey } from './utils/states';
|
import states, { initStates, statusKey } from './utils/states';
|
||||||
import store from './utils/store';
|
import store from './utils/store';
|
||||||
import { getCurrentAccount, setCurrentAccountID } from './utils/store-utils';
|
import {
|
||||||
|
getAccount,
|
||||||
|
getCurrentAccount,
|
||||||
|
setCurrentAccountID,
|
||||||
|
} from './utils/store-utils';
|
||||||
|
|
||||||
import './utils/toast-alert';
|
import './utils/toast-alert';
|
||||||
|
|
||||||
|
@ -317,9 +321,10 @@ function App() {
|
||||||
window.location.pathname || '/',
|
window.location.pathname || '/',
|
||||||
);
|
);
|
||||||
|
|
||||||
const clientID = store.session.get('clientID');
|
const clientID = store.sessionCookie.get('clientID');
|
||||||
const clientSecret = store.session.get('clientSecret');
|
const clientSecret = store.sessionCookie.get('clientSecret');
|
||||||
const vapidKey = store.session.get('vapidKey');
|
const vapidKey = store.sessionCookie.get('vapidKey');
|
||||||
|
const verifier = store.sessionCookie.get('codeVerifier');
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
setUIState('loading');
|
setUIState('loading');
|
||||||
|
@ -328,8 +333,10 @@ function App() {
|
||||||
client_id: clientID,
|
client_id: clientID,
|
||||||
client_secret: clientSecret,
|
client_secret: clientSecret,
|
||||||
code,
|
code,
|
||||||
|
code_verifier: verifier || undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (accessToken) {
|
||||||
const client = initClient({ instance: instanceURL, accessToken });
|
const client = initClient({ instance: instanceURL, accessToken });
|
||||||
await Promise.allSettled([
|
await Promise.allSettled([
|
||||||
initPreferences(client),
|
initPreferences(client),
|
||||||
|
@ -337,13 +344,35 @@ function App() {
|
||||||
initAccount(client, instanceURL, accessToken, vapidKey),
|
initAccount(client, instanceURL, accessToken, vapidKey),
|
||||||
]);
|
]);
|
||||||
initStates();
|
initStates();
|
||||||
|
window.__IGNORE_GET_ACCOUNT_ERROR__ = true;
|
||||||
|
|
||||||
setIsLoggedIn(true);
|
setIsLoggedIn(true);
|
||||||
setUIState('default');
|
setUIState('default');
|
||||||
|
} else {
|
||||||
|
setUIState('error');
|
||||||
|
}
|
||||||
})();
|
})();
|
||||||
} else {
|
} else {
|
||||||
window.__IGNORE_GET_ACCOUNT_ERROR__ = true;
|
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) {
|
if (account) {
|
||||||
setCurrentAccountID(account.info.id);
|
setCurrentAccountID(account.info.id);
|
||||||
const { client } = api({ account });
|
const { client } = api({ account });
|
||||||
|
@ -365,6 +394,11 @@ function App() {
|
||||||
setUIState('default');
|
setUIState('default');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Cleanup
|
||||||
|
store.sessionCookie.del('clientID');
|
||||||
|
store.sessionCookie.del('clientSecret');
|
||||||
|
store.sessionCookie.del('codeVerifier');
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
let location = useLocation();
|
let location = useLocation();
|
||||||
|
|
|
@ -63,7 +63,6 @@ const MUTE_DURATIONS_LABELS = {
|
||||||
259_200: i18nDuration(3, 'day'),
|
259_200: i18nDuration(3, 'day'),
|
||||||
604_800: i18nDuration(1, 'week'),
|
604_800: i18nDuration(1, 'week'),
|
||||||
};
|
};
|
||||||
console.log({ MUTE_DURATIONS_LABELS });
|
|
||||||
|
|
||||||
const LIMIT = 80;
|
const LIMIT = 80;
|
||||||
|
|
||||||
|
|
|
@ -16,7 +16,18 @@ export default memo(function BackgroundService({ isLoggedIn }) {
|
||||||
// Notifications service
|
// Notifications service
|
||||||
// - WebSocket to receive notifications when page is visible
|
// - WebSocket to receive notifications when page is visible
|
||||||
const [visible, setVisible] = useState(true);
|
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) => {
|
const checkLatestNotification = async (masto, instance, skipCheckMarkers) => {
|
||||||
if (states.notificationsLast) {
|
if (states.notificationsLast) {
|
||||||
const notificationsIterator = masto.v1.notifications.list({
|
const notificationsIterator = masto.v1.notifications.list({
|
||||||
|
|
|
@ -1154,7 +1154,7 @@ function Compose({
|
||||||
class={`toolbar-button ${
|
class={`toolbar-button ${
|
||||||
visibility !== 'public' && !sensitive ? 'show-field' : ''
|
visibility !== 'public' && !sensitive ? 'show-field' : ''
|
||||||
} ${visibility !== 'public' ? 'highlight' : ''}`}
|
} ${visibility !== 'public' ? 'highlight' : ''}`}
|
||||||
title={`Visibility: ${visibility}`}
|
title={visibility}
|
||||||
>
|
>
|
||||||
<Icon icon={visibilityIconsMap[visibility]} alt={visibility} />
|
<Icon icon={visibilityIconsMap[visibility]} alt={visibility} />
|
||||||
<select
|
<select
|
||||||
|
@ -1471,7 +1471,14 @@ function Compose({
|
||||||
class="large"
|
class="large"
|
||||||
disabled={uiState === 'loading'}
|
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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
|
@ -1,13 +1,15 @@
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
import { useMemo } from 'preact/hooks';
|
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 { activateLang } from '../utils/lang';
|
||||||
import localeCode2Text from '../utils/localeCode2Text';
|
import localeCode2Text from '../utils/localeCode2Text';
|
||||||
|
import store from '../utils/store';
|
||||||
|
|
||||||
const regionMaps = {
|
const regionMaps = {
|
||||||
'zh-CN': 'zh-Hans',
|
'zh-CN': 'zh-Hans',
|
||||||
'zh-TW': 'zh-Hant',
|
'zh-TW': 'zh-Hant',
|
||||||
|
'pt-BR': 'pt-BR',
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function LangSelector() {
|
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
|
// Sorted on render, so the order won't suddenly change based on current locale
|
||||||
const populatedLocales = useMemo(() => {
|
const populatedLocales = useMemo(() => {
|
||||||
return LOCALES.map((lang) => {
|
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
|
// Don't need regions for now, it makes text too noisy
|
||||||
// Wait till there's too many languages and there are regional clashes
|
// Wait till there's too many languages and there are regional clashes
|
||||||
const regionlessCode = regionMaps[lang] || lang.replace(/-[a-z]+$/i, '');
|
const regionlessCode = regionMaps[lang] || lang.replace(/-[a-z]+$/i, '');
|
||||||
|
@ -45,9 +43,6 @@ export default function LangSelector() {
|
||||||
native,
|
native,
|
||||||
};
|
};
|
||||||
}).sort((a, b) => {
|
}).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
|
// Sort by common name
|
||||||
const order = a._common.localeCompare(b._common, i18n.locale);
|
const order = a._common.localeCompare(b._common, i18n.locale);
|
||||||
if (order !== 0) return order;
|
if (order !== 0) return order;
|
||||||
|
@ -65,21 +60,11 @@ export default function LangSelector() {
|
||||||
class="small"
|
class="small"
|
||||||
value={i18n.locale || DEFAULT_LANG}
|
value={i18n.locale || DEFAULT_LANG}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
localStorage.setItem('lang', e.target.value);
|
store.local.set('lang', e.target.value);
|
||||||
activateLang(e.target.value);
|
activateLang(e.target.value);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{populatedLocales.map(({ code, regionlessCode, native }) => {
|
{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
|
// Common name changes based on current locale
|
||||||
const common = localeCode2Text({
|
const common = localeCode2Text({
|
||||||
code: regionlessCode,
|
code: regionlessCode,
|
||||||
|
@ -97,6 +82,33 @@ export default function LangSelector() {
|
||||||
</option>
|
</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} ‎[{completion}%]
|
||||||
|
</option>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</optgroup>
|
||||||
|
)}
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,7 +1,18 @@
|
||||||
.nav-menu section:last-child {
|
.nav-menu {
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
section:last-child {
|
||||||
background-color: var(--bg-faded-color);
|
background-color: var(--bg-faded-color);
|
||||||
margin-bottom: -8px;
|
margin-bottom: -4px;
|
||||||
padding-bottom: 8px;
|
padding-bottom: 4px;
|
||||||
|
|
||||||
|
.szh-menu__item:before {
|
||||||
|
z-index: 0;
|
||||||
|
}
|
||||||
|
.szh-menu__item > * {
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 23em) {
|
@media (min-width: 23em) {
|
||||||
|
@ -13,16 +24,16 @@
|
||||||
'top top'
|
'top top'
|
||||||
'left right';
|
'left right';
|
||||||
padding: 0;
|
padding: 0;
|
||||||
width: 22em;
|
/* min-width: 22em; */
|
||||||
max-width: calc(100vw - 16px);
|
max-width: calc(100vw - 16px);
|
||||||
}
|
}
|
||||||
.nav-menu .top-menu {
|
.nav-menu .top-menu {
|
||||||
grid-area: top;
|
grid-area: top;
|
||||||
padding-top: 8px;
|
padding-top: 4px;
|
||||||
margin-bottom: -8px;
|
margin-bottom: -4px;
|
||||||
}
|
}
|
||||||
.nav-menu section {
|
.nav-menu section {
|
||||||
padding: 8px 0;
|
padding: 4px 0;
|
||||||
/* width: 50%; */
|
/* width: 50%; */
|
||||||
}
|
}
|
||||||
@keyframes phanpying {
|
@keyframes phanpying {
|
||||||
|
|
|
@ -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 { useState } from 'preact/hooks';
|
||||||
|
|
||||||
import shortenNumber from '../utils/shorten-number';
|
import shortenNumber from '../utils/shorten-number';
|
||||||
|
@ -113,9 +113,10 @@ export default function Poll({
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="poll-option-votes"
|
class="poll-option-votes"
|
||||||
title={`${optionVotesCount} vote${
|
title={plural(optionVotesCount, {
|
||||||
optionVotesCount === 1 ? '' : 's'
|
one: `# vote`,
|
||||||
}`}
|
other: `# votes`,
|
||||||
|
})}
|
||||||
>
|
>
|
||||||
{percentage}
|
{percentage}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -16,7 +16,8 @@ function isValidDate(value) {
|
||||||
|
|
||||||
const resolvedLocale = new Intl.DateTimeFormat().resolvedOptions().locale;
|
const resolvedLocale = new Intl.DateTimeFormat().resolvedOptions().locale;
|
||||||
const DTF = mem((locale, opts = {}) => {
|
const DTF = mem((locale, opts = {}) => {
|
||||||
const lang = localeMatch([locale], [resolvedLocale]);
|
const regionlessLocale = locale.replace(/-[a-z]+$/i, '');
|
||||||
|
const lang = localeMatch([regionlessLocale], [resolvedLocale], locale);
|
||||||
try {
|
try {
|
||||||
return new Intl.DateTimeFormat(lang, opts);
|
return new Intl.DateTimeFormat(lang, opts);
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
|
|
|
@ -121,13 +121,31 @@
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
overflow: hidden;
|
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 {
|
#shortcuts .tab-bar li a.is-active {
|
||||||
color: var(--link-color);
|
color: var(--link-color);
|
||||||
background-image: radial-gradient(
|
/* background-image: radial-gradient(
|
||||||
closest-side at 50% 50%,
|
closest-side at 50% 50%,
|
||||||
var(--bg-color),
|
var(--bg-color),
|
||||||
transparent
|
transparent
|
||||||
);
|
); */
|
||||||
|
&:before {
|
||||||
|
transform: scale(1);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
#app:has(#home-page):not(:has(#home-page ~ .deck-container)):has(header[hidden])
|
#app:has(#home-page):not(:has(#home-page ~ .deck-container)):has(header[hidden])
|
||||||
#shortcuts
|
#shortcuts
|
||||||
|
|
|
@ -430,6 +430,7 @@
|
||||||
|
|
||||||
> span + span {
|
> span + span {
|
||||||
position: static;
|
position: static;
|
||||||
|
width: auto;
|
||||||
|
|
||||||
&:empty {
|
&:empty {
|
||||||
display: none;
|
display: none;
|
||||||
|
@ -1895,6 +1896,7 @@ a:focus-visible .card img {
|
||||||
.meta-container {
|
.meta-container {
|
||||||
align-self: flex-start;
|
align-self: flex-start;
|
||||||
flex-grow: 0;
|
flex-grow: 0;
|
||||||
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
.card .title {
|
.card .title {
|
||||||
line-height: 1.25;
|
line-height: 1.25;
|
||||||
|
|
|
@ -1061,7 +1061,14 @@ function Status({
|
||||||
)}
|
)}
|
||||||
<MenuItem href={url} target="_blank">
|
<MenuItem href={url} target="_blank">
|
||||||
<Icon icon="external" />
|
<Icon icon="external" />
|
||||||
<small class="menu-double-lines">{nicePostURL(url)}</small>
|
<small
|
||||||
|
class="menu-double-lines"
|
||||||
|
style={{
|
||||||
|
maxWidth: '16em',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{nicePostURL(url)}
|
||||||
|
</small>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<div class="menu-horizontal">
|
<div class="menu-horizontal">
|
||||||
<MenuItem
|
<MenuItem
|
||||||
|
@ -2653,8 +2660,13 @@ function Card({ card, selfReferential, instance }) {
|
||||||
const imageData = ctx.createImageData(w, h);
|
const imageData = ctx.createImageData(w, h);
|
||||||
imageData.data.set(blurhashPixels);
|
imageData.data.set(blurhashPixels);
|
||||||
ctx.putImageData(imageData, 0, 0);
|
ctx.putImageData(imageData, 0, 0);
|
||||||
|
if (window.OffscreenCanvas) {
|
||||||
|
const blob = canvas.convertToBlob();
|
||||||
|
blurhashImage = URL.createObjectURL(blob);
|
||||||
|
} else {
|
||||||
blurhashImage = canvas.toDataURL();
|
blurhashImage = canvas.toDataURL();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const isPost = isCardPost(domain);
|
const isPost = isCardPost(domain);
|
||||||
|
|
||||||
|
|
|
@ -15,6 +15,7 @@ import { useSnapshot } from 'valtio';
|
||||||
import FilterContext from '../utils/filter-context';
|
import FilterContext from '../utils/filter-context';
|
||||||
import { filteredItems, isFiltered } from '../utils/filters';
|
import { filteredItems, isFiltered } from '../utils/filters';
|
||||||
import isRTL from '../utils/is-rtl';
|
import isRTL from '../utils/is-rtl';
|
||||||
|
import showToast from '../utils/show-toast';
|
||||||
import states, { statusKey } from '../utils/states';
|
import states, { statusKey } from '../utils/states';
|
||||||
import statusPeek from '../utils/status-peek';
|
import statusPeek from '../utils/status-peek';
|
||||||
import { isMediaFirstInstance } from '../utils/store-utils';
|
import { isMediaFirstInstance } from '../utils/store-utils';
|
||||||
|
@ -121,6 +122,9 @@ function Timeline({
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
setUIState('error');
|
setUIState('error');
|
||||||
|
if (firstLoad && !items.length && errorText) {
|
||||||
|
showToast(errorText);
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
loadItems.cancel();
|
loadItems.cancel();
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,8 +9,10 @@ import { render } from 'preact';
|
||||||
import { useEffect, useState } from 'preact/hooks';
|
import { useEffect, useState } from 'preact/hooks';
|
||||||
|
|
||||||
import ComposeSuspense from './components/compose-suspense';
|
import ComposeSuspense from './components/compose-suspense';
|
||||||
|
import Loader from './components/loader';
|
||||||
import { initActivateLang } from './utils/lang';
|
import { initActivateLang } from './utils/lang';
|
||||||
import { initStates } from './utils/states';
|
import { initStates } from './utils/states';
|
||||||
|
import { getCurrentAccount, setCurrentAccountID } from './utils/store-utils';
|
||||||
import useTitle from './utils/useTitle';
|
import useTitle from './utils/useTitle';
|
||||||
|
|
||||||
initActivateLang();
|
initActivateLang();
|
||||||
|
@ -21,6 +23,7 @@ if (window.opener) {
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const [uiState, setUIState] = useState('default');
|
const [uiState, setUIState] = useState('default');
|
||||||
|
const [isLoggedIn, setIsLoggedIn] = useState(null);
|
||||||
|
|
||||||
const { editStatus, replyToStatus, draftStatus } = window.__COMPOSE__ || {};
|
const { editStatus, replyToStatus, draftStatus } = window.__COMPOSE__ || {};
|
||||||
|
|
||||||
|
@ -35,7 +38,11 @@ function App() {
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
const account = getCurrentAccount();
|
||||||
|
setIsLoggedIn(!!account);
|
||||||
|
if (account) {
|
||||||
initStates();
|
initStates();
|
||||||
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -69,6 +76,25 @@ function App() {
|
||||||
|
|
||||||
console.debug('OPEN COMPOSE');
|
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 (
|
return (
|
||||||
<ComposeSuspense
|
<ComposeSuspense
|
||||||
editStatus={editStatus}
|
editStatus={editStatus}
|
||||||
|
@ -88,6 +114,13 @@ function App() {
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class="box">
|
||||||
|
<Loader />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
render(
|
render(
|
||||||
|
|
|
@ -1,16 +1,52 @@
|
||||||
[
|
[
|
||||||
|
{
|
||||||
|
"code": "ar-SA",
|
||||||
|
"nativeName": "العربية",
|
||||||
|
"name": "Arabic",
|
||||||
|
"completion": 26
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"code": "ca-ES",
|
"code": "ca-ES",
|
||||||
"nativeName": "català",
|
"nativeName": "català",
|
||||||
"name": "Catalan",
|
"name": "Catalan",
|
||||||
"completion": 100
|
"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",
|
"code": "es-ES",
|
||||||
"nativeName": "español",
|
"nativeName": "español",
|
||||||
"name": "Spanish",
|
"name": "Spanish",
|
||||||
"completion": 100
|
"completion": 100
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"code": "eu-ES",
|
||||||
|
"nativeName": "euskara",
|
||||||
|
"name": "Basque",
|
||||||
|
"completion": 100
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "fa-IR",
|
||||||
|
"nativeName": "فارسی",
|
||||||
|
"name": "Persian",
|
||||||
|
"completion": 73
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"code": "fi-FI",
|
"code": "fi-FI",
|
||||||
"nativeName": "suomi",
|
"nativeName": "suomi",
|
||||||
|
@ -18,58 +54,52 @@
|
||||||
"completion": 100
|
"completion": 100
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"code": "zh-CN",
|
"code": "fr-FR",
|
||||||
"nativeName": "简体中文",
|
"nativeName": "français",
|
||||||
"name": "Simplified Chinese",
|
"name": "French",
|
||||||
"completion": 100
|
"completion": 99
|
||||||
},
|
|
||||||
{
|
|
||||||
"code": "de-DE",
|
|
||||||
"nativeName": "Deutsch",
|
|
||||||
"name": "German",
|
|
||||||
"completion": 98
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"code": "eu-ES",
|
|
||||||
"nativeName": "euskara",
|
|
||||||
"name": "Basque",
|
|
||||||
"completion": 98
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"code": "gl-ES",
|
"code": "gl-ES",
|
||||||
"nativeName": "galego",
|
"nativeName": "galego",
|
||||||
"name": "Galician",
|
"name": "Galician",
|
||||||
"completion": 98
|
"completion": 100
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"code": "fr-FR",
|
"code": "he-IL",
|
||||||
"nativeName": "français",
|
"nativeName": "עברית",
|
||||||
"name": "French",
|
"name": "Hebrew",
|
||||||
"completion": 97
|
"completion": 12
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"code": "ko-KR",
|
"code": "it-IT",
|
||||||
"nativeName": "한국어",
|
"nativeName": "italiano",
|
||||||
"name": "Korean",
|
"name": "Italian",
|
||||||
"completion": 75
|
"completion": 34
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"code": "cs-CZ",
|
"code": "ja-JP",
|
||||||
"nativeName": "čeština",
|
"nativeName": "日本語",
|
||||||
"name": "Czech",
|
"name": "Japanese",
|
||||||
"completion": 72
|
"completion": 31
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"code": "kab",
|
"code": "kab",
|
||||||
"nativeName": "Taqbaylit",
|
"nativeName": "Taqbaylit",
|
||||||
"name": "Kabyle",
|
"name": "Kabyle",
|
||||||
"completion": 67
|
"completion": 100
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"code": "fa-IR",
|
"code": "ko-KR",
|
||||||
"nativeName": "فارسی",
|
"nativeName": "한국어",
|
||||||
"name": "Persian",
|
"name": "Korean",
|
||||||
"completion": 62
|
"completion": 86
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "lt-LT",
|
||||||
|
"nativeName": "lietuvių",
|
||||||
|
"name": "Lithuanian",
|
||||||
|
"completion": 43
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"code": "nl-NL",
|
"code": "nl-NL",
|
||||||
|
@ -78,46 +108,28 @@
|
||||||
"completion": 48
|
"completion": 48
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"code": "ja-JP",
|
"code": "pl-PL",
|
||||||
"nativeName": "日本語",
|
"nativeName": "polski",
|
||||||
"name": "Japanese",
|
"name": "Polish",
|
||||||
"completion": 32
|
"completion": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"code": "lt-LT",
|
"code": "pt-BR",
|
||||||
"nativeName": "lietuvių",
|
"nativeName": "português",
|
||||||
"name": "Lithuanian",
|
"name": "Portuguese",
|
||||||
"completion": 28
|
"completion": 100
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "pt-PT",
|
||||||
|
"nativeName": "português",
|
||||||
|
"name": "Portuguese",
|
||||||
|
"completion": 100
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"code": "ru-RU",
|
"code": "ru-RU",
|
||||||
"nativeName": "русский",
|
"nativeName": "русский",
|
||||||
"name": "Russian",
|
"name": "Russian",
|
||||||
"completion": 23
|
"completion": 100
|
||||||
},
|
|
||||||
{
|
|
||||||
"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
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"code": "th-TH",
|
"code": "th-TH",
|
||||||
|
@ -125,10 +137,22 @@
|
||||||
"name": "Thai",
|
"name": "Thai",
|
||||||
"completion": 3
|
"completion": 3
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"code": "uk-UA",
|
||||||
|
"nativeName": "українська",
|
||||||
|
"name": "Ukrainian",
|
||||||
|
"completion": 26
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "zh-CN",
|
||||||
|
"nativeName": "简体中文",
|
||||||
|
"name": "Simplified Chinese",
|
||||||
|
"completion": 100
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"code": "zh-TW",
|
"code": "zh-TW",
|
||||||
"nativeName": "繁體中文",
|
"nativeName": "繁體中文",
|
||||||
"name": "Traditional Chinese",
|
"name": "Traditional Chinese",
|
||||||
"completion": 3
|
"completion": 14
|
||||||
}
|
}
|
||||||
]
|
]
|
|
@ -84,6 +84,7 @@
|
||||||
var(--text-color) 60%
|
var(--text-color) 60%
|
||||||
);
|
);
|
||||||
--outline-color: rgba(128, 128, 128, 0.2);
|
--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);
|
--outline-hover-color: rgba(128, 128, 128, 0.7);
|
||||||
--divider-color: rgba(0, 0, 0, 0.1);
|
--divider-color: rgba(0, 0, 0, 0.1);
|
||||||
--backdrop-color: rgba(0, 0, 0, 0.1);
|
--backdrop-color: rgba(0, 0, 0, 0.1);
|
||||||
|
@ -149,6 +150,11 @@
|
||||||
mediumslateblue 70%,
|
mediumslateblue 70%,
|
||||||
var(--text-color) 30%
|
var(--text-color) 30%
|
||||||
);
|
);
|
||||||
|
--button-bg-color: color-mix(
|
||||||
|
in srgb,
|
||||||
|
var(--blue-color) 80%,
|
||||||
|
var(--bg-color) 20%
|
||||||
|
);
|
||||||
--reblog-faded-color: #b190f141;
|
--reblog-faded-color: #b190f141;
|
||||||
--reply-to-text-color: var(--reply-to-color);
|
--reply-to-text-color: var(--reply-to-color);
|
||||||
--reply-to-faded-color: #ffa60017;
|
--reply-to-faded-color: #ffa60017;
|
||||||
|
|
|
@ -12,7 +12,15 @@ const locales = [
|
||||||
.filter(({ completion }) => completion >= PERCENTAGE_THRESHOLD)
|
.filter(({ completion }) => completion >= PERCENTAGE_THRESHOLD)
|
||||||
.map(({ code }) => code),
|
.map(({ code }) => code),
|
||||||
];
|
];
|
||||||
if (import.meta.env.DEV) {
|
|
||||||
locales.push('pseudo-LOCALE');
|
|
||||||
}
|
|
||||||
export const LOCALES = locales;
|
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];
|
||||||
|
|
1045
src/locales/ar-SA.po
1045
src/locales/ar-SA.po
File diff suppressed because it is too large
Load diff
1018
src/locales/ca-ES.po
1018
src/locales/ca-ES.po
File diff suppressed because it is too large
Load diff
1143
src/locales/cs-CZ.po
1143
src/locales/cs-CZ.po
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
1030
src/locales/en.po
1030
src/locales/en.po
File diff suppressed because it is too large
Load diff
1263
src/locales/eo-UY.po
1263
src/locales/eo-UY.po
File diff suppressed because it is too large
Load diff
1147
src/locales/es-ES.po
1147
src/locales/es-ES.po
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
1069
src/locales/fa-IR.po
1069
src/locales/fa-IR.po
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
1011
src/locales/fr-FR.po
1011
src/locales/fr-FR.po
File diff suppressed because it is too large
Load diff
1027
src/locales/gl-ES.po
1027
src/locales/gl-ES.po
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
1215
src/locales/it-IT.po
1215
src/locales/it-IT.po
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
1357
src/locales/kab.po
1357
src/locales/kab.po
File diff suppressed because it is too large
Load diff
1229
src/locales/ko-KR.po
1229
src/locales/ko-KR.po
File diff suppressed because it is too large
Load diff
1081
src/locales/lt-LT.po
1081
src/locales/lt-LT.po
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
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
3733
src/locales/pt-BR.po
Normal file
File diff suppressed because it is too large
Load diff
2710
src/locales/pt-PT.po
2710
src/locales/pt-PT.po
File diff suppressed because it is too large
Load diff
2341
src/locales/ru-RU.po
2341
src/locales/ru-RU.po
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
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
1161
src/locales/zh-TW.po
1161
src/locales/zh-TW.po
File diff suppressed because it is too large
Load diff
|
@ -9,6 +9,7 @@ import Avatar from '../components/avatar';
|
||||||
import Icon from '../components/icon';
|
import Icon from '../components/icon';
|
||||||
import Link from '../components/link';
|
import Link from '../components/link';
|
||||||
import MenuConfirm from '../components/menu-confirm';
|
import MenuConfirm from '../components/menu-confirm';
|
||||||
|
import MenuLink from '../components/menu-link';
|
||||||
import Menu2 from '../components/menu2';
|
import Menu2 from '../components/menu2';
|
||||||
import NameText from '../components/name-text';
|
import NameText from '../components/name-text';
|
||||||
import { api } from '../utils/api';
|
import { api } from '../utils/api';
|
||||||
|
@ -16,6 +17,8 @@ import states from '../utils/states';
|
||||||
import store from '../utils/store';
|
import store from '../utils/store';
|
||||||
import { getCurrentAccountID, setCurrentAccountID } from '../utils/store-utils';
|
import { getCurrentAccountID, setCurrentAccountID } from '../utils/store-utils';
|
||||||
|
|
||||||
|
const isStandalone = window.matchMedia('(display-mode: standalone)').matches;
|
||||||
|
|
||||||
function Accounts({ onClose }) {
|
function Accounts({ onClose }) {
|
||||||
const { masto } = api();
|
const { masto } = api();
|
||||||
// Accounts
|
// Accounts
|
||||||
|
@ -107,6 +110,32 @@ function Accounts({ onClose }) {
|
||||||
</button>
|
</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
|
<MenuItem
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
states.showAccount = `${account.info.username}@${account.instanceURL}`;
|
states.showAccount = `${account.info.username}@${account.instanceURL}`;
|
||||||
|
|
|
@ -22,7 +22,7 @@ function Bookmarks() {
|
||||||
<Timeline
|
<Timeline
|
||||||
title={t`Bookmarks`}
|
title={t`Bookmarks`}
|
||||||
id="bookmarks"
|
id="bookmarks"
|
||||||
emptyText={`No bookmarks yet. Go bookmark something!`}
|
emptyText={t`No bookmarks yet. Go bookmark something!`}
|
||||||
errorText={t`Unable to load bookmarks.`}
|
errorText={t`Unable to load bookmarks.`}
|
||||||
instance={instance}
|
instance={instance}
|
||||||
fetchItems={fetchBookmarks}
|
fetchItems={fetchBookmarks}
|
||||||
|
|
|
@ -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 (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={(node) => {
|
ref={(node) => {
|
||||||
|
@ -883,7 +899,7 @@ function Catchup() {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<main>
|
<main onKeyDown={handleArrowKeys}>
|
||||||
{uiState === 'start' && (
|
{uiState === 'start' && (
|
||||||
<div class="catchup-start">
|
<div class="catchup-start">
|
||||||
<h1>
|
<h1>
|
||||||
|
|
|
@ -22,7 +22,7 @@ function Favourites() {
|
||||||
<Timeline
|
<Timeline
|
||||||
title={t`Likes`}
|
title={t`Likes`}
|
||||||
id="favourites"
|
id="favourites"
|
||||||
emptyText={`No likes yet. Go like something!`}
|
emptyText={t`No likes yet. Go like something!`}
|
||||||
errorText={t`Unable to load likes.`}
|
errorText={t`Unable to load likes.`}
|
||||||
instance={instance}
|
instance={instance}
|
||||||
fetchItems={fetchFavourites}
|
fetchItems={fetchFavourites}
|
||||||
|
|
|
@ -205,7 +205,7 @@ function Hashtags({ media: mediaView, columnMode, ...props }) {
|
||||||
<MenuConfirm
|
<MenuConfirm
|
||||||
subMenu
|
subMenu
|
||||||
confirm={info.following}
|
confirm={info.following}
|
||||||
confirmLabel={`Unfollow #${hashtag}?`}
|
confirmLabel={t`Unfollow #${hashtag}?`}
|
||||||
disabled={followUIState === 'loading' || !authenticated}
|
disabled={followUIState === 'loading' || !authenticated}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setFollowUIState('loading');
|
setFollowUIState('loading');
|
||||||
|
|
|
@ -11,7 +11,12 @@ import LangSelector from '../components/lang-selector';
|
||||||
import Link from '../components/link';
|
import Link from '../components/link';
|
||||||
import Loader from '../components/loader';
|
import Loader from '../components/loader';
|
||||||
import instancesListURL from '../data/instances.json?url';
|
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 store from '../utils/store';
|
||||||
import useTitle from '../utils/useTitle';
|
import useTitle from '../utils/useTitle';
|
||||||
|
|
||||||
|
@ -63,17 +68,36 @@ function Login() {
|
||||||
instanceURL,
|
instanceURL,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const authPKCE = await supportsPKCE({ instanceURL });
|
||||||
|
console.log({ authPKCE });
|
||||||
|
if (authPKCE) {
|
||||||
if (client_id && client_secret) {
|
if (client_id && client_secret) {
|
||||||
store.session.set('clientID', client_id);
|
store.sessionCookie.set('clientID', client_id);
|
||||||
store.session.set('clientSecret', client_secret);
|
store.sessionCookie.set('clientSecret', client_secret);
|
||||||
store.session.set('vapidKey', vapid_key);
|
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({
|
location.href = await getAuthorizationURL({
|
||||||
instanceURL,
|
instanceURL,
|
||||||
client_id,
|
client_id,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
alert('Failed to register application');
|
alert(t`Failed to register application`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
setUIState('default');
|
setUIState('default');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
@ -158,7 +182,7 @@ function Login() {
|
||||||
autocapitalize="off"
|
autocapitalize="off"
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
spellCheck={false}
|
spellCheck={false}
|
||||||
placeholder={`instance domain`}
|
placeholder={t`instance domain`}
|
||||||
onInput={(e) => {
|
onInput={(e) => {
|
||||||
setInstanceText(e.target.value);
|
setInstanceText(e.target.value);
|
||||||
}}
|
}}
|
||||||
|
|
|
@ -232,9 +232,21 @@ function Settings({ onClose }) {
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
|
<span>
|
||||||
<label>
|
<label>
|
||||||
<Trans>Display language</Trans>
|
<Trans>Display language</Trans>
|
||||||
</label>
|
</label>
|
||||||
|
<br />
|
||||||
|
<small>
|
||||||
|
<a
|
||||||
|
href="https://crowdin.com/project/phanpy"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
<Trans>Volunteer translations</Trans>
|
||||||
|
</a>
|
||||||
|
</small>
|
||||||
|
</span>
|
||||||
<LangSelector />
|
<LangSelector />
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
|
@ -1,12 +1,32 @@
|
||||||
const { PHANPY_CLIENT_NAME: CLIENT_NAME, PHANPY_WEBSITE: WEBSITE } = import.meta
|
import { generateCodeChallenge, verifier } from './oauth-pkce';
|
||||||
.env;
|
|
||||||
|
const {
|
||||||
|
DEV,
|
||||||
|
PHANPY_CLIENT_NAME: CLIENT_NAME,
|
||||||
|
PHANPY_WEBSITE: WEBSITE,
|
||||||
|
} = import.meta.env;
|
||||||
|
|
||||||
const SCOPES = 'read write follow push';
|
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 }) {
|
export async function registerApplication({ instanceURL }) {
|
||||||
const registrationParams = new URLSearchParams({
|
const registrationParams = new URLSearchParams({
|
||||||
client_name: CLIENT_NAME,
|
client_name: CLIENT_NAME,
|
||||||
redirect_uris: location.origin + location.pathname,
|
redirect_uris: REDIRECT_URI,
|
||||||
scopes: SCOPES,
|
scopes: SCOPES,
|
||||||
website: WEBSITE,
|
website: WEBSITE,
|
||||||
});
|
});
|
||||||
|
@ -25,11 +45,26 @@ export async function registerApplication({ instanceURL }) {
|
||||||
return registrationJSON;
|
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 }) {
|
export async function getAuthorizationURL({ instanceURL, client_id }) {
|
||||||
const authorizationParams = new URLSearchParams({
|
const authorizationParams = new URLSearchParams({
|
||||||
client_id,
|
client_id,
|
||||||
scope: SCOPES,
|
scope: SCOPES,
|
||||||
redirect_uri: location.origin + location.pathname,
|
redirect_uri: REDIRECT_URI,
|
||||||
// redirect_uri: 'urn:ietf:wg:oauth:2.0:oob',
|
// redirect_uri: 'urn:ietf:wg:oauth:2.0:oob',
|
||||||
response_type: 'code',
|
response_type: 'code',
|
||||||
});
|
});
|
||||||
|
@ -42,15 +77,23 @@ export async function getAccessToken({
|
||||||
client_id,
|
client_id,
|
||||||
client_secret,
|
client_secret,
|
||||||
code,
|
code,
|
||||||
|
code_verifier,
|
||||||
}) {
|
}) {
|
||||||
const params = new URLSearchParams({
|
const params = new URLSearchParams({
|
||||||
client_id,
|
client_id,
|
||||||
client_secret,
|
redirect_uri: REDIRECT_URI,
|
||||||
redirect_uri: location.origin + location.pathname,
|
|
||||||
grant_type: 'authorization_code',
|
grant_type: 'authorization_code',
|
||||||
code,
|
code,
|
||||||
scope: SCOPES,
|
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`, {
|
const tokenResponse = await fetch(`https://${instanceURL}/oauth/token`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
|
|
|
@ -5,6 +5,7 @@ const statusPostRegexes = [
|
||||||
/^\/@[^@\/]+\/(?:statuses|posts)\/([^\/]+)/i, // GoToSocial, Takahe
|
/^\/@[^@\/]+\/(?:statuses|posts)\/([^\/]+)/i, // GoToSocial, Takahe
|
||||||
/\/notes\/([^\/]+)/i, // Misskey, Firefish
|
/\/notes\/([^\/]+)/i, // Misskey, Firefish
|
||||||
/^\/(?:notice|objects)\/([a-z0-9-]+)/i, // Pleroma
|
/^\/(?:notice|objects)\/([a-z0-9-]+)/i, // Pleroma
|
||||||
|
/\/@[^@\/]+\/post\/([^\/]+)/i, // Threads
|
||||||
/\/@[^@\/]+@?[^\/]+?\/([^\/]+)/i, // Mastodon
|
/\/@[^@\/]+@?[^\/]+?\/([^\/]+)/i, // Mastodon
|
||||||
/^\/p\/[^\/]+\/([^\/]+)/i, // Pixelfed
|
/^\/p\/[^\/]+\/([^\/]+)/i, // Pixelfed
|
||||||
];
|
];
|
||||||
|
|
|
@ -6,7 +6,7 @@ export default function isMastodonLinkMaybe(url) {
|
||||||
/^\/(@[^/]+|users\/[^/]+)\/(statuses|posts)\/\w+\/?$/i.test(pathname) || // GoToSocial, Takahe
|
/^\/(@[^/]+|users\/[^/]+)\/(statuses|posts)\/\w+\/?$/i.test(pathname) || // GoToSocial, Takahe
|
||||||
/^\/notes\/[a-z0-9]+$/i.test(pathname) || // Misskey, Firefish
|
/^\/notes\/[a-z0-9]+$/i.test(pathname) || // Misskey, Firefish
|
||||||
/^\/(notice|objects)\/[a-z0-9-]+$/i.test(pathname) || // Pleroma
|
/^\/(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
|
/^\/@[^/]+\/[a-z0-9]+[a-z0-9\-]+[a-z0-9]+$/i.test(pathname) || // Hollo
|
||||||
(hostname === 'fed.brid.gy' && pathname.startsWith('/r/http')) || // Bridgy Fed
|
(hostname === 'fed.brid.gy' && pathname.startsWith('/r/http')) || // Bridgy Fed
|
||||||
/#\/[^\/]+\.[^\/]+\/s\/.+/i.test(hash) // Phanpy 🫣
|
/#\/[^\/]+\.[^\/]+\/s\/.+/i.test(hash) // Phanpy 🫣
|
||||||
|
|
|
@ -7,7 +7,7 @@ import {
|
||||||
} from '@lingui/detect-locale';
|
} from '@lingui/detect-locale';
|
||||||
import Locale from 'intl-locale-textinfo-polyfill';
|
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 { messages } from '../locales/en.po';
|
||||||
import localeMatch from '../utils/locale-match';
|
import localeMatch from '../utils/locale-match';
|
||||||
|
|
||||||
|
@ -62,7 +62,7 @@ export function initActivateLang() {
|
||||||
DEFAULT_LANG,
|
DEFAULT_LANG,
|
||||||
);
|
);
|
||||||
const matchedLang =
|
const matchedLang =
|
||||||
LOCALES.find((l) => l === lang) || localeMatch(lang, LOCALES);
|
ALL_LOCALES.find((l) => l === lang) || localeMatch(lang, ALL_LOCALES);
|
||||||
activateLang(matchedLang);
|
activateLang(matchedLang);
|
||||||
|
|
||||||
// const yes = confirm(t`Reload to apply language setting?`);
|
// const yes = confirm(t`Reload to apply language setting?`);
|
||||||
|
|
|
@ -1,12 +1,14 @@
|
||||||
import { i18n } from '@lingui/core';
|
import { i18n } from '@lingui/core';
|
||||||
|
|
||||||
|
import localeMatch from './locale-match';
|
||||||
import mem from './mem';
|
import mem from './mem';
|
||||||
|
|
||||||
const defaultLocale = new Intl.DateTimeFormat().resolvedOptions().locale;
|
const defaultLocale = new Intl.DateTimeFormat().resolvedOptions().locale;
|
||||||
|
|
||||||
const _DateTimeFormat = (opts) => {
|
const _DateTimeFormat = (opts) => {
|
||||||
const { locale, dateYear, hideTime, formatOpts } = 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 currentYear = new Date().getFullYear();
|
||||||
const options = {
|
const options = {
|
||||||
// Show year if not current year
|
// Show year if not current year
|
||||||
|
@ -20,9 +22,11 @@ const _DateTimeFormat = (opts) => {
|
||||||
};
|
};
|
||||||
try {
|
try {
|
||||||
return Intl.DateTimeFormat(loc, options);
|
return Intl.DateTimeFormat(loc, options);
|
||||||
} catch (e) {
|
} catch (e) {}
|
||||||
|
try {
|
||||||
|
return Intl.DateTimeFormat(locale, options);
|
||||||
|
} catch (e) {}
|
||||||
return Intl.DateTimeFormat(undefined, options);
|
return Intl.DateTimeFormat(undefined, options);
|
||||||
}
|
|
||||||
};
|
};
|
||||||
const DateTimeFormat = mem(_DateTimeFormat);
|
const DateTimeFormat = mem(_DateTimeFormat);
|
||||||
|
|
||||||
|
|
46
src/utils/oauth-pkce.js
Normal file
46
src/utils/oauth-pkce.js
Normal 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;
|
|
@ -1,6 +1,6 @@
|
||||||
// Utils for push notifications
|
// Utils for push notifications
|
||||||
import { api } from './api';
|
import { api } from './api';
|
||||||
import { getCurrentAccount } from './store-utils';
|
import { getVapidKey } from './store-utils';
|
||||||
|
|
||||||
// Subscription is an object with the following structure:
|
// Subscription is an object with the following structure:
|
||||||
// {
|
// {
|
||||||
|
@ -113,7 +113,7 @@ export async function initSubscription() {
|
||||||
// Check if the subscription changed
|
// Check if the subscription changed
|
||||||
if (backendSubscription && subscription) {
|
if (backendSubscription && subscription) {
|
||||||
const sameEndpoint = backendSubscription.endpoint === subscription.endpoint;
|
const sameEndpoint = backendSubscription.endpoint === subscription.endpoint;
|
||||||
const { vapidKey } = getCurrentAccount();
|
const vapidKey = getVapidKey();
|
||||||
const sameKey = backendSubscription.serverKey === vapidKey;
|
const sameKey = backendSubscription.serverKey === vapidKey;
|
||||||
if (!sameEndpoint) {
|
if (!sameEndpoint) {
|
||||||
throw new Error('Backend subscription endpoint changed');
|
throw new Error('Backend subscription endpoint changed');
|
||||||
|
@ -146,7 +146,7 @@ export async function initSubscription() {
|
||||||
|
|
||||||
if (subscription && !backendSubscription) {
|
if (subscription && !backendSubscription) {
|
||||||
// check if account's vapidKey is same as subscription's applicationServerKey
|
// check if account's vapidKey is same as subscription's applicationServerKey
|
||||||
const { vapidKey } = getCurrentAccount();
|
const vapidKey = getVapidKey();
|
||||||
if (vapidKey) {
|
if (vapidKey) {
|
||||||
const { applicationServerKey } = subscription.options;
|
const { applicationServerKey } = subscription.options;
|
||||||
const vapidKeyStr = urlBase64ToUint8Array(vapidKey).toString();
|
const vapidKeyStr = urlBase64ToUint8Array(vapidKey).toString();
|
||||||
|
@ -210,7 +210,7 @@ export async function updateSubscription({ data, policy }) {
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// User is not subscribed
|
// User is not subscribed
|
||||||
const { vapidKey } = getCurrentAccount();
|
const vapidKey = getVapidKey();
|
||||||
if (!vapidKey) throw new Error('No server key found');
|
if (!vapidKey) throw new Error('No server key found');
|
||||||
subscription = await registration.pushManager.subscribe({
|
subscription = await registration.pushManager.subscribe({
|
||||||
userVisibleOnly: true,
|
userVisibleOnly: true,
|
||||||
|
|
|
@ -154,6 +154,13 @@ export function getCurrentInstanceConfiguration() {
|
||||||
return getInstanceConfiguration(instance);
|
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() {
|
export function isMediaFirstInstance() {
|
||||||
const instance = getCurrentInstance();
|
const instance = getCurrentInstance();
|
||||||
return /pixelfed/i.test(instance?.version);
|
return /pixelfed/i.test(instance?.version);
|
||||||
|
|
|
@ -1,5 +1,9 @@
|
||||||
|
import Cookies from 'js-cookie';
|
||||||
|
|
||||||
import { getCurrentAccountNS } from './store-utils';
|
import { getCurrentAccountNS } from './store-utils';
|
||||||
|
|
||||||
|
const cookies = Cookies.withAttributes({ sameSite: 'strict', secure: true });
|
||||||
|
|
||||||
const local = {
|
const local = {
|
||||||
get: (key) => {
|
get: (key) => {
|
||||||
try {
|
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
|
// Store with account namespace (id@domain.tld) <- uses id, not username
|
||||||
const account = {
|
const account = {
|
||||||
get: (key) => {
|
get: (key) => {
|
||||||
|
@ -118,4 +154,4 @@ const account = {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default { local, session, account };
|
export default { local, session, sessionCookie, cookie, account };
|
||||||
|
|
|
@ -12,9 +12,12 @@ import { VitePWA } from 'vite-plugin-pwa';
|
||||||
import removeConsole from 'vite-plugin-remove-console';
|
import removeConsole from 'vite-plugin-remove-console';
|
||||||
import { run } from 'vite-plugin-run';
|
import { run } from 'vite-plugin-run';
|
||||||
|
|
||||||
|
import { ALL_LOCALES } from './src/locales';
|
||||||
|
|
||||||
const allowedEnvPrefixes = ['VITE_', 'PHANPY_'];
|
const allowedEnvPrefixes = ['VITE_', 'PHANPY_'];
|
||||||
const { NODE_ENV } = process.env;
|
const { NODE_ENV } = process.env;
|
||||||
const {
|
const {
|
||||||
|
PHANPY_WEBSITE: WEBSITE,
|
||||||
PHANPY_CLIENT_NAME: CLIENT_NAME,
|
PHANPY_CLIENT_NAME: CLIENT_NAME,
|
||||||
PHANPY_APP_ERROR_LOGGING: ERROR_LOGGING,
|
PHANPY_APP_ERROR_LOGGING: ERROR_LOGGING,
|
||||||
} = loadEnv('production', process.cwd(), allowedEnvPrefixes);
|
} = loadEnv('production', process.cwd(), allowedEnvPrefixes);
|
||||||
|
@ -70,6 +73,11 @@ export default defineConfig({
|
||||||
run: ['npm', 'run', 'messages:extract:clean'],
|
run: ['npm', 'run', 'messages:extract:clean'],
|
||||||
pattern: 'src/**/*.{js,jsx,ts,tsx}',
|
pattern: 'src/**/*.{js,jsx,ts,tsx}',
|
||||||
},
|
},
|
||||||
|
// {
|
||||||
|
// name: 'update-catalogs',
|
||||||
|
// run: ['node', 'scripts/catalogs.js'],
|
||||||
|
// pattern: 'src/locales/*.po',
|
||||||
|
// },
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
splitVendorChunkPlugin(),
|
splitVendorChunkPlugin(),
|
||||||
|
@ -78,6 +86,20 @@ export default defineConfig({
|
||||||
}),
|
}),
|
||||||
htmlPlugin({
|
htmlPlugin({
|
||||||
headScripts: ERROR_LOGGING ? [rollbarCode] : [],
|
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([
|
generateFile([
|
||||||
{
|
{
|
||||||
|
|
Loading…
Reference in a new issue