diff --git a/.github/workflows/i18n-update-readme.yml b/.github/workflows/i18n-update-readme.yml
new file mode 100644
index 00000000..78b50c69
--- /dev/null
+++ b/.github/workflows/i18n-update-readme.yml
@@ -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 }}
diff --git a/README.md b/README.md
index 7cbf9988..18529e68 100644
--- a/README.md
+++ b/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)
+### Translation volunteers
+
+
+- alidsds11 (Arabic)
+- BoFFire (Arabic, French, Kabyle)
+- Brawaru (Russian)
+- cbasje (Dutch)
+- cbo92 (French)
+- CDN (Chinese Simplified)
+- dannypsnl (Chinese Traditional)
+- databio (Catalan)
+- drydenwu (Chinese Traditional)
+- elissarc (French)
+- ElPamplina (Spanish)
+- Fitik (Esperanto, Hebrew)
+- Freeesia (Japanese)
+- ghose (Galician)
+- hongminhee (Korean)
+- isard (Catalan)
+- karlafej (Czech)
+- katullo11 (Italian)
+- Kytta (German)
+- llun (Thai)
+- lucasofchirst (Occitan, Portuguese, Portuguese, Brazilian)
+- marcin.kozinski (Polish)
+- mojosoeun (Korean)
+- moreal (Korean)
+- MrWillCom (Chinese Simplified)
+- nclm (French)
+- pazpi (Italian)
+- punkrockgirl (Basque)
+- radecos (French)
+- Razem (Czech)
+- realpixelcode (German)
+- rezahosseinzadeh (Persian)
+- rwmpelstilzchen (Esperanto, Hebrew)
+- SadmL (Russian)
+- Sky_NiniKo (French)
+- Su5hicz (Czech)
+- Talos00 (Italian)
+- tferrermo (Spanish)
+- tux93 (German)
+- Urbestro (Esperanto, Spanish)
+- UsualUsername (Russian)
+- Vac31. (Lithuanian)
+- valtlai (Finnish)
+- xabi_itzultzaile (Basque)
+- xen4n (Ukrainian)
+- xqueralt (Catalan)
+- ZiriSut (Kabyle)
+- zkreml (Czech)
+
+
## 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.
diff --git a/i18n-volunteers.json b/i18n-volunteers.json
new file mode 100644
index 00000000..4c9aa6a5
--- /dev/null
+++ b/i18n-volunteers.json
@@ -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"
+ ]
+ }
+]
\ No newline at end of file
diff --git a/package.json b/package.json
index 577e19ed..290d69c9 100644
--- a/package.json
+++ b/package.json
@@ -11,7 +11,9 @@
"bundle-visualizer": "npx vite-bundle-visualizer",
"messages:extract": "lingui extract",
"messages:extract:clean": "lingui extract --locale en --clean",
- "messages:compile": "lingui compile"
+ "messages:compile": "lingui compile",
+ "fetch-i18n-volunteers": "env $(cat .env.local | grep -v \"#\" | xargs) node scripts/fetch-i18n-volunteers.js",
+ "readme:i18n-volunteers": "node scripts/update-i18n-volunteers-readme.js"
},
"dependencies": {
"@formatjs/intl-localematcher": "~0.5.4",
diff --git a/scripts/fetch-i18n-volunteers.js b/scripts/fetch-i18n-volunteers.js
new file mode 100644
index 00000000..b0e4132e
--- /dev/null
+++ b/scripts/fetch-i18n-volunteers.js
@@ -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');
+}
diff --git a/scripts/update-i18n-volunteers-readme.js b/scripts/update-i18n-volunteers-readme.js
new file mode 100644
index 00000000..571c8677
--- /dev/null
+++ b/scripts/update-i18n-volunteers-readme.js
@@ -0,0 +1,27 @@
+// Find for 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 = '';
+const i18nVolunteersEnd = '';
+
+const i18nVolunteersList = i18nVolunteers
+ .map((member) => {
+ return `- ${
+ 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');