A bunch of vocab, and (working in dev) page for adding vocab
All checks were successful
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 4s
All checks were successful
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 4s
This commit is contained in:
parent
15f7d2e679
commit
d302c19265
@ -1 +0,0 @@
|
||||
/nix/store/ds5hg5lwbpkp0pmwvq1r1khdj9dhgr68-source
|
||||
@ -1 +0,0 @@
|
||||
/nix/store/piq8ffvrjw88nxj69ib64pfqg7vj19mp-source
|
||||
@ -1 +1 @@
|
||||
/nix/store/z7x7krys4bv786dsqsb015ancd5glyab-nix-shell-env
|
||||
/nix/store/2l63479bllklr4bzi4vscn7ndj6lg614-nix-shell-env
|
||||
@ -15,7 +15,7 @@ export CONFIG_SHELL
|
||||
CXX='g++'
|
||||
export CXX
|
||||
HOSTTYPE='x86_64'
|
||||
HOST_PATH='/nix/store/ddx7976jyll30xjbasghv9jailswprcp-bash-interactive-5.3p3/bin:/nix/store/q1zaii9cirbfpmwr7d86hpppql3kjcpf-git-2.51.0/bin:/nix/store/a99hiwhamgzds70gxkfnb4cm8i926356-nodejs-22.19.0-dev/bin:/nix/store/r4557ald6zn4dzmvgh8na9vwnwzgrjgc-nodejs-22.19.0/bin:/nix/store/967gn7p1p47ic924r2fx4rgbfp49fhsy-pnpm-10.15.1/bin:/nix/store/l8m7mbvqxdi9bd5apl8s49kjpnzrcv6c-postgresql-17.6-dev/bin:/nix/store/jq2kbdw6ljv9i47jz23pm072cfyxwpfj-postgresql-17.6/bin:/nix/store/n4lyyqirbz2j0igs12m2pyqrs7zyyvld-netlify-cli-19.0.2/bin:/nix/store/ks5kxqrg113jkv9bsvhgpavrq1z1ks4g-inotify-tools-4.23.9.0/bin:/nix/store/8ksax0a2mxglr5hlkj2dzl556jx7xqn5-coreutils-9.7/bin:/nix/store/l964krgbp613d5jxga2vy5qdssj7zfzj-findutils-4.10.0/bin:/nix/store/s2fvny566vls74p4qm9v3fdqd741fh3f-diffutils-3.12/bin:/nix/store/pmhkmqy0vxk47r6ndh0azybhf6gs6k25-gnused-4.9/bin:/nix/store/vlckk0vnmawq9wwh7ndkrwxlpv4h29yh-gnugrep-3.12/bin:/nix/store/03nvbw411p097h6yxjghc33rbcrjfb9d-gawk-5.3.2/bin:/nix/store/8av8pfs7bnyc6hqj764ns4z1fnr9bva1-gnutar-1.35/bin:/nix/store/8gsxxh82rf957ffbsk0q9670nhvl5lia-gzip-1.14/bin:/nix/store/6yjb3zdj448rm8qsmpiq3f67kvj5683a-bzip2-1.0.8-bin/bin:/nix/store/aqdvlkh0jdwkc22hh5vr9sl6qlw5ha74-gnumake-4.4.1/bin:/nix/store/q7sqwn7i6w2b67adw0bmix29pxg85x3w-bash-5.3p3/bin:/nix/store/856i1ajaci3kmmp15rifacfz3jvn5l3q-patch-2.8/bin:/nix/store/y9kgzp85ykrhd7l691w4djx121qygy68-xz-5.8.1-bin/bin:/nix/store/v40ijzz8p2fpk9ihjck3a1ncqaqfmn3c-file-5.45/bin'
|
||||
HOST_PATH='/nix/store/ddx7976jyll30xjbasghv9jailswprcp-bash-interactive-5.3p3/bin:/nix/store/q1zaii9cirbfpmwr7d86hpppql3kjcpf-git-2.51.0/bin:/nix/store/a99hiwhamgzds70gxkfnb4cm8i926356-nodejs-22.19.0-dev/bin:/nix/store/r4557ald6zn4dzmvgh8na9vwnwzgrjgc-nodejs-22.19.0/bin:/nix/store/967gn7p1p47ic924r2fx4rgbfp49fhsy-pnpm-10.15.1/bin:/nix/store/l8m7mbvqxdi9bd5apl8s49kjpnzrcv6c-postgresql-17.6-dev/bin:/nix/store/jq2kbdw6ljv9i47jz23pm072cfyxwpfj-postgresql-17.6/bin:/nix/store/ks5kxqrg113jkv9bsvhgpavrq1z1ks4g-inotify-tools-4.23.9.0/bin:/nix/store/1p5n2mzy33ayzc1scdnz82h53d192knh-claude-code-1.0.117/bin:/nix/store/8ksax0a2mxglr5hlkj2dzl556jx7xqn5-coreutils-9.7/bin:/nix/store/l964krgbp613d5jxga2vy5qdssj7zfzj-findutils-4.10.0/bin:/nix/store/s2fvny566vls74p4qm9v3fdqd741fh3f-diffutils-3.12/bin:/nix/store/pmhkmqy0vxk47r6ndh0azybhf6gs6k25-gnused-4.9/bin:/nix/store/vlckk0vnmawq9wwh7ndkrwxlpv4h29yh-gnugrep-3.12/bin:/nix/store/03nvbw411p097h6yxjghc33rbcrjfb9d-gawk-5.3.2/bin:/nix/store/8av8pfs7bnyc6hqj764ns4z1fnr9bva1-gnutar-1.35/bin:/nix/store/8gsxxh82rf957ffbsk0q9670nhvl5lia-gzip-1.14/bin:/nix/store/6yjb3zdj448rm8qsmpiq3f67kvj5683a-bzip2-1.0.8-bin/bin:/nix/store/aqdvlkh0jdwkc22hh5vr9sl6qlw5ha74-gnumake-4.4.1/bin:/nix/store/q7sqwn7i6w2b67adw0bmix29pxg85x3w-bash-5.3p3/bin:/nix/store/856i1ajaci3kmmp15rifacfz3jvn5l3q-patch-2.8/bin:/nix/store/y9kgzp85ykrhd7l691w4djx121qygy68-xz-5.8.1-bin/bin:/nix/store/v40ijzz8p2fpk9ihjck3a1ncqaqfmn3c-file-5.45/bin'
|
||||
export HOST_PATH
|
||||
IFS='
|
||||
'
|
||||
@ -37,7 +37,7 @@ NIX_CC='/nix/store/95k9rsn1zsw1yvir8mj824ldhf90i4qw-gcc-wrapper-14.3.0'
|
||||
export NIX_CC
|
||||
NIX_CC_WRAPPER_TARGET_HOST_x86_64_unknown_linux_gnu='1'
|
||||
export NIX_CC_WRAPPER_TARGET_HOST_x86_64_unknown_linux_gnu
|
||||
NIX_CFLAGS_COMPILE=' -frandom-seed=z7x7krys4b -isystem /nix/store/7zwa3r9agcyzf21d0792fvhrsl6gajiy-bash-interactive-5.3p3-dev/include -isystem /nix/store/a99hiwhamgzds70gxkfnb4cm8i926356-nodejs-22.19.0-dev/include -isystem /nix/store/r4557ald6zn4dzmvgh8na9vwnwzgrjgc-nodejs-22.19.0/include -isystem /nix/store/l8m7mbvqxdi9bd5apl8s49kjpnzrcv6c-postgresql-17.6-dev/include -isystem /nix/store/ks5kxqrg113jkv9bsvhgpavrq1z1ks4g-inotify-tools-4.23.9.0/include -isystem /nix/store/7zwa3r9agcyzf21d0792fvhrsl6gajiy-bash-interactive-5.3p3-dev/include -isystem /nix/store/a99hiwhamgzds70gxkfnb4cm8i926356-nodejs-22.19.0-dev/include -isystem /nix/store/r4557ald6zn4dzmvgh8na9vwnwzgrjgc-nodejs-22.19.0/include -isystem /nix/store/l8m7mbvqxdi9bd5apl8s49kjpnzrcv6c-postgresql-17.6-dev/include -isystem /nix/store/ks5kxqrg113jkv9bsvhgpavrq1z1ks4g-inotify-tools-4.23.9.0/include'
|
||||
NIX_CFLAGS_COMPILE=' -frandom-seed=2l63479bll -isystem /nix/store/7zwa3r9agcyzf21d0792fvhrsl6gajiy-bash-interactive-5.3p3-dev/include -isystem /nix/store/a99hiwhamgzds70gxkfnb4cm8i926356-nodejs-22.19.0-dev/include -isystem /nix/store/r4557ald6zn4dzmvgh8na9vwnwzgrjgc-nodejs-22.19.0/include -isystem /nix/store/l8m7mbvqxdi9bd5apl8s49kjpnzrcv6c-postgresql-17.6-dev/include -isystem /nix/store/ks5kxqrg113jkv9bsvhgpavrq1z1ks4g-inotify-tools-4.23.9.0/include -isystem /nix/store/7zwa3r9agcyzf21d0792fvhrsl6gajiy-bash-interactive-5.3p3-dev/include -isystem /nix/store/a99hiwhamgzds70gxkfnb4cm8i926356-nodejs-22.19.0-dev/include -isystem /nix/store/r4557ald6zn4dzmvgh8na9vwnwzgrjgc-nodejs-22.19.0/include -isystem /nix/store/l8m7mbvqxdi9bd5apl8s49kjpnzrcv6c-postgresql-17.6-dev/include -isystem /nix/store/ks5kxqrg113jkv9bsvhgpavrq1z1ks4g-inotify-tools-4.23.9.0/include'
|
||||
export NIX_CFLAGS_COMPILE
|
||||
NIX_ENFORCE_NO_NATIVE='1'
|
||||
export NIX_ENFORCE_NO_NATIVE
|
||||
@ -50,7 +50,7 @@ NIX_STORE='/nix/store'
|
||||
export NIX_STORE
|
||||
NM='nm'
|
||||
export NM
|
||||
NODE_PATH='/nix/store/r4557ald6zn4dzmvgh8na9vwnwzgrjgc-nodejs-22.19.0/lib/node_modules:/nix/store/n4lyyqirbz2j0igs12m2pyqrs7zyyvld-netlify-cli-19.0.2/lib/node_modules'
|
||||
NODE_PATH='/nix/store/r4557ald6zn4dzmvgh8na9vwnwzgrjgc-nodejs-22.19.0/lib/node_modules:/nix/store/1p5n2mzy33ayzc1scdnz82h53d192knh-claude-code-1.0.117/lib/node_modules'
|
||||
export NODE_PATH
|
||||
OBJCOPY='objcopy'
|
||||
export OBJCOPY
|
||||
@ -60,7 +60,7 @@ OLDPWD=''
|
||||
export OLDPWD
|
||||
OPTERR='1'
|
||||
OSTYPE='linux-gnu'
|
||||
PATH='/nix/store/gx2l0rnp3qcnysdddkg9dqnh2mz6w08k-patchelf-0.15.2/bin:/nix/store/95k9rsn1zsw1yvir8mj824ldhf90i4qw-gcc-wrapper-14.3.0/bin:/nix/store/82kmz7r96navanrc2fgckh2bamiqrgsw-gcc-14.3.0/bin:/nix/store/4jxivbjpr86wmsziqlf7iljlwjlxz8bh-glibc-2.40-66-bin/bin:/nix/store/8ksax0a2mxglr5hlkj2dzl556jx7xqn5-coreutils-9.7/bin:/nix/store/l19cddv64i52rhcwahif8sgyrd3mhiqb-binutils-wrapper-2.44/bin:/nix/store/c43ry7z24x3jhnjlj4gpay8a4g2p3x1h-binutils-2.44/bin:/nix/store/ddx7976jyll30xjbasghv9jailswprcp-bash-interactive-5.3p3/bin:/nix/store/q1zaii9cirbfpmwr7d86hpppql3kjcpf-git-2.51.0/bin:/nix/store/a99hiwhamgzds70gxkfnb4cm8i926356-nodejs-22.19.0-dev/bin:/nix/store/r4557ald6zn4dzmvgh8na9vwnwzgrjgc-nodejs-22.19.0/bin:/nix/store/967gn7p1p47ic924r2fx4rgbfp49fhsy-pnpm-10.15.1/bin:/nix/store/l8m7mbvqxdi9bd5apl8s49kjpnzrcv6c-postgresql-17.6-dev/bin:/nix/store/jq2kbdw6ljv9i47jz23pm072cfyxwpfj-postgresql-17.6/bin:/nix/store/n4lyyqirbz2j0igs12m2pyqrs7zyyvld-netlify-cli-19.0.2/bin:/nix/store/ks5kxqrg113jkv9bsvhgpavrq1z1ks4g-inotify-tools-4.23.9.0/bin:/nix/store/8ksax0a2mxglr5hlkj2dzl556jx7xqn5-coreutils-9.7/bin:/nix/store/l964krgbp613d5jxga2vy5qdssj7zfzj-findutils-4.10.0/bin:/nix/store/s2fvny566vls74p4qm9v3fdqd741fh3f-diffutils-3.12/bin:/nix/store/pmhkmqy0vxk47r6ndh0azybhf6gs6k25-gnused-4.9/bin:/nix/store/vlckk0vnmawq9wwh7ndkrwxlpv4h29yh-gnugrep-3.12/bin:/nix/store/03nvbw411p097h6yxjghc33rbcrjfb9d-gawk-5.3.2/bin:/nix/store/8av8pfs7bnyc6hqj764ns4z1fnr9bva1-gnutar-1.35/bin:/nix/store/8gsxxh82rf957ffbsk0q9670nhvl5lia-gzip-1.14/bin:/nix/store/6yjb3zdj448rm8qsmpiq3f67kvj5683a-bzip2-1.0.8-bin/bin:/nix/store/aqdvlkh0jdwkc22hh5vr9sl6qlw5ha74-gnumake-4.4.1/bin:/nix/store/q7sqwn7i6w2b67adw0bmix29pxg85x3w-bash-5.3p3/bin:/nix/store/856i1ajaci3kmmp15rifacfz3jvn5l3q-patch-2.8/bin:/nix/store/y9kgzp85ykrhd7l691w4djx121qygy68-xz-5.8.1-bin/bin:/nix/store/v40ijzz8p2fpk9ihjck3a1ncqaqfmn3c-file-5.45/bin'
|
||||
PATH='/nix/store/gx2l0rnp3qcnysdddkg9dqnh2mz6w08k-patchelf-0.15.2/bin:/nix/store/95k9rsn1zsw1yvir8mj824ldhf90i4qw-gcc-wrapper-14.3.0/bin:/nix/store/82kmz7r96navanrc2fgckh2bamiqrgsw-gcc-14.3.0/bin:/nix/store/4jxivbjpr86wmsziqlf7iljlwjlxz8bh-glibc-2.40-66-bin/bin:/nix/store/8ksax0a2mxglr5hlkj2dzl556jx7xqn5-coreutils-9.7/bin:/nix/store/l19cddv64i52rhcwahif8sgyrd3mhiqb-binutils-wrapper-2.44/bin:/nix/store/c43ry7z24x3jhnjlj4gpay8a4g2p3x1h-binutils-2.44/bin:/nix/store/ddx7976jyll30xjbasghv9jailswprcp-bash-interactive-5.3p3/bin:/nix/store/q1zaii9cirbfpmwr7d86hpppql3kjcpf-git-2.51.0/bin:/nix/store/a99hiwhamgzds70gxkfnb4cm8i926356-nodejs-22.19.0-dev/bin:/nix/store/r4557ald6zn4dzmvgh8na9vwnwzgrjgc-nodejs-22.19.0/bin:/nix/store/967gn7p1p47ic924r2fx4rgbfp49fhsy-pnpm-10.15.1/bin:/nix/store/l8m7mbvqxdi9bd5apl8s49kjpnzrcv6c-postgresql-17.6-dev/bin:/nix/store/jq2kbdw6ljv9i47jz23pm072cfyxwpfj-postgresql-17.6/bin:/nix/store/ks5kxqrg113jkv9bsvhgpavrq1z1ks4g-inotify-tools-4.23.9.0/bin:/nix/store/1p5n2mzy33ayzc1scdnz82h53d192knh-claude-code-1.0.117/bin:/nix/store/8ksax0a2mxglr5hlkj2dzl556jx7xqn5-coreutils-9.7/bin:/nix/store/l964krgbp613d5jxga2vy5qdssj7zfzj-findutils-4.10.0/bin:/nix/store/s2fvny566vls74p4qm9v3fdqd741fh3f-diffutils-3.12/bin:/nix/store/pmhkmqy0vxk47r6ndh0azybhf6gs6k25-gnused-4.9/bin:/nix/store/vlckk0vnmawq9wwh7ndkrwxlpv4h29yh-gnugrep-3.12/bin:/nix/store/03nvbw411p097h6yxjghc33rbcrjfb9d-gawk-5.3.2/bin:/nix/store/8av8pfs7bnyc6hqj764ns4z1fnr9bva1-gnutar-1.35/bin:/nix/store/8gsxxh82rf957ffbsk0q9670nhvl5lia-gzip-1.14/bin:/nix/store/6yjb3zdj448rm8qsmpiq3f67kvj5683a-bzip2-1.0.8-bin/bin:/nix/store/aqdvlkh0jdwkc22hh5vr9sl6qlw5ha74-gnumake-4.4.1/bin:/nix/store/q7sqwn7i6w2b67adw0bmix29pxg85x3w-bash-5.3p3/bin:/nix/store/856i1ajaci3kmmp15rifacfz3jvn5l3q-patch-2.8/bin:/nix/store/y9kgzp85ykrhd7l691w4djx121qygy68-xz-5.8.1-bin/bin:/nix/store/v40ijzz8p2fpk9ihjck3a1ncqaqfmn3c-file-5.45/bin'
|
||||
export PATH
|
||||
PS4='+ '
|
||||
RANLIB='ranlib'
|
||||
@ -82,7 +82,7 @@ export XDG_DATA_DIRS
|
||||
__structuredAttrs=''
|
||||
export __structuredAttrs
|
||||
_substituteStream_has_warned_replace_deprecation='false'
|
||||
buildInputs='/nix/store/7zwa3r9agcyzf21d0792fvhrsl6gajiy-bash-interactive-5.3p3-dev /nix/store/008h0z2m22alg2v8kcdcw4v0f7c39lmm-glibc-locales-2.40-66 /nix/store/q1zaii9cirbfpmwr7d86hpppql3kjcpf-git-2.51.0 /nix/store/a99hiwhamgzds70gxkfnb4cm8i926356-nodejs-22.19.0-dev /nix/store/967gn7p1p47ic924r2fx4rgbfp49fhsy-pnpm-10.15.1 /nix/store/l8m7mbvqxdi9bd5apl8s49kjpnzrcv6c-postgresql-17.6-dev /nix/store/n4lyyqirbz2j0igs12m2pyqrs7zyyvld-netlify-cli-19.0.2 /nix/store/ks5kxqrg113jkv9bsvhgpavrq1z1ks4g-inotify-tools-4.23.9.0'
|
||||
buildInputs='/nix/store/7zwa3r9agcyzf21d0792fvhrsl6gajiy-bash-interactive-5.3p3-dev /nix/store/008h0z2m22alg2v8kcdcw4v0f7c39lmm-glibc-locales-2.40-66 /nix/store/q1zaii9cirbfpmwr7d86hpppql3kjcpf-git-2.51.0 /nix/store/a99hiwhamgzds70gxkfnb4cm8i926356-nodejs-22.19.0-dev /nix/store/967gn7p1p47ic924r2fx4rgbfp49fhsy-pnpm-10.15.1 /nix/store/l8m7mbvqxdi9bd5apl8s49kjpnzrcv6c-postgresql-17.6-dev /nix/store/ks5kxqrg113jkv9bsvhgpavrq1z1ks4g-inotify-tools-4.23.9.0 /nix/store/1p5n2mzy33ayzc1scdnz82h53d192knh-claude-code-1.0.117'
|
||||
export buildInputs
|
||||
buildPhase='{ echo "------------------------------------------------------------";
|
||||
echo " WARNING: the existence of this path is not guaranteed.";
|
||||
@ -161,7 +161,7 @@ declare -a pkgsBuildBuild=()
|
||||
declare -a pkgsBuildHost=('/nix/store/gx2l0rnp3qcnysdddkg9dqnh2mz6w08k-patchelf-0.15.2' '/nix/store/jwjq0fjgn7d00kswhaw2m8hbgws5vbi4-update-autotools-gnu-config-scripts-hook' '/nix/store/0y5xmdb7qfvimjwbq7ibg1xdgkgjwqng-no-broken-symlinks.sh' '/nix/store/cv1d7p48379km6a85h4zp6kr86brh32q-audit-tmpdir.sh' '/nix/store/85clx3b0xkdf58jn161iy80y5223ilbi-compress-man-pages.sh' '/nix/store/wgrbkkaldkrlrni33ccvm3b6vbxzb656-make-symlinks-relative.sh' '/nix/store/5yzw0vhkyszf2d179m0qfkgxmp5wjjx4-move-docs.sh' '/nix/store/fyaryjvghbkpfnsyw97hb3lyb37s1pd6-move-lib64.sh' '/nix/store/kd4xwxjpjxi71jkm6ka0np72if9rm3y0-move-sbin.sh' '/nix/store/pag6l61paj1dc9sv15l7bm5c17xn5kyk-move-systemd-user-units.sh' '/nix/store/cmzya9irvxzlkh7lfy6i82gbp0saxqj3-multiple-outputs.sh' '/nix/store/x8c40nfigps493a07sdr2pm5s9j1cdc0-patch-shebangs.sh' '/nix/store/cickvswrvann041nqxb0rxilc46svw1n-prune-libtool-files.sh' '/nix/store/xyff06pkhki3qy1ls77w10s0v79c9il0-reproducible-builds.sh' '/nix/store/z7k98578dfzi6l3hsvbivzm7hfqlk0zc-set-source-date-epoch-to-latest.sh' '/nix/store/pilsssjjdxvdphlg2h19p0bfx5q0jzkn-strip.sh' '/nix/store/95k9rsn1zsw1yvir8mj824ldhf90i4qw-gcc-wrapper-14.3.0' '/nix/store/l19cddv64i52rhcwahif8sgyrd3mhiqb-binutils-wrapper-2.44' )
|
||||
declare -a pkgsBuildTarget=()
|
||||
declare -a pkgsHostHost=()
|
||||
declare -a pkgsHostTarget=('/nix/store/7zwa3r9agcyzf21d0792fvhrsl6gajiy-bash-interactive-5.3p3-dev' '/nix/store/ddx7976jyll30xjbasghv9jailswprcp-bash-interactive-5.3p3' '/nix/store/008h0z2m22alg2v8kcdcw4v0f7c39lmm-glibc-locales-2.40-66' '/nix/store/q1zaii9cirbfpmwr7d86hpppql3kjcpf-git-2.51.0' '/nix/store/a99hiwhamgzds70gxkfnb4cm8i926356-nodejs-22.19.0-dev' '/nix/store/r4557ald6zn4dzmvgh8na9vwnwzgrjgc-nodejs-22.19.0' '/nix/store/967gn7p1p47ic924r2fx4rgbfp49fhsy-pnpm-10.15.1' '/nix/store/l8m7mbvqxdi9bd5apl8s49kjpnzrcv6c-postgresql-17.6-dev' '/nix/store/msjxcqa4x2f52dyq10rbrbw6k0m0hi90-postgresql-17.6-lib' '/nix/store/jq2kbdw6ljv9i47jz23pm072cfyxwpfj-postgresql-17.6' '/nix/store/n4lyyqirbz2j0igs12m2pyqrs7zyyvld-netlify-cli-19.0.2' '/nix/store/ks5kxqrg113jkv9bsvhgpavrq1z1ks4g-inotify-tools-4.23.9.0' )
|
||||
declare -a pkgsHostTarget=('/nix/store/7zwa3r9agcyzf21d0792fvhrsl6gajiy-bash-interactive-5.3p3-dev' '/nix/store/ddx7976jyll30xjbasghv9jailswprcp-bash-interactive-5.3p3' '/nix/store/008h0z2m22alg2v8kcdcw4v0f7c39lmm-glibc-locales-2.40-66' '/nix/store/q1zaii9cirbfpmwr7d86hpppql3kjcpf-git-2.51.0' '/nix/store/a99hiwhamgzds70gxkfnb4cm8i926356-nodejs-22.19.0-dev' '/nix/store/r4557ald6zn4dzmvgh8na9vwnwzgrjgc-nodejs-22.19.0' '/nix/store/967gn7p1p47ic924r2fx4rgbfp49fhsy-pnpm-10.15.1' '/nix/store/l8m7mbvqxdi9bd5apl8s49kjpnzrcv6c-postgresql-17.6-dev' '/nix/store/msjxcqa4x2f52dyq10rbrbw6k0m0hi90-postgresql-17.6-lib' '/nix/store/jq2kbdw6ljv9i47jz23pm072cfyxwpfj-postgresql-17.6' '/nix/store/ks5kxqrg113jkv9bsvhgpavrq1z1ks4g-inotify-tools-4.23.9.0' '/nix/store/1p5n2mzy33ayzc1scdnz82h53d192knh-claude-code-1.0.117' )
|
||||
declare -a pkgsTargetTarget=()
|
||||
declare -a postFixupHooks=('noBrokenSymlinksInAllOutputs' '_makeSymlinksRelativeInAllOutputs' '_multioutPropagateDev' )
|
||||
declare -a postUnpackHooks=('_updateSourceDateEpochFromSourceRoot' )
|
||||
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@ -19,3 +19,7 @@ pnpm-debug.log*
|
||||
|
||||
# macOS-specific files
|
||||
.DS_Store
|
||||
|
||||
# Direnv junk
|
||||
.direnv/*
|
||||
*/.direnv/*
|
||||
34
flake.nix
34
flake.nix
@ -10,31 +10,25 @@
|
||||
system:
|
||||
let
|
||||
inherit (pkgs.lib) optional optionals;
|
||||
pkgs = import nixpkgs { inherit system; };
|
||||
pkgs = import nixpkgs {
|
||||
inherit system;
|
||||
config.allowUnfree = true;
|
||||
};
|
||||
|
||||
in
|
||||
with pkgs;
|
||||
{
|
||||
devShell = pkgs.mkShell {
|
||||
buildInputs =
|
||||
[
|
||||
bashInteractive
|
||||
glibcLocales
|
||||
git
|
||||
nodejs
|
||||
pnpm
|
||||
postgresql
|
||||
netlify-cli
|
||||
]
|
||||
++ optional stdenv.isLinux inotify-tools
|
||||
++ optional stdenv.isDarwin terminal-notifier
|
||||
++ optionals stdenv.isDarwin (
|
||||
with darwin.apple_sdk.frameworks;
|
||||
[
|
||||
CoreFoundation
|
||||
CoreServices
|
||||
]
|
||||
);
|
||||
buildInputs = [
|
||||
bashInteractive
|
||||
glibcLocales
|
||||
git
|
||||
nodejs
|
||||
pnpm
|
||||
postgresql
|
||||
inotify-tools
|
||||
claude-code
|
||||
];
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
14
hindki/.env.example
Normal file
14
hindki/.env.example
Normal file
@ -0,0 +1,14 @@
|
||||
# Storage configuration for vocabulary updates
|
||||
# Options: filesystem (default), git
|
||||
|
||||
# For local development (default)
|
||||
VOCAB_STORAGE_TYPE=filesystem
|
||||
|
||||
# For Gitea/GitHub storage in production
|
||||
# VOCAB_STORAGE_TYPE=git
|
||||
# GIT_API_URL=https://gitea.example.com # Your Gitea instance URL (omit for GitHub)
|
||||
# GIT_OWNER=your-username
|
||||
# GIT_REPO=hindki
|
||||
# GIT_PATH=src/vocab_list.yaml
|
||||
# GIT_BRANCH=main
|
||||
# GIT_TOKEN=your-personal-access-token
|
||||
500
hindki/src/components/AddVocabForm.tsx
Normal file
500
hindki/src/components/AddVocabForm.tsx
Normal file
@ -0,0 +1,500 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
|
||||
interface VocabWord {
|
||||
english: string;
|
||||
hindi: string;
|
||||
gender?: string;
|
||||
type?: string;
|
||||
note?: string;
|
||||
examples?: Array<{
|
||||
english: string;
|
||||
hindi: string;
|
||||
note?: string;
|
||||
}>;
|
||||
see_also?: string[];
|
||||
}
|
||||
|
||||
interface VocabCategory {
|
||||
slug: string;
|
||||
about: string;
|
||||
words: VocabWord[];
|
||||
}
|
||||
|
||||
export default function AddVocabForm() {
|
||||
const [categories, setCategories] = useState<VocabCategory[]>([]);
|
||||
const [selectedCategory, setSelectedCategory] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [categoriesLoading, setCategoriesLoading] = useState(true);
|
||||
const [message, setMessage] = useState('');
|
||||
const [showNewCategory, setShowNewCategory] = useState(false);
|
||||
const [newCategorySlug, setNewCategorySlug] = useState('');
|
||||
const [newCategoryAbout, setNewCategoryAbout] = useState('');
|
||||
const [formData, setFormData] = useState<VocabWord>({
|
||||
english: '',
|
||||
hindi: '',
|
||||
gender: '',
|
||||
type: 'noun',
|
||||
note: '',
|
||||
examples: [],
|
||||
see_also: [],
|
||||
});
|
||||
const englishInputRef = React.useRef<HTMLInputElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
// Get URL parameters
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const catFromUrl = urlParams.get('cat');
|
||||
const typeFromUrl = urlParams.get('type');
|
||||
|
||||
setCategoriesLoading(true);
|
||||
fetch('/api/vocab.json')
|
||||
.then(res => {
|
||||
if (!res.ok) {
|
||||
throw new Error(`HTTP error! status: ${res.status}`);
|
||||
}
|
||||
return res.json();
|
||||
})
|
||||
.then(data => {
|
||||
if (Array.isArray(data) && data.length > 0) {
|
||||
setCategories(data);
|
||||
|
||||
// Use category from URL if provided, otherwise first category
|
||||
if (catFromUrl && data.some(cat => cat.slug === catFromUrl)) {
|
||||
setSelectedCategory(catFromUrl);
|
||||
} else if (!selectedCategory && data.length > 0) {
|
||||
setSelectedCategory(data[0].slug);
|
||||
}
|
||||
|
||||
// Use type from URL if provided
|
||||
if (typeFromUrl) {
|
||||
setFormData(prev => ({ ...prev, type: typeFromUrl }));
|
||||
}
|
||||
} else {
|
||||
setMessage('No categories found');
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
console.error('Failed to load categories:', err);
|
||||
setMessage(`Failed to load categories: ${err.message}`);
|
||||
})
|
||||
.finally(() => {
|
||||
setCategoriesLoading(false);
|
||||
});
|
||||
}, []);
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setLoading(true);
|
||||
setMessage('');
|
||||
|
||||
try {
|
||||
// Build word object with only non-empty fields
|
||||
const word: any = {
|
||||
english: formData.english,
|
||||
hindi: formData.hindi,
|
||||
type: formData.type,
|
||||
};
|
||||
|
||||
// Only add optional fields if they have values
|
||||
if (formData.gender) {
|
||||
word.gender = formData.gender;
|
||||
}
|
||||
if (formData.note && formData.note.trim()) {
|
||||
word.note = formData.note;
|
||||
}
|
||||
if (formData.examples && formData.examples.length > 0) {
|
||||
// Filter out empty examples
|
||||
const validExamples = formData.examples.filter(
|
||||
ex => ex.english.trim() || ex.hindi.trim()
|
||||
);
|
||||
if (validExamples.length > 0) {
|
||||
word.examples = validExamples;
|
||||
}
|
||||
}
|
||||
|
||||
if (formData.see_also && formData.see_also.length > 0) {
|
||||
// Filter out empty see_also fields
|
||||
const validSeeAlsos = formData.see_also.filter(
|
||||
ex => ex.trim()
|
||||
);
|
||||
if (validSeeAlsos.length > 0) {
|
||||
word.see_also = validSeeAlsos;
|
||||
}
|
||||
}
|
||||
|
||||
// If creating a new category, use the new category details
|
||||
const categoryToUse = showNewCategory ? newCategorySlug : selectedCategory;
|
||||
|
||||
const requestBody: any = {
|
||||
category: categoryToUse,
|
||||
word: word,
|
||||
};
|
||||
|
||||
// Include new category information if creating one
|
||||
if (showNewCategory) {
|
||||
requestBody.newCategory = {
|
||||
slug: newCategorySlug,
|
||||
about: newCategoryAbout,
|
||||
};
|
||||
}
|
||||
|
||||
const response = await fetch('/api/vocab.json', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(requestBody),
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
setMessage('Word added successfully!');
|
||||
|
||||
// Remember the current type and category for bulk entry
|
||||
const currentType = formData.type;
|
||||
const currentCategory = showNewCategory ? newCategorySlug : selectedCategory;
|
||||
|
||||
// Update URL with current category and type to persist through reloads
|
||||
const newUrl = `${window.location.pathname}?cat=${currentCategory}&type=${currentType}`;
|
||||
window.history.replaceState({}, '', newUrl);
|
||||
|
||||
// Reset form but keep the type
|
||||
setFormData({
|
||||
english: '',
|
||||
hindi: '',
|
||||
gender: '',
|
||||
type: currentType, // Keep the last used type
|
||||
note: '',
|
||||
examples: [],
|
||||
see_also: [],
|
||||
});
|
||||
|
||||
// Focus back on English input for quick bulk entry
|
||||
setTimeout(() => {
|
||||
englishInputRef.current?.focus();
|
||||
}, 100);
|
||||
|
||||
// If we created a new category, refresh the categories list
|
||||
if (showNewCategory) {
|
||||
setShowNewCategory(false);
|
||||
setNewCategorySlug('');
|
||||
setNewCategoryAbout('');
|
||||
|
||||
// Refresh categories and set the new category as selected
|
||||
fetch('/api/vocab.json')
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
if (Array.isArray(data) && data.length > 0) {
|
||||
setCategories(data);
|
||||
// Set the newly created category as selected
|
||||
setSelectedCategory(currentCategory);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// For existing categories, just keep it selected
|
||||
setSelectedCategory(currentCategory);
|
||||
}
|
||||
} else {
|
||||
setMessage(`Error: ${result.error}`);
|
||||
}
|
||||
} catch (error) {
|
||||
setMessage('Failed to add word');
|
||||
console.error('Error:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const addExample = () => {
|
||||
setFormData({
|
||||
...formData,
|
||||
examples: [...(formData.examples || []), { english: '', hindi: '' }],
|
||||
});
|
||||
};
|
||||
|
||||
const updateExample = (index: number, field: 'english' | 'hindi', value: string) => {
|
||||
const newExamples = [...(formData.examples || [])];
|
||||
newExamples[index] = { ...newExamples[index], [field]: value };
|
||||
setFormData({ ...formData, examples: newExamples });
|
||||
};
|
||||
|
||||
const removeExample = (index: number) => {
|
||||
const newExamples = (formData.examples || []).filter((_, i) => i !== index);
|
||||
setFormData({ ...formData, examples: newExamples });
|
||||
};
|
||||
|
||||
// Same thing for see_also
|
||||
const addSeeAlso = () => {
|
||||
setFormData({
|
||||
...formData,
|
||||
see_also: [...(formData.see_also || []), ''],
|
||||
});
|
||||
};
|
||||
|
||||
const updateSeeAlso = (index: number, value: string) => {
|
||||
const newSeeAlso = [...(formData.see_also || [])];
|
||||
newSeeAlso[index] = value;
|
||||
setFormData({ ...formData, see_also: newSeeAlso });
|
||||
}
|
||||
|
||||
const removeSeeAlso = (index: number) => {
|
||||
const newSeeAlso = (formData.see_also || []).filter((_, i) => i !== index);
|
||||
setFormData({ ...formData, see_also: newSeeAlso });
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="vocab-form">
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="form-group">
|
||||
<label htmlFor="category">Category:</label>
|
||||
<select
|
||||
id="category"
|
||||
value={showNewCategory ? '__new__' : selectedCategory}
|
||||
onChange={(e) => {
|
||||
if (e.target.value === '__new__') {
|
||||
setShowNewCategory(true);
|
||||
} else {
|
||||
setShowNewCategory(false);
|
||||
setSelectedCategory(e.target.value);
|
||||
}
|
||||
}}
|
||||
required={!showNewCategory}
|
||||
>
|
||||
{categories.map((cat) => (
|
||||
<option key={cat.slug} value={cat.slug}>
|
||||
{cat.slug.charAt(0).toUpperCase() + cat.slug.slice(1)} - {cat.about}
|
||||
</option>
|
||||
))}
|
||||
<option value="__new__">+ Create New Category</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{showNewCategory && (
|
||||
<>
|
||||
<div className="form-group">
|
||||
<label htmlFor="newCategorySlug">Category ID (slug):</label>
|
||||
<input
|
||||
type="text"
|
||||
id="newCategorySlug"
|
||||
value={newCategorySlug}
|
||||
onChange={(e) => setNewCategorySlug(e.target.value.toLowerCase().replace(/\s+/g, '-'))}
|
||||
placeholder="e.g., food-and-drink"
|
||||
required={showNewCategory}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label htmlFor="newCategoryAbout">Category Description:</label>
|
||||
<input
|
||||
type="text"
|
||||
id="newCategoryAbout"
|
||||
value={newCategoryAbout}
|
||||
onChange={(e) => setNewCategoryAbout(e.target.value)}
|
||||
placeholder="e.g., Words related to food and beverages"
|
||||
required={showNewCategory}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className="form-group">
|
||||
<label htmlFor="english">English:</label>
|
||||
<input
|
||||
type="text"
|
||||
id="english"
|
||||
ref={englishInputRef}
|
||||
value={formData.english}
|
||||
onChange={(e) => setFormData({ ...formData, english: e.target.value })}
|
||||
autoFocus
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label htmlFor="hindi">Hindi:</label>
|
||||
<input
|
||||
type="text"
|
||||
id="hindi"
|
||||
value={formData.hindi}
|
||||
onChange={(e) => setFormData({ ...formData, hindi: e.target.value })}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label htmlFor="type">Type:</label>
|
||||
<select
|
||||
id="type"
|
||||
value={formData.type}
|
||||
onChange={(e) => setFormData({ ...formData, type: e.target.value })}
|
||||
>
|
||||
<option value="noun">Noun</option>
|
||||
<option value="verb">Verb</option>
|
||||
<option value="adjective">Adjective</option>
|
||||
<option value="adverb">Adverb</option>
|
||||
<option value="pronoun">Pronoun</option>
|
||||
<option value="conjunction">Conjunction</option>
|
||||
<option value="preposition">Preposition</option>
|
||||
<option value="interjection">Interjection</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label htmlFor="gender">Gender (for nouns):</label>
|
||||
<select
|
||||
id="gender"
|
||||
value={formData.gender}
|
||||
onChange={(e) => setFormData({ ...formData, gender: e.target.value })}
|
||||
>
|
||||
<option value="">N/A</option>
|
||||
<option value="m">Masculine (m)</option>
|
||||
<option value="f">Feminine (f)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label htmlFor="note">Note:</label>
|
||||
<textarea
|
||||
id="note"
|
||||
value={formData.note}
|
||||
onChange={(e) => setFormData({ ...formData, note: e.target.value })}
|
||||
rows={3}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label>Examples:</label>
|
||||
{formData.examples?.map((example, index) => (
|
||||
<div key={index} className="example-group">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="English example"
|
||||
value={example.english}
|
||||
onChange={(e) => updateExample(index, 'english', e.target.value)}
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Hindi example"
|
||||
value={example.hindi}
|
||||
onChange={(e) => updateExample(index, 'hindi', e.target.value)}
|
||||
/>
|
||||
<button type="button" onClick={() => removeExample(index)}>
|
||||
Remove
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
<button type="button" onClick={addExample}>
|
||||
Add Example
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label>See Also:</label>
|
||||
{formData.see_also?.map((see_also, index) => (
|
||||
<div key={index} className="see-also-group">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Format: [text](link)"
|
||||
value={see_also}
|
||||
onChange={(e) => updateSeeAlso(index, e.target.value)}
|
||||
/>
|
||||
<button type="button" onClick={() => removeSeeAlso(index)}>
|
||||
Remove
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
<button type="button" onClick={addSeeAlso}>
|
||||
Add Reference
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button type="submit" disabled={loading}>
|
||||
{loading ? 'Adding...' : 'Add Word'}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
{message && (
|
||||
<div className={`message ${message.includes('Error') ? 'error' : 'success'}`}>
|
||||
{message}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<style>{`
|
||||
.vocab-form {
|
||||
max-width: 600px;
|
||||
margin: 2rem 0;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.form-group input,
|
||||
.form-group select,
|
||||
.form-group textarea {
|
||||
width: 100%;
|
||||
padding: 0.5rem;
|
||||
border: 1px solid var(--sl-color-gray-5);
|
||||
border-radius: 4px;
|
||||
background: var(--sl-color-bg);
|
||||
color: var(--sl-color-text);
|
||||
}
|
||||
|
||||
.example-group {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr auto;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.example-group input {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
button:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
button[type="button"] {
|
||||
background: var(--sl-color-gray-6);
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
.message {
|
||||
margin-top: 1rem;
|
||||
padding: 1rem;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.message.success {
|
||||
background: var(--sl-color-green);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.message.error {
|
||||
background: var(--sl-color-red);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.vocab-form .form-group {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 1em;
|
||||
}
|
||||
.vocab-form .form-group label,
|
||||
.vocab-form .form-group select {
|
||||
margin: 0;
|
||||
line-height: 1em;
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -27,12 +27,24 @@ const gender_lookup: Record<"m" | "f", ["note" | "tip", string]> = {
|
||||
"f": ["tip", "feminine"],
|
||||
};
|
||||
|
||||
function renderInline(rawText:string){
|
||||
// Swap double-dash for em-dash
|
||||
const text = rawText.replace(/---/g, '—').replace(/--/g, '–');
|
||||
return md.renderInline(text);
|
||||
}
|
||||
function render(rawText:string){
|
||||
// Swap double-dash for em-dash
|
||||
const text = rawText.replace(/---/g, '—').replace(/--/g, '–');
|
||||
return md.render(text);
|
||||
}
|
||||
|
||||
function highlight(text: string, term: string) {
|
||||
const terms = term.split(',').map(t => t.trim()).filter(Boolean);
|
||||
const regex = new RegExp(`(${terms.map(t => t.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|')})`, 'gi');
|
||||
const parts = text.split(regex);
|
||||
return md.renderInline(parts.map((part) => regex.test(part) ? `**${part}**` : part).join(''));
|
||||
return renderInline(parts.map((part) => regex.test(part) ? `**${part}**` : part).join(''));
|
||||
}
|
||||
|
||||
---
|
||||
<li id={word.hindi} class="word-entry">
|
||||
<AnchorHeading level="4" id={word.hindi} class="word-heading" style="font-weight: normal;">
|
||||
@ -40,7 +52,7 @@ function highlight(text: string, term: string) {
|
||||
{word.gender && <Badge variant={gender_lookup[word.gender][0]} text={gender_lookup[word.gender][1]} class="gender-badge"/>}
|
||||
</AnchorHeading>
|
||||
{word.note &&
|
||||
<div set:html={md.renderInline(word.note)}></div>
|
||||
<div set:html={render(word.note)}></div>
|
||||
}
|
||||
{
|
||||
word.examples &&(
|
||||
@ -65,7 +77,7 @@ word.examples &&(
|
||||
<p><b>See also:</b>
|
||||
{ word.see_also.map((ref, i) => (
|
||||
<>
|
||||
<span set:html={md.renderInline(ref)}></span>{ i < word.see_also!.length - 1 ? "; " : '' }
|
||||
<span set:html={renderInline(ref)}></span>{ i < word.see_also!.length - 1 ? "; " : '' }
|
||||
</>
|
||||
)) }
|
||||
</p>
|
||||
|
||||
156
hindki/src/lib/storage.ts
Normal file
156
hindki/src/lib/storage.ts
Normal file
@ -0,0 +1,156 @@
|
||||
import fs from 'fs/promises';
|
||||
import path from 'path';
|
||||
import YAML from 'yaml';
|
||||
|
||||
interface StorageAdapter {
|
||||
read(): Promise<any[]>;
|
||||
write(data: any[]): Promise<void>;
|
||||
}
|
||||
|
||||
class FileSystemAdapter implements StorageAdapter {
|
||||
private filePath: string;
|
||||
|
||||
constructor(filePath: string) {
|
||||
this.filePath = filePath;
|
||||
}
|
||||
|
||||
async read(): Promise<any[]> {
|
||||
const content = await fs.readFile(this.filePath, 'utf-8');
|
||||
return YAML.parse(content);
|
||||
}
|
||||
|
||||
async write(data: any[]): Promise<void> {
|
||||
const yaml = YAML.stringify(data);
|
||||
await fs.writeFile(this.filePath, yaml, 'utf-8');
|
||||
}
|
||||
}
|
||||
|
||||
class GitAdapter implements StorageAdapter {
|
||||
private baseUrl: string;
|
||||
private owner: string;
|
||||
private repo: string;
|
||||
private path: string;
|
||||
private branch: string;
|
||||
private token: string;
|
||||
|
||||
constructor(config: {
|
||||
baseUrl?: string; // For Gitea, e.g., 'https://gitea.example.com'
|
||||
owner: string;
|
||||
repo: string;
|
||||
path: string;
|
||||
branch?: string;
|
||||
token: string;
|
||||
}) {
|
||||
this.baseUrl = config.baseUrl || 'https://api.github.com';
|
||||
this.owner = config.owner;
|
||||
this.repo = config.repo;
|
||||
this.path = config.path;
|
||||
this.branch = config.branch || 'main';
|
||||
this.token = config.token;
|
||||
}
|
||||
|
||||
private get apiBase(): string {
|
||||
// Remove trailing slash if present
|
||||
const base = this.baseUrl.replace(/\/$/, '');
|
||||
// Add /api/v1 for Gitea if not GitHub
|
||||
if (!base.includes('api.github.com')) {
|
||||
return base.includes('/api/v1') ? base : `${base}/api/v1`;
|
||||
}
|
||||
return base;
|
||||
}
|
||||
|
||||
async read(): Promise<any[]> {
|
||||
const response = await fetch(
|
||||
`${this.apiBase}/repos/${this.owner}/${this.repo}/contents/${this.path}?ref=${this.branch}`,
|
||||
{
|
||||
headers: {
|
||||
'Authorization': `Bearer ${this.token}`,
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to read from Git: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
const content = Buffer.from(data.content, 'base64').toString('utf-8');
|
||||
return YAML.parse(content);
|
||||
}
|
||||
|
||||
async write(data: any[]): Promise<void> {
|
||||
// First, get the current file to get its SHA
|
||||
const currentResponse = await fetch(
|
||||
`${this.apiBase}/repos/${this.owner}/${this.repo}/contents/${this.path}?ref=${this.branch}`,
|
||||
{
|
||||
headers: {
|
||||
'Authorization': `Bearer ${this.token}`,
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
if (!currentResponse.ok) {
|
||||
throw new Error(`Failed to get current file from Git: ${currentResponse.statusText}`);
|
||||
}
|
||||
|
||||
const currentData = await currentResponse.json();
|
||||
const sha = currentData.sha;
|
||||
|
||||
// Now update the file
|
||||
const yaml = YAML.stringify(data);
|
||||
const content = Buffer.from(yaml).toString('base64');
|
||||
|
||||
const updateResponse = await fetch(
|
||||
`${this.apiBase}/repos/${this.owner}/${this.repo}/contents/${this.path}`,
|
||||
{
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${this.token}`,
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
message: 'Update vocab list via web form',
|
||||
content: content,
|
||||
sha: sha,
|
||||
branch: this.branch,
|
||||
}),
|
||||
}
|
||||
);
|
||||
|
||||
if (!updateResponse.ok) {
|
||||
const errorText = await updateResponse.text();
|
||||
throw new Error(`Failed to update Git file: ${updateResponse.statusText} - ${errorText}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function getStorageAdapter(): StorageAdapter {
|
||||
const storageType = import.meta.env.VOCAB_STORAGE_TYPE || 'filesystem';
|
||||
|
||||
switch (storageType) {
|
||||
case 'git':
|
||||
case 'github':
|
||||
case 'gitea':
|
||||
if (!import.meta.env.GIT_TOKEN) {
|
||||
throw new Error('GIT_TOKEN environment variable is required for Git storage');
|
||||
}
|
||||
return new GitAdapter({
|
||||
baseUrl: import.meta.env.GIT_API_URL, // e.g., 'https://gitea.example.com' for Gitea
|
||||
owner: import.meta.env.GIT_OWNER || 'your-username',
|
||||
repo: import.meta.env.GIT_REPO || 'your-repo',
|
||||
path: import.meta.env.GIT_PATH || 'src/vocab_list.yaml',
|
||||
branch: import.meta.env.GIT_BRANCH || 'main',
|
||||
token: import.meta.env.GIT_TOKEN,
|
||||
});
|
||||
|
||||
case 'filesystem':
|
||||
default:
|
||||
const filePath = path.join(process.cwd(), 'src', 'vocab_list.yaml');
|
||||
return new FileSystemAdapter(filePath);
|
||||
}
|
||||
}
|
||||
|
||||
export { StorageAdapter, FileSystemAdapter, GitAdapter };
|
||||
108
hindki/src/pages/api/vocab.json.ts
Normal file
108
hindki/src/pages/api/vocab.json.ts
Normal file
@ -0,0 +1,108 @@
|
||||
import type { APIRoute } from 'astro';
|
||||
import { getStorageAdapter } from '../../lib/storage';
|
||||
export const prerender = false;
|
||||
export const GET: APIRoute = async () => {
|
||||
try {
|
||||
const storage = getStorageAdapter();
|
||||
const data = await storage.read();
|
||||
|
||||
return new Response(JSON.stringify(data), {
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error in GET /api/vocab:', error);
|
||||
return new Response(JSON.stringify({ error: 'Failed to read vocab list' }), {
|
||||
status: 500,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const POST: APIRoute = async ({ request }) => {
|
||||
try {
|
||||
const data = await request.json();
|
||||
const storage = getStorageAdapter();
|
||||
|
||||
// Read existing data
|
||||
const vocabList = await storage.read();
|
||||
|
||||
// Validate the new entry
|
||||
if (!data.category || !data.word) {
|
||||
return new Response(JSON.stringify({ error: 'Missing required fields' }), {
|
||||
status: 400,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Check if we're creating a new category
|
||||
if (data.newCategory) {
|
||||
// Validate new category data
|
||||
if (!data.newCategory.slug || !data.newCategory.about) {
|
||||
return new Response(JSON.stringify({ error: 'New category requires slug and description' }), {
|
||||
status: 400,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Check if category already exists
|
||||
const existingCategory = vocabList.find((cat: any) => cat.slug === data.newCategory.slug);
|
||||
if (existingCategory) {
|
||||
return new Response(JSON.stringify({ error: 'Category already exists' }), {
|
||||
status: 400,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Create the new category with the first word
|
||||
vocabList.push({
|
||||
slug: data.newCategory.slug,
|
||||
about: data.newCategory.about,
|
||||
words: [data.word],
|
||||
});
|
||||
} else {
|
||||
// Find the existing category to add to
|
||||
const categoryIndex = vocabList.findIndex((cat: any) => cat.slug === data.category);
|
||||
|
||||
if (categoryIndex === -1) {
|
||||
return new Response(JSON.stringify({ error: 'Category not found' }), {
|
||||
status: 404,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Add the new word to the existing category
|
||||
vocabList[categoryIndex].words.push(data.word);
|
||||
}
|
||||
|
||||
// Write back using the storage adapter
|
||||
await storage.write(vocabList);
|
||||
|
||||
return new Response(JSON.stringify({ success: true, message: 'Word added successfully' }), {
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error in POST /api/vocab:', error);
|
||||
return new Response(JSON.stringify({ error: 'Failed to update vocab list' }), {
|
||||
status: 500,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
@ -1,8 +1,6 @@
|
||||
---
|
||||
import StarlightPage from "@astrojs/starlight/components/StarlightPage.astro";
|
||||
import fs from "fs";
|
||||
|
||||
const raw_yaml = fs.readFileSync("src/vocab_list.yaml", "utf-8");
|
||||
import AddVocabForm from "../../components/AddVocabForm.tsx";
|
||||
---
|
||||
|
||||
<StarlightPage
|
||||
@ -12,17 +10,5 @@ const raw_yaml = fs.readFileSync("src/vocab_list.yaml", "utf-8");
|
||||
prev: false,
|
||||
}}
|
||||
>
|
||||
{raw_yaml}
|
||||
<AddVocabForm client:only="react" />
|
||||
</StarlightPage>
|
||||
|
||||
<style>
|
||||
:global(div.sl-container) {
|
||||
margin-inline: 0 !important;
|
||||
}
|
||||
|
||||
:global([yaml-editor]) {
|
||||
width: calc(
|
||||
100vw - var(--sl-sidebar-width) - 2 * var(--sl-content-pad-x)
|
||||
);
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -3,6 +3,10 @@
|
||||
import StarlightPage from '@astrojs/starlight/components/StarlightPage.astro';
|
||||
import AnchorHeading from '@astrojs/starlight/components/AnchorHeading.astro';
|
||||
import VocabWord from '@/components/VocabWord.astro';
|
||||
import markdownit from 'markdown-it'
|
||||
import markdownItMark from 'markdown-it-mark'
|
||||
const md = markdownit().use(markdownItMark);
|
||||
|
||||
|
||||
function titlecase(str: string) {
|
||||
return str.charAt(0).toUpperCase() + str.slice(1);
|
||||
@ -43,9 +47,7 @@ const headings = wordsByType.map(({type, words}) => {
|
||||
frontmatter={{ title: "Vocabulary: " + (entry?.id ?? "Uncategorized") }}
|
||||
headings={headings}
|
||||
>
|
||||
<div>
|
||||
{ entry?.data.about}
|
||||
</div>
|
||||
<div set:html={md.render(entry!.data.about!)}/>
|
||||
{
|
||||
wordsByType.map(({type, words}) => (
|
||||
<div class="word-type-section">
|
||||
|
||||
@ -282,6 +282,7 @@ mark {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
|
||||
/* =================================================================
|
||||
LANGUAGE-SPECIFIC STYLES
|
||||
================================================================= */
|
||||
|
||||
@ -1 +0,0 @@
|
||||
/home/ryan/Documents/Code/hindi-server/hindi_server/vocab_list.yaml
|
||||
325
hindki/src/vocab_list.yaml
Normal file
325
hindki/src/vocab_list.yaml
Normal file
@ -0,0 +1,325 @@
|
||||
- about: Common everyday vocabulary words.
|
||||
slug: general
|
||||
words:
|
||||
- english: thing
|
||||
examples:
|
||||
- english: "No problem. *(Literally: [there is] nothing.)*"
|
||||
hindi: कोई बात नहीं.
|
||||
- english: I did not understand this thing (matter).
|
||||
hindi: मुझे यह बात [समझ](#understand) नहीं आई.
|
||||
note: >
|
||||
आई, feminine, matches बात because the postposition in मुझे blocks
|
||||
the gender matching on मैं (Recall [मुझे = मैं +
|
||||
की](/grammar/pronouns)).
|
||||
- english: "to talk (seems to be more like: *to chat*)."
|
||||
hindi: बात करना
|
||||
gender: f
|
||||
hindi: बात
|
||||
note: More abstract than "[चीज़](#चीज़)", often used for matters or topics -- as
|
||||
in, things that are discussed.
|
||||
see_also:
|
||||
- "[चीज़](#चीज़)"
|
||||
type: noun
|
||||
- english: help
|
||||
examples:
|
||||
- english: Please help me.
|
||||
hindi: मेरी मदद किजिए.
|
||||
- english: Have you been helped?
|
||||
hindi: क्या आपकी मदद हुई है?
|
||||
gender: f
|
||||
hindi: मदद
|
||||
note: Used with का/के/की to indicate who is receiving help, and with either करना
|
||||
(to actively help someone), or होना (to passively be helped). See the
|
||||
links below for the generalized rules of these.
|
||||
see_also:
|
||||
- "[Reflexive verbs](/grammar/reflexive-verbs)"
|
||||
- "[Active and passive, transitive and
|
||||
intransitive](/grammar/active-passive-transitive-intransitive)"
|
||||
type: noun
|
||||
- english: message
|
||||
hindi: संदेश
|
||||
type: noun
|
||||
gender: m
|
||||
- about: Hindi often uses repetition of words for emphasis or to indicate a
|
||||
variety of related meanings.
|
||||
slug: repetition
|
||||
words:
|
||||
- english: what all, which all
|
||||
examples:
|
||||
- english: What all did you do last week?
|
||||
hindi: पिछले हफ्ते तुमने क्या-क्या किया?
|
||||
hindi: क्या-क्या
|
||||
type: noun
|
||||
- hindi: धीरे-धीरे
|
||||
english: slowly, gradually
|
||||
examples:
|
||||
- english: Please speak slowly.
|
||||
hindi: कृपया धीरे-धीरे बोलिए.
|
||||
type: adverb
|
||||
- about: Words for people from different countries.
|
||||
slug: nationalities
|
||||
words:
|
||||
- english: Afghan
|
||||
hindi: अफ़्गानी
|
||||
type: adjective
|
||||
- english: African
|
||||
hindi: अफ़्रीकी
|
||||
type: adjective
|
||||
- english: American
|
||||
hindi: अमरीकी
|
||||
type: adjective
|
||||
- english: Asian
|
||||
hindi: एशियाई
|
||||
type: adjective
|
||||
- english: Australian
|
||||
hindi: ओस्ट्रेलियन
|
||||
type: adjective
|
||||
- english: Bangladeshi
|
||||
hindi: बंगला, बंगलादेशी
|
||||
type: adjective
|
||||
- english: Bhutanese
|
||||
hindi: भूटानी
|
||||
type: adjective
|
||||
- english: British/English
|
||||
hindi: ब्रितानी/अँग्रेज़
|
||||
type: adjective
|
||||
- english: Chinese
|
||||
hindi: चीनी
|
||||
type: adjective
|
||||
- english: French
|
||||
hindi: फ़्रांसीसी
|
||||
type: adjective
|
||||
- english: German
|
||||
hindi: जर्मन
|
||||
type: adjective
|
||||
- english: Greek
|
||||
hindi: यूनानी
|
||||
type: adjective
|
||||
- english: Indian
|
||||
hindi: भारतीय, हिंदुस्तानी
|
||||
note: There is some cultural context here, as obviously [not everyone in India
|
||||
is Hindu](/culture/bharat-hindustan).
|
||||
type: adjective
|
||||
- english: Iranian
|
||||
hindi: ईरानी
|
||||
type: adjective
|
||||
- english: Iraqi
|
||||
hindi: ईराकी
|
||||
type: adjective
|
||||
- english: Israeli
|
||||
hindi: इज़राईली
|
||||
type: adjective
|
||||
- english: Italian
|
||||
hindi: इतालवी
|
||||
type: adjective
|
||||
- english: Japanese
|
||||
hindi: जापानी
|
||||
type: adjective
|
||||
- english: Nepalese
|
||||
hindi: नेपाली
|
||||
type: adjective
|
||||
- english: Pakistani
|
||||
hindi: पाकिस्तानी
|
||||
type: adjective
|
||||
- english: Russian
|
||||
hindi: रूसी
|
||||
type: adjective
|
||||
- english: Vietnamese
|
||||
hindi: वियतनामी
|
||||
type: adjective
|
||||
- slug: vocations
|
||||
about: >-
|
||||
Common occupations. Almost all of these are unmarked masculine nouns, except
|
||||
the few that have modified forms for females. I assume, though I should
|
||||
definitely confirm such a basic thing, that the words can be applied as-is
|
||||
for females too (e.g., मेरा नर्स / मेरी नर्स both work, based on if your
|
||||
nurse is male or female).
|
||||
|
||||
|
||||
It seems like this is one of those cases of [ingrained
|
||||
sexism](/culture/sexism) you often find in Hindi.
|
||||
words:
|
||||
- english: accountant
|
||||
hindi: मुनीम
|
||||
type: noun
|
||||
- english: actor, actress
|
||||
hindi: अभिनेता, अभिनेत्री
|
||||
type: noun
|
||||
- english: ambassador
|
||||
hindi: राजदूत
|
||||
type: noun
|
||||
- english: artisan
|
||||
hindi: कारीगर
|
||||
type: noun
|
||||
- english: artist
|
||||
hindi: कलाकार
|
||||
type: noun
|
||||
- english: bank teller
|
||||
hindi: खज़ांची
|
||||
type: noun
|
||||
- english: businessman/businesswoman
|
||||
hindi: व्यवसायी
|
||||
type: noun
|
||||
- english: carpenter
|
||||
hindi: बढ़ई
|
||||
type: noun
|
||||
- english: confectioner
|
||||
hindi: हलवाई
|
||||
type: noun
|
||||
- english: dancer
|
||||
hindi: नर्तकी
|
||||
gender: f
|
||||
type: noun
|
||||
note: There's also नर्तक, the masculine form.
|
||||
- english: diplomat
|
||||
hindi: राजनायक
|
||||
type: noun
|
||||
- english: doctor
|
||||
hindi: चिकित्सक, डाक्टर
|
||||
type: noun
|
||||
- english: driver
|
||||
hindi: चालक, ड्राइवर
|
||||
type: noun
|
||||
- english: engineer
|
||||
hindi: अभियंता
|
||||
type: noun
|
||||
- english: farmer
|
||||
hindi: किसान, कृषक
|
||||
type: noun
|
||||
- english: gardener
|
||||
hindi: माली
|
||||
type: noun
|
||||
- english: hairdresser
|
||||
hindi: नाई
|
||||
type: noun
|
||||
- english: jeweler
|
||||
hindi: जौहरी
|
||||
type: noun
|
||||
note: In addition to being a cognate (in my opinion at least), this was the name
|
||||
of a favorite hotel ("The Johri") in Jaipur's Johri Bazaar (Jeweller's
|
||||
Market).
|
||||
- english: judge
|
||||
hindi: न्यायधीश
|
||||
type: noun
|
||||
- english: laborer
|
||||
hindi: मज़दूर
|
||||
type: noun
|
||||
note: I often confuse this word with मज़बूर (forced), which seems to have
|
||||
obvious etymological overlap.
|
||||
- english: lawyer
|
||||
hindi: वक़ील
|
||||
type: noun
|
||||
- english: mailman
|
||||
hindi: डाकिया
|
||||
type: noun
|
||||
- english: mechanic
|
||||
hindi: मिस्त्री
|
||||
type: noun
|
||||
- english: merchant
|
||||
hindi: व्यापारी
|
||||
type: noun
|
||||
note: In need of a good way to differentiate this from व्यवसायी.
|
||||
- english: musician
|
||||
hindi: संगीतकार
|
||||
type: noun
|
||||
- english: nurse
|
||||
hindi: नर्स
|
||||
type: noun
|
||||
note: It's just straight-up the English word.
|
||||
- english: athlete/player
|
||||
hindi: खिलाड़ी
|
||||
type: noun
|
||||
see_also:
|
||||
- "[खेलना](/vocabulary/general#खेलना), to play"
|
||||
- english: police officer
|
||||
hindi: पुलिस अधिकारी
|
||||
type: noun
|
||||
note: >-
|
||||
The first word is obviously just "police", but I thought it was
|
||||
interesting that buried in the second word is "धिकार" -- "shame" --
|
||||
[prefixed with "अ-"](/grammar/prefixes), as in "without" (in the same
|
||||
way that, say, "amoral" is the opposite of "moral" in English).
|
||||
|
||||
|
||||
I'm not saying the etymology is implying police are without shame: taken
|
||||
as a whole, "अधिकार" just means "authority."
|
||||
- english: potter
|
||||
hindi: कुम्हार
|
||||
type: noun
|
||||
note: Interesting that "Kumar" is a common surname in Hindi, as "Potter" is
|
||||
fairly common in English.
|
||||
- english: president
|
||||
hindi: राष्ट्रपति
|
||||
type: noun
|
||||
note: 'Literally: "Nation Husband". Weird, but I guess kind of makes sense?
|
||||
Would they change this if India ever had a female president? (Could
|
||||
India ever?)'
|
||||
- english: prime minister
|
||||
hindi: प्रधानमंत्री
|
||||
type: noun
|
||||
note: "As you'd hope: प्रधान means \"prime/head/chief\", मंत्री means
|
||||
\"minister.\""
|
||||
- english: publisher
|
||||
hindi: प्रकाशक
|
||||
type: noun
|
||||
- english: salesman
|
||||
hindi: विक्रेता
|
||||
type: noun
|
||||
- english: sculptor
|
||||
hindi: मूर्तिकार
|
||||
type: noun
|
||||
- english: security guard
|
||||
hindi: सुरक्षा कर्मचारी
|
||||
type: noun
|
||||
note: I put the translation as "security guard", but while सुरक्षा does mean
|
||||
"security", कर्मचारी just means "employee."
|
||||
- english: shopkeeper
|
||||
hindi: दुकानदार
|
||||
type: noun
|
||||
- english: singer
|
||||
hindi: गायिका
|
||||
type: noun
|
||||
gender: f
|
||||
note: >-
|
||||
Like नर्तकी / नर्तक, there is also गायक for the masculine form.
|
||||
|
||||
|
||||
Take note of the difference between this word, which has गाना -- to sing
|
||||
-- as its root, and संगीतकार, which has संगीत -- music -- as its root.
|
||||
- english: student
|
||||
hindi: विद्यार्थी
|
||||
type: noun
|
||||
note: >-
|
||||
विद्या = knowledge, अर्थ = meaning.
|
||||
|
||||
|
||||
Feels kind of redundant (I would have expected the compound word to be,
|
||||
say, "knowledge + seeker" or something) but unless I'm missing something
|
||||
... there you have it.
|
||||
- english: tailor
|
||||
hindi: दर्ज़ी
|
||||
type: noun
|
||||
- english: teacher
|
||||
hindi: अध्यापिका
|
||||
type: noun
|
||||
gender: f
|
||||
note: अध्यापक for the masculine form.
|
||||
- english: thief
|
||||
hindi: चोर
|
||||
type: noun
|
||||
note: Mumbai has a चोर बाज़ार, "thief's market."
|
||||
- english: tourist
|
||||
hindi: पर्यटक
|
||||
type: noun
|
||||
- english: washerman
|
||||
hindi: धोबी
|
||||
type: noun
|
||||
note: From धोना, to wash.
|
||||
- english: writer
|
||||
hindi: लेखिका
|
||||
type: noun
|
||||
gender: f
|
||||
note: |-
|
||||
Again, लेखक for masculine form.
|
||||
|
||||
From लिखना, to write.
|
||||
Loading…
Reference in New Issue
Block a user