Compare commits
290 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b7757172b7 | |||
| 2b4cb9dd73 | |||
| 5466f687ac | |||
| d47a71a03b | |||
| 528d088e54 | |||
| c6a4376d7b | |||
| fc8c4aaa45 | |||
| 4baba58dc3 | |||
| d49cf5c82c | |||
| 78f967e5fc | |||
| 4fee9f635e | |||
| f4a3741e9b | |||
| 2a393aca89 | |||
| 5e3602d3a1 | |||
| 77c1c0934b | |||
| 6cf36402ec | |||
| 8e2097da87 | |||
| f37788cf26 | |||
| 6d7a0d6574 | |||
| 905b3ab94d | |||
| 9e4289041e | |||
| de0c8737f3 | |||
| fc0999e271 | |||
| 0b5b072332 | |||
| 25e349a979 | |||
| 8c8c75d18c | |||
| 346422003f | |||
| d48cad5e6a | |||
| 3c3b680f93 | |||
| d7ec692d03 | |||
| 9bdc0e2c60 | |||
| e623271476 | |||
| 8bbb7853ef | |||
| f2781fb5a9 | |||
| 52e8ff6236 | |||
| 1521b22cdd | |||
| 35c9ea3bbc | |||
| 079fdd4841 | |||
| 4337580806 | |||
| 5ea549abf4 | |||
| 5fecba6ff5 | |||
| debad91e29 | |||
| 567c054fe6 | |||
| 54af417ec0 | |||
| 51de695d4e | |||
| f39785096d | |||
| 705b954fe9 | |||
| 78c5667a18 | |||
| befff36133 | |||
| e3b4545dc4 | |||
| 26d3d41790 | |||
| 811eff825c | |||
| 44f9363ecf | |||
| 3b80650972 | |||
| bb1023e51b | |||
| c3bd0c00d0 | |||
| 773589a2f1 | |||
| 94951e8ca9 | |||
| f72a9718a3 | |||
| 7223e65a7a | |||
| 4a91998a44 | |||
| 6af8fd1c26 | |||
| f7178254e0 | |||
| 1f224107a0 | |||
| d24a09074a | |||
| 062cf2f2ab | |||
| 2790d9a592 | |||
| c6dbec5109 | |||
| fdbcbf0bdd | |||
| 2f52df509e | |||
| 5da92d0147 | |||
| c178f84bf0 | |||
| fbc6599ab0 | |||
| 939d1b18d1 | |||
| 4d45a330dc | |||
| b51aac1e46 | |||
| ef4a7a5d19 | |||
| 20e21d0364 | |||
| ab029b2647 | |||
| 436674e08a | |||
| d804d2e126 | |||
| d4cc7dbe98 | |||
| 4b0071fa91 | |||
| b7adae89c7 | |||
| 3c09baf25a | |||
| ea07fd2081 | |||
| c0cdeb6d6b | |||
| e9f3bd453b | |||
| 9b1cec2063 | |||
| c3d6c31aca | |||
| 3237174c2e | |||
| 0c7a1404fa | |||
| d80218a13c | |||
| 472e2f0915 | |||
| 2592eadc9f | |||
| 2acfd46784 | |||
| 51d1675ad6 | |||
| 5d500540c8 | |||
| 6399222908 | |||
| 9e97d532e7 | |||
| 6d0dde523f | |||
| e56d9d05e2 | |||
| 69f647a009 | |||
| 8a382aa308 | |||
| 63d0cb7dee | |||
| 2e2d96016e | |||
| 26da66710e | |||
| 90c294bf26 | |||
| 3a69b72d9d | |||
| bdedcba498 | |||
| 037e727756 | |||
| ce024443ac | |||
| 1141f97b22 | |||
| e036a9692a | |||
| ef58f260e8 | |||
| 210c4ab662 | |||
| c0b622f694 | |||
| 42d70e1a5e | |||
| e9f0c3f0e2 | |||
| a437aad2f8 | |||
| d5d1212a44 | |||
| ab2ac7c3ac | |||
| 3db6375459 | |||
| d5ae136cf1 | |||
| a7c2443f3b | |||
| ae0f36d287 | |||
| 9f019edfeb | |||
| 093c5014ef | |||
| 65d8d0f7bd | |||
| d4dcbb115f | |||
| 0bf87a6cff | |||
| a57121959a | |||
| ba8e81ef5f | |||
| e492703a0c | |||
| 89791793ed | |||
| 278a946980 | |||
| e8d71afc7e | |||
| 1f36631777 | |||
| 1e55ef5dfb | |||
| 03c1770892 | |||
| 3c32474f75 | |||
| 30c0ed9a12 | |||
| 22bf3359f5 | |||
| 53e18f06c7 | |||
| c78d7b0e60 | |||
| ce23214d98 | |||
| e6e262009e | |||
| 2478a7c08a | |||
| d95efef896 | |||
| 698806421f | |||
| 61545b767d | |||
| 87f82eb61e | |||
| b3165c3964 | |||
| 4d1db9cb28 | |||
| 138785cf67 | |||
| 9caa2b6809 | |||
| b00719f2f9 | |||
| c83771bbf8 | |||
| 54344bd1d3 | |||
| 23bb845689 | |||
| aa39737d3b | |||
| 5733b9a894 | |||
| 8f1a955f9a | |||
| ae4306febe | |||
| 989fd5b04a | |||
| acf9a140f6 | |||
| db793bc13d | |||
| 71bf9b90e5 | |||
| 5869ff78be | |||
| 300d9cea4c | |||
| 337220299f | |||
| 12f78f3af7 | |||
| bb2e8602ff | |||
| 78e141f319 | |||
| c701f0f480 | |||
| 1e8e0f6526 | |||
| 9262d7429b | |||
| 36e63d4751 | |||
| f0b5410c13 | |||
| 84cc9d663b | |||
| 2235e64bac | |||
| 2b5cb8ad55 | |||
| 1fdbd80282 | |||
| d0cb76fa02 | |||
| 991670d863 | |||
| 55b78b78ef | |||
| 919cfcb71e | |||
| d60e71d1e0 | |||
| 24abc7e7b3 | |||
| 11033b93fe | |||
| dfbd85c7b3 | |||
| 7eb0683646 | |||
| 9dba2163fa | |||
| 313a14b82c | |||
| 222f149fc2 | |||
| 6ea94989bf | |||
| a35fb7956c | |||
| 6a0c2565b5 | |||
| b7c3b95f51 | |||
| 22292ef79c | |||
| cc59be0834 | |||
| 3eff81e18d | |||
| dbc988f9ae | |||
| 9768de2fbd | |||
| 2e6cff9eeb | |||
| ba504aa266 | |||
| 49fbebd4ce | |||
| 72181ec9eb | |||
| 5005d7cb2d | |||
| 96daed3c84 | |||
| f715149302 | |||
| af30af48e4 | |||
| f0b854171c | |||
| 0644807f51 | |||
| 8e1b3ce847 | |||
| fb4f27a88f | |||
| e5d1f0c6db | |||
| f5c449463b | |||
| 7e3f335e79 | |||
| 726e96130c | |||
| 851ae1c565 | |||
| 5445b48a69 | |||
| 56099539aa | |||
| bb3b8c4ecc | |||
| 2578a35cf7 | |||
| 63d5ca2b5f | |||
| 8c48d2f5ce | |||
| bfed0a1cb9 | |||
| 63bf92f172 | |||
| fc348c1843 | |||
| ea70923e83 | |||
| 3ebae8807c | |||
| ca038c6e14 | |||
| 6ce70aee7e | |||
| de7007269f | |||
| 6dff408766 | |||
| 6621dc6aaa | |||
| 329067e1c2 | |||
| 0f46145f46 | |||
| ba5ddf07ac | |||
| c05074faac | |||
| c4d26d3b7f | |||
| 695c3e6d4f | |||
| dda07dee0a | |||
| 1ac1186319 | |||
| 4cad00079e | |||
| 143f4230c9 | |||
| f714e4498a | |||
| 2e8829ef83 | |||
| 9d74d6fac5 | |||
| 4d5db923ea | |||
| f210180de6 | |||
| 1b5352da0d | |||
| 2cc023b905 | |||
| 993a438e18 | |||
| b1c3c07dac | |||
| 878f7728eb | |||
| 60e4e8b5a9 | |||
| b9ce88e996 | |||
| 60414e9477 | |||
| 7eefb54075 | |||
| cda1be992f | |||
| 9257f82dcf | |||
| 7f09598298 | |||
| 03741f7839 | |||
| 78f629d8d3 | |||
| 68d72fa853 | |||
| 95403516d5 | |||
| f2402eb24d | |||
| 413746dde3 | |||
| c3b23dc1a7 | |||
| 15c19303ff | |||
| 0438d70cae | |||
| 86f042facb | |||
| 6dbd0f8e9d | |||
| c72cda3039 | |||
| d53345ffdd | |||
| b1b9bd61af | |||
| a12f894dff | |||
| 0c60368280 | |||
| 9d36562bab | |||
| d8cf06b577 | |||
| 908df117e6 | |||
| 906fee9c71 | |||
| 32696438d3 | |||
| aa9847bb41 | |||
| ce1c071f3c | |||
| 87efc8bb2d | |||
| 8a6d442544 | |||
| 1848d0c796 |
@@ -1,71 +0,0 @@
|
|||||||
name: Test build
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
tags-ignore:
|
|
||||||
- "*"
|
|
||||||
branches-ignore:
|
|
||||||
- master
|
|
||||||
- ci
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: ubuntu
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v6
|
|
||||||
- name: Checkout submodules
|
|
||||||
run: |
|
|
||||||
sed -i 's#https://code\.gri\.mw#${{ secrets.REPO_HOST }}#g' .gitmodules
|
|
||||||
git submodule update --init
|
|
||||||
- name: Check commit
|
|
||||||
id: check
|
|
||||||
run: |
|
|
||||||
git fetch && git checkout master
|
|
||||||
sha=$(git rev-parse HEAD)
|
|
||||||
[[ "${{ github.sha }}" == "${sha}" ]] && test=false || test=true
|
|
||||||
echo "test=${test}" >> "$FORGEJO_OUTPUT"
|
|
||||||
- name: Restore cargo cache
|
|
||||||
id: cache-cargo-restore
|
|
||||||
uses: actions/cache/restore@v5
|
|
||||||
with:
|
|
||||||
path: |
|
|
||||||
~/.cargo/bin/
|
|
||||||
~/.cargo/registry/index/
|
|
||||||
~/.cargo/registry/cache/
|
|
||||||
~/.cargo/git/db/
|
|
||||||
target/
|
|
||||||
key: ${{ runner.os }}-test-cargo-${{ hashFiles('**/Cargo.lock') }}
|
|
||||||
- name: Setup registry
|
|
||||||
run: |
|
|
||||||
echo -e '[registries.nexus]\nindex = "sparse+${{ secrets.MAVEN_HOST }}/repository/cargo/"\n[registry]\ndefault = "nexus"\n[source.crates-io]\nreplace-with = "nexus"\n[source.nexus]\nregistry = "sparse+${{ secrets.MAVEN_HOST }}/repository/cargo/"' > ~/.cargo/config.toml
|
|
||||||
- name: Build
|
|
||||||
if: ${{ steps.check.outputs.test == 'true' }}
|
|
||||||
run: cargo build --release
|
|
||||||
- name: Save cargo cache
|
|
||||||
uses: actions/cache/save@v5
|
|
||||||
with:
|
|
||||||
path: |
|
|
||||||
~/.cargo/bin/
|
|
||||||
~/.cargo/registry/index/
|
|
||||||
~/.cargo/registry/cache/
|
|
||||||
~/.cargo/git/db/
|
|
||||||
target/
|
|
||||||
key: ${{ steps.cache-cargo-restore.outputs.cache-primary-key }}
|
|
||||||
- name: Telegram Notify Channel
|
|
||||||
if: ${{ steps.check.outputs.test == 'true' && (success() || failure()) }}
|
|
||||||
uses: actions/telegram-notifier@main
|
|
||||||
with:
|
|
||||||
api_url: ${{ secrets.TELEGRAM_API_URL }}
|
|
||||||
token: ${{ secrets.TELEGRAM_BOT_TOKEN }}
|
|
||||||
chat_id: ${{ secrets.TELEGRAM_CHANNEL_ID }}
|
|
||||||
status: ${{ job.status }}
|
|
||||||
notify_fields: "actor,repository,workflow,branch,commit"
|
|
||||||
- name: Telegram Notify Group
|
|
||||||
if: ${{ steps.check.outputs.test == 'true' && (success() || failure()) }}
|
|
||||||
uses: actions/telegram-notifier@main
|
|
||||||
with:
|
|
||||||
api_url: ${{ secrets.TELEGRAM_API_URL }}
|
|
||||||
token: ${{ secrets.TELEGRAM_BOT_TOKEN }}
|
|
||||||
chat_id: ${{ secrets.TELEGRAM_GROUP_ID }}
|
|
||||||
status: ${{ job.status }}
|
|
||||||
notify_fields: "actor,repository,workflow,branch,commit"
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
name: Pull Request
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
types:
|
|
||||||
- closed
|
|
||||||
- opened
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
notify:
|
|
||||||
runs-on: debian-release
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Telegram Notify Channel
|
|
||||||
uses: actions/telegram-notifier@main
|
|
||||||
with:
|
|
||||||
api_url: ${{ secrets.TELEGRAM_API_URL }}
|
|
||||||
token: ${{ secrets.TELEGRAM_BOT_TOKEN }}
|
|
||||||
chat_id: ${{ secrets.TELEGRAM_CHANNEL_ID }}
|
|
||||||
status: ${{ job.status }}
|
|
||||||
notify_fields: "actor,repository"
|
|
||||||
- name: Telegram Notify Group
|
|
||||||
uses: actions/telegram-notifier@main
|
|
||||||
with:
|
|
||||||
api_url: ${{ secrets.TELEGRAM_API_URL }}
|
|
||||||
token: ${{ secrets.TELEGRAM_BOT_TOKEN }}
|
|
||||||
chat_id: ${{ secrets.TELEGRAM_GROUP_ID }}
|
|
||||||
status: ${{ job.status }}
|
|
||||||
notify_fields: "actor,repository"
|
|
||||||
@@ -1,474 +0,0 @@
|
|||||||
name: Release build
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- master
|
|
||||||
- ci
|
|
||||||
tags-ignore:
|
|
||||||
- "*-dev*"
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
version:
|
|
||||||
runs-on: debian-release
|
|
||||||
outputs:
|
|
||||||
v: ${{ steps.version.outputs.v }}
|
|
||||||
pre: ${{ steps.version.outputs.pre }}
|
|
||||||
exists: ${{ steps.check.outputs.exists }}
|
|
||||||
last_tag: ${{ steps.check_prev.outputs.last_tag }}
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v6
|
|
||||||
- name: Checkout submodules
|
|
||||||
run: |
|
|
||||||
sed -i 's#https://code\.gri\.mw#${{ secrets.REPO_HOST }}#g' .gitmodules
|
|
||||||
git submodule update --init
|
|
||||||
- name: Get version
|
|
||||||
id: version
|
|
||||||
run: |
|
|
||||||
ver="$(grep -m 1 -Po 'version = "\K[^"]*' Cargo.toml)"
|
|
||||||
[[ $ver == *"-dev"* ]] && ver=${ver} || ver=${ver}-dev
|
|
||||||
[[ ${{ forgejo.ref_type }} == 'tag' ]] && app_ver=${{ forgejo.ref_name }} || app_ver=v${ver}
|
|
||||||
echo "v=${app_ver}" >> "$FORGEJO_OUTPUT"
|
|
||||||
echo $app_ver
|
|
||||||
[[ ${{ forgejo.ref_type }} == 'tag' ]] && pre='false' || pre='true'
|
|
||||||
echo "pre=${pre}" >> "$FORGEJO_OUTPUT"
|
|
||||||
echo "pre-release: ${pre}"
|
|
||||||
- name: Check existing release
|
|
||||||
if: ${{ forgejo.ref_type == 'tag' }}
|
|
||||||
id: check
|
|
||||||
run: |
|
|
||||||
git fetch --tags
|
|
||||||
dev_sha=$(git rev-parse refs/tags/${{ forgejo.ref_name }}-dev) || :
|
|
||||||
[[ "$(git show-ref)" == *"${dev_sha}"* ]] && exists='true' || exists='false'
|
|
||||||
echo "exists=${exists}" >> "$FORGEJO_OUTPUT"
|
|
||||||
echo ${exists}
|
|
||||||
mkdir release
|
|
||||||
- uses: actions/forgejo-release@v2.11.3
|
|
||||||
if: ${{ forgejo.ref_type == 'tag' && steps.check.outputs.exists == 'true' }}
|
|
||||||
with:
|
|
||||||
direction: download
|
|
||||||
token: ${{ secrets.RELEASE_TOKEN }}
|
|
||||||
tag: "${{ forgejo.ref_name }}-dev"
|
|
||||||
release-dir: ./release
|
|
||||||
- name: Rename files
|
|
||||||
if: ${{ forgejo.ref_type == 'tag' && steps.check.outputs.exists == 'true' }}
|
|
||||||
working-directory: release
|
|
||||||
run: for f in *; do mv "$f" "$(echo "$f" | sed s/-dev-/-/)"; done
|
|
||||||
- name: Delete dev release
|
|
||||||
if: ${{ forgejo.ref_type == 'tag' && steps.check.outputs.exists == 'true' }}
|
|
||||||
uses: actions/delete-release@v1
|
|
||||||
with:
|
|
||||||
release_name: "${{ forgejo.ref_name }}-dev"
|
|
||||||
- name: Check previous release
|
|
||||||
id: check_prev
|
|
||||||
run: |
|
|
||||||
git fetch --tags
|
|
||||||
[[ ${{ forgejo.ref_type }} == 'tag' ]] && skip=0 || skip=1
|
|
||||||
last_tag=$(git describe --abbrev=0 --tags $(git rev-list --tags --skip=${skip} --max-count=1)) || true
|
|
||||||
echo "last_tag=${last_tag}" >> "$FORGEJO_OUTPUT"
|
|
||||||
- uses: actions/forgejo-release@v2.11.3
|
|
||||||
if: ${{ forgejo.ref_type == 'tag' && steps.check.outputs.exists == 'true' }}
|
|
||||||
with:
|
|
||||||
direction: upload
|
|
||||||
token: ${{ secrets.RELEASE_TOKEN }}
|
|
||||||
tag: ${{ forgejo.ref_name }}
|
|
||||||
override: false
|
|
||||||
prerelease: false
|
|
||||||
release-dir: ./release
|
|
||||||
release-notes: "Full Changelog: [${{ steps.check_prev.outputs.last_tag }}...${{ steps.version.outputs.v }}](https://code.gri.mw/${{ forgejo.repository }}/compare/${{ steps.check_prev.outputs.last_tag }}...${{ steps.version.outputs.v }})"
|
|
||||||
|
|
||||||
android:
|
|
||||||
if: ${{ forgejo.ref_type == 'branch' || needs.version.outputs.exists == 'false' }}
|
|
||||||
runs-on: macos
|
|
||||||
needs: version
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v6
|
|
||||||
- name: Checkout submodules
|
|
||||||
run: |
|
|
||||||
sed -i -- 's#https://code\.gri\.mw#${{ secrets.REPO_HOST }}#g' .gitmodules
|
|
||||||
git submodule update --init
|
|
||||||
- name: Restore cargo cache
|
|
||||||
id: cache-cargo-restore
|
|
||||||
uses: actions/cache/restore@v5
|
|
||||||
with:
|
|
||||||
path: |
|
|
||||||
~/.cargo/bin/
|
|
||||||
~/.cargo/registry/index/
|
|
||||||
~/.cargo/registry/cache/
|
|
||||||
~/.cargo/git/db/
|
|
||||||
target/
|
|
||||||
key: grim-android-cargo-${{ hashFiles('**/Cargo.lock') }}
|
|
||||||
- name: Build libs
|
|
||||||
run: |
|
|
||||||
rustup -q update
|
|
||||||
chmod +x scripts/android.sh && ./scripts/android.sh lib ${{ needs.version.outputs.v }}
|
|
||||||
- name: Save cargo cache
|
|
||||||
uses: actions/cache/save@v5
|
|
||||||
with:
|
|
||||||
path: |
|
|
||||||
~/.cargo/bin/
|
|
||||||
~/.cargo/registry/index/
|
|
||||||
~/.cargo/registry/cache/
|
|
||||||
~/.cargo/git/db/
|
|
||||||
target/
|
|
||||||
key: ${{ steps.cache-cargo-restore.outputs.cache-primary-key }}
|
|
||||||
- name: Restore gradle cache
|
|
||||||
id: cache-gradle-restore
|
|
||||||
uses: actions/cache/restore@v5
|
|
||||||
with:
|
|
||||||
path: |
|
|
||||||
~/.gradle/caches
|
|
||||||
~/.gradle/wrapper
|
|
||||||
key: grim-android-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-gradle-
|
|
||||||
- name: Setup build
|
|
||||||
run: |
|
|
||||||
chmod +x android/gradlew
|
|
||||||
echo "${{ secrets.ANDROID_KEYSTORE }}" > release.keystore.txt
|
|
||||||
base64 -d < release.keystore.txt -o android/keystore
|
|
||||||
echo "${{ secrets.ANDROID_KEYSTORE_PROPS }}" > release.keystore.props.txt
|
|
||||||
base64 -d < release.keystore.props.txt -o android/keystore.properties
|
|
||||||
mkdir ~/.gradle && touch ~/.gradle/gradle.properties
|
|
||||||
printf "mavenHost=${{ secrets.MAVEN_LOCAL_HOST }}\n" >> ~/.gradle/gradle.properties
|
|
||||||
printf "mavenUser=${{ secrets.MAVEN_USER }}\n" >> ~/.gradle/gradle.properties
|
|
||||||
printf "mavenPassword=${{ secrets.MAVEN_PASSWORD }}" >> ~/.gradle/gradle.properties
|
|
||||||
- name: Release ARMv7+v8 APK
|
|
||||||
working-directory: android
|
|
||||||
run: |
|
|
||||||
jni_path=app/src/main/jniLibs
|
|
||||||
mv ${jni_path}/x86_64 x86_64
|
|
||||||
./gradlew assembleCiSignedRelease
|
|
||||||
apk_path=app/build/outputs/apk/ci/signedRelease/app-ci-signedRelease.apk
|
|
||||||
name=grim-${{ needs.version.outputs.v }}-android.apk
|
|
||||||
mv ${apk_path} "${name}"
|
|
||||||
- name: Checksum ARM APK
|
|
||||||
working-directory: android
|
|
||||||
run: |
|
|
||||||
name=grim-${{ needs.version.outputs.v }}-android.apk
|
|
||||||
checksum=grim-${{ needs.version.outputs.v }}-android-sha256sum.txt
|
|
||||||
sha256sum "${name}" > "${checksum}"
|
|
||||||
- name: Release x86_64 APK
|
|
||||||
working-directory: android
|
|
||||||
run: |
|
|
||||||
./gradlew clean
|
|
||||||
jni_path=app/src/main/jniLibs
|
|
||||||
rm -rf ${jni_path}/*
|
|
||||||
mv x86_64 ${jni_path}
|
|
||||||
./gradlew assembleCiSignedRelease
|
|
||||||
apk_path=app/build/outputs/apk/ci/signedRelease/app-ci-signedRelease.apk
|
|
||||||
name=grim-${{ needs.version.outputs.v }}-android-x86_64.apk
|
|
||||||
mv ${apk_path} "${name}"
|
|
||||||
- name: Save gradle cache
|
|
||||||
uses: actions/cache/save@v5
|
|
||||||
with:
|
|
||||||
path: |
|
|
||||||
~/.gradle/caches
|
|
||||||
~/.gradle/wrapper
|
|
||||||
key: ${{ steps.cache-gradle-restore.outputs.cache-primary-key }}
|
|
||||||
- name: Checksum x86_64 APK
|
|
||||||
working-directory: android
|
|
||||||
run: |
|
|
||||||
name=grim-${{ needs.version.outputs.v }}-android.apk
|
|
||||||
checksum=grim-${{ needs.version.outputs.v }}-android-x86_64-sha256sum.txt
|
|
||||||
sha256sum "${name}" > "${checksum}"
|
|
||||||
- name: Upload artifacts
|
|
||||||
run: |
|
|
||||||
mkdir release
|
|
||||||
mv android/grim* release
|
|
||||||
tar -czf android.tar.gz release
|
|
||||||
curl -s -u ${{ secrets.MAVEN_USER }}:${{ secrets.MAVEN_PASSWORD }} --upload-file android.tar.gz ${{ secrets.MAVEN_LOCAL_HOST }}/repository/grim-ci/${{ forgejo.repository }}/android.tar.gz
|
|
||||||
|
|
||||||
linux_arm:
|
|
||||||
if: ${{ forgejo.ref_type == 'branch' || needs.version.outputs.exists == 'false' }}
|
|
||||||
runs-on: debian-rust-arm
|
|
||||||
needs: [version]
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v6
|
|
||||||
- name: Checkout submodules
|
|
||||||
run: |
|
|
||||||
sed -i 's#https://code\.gri\.mw#${{ secrets.REPO_HOST }}#g' .gitmodules
|
|
||||||
git submodule update --init
|
|
||||||
- name: Restore cargo cache
|
|
||||||
id: cache-cargo-restore
|
|
||||||
uses: actions/cache/restore@v5
|
|
||||||
with:
|
|
||||||
path: |
|
|
||||||
~/.cargo/bin/
|
|
||||||
~/.cargo/registry/index/
|
|
||||||
~/.cargo/registry/cache/
|
|
||||||
~/.cargo/git/db/
|
|
||||||
target/
|
|
||||||
key: grim-linux-arm-cargo-${{ hashFiles('**/Cargo.lock') }}
|
|
||||||
- name: Setup registry
|
|
||||||
run: |
|
|
||||||
echo -e '[registries.nexus]\nindex = "sparse+${{ secrets.MAVEN_LOCAL_HOST }}/repository/cargo/"\n[registry]\ndefault = "nexus"\n[source.crates-io]\nreplace-with = "nexus"\n[source.nexus]\nregistry = "sparse+${{ secrets.MAVEN_LOCAL_HOST }}/repository/cargo/"' > ~/.cargo/config.toml
|
|
||||||
- name: Release Linux ARM
|
|
||||||
run: |
|
|
||||||
rustup -q update
|
|
||||||
cargo zigbuild --release --target aarch64-unknown-linux-gnu
|
|
||||||
- name: Save cargo cache
|
|
||||||
uses: actions/cache/save@v5
|
|
||||||
with:
|
|
||||||
path: |
|
|
||||||
~/.cargo/bin/
|
|
||||||
~/.cargo/registry/index/
|
|
||||||
~/.cargo/registry/cache/
|
|
||||||
~/.cargo/git/db/
|
|
||||||
target/
|
|
||||||
key: ${{ steps.cache-cargo-restore.outputs.cache-primary-key }}
|
|
||||||
- name: Upload artifacts
|
|
||||||
run: |
|
|
||||||
curl -s -u ${{ secrets.MAVEN_USER }}:${{ secrets.MAVEN_PASSWORD }} --upload-file target/aarch64-unknown-linux-gnu/release/grim ${{ secrets.MAVEN_LOCAL_HOST }}/repository/grim-ci/${{ forgejo.repository }}/grim-linux-arm
|
|
||||||
|
|
||||||
linux_arm_appimage:
|
|
||||||
if: ${{ forgejo.ref_type == 'branch' || needs.version.outputs.exists == 'false' }}
|
|
||||||
runs-on: debian-arm
|
|
||||||
needs: [version, linux_arm]
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v6
|
|
||||||
- name: Download Artifact
|
|
||||||
run: |
|
|
||||||
wget ${{ secrets.MAVEN_LOCAL_HOST }}/repository/grim-ci/${{ forgejo.repository }}/grim-linux-arm
|
|
||||||
- name: AppImage
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
mkdir release
|
|
||||||
chmod +x grim-linux-arm
|
|
||||||
mv grim-linux-arm linux/Grim.AppDir/AppRun
|
|
||||||
ARCH=aarch64 appimagetool linux/Grim.AppDir grim-${{ needs.version.outputs.v }}-linux-arm.AppImage
|
|
||||||
mv grim-${{ needs.version.outputs.v }}-linux-arm.AppImage release/
|
|
||||||
- name: Checksum AppImage ARM
|
|
||||||
working-directory: release
|
|
||||||
run: sha256sum grim-${{ needs.version.outputs.v }}-linux-arm.AppImage > grim-${{ needs.version.outputs.v }}-linux-arm-appimage-sha256sum.txt
|
|
||||||
- name: Upload artifacts
|
|
||||||
run: |
|
|
||||||
tar -czf linux-arm.appimage.tar.gz release
|
|
||||||
curl -s -u ${{ secrets.MAVEN_USER }}:${{ secrets.MAVEN_PASSWORD }} --upload-file linux-arm.appimage.tar.gz ${{ secrets.MAVEN_LOCAL_HOST }}/repository/grim-ci/${{ forgejo.repository }}/linux-arm.appimage.tar.gz
|
|
||||||
|
|
||||||
linux_x86:
|
|
||||||
if: ${{ forgejo.ref_type == 'branch' || needs.version.outputs.exists == 'false' }}
|
|
||||||
runs-on: debian-rust-x86_64
|
|
||||||
needs: [version]
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v6
|
|
||||||
- name: Checkout submodules
|
|
||||||
run: |
|
|
||||||
sed -i 's#https://code\.gri\.mw#${{ secrets.REPO_HOST }}#g' .gitmodules
|
|
||||||
git submodule update --init
|
|
||||||
- name: Restore cargo cache
|
|
||||||
id: cache-cargo-restore
|
|
||||||
uses: actions/cache/restore@v5
|
|
||||||
with:
|
|
||||||
path: |
|
|
||||||
~/.cargo/bin/
|
|
||||||
~/.cargo/registry/index/
|
|
||||||
~/.cargo/registry/cache/
|
|
||||||
~/.cargo/git/db/
|
|
||||||
target/
|
|
||||||
key: grim-linux-arm-cargo-${{ hashFiles('**/Cargo.lock') }}
|
|
||||||
- name: Setup registry
|
|
||||||
run: |
|
|
||||||
echo -e '[registries.nexus]\nindex = "sparse+${{ secrets.MAVEN_LOCAL_HOST }}/repository/cargo/"\n[registry]\ndefault = "nexus"\n[source.crates-io]\nreplace-with = "nexus"\n[source.nexus]\nregistry = "sparse+${{ secrets.MAVEN_LOCAL_HOST }}/repository/cargo/"' > ~/.cargo/config.toml
|
|
||||||
- name: Release Linux x86
|
|
||||||
run: |
|
|
||||||
rustup -q update
|
|
||||||
cargo zigbuild --release --target x86_64-unknown-linux-gnu
|
|
||||||
- name: Save cargo cache
|
|
||||||
uses: actions/cache/save@v5
|
|
||||||
with:
|
|
||||||
path: |
|
|
||||||
~/.cargo/bin/
|
|
||||||
~/.cargo/registry/index/
|
|
||||||
~/.cargo/registry/cache/
|
|
||||||
~/.cargo/git/db/
|
|
||||||
target/
|
|
||||||
key: ${{ steps.cache-cargo-restore.outputs.cache-primary-key }}
|
|
||||||
- name: AppImage x86
|
|
||||||
run: |
|
|
||||||
mkdir release
|
|
||||||
mv target/x86_64-unknown-linux-gnu/release/grim linux/Grim.AppDir/AppRun
|
|
||||||
appimagetool linux/Grim.AppDir grim-${{ needs.version.outputs.v }}-linux-x86_64.AppImage
|
|
||||||
mv grim-${{ needs.version.outputs.v }}-linux-x86_64.AppImage release/
|
|
||||||
- name: Checksum AppImage x86
|
|
||||||
working-directory: release
|
|
||||||
run: sha256sum grim-${{ needs.version.outputs.v }}-linux-x86_64.AppImage > grim-${{ needs.version.outputs.v }}-linux-x86_64-appimage-sha256sum.txt
|
|
||||||
- name: Upload artifacts
|
|
||||||
run: |
|
|
||||||
tar -czf linux-x86_64.appimage.tar.gz release
|
|
||||||
curl -s -u ${{ secrets.MAVEN_USER }}:${{ secrets.MAVEN_PASSWORD }} --upload-file linux-x86_64.appimage.tar.gz ${{ secrets.MAVEN_LOCAL_HOST }}/repository/grim-ci/${{ forgejo.repository }}/linux-x86_64.appimage.tar.gz
|
|
||||||
|
|
||||||
macos:
|
|
||||||
if: ${{ forgejo.ref_type == 'branch' || needs.version.outputs.exists == 'false' }}
|
|
||||||
runs-on: macos
|
|
||||||
needs: [version, android]
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v6
|
|
||||||
- name: Checkout submodules
|
|
||||||
run: |
|
|
||||||
sed -i -- 's#https://code\.gri\.mw#${{ secrets.REPO_HOST }}#g' .gitmodules
|
|
||||||
git submodule update --init
|
|
||||||
- run: mkdir release
|
|
||||||
- name: Restore cargo cache
|
|
||||||
id: cache-cargo-restore
|
|
||||||
uses: actions/cache/restore@v5
|
|
||||||
with:
|
|
||||||
path: |
|
|
||||||
~/.cargo/bin/
|
|
||||||
~/.cargo/registry/index/
|
|
||||||
~/.cargo/registry/cache/
|
|
||||||
~/.cargo/git/db/
|
|
||||||
target/
|
|
||||||
key: grim-macos-cargo-${{ hashFiles('**/Cargo.lock') }}
|
|
||||||
- name: Release MacOS Universal
|
|
||||||
run: |
|
|
||||||
rustup -q update
|
|
||||||
export MACOSX_DEPLOYMENT_TARGET=11.0
|
|
||||||
cargo build --release --target x86_64-apple-darwin
|
|
||||||
cargo build --release --target aarch64-apple-darwin
|
|
||||||
lipo -create -output grim "target/x86_64-apple-darwin/release/grim" "target/aarch64-apple-darwin/release/grim"
|
|
||||||
mv grim macos/Grim.app/Contents/MacOS
|
|
||||||
- name: Save cargo cache
|
|
||||||
uses: actions/cache/save@v5
|
|
||||||
with:
|
|
||||||
path: |
|
|
||||||
~/.cargo/bin/
|
|
||||||
~/.cargo/registry/index/
|
|
||||||
~/.cargo/registry/cache/
|
|
||||||
~/.cargo/git/db/
|
|
||||||
target/
|
|
||||||
key: ${{ steps.cache-cargo-restore.outputs.cache-primary-key }}
|
|
||||||
- name: Archive Universal
|
|
||||||
working-directory: macos
|
|
||||||
run: |
|
|
||||||
zip -r grim-${{ needs.version.outputs.v }}-macos-universal.zip Grim.app
|
|
||||||
mv grim-${{ needs.version.outputs.v }}-macos-universal.zip ../release
|
|
||||||
- name: Checksum Release Universal
|
|
||||||
working-directory: release
|
|
||||||
run: sha256sum grim-${{ needs.version.outputs.v }}-macos-universal.zip > grim-${{ needs.version.outputs.v }}-macos-universal-sha256sum.txt
|
|
||||||
- name: Upload artifacts
|
|
||||||
run: |
|
|
||||||
tar -czf macos.tar.gz release
|
|
||||||
curl -s -u ${{ secrets.MAVEN_USER }}:${{ secrets.MAVEN_PASSWORD }} --upload-file macos.tar.gz ${{ secrets.MAVEN_LOCAL_HOST }}/repository/grim-ci/${{ forgejo.repository }}/macos.tar.gz
|
|
||||||
|
|
||||||
windows:
|
|
||||||
if: ${{ forgejo.ref_type == 'branch' || needs.version.outputs.exists == 'false' }}
|
|
||||||
runs-on: windows
|
|
||||||
needs: [version]
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v6
|
|
||||||
- name: Checkout submodules
|
|
||||||
run: |
|
|
||||||
(Get-content .gitmodules) | Foreach-Object {$_ -replace "https://code.gri.mw", "${{ secrets.REPO_HOST }}"} | Set-Content .gitmodules
|
|
||||||
git submodule update --init
|
|
||||||
- name: Update UpgradeCode
|
|
||||||
shell: powershell
|
|
||||||
run: |
|
|
||||||
$guid = [guid]::NewGuid().ToString()
|
|
||||||
$wix = [xml](Get-Content wix/main.wxs)
|
|
||||||
$wix.Wix.Product.UpgradeCode = $guid
|
|
||||||
$wix.Save("wix/main.wxs")
|
|
||||||
Get-Content wix/main.wxs
|
|
||||||
- run: mkdir release
|
|
||||||
- name: Release Windows x86
|
|
||||||
run: |
|
|
||||||
rustup -q update
|
|
||||||
cargo wix -p grim -o grim-${{ needs.version.outputs.v }}-win-x86_64.msi --nocapture
|
|
||||||
mv grim-${{ needs.version.outputs.v }}-win-x86_64.msi release\
|
|
||||||
Compress-Archive -Path target\release\grim.exe -DestinationPath grim-${{ needs.version.outputs.v }}-win-x86_64.zip
|
|
||||||
mv grim-${{ needs.version.outputs.v }}-win-x86_64.zip release\
|
|
||||||
- name: Checksum Archive x86
|
|
||||||
working-directory: release
|
|
||||||
run: |
|
|
||||||
certutil -hashfile grim-${{ needs.version.outputs.v }}-win-x86_64.msi SHA256 > grim-${{ needs.version.outputs.v }}-win-x86_64-msi-sha256sum.txt
|
|
||||||
certutil -hashfile grim-${{ needs.version.outputs.v }}-win-x86_64.zip SHA256 > grim-${{ needs.version.outputs.v }}-win-x86_64-sha256sum.txt
|
|
||||||
- name: Upload artifacts
|
|
||||||
run: |
|
|
||||||
tar -czf windows.tar.gz release
|
|
||||||
Remove-Item alias:curl
|
|
||||||
curl -s -u ${{ secrets.MAVEN_USER }}:${{ secrets.MAVEN_PASSWORD }} --upload-file windows.tar.gz ${{ secrets.MAVEN_LOCAL_HOST }}/repository/grim-ci/${{ forgejo.repository }}/windows.tar.gz
|
|
||||||
|
|
||||||
release:
|
|
||||||
if: ${{ forgejo.ref_type == 'branch' || needs.version.outputs.exists == 'false' }}
|
|
||||||
runs-on: debian-release
|
|
||||||
needs: [version, android, linux_x86, linux_arm_appimage, macos, windows]
|
|
||||||
steps:
|
|
||||||
- name: Download All Artifacts
|
|
||||||
run: |
|
|
||||||
curl -s -o android.tar.gz -u ${{ secrets.MAVEN_USER }}:${{ secrets.MAVEN_PASSWORD }} ${{ secrets.MAVEN_HOST }}/repository/grim-ci/${{ forgejo.repository }}/android.tar.gz
|
|
||||||
tar -xzf android.tar.gz
|
|
||||||
rm android.tar.gz
|
|
||||||
curl -s -o linux-x86_64.appimage.tar.gz -u ${{ secrets.MAVEN_USER }}:${{ secrets.MAVEN_PASSWORD }} ${{ secrets.MAVEN_HOST }}/repository/grim-ci/${{ forgejo.repository }}/linux-x86_64.appimage.tar.gz
|
|
||||||
tar -xzf linux-x86_64.appimage.tar.gz
|
|
||||||
rm linux-x86_64.appimage.tar.gz
|
|
||||||
curl -s -o linux-arm.appimage.tar.gz -u ${{ secrets.MAVEN_USER }}:${{ secrets.MAVEN_PASSWORD }} ${{ secrets.MAVEN_HOST }}/repository/grim-ci/${{ forgejo.repository }}/linux-arm.appimage.tar.gz
|
|
||||||
tar -xzf linux-arm.appimage.tar.gz
|
|
||||||
rm linux-arm.appimage.tar.gz
|
|
||||||
curl -s -o macos.tar.gz -u ${{ secrets.MAVEN_USER }}:${{ secrets.MAVEN_PASSWORD }} ${{ secrets.MAVEN_HOST }}/repository/grim-ci/${{ forgejo.repository }}/macos.tar.gz
|
|
||||||
tar -xzf macos.tar.gz
|
|
||||||
rm macos.tar.gz
|
|
||||||
curl -s -o windows.tar.gz -u ${{ secrets.MAVEN_USER }}:${{ secrets.MAVEN_PASSWORD }} ${{ secrets.MAVEN_HOST }}/repository/grim-ci/${{ forgejo.repository }}/windows.tar.gz
|
|
||||||
tar -xzf windows.tar.gz
|
|
||||||
rm windows.tar.gz
|
|
||||||
- name: Upload release to Forgejo
|
|
||||||
uses: actions/forgejo-release@v2.11.3
|
|
||||||
with:
|
|
||||||
direction: upload
|
|
||||||
token: ${{ secrets.RELEASE_TOKEN }}
|
|
||||||
tag: ${{ needs.version.outputs.v }}
|
|
||||||
override: true
|
|
||||||
prerelease: ${{ needs.version.outputs.pre == 'true' }}
|
|
||||||
release-dir: release
|
|
||||||
release-notes: "Full Changelog: [${{ needs.version.outputs.last_tag }}...${{ needs.version.outputs.v }}](https://code.gri.mw/${{ forgejo.repository }}/compare/${{ needs.version.outputs.last_tag }}...${{ needs.version.outputs.v }})"
|
|
||||||
- name: Telegram Notify Channel
|
|
||||||
if: always()
|
|
||||||
uses: actions/telegram-notifier@main
|
|
||||||
with:
|
|
||||||
api_url: ${{ secrets.TELEGRAM_API_URL }}
|
|
||||||
token: ${{ secrets.TELEGRAM_BOT_TOKEN }}
|
|
||||||
chat_id: ${{ secrets.TELEGRAM_CHANNEL_ID }}
|
|
||||||
status: ${{ job.status }}
|
|
||||||
notify_fields: "actor,repository,workflow,branch,commit"
|
|
||||||
- name: Telegram Notify Group
|
|
||||||
if: always()
|
|
||||||
uses: actions/telegram-notifier@main
|
|
||||||
with:
|
|
||||||
api_url: ${{ secrets.TELEGRAM_API_URL }}
|
|
||||||
token: ${{ secrets.TELEGRAM_BOT_TOKEN }}
|
|
||||||
chat_id: ${{ secrets.TELEGRAM_GROUP_ID }}
|
|
||||||
status: ${{ job.status }}
|
|
||||||
notify_fields: "actor,repository,workflow,branch,commit"
|
|
||||||
|
|
||||||
release-telegram:
|
|
||||||
runs-on: debian-release
|
|
||||||
needs: [version, release]
|
|
||||||
steps:
|
|
||||||
- name: Download All Artifacts
|
|
||||||
run: |
|
|
||||||
mkdir release
|
|
||||||
cd release
|
|
||||||
wget ${{ secrets.REPO_HOST }}/${{ forgejo.repository }}/releases/download/${{ needs.version.outputs.v }}/grim-${{ needs.version.outputs.v }}-android.apk
|
|
||||||
wget ${{ secrets.REPO_HOST }}/${{ forgejo.repository }}/releases/download/${{ needs.version.outputs.v }}/grim-${{ needs.version.outputs.v }}-android-x86_64.apk
|
|
||||||
wget ${{ secrets.REPO_HOST }}/${{ forgejo.repository }}/releases/download/${{ needs.version.outputs.v }}/grim-${{ needs.version.outputs.v }}-linux-arm.AppImage
|
|
||||||
wget ${{ secrets.REPO_HOST }}/${{ forgejo.repository }}/releases/download/${{ needs.version.outputs.v }}/grim-${{ needs.version.outputs.v }}-linux-x86_64.AppImage
|
|
||||||
wget ${{ secrets.REPO_HOST }}/${{ forgejo.repository }}/releases/download/${{ needs.version.outputs.v }}/grim-${{ needs.version.outputs.v }}-macos-universal.zip
|
|
||||||
wget ${{ secrets.REPO_HOST }}/${{ forgejo.repository }}/releases/download/${{ needs.version.outputs.v }}/grim-${{ needs.version.outputs.v }}-win-x86_64.msi
|
|
||||||
wget ${{ secrets.REPO_HOST }}/${{ forgejo.repository }}/releases/download/${{ needs.version.outputs.v }}/grim-${{ needs.version.outputs.v }}-win-x86_64.zip
|
|
||||||
- name: Upload files to Telegram
|
|
||||||
uses: actions/telegram-send-file@main
|
|
||||||
with:
|
|
||||||
api_url: ${{ secrets.TELEGRAM_API_URL }}
|
|
||||||
chat_ids: |
|
|
||||||
${{ secrets.TELEGRAM_CHANNEL_ID }}
|
|
||||||
${{ secrets.TELEGRAM_GROUP_ID }}
|
|
||||||
body: '🎁 Release <a href="https://code.gri.mw/${{ forgejo.repository }}/releases">${{ needs.version.outputs.v }}</a> is ready!'
|
|
||||||
token: ${{ secrets.TELEGRAM_BOT_TOKEN }}
|
|
||||||
pin: true
|
|
||||||
files: |
|
|
||||||
release/grim-${{ needs.version.outputs.v }}-android.apk
|
|
||||||
release/grim-${{ needs.version.outputs.v }}-android-x86_64.apk
|
|
||||||
release/grim-${{ needs.version.outputs.v }}-linux-arm.AppImage
|
|
||||||
release/grim-${{ needs.version.outputs.v }}-linux-x86_64.AppImage
|
|
||||||
release/grim-${{ needs.version.outputs.v }}-macos-universal.zip
|
|
||||||
release/grim-${{ needs.version.outputs.v }}-win-x86_64.msi
|
|
||||||
release/grim-${{ needs.version.outputs.v }}-win-x86_64.zip
|
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
name: Fetch nip44 crate
|
||||||
|
description: >
|
||||||
|
Clone the nip44 crate (v2 + v3 encryption) from our mirror
|
||||||
|
(github.com/2ro/nip44, branch `v3`) into ../nip44, so the
|
||||||
|
`nip44 = { path = "../nip44" }` dependency resolves.
|
||||||
|
|
||||||
|
runs:
|
||||||
|
using: composite
|
||||||
|
steps:
|
||||||
|
- name: Clone nip44
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
DEST="$(dirname "$GITHUB_WORKSPACE")/nip44"
|
||||||
|
if [ -e "$DEST/Cargo.toml" ]; then
|
||||||
|
echo "nip44 already present at $DEST"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
rm -rf "$DEST"
|
||||||
|
git clone --branch v3 --depth 1 https://github.com/2ro/nip44.git "$DEST"
|
||||||
|
echo "nip44 cloned from 2ro/nip44@v3 -> $DEST"
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
HOST=https://code.gri.mw
|
|
||||||
REPO_NAME=$1
|
|
||||||
TAG=$2
|
|
||||||
DOWNLOAD_URL=${HOST}/${REPO_NAME}/releases/download/${TAG}
|
|
||||||
|
|
||||||
FILES=( "grim-${TAG}-android.apk" "grim-${TAG}-android-x86_64.apk" "grim-${TAG}-linux-arm.AppImage" "grim-${TAG}-linux-x86_64.AppImage" "grim-${TAG}-macos-universal.zip" "grim-${TAG}-win-x86_64.msi" "grim-${TAG}-win-x86_64.zip" )
|
|
||||||
|
|
||||||
# Download release files
|
|
||||||
for f in "${FILES[@]}"; do
|
|
||||||
wget -q ${DOWNLOAD_URL}/${f}
|
|
||||||
echo Downloading ${f}...
|
|
||||||
while [ ! -f ${f} ]; do
|
|
||||||
sleep 5
|
|
||||||
echo Retry ${f}...
|
|
||||||
wget -q ${DOWNLOAD_URL}/${f}
|
|
||||||
done
|
|
||||||
done
|
|
||||||
|
|
||||||
# Save release notes
|
|
||||||
INFO_URL=${HOST}/api/v1/repos/${REPO_NAME}/releases/tags/${TAG}
|
|
||||||
curl -s "${INFO_URL}" | jq -r '.body' > release_notes.txt
|
|
||||||
@@ -9,6 +9,8 @@ jobs:
|
|||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
submodules: recursive
|
submodules: recursive
|
||||||
|
# nip44 is a path dep on ../nip44; materialize it before building.
|
||||||
|
- uses: ./.github/actions/fetch-nip44
|
||||||
- name: Release build
|
- name: Release build
|
||||||
run: cargo build --release
|
run: cargo build --release
|
||||||
|
|
||||||
@@ -19,9 +21,10 @@ jobs:
|
|||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
submodules: recursive
|
submodules: recursive
|
||||||
|
- uses: ./.github/actions/fetch-nip44
|
||||||
- name: Release build
|
- name: Release build
|
||||||
run: cargo build --release
|
run: cargo build --release
|
||||||
|
|
||||||
macos:
|
macos:
|
||||||
name: MacOS Build
|
name: MacOS Build
|
||||||
runs-on: macos-latest
|
runs-on: macos-latest
|
||||||
@@ -29,5 +32,6 @@ jobs:
|
|||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
submodules: recursive
|
submodules: recursive
|
||||||
|
- uses: ./.github/actions/fetch-nip44
|
||||||
- name: Release build
|
- name: Release build
|
||||||
run: cargo build --release
|
run: cargo build --release
|
||||||
|
|||||||
@@ -1,31 +1,145 @@
|
|||||||
|
# Release builds on native runners — one per platform, no cross-compilation
|
||||||
|
# (nokhwa's camera backends want each platform's own SDK; see NEXT-STEPS judgment).
|
||||||
|
#
|
||||||
|
# Manually triggered (Actions → Release → Run workflow) against an existing tag
|
||||||
|
# until a run has been validated end-to-end; then this can move to a tag trigger.
|
||||||
|
# Android is built locally via scripts/android.sh for now — the gradle `ci`
|
||||||
|
# flavor expects maven credentials this repository does not carry.
|
||||||
name: Release
|
name: Release
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
# macOS builds on its native runner automatically when a release is published
|
||||||
tags:
|
# (the macOS job has no dispatch-only gate). Linux/Windows stay dispatch-only —
|
||||||
- "v*.*.*"
|
# they are built locally and uploaded with the release; run the workflow by hand
|
||||||
|
# to (re)build those on runners against an existing tag.
|
||||||
|
release:
|
||||||
|
types: [published]
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
tag:
|
||||||
|
description: "Existing release tag to build and upload artifacts to (e.g. build27)"
|
||||||
|
required: true
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
env:
|
||||||
|
TAG: ${{ inputs.tag || github.event.release.tag_name }}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
create_release:
|
linux:
|
||||||
name: Create Release
|
name: Linux x86_64
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
# Built locally and uploaded with the release; only run on manual dispatch.
|
||||||
contents: write
|
if: github.event_name == 'workflow_dispatch'
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- uses: actions/checkout@v6
|
||||||
uses: actions/checkout@v4
|
|
||||||
- name: Download release
|
|
||||||
run: chmod +x .github/download_release.sh && .github/download_release.sh GUI/grim ${{ github.ref_name }}
|
|
||||||
- name: Release
|
|
||||||
uses: softprops/action-gh-release@v2
|
|
||||||
with:
|
with:
|
||||||
body_path: release_notes.txt
|
ref: ${{ inputs.tag || github.event.release.tag_name }}
|
||||||
overwrite_files: true
|
submodules: recursive
|
||||||
|
- uses: ./.github/actions/fetch-nip44
|
||||||
|
- name: Build
|
||||||
|
shell: bash
|
||||||
|
run: GOBLIN_BUILD="${TAG#build}" cargo build --release
|
||||||
|
- name: Package
|
||||||
|
run: |
|
||||||
|
tar -C target/release -czf "goblin-$TAG-linux-x86_64.tar.gz" goblin
|
||||||
|
sha256sum "goblin-$TAG-linux-x86_64.tar.gz" > "goblin-$TAG-linux-x86_64-sha256sum.txt"
|
||||||
|
- uses: softprops/action-gh-release@v2
|
||||||
|
with:
|
||||||
|
tag_name: ${{ inputs.tag || github.event.release.tag_name }}
|
||||||
files: |
|
files: |
|
||||||
grim-${{ github.ref_name }}-android.apk
|
goblin-${{ inputs.tag || github.event.release.tag_name }}-linux-x86_64.tar.gz
|
||||||
grim-${{ github.ref_name }}-android-x86_64.apk
|
goblin-${{ inputs.tag || github.event.release.tag_name }}-linux-x86_64-sha256sum.txt
|
||||||
grim-${{ github.ref_name }}-linux-arm.AppImage
|
|
||||||
grim-${{ github.ref_name }}-linux-x86_64.AppImage
|
windows:
|
||||||
grim-${{ github.ref_name }}-macos-universal.zip
|
name: Windows x86_64 (MSVC)
|
||||||
grim-${{ github.ref_name }}-win-x86_64.msi
|
runs-on: windows-latest
|
||||||
grim-${{ github.ref_name }}-win-x86_64.zip
|
# Built locally and uploaded with the release; only run on manual dispatch.
|
||||||
|
if: github.event_name == 'workflow_dispatch'
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v6
|
||||||
|
with:
|
||||||
|
ref: ${{ inputs.tag || github.event.release.tag_name }}
|
||||||
|
submodules: recursive
|
||||||
|
- uses: ./.github/actions/fetch-nip44
|
||||||
|
- name: Build
|
||||||
|
shell: bash
|
||||||
|
run: GOBLIN_BUILD="${TAG#build}" cargo build --release
|
||||||
|
- name: Build MSI installer (cargo-wix / WiX 3 — same packaging as GRIM)
|
||||||
|
shell: pwsh
|
||||||
|
run: |
|
||||||
|
# The .msi is built from wix/main.wxs (the cargo-wix default template:
|
||||||
|
# WixUI_Minimal + launch-after-install), so `cargo wix` wires up the
|
||||||
|
# WixUI/WixUtil extensions, cultures and CargoTargetBinDir for us. The
|
||||||
|
# installer + shortcuts + Add/Remove-Programs entry carry wix/Product.ico
|
||||||
|
# (the yellow Goblin icon). --no-build reuses the release exe above so the
|
||||||
|
# embedded GOBLIN_BUILD number is preserved.
|
||||||
|
cargo install cargo-wix --locked
|
||||||
|
$wix = Get-ChildItem 'C:\Program Files (x86)' -Directory -Filter 'WiX Toolset v3*' -ErrorAction SilentlyContinue | Select-Object -Last 1
|
||||||
|
if (-not $wix) {
|
||||||
|
choco install wixtoolset --no-progress -y | Out-Null
|
||||||
|
$wix = Get-ChildItem 'C:\Program Files (x86)' -Directory -Filter 'WiX Toolset v3*' | Select-Object -Last 1
|
||||||
|
}
|
||||||
|
$env:WIX = "$($wix.FullName)\"
|
||||||
|
$env:PATH = "$($wix.FullName)\bin;$env:PATH"
|
||||||
|
$msi = "goblin-$env:TAG-win-x86_64.msi"
|
||||||
|
cargo wix --no-build --nocapture -o "$msi"
|
||||||
|
if ($LASTEXITCODE -ne 0 -or -not (Test-Path "$msi")) { throw "cargo wix failed to produce $msi" }
|
||||||
|
(Get-FileHash "$msi" -Algorithm SHA256).Hash.ToLower() + " $msi" | Out-File -Encoding ascii "goblin-$env:TAG-win-x86_64-msi-sha256sum.txt"
|
||||||
|
- name: Package portable zip
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
7z a "goblin-$TAG-win-x86_64.zip" ./target/release/goblin.exe
|
||||||
|
sha256sum "goblin-$TAG-win-x86_64.zip" > "goblin-$TAG-win-x86_64-sha256sum.txt"
|
||||||
|
- uses: softprops/action-gh-release@v2
|
||||||
|
with:
|
||||||
|
tag_name: ${{ inputs.tag || github.event.release.tag_name }}
|
||||||
|
files: |
|
||||||
|
goblin-${{ inputs.tag || github.event.release.tag_name }}-win-x86_64.msi
|
||||||
|
goblin-${{ inputs.tag || github.event.release.tag_name }}-win-x86_64-msi-sha256sum.txt
|
||||||
|
goblin-${{ inputs.tag || github.event.release.tag_name }}-win-x86_64.zip
|
||||||
|
goblin-${{ inputs.tag || github.event.release.tag_name }}-win-x86_64-sha256sum.txt
|
||||||
|
|
||||||
|
macos:
|
||||||
|
name: macOS universal
|
||||||
|
runs-on: macos-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v6
|
||||||
|
with:
|
||||||
|
ref: ${{ inputs.tag || github.event.release.tag_name }}
|
||||||
|
submodules: recursive
|
||||||
|
- uses: ./.github/actions/fetch-nip44
|
||||||
|
- name: Build both architectures
|
||||||
|
run: |
|
||||||
|
export GOBLIN_BUILD="${TAG#build}"
|
||||||
|
rustup target add aarch64-apple-darwin x86_64-apple-darwin
|
||||||
|
cargo build --release --target aarch64-apple-darwin
|
||||||
|
cargo build --release --target x86_64-apple-darwin
|
||||||
|
- name: Universal binary into Goblin.app bundle
|
||||||
|
run: |
|
||||||
|
# Combine both arches into one universal Mach-O and drop it into the
|
||||||
|
# app bundle's executable slot (CFBundleExecutable=goblin).
|
||||||
|
lipo -create -output goblin \
|
||||||
|
target/aarch64-apple-darwin/release/goblin \
|
||||||
|
target/x86_64-apple-darwin/release/goblin
|
||||||
|
cp goblin macos/Goblin.app/Contents/MacOS/goblin
|
||||||
|
chmod +x macos/Goblin.app/Contents/MacOS/goblin
|
||||||
|
# Drop the placeholder that kept the empty dir tracked in git.
|
||||||
|
rm -f macos/Goblin.app/Contents/MacOS/.gitignore
|
||||||
|
# Ad-hoc sign (no Apple cert in CI). REQUIRED on Apple Silicon: lipo
|
||||||
|
# strips the per-arch signatures cargo/ld add, and an unsigned arm64
|
||||||
|
# Mach-O is killed by the OS. Ad-hoc gives a valid (if unidentified)
|
||||||
|
# signature; users still right-click → Open past Gatekeeper.
|
||||||
|
codesign --force --sign - macos/Goblin.app/Contents/MacOS/goblin
|
||||||
|
codesign --force --sign - macos/Goblin.app
|
||||||
|
# ditto is the macOS-correct way to zip an .app (preserves the bundle
|
||||||
|
# layout, symlinks and permissions; plain `zip` mangles bundles).
|
||||||
|
ditto -c -k --keepParent macos/Goblin.app "goblin-$TAG-macos-universal.zip"
|
||||||
|
shasum -a 256 "goblin-$TAG-macos-universal.zip" > "goblin-$TAG-macos-universal-sha256sum.txt"
|
||||||
|
- uses: softprops/action-gh-release@v2
|
||||||
|
with:
|
||||||
|
tag_name: ${{ inputs.tag || github.event.release.tag_name }}
|
||||||
|
files: |
|
||||||
|
goblin-${{ inputs.tag || github.event.release.tag_name }}-macos-universal.zip
|
||||||
|
goblin-${{ inputs.tag || github.event.release.tag_name }}-macos-universal-sha256sum.txt
|
||||||
|
|||||||
@@ -1,170 +0,0 @@
|
|||||||
name: Release
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
tags:
|
|
||||||
- "v*.*.*"
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
linux_release:
|
|
||||||
name: Linux Release
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
- name: Download appimagetools
|
|
||||||
run: |
|
|
||||||
wget https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage
|
|
||||||
chmod +x appimagetool-x86_64.AppImage
|
|
||||||
sudo apt install libfuse2
|
|
||||||
- name: Zig Setup
|
|
||||||
uses: goto-bus-stop/setup-zig@v2
|
|
||||||
with:
|
|
||||||
version: 0.12.1
|
|
||||||
- name: Install cargo-zigbuild
|
|
||||||
run: cargo install cargo-zigbuild
|
|
||||||
- name: Release x86
|
|
||||||
run: cargo zigbuild --release --target x86_64-unknown-linux-gnu
|
|
||||||
- name: Release ARM
|
|
||||||
run: |
|
|
||||||
rustup target add aarch64-unknown-linux-gnu
|
|
||||||
cargo zigbuild --release --target aarch64-unknown-linux-gnu
|
|
||||||
- name: AppImage x86
|
|
||||||
run: |
|
|
||||||
cp target/x86_64-unknown-linux-gnu/release/grim linux/Grim.AppDir/AppRun
|
|
||||||
./appimagetool-x86_64.AppImage linux/Grim.AppDir target/x86_64-unknown-linux-gnu/release/grim-${{ github.ref_name }}-linux-x86_64.AppImage
|
|
||||||
- name: Checksum AppImage x86
|
|
||||||
working-directory: target/x86_64-unknown-linux-gnu/release
|
|
||||||
shell: bash
|
|
||||||
run: sha256sum grim-${{ github.ref_name }}-linux-x86_64.AppImage > grim-${{ github.ref_name }}-linux-x86_64-appimage-sha256sum.txt
|
|
||||||
- name: AppImage ARM
|
|
||||||
run: |
|
|
||||||
cp target/aarch64-unknown-linux-gnu/release/grim linux/Grim.AppDir/AppRun
|
|
||||||
./appimagetool-x86_64.AppImage linux/Grim.AppDir target/aarch64-unknown-linux-gnu/release/grim-${{ github.ref_name }}-linux-arm.AppImage
|
|
||||||
- name: Checksum AppImage ARM
|
|
||||||
working-directory: target/aarch64-unknown-linux-gnu/release
|
|
||||||
shell: bash
|
|
||||||
run: sha256sum grim-${{ github.ref_name }}-linux-arm.AppImage > grim-${{ github.ref_name }}-linux-arm-appimage-sha256sum.txt
|
|
||||||
- name: Release
|
|
||||||
uses: softprops/action-gh-release@v1
|
|
||||||
with:
|
|
||||||
files: |
|
|
||||||
target/x86_64-unknown-linux-gnu/release/grim-${{ github.ref_name }}-linux-x86_64.AppImage
|
|
||||||
target/x86_64-unknown-linux-gnu/release/grim-${{ github.ref_name }}-linux-x86_64-appimage-sha256sum.txt
|
|
||||||
target/aarch64-unknown-linux-gnu/release/grim-${{ github.ref_name }}-linux-arm.AppImage
|
|
||||||
target/aarch64-unknown-linux-gnu/release/grim-${{ github.ref_name }}-linux-arm-appimage-sha256sum.txt
|
|
||||||
|
|
||||||
windows_release:
|
|
||||||
name: Windows Release
|
|
||||||
runs-on: windows-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
- name: Build release
|
|
||||||
run: cargo build --release
|
|
||||||
- name: Archive release
|
|
||||||
uses: vimtor/action-zip@v1
|
|
||||||
with:
|
|
||||||
files: target/release/grim.exe
|
|
||||||
dest: target/release/grim-${{ github.ref_name }}-win-x86_64.zip
|
|
||||||
- name: Checksum release
|
|
||||||
working-directory: target/release
|
|
||||||
shell: bash
|
|
||||||
run: sha256sum grim-${{ github.ref_name }}-win-x86_64.zip > grim-${{ github.ref_name }}-win-x86_64-sha256sum.txt
|
|
||||||
- name: Install cargo-wix
|
|
||||||
run: cargo install cargo-wix
|
|
||||||
- name: Run cargo-wix
|
|
||||||
run: cargo wix -p grim -o ./target/wix/grim-${{ github.ref_name }}-win-x86_64.msi --nocapture
|
|
||||||
- name: Checksum msi
|
|
||||||
working-directory: target/wix
|
|
||||||
shell: bash
|
|
||||||
run: sha256sum grim-${{ github.ref_name }}-win-x86_64.msi > grim-${{ github.ref_name }}-win-x86_64-msi-sha256sum.txt
|
|
||||||
- name: Release
|
|
||||||
uses: softprops/action-gh-release@v1
|
|
||||||
with:
|
|
||||||
files: |
|
|
||||||
target/release/grim-${{ github.ref_name }}-win-x86_64.zip
|
|
||||||
target/release/grim-${{ github.ref_name }}-win-x86_64-sha256sum.txt
|
|
||||||
target/wix/grim-${{ github.ref_name }}-win-x86_64.msi
|
|
||||||
target/wix/grim-${{ github.ref_name }}-win-x86_64-msi-sha256sum.txt
|
|
||||||
|
|
||||||
macos_release:
|
|
||||||
name: MacOS Release
|
|
||||||
runs-on: macos-12
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
- name: Install coreutils
|
|
||||||
run: brew install coreutils
|
|
||||||
- name: Zig Setup
|
|
||||||
uses: goto-bus-stop/setup-zig@v2
|
|
||||||
with:
|
|
||||||
version: 0.12.1
|
|
||||||
- name: Install cargo-zigbuild
|
|
||||||
run: cargo install cargo-zigbuild
|
|
||||||
- name: Download SDK
|
|
||||||
run: wget https://github.com/phracker/MacOSX-SDKs/releases/download/11.3/MacOSX11.0.sdk.tar.xz
|
|
||||||
- name: Setup SDK env
|
|
||||||
run: tar xf ${{ github.workspace }}/MacOSX11.0.sdk.tar.xz && echo "SDKROOT=${{ github.workspace }}/MacOSX11.0.sdk" >> $GITHUB_ENV
|
|
||||||
- name: Setup platform env
|
|
||||||
run: echo "MACOSX_DEPLOYMENT_TARGET=11.0" >> $GITHUB_ENV
|
|
||||||
- name: Release x86
|
|
||||||
run: |
|
|
||||||
rustup target add x86_64-apple-darwin
|
|
||||||
cargo zigbuild --release --target x86_64-apple-darwin
|
|
||||||
yes | cp -rf target/x86_64-apple-darwin/release/grim macos/Grim.app/Contents/MacOS
|
|
||||||
- name: Archive x86
|
|
||||||
run: |
|
|
||||||
cd macos
|
|
||||||
zip -r grim-${{ github.ref_name }}-macos-x86_64.zip Grim.app
|
|
||||||
mv grim-${{ github.ref_name }}-macos-x86_64.zip ../target/x86_64-apple-darwin/release
|
|
||||||
cd ..
|
|
||||||
- name: Checksum Release x86
|
|
||||||
working-directory: target/x86_64-apple-darwin/release
|
|
||||||
shell: bash
|
|
||||||
run: sha256sum grim-${{ github.ref_name }}-macos-x86_64.zip > grim-${{ github.ref_name }}-macos-x86_64-sha256sum.txt
|
|
||||||
- name: Release ARM
|
|
||||||
run: |
|
|
||||||
rustup target add aarch64-apple-darwin
|
|
||||||
cargo zigbuild --release --target aarch64-apple-darwin
|
|
||||||
yes | cp -rf target/aarch64-apple-darwin/release/grim macos/Grim.app/Contents/MacOS
|
|
||||||
- name: Archive ARM
|
|
||||||
run: |
|
|
||||||
cd macos
|
|
||||||
zip -r grim-${{ github.ref_name }}-macos-arm.zip Grim.app
|
|
||||||
mv grim-${{ github.ref_name }}-macos-arm.zip ../target/aarch64-apple-darwin/release
|
|
||||||
cd ..
|
|
||||||
- name: Checksum Release ARM
|
|
||||||
working-directory: target/aarch64-apple-darwin/release
|
|
||||||
shell: bash
|
|
||||||
run: sha256sum grim-${{ github.ref_name }}-macos-arm.zip > grim-${{ github.ref_name }}-macos-arm-sha256sum.txt
|
|
||||||
- name: Release Universal
|
|
||||||
run: |
|
|
||||||
cargo zigbuild --release --target universal2-apple-darwin
|
|
||||||
yes | cp -rf target/universal2-apple-darwin/release/grim macos/Grim.app/Contents/MacOS
|
|
||||||
- name: Archive Universal
|
|
||||||
run: |
|
|
||||||
cd macos
|
|
||||||
zip -r grim-${{ github.ref_name }}-macos-universal.zip Grim.app
|
|
||||||
mv grim-${{ github.ref_name }}-macos-universal.zip ../target/universal2-apple-darwin/release
|
|
||||||
cd ..
|
|
||||||
- name: Checksum Release Universal
|
|
||||||
working-directory: target/universal2-apple-darwin/release
|
|
||||||
shell: pwsh
|
|
||||||
run: sha256sum grim-${{ github.ref_name }}-macos-universal.zip > grim-${{ github.ref_name }}-macos-universal-sha256sum.txt
|
|
||||||
- name: Release
|
|
||||||
uses: softprops/action-gh-release@v1
|
|
||||||
with:
|
|
||||||
files: |
|
|
||||||
target/x86_64-apple-darwin/release/grim-${{ github.ref_name }}-macos-x86_64.zip
|
|
||||||
target/x86_64-apple-darwin/release/grim-${{ github.ref_name }}-macos-x86_64-sha256sum.txt
|
|
||||||
target/aarch64-apple-darwin/release/grim-${{ github.ref_name }}-macos-arm.zip
|
|
||||||
target/aarch64-apple-darwin/release/grim-${{ github.ref_name }}-macos-arm-sha256sum.txt
|
|
||||||
target/universal2-apple-darwin/release/grim-${{ github.ref_name }}-macos-universal.zip
|
|
||||||
target/universal2-apple-darwin/release/grim-${{ github.ref_name }}-macos-universal-sha256sum.txt
|
|
||||||
@@ -6,7 +6,13 @@ android/local.properties
|
|||||||
android/keystore
|
android/keystore
|
||||||
android/keystore.asc
|
android/keystore.asc
|
||||||
android/keystore.properties
|
android/keystore.properties
|
||||||
|
# Signing keystores: the upload/app-signing keys must NEVER be committed
|
||||||
|
*.jks
|
||||||
|
android/*.jks
|
||||||
android/*.apk
|
android/*.apk
|
||||||
|
android/*.apk.sha256
|
||||||
|
android/*.AppImage
|
||||||
|
android/*.AppImage.sha256
|
||||||
android/*sha256sum.txt
|
android/*sha256sum.txt
|
||||||
/.idea
|
/.idea
|
||||||
.DS_Store
|
.DS_Store
|
||||||
@@ -18,6 +24,21 @@ target
|
|||||||
.cargo/
|
.cargo/
|
||||||
app/src/main/jniLibs
|
app/src/main/jniLibs
|
||||||
macos/cert.pem
|
macos/cert.pem
|
||||||
linux/Grim.AppDir/AppRun
|
linux/Goblin.AppDir/AppRun
|
||||||
.intentionally-empty-file.o
|
.intentionally-empty-file.o
|
||||||
Cargo.toml-e
|
Cargo.toml-e
|
||||||
|
screenshots/
|
||||||
|
# GRIM-canonical build toolchains fetched by scripts/toolchain.sh
|
||||||
|
.toolchains/
|
||||||
|
# Runtime wallet/node artifacts + secrets generated by running locally — NEVER commit
|
||||||
|
.owner_api_secret
|
||||||
|
.foreign_api_secret
|
||||||
|
grin-wallet.log
|
||||||
|
grin-wallet.toml
|
||||||
|
# Internal E2E harnesses — reference funded wallets / live relays; kept on disk,
|
||||||
|
# untracked (mod e2e is gated behind the `e2e-internal` feature)
|
||||||
|
src/wallet/e2e.rs
|
||||||
|
tests/nostr_e2e.rs
|
||||||
|
# Local agent tooling + in-flight worktrees — never commit
|
||||||
|
.claude/
|
||||||
|
.worktrees/
|
||||||
|
|||||||
@@ -5,7 +5,3 @@
|
|||||||
path = wallet
|
path = wallet
|
||||||
url = https://code.gri.mw/ardocrat/wallet
|
url = https://code.gri.mw/ardocrat/wallet
|
||||||
branch = grim
|
branch = grim
|
||||||
[submodule "tor/webtunnel"]
|
|
||||||
path = tor/webtunnel
|
|
||||||
url = https://code.gri.mw/WEB/webtunnel
|
|
||||||
branch = grim
|
|
||||||
|
|||||||
@@ -2,21 +2,27 @@
|
|||||||
name = "grim"
|
name = "grim"
|
||||||
version = "0.3.6"
|
version = "0.3.6"
|
||||||
authors = ["Ardocrat <ardocrat@gri.mw>"]
|
authors = ["Ardocrat <ardocrat@gri.mw>"]
|
||||||
description = "Cross-platform GUI for Grin with focus on usability and availability to be used by anyone, anywhere."
|
description = "Goblin: a peer-to-peer wallet for Grin. Send and receive instantly with a handle - slatepacks and Tor handled for you."
|
||||||
license = "Apache-2.0"
|
license = "Apache-2.0"
|
||||||
repository = "https://code.gri.mw/GUI/grim"
|
repository = "https://code.gri.mw/GUI/grim"
|
||||||
keywords = [ "crypto", "grin", "mimblewimble" ]
|
keywords = [ "crypto", "grin", "mimblewimble", "nostr" ]
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
build = "build.rs"
|
build = "build.rs"
|
||||||
|
|
||||||
[[bin]]
|
[[bin]]
|
||||||
name = "grim"
|
name = "goblin"
|
||||||
path = "src/main.rs"
|
path = "src/main.rs"
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
name="grim"
|
name="grim"
|
||||||
crate-type = ["rlib"]
|
crate-type = ["rlib"]
|
||||||
|
|
||||||
|
# Desktop/CI release binaries ship stripped of debug symbols — the nostr + grin
|
||||||
|
# tree leaves a large symbol table that's dead weight for users (~16 MB on
|
||||||
|
# Linux). opt-level stays at the default 3 for wallet/runtime speed.
|
||||||
|
[profile.release]
|
||||||
|
strip = true
|
||||||
|
|
||||||
[profile.release-apk]
|
[profile.release-apk]
|
||||||
inherits = "release"
|
inherits = "release"
|
||||||
strip = true
|
strip = true
|
||||||
@@ -25,6 +31,13 @@ lto = true
|
|||||||
codegen-units = 1
|
codegen-units = 1
|
||||||
panic = "abort"
|
panic = "abort"
|
||||||
|
|
||||||
|
[features]
|
||||||
|
## Default build uses the Tor transport only.
|
||||||
|
default = []
|
||||||
|
## Compiles the internal E2E harness (src/wallet/e2e.rs); off by default and the
|
||||||
|
## file is untracked, so a fresh clone builds without it.
|
||||||
|
e2e-internal = []
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
log = "0.4.27"
|
log = "0.4.27"
|
||||||
|
|
||||||
@@ -81,34 +94,65 @@ rkv = "0.20.0"
|
|||||||
usvg = "0.45.1"
|
usvg = "0.45.1"
|
||||||
ring = "0.16.20"
|
ring = "0.16.20"
|
||||||
hyper = { version = "1.6.0", features = ["full"], package = "hyper" }
|
hyper = { version = "1.6.0", features = ["full"], package = "hyper" }
|
||||||
hyper-util = { version = "0.1.19", features = ["http1", "client", "client-legacy"] }
|
hyper-util = { version = "0.1.19", features = ["http1", "client", "client-legacy", "tokio"] }
|
||||||
http-body-util = "0.1.3"
|
http-body-util = "0.1.3"
|
||||||
bytes = "1.11.0"
|
bytes = "1.11.0"
|
||||||
hyper-socks2 = "0.9.1"
|
hyper-socks2 = "0.9.1"
|
||||||
hyper-proxy2 = "0.1.0"
|
hyper-proxy2 = "0.1.0"
|
||||||
hyper-tls = "0.6.0"
|
hyper-tls = "0.6.0"
|
||||||
|
# Raw SOCKS5 stream for the clearnet relay websocket, so a Tor-off wallet can
|
||||||
|
# still be fronted by the user's own SOCKS5 proxy/VPN (AppConfig.socks_proxy_url).
|
||||||
|
# Version already present transitively (async-socks5 tree) — no new download.
|
||||||
|
tokio-socks = "0.5"
|
||||||
async-std = "1.13.2"
|
async-std = "1.13.2"
|
||||||
uuid = { version = "0.8.2", features = ["v4"] }
|
uuid = { version = "0.8.2", features = ["v4"] }
|
||||||
num-bigint = "0.4.6"
|
num-bigint = "0.4.6"
|
||||||
|
|
||||||
## tor
|
## nostr
|
||||||
arti-client = { version = "0.42.0", features = ["static", "pt-client", "onion-service-service", "onion-service-client"] }
|
nostr-sdk = { version = "0.44", features = ["nip06", "nip44", "nip49", "nip59", "nip98"] }
|
||||||
tor-rtcompat = { version = "0.42.0", features = ["static"] }
|
nostr-relay-pool = "0.44"
|
||||||
tor-config = "0.42.0"
|
## NIP-44 v3 (+ v2) encryption for the NIP-17 backward-compat extension (G4).
|
||||||
fs-mistrust = "0.14.1"
|
## Now published to crates.io as v0.3.0 (the M0 deliverable, all upstream test
|
||||||
tor-hsservice = "0.42.0"
|
## vectors green) — no local sibling checkout required. secp256k1 0.31, bridged
|
||||||
tor-hsrproxy = "0.42.0"
|
## to nostr-sdk's 0.29 in wrapv3.rs (see the secp256k1 note below).
|
||||||
tor-keymgr = "0.42.0"
|
nip44 = "0.3.0"
|
||||||
tor-llcrypto = "0.42.0"
|
## Only to construct the key types the `nip44` crate takes: nostr-sdk pins
|
||||||
tor-hscrypto = "0.42.0"
|
## secp256k1 0.29, the nip44 crate 0.31 — bridged via byte arrays in wrapv3.rs.
|
||||||
tor-error = "0.42.0"
|
secp256k1 = "0.31"
|
||||||
|
## Scrub the NIP-44 conversation-key buffers (raw ECDH secret) from memory on
|
||||||
|
## drop in wrapv3.rs. Already a transitive dep; pulled in directly here.
|
||||||
|
zeroize = "1"
|
||||||
|
async-wsocket = "0.13"
|
||||||
|
tokio-tungstenite = { version = "0.26", features = ["rustls-tls-webpki-roots"] }
|
||||||
|
regex = "1"
|
||||||
|
base64 = "0.22"
|
||||||
|
hex = "0.4"
|
||||||
|
## rustls is pulled by our TLS (tungstenite, ring). We install the ring provider
|
||||||
|
## explicitly at startup (see lib.rs) so rustls 0.23 selects a default
|
||||||
|
## deterministically. Direct dep just to make
|
||||||
|
## `rustls::crypto::ring::default_provider()` reachable. NOTHING here may pull
|
||||||
|
## rustls-platform-verifier — it panics on Android outside a full app context.
|
||||||
|
rustls = { version = "0.23", features = ["ring"] }
|
||||||
|
## TLS for the HTTPS-over-Tor client (webpki roots, never the platform
|
||||||
|
## verifier — see the rustls note above).
|
||||||
|
tokio-rustls = { version = "0.26", default-features = false, features = ["ring"] }
|
||||||
|
webpki-roots = "1"
|
||||||
|
|
||||||
|
## Tor — embedded arti (the DIALING half only: connect OUT to the relay's .onion,
|
||||||
|
## and to clearnet HTTP hosts through a Tor exit). Copied from our sister wallet
|
||||||
|
## GRIM's proven, shipping engine. Two choices inherited VERBATIM from GRIM: arti
|
||||||
|
## 0.43 across the family, and the native-tls Tor runtime (TokioNativeTlsRuntime),
|
||||||
|
## NOT rustls — this keeps arti's TLS on native-tls and off the rustls provider
|
||||||
|
## entirely (our relay/HTTP rustls still uses ring, see lib.rs; arti's own TLS is
|
||||||
|
## native-tls and never touches the rustls provider).
|
||||||
|
## `static` vendors openssl (self-contained Android/cross builds, as GRIM ships);
|
||||||
|
## `onion-service-client` enables dialing .onion. We drop GRIM's `pt-client`
|
||||||
|
## (bridges) and `onion-service-service` (hosting) — Goblin only dials.
|
||||||
|
arti-client = { version = "0.43.0", features = ["static", "onion-service-client"] }
|
||||||
|
tor-rtcompat = { version = "0.43.0", features = ["static"] }
|
||||||
|
|
||||||
|
## NIP-98 payload hashing
|
||||||
sha2 = "0.10.8"
|
sha2 = "0.10.8"
|
||||||
ed25519-dalek = "2.1.1"
|
|
||||||
curve25519-dalek = "4.1.3"
|
|
||||||
hyper-tor = { version = "0.14.32", features = ["full"], package = "hyper" }
|
|
||||||
tls-api = "0.12.0"
|
|
||||||
tls-api-native-tls = "0.12.1"
|
|
||||||
safelog = "0.8.1"
|
|
||||||
|
|
||||||
## stratum server
|
## stratum server
|
||||||
tokio-old = { version = "0.2", features = ["full"], package = "tokio" }
|
tokio-old = { version = "0.2", features = ["full"], package = "tokio" }
|
||||||
@@ -122,6 +166,11 @@ nokhwa = { version = "0.10.10", default-features = false, features = ["input-msm
|
|||||||
|
|
||||||
[target.'cfg(target_os = "macos")'.dependencies]
|
[target.'cfg(target_os = "macos")'.dependencies]
|
||||||
nokhwa = { version = "0.10.10", default-features = false, features = ["input-avfoundation", "output-threaded"] }
|
nokhwa = { version = "0.10.10", default-features = false, features = ["input-avfoundation", "output-threaded"] }
|
||||||
|
## Objective-C runtime shim for the macOS `goblin:` deep-link bridge (src/lib.rs,
|
||||||
|
## mac_deeplink). Already in the macOS build graph via nokhwa/cocoa/metal/wgpu, so
|
||||||
|
## this pulls no new crate — it just lets us name it directly. Not compiled on any
|
||||||
|
## other platform.
|
||||||
|
objc = "0.2"
|
||||||
|
|
||||||
[target.'cfg(not(target_os = "android"))'.dependencies]
|
[target.'cfg(not(target_os = "android"))'.dependencies]
|
||||||
env_logger = "0.11.3"
|
env_logger = "0.11.3"
|
||||||
@@ -132,6 +181,17 @@ arboard = "3.2.0"
|
|||||||
rfd = "0.17.2"
|
rfd = "0.17.2"
|
||||||
interprocess = { version = "2.2.1", features = ["tokio"] }
|
interprocess = { version = "2.2.1", features = ["tokio"] }
|
||||||
|
|
||||||
|
## native-tls (via hyper-tls) uses OpenSSL only on Linux/Android. arti's `static`
|
||||||
|
## feature already vendors a statically-linked OpenSSL, but we pin the vendored
|
||||||
|
## build explicitly for exactly those two targets so each is self-contained even
|
||||||
|
## if that transitive path changes (no system OpenSSL is assumed for the target).
|
||||||
|
## Windows (SChannel) and macOS (Security.framework)
|
||||||
|
## don't use OpenSSL at all, so they must NOT pull it — building openssl-src
|
||||||
|
## there is both pointless and fragile (the Windows MSVC runner's bash perl is
|
||||||
|
## missing modules its Configure needs).
|
||||||
|
[target.'cfg(any(target_os = "linux", target_os = "android"))'.dependencies]
|
||||||
|
openssl = { version = "0.10", features = ["vendored"] }
|
||||||
|
|
||||||
[target.'cfg(target_os = "android")'.dependencies]
|
[target.'cfg(target_os = "android")'.dependencies]
|
||||||
android_logger = "0.15.0"
|
android_logger = "0.15.0"
|
||||||
jni = "0.21.1"
|
jni = "0.21.1"
|
||||||
@@ -141,3 +201,22 @@ eframe = { version = "0.33.2", default-features = false, features = ["glow", "an
|
|||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
built = "0.8.0"
|
built = "0.8.0"
|
||||||
|
|
||||||
|
# Windows hosts only: embed the Goblin icon (wix/Product.ico) into goblin.exe via
|
||||||
|
# build.rs. Not compiled on Linux/macOS/Android hosts, so other builds are
|
||||||
|
# unaffected.
|
||||||
|
[target.'cfg(windows)'.build-dependencies]
|
||||||
|
winresource = "0.1"
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
nostr-sdk = { version = "0.44", features = ["nip06", "nip44", "nip49", "nip59", "nip98"] }
|
||||||
|
tokio = { version = "1.49.0", features = ["full"] }
|
||||||
|
base64 = "0.22"
|
||||||
|
sha2 = "0.10"
|
||||||
|
hex = "0.4"
|
||||||
|
serde_yaml = "0.9"
|
||||||
|
## Re-expose deps that already live in the main graph so the E2E harness can be
|
||||||
|
## exercised and its logs captured. No new compiles — same versions unify.
|
||||||
|
log = "0.4.27"
|
||||||
|
env_logger = "0.11.3"
|
||||||
|
rustls = { version = "0.23", features = ["ring"] }
|
||||||
|
|||||||
|
After Width: | Height: | Size: 17 KiB |
@@ -1,39 +1,81 @@
|
|||||||
# Grim <img height="20" src="img/grin-logo.png"/> <img height="20" src="img/logo.png"/>
|
<p align="center">
|
||||||
Cross-platform GUI for [GRiN ツ](https://grin.mw) in [Rust](https://www.rust-lang.org/)
|
<img src="Goblin-Banner.png" alt="Goblin" width="680"/>
|
||||||
for maximum compatibility with original [Mimblewimble](https://github.com/mimblewimble/grin) implementation.
|
</p>
|
||||||
Initially supported platforms are Linux, Mac, Windows, limited Android and possible web support with help of [egui](https://github.com/emilk/egui) - immediate mode GUI library in pure Rust.
|
|
||||||
|
|
||||||
Named by the character [Grim](http://harrypotter.wikia.com/wiki/Grim) - the shape of a large, black, menacing, spectral giant dog.
|
# Goblin
|
||||||
|
|
||||||

|
Goblin is a private, pay-by-username wallet for [GRIN ツ](https://grin.mw) - confidential digital cash on [Mimblewimble](https://github.com/mimblewimble/grin), with no amounts or addresses on the chain.
|
||||||
|
|
||||||
|
Instead of passing slatepack files back and forth, you **pay a `username` (or an `npub`)** and the payment is delivered for you as an **end-to-end encrypted message over [nostr](https://github.com/nostr-protocol/nips), routed through [Tor](https://www.torproject.org)**. Relays only ever see ciphertext - never the amount, the sender, or the recipient. Tor hides your IP from the relay; the relay and encryption hide the rest - content, sender, timing.
|
||||||
|
|
||||||
## Build instructions
|
Goblin is a fork of the **Grim** egui GRIN wallet: it keeps Grim's full GRIN node/wallet engine and layers a Nostr-native, mobile-first payments experience on top.
|
||||||
### Install Rust
|
|
||||||
|
|
||||||
Follow instructions on [Windows](https://forge.rust-lang.org/infra/other-installation-methods.html).
|
## What it does
|
||||||
|
|
||||||
`curl https://sh.rustup.rs -sSf | sh`
|
- **Send to people** - pay a `username` or `npub`; the GRIN slatepack travels as a [NIP-17](https://nips.nostr.com/17) gift-wrapped DM ([kind 1059](https://nostrbook.dev/kinds/1059)) over Tor and is applied automatically by the recipient's wallet. No files to swap, no need to both be online at once.
|
||||||
|
- **Manual slatepacks too** - when you need to pay or get paid without a handle, **Settings → Wallet → Slatepacks** exposes the classic by-hand flow: create a slatepack to send, or paste one to receive, finalize, or pay.
|
||||||
|
- **Open-to-pay links** - a `goblin:` or `nostr:` pay link, or a scanned checkout QR, opens the wallet straight to a prefilled review screen (recipient, amount and note filled in, ready to hold-to-send) on desktop, macOS and Android.
|
||||||
|
- **Proofs on request** - payments can include a native Grin payment proof when the payment request asks for one, off by default, shown on the review screen. An ordinary person-to-person send carries none.
|
||||||
|
- **In-app identity** - a nostr payment key that is deliberately *not* part of your seed, so you can rotate it any time to stay unlinkable without touching your funds. An optional human-readable `name` comes from the goblin.st identity service.
|
||||||
|
- **Private by construction** - GRIN's address-less, confidential chain; your payments and identity (nostr relays, NIP-05 lookups, price) are routed through [Tor](https://www.torproject.org), so who-pays-whom never touches the clear net. The GRIN node connection - block sync and broadcasting your transaction - is direct: public chain data, the same for everyone, and not tied to your identity. Keys, names and history stay on your device.
|
||||||
|
- **Configurable amount pairing** - show balances against a world currency, Bitcoin, or sats (rates fetched over Tor), or turn the preview off.
|
||||||
|
- **News on Home** - the latest post from the official Goblin news key (a [kind 30023](https://nostrbook.dev/kinds/30023) long-form article) appears on the Home screen in your wallet's language, falling back to English; it stays hidden when there is nothing to show, and only ever shows the newest article.
|
||||||
|
- **Cross-platform** - Linux, macOS, Windows, Android, built in pure Rust on [egui](https://github.com/emilk/egui).
|
||||||
|
|
||||||
### Desktop
|
## How a payment travels
|
||||||
|
|
||||||
To build and run application go to project directory and run:
|
```
|
||||||
|
you ──slatepack──▶ NIP-17 gift wrap (kind 1059, NIP-44 encrypted)
|
||||||
|
│
|
||||||
|
Tor
|
||||||
|
│
|
||||||
|
┌─────────────┴─────────────┐
|
||||||
|
your relays recipient's DM relays (kind 10050)
|
||||||
|
└─────────────┬─────────────┘
|
||||||
|
▼
|
||||||
|
recipient ◀──unwrap, verify seal author, apply slatepack
|
||||||
|
```
|
||||||
|
|
||||||
|
The wrap is [NIP-44](https://nips.nostr.com/44)-encrypted, and delivery uses the recipient's DM relay list ([kind 10050](https://nostrbook.dev/kinds/10050)). Tor hides your IP from the relay; the relay and the encryption above hide the rest - content, sender, timing.
|
||||||
|
|
||||||
|
Both parties only need one relay in common. The default set is the Floonet relay (`relay.floonet.dev`) plus Tor-friendly public relays (`relay.0xchat.com`, `offchain.pub`) that accept connections from Tor exits, and the set is editable in **Settings → Relays**.
|
||||||
|
|
||||||
|
## Build
|
||||||
|
|
||||||
|
### Desktop (Linux / macOS / Windows)
|
||||||
|
|
||||||
|
Goblin links [Tor](https://www.torproject.org) **in-process** via [arti](https://gitlab.torproject.org/tpo/core/arti) - the wallet is a single self-contained binary, no sidecar, nothing separate to install:
|
||||||
|
|
||||||
```
|
```
|
||||||
git submodule update --init --recursive
|
git submodule update --init --recursive
|
||||||
cargo build --release
|
cargo build --release
|
||||||
./target/release/grim
|
./target/release/goblin
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Goblin's identity and payment traffic (nostr relays, NIP-05 lookups and price fetches) rides Tor: every relay, the money-path relay included, is reached over a Tor exit to its ordinary clearnet host. The GRIN node connection (block sync and transaction broadcast) is **not** routed through Tor: it connects directly, as it carries only public chain data that isn't linked to your wallet.
|
||||||
|
|
||||||
### Android
|
### Android
|
||||||
#### Set up the environment
|
|
||||||
|
|
||||||
Install Android SDK / NDK / Platform Tools for your OS according to this [FAQ](https://github.com/codepath/android_guides/wiki/installing-android-sdk-tools).
|
Install the Android SDK / NDK, then from the repo root:
|
||||||
|
|
||||||
#### Build the project
|
```
|
||||||
Run Android emulator or connect a real device. Command `adb devices` should show at least one device.
|
./scripts/android.sh build|release v7|v8|x86
|
||||||
In the root of the repo run `./scripts/android.sh build|release v7|v8|x86`, where is `v7`, `v8`, `x86` - device CPU architecture for `build` type, for `release` specify version number in format `major.minor.patch`.
|
```
|
||||||
|
|
||||||
|
`v7`/`v8`/`x86` is the device CPU architecture for `build`; for `release` pass a version in `major.minor.patch` form.
|
||||||
|
|
||||||
|
## Identity service (`goblin-nip05d`)
|
||||||
|
|
||||||
|
The optional `name` service lives in `goblin-nip05d/` (axum + SQLite) and is deployed at [goblin.st](https://goblin.st). It implements [NIP-05](https://nips.nostr.com/5) resolution, [NIP-98](https://nips.nostr.com/98)-authenticated registration and release (names are never transferred - on a key rotation you release the old name and re-register, or import your existing identity). The wallet is fully usable - and fully anonymous - without it. Avatars aren't stored or served - clients render them from the pubkey (an npub gradient with the username's first letter, else the Grin mark).
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
Apache License v2.0.
|
Apache License v2.0.
|
||||||
|
|
||||||
|
## Credits
|
||||||
|
|
||||||
|
🤖 Built with AI pair-programming assistance (Claude)
|
||||||
|
|
||||||
|
The underlying cross-platform GRIN wallet engine is the upstream **Grim** project.
|
||||||
|
|
||||||
|
Simplified and Traditional Chinese translations by jasperli2026 (https://github.com/jasperli2026).
|
||||||
|
|||||||
@@ -8,11 +8,18 @@ android {
|
|||||||
buildToolsVersion = '36.1.0'
|
buildToolsVersion = '36.1.0'
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId "mw.gri.android"
|
applicationId "st.goblin.wallet"
|
||||||
minSdk 24
|
minSdk 24
|
||||||
targetSdk 36
|
targetSdk 36
|
||||||
versionCode 5
|
// Version tracks Goblin's build number (GOBLIN_BUILD) so the Android
|
||||||
versionName "0.3.6"
|
// package identity moves forward with every release instead of freezing.
|
||||||
|
// Same env the Rust side stamps into crate::BUILD; the in-app updater
|
||||||
|
// compares build numbers, and this keeps versionCode (Android's
|
||||||
|
// upgrade/downgrade integer) in lockstep. Bare local gradle → fallback.
|
||||||
|
// ponytail: fallback keeps a keyless ./gradlew working; CI/release sets the env
|
||||||
|
def goblinBuild = (System.getenv("GOBLIN_BUILD") ?: "").trim()
|
||||||
|
versionCode goblinBuild.isEmpty() ? 5 : goblinBuild.toInteger()
|
||||||
|
versionName goblinBuild.isEmpty() ? "0.3.6" : goblinBuild
|
||||||
}
|
}
|
||||||
|
|
||||||
lint {
|
lint {
|
||||||
@@ -39,6 +46,12 @@ android {
|
|||||||
release {
|
release {
|
||||||
minifyEnabled false
|
minifyEnabled false
|
||||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||||
|
// When android/keystore.properties exists the release build signs with
|
||||||
|
// the configured upload key; without it release stays unsigned and the
|
||||||
|
// build falls back to the debug-signed variant (see scripts/android.sh).
|
||||||
|
if (keystorePropertiesFile.exists()) {
|
||||||
|
signingConfig signingConfigs.release
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (keystorePropertiesFile.exists()) {
|
if (keystorePropertiesFile.exists()) {
|
||||||
signedRelease {
|
signedRelease {
|
||||||
@@ -71,7 +84,9 @@ android {
|
|||||||
applicationVariants.all { variant ->
|
applicationVariants.all { variant ->
|
||||||
def flavor = variant.productFlavors[0].name
|
def flavor = variant.productFlavors[0].name
|
||||||
|
|
||||||
if (flavor == "ci") {
|
// The ci branch reads the private-mirror properties at configuration time,
|
||||||
|
// which runs for every variant — only enter it when the mirror is configured.
|
||||||
|
if (flavor == "ci" && project.hasProperty("mavenHost")) {
|
||||||
repositories {
|
repositories {
|
||||||
maven {
|
maven {
|
||||||
credentials {
|
credentials {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
|
||||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||||
|
<uses-permission android:name="android.permission.VIBRATE" />
|
||||||
<uses-permission android:name="android.permission.CAMERA"/>
|
<uses-permission android:name="android.permission.CAMERA"/>
|
||||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" tools:ignore="ScopedStorage"/>
|
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" tools:ignore="ScopedStorage"/>
|
||||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" tools:ignore="ScopedStorage"/>
|
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" tools:ignore="ScopedStorage"/>
|
||||||
@@ -18,7 +19,7 @@
|
|||||||
android:largeHeap="true"
|
android:largeHeap="true"
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
android:label="Grim"
|
android:label="Goblin"
|
||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
android:theme="@style/Theme.Main"
|
android:theme="@style/Theme.Main"
|
||||||
android:enableOnBackInvokedCallback="false"
|
android:enableOnBackInvokedCallback="false"
|
||||||
@@ -29,7 +30,7 @@
|
|||||||
|
|
||||||
<provider
|
<provider
|
||||||
android:name=".FileProvider"
|
android:name=".FileProvider"
|
||||||
android:authorities="mw.gri.android.fileprovider"
|
android:authorities="st.goblin.wallet.fileprovider"
|
||||||
android:exported="false"
|
android:exported="false"
|
||||||
android:grantUriPermissions="true">
|
android:grantUriPermissions="true">
|
||||||
<meta-data
|
<meta-data
|
||||||
@@ -40,7 +41,8 @@
|
|||||||
<activity
|
<activity
|
||||||
android:launchMode="singleTask"
|
android:launchMode="singleTask"
|
||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
android:configChanges="orientation|screenSize"
|
android:screenOrientation="portrait"
|
||||||
|
android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|uiMode|density|locale|layoutDirection|fontScale|colorMode"
|
||||||
android:exported="true">
|
android:exported="true">
|
||||||
|
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
@@ -58,6 +60,16 @@
|
|||||||
<data android:pathPattern=".*\\.slatepack" />
|
<data android:pathPattern=".*\\.slatepack" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
|
||||||
|
<!-- Goblin payment deep link: web "Open in Goblin" buttons and
|
||||||
|
goblin: QR/links open the wallet straight to the send-review
|
||||||
|
screen. BROWSABLE so a browser/click can resolve it. -->
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
<data android:scheme="goblin" />
|
||||||
|
</intent-filter>
|
||||||
|
|
||||||
<meta-data android:name="android.app.lib_name" android:value="grim" />
|
<meta-data android:name="android.app.lib_name" android:value="grim" />
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,14 @@ public class BackgroundService extends Service {
|
|||||||
private boolean mStopped = false;
|
private boolean mStopped = false;
|
||||||
|
|
||||||
private static final int NOTIFICATION_ID = 1;
|
private static final int NOTIFICATION_ID = 1;
|
||||||
|
// One-shot "payment received" notification, separate from the persistent
|
||||||
|
// sync notification above.
|
||||||
|
private static final int PAYMENT_NOTIFICATION_ID = 2;
|
||||||
|
private static final String PAYMENT_CHANNEL_ID = "PaymentReceived";
|
||||||
|
// One-shot "payment requested" notification (someone asking us to pay them),
|
||||||
|
// separate from both the sync (id=1) and received-payment (id=2) notifications.
|
||||||
|
private static final int REQUEST_NOTIFICATION_ID = 3;
|
||||||
|
private static final String REQUEST_CHANNEL_ID = "PaymentRequested";
|
||||||
private NotificationCompat.Builder mNotificationBuilder;
|
private NotificationCompat.Builder mNotificationBuilder;
|
||||||
|
|
||||||
private String mNotificationContentText = "";
|
private String mNotificationContentText = "";
|
||||||
@@ -70,12 +78,13 @@ public class BackgroundService extends Service {
|
|||||||
if (Build.VERSION.SDK_INT > 25) {
|
if (Build.VERSION.SDK_INT > 25) {
|
||||||
startStopIntent.putExtra(EXTRA_NOTIFICATION_ID, NOTIFICATION_ID);
|
startStopIntent.putExtra(EXTRA_NOTIFICATION_ID, NOTIFICATION_ID);
|
||||||
}
|
}
|
||||||
if (canStart) {
|
// Goblin's background job is the light Nostr-over-Tor payment
|
||||||
startStopIntent.setAction(ACTION_START_NODE);
|
// listen (the "Listening for payments" status); the heavy
|
||||||
PendingIntent i = PendingIntent
|
// integrated node is never STARTED from this notification --
|
||||||
.getBroadcast(BackgroundService.this, 1, startStopIntent, PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_ONE_SHOT);
|
// Goblin defaults to an external node, so the GRIM "Enable"
|
||||||
mNotificationBuilder.addAction(R.drawable.ic_start, getStartText(), i);
|
// action is removed. Only offer STOP as a safety valve if the
|
||||||
} else if (canStop) {
|
// node is somehow already running (started elsewhere).
|
||||||
|
if (canStop) {
|
||||||
startStopIntent.setAction(ACTION_STOP_NODE);
|
startStopIntent.setAction(ACTION_STOP_NODE);
|
||||||
PendingIntent i = PendingIntent
|
PendingIntent i = PendingIntent
|
||||||
.getBroadcast(BackgroundService.this, 1, startStopIntent, PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_ONE_SHOT);
|
.getBroadcast(BackgroundService.this, 1, startStopIntent, PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_ONE_SHOT);
|
||||||
@@ -189,6 +198,80 @@ public class BackgroundService extends Service {
|
|||||||
notificationManager.cancel(NOTIFICATION_ID);
|
notificationManager.cancel(NOTIFICATION_ID);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Show a one-shot "payment received" notification (id=2), separate from
|
||||||
|
// the persistent sync notification (id=1). Called from native code via
|
||||||
|
// MainActivity when a payment slatepack is received over nostr, possibly
|
||||||
|
// while the app is backgrounded. Localization of the fixed strings is a
|
||||||
|
// follow-up (text is composed here at Java side).
|
||||||
|
public static void notifyPaymentReceived(Context context, String name, String amount) {
|
||||||
|
NotificationManager manager = context.getSystemService(NotificationManager.class);
|
||||||
|
if (manager == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// High-importance channel so the notification pops with sound + vibration.
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
NotificationChannel channel = new NotificationChannel(
|
||||||
|
PAYMENT_CHANNEL_ID, "Payments", NotificationManager.IMPORTANCE_HIGH
|
||||||
|
);
|
||||||
|
manager.createNotificationChannel(channel);
|
||||||
|
}
|
||||||
|
Intent i = context.getPackageManager().getLaunchIntentForPackage(context.getPackageName());
|
||||||
|
PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, i, PendingIntent.FLAG_IMMUTABLE);
|
||||||
|
// An empty amount is the "hide all details" private alert: the native
|
||||||
|
// side already composed the whole localized line into `name`, so show it
|
||||||
|
// verbatim rather than the "X paid Y ツ" scaffolding.
|
||||||
|
String receivedText = amount.isEmpty() ? name : name + " paid " + amount + " ツ";
|
||||||
|
NotificationCompat.Builder builder = new NotificationCompat.Builder(context, PAYMENT_CHANNEL_ID)
|
||||||
|
.setContentTitle("Payment received")
|
||||||
|
.setContentText(receivedText)
|
||||||
|
.setSmallIcon(R.drawable.ic_stat_name)
|
||||||
|
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
||||||
|
.setAutoCancel(true)
|
||||||
|
.setDefaults(NotificationCompat.DEFAULT_ALL)
|
||||||
|
.setContentIntent(pendingIntent);
|
||||||
|
try {
|
||||||
|
manager.notify(PAYMENT_NOTIFICATION_ID, builder.build());
|
||||||
|
} catch (SecurityException e) {
|
||||||
|
// POST_NOTIFICATIONS not granted: skip the notification, never the payment.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show a one-shot "payment requested" notification (id=3), separate from both
|
||||||
|
// the persistent sync notification (id=1) and the received-payment one (id=2).
|
||||||
|
// Called from native code via MainActivity when a payment request (Invoice1)
|
||||||
|
// arrives over nostr, possibly while the app is backgrounded. Mirrors
|
||||||
|
// notifyPaymentReceived; strings are composed here Java-side.
|
||||||
|
public static void notifyPaymentRequested(Context context, String name, String amount) {
|
||||||
|
NotificationManager manager = context.getSystemService(NotificationManager.class);
|
||||||
|
if (manager == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// High-importance channel so the notification pops with sound + vibration.
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
NotificationChannel channel = new NotificationChannel(
|
||||||
|
REQUEST_CHANNEL_ID, "Payment requests", NotificationManager.IMPORTANCE_HIGH
|
||||||
|
);
|
||||||
|
manager.createNotificationChannel(channel);
|
||||||
|
}
|
||||||
|
Intent i = context.getPackageManager().getLaunchIntentForPackage(context.getPackageName());
|
||||||
|
PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, i, PendingIntent.FLAG_IMMUTABLE);
|
||||||
|
// Empty amount = the "hide all details" private alert (see notifyPaymentReceived).
|
||||||
|
String requestedText = amount.isEmpty() ? name : name + " requested " + amount + " ツ";
|
||||||
|
NotificationCompat.Builder builder = new NotificationCompat.Builder(context, REQUEST_CHANNEL_ID)
|
||||||
|
.setContentTitle("Payment requested")
|
||||||
|
.setContentText(requestedText)
|
||||||
|
.setSmallIcon(R.drawable.ic_stat_name)
|
||||||
|
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
||||||
|
.setAutoCancel(true)
|
||||||
|
.setDefaults(NotificationCompat.DEFAULT_ALL)
|
||||||
|
.setContentIntent(pendingIntent);
|
||||||
|
try {
|
||||||
|
manager.notify(REQUEST_NOTIFICATION_ID, builder.build());
|
||||||
|
} catch (SecurityException e) {
|
||||||
|
// POST_NOTIFICATIONS not granted: skip the notification, never the request.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Start the service.
|
// Start the service.
|
||||||
public static void start(Context c) {
|
public static void start(Context c) {
|
||||||
if (!isServiceRunning(c)) {
|
if (!isServiceRunning(c)) {
|
||||||
|
|||||||
@@ -69,6 +69,9 @@ public class MainActivity extends GameActivity {
|
|||||||
|
|
||||||
private ActivityResultLauncher<Intent> mFilePickResult = null;
|
private ActivityResultLauncher<Intent> mFilePickResult = null;
|
||||||
private ActivityResultLauncher<Intent> mOpenFilePermissionsResult = null;
|
private ActivityResultLauncher<Intent> mOpenFilePermissionsResult = null;
|
||||||
|
private ActivityResultLauncher<Intent> mFileSaveResult = null;
|
||||||
|
// Source path (in the share cache) staged by Rust for the next saveFile().
|
||||||
|
private String mPendingSavePath = null;
|
||||||
|
|
||||||
@SuppressLint("UnspecifiedRegisterReceiverFlag")
|
@SuppressLint("UnspecifiedRegisterReceiverFlag")
|
||||||
@Override
|
@Override
|
||||||
@@ -145,6 +148,32 @@ public class MainActivity extends GameActivity {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Register file SAVE result (Storage Access Framework CREATE_DOCUMENT):
|
||||||
|
// copy the staged source file into the user-chosen document.
|
||||||
|
mFileSaveResult = registerForActivityResult(
|
||||||
|
new ActivityResultContracts.StartActivityForResult(),
|
||||||
|
result -> {
|
||||||
|
String src = mPendingSavePath;
|
||||||
|
mPendingSavePath = null;
|
||||||
|
if (result.getResultCode() == Activity.RESULT_OK && src != null) {
|
||||||
|
Intent data = result.getData();
|
||||||
|
if (data != null && data.getData() != null) {
|
||||||
|
Uri uri = data.getData();
|
||||||
|
try (InputStream is = new FileInputStream(new File(src));
|
||||||
|
OutputStream os = getContentResolver().openOutputStream(uri)) {
|
||||||
|
byte[] buffer = new byte[4096];
|
||||||
|
int length;
|
||||||
|
while (is != null && (length = is.read(buffer)) > 0) {
|
||||||
|
os.write(buffer, 0, length);
|
||||||
|
}
|
||||||
|
if (os != null) os.flush();
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e("grim", e.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Listener for display insets (cutouts) to pass values into native code.
|
// Listener for display insets (cutouts) to pass values into native code.
|
||||||
View content = getWindow().getDecorView().findViewById(android.R.id.content);
|
View content = getWindow().getDecorView().findViewById(android.R.id.content);
|
||||||
ViewCompat.setOnApplyWindowInsetsListener(content, (v, insets) -> {
|
ViewCompat.setOnApplyWindowInsetsListener(content, (v, insets) -> {
|
||||||
@@ -206,6 +235,16 @@ public class MainActivity extends GameActivity {
|
|||||||
String action = intent.getAction();
|
String action = intent.getAction();
|
||||||
// Check if file was open with the application.
|
// Check if file was open with the application.
|
||||||
if (action != null && action.equals(Intent.ACTION_VIEW)) {
|
if (action != null && action.equals(Intent.ACTION_VIEW)) {
|
||||||
|
Uri data = intent.getData();
|
||||||
|
String scheme = data != null ? data.getScheme() : null;
|
||||||
|
// Goblin payment deep link (goblin: / nostr:): pass the URI text
|
||||||
|
// straight to native code, which routes it to the send-review flow.
|
||||||
|
// These are NOT files, so they must skip the file-descriptor path.
|
||||||
|
if (scheme != null && (scheme.equalsIgnoreCase("goblin")
|
||||||
|
|| scheme.equalsIgnoreCase("nostr"))) {
|
||||||
|
onData(data.toString());
|
||||||
|
return;
|
||||||
|
}
|
||||||
Intent i = getIntent();
|
Intent i = getIntent();
|
||||||
i.setData(intent.getData());
|
i.setData(intent.getData());
|
||||||
setIntent(i);
|
setIntent(i);
|
||||||
@@ -363,21 +402,37 @@ public class MainActivity extends GameActivity {
|
|||||||
finishAndRemoveTask();
|
finishAndRemoveTask();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Called from native code after a successful same-device sign-in/authorize
|
||||||
|
// to hand focus back to the app that deep-linked into us (the browser tab
|
||||||
|
// that is polling the callback). Backgrounds our task so the OS surfaces the
|
||||||
|
// previous app; keeps the process alive (unlike exit()) so the callback POST
|
||||||
|
// finishes and the success toast shows on next foreground.
|
||||||
|
public void returnToCaller() {
|
||||||
|
moveTaskToBack(true);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onDestroy() {
|
protected void onDestroy() {
|
||||||
unregisterReceiver(mBroadcastReceiver);
|
unregisterReceiver(mBroadcastReceiver);
|
||||||
BackgroundService.stop(this);
|
|
||||||
|
|
||||||
// Kill process after 3 secs if app was terminated from recent apps to prevent app hang.
|
// Only tear the process down when the activity is actually finishing.
|
||||||
new Thread(() -> {
|
// onDestroy also fires for configuration-change recreations (rotation,
|
||||||
try {
|
// density, uiMode); killing the process there takes the whole app down
|
||||||
onTermination();
|
// right as Android is about to recreate the activity.
|
||||||
Thread.sleep(3000);
|
if (isFinishing()) {
|
||||||
Process.killProcess(Process.myPid());
|
BackgroundService.stop(this);
|
||||||
} catch (InterruptedException e) {
|
|
||||||
throw new RuntimeException(e);
|
// Kill process after 3 secs if app was terminated from recent apps to prevent app hang.
|
||||||
}
|
new Thread(() -> {
|
||||||
}).start();
|
try {
|
||||||
|
onTermination();
|
||||||
|
Thread.sleep(3000);
|
||||||
|
Process.killProcess(Process.myPid());
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}).start();
|
||||||
|
}
|
||||||
|
|
||||||
super.onDestroy();
|
super.onDestroy();
|
||||||
}
|
}
|
||||||
@@ -385,6 +440,18 @@ public class MainActivity extends GameActivity {
|
|||||||
// Notify native code to stop activity (e.g. node) if app was terminated from recent apps.
|
// Notify native code to stop activity (e.g. node) if app was terminated from recent apps.
|
||||||
public native void onTermination();
|
public native void onTermination();
|
||||||
|
|
||||||
|
// Called from native code to show a "payment received" notification
|
||||||
|
// (BackgroundService id=2) when a payment arrives over nostr.
|
||||||
|
public void notifyPaymentReceived(String name, String amount) {
|
||||||
|
BackgroundService.notifyPaymentReceived(this, name, amount);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Called from native code to show a "payment requested" notification
|
||||||
|
// (BackgroundService id=3) when a payment request arrives over nostr.
|
||||||
|
public void notifyPaymentRequested(String name, String amount) {
|
||||||
|
BackgroundService.notifyPaymentRequested(this, name, amount);
|
||||||
|
}
|
||||||
|
|
||||||
// Called from native code to set text into clipboard.
|
// Called from native code to set text into clipboard.
|
||||||
public void copyText(String data) {
|
public void copyText(String data) {
|
||||||
ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
|
ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
|
||||||
@@ -496,13 +563,84 @@ public class MainActivity extends GameActivity {
|
|||||||
// Called from native code to share data from provided path.
|
// Called from native code to share data from provided path.
|
||||||
public void shareData(String path) {
|
public void shareData(String path) {
|
||||||
File file = new File(path);
|
File file = new File(path);
|
||||||
Uri uri = FileProvider.getUriForFile(this, "mw.gri.android.fileprovider", file);
|
Uri uri = FileProvider.getUriForFile(this, "st.goblin.wallet.fileprovider", file);
|
||||||
Intent intent = new Intent(Intent.ACTION_SEND);
|
Intent intent = new Intent(Intent.ACTION_SEND);
|
||||||
intent.putExtra(Intent.EXTRA_STREAM, uri);
|
intent.putExtra(Intent.EXTRA_STREAM, uri);
|
||||||
intent.setType("text/*");
|
intent.setType("text/*");
|
||||||
startActivity(Intent.createChooser(intent, "Share data"));
|
startActivity(Intent.createChooser(intent, "Share data"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Called from native code to SAVE a staged file to a user-chosen location.
|
||||||
|
// Launches the Storage Access Framework create-document picker; the result
|
||||||
|
// handler copies the staged source file into the chosen document.
|
||||||
|
public void saveFile(String path, String name) {
|
||||||
|
mPendingSavePath = path;
|
||||||
|
Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT);
|
||||||
|
intent.addCategory(Intent.CATEGORY_OPENABLE);
|
||||||
|
intent.setType("application/octet-stream");
|
||||||
|
intent.putExtra(Intent.EXTRA_TITLE, name);
|
||||||
|
try {
|
||||||
|
mFileSaveResult.launch(intent);
|
||||||
|
} catch (android.content.ActivityNotFoundException ex) {
|
||||||
|
Log.e("grim", ex.toString());
|
||||||
|
mPendingSavePath = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Called from native code to share plain text (e.g. a payment link) via the
|
||||||
|
// system share sheet.
|
||||||
|
public void shareText(String text) {
|
||||||
|
Intent intent = new Intent(Intent.ACTION_SEND);
|
||||||
|
intent.putExtra(Intent.EXTRA_TEXT, text);
|
||||||
|
intent.setType("text/plain");
|
||||||
|
startActivity(Intent.createChooser(intent, "Share"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Called from native code to play a short "error" haptic (rejected payment).
|
||||||
|
public void vibrateError() {
|
||||||
|
Vibrator vibrator = (Vibrator) getSystemService(Context.VIBRATOR_SERVICE);
|
||||||
|
if (vibrator == null || !vibrator.hasVibrator()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Two short pulses read as "no" / rejected, distinct from a tap.
|
||||||
|
long[] pattern = new long[]{0, 40, 70, 40};
|
||||||
|
if (Build.VERSION.SDK_INT >= 26) {
|
||||||
|
vibrator.vibrate(VibrationEffect.createWaveform(pattern, -1));
|
||||||
|
} else {
|
||||||
|
vibrator.vibrate(pattern, -1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Called from native code to play a tiny "tick" haptic on a successful copy.
|
||||||
|
public void vibrateCopy() {
|
||||||
|
Vibrator vibrator = (Vibrator) getSystemService(Context.VIBRATOR_SERVICE);
|
||||||
|
if (vibrator == null || !vibrator.hasVibrator()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// One short, light tick — a confirmation, not an alert.
|
||||||
|
if (Build.VERSION.SDK_INT >= 29) {
|
||||||
|
vibrator.vibrate(VibrationEffect.createPredefined(VibrationEffect.EFFECT_TICK));
|
||||||
|
} else if (Build.VERSION.SDK_INT >= 26) {
|
||||||
|
vibrator.vibrate(VibrationEffect.createOneShot(20, VibrationEffect.DEFAULT_AMPLITUDE));
|
||||||
|
} else {
|
||||||
|
vibrator.vibrate(20);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Called from native code to set status-bar icon color to contrast the
|
||||||
|
// in-app theme. white = light icons for a dark background. The app draws
|
||||||
|
// edge-to-edge, so the OS status-bar background is the app's own content;
|
||||||
|
// the icons must follow the theme or they vanish (dark-on-dark).
|
||||||
|
public void setStatusBarWhiteIcons(boolean white) {
|
||||||
|
runOnUiThread(() -> {
|
||||||
|
androidx.core.view.WindowInsetsControllerCompat c =
|
||||||
|
androidx.core.view.WindowCompat.getInsetsController(getWindow(),
|
||||||
|
getWindow().getDecorView());
|
||||||
|
// isAppearanceLightStatusBars == true means DARK icons.
|
||||||
|
c.setAppearanceLightStatusBars(!white);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Called from native code to check if device is using dark theme.
|
// Called from native code to check if device is using dark theme.
|
||||||
public boolean useDarkTheme() {
|
public boolean useDarkTheme() {
|
||||||
int currentNightMode = getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK;
|
int currentNightMode = getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK;
|
||||||
@@ -512,7 +650,12 @@ public class MainActivity extends GameActivity {
|
|||||||
// Called from native code to pick the file.
|
// Called from native code to pick the file.
|
||||||
public void pickFile() {
|
public void pickFile() {
|
||||||
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
|
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
|
||||||
intent.setType("text/*");
|
// Permissive type: custom extensions like .backup have no registered
|
||||||
|
// MIME type, so any narrower filter greys them out in the picker and
|
||||||
|
// locks users out of restoring their identity. Content is validated
|
||||||
|
// on the native side after selection.
|
||||||
|
intent.setType("*/*");
|
||||||
|
intent.addCategory(Intent.CATEGORY_OPENABLE);
|
||||||
try {
|
try {
|
||||||
mFilePickResult.launch(Intent.createChooser(intent, "Pick file"));
|
mFilePickResult.launch(Intent.createChooser(intent, "Pick file"));
|
||||||
} catch (android.content.ActivityNotFoundException ex) {
|
} catch (android.content.ActivityNotFoundException ex) {
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 984 B After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 615 B After Width: | Height: | Size: 965 B |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 6.2 KiB |
|
Before Width: | Height: | Size: 5.1 KiB After Width: | Height: | Size: 4.0 KiB |
|
Before Width: | Height: | Size: 5.3 KiB After Width: | Height: | Size: 6.2 KiB |
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 3.6 KiB |
|
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 3.6 KiB |
|
Before Width: | Height: | Size: 4.3 KiB After Width: | Height: | Size: 9.2 KiB |
|
Before Width: | Height: | Size: 7.4 KiB After Width: | Height: | Size: 5.5 KiB |
|
Before Width: | Height: | Size: 7.5 KiB After Width: | Height: | Size: 9.2 KiB |
|
Before Width: | Height: | Size: 6.9 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 8.6 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 9.9 KiB After Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 27 KiB |
@@ -1,4 +1,4 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
<color name="ic_launcher_background">#FCEF03</color>
|
<color name="ic_launcher_background">#FFD60A</color>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -1,29 +1,26 @@
|
|||||||
pluginManagement {
|
pluginManagement {
|
||||||
repositories {
|
repositories {
|
||||||
maven {
|
// Private mirror only when its coordinates are supplied (-PmavenHost=… as in upstream CI);
|
||||||
credentials {
|
// everyone else resolves plugins from the public repositories.
|
||||||
username "$mavenUser"
|
def mavenHost = providers.gradleProperty("mavenHost")
|
||||||
password "$mavenPassword"
|
if (mavenHost.present) {
|
||||||
|
def mavenUser = providers.gradleProperty("mavenUser").get()
|
||||||
|
def mavenPassword = providers.gradleProperty("mavenPassword").get()
|
||||||
|
["gradle-plugin-portal", "google-maven", "maven-central"].each { repo ->
|
||||||
|
maven {
|
||||||
|
credentials {
|
||||||
|
username mavenUser
|
||||||
|
password mavenPassword
|
||||||
|
}
|
||||||
|
url "${mavenHost.get()}/repository/${repo}/"
|
||||||
|
allowInsecureProtocol = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
url "$mavenHost/repository/gradle-plugin-portal/"
|
} else {
|
||||||
allowInsecureProtocol = true
|
gradlePluginPortal()
|
||||||
}
|
google()
|
||||||
maven {
|
mavenCentral()
|
||||||
credentials {
|
|
||||||
username "$mavenUser"
|
|
||||||
password "$mavenPassword"
|
|
||||||
}
|
|
||||||
url "$mavenHost/repository/google-maven/"
|
|
||||||
allowInsecureProtocol = true
|
|
||||||
}
|
|
||||||
maven {
|
|
||||||
credentials {
|
|
||||||
username "$mavenUser"
|
|
||||||
password "$mavenPassword"
|
|
||||||
}
|
|
||||||
url "$mavenHost/repository/maven-central/"
|
|
||||||
allowInsecureProtocol = true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
include ':app'
|
include ':app'
|
||||||
|
|||||||
@@ -1,10 +1,43 @@
|
|||||||
|
use std::env;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
use std::{env, fs};
|
|
||||||
|
/// The GRIM commit Goblin forked from; builds count commits on top of it.
|
||||||
|
const GOBLIN_FORK_BASE: &str = "b51a46b";
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
built::write_built_file().expect("Failed to acquire build-time information");
|
built::write_built_file().expect("Failed to acquire build-time information");
|
||||||
|
|
||||||
|
// Goblin versioning is build-based: Build N = commits since the fork.
|
||||||
|
// An explicit GOBLIN_BUILD env wins (CI builds from the public single-commit
|
||||||
|
// squash where the fork base isn't an ancestor, so the git count can't run);
|
||||||
|
// otherwise count commits since the fork; "dev" only as a last resort.
|
||||||
|
let build = env::var("GOBLIN_BUILD")
|
||||||
|
.ok()
|
||||||
|
.map(|s| s.trim().to_string())
|
||||||
|
.filter(|s| !s.is_empty())
|
||||||
|
.or_else(|| {
|
||||||
|
Command::new("git")
|
||||||
|
.args([
|
||||||
|
"rev-list",
|
||||||
|
"--count",
|
||||||
|
&format!("{}..HEAD", GOBLIN_FORK_BASE),
|
||||||
|
])
|
||||||
|
.output()
|
||||||
|
.ok()
|
||||||
|
.filter(|o| o.status.success())
|
||||||
|
.and_then(|o| String::from_utf8(o.stdout).ok())
|
||||||
|
.map(|s| s.trim().to_string())
|
||||||
|
.filter(|s| !s.is_empty())
|
||||||
|
})
|
||||||
|
.unwrap_or_else(|| "dev".to_string());
|
||||||
|
println!("cargo:rustc-env=GOBLIN_BUILD={}", build);
|
||||||
|
// .git/HEAD only changes on branch switches; the reflog is appended on
|
||||||
|
// every commit, so the build number stays current.
|
||||||
|
println!("cargo:rerun-if-changed=.git/HEAD");
|
||||||
|
println!("cargo:rerun-if-changed=.git/logs/HEAD");
|
||||||
|
println!("cargo:rerun-if-env-changed=GOBLIN_BUILD");
|
||||||
|
|
||||||
// Setting up git hooks in the project: rustfmt and so on.
|
// Setting up git hooks in the project: rustfmt and so on.
|
||||||
let git_hooks = format!(
|
let git_hooks = format!(
|
||||||
"git config core.hooksPath {}",
|
"git config core.hooksPath {}",
|
||||||
@@ -13,89 +46,43 @@ fn main() {
|
|||||||
|
|
||||||
if cfg!(target_os = "windows") {
|
if cfg!(target_os = "windows") {
|
||||||
Command::new("cmd")
|
Command::new("cmd")
|
||||||
.args(&["/C", &git_hooks])
|
.args(["/C", &git_hooks])
|
||||||
.output()
|
.output()
|
||||||
.expect("failed to execute git config for hooks");
|
.expect("failed to execute git config for hooks");
|
||||||
} else {
|
} else {
|
||||||
Command::new("sh")
|
Command::new("sh")
|
||||||
.args(&["-c", &git_hooks])
|
.args(["-c", &git_hooks])
|
||||||
.output()
|
.output()
|
||||||
.expect("failed to execute git config for hooks");
|
.expect("failed to execute git config for hooks");
|
||||||
}
|
}
|
||||||
|
|
||||||
let out_dir = env::var("OUT_DIR").unwrap();
|
// Goblin's private transport is Tor via embedded arti (see src/tor/), linked
|
||||||
let tor_out_dir = format!("{}/tor", out_dir);
|
// in-process — no sidecar subprocess and no bundled/embedded helper binary.
|
||||||
let mut webtunnel_file = format!("{}/webtunnel", tor_out_dir);
|
// There is nothing transport-related to build or embed here.
|
||||||
let exists = fs::exists(&webtunnel_file).unwrap();
|
|
||||||
if !exists {
|
|
||||||
// Create empty webtunnel file to allow build with include_bytes! macro.
|
|
||||||
fs::create_dir(&tor_out_dir).unwrap_or_default();
|
|
||||||
fs::File::create(&webtunnel_file).unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
let target = env::var("TARGET").unwrap();
|
// Embed the Goblin icon into goblin.exe so Explorer, the taskbar and Alt-Tab
|
||||||
let target_os = env::var("CARGO_CFG_TARGET_OS").unwrap();
|
// show it even for the bare exe (the .msi shortcuts already carry it). No-op
|
||||||
if target_os == "ios" {
|
// on every non-Windows platform.
|
||||||
|
embed_windows_icon();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Embed `wix/Product.ico` (the yellow Goblin icon) as goblin.exe's application
|
||||||
|
/// icon resource. Gated to Windows hosts — that's where the `winresource`
|
||||||
|
/// build-dependency is compiled and where the MSVC resource compiler (`rc.exe`,
|
||||||
|
/// shipped on the windows-latest runner) is available; our Windows builds are
|
||||||
|
/// always native MSVC, so host == target == windows.
|
||||||
|
#[cfg(windows)]
|
||||||
|
fn embed_windows_icon() {
|
||||||
|
if env::var("CARGO_CFG_TARGET_OS").as_deref() != Ok("windows") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
let mut res = winresource::WindowsResource::new();
|
||||||
let is_android = target_os == "android";
|
res.set_icon("wix/Product.ico");
|
||||||
if is_android {
|
if let Err(e) = res.compile() {
|
||||||
// Set a path to Android Webtunnel binary.
|
// Don't fail the build over the icon — just flag it.
|
||||||
let arch = if target.contains("aarch64") {
|
println!("cargo:warning=winresource icon embed failed: {e}");
|
||||||
"arm64-v8a"
|
|
||||||
} else if target.contains("arm") {
|
|
||||||
"armeabi-v7a"
|
|
||||||
} else {
|
|
||||||
"x86_64"
|
|
||||||
};
|
|
||||||
let root = env::var("CARGO_MANIFEST_DIR").unwrap();
|
|
||||||
webtunnel_file = format!(
|
|
||||||
"{}/android/app/src/main/jniLibs/{}/libwebtunnel.so",
|
|
||||||
root, arch
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build if Webtunnel binary is empty or not exists.
|
|
||||||
let empty = match fs::File::open(&webtunnel_file) {
|
|
||||||
Ok(file) => file.metadata().unwrap().len() == 0,
|
|
||||||
Err(_) => true,
|
|
||||||
};
|
|
||||||
let build = !exists || empty;
|
|
||||||
if build {
|
|
||||||
// Setup GOOS env variable.
|
|
||||||
let go_os = if target_os == "macos" {
|
|
||||||
"darwin"
|
|
||||||
} else {
|
|
||||||
target_os.as_str()
|
|
||||||
};
|
|
||||||
// Setup GOARCH env variable.
|
|
||||||
let go_arch = if target.contains("aarch64") {
|
|
||||||
"arm64"
|
|
||||||
} else if target.contains("arm") {
|
|
||||||
"arm"
|
|
||||||
} else {
|
|
||||||
"amd64"
|
|
||||||
};
|
|
||||||
// Run Webtunnel Go build.
|
|
||||||
let output = if env::consts::OS == "windows" {
|
|
||||||
Command::new("./scripts/webtunnel.bat")
|
|
||||||
.arg(go_os)
|
|
||||||
.arg(go_arch)
|
|
||||||
.arg(webtunnel_file)
|
|
||||||
.output()
|
|
||||||
} else {
|
|
||||||
Command::new("bash")
|
|
||||||
.arg("./scripts/webtunnel.sh")
|
|
||||||
.arg(go_os)
|
|
||||||
.arg(go_arch)
|
|
||||||
.arg(webtunnel_file)
|
|
||||||
.output()
|
|
||||||
};
|
|
||||||
if let Ok(out) = output {
|
|
||||||
if out.status.code().is_none() || out.status.code().unwrap() != 0 {
|
|
||||||
panic!("webtunnel go build failed:\n{:?}", out);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(not(windows))]
|
||||||
|
fn embed_windows_icon() {}
|
||||||
|
|||||||
@@ -0,0 +1,238 @@
|
|||||||
|
# Goblin Privacy Transport Redesign
|
||||||
|
|
||||||
|
**Status:** Decided — implementation plan · **Date:** 2026-07-04 · **Author:** Architecture
|
||||||
|
**Scope:** Replace the Nym-mixnet transport that carries Goblin's Nostr traffic with embedded Tor, GRIM-style, and rebuild the one privacy property Tor lacks (send/receive timing unlinkability) on our own relay.
|
||||||
|
**Hard constraints (owner):** No clearnet money path · No NYM tokens · No rented/metered privacy bandwidth · In-process only (no sidecar daemon, no system VPN/TUN — must work on Android and iOS).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## The decision (read this first)
|
||||||
|
|
||||||
|
**Goblin is dropping the Nym mixnet and returning to Tor, embedded in-process, exactly the way our sister wallet GRIM already ships it.** The wallet keeps everything else it does today — payments are still gift-wrapped Nostr messages that our own relay stores and forwards, so a recipient can be paid while offline. The only thing that changes is the private pipe *underneath* the relay connection: instead of tunnelling to the relay across the Nym mixnet, the wallet dials the relay's **pinned `.onion` address over embedded arti** (Tor written in Rust).
|
||||||
|
|
||||||
|
We do not invent our own Tor engine. We **copy GRIM's** — a proven ~1,300-line engine in four files (`grim/src/tor/`) that already runs in production on desktop and Android. And because a mixnet's real gift was hiding the *timing* link between a sender uploading a payment and a recipient downloading it — something Tor alone does not do — **we rebuild that timing privacy on our own relay**: it holds each incoming gift-wrap and releases it to the recipient after a short randomized (Poisson) delay. Same fuzzing a mixnet performs, done by the one server we fully control, unmetered, and unable to collapse the way rented mixnet bandwidth did.
|
||||||
|
|
||||||
|
This trades exactly one thing: a real distributed mixnet resists a *global passive adversary* who can watch the whole internet at once. A single relay plus Tor does not. That adversary is out of scope for a low-value payments wallet, and Tor never claimed to stop it either. In return we get a transport that is mature, free, unmetered, has a huge anonymity set, is lighter on the battery, and — measured where the user actually waits — **faster**.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Why we are leaving the mixnet: confirmed root cause
|
||||||
|
|
||||||
|
This is not a transient outage we can wait out. It is **removal by design**, traced to Nym's own source:
|
||||||
|
|
||||||
|
- A credential-less Nym client is granted a hard-coded `FREE_TESTNET_BANDWIDTH_VALUE = 64 GiB`, and that grant **expires at the next UTC-midnight rollover** (`ecash_today()`).
|
||||||
|
- An entry gateway only honours that free grant while `enforce_zk_nyms = false`.
|
||||||
|
- Public entry gateways are flipping `enforce_zk_nyms = true` **network-wide** as Nym rolls out paid, NYM-token-gated "zk-nym ticketbook" bandwidth (≈ 225 NYM for ~25 GB).
|
||||||
|
|
||||||
|
So the free tier the wallet floated on is **testnet scaffolding Nym is actively deleting**, and its failure recurs *on a schedule*. It went dark on us more than once. The only supported replacement — paid ticketbooks — **requires holding NYM tokens** (an owner red line) and offers no sats/Lightning purchase path. A payments wallet cannot stand on a foundation that disappears at midnight and can only be rented back with a specific speculative token. The mixnet, as we were able to consume it, is a dead end.
|
||||||
|
|
||||||
|
Tor has none of these properties. It is free, unmetered, has no token, no bonding, no grant to expire, and the largest anonymity set of any deployed privacy network. It runs in-process on a phone, and GRIM has already proven the whole embedded path.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Threat model: what we are actually protecting
|
||||||
|
|
||||||
|
We reason about leakage at six levels, sender → relay → receiver:
|
||||||
|
|
||||||
|
| Level | Leak | Status under the Tor plan |
|
||||||
|
|---|---|---|
|
||||||
|
| **L1 network** | sender/receiver IP as seen by the relay and on-path observers | **Covered by Tor** — an onion connection has no exit node, so the relay and every hop see a Tor address, never the phone's IP. |
|
||||||
|
| **L2 timing** | correlation of send-time with receive-time (the mixnet's core property) | **Rebuilt on our relay** — Poisson release delay (see below), plus NIP-59 timestamp backdating already in the wallet. |
|
||||||
|
| **L3 volume** | message-size correlation | NIP-44 v2 padding blunts; gift-wraps are already near-uniform at payments volume. |
|
||||||
|
| **L4 relay-visible** | recipient p-tags, kinds, subscription filters, connection cadence | NIP-59 gift-wrap blunts; unchanged by the transport swap. |
|
||||||
|
| **L5 content** | message plaintext | **Already solved** — NIP-44 v2 inside NIP-59 gift-wrap (kind 1059). |
|
||||||
|
| **L6 long-term intersection** | repeated-pattern deanonymization | NIP-59 blunts; unchanged. |
|
||||||
|
|
||||||
|
Our **realistic adversary** is the relay operator, ISPs, near-endpoint observers, and chain-analysts — **not a global passive adversary (GPA)**. GPA-resistance is explicitly out of scope for a low-value payments wallet; Tor itself states plainly that it "does not defend against" an attacker who can watch **both ends** of a circuit. We inherit that boundary knowingly and, for the adversary that actually matters, we cover every level.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## The architecture
|
||||||
|
|
||||||
|
The whole change is contained because Goblin already isolates its transport behind one trait. Payments still ride the relay-mediated, store-and-forward model unchanged. Four things move:
|
||||||
|
|
||||||
|
**1. The relay hosts an onion service.** Plain, mature **system Tor** runs on the relay's own server (`torrc`: `HiddenServiceDir` + `HiddenServicePort 443 127.0.0.1:443`) and forwards the onion straight to the relay's existing secure-websocket port. This is battle-tested C-Tor doing the hosting half — the strongest, most-audited part of the Tor codebase — and it replaces the custom `floonet-mixexit` byte-pipe binary entirely.
|
||||||
|
|
||||||
|
**2. The wallet dials that onion over embedded arti.** Goblin only needs the **dialing half** of Tor — connect *out* to the relay's onion. It never hosts a service (that is what makes it simpler than GRIM, which hosts an onion to *receive*). `arti-client`'s `TorClient::connect()` returns a `DataStream` that implements `AsyncRead + AsyncWrite` — a drop-in for the byte source the wallet feeds to its websocket layer today.
|
||||||
|
|
||||||
|
**3. We copy GRIM's Tor engine rather than write our own.** GRIM's `src/tor/` is ~1,300 lines across four files (`config.rs`, `mod.rs`, `tor.rs`, `types.rs`) and is already in production. Two technical choices we inherit verbatim because GRIM already paid for them:
|
||||||
|
- **arti 0.43 across the whole arti family** (`arti-client`, `tor-rtcompat`, `tor-config`, `tor-hsservice`, `tor-hsrproxy`, `tor-keymgr`, `tor-llcrypto`, `tor-hscrypto` — all `0.43.0`).
|
||||||
|
- **The native-tls Tor runtime** (`TokioNativeTlsRuntime`), **not rustls**. This deliberately sidesteps the rustls crypto-provider (ring) conflict we fought all through the Nym era. We take GRIM's known-good TLS path and never re-open that wound.
|
||||||
|
|
||||||
|
**4. The two code seams that carry private traffic switch to Tor; the money node does not.** Exactly two paths leave the device with anything sensitive on them: the **relay websocket**, and the **one HTTP helper** that carries all the small lookups (names at `goblin.st`, relay hints, the pinned-pool refresh, price, avatars). Both re-route through arti. The **Grin blockchain node stays on the clear internet exactly as today, unchanged** — it never sees who is paying whom, only opaque transaction data, and Tor-wrapping it would buy nothing but latency.
|
||||||
|
|
||||||
|
**The pin lives where the old Nym exit pin lived.** Our relay-pool gist already carries a per-relay field for exactly this, and its parser is deliberately tolerant of unknown fields (no `deny_unknown_fields`, `version` stays `1`). So we add an `onion` address next to each relay entry and **older builds simply ignore it** — no flag day, no schema break. The plumbing that already resolves the co-located Nym `exit` for a relay URL generalizes one-for-one to resolve its `onion`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Recovering timing privacy: Poisson on the relay
|
||||||
|
|
||||||
|
Here is the one property we cannot get from Tor for free, and how we rebuild it.
|
||||||
|
|
||||||
|
A mixnet's genuine value was **timing unlinkability**: even someone who can see traffic near both ends cannot match "this sender uploaded at 10:01:03" to "that recipient downloaded at 10:01:04," because the mixnet deliberately shuffles and delays messages so the two events do not line up. Tor is low-latency by design and does *not* do this — a payment flows through as fast as the circuit allows.
|
||||||
|
|
||||||
|
We rebuild it in the one place we fully own: **our relay holds each incoming gift-wrap and releases it to the recipient after a randomized, exponentially-distributed (Poisson) delay.** This is the same fuzzing a mixnet performs, collapsed onto a single hop we operate ourselves — unmetered, always on, and immune to the rented-bandwidth failure that just killed Nym for us.
|
||||||
|
|
||||||
|
**The elegant part: this costs the user nothing they can see.** The sender's on-screen "Sent" clears the moment the relay **confirms it holds the message** — not when the recipient receives it. Delivery to the recipient is already asynchronous and invisible (they might be offline for hours). The Poisson delay lands *entirely inside that already-invisible gap.* We buy back the mixnet's timing privacy at **zero visible latency cost**. And it stacks on top of a fuzz the wallet already applies: NIP-59 backdates every gift-wrap's timestamp by a random offset of up to two days, so even the timestamps on the wire are decorrelated from real send time.
|
||||||
|
|
||||||
|
**The honest trade-off, stated plainly:** a real distributed mixnet spreads its mixing across many independent nodes, which is what lets it resist a global passive adversary watching the entire network at once. Our single relay plus Tor does not — a party who could simultaneously observe our relay *and* the recipient's Tor guard *and* correlate the Poisson-delayed release could, in principle, still link the two ends. That is the global-adversary threat, it is out of scope for a Grin payments wallet, and Tor never defended against it either. For the adversary who actually exists — the relay operator, an ISP, a network snoop, a chain-analyst — relay-side Poisson closes the timing gap. We give up one theoretical guarantee and gain reliability, speed, and battery.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## What we must preserve (load-bearing, transport-agnostic)
|
||||||
|
|
||||||
|
Three things in today's wallet are not Nym-specific and must survive the swap intact.
|
||||||
|
|
||||||
|
**1. The "connection is genuinely live and carrying traffic" readiness signal.** The UI carefully refuses to show "Connected" until a relay is *actually subscribed and carrying traffic on the current tunnel*, not merely until the pipe opened (`transport_ready()` / `warm_up()` / relay-gated generation counter in `src/nym/nymproc.rs`). This is what prevents a reassuring-but-false "Connected" over a tunnel that cannot yet deliver. **Keep the mechanism; re-point it at Tor** — readiness becomes "arti has bootstrapped, the onion circuit is up, and a required relay is subscribed on it."
|
||||||
|
|
||||||
|
**2. The read-back confirm on sending (keep verbatim).** The wallet does **not** report a payment "Sent" on a transport-write success. It performs a genuine read-back: after publishing, it polls the target relays for the event id until one confirms it actually **holds the gift-wrap**, or a timeout is hit — in which case it surfaces failure so the caller retries instead of silently dropping money (`src/nostr/client.rs`, the SILENT-LOSS GUARD loop). This is pure money-safety and is completely transport-agnostic. **Keep the logic exactly as written.** Only the *comments* that say "over the scoped Nym exit / mixnet transport" need their wording generalized to "the transport"; the guard itself does not change a line, and it is in fact the very mechanism that makes relay-side Poisson safe (the sender is told "Sent" precisely when the relay confirms it holds the message).
|
||||||
|
|
||||||
|
**3. User-facing copy that says "Nym" / "Mixnet."** The strings need rewording to Tor across all six locales (`locales/en.yml` plus `tr`, `fr`, `de`, `ru`, `zh-CN`): `connected_nym` ("Connected over Nym" → "Connected over Tor"), `connecting_nym`, `nym_ready`, `mixnet_routing`, `network_value` ("MW + Nym mixnet + nostr"), `privacy_value` ("Mimblewimble + Nym"), `over_mixnet`, `rates_note`, `send_like_message_body`, `row_delivery_val`, `row_privacy_val`, and the onboarding `intro` that describes "a five-hop network." These become plain, honest Tor language — see the forum post at the end for the tone.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Mobile reality (told straight)
|
||||||
|
|
||||||
|
**Android is solved.** GRIM already ships embedded Tor on Android and the recipe is copyable. It comes down to two things: **build Tor into the app's native library** (arti compiles into the same `.so` the rest of the Rust already lives in — no separate process, no sidecar), and **set a few environment variables before the Rust runtime starts.** The critical one is `ARTI_FS_DISABLE_PERMISSION_CHECKS=true` (GRIM sets it in `MainActivity.java` before loading native code): arti's `fs-mistrust` layer normally refuses to start if its state directory has "too-open" Unix permissions, and Android's app-sandbox filesystem always trips that check — so without this flag Tor simply never boots on a phone. This is a known, small, copy-paste fix, not a research problem.
|
||||||
|
|
||||||
|
**iOS is green-field and needs its own spike.** The arti library should compile for iOS — nothing about it is Android-specific — but nobody on our side has shipped it there yet, so we treat it as unproven until we have. Two things to flag going in: (a) we run **without pluggable-transport bridges** on iOS, because the platform won't let an app spawn the helper processes those need — plain Tor only, which is fine for our reach-the-relay use case; and (b) the same `fs-mistrust` and state-directory questions need answering against iOS's sandbox. **Action: an iOS Tor spike is a named prerequisite before we claim iOS support.** Android does not wait on it.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Integration surface (code anchors)
|
||||||
|
|
||||||
|
The transport is isolated behind one trait, so this is a contained swap, not a rewrite.
|
||||||
|
|
||||||
|
| Seam | Location today | Change |
|
||||||
|
|---|---|---|
|
||||||
|
| Transport trait | `nostr-relay-pool` `WebSocketTransport::connect(url, mode, timeout) -> (WebSocketSink, WebSocketStream)`; impl `NymWebSocketTransport` at **`src/nym/transport.rs:47`** | New arti-backed impl in a new `src/tor/`, engine copied from GRIM. |
|
||||||
|
| Byte source | Nym `open_stream` at **`src/nym/streamexit.rs`** (returns `AsyncRead + AsyncWrite`) | Swap for arti `TorClient::connect(<onion>:443)` → `DataStream`. Keep the rest of the path. |
|
||||||
|
| TLS + ws wrap | `client_async_tls(url, stream)` at **`src/nym/transport.rs:126` / `:158`** (SNI = relay host) | **Unchanged.** |
|
||||||
|
| Injection point | `Client::builder().websocket_transport(...)` in **`src/nostr/client.rs`** | Inject `TorWebSocketTransport`. |
|
||||||
|
| HTTP chokepoint | `http_request_bytes()` **`src/nym/mod.rs:71`** / `http_request()` **`src/nym/mod.rs:111`** — carries NIP-05 / name authority (`src/nostr/nip05.rs`), NIP-11 hints, pool gist, price, avatars | Re-route through arti: Tor→onion for the `goblin.st` name authority (pin a second onion); Tor→clearnet for the gist + price + avatars. **Grin node stays clearnet as today.** |
|
||||||
|
| Pin plumbing (forward-safe) | `PoolRelay` at **`src/nostr/pool.rs:86`**; `exit: Option<String>` at **`:105`**; serde tolerant (no `deny_unknown_fields`); `exit_for()` **`:157`**, `exit_for_host()` **`:170`**, `has_exit()` **`:187`** | Add an `onion` field beside `exit` in the gist **without breaking old builds**; keep `version:1`. `exit_for*`/`has_exit` generalize to `onion_for*`/`has_onion`. |
|
||||||
|
| Readiness lifecycle (**preserve**) | `warm_up()` **`src/nym/nymproc.rs:107`**, `transport_ready()` relay-gated readiness **`:127`–`:135`** | Re-point at Tor: bootstrapped + onion up + relay subscribed. Keep the gating semantics. |
|
||||||
|
| Confirm-before-sent (**preserve verbatim**) | SILENT-LOSS GUARD read-back loop, doc at **`src/nostr/client.rs:63`** | Unchanged logic. Only reword the Nym-specific comments to "the transport." |
|
||||||
|
| Footprint retired | `src/nym/*` = **2,842 lines** (`nymproc` 1073, `dns` 662, `streamexit` 465, `mod` 411, `transport` 231) + `nym-sdk`/`smolmix` path-deps + `floonet-mixexit` | Deleted once Tor is proven. Net code likely **shrinks** — most of `nymproc` exists only to fight Nym flakiness (gateway race / probe / condemn). |
|
||||||
|
|
||||||
|
The live pool already ships the floonet relay inline (`pool.rs:69` pins `wss://relay.floonet.dev` with its Nym `exit`). We add its `onion` alongside, in the same gist, and roll forward.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## What the wallet will feel like on Tor
|
||||||
|
|
||||||
|
An honest, moment-by-moment read on whether each everyday interaction gets faster, stays the same, or gets slower than it was on the mixnet. Short version: on pure speed, Tor is a lateral-to-favorable move everywhere the user actually waits.
|
||||||
|
|
||||||
|
**Receiving / listening — same-to-better, and never a spinner.** The wallet does not poll for incoming payments. It holds a **live, open subscription** to the relay, and payments are *pushed* down that already-open connection and processed with no human watching. That is invisible on either transport. The difference is that on Tor each pushed message arrives quicker (no per-hop mixing delay), and the deliberate Poisson privacy-delay hides inside the already-invisible delivery gap. Nothing the user sees changes; what is behind it is faster. **Verdict: same-to-better.**
|
||||||
|
|
||||||
|
**Sending — faster where it matters most.** The only part of a send the user actually watches is the marquee moment: find the recipient's relay, publish the wrapped payment, and wait for the relay to confirm it holds it. On the mixnet that moment paid three separate taxes — a per-message mixing delay, a fixed ~3-second stream-settle, and multi-fragment delivery lag. On Tor all three are gone. So this spinner should feel **shorter**, not longer. Everything after it — building the reply, finalizing, posting the finished transaction to the Grin node — is background work, and the node post is plain clear-internet exactly as before. **Verdict: faster where it matters most.**
|
||||||
|
|
||||||
|
**Invoices / requests — same-to-faster, dominated by the human.** There is no separate "invoice" message type on the wire; a request is the same gift-wrapped DM as a payment, just with a human approval gate in front of it. Transport latency matters even less here because a person is tapping "approve." **Verdict: same-to-faster, dominated by the human step.**
|
||||||
|
|
||||||
|
**Name and identity lookups — fast, unchanged in feel.** Resolving `name@goblin.st` is a single HTTP GET behind a short "Searching…" spinner — the one genuinely blocking lookup in the app. Everything else (reverse name lookups, avatars, relay hints, pool refresh, price) is cached, quiet background work the user never waits on. A warm keep-alive connection pool over Tor makes repeat lookups cheap. **Verdict: fast and unchanged in feel — provided we keep the connection warm.**
|
||||||
|
|
||||||
|
**Cold start — simpler, comparable, warm-able.** The mixnet needed *two* separate mixnet clients racing each other for bandwidth grants, plus a whole sequencer, just to get connected "in seconds instead of a minute." Tor is **one** bootstrap — dramatically simpler. It overlaps with app launch, so it is mostly invisible; its only visible edge is the very first send of a session if Tor has not finished bootstrapping yet, which would surface as a slightly longer first "Sending…." Warming the circuit at launch hides even that. **Verdict: simpler, comparable, warm-able.**
|
||||||
|
|
||||||
|
**Battery / always-listening — improves.** A persistent Tor circuit is lighter than a live mixnet client: there is no continuous cover-traffic machinery to run and no per-hop delay work to perform. The wallet's existing "the connection died, rebuild it" logic and its background/foreground handling map cleanly onto Tor circuits. **Verdict: battery should improve.**
|
||||||
|
|
||||||
|
**Net verdict.** On pure speed and feel, Tor is lateral-to-favorable everywhere the user actually waits — and better in the two places they wait most (sending and cold start). What we trade is not speed. It is the mixnet's global-adversary timing guarantee, and we largely rebuild even that, for the realistic adversary, with relay-side Poisson.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phased implementation plan
|
||||||
|
|
||||||
|
### Phase 0 — iOS + mobile spike (de-risk before commit)
|
||||||
|
**Goal:** prove the copied engine drops into the seam on the platforms we are less sure of.
|
||||||
|
**Tasks:** (a) stand up `arti-client` → connect a test `.onion:443` → `client_async_tls` → Nostr ws handshake, proving `DataStream` satisfies the transport seam on desktop; (b) confirm the Android recipe holds in a Goblin dev build (native-lib link + `ARTI_FS_DISABLE_PERMISSION_CHECKS`); (c) **the iOS spike** — does arti compile and bootstrap inside the iOS sandbox, plain-Tor (no PT bridges), and where does `fs-mistrust` land.
|
||||||
|
**Exit criteria:** a Nostr ws session to the relay over an onion, in acceptable time, on a real Android device; a clear yes/no + punch-list for iOS.
|
||||||
|
**Risk:** low on desktop/Android (GRIM shipped it); iOS unknown, which is why it is Phase 0.
|
||||||
|
|
||||||
|
### Phase 1 — Onion service on the relay
|
||||||
|
**Goal:** the relay is reachable over a stable `.onion`.
|
||||||
|
**Tasks:** system-Tor `torrc` onion service on the floonet box fronting the relay's ws port; publish and pin the `onion` in the gist beside the existing entry; add a second onion for the `goblin.st` name authority. Enable Vanguards on the service side.
|
||||||
|
**Validation:** `torify` a plain Nostr client to the onion and complete a handshake.
|
||||||
|
**Risk:** low — mature C-Tor doing the hosting half.
|
||||||
|
|
||||||
|
### Phase 2 — Wallet transport swap
|
||||||
|
**Goal:** all Nostr + HTTP traffic rides Tor; Nym is off the live path.
|
||||||
|
**Tasks:** copy GRIM's `src/tor/` into Goblin; implement `WebSocketTransport` against arti `DataStream`; re-route `http_request*` (Tor→onion for the authority, Tor→clearnet for gist/price/avatars); re-point `warm_up()`/`transport_ready()` at Tor readiness; keep the confirm-before-sent guard; **no clearnet fallback for the relay path — fail loudly**.
|
||||||
|
**Validation:** a real payment between two devices completes over the onion, with the read-back confirm firing.
|
||||||
|
**Risk:** low-medium — mitigated by copying a proven engine and native-tls.
|
||||||
|
|
||||||
|
### Phase 3 — Relay-side Poisson (timing privacy)
|
||||||
|
**Goal:** send-time and receive-time are decorrelated for the realistic adversary.
|
||||||
|
**Tasks:** the relay holds each inbound gift-wrap and releases it to the recipient after an exponentially-distributed delay (mean tuned so it stays inside the invisible delivery gap); confirm the sender's "Sent" still fires on *hold*, not on delivery, so the delay is free to the user.
|
||||||
|
**Validation:** relay logs + on-path capture show release timing decorrelated from arrival timing; sender UX latency unchanged.
|
||||||
|
**Risk:** low — it is a delay queue on a server we own.
|
||||||
|
|
||||||
|
### Phase 4 — Copy reword + iOS follow-through
|
||||||
|
**Goal:** the product says "Tor," honestly, everywhere; iOS is either shipped or explicitly deferred.
|
||||||
|
**Tasks:** reword all Nym/Mixnet strings across the six locales; publish the forum post; land iOS per the Phase-0 punch-list or file it as a tracked follow-up.
|
||||||
|
**Validation:** locale drift test green; no "Nym"/"Mixnet" left in user-facing copy.
|
||||||
|
**Risk:** low.
|
||||||
|
|
||||||
|
### Phase 5 — Retire Nym
|
||||||
|
**Goal:** shed the dead dependency and shrink the binary.
|
||||||
|
**Tasks:** delete `src/nym/*`, the `nym-sdk`/`smolmix` path-deps, and `floonet-mixexit`; shrink the Android native lib; simplify the all-in-one floonet package to **relay + torrc-onion + name-authority-onion**.
|
||||||
|
**Validation:** clean build, smaller Android lib, green E2E.
|
||||||
|
**Risk:** low — deletion after the replacement is proven.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Migration & rollout
|
||||||
|
|
||||||
|
The pin format is forward-safe (serde-tolerant, `version:1` preserved), so rollout is graceful:
|
||||||
|
|
||||||
|
1. Ship the **Tor-capable build** via the in-app updater (GitHub releases).
|
||||||
|
2. Run the **system-Tor onion service alongside** the existing relay through the transition.
|
||||||
|
3. As users update, they pick up the `onion` pin from the gist and connect over Tor.
|
||||||
|
4. **Old Nym pins go dark gracefully** — Nym is failing on its own schedule anyway, so there is no working state to "cut over" from and no regression to manage.
|
||||||
|
|
||||||
|
No coordinated flag day. The network is already in the failure state the new build fixes.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Risks & mitigations
|
||||||
|
|
||||||
|
| Risk | Mitigation |
|
||||||
|
|---|---|
|
||||||
|
| arti **onion-client** maturity ("not yet as secure as C-Tor") | Mature **system-Tor** hosts the service side; Vanguards on the service; onion-client is actively hardening and we are only the dialer. |
|
||||||
|
| **iOS** unproven | Named Phase-0 spike; Android does not wait on it; plain-Tor (no PT bridges) is the accepted iOS mode. |
|
||||||
|
| **Android build** friction | Direct copy of GRIM's working recipe — native-lib link + `ARTI_FS_DISABLE_PERMISSION_CHECKS`; native-tls dodges the rustls/ring provider conflict. |
|
||||||
|
| **Bootstrap latency** on first send | Warm the circuit at launch; keep the "Connecting…" readiness UX; the confirm-before-sent guard makes a slow first send safe, never lost. |
|
||||||
|
| **Poisson delay tuned too long** (feels laggy to recipient) | Tune mean to sit inside the already-async delivery gap; sender "Sent" fires on relay-hold regardless, so it is never on the user's critical path. |
|
||||||
|
| **Single-relay availability** | The relay was always a required component; onion descriptor HA via a 24/7 host (and onionbalance if needed). |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Far-future option (not part of this plan)
|
||||||
|
|
||||||
|
If we ever want to close the one remaining gap — resistance to a global passive adversary — a self-run, tokenless, unbonded mixnet layer could in principle sit *on top of* this Tor foundation (wallet → Tor → our own entry → mixnodes → relay). We are **not building it.** It leans on exactly the free Nym grant that is being deleted, it adds latency, and it defends against a threat a Grin payments wallet does not face. It is noted here only so the door is documented, not opened. **Tor is the plan.**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Bottom line
|
||||||
|
|
||||||
|
Return to Tor, embedded in-process, copied straight from GRIM's proven engine (arti 0.43, native-tls), dialing the relay's pinned onion — and rebuild the mixnet's one real advantage, timing unlinkability, as a Poisson release delay on the relay we already own and fully control. Keep the relay-mediated store-and-forward model, the genuine-readiness signal, and the read-back confirm-before-sent guard exactly as they are. Android is solved today; iOS gets a named spike. We give up one theoretical guarantee against a global adversary that is out of scope, and in exchange the wallet becomes more reliable, lighter on the battery, faster where users wait — and free of any token or rented bandwidth that can go dark at midnight.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Forum post: why we're moving from the mixnet to Tor
|
||||||
|
|
||||||
|
*(Ready to publish, owner's voice.)*
|
||||||
|
|
||||||
|
**We're switching Goblin's private plumbing from the Nym mixnet back to Tor. Here's the honest why.**
|
||||||
|
|
||||||
|
When I built Goblin's privacy layer on the Nym mixnet, I meant it. A mixnet is the strongest metadata-privacy tool we have — it doesn't just hide your IP, it deliberately shuffles the timing of messages so nobody can even tell that *you sending* and *someone receiving* are the same payment. For a money wallet, that's exactly the property you want. I wasn't hedging when I picked it.
|
||||||
|
|
||||||
|
But a payments wallet has to stand on ground that doesn't move, and the ground moved. The free bandwidth tier that Goblin relied on turns out to be temporary testnet scaffolding that Nym is actively removing — it's written right into their code to expire, and their public gateways are switching over to a paid model. And the paid model means holding a specific crypto token to buy bandwidth. That's not a foundation I'm willing to build your money on. It went dark on us more than once, on a schedule, and "your payments work unless it's the wrong time of day" is not something I'll ship.
|
||||||
|
|
||||||
|
So we're going back to **Tor** — the most battle-tested privacy network on the internet, with no token, no rented bandwidth, and nothing to expire. And we're embedding it **right inside the app**, the same way our sibling wallet GRIM already does. No separate program to install, no server in the middle you have to trust. It just works, on your phone, out of the box.
|
||||||
|
|
||||||
|
Here's what that means for you, in plain terms. Tor hides your IP address — from our own relay and from your internet provider — so nobody watching the network can see it's *you* sending a payment. And the one thing Tor doesn't do on its own — that clever timing-shuffle a mixnet does — **we rebuild ourselves**: our relay briefly holds each payment and releases it on a randomized delay, so nobody can match "you sent" to "they received." You won't feel that delay, because it happens in the gap where a payment is already in flight. Your screen says "Sent" the instant our relay has your payment safely in hand.
|
||||||
|
|
||||||
|
And honestly? It's **better** day to day. Tor is faster for the moments you actually wait on — sending a payment, opening the app — because it skips the mixnet's built-in delays. It's easier on your battery. And it's simpler and more reliable, because there's no metered bandwidth left to run out.
|
||||||
|
|
||||||
|
I'll be straight about the one thing we give up: a full mixnet can resist an adversary powerful enough to watch the *entire* internet at once. Tor doesn't claim to stop that, and neither do we — it's not the threat a Grin payments wallet realistically faces. For every attacker that actually exists, you're covered.
|
||||||
|
|
||||||
|
Faster, more reliable, works on your phone, no tokens, no rented bandwidth to fail. That's the trade, and I'm glad to make it.
|
||||||
@@ -0,0 +1,379 @@
|
|||||||
|
# Goblin transactions — how a payment works, end to end
|
||||||
|
|
||||||
|
This document explains the full lifecycle of a Goblin payment: how money moves,
|
||||||
|
every status it passes through, and the small guarantees that keep it safe. It is
|
||||||
|
written against the code in `src/nostr/` and `src/wallet/` — function names and
|
||||||
|
files are cited (line numbers drift, so they aren't).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. The big picture: two layers
|
||||||
|
|
||||||
|
A Goblin payment is **a Grin transaction wrapped in a private nostr message**.
|
||||||
|
|
||||||
|
1. **Grin layer (the money).** Grin/Mimblewimble transactions are *interactive*:
|
||||||
|
the sender and recipient exchange a "slate" that passes through states until
|
||||||
|
it's finalized and posted on-chain. There are no addresses and no amounts on
|
||||||
|
the chain. Goblin reuses GRIM's full Grin node + wallet engine for this.
|
||||||
|
|
||||||
|
2. **Nostr layer (the delivery).** Instead of making you hand slate files back
|
||||||
|
and forth, Goblin delivers each slate as an **end-to-end-encrypted nostr
|
||||||
|
direct message**, routed through **Tor**. You pay a `username` or
|
||||||
|
`npub`; the recipient's wallet applies the slate automatically.
|
||||||
|
|
||||||
|
The slate is the payload; nostr is the transport. Everything below is about how
|
||||||
|
those two layers move together and what state is tracked at each step.
|
||||||
|
|
||||||
|
### Slate states (Grin layer)
|
||||||
|
|
||||||
|
Interactive Grin slates pass through numbered states. Goblin uses two flows:
|
||||||
|
|
||||||
|
| Flow | States | Who builds what |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| **Standard** (sender pushes money) | `Standard1` → `Standard2` → `Standard3` | Sender builds S1 (locks their outputs), recipient replies S2, sender finalizes S3 and posts |
|
||||||
|
| **Invoice** (recipient pulls money) | `Invoice1` → `Invoice2` → `Invoice3` | Requester builds I1 (the ask), payer replies I2 (pays), requester finalizes I3 and posts |
|
||||||
|
|
||||||
|
### Status + direction (Goblin's nostr metadata)
|
||||||
|
|
||||||
|
For each payment Goblin stores a `TxNostrMeta` (`src/nostr/types.rs`) keyed by
|
||||||
|
slate id, with a **direction** and a **status**:
|
||||||
|
|
||||||
|
`NostrTxDirection`:
|
||||||
|
- `Sent` — we pushed funds (we created S1).
|
||||||
|
- `Received` — we were paid (we replied S2).
|
||||||
|
- `RequestedByUs` — we issued an invoice (we created I1).
|
||||||
|
- `RequestedOfUs` — someone invoiced us and we paid it (we replied I2).
|
||||||
|
|
||||||
|
`NostrSendStatus`:
|
||||||
|
- `Created` — slate built locally, DM not dispatched yet (durable checkpoint).
|
||||||
|
- `AwaitingS2` — S1 sent, waiting for the recipient's S2 reply.
|
||||||
|
- `ReceivedNoReply` — we processed an incoming S1 (or I1) and built our reply, but haven't dispatched it yet (crash-recovery point).
|
||||||
|
- `RepliedS2` — our S2 reply was dispatched (we received a payment).
|
||||||
|
- `AwaitingI2` — our I1 invoice was sent, waiting for the payer's I2.
|
||||||
|
- `PaidAwaitingFinalize` — we paid an invoice (sent I2); the requester finalizes.
|
||||||
|
- `Finalized` — slate finalized and posted on-chain.
|
||||||
|
- `SendFailed` — DM dispatch failed; eligible for retry.
|
||||||
|
- `Cancelled` — cancelled locally (manual cancel or 24h expiry).
|
||||||
|
|
||||||
|
`Finalized` and `Cancelled` are **terminal**.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Identity & addressing
|
||||||
|
|
||||||
|
Your nostr identity is a key that is **deliberately not derived from your wallet
|
||||||
|
seed** (`src/nostr/identity.rs`) — so you can rotate it any time to stay
|
||||||
|
unlinkable without ever touching your funds. It's stored encrypted at rest
|
||||||
|
(NIP-49 ncryptsec under your wallet password).
|
||||||
|
|
||||||
|
You can optionally claim a human-readable **`username`** from a **name authority**
|
||||||
|
(a NIP-05 server). The authority is configurable (Settings → Identity → Name
|
||||||
|
authority; `NostrConfig::{nip05_server, home_domain, set_nip05_server}`), which is
|
||||||
|
what makes Goblin **federated**: a user on `bob@otherinstance.com` can pay
|
||||||
|
`alice@goblin.st`, because a full `name@domain` always resolves against that
|
||||||
|
domain's `/.well-known/nostr.json`. Bare names (`alice`) resolve against *your*
|
||||||
|
configured home authority.
|
||||||
|
|
||||||
|
Display rules (`data::display_name`, no `@` ever shown):
|
||||||
|
- A local **petname** wins.
|
||||||
|
- A verified name on **your home authority** shows bare (`alice`) + a check.
|
||||||
|
- A verified name on a **foreign authority** shows `alice · domain` + a check, so
|
||||||
|
it can never masquerade as a home name.
|
||||||
|
- Otherwise: a short npub.
|
||||||
|
|
||||||
|
Names are kept fresh: see [§11 Name freshness](#11-contacts--name-freshness).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Transport: NIP-17 gift wraps over Tor
|
||||||
|
|
||||||
|
A payment DM is built and sent by `send_payment_dm`; control messages (voids) by
|
||||||
|
`send_control_dm` (both in `src/nostr/client.rs`). The message structure
|
||||||
|
(`src/nostr/protocol.rs`):
|
||||||
|
|
||||||
|
- The **payload** is the raw Grin slatepack armor (`BEGINSLATEPACK… ENDSLATEPACK`)
|
||||||
|
inside a kind-14 rumor, prefixed with a human preamble (`[Goblin] GRIN payment
|
||||||
|
message — open in Goblin …`) so a non-Goblin nostr client shows something sane.
|
||||||
|
- **Tags:** a `["goblin","1"]` protocol marker always; an optional `["subject", …]`
|
||||||
|
carrying the user's note (sanitized); control DMs carry
|
||||||
|
`["goblin-action","void", slate_id]` and **no** slatepack.
|
||||||
|
- The rumor is sealed and wrapped as a **kind-1059 gift wrap** (NIP-59 + NIP-44
|
||||||
|
encryption) via nostr-sdk's `send_private_msg_to`. Relays only ever see
|
||||||
|
ciphertext — never the amount, sender, or recipient.
|
||||||
|
|
||||||
|
**Where it's delivered:** the recipient's **kind-10050 DM-relay list**
|
||||||
|
(`fetch_dm_relays`), with our own default relays as fallback, plus any relay
|
||||||
|
hints carried by a pasted `nprofile`. Default relays: `relay.goblin.st`,
|
||||||
|
`relay.damus.io`, `nos.lol` (`src/nostr/relays.rs`), capped at `MAX_DM_RELAYS`.
|
||||||
|
|
||||||
|
**How relays are reached:** every relay connection runs through an in-process
|
||||||
|
**Tor** client (arti, linked directly into the wallet binary — no sidecar), via
|
||||||
|
`TorWebSocketTransport` (`run_service` waits for Tor to be ready before dialing).
|
||||||
|
So the relay never sees your IP: the money-path relay is dialed at its pinned
|
||||||
|
`.onion` address, and any relay without one is reached over a Tor exit to its
|
||||||
|
clearnet host. The Grin *node* connection (block sync + broadcasting the final tx)
|
||||||
|
is direct clearnet — it's public chain data, the same for everyone, not tied to
|
||||||
|
your identity.
|
||||||
|
|
||||||
|
The UI tracks an outgoing attempt via a coarse **send phase**
|
||||||
|
(`client::send_phase`): `IDLE / WORKING / SENT / FAILED / REQUEST_BLOCKED`, with a
|
||||||
|
human-readable failure reason on `FAILED`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Flow A — Pay by username/npub (Standard, we send)
|
||||||
|
|
||||||
|
Dispatched as `WalletTask::NostrSend(amount, npub, note, relay_hints)`; handled in
|
||||||
|
`wallet.rs`.
|
||||||
|
|
||||||
|
1. `w.send(amount)` builds the **S1** slate and **locks our outputs** (the funds
|
||||||
|
are reserved but not yet spent).
|
||||||
|
2. **Save meta `Created`** *before* any network call — this is the durable point
|
||||||
|
a crash recovers from.
|
||||||
|
3. `send_payment_dm` delivers S1 → **`AwaitingS2`** (storing the gift-wrap event
|
||||||
|
id). On dispatch failure → **`SendFailed`** (retryable). On success the contact
|
||||||
|
is created/refreshed (so people you pay appear in Suggested) and a background
|
||||||
|
NIP-05 lookup resolves their name.
|
||||||
|
4. The recipient replies S2 (Flow B). When their S2 gift wrap arrives, the ingest
|
||||||
|
guard routes it to `nostr_finalize_post`, which finalizes **S3** and posts it
|
||||||
|
on-chain → **`Finalized`**.
|
||||||
|
|
||||||
|
```
|
||||||
|
Created ──(S1 sent)──▶ AwaitingS2 ──(their S2 arrives)──▶ Finalized
|
||||||
|
└──(dispatch fails)──▶ SendFailed ──(retry)──▶ AwaitingS2
|
||||||
|
└──(manual cancel / 24h expiry)──▶ Cancelled (outputs unlocked)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Flow B — Receiving (Standard, we're paid)
|
||||||
|
|
||||||
|
Our service subscribes to kind-1059 gift wraps addressed to us
|
||||||
|
(`run_service`). When an **S1** arrives, `handle_wrap` runs the ingest pipeline
|
||||||
|
(§7) and `decide()` classifies it by the **accept policy**:
|
||||||
|
|
||||||
|
- `Everyone` → **AutoReceive** (auto-reply S2).
|
||||||
|
- `Contacts` → AutoReceive if the sender is a known contact, else **SurfaceIncoming** (an approval card).
|
||||||
|
- `Ask` → always SurfaceIncoming.
|
||||||
|
|
||||||
|
**AutoReceive:** `nostr_receive` builds the **S2** reply; we save meta
|
||||||
|
`Received` / **`ReceivedNoReply`**, mark the message processed, then dispatch S2 →
|
||||||
|
**`RepliedS2`**. If the S2 dispatch fails we stay at `ReceivedNoReply` and resend
|
||||||
|
on the next start (§9). The sender then finalizes S3 (Flow A step 4).
|
||||||
|
|
||||||
|
```
|
||||||
|
(incoming S1) ──▶ ReceivedNoReply ──(S2 dispatched)──▶ RepliedS2
|
||||||
|
└──(dispatch fails)──▶ stays ReceivedNoReply → resent on restart
|
||||||
|
```
|
||||||
|
|
||||||
|
**SurfaceIncoming** instead stores a `PaymentRequest` (status `Pending`) for the
|
||||||
|
user to approve or decline — see Flow D.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Flow C — Request money (Invoice)
|
||||||
|
|
||||||
|
**We request** — `WalletTask::NostrRequest(amount, npub, note, …)`:
|
||||||
|
|
||||||
|
1. First we check the recipient hasn't opted out: `accepts_requests` reads their
|
||||||
|
kind-0 `goblin_accepts_requests` field; an explicit `false` → phase
|
||||||
|
`REQUEST_BLOCKED` and we stop (fail-open: unknown/unreachable = allowed).
|
||||||
|
2. `issue_invoice(amount)` builds **I1** (no outputs locked — it's just an ask).
|
||||||
|
3. Save meta `RequestedByUs / Created`, dispatch I1 → **`AwaitingI2`**.
|
||||||
|
4. When the payer's **I2** arrives, the ingest guard finalizes **I3** and posts →
|
||||||
|
**`Finalized`**.
|
||||||
|
|
||||||
|
**They approve & pay** (the other side of the same flow) is Flow D.
|
||||||
|
|
||||||
|
```
|
||||||
|
Created ──(I1 sent)──▶ AwaitingI2 ──(their I2 arrives)──▶ Finalized
|
||||||
|
└──(SendFailed → retry) └──(cancel / expiry)──▶ Cancelled
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Flow D — Approving an incoming request (we pay an invoice)
|
||||||
|
|
||||||
|
Someone's **I1** is *always* surfaced as a `PaymentRequest`, **never auto-paid**
|
||||||
|
(a hard security invariant). It shows in the Requests list. The user can:
|
||||||
|
|
||||||
|
- **Approve** → `WalletTask::NostrPayRequest(rumor_id)`: re-parse the stored
|
||||||
|
slatepack (must still be I1), `nostr_pay` builds **I2** (this is where *we* pay),
|
||||||
|
save meta `RequestedOfUs / ReceivedNoReply`, dispatch I2 → **`PaidAwaitingFinalize`**,
|
||||||
|
mark the request `Approved`. The requester then finalizes I3.
|
||||||
|
A "Paying…" spinner shows while this runs; the card clears on success.
|
||||||
|
- **Decline** → `WalletTask::NostrDeclineRequest(rumor_id)`: mark `Declined` and
|
||||||
|
send a **void** control DM so the requester's side clears too.
|
||||||
|
|
||||||
|
A surfaced incoming *Standard* S1 (from SurfaceIncoming) is approved the same way,
|
||||||
|
but routes through `nostr_receive` (Flow B) rather than `nostr_pay`.
|
||||||
|
|
||||||
|
`RequestStatus`: `Pending → Approved | Declined | Expired | Cancelled`
|
||||||
|
(`Cancelled` = the requester withdrew it via a void).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. The ingest guard — `decide()`
|
||||||
|
|
||||||
|
Every incoming gift wrap is judged by `decide()` (`src/nostr/ingest.rs`), a
|
||||||
|
**positive allow-list**: anything not explicitly accepted is `Drop`ped. This is
|
||||||
|
the security core. Summary:
|
||||||
|
|
||||||
|
| Incoming state | Condition | Decision |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `Standard1` | amount 0, or slate already known | **Drop** |
|
||||||
|
| `Standard1` | new, policy `Everyone` (or `Contacts` + known) | **AutoReceive** |
|
||||||
|
| `Standard1` | new, policy `Contacts` + unknown, or `Ask` | **SurfaceIncoming** |
|
||||||
|
| `Standard2` | matches our pending `Sent` tx (status `AwaitingS2/Created/SendFailed`) **and** sender == stored counterparty **and** the tx exists | **FinalizePost** |
|
||||||
|
| `Standard2` | sender mismatch, or status `Cancelled`/`Finalized`, or no meta | **Drop** |
|
||||||
|
| `Invoice1` | amount 0, already known, or incoming-requests disabled | **Drop** |
|
||||||
|
| `Invoice1` | otherwise | **SurfaceRequest** (never auto-pay) |
|
||||||
|
| `Invoice2` | matches our pending `RequestedByUs` tx + sender match | **FinalizePost** |
|
||||||
|
| `Invoice3` / unknown | — | **Drop** |
|
||||||
|
|
||||||
|
Key consequences:
|
||||||
|
- A **late S2 on a cancelled send** falls through to `Drop` — so cancelling is
|
||||||
|
safe even if the recipient's reply is still in flight (the cancel marks the meta
|
||||||
|
`Cancelled` *first*, and `decide()` then drops the S2).
|
||||||
|
- Finalize only happens for a slate we are actually waiting on, from the exact key
|
||||||
|
we sent to.
|
||||||
|
- Invoices are never auto-paid.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Cancel & reclaim
|
||||||
|
|
||||||
|
A stuck outgoing send (recipient never replied) locks your outputs. You can
|
||||||
|
reclaim them manually from the receipt's **"Cancel payment"** button, or the 24h
|
||||||
|
auto-expiry does it for you (§10).
|
||||||
|
|
||||||
|
`WalletTask::NostrCancelSend(slate_id)` (`wallet.rs`):
|
||||||
|
1. Take the `cancel_finalize_lock` — this **serializes against a concurrent S2
|
||||||
|
finalize** so the two can't both win (cancel-and-post would be a double action).
|
||||||
|
2. **Re-check live state under the lock.** If the tx is already `Finalized`, or
|
||||||
|
confirmed/posted on-chain → do nothing and return `CancelOutcome::AlreadyCompleted`
|
||||||
|
("This payment already went through and can't be cancelled"). If already
|
||||||
|
`Cancelled` → idempotent success.
|
||||||
|
3. Otherwise mark the meta **`Cancelled` first**, then `w.cancel(tx_id)` to unlock
|
||||||
|
the Grin outputs, then best-effort send a **void** control DM to the recipient
|
||||||
|
(they're likely offline). → `CancelOutcome::Cancelled` ("your funds are
|
||||||
|
available again").
|
||||||
|
|
||||||
|
**Receipt button visibility** (`cancelable_send` gate): shown only while the send
|
||||||
|
is still unanswered — direction `Sent`, status in `{Created, AwaitingS2,
|
||||||
|
SendFailed}`, **not** confirmed, **not** already cancelled, and either it never
|
||||||
|
reached a relay (`SendFailed`, shown immediately) or the grace window
|
||||||
|
(`cancel_grace_secs`, default 600s) has passed. The instant the recipient accepts
|
||||||
|
(status leaves that set) the button disappears.
|
||||||
|
|
||||||
|
**Recipient side / void ordering:** a void control message marks the slate so that
|
||||||
|
if the recipient's wallet later (or earlier) sees the S1, it's dropped — including
|
||||||
|
the **void-before-S1** race, where the void arrives first and is recorded as
|
||||||
|
`void:{slate_id}:{sender}` so the subsequent S1 is dropped.
|
||||||
|
|
||||||
|
There are sibling tasks for the other directions: `NostrCancelOutgoing` (withdraw
|
||||||
|
an invoice we issued) and `NostrDeclineRequest` (decline an incoming request) —
|
||||||
|
both send a void and mark the local record.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. Auto-expiry (24h)
|
||||||
|
|
||||||
|
`expire_stale` (`src/nostr/client.rs`) runs from the sync loop. Any non-terminal
|
||||||
|
meta older than `expiry_secs` (default 24h) is expired:
|
||||||
|
|
||||||
|
- If it **locked our outputs** (`expiry_locks_outputs`: a `Sent` send in
|
||||||
|
`Created/AwaitingS2/SendFailed`, or a `RequestedOfUs` invoice we paid in
|
||||||
|
`PaidAwaitingFinalize`) → cancel the Grin tx to unlock, and mark meta `Cancelled`.
|
||||||
|
- If it locked nothing of ours (incoming payments, invoices we issued) → just
|
||||||
|
annotate `Cancelled`.
|
||||||
|
- Pending incoming `PaymentRequest`s flip to `Expired`.
|
||||||
|
|
||||||
|
This is the same unlock path as manual cancel; the manual button just lets you act
|
||||||
|
before the 24h.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. Crash recovery (`reconcile`)
|
||||||
|
|
||||||
|
On service start, `reconcile` (`client.rs`) re-dispatches any pending outgoing
|
||||||
|
message within a 7-day window, by `(direction, status)`:
|
||||||
|
|
||||||
|
| Direction · status | Slate | Action |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `Sent` · `Created`/`SendFailed` | Standard1 | resend S1 → `AwaitingS2` |
|
||||||
|
| `RequestedByUs` · `Created`/`SendFailed` | Invoice1 | resend I1 → `AwaitingI2` |
|
||||||
|
| `Received` · `ReceivedNoReply` | Standard2 | resend S2 → `RepliedS2` |
|
||||||
|
| `RequestedOfUs` · `ReceivedNoReply` | Invoice2 | resend I2 → `PaidAwaitingFinalize` |
|
||||||
|
|
||||||
|
Because the slatepack text is persisted and the meta is written *before* every
|
||||||
|
dispatch, a crash at any point is recoverable: re-sending an already-delivered
|
||||||
|
message is harmless (the peer dedups it; see §12).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 12. Confirmations (X / N)
|
||||||
|
|
||||||
|
A posted Grin tx matures over `min_confirmations` blocks (default 10) before it's
|
||||||
|
spendable. Grin marks a tx `confirmed` at the **first** block, but Goblin's
|
||||||
|
receipt counts toward the spendable threshold so the number actually moves
|
||||||
|
(`data::receipt_detail`):
|
||||||
|
|
||||||
|
- broadcast, no block yet → `0 / N`
|
||||||
|
- on-chain, immature → `count / N` where `count = tip − inclusion_height + 1`
|
||||||
|
- `count ≥ N` → matured (shown as complete; the receipt's network-fee row is shown
|
||||||
|
only for outgoing payments — a recipient pays no fee).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 13. Reliability primitives
|
||||||
|
|
||||||
|
- **Dedup / processed markers** (`store::{is_processed, mark_processed,
|
||||||
|
prune_processed}`): every wrap is recorded at three levels — the gift-wrap event
|
||||||
|
id, the inner rumor id, and `slate:{id}:{state}` — so a replayed or re-sent
|
||||||
|
message is processed exactly once. Markers TTL out after 30 days
|
||||||
|
(pruned on start + hourly).
|
||||||
|
- **Rate limiting** (`allow_sender`): per-sender sliding window — 30 events/hour
|
||||||
|
for known contacts, 10/hour for unknowns — plus a global decrypt ceiling
|
||||||
|
(~120 NIP-44 unwraps/min) to bound CPU/battery against fresh-keypair spam. A
|
||||||
|
message dropped purely for the *global* ceiling isn't marked processed, so it can
|
||||||
|
be retried later.
|
||||||
|
- **Seal integrity:** the gift-wrap seal signer must equal the inner rumor author,
|
||||||
|
and self-addressed messages are dropped.
|
||||||
|
- **The cancel/finalize lock** (§9) prevents a cancel and a finalize from both
|
||||||
|
succeeding on the same slate.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 14. Name freshness (contacts)
|
||||||
|
|
||||||
|
Cached `@usernames` are re-validated against the name authority on a periodic
|
||||||
|
sweep (`NAME_REVERIFY_INTERVAL_SECS`, ~78s, capped per tick), and once at app open
|
||||||
|
(persisted `last_name_sweep_at`, gated to the interval).
|
||||||
|
`nip05::check` returns `Verified / Mismatch / Unreachable`: a name is only
|
||||||
|
**cleared** (falls back to the npub) on a definitive `Mismatch` (the server says
|
||||||
|
it's gone or now maps to a different key) — never on a transient network failure.
|
||||||
|
This catches released or reassigned names and stops a freed name from
|
||||||
|
impersonating someone. A user-set petname is never touched.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 15. File map
|
||||||
|
|
||||||
|
| Concern | File |
|
||||||
|
| --- | --- |
|
||||||
|
| Status / direction / meta types | `src/nostr/types.rs` |
|
||||||
|
| Gift-wrap + control message build/parse | `src/nostr/protocol.rs` |
|
||||||
|
| Service loop, send/receive/finalize, expiry, reconcile, name sweep | `src/nostr/client.rs` |
|
||||||
|
| Ingest allow-list | `src/nostr/ingest.rs` |
|
||||||
|
| Wallet task handlers (NostrSend / Request / PayRequest / CancelSend / finalize) | `src/wallet/wallet.rs` |
|
||||||
|
| Task definitions | `src/wallet/types.rs` |
|
||||||
|
| Metadata + dedup + contacts store | `src/nostr/store.rs` |
|
||||||
|
| NIP-05 resolve / verify / register, name authority | `src/nostr/nip05.rs` |
|
||||||
|
| Identity (key, NIP-49 backup) | `src/nostr/identity.rs` |
|
||||||
|
| Receipt / activity / confirmations / display name | `src/gui/views/goblin/data.rs` |
|
||||||
|
| Relay defaults + name-authority defaults | `src/nostr/relays.rs` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
🤖 Documentation written with AI pair-programming assistance (Claude).
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
//! Avatar sizing-checkpoint harness: renders the REAL `avatar_tex` (custom
|
||||||
|
//! image, no ring) and `gradient_avatar` across every size the app uses.
|
||||||
|
//! Names never affect the avatar — this just checks sizing by eye.
|
||||||
|
//! Run: `cargo run --example avatar_ring` (screenshots taken externally).
|
||||||
|
|
||||||
|
use eframe::egui;
|
||||||
|
use grim::gui::views::goblin::widgets as w;
|
||||||
|
|
||||||
|
const SIZES: [f32; 6] = [28.0, 40.0, 48.0, 56.0, 72.0, 96.0];
|
||||||
|
const NAMES: [&str; 3] = ["alice", "bob", "carmen"];
|
||||||
|
|
||||||
|
struct App {
|
||||||
|
tex: Vec<egui::TextureHandle>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A synthetic "profile photo": diagonal two-tone blend with a light disc, so
|
||||||
|
/// sizing is judged against something photo-like rather than a flat fill.
|
||||||
|
fn photo(ctx: &egui::Context, name: &str, a: [u8; 3], b: [u8; 3]) -> egui::TextureHandle {
|
||||||
|
const N: usize = 128;
|
||||||
|
let mut px = Vec::with_capacity(N * N);
|
||||||
|
for y in 0..N {
|
||||||
|
for x in 0..N {
|
||||||
|
let t = (x + y) as f32 / (2 * N) as f32;
|
||||||
|
let mut r = a[0] as f32 * (1.0 - t) + b[0] as f32 * t;
|
||||||
|
let mut g = a[1] as f32 * (1.0 - t) + b[1] as f32 * t;
|
||||||
|
let mut bl = a[2] as f32 * (1.0 - t) + b[2] as f32 * t;
|
||||||
|
let dx = x as f32 - 44.0;
|
||||||
|
let dy = y as f32 - 40.0;
|
||||||
|
if (dx * dx + dy * dy).sqrt() < 26.0 {
|
||||||
|
r = (r + 90.0).min(255.0);
|
||||||
|
g = (g + 90.0).min(255.0);
|
||||||
|
bl = (bl + 90.0).min(255.0);
|
||||||
|
}
|
||||||
|
px.push(egui::Color32::from_rgb(r as u8, g as u8, bl as u8));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let img = egui::ColorImage {
|
||||||
|
size: [N, N],
|
||||||
|
source_size: egui::Vec2::splat(N as f32),
|
||||||
|
pixels: px,
|
||||||
|
};
|
||||||
|
ctx.load_texture(name.to_string(), img, Default::default())
|
||||||
|
}
|
||||||
|
|
||||||
|
impl App {
|
||||||
|
fn new(cc: &eframe::CreationContext) -> Self {
|
||||||
|
egui_extras::install_image_loaders(&cc.egui_ctx);
|
||||||
|
let tex = vec![
|
||||||
|
photo(&cc.egui_ctx, "alice", [180, 120, 90], [90, 60, 120]),
|
||||||
|
photo(&cc.egui_ctx, "bob", [70, 110, 160], [40, 160, 120]),
|
||||||
|
photo(&cc.egui_ctx, "carmen", [160, 70, 90], [220, 170, 80]),
|
||||||
|
];
|
||||||
|
Self { tex }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl eframe::App for App {
|
||||||
|
fn update(&mut self, ctx: &egui::Context, _f: &mut eframe::Frame) {
|
||||||
|
egui::CentralPanel::default()
|
||||||
|
.frame(egui::Frame::default().fill(egui::Color32::from_rgb(0xFA, 0xFA, 0xF7)))
|
||||||
|
.show(ctx, |ui| {
|
||||||
|
ui.add_space(10.0);
|
||||||
|
ui.heading("avatar sizing sheet (no ring — names never affect the avatar)");
|
||||||
|
ui.add_space(12.0);
|
||||||
|
for (i, name) in NAMES.iter().enumerate() {
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
ui.add_space(12.0);
|
||||||
|
ui.label(format!("{name:>7}"));
|
||||||
|
for size in SIZES {
|
||||||
|
ui.add_space(14.0);
|
||||||
|
w::avatar_tex(ui, &self.tex[i], name, size);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
ui.add_space(14.0);
|
||||||
|
}
|
||||||
|
ui.separator();
|
||||||
|
ui.label("anonymous npub (grinmark gradient):");
|
||||||
|
ui.add_space(8.0);
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
ui.add_space(12.0);
|
||||||
|
ui.label(" ");
|
||||||
|
for (i, size) in SIZES.iter().enumerate() {
|
||||||
|
ui.add_space(14.0);
|
||||||
|
w::gradient_avatar(ui, &format!("{i}deadbeef{i}"), *size);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
ui.add_space(10.0);
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
ui.add_space(12.0);
|
||||||
|
ui.label("sizes: ");
|
||||||
|
for size in SIZES {
|
||||||
|
ui.add_space(14.0);
|
||||||
|
ui.allocate_ui(egui::Vec2::new(size, 16.0), |ui| {
|
||||||
|
ui.centered_and_justified(|ui| ui.small(format!("{size}")));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() -> eframe::Result {
|
||||||
|
let opts = eframe::NativeOptions {
|
||||||
|
viewport: egui::ViewportBuilder::default().with_inner_size([900.0, 640.0]),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
eframe::run_native(
|
||||||
|
"avatar-ring",
|
||||||
|
opts,
|
||||||
|
Box::new(|cc| Ok(Box::new(App::new(cc)))),
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
Copyright 2024 The Geist Project Authors (https://github.com/vercel/geist-font)
|
||||||
|
|
||||||
|
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||||
|
This license is copied below, and is also available with a FAQ at:
|
||||||
|
https://openfontlicense.org
|
||||||
|
|
||||||
|
|
||||||
|
-----------------------------------------------------------
|
||||||
|
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||||
|
-----------------------------------------------------------
|
||||||
|
|
||||||
|
PREAMBLE
|
||||||
|
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||||
|
development of collaborative font projects, to support the font creation
|
||||||
|
efforts of academic and linguistic communities, and to provide a free and
|
||||||
|
open framework in which fonts may be shared and improved in partnership
|
||||||
|
with others.
|
||||||
|
|
||||||
|
The OFL allows the licensed fonts to be used, studied, modified and
|
||||||
|
redistributed freely as long as they are not sold by themselves. The
|
||||||
|
fonts, including any derivative works, can be bundled, embedded,
|
||||||
|
redistributed and/or sold with any software provided that any reserved
|
||||||
|
names are not used by derivative works. The fonts and derivatives,
|
||||||
|
however, cannot be released under any other type of license. The
|
||||||
|
requirement for fonts to remain under this license does not apply
|
||||||
|
to any document created using the fonts or their derivatives.
|
||||||
|
|
||||||
|
DEFINITIONS
|
||||||
|
"Font Software" refers to the set of files released by the Copyright
|
||||||
|
Holder(s) under this license and clearly marked as such. This may
|
||||||
|
include source files, build scripts and documentation.
|
||||||
|
|
||||||
|
"Reserved Font Name" refers to any names specified as such after the
|
||||||
|
copyright statement(s).
|
||||||
|
|
||||||
|
"Original Version" refers to the collection of Font Software components as
|
||||||
|
distributed by the Copyright Holder(s).
|
||||||
|
|
||||||
|
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||||
|
or substituting -- in part or in whole -- any of the components of the
|
||||||
|
Original Version, by changing formats or by porting the Font Software to a
|
||||||
|
new environment.
|
||||||
|
|
||||||
|
"Author" refers to any designer, engineer, programmer, technical
|
||||||
|
writer or other person who contributed to the Font Software.
|
||||||
|
|
||||||
|
PERMISSION & CONDITIONS
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||||
|
redistribute, and sell modified and unmodified copies of the Font
|
||||||
|
Software, subject to the following conditions:
|
||||||
|
|
||||||
|
1) Neither the Font Software nor any of its individual components,
|
||||||
|
in Original or Modified Versions, may be sold by itself.
|
||||||
|
|
||||||
|
2) Original or Modified Versions of the Font Software may be bundled,
|
||||||
|
redistributed and/or sold with any software, provided that each copy
|
||||||
|
contains the above copyright notice and this license. These can be
|
||||||
|
included either as stand-alone text files, human-readable headers or
|
||||||
|
in the appropriate machine-readable metadata fields within text or
|
||||||
|
binary files as long as those fields can be easily viewed by the user.
|
||||||
|
|
||||||
|
3) No Modified Version of the Font Software may use the Reserved Font
|
||||||
|
Name(s) unless explicit written permission is granted by the corresponding
|
||||||
|
Copyright Holder. This restriction only applies to the primary font name as
|
||||||
|
presented to the users.
|
||||||
|
|
||||||
|
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||||
|
Software shall not be used to promote, endorse or advertise any
|
||||||
|
Modified Version, except to acknowledge the contribution(s) of the
|
||||||
|
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||||
|
permission.
|
||||||
|
|
||||||
|
5) The Font Software, modified or unmodified, in part or in whole,
|
||||||
|
must be distributed entirely under this license, and must not be
|
||||||
|
distributed under any other license. The requirement for fonts to
|
||||||
|
remain under this license does not apply to any document created
|
||||||
|
using the Font Software.
|
||||||
|
|
||||||
|
TERMINATION
|
||||||
|
This license becomes null and void if any of the above conditions are
|
||||||
|
not met.
|
||||||
|
|
||||||
|
DISCLAIMER
|
||||||
|
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||||
|
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||||
|
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||||
|
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||||
|
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||||
|
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||||
|
Before Width: | Height: | Size: 191 KiB |
|
After Width: | Height: | Size: 37 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
@@ -0,0 +1,48 @@
|
|||||||
|
<?xml version="1.0" standalone="no"?>
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
|
||||||
|
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
|
||||||
|
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="600.000000pt" height="601.000000pt" viewBox="0 0 600.000000 601.000000"
|
||||||
|
preserveAspectRatio="xMidYMid meet">
|
||||||
|
<g transform="translate(0.000000,601.000000) scale(0.050000,-0.050000)"
|
||||||
|
fill="#ffffff" stroke="none">
|
||||||
|
<path d="M195 11784 c-515 -1551 98 -2966 1520 -3514 171 -66 171 -72 2 -72
|
||||||
|
-415 0 -893 215 -1273 572 -178 167 -181 163 -77 -83 478 -1130 1770 -1734
|
||||||
|
2963 -1384 283 83 309 101 420 292 376 648 1038 1116 1763 1245 l143 26 100
|
||||||
|
202 c887 1780 -911 3344 -3076 2675 l-170 -52 220 -14 c480 -32 818 -114 1118
|
||||||
|
-273 258 -137 548 -414 624 -597 l32 -76 -87 76 c-435 375 -938 524 -1557 461
|
||||||
|
-273 -28 -340 -39 -740 -120 -893 -182 -1449 24 -1756 650 l-98 200 -71 -214z"/>
|
||||||
|
<path d="M9720 10053 c0 -146 -256 -556 -441 -705 -381 -308 -766 -380 -1559
|
||||||
|
-290 -1209 137 -2026 -255 -2519 -1208 -78 -151 -82 -156 -72 -77 18 140 128
|
||||||
|
450 210 592 43 74 73 135 67 135 -25 0 -397 -184 -512 -253 -966 -580 -1594
|
||||||
|
-1674 -1594 -2777 0 -104 -4 -190 -10 -190 -5 0 -65 43 -132 95 -650 503
|
||||||
|
-1118 775 -1606 935 -290 95 -302 94 -242 -15 52 -95 166 -372 191 -465 9 -33
|
||||||
|
34 -121 56 -195 48 -161 120 -508 194 -935 118 -684 437 -1371 795 -1715 185
|
||||||
|
-178 324 -239 594 -262 144 -13 150 -15 147 -63 -1 -27 -14 -174 -28 -326 -83
|
||||||
|
-902 258 -1568 1037 -2024 139 -81 161 -80 127 4 -37 96 -85 369 -96 546 l-10
|
||||||
|
170 46 -60 c328 -431 869 -753 1497 -891 351 -77 1285 -67 1426 15 9 5 -104
|
||||||
|
50 -250 98 -649 217 -1341 668 -1680 1096 -81 102 -88 147 -11 78 157 -142
|
||||||
|
653 -406 925 -493 783 -249 1542 -242 2332 20 l274 91 106 -83 c298 -236 798
|
||||||
|
-328 1259 -231 197 42 196 38 32 169 -156 124 -344 356 -443 546 l-60 116 70
|
||||||
|
52 c544 408 783 906 627 1300 l-41 102 170 138 c442 355 584 624 813 1537 186
|
||||||
|
742 257 952 452 1328 l118 227 -145 -14 c-693 -68 -1425 -411 -1729 -810 -99
|
||||||
|
-130 -112 -127 -165 44 -54 175 -217 511 -308 636 -64 88 -70 91 -224 117
|
||||||
|
-314 52 -637 184 -956 390 -260 168 -509 436 -280 302 127 -74 302 -160 430
|
||||||
|
-211 97 -38 146 -55 348 -118 335 -105 983 -129 1280 -47 961 264 1554 1048
|
||||||
|
1729 2286 28 199 45 685 23 677 -9 -4 -73 -77 -141 -163 -650 -810 -1594
|
||||||
|
-1147 -2451 -873 -251 80 -246 76 -171 121 395 240 638 767 578 1253 -25 209
|
||||||
|
-77 395 -77 278z m-682 -4962 c306 -158 601 -1396 416 -1747 -118 -224 -283
|
||||||
|
-345 -526 -387 -455 -78 -577 227 -456 1143 96 725 320 1118 566 991z m-3115
|
||||||
|
-156 c371 -172 699 -815 799 -1565 34 -251 19 -307 -108 -422 -575 -519 -1624
|
||||||
|
-162 -1931 657 -265 709 593 1630 1240 1330z m5261 -120 c20 -377 -135 -912
|
||||||
|
-290 -995 -70 -38 -72 -35 -157 230 l-64 200 -24 -90 c-50 -192 -144 -340
|
||||||
|
-215 -340 -111 0 -316 804 -228 893 53 53 200 -130 226 -283 l14 -80 36 105
|
||||||
|
c100 293 222 333 332 109 l54 -110 35 105 c89 260 271 427 281 256z m-8491
|
||||||
|
-284 l75 -230 52 160 c89 272 210 336 295 154 126 -268 210 -757 142 -825 -44
|
||||||
|
-44 -163 120 -247 340 -12 33 -19 24 -39 -50 -89 -330 -286 -346 -374 -30
|
||||||
|
l-22 80 -35 -125 c-54 -196 -223 -428 -275 -377 -37 37 -29 448 11 608 70 276
|
||||||
|
219 524 314 524 17 0 56 -87 103 -229z m4164 -2698 c137 -311 883 -373 1408
|
||||||
|
-116 162 78 171 64 42 -61 -317 -305 -854 -408 -1271 -244 -223 87 -516 338
|
||||||
|
-516 442 0 53 140 267 212 324 l58 46 14 -152 c8 -84 31 -191 53 -239z"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 3.2 KiB |
@@ -0,0 +1,48 @@
|
|||||||
|
<?xml version="1.0" standalone="no"?>
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
|
||||||
|
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
|
||||||
|
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="600.000000pt" height="601.000000pt" viewBox="0 0 600.000000 601.000000"
|
||||||
|
preserveAspectRatio="xMidYMid meet">
|
||||||
|
<g transform="translate(0.000000,601.000000) scale(0.050000,-0.050000)"
|
||||||
|
fill="#000000" stroke="none">
|
||||||
|
<path d="M195 11784 c-515 -1551 98 -2966 1520 -3514 171 -66 171 -72 2 -72
|
||||||
|
-415 0 -893 215 -1273 572 -178 167 -181 163 -77 -83 478 -1130 1770 -1734
|
||||||
|
2963 -1384 283 83 309 101 420 292 376 648 1038 1116 1763 1245 l143 26 100
|
||||||
|
202 c887 1780 -911 3344 -3076 2675 l-170 -52 220 -14 c480 -32 818 -114 1118
|
||||||
|
-273 258 -137 548 -414 624 -597 l32 -76 -87 76 c-435 375 -938 524 -1557 461
|
||||||
|
-273 -28 -340 -39 -740 -120 -893 -182 -1449 24 -1756 650 l-98 200 -71 -214z"/>
|
||||||
|
<path d="M9720 10053 c0 -146 -256 -556 -441 -705 -381 -308 -766 -380 -1559
|
||||||
|
-290 -1178 133 -2048 -270 -2488 -1152 -57 -115 -92 -149 -92 -89 0 78 123
|
||||||
|
415 199 548 43 74 73 135 67 135 -25 0 -397 -184 -512 -253 -962 -578 -1594
|
||||||
|
-1675 -1594 -2767 0 -209 -2 -210 -154 -95 -393 297 -523 388 -751 525 -321
|
||||||
|
192 -571 311 -843 400 -290 95 -302 94 -242 -15 52 -95 166 -372 191 -465 9
|
||||||
|
-33 34 -121 56 -195 48 -161 120 -508 194 -935 118 -684 437 -1371 795 -1715
|
||||||
|
185 -178 324 -239 594 -262 144 -13 150 -15 147 -63 -1 -27 -14 -174 -28 -326
|
||||||
|
-83 -902 258 -1568 1037 -2024 139 -81 161 -80 127 4 -37 96 -85 369 -96 546
|
||||||
|
l-10 170 46 -60 c328 -431 869 -753 1497 -891 351 -77 1285 -67 1426 15 9 5
|
||||||
|
-104 50 -250 98 -653 218 -1499 778 -1704 1129 -43 73 -54 78 188 -79 230
|
||||||
|
-149 543 -303 750 -369 783 -249 1542 -242 2332 20 l274 91 106 -83 c298 -236
|
||||||
|
798 -328 1259 -231 197 42 196 38 32 169 -156 124 -344 356 -443 546 l-60 116
|
||||||
|
70 52 c544 408 783 906 627 1300 l-41 102 170 138 c442 355 584 624 813 1537
|
||||||
|
186 742 257 952 452 1328 l118 227 -145 -14 c-693 -68 -1425 -411 -1729 -810
|
||||||
|
-99 -130 -112 -127 -165 44 -54 175 -217 511 -308 636 -64 88 -70 91 -224 117
|
||||||
|
-419 70 -947 328 -1223 597 -98 96 -153 192 -70 122 41 -35 332 -177 487 -238
|
||||||
|
97 -38 146 -55 348 -118 335 -105 983 -129 1280 -47 1070 294 1688 1238 1761
|
||||||
|
2691 16 304 7 325 -82 190 -88 -134 -504 -535 -669 -645 -618 -412 -1228 -507
|
||||||
|
-1895 -296 -192 60 -199 77 -67 163 428 277 628 871 480 1428 -20 75 -38 98
|
||||||
|
-38 48z m-682 -4962 c306 -158 601 -1396 416 -1747 -118 -224 -283 -345 -526
|
||||||
|
-387 -455 -78 -577 227 -456 1143 96 725 320 1118 566 991z m-3115 -156 c371
|
||||||
|
-172 699 -815 799 -1565 34 -251 19 -307 -108 -422 -575 -519 -1624 -162
|
||||||
|
-1931 657 -265 709 593 1630 1240 1330z m5261 -120 c20 -377 -135 -912 -290
|
||||||
|
-995 -70 -38 -72 -35 -157 230 l-64 200 -24 -90 c-50 -192 -144 -340 -215
|
||||||
|
-340 -111 0 -316 804 -228 893 53 53 200 -130 226 -283 l14 -80 36 105 c100
|
||||||
|
293 222 333 332 109 l54 -110 35 105 c89 260 271 427 281 256z m-8491 -284
|
||||||
|
l75 -230 52 160 c89 272 210 336 295 154 126 -268 210 -757 142 -825 -44 -44
|
||||||
|
-163 120 -247 340 -12 33 -19 24 -39 -50 -89 -330 -286 -346 -374 -30 l-22 80
|
||||||
|
-35 -125 c-54 -196 -223 -428 -275 -377 -37 37 -29 448 11 608 70 276 219 524
|
||||||
|
314 524 17 0 56 -87 103 -229z m4164 -2698 c137 -310 856 -372 1401 -121 175
|
||||||
|
81 194 76 89 -23 -356 -336 -878 -447 -1310 -278 -224 88 -517 339 -517 443 0
|
||||||
|
53 140 267 212 324 l58 46 14 -152 c8 -84 31 -191 53 -239z"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 3.2 KiB |
@@ -0,0 +1,48 @@
|
|||||||
|
<?xml version="1.0" standalone="no"?>
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
|
||||||
|
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
|
||||||
|
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="600.000000pt" height="601.000000pt" viewBox="0 0 600.000000 601.000000"
|
||||||
|
preserveAspectRatio="xMidYMid meet">
|
||||||
|
<g transform="translate(0.000000,601.000000) scale(0.050000,-0.050000)"
|
||||||
|
fill="#ffffff" stroke="none">
|
||||||
|
<path d="M195 11784 c-515 -1551 98 -2966 1520 -3514 171 -66 171 -72 2 -72
|
||||||
|
-415 0 -893 215 -1273 572 -178 167 -181 163 -77 -83 478 -1130 1770 -1734
|
||||||
|
2963 -1384 283 83 309 101 420 292 376 648 1038 1116 1763 1245 l143 26 100
|
||||||
|
202 c887 1780 -911 3344 -3076 2675 l-170 -52 220 -14 c480 -32 818 -114 1118
|
||||||
|
-273 258 -137 548 -414 624 -597 l32 -76 -87 76 c-435 375 -938 524 -1557 461
|
||||||
|
-273 -28 -340 -39 -740 -120 -893 -182 -1449 24 -1756 650 l-98 200 -71 -214z"/>
|
||||||
|
<path d="M9720 10053 c0 -146 -256 -556 -441 -705 -381 -308 -766 -380 -1559
|
||||||
|
-290 -1209 137 -2026 -255 -2519 -1208 -78 -151 -82 -156 -72 -77 18 140 128
|
||||||
|
450 210 592 43 74 73 135 67 135 -25 0 -397 -184 -512 -253 -966 -580 -1594
|
||||||
|
-1674 -1594 -2777 0 -104 -4 -190 -10 -190 -5 0 -65 43 -132 95 -650 503
|
||||||
|
-1118 775 -1606 935 -290 95 -302 94 -242 -15 52 -95 166 -372 191 -465 9 -33
|
||||||
|
34 -121 56 -195 48 -161 120 -508 194 -935 118 -684 437 -1371 795 -1715 185
|
||||||
|
-178 324 -239 594 -262 144 -13 150 -15 147 -63 -1 -27 -14 -174 -28 -326 -83
|
||||||
|
-902 258 -1568 1037 -2024 139 -81 161 -80 127 4 -37 96 -85 369 -96 546 l-10
|
||||||
|
170 46 -60 c328 -431 869 -753 1497 -891 351 -77 1285 -67 1426 15 9 5 -104
|
||||||
|
50 -250 98 -649 217 -1341 668 -1680 1096 -81 102 -88 147 -11 78 157 -142
|
||||||
|
653 -406 925 -493 783 -249 1542 -242 2332 20 l274 91 106 -83 c298 -236 798
|
||||||
|
-328 1259 -231 197 42 196 38 32 169 -156 124 -344 356 -443 546 l-60 116 70
|
||||||
|
52 c544 408 783 906 627 1300 l-41 102 170 138 c442 355 584 624 813 1537 186
|
||||||
|
742 257 952 452 1328 l118 227 -145 -14 c-693 -68 -1425 -411 -1729 -810 -99
|
||||||
|
-130 -112 -127 -165 44 -54 175 -217 511 -308 636 -64 88 -70 91 -224 117
|
||||||
|
-314 52 -637 184 -956 390 -260 168 -509 436 -280 302 127 -74 302 -160 430
|
||||||
|
-211 97 -38 146 -55 348 -118 335 -105 983 -129 1280 -47 961 264 1554 1048
|
||||||
|
1729 2286 28 199 45 685 23 677 -9 -4 -73 -77 -141 -163 -650 -810 -1594
|
||||||
|
-1147 -2451 -873 -251 80 -246 76 -171 121 395 240 638 767 578 1253 -25 209
|
||||||
|
-77 395 -77 278z m-682 -4962 c306 -158 601 -1396 416 -1747 -118 -224 -283
|
||||||
|
-345 -526 -387 -455 -78 -577 227 -456 1143 96 725 320 1118 566 991z m-3115
|
||||||
|
-156 c371 -172 699 -815 799 -1565 34 -251 19 -307 -108 -422 -575 -519 -1624
|
||||||
|
-162 -1931 657 -265 709 593 1630 1240 1330z m5261 -120 c20 -377 -135 -912
|
||||||
|
-290 -995 -70 -38 -72 -35 -157 230 l-64 200 -24 -90 c-50 -192 -144 -340
|
||||||
|
-215 -340 -111 0 -316 804 -228 893 53 53 200 -130 226 -283 l14 -80 36 105
|
||||||
|
c100 293 222 333 332 109 l54 -110 35 105 c89 260 271 427 281 256z m-8491
|
||||||
|
-284 l75 -230 52 160 c89 272 210 336 295 154 126 -268 210 -757 142 -825 -44
|
||||||
|
-44 -163 120 -247 340 -12 33 -19 24 -39 -50 -89 -330 -286 -346 -374 -30
|
||||||
|
l-22 80 -35 -125 c-54 -196 -223 -428 -275 -377 -37 37 -29 448 11 608 70 276
|
||||||
|
219 524 314 524 17 0 56 -87 103 -229z m4164 -2698 c137 -311 883 -373 1408
|
||||||
|
-116 162 78 171 64 42 -61 -317 -305 -854 -408 -1271 -244 -223 87 -516 338
|
||||||
|
-516 442 0 53 140 267 212 324 l58 46 14 -152 c8 -84 31 -191 53 -239z"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 27 KiB |
@@ -0,0 +1 @@
|
|||||||
|
goblin.png
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
[Desktop Entry]
|
||||||
|
Name=Goblin
|
||||||
|
Exec=goblin %u
|
||||||
|
Icon=goblin
|
||||||
|
Type=Application
|
||||||
|
Categories=Finance
|
||||||
|
MimeType=application/x-slatepack;text/plain;x-scheme-handler/goblin;
|
||||||
|
After Width: | Height: | Size: 37 KiB |
@@ -1 +0,0 @@
|
|||||||
grim.png
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
[Desktop Entry]
|
|
||||||
Name=Grim
|
|
||||||
Exec=grim
|
|
||||||
Icon=grim
|
|
||||||
Type=Application
|
|
||||||
Categories=Finance
|
|
||||||
MimeType=application/x-slatepack;text/plain;
|
|
||||||
|
Before Width: | Height: | Size: 24 KiB |
@@ -1,27 +1,63 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
#
|
||||||
|
# Build a portable, single-file Goblin AppImage.
|
||||||
|
#
|
||||||
|
# Usage: linux/build_release.sh [platform]
|
||||||
|
# platform: 'x86_64' (default) or 'arm'
|
||||||
|
#
|
||||||
|
# Goblin links the Tor transport (embedded arti) IN-PROCESS, so the AppImage is
|
||||||
|
# one self-contained binary with no sidecar to embed or ship beside it.
|
||||||
|
|
||||||
case $2 in
|
set -euo pipefail
|
||||||
x86_64|arm)
|
|
||||||
;;
|
platform="${1:-x86_64}"
|
||||||
*)
|
case "${platform}" in
|
||||||
echo "Usage: release_linux.sh [platform] [version]\n - platform: 'x86_64', 'arm'" >&2
|
x86_64) arch="x86_64-unknown-linux-gnu"; appimage_arch="x86_64" ;;
|
||||||
exit 1
|
arm) arch="aarch64-unknown-linux-gnu"; appimage_arch="aarch64" ;;
|
||||||
|
*) echo "Usage: build_release.sh [platform] (platform: 'x86_64' | 'arm')" >&2; exit 1 ;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
# Setup build directory
|
# Repo root (this script lives in linux/).
|
||||||
BASEDIR=$(cd $(dirname $0) && pwd)
|
BASEDIR=$(cd "$(dirname "$0")" && pwd)
|
||||||
cd ${BASEDIR}
|
cd "${BASEDIR}/.."
|
||||||
cd ..
|
|
||||||
|
|
||||||
# Setup platform argument
|
# Prefer the GRIM-canonical toolchains (zig + appimagetool from code.gri.mw/DEV);
|
||||||
[[ $2 == "x86_64" ]] && arch+=(x86_64-unknown-linux-gnu)
|
# scripts/toolchain.sh fetches them and writes this env. Falls back to system
|
||||||
[[ $2 == "arm" ]] && arch+=(aarch64-unknown-linux-gnu)
|
# installs when it's absent.
|
||||||
|
[ -f .toolchains/env.sh ] && source .toolchains/env.sh
|
||||||
|
|
||||||
rustup target add ${arch}
|
rustup target add "${arch}"
|
||||||
cargo install cargo-zigbuild
|
command -v cargo-zigbuild >/dev/null || cargo install cargo-zigbuild
|
||||||
cargo zigbuild --release --target ${arch}
|
|
||||||
|
|
||||||
# Create AppImage with https://github.com/AppImage/appimagetool
|
# Portable cross-build to glibc 2.17. Three zig-specific fixes:
|
||||||
cp target/${arch}/release/grim linux/Grim.AppDir/AppRun
|
# - CRoaring's AVX512 path won't compile under zig's clang (evex512 error).
|
||||||
rm target/${arch}/release/*.AppImage
|
# - OpenSSL is vendored in Cargo.toml, so no system libssl is needed.
|
||||||
appimagetool linux/Grim.AppDir target/${arch}/release/grim-v$2-linux-$1.AppImage
|
# - v4l2-sys (camera/QR backend) runs bindgen over linux/videodev2.h, a kernel
|
||||||
|
# UAPI header missing from zig 0.12.1's glibc-2.17 sysroot; point bindgen at
|
||||||
|
# the host's kernel headers. This only reads struct layouts — the actual libc
|
||||||
|
# linkage stays glibc-2.17, so portability is unaffected.
|
||||||
|
export CFLAGS_x86_64_unknown_linux_gnu="-DCROARING_COMPILER_SUPPORTS_AVX512=0"
|
||||||
|
export CXXFLAGS_x86_64_unknown_linux_gnu="-DCROARING_COMPILER_SUPPORTS_AVX512=0"
|
||||||
|
export BINDGEN_EXTRA_CLANG_ARGS="${BINDGEN_EXTRA_CLANG_ARGS:-} -I/usr/include"
|
||||||
|
cargo zigbuild --release --target "${arch}.2.17"
|
||||||
|
|
||||||
|
# Assemble the AppDir: AppRun IS the goblin binary (Tor/arti linked in), plus the
|
||||||
|
# icon + desktop entry. Nothing else.
|
||||||
|
appdir="linux/Goblin.AppDir"
|
||||||
|
cp "target/${arch}/release/goblin" "${appdir}/AppRun"
|
||||||
|
chmod +x "${appdir}/AppRun"
|
||||||
|
|
||||||
|
out="target/${arch}/release/Goblin-${platform}.AppImage"
|
||||||
|
rm -f "target/${arch}/release/"*.AppImage
|
||||||
|
# Use the DEV appimagetool + type2 runtime when fetched, else the system tool.
|
||||||
|
appimagetool_bin="${GOBLIN_APPIMAGETOOL:-appimagetool}"
|
||||||
|
# The type2 runtime must match the target arch. env.sh sets GOBLIN_APPIMAGE_RUNTIME
|
||||||
|
# to the x86_64 runtime; for a non-x86_64 target use the sibling runtime-<arch>.
|
||||||
|
runtime_file="${GOBLIN_APPIMAGE_RUNTIME:-}"
|
||||||
|
if [ "${appimage_arch}" != "x86_64" ] && [ -n "${runtime_file}" ]; then
|
||||||
|
runtime_file="$(dirname "${runtime_file}")/runtime-${appimage_arch}"
|
||||||
|
fi
|
||||||
|
runtime_arg=()
|
||||||
|
[ -n "${runtime_file}" ] && [ -e "${runtime_file}" ] && runtime_arg=(--runtime-file "${runtime_file}")
|
||||||
|
ARCH="${appimage_arch}" "${appimagetool_bin}" "${runtime_arg[@]}" "${appdir}" "${out}"
|
||||||
|
echo "built: ${out}"
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ wallets:
|
|||||||
await_fin_amount: Warten auf die Fertigstellung
|
await_fin_amount: Warten auf die Fertigstellung
|
||||||
locked_amount: Gesperrt
|
locked_amount: Gesperrt
|
||||||
txs_empty: 'Um Geld manuell oder per Transport zu empfangen oder zu senden, verwenden Sie die Schaltflächen %{message} oder %{transport} unten auf dem Bildschirm. Um die Wallet-Einstellungen zu ändern, drücken Sie %{settings}.'
|
txs_empty: 'Um Geld manuell oder per Transport zu empfangen oder zu senden, verwenden Sie die Schaltflächen %{message} oder %{transport} unten auf dem Bildschirm. Um die Wallet-Einstellungen zu ändern, drücken Sie %{settings}.'
|
||||||
title: Wallets
|
title: Goblin
|
||||||
create_desc: Erstellen oder importieren Sie ein bestehendes Wallet mit dem Seed-Phrase.
|
create_desc: Erstellen oder importieren Sie ein bestehendes Wallet mit dem Seed-Phrase.
|
||||||
add: Wallet hinzufügen
|
add: Wallet hinzufügen
|
||||||
name: 'Name:'
|
name: 'Name:'
|
||||||
@@ -95,6 +95,7 @@ wallets:
|
|||||||
tx_finalizing: Finalisierung
|
tx_finalizing: Finalisierung
|
||||||
tx_posting: Buchungsvorgang
|
tx_posting: Buchungsvorgang
|
||||||
tx_confirmed: Bestätigt
|
tx_confirmed: Bestätigt
|
||||||
|
tx_stale: Zu lange ausstehend
|
||||||
txs: Transaktionen
|
txs: Transaktionen
|
||||||
tx: Transaktion
|
tx: Transaktion
|
||||||
messages: Nachrichten
|
messages: Nachrichten
|
||||||
@@ -355,4 +356,616 @@ keyboard:
|
|||||||
m: m
|
m: m
|
||||||
m1: ','
|
m1: ','
|
||||||
m2: .
|
m2: .
|
||||||
m3: '/'
|
m3: '/'
|
||||||
|
goblin:
|
||||||
|
home:
|
||||||
|
anonymous: "Anonym"
|
||||||
|
connected_tor: "Über Tor verbunden"
|
||||||
|
tor_ready: "Tor bereit · Relays…"
|
||||||
|
connecting_tor: "Verbinde mit Tor…"
|
||||||
|
connected_direct: "Verbunden (direkt)"
|
||||||
|
connecting_direct: "Verbinde (direkt)…"
|
||||||
|
cant_reach_node: "Node nicht erreichbar"
|
||||||
|
node_synced: "Node synchronisiert"
|
||||||
|
syncing: "Synchronisiere…"
|
||||||
|
balance_updating: "Guthaben wird aktualisiert…"
|
||||||
|
balance_stale: "Knoten nicht erreichbar · letzter bekannter Saldo"
|
||||||
|
fiat_unavailable: "Kurs nicht verfügbar"
|
||||||
|
listening: "Wartet auf Zahlungen"
|
||||||
|
block: "Block %{height}"
|
||||||
|
waiting_for_chain: "Warte auf Chain…"
|
||||||
|
nav_wallet: "Wallet"
|
||||||
|
nav_pay: "Zahlen"
|
||||||
|
nav_activity: "Aktivität"
|
||||||
|
nav_receive: "Empfangen"
|
||||||
|
nav_settings: "Einstellungen"
|
||||||
|
back_again: "Erneut Zurück drücken, um die Wallet zu wechseln"
|
||||||
|
activity: "Aktivität"
|
||||||
|
news: "Neuigkeiten"
|
||||||
|
empty_title: "Noch keine Aktivität"
|
||||||
|
empty_sub: "Sende oder empfange grin, um zu starten."
|
||||||
|
recent: "Zuletzt"
|
||||||
|
scan_to_pay: "Zum Zahlen scannen"
|
||||||
|
type_amount: "Betrag eingeben"
|
||||||
|
request: "Anfordern"
|
||||||
|
pay: "Zahlen"
|
||||||
|
enter_amount: "Betrag zum Zahlen oder Anfordern eingeben"
|
||||||
|
activity:
|
||||||
|
canceled: "abgebrochen"
|
||||||
|
pending: "ausstehend"
|
||||||
|
earlier: "Früher"
|
||||||
|
today: "Heute"
|
||||||
|
yesterday: "Gestern"
|
||||||
|
title: "Aktivität"
|
||||||
|
requests: "Anfragen"
|
||||||
|
empty_title: "Noch keine Aktivität"
|
||||||
|
empty_sub: "Deine Zahlungen erscheinen hier."
|
||||||
|
pending_header: "Ausstehend"
|
||||||
|
receipt:
|
||||||
|
title: "Beleg"
|
||||||
|
not_found: "Transaktion nicht gefunden"
|
||||||
|
for_note: "Für %{note}"
|
||||||
|
details: "Transaktionsdetails"
|
||||||
|
canceled: "Abgebrochen"
|
||||||
|
expired: "Abgelaufen"
|
||||||
|
funds_returned: "Guthaben zurückerstattet"
|
||||||
|
complete: "Abgeschlossen"
|
||||||
|
payment_received: "Zahlung empfangen"
|
||||||
|
payment_sent: "Zahlung erfolgreich gesendet"
|
||||||
|
pending: "Ausstehend"
|
||||||
|
confs: "%{c}/%{r} Bestätigungen"
|
||||||
|
waiting_to_confirm: "Warte auf Bestätigung"
|
||||||
|
paying: "Zahlung läuft…"
|
||||||
|
you: "Du"
|
||||||
|
to: "An"
|
||||||
|
from: "Von"
|
||||||
|
nostr: "nostr"
|
||||||
|
identity: "Identität"
|
||||||
|
fee_none: "Keine"
|
||||||
|
network_fee: "Netzwerkgebühr"
|
||||||
|
privacy: "Privatsphäre"
|
||||||
|
privacy_value: "Mimblewimble + Tor"
|
||||||
|
transaction: "Transaktion"
|
||||||
|
cancel_request: "Anfrage abbrechen"
|
||||||
|
cancel_send: "Zahlung abbrechen"
|
||||||
|
stale_note: "Diese Zahlung ist seit einer Weile ausstehend."
|
||||||
|
cancel_send_confirm: "Zum Abbrechen erneut tippen — sie könnten sie noch erhalten"
|
||||||
|
cancel_send_done: "Zahlung abgebrochen — dein Guthaben ist wieder verfügbar"
|
||||||
|
cancel_send_too_late: "Diese Zahlung ist bereits durchgegangen und kann nicht abgebrochen werden"
|
||||||
|
waiting_to_receive: "Warte, bis %{name} empfängt…"
|
||||||
|
request:
|
||||||
|
title: "%{name} fordert an"
|
||||||
|
approve: "Annehmen"
|
||||||
|
decline: "Ablehnen"
|
||||||
|
review_title: "Anfrage prüfen"
|
||||||
|
hold_to_accept: "Zum Annehmen halten"
|
||||||
|
hold_accept_hint: "Halte gedrückt, um diese Anfrage zu bezahlen"
|
||||||
|
receive:
|
||||||
|
title: "Empfangen"
|
||||||
|
requesting: "Fordere %{amt}%{tsu} an — teilen, um bezahlt zu werden"
|
||||||
|
clear_request: "Anfrage löschen"
|
||||||
|
share_handle: "Teile deinen Handle, um bezahlt zu werden"
|
||||||
|
share_npub: "Teile deinen npub, um bezahlt zu werden"
|
||||||
|
copied: "Kopiert"
|
||||||
|
copy_nostr_id: "nostr-ID kopieren"
|
||||||
|
copy_address: "Adresse kopieren"
|
||||||
|
copy: "Kopieren"
|
||||||
|
share_message: "Bezahl mich auf Goblin (goblin.st) — %{npub}"
|
||||||
|
privacy_note: "Dein Benutzername ist öffentlich. Zahlungsinhalte bleiben im Netzwerk verschlüsselt."
|
||||||
|
privacy_note_npub: "Dein npub ist öffentlich. Zahlungsinhalte bleiben im Netzwerk verschlüsselt."
|
||||||
|
profile:
|
||||||
|
title: "Profil"
|
||||||
|
activity: "Aktivität"
|
||||||
|
no_activity: "Noch keine Aktivität mit ihnen."
|
||||||
|
unblock: "Entsperren"
|
||||||
|
block: "Sperren"
|
||||||
|
blocked_blurb: "Gesperrt — ihre Zahlungen und Anfragen werden verworfen."
|
||||||
|
block_blurb: "Sperren verwirft eingehende Zahlungen und Anfragen von ihnen."
|
||||||
|
settings:
|
||||||
|
title: "Einstellungen"
|
||||||
|
connected_nostr: "Mit nostr verbunden"
|
||||||
|
connecting_relays: "Verbinde mit Relays…"
|
||||||
|
identity: "Identität"
|
||||||
|
copy_npub: "npub kopieren (öffentlich)"
|
||||||
|
backup_note: "Gerät wechseln? Sichere BEIDES: deine Seed-Phrase (Guthaben) und deine Identitäts-.backup-Datei (Name + Schlüssel)."
|
||||||
|
wallet: "Wallet"
|
||||||
|
display_unit: "Anzeigeeinheit"
|
||||||
|
relays: "Relays"
|
||||||
|
nostr_relays: "Nostr-Relays"
|
||||||
|
node: "Node"
|
||||||
|
min_conf: "Mindestbestätigungen"
|
||||||
|
integrated_node: "Einstellungen des integrierten Nodes"
|
||||||
|
node_advanced: "Erweitert"
|
||||||
|
slatepacks: "Slatepacks"
|
||||||
|
slatepacks_value: "Manuelle Transaktion"
|
||||||
|
trusted_sites: "Vertrauenswürdige Seiten"
|
||||||
|
lock_wallet: "Wallet sperren"
|
||||||
|
switch_wallet: "Wallet wechseln"
|
||||||
|
advanced: "Erweitert"
|
||||||
|
privacy: "Privatsphäre"
|
||||||
|
tor_routing: "Tor-Routing"
|
||||||
|
messages_lookups: "Nachrichten & Abfragen"
|
||||||
|
tor_on: "Ein"
|
||||||
|
tor_off: "Aus"
|
||||||
|
auto_accept: "Automatisch annehmen"
|
||||||
|
pairing: "Preiswährung"
|
||||||
|
accept_anyone: "Jeder"
|
||||||
|
accept_contacts: "Nur Kontakte"
|
||||||
|
accept_ask: "Immer fragen"
|
||||||
|
requests: "Anfragen"
|
||||||
|
incoming_requests: "Eingehende Anfragen"
|
||||||
|
incoming_requests_sub: "Erlaube anderen, Geld von dir anzufordern"
|
||||||
|
hide_amounts: "Beträge verbergen"
|
||||||
|
hide_amounts_sub: "Empfangene Beträge in Benachrichtigungen verbergen"
|
||||||
|
language: "Sprache"
|
||||||
|
update_available: "Update verfügbar"
|
||||||
|
appearance: "Erscheinungsbild"
|
||||||
|
theme: "Design"
|
||||||
|
theme_light: "Hell"
|
||||||
|
theme_dark: "Dunkel"
|
||||||
|
theme_yellow: "Gelb"
|
||||||
|
archive: "Archiv"
|
||||||
|
export_archive: "Archiv exportieren"
|
||||||
|
export_archive_caption: "Kopiert deine Kontakte, deinen Zahlungsverlauf und Anfragen als JSON in die Zwischenablage."
|
||||||
|
wipe_history: "Zahlungsverlauf löschen"
|
||||||
|
wipe_history_confirm: "Zum Löschen erneut tippen — kann nicht rückgängig gemacht werden"
|
||||||
|
about: "Über"
|
||||||
|
goblin: "Goblin"
|
||||||
|
build: "Build %{build}"
|
||||||
|
network: "Netzwerk"
|
||||||
|
network_value: "MW + Tor + nostr"
|
||||||
|
third_party: "Drittanbieter"
|
||||||
|
grim: "GRIM (Upstream-Wallet)"
|
||||||
|
grin_node: "Grin-Node"
|
||||||
|
sp_intro: "Erweitert — rohe slatepacks von Hand austauschen, so wie GRIM es macht. Nur nutzen, wenn du nicht über einen username zahlen oder bezahlt werden kannst."
|
||||||
|
sp_receive_group: "Empfangen oder abschließen"
|
||||||
|
sp_receive_blurb: "Füge einen slatepack ein, den dir jemand gegeben hat. Goblin empfängt die Zahlung, begleicht die Rechnung oder schließt sie ab und sendet sie."
|
||||||
|
sp_process: "Slatepack verarbeiten"
|
||||||
|
sp_paste_first: "Füge zuerst einen slatepack ein."
|
||||||
|
sp_reply_ready: "Antwort bereit — sende sie an den Absender zurück."
|
||||||
|
sp_finalizing: "Schließe ab und sende an die Chain…"
|
||||||
|
sp_create_group: "Zahlung erstellen"
|
||||||
|
sp_create_blurb: "Erstelle einen slatepack zum Übergeben. Der Empfänger nimmt ihn an, sendet die Antwort zurück, und du schließt sie oben ab."
|
||||||
|
sp_amount_hint: "Betrag in grin"
|
||||||
|
sp_addr_hint: "Empfängeradresse (optional)"
|
||||||
|
sp_create: "Slatepack erstellen"
|
||||||
|
sp_ready: "Slatepack bereit — übergib ihn dem Empfänger."
|
||||||
|
sp_amount_gt_zero: "Gib einen Betrag größer als null ein."
|
||||||
|
sp_to_send: "Zu sendender slatepack"
|
||||||
|
sp_copy: "Slatepack kopieren"
|
||||||
|
cancel: "Abbrechen"
|
||||||
|
continue: "Weiter"
|
||||||
|
final_confirmation: "Endgültige Bestätigung"
|
||||||
|
type_reset: "RESET tippen"
|
||||||
|
wallet_password: "Wallet-Passwort"
|
||||||
|
done: "Fertig"
|
||||||
|
close: "Schließen"
|
||||||
|
import_nsec_hint: "nsec1… oder eingefügte Sicherung"
|
||||||
|
backup_password_hint: "Backup-Passwort (nur wenn anderswo exportiert)"
|
||||||
|
import_btn: "Importieren"
|
||||||
|
importing: "Importiere…"
|
||||||
|
import_failed: "Import fehlgeschlagen"
|
||||||
|
name_authority: "Namensautorität"
|
||||||
|
name_authority_title: "Namensautorität ändern"
|
||||||
|
name_authority_blurb: "Der Server, der Namen registriert und verifiziert. Auf eine andere Instanz zeigen, um dort gehostete Namen zu nutzen und zu bezahlen."
|
||||||
|
name_authority_invalid: "Vollständige URL eingeben (https://…)."
|
||||||
|
reset: "Zurücksetzen"
|
||||||
|
save: "Speichern"
|
||||||
|
backup_file: "In Datei sichern"
|
||||||
|
choose_backup_file: "Eine .backup-Datei wählen"
|
||||||
|
backup_read_failed: "Datei konnte nicht gelesen werden."
|
||||||
|
backup_saved: "Sicherung gespeichert"
|
||||||
|
backup_saved_sub: "Bewahre die .backup-Datei sicher auf. Wer sie UND dein Passwort hat, kann dein gesamtes Wallet wiederherstellen."
|
||||||
|
backup_file_title: "Wallet sichern"
|
||||||
|
backup_file_blurb: "Erstellt eine verschlüsselte .backup-Datei mit deinem Wallet-Seed und allen Identitäten. Gib dein Wallet-Passwort ein, um sie zu versiegeln."
|
||||||
|
backup_write_failed: "Datei konnte nicht gespeichert werden."
|
||||||
|
create_backup: "Sicherung erstellen"
|
||||||
|
registered: "%{name} registriert"
|
||||||
|
released_msg: "Freigegeben — der Name ist frei"
|
||||||
|
release_confirm: "%{name} freigeben?"
|
||||||
|
release_blurb: "Sobald er frei ist, ist er verfügbar — jeder kann ihn beanspruchen, auch dein nächster rotierter Schlüssel. Du kannst 10 Minuten lang keinen anderen Benutzernamen registrieren."
|
||||||
|
releasing: "Gebe frei…"
|
||||||
|
keep_it: "Behalten"
|
||||||
|
release_it: "Freigeben"
|
||||||
|
username: "Benutzername"
|
||||||
|
username_note: "Wird als dein Name angezeigt. Öffentlich auf goblin.st. Zahlungen bleiben verschlüsselt."
|
||||||
|
release_username: "Benutzername freigeben"
|
||||||
|
pick_username: "Benutzernamen wählen — optional"
|
||||||
|
working: "Arbeite…"
|
||||||
|
claim: "Sichern"
|
||||||
|
err_just_taken: "Dieser Benutzername wurde gerade vergeben"
|
||||||
|
err_cooldown: "Du hast kürzlich einen Benutzernamen freigegeben — du kannst innerhalb von 10 Minuten einen neuen registrieren."
|
||||||
|
err_unreachable: "goblin.st nicht erreichbar — Verbindungsproblem. Versuche es erneut."
|
||||||
|
err_release: "Freigabe fehlgeschlagen: %{err}"
|
||||||
|
avail_available: "Verfügbar!"
|
||||||
|
avail_taken: "Vergeben"
|
||||||
|
avail_reserved: "Reserviert"
|
||||||
|
avail_invalid: "Namen haben 3–20 Zeichen: a–z, 0–9, _ oder -"
|
||||||
|
avail_quarantined: "Nicht verfügbar"
|
||||||
|
avail_unknown: "Prüfung fehlgeschlagen — Verbindungsproblem. Versuche es erneut."
|
||||||
|
username_none: "Nicht festgelegt"
|
||||||
|
advanced_privacy: "Erweiterte Privatsphäre"
|
||||||
|
tap_reveal: "Zum Anzeigen tippen"
|
||||||
|
notif_someone: "Jemand"
|
||||||
|
notif_private_received: "Du wurdest bezahlt. Öffne Goblin, um Details zu sehen."
|
||||||
|
notif_private_requested: "Neue Zahlungsanfrage. Öffne Goblin, um Details zu sehen."
|
||||||
|
username:
|
||||||
|
title: "Benutzername"
|
||||||
|
authority: "Namensautorität"
|
||||||
|
authority_blurb: "Als Standard belassen oder auf eine andere Instanz verweisen, um dort gehostete Namen zu nutzen."
|
||||||
|
learn_more: "Mehr erfahren"
|
||||||
|
custom: "Eigene Autorität"
|
||||||
|
advprivacy:
|
||||||
|
intro: "Steuere, was deine Benachrichtigungen preisgeben, und verbirg dein Guthaben und deinen Verlauf auf diesem Gerät."
|
||||||
|
notifications: "Benachrichtigungen"
|
||||||
|
hide_names: "Namen verbergen"
|
||||||
|
hide_names_sub: "Wer bezahlt hat, wird in Benachrichtigungen weggelassen"
|
||||||
|
hide_details: "Alle Details verbergen"
|
||||||
|
hide_details_sub: "Nur ein privater Hinweis, ohne Name oder Betrag"
|
||||||
|
anon: "Anonymer Modus"
|
||||||
|
anon_toggle: "Guthaben und Aktivität verwischen"
|
||||||
|
anon_sub: "Guthaben und Aktivität erscheinen als Punkte, bis du zum Anzeigen tippst"
|
||||||
|
advanced:
|
||||||
|
title: "Erweitert"
|
||||||
|
intro: "Wallet-Werkzeuge auf niedriger Ebene von GRIM. Normalerweise brauchst du diese nicht."
|
||||||
|
own_node_desc: "Synchronisiere einen vollständigen Grin-Node auf diesem Gerät, statt einem öffentlichen zu vertrauen."
|
||||||
|
own_node_active: "Eigener Node läuft"
|
||||||
|
repair: "Wallet reparieren"
|
||||||
|
repair_desc: "Die Kette neu scannen und fehlende Outputs wiederherstellen. Das kann dauern."
|
||||||
|
repair_unavailable: "Benötigt zuerst eine synchronisierte Node-Verbindung."
|
||||||
|
repairing: "Repariere… %{pct}%"
|
||||||
|
restore: "Wallet wiederherstellen"
|
||||||
|
restore_desc: "Lokale Daten löschen und aus deinem Seed neu aufbauen. Nutze das, wenn eine Reparatur nicht half — danach öffnest du die Wallet erneut."
|
||||||
|
restore_confirm: "Zum Wiederherstellen erneut tippen"
|
||||||
|
show_phrase: "Wiederherstellungsphrase"
|
||||||
|
phrase_desc: "Deine 24 grin-Seed-Wörter — der einzige Weg, Guthaben wiederherzustellen. Halte sie offline und privat."
|
||||||
|
reveal: "Phrase anzeigen"
|
||||||
|
hide: "Verbergen"
|
||||||
|
password: "Wallet-Passwort"
|
||||||
|
wrong_password: "Falsches Passwort."
|
||||||
|
delete: "Wallet löschen"
|
||||||
|
delete_desc: "Diese Wallet und jede ihrer Identitäten dauerhaft von diesem Gerät entfernen. Sichere zuerst ein Backup. Ohne deinen Seed sind Guthaben nicht wiederherstellbar."
|
||||||
|
delete_confirm: "Zum Löschen erneut tippen"
|
||||||
|
manage_node: "Node-Verbindung verwalten"
|
||||||
|
repair_confirm: "Ja, jetzt reparieren"
|
||||||
|
repair_confirm_note: "Die Reparatur scannt die Chain neu und kann einige Minuten dauern."
|
||||||
|
restore_confirm_note: "Dies löscht lokale Daten und baut sie aus deinem Seed neu auf — das kann einige Minuten dauern."
|
||||||
|
nostr_key: "Nostr-Schlüssel"
|
||||||
|
nostr_key_desc: "Dein nsec, der geheime Schlüssel deiner Nostr-Identität. Kopiere ihn oder zeige den QR-Code, um dich bei Nostr-Apps wie magick.market anzumelden. Wer ihn hat, kontrolliert deine Identität, also halte ihn geheim."
|
||||||
|
reveal_nsec: "Schlüssel anzeigen"
|
||||||
|
copy_nsec: "nsec kopieren"
|
||||||
|
show_qr: "QR anzeigen"
|
||||||
|
hide_qr: "QR ausblenden"
|
||||||
|
nostr_section: "Erweiterte Nostr-Einstellungen"
|
||||||
|
backup_caption: "Enthält dein Wallet und alle Identitäten."
|
||||||
|
danger_zone: "Gefahrenzone"
|
||||||
|
delete_warning: "Dies löscht deine Wiederherstellungsphrase und jede Identität auf diesem Gerät. Alles, was du nicht gesichert hast, ist unwiederbringlich verloren."
|
||||||
|
download_backup: "Backup herunterladen"
|
||||||
|
delete_final: "Endgültig löschen"
|
||||||
|
privacy:
|
||||||
|
title: "Netzwerk-Privatsphäre"
|
||||||
|
intro: "Goblin sendet nur wenige Dinge über das Netzwerk, jedes Ende-zu-Ende-verschlüsselt versiegelt, sodass Relays sie weder lesen noch mit dir verknüpfen können."
|
||||||
|
payments: "Zahlungen"
|
||||||
|
payments_blurb: "Jede nostr-Nachricht, die einen slatepack trägt."
|
||||||
|
usernames: "Benutzernamen"
|
||||||
|
usernames_blurb: "NIP-05-Namensabfragen zu und von Namensautoritäten."
|
||||||
|
price_avatars: "Preis"
|
||||||
|
price_avatars_blurb: "Der Live-Wechselkurs neben den Beträgen."
|
||||||
|
grin_node: "Grin-Node"
|
||||||
|
grin_node_blurb: "Synchronisiert Blöcke und überträgt deine Transaktionen. Öffentliche Chain-Daten, für alle gleich, nie mit deiner Identität verknüpft."
|
||||||
|
how_privacy: "So funktioniert deine Privatsphäre"
|
||||||
|
how_privacy_blurb: "Zahlungen und Nachrichten werden mit authentifizierter Verschlüsselung versiegelt und als private Nachrichten zugestellt, sodass ein Relay sie weder lesen noch eine Zahlung zu dir zurückverfolgen kann."
|
||||||
|
always_direct: "Immer direkt"
|
||||||
|
tor_off: "Tor aus"
|
||||||
|
tor_on: "Tor an"
|
||||||
|
tor_switch: "Über Tor leiten"
|
||||||
|
tor_switch_sub: "Verbirg deine IP vor Relays."
|
||||||
|
vpn_note: "Wenn Tor aus ist, können Relays deine IP-Adresse sehen. Wir empfehlen, zumindest ein VPN zu verwenden."
|
||||||
|
pairing:
|
||||||
|
title: "Kopplung"
|
||||||
|
intro: "Womit dein Guthaben und deine Beträge verglichen werden."
|
||||||
|
pair_with: "Koppeln mit"
|
||||||
|
rates_note: "Kurse werden über Tor abgerufen, nur solange eine Kopplung aktiv ist — aus bedeutet, dass keine Kursanfrage dein Gerät verlässt."
|
||||||
|
relays:
|
||||||
|
title: "Relays"
|
||||||
|
intro: "Zahlungsnachrichten werden an jedes Relay unten gespiegelt; ein erreichbares Relay genügt zum Empfangen."
|
||||||
|
your_relays: "Deine Relays"
|
||||||
|
add_relay: "Relay hinzufügen"
|
||||||
|
add_relay_btn: "Relay hinzufügen"
|
||||||
|
save_reconnect: "Speichern & neu verbinden"
|
||||||
|
none: "keine"
|
||||||
|
count: "%{n} Relays"
|
||||||
|
node:
|
||||||
|
title: "Node"
|
||||||
|
connection: "Verbindung"
|
||||||
|
integrated: "Integrierter Node"
|
||||||
|
applies_after: "Wird wirksam, nachdem das Wallet gesperrt und wieder entsperrt wurde."
|
||||||
|
add_external: "Externen Node hinzufügen"
|
||||||
|
api_secret_hint: "API-Secret (optional)"
|
||||||
|
add_node: "Node hinzufügen"
|
||||||
|
integrated_host: "integrierter Node"
|
||||||
|
summary_syncing: "%{conn} · synchronisiere"
|
||||||
|
summary_block: "Block %{height} · %{conn}"
|
||||||
|
nips:
|
||||||
|
title: "nostr & NIPs"
|
||||||
|
intro1: "Goblin spricht nostr — ein offenes Protokoll signierter Nachrichten, die über einfache Relay-Server weitergereicht werden. Dein Wallet trägt seine eigene nostr-Identität: einen eigenständigen Zufallsschlüssel, bewusst unabhängig von deinem Guthaben und Seed gehalten. Jede Zahlung reist als Ende-zu-Ende-verschlüsselte Direktnachricht zwischen Identitäten, mit dem slatepack im Inneren."
|
||||||
|
intro2: "goblin.st ist Goblins Namensdienst: Das Sichern eines Benutzernamens veröffentlicht dort eine Name → Schlüssel-Zuordnung (NIP-05), sodass Leute you statt eines langen npub bezahlen können. Der Benutzername ist öffentlich; Zahlungsinhalte sind es nie. NIPs sind die Bausteine des Protokolls — tippe auf einen, um die Spezifikation zu lesen."
|
||||||
|
n05_title: "Namen"
|
||||||
|
n05_blurb: "Ordnet username@goblin.st deinem Schlüssel zu, sodass Handles wie Adressen funktionieren."
|
||||||
|
n17_title: "Private Nachrichten"
|
||||||
|
n17_blurb: "Die verschlüsselte DM-Hülle, in der jede Zahlung reist."
|
||||||
|
n44_title: "Verschlüsselung"
|
||||||
|
n44_blurb: "Die authentifizierte Chiffre, die in diesen Nachrichten verwendet wird."
|
||||||
|
n49_title: "Schlüsselverschlüsselung"
|
||||||
|
n49_blurb: "Wie der geheime Schlüssel im Ruhezustand gespeichert wird, gesperrt durch dein Passwort."
|
||||||
|
n59_title: "Gift Wrap"
|
||||||
|
n59_blurb: "Verpackt Nachrichten, sodass Relays nicht sehen können, wer mit wem kommuniziert."
|
||||||
|
n98_title: "HTTP-Auth"
|
||||||
|
n98_blurb: "Signiert die Benutzernamen-Registrierungsanfrage an goblin.st."
|
||||||
|
batch:
|
||||||
|
title: "%{n} Rechnungen bestätigen"
|
||||||
|
blurb: "Stelle %{n} Zahlungsanforderungen über je %{amount} an %{name} — insgesamt %{total}. Jede Anforderung erhält ihre eigene neue Zahlungsadresse."
|
||||||
|
approve: "Bestätigen"
|
||||||
|
login:
|
||||||
|
title: "Anmelden"
|
||||||
|
headline: "Bei %{domain} anmelden?"
|
||||||
|
identity: "Anmelden als"
|
||||||
|
explain: "Dies signiert eine einmalige Anmeldeanfrage. Dein Schlüssel wird nicht geteilt, und die Seite kann nicht in deinem Namen handeln."
|
||||||
|
pass_prompt: "Gib dein Wallet-Passwort ein, um dich anzumelden."
|
||||||
|
confirm: "Anmelden"
|
||||||
|
sent: "Bei %{domain} angemeldet"
|
||||||
|
failed: "%{domain} war nicht erreichbar. Die Anmeldung wurde nicht zugestellt."
|
||||||
|
authorize:
|
||||||
|
title: "Autorisieren"
|
||||||
|
headline: "%{domain} autorisieren?"
|
||||||
|
kind_post: "Öffentlicher Beitrag"
|
||||||
|
kind_repost: "Teilen"
|
||||||
|
kind_reaction: "Reaktion"
|
||||||
|
kind_article: "Artikel (Langform)"
|
||||||
|
kind_unknown: "Art %{n} (unbekannt)"
|
||||||
|
unknown_caution: "Goblin kennt diesen Ereignistyp nicht. Nur bestätigen, wenn du %{domain} vertraust."
|
||||||
|
identity: "Signieren als"
|
||||||
|
explain: "Beim Bestätigen wird dieses eine Ereignis signiert und an %{domain} übergeben. Goblin veröffentlicht nichts, teilt keinen Schlüssel, und die Seite erhält keine Sitzung und keine künftige Berechtigung."
|
||||||
|
pass_prompt: "Gib dein Wallet-Passwort ein, um zu autorisieren."
|
||||||
|
confirm: "Autorisieren"
|
||||||
|
sent: "%{domain} autorisiert"
|
||||||
|
failed: "%{domain} war nicht erreichbar. Es wurde nichts zugestellt."
|
||||||
|
truncated: "gekürzt, %{n} weitere Zeichen"
|
||||||
|
show_full: "Vollständig anzeigen"
|
||||||
|
show_less: "Weniger anzeigen"
|
||||||
|
replying_to: "Antwort auf %{id}"
|
||||||
|
mentions: "erwähnt %{id}"
|
||||||
|
repost_of: "teilt %{id}"
|
||||||
|
by_author: "von %{id}"
|
||||||
|
reacts_to: "reagiert auf %{id}"
|
||||||
|
article_title: "Titel: %{title}"
|
||||||
|
trust:
|
||||||
|
title: "Vertrauen"
|
||||||
|
headline: "%{domain} vertrauen?"
|
||||||
|
identity: "Anmeldung als"
|
||||||
|
lead: "%{domain} kann in dieser Sitzung für dich handeln, ohne jedes Mal zu fragen. Alles, was Geld ausgibt, fragt immer nach."
|
||||||
|
grant_intro: "Darf ohne Nachfrage signieren:"
|
||||||
|
cat_social: "Beiträge und Reaktionen"
|
||||||
|
cat_dm: "Direktnachrichten"
|
||||||
|
cat_market: "Angebote"
|
||||||
|
cat_identity: "Profil und Listen"
|
||||||
|
cat_delete: "Löschungen"
|
||||||
|
cat_http: "Uploads und HTTP-Authentifizierung"
|
||||||
|
cat_unknown: "Anderer Ereignistyp (Art %{n})"
|
||||||
|
login_excluded: "Anmeldung wird einer Seite nie gewährt."
|
||||||
|
money_line: "Geld fragt immer nach deinem Passwort. Nichts wird still ausgegeben oder verkauft."
|
||||||
|
duration: "Gilt nur für diese Sitzung. Jederzeit beendbar unter Einstellungen, Vertrauenswürdige Seiten."
|
||||||
|
pass_prompt: "Gib dein Wallet-Passwort ein, um dieser Seite zu vertrauen."
|
||||||
|
confirm_hold: "Halten, um für diese Sitzung zu vertrauen"
|
||||||
|
sent: "%{domain} wird für diese Sitzung vertraut"
|
||||||
|
failed: "%{domain} konnte nicht erreicht werden. Die Sitzung wurde nicht gestartet."
|
||||||
|
announce_failed: "Vertraut, aber %{domain} hat die Sitzung noch nicht bestätigt."
|
||||||
|
notice_volume: "Eine vertrauenswürdige Seite signiert sehr viel."
|
||||||
|
notice_decrypt: "Eine vertrauenswürdige Seite liest deine Nachrichten."
|
||||||
|
money:
|
||||||
|
title: "Bestätigen"
|
||||||
|
headline: "In %{domain} bestätigen?"
|
||||||
|
identity: "Signiert als %{id}"
|
||||||
|
explain: "Dies bewegt oder bindet Werte, daher wird immer gefragt. Mit dem Genehmigen wird diese eine Aktion signiert."
|
||||||
|
pass_prompt: "Gib dein Wallet-Passwort zum Bestätigen ein."
|
||||||
|
confirm_hold: "Halten zum Bestätigen"
|
||||||
|
encrypt_desc: "Eine verschlüsselte Bestellnachricht, die eine Zahlung zusichert."
|
||||||
|
trusted_sites:
|
||||||
|
title: "Vertrauenswürdige Seiten"
|
||||||
|
intro: "Seiten, denen du in dieser Sitzung vertraut hast, können risikoarme Aktionen ohne Nachfrage signieren. Bei Geld wird immer gefragt. Beende jede Sitzung hier."
|
||||||
|
empty: "Keine vertrauenswürdigen Seiten. Vertraue einer Seite bei der Anmeldung, um ihr eine Signatur-Sitzung zu geben."
|
||||||
|
none: "nichts"
|
||||||
|
can_sign: "Kann still signieren: %{cats}"
|
||||||
|
time_left: "Endet in %{mins} Min."
|
||||||
|
paused: "Pausiert, signiert sehr viel"
|
||||||
|
resume: "Fortsetzen"
|
||||||
|
end: "Sitzung beenden"
|
||||||
|
identities:
|
||||||
|
title: "Identitäten"
|
||||||
|
switch_hint: "Identität wechseln"
|
||||||
|
blurb: "Deine Identitäten sind die Namen, unter denen du bezahlt wirst. Alle zahlen in diese eine Wallet. Füge eine neue hinzu, wechsle zwischen ihnen oder entferne eine, die du nicht mehr brauchst."
|
||||||
|
privacy_note: "Diese Identitäten teilen sich eine Wallet und ein Guthaben — getrennte Eingänge, keine getrennten Gelder. Für wirklich getrenntes Geld nutze eine separate Wallet."
|
||||||
|
held: "Vorhandene Identitäten"
|
||||||
|
add: "Identität hinzufügen"
|
||||||
|
add_title: "Eine Identität hinzufügen"
|
||||||
|
nsec_hint: "Nsec einfügen"
|
||||||
|
generate_note: "Ein neuer, anonymer Schlüssel wird für diese Wallet erstellt."
|
||||||
|
generate: "Erstellen"
|
||||||
|
import: "Importieren"
|
||||||
|
choose_backup: "Eine .backup-Datei wählen"
|
||||||
|
backup_selected: "Backup-Datei ausgewählt"
|
||||||
|
delete_short: "Identität löschen"
|
||||||
|
delete_title: "%{name} löschen?"
|
||||||
|
delete_blurb: "Dies entfernt die Identität dauerhaft aus dieser Wallet. Bereits erhaltenes Geld bleibt in deinem Guthaben."
|
||||||
|
delete_backup_note: "Sichere sie zuerst — ohne ihren nsec oder ihre .backup-Datei ist sie nicht wiederherstellbar."
|
||||||
|
delete_confirm: "Löschen"
|
||||||
|
manage_title: "Identität verwalten"
|
||||||
|
tag_note: "Gib ihr einen privaten Namen. Nur du siehst ihn — er wird nie geteilt."
|
||||||
|
tag_hint: "Privater Name"
|
||||||
|
tag_save: "Speichern"
|
||||||
|
pass_prompt: "Dein Wallet-Passwort verschlüsselt und entsperrt jede Identität. Gib es ein, um fortzufahren."
|
||||||
|
working: "Arbeite…"
|
||||||
|
onboarding:
|
||||||
|
privacy:
|
||||||
|
kicker: "NETZWERK-PRIVATSPHÄRE"
|
||||||
|
continue: "Weiter"
|
||||||
|
intro:
|
||||||
|
private_money_head: "Privates Geld"
|
||||||
|
private_money_body: "Goblin ist ein Wallet für grin — digitales Bargeld ohne Beträge oder Adressen auf seiner Chain."
|
||||||
|
send_like_message_head: "Senden wie eine Nachricht"
|
||||||
|
send_like_message_body: "Zahle an einen username oder npub und es kommt als Ende-zu-Ende-verschlüsselte Nachricht über nostr und Tor an — niemand dazwischen sieht den Betrag oder die Beteiligten."
|
||||||
|
yours_alone_head: "Nur deins"
|
||||||
|
yours_alone_body: "Schlüssel, Namen und Verlauf bleiben auf diesem Gerät. Basiert auf dem GRIM-Wallet."
|
||||||
|
get_started: "Loslegen"
|
||||||
|
footnote: "Dauert etwa eine Minute. Du kannst später alles ändern."
|
||||||
|
node:
|
||||||
|
kicker: "SCHRITT 1 VON 3 · NETZWERK"
|
||||||
|
title: "Wie soll Goblin\ndie Chain beobachten?"
|
||||||
|
own_title: "Eigenen Node betreiben"
|
||||||
|
own_badge: "Privat"
|
||||||
|
own_body: "Vertraut niemandem — dein Wallet prüft die Chain selbst. Synchronisiert im Hintergrund, während du die Einrichtung beendest."
|
||||||
|
connect_title: "Mit einem Node verbinden"
|
||||||
|
connect_badge: "Sofort"
|
||||||
|
connect_body: "Kein Warten auf Sync. Der gewählte Node kann die Abfragen deines Wallets sehen."
|
||||||
|
changeable: "Jederzeit änderbar unter Einstellungen → Node."
|
||||||
|
continue: "Weiter"
|
||||||
|
url_invalid: "Node-URL muss mit http:// oder https:// beginnen"
|
||||||
|
wallet:
|
||||||
|
kicker: "SCHRITT 2 VON 3 · WALLET"
|
||||||
|
title: "Richte dein Wallet ein"
|
||||||
|
create_new: "Neu erstellen"
|
||||||
|
restore_from_seed: "Aus Seed wiederherstellen"
|
||||||
|
import_identity: "Identität importieren"
|
||||||
|
import_identity_hint: "Nach diesem Schritt bringst du deinen vorhandenen Schlüssel mit (eine .backup-Datei oder nsec)."
|
||||||
|
name_hint: "Wallet-Name"
|
||||||
|
password_hint: "Passwort"
|
||||||
|
repeat_password_hint: "Passwort wiederholen"
|
||||||
|
restore_hint: "Halte deine Seed-Wörter bereit — du gibst sie als Nächstes ein."
|
||||||
|
create_hint: "Als Nächstes erhältst du 24 Seed-Wörter zum Aufschreiben. Sie sind das Geld — wer sie hat, hält dein Guthaben."
|
||||||
|
continue: "Weiter"
|
||||||
|
passwords_no_match: "Passwörter stimmen nicht überein"
|
||||||
|
words:
|
||||||
|
kicker: "SCHRITT 2 VON 3 · WALLET"
|
||||||
|
title_restore: "Gib deine Seed-Wörter ein"
|
||||||
|
title_create: "Schreibe diese Wörter auf"
|
||||||
|
write_down_hint: "Auf Papier, in Reihenfolge. Wer diese Wörter hat, kann dein Guthaben nehmen; ohne sie bedeutet ein verlorenes Gerät verlorenes Guthaben."
|
||||||
|
paste: "Einfügen"
|
||||||
|
scan_qr: "QR scannen"
|
||||||
|
copy_clipboard: "In Zwischenablage kopieren (vermeiden)"
|
||||||
|
restore_wallet: "Wallet wiederherstellen"
|
||||||
|
wrote_them_down: "Ich habe sie aufgeschrieben"
|
||||||
|
fill_every_word: "Fülle jedes Wort aus — tippe ein Wort an, um es zu bearbeiten, oder füge die Phrase ein."
|
||||||
|
unlock_backup: "Backup entsperren"
|
||||||
|
backup_unlocked: "Backup entsperrt. Deine Identitäten werden beim Öffnen des Wallets wiederhergestellt."
|
||||||
|
confirm:
|
||||||
|
kicker: "SCHRITT 2 VON 3 · WALLET"
|
||||||
|
title: "Jetzt beweise es"
|
||||||
|
enter_hint: "Gib die soeben aufgeschriebenen Wörter ein. Tippe ein Wort an, um es zu tippen."
|
||||||
|
paste: "Einfügen"
|
||||||
|
create_wallet: "Wallet erstellen"
|
||||||
|
keep_going: "Weiter so — jedes Wort, in Reihenfolge."
|
||||||
|
identity:
|
||||||
|
kicker: "SCHRITT 3 VON 3 · IDENTITÄT"
|
||||||
|
title: "Deine Zahlungsidentität"
|
||||||
|
key_being_made: "Schlüssel wird erstellt…"
|
||||||
|
connected_tor: "über Tor verbunden"
|
||||||
|
connecting_tor: "verbinde über Tor…"
|
||||||
|
connected_direct: "verbunden (direkt)"
|
||||||
|
connecting_direct: "verbinde (direkt)…"
|
||||||
|
fresh_key_blurb: "Ein Zahlungsschlüssel, der nicht Teil deines Seeds ist — jederzeit rotierbar, ohne deine Mittel zu berühren."
|
||||||
|
restoring: "Deine Identitäten werden wiederhergestellt…"
|
||||||
|
clean_slate_blurb: "Lust auf einen Neuanfang? Tausche jederzeit einen brandneuen Schlüssel ein — das neue Du ist nicht mit dem alten verknüpft. Gleiches Wallet, frisches Gesicht."
|
||||||
|
pick_username: "Benutzernamen wählen — optional"
|
||||||
|
username_blurb: "Freunde zahlen an deinen Namen statt an einen langen Schlüssel. Optional — jederzeit beanspruchbar."
|
||||||
|
username_field_hint: "deinname"
|
||||||
|
working: "Arbeite…"
|
||||||
|
claim_username: "Benutzernamen sichern"
|
||||||
|
available_when_connected: "Verfügbar, sobald Tor verbindet — oder überspringen und später sichern."
|
||||||
|
youre: "Du bist %{name}"
|
||||||
|
claimed_title: "%{name} gehört dir"
|
||||||
|
claimed_blurb: "Freunde können dich jetzt per Namen bezahlen. Alles bereit — öffne dein Wallet."
|
||||||
|
open_wallet: "Mein Wallet öffnen"
|
||||||
|
skip_for_now: "Vorerst überspringen"
|
||||||
|
import_existing: "Schon eine Goblin-Identität? Importieren"
|
||||||
|
import_title: "Identität importieren"
|
||||||
|
import_blurb: "Füge deinen nsec ein oder wähle eine .backup-Datei, um deinen vorhandenen Schlüssel und Benutzernamen zu behalten statt diesen neuen."
|
||||||
|
errors:
|
||||||
|
cant_open: "Wallet konnte nicht geöffnet werden: %{err}"
|
||||||
|
cant_create: "Wallet konnte nicht erstellt werden: %{err}"
|
||||||
|
send:
|
||||||
|
scan_to_request: "Zum Anfordern scannen"
|
||||||
|
scan_to_pay: "Zum Zahlen scannen"
|
||||||
|
tab_scan: "Scannen"
|
||||||
|
tab_my_code: "Mein Code"
|
||||||
|
request_from: "Anfordern von"
|
||||||
|
send_to: "Senden an"
|
||||||
|
search_hint: "handle, npub oder Name"
|
||||||
|
suggested: "%{icon} Vorgeschlagen"
|
||||||
|
no_contacts: "Noch keine Kontakte. Finde jemanden über seinen handle."
|
||||||
|
no_profile: "kein Profil"
|
||||||
|
tag_contact: "Kontakt"
|
||||||
|
tag_on_nostr: "auf nostr"
|
||||||
|
searching_nostr: "Durchsuche nostr…"
|
||||||
|
unverified_title: "Unverifizierten Schlüssel bezahlen?"
|
||||||
|
unverified_body: "Für diesen Schlüssel ist kein nostr-Profil veröffentlicht — er könnte brandneu, anonym oder vertippt sein. Prüfe genau, ob es der richtige ist, bevor du sendest."
|
||||||
|
keep_looking: "Weitersuchen"
|
||||||
|
pay_anyway: "Trotzdem zahlen"
|
||||||
|
scan_not_recipient: "Dieser QR ist kein goblin-Empfänger — erwartet wurde ein npub oder handle"
|
||||||
|
scan_prompt: "Halte einen goblin-Code ins Bild, um zu aktivieren"
|
||||||
|
scan_to_pay_me: "Scannen, um mich zu bezahlen"
|
||||||
|
share_btn: "%{icon} Teilen"
|
||||||
|
share_message: "Bezahl mich auf Goblin — %{handle}\n%{link}\nnpub: %{npub}"
|
||||||
|
none_found: "Niemand gefunden für %{label}"
|
||||||
|
enter_recipient: "Gib einen handle, npub oder Namen ein"
|
||||||
|
amount_title: "Betrag"
|
||||||
|
to_name: "An %{name}"
|
||||||
|
not_enough: "Du hast nicht genug grin"
|
||||||
|
max: "Max"
|
||||||
|
note_label: "Notiz"
|
||||||
|
note_hint: "Notiz hinzufügen…"
|
||||||
|
add_note: "Notiz hinzufügen"
|
||||||
|
edit_note: "Notiz bearbeiten"
|
||||||
|
note_cancel: "Abbrechen"
|
||||||
|
note_save: "Speichern"
|
||||||
|
review_btn: "Prüfen"
|
||||||
|
confirm_request: "Anfrage bestätigen"
|
||||||
|
review_title: "Prüfen"
|
||||||
|
requesting_from: "Fordere an von %{name}"
|
||||||
|
youre_sending: "Du sendest %{name}"
|
||||||
|
row_from: "Von"
|
||||||
|
row_to: "An"
|
||||||
|
row_note: "Notiz"
|
||||||
|
row_proof: "Zahlungsnachweis"
|
||||||
|
row_proof_val: "Enthalten"
|
||||||
|
row_proof_shared: "Nachweis geteilt mit"
|
||||||
|
row_they_pay: "Sie zahlen"
|
||||||
|
row_they_pay_val: "Nur wenn sie zustimmen"
|
||||||
|
row_delivery: "Zustellung"
|
||||||
|
row_delivery_val: "NIP-44-verschlüsselt, über Tor"
|
||||||
|
row_network_fee: "Netzwerkgebühr"
|
||||||
|
row_network_fee_val: "Von deinem Guthaben abgezogen"
|
||||||
|
row_privacy: "Privatsphäre"
|
||||||
|
row_privacy_val: "Mimblewimble + Tor"
|
||||||
|
send_request_btn: "Anfrage senden"
|
||||||
|
request_approve_hint: "Sie erhalten eine Anfrage zum Zustimmen"
|
||||||
|
hold_to_send: "Zum Senden halten"
|
||||||
|
lower_amount: "Zurückgehen und Betrag verringern"
|
||||||
|
hold_confirm_hint: "Gedrückt halten zum Bestätigen"
|
||||||
|
requesting: "Fordere an…"
|
||||||
|
sending: "Sende…"
|
||||||
|
they: "Sie"
|
||||||
|
request_blocked: "%{who} nimmt keine Anfragen an. Bitte sie, dir stattdessen grin zu senden."
|
||||||
|
failed_request_title: "Anfrage fehlgeschlagen"
|
||||||
|
failed_send_title: "Senden fehlgeschlagen"
|
||||||
|
failed_request_body: "Die Anfrage konnte nicht zugestellt werden. Bitte sie, dir stattdessen grin zu senden."
|
||||||
|
failed_send_body: "Die Zahlung wurde nicht zugestellt. Dein grin ist sicher — versuche es erneut."
|
||||||
|
try_again_btn: "Erneut versuchen"
|
||||||
|
close_btn: "Schließen"
|
||||||
|
success:
|
||||||
|
requested: "Angefordert"
|
||||||
|
sent: "Gesendet"
|
||||||
|
from: "von"
|
||||||
|
to: "an"
|
||||||
|
subtitle: "%{dir} %{who} · gerade eben"
|
||||||
|
done_btn: "Fertig"
|
||||||
|
receipt_btn: "Beleg"
|
||||||
@@ -44,12 +44,12 @@ wallets:
|
|||||||
await_fin_amount: Awaiting finalization
|
await_fin_amount: Awaiting finalization
|
||||||
locked_amount: Locked
|
locked_amount: Locked
|
||||||
txs_empty: 'To receive funds manually or over transport use %{message} or %{transport} buttons at the bottom of the screen, to change wallet settings press %{settings} button.'
|
txs_empty: 'To receive funds manually or over transport use %{message} or %{transport} buttons at the bottom of the screen, to change wallet settings press %{settings} button.'
|
||||||
title: Wallets
|
title: Goblin
|
||||||
create_desc: Create or import existing wallet from saved recovery phrase.
|
create_desc: Create or import existing wallet from saved recovery phrase.
|
||||||
add: Add wallet
|
add: Add wallet
|
||||||
name: 'Name:'
|
name: 'Name:'
|
||||||
pass: 'Password:'
|
pass: 'Password:'
|
||||||
pass_empty: Enter password from the wallet
|
pass_empty: Enter the wallet password
|
||||||
current_pass: 'Current password:'
|
current_pass: 'Current password:'
|
||||||
new_pass: 'New password:'
|
new_pass: 'New password:'
|
||||||
min_tx_conf_count: 'Minimum amount of confirmations for transactions:'
|
min_tx_conf_count: 'Minimum amount of confirmations for transactions:'
|
||||||
@@ -95,6 +95,7 @@ wallets:
|
|||||||
tx_finalizing: Finalizing
|
tx_finalizing: Finalizing
|
||||||
tx_posting: Posting
|
tx_posting: Posting
|
||||||
tx_confirmed: Confirmed
|
tx_confirmed: Confirmed
|
||||||
|
tx_stale: Pending too long
|
||||||
txs: Transactions
|
txs: Transactions
|
||||||
tx: Transaction
|
tx: Transaction
|
||||||
messages: Messages
|
messages: Messages
|
||||||
@@ -180,7 +181,7 @@ network:
|
|||||||
available: Available
|
available: Available
|
||||||
not_available: Not available
|
not_available: Not available
|
||||||
availability_check: Availability check
|
availability_check: Availability check
|
||||||
android_warning: Attention to Android users. To synchronize integrated node successfully, you must allow access to notifications and remove battery usage restrictions for the Grim application at system settings of your phone. This is necessary operation for correct work of application in the background.
|
android_warning: Attention to Android users. To synchronize integrated node successfully, you must allow access to notifications and remove battery usage restrictions for the Goblin application at system settings of your phone. This is necessary operation for correct work of application in the background.
|
||||||
sync_status:
|
sync_status:
|
||||||
node_restarting: Node is restarting
|
node_restarting: Node is restarting
|
||||||
node_down: Node is down
|
node_down: Node is down
|
||||||
@@ -355,4 +356,616 @@ keyboard:
|
|||||||
m: m
|
m: m
|
||||||
m1: ','
|
m1: ','
|
||||||
m2: .
|
m2: .
|
||||||
m3: /
|
m3: /
|
||||||
|
goblin:
|
||||||
|
home:
|
||||||
|
anonymous: "Anonymous"
|
||||||
|
connected_tor: "Connected over Tor"
|
||||||
|
tor_ready: "Tor ready · relays…"
|
||||||
|
connecting_tor: "Connecting to Tor…"
|
||||||
|
connected_direct: "Connected (direct)"
|
||||||
|
connecting_direct: "Connecting (direct)…"
|
||||||
|
cant_reach_node: "Can't reach node"
|
||||||
|
node_synced: "Node synced"
|
||||||
|
syncing: "Syncing…"
|
||||||
|
balance_updating: "Balance updating…"
|
||||||
|
balance_stale: "Can't reach node · last known balance"
|
||||||
|
fiat_unavailable: "Rate unavailable"
|
||||||
|
listening: "Listening for payments"
|
||||||
|
block: "Block %{height}"
|
||||||
|
waiting_for_chain: "Waiting for chain…"
|
||||||
|
nav_wallet: "Wallet"
|
||||||
|
nav_pay: "Pay"
|
||||||
|
nav_activity: "Activity"
|
||||||
|
nav_receive: "Receive"
|
||||||
|
nav_settings: "Settings"
|
||||||
|
back_again: "Press back again to switch wallets"
|
||||||
|
activity: "Activity"
|
||||||
|
news: "News"
|
||||||
|
empty_title: "No activity yet"
|
||||||
|
empty_sub: "Send or receive grin to get started."
|
||||||
|
recent: "Recent"
|
||||||
|
scan_to_pay: "Scan to pay"
|
||||||
|
type_amount: "Type an amount"
|
||||||
|
request: "Request"
|
||||||
|
pay: "Pay"
|
||||||
|
enter_amount: "Enter an amount to pay or request"
|
||||||
|
activity:
|
||||||
|
canceled: "canceled"
|
||||||
|
pending: "pending"
|
||||||
|
earlier: "Earlier"
|
||||||
|
today: "Today"
|
||||||
|
yesterday: "Yesterday"
|
||||||
|
title: "Activity"
|
||||||
|
requests: "Requests"
|
||||||
|
empty_title: "No activity yet"
|
||||||
|
empty_sub: "Your payments will appear here."
|
||||||
|
pending_header: "Pending"
|
||||||
|
receipt:
|
||||||
|
title: "Receipt"
|
||||||
|
not_found: "Transaction not found"
|
||||||
|
for_note: "For %{note}"
|
||||||
|
details: "Transaction details"
|
||||||
|
canceled: "Canceled"
|
||||||
|
expired: "Expired"
|
||||||
|
funds_returned: "Funds returned"
|
||||||
|
complete: "Complete"
|
||||||
|
payment_received: "Payment received"
|
||||||
|
payment_sent: "Payment sent successfully"
|
||||||
|
pending: "Pending"
|
||||||
|
confs: "%{c}/%{r} confirmations"
|
||||||
|
waiting_to_confirm: "Waiting to confirm"
|
||||||
|
paying: "Paying…"
|
||||||
|
you: "You"
|
||||||
|
to: "To"
|
||||||
|
from: "From"
|
||||||
|
nostr: "nostr"
|
||||||
|
identity: "Identity"
|
||||||
|
fee_none: "None"
|
||||||
|
network_fee: "Network fee"
|
||||||
|
privacy: "Privacy"
|
||||||
|
privacy_value: "Mimblewimble + Tor"
|
||||||
|
transaction: "Transaction"
|
||||||
|
cancel_request: "Cancel request"
|
||||||
|
cancel_send: "Cancel payment"
|
||||||
|
stale_note: "This payment has been pending a while."
|
||||||
|
cancel_send_confirm: "Tap again to cancel — they may still receive it"
|
||||||
|
cancel_send_done: "Payment cancelled — your funds are available again"
|
||||||
|
cancel_send_too_late: "This payment already went through and can't be cancelled"
|
||||||
|
waiting_to_receive: "Waiting for %{name} to receive…"
|
||||||
|
request:
|
||||||
|
title: "%{name} requests"
|
||||||
|
approve: "Approve"
|
||||||
|
decline: "Decline"
|
||||||
|
review_title: "Review request"
|
||||||
|
hold_to_accept: "Hold to accept"
|
||||||
|
hold_accept_hint: "Press and hold to pay this request"
|
||||||
|
receive:
|
||||||
|
title: "Receive"
|
||||||
|
requesting: "Requesting %{amt}%{tsu} — share to get paid"
|
||||||
|
clear_request: "Clear request"
|
||||||
|
share_handle: "Share your handle to get paid"
|
||||||
|
share_npub: "Share your npub to get paid"
|
||||||
|
copied: "Copied"
|
||||||
|
copy_nostr_id: "Copy nostr ID"
|
||||||
|
copy_address: "Copy address"
|
||||||
|
copy: "Copy"
|
||||||
|
share_message: "Pay me on Goblin (goblin.st) — %{npub}"
|
||||||
|
privacy_note: "Your username is public. Payment contents stay encrypted over the network."
|
||||||
|
privacy_note_npub: "Your npub is public. Payment contents stay encrypted over the network."
|
||||||
|
profile:
|
||||||
|
title: "Profile"
|
||||||
|
activity: "Activity"
|
||||||
|
no_activity: "No activity with them yet."
|
||||||
|
unblock: "Unblock"
|
||||||
|
block: "Block"
|
||||||
|
blocked_blurb: "Blocked — their payments and requests are dropped."
|
||||||
|
block_blurb: "Blocking drops their incoming payments and requests."
|
||||||
|
settings:
|
||||||
|
title: "Settings"
|
||||||
|
connected_nostr: "Connected to nostr"
|
||||||
|
connecting_relays: "Connecting to relays…"
|
||||||
|
identity: "Identity"
|
||||||
|
copy_npub: "Copy npub (public)"
|
||||||
|
backup_note: "Moving devices? Back up BOTH: your seed phrase (funds) and your identity .backup file (name + key)."
|
||||||
|
wallet: "Wallet"
|
||||||
|
display_unit: "Display unit"
|
||||||
|
relays: "Relays"
|
||||||
|
nostr_relays: "Nostr Relays"
|
||||||
|
node: "Node"
|
||||||
|
min_conf: "Minimum confirmations"
|
||||||
|
integrated_node: "Integrated node settings"
|
||||||
|
node_advanced: "Advanced"
|
||||||
|
slatepacks: "Slatepacks"
|
||||||
|
slatepacks_value: "Manual transaction"
|
||||||
|
trusted_sites: "Trusted Sites"
|
||||||
|
lock_wallet: "Lock wallet"
|
||||||
|
switch_wallet: "Switch wallet"
|
||||||
|
advanced: "Advanced"
|
||||||
|
privacy: "Privacy"
|
||||||
|
tor_routing: "Tor routing"
|
||||||
|
messages_lookups: "Messages & lookups"
|
||||||
|
tor_on: "On"
|
||||||
|
tor_off: "Off"
|
||||||
|
auto_accept: "Auto-accept"
|
||||||
|
pairing: "Price currency"
|
||||||
|
accept_anyone: "Anyone"
|
||||||
|
accept_contacts: "Contacts only"
|
||||||
|
accept_ask: "Always ask"
|
||||||
|
requests: "Requests"
|
||||||
|
incoming_requests: "Incoming requests"
|
||||||
|
incoming_requests_sub: "Let others request money from you"
|
||||||
|
hide_amounts: "Hide amounts"
|
||||||
|
hide_amounts_sub: "Hide received amounts in notifications"
|
||||||
|
language: "Language"
|
||||||
|
update_available: "Update available"
|
||||||
|
appearance: "Appearance"
|
||||||
|
theme: "Theme"
|
||||||
|
theme_light: "Light"
|
||||||
|
theme_dark: "Dark"
|
||||||
|
theme_yellow: "Yellow"
|
||||||
|
archive: "Archive"
|
||||||
|
export_archive: "Export archive"
|
||||||
|
export_archive_caption: "Copies your contacts, payment history, and requests to the clipboard as JSON."
|
||||||
|
wipe_history: "Wipe payment history"
|
||||||
|
wipe_history_confirm: "Tap again to wipe — this can't be undone"
|
||||||
|
about: "About"
|
||||||
|
goblin: "Goblin"
|
||||||
|
build: "Build %{build}"
|
||||||
|
network: "Network"
|
||||||
|
network_value: "MW + Tor + nostr"
|
||||||
|
third_party: "Third party"
|
||||||
|
grim: "GRIM (upstream wallet)"
|
||||||
|
grin_node: "Grin node"
|
||||||
|
sp_intro: "Advanced — exchange raw slatepacks by hand, the way GRIM does. Use this only when you can't pay or get paid through a username."
|
||||||
|
sp_receive_group: "Receive or finalize"
|
||||||
|
sp_receive_blurb: "Paste a slatepack someone gave you. Goblin receives the payment, pays the invoice, or finalizes and posts it."
|
||||||
|
sp_process: "Process slatepack"
|
||||||
|
sp_paste_first: "Paste a slatepack first."
|
||||||
|
sp_reply_ready: "Reply ready — send it back to the sender."
|
||||||
|
sp_finalizing: "Finalizing and posting to the chain…"
|
||||||
|
sp_create_group: "Create a payment"
|
||||||
|
sp_create_blurb: "Make a slatepack to hand to someone. They receive it, send the reply back, and you finalize it above."
|
||||||
|
sp_amount_hint: "Amount in grin"
|
||||||
|
sp_addr_hint: "Recipient address (optional)"
|
||||||
|
sp_create: "Create slatepack"
|
||||||
|
sp_ready: "Slatepack ready — hand it to the recipient."
|
||||||
|
sp_amount_gt_zero: "Enter an amount greater than zero."
|
||||||
|
sp_to_send: "Slatepack to send"
|
||||||
|
sp_copy: "Copy slatepack"
|
||||||
|
cancel: "Cancel"
|
||||||
|
continue: "Continue"
|
||||||
|
final_confirmation: "Final confirmation"
|
||||||
|
type_reset: "Type RESET"
|
||||||
|
wallet_password: "Wallet password"
|
||||||
|
done: "Done"
|
||||||
|
close: "Close"
|
||||||
|
import_nsec_hint: "nsec1… or pasted backup"
|
||||||
|
backup_password_hint: "Backup password (only if exported elsewhere)"
|
||||||
|
import_btn: "Import"
|
||||||
|
importing: "Importing…"
|
||||||
|
import_failed: "Import failed"
|
||||||
|
name_authority: "Name authority"
|
||||||
|
name_authority_title: "Change name authority"
|
||||||
|
name_authority_blurb: "The server that registers and verifies names. Point it at another instance to use and pay names hosted there."
|
||||||
|
name_authority_invalid: "Enter a full URL (https://…)."
|
||||||
|
reset: "Reset"
|
||||||
|
save: "Save"
|
||||||
|
backup_file: "Back up to a file"
|
||||||
|
choose_backup_file: "Choose a .backup file"
|
||||||
|
backup_read_failed: "Couldn't read that file."
|
||||||
|
backup_saved: "Backup saved"
|
||||||
|
backup_saved_sub: "Keep the .backup file safe. Anyone with it AND your password can restore your whole wallet."
|
||||||
|
backup_file_title: "Back up wallet"
|
||||||
|
backup_file_blurb: "Creates one encrypted .backup file with your wallet seed and every identity. Enter your wallet password to seal it."
|
||||||
|
backup_write_failed: "Couldn't save the file."
|
||||||
|
create_backup: "Create backup"
|
||||||
|
registered: "Registered %{name}"
|
||||||
|
released_msg: "Released — the name is up for grabs"
|
||||||
|
release_confirm: "Release %{name}?"
|
||||||
|
release_blurb: "It's up for grabs the moment it's free — anyone can claim it, including the next key you rotate to. You won't be able to register another username for 10 minutes."
|
||||||
|
releasing: "Releasing…"
|
||||||
|
keep_it: "Keep it"
|
||||||
|
release_it: "Release it"
|
||||||
|
username: "Username"
|
||||||
|
username_note: "Shown as your name. Public on goblin.st. Payments stay encrypted."
|
||||||
|
release_username: "Release username"
|
||||||
|
pick_username: "Pick a username — optional"
|
||||||
|
working: "Working…"
|
||||||
|
claim: "Claim"
|
||||||
|
err_just_taken: "That username was just taken"
|
||||||
|
err_cooldown: "You recently released a username — you can register a new one within 10 minutes."
|
||||||
|
err_unreachable: "Couldn't reach goblin.st — connection hiccup. Try again."
|
||||||
|
err_release: "Couldn't release: %{err}"
|
||||||
|
avail_available: "Available!"
|
||||||
|
avail_taken: "Taken"
|
||||||
|
avail_reserved: "Reserved"
|
||||||
|
avail_invalid: "Names are 3–20 chars: a–z, 0–9, _ or -"
|
||||||
|
avail_quarantined: "Not available"
|
||||||
|
avail_unknown: "Couldn't check — connection hiccup. Try again."
|
||||||
|
username_none: "Not set"
|
||||||
|
advanced_privacy: "Advanced Privacy"
|
||||||
|
tap_reveal: "Tap to reveal"
|
||||||
|
notif_someone: "Someone"
|
||||||
|
notif_private_received: "You got paid. Open Goblin to see."
|
||||||
|
notif_private_requested: "New payment request. Open Goblin to see."
|
||||||
|
username:
|
||||||
|
title: "Username"
|
||||||
|
authority: "Name authority"
|
||||||
|
authority_blurb: "Leave as default, or point to another instance to use names hosted there."
|
||||||
|
learn_more: "Learn more"
|
||||||
|
custom: "Custom authority"
|
||||||
|
advprivacy:
|
||||||
|
intro: "Control what your notifications reveal, and hide your balance and history on this device."
|
||||||
|
notifications: "Notifications"
|
||||||
|
hide_names: "Hide names"
|
||||||
|
hide_names_sub: "Leave out who paid in notifications"
|
||||||
|
hide_details: "Hide all details"
|
||||||
|
hide_details_sub: "Show only a private alert, no name or amount"
|
||||||
|
anon: "Anonymous mode"
|
||||||
|
anon_toggle: "Blur balance and activity"
|
||||||
|
anon_sub: "Your balance and activity show as dots until you tap to reveal"
|
||||||
|
advanced:
|
||||||
|
title: "Advanced"
|
||||||
|
intro: "Low-level wallet tools from GRIM. You won't normally need these."
|
||||||
|
own_node_desc: "Sync a full Grin node on this device instead of trusting a public one."
|
||||||
|
own_node_active: "Running your own node"
|
||||||
|
repair: "Repair wallet"
|
||||||
|
repair_desc: "Re-scan the chain and restore any missing outputs. This can take a while."
|
||||||
|
repair_unavailable: "Needs a synced node connection first."
|
||||||
|
repairing: "Repairing… %{pct}%"
|
||||||
|
restore: "Restore wallet"
|
||||||
|
restore_desc: "Delete local data and rebuild from your seed. Use this if a repair didn't help — you'll re-open the wallet after."
|
||||||
|
restore_confirm: "Tap again to restore"
|
||||||
|
show_phrase: "Recovery phrase"
|
||||||
|
phrase_desc: "Your 24 grin seed words — the only way to recover funds. Keep them offline and private."
|
||||||
|
reveal: "Show phrase"
|
||||||
|
hide: "Hide"
|
||||||
|
password: "Wallet password"
|
||||||
|
wrong_password: "Wrong password."
|
||||||
|
delete: "Delete wallet"
|
||||||
|
delete_desc: "Permanently remove this wallet and every identity on this device. Back up first. Without your seed, funds can't be recovered."
|
||||||
|
delete_confirm: "Tap again to delete"
|
||||||
|
manage_node: "Manage node connection"
|
||||||
|
repair_confirm: "Yes, repair now"
|
||||||
|
repair_confirm_note: "Repair re-scans the chain and can take a few minutes."
|
||||||
|
restore_confirm_note: "This erases local data and rebuilds it from your seed — it can take several minutes."
|
||||||
|
nostr_key: "Nostr key"
|
||||||
|
nostr_key_desc: "Your nsec, the secret key to your nostr identity. Copy it or show its QR to log in to nostr apps like magick.market. Anyone who has it controls your identity, so keep it private."
|
||||||
|
reveal_nsec: "Show key"
|
||||||
|
copy_nsec: "Copy nsec"
|
||||||
|
show_qr: "Show QR"
|
||||||
|
hide_qr: "Hide QR"
|
||||||
|
nostr_section: "Advanced nostr settings"
|
||||||
|
backup_caption: "Contains your wallet and all identities."
|
||||||
|
danger_zone: "Danger zone"
|
||||||
|
delete_warning: "This erases your recovery phrase and every identity on this device. Anything you haven't backed up is gone for good."
|
||||||
|
download_backup: "Download backup"
|
||||||
|
delete_final: "Delete permanently"
|
||||||
|
privacy:
|
||||||
|
title: "Network privacy"
|
||||||
|
intro: "Goblin sends only a few things over the network, each sealed with end-to-end encryption so relays can't read them or link them to you."
|
||||||
|
payments: "Payments"
|
||||||
|
payments_blurb: "Every nostr message carrying a slatepack."
|
||||||
|
usernames: "Usernames"
|
||||||
|
usernames_blurb: "NIP-05 name lookups to and from name authorities."
|
||||||
|
price_avatars: "Price"
|
||||||
|
price_avatars_blurb: "The live fiat rate shown next to amounts."
|
||||||
|
grin_node: "Grin node"
|
||||||
|
grin_node_blurb: "Syncs blocks and broadcasts your transactions. Public chain data, the same for everyone, never linked to your identity."
|
||||||
|
how_privacy: "How your privacy works"
|
||||||
|
how_privacy_blurb: "Payments and messages are sealed with authenticated encryption and delivered as private messages, so a relay can't read them or link a payment back to you."
|
||||||
|
always_direct: "Always direct"
|
||||||
|
tor_off: "Tor off"
|
||||||
|
tor_on: "Tor on"
|
||||||
|
tor_switch: "Route through Tor"
|
||||||
|
tor_switch_sub: "Hide your IP from relays."
|
||||||
|
vpn_note: "With Tor off, relays can see your IP address. We recommend using at least a VPN."
|
||||||
|
pairing:
|
||||||
|
title: "Pairing"
|
||||||
|
intro: "What your balance and amounts are shown against."
|
||||||
|
pair_with: "Pair with"
|
||||||
|
rates_note: "Rates fetch over Tor, only while a pairing is on — off means no rate request leaves your device."
|
||||||
|
relays:
|
||||||
|
title: "Relays"
|
||||||
|
intro: "Payment messages are mirrored to every relay below; one reachable relay is enough to receive."
|
||||||
|
your_relays: "Your relays"
|
||||||
|
add_relay: "Add relay"
|
||||||
|
add_relay_btn: "Add relay"
|
||||||
|
save_reconnect: "Save & reconnect"
|
||||||
|
none: "none"
|
||||||
|
count: "%{n} relays"
|
||||||
|
node:
|
||||||
|
title: "Node"
|
||||||
|
connection: "Connection"
|
||||||
|
integrated: "Integrated node"
|
||||||
|
applies_after: "Applies after the wallet is locked and unlocked again."
|
||||||
|
add_external: "Add external node"
|
||||||
|
api_secret_hint: "API secret (optional)"
|
||||||
|
add_node: "Add node"
|
||||||
|
integrated_host: "integrated node"
|
||||||
|
summary_syncing: "%{conn} · syncing"
|
||||||
|
summary_block: "Block %{height} · %{conn}"
|
||||||
|
nips:
|
||||||
|
title: "nostr & NIPs"
|
||||||
|
intro1: "Goblin speaks nostr — an open protocol of signed messages passed through simple relay servers. Your wallet carries its own nostr identity: a standalone random key, kept deliberately independent of your funds and seed. Every payment travels as an end-to-end encrypted direct message between identities, with the slatepack riding inside."
|
||||||
|
intro2: "goblin.st is Goblin's name service: claiming a username publishes a name → key mapping there (NIP-05), so people can pay you instead of a long npub. The username is public; payment contents never are. NIPs are the protocol's building blocks — tap one to read the spec."
|
||||||
|
n05_title: "Names"
|
||||||
|
n05_blurb: "Maps username@goblin.st to your key, so handles work like addresses."
|
||||||
|
n17_title: "Private messages"
|
||||||
|
n17_blurb: "The encrypted DM envelope every payment travels in."
|
||||||
|
n44_title: "Encryption"
|
||||||
|
n44_blurb: "The authenticated cipher used inside those messages."
|
||||||
|
n49_title: "Key encryption"
|
||||||
|
n49_blurb: "How the secret key is stored at rest, locked by your password."
|
||||||
|
n59_title: "Gift wrap"
|
||||||
|
n59_blurb: "Wraps messages so relays can't see who is talking to whom."
|
||||||
|
n98_title: "HTTP auth"
|
||||||
|
n98_blurb: "Signs the username registration request to goblin.st."
|
||||||
|
batch:
|
||||||
|
title: "Approve %{n} invoices"
|
||||||
|
blurb: "Issue %{n} payment requests of %{amount} each to %{name} — %{total} in total. Each request gets its own fresh payment address."
|
||||||
|
approve: "Approve"
|
||||||
|
login:
|
||||||
|
title: "Sign in"
|
||||||
|
headline: "Sign in to %{domain}?"
|
||||||
|
identity: "Signing in as"
|
||||||
|
explain: "This signs a one-time login request. It does not share your key or let the site act as you."
|
||||||
|
pass_prompt: "Enter your wallet password to sign in."
|
||||||
|
confirm: "Sign in"
|
||||||
|
sent: "Signed in to %{domain}"
|
||||||
|
failed: "Could not reach %{domain}. The sign-in was not delivered."
|
||||||
|
authorize:
|
||||||
|
title: "Authorize"
|
||||||
|
headline: "Authorize %{domain}?"
|
||||||
|
kind_post: "Public post"
|
||||||
|
kind_repost: "Repost"
|
||||||
|
kind_reaction: "Reaction"
|
||||||
|
kind_article: "Article (long-form)"
|
||||||
|
kind_unknown: "Kind %{n} (unrecognized)"
|
||||||
|
unknown_caution: "Goblin does not recognize this event type. Only approve if you trust %{domain}."
|
||||||
|
identity: "Signing as"
|
||||||
|
explain: "Approving signs this one event and hands it to %{domain}. Nothing is published by Goblin, no key is shared, and the site gets no session or future capability."
|
||||||
|
pass_prompt: "Enter your wallet password to authorize."
|
||||||
|
confirm: "Authorize"
|
||||||
|
sent: "Authorized %{domain}"
|
||||||
|
failed: "Could not reach %{domain}. Nothing was delivered."
|
||||||
|
truncated: "truncated, %{n} more characters"
|
||||||
|
show_full: "Show full"
|
||||||
|
show_less: "Show less"
|
||||||
|
replying_to: "replying to %{id}"
|
||||||
|
mentions: "mentions %{id}"
|
||||||
|
repost_of: "reposts %{id}"
|
||||||
|
by_author: "by %{id}"
|
||||||
|
reacts_to: "reacts to %{id}"
|
||||||
|
article_title: "Title: %{title}"
|
||||||
|
trust:
|
||||||
|
title: "Trust"
|
||||||
|
headline: "Trust %{domain}?"
|
||||||
|
identity: "Signing in as"
|
||||||
|
lead: "%{domain} can act for you this session without asking each time. Anything that spends money always asks."
|
||||||
|
grant_intro: "Can sign for you without asking:"
|
||||||
|
cat_social: "Posts and reactions"
|
||||||
|
cat_dm: "Direct messages"
|
||||||
|
cat_market: "Listings"
|
||||||
|
cat_identity: "Profile and lists"
|
||||||
|
cat_delete: "Deletes"
|
||||||
|
cat_http: "Uploads and HTTP auth"
|
||||||
|
cat_unknown: "Other event type (kind %{n})"
|
||||||
|
login_excluded: "Login is never granted to a site."
|
||||||
|
money_line: "Money always asks for your password. Nothing is spent or sold silently."
|
||||||
|
duration: "Lasts this session only. End it any time in Settings, Trusted Sites."
|
||||||
|
pass_prompt: "Enter your wallet password to trust this site."
|
||||||
|
confirm_hold: "Hold to trust for this session"
|
||||||
|
sent: "Trusting %{domain} for this session"
|
||||||
|
failed: "Could not reach %{domain}. The session was not started."
|
||||||
|
announce_failed: "Trusted, but %{domain} has not confirmed the session yet."
|
||||||
|
notice_volume: "A trusted site is signing a lot."
|
||||||
|
notice_decrypt: "A trusted site is reading your messages."
|
||||||
|
money:
|
||||||
|
title: "Confirm"
|
||||||
|
headline: "Confirm in %{domain}?"
|
||||||
|
identity: "Signing as %{id}"
|
||||||
|
explain: "This moves or commits value, so it always asks. Approving signs this one action."
|
||||||
|
pass_prompt: "Enter your wallet password to confirm."
|
||||||
|
confirm_hold: "Hold to confirm"
|
||||||
|
encrypt_desc: "An encrypted order message that commits to a payment."
|
||||||
|
trusted_sites:
|
||||||
|
title: "Trusted Sites"
|
||||||
|
intro: "Sites you trusted this session can sign low-risk actions without asking. Money always asks. End any session here."
|
||||||
|
empty: "No trusted sites. Trust a site when you sign in to give it a signing session."
|
||||||
|
none: "nothing"
|
||||||
|
can_sign: "Can sign silently: %{cats}"
|
||||||
|
time_left: "Ends in %{mins} min"
|
||||||
|
paused: "Paused, signing a lot"
|
||||||
|
resume: "Resume"
|
||||||
|
end: "End session"
|
||||||
|
identities:
|
||||||
|
title: "Identities"
|
||||||
|
switch_hint: "Switch identity"
|
||||||
|
blurb: "Your identities are the names you get paid under. They all pay into this one wallet. Add a new one, switch between them, or remove one you don't need."
|
||||||
|
privacy_note: "These identities share one wallet and one balance — separate front doors, not separate funds. For truly separate money, use a separate wallet."
|
||||||
|
held: "Held identities"
|
||||||
|
add: "Add identity"
|
||||||
|
add_title: "Add an identity"
|
||||||
|
nsec_hint: "Paste an nsec"
|
||||||
|
generate_note: "A fresh, anonymous key will be created for this wallet."
|
||||||
|
generate: "Generate"
|
||||||
|
import: "Import"
|
||||||
|
choose_backup: "Choose a .backup file"
|
||||||
|
backup_selected: "Backup file selected"
|
||||||
|
delete_short: "Delete identity"
|
||||||
|
delete_title: "Delete %{name}?"
|
||||||
|
delete_blurb: "This permanently removes the identity from this wallet. Money already received stays in your balance."
|
||||||
|
delete_backup_note: "Back it up first — without its nsec or .backup file it cannot be recovered."
|
||||||
|
delete_confirm: "Delete"
|
||||||
|
manage_title: "Manage identity"
|
||||||
|
tag_note: "Give it a private name. Only you see it — it is never shared."
|
||||||
|
tag_hint: "Private name"
|
||||||
|
tag_save: "Save"
|
||||||
|
pass_prompt: "Your wallet password encrypts and unlocks each identity. Enter it to continue."
|
||||||
|
working: "Working…"
|
||||||
|
onboarding:
|
||||||
|
privacy:
|
||||||
|
kicker: "NETWORK PRIVACY"
|
||||||
|
continue: "Continue"
|
||||||
|
intro:
|
||||||
|
private_money_head: "Private money"
|
||||||
|
private_money_body: "Goblin is a wallet for grin — digital cash with no amounts or addresses on its chain."
|
||||||
|
send_like_message_head: "Send like a message"
|
||||||
|
send_like_message_body: "Pay a username or npub and it arrives as an end-to-end encrypted message over nostr and Tor — no one in between can see the amount or who's involved."
|
||||||
|
yours_alone_head: "Yours alone"
|
||||||
|
yours_alone_body: "Keys, names and history live on this device. Built on the GRIM wallet."
|
||||||
|
get_started: "Get started"
|
||||||
|
footnote: "Takes about a minute. You can change everything later."
|
||||||
|
node:
|
||||||
|
kicker: "STEP 1 OF 3 · NETWORK"
|
||||||
|
title: "How should Goblin\nwatch the chain?"
|
||||||
|
own_title: "Run my own node"
|
||||||
|
own_badge: "Private"
|
||||||
|
own_body: "Trusts no one — your wallet checks the chain itself. Syncs in the background while you finish setup."
|
||||||
|
connect_title: "Connect to a node"
|
||||||
|
connect_badge: "Instant"
|
||||||
|
connect_body: "No sync wait. The node you pick can see your wallet's queries."
|
||||||
|
changeable: "Changeable any time in Settings → Node."
|
||||||
|
continue: "Continue"
|
||||||
|
url_invalid: "Node URL must start with http:// or https://"
|
||||||
|
wallet:
|
||||||
|
kicker: "STEP 2 OF 3 · WALLET"
|
||||||
|
title: "Set up your wallet"
|
||||||
|
create_new: "Create new"
|
||||||
|
restore_from_seed: "Restore from seed"
|
||||||
|
import_identity: "Import identity"
|
||||||
|
import_identity_hint: "You'll bring in your existing key (a .backup file or nsec) after this step."
|
||||||
|
name_hint: "Wallet name"
|
||||||
|
password_hint: "Password"
|
||||||
|
repeat_password_hint: "Repeat password"
|
||||||
|
restore_hint: "Have your seed words ready — you'll enter them next."
|
||||||
|
create_hint: "Next you'll get 24 seed words to write down. They are the money — anyone holding them holds your funds."
|
||||||
|
continue: "Continue"
|
||||||
|
passwords_no_match: "Passwords don't match"
|
||||||
|
words:
|
||||||
|
kicker: "STEP 2 OF 3 · WALLET"
|
||||||
|
title_restore: "Enter your seed words"
|
||||||
|
title_create: "Write these words down"
|
||||||
|
write_down_hint: "On paper, in order. Anyone with these words can take your funds; without them a lost device means lost funds."
|
||||||
|
paste: "Paste"
|
||||||
|
scan_qr: "Scan QR"
|
||||||
|
copy_clipboard: "Copy to clipboard (avoid this)"
|
||||||
|
restore_wallet: "Restore wallet"
|
||||||
|
wrote_them_down: "I wrote them down"
|
||||||
|
fill_every_word: "Fill every word — tap a word to edit it, or paste the phrase."
|
||||||
|
unlock_backup: "Unlock backup"
|
||||||
|
backup_unlocked: "Backup unlocked. Your identities restore when the wallet opens."
|
||||||
|
confirm:
|
||||||
|
kicker: "STEP 2 OF 3 · WALLET"
|
||||||
|
title: "Now prove it"
|
||||||
|
enter_hint: "Enter the words you just wrote down. Tap a word to type it."
|
||||||
|
paste: "Paste"
|
||||||
|
create_wallet: "Create wallet"
|
||||||
|
keep_going: "Keep going — every word, in order."
|
||||||
|
identity:
|
||||||
|
kicker: "STEP 3 OF 3 · IDENTITY"
|
||||||
|
title: "Your payment identity"
|
||||||
|
key_being_made: "key being made…"
|
||||||
|
connected_tor: "connected over Tor"
|
||||||
|
connecting_tor: "connecting over Tor…"
|
||||||
|
connected_direct: "connected (direct)"
|
||||||
|
connecting_direct: "connecting (direct)…"
|
||||||
|
fresh_key_blurb: "A payment key that isn't part of your seed — rotate it anytime to stay private, without touching your funds."
|
||||||
|
restoring: "Restoring your identities…"
|
||||||
|
clean_slate_blurb: "Want a clean slate? Swap in a brand-new key any time — the new you isn't linked to the old one. Same wallet, fresh face."
|
||||||
|
pick_username: "Pick a username — optional"
|
||||||
|
username_blurb: "Friends pay your name instead of a long key. Optional — claim one any time."
|
||||||
|
username_field_hint: "yourname"
|
||||||
|
working: "Working…"
|
||||||
|
claim_username: "Claim username"
|
||||||
|
available_when_connected: "Available once Tor connects — or skip and claim later."
|
||||||
|
youre: "You're %{name}"
|
||||||
|
claimed_title: "%{name} is yours"
|
||||||
|
claimed_blurb: "Friends can now pay you by name. You're all set — open your wallet."
|
||||||
|
open_wallet: "Open my wallet"
|
||||||
|
skip_for_now: "Skip for now"
|
||||||
|
import_existing: "Already have a Goblin identity? Import it"
|
||||||
|
import_title: "Import your identity"
|
||||||
|
import_blurb: "Paste your nsec or pick a .backup file to keep your existing key and username instead of this new one."
|
||||||
|
errors:
|
||||||
|
cant_open: "Couldn't open the wallet: %{err}"
|
||||||
|
cant_create: "Couldn't create the wallet: %{err}"
|
||||||
|
send:
|
||||||
|
scan_to_request: "Scan to request"
|
||||||
|
scan_to_pay: "Scan to pay"
|
||||||
|
tab_scan: "Scan"
|
||||||
|
tab_my_code: "My Code"
|
||||||
|
request_from: "Request from"
|
||||||
|
send_to: "Send to"
|
||||||
|
search_hint: "handle, npub, or name"
|
||||||
|
suggested: "%{icon} Suggested"
|
||||||
|
no_contacts: "No contacts yet. Find someone by their handle."
|
||||||
|
no_profile: "no profile"
|
||||||
|
tag_contact: "contact"
|
||||||
|
tag_on_nostr: "on nostr"
|
||||||
|
searching_nostr: "Searching nostr…"
|
||||||
|
unverified_title: "Pay an unverified key?"
|
||||||
|
unverified_body: "No nostr profile is published for this key — it may be brand new, anonymous, or mistyped. Double-check it's the right one before sending."
|
||||||
|
keep_looking: "Keep looking"
|
||||||
|
pay_anyway: "Pay anyway"
|
||||||
|
scan_not_recipient: "That QR isn't a goblin recipient — expected an npub or handle"
|
||||||
|
scan_prompt: "Position a goblin code in view to activate"
|
||||||
|
scan_to_pay_me: "Scan to pay me"
|
||||||
|
share_btn: "%{icon} Share"
|
||||||
|
share_message: "Pay me on Goblin — %{handle}\n%{link}\nnpub: %{npub}"
|
||||||
|
none_found: "No one found for %{label}"
|
||||||
|
enter_recipient: "Enter a handle, npub, or name"
|
||||||
|
amount_title: "Amount"
|
||||||
|
to_name: "To %{name}"
|
||||||
|
not_enough: "You don't have enough grin"
|
||||||
|
max: "Max"
|
||||||
|
note_label: "Note"
|
||||||
|
note_hint: "Add a note…"
|
||||||
|
add_note: "Add a note"
|
||||||
|
edit_note: "Edit note"
|
||||||
|
note_cancel: "Cancel"
|
||||||
|
note_save: "Save"
|
||||||
|
review_btn: "Review"
|
||||||
|
confirm_request: "Confirm request"
|
||||||
|
review_title: "Review"
|
||||||
|
requesting_from: "Requesting from %{name}"
|
||||||
|
youre_sending: "You're sending %{name}"
|
||||||
|
row_from: "From"
|
||||||
|
row_to: "To"
|
||||||
|
row_note: "Note"
|
||||||
|
row_proof: "Payment proof"
|
||||||
|
row_proof_val: "Included"
|
||||||
|
row_proof_shared: "Proof shared with"
|
||||||
|
row_they_pay: "They pay"
|
||||||
|
row_they_pay_val: "Only if they approve"
|
||||||
|
row_delivery: "Delivery"
|
||||||
|
row_delivery_val: "NIP-44 encrypted, over Tor"
|
||||||
|
row_network_fee: "Network fee"
|
||||||
|
row_network_fee_val: "Deducted from your balance"
|
||||||
|
row_privacy: "Privacy"
|
||||||
|
row_privacy_val: "Mimblewimble + Tor"
|
||||||
|
send_request_btn: "Send request"
|
||||||
|
request_approve_hint: "They'll get a request to approve"
|
||||||
|
hold_to_send: "Hold to send"
|
||||||
|
lower_amount: "Go back and lower the amount"
|
||||||
|
hold_confirm_hint: "Press and hold to confirm"
|
||||||
|
requesting: "Requesting…"
|
||||||
|
sending: "Sending…"
|
||||||
|
they: "They"
|
||||||
|
request_blocked: "%{who} isn't accepting requests. Ask them to send you grin instead."
|
||||||
|
failed_request_title: "Couldn't request"
|
||||||
|
failed_send_title: "Couldn't send"
|
||||||
|
failed_request_body: "We couldn't deliver the request. Ask them to send you grin instead."
|
||||||
|
failed_send_body: "The payment wasn't delivered. Your grin is safe — try again."
|
||||||
|
try_again_btn: "Try again"
|
||||||
|
close_btn: "Close"
|
||||||
|
success:
|
||||||
|
requested: "Requested"
|
||||||
|
sent: "Sent"
|
||||||
|
from: "from"
|
||||||
|
to: "to"
|
||||||
|
subtitle: "%{dir} %{who} · just now"
|
||||||
|
done_btn: "Done"
|
||||||
|
receipt_btn: "Receipt"
|
||||||
@@ -0,0 +1,971 @@
|
|||||||
|
lang_name: Español
|
||||||
|
copy: Copiar
|
||||||
|
paste: Pegar
|
||||||
|
continue: Continuar
|
||||||
|
complete: Completar
|
||||||
|
error: Error
|
||||||
|
retry: Reintentar
|
||||||
|
close: Cerrar
|
||||||
|
change: Cambiar
|
||||||
|
show: Mostrar
|
||||||
|
delete: Eliminar
|
||||||
|
clear: Borrar
|
||||||
|
create: Crear
|
||||||
|
id: Identificador
|
||||||
|
kernel: Kernel
|
||||||
|
settings: Ajustes
|
||||||
|
language: Idioma
|
||||||
|
scan: Escanear
|
||||||
|
qr_code: Código QR
|
||||||
|
scan_qr: Escanear código QR
|
||||||
|
repeat: Repetir
|
||||||
|
scan_result: Resultado del escaneo
|
||||||
|
back: Atrás
|
||||||
|
share: Compartir
|
||||||
|
theme: 'Tema:'
|
||||||
|
dark: Oscuro
|
||||||
|
light: Claro
|
||||||
|
file: Archivo
|
||||||
|
choose_file: Elegir archivo
|
||||||
|
choose_folder: Elegir carpeta
|
||||||
|
crash_report: Informe de fallos
|
||||||
|
crash_report_warning: La aplicación se cerró inesperadamente la última vez, puedes compartir el informe de fallos con los desarrolladores.
|
||||||
|
confirmation: Confirmación
|
||||||
|
enter_url: Ingresa la URL
|
||||||
|
max_short: MÁX
|
||||||
|
files_location: Ubicación de archivos
|
||||||
|
moving_files: Moviendo archivos
|
||||||
|
wrong_path_error: Ruta especificada incorrecta
|
||||||
|
check_updates: Buscar actualizaciones al iniciar
|
||||||
|
update_available: ¡Hay una actualización disponible!
|
||||||
|
changelog: 'Registro de cambios:'
|
||||||
|
wallets:
|
||||||
|
await_conf_amount: Esperando confirmación
|
||||||
|
await_fin_amount: Esperando finalización
|
||||||
|
locked_amount: Bloqueado
|
||||||
|
txs_empty: 'Para recibir fondos manualmente o por transporte, usa los botones %{message} o %{transport} en la parte inferior de la pantalla; para cambiar los ajustes de la billetera, presiona el botón %{settings}.'
|
||||||
|
title: Goblin
|
||||||
|
create_desc: Crea o importa una billetera existente a partir de una frase de recuperación guardada.
|
||||||
|
add: Agregar billetera
|
||||||
|
name: 'Nombre:'
|
||||||
|
pass: 'Contraseña:'
|
||||||
|
pass_empty: Ingresa la contraseña de la billetera
|
||||||
|
current_pass: 'Contraseña actual:'
|
||||||
|
new_pass: 'Contraseña nueva:'
|
||||||
|
min_tx_conf_count: 'Cantidad mínima de confirmaciones para las transacciones:'
|
||||||
|
recover: Restaurar
|
||||||
|
recovery_phrase: Frase de recuperación
|
||||||
|
words_count: 'Cantidad de palabras:'
|
||||||
|
enter_word: 'Ingresa la palabra #%{number}:'
|
||||||
|
not_valid_word: La palabra ingresada no es válida
|
||||||
|
not_valid_phrase: La frase ingresada no es válida
|
||||||
|
create_phrase_desc: Escribe y guarda tu frase de recuperación en un lugar seguro.
|
||||||
|
restore_phrase_desc: Ingresa las palabras de tu frase de recuperación guardada.
|
||||||
|
setup_conn_desc: Elige cómo se conecta tu billetera a la red.
|
||||||
|
conn_method: Método de conexión
|
||||||
|
ext_conn: 'Conexiones externas:'
|
||||||
|
add_node: Agregar nodo
|
||||||
|
node_url: 'URL del nodo:'
|
||||||
|
node_secret: 'Secreto de API (opcional):'
|
||||||
|
invalid_url: La URL ingresada no es válida
|
||||||
|
open: Abrir la billetera
|
||||||
|
wrong_pass: La contraseña ingresada es incorrecta
|
||||||
|
locked: Bloqueada
|
||||||
|
unlocked: Desbloqueada
|
||||||
|
enable_node: 'Activa el nodo integrado para usar la billetera, o cambia los ajustes de conexión seleccionando %{settings} en la parte inferior de la pantalla.'
|
||||||
|
node_loading: 'La billetera se cargará después de sincronizar el nodo integrado; puedes cambiar la conexión seleccionando %{settings} en la parte inferior de la pantalla.'
|
||||||
|
loading: Cargando
|
||||||
|
closing: Cerrando
|
||||||
|
checking: Verificando
|
||||||
|
default_wallet: Billetera predeterminada
|
||||||
|
new_account_desc: 'Ingresa el nombre de la nueva cuenta:'
|
||||||
|
wallet_loading: Cargando billetera
|
||||||
|
wallet_closing: Cerrando billetera
|
||||||
|
wallet_checking: Verificando billetera
|
||||||
|
tx_loading: Cargando transacciones
|
||||||
|
default_account: Cuenta predeterminada
|
||||||
|
accounts: Cuentas
|
||||||
|
tx_sent: Enviado
|
||||||
|
tx_received: Recibido
|
||||||
|
tx_sending: Enviando
|
||||||
|
tx_receiving: Recibiendo
|
||||||
|
tx_confirming: Esperando confirmación
|
||||||
|
tx_canceled: Cancelado
|
||||||
|
tx_cancelling: Cancelando
|
||||||
|
tx_finalizing: Finalizando
|
||||||
|
tx_posting: Publicando
|
||||||
|
tx_confirmed: Confirmado
|
||||||
|
tx_stale: Pendiente por demasiado tiempo
|
||||||
|
txs: Transacciones
|
||||||
|
tx: Transacción
|
||||||
|
messages: Mensajes
|
||||||
|
transport: Transporte
|
||||||
|
input_slatepack_desc: 'Ingresa el mensaje Slatepack recibido para crear una respuesta o finalizar la solicitud:'
|
||||||
|
parse_slatepack_err: 'Ocurrió un error al leer el mensaje, verifica los datos ingresados:'
|
||||||
|
pay_balance_error: 'El saldo de la cuenta es insuficiente para pagar %{amount} ツ y la comisión de red.'
|
||||||
|
parse_i1_slatepack_desc: 'Para pagar %{amount} ツ, envía este mensaje al destinatario:'
|
||||||
|
parse_i2_slatepack_desc: 'Finaliza la transacción para recibir %{amount} ツ:'
|
||||||
|
parse_i3_slatepack_desc: 'Publica la transacción para finalizar la recepción de %{amount} ツ:'
|
||||||
|
parse_s1_slatepack_desc: 'Para recibir %{amount} ツ, envía este mensaje al remitente:'
|
||||||
|
parse_s2_slatepack_desc: 'Finaliza la transacción para enviar %{amount} ツ:'
|
||||||
|
parse_s3_slatepack_desc: 'Publica la transacción para finalizar el envío de %{amount} ツ:'
|
||||||
|
resp_slatepack_err: 'Ocurrió un error al crear la respuesta, verifica los datos ingresados o inténtalo de nuevo:'
|
||||||
|
resp_exists_err: Esa transacción ya existe.
|
||||||
|
resp_canceled_err: Esa transacción ya fue cancelada.
|
||||||
|
create_request_desc: 'Crea una solicitud para enviar o recibir los fondos:'
|
||||||
|
send_request_desc: 'Has creado una solicitud para enviar %{amount} ツ. Envía este mensaje al destinatario:'
|
||||||
|
send_slatepack_err: Ocurrió un error al crear la solicitud para enviar fondos, verifica los datos ingresados o inténtalo de nuevo.
|
||||||
|
invoice_desc: 'Has creado una solicitud para recibir %{amount} ツ. Envía este mensaje al remitente:'
|
||||||
|
invoice_slatepack_err: Ocurrió un error al emitir la factura, verifica los datos ingresados o inténtalo de nuevo.
|
||||||
|
finalize_slatepack_err: 'Ocurrió un error durante la finalización, verifica los datos ingresados o inténtalo de nuevo:'
|
||||||
|
finalize: Finalizar
|
||||||
|
use_dandelion: Usar Dandelion
|
||||||
|
enter_amount_send: 'Tienes %{amount} ツ. Ingresa el monto a enviar:'
|
||||||
|
enter_amount_receive: 'Ingresa el monto a recibir:'
|
||||||
|
recovery: Recuperación
|
||||||
|
repair_wallet: Reparar billetera
|
||||||
|
repair_desc: Verifica la billetera, reparando y restaurando salidas faltantes si es necesario. Esta operación tomará tiempo.
|
||||||
|
repair_unavailable: Necesitas una conexión activa al nodo y la sincronización completa de la billetera.
|
||||||
|
delete: Eliminar billetera
|
||||||
|
delete_conf: ¿Seguro que quieres eliminar la billetera?
|
||||||
|
delete_desc: Asegúrate de haber guardado tu frase de recuperación para acceder a los fondos más adelante.
|
||||||
|
wallet_loading_err: 'Ocurrió un error al sincronizar la billetera, puedes reintentar o cambiar los ajustes de conexión seleccionando %{settings} en la parte inferior de la pantalla.'
|
||||||
|
wallet: Billetera
|
||||||
|
send: Enviar
|
||||||
|
receive: Recibir
|
||||||
|
settings: Ajustes de la billetera
|
||||||
|
tx_send_cancel_conf: '¿Seguro que quieres cancelar el envío de %{amount} ツ?'
|
||||||
|
tx_receive_cancel_conf: '¿Seguro que quieres cancelar la recepción de %{amount} ツ?'
|
||||||
|
rec_phrase_not_found: Frase de recuperación no encontrada.
|
||||||
|
restore_wallet_desc: Restaura la billetera eliminando todos los archivos si la reparación habitual no ayudó, deberás volver a abrir tu billetera.
|
||||||
|
fee_base_desc: 'Comisión (valor base%{value}):'
|
||||||
|
payment_proof: Comprobante de pago
|
||||||
|
payment_proof_desc: 'Ingresa el comprobante de pago recibido para verificar la transacción:'
|
||||||
|
payment_proof_valid: 'El comprobante de pago ingresado es válido:'
|
||||||
|
payment_proof_error: 'El comprobante de pago ingresado no es válido:'
|
||||||
|
tx_delete_confirmation: ¿Seguro que quieres eliminar la transacción del historial?
|
||||||
|
transport:
|
||||||
|
desc: 'Usa el transporte para recibir o enviar mensajes de forma síncrona:'
|
||||||
|
tor_network: Red Tor
|
||||||
|
connected: Conectado
|
||||||
|
connecting: Conectando
|
||||||
|
disconnecting: Desconectando
|
||||||
|
conn_error: Error de conexión
|
||||||
|
disconnected: Desconectado
|
||||||
|
receiver_address: 'Dirección del destinatario:'
|
||||||
|
incorrect_addr_err: 'La dirección ingresada es incorrecta:'
|
||||||
|
tor_send_error: Ocurrió un error al enviar por Tor, asegúrate de que el destinatario esté en línea, la transacción fue cancelada.
|
||||||
|
tor_autorun_desc: Si se debe iniciar el servicio Tor al abrir la billetera para recibir transacciones de forma síncrona.
|
||||||
|
tor_sending: Enviando por Tor
|
||||||
|
tor_settings: Ajustes de Tor
|
||||||
|
bridges: Puentes
|
||||||
|
bridges_desc: Configura puentes para evitar la censura de la red Tor si la conexión habitual no funciona.
|
||||||
|
bin_file: 'Archivo binario:'
|
||||||
|
conn_line: 'Línea de conexión:'
|
||||||
|
bridges_disabled: Puentes desactivados
|
||||||
|
bridge_name: 'Puente %{b}'
|
||||||
|
network:
|
||||||
|
self: Red
|
||||||
|
type: 'Tipo de red:'
|
||||||
|
mainnet: Principal
|
||||||
|
testnet: Prueba
|
||||||
|
connections: Conexiones
|
||||||
|
node: Nodo integrado
|
||||||
|
metrics: Métricas
|
||||||
|
mining: Minería
|
||||||
|
settings: Ajustes del nodo
|
||||||
|
enable_node: Activar nodo
|
||||||
|
autorun: Inicio automático
|
||||||
|
disabled_server: 'Activa el nodo integrado o agrega otro método de conexión presionando %{dots} en la esquina superior izquierda de la pantalla.'
|
||||||
|
no_ips: No hay direcciones IP disponibles en tu sistema, el servidor no puede iniciarse, verifica tu conectividad de red.
|
||||||
|
available: Disponible
|
||||||
|
not_available: No disponible
|
||||||
|
availability_check: Verificación de disponibilidad
|
||||||
|
android_warning: Atención usuarios de Android. Para sincronizar el nodo integrado correctamente, debes permitir el acceso a las notificaciones y quitar las restricciones de uso de batería para la aplicación Goblin en los ajustes del sistema de tu teléfono. Esto es necesario para que la aplicación funcione correctamente en segundo plano.
|
||||||
|
sync_status:
|
||||||
|
node_restarting: El nodo se está reiniciando
|
||||||
|
node_down: El nodo está caído
|
||||||
|
initial: El nodo está iniciando
|
||||||
|
no_sync: El nodo está en ejecución
|
||||||
|
awaiting_peers: Esperando pares
|
||||||
|
header_sync: Descargando encabezados
|
||||||
|
header_sync_percent: 'Descargando encabezados: %{percent}%'
|
||||||
|
tx_hashset_pibd: Descargando estado (PIBD)
|
||||||
|
tx_hashset_pibd_percent: 'Descargando estado (PIBD): %{percent}%'
|
||||||
|
tx_hashset_download: Descargando estado
|
||||||
|
tx_hashset_download_percent: 'Descargando estado: %{percent}%'
|
||||||
|
tx_hashset_setup_history: 'Preparando estado (historial): %{percent}%'
|
||||||
|
tx_hashset_setup_position: 'Preparando estado (posición): %{percent}%'
|
||||||
|
tx_hashset_setup: Preparando estado
|
||||||
|
tx_hashset_range_proofs_validation: 'Validando estado (pruebas de rango): %{percent}%'
|
||||||
|
tx_hashset_kernels_validation: 'Validando estado (kernels): %{percent}%'
|
||||||
|
tx_hashset_save: Finalizando estado de la cadena
|
||||||
|
body_sync: Descargando bloques
|
||||||
|
body_sync_percent: 'Descargando bloques: %{percent}%'
|
||||||
|
shutdown: El nodo se está apagando
|
||||||
|
network_node:
|
||||||
|
header: Encabezado
|
||||||
|
block: Bloque
|
||||||
|
hash: Hash
|
||||||
|
height: Altura
|
||||||
|
difficulty: Dificultad
|
||||||
|
time: Tiempo
|
||||||
|
main_pool: Pool principal
|
||||||
|
stem_pool: Pool stem
|
||||||
|
data: Datos
|
||||||
|
size: Tamaño (GB)
|
||||||
|
peers: Pares
|
||||||
|
error_clean: Los datos del nodo se corrompieron, se requiere resincronización.
|
||||||
|
resync: Resincronizar
|
||||||
|
error_p2p_api: 'Ocurrió un error al inicializar el servidor %{p2p_api}, verifica los ajustes de %{p2p_api} seleccionando %{settings} en la parte inferior de la pantalla.'
|
||||||
|
error_config: 'Ocurrió un error al inicializar la configuración, verifica los ajustes seleccionando %{settings} en la parte inferior de la pantalla.'
|
||||||
|
error_unknown: 'Ocurrió un error durante la inicialización, verifica los ajustes del nodo integrado seleccionando %{settings} en la parte inferior de la pantalla o resincroniza.'
|
||||||
|
network_metrics:
|
||||||
|
loading: Las métricas estarán disponibles después de la sincronización
|
||||||
|
emission: Emisión
|
||||||
|
inflation: Inflación
|
||||||
|
supply: Oferta
|
||||||
|
block_time: Tiempo de bloque
|
||||||
|
reward: Recompensa
|
||||||
|
difficulty_window: 'Ventana de dificultad %{size}'
|
||||||
|
network_mining:
|
||||||
|
loading: La minería estará disponible después de la sincronización
|
||||||
|
info: 'El servidor de minería está activado, puedes cambiar sus ajustes seleccionando %{settings} en la parte inferior de la pantalla. Los datos se actualizan cuando hay dispositivos conectados.'
|
||||||
|
restart_server_required: Se requiere reiniciar el servidor para aplicar los cambios.
|
||||||
|
rewards_wallet: Billetera para recompensas
|
||||||
|
server: Servidor Stratum
|
||||||
|
address: Dirección
|
||||||
|
miners: Mineros
|
||||||
|
devices: Dispositivos
|
||||||
|
blocks_found: Bloques encontrados
|
||||||
|
hashrate: 'Hashrate (C%{bits})'
|
||||||
|
connected: Conectado
|
||||||
|
disconnected: Desconectado
|
||||||
|
network_settings:
|
||||||
|
change_value: Cambiar valor
|
||||||
|
stratum_ip: 'Dirección IP de Stratum:'
|
||||||
|
stratum_port: 'Puerto de Stratum:'
|
||||||
|
port_unavailable: El puerto especificado no está disponible
|
||||||
|
restart_node_required: Se requiere reiniciar el nodo para aplicar los cambios.
|
||||||
|
choose_wallet: Elegir billetera
|
||||||
|
stratum_wallet_warning: La billetera debe estar abierta para recibir recompensas.
|
||||||
|
enable: Activar
|
||||||
|
disable: Desactivar
|
||||||
|
restart: Reiniciar
|
||||||
|
server: Servidor
|
||||||
|
api_ip: 'Dirección IP de la API:'
|
||||||
|
api_port: 'Puerto de la API:'
|
||||||
|
api_secret: 'Token de la API REST y de la API de propietario V2:'
|
||||||
|
foreign_api_secret: 'Token de API externa:'
|
||||||
|
disabled: Desactivado
|
||||||
|
enabled: Activado
|
||||||
|
ftl: 'El límite de tiempo futuro (FTL):'
|
||||||
|
ftl_description: Límite de cuán adelantada al futuro, en segundos respecto a la hora local del nodo, puede estar la marca de tiempo de un nuevo bloque para que sea aceptado.
|
||||||
|
not_valid_value: El valor ingresado no es válido
|
||||||
|
full_validation: Validación completa
|
||||||
|
full_validation_description: Si se debe ejecutar una validación completa de la cadena al procesar cada bloque (excepto durante la sincronización).
|
||||||
|
archive_mode: Modo archivo
|
||||||
|
archive_mode_desc: Ejecuta el nodo en modo archivo completo (se requerirá más espacio en disco y tiempo para la sincronización).
|
||||||
|
attempt_time: 'Tiempo de intento de minería (en segundos):'
|
||||||
|
attempt_time_desc: El tiempo durante el cual se intenta minar sobre un encabezado en particular antes de detenerse y volver a recolectar transacciones del pool
|
||||||
|
min_share_diff: 'La dificultad mínima aceptable del share:'
|
||||||
|
reset_settings_desc: Restablece los ajustes del nodo a los valores predeterminados
|
||||||
|
reset_settings: Restablecer ajustes
|
||||||
|
reset: Restablecer
|
||||||
|
tx_pool: Pool de transacciones
|
||||||
|
pool_fee: 'Comisión base aceptada en el pool:'
|
||||||
|
reorg_period: 'Período de retención de la caché de reorganización (en minutos):'
|
||||||
|
max_tx_pool: 'Cantidad máxima de transacciones en el pool:'
|
||||||
|
max_tx_stempool: 'Cantidad máxima de transacciones en el stem-pool:'
|
||||||
|
max_tx_weight: 'Peso total máximo de las transacciones que pueden seleccionarse para construir un bloque:'
|
||||||
|
epoch_duration: 'Duración de la época (en segundos):'
|
||||||
|
embargo_timer: 'Temporizador de embargo (en segundos):'
|
||||||
|
aggregation_period: 'Período de agregación (en segundos):'
|
||||||
|
stem_probability: 'Probabilidad de la fase stem:'
|
||||||
|
stem_txs: Transacciones stem
|
||||||
|
p2p_server: Servidor P2P
|
||||||
|
p2p_port: 'Puerto P2P:'
|
||||||
|
add_seed: Agregar semilla DNS
|
||||||
|
seed_address: 'Dirección de semilla DNS:'
|
||||||
|
add_peer: Agregar par
|
||||||
|
peer_address: 'Dirección del par:'
|
||||||
|
peer_address_error: 'Ingresa la dirección IP o el nombre DNS (asegúrate de que el host especificado esté disponible) en el formato correcto, por ejemplo: 192.168.0.1:1234 o example.com:5678'
|
||||||
|
default: Predeterminado
|
||||||
|
allow_list: Lista de permitidos
|
||||||
|
allow_list_desc: Conéctate solo a los pares de esta lista.
|
||||||
|
deny_list: Lista de bloqueados
|
||||||
|
deny_list_desc: Nunca te conectes a los pares de esta lista.
|
||||||
|
favourites: Favoritos
|
||||||
|
favourites_desc: Una lista de pares preferidos a los que conectarse.
|
||||||
|
ban_window: 'Cuánto tiempo (en segundos) debe permanecer baneado un par baneado:'
|
||||||
|
ban_window_desc: La decisión de banear la toma el nodo, según la corrección de los datos recibidos del par.
|
||||||
|
max_inbound_count: 'Cantidad máxima de conexiones entrantes de pares:'
|
||||||
|
max_outbound_count: 'Cantidad máxima de conexiones salientes de pares:'
|
||||||
|
reset_data_desc: Restablece los datos del nodo. Úsalo con precaución, solo si hay problemas de sincronización.
|
||||||
|
reset_data: Restablecer datos
|
||||||
|
modal:
|
||||||
|
cancel: Cancelar
|
||||||
|
save: Guardar
|
||||||
|
add: Agregar
|
||||||
|
modal_exit:
|
||||||
|
description: ¿Seguro que quieres salir de la aplicación?
|
||||||
|
exit: Salir
|
||||||
|
app_settings:
|
||||||
|
proxy: Proxy
|
||||||
|
proxy_desc: Si se debe usar un proxy para las solicitudes de red de la aplicación.
|
||||||
|
keyboard:
|
||||||
|
1: 1
|
||||||
|
2: 2
|
||||||
|
3: 3
|
||||||
|
4: 4
|
||||||
|
5: 5
|
||||||
|
6: 6
|
||||||
|
7: 7
|
||||||
|
8: 8
|
||||||
|
9: 9
|
||||||
|
0: 0
|
||||||
|
01: '-'
|
||||||
|
q: q
|
||||||
|
w: w
|
||||||
|
e: e
|
||||||
|
r: r
|
||||||
|
t: t
|
||||||
|
y: y
|
||||||
|
u: u
|
||||||
|
i: i
|
||||||
|
o: o
|
||||||
|
p: p
|
||||||
|
p1: '"'
|
||||||
|
a: a
|
||||||
|
s: s
|
||||||
|
d: d
|
||||||
|
f: f
|
||||||
|
g: g
|
||||||
|
h: h
|
||||||
|
j: j
|
||||||
|
k: k
|
||||||
|
l: l
|
||||||
|
l1: ñ
|
||||||
|
l2: ':'
|
||||||
|
z: z
|
||||||
|
x: x
|
||||||
|
c: c
|
||||||
|
v: v
|
||||||
|
b: b
|
||||||
|
n: n
|
||||||
|
m: m
|
||||||
|
m1: ','
|
||||||
|
m2: .
|
||||||
|
m3: /
|
||||||
|
goblin:
|
||||||
|
home:
|
||||||
|
anonymous: "Anónimo"
|
||||||
|
connected_tor: "Conectado por Tor"
|
||||||
|
tor_ready: "Tor listo · relés…"
|
||||||
|
connecting_tor: "Conectando a Tor…"
|
||||||
|
connected_direct: "Conectado (directo)"
|
||||||
|
connecting_direct: "Conectando (directo)…"
|
||||||
|
cant_reach_node: "No se puede conectar con el nodo"
|
||||||
|
node_synced: "Nodo sincronizado"
|
||||||
|
syncing: "Sincronizando…"
|
||||||
|
balance_updating: "Actualizando saldo…"
|
||||||
|
balance_stale: "No se puede conectar con el nodo · último saldo conocido"
|
||||||
|
fiat_unavailable: "Tasa no disponible"
|
||||||
|
listening: "Esperando pagos"
|
||||||
|
block: "Bloque %{height}"
|
||||||
|
waiting_for_chain: "Esperando la cadena…"
|
||||||
|
nav_wallet: "Billetera"
|
||||||
|
nav_pay: "Pagar"
|
||||||
|
nav_activity: "Actividad"
|
||||||
|
nav_receive: "Recibir"
|
||||||
|
nav_settings: "Ajustes"
|
||||||
|
back_again: "Presiona atrás de nuevo para cambiar de billetera"
|
||||||
|
activity: "Actividad"
|
||||||
|
news: "Novedades"
|
||||||
|
empty_title: "Aún no hay actividad"
|
||||||
|
empty_sub: "Envía o recibe grin para comenzar."
|
||||||
|
recent: "Reciente"
|
||||||
|
scan_to_pay: "Escanear para pagar"
|
||||||
|
type_amount: "Escribe un monto"
|
||||||
|
request: "Solicitar"
|
||||||
|
pay: "Pagar"
|
||||||
|
enter_amount: "Ingresa un monto para pagar o solicitar"
|
||||||
|
activity:
|
||||||
|
canceled: "cancelado"
|
||||||
|
pending: "pendiente"
|
||||||
|
earlier: "Antes"
|
||||||
|
today: "Hoy"
|
||||||
|
yesterday: "Ayer"
|
||||||
|
title: "Actividad"
|
||||||
|
requests: "Solicitudes"
|
||||||
|
empty_title: "Aún no hay actividad"
|
||||||
|
empty_sub: "Tus pagos aparecerán aquí."
|
||||||
|
pending_header: "Pendiente"
|
||||||
|
receipt:
|
||||||
|
title: "Recibo"
|
||||||
|
not_found: "Transacción no encontrada"
|
||||||
|
for_note: "Por %{note}"
|
||||||
|
details: "Detalles de la transacción"
|
||||||
|
canceled: "Cancelado"
|
||||||
|
expired: "Expirado"
|
||||||
|
funds_returned: "Fondos devueltos"
|
||||||
|
complete: "Completo"
|
||||||
|
payment_received: "Pago recibido"
|
||||||
|
payment_sent: "Pago enviado con éxito"
|
||||||
|
pending: "Pendiente"
|
||||||
|
confs: "%{c}/%{r} confirmaciones"
|
||||||
|
waiting_to_confirm: "Esperando confirmación"
|
||||||
|
paying: "Pagando…"
|
||||||
|
you: "Tú"
|
||||||
|
to: "Para"
|
||||||
|
from: "De"
|
||||||
|
nostr: "nostr"
|
||||||
|
identity: "Identidad"
|
||||||
|
fee_none: "Ninguna"
|
||||||
|
network_fee: "Comisión de red"
|
||||||
|
privacy: "Privacidad"
|
||||||
|
privacy_value: "Mimblewimble + Tor"
|
||||||
|
transaction: "Transacción"
|
||||||
|
cancel_request: "Cancelar solicitud"
|
||||||
|
cancel_send: "Cancelar pago"
|
||||||
|
stale_note: "Este pago lleva pendiente un tiempo."
|
||||||
|
cancel_send_confirm: "Toca de nuevo para cancelar — igual podrían recibirlo"
|
||||||
|
cancel_send_done: "Pago cancelado — tus fondos están disponibles de nuevo"
|
||||||
|
cancel_send_too_late: "Este pago ya se completó y no se puede cancelar"
|
||||||
|
waiting_to_receive: "Esperando a que %{name} lo reciba…"
|
||||||
|
request:
|
||||||
|
title: "Solicitudes de %{name}"
|
||||||
|
approve: "Aprobar"
|
||||||
|
decline: "Rechazar"
|
||||||
|
review_title: "Revisar solicitud"
|
||||||
|
hold_to_accept: "Mantén presionado para aceptar"
|
||||||
|
hold_accept_hint: "Mantén presionado para pagar esta solicitud"
|
||||||
|
receive:
|
||||||
|
title: "Recibir"
|
||||||
|
requesting: "Solicitando %{amt}%{tsu} — comparte para que te paguen"
|
||||||
|
clear_request: "Borrar solicitud"
|
||||||
|
share_handle: "Comparte tu handle para que te paguen"
|
||||||
|
share_npub: "Comparte tu npub para que te paguen"
|
||||||
|
copied: "Copiado"
|
||||||
|
copy_nostr_id: "Copiar ID de nostr"
|
||||||
|
copy_address: "Copiar dirección"
|
||||||
|
copy: "Copiar"
|
||||||
|
share_message: "Págame en Goblin (goblin.st) — %{npub}"
|
||||||
|
privacy_note: "Tu nombre de usuario es público. El contenido de los pagos permanece cifrado en la red."
|
||||||
|
privacy_note_npub: "Tu npub es público. El contenido de los pagos permanece cifrado en la red."
|
||||||
|
profile:
|
||||||
|
title: "Perfil"
|
||||||
|
activity: "Actividad"
|
||||||
|
no_activity: "Aún no hay actividad con esta persona."
|
||||||
|
unblock: "Desbloquear"
|
||||||
|
block: "Bloquear"
|
||||||
|
blocked_blurb: "Bloqueado — sus pagos y solicitudes se descartan."
|
||||||
|
block_blurb: "Bloquear descarta sus pagos y solicitudes entrantes."
|
||||||
|
settings:
|
||||||
|
title: "Ajustes"
|
||||||
|
connected_nostr: "Conectado a nostr"
|
||||||
|
connecting_relays: "Conectando a relés…"
|
||||||
|
identity: "Identidad"
|
||||||
|
copy_npub: "Copiar npub (público)"
|
||||||
|
backup_note: "¿Cambiando de dispositivo? Respalda AMBOS: tu frase de recuperación (fondos) y tu archivo .backup de identidad (nombre + clave)."
|
||||||
|
wallet: "Billetera"
|
||||||
|
display_unit: "Unidad de visualización"
|
||||||
|
relays: "Relés"
|
||||||
|
nostr_relays: "Relés de nostr"
|
||||||
|
node: "Nodo"
|
||||||
|
min_conf: "Confirmaciones mínimas"
|
||||||
|
integrated_node: "Ajustes del nodo integrado"
|
||||||
|
node_advanced: "Avanzado"
|
||||||
|
slatepacks: "Slatepacks"
|
||||||
|
slatepacks_value: "Transacción manual"
|
||||||
|
trusted_sites: "Sitios de confianza"
|
||||||
|
lock_wallet: "Bloquear billetera"
|
||||||
|
switch_wallet: "Cambiar de billetera"
|
||||||
|
advanced: "Avanzado"
|
||||||
|
privacy: "Privacidad"
|
||||||
|
tor_routing: "Enrutamiento por Tor"
|
||||||
|
messages_lookups: "Mensajes y consultas"
|
||||||
|
tor_on: "Activado"
|
||||||
|
tor_off: "Desactivado"
|
||||||
|
auto_accept: "Aceptación automática"
|
||||||
|
pairing: "Moneda de referencia"
|
||||||
|
accept_anyone: "Cualquiera"
|
||||||
|
accept_contacts: "Solo contactos"
|
||||||
|
accept_ask: "Preguntar siempre"
|
||||||
|
requests: "Solicitudes"
|
||||||
|
incoming_requests: "Solicitudes entrantes"
|
||||||
|
incoming_requests_sub: "Permite que otros te soliciten dinero"
|
||||||
|
hide_amounts: "Ocultar montos"
|
||||||
|
hide_amounts_sub: "Ocultar los montos recibidos en las notificaciones"
|
||||||
|
language: "Idioma"
|
||||||
|
update_available: "Actualización disponible"
|
||||||
|
appearance: "Apariencia"
|
||||||
|
theme: "Tema"
|
||||||
|
theme_light: "Claro"
|
||||||
|
theme_dark: "Oscuro"
|
||||||
|
theme_yellow: "Amarillo"
|
||||||
|
archive: "Archivo"
|
||||||
|
export_archive: "Exportar archivo"
|
||||||
|
export_archive_caption: "Copia tus contactos, historial de pagos y solicitudes al portapapeles en formato JSON."
|
||||||
|
wipe_history: "Borrar historial de pagos"
|
||||||
|
wipe_history_confirm: "Toca de nuevo para borrar — esto no se puede deshacer"
|
||||||
|
about: "Acerca de"
|
||||||
|
goblin: "Goblin"
|
||||||
|
build: "Build %{build}"
|
||||||
|
network: "Red"
|
||||||
|
network_value: "MW + Tor + nostr"
|
||||||
|
third_party: "Terceros"
|
||||||
|
grim: "GRIM (billetera original)"
|
||||||
|
grin_node: "Nodo de Grin"
|
||||||
|
sp_intro: "Avanzado — intercambia slatepacks en bruto a mano, como lo hace GRIM. Úsalo solo cuando no puedas pagar o recibir pagos mediante un nombre de usuario."
|
||||||
|
sp_receive_group: "Recibir o finalizar"
|
||||||
|
sp_receive_blurb: "Pega un slatepack que alguien te dio. Goblin recibe el pago, paga la factura, o lo finaliza y lo publica."
|
||||||
|
sp_process: "Procesar slatepack"
|
||||||
|
sp_paste_first: "Pega primero un slatepack."
|
||||||
|
sp_reply_ready: "Respuesta lista — envíasela de vuelta al remitente."
|
||||||
|
sp_finalizing: "Finalizando y publicando en la cadena…"
|
||||||
|
sp_create_group: "Crear un pago"
|
||||||
|
sp_create_blurb: "Crea un slatepack para entregárselo a alguien. Lo recibe, te envía la respuesta de vuelta, y tú lo finalizas arriba."
|
||||||
|
sp_amount_hint: "Monto en grin"
|
||||||
|
sp_addr_hint: "Dirección del destinatario (opcional)"
|
||||||
|
sp_create: "Crear slatepack"
|
||||||
|
sp_ready: "Slatepack listo — entrégaselo al destinatario."
|
||||||
|
sp_amount_gt_zero: "Ingresa un monto mayor que cero."
|
||||||
|
sp_to_send: "Slatepack para enviar"
|
||||||
|
sp_copy: "Copiar slatepack"
|
||||||
|
cancel: "Cancelar"
|
||||||
|
continue: "Continuar"
|
||||||
|
final_confirmation: "Confirmación final"
|
||||||
|
type_reset: "Escribe RESET"
|
||||||
|
wallet_password: "Contraseña de la billetera"
|
||||||
|
done: "Listo"
|
||||||
|
close: "Cerrar"
|
||||||
|
import_nsec_hint: "nsec1… o respaldo pegado"
|
||||||
|
backup_password_hint: "Contraseña del respaldo (solo si se exportó en otro lugar)"
|
||||||
|
import_btn: "Importar"
|
||||||
|
importing: "Importando…"
|
||||||
|
import_failed: "Error al importar"
|
||||||
|
name_authority: "Autoridad de nombres"
|
||||||
|
name_authority_title: "Cambiar autoridad de nombres"
|
||||||
|
name_authority_blurb: "El servidor que registra y verifica los nombres. Apúntalo a otra instancia para usar y pagar nombres alojados allí."
|
||||||
|
name_authority_invalid: "Ingresa una URL completa (https://…)."
|
||||||
|
reset: "Restablecer"
|
||||||
|
save: "Guardar"
|
||||||
|
backup_file: "Respaldar en un archivo"
|
||||||
|
choose_backup_file: "Elegir un archivo .backup"
|
||||||
|
backup_read_failed: "No se pudo leer ese archivo."
|
||||||
|
backup_saved: "Respaldo guardado"
|
||||||
|
backup_saved_sub: "Mantén el archivo .backup a salvo. Cualquiera que lo tenga JUNTO CON tu contraseña puede restaurar toda tu billetera."
|
||||||
|
backup_file_title: "Respaldar billetera"
|
||||||
|
backup_file_blurb: "Crea un archivo .backup cifrado con la semilla de tu billetera y todas tus identidades. Ingresa tu contraseña de billetera para sellarlo."
|
||||||
|
backup_write_failed: "No se pudo guardar el archivo."
|
||||||
|
create_backup: "Crear respaldo"
|
||||||
|
registered: "%{name} registrado"
|
||||||
|
released_msg: "Liberado — el nombre está disponible"
|
||||||
|
release_confirm: "¿Liberar %{name}?"
|
||||||
|
release_blurb: "Queda disponible en cuanto se libera — cualquiera puede reclamarlo, incluida la próxima clave a la que rotes. No podrás registrar otro nombre de usuario durante 10 minutos."
|
||||||
|
releasing: "Liberando…"
|
||||||
|
keep_it: "Conservarlo"
|
||||||
|
release_it: "Liberarlo"
|
||||||
|
username: "Nombre de usuario"
|
||||||
|
username_note: "Se muestra como tu nombre. Público en goblin.st. Los pagos permanecen cifrados."
|
||||||
|
release_username: "Liberar nombre de usuario"
|
||||||
|
pick_username: "Elige un nombre de usuario — opcional"
|
||||||
|
working: "Procesando…"
|
||||||
|
claim: "Reclamar"
|
||||||
|
err_just_taken: "Ese nombre de usuario acaba de ser tomado"
|
||||||
|
err_cooldown: "Liberaste recientemente un nombre de usuario — podrás registrar uno nuevo dentro de 10 minutos."
|
||||||
|
err_unreachable: "No se pudo conectar con goblin.st — problema de conexión. Inténtalo de nuevo."
|
||||||
|
err_release: "No se pudo liberar: %{err}"
|
||||||
|
avail_available: "¡Disponible!"
|
||||||
|
avail_taken: "Ocupado"
|
||||||
|
avail_reserved: "Reservado"
|
||||||
|
avail_invalid: "Los nombres tienen entre 3 y 20 caracteres: a–z, 0–9, _ o -"
|
||||||
|
avail_quarantined: "No disponible"
|
||||||
|
avail_unknown: "No se pudo verificar — problema de conexión. Inténtalo de nuevo."
|
||||||
|
username_none: "No establecido"
|
||||||
|
advanced_privacy: "Privacidad avanzada"
|
||||||
|
tap_reveal: "Toca para revelar"
|
||||||
|
notif_someone: "Alguien"
|
||||||
|
notif_private_received: "Te pagaron. Abre Goblin para ver los detalles."
|
||||||
|
notif_private_requested: "Nueva solicitud de pago. Abre Goblin para ver los detalles."
|
||||||
|
username:
|
||||||
|
title: "Nombre de usuario"
|
||||||
|
authority: "Autoridad de nombres"
|
||||||
|
authority_blurb: "Déjala por defecto o apunta a otra instancia para usar los nombres alojados allí."
|
||||||
|
learn_more: "Más información"
|
||||||
|
custom: "Autoridad personalizada"
|
||||||
|
advprivacy:
|
||||||
|
intro: "Controla lo que revelan tus notificaciones, y oculta tu saldo e historial en este dispositivo."
|
||||||
|
notifications: "Notificaciones"
|
||||||
|
hide_names: "Ocultar nombres"
|
||||||
|
hide_names_sub: "Omite quién pagó en las notificaciones"
|
||||||
|
hide_details: "Ocultar todos los detalles"
|
||||||
|
hide_details_sub: "Muestra solo una alerta privada, sin nombre ni monto"
|
||||||
|
anon: "Modo anónimo"
|
||||||
|
anon_toggle: "Difuminar saldo y actividad"
|
||||||
|
anon_sub: "Tu saldo y actividad se muestran como puntos hasta que toques para revelarlos"
|
||||||
|
advanced:
|
||||||
|
title: "Avanzado"
|
||||||
|
intro: "Herramientas de billetera de bajo nivel de GRIM. Normalmente no las necesitarás."
|
||||||
|
own_node_desc: "Sincroniza un nodo Grin completo en este dispositivo en lugar de confiar en uno público."
|
||||||
|
own_node_active: "Ejecutando tu propio nodo"
|
||||||
|
repair: "Reparar billetera"
|
||||||
|
repair_desc: "Vuelve a escanear la cadena y restaura las salidas faltantes. Esto puede tardar un rato."
|
||||||
|
repair_unavailable: "Primero necesitas una conexión sincronizada al nodo."
|
||||||
|
repairing: "Reparando… %{pct}%"
|
||||||
|
restore: "Restaurar billetera"
|
||||||
|
restore_desc: "Elimina los datos locales y reconstrúyelos desde tu frase de recuperación. Usa esto si una reparación no ayudó — volverás a abrir la billetera después."
|
||||||
|
restore_confirm: "Toca de nuevo para restaurar"
|
||||||
|
show_phrase: "Frase de recuperación"
|
||||||
|
phrase_desc: "Tus 24 palabras semilla de grin — la única forma de recuperar los fondos. Mantenlas fuera de línea y en privado."
|
||||||
|
reveal: "Mostrar frase"
|
||||||
|
hide: "Ocultar"
|
||||||
|
password: "Contraseña de la billetera"
|
||||||
|
wrong_password: "Contraseña incorrecta."
|
||||||
|
delete: "Eliminar billetera"
|
||||||
|
delete_desc: "Elimina permanentemente esta billetera y todas sus identidades de este dispositivo. Haz una copia de seguridad primero. Sin tu frase de recuperación, los fondos no se pueden recuperar."
|
||||||
|
delete_confirm: "Toca de nuevo para eliminar"
|
||||||
|
manage_node: "Administrar conexión del nodo"
|
||||||
|
repair_confirm: "Sí, reparar ahora"
|
||||||
|
repair_confirm_note: "La reparación vuelve a escanear la cadena y puede tardar unos minutos."
|
||||||
|
restore_confirm_note: "Esto borra los datos locales y los reconstruye desde tu frase de recuperación — puede tardar varios minutos."
|
||||||
|
nostr_key: "Clave nostr"
|
||||||
|
nostr_key_desc: "Tu nsec, la clave secreta de tu identidad nostr. Cópiala o muestra su código QR para iniciar sesión en apps de nostr como magick.market. Quien la tenga controla tu identidad, así que mantenla en privado."
|
||||||
|
reveal_nsec: "Mostrar clave"
|
||||||
|
copy_nsec: "Copiar nsec"
|
||||||
|
show_qr: "Mostrar QR"
|
||||||
|
hide_qr: "Ocultar QR"
|
||||||
|
nostr_section: "Ajustes avanzados de nostr"
|
||||||
|
backup_caption: "Contiene tu billetera y todas tus identidades."
|
||||||
|
danger_zone: "Zona de peligro"
|
||||||
|
delete_warning: "Esto borra tu frase de recuperación y todas las identidades de este dispositivo. Todo lo que no hayas respaldado se perderá para siempre."
|
||||||
|
download_backup: "Descargar copia de seguridad"
|
||||||
|
delete_final: "Eliminar permanentemente"
|
||||||
|
privacy:
|
||||||
|
title: "Privacidad de red"
|
||||||
|
intro: "Goblin envía solo unas pocas cosas a través de la red, cada una sellada con cifrado de extremo a extremo para que los relés no puedan leerlas ni vincularlas contigo."
|
||||||
|
payments: "Pagos"
|
||||||
|
payments_blurb: "Cada mensaje de nostr que transporta un slatepack."
|
||||||
|
usernames: "Nombres de usuario"
|
||||||
|
usernames_blurb: "Búsquedas de nombres NIP-05 hacia y desde autoridades de nombres."
|
||||||
|
price_avatars: "Precio"
|
||||||
|
price_avatars_blurb: "La tasa de cambio en vivo que se muestra junto a los montos."
|
||||||
|
grin_node: "Nodo de Grin"
|
||||||
|
grin_node_blurb: "Sincroniza bloques y transmite tus transacciones. Datos públicos de la cadena, iguales para todos, nunca vinculados a tu identidad."
|
||||||
|
how_privacy: "Cómo funciona tu privacidad"
|
||||||
|
how_privacy_blurb: "Los pagos y mensajes se sellan con cifrado autenticado y se entregan como mensajes privados, de modo que un relé no puede leerlos ni vincular un pago contigo."
|
||||||
|
always_direct: "Siempre directo"
|
||||||
|
tor_off: "Tor desactivado"
|
||||||
|
tor_on: "Tor activado"
|
||||||
|
tor_switch: "Enrutar a través de Tor"
|
||||||
|
tor_switch_sub: "Oculta tu IP de los relés."
|
||||||
|
vpn_note: "Con Tor desactivado, los relés pueden ver tu dirección IP. Te recomendamos usar al menos una VPN."
|
||||||
|
pairing:
|
||||||
|
title: "Referencia de precio"
|
||||||
|
intro: "Aquello contra lo que se muestran tu saldo y tus montos."
|
||||||
|
pair_with: "Vincular con"
|
||||||
|
rates_note: "Las tasas se obtienen por Tor, solo mientras haya una referencia activa — desactivado significa que ninguna solicitud de tasa sale de tu dispositivo."
|
||||||
|
relays:
|
||||||
|
title: "Relés"
|
||||||
|
intro: "Los mensajes de pago se replican en cada relé de abajo; basta con que uno sea alcanzable para recibir."
|
||||||
|
your_relays: "Tus relés"
|
||||||
|
add_relay: "Agregar relé"
|
||||||
|
add_relay_btn: "Agregar relé"
|
||||||
|
save_reconnect: "Guardar y reconectar"
|
||||||
|
none: "ninguno"
|
||||||
|
count: "%{n} relés"
|
||||||
|
node:
|
||||||
|
title: "Nodo"
|
||||||
|
connection: "Conexión"
|
||||||
|
integrated: "Nodo integrado"
|
||||||
|
applies_after: "Se aplica después de bloquear y desbloquear la billetera de nuevo."
|
||||||
|
add_external: "Agregar nodo externo"
|
||||||
|
api_secret_hint: "Secreto de API (opcional)"
|
||||||
|
add_node: "Agregar nodo"
|
||||||
|
integrated_host: "nodo integrado"
|
||||||
|
summary_syncing: "%{conn} · sincronizando"
|
||||||
|
summary_block: "Bloque %{height} · %{conn}"
|
||||||
|
nips:
|
||||||
|
title: "nostr y NIPs"
|
||||||
|
intro1: "Goblin habla nostr — un protocolo abierto de mensajes firmados que se transmiten a través de servidores relé simples. Tu billetera lleva su propia identidad nostr: una clave aleatoria independiente, mantenida deliberadamente separada de tus fondos y tu frase de recuperación. Cada pago viaja como un mensaje directo cifrado de extremo a extremo entre identidades, con el slatepack dentro."
|
||||||
|
intro2: "goblin.st es el servicio de nombres de Goblin: reclamar un nombre de usuario publica allí una asignación de nombre → clave (NIP-05), para que la gente pueda pagarte a ti en lugar de a un npub largo. El nombre de usuario es público; el contenido de los pagos nunca lo es. Los NIPs son los componentes del protocolo — toca uno para leer la especificación."
|
||||||
|
n05_title: "Nombres"
|
||||||
|
n05_blurb: "Asocia usuario@goblin.st con tu clave, de modo que los handles funcionen como direcciones."
|
||||||
|
n17_title: "Mensajes privados"
|
||||||
|
n17_blurb: "El sobre de mensaje directo cifrado en el que viaja cada pago."
|
||||||
|
n44_title: "Cifrado"
|
||||||
|
n44_blurb: "El cifrado autenticado usado dentro de esos mensajes."
|
||||||
|
n49_title: "Cifrado de clave"
|
||||||
|
n49_blurb: "Cómo se almacena la clave secreta en reposo, bloqueada por tu contraseña."
|
||||||
|
n59_title: "Envoltura de regalo"
|
||||||
|
n59_blurb: "Envuelve los mensajes para que los relés no puedan ver quién habla con quién."
|
||||||
|
n98_title: "Autenticación HTTP"
|
||||||
|
n98_blurb: "Firma la solicitud de registro de nombre de usuario ante goblin.st."
|
||||||
|
batch:
|
||||||
|
title: "Aprobar %{n} facturas"
|
||||||
|
blurb: "Emite %{n} solicitudes de pago de %{amount} cada una a %{name} — %{total} en total. Cada solicitud recibe su propia dirección de pago nueva."
|
||||||
|
approve: "Aprobar"
|
||||||
|
login:
|
||||||
|
title: "Iniciar sesión"
|
||||||
|
headline: "¿Iniciar sesión en %{domain}?"
|
||||||
|
identity: "Iniciando sesión como"
|
||||||
|
explain: "Esto firma una solicitud de inicio de sesión de un solo uso. No comparte tu clave ni permite que el sitio actúe en tu nombre."
|
||||||
|
pass_prompt: "Ingresa tu contraseña de billetera para iniciar sesión."
|
||||||
|
confirm: "Iniciar sesión"
|
||||||
|
sent: "Sesión iniciada en %{domain}"
|
||||||
|
failed: "No se pudo conectar con %{domain}. El inicio de sesión no se entregó."
|
||||||
|
authorize:
|
||||||
|
title: "Autorizar"
|
||||||
|
headline: "¿Autorizar %{domain}?"
|
||||||
|
kind_post: "Publicación pública"
|
||||||
|
kind_repost: "Republicación"
|
||||||
|
kind_reaction: "Reacción"
|
||||||
|
kind_article: "Artículo (formato largo)"
|
||||||
|
kind_unknown: "Tipo %{n} (no reconocido)"
|
||||||
|
unknown_caution: "Goblin no reconoce este tipo de evento. Aprueba solo si confías en %{domain}."
|
||||||
|
identity: "Firmando como"
|
||||||
|
explain: "Al aprobar se firma este único evento y se entrega a %{domain}. Goblin no publica nada, no se comparte ninguna clave, y el sitio no obtiene sesión ni capacidad futura."
|
||||||
|
pass_prompt: "Ingresa tu contraseña de billetera para autorizar."
|
||||||
|
confirm: "Autorizar"
|
||||||
|
sent: "%{domain} autorizado"
|
||||||
|
failed: "No se pudo conectar con %{domain}. No se entregó nada."
|
||||||
|
truncated: "truncado, %{n} caracteres más"
|
||||||
|
show_full: "Mostrar todo"
|
||||||
|
show_less: "Mostrar menos"
|
||||||
|
replying_to: "respondiendo a %{id}"
|
||||||
|
mentions: "menciona a %{id}"
|
||||||
|
repost_of: "republica %{id}"
|
||||||
|
by_author: "por %{id}"
|
||||||
|
reacts_to: "reacciona a %{id}"
|
||||||
|
article_title: "Título: %{title}"
|
||||||
|
trust:
|
||||||
|
title: "Confianza"
|
||||||
|
headline: "¿Confiar en %{domain}?"
|
||||||
|
identity: "Iniciando sesión como"
|
||||||
|
lead: "%{domain} puede actuar por ti durante esta sesión sin preguntar cada vez. Todo lo que gasta dinero siempre pregunta."
|
||||||
|
grant_intro: "Puede firmar sin preguntar:"
|
||||||
|
cat_social: "Publicaciones y reacciones"
|
||||||
|
cat_dm: "Mensajes directos"
|
||||||
|
cat_market: "Anuncios"
|
||||||
|
cat_identity: "Perfil y listas"
|
||||||
|
cat_delete: "Eliminaciones"
|
||||||
|
cat_http: "Subidas y autenticación HTTP"
|
||||||
|
cat_unknown: "Otro tipo de evento (tipo %{n})"
|
||||||
|
login_excluded: "El inicio de sesión nunca se otorga a un sitio."
|
||||||
|
money_line: "El dinero siempre pide tu contraseña. Nada se gasta ni se vende en silencio."
|
||||||
|
duration: "Dura solo esta sesión. Termínala cuando quieras en Ajustes, Sitios de confianza."
|
||||||
|
pass_prompt: "Ingresa tu contraseña de billetera para confiar en este sitio."
|
||||||
|
confirm_hold: "Mantén presionado para confiar durante esta sesión"
|
||||||
|
sent: "Confiando en %{domain} durante esta sesión"
|
||||||
|
failed: "No se pudo conectar con %{domain}. La sesión no se inició."
|
||||||
|
announce_failed: "Confianza otorgada, pero %{domain} aún no confirmó la sesión."
|
||||||
|
notice_volume: "Un sitio de confianza está firmando mucho."
|
||||||
|
notice_decrypt: "Un sitio de confianza está leyendo tus mensajes."
|
||||||
|
money:
|
||||||
|
title: "Confirmar"
|
||||||
|
headline: "¿Confirmar en %{domain}?"
|
||||||
|
identity: "Firmando como %{id}"
|
||||||
|
explain: "Esto mueve o compromete valor, así que siempre pregunta. Aprobar firma esta única acción."
|
||||||
|
pass_prompt: "Ingresa tu contraseña de billetera para confirmar."
|
||||||
|
confirm_hold: "Mantén presionado para confirmar"
|
||||||
|
encrypt_desc: "Un mensaje de pedido cifrado que compromete un pago."
|
||||||
|
trusted_sites:
|
||||||
|
title: "Sitios de confianza"
|
||||||
|
intro: "Los sitios en los que confiaste esta sesión pueden firmar acciones de bajo riesgo sin preguntar. El dinero siempre pregunta. Termina cualquier sesión aquí."
|
||||||
|
empty: "No hay sitios de confianza. Confía en un sitio al iniciar sesión para darle una sesión de firma."
|
||||||
|
none: "nada"
|
||||||
|
can_sign: "Puede firmar en silencio: %{cats}"
|
||||||
|
time_left: "Termina en %{mins} min"
|
||||||
|
paused: "En pausa, firmando mucho"
|
||||||
|
resume: "Reanudar"
|
||||||
|
end: "Terminar sesión"
|
||||||
|
identities:
|
||||||
|
title: "Identidades"
|
||||||
|
switch_hint: "Cambiar de identidad"
|
||||||
|
blurb: "Tus identidades son los nombres bajo los que te pagan. Todas pagan a esta misma billetera. Agrega una nueva, cambia entre ellas, o elimina la que no necesites."
|
||||||
|
privacy_note: "Estas identidades comparten una billetera y un saldo — puertas de entrada separadas, no fondos separados. Para dinero verdaderamente separado, usa una billetera aparte."
|
||||||
|
held: "Identidades guardadas"
|
||||||
|
add: "Agregar identidad"
|
||||||
|
add_title: "Agregar una identidad"
|
||||||
|
nsec_hint: "Pega un nsec"
|
||||||
|
generate_note: "Se creará una clave nueva y anónima para esta billetera."
|
||||||
|
generate: "Generar"
|
||||||
|
import: "Importar"
|
||||||
|
choose_backup: "Elegir un archivo .backup"
|
||||||
|
backup_selected: "Archivo de respaldo seleccionado"
|
||||||
|
delete_short: "Eliminar identidad"
|
||||||
|
delete_title: "¿Eliminar %{name}?"
|
||||||
|
delete_blurb: "Esto elimina permanentemente la identidad de esta billetera. El dinero ya recibido permanece en tu saldo."
|
||||||
|
delete_backup_note: "Respáldala primero — sin su nsec o su archivo .backup no se puede recuperar."
|
||||||
|
delete_confirm: "Eliminar"
|
||||||
|
manage_title: "Administrar identidad"
|
||||||
|
tag_note: "Dale un nombre privado. Solo tú lo ves — nunca se comparte."
|
||||||
|
tag_hint: "Nombre privado"
|
||||||
|
tag_save: "Guardar"
|
||||||
|
pass_prompt: "Tu contraseña de billetera cifra y desbloquea cada identidad. Ingrésala para continuar."
|
||||||
|
working: "Procesando…"
|
||||||
|
onboarding:
|
||||||
|
privacy:
|
||||||
|
kicker: "PRIVACIDAD DE RED"
|
||||||
|
continue: "Continuar"
|
||||||
|
intro:
|
||||||
|
private_money_head: "Dinero privado"
|
||||||
|
private_money_body: "Goblin es una billetera para grin — dinero digital sin montos ni direcciones en su cadena."
|
||||||
|
send_like_message_head: "Envía como un mensaje"
|
||||||
|
send_like_message_body: "Paga a un nombre de usuario o npub y llega como un mensaje cifrado de extremo a extremo por nostr y Tor — nadie en el medio puede ver el monto ni quién está involucrado."
|
||||||
|
yours_alone_head: "Solo tuyo"
|
||||||
|
yours_alone_body: "Las claves, los nombres y el historial viven en este dispositivo. Basado en la billetera GRIM."
|
||||||
|
get_started: "Comenzar"
|
||||||
|
footnote: "Toma alrededor de un minuto. Puedes cambiar todo después."
|
||||||
|
node:
|
||||||
|
kicker: "PASO 1 DE 3 · RED"
|
||||||
|
title: "¿Cómo debería Goblin\nvigilar la cadena?"
|
||||||
|
own_title: "Ejecutar mi propio nodo"
|
||||||
|
own_badge: "Privado"
|
||||||
|
own_body: "No confía en nadie — tu billetera verifica la cadena por sí misma. Se sincroniza en segundo plano mientras terminas la configuración."
|
||||||
|
connect_title: "Conectar a un nodo"
|
||||||
|
connect_badge: "Instantáneo"
|
||||||
|
connect_body: "Sin espera de sincronización. El nodo que elijas puede ver las consultas de tu billetera."
|
||||||
|
changeable: "Modificable en cualquier momento en Ajustes → Nodo."
|
||||||
|
continue: "Continuar"
|
||||||
|
url_invalid: "La URL del nodo debe comenzar con http:// o https://"
|
||||||
|
wallet:
|
||||||
|
kicker: "PASO 2 DE 3 · BILLETERA"
|
||||||
|
title: "Configura tu billetera"
|
||||||
|
create_new: "Crear nueva"
|
||||||
|
restore_from_seed: "Restaurar desde la frase semilla"
|
||||||
|
import_identity: "Importar identidad"
|
||||||
|
import_identity_hint: "Traerás tu clave existente (un archivo .backup o nsec) después de este paso."
|
||||||
|
name_hint: "Nombre de la billetera"
|
||||||
|
password_hint: "Contraseña"
|
||||||
|
repeat_password_hint: "Repite la contraseña"
|
||||||
|
restore_hint: "Ten a mano tus palabras semilla — las ingresarás a continuación."
|
||||||
|
create_hint: "A continuación obtendrás 24 palabras semilla para anotar. Son el dinero — quien las tenga tiene tus fondos."
|
||||||
|
continue: "Continuar"
|
||||||
|
passwords_no_match: "Las contraseñas no coinciden"
|
||||||
|
words:
|
||||||
|
kicker: "PASO 2 DE 3 · BILLETERA"
|
||||||
|
title_restore: "Ingresa tus palabras semilla"
|
||||||
|
title_create: "Anota estas palabras"
|
||||||
|
write_down_hint: "En papel, en orden. Cualquiera con estas palabras puede tomar tus fondos; sin ellas, un dispositivo perdido significa fondos perdidos."
|
||||||
|
paste: "Pegar"
|
||||||
|
scan_qr: "Escanear QR"
|
||||||
|
copy_clipboard: "Copiar al portapapeles (evita esto)"
|
||||||
|
restore_wallet: "Restaurar billetera"
|
||||||
|
wrote_them_down: "Ya las anoté"
|
||||||
|
fill_every_word: "Completa cada palabra — toca una palabra para editarla, o pega la frase."
|
||||||
|
unlock_backup: "Desbloquear respaldo"
|
||||||
|
backup_unlocked: "Respaldo desbloqueado. Tus identidades se restauran cuando se abre la billetera."
|
||||||
|
confirm:
|
||||||
|
kicker: "PASO 2 DE 3 · BILLETERA"
|
||||||
|
title: "Ahora demuéstralo"
|
||||||
|
enter_hint: "Ingresa las palabras que acabas de anotar. Toca una palabra para escribirla."
|
||||||
|
paste: "Pegar"
|
||||||
|
create_wallet: "Crear billetera"
|
||||||
|
keep_going: "Sigue así — cada palabra, en orden."
|
||||||
|
identity:
|
||||||
|
kicker: "PASO 3 DE 3 · IDENTIDAD"
|
||||||
|
title: "Tu identidad de pago"
|
||||||
|
key_being_made: "creando clave…"
|
||||||
|
connected_tor: "conectado por Tor"
|
||||||
|
connecting_tor: "conectando por Tor…"
|
||||||
|
connected_direct: "conectado (directo)"
|
||||||
|
connecting_direct: "conectando (directo)…"
|
||||||
|
fresh_key_blurb: "Una clave de pago que no forma parte de tu frase de recuperación — rótala cuando quieras para mantener la privacidad, sin tocar tus fondos."
|
||||||
|
restoring: "Restaurando tus identidades…"
|
||||||
|
clean_slate_blurb: "¿Quieres empezar de cero? Cambia a una clave totalmente nueva cuando quieras — el nuevo tú no está vinculado al anterior. Misma billetera, cara nueva."
|
||||||
|
pick_username: "Elige un nombre de usuario — opcional"
|
||||||
|
username_blurb: "Tus amigos pagan a tu nombre en lugar de a una clave larga. Opcional — reclama uno cuando quieras."
|
||||||
|
username_field_hint: "tunombre"
|
||||||
|
working: "Procesando…"
|
||||||
|
claim_username: "Reclamar nombre de usuario"
|
||||||
|
available_when_connected: "Disponible en cuanto se conecte Tor — o sáltalo y reclama uno después."
|
||||||
|
youre: "Eres %{name}"
|
||||||
|
claimed_title: "%{name} es tuyo"
|
||||||
|
claimed_blurb: "Tus amigos ya pueden pagarte por nombre. Todo listo — abre tu billetera."
|
||||||
|
open_wallet: "Abrir mi billetera"
|
||||||
|
skip_for_now: "Saltar por ahora"
|
||||||
|
import_existing: "¿Ya tienes una identidad de Goblin? Impórtala"
|
||||||
|
import_title: "Importa tu identidad"
|
||||||
|
import_blurb: "Pega tu nsec o elige un archivo .backup para conservar tu clave y nombre de usuario existentes en lugar de este nuevo."
|
||||||
|
errors:
|
||||||
|
cant_open: "No se pudo abrir la billetera: %{err}"
|
||||||
|
cant_create: "No se pudo crear la billetera: %{err}"
|
||||||
|
send:
|
||||||
|
scan_to_request: "Escanear para solicitar"
|
||||||
|
scan_to_pay: "Escanear para pagar"
|
||||||
|
tab_scan: "Escanear"
|
||||||
|
tab_my_code: "Mi código"
|
||||||
|
request_from: "Solicitar a"
|
||||||
|
send_to: "Enviar a"
|
||||||
|
search_hint: "handle, npub o nombre"
|
||||||
|
suggested: "%{icon} Sugerido"
|
||||||
|
no_contacts: "Aún no hay contactos. Busca a alguien por su handle."
|
||||||
|
no_profile: "sin perfil"
|
||||||
|
tag_contact: "contacto"
|
||||||
|
tag_on_nostr: "en nostr"
|
||||||
|
searching_nostr: "Buscando en nostr…"
|
||||||
|
unverified_title: "¿Pagar a una clave no verificada?"
|
||||||
|
unverified_body: "No hay ningún perfil de nostr publicado para esta clave — puede ser nueva, anónima o estar mal escrita. Verifica bien que sea la correcta antes de enviar."
|
||||||
|
keep_looking: "Seguir buscando"
|
||||||
|
pay_anyway: "Pagar de todos modos"
|
||||||
|
scan_not_recipient: "Ese código QR no es un destinatario de goblin — se esperaba un npub o handle"
|
||||||
|
scan_prompt: "Coloca un código goblin en la vista para activar"
|
||||||
|
scan_to_pay_me: "Escanear para pagarme"
|
||||||
|
share_btn: "%{icon} Compartir"
|
||||||
|
share_message: "Págame en Goblin — %{handle}\n%{link}\nnpub: %{npub}"
|
||||||
|
none_found: "No se encontró nadie para %{label}"
|
||||||
|
enter_recipient: "Ingresa un handle, npub o nombre"
|
||||||
|
amount_title: "Monto"
|
||||||
|
to_name: "Para %{name}"
|
||||||
|
not_enough: "No tienes suficiente grin"
|
||||||
|
max: "Máx"
|
||||||
|
note_label: "Nota"
|
||||||
|
note_hint: "Agregar una nota…"
|
||||||
|
add_note: "Agregar nota"
|
||||||
|
edit_note: "Editar nota"
|
||||||
|
note_cancel: "Cancelar"
|
||||||
|
note_save: "Guardar"
|
||||||
|
review_btn: "Revisar"
|
||||||
|
confirm_request: "Confirmar solicitud"
|
||||||
|
review_title: "Revisar"
|
||||||
|
requesting_from: "Solicitando a %{name}"
|
||||||
|
youre_sending: "Le estás enviando a %{name}"
|
||||||
|
row_from: "De"
|
||||||
|
row_to: "Para"
|
||||||
|
row_note: "Nota"
|
||||||
|
row_proof: "Comprobante de pago"
|
||||||
|
row_proof_val: "Incluido"
|
||||||
|
row_proof_shared: "Comprobante compartido con"
|
||||||
|
row_they_pay: "Ellos pagan"
|
||||||
|
row_they_pay_val: "Solo si aprueban"
|
||||||
|
row_delivery: "Entrega"
|
||||||
|
row_delivery_val: "Cifrado NIP-44, por Tor"
|
||||||
|
row_network_fee: "Comisión de red"
|
||||||
|
row_network_fee_val: "Deducida de tu saldo"
|
||||||
|
row_privacy: "Privacidad"
|
||||||
|
row_privacy_val: "Mimblewimble + Tor"
|
||||||
|
send_request_btn: "Enviar solicitud"
|
||||||
|
request_approve_hint: "Recibirán una solicitud para aprobar"
|
||||||
|
hold_to_send: "Mantén presionado para enviar"
|
||||||
|
lower_amount: "Vuelve atrás y reduce el monto"
|
||||||
|
hold_confirm_hint: "Mantén presionado para confirmar"
|
||||||
|
requesting: "Solicitando…"
|
||||||
|
sending: "Enviando…"
|
||||||
|
they: "Ellos"
|
||||||
|
request_blocked: "%{who} no está aceptando solicitudes. Pídele que te envíe grin en su lugar."
|
||||||
|
failed_request_title: "No se pudo solicitar"
|
||||||
|
failed_send_title: "No se pudo enviar"
|
||||||
|
failed_request_body: "No pudimos entregar la solicitud. Pídele que te envíe grin en su lugar."
|
||||||
|
failed_send_body: "El pago no se entregó. Tu grin está a salvo — inténtalo de nuevo."
|
||||||
|
try_again_btn: "Intentar de nuevo"
|
||||||
|
close_btn: "Cerrar"
|
||||||
|
success:
|
||||||
|
requested: "Solicitado"
|
||||||
|
sent: "Enviado"
|
||||||
|
from: "de"
|
||||||
|
to: "a"
|
||||||
|
subtitle: "%{dir} %{who} · justo ahora"
|
||||||
|
done_btn: "Listo"
|
||||||
|
receipt_btn: "Recibo"
|
||||||
@@ -44,7 +44,7 @@ wallets:
|
|||||||
await_fin_amount: En attente de finalisation
|
await_fin_amount: En attente de finalisation
|
||||||
locked_amount: Verrouillé
|
locked_amount: Verrouillé
|
||||||
txs_empty: "Pour recevoir des fonds manuellement ou par transport, utilisez les boutons %{message} ou %{transport} en bas de l'écran. Pour modifier les paramètres du portefeuille, appuyez sur le bouton %{settings}."
|
txs_empty: "Pour recevoir des fonds manuellement ou par transport, utilisez les boutons %{message} ou %{transport} en bas de l'écran. Pour modifier les paramètres du portefeuille, appuyez sur le bouton %{settings}."
|
||||||
title: Portefeuilles
|
title: Goblin
|
||||||
create_desc: Créer ou importer un portefeuille existant à partir de la phrase de récupération sauvegardée.
|
create_desc: Créer ou importer un portefeuille existant à partir de la phrase de récupération sauvegardée.
|
||||||
add: Ajouter un portefeuille
|
add: Ajouter un portefeuille
|
||||||
name: 'Nom:'
|
name: 'Nom:'
|
||||||
@@ -95,6 +95,7 @@ wallets:
|
|||||||
tx_finalizing: Finalisation
|
tx_finalizing: Finalisation
|
||||||
tx_posting: Publication
|
tx_posting: Publication
|
||||||
tx_confirmed: Confirmé
|
tx_confirmed: Confirmé
|
||||||
|
tx_stale: En attente trop longue
|
||||||
txs: Transactions
|
txs: Transactions
|
||||||
tx: Transaction
|
tx: Transaction
|
||||||
messages: Messages
|
messages: Messages
|
||||||
@@ -355,4 +356,616 @@ keyboard:
|
|||||||
m: ','
|
m: ','
|
||||||
m1: .
|
m1: .
|
||||||
m2: ':'
|
m2: ':'
|
||||||
m3: /
|
m3: /
|
||||||
|
goblin:
|
||||||
|
home:
|
||||||
|
anonymous: "Anonyme"
|
||||||
|
connected_tor: "Connecté via Tor"
|
||||||
|
tor_ready: "Tor prêt · relais…"
|
||||||
|
connecting_tor: "Connexion à Tor…"
|
||||||
|
connected_direct: "Connecté (direct)"
|
||||||
|
connecting_direct: "Connexion (directe)…"
|
||||||
|
cant_reach_node: "Nœud injoignable"
|
||||||
|
node_synced: "Nœud synchronisé"
|
||||||
|
syncing: "Synchronisation…"
|
||||||
|
balance_updating: "Solde en cours de mise à jour…"
|
||||||
|
balance_stale: "Nœud injoignable · dernier solde connu"
|
||||||
|
fiat_unavailable: "Taux indisponible"
|
||||||
|
listening: "En attente de paiements"
|
||||||
|
block: "Bloc %{height}"
|
||||||
|
waiting_for_chain: "En attente de la chaîne…"
|
||||||
|
nav_wallet: "Portefeuille"
|
||||||
|
nav_pay: "Payer"
|
||||||
|
nav_activity: "Activité"
|
||||||
|
nav_receive: "Recevoir"
|
||||||
|
nav_settings: "Réglages"
|
||||||
|
back_again: "Appuyez à nouveau sur retour pour changer de portefeuille"
|
||||||
|
activity: "Activité"
|
||||||
|
news: "Actualités"
|
||||||
|
empty_title: "Aucune activité"
|
||||||
|
empty_sub: "Envoyez ou recevez des grin pour commencer."
|
||||||
|
recent: "Récent"
|
||||||
|
scan_to_pay: "Scanner pour payer"
|
||||||
|
type_amount: "Saisir un montant"
|
||||||
|
request: "Demander"
|
||||||
|
pay: "Payer"
|
||||||
|
enter_amount: "Saisissez un montant à payer ou demander"
|
||||||
|
activity:
|
||||||
|
canceled: "annulé"
|
||||||
|
pending: "en attente"
|
||||||
|
earlier: "Plus tôt"
|
||||||
|
today: "Aujourd'hui"
|
||||||
|
yesterday: "Hier"
|
||||||
|
title: "Activité"
|
||||||
|
requests: "Demandes"
|
||||||
|
empty_title: "Aucune activité"
|
||||||
|
empty_sub: "Vos paiements apparaîtront ici."
|
||||||
|
pending_header: "En attente"
|
||||||
|
receipt:
|
||||||
|
title: "Reçu"
|
||||||
|
not_found: "Transaction introuvable"
|
||||||
|
for_note: "Pour %{note}"
|
||||||
|
details: "Détails de la transaction"
|
||||||
|
canceled: "Annulé"
|
||||||
|
expired: "Expiré"
|
||||||
|
funds_returned: "Fonds retournés"
|
||||||
|
complete: "Terminé"
|
||||||
|
payment_received: "Paiement reçu"
|
||||||
|
payment_sent: "Paiement envoyé avec succès"
|
||||||
|
pending: "En attente"
|
||||||
|
confs: "%{c}/%{r} confirmations"
|
||||||
|
waiting_to_confirm: "En attente de confirmation"
|
||||||
|
paying: "Paiement…"
|
||||||
|
you: "Vous"
|
||||||
|
to: "À"
|
||||||
|
from: "De"
|
||||||
|
nostr: "nostr"
|
||||||
|
identity: "Identité"
|
||||||
|
fee_none: "Aucun"
|
||||||
|
network_fee: "Frais de réseau"
|
||||||
|
privacy: "Confidentialité"
|
||||||
|
privacy_value: "Mimblewimble + Tor"
|
||||||
|
transaction: "Transaction"
|
||||||
|
cancel_request: "Annuler la demande"
|
||||||
|
cancel_send: "Annuler le paiement"
|
||||||
|
stale_note: "Ce paiement est en attente depuis un moment."
|
||||||
|
cancel_send_confirm: "Appuyez à nouveau pour annuler — il peut encore le recevoir"
|
||||||
|
cancel_send_done: "Paiement annulé — vos fonds sont à nouveau disponibles"
|
||||||
|
cancel_send_too_late: "Ce paiement est déjà passé et ne peut pas être annulé"
|
||||||
|
waiting_to_receive: "En attente de réception par %{name}…"
|
||||||
|
request:
|
||||||
|
title: "%{name} demande"
|
||||||
|
approve: "Approuver"
|
||||||
|
decline: "Refuser"
|
||||||
|
review_title: "Vérifier la demande"
|
||||||
|
hold_to_accept: "Maintenir pour accepter"
|
||||||
|
hold_accept_hint: "Maintenez pour payer cette demande"
|
||||||
|
receive:
|
||||||
|
title: "Recevoir"
|
||||||
|
requesting: "Demande de %{amt}%{tsu} — partagez pour être payé"
|
||||||
|
clear_request: "Effacer la demande"
|
||||||
|
share_handle: "Partagez votre identifiant pour être payé"
|
||||||
|
share_npub: "Partagez votre npub pour être payé"
|
||||||
|
copied: "Copié"
|
||||||
|
copy_nostr_id: "Copier l'ID nostr"
|
||||||
|
copy_address: "Copier l'adresse"
|
||||||
|
copy: "Copier"
|
||||||
|
share_message: "Payez-moi sur Goblin (goblin.st) — %{npub}"
|
||||||
|
privacy_note: "Votre nom d'utilisateur est public. Le contenu des paiements reste chiffré sur le réseau."
|
||||||
|
privacy_note_npub: "Votre npub est public. Le contenu des paiements reste chiffré sur le réseau."
|
||||||
|
profile:
|
||||||
|
title: "Profil"
|
||||||
|
activity: "Activité"
|
||||||
|
no_activity: "Aucune activité avec cette personne."
|
||||||
|
unblock: "Débloquer"
|
||||||
|
block: "Bloquer"
|
||||||
|
blocked_blurb: "Bloqué — ses paiements et demandes sont ignorés."
|
||||||
|
block_blurb: "Le blocage ignore ses paiements et demandes entrants."
|
||||||
|
settings:
|
||||||
|
title: "Réglages"
|
||||||
|
connected_nostr: "Connecté à nostr"
|
||||||
|
connecting_relays: "Connexion aux relais…"
|
||||||
|
identity: "Identité"
|
||||||
|
copy_npub: "Copier le npub (public)"
|
||||||
|
backup_note: "Changement d'appareil ? Sauvegardez les DEUX : votre phrase seed (fonds) et votre fichier .backup d'identité (nom + clé)."
|
||||||
|
wallet: "Portefeuille"
|
||||||
|
display_unit: "Unité d'affichage"
|
||||||
|
relays: "Relais"
|
||||||
|
nostr_relays: "Relais Nostr"
|
||||||
|
node: "Nœud"
|
||||||
|
min_conf: "Confirmations minimales"
|
||||||
|
integrated_node: "Paramètres du nœud intégré"
|
||||||
|
node_advanced: "Avancé"
|
||||||
|
slatepacks: "Slatepacks"
|
||||||
|
slatepacks_value: "Transaction manuelle"
|
||||||
|
trusted_sites: "Sites de confiance"
|
||||||
|
lock_wallet: "Verrouiller le portefeuille"
|
||||||
|
switch_wallet: "Changer de portefeuille"
|
||||||
|
advanced: "Avancé"
|
||||||
|
privacy: "Confidentialité"
|
||||||
|
tor_routing: "Routage par Tor"
|
||||||
|
messages_lookups: "Messages et recherches"
|
||||||
|
tor_on: "Activé"
|
||||||
|
tor_off: "Désactivé"
|
||||||
|
auto_accept: "Acceptation auto"
|
||||||
|
pairing: "Devise des prix"
|
||||||
|
accept_anyone: "Tout le monde"
|
||||||
|
accept_contacts: "Contacts seulement"
|
||||||
|
accept_ask: "Toujours demander"
|
||||||
|
requests: "Demandes"
|
||||||
|
incoming_requests: "Demandes entrantes"
|
||||||
|
incoming_requests_sub: "Laisser les autres vous demander de l'argent"
|
||||||
|
hide_amounts: "Masquer les montants"
|
||||||
|
hide_amounts_sub: "Masquer les montants reçus dans les notifications"
|
||||||
|
language: "Langue"
|
||||||
|
update_available: "Mise à jour disponible"
|
||||||
|
appearance: "Apparence"
|
||||||
|
theme: "Thème"
|
||||||
|
theme_light: "Clair"
|
||||||
|
theme_dark: "Sombre"
|
||||||
|
theme_yellow: "Jaune"
|
||||||
|
archive: "Archive"
|
||||||
|
export_archive: "Exporter l'archive"
|
||||||
|
export_archive_caption: "Copie vos contacts, votre historique de paiement et vos demandes dans le presse-papiers au format JSON."
|
||||||
|
wipe_history: "Effacer l'historique des paiements"
|
||||||
|
wipe_history_confirm: "Appuyez à nouveau pour effacer — action irréversible"
|
||||||
|
about: "À propos"
|
||||||
|
goblin: "Goblin"
|
||||||
|
build: "Build %{build}"
|
||||||
|
network: "Réseau"
|
||||||
|
network_value: "MW + Tor + nostr"
|
||||||
|
third_party: "Tiers"
|
||||||
|
grim: "GRIM (portefeuille amont)"
|
||||||
|
grin_node: "Nœud grin"
|
||||||
|
sp_intro: "Avancé — échangez des slatepacks bruts à la main, comme le fait GRIM. À utiliser seulement si vous ne pouvez pas payer ou être payé via un username."
|
||||||
|
sp_receive_group: "Recevoir ou finaliser"
|
||||||
|
sp_receive_blurb: "Collez un slatepack qu'on vous a donné. Goblin reçoit le paiement, règle la facture, ou le finalise et le publie."
|
||||||
|
sp_process: "Traiter le slatepack"
|
||||||
|
sp_paste_first: "Collez d'abord un slatepack."
|
||||||
|
sp_reply_ready: "Réponse prête — renvoyez-la à l'expéditeur."
|
||||||
|
sp_finalizing: "Finalisation et publication sur la chaîne…"
|
||||||
|
sp_create_group: "Créer un paiement"
|
||||||
|
sp_create_blurb: "Créez un slatepack à remettre à quelqu'un. Il le reçoit, vous renvoie la réponse, et vous le finalisez ci-dessus."
|
||||||
|
sp_amount_hint: "Montant en grin"
|
||||||
|
sp_addr_hint: "Adresse du destinataire (facultatif)"
|
||||||
|
sp_create: "Créer un slatepack"
|
||||||
|
sp_ready: "Slatepack prêt — remettez-le au destinataire."
|
||||||
|
sp_amount_gt_zero: "Saisissez un montant supérieur à zéro."
|
||||||
|
sp_to_send: "Slatepack à envoyer"
|
||||||
|
sp_copy: "Copier le slatepack"
|
||||||
|
cancel: "Annuler"
|
||||||
|
continue: "Continuer"
|
||||||
|
final_confirmation: "Confirmation finale"
|
||||||
|
type_reset: "Tapez RESET"
|
||||||
|
wallet_password: "Mot de passe du portefeuille"
|
||||||
|
done: "Terminé"
|
||||||
|
close: "Fermer"
|
||||||
|
import_nsec_hint: "nsec1… ou sauvegarde collée"
|
||||||
|
backup_password_hint: "Mot de passe de sauvegarde (uniquement si exportée ailleurs)"
|
||||||
|
import_btn: "Importer"
|
||||||
|
importing: "Importation…"
|
||||||
|
import_failed: "Échec de l'importation"
|
||||||
|
name_authority: "Autorité de noms"
|
||||||
|
name_authority_title: "Changer l'autorité de noms"
|
||||||
|
name_authority_blurb: "Le serveur qui enregistre et vérifie les noms. Pointez-le vers une autre instance pour utiliser et payer des noms qui y sont hébergés."
|
||||||
|
name_authority_invalid: "Saisissez une URL complète (https://…)."
|
||||||
|
reset: "Réinitialiser"
|
||||||
|
save: "Enregistrer"
|
||||||
|
backup_file: "Sauvegarder dans un fichier"
|
||||||
|
choose_backup_file: "Choisir un fichier .backup"
|
||||||
|
backup_read_failed: "Impossible de lire ce fichier."
|
||||||
|
backup_saved: "Sauvegarde enregistrée"
|
||||||
|
backup_saved_sub: "Conservez le fichier .backup en lieu sûr. Quiconque l'a AVEC votre mot de passe peut restaurer tout votre portefeuille."
|
||||||
|
backup_file_title: "Sauvegarder le portefeuille"
|
||||||
|
backup_file_blurb: "Crée un fichier .backup chiffré avec la graine de votre portefeuille et toutes vos identités. Saisissez le mot de passe du portefeuille pour le sceller."
|
||||||
|
backup_write_failed: "Impossible d'enregistrer le fichier."
|
||||||
|
create_backup: "Créer la sauvegarde"
|
||||||
|
registered: "%{name} enregistré"
|
||||||
|
released_msg: "Libéré — le nom est disponible"
|
||||||
|
release_confirm: "Libérer %{name} ?"
|
||||||
|
release_blurb: "Dès qu'il est libre, il est disponible — n'importe qui peut le réclamer, y compris votre prochaine clé. Vous ne pourrez pas enregistrer un autre nom d'utilisateur pendant 10 minutes."
|
||||||
|
releasing: "Libération…"
|
||||||
|
keep_it: "Le garder"
|
||||||
|
release_it: "Le libérer"
|
||||||
|
username: "Nom d'utilisateur"
|
||||||
|
username_note: "Affiché comme votre nom. Public sur goblin.st. Les paiements restent chiffrés."
|
||||||
|
release_username: "Libérer le nom d'utilisateur"
|
||||||
|
pick_username: "Choisir un nom d'utilisateur — facultatif"
|
||||||
|
working: "En cours…"
|
||||||
|
claim: "Réserver"
|
||||||
|
err_just_taken: "Ce nom d'utilisateur vient d'être pris"
|
||||||
|
err_cooldown: "Vous avez récemment libéré un nom d'utilisateur — vous pouvez en enregistrer un nouveau dans les 10 minutes."
|
||||||
|
err_unreachable: "Impossible de joindre goblin.st — souci de connexion. Réessayez."
|
||||||
|
err_release: "Impossible de libérer : %{err}"
|
||||||
|
avail_available: "Disponible !"
|
||||||
|
avail_taken: "Pris"
|
||||||
|
avail_reserved: "Réservé"
|
||||||
|
avail_invalid: "Les noms font 3 à 20 caractères : a–z, 0–9, _ ou -"
|
||||||
|
avail_quarantined: "Indisponible"
|
||||||
|
avail_unknown: "Vérification impossible — souci de connexion. Réessayez."
|
||||||
|
username_none: "Non défini"
|
||||||
|
advanced_privacy: "Confidentialité avancée"
|
||||||
|
tap_reveal: "Toucher pour révéler"
|
||||||
|
notif_someone: "Quelqu'un"
|
||||||
|
notif_private_received: "Vous avez été payé. Ouvrez Goblin pour voir."
|
||||||
|
notif_private_requested: "Nouvelle demande de paiement. Ouvrez Goblin pour voir."
|
||||||
|
username:
|
||||||
|
title: "Nom d'utilisateur"
|
||||||
|
authority: "Autorité de noms"
|
||||||
|
authority_blurb: "Laissez la valeur par défaut ou pointez vers une autre instance pour utiliser les noms qui y sont hébergés."
|
||||||
|
learn_more: "En savoir plus"
|
||||||
|
custom: "Autorité personnalisée"
|
||||||
|
advprivacy:
|
||||||
|
intro: "Contrôlez ce que révèlent vos notifications, et masquez votre solde et votre historique sur cet appareil."
|
||||||
|
notifications: "Notifications"
|
||||||
|
hide_names: "Masquer les noms"
|
||||||
|
hide_names_sub: "Ne pas indiquer qui a payé dans les notifications"
|
||||||
|
hide_details: "Masquer tous les détails"
|
||||||
|
hide_details_sub: "Afficher seulement une alerte privée, sans nom ni montant"
|
||||||
|
anon: "Mode anonyme"
|
||||||
|
anon_toggle: "Flouter le solde et l'activité"
|
||||||
|
anon_sub: "Votre solde et votre activité s'affichent en points jusqu'à ce que vous touchiez pour révéler"
|
||||||
|
advanced:
|
||||||
|
title: "Avancé"
|
||||||
|
intro: "Outils de portefeuille bas niveau de GRIM. Vous n'en aurez normalement pas besoin."
|
||||||
|
own_node_desc: "Synchronisez un nœud Grin complet sur cet appareil au lieu de faire confiance à un nœud public."
|
||||||
|
own_node_active: "Votre propre nœud est actif"
|
||||||
|
repair: "Réparer le portefeuille"
|
||||||
|
repair_desc: "Re-scanner la chaîne et restaurer les sorties manquantes. Cela peut prendre du temps."
|
||||||
|
repair_unavailable: "Nécessite d'abord une connexion à un nœud synchronisé."
|
||||||
|
repairing: "Réparation… %{pct}%"
|
||||||
|
restore: "Restaurer le portefeuille"
|
||||||
|
restore_desc: "Supprimer les données locales et reconstruire depuis votre seed. À utiliser si une réparation n'a pas aidé — vous rouvrirez le portefeuille ensuite."
|
||||||
|
restore_confirm: "Touchez à nouveau pour restaurer"
|
||||||
|
show_phrase: "Phrase de récupération"
|
||||||
|
phrase_desc: "Vos 24 mots de seed grin — le seul moyen de récupérer les fonds. Gardez-les hors ligne et privés."
|
||||||
|
reveal: "Afficher la phrase"
|
||||||
|
hide: "Masquer"
|
||||||
|
password: "Mot de passe du portefeuille"
|
||||||
|
wrong_password: "Mot de passe incorrect."
|
||||||
|
delete: "Supprimer le portefeuille"
|
||||||
|
delete_desc: "Supprimer définitivement ce portefeuille et toutes ses identités de cet appareil. Faites d'abord une sauvegarde. Sans votre seed, les fonds sont irrécupérables."
|
||||||
|
delete_confirm: "Touchez à nouveau pour supprimer"
|
||||||
|
manage_node: "Gérer la connexion au nœud"
|
||||||
|
repair_confirm: "Oui, réparer maintenant"
|
||||||
|
repair_confirm_note: "La réparation réanalyse la chaîne et peut prendre quelques minutes."
|
||||||
|
restore_confirm_note: "Cela efface les données locales et les reconstruit depuis votre seed — cela peut prendre plusieurs minutes."
|
||||||
|
nostr_key: "Clé Nostr"
|
||||||
|
nostr_key_desc: "Votre nsec, la clé secrète de votre identité Nostr. Copiez-la ou affichez son QR pour vous connecter à des applis Nostr comme magick.market. Quiconque la possède contrôle votre identité, gardez-la privée."
|
||||||
|
reveal_nsec: "Afficher la clé"
|
||||||
|
copy_nsec: "Copier le nsec"
|
||||||
|
show_qr: "Afficher le QR"
|
||||||
|
hide_qr: "Masquer le QR"
|
||||||
|
nostr_section: "Paramètres nostr avancés"
|
||||||
|
backup_caption: "Contient votre portefeuille et toutes vos identités."
|
||||||
|
danger_zone: "Zone de danger"
|
||||||
|
delete_warning: "Ceci efface votre phrase de récupération et toutes les identités de cet appareil. Tout ce qui n'a pas été sauvegardé est perdu définitivement."
|
||||||
|
download_backup: "Télécharger la sauvegarde"
|
||||||
|
delete_final: "Supprimer définitivement"
|
||||||
|
privacy:
|
||||||
|
title: "Confidentialité réseau"
|
||||||
|
intro: "Goblin n'envoie que peu de choses sur le réseau, chacune scellée par un chiffrement de bout en bout pour que les relais ne puissent ni les lire ni les relier à vous."
|
||||||
|
payments: "Paiements"
|
||||||
|
payments_blurb: "Chaque message nostr transportant un slatepack."
|
||||||
|
usernames: "Noms d'utilisateur"
|
||||||
|
usernames_blurb: "Recherches de noms NIP-05 vers et depuis les autorités de noms."
|
||||||
|
price_avatars: "Prix"
|
||||||
|
price_avatars_blurb: "Le taux en temps réel affiché à côté des montants."
|
||||||
|
grin_node: "Nœud grin"
|
||||||
|
grin_node_blurb: "Synchronise les blocs et diffuse vos transactions. Données de chaîne publiques, identiques pour tous, jamais liées à votre identité."
|
||||||
|
how_privacy: "Comment fonctionne votre confidentialité"
|
||||||
|
how_privacy_blurb: "Les paiements et les messages sont scellés par un chiffrement authentifié et envoyés comme messages privés, de sorte qu'un relais ne peut ni les lire ni relier un paiement à vous."
|
||||||
|
always_direct: "Toujours direct"
|
||||||
|
tor_off: "Tor désactivé"
|
||||||
|
tor_on: "Tor activé"
|
||||||
|
tor_switch: "Acheminer via Tor"
|
||||||
|
tor_switch_sub: "Masquez votre IP aux relais."
|
||||||
|
vpn_note: "Lorsque Tor est désactivé, les relais peuvent voir votre adresse IP. Nous vous recommandons d'utiliser au moins un VPN."
|
||||||
|
pairing:
|
||||||
|
title: "Appairage"
|
||||||
|
intro: "Ce à quoi votre solde et vos montants sont comparés."
|
||||||
|
pair_with: "Apparier avec"
|
||||||
|
rates_note: "Les cours sont récupérés via Tor, uniquement tant qu'un appairage est actif — désactivé, aucune requête de cours ne quitte votre appareil."
|
||||||
|
relays:
|
||||||
|
title: "Relais"
|
||||||
|
intro: "Les messages de paiement sont répliqués sur tous les relais ci-dessous ; un seul relais joignable suffit pour recevoir."
|
||||||
|
your_relays: "Vos relais"
|
||||||
|
add_relay: "Ajouter un relais"
|
||||||
|
add_relay_btn: "Ajouter un relais"
|
||||||
|
save_reconnect: "Enregistrer et reconnecter"
|
||||||
|
none: "aucun"
|
||||||
|
count: "%{n} relais"
|
||||||
|
node:
|
||||||
|
title: "Nœud"
|
||||||
|
connection: "Connexion"
|
||||||
|
integrated: "Nœud intégré"
|
||||||
|
applies_after: "S'applique après le verrouillage puis déverrouillage du portefeuille."
|
||||||
|
add_external: "Ajouter un nœud externe"
|
||||||
|
api_secret_hint: "Secret API (facultatif)"
|
||||||
|
add_node: "Ajouter le nœud"
|
||||||
|
integrated_host: "nœud intégré"
|
||||||
|
summary_syncing: "%{conn} · synchronisation"
|
||||||
|
summary_block: "Bloc %{height} · %{conn}"
|
||||||
|
nips:
|
||||||
|
title: "nostr et NIP"
|
||||||
|
intro1: "Goblin parle nostr — un protocole ouvert de messages signés transmis via de simples serveurs relais. Votre portefeuille porte sa propre identité nostr : une clé aléatoire autonome, gardée délibérément indépendante de vos fonds et de votre phrase de récupération. Chaque paiement voyage comme un message direct chiffré de bout en bout entre identités, le slatepack à l'intérieur."
|
||||||
|
intro2: "goblin.st est le service de noms de Goblin : réserver un nom d'utilisateur y publie une correspondance nom → clé (NIP-05), pour qu'on puisse payer you au lieu d'un long npub. Le nom d'utilisateur est public ; le contenu des paiements ne l'est jamais. Les NIP sont les briques du protocole — touchez-en un pour lire la spécification."
|
||||||
|
n05_title: "Noms"
|
||||||
|
n05_blurb: "Associe username@goblin.st à votre clé, pour que les identifiants fonctionnent comme des adresses."
|
||||||
|
n17_title: "Messages privés"
|
||||||
|
n17_blurb: "L'enveloppe de DM chiffré dans laquelle voyage chaque paiement."
|
||||||
|
n44_title: "Chiffrement"
|
||||||
|
n44_blurb: "Le chiffrement authentifié utilisé à l'intérieur de ces messages."
|
||||||
|
n49_title: "Chiffrement de clé"
|
||||||
|
n49_blurb: "Comment la clé secrète est stockée au repos, verrouillée par votre mot de passe."
|
||||||
|
n59_title: "Emballage cadeau"
|
||||||
|
n59_blurb: "Enveloppe les messages pour que les relais ne voient pas qui parle à qui."
|
||||||
|
n98_title: "Auth HTTP"
|
||||||
|
n98_blurb: "Signe la demande d'enregistrement du nom d'utilisateur auprès de goblin.st."
|
||||||
|
batch:
|
||||||
|
title: "Approuver %{n} factures"
|
||||||
|
blurb: "Émettre %{n} demandes de paiement de %{amount} chacune à %{name} — %{total} au total. Chaque demande reçoit sa propre adresse de paiement."
|
||||||
|
approve: "Approuver"
|
||||||
|
login:
|
||||||
|
title: "Connexion"
|
||||||
|
headline: "Se connecter à %{domain} ?"
|
||||||
|
identity: "Se connecter en tant que"
|
||||||
|
explain: "Ceci signe une demande de connexion à usage unique. Votre clé n'est pas partagée et le site ne peut pas agir en votre nom."
|
||||||
|
pass_prompt: "Saisissez le mot de passe de votre portefeuille pour vous connecter."
|
||||||
|
confirm: "Se connecter"
|
||||||
|
sent: "Connecté à %{domain}"
|
||||||
|
failed: "Impossible de joindre %{domain}. La connexion n'a pas été transmise."
|
||||||
|
authorize:
|
||||||
|
title: "Autoriser"
|
||||||
|
headline: "Autoriser %{domain} ?"
|
||||||
|
kind_post: "Publication publique"
|
||||||
|
kind_repost: "Partage"
|
||||||
|
kind_reaction: "Réaction"
|
||||||
|
kind_article: "Article (format long)"
|
||||||
|
kind_unknown: "Type %{n} (non reconnu)"
|
||||||
|
unknown_caution: "Goblin ne reconnaît pas ce type d'événement. N'approuvez que si vous faites confiance à %{domain}."
|
||||||
|
identity: "Signer en tant que"
|
||||||
|
explain: "Approuver signe cet unique événement et le transmet à %{domain}. Goblin ne publie rien, aucune clé n'est partagée, et le site n'obtient ni session ni capacité future."
|
||||||
|
pass_prompt: "Saisissez le mot de passe de votre portefeuille pour autoriser."
|
||||||
|
confirm: "Autoriser"
|
||||||
|
sent: "%{domain} autorisé"
|
||||||
|
failed: "Impossible de joindre %{domain}. Rien n'a été transmis."
|
||||||
|
truncated: "tronqué, %{n} caractères de plus"
|
||||||
|
show_full: "Tout afficher"
|
||||||
|
show_less: "Afficher moins"
|
||||||
|
replying_to: "en réponse à %{id}"
|
||||||
|
mentions: "mentionne %{id}"
|
||||||
|
repost_of: "partage %{id}"
|
||||||
|
by_author: "par %{id}"
|
||||||
|
reacts_to: "réagit à %{id}"
|
||||||
|
article_title: "Titre : %{title}"
|
||||||
|
trust:
|
||||||
|
title: "Confiance"
|
||||||
|
headline: "Faire confiance à %{domain} ?"
|
||||||
|
identity: "Connexion en tant que"
|
||||||
|
lead: "%{domain} peut agir pour vous pendant cette session sans redemander à chaque fois. Tout ce qui dépense de l'argent demande toujours."
|
||||||
|
grant_intro: "Peut signer sans demander :"
|
||||||
|
cat_social: "Publications et réactions"
|
||||||
|
cat_dm: "Messages privés"
|
||||||
|
cat_market: "Annonces"
|
||||||
|
cat_identity: "Profil et listes"
|
||||||
|
cat_delete: "Suppressions"
|
||||||
|
cat_http: "Envois et authentification HTTP"
|
||||||
|
cat_unknown: "Autre type d'événement (type %{n})"
|
||||||
|
login_excluded: "La connexion n'est jamais accordée à un site."
|
||||||
|
money_line: "L'argent demande toujours votre mot de passe. Rien n'est dépensé ni vendu en silence."
|
||||||
|
duration: "Valable pour cette session seulement. Terminez-la à tout moment dans Réglages, Sites de confiance."
|
||||||
|
pass_prompt: "Saisissez le mot de passe de votre portefeuille pour faire confiance à ce site."
|
||||||
|
confirm_hold: "Maintenez pour faire confiance pour cette session"
|
||||||
|
sent: "Confiance accordée à %{domain} pour cette session"
|
||||||
|
failed: "Impossible de joindre %{domain}. La session n'a pas démarré."
|
||||||
|
announce_failed: "Confiance accordée, mais %{domain} n'a pas encore confirmé la session."
|
||||||
|
notice_volume: "Un site de confiance signe beaucoup."
|
||||||
|
notice_decrypt: "Un site de confiance lit vos messages."
|
||||||
|
money:
|
||||||
|
title: "Confirmer"
|
||||||
|
headline: "Confirmer dans %{domain} ?"
|
||||||
|
identity: "Signature en tant que %{id}"
|
||||||
|
explain: "Ceci déplace ou engage une valeur, donc une confirmation est toujours demandée. Approuver signe cette seule action."
|
||||||
|
pass_prompt: "Saisissez le mot de passe de votre portefeuille pour confirmer."
|
||||||
|
confirm_hold: "Maintenez pour confirmer"
|
||||||
|
encrypt_desc: "Un message de commande chiffré qui engage un paiement."
|
||||||
|
trusted_sites:
|
||||||
|
title: "Sites de confiance"
|
||||||
|
intro: "Les sites auxquels vous avez fait confiance cette session peuvent signer des actions à faible risque sans demander. L'argent demande toujours. Terminez toute session ici."
|
||||||
|
empty: "Aucun site de confiance. Faites confiance à un site lors de la connexion pour lui donner une session de signature."
|
||||||
|
none: "rien"
|
||||||
|
can_sign: "Peut signer en silence : %{cats}"
|
||||||
|
time_left: "Se termine dans %{mins} min"
|
||||||
|
paused: "En pause, signe beaucoup"
|
||||||
|
resume: "Reprendre"
|
||||||
|
end: "Terminer la session"
|
||||||
|
identities:
|
||||||
|
title: "Identités"
|
||||||
|
switch_hint: "Changer d'identité"
|
||||||
|
blurb: "Vos identités sont les noms sous lesquels on vous paie. Elles alimentent toutes ce même portefeuille. Ajoutez-en une nouvelle, passez de l'une à l'autre, ou supprimez celle dont vous n'avez plus besoin."
|
||||||
|
privacy_note: "Ces identités partagent un portefeuille et un solde — des portes d'entrée distinctes, pas des fonds distincts. Pour un argent vraiment séparé, utilisez un portefeuille distinct."
|
||||||
|
held: "Identités détenues"
|
||||||
|
add: "Ajouter une identité"
|
||||||
|
add_title: "Ajouter une identité"
|
||||||
|
nsec_hint: "Coller un nsec"
|
||||||
|
generate_note: "Une nouvelle clé anonyme sera créée pour ce portefeuille."
|
||||||
|
generate: "Créer"
|
||||||
|
import: "Importer"
|
||||||
|
choose_backup: "Choisir un fichier .backup"
|
||||||
|
backup_selected: "Fichier de sauvegarde sélectionné"
|
||||||
|
delete_short: "Supprimer l'identité"
|
||||||
|
delete_title: "Supprimer %{name} ?"
|
||||||
|
delete_blurb: "Cela supprime définitivement l'identité de ce portefeuille. L'argent déjà reçu reste dans votre solde."
|
||||||
|
delete_backup_note: "Sauvegardez-la d'abord — sans son nsec ou son fichier .backup, elle est irrécupérable."
|
||||||
|
delete_confirm: "Supprimer"
|
||||||
|
manage_title: "Gérer l'identité"
|
||||||
|
tag_note: "Donnez-lui un nom privé. Vous seul le voyez — il n’est jamais partagé."
|
||||||
|
tag_hint: "Nom privé"
|
||||||
|
tag_save: "Enregistrer"
|
||||||
|
pass_prompt: "Le mot de passe de votre portefeuille chiffre et déverrouille chaque identité. Saisissez-le pour continuer."
|
||||||
|
working: "En cours…"
|
||||||
|
onboarding:
|
||||||
|
privacy:
|
||||||
|
kicker: "CONFIDENTIALITÉ RÉSEAU"
|
||||||
|
continue: "Continuer"
|
||||||
|
intro:
|
||||||
|
private_money_head: "Argent privé"
|
||||||
|
private_money_body: "Goblin est un portefeuille pour grin — de l'argent numérique sans montants ni adresses sur sa chaîne."
|
||||||
|
send_like_message_head: "Envoyer comme un message"
|
||||||
|
send_like_message_body: "Payez un username ou un npub et cela arrive comme un message chiffré de bout en bout via nostr et Tor — personne entre les deux ne voit le montant ni les personnes impliquées."
|
||||||
|
yours_alone_head: "À vous seul"
|
||||||
|
yours_alone_body: "Clés, noms et historique vivent sur cet appareil. Construit sur le portefeuille GRIM."
|
||||||
|
get_started: "Commencer"
|
||||||
|
footnote: "Environ une minute. Vous pourrez tout changer plus tard."
|
||||||
|
node:
|
||||||
|
kicker: "ÉTAPE 1 SUR 3 · RÉSEAU"
|
||||||
|
title: "Comment Goblin doit-il\nsurveiller la chaîne ?"
|
||||||
|
own_title: "Lancer mon propre nœud"
|
||||||
|
own_badge: "Privé"
|
||||||
|
own_body: "Ne fait confiance à personne — votre portefeuille vérifie la chaîne lui-même. Se synchronise en arrière-plan pendant que vous terminez la configuration."
|
||||||
|
connect_title: "Se connecter à un nœud"
|
||||||
|
connect_badge: "Instantané"
|
||||||
|
connect_body: "Aucune attente de synchronisation. Le nœud que vous choisissez peut voir les requêtes de votre portefeuille."
|
||||||
|
changeable: "Modifiable à tout moment dans Réglages → Nœud."
|
||||||
|
continue: "Continuer"
|
||||||
|
url_invalid: "L'URL du nœud doit commencer par http:// ou https://"
|
||||||
|
wallet:
|
||||||
|
kicker: "ÉTAPE 2 SUR 3 · PORTEFEUILLE"
|
||||||
|
title: "Configurez votre portefeuille"
|
||||||
|
create_new: "Créer un nouveau"
|
||||||
|
restore_from_seed: "Restaurer depuis la phrase"
|
||||||
|
import_identity: "Importer une identité"
|
||||||
|
import_identity_hint: "Vous importerez votre clé existante (un fichier .backup ou un nsec) après cette étape."
|
||||||
|
name_hint: "Nom du portefeuille"
|
||||||
|
password_hint: "Mot de passe"
|
||||||
|
repeat_password_hint: "Répéter le mot de passe"
|
||||||
|
restore_hint: "Préparez vos mots de récupération — vous les saisirez ensuite."
|
||||||
|
create_hint: "Vous obtiendrez ensuite 24 mots de récupération à noter. Ce sont l'argent — quiconque les détient détient vos fonds."
|
||||||
|
continue: "Continuer"
|
||||||
|
passwords_no_match: "Les mots de passe ne correspondent pas"
|
||||||
|
words:
|
||||||
|
kicker: "ÉTAPE 2 SUR 3 · PORTEFEUILLE"
|
||||||
|
title_restore: "Saisissez vos mots de récupération"
|
||||||
|
title_create: "Notez ces mots"
|
||||||
|
write_down_hint: "Sur papier, dans l'ordre. Quiconque a ces mots peut prendre vos fonds ; sans eux, un appareil perdu signifie des fonds perdus."
|
||||||
|
paste: "Coller"
|
||||||
|
scan_qr: "Scanner le QR"
|
||||||
|
copy_clipboard: "Copier dans le presse-papiers (à éviter)"
|
||||||
|
restore_wallet: "Restaurer le portefeuille"
|
||||||
|
wrote_them_down: "Je les ai notés"
|
||||||
|
fill_every_word: "Remplissez chaque mot — touchez un mot pour le modifier, ou collez la phrase."
|
||||||
|
unlock_backup: "Déverrouiller la sauvegarde"
|
||||||
|
backup_unlocked: "Sauvegarde déverrouillée. Vos identités seront restaurées à l'ouverture du portefeuille."
|
||||||
|
confirm:
|
||||||
|
kicker: "ÉTAPE 2 SUR 3 · PORTEFEUILLE"
|
||||||
|
title: "Maintenant prouvez-le"
|
||||||
|
enter_hint: "Saisissez les mots que vous venez de noter. Touchez un mot pour le taper."
|
||||||
|
paste: "Coller"
|
||||||
|
create_wallet: "Créer le portefeuille"
|
||||||
|
keep_going: "Continuez — chaque mot, dans l'ordre."
|
||||||
|
identity:
|
||||||
|
kicker: "ÉTAPE 3 SUR 3 · IDENTITÉ"
|
||||||
|
title: "Votre identité de paiement"
|
||||||
|
key_being_made: "clé en cours de création…"
|
||||||
|
connected_tor: "connecté via Tor"
|
||||||
|
connecting_tor: "connexion via Tor…"
|
||||||
|
connected_direct: "connecté (direct)"
|
||||||
|
connecting_direct: "connexion (directe)…"
|
||||||
|
fresh_key_blurb: "Une clé de paiement qui ne fait pas partie de votre seed — renouvelable à tout moment, sans toucher à vos fonds."
|
||||||
|
restoring: "Restauration de vos identités…"
|
||||||
|
clean_slate_blurb: "Envie de repartir à zéro ? Remplacez par une toute nouvelle clé à tout moment — le nouveau vous n'est pas lié à l'ancien. Même portefeuille, nouveau visage."
|
||||||
|
pick_username: "Choisir un nom d'utilisateur — facultatif"
|
||||||
|
username_blurb: "Vos amis paient votre nom au lieu d'une longue clé. Facultatif — réclamez-en un à tout moment."
|
||||||
|
username_field_hint: "votrenom"
|
||||||
|
working: "En cours…"
|
||||||
|
claim_username: "Réserver le nom d'utilisateur"
|
||||||
|
available_when_connected: "Disponible une fois Tor connecté — ou passez et réservez plus tard."
|
||||||
|
youre: "Vous êtes %{name}"
|
||||||
|
claimed_title: "%{name} est à vous"
|
||||||
|
claimed_blurb: "Vos amis peuvent désormais vous payer par votre nom. Tout est prêt — ouvrez votre portefeuille."
|
||||||
|
open_wallet: "Ouvrir mon portefeuille"
|
||||||
|
skip_for_now: "Passer pour l'instant"
|
||||||
|
import_existing: "Vous avez déjà une identité Goblin ? Importez-la"
|
||||||
|
import_title: "Importer votre identité"
|
||||||
|
import_blurb: "Collez votre nsec ou choisissez un fichier .backup pour conserver votre clé et votre nom existants au lieu de ce nouveau."
|
||||||
|
errors:
|
||||||
|
cant_open: "Impossible d'ouvrir le portefeuille : %{err}"
|
||||||
|
cant_create: "Impossible de créer le portefeuille : %{err}"
|
||||||
|
send:
|
||||||
|
scan_to_request: "Scanner pour demander"
|
||||||
|
scan_to_pay: "Scanner pour payer"
|
||||||
|
tab_scan: "Scanner"
|
||||||
|
tab_my_code: "Mon code"
|
||||||
|
request_from: "Demander à"
|
||||||
|
send_to: "Envoyer à"
|
||||||
|
search_hint: "handle, npub ou nom"
|
||||||
|
suggested: "%{icon} Suggéré"
|
||||||
|
no_contacts: "Aucun contact pour l'instant. Trouvez quelqu'un par son handle."
|
||||||
|
no_profile: "pas de profil"
|
||||||
|
tag_contact: "contact"
|
||||||
|
tag_on_nostr: "sur nostr"
|
||||||
|
searching_nostr: "Recherche sur nostr…"
|
||||||
|
unverified_title: "Payer une clé non vérifiée ?"
|
||||||
|
unverified_body: "Aucun profil nostr n'est publié pour cette clé — elle peut être toute neuve, anonyme ou mal saisie. Vérifiez bien qu'il s'agit de la bonne avant d'envoyer."
|
||||||
|
keep_looking: "Continuer à chercher"
|
||||||
|
pay_anyway: "Payer quand même"
|
||||||
|
scan_not_recipient: "Ce QR n'est pas un destinataire goblin — un npub ou handle est attendu"
|
||||||
|
scan_prompt: "Placez un code goblin dans le champ pour activer"
|
||||||
|
scan_to_pay_me: "Scannez pour me payer"
|
||||||
|
share_btn: "%{icon} Partager"
|
||||||
|
share_message: "Payez-moi sur Goblin — %{handle}\n%{link}\nnpub : %{npub}"
|
||||||
|
none_found: "Personne trouvé pour %{label}"
|
||||||
|
enter_recipient: "Saisissez un handle, un npub ou un nom"
|
||||||
|
amount_title: "Montant"
|
||||||
|
to_name: "À %{name}"
|
||||||
|
not_enough: "Vous n'avez pas assez de grin"
|
||||||
|
max: "Max"
|
||||||
|
note_label: "Note"
|
||||||
|
note_hint: "Ajouter une note…"
|
||||||
|
add_note: "Ajouter une note"
|
||||||
|
edit_note: "Modifier la note"
|
||||||
|
note_cancel: "Annuler"
|
||||||
|
note_save: "Enregistrer"
|
||||||
|
review_btn: "Vérifier"
|
||||||
|
confirm_request: "Confirmer la demande"
|
||||||
|
review_title: "Vérification"
|
||||||
|
requesting_from: "Demande à %{name}"
|
||||||
|
youre_sending: "Vous envoyez %{name}"
|
||||||
|
row_from: "De"
|
||||||
|
row_to: "À"
|
||||||
|
row_note: "Note"
|
||||||
|
row_proof: "Preuve de paiement"
|
||||||
|
row_proof_val: "Incluse"
|
||||||
|
row_proof_shared: "Preuve partagée avec"
|
||||||
|
row_they_pay: "Ils paient"
|
||||||
|
row_they_pay_val: "Seulement s'ils approuvent"
|
||||||
|
row_delivery: "Livraison"
|
||||||
|
row_delivery_val: "Chiffré NIP-44, via Tor"
|
||||||
|
row_network_fee: "Frais de réseau"
|
||||||
|
row_network_fee_val: "Déduit de votre solde"
|
||||||
|
row_privacy: "Confidentialité"
|
||||||
|
row_privacy_val: "Mimblewimble + Tor"
|
||||||
|
send_request_btn: "Envoyer la demande"
|
||||||
|
request_approve_hint: "Ils recevront une demande à approuver"
|
||||||
|
hold_to_send: "Maintenir pour envoyer"
|
||||||
|
lower_amount: "Revenez et baissez le montant"
|
||||||
|
hold_confirm_hint: "Appuyez et maintenez pour confirmer"
|
||||||
|
requesting: "Demande en cours…"
|
||||||
|
sending: "Envoi…"
|
||||||
|
they: "Ils"
|
||||||
|
request_blocked: "%{who} n'accepte pas les demandes. Demandez-lui de vous envoyer des grin à la place."
|
||||||
|
failed_request_title: "Échec de la demande"
|
||||||
|
failed_send_title: "Échec de l'envoi"
|
||||||
|
failed_request_body: "Impossible de livrer la demande. Demandez-lui de vous envoyer des grin à la place."
|
||||||
|
failed_send_body: "Le paiement n'a pas été livré. Vos grin sont en sécurité — réessayez."
|
||||||
|
try_again_btn: "Réessayer"
|
||||||
|
close_btn: "Fermer"
|
||||||
|
success:
|
||||||
|
requested: "Demandé"
|
||||||
|
sent: "Envoyé"
|
||||||
|
from: "de"
|
||||||
|
to: "à"
|
||||||
|
subtitle: "%{dir} %{who} · à l'instant"
|
||||||
|
done_btn: "Terminé"
|
||||||
|
receipt_btn: "Reçu"
|
||||||
@@ -0,0 +1,974 @@
|
|||||||
|
lang_name: 日本語
|
||||||
|
copy: コピー
|
||||||
|
paste: 貼り付け
|
||||||
|
continue: 続ける
|
||||||
|
complete: 完了
|
||||||
|
error: エラー
|
||||||
|
retry: 再試行
|
||||||
|
close: 閉じる
|
||||||
|
change: 変更
|
||||||
|
show: 表示
|
||||||
|
delete: 削除
|
||||||
|
clear: クリア
|
||||||
|
create: 作成
|
||||||
|
id: 識別子
|
||||||
|
kernel: カーネル
|
||||||
|
settings: 設定
|
||||||
|
language: 言語
|
||||||
|
scan: スキャン
|
||||||
|
qr_code: QRコード
|
||||||
|
scan_qr: QRコードをスキャン
|
||||||
|
repeat: 繰り返し
|
||||||
|
scan_result: スキャン結果
|
||||||
|
back: 戻る
|
||||||
|
share: 共有
|
||||||
|
theme: 'テーマ:'
|
||||||
|
dark: ダーク
|
||||||
|
light: ライト
|
||||||
|
file: ファイル
|
||||||
|
choose_file: ファイルを選択
|
||||||
|
choose_folder: フォルダを選択
|
||||||
|
crash_report: クラッシュレポート
|
||||||
|
crash_report_warning: 前回、アプリが予期せず終了しました。クラッシュレポートを開発者と共有できます。
|
||||||
|
confirmation: 確認
|
||||||
|
enter_url: URLを入力してください
|
||||||
|
max_short: 最大
|
||||||
|
files_location: ファイルの保存場所
|
||||||
|
moving_files: ファイルを移動中
|
||||||
|
wrong_path_error: 指定されたパスが正しくありません
|
||||||
|
check_updates: 起動時にアップデートを確認
|
||||||
|
update_available: アップデートがあります!
|
||||||
|
changelog: '更新履歴:'
|
||||||
|
wallets:
|
||||||
|
await_conf_amount: 確認待ち
|
||||||
|
await_fin_amount: 確定待ち
|
||||||
|
locked_amount: ロック中
|
||||||
|
txs_empty: '資金を手動または転送機能で受け取るには、画面下部の%{message}または%{transport}ボタンを使用してください。ウォレットの設定を変更するには%{settings}ボタンを押してください。'
|
||||||
|
title: Goblin
|
||||||
|
create_desc: 保存したリカバリーフレーズから既存のウォレットを作成またはインポートします。
|
||||||
|
add: ウォレットを追加
|
||||||
|
name: '名前:'
|
||||||
|
pass: 'パスワード:'
|
||||||
|
pass_empty: ウォレットのパスワードを入力してください
|
||||||
|
current_pass: '現在のパスワード:'
|
||||||
|
new_pass: '新しいパスワード:'
|
||||||
|
min_tx_conf_count: '取引に必要な最小確認数:'
|
||||||
|
recover: 復元
|
||||||
|
recovery_phrase: リカバリーフレーズ
|
||||||
|
words_count: '単語数:'
|
||||||
|
enter_word: '単語 #%{number} を入力してください:'
|
||||||
|
not_valid_word: 入力した単語が正しくありません
|
||||||
|
not_valid_phrase: 入力したフレーズが正しくありません
|
||||||
|
create_phrase_desc: リカバリーフレーズを安全に書き留めて保管してください。
|
||||||
|
restore_phrase_desc: 保存したリカバリーフレーズの単語を入力してください。
|
||||||
|
setup_conn_desc: ウォレットがネットワークに接続する方法を選択してください。
|
||||||
|
conn_method: 接続方法
|
||||||
|
ext_conn: '外部接続:'
|
||||||
|
add_node: ノードを追加
|
||||||
|
node_url: 'ノードURL:'
|
||||||
|
node_secret: 'APIシークレット(任意):'
|
||||||
|
invalid_url: 入力したURLが正しくありません
|
||||||
|
open: ウォレットを開く
|
||||||
|
wrong_pass: 入力したパスワードが間違っています
|
||||||
|
locked: ロック中
|
||||||
|
unlocked: ロック解除済み
|
||||||
|
enable_node: 'ウォレットを使用するには統合ノードを有効にするか、画面下部の%{settings}を選択して接続設定を変更してください。'
|
||||||
|
node_loading: '統合ノードの同期が完了するとウォレットが読み込まれます。画面下部の%{settings}を選択すると接続を変更できます。'
|
||||||
|
loading: 読み込み中
|
||||||
|
closing: 終了中
|
||||||
|
checking: 確認中
|
||||||
|
default_wallet: デフォルトウォレット
|
||||||
|
new_account_desc: '新しいアカウントの名前を入力してください:'
|
||||||
|
wallet_loading: ウォレットを読み込み中
|
||||||
|
wallet_closing: ウォレットを終了中
|
||||||
|
wallet_checking: ウォレットを確認中
|
||||||
|
tx_loading: 取引を読み込み中
|
||||||
|
default_account: デフォルトアカウント
|
||||||
|
accounts: アカウント
|
||||||
|
tx_sent: 送信済み
|
||||||
|
tx_received: 受信済み
|
||||||
|
tx_sending: 送信中
|
||||||
|
tx_receiving: 受信中
|
||||||
|
tx_confirming: 確認待ち
|
||||||
|
tx_canceled: キャンセル済み
|
||||||
|
tx_cancelling: キャンセル中
|
||||||
|
tx_finalizing: 確定中
|
||||||
|
tx_posting: 送信中
|
||||||
|
tx_confirmed: 確認済み
|
||||||
|
tx_stale: 保留が長すぎます
|
||||||
|
txs: 取引
|
||||||
|
tx: 取引
|
||||||
|
messages: メッセージ
|
||||||
|
transport: 転送
|
||||||
|
input_slatepack_desc: '受信したSlatepackメッセージを入力して、返信を作成するか要求を確定してください:'
|
||||||
|
parse_slatepack_err: 'メッセージの読み取り中にエラーが発生しました。入力内容を確認してください:'
|
||||||
|
pay_balance_error: 'アカウント残高が%{amount}ツとネットワーク手数料の支払いに不足しています。'
|
||||||
|
parse_i1_slatepack_desc: '%{amount}ツを支払うには、このメッセージを受取人に送ってください:'
|
||||||
|
parse_i2_slatepack_desc: '%{amount}ツを受け取るには取引を確定してください:'
|
||||||
|
parse_i3_slatepack_desc: '%{amount}ツの受け取りを確定するには取引を送信してください:'
|
||||||
|
parse_s1_slatepack_desc: '%{amount}ツを受け取るには、このメッセージを送信者に送ってください:'
|
||||||
|
parse_s2_slatepack_desc: '%{amount}ツを送るには取引を確定してください:'
|
||||||
|
parse_s3_slatepack_desc: '%{amount}ツの送信を確定するには取引を送信してください:'
|
||||||
|
resp_slatepack_err: '返信の作成中にエラーが発生しました。入力データを確認するか、もう一度お試しください:'
|
||||||
|
resp_exists_err: その取引はすでに存在します。
|
||||||
|
resp_canceled_err: その取引はすでにキャンセルされています。
|
||||||
|
create_request_desc: '資金を送るか受け取るための要求を作成してください:'
|
||||||
|
send_request_desc: '%{amount}ツを送るための要求を作成しました。このメッセージを受取人に送ってください:'
|
||||||
|
send_slatepack_err: 資金送付の要求作成中にエラーが発生しました。入力データを確認するか、もう一度お試しください。
|
||||||
|
invoice_desc: '%{amount}ツを受け取るための要求を作成しました。このメッセージを送信者に送ってください:'
|
||||||
|
invoice_slatepack_err: 請求書の発行中にエラーが発生しました。入力データを確認するか、もう一度お試しください。
|
||||||
|
finalize_slatepack_err: '確定処理中にエラーが発生しました。入力データを確認するか、もう一度お試しください:'
|
||||||
|
finalize: 確定
|
||||||
|
use_dandelion: Dandelionを使用
|
||||||
|
enter_amount_send: '%{amount}ツを保有しています。送る金額を入力してください:'
|
||||||
|
enter_amount_receive: '受け取る金額を入力してください:'
|
||||||
|
recovery: 復元
|
||||||
|
repair_wallet: ウォレットを修復
|
||||||
|
repair_desc: ウォレットを点検し、必要に応じて欠落しているアウトプットを修復・復元します。この操作には時間がかかります。
|
||||||
|
repair_unavailable: ノードへの有効な接続とウォレットの同期完了が必要です。
|
||||||
|
delete: ウォレットを削除
|
||||||
|
delete_conf: 本当にウォレットを削除しますか?
|
||||||
|
delete_desc: 後で資金にアクセスできるよう、リカバリーフレーズを必ず保存してください。
|
||||||
|
wallet_loading_err: 'ウォレットの同期中にエラーが発生しました。再試行するか、画面下部の%{settings}を選択して接続設定を変更できます。'
|
||||||
|
wallet: ウォレット
|
||||||
|
send: 送る
|
||||||
|
receive: 受け取る
|
||||||
|
settings: ウォレット設定
|
||||||
|
tx_send_cancel_conf: '%{amount}ツの送信をキャンセルしますか?'
|
||||||
|
tx_receive_cancel_conf: '%{amount}ツの受信をキャンセルしますか?'
|
||||||
|
rec_phrase_not_found: リカバリーフレーズが見つかりません。
|
||||||
|
restore_wallet_desc: 通常の修復で解決しない場合は、すべてのファイルを削除してウォレットを復元してください。その後、ウォレットを再度開く必要があります。
|
||||||
|
fee_base_desc: '手数料(基本値%{value}):'
|
||||||
|
payment_proof: 支払い証明
|
||||||
|
payment_proof_desc: '受信した支払い証明を入力して取引を検証してください:'
|
||||||
|
payment_proof_valid: '入力した支払い証明は有効です:'
|
||||||
|
payment_proof_error: '入力した支払い証明は無効です:'
|
||||||
|
tx_delete_confirmation: 履歴からこの取引を削除しますか?
|
||||||
|
transport:
|
||||||
|
desc: '転送機能を使ってメッセージを同期的に送受信してください:'
|
||||||
|
tor_network: Torネットワーク
|
||||||
|
connected: 接続済み
|
||||||
|
connecting: 接続中
|
||||||
|
disconnecting: 切断中
|
||||||
|
conn_error: 接続エラー
|
||||||
|
disconnected: 切断済み
|
||||||
|
receiver_address: '受取人のアドレス:'
|
||||||
|
incorrect_addr_err: '入力したアドレスが正しくありません:'
|
||||||
|
tor_send_error: Tor経由の送信中にエラーが発生しました。受取人がオンラインか確認してください。取引はキャンセルされました。
|
||||||
|
tor_autorun_desc: ウォレットを開いたときにTorサービスを起動し、取引を同期的に受け取るかどうかです。
|
||||||
|
tor_sending: Tor経由で送信中
|
||||||
|
tor_settings: Tor設定
|
||||||
|
bridges: ブリッジ
|
||||||
|
bridges_desc: 通常の接続が機能しない場合、ブリッジを設定してTorネットワークの検閲を回避してください。
|
||||||
|
bin_file: 'バイナリファイル:'
|
||||||
|
conn_line: '接続文字列:'
|
||||||
|
bridges_disabled: ブリッジ無効
|
||||||
|
bridge_name: 'ブリッジ %{b}'
|
||||||
|
network:
|
||||||
|
self: ネットワーク
|
||||||
|
type: 'ネットワークの種類:'
|
||||||
|
mainnet: メイン
|
||||||
|
testnet: テスト
|
||||||
|
connections: 接続
|
||||||
|
node: 統合ノード
|
||||||
|
metrics: 指標
|
||||||
|
mining: マイニング
|
||||||
|
settings: ノード設定
|
||||||
|
enable_node: ノードを有効化
|
||||||
|
autorun: 自動起動
|
||||||
|
disabled_server: '画面左上の%{dots}を押して統合ノードを有効にするか、別の接続方法を追加してください。'
|
||||||
|
no_ips: お使いのシステムに利用可能なIPアドレスがないため、サーバーを起動できません。ネットワーク接続を確認してください。
|
||||||
|
available: 利用可能
|
||||||
|
not_available: 利用不可
|
||||||
|
availability_check: 利用可否の確認
|
||||||
|
android_warning: Androidをお使いの方へ。統合ノードを正しく同期させるには、スマートフォンのシステム設定でGoblinアプリの通知アクセスを許可し、バッテリー使用の制限を解除する必要があります。これはアプリがバックグラウンドで正しく動作するために必要な操作です。
|
||||||
|
sync_status:
|
||||||
|
node_restarting: ノードを再起動中です
|
||||||
|
node_down: ノードがダウンしています
|
||||||
|
initial: ノードを起動中です
|
||||||
|
no_sync: ノードは稼働中です
|
||||||
|
awaiting_peers: ピアを待っています
|
||||||
|
header_sync: ヘッダーをダウンロード中
|
||||||
|
header_sync_percent: 'ヘッダーをダウンロード中: %{percent}%'
|
||||||
|
tx_hashset_pibd: 状態をダウンロード中(PIBD)
|
||||||
|
tx_hashset_pibd_percent: '状態をダウンロード中(PIBD): %{percent}%'
|
||||||
|
tx_hashset_download: 状態をダウンロード中
|
||||||
|
tx_hashset_download_percent: '状態をダウンロード中: %{percent}%'
|
||||||
|
tx_hashset_setup_history: '状態を準備中(履歴): %{percent}%'
|
||||||
|
tx_hashset_setup_position: '状態を準備中(位置): %{percent}%'
|
||||||
|
tx_hashset_setup: 状態を準備中
|
||||||
|
tx_hashset_range_proofs_validation: '状態を検証中(範囲証明): %{percent}%'
|
||||||
|
tx_hashset_kernels_validation: '状態を検証中(カーネル): %{percent}%'
|
||||||
|
tx_hashset_save: チェーン状態を確定中
|
||||||
|
body_sync: ブロックをダウンロード中
|
||||||
|
body_sync_percent: 'ブロックをダウンロード中: %{percent}%'
|
||||||
|
shutdown: ノードを終了中です
|
||||||
|
network_node:
|
||||||
|
header: ヘッダー
|
||||||
|
block: ブロック
|
||||||
|
hash: ハッシュ
|
||||||
|
height: 高さ
|
||||||
|
difficulty: 難易度
|
||||||
|
time: 時刻
|
||||||
|
main_pool: メインプール
|
||||||
|
stem_pool: ステムプール
|
||||||
|
data: データ
|
||||||
|
size: サイズ(GB)
|
||||||
|
peers: ピア
|
||||||
|
error_clean: ノードデータが破損しました。再同期が必要です。
|
||||||
|
resync: 再同期
|
||||||
|
error_p2p_api: '%{p2p_api}サーバーの初期化中にエラーが発生しました。画面下部の%{settings}を選択して%{p2p_api}の設定を確認してください。'
|
||||||
|
error_config: '設定の初期化中にエラーが発生しました。画面下部の%{settings}を選択して設定を確認してください。'
|
||||||
|
error_unknown: '初期化中にエラーが発生しました。画面下部の%{settings}を選択して統合ノードの設定を確認するか、再同期してください。'
|
||||||
|
network_metrics:
|
||||||
|
loading: 指標は同期が完了すると利用できます
|
||||||
|
emission: 発行量
|
||||||
|
inflation: インフレ率
|
||||||
|
supply: 供給量
|
||||||
|
block_time: ブロックタイム
|
||||||
|
reward: 報酬
|
||||||
|
difficulty_window: '難易度ウィンドウ %{size}'
|
||||||
|
network_mining:
|
||||||
|
loading: マイニングは同期が完了すると利用できます
|
||||||
|
info: 'マイニングサーバーが有効になっています。画面下部の%{settings}を選択して設定を変更できます。デバイスが接続されるとデータが更新されます。'
|
||||||
|
restart_server_required: 変更を適用するにはサーバーの再起動が必要です。
|
||||||
|
rewards_wallet: 報酬受取用ウォレット
|
||||||
|
server: Stratumサーバー
|
||||||
|
address: アドレス
|
||||||
|
miners: マイナー
|
||||||
|
devices: デバイス
|
||||||
|
blocks_found: 発見したブロック数
|
||||||
|
hashrate: 'ハッシュレート(C%{bits})'
|
||||||
|
connected: 接続済み
|
||||||
|
disconnected: 切断済み
|
||||||
|
network_settings:
|
||||||
|
change_value: 値を変更
|
||||||
|
stratum_ip: 'Stratum IPアドレス:'
|
||||||
|
stratum_port: 'Stratumポート:'
|
||||||
|
port_unavailable: 指定したポートは使用できません
|
||||||
|
restart_node_required: 変更を適用するにはノードの再起動が必要です。
|
||||||
|
choose_wallet: ウォレットを選択
|
||||||
|
stratum_wallet_warning: 報酬を受け取るにはウォレットを開いておく必要があります。
|
||||||
|
enable: 有効化
|
||||||
|
disable: 無効化
|
||||||
|
restart: 再起動
|
||||||
|
server: サーバー
|
||||||
|
api_ip: 'API IPアドレス:'
|
||||||
|
api_port: 'APIポート:'
|
||||||
|
api_secret: 'REST APIおよびV2 Owner APIトークン:'
|
||||||
|
foreign_api_secret: 'Foreign APIトークン:'
|
||||||
|
disabled: 無効
|
||||||
|
enabled: 有効
|
||||||
|
ftl: 'Future Time Limit(FTL):'
|
||||||
|
ftl_description: 新しいブロックが承認されるために許容される、ノードのローカル時刻から見た未来方向のタイムスタンプのずれの上限(秒単位)です。
|
||||||
|
not_valid_value: 入力した値が正しくありません
|
||||||
|
full_validation: 完全検証
|
||||||
|
full_validation_description: 各ブロックの処理時にチェーン全体の完全な検証を実行するかどうかです(同期中を除く)。
|
||||||
|
archive_mode: アーカイブモード
|
||||||
|
archive_mode_desc: ノードを完全アーカイブモードで実行します(同期により多くのディスク容量と時間が必要になります)。
|
||||||
|
attempt_time: 'マイニング試行時間(秒):'
|
||||||
|
attempt_time_desc: 特定のヘッダーでマイニングを試みてから、いったん停止してプールから取引を再収集するまでの時間です
|
||||||
|
min_share_diff: '許容される最小シェア難易度:'
|
||||||
|
reset_settings_desc: ノード設定を初期値にリセットします
|
||||||
|
reset_settings: 設定をリセット
|
||||||
|
reset: リセット
|
||||||
|
tx_pool: 取引プール
|
||||||
|
pool_fee: 'プールに受け入れられる基本手数料:'
|
||||||
|
reorg_period: '再編成キャッシュの保持期間(分):'
|
||||||
|
max_tx_pool: 'プール内の取引の最大数:'
|
||||||
|
max_tx_stempool: 'ステムプール内の取引の最大数:'
|
||||||
|
max_tx_weight: 'ブロック生成のために選択できる取引の最大合計ウェイト:'
|
||||||
|
epoch_duration: 'エポック期間(秒):'
|
||||||
|
embargo_timer: 'エンバーゴタイマー(秒):'
|
||||||
|
aggregation_period: '集約期間(秒):'
|
||||||
|
stem_probability: 'ステムフェーズの確率:'
|
||||||
|
stem_txs: ステム取引
|
||||||
|
p2p_server: P2Pサーバー
|
||||||
|
p2p_port: 'P2Pポート:'
|
||||||
|
add_seed: DNSシードを追加
|
||||||
|
seed_address: 'DNSシードアドレス:'
|
||||||
|
add_peer: ピアを追加
|
||||||
|
peer_address: 'ピアアドレス:'
|
||||||
|
peer_address_error: 'IPアドレスまたはDNS名を正しい形式で入力してください(指定したホストが利用可能であることを確認してください)。例: 192.168.0.1:1234 または example.com:5678'
|
||||||
|
default: デフォルト
|
||||||
|
allow_list: 許可リスト
|
||||||
|
allow_list_desc: このリストにあるピアにのみ接続します。
|
||||||
|
deny_list: 拒否リスト
|
||||||
|
deny_list_desc: このリストにあるピアには接続しません。
|
||||||
|
favourites: お気に入り
|
||||||
|
favourites_desc: 優先的に接続するピアのリストです。
|
||||||
|
ban_window: 'BANされたピアがBAN状態を維持する時間(秒):'
|
||||||
|
ban_window_desc: BANするかどうかは、ピアから受信したデータの正確性に基づいてノードが判断します。
|
||||||
|
max_inbound_count: '受信ピア接続の最大数:'
|
||||||
|
max_outbound_count: '送信ピア接続の最大数:'
|
||||||
|
reset_data_desc: ノードデータをリセットします。同期に問題がある場合にのみ、注意して使用してください。
|
||||||
|
reset_data: データをリセット
|
||||||
|
modal:
|
||||||
|
cancel: キャンセル
|
||||||
|
save: 保存
|
||||||
|
add: 追加
|
||||||
|
modal_exit:
|
||||||
|
description: 本当にアプリを終了しますか?
|
||||||
|
exit: 終了
|
||||||
|
app_settings:
|
||||||
|
proxy: プロキシ
|
||||||
|
proxy_desc: アプリからのネットワークリクエストにプロキシを使用するかどうかです。
|
||||||
|
keyboard:
|
||||||
|
1: 1
|
||||||
|
2: 2
|
||||||
|
3: 3
|
||||||
|
4: 4
|
||||||
|
5: 5
|
||||||
|
6: 6
|
||||||
|
7: 7
|
||||||
|
8: 8
|
||||||
|
9: 9
|
||||||
|
0: 0
|
||||||
|
01: '-'
|
||||||
|
q: q
|
||||||
|
w: w
|
||||||
|
e: e
|
||||||
|
r: r
|
||||||
|
t: t
|
||||||
|
y: y
|
||||||
|
u: u
|
||||||
|
i: i
|
||||||
|
o: o
|
||||||
|
p: p
|
||||||
|
p1: '"'
|
||||||
|
a: a
|
||||||
|
s: s
|
||||||
|
d: d
|
||||||
|
f: f
|
||||||
|
g: g
|
||||||
|
h: h
|
||||||
|
j: j
|
||||||
|
k: k
|
||||||
|
l: l
|
||||||
|
l1: \
|
||||||
|
l2: ':'
|
||||||
|
z: z
|
||||||
|
x: x
|
||||||
|
c: c
|
||||||
|
v: v
|
||||||
|
b: b
|
||||||
|
n: n
|
||||||
|
m: m
|
||||||
|
m1: ','
|
||||||
|
m2: .
|
||||||
|
m3: /
|
||||||
|
goblin:
|
||||||
|
home:
|
||||||
|
anonymous: "匿名"
|
||||||
|
connected_tor: "Tor経由で接続済み"
|
||||||
|
tor_ready: "Tor準備完了 · リレー…"
|
||||||
|
connecting_tor: "Torに接続中…"
|
||||||
|
connected_direct: "接続済み(直接)"
|
||||||
|
connecting_direct: "接続中(直接)…"
|
||||||
|
cant_reach_node: "ノードに接続できません"
|
||||||
|
node_synced: "ノード同期済み"
|
||||||
|
syncing: "同期中…"
|
||||||
|
balance_updating: "残高を更新中…"
|
||||||
|
balance_stale: "ノードに接続できません · 最終確認済みの残高"
|
||||||
|
fiat_unavailable: "レート取得不可"
|
||||||
|
listening: "支払いの受信を待機中"
|
||||||
|
block: "ブロック %{height}"
|
||||||
|
waiting_for_chain: "チェーンを待機中…"
|
||||||
|
nav_wallet: "ウォレット"
|
||||||
|
nav_pay: "支払う"
|
||||||
|
nav_activity: "アクティビティ"
|
||||||
|
nav_receive: "受け取る"
|
||||||
|
nav_settings: "設定"
|
||||||
|
back_again: "もう一度戻るを押すとウォレットを切り替えます"
|
||||||
|
activity: "アクティビティ"
|
||||||
|
news: "お知らせ"
|
||||||
|
empty_title: "まだアクティビティがありません"
|
||||||
|
empty_sub: "grinを送るか受け取って始めましょう。"
|
||||||
|
recent: "最近"
|
||||||
|
scan_to_pay: "スキャンして支払う"
|
||||||
|
type_amount: "金額を入力"
|
||||||
|
request: "リクエスト"
|
||||||
|
pay: "支払う"
|
||||||
|
enter_amount: "支払うかリクエストする金額を入力してください"
|
||||||
|
activity:
|
||||||
|
canceled: "キャンセル済み"
|
||||||
|
pending: "保留中"
|
||||||
|
earlier: "それ以前"
|
||||||
|
today: "今日"
|
||||||
|
yesterday: "昨日"
|
||||||
|
title: "アクティビティ"
|
||||||
|
requests: "リクエスト"
|
||||||
|
empty_title: "まだアクティビティがありません"
|
||||||
|
empty_sub: "支払いの履歴がここに表示されます。"
|
||||||
|
pending_header: "保留中"
|
||||||
|
receipt:
|
||||||
|
title: "領収書"
|
||||||
|
not_found: "取引が見つかりません"
|
||||||
|
for_note: "%{note}について"
|
||||||
|
details: "取引の詳細"
|
||||||
|
canceled: "キャンセル済み"
|
||||||
|
expired: "期限切れ"
|
||||||
|
funds_returned: "資金は返却されました"
|
||||||
|
complete: "完了"
|
||||||
|
payment_received: "支払いを受け取りました"
|
||||||
|
payment_sent: "支払いを送信しました"
|
||||||
|
pending: "保留中"
|
||||||
|
confs: "確認 %{c}/%{r}"
|
||||||
|
waiting_to_confirm: "確認を待っています"
|
||||||
|
paying: "支払い中…"
|
||||||
|
you: "自分"
|
||||||
|
to: "宛先"
|
||||||
|
from: "送信元"
|
||||||
|
nostr: "nostr"
|
||||||
|
identity: "アイデンティティ"
|
||||||
|
fee_none: "なし"
|
||||||
|
network_fee: "ネットワーク手数料"
|
||||||
|
privacy: "プライバシー"
|
||||||
|
privacy_value: "Mimblewimble + Tor"
|
||||||
|
transaction: "取引"
|
||||||
|
cancel_request: "リクエストをキャンセル"
|
||||||
|
cancel_send: "支払いをキャンセル"
|
||||||
|
stale_note: "この支払いはしばらく保留状態です。"
|
||||||
|
cancel_send_confirm: "もう一度タップしてキャンセル — 相手にはすでに届いている可能性があります"
|
||||||
|
cancel_send_done: "支払いをキャンセルしました — 資金は再び利用可能です"
|
||||||
|
cancel_send_too_late: "この支払いはすでに完了しており、キャンセルできません"
|
||||||
|
waiting_to_receive: "%{name}が受け取るのを待っています…"
|
||||||
|
request:
|
||||||
|
title: "%{name}のリクエスト"
|
||||||
|
approve: "承認"
|
||||||
|
decline: "拒否"
|
||||||
|
review_title: "リクエストを確認"
|
||||||
|
hold_to_accept: "長押しで承認"
|
||||||
|
hold_accept_hint: "長押ししてこのリクエストに支払う"
|
||||||
|
receive:
|
||||||
|
title: "受け取る"
|
||||||
|
requesting: "%{amt}%{tsu}をリクエスト中 — 共有して支払いを受け取りましょう"
|
||||||
|
clear_request: "リクエストを消去"
|
||||||
|
share_handle: "ハンドルを共有して支払いを受け取りましょう"
|
||||||
|
share_npub: "npubを共有して支払いを受け取りましょう"
|
||||||
|
copied: "コピーしました"
|
||||||
|
copy_nostr_id: "nostr IDをコピー"
|
||||||
|
copy_address: "アドレスをコピー"
|
||||||
|
copy: "コピー"
|
||||||
|
share_message: "Goblin(goblin.st)で支払ってください — %{npub}"
|
||||||
|
privacy_note: "ユーザー名は公開されます。支払い内容はネットワーク上で暗号化されたままです。"
|
||||||
|
privacy_note_npub: "npubは公開されます。支払い内容はネットワーク上で暗号化されたままです。"
|
||||||
|
profile:
|
||||||
|
title: "プロフィール"
|
||||||
|
activity: "アクティビティ"
|
||||||
|
no_activity: "この相手とのアクティビティはまだありません。"
|
||||||
|
unblock: "ブロック解除"
|
||||||
|
block: "ブロック"
|
||||||
|
blocked_blurb: "ブロック済み — 相手の支払いとリクエストは破棄されます。"
|
||||||
|
block_blurb: "ブロックすると、相手からの支払いとリクエストは破棄されます。"
|
||||||
|
settings:
|
||||||
|
title: "設定"
|
||||||
|
connected_nostr: "nostrに接続済み"
|
||||||
|
connecting_relays: "リレーに接続中…"
|
||||||
|
identity: "アイデンティティ"
|
||||||
|
copy_npub: "npubをコピー(公開)"
|
||||||
|
backup_note: "機種変更しますか? 両方をバックアップしてください: リカバリーフレーズ(資金)と、アイデンティティの.backupファイル(名前 + キー)。"
|
||||||
|
wallet: "ウォレット"
|
||||||
|
display_unit: "表示単位"
|
||||||
|
relays: "リレー"
|
||||||
|
nostr_relays: "Nostrリレー"
|
||||||
|
node: "ノード"
|
||||||
|
min_conf: "最小確認数"
|
||||||
|
integrated_node: "統合ノードの設定"
|
||||||
|
node_advanced: "詳細設定"
|
||||||
|
slatepacks: "Slatepack"
|
||||||
|
slatepacks_value: "手動取引"
|
||||||
|
trusted_sites: "信頼済みサイト"
|
||||||
|
lock_wallet: "ウォレットをロック"
|
||||||
|
switch_wallet: "ウォレットを切り替え"
|
||||||
|
advanced: "詳細設定"
|
||||||
|
privacy: "プライバシー"
|
||||||
|
tor_routing: "Torルーティング"
|
||||||
|
messages_lookups: "メッセージと検索"
|
||||||
|
tor_on: "オン"
|
||||||
|
tor_off: "オフ"
|
||||||
|
auto_accept: "自動承認"
|
||||||
|
pairing: "価格通貨"
|
||||||
|
accept_anyone: "誰でも"
|
||||||
|
accept_contacts: "連絡先のみ"
|
||||||
|
accept_ask: "毎回確認"
|
||||||
|
requests: "リクエスト"
|
||||||
|
incoming_requests: "受信リクエスト"
|
||||||
|
incoming_requests_sub: "他の人が自分にお金をリクエストできるようにします"
|
||||||
|
hide_amounts: "金額を非表示"
|
||||||
|
hide_amounts_sub: "通知で受信金額を非表示にします"
|
||||||
|
language: "言語"
|
||||||
|
update_available: "アップデートあり"
|
||||||
|
appearance: "外観"
|
||||||
|
theme: "テーマ"
|
||||||
|
theme_light: "ライト"
|
||||||
|
theme_dark: "ダーク"
|
||||||
|
theme_yellow: "イエロー"
|
||||||
|
archive: "アーカイブ"
|
||||||
|
export_archive: "アーカイブをエクスポート"
|
||||||
|
export_archive_caption: "連絡先、支払い履歴、リクエストを JSON 形式でクリップボードにコピーします。"
|
||||||
|
wipe_history: "支払い履歴を消去"
|
||||||
|
wipe_history_confirm: "もう一度タップして消去 — 元に戻せません"
|
||||||
|
about: "アプリについて"
|
||||||
|
goblin: "Goblin"
|
||||||
|
build: "ビルド %{build}"
|
||||||
|
network: "ネットワーク"
|
||||||
|
network_value: "MW + Tor + nostr"
|
||||||
|
third_party: "サードパーティ"
|
||||||
|
grim: "GRIM(アップストリームウォレット)"
|
||||||
|
grin_node: "Grinノード"
|
||||||
|
sp_intro: "詳細設定 — GRIMと同じように、生のslatepackを手動でやり取りします。ユーザー名で支払いや受け取りができない場合にのみ使用してください。"
|
||||||
|
sp_receive_group: "受け取り・確定"
|
||||||
|
sp_receive_blurb: "誰かから受け取ったslatepackを貼り付けてください。Goblinが支払いを受け取ったり、請求書を支払ったり、確定して送信したりします。"
|
||||||
|
sp_process: "slatepackを処理"
|
||||||
|
sp_paste_first: "まずslatepackを貼り付けてください。"
|
||||||
|
sp_reply_ready: "返信の準備ができました — 送信者に送り返してください。"
|
||||||
|
sp_finalizing: "確定してチェーンに送信中…"
|
||||||
|
sp_create_group: "支払いを作成"
|
||||||
|
sp_create_blurb: "誰かに渡すためのslatepackを作成します。相手がそれを受け取って返信を送り返したら、上で確定してください。"
|
||||||
|
sp_amount_hint: "金額(grin)"
|
||||||
|
sp_addr_hint: "受取人のアドレス(任意)"
|
||||||
|
sp_create: "slatepackを作成"
|
||||||
|
sp_ready: "slatepackの準備ができました — 受取人に渡してください。"
|
||||||
|
sp_amount_gt_zero: "0より大きい金額を入力してください。"
|
||||||
|
sp_to_send: "送信するslatepack"
|
||||||
|
sp_copy: "slatepackをコピー"
|
||||||
|
cancel: "キャンセル"
|
||||||
|
continue: "続ける"
|
||||||
|
final_confirmation: "最終確認"
|
||||||
|
type_reset: "RESETと入力"
|
||||||
|
wallet_password: "ウォレットのパスワード"
|
||||||
|
done: "完了"
|
||||||
|
close: "閉じる"
|
||||||
|
import_nsec_hint: "nsec1…、または貼り付けたバックアップ"
|
||||||
|
backup_password_hint: "バックアップのパスワード(他の場所でエクスポートした場合のみ)"
|
||||||
|
import_btn: "インポート"
|
||||||
|
importing: "インポート中…"
|
||||||
|
import_failed: "インポートに失敗しました"
|
||||||
|
name_authority: "名前認証局"
|
||||||
|
name_authority_title: "名前認証局を変更"
|
||||||
|
name_authority_blurb: "名前を登録・検証するサーバーです。別のインスタンスを指定すると、そこでホストされている名前を使用・支払いに利用できます。"
|
||||||
|
name_authority_invalid: "完全なURLを入力してください(https://…)。"
|
||||||
|
reset: "リセット"
|
||||||
|
save: "保存"
|
||||||
|
backup_file: "ファイルにバックアップ"
|
||||||
|
choose_backup_file: ".backupファイルを選択"
|
||||||
|
backup_read_failed: "そのファイルを読み込めませんでした。"
|
||||||
|
backup_saved: "バックアップを保存しました"
|
||||||
|
backup_saved_sub: ".backupファイルは安全に保管してください。このファイルとパスワードの両方を持つ人は誰でもウォレット全体を復元できます。"
|
||||||
|
backup_file_title: "ウォレットをバックアップ"
|
||||||
|
backup_file_blurb: "ウォレットのシードとすべてのアイデンティティを含む暗号化された.backupファイルを1つ作成します。ウォレットのパスワードを入力して保護してください。"
|
||||||
|
backup_write_failed: "ファイルを保存できませんでした。"
|
||||||
|
create_backup: "バックアップを作成"
|
||||||
|
registered: "%{name}を登録しました"
|
||||||
|
released_msg: "解放されました — この名前は誰でも取得できます"
|
||||||
|
release_confirm: "%{name}を解放しますか?"
|
||||||
|
release_blurb: "解放された瞬間、誰でも取得できるようになります — 次にローテーションするキーも例外ではありません。10分間は別のユーザー名を登録できなくなります。"
|
||||||
|
releasing: "解放中…"
|
||||||
|
keep_it: "維持する"
|
||||||
|
release_it: "解放する"
|
||||||
|
username: "ユーザー名"
|
||||||
|
username_note: "あなたの名前として表示されます。goblin.stで公開されます。支払いは暗号化されたままです。"
|
||||||
|
release_username: "ユーザー名を解放"
|
||||||
|
pick_username: "ユーザー名を選択 — 任意"
|
||||||
|
working: "処理中…"
|
||||||
|
claim: "登録"
|
||||||
|
err_just_taken: "そのユーザー名はたった今取得されました"
|
||||||
|
err_cooldown: "最近ユーザー名を解放しました — 10分以内に新しい名前を登録できます。"
|
||||||
|
err_unreachable: "goblin.stに接続できませんでした — 接続に問題が発生しました。もう一度お試しください。"
|
||||||
|
err_release: "解放できませんでした: %{err}"
|
||||||
|
avail_available: "利用可能です!"
|
||||||
|
avail_taken: "使用済み"
|
||||||
|
avail_reserved: "予約済み"
|
||||||
|
avail_invalid: "名前は3~20文字: a~z、0~9、_ または -"
|
||||||
|
avail_quarantined: "利用できません"
|
||||||
|
avail_unknown: "確認できませんでした — 接続に問題が発生しました。もう一度お試しください。"
|
||||||
|
username_none: "未設定"
|
||||||
|
advanced_privacy: "詳細プライバシー"
|
||||||
|
tap_reveal: "タップして表示"
|
||||||
|
notif_someone: "誰か"
|
||||||
|
notif_private_received: "支払いを受け取りました。Goblinを開いて確認してください。"
|
||||||
|
notif_private_requested: "新しい支払いリクエストがあります。Goblinを開いて確認してください。"
|
||||||
|
username:
|
||||||
|
title: "ユーザー名"
|
||||||
|
authority: "名前認証局"
|
||||||
|
authority_blurb: "デフォルトのままにするか、別のインスタンスを指定して、そこでホストされている名前を使用してください。"
|
||||||
|
learn_more: "詳しく見る"
|
||||||
|
custom: "カスタム認証局"
|
||||||
|
advprivacy:
|
||||||
|
intro: "通知に表示する内容を制御し、この端末上で残高と履歴を非表示にします。"
|
||||||
|
notifications: "通知"
|
||||||
|
hide_names: "名前を非表示"
|
||||||
|
hide_names_sub: "通知で支払った相手の名前を省略します"
|
||||||
|
hide_details: "すべての詳細を非表示"
|
||||||
|
hide_details_sub: "名前や金額を含まない、非公開の通知のみを表示します"
|
||||||
|
anon: "匿名モード"
|
||||||
|
anon_toggle: "残高とアクティビティをぼかす"
|
||||||
|
anon_sub: "タップして表示するまで、残高とアクティビティは点で表示されます"
|
||||||
|
advanced:
|
||||||
|
title: "詳細設定"
|
||||||
|
intro: "GRIM由来の低レベルなウォレットツールです。通常は必要ありません。"
|
||||||
|
own_node_desc: "公開ノードを信頼する代わりに、この端末でGrinのフルノードを同期します。"
|
||||||
|
own_node_active: "自分のノードを実行中"
|
||||||
|
repair: "ウォレットを修復"
|
||||||
|
repair_desc: "チェーンを再スキャンし、欠落しているアウトプットを復元します。しばらく時間がかかることがあります。"
|
||||||
|
repair_unavailable: "先に同期済みのノードへの接続が必要です。"
|
||||||
|
repairing: "修復中… %{pct}%"
|
||||||
|
restore: "ウォレットを復元"
|
||||||
|
restore_desc: "ローカルデータを削除し、リカバリーフレーズから再構築します。修復で解決しない場合に使用してください — その後ウォレットを再度開くことになります。"
|
||||||
|
restore_confirm: "もう一度タップして復元"
|
||||||
|
show_phrase: "リカバリーフレーズ"
|
||||||
|
phrase_desc: "24個のgrinリカバリー単語です — 資金を復元する唯一の方法です。オフラインで、他人に見せずに保管してください。"
|
||||||
|
reveal: "フレーズを表示"
|
||||||
|
hide: "非表示"
|
||||||
|
password: "ウォレットのパスワード"
|
||||||
|
wrong_password: "パスワードが間違っています。"
|
||||||
|
delete: "ウォレットを削除"
|
||||||
|
delete_desc: "この端末からこのウォレットとすべてのアイデンティティを完全に削除します。先にバックアップしてください。リカバリーフレーズがなければ資金は復元できません。"
|
||||||
|
delete_confirm: "もう一度タップして削除"
|
||||||
|
manage_node: "ノード接続を管理"
|
||||||
|
repair_confirm: "はい、今すぐ修復する"
|
||||||
|
repair_confirm_note: "修復はチェーンを再スキャンするため、数分かかることがあります。"
|
||||||
|
restore_confirm_note: "ローカルデータを消去し、リカバリーフレーズから再構築します — 数分かかることがあります。"
|
||||||
|
nostr_key: "Nostrキー"
|
||||||
|
nostr_key_desc: "nostrアイデンティティの秘密鍵であるnsecです。コピーするかQRコードを表示して、magick.marketのようなnostrアプリにログインできます。これを持つ人は誰でもあなたのアイデンティティを操作できるため、他人に見せないよう保管してください。"
|
||||||
|
reveal_nsec: "キーを表示"
|
||||||
|
copy_nsec: "nsecをコピー"
|
||||||
|
show_qr: "QRを表示"
|
||||||
|
hide_qr: "QRを非表示"
|
||||||
|
nostr_section: "詳細なnostr設定"
|
||||||
|
backup_caption: "ウォレットとすべてのアイデンティティが含まれます。"
|
||||||
|
danger_zone: "危険ゾーン"
|
||||||
|
delete_warning: "この端末のリカバリーフレーズとすべてのアイデンティティが消去されます。バックアップしていないものは完全に失われます。"
|
||||||
|
download_backup: "バックアップをダウンロード"
|
||||||
|
delete_final: "完全に削除"
|
||||||
|
privacy:
|
||||||
|
title: "ネットワークプライバシー"
|
||||||
|
intro: "Goblinがネットワーク経由で送信する情報はごくわずかで、そのすべてがエンドツーエンド暗号化で封印されるため、リレーは内容を読むことも、あなたに結び付けることもできません。"
|
||||||
|
payments: "支払い"
|
||||||
|
payments_blurb: "slatepackを運ぶすべてのnostrメッセージです。"
|
||||||
|
usernames: "ユーザー名"
|
||||||
|
usernames_blurb: "名前認証局との間で行うNIP-05の名前検索です。"
|
||||||
|
price_avatars: "価格"
|
||||||
|
price_avatars_blurb: "金額の隣に表示されるリアルタイムの法定通貨レートです。"
|
||||||
|
grin_node: "Grinノード"
|
||||||
|
grin_node_blurb: "ブロックを同期し、あなたの取引をネットワークにブロードキャストします。誰にとっても同じ公開チェーンデータであり、あなたのアイデンティティとは決して結び付きません。"
|
||||||
|
how_privacy: "プライバシーの仕組み"
|
||||||
|
how_privacy_blurb: "支払いとメッセージは認証付き暗号化で封印され、プライベートメッセージとして届けられます。そのためリレーはそれらを読み取ることも、支払いをあなたに結び付けることもできません。"
|
||||||
|
always_direct: "常に直接接続"
|
||||||
|
tor_off: "Torオフ"
|
||||||
|
tor_on: "Torオン"
|
||||||
|
tor_switch: "Tor経由でルーティング"
|
||||||
|
tor_switch_sub: "リレーからIPアドレスを隠します。"
|
||||||
|
vpn_note: "Torがオフの場合、リレーはあなたのIPアドレスを見ることができます。少なくともVPNの利用をお勧めします。"
|
||||||
|
pairing:
|
||||||
|
title: "価格通貨"
|
||||||
|
intro: "残高や金額を表示する際の基準となる通貨です。"
|
||||||
|
pair_with: "通貨を選択"
|
||||||
|
rates_note: "レートはペアリングがオンのときのみTor経由で取得されます — オフの場合、レートのリクエストは端末から送信されません。"
|
||||||
|
relays:
|
||||||
|
title: "リレー"
|
||||||
|
intro: "支払いメッセージは以下のすべてのリレーに複製されます。1つでも接続可能なリレーがあれば受信できます。"
|
||||||
|
your_relays: "自分のリレー"
|
||||||
|
add_relay: "リレーを追加"
|
||||||
|
add_relay_btn: "リレーを追加"
|
||||||
|
save_reconnect: "保存して再接続"
|
||||||
|
none: "なし"
|
||||||
|
count: "リレー %{n}件"
|
||||||
|
node:
|
||||||
|
title: "ノード"
|
||||||
|
connection: "接続"
|
||||||
|
integrated: "統合ノード"
|
||||||
|
applies_after: "ウォレットを一度ロックしてから再度ロック解除すると適用されます。"
|
||||||
|
add_external: "外部ノードを追加"
|
||||||
|
api_secret_hint: "APIシークレット(任意)"
|
||||||
|
add_node: "ノードを追加"
|
||||||
|
integrated_host: "統合ノード"
|
||||||
|
summary_syncing: "%{conn} · 同期中"
|
||||||
|
summary_block: "ブロック %{height} · %{conn}"
|
||||||
|
nips:
|
||||||
|
title: "nostrとNIP"
|
||||||
|
intro1: "Goblinはnostrを利用します — 署名されたメッセージをシンプルなリレーサーバー経由でやり取りするオープンなプロトコルです。あなたのウォレットは独自のnostrアイデンティティを持っています。これは資金やリカバリーフレーズとは意図的に切り離された、単独のランダムなキーです。すべての支払いは、アイデンティティ間のエンドツーエンド暗号化されたダイレクトメッセージとして送られ、その中にslatepackが含まれています。"
|
||||||
|
intro2: "goblin.stはGoblinのネームサービスです。ユーザー名を登録すると、名前→キーのマッピングがそこに公開され(NIP-05)、長いnpubの代わりに名前で支払いを受けられるようになります。ユーザー名は公開されますが、支払い内容は決して公開されません。NIPはこのプロトコルを構成する要素です — タップして仕様を読んでみましょう。"
|
||||||
|
n05_title: "名前"
|
||||||
|
n05_blurb: "username@goblin.stをあなたのキーに紐付け、ハンドルがアドレスのように機能するようにします。"
|
||||||
|
n17_title: "プライベートメッセージ"
|
||||||
|
n17_blurb: "すべての支払いが送られる、暗号化されたDMの封筒です。"
|
||||||
|
n44_title: "暗号化"
|
||||||
|
n44_blurb: "それらのメッセージ内で使用される認証付き暗号方式です。"
|
||||||
|
n49_title: "キーの暗号化"
|
||||||
|
n49_blurb: "秘密鍵が保存時にパスワードでロックされて保管される方法です。"
|
||||||
|
n59_title: "ギフトラップ"
|
||||||
|
n59_blurb: "メッセージを包み、誰が誰と話しているのかをリレーが見られないようにします。"
|
||||||
|
n98_title: "HTTP認証"
|
||||||
|
n98_blurb: "goblin.stへのユーザー名登録リクエストに署名します。"
|
||||||
|
batch:
|
||||||
|
title: "%{n}件の請求書を承認"
|
||||||
|
blurb: "%{name}に対して、それぞれ%{amount}の支払いリクエストを%{n}件発行します — 合計%{total}。各リクエストには新しい支払いアドレスが割り当てられます。"
|
||||||
|
approve: "承認"
|
||||||
|
login:
|
||||||
|
title: "サインイン"
|
||||||
|
headline: "%{domain}にサインインしますか?"
|
||||||
|
identity: "サインインするアイデンティティ:"
|
||||||
|
explain: "これは一度限りのログインリクエストに署名します。キーが共有されることも、サイトがあなたに成り代わって行動することもありません。"
|
||||||
|
pass_prompt: "サインインするにはウォレットのパスワードを入力してください。"
|
||||||
|
confirm: "サインイン"
|
||||||
|
sent: "%{domain}にサインインしました"
|
||||||
|
failed: "%{domain}に接続できませんでした。サインインは届いていません。"
|
||||||
|
authorize:
|
||||||
|
title: "承認"
|
||||||
|
headline: "%{domain}を承認しますか?"
|
||||||
|
kind_post: "公開投稿"
|
||||||
|
kind_repost: "リポスト"
|
||||||
|
kind_reaction: "リアクション"
|
||||||
|
kind_article: "記事(長文)"
|
||||||
|
kind_unknown: "種類 %{n}(未確認)"
|
||||||
|
unknown_caution: "Goblinはこのイベントの種類を認識できません。%{domain}を信頼できる場合にのみ承認してください。"
|
||||||
|
identity: "署名するアイデンティティ:"
|
||||||
|
explain: "承認すると、このイベント1件に署名して%{domain}に渡します。Goblinが何かを公開することはなく、キーが共有されることもなく、サイトがセッションや今後の権限を得ることもありません。"
|
||||||
|
pass_prompt: "承認するにはウォレットのパスワードを入力してください。"
|
||||||
|
confirm: "承認"
|
||||||
|
sent: "%{domain}を承認しました"
|
||||||
|
failed: "%{domain}に接続できませんでした。何も送信されていません。"
|
||||||
|
truncated: "省略、他%{n}文字"
|
||||||
|
show_full: "全文を表示"
|
||||||
|
show_less: "折りたたむ"
|
||||||
|
replying_to: "%{id}への返信"
|
||||||
|
mentions: "%{id}に言及"
|
||||||
|
repost_of: "%{id}をリポスト"
|
||||||
|
by_author: "%{id}による"
|
||||||
|
reacts_to: "%{id}にリアクション"
|
||||||
|
article_title: "タイトル: %{title}"
|
||||||
|
trust:
|
||||||
|
title: "信頼"
|
||||||
|
headline: "%{domain}を信頼しますか?"
|
||||||
|
identity: "サインインするアイデンティティ:"
|
||||||
|
lead: "%{domain}はこのセッション中、毎回確認することなくあなたの代わりに行動できます。お金が動く操作は必ず確認を求めます。"
|
||||||
|
grant_intro: "確認なしで署名できる操作:"
|
||||||
|
cat_social: "投稿とリアクション"
|
||||||
|
cat_dm: "ダイレクトメッセージ"
|
||||||
|
cat_market: "出品"
|
||||||
|
cat_identity: "プロフィールとリスト"
|
||||||
|
cat_delete: "削除"
|
||||||
|
cat_http: "アップロードとHTTP認証"
|
||||||
|
cat_unknown: "その他のイベントの種類(種類%{n})"
|
||||||
|
login_excluded: "ログイン権限がサイトに付与されることはありません。"
|
||||||
|
money_line: "お金が動く操作は必ずパスワードを求めます。無断で支出や販売が行われることはありません。"
|
||||||
|
duration: "このセッションの間のみ有効です。設定の信頼済みサイトからいつでも終了できます。"
|
||||||
|
pass_prompt: "このサイトを信頼するにはウォレットのパスワードを入力してください。"
|
||||||
|
confirm_hold: "長押しでこのセッションを信頼"
|
||||||
|
sent: "このセッションで%{domain}を信頼しています"
|
||||||
|
failed: "%{domain}に接続できませんでした。セッションは開始されていません。"
|
||||||
|
announce_failed: "信頼しましたが、%{domain}はまだセッションを確認していません。"
|
||||||
|
notice_volume: "信頼済みサイトが大量の署名を行っています。"
|
||||||
|
notice_decrypt: "信頼済みサイトがあなたのメッセージを読んでいます。"
|
||||||
|
money:
|
||||||
|
title: "確認"
|
||||||
|
headline: "%{domain}で確認しますか?"
|
||||||
|
identity: "%{id}として署名"
|
||||||
|
explain: "これは価値を移動または確定させる操作のため、必ず確認を求めます。承認するとこの1つの操作に署名します。"
|
||||||
|
pass_prompt: "確認するにはウォレットのパスワードを入力してください。"
|
||||||
|
confirm_hold: "長押しで確認"
|
||||||
|
encrypt_desc: "支払いを確定する、暗号化された注文メッセージです。"
|
||||||
|
trusted_sites:
|
||||||
|
title: "信頼済みサイト"
|
||||||
|
intro: "このセッションで信頼したサイトは、リスクの低い操作を確認なしで署名できます。お金が動く操作は必ず確認します。ここでいつでもセッションを終了できます。"
|
||||||
|
empty: "信頼済みサイトはありません。サインイン時にサイトを信頼すると、署名セッションが付与されます。"
|
||||||
|
none: "なし"
|
||||||
|
can_sign: "確認なしで署名可能: %{cats}"
|
||||||
|
time_left: "あと%{mins}分で終了"
|
||||||
|
paused: "一時停止中(署名が多いため)"
|
||||||
|
resume: "再開"
|
||||||
|
end: "セッションを終了"
|
||||||
|
identities:
|
||||||
|
title: "アイデンティティ"
|
||||||
|
switch_hint: "アイデンティティを切り替え"
|
||||||
|
blurb: "アイデンティティとは、支払いを受け取る際の名前です。すべてこの1つのウォレットに支払われます。新しいアイデンティティを追加したり、切り替えたり、不要なものを削除したりできます。"
|
||||||
|
privacy_note: "これらのアイデンティティは1つのウォレットと1つの残高を共有します — 別々の玄関口であり、別々の資金ではありません。本当に資金を分けたい場合は、別のウォレットを使用してください。"
|
||||||
|
held: "保有しているアイデンティティ"
|
||||||
|
add: "アイデンティティを追加"
|
||||||
|
add_title: "アイデンティティを追加"
|
||||||
|
nsec_hint: "nsecを貼り付け"
|
||||||
|
generate_note: "このウォレット用に、新しい匿名のキーが作成されます。"
|
||||||
|
generate: "生成"
|
||||||
|
import: "インポート"
|
||||||
|
choose_backup: ".backupファイルを選択"
|
||||||
|
backup_selected: "バックアップファイルを選択しました"
|
||||||
|
delete_short: "アイデンティティを削除"
|
||||||
|
delete_title: "%{name}を削除しますか?"
|
||||||
|
delete_blurb: "このウォレットからアイデンティティを完全に削除します。すでに受け取った資金は残高に残ります。"
|
||||||
|
delete_backup_note: "先にバックアップしてください — nsecや.backupファイルがなければ復元できません。"
|
||||||
|
delete_confirm: "削除"
|
||||||
|
manage_title: "アイデンティティを管理"
|
||||||
|
tag_note: "非公開の名前を付けましょう。自分にしか見えず、共有されることはありません。"
|
||||||
|
tag_hint: "非公開の名前"
|
||||||
|
tag_save: "保存"
|
||||||
|
pass_prompt: "ウォレットのパスワードで各アイデンティティが暗号化・解除されます。続けるには入力してください。"
|
||||||
|
working: "処理中…"
|
||||||
|
onboarding:
|
||||||
|
privacy:
|
||||||
|
kicker: "ネットワークプライバシー"
|
||||||
|
continue: "続ける"
|
||||||
|
intro:
|
||||||
|
private_money_head: "プライベートなお金"
|
||||||
|
private_money_body: "Goblinはgrin用のウォレットです — チェーン上に金額もアドレスも残らないデジタルキャッシュです。"
|
||||||
|
send_like_message_head: "メッセージのように送る"
|
||||||
|
send_like_message_body: "ユーザー名かnpubに支払うと、nostrとTor経由でエンドツーエンド暗号化されたメッセージとして届きます — 途中の誰にも金額や関係者を見られません。"
|
||||||
|
yours_alone_head: "あなただけのもの"
|
||||||
|
yours_alone_body: "キー、名前、履歴はすべてこの端末上にあります。GRIMウォレットをベースに構築されています。"
|
||||||
|
get_started: "始める"
|
||||||
|
footnote: "所要時間は約1分です。後からすべて変更できます。"
|
||||||
|
node:
|
||||||
|
kicker: "ステップ1/3 · ネットワーク"
|
||||||
|
title: "Goblinはどうやって
|
||||||
|
チェーンを監視しますか?"
|
||||||
|
own_title: "自分のノードを実行"
|
||||||
|
own_badge: "プライベート"
|
||||||
|
own_body: "誰も信頼しません — ウォレット自身がチェーンを確認します。セットアップを完了する間、バックグラウンドで同期します。"
|
||||||
|
connect_title: "ノードに接続"
|
||||||
|
connect_badge: "即時"
|
||||||
|
connect_body: "同期を待つ必要はありません。選んだノードはウォレットの問い合わせ内容を見ることができます。"
|
||||||
|
changeable: "設定 → ノードでいつでも変更できます。"
|
||||||
|
continue: "続ける"
|
||||||
|
url_invalid: "ノードのURLはhttp://またはhttps://で始まる必要があります"
|
||||||
|
wallet:
|
||||||
|
kicker: "ステップ2/3 · ウォレット"
|
||||||
|
title: "ウォレットをセットアップ"
|
||||||
|
create_new: "新規作成"
|
||||||
|
restore_from_seed: "リカバリーフレーズから復元"
|
||||||
|
import_identity: "アイデンティティをインポート"
|
||||||
|
import_identity_hint: "このステップの後で、既存のキー(.backupファイルまたはnsec)を取り込みます。"
|
||||||
|
name_hint: "ウォレット名"
|
||||||
|
password_hint: "パスワード"
|
||||||
|
repeat_password_hint: "パスワード(確認)"
|
||||||
|
restore_hint: "リカバリー単語を手元に準備してください — 次に入力します。"
|
||||||
|
create_hint: "次に、書き留めるべき24個のリカバリー単語が表示されます。これがお金そのものです — この単語を持つ人が資金を持つことになります。"
|
||||||
|
continue: "続ける"
|
||||||
|
passwords_no_match: "パスワードが一致しません"
|
||||||
|
words:
|
||||||
|
kicker: "ステップ2/3 · ウォレット"
|
||||||
|
title_restore: "リカバリー単語を入力してください"
|
||||||
|
title_create: "これらの単語を書き留めてください"
|
||||||
|
write_down_hint: "紙に、順番通りに書いてください。この単語を持つ人は誰でも資金を奪えます。単語がなければ、端末を失うことは資金を失うことを意味します。"
|
||||||
|
paste: "貼り付け"
|
||||||
|
scan_qr: "QRをスキャン"
|
||||||
|
copy_clipboard: "クリップボードにコピー(推奨しません)"
|
||||||
|
restore_wallet: "ウォレットを復元"
|
||||||
|
wrote_them_down: "書き留めました"
|
||||||
|
fill_every_word: "すべての単語を入力してください — 単語をタップして編集するか、フレーズを貼り付けてください。"
|
||||||
|
unlock_backup: "バックアップを解除"
|
||||||
|
backup_unlocked: "バックアップを解除しました。ウォレットを開くとアイデンティティが復元されます。"
|
||||||
|
confirm:
|
||||||
|
kicker: "ステップ2/3 · ウォレット"
|
||||||
|
title: "確認してみましょう"
|
||||||
|
enter_hint: "先ほど書き留めた単語を入力してください。単語をタップして入力します。"
|
||||||
|
paste: "貼り付け"
|
||||||
|
create_wallet: "ウォレットを作成"
|
||||||
|
keep_going: "続けてください — すべての単語を、順番通りに。"
|
||||||
|
identity:
|
||||||
|
kicker: "ステップ3/3 · アイデンティティ"
|
||||||
|
title: "あなたの支払いアイデンティティ"
|
||||||
|
key_being_made: "キーを作成中…"
|
||||||
|
connected_tor: "Tor経由で接続済み"
|
||||||
|
connecting_tor: "Tor経由で接続中…"
|
||||||
|
connected_direct: "接続済み(直接)"
|
||||||
|
connecting_direct: "接続中(直接)…"
|
||||||
|
fresh_key_blurb: "リカバリーフレーズの一部ではない支払い用のキーです — 資金に影響を与えることなく、いつでもローテーションしてプライバシーを保てます。"
|
||||||
|
restoring: "アイデンティティを復元しています…"
|
||||||
|
clean_slate_blurb: "まっさらな状態にしたいですか? いつでもまったく新しいキーに切り替えられます — 新しいあなたは古いあなたと結び付きません。同じウォレットで、新しい顔になります。"
|
||||||
|
pick_username: "ユーザー名を選択 — 任意"
|
||||||
|
username_blurb: "友人は長いキーの代わりに、あなたの名前で支払います。任意です — いつでも登録できます。"
|
||||||
|
username_field_hint: "yourname"
|
||||||
|
working: "処理中…"
|
||||||
|
claim_username: "ユーザー名を登録"
|
||||||
|
available_when_connected: "Torが接続されると利用できます — またはスキップして後で登録することもできます。"
|
||||||
|
youre: "あなたは%{name}です"
|
||||||
|
claimed_title: "%{name}はあなたのものです"
|
||||||
|
claimed_blurb: "友人は名前であなたに支払えるようになりました。準備完了です — ウォレットを開きましょう。"
|
||||||
|
open_wallet: "ウォレットを開く"
|
||||||
|
skip_for_now: "今はスキップ"
|
||||||
|
import_existing: "すでにGoblinのアイデンティティをお持ちですか? インポートする"
|
||||||
|
import_title: "アイデンティティをインポート"
|
||||||
|
import_blurb: "nsecを貼り付けるか.backupファイルを選んで、この新しいものではなく既存のキーとユーザー名を維持してください。"
|
||||||
|
errors:
|
||||||
|
cant_open: "ウォレットを開けませんでした: %{err}"
|
||||||
|
cant_create: "ウォレットを作成できませんでした: %{err}"
|
||||||
|
send:
|
||||||
|
scan_to_request: "スキャンしてリクエスト"
|
||||||
|
scan_to_pay: "スキャンして支払う"
|
||||||
|
tab_scan: "スキャン"
|
||||||
|
tab_my_code: "マイコード"
|
||||||
|
request_from: "リクエスト先"
|
||||||
|
send_to: "送信先"
|
||||||
|
search_hint: "ハンドル、npub、または名前"
|
||||||
|
suggested: "%{icon} おすすめ"
|
||||||
|
no_contacts: "まだ連絡先がありません。ハンドルで相手を探してください。"
|
||||||
|
no_profile: "プロフィールなし"
|
||||||
|
tag_contact: "連絡先"
|
||||||
|
tag_on_nostr: "nostr上"
|
||||||
|
searching_nostr: "nostrを検索中…"
|
||||||
|
unverified_title: "未検証のキーに支払いますか?"
|
||||||
|
unverified_body: "このキーに対して公開されているnostrプロフィールがありません — 新規、匿名、または入力ミスの可能性があります。送信する前に正しい相手か再確認してください。"
|
||||||
|
keep_looking: "探し続ける"
|
||||||
|
pay_anyway: "このまま支払う"
|
||||||
|
scan_not_recipient: "そのQRコードはgoblinの受取人ではありません — npubまたはハンドルが必要です"
|
||||||
|
scan_prompt: "goblinコードを画面内に収めると起動します"
|
||||||
|
scan_to_pay_me: "スキャンして私に支払う"
|
||||||
|
share_btn: "%{icon} 共有"
|
||||||
|
share_message: "Goblinで私に支払ってください — %{handle}
|
||||||
|
%{link}
|
||||||
|
npub: %{npub}"
|
||||||
|
none_found: "%{label}に該当する相手が見つかりません"
|
||||||
|
enter_recipient: "ハンドル、npub、または名前を入力してください"
|
||||||
|
amount_title: "金額"
|
||||||
|
to_name: "%{name}へ"
|
||||||
|
not_enough: "grinが不足しています"
|
||||||
|
max: "最大"
|
||||||
|
note_label: "メモ"
|
||||||
|
note_hint: "メモを追加…"
|
||||||
|
add_note: "メモを追加"
|
||||||
|
edit_note: "メモを編集"
|
||||||
|
note_cancel: "キャンセル"
|
||||||
|
note_save: "保存"
|
||||||
|
review_btn: "確認"
|
||||||
|
confirm_request: "リクエストを確認"
|
||||||
|
review_title: "確認"
|
||||||
|
requesting_from: "%{name}にリクエスト中"
|
||||||
|
youre_sending: "%{name}に送信中"
|
||||||
|
row_from: "送信元"
|
||||||
|
row_to: "宛先"
|
||||||
|
row_note: "メモ"
|
||||||
|
row_proof: "支払い証明"
|
||||||
|
row_proof_val: "あり"
|
||||||
|
row_proof_shared: "証明の共有先"
|
||||||
|
row_they_pay: "相手の支払い"
|
||||||
|
row_they_pay_val: "承認した場合のみ"
|
||||||
|
row_delivery: "配信方法"
|
||||||
|
row_delivery_val: "NIP-44暗号化、Tor経由"
|
||||||
|
row_network_fee: "ネットワーク手数料"
|
||||||
|
row_network_fee_val: "残高から差し引かれます"
|
||||||
|
row_privacy: "プライバシー"
|
||||||
|
row_privacy_val: "Mimblewimble + Tor"
|
||||||
|
send_request_btn: "リクエストを送信"
|
||||||
|
request_approve_hint: "相手に承認用のリクエストが届きます"
|
||||||
|
hold_to_send: "長押しで送信"
|
||||||
|
lower_amount: "戻って金額を減らしてください"
|
||||||
|
hold_confirm_hint: "長押しして確認"
|
||||||
|
requesting: "リクエスト中…"
|
||||||
|
sending: "送信中…"
|
||||||
|
they: "相手"
|
||||||
|
request_blocked: "%{who}はリクエストを受け付けていません。代わりにgrinを送ってもらうよう頼んでください。"
|
||||||
|
failed_request_title: "リクエストできませんでした"
|
||||||
|
failed_send_title: "送信できませんでした"
|
||||||
|
failed_request_body: "リクエストを届けられませんでした。代わりにgrinを送ってもらうよう頼んでください。"
|
||||||
|
failed_send_body: "支払いは届きませんでした。grinは安全です — もう一度お試しください。"
|
||||||
|
try_again_btn: "もう一度試す"
|
||||||
|
close_btn: "閉じる"
|
||||||
|
success:
|
||||||
|
requested: "リクエスト済み"
|
||||||
|
sent: "送信済み"
|
||||||
|
from: "送信元"
|
||||||
|
to: "宛先"
|
||||||
|
subtitle: "%{dir}: %{who} · たった今"
|
||||||
|
done_btn: "完了"
|
||||||
|
receipt_btn: "領収書"
|
||||||
@@ -0,0 +1,971 @@
|
|||||||
|
lang_name: 한국어
|
||||||
|
copy: 복사
|
||||||
|
paste: 붙여넣기
|
||||||
|
continue: 계속
|
||||||
|
complete: 완료
|
||||||
|
error: 오류
|
||||||
|
retry: 다시 시도
|
||||||
|
close: 닫기
|
||||||
|
change: 변경
|
||||||
|
show: 보기
|
||||||
|
delete: 삭제
|
||||||
|
clear: 지우기
|
||||||
|
create: 만들기
|
||||||
|
id: 식별자
|
||||||
|
kernel: 커널
|
||||||
|
settings: 설정
|
||||||
|
language: 언어
|
||||||
|
scan: 스캔
|
||||||
|
qr_code: QR 코드
|
||||||
|
scan_qr: QR 코드 스캔
|
||||||
|
repeat: 반복
|
||||||
|
scan_result: 스캔 결과
|
||||||
|
back: 뒤로
|
||||||
|
share: 공유
|
||||||
|
theme: '테마:'
|
||||||
|
dark: 어둡게
|
||||||
|
light: 밝게
|
||||||
|
file: 파일
|
||||||
|
choose_file: 파일 선택
|
||||||
|
choose_folder: 폴더 선택
|
||||||
|
crash_report: 충돌 보고서
|
||||||
|
crash_report_warning: 지난번에 앱이 예기치 않게 종료되었습니다. 충돌 보고서를 개발자와 공유할 수 있습니다.
|
||||||
|
confirmation: 확인
|
||||||
|
enter_url: URL을 입력하세요
|
||||||
|
max_short: 최대
|
||||||
|
files_location: 파일 위치
|
||||||
|
moving_files: 파일 이동 중
|
||||||
|
wrong_path_error: 잘못된 경로가 지정되었습니다
|
||||||
|
check_updates: 시작할 때 업데이트 확인
|
||||||
|
update_available: 업데이트가 있습니다!
|
||||||
|
changelog: '변경 내역:'
|
||||||
|
wallets:
|
||||||
|
await_conf_amount: 확인 대기 중
|
||||||
|
await_fin_amount: 완료 대기 중
|
||||||
|
locked_amount: 잠김
|
||||||
|
txs_empty: '자금을 수동으로 또는 전송을 통해 받으려면 화면 하단의 %{message} 또는 %{transport} 버튼을 사용하세요. 지갑 설정을 변경하려면 %{settings} 버튼을 누르세요.'
|
||||||
|
title: Goblin
|
||||||
|
create_desc: 저장된 복구 문구로 지갑을 새로 만들거나 기존 지갑을 가져오세요.
|
||||||
|
add: 지갑 추가
|
||||||
|
name: '이름:'
|
||||||
|
pass: '비밀번호:'
|
||||||
|
pass_empty: 지갑 비밀번호를 입력하세요
|
||||||
|
current_pass: '현재 비밀번호:'
|
||||||
|
new_pass: '새 비밀번호:'
|
||||||
|
min_tx_conf_count: '거래에 필요한 최소 확인 횟수:'
|
||||||
|
recover: 복원
|
||||||
|
recovery_phrase: 복구 문구
|
||||||
|
words_count: '단어 수:'
|
||||||
|
enter_word: '%{number}번째 단어를 입력하세요:'
|
||||||
|
not_valid_word: 입력한 단어가 올바르지 않습니다
|
||||||
|
not_valid_phrase: 입력한 문구가 올바르지 않습니다
|
||||||
|
create_phrase_desc: 복구 문구를 안전하게 적어서 보관하세요.
|
||||||
|
restore_phrase_desc: 저장해 둔 복구 문구의 단어를 입력하세요.
|
||||||
|
setup_conn_desc: 지갑이 네트워크에 연결되는 방식을 선택하세요.
|
||||||
|
conn_method: 연결 방법
|
||||||
|
ext_conn: '외부 연결:'
|
||||||
|
add_node: 노드 추가
|
||||||
|
node_url: '노드 URL:'
|
||||||
|
node_secret: 'API 시크릿(선택 사항):'
|
||||||
|
invalid_url: 입력한 URL이 올바르지 않습니다
|
||||||
|
open: 지갑 열기
|
||||||
|
wrong_pass: 입력한 비밀번호가 틀렸습니다
|
||||||
|
locked: 잠김
|
||||||
|
unlocked: 잠금 해제됨
|
||||||
|
enable_node: '지갑을 사용하려면 통합 노드를 활성화하거나, 화면 하단의 %{settings}을(를) 선택해 연결 설정을 변경하세요.'
|
||||||
|
node_loading: '지갑은 통합 노드 동기화가 끝난 후 불러옵니다. 화면 하단의 %{settings}을(를) 선택하면 연결을 변경할 수 있습니다.'
|
||||||
|
loading: 불러오는 중
|
||||||
|
closing: 닫는 중
|
||||||
|
checking: 확인하는 중
|
||||||
|
default_wallet: 기본 지갑
|
||||||
|
new_account_desc: '새 계정의 이름을 입력하세요:'
|
||||||
|
wallet_loading: 지갑 불러오는 중
|
||||||
|
wallet_closing: 지갑 닫는 중
|
||||||
|
wallet_checking: 지갑 확인하는 중
|
||||||
|
tx_loading: 거래 내역 불러오는 중
|
||||||
|
default_account: 기본 계정
|
||||||
|
accounts: 계정
|
||||||
|
tx_sent: 보냄
|
||||||
|
tx_received: 받음
|
||||||
|
tx_sending: 보내는 중
|
||||||
|
tx_receiving: 받는 중
|
||||||
|
tx_confirming: 확인 대기 중
|
||||||
|
tx_canceled: 취소됨
|
||||||
|
tx_cancelling: 취소하는 중
|
||||||
|
tx_finalizing: 완료하는 중
|
||||||
|
tx_posting: 게시하는 중
|
||||||
|
tx_confirmed: 확인됨
|
||||||
|
tx_stale: 너무 오래 대기 중
|
||||||
|
txs: 거래 내역
|
||||||
|
tx: 거래
|
||||||
|
messages: 메시지
|
||||||
|
transport: 전송
|
||||||
|
input_slatepack_desc: '받은 Slatepack 메시지를 입력해 응답을 만들거나 요청을 완료하세요:'
|
||||||
|
parse_slatepack_err: '메시지를 읽는 중 오류가 발생했습니다. 입력값을 확인하세요:'
|
||||||
|
pay_balance_error: '계정 잔액이 %{amount} ツ와(과) 네트워크 수수료를 지불하기에 부족합니다.'
|
||||||
|
parse_i1_slatepack_desc: '%{amount} ツ를 지불하려면 이 메시지를 받는 사람에게 보내세요:'
|
||||||
|
parse_i2_slatepack_desc: '%{amount} ツ를 받으려면 거래를 완료하세요:'
|
||||||
|
parse_i3_slatepack_desc: '%{amount} ツ 수신을 완료하려면 거래를 게시하세요:'
|
||||||
|
parse_s1_slatepack_desc: '%{amount} ツ를 받으려면 이 메시지를 보낸 사람에게 보내세요:'
|
||||||
|
parse_s2_slatepack_desc: '%{amount} ツ를 보내려면 거래를 완료하세요:'
|
||||||
|
parse_s3_slatepack_desc: '%{amount} ツ 전송을 완료하려면 거래를 게시하세요:'
|
||||||
|
resp_slatepack_err: '응답을 만드는 중 오류가 발생했습니다. 입력 데이터를 확인하거나 다시 시도하세요:'
|
||||||
|
resp_exists_err: 해당 거래가 이미 존재합니다.
|
||||||
|
resp_canceled_err: 해당 거래는 이미 취소되었습니다.
|
||||||
|
create_request_desc: '자금을 보내거나 받기 위한 요청을 만드세요:'
|
||||||
|
send_request_desc: '%{amount} ツ를 보내는 요청을 만들었습니다. 이 메시지를 받는 사람에게 보내세요:'
|
||||||
|
send_slatepack_err: 자금 전송 요청을 만드는 중 오류가 발생했습니다. 입력 데이터를 확인하거나 다시 시도하세요.
|
||||||
|
invoice_desc: '%{amount} ツ를 받는 요청을 만들었습니다. 이 메시지를 보낸 사람에게 보내세요:'
|
||||||
|
invoice_slatepack_err: 청구서를 발행하는 중 오류가 발생했습니다. 입력 데이터를 확인하거나 다시 시도하세요.
|
||||||
|
finalize_slatepack_err: '완료하는 중 오류가 발생했습니다. 입력 데이터를 확인하거나 다시 시도하세요:'
|
||||||
|
finalize: 완료
|
||||||
|
use_dandelion: Dandelion 사용
|
||||||
|
enter_amount_send: '%{amount} ツ를 보유하고 있습니다. 보낼 금액을 입력하세요:'
|
||||||
|
enter_amount_receive: '받을 금액을 입력하세요:'
|
||||||
|
recovery: 복구
|
||||||
|
repair_wallet: 지갑 복구
|
||||||
|
repair_desc: 지갑을 점검하여 필요한 경우 누락된 출력을 복구합니다. 이 작업은 시간이 걸릴 수 있습니다.
|
||||||
|
repair_unavailable: 노드에 활성 연결이 있고 지갑 동기화가 완료되어야 합니다.
|
||||||
|
delete: 지갑 삭제
|
||||||
|
delete_conf: 지갑을 삭제하시겠어요?
|
||||||
|
delete_desc: 나중에 자금에 접근할 수 있도록 복구 문구를 저장해 두었는지 확인하세요.
|
||||||
|
wallet_loading_err: '지갑을 동기화하는 중 오류가 발생했습니다. 다시 시도하거나, 화면 하단의 %{settings}을(를) 선택해 연결 설정을 변경할 수 있습니다.'
|
||||||
|
wallet: 지갑
|
||||||
|
send: 보내기
|
||||||
|
receive: 받기
|
||||||
|
settings: 지갑 설정
|
||||||
|
tx_send_cancel_conf: '%{amount} ツ 보내기를 취소하시겠어요?'
|
||||||
|
tx_receive_cancel_conf: '%{amount} ツ 받기를 취소하시겠어요?'
|
||||||
|
rec_phrase_not_found: 복구 문구를 찾을 수 없습니다.
|
||||||
|
restore_wallet_desc: 일반 복구로 해결되지 않으면 모든 파일을 삭제해 지갑을 복원하세요. 이후 지갑을 다시 열어야 합니다.
|
||||||
|
fee_base_desc: '수수료(기준값%{value}):'
|
||||||
|
payment_proof: 결제 증명
|
||||||
|
payment_proof_desc: '받은 결제 증명을 입력해 거래를 확인하세요:'
|
||||||
|
payment_proof_valid: '입력한 결제 증명이 유효합니다:'
|
||||||
|
payment_proof_error: '입력한 결제 증명이 유효하지 않습니다:'
|
||||||
|
tx_delete_confirmation: 내역에서 이 거래를 삭제하시겠어요?
|
||||||
|
transport:
|
||||||
|
desc: '전송 기능을 사용해 메시지를 동기적으로 주고받으세요:'
|
||||||
|
tor_network: Tor 네트워크
|
||||||
|
connected: 연결됨
|
||||||
|
connecting: 연결하는 중
|
||||||
|
disconnecting: 연결 해제하는 중
|
||||||
|
conn_error: 연결 오류
|
||||||
|
disconnected: 연결 끊김
|
||||||
|
receiver_address: '받는 사람의 주소:'
|
||||||
|
incorrect_addr_err: '입력한 주소가 올바르지 않습니다:'
|
||||||
|
tor_send_error: Tor로 전송하는 중 오류가 발생했습니다. 받는 사람이 온라인 상태인지 확인하세요. 거래가 취소되었습니다.
|
||||||
|
tor_autorun_desc: 지갑을 열 때 Tor 서비스를 실행해 거래를 동기적으로 받을지 여부입니다.
|
||||||
|
tor_sending: Tor로 전송하는 중
|
||||||
|
tor_settings: Tor 설정
|
||||||
|
bridges: 브리지
|
||||||
|
bridges_desc: 일반 연결이 작동하지 않을 때 Tor 네트워크 검열을 우회하도록 브리지를 설정하세요.
|
||||||
|
bin_file: '바이너리 파일:'
|
||||||
|
conn_line: '연결 라인:'
|
||||||
|
bridges_disabled: 브리지 비활성화됨
|
||||||
|
bridge_name: '브리지 %{b}'
|
||||||
|
network:
|
||||||
|
self: 네트워크
|
||||||
|
type: '네트워크 유형:'
|
||||||
|
mainnet: 메인넷
|
||||||
|
testnet: 테스트넷
|
||||||
|
connections: 연결
|
||||||
|
node: 통합 노드
|
||||||
|
metrics: 지표
|
||||||
|
mining: 채굴
|
||||||
|
settings: 노드 설정
|
||||||
|
enable_node: 노드 활성화
|
||||||
|
autorun: 자동 실행
|
||||||
|
disabled_server: '화면 왼쪽 상단의 %{dots}을(를) 눌러 통합 노드를 활성화하거나 다른 연결 방법을 추가하세요.'
|
||||||
|
no_ips: 시스템에 사용 가능한 IP 주소가 없어 서버를 시작할 수 없습니다. 네트워크 연결 상태를 확인하세요.
|
||||||
|
available: 사용 가능
|
||||||
|
not_available: 사용 불가
|
||||||
|
availability_check: 사용 가능 여부 확인
|
||||||
|
android_warning: Android 사용자는 참고하세요. 통합 노드를 성공적으로 동기화하려면 휴대전화의 시스템 설정에서 알림 접근을 허용하고 Goblin 앱의 배터리 사용 제한을 해제해야 합니다. 앱이 백그라운드에서 올바르게 작동하는 데 필요한 작업입니다.
|
||||||
|
sync_status:
|
||||||
|
node_restarting: 노드가 재시작하는 중입니다
|
||||||
|
node_down: 노드가 다운되었습니다
|
||||||
|
initial: 노드가 시작하는 중입니다
|
||||||
|
no_sync: 노드가 실행 중입니다
|
||||||
|
awaiting_peers: 피어를 기다리는 중
|
||||||
|
header_sync: 헤더 다운로드 중
|
||||||
|
header_sync_percent: '헤더 다운로드 중: %{percent}%'
|
||||||
|
tx_hashset_pibd: 상태 다운로드 중(PIBD)
|
||||||
|
tx_hashset_pibd_percent: '상태 다운로드 중(PIBD): %{percent}%'
|
||||||
|
tx_hashset_download: 상태 다운로드 중
|
||||||
|
tx_hashset_download_percent: '상태 다운로드 중: %{percent}%'
|
||||||
|
tx_hashset_setup_history: '상태 준비 중(히스토리): %{percent}%'
|
||||||
|
tx_hashset_setup_position: '상태 준비 중(위치): %{percent}%'
|
||||||
|
tx_hashset_setup: 상태 준비 중
|
||||||
|
tx_hashset_range_proofs_validation: '상태 검증 중(범위 증명): %{percent}%'
|
||||||
|
tx_hashset_kernels_validation: '상태 검증 중(커널): %{percent}%'
|
||||||
|
tx_hashset_save: 체인 상태 완료하는 중
|
||||||
|
body_sync: 블록 다운로드 중
|
||||||
|
body_sync_percent: '블록 다운로드 중: %{percent}%'
|
||||||
|
shutdown: 노드가 종료되는 중입니다
|
||||||
|
network_node:
|
||||||
|
header: 헤더
|
||||||
|
block: 블록
|
||||||
|
hash: Hash
|
||||||
|
height: 높이
|
||||||
|
difficulty: 난이도
|
||||||
|
time: 시간
|
||||||
|
main_pool: 메인 풀
|
||||||
|
stem_pool: 스템 풀
|
||||||
|
data: 데이터
|
||||||
|
size: 크기(GB)
|
||||||
|
peers: 피어
|
||||||
|
error_clean: 노드 데이터가 손상되어 재동기화가 필요합니다.
|
||||||
|
resync: 재동기화
|
||||||
|
error_p2p_api: '%{p2p_api} 서버를 초기화하는 중 오류가 발생했습니다. 화면 하단의 %{settings}을(를) 선택해 %{p2p_api} 설정을 확인하세요.'
|
||||||
|
error_config: '구성을 초기화하는 중 오류가 발생했습니다. 화면 하단의 %{settings}을(를) 선택해 설정을 확인하세요.'
|
||||||
|
error_unknown: '초기화하는 중 오류가 발생했습니다. 화면 하단의 %{settings}을(를) 선택해 통합 노드 설정을 확인하거나 재동기화하세요.'
|
||||||
|
network_metrics:
|
||||||
|
loading: 지표는 동기화가 끝난 후 확인할 수 있습니다
|
||||||
|
emission: 발행량
|
||||||
|
inflation: 인플레이션
|
||||||
|
supply: 공급량
|
||||||
|
block_time: 블록 시간
|
||||||
|
reward: 보상
|
||||||
|
difficulty_window: '난이도 윈도우 %{size}'
|
||||||
|
network_mining:
|
||||||
|
loading: 채굴은 동기화가 끝난 후 이용할 수 있습니다
|
||||||
|
info: '채굴 서버가 활성화되어 있습니다. 화면 하단의 %{settings}을(를) 선택해 설정을 변경할 수 있습니다. 장치가 연결되면 데이터가 업데이트됩니다.'
|
||||||
|
restart_server_required: 변경 사항을 적용하려면 서버를 재시작해야 합니다.
|
||||||
|
rewards_wallet: 보상 받을 지갑
|
||||||
|
server: Stratum 서버
|
||||||
|
address: 주소
|
||||||
|
miners: 채굴자
|
||||||
|
devices: 장치
|
||||||
|
blocks_found: 찾은 블록
|
||||||
|
hashrate: '해시레이트(C%{bits})'
|
||||||
|
connected: 연결됨
|
||||||
|
disconnected: 연결 끊김
|
||||||
|
network_settings:
|
||||||
|
change_value: 값 변경
|
||||||
|
stratum_ip: 'Stratum IP 주소:'
|
||||||
|
stratum_port: 'Stratum 포트:'
|
||||||
|
port_unavailable: 지정한 포트를 사용할 수 없습니다
|
||||||
|
restart_node_required: 변경 사항을 적용하려면 노드를 재시작해야 합니다.
|
||||||
|
choose_wallet: 지갑 선택
|
||||||
|
stratum_wallet_warning: 보상을 받으려면 지갑이 열려 있어야 합니다.
|
||||||
|
enable: 활성화
|
||||||
|
disable: 비활성화
|
||||||
|
restart: 재시작
|
||||||
|
server: 서버
|
||||||
|
api_ip: 'API IP 주소:'
|
||||||
|
api_port: 'API 포트:'
|
||||||
|
api_secret: 'REST API 및 V2 소유자 API 토큰:'
|
||||||
|
foreign_api_secret: '외부 API 토큰:'
|
||||||
|
disabled: 비활성화됨
|
||||||
|
enabled: 활성화됨
|
||||||
|
ftl: '미래 시간 제한(FTL):'
|
||||||
|
ftl_description: 새 블록의 타임스탬프가 노드의 로컬 시간(초 단위) 기준으로 얼마나 미래일 수 있는지에 대한 제한으로, 블록이 승인되기 위한 조건입니다.
|
||||||
|
not_valid_value: 입력한 값이 올바르지 않습니다
|
||||||
|
full_validation: 전체 검증
|
||||||
|
full_validation_description: 각 블록을 처리할 때 체인 전체 검증을 실행할지 여부입니다(동기화 중에는 제외).
|
||||||
|
archive_mode: 아카이브 모드
|
||||||
|
archive_mode_desc: 노드를 전체 아카이브 모드로 실행합니다(동기화에 더 많은 디스크 공간과 시간이 필요합니다).
|
||||||
|
attempt_time: '채굴 시도 시간(초):'
|
||||||
|
attempt_time_desc: 특정 헤더에서 채굴을 시도하다가 중단하고 풀에서 거래를 다시 수집하기까지의 시간
|
||||||
|
min_share_diff: '허용되는 최소 셰어 난이도:'
|
||||||
|
reset_settings_desc: 노드 설정을 기본값으로 재설정합니다
|
||||||
|
reset_settings: 설정 재설정
|
||||||
|
reset: 재설정
|
||||||
|
tx_pool: 거래 풀
|
||||||
|
pool_fee: '풀에 허용되는 기본 수수료:'
|
||||||
|
reorg_period: '재구성 캐시 보관 기간(분):'
|
||||||
|
max_tx_pool: '풀에 담을 수 있는 최대 거래 수:'
|
||||||
|
max_tx_stempool: '스템 풀에 담을 수 있는 최대 거래 수:'
|
||||||
|
max_tx_weight: '블록을 구성하기 위해 선택될 수 있는 거래의 최대 총 가중치:'
|
||||||
|
epoch_duration: '에포크 기간(초):'
|
||||||
|
embargo_timer: '엠바고 타이머(초):'
|
||||||
|
aggregation_period: '집계 기간(초):'
|
||||||
|
stem_probability: '스템 단계 확률:'
|
||||||
|
stem_txs: 스템 거래
|
||||||
|
p2p_server: P2P 서버
|
||||||
|
p2p_port: 'P2P 포트:'
|
||||||
|
add_seed: DNS 시드 추가
|
||||||
|
seed_address: 'DNS 시드 주소:'
|
||||||
|
add_peer: 피어 추가
|
||||||
|
peer_address: '피어 주소:'
|
||||||
|
peer_address_error: 'IP 주소나 DNS 이름을 올바른 형식으로 입력하세요(지정한 호스트가 사용 가능한지 확인하세요). 예: 192.168.0.1:1234 또는 example.com:5678'
|
||||||
|
default: 기본값
|
||||||
|
allow_list: 허용 목록
|
||||||
|
allow_list_desc: 이 목록에 있는 피어에만 연결합니다.
|
||||||
|
deny_list: 차단 목록
|
||||||
|
deny_list_desc: 이 목록에 있는 피어에는 절대 연결하지 않습니다.
|
||||||
|
favourites: 즐겨찾기
|
||||||
|
favourites_desc: 우선적으로 연결할 피어 목록입니다.
|
||||||
|
ban_window: '차단된 피어가 차단 상태로 유지되는 시간(초):'
|
||||||
|
ban_window_desc: 차단 여부는 피어로부터 받은 데이터의 정확성을 바탕으로 노드가 결정합니다.
|
||||||
|
max_inbound_count: '수신 피어 연결의 최대 수:'
|
||||||
|
max_outbound_count: '발신 피어 연결의 최대 수:'
|
||||||
|
reset_data_desc: 노드 데이터를 재설정합니다. 동기화에 문제가 있을 때만 주의해서 사용하세요.
|
||||||
|
reset_data: 데이터 재설정
|
||||||
|
modal:
|
||||||
|
cancel: 취소
|
||||||
|
save: 저장
|
||||||
|
add: 추가
|
||||||
|
modal_exit:
|
||||||
|
description: 앱을 종료하시겠어요?
|
||||||
|
exit: 종료
|
||||||
|
app_settings:
|
||||||
|
proxy: 프록시
|
||||||
|
proxy_desc: 앱의 네트워크 요청에 프록시를 사용할지 여부입니다.
|
||||||
|
keyboard:
|
||||||
|
1: 1
|
||||||
|
2: 2
|
||||||
|
3: 3
|
||||||
|
4: 4
|
||||||
|
5: 5
|
||||||
|
6: 6
|
||||||
|
7: 7
|
||||||
|
8: 8
|
||||||
|
9: 9
|
||||||
|
0: 0
|
||||||
|
01: '-'
|
||||||
|
q: ㅂ
|
||||||
|
w: ㅈ
|
||||||
|
e: ㄷ
|
||||||
|
r: ㄱ
|
||||||
|
t: ㅅ
|
||||||
|
y: ㅛ
|
||||||
|
u: ㅕ
|
||||||
|
i: ㅑ
|
||||||
|
o: ㅐ
|
||||||
|
p: ㅔ
|
||||||
|
p1: '"'
|
||||||
|
a: ㅁ
|
||||||
|
s: ㄴ
|
||||||
|
d: ㅇ
|
||||||
|
f: ㄹ
|
||||||
|
g: ㅎ
|
||||||
|
h: ㅗ
|
||||||
|
j: ㅓ
|
||||||
|
k: ㅏ
|
||||||
|
l: ㅣ
|
||||||
|
l1: \
|
||||||
|
l2: ':'
|
||||||
|
z: ㅋ
|
||||||
|
x: ㅌ
|
||||||
|
c: ㅊ
|
||||||
|
v: ㅍ
|
||||||
|
b: ㅠ
|
||||||
|
n: ㅜ
|
||||||
|
m: ㅡ
|
||||||
|
m1: ','
|
||||||
|
m2: .
|
||||||
|
m3: /
|
||||||
|
goblin:
|
||||||
|
home:
|
||||||
|
anonymous: "익명"
|
||||||
|
connected_tor: "Tor로 연결됨"
|
||||||
|
tor_ready: "Tor 준비 완료 · 릴레이…"
|
||||||
|
connecting_tor: "Tor에 연결하는 중…"
|
||||||
|
connected_direct: "연결됨 (직접)"
|
||||||
|
connecting_direct: "연결 중 (직접)…"
|
||||||
|
cant_reach_node: "노드에 연결할 수 없음"
|
||||||
|
node_synced: "노드 동기화됨"
|
||||||
|
syncing: "동기화하는 중…"
|
||||||
|
balance_updating: "잔액 업데이트하는 중…"
|
||||||
|
balance_stale: "노드에 연결할 수 없음 · 마지막으로 확인된 잔액"
|
||||||
|
fiat_unavailable: "환율 사용 불가"
|
||||||
|
listening: "결제 대기 중"
|
||||||
|
block: "블록 %{height}"
|
||||||
|
waiting_for_chain: "체인을 기다리는 중…"
|
||||||
|
nav_wallet: "지갑"
|
||||||
|
nav_pay: "결제"
|
||||||
|
nav_activity: "활동"
|
||||||
|
nav_receive: "받기"
|
||||||
|
nav_settings: "설정"
|
||||||
|
back_again: "지갑을 전환하려면 뒤로를 한 번 더 누르세요"
|
||||||
|
activity: "활동"
|
||||||
|
news: "소식"
|
||||||
|
empty_title: "아직 활동이 없습니다"
|
||||||
|
empty_sub: "grin을 보내거나 받아서 시작해 보세요."
|
||||||
|
recent: "최근"
|
||||||
|
scan_to_pay: "스캔해서 결제"
|
||||||
|
type_amount: "금액 입력"
|
||||||
|
request: "요청"
|
||||||
|
pay: "결제"
|
||||||
|
enter_amount: "결제하거나 요청할 금액을 입력하세요"
|
||||||
|
activity:
|
||||||
|
canceled: "취소됨"
|
||||||
|
pending: "대기 중"
|
||||||
|
earlier: "이전"
|
||||||
|
today: "오늘"
|
||||||
|
yesterday: "어제"
|
||||||
|
title: "활동"
|
||||||
|
requests: "요청"
|
||||||
|
empty_title: "아직 활동이 없습니다"
|
||||||
|
empty_sub: "결제 내역이 여기에 표시됩니다."
|
||||||
|
pending_header: "대기 중"
|
||||||
|
receipt:
|
||||||
|
title: "영수증"
|
||||||
|
not_found: "거래를 찾을 수 없음"
|
||||||
|
for_note: "%{note} 관련"
|
||||||
|
details: "거래 세부정보"
|
||||||
|
canceled: "취소됨"
|
||||||
|
expired: "만료됨"
|
||||||
|
funds_returned: "자금 반환됨"
|
||||||
|
complete: "완료"
|
||||||
|
payment_received: "결제 받음"
|
||||||
|
payment_sent: "결제 성공적으로 보냄"
|
||||||
|
pending: "대기 중"
|
||||||
|
confs: "확인 %{c}/%{r}회"
|
||||||
|
waiting_to_confirm: "확인 대기 중"
|
||||||
|
paying: "결제하는 중…"
|
||||||
|
you: "나"
|
||||||
|
to: "받는 사람"
|
||||||
|
from: "보낸 사람"
|
||||||
|
nostr: "nostr"
|
||||||
|
identity: "신원"
|
||||||
|
fee_none: "없음"
|
||||||
|
network_fee: "네트워크 수수료"
|
||||||
|
privacy: "프라이버시"
|
||||||
|
privacy_value: "Mimblewimble + Tor"
|
||||||
|
transaction: "거래"
|
||||||
|
cancel_request: "요청 취소"
|
||||||
|
cancel_send: "결제 취소"
|
||||||
|
stale_note: "이 결제가 한동안 대기 중입니다."
|
||||||
|
cancel_send_confirm: "취소하려면 다시 탭하세요 — 상대방이 이미 받았을 수도 있습니다"
|
||||||
|
cancel_send_done: "결제가 취소되었습니다 — 자금을 다시 사용할 수 있습니다"
|
||||||
|
cancel_send_too_late: "이 결제는 이미 완료되어 취소할 수 없습니다"
|
||||||
|
waiting_to_receive: "%{name}님이 받기를 기다리는 중…"
|
||||||
|
request:
|
||||||
|
title: "%{name}님의 요청"
|
||||||
|
approve: "승인"
|
||||||
|
decline: "거절"
|
||||||
|
review_title: "요청 검토"
|
||||||
|
hold_to_accept: "길게 눌러서 수락"
|
||||||
|
hold_accept_hint: "길게 눌러 이 요청을 결제하세요"
|
||||||
|
receive:
|
||||||
|
title: "받기"
|
||||||
|
requesting: "%{amt}%{tsu} 요청 중 — 공유해서 결제받으세요"
|
||||||
|
clear_request: "요청 지우기"
|
||||||
|
share_handle: "핸들을 공유해서 결제받으세요"
|
||||||
|
share_npub: "npub을 공유해서 결제받으세요"
|
||||||
|
copied: "복사됨"
|
||||||
|
copy_nostr_id: "nostr ID 복사"
|
||||||
|
copy_address: "주소 복사"
|
||||||
|
copy: "복사"
|
||||||
|
share_message: "Goblin(goblin.st)에서 저에게 결제하세요 — %{npub}"
|
||||||
|
privacy_note: "사용자 이름은 공개됩니다. 결제 내용은 네트워크에서 계속 암호화됩니다."
|
||||||
|
privacy_note_npub: "npub은 공개됩니다. 결제 내용은 네트워크에서 계속 암호화됩니다."
|
||||||
|
profile:
|
||||||
|
title: "프로필"
|
||||||
|
activity: "활동"
|
||||||
|
no_activity: "아직 이 상대와의 활동이 없습니다."
|
||||||
|
unblock: "차단 해제"
|
||||||
|
block: "차단"
|
||||||
|
blocked_blurb: "차단됨 — 상대방의 결제와 요청이 무시됩니다."
|
||||||
|
block_blurb: "차단하면 상대방이 보낸 결제와 요청이 무시됩니다."
|
||||||
|
settings:
|
||||||
|
title: "설정"
|
||||||
|
connected_nostr: "nostr에 연결됨"
|
||||||
|
connecting_relays: "릴레이에 연결하는 중…"
|
||||||
|
identity: "신원"
|
||||||
|
copy_npub: "npub 복사(공개)"
|
||||||
|
backup_note: "기기를 바꾸시나요? 두 가지 모두 백업하세요: 복구 문구(자금)와 신원 .backup 파일(이름 + 키)."
|
||||||
|
wallet: "지갑"
|
||||||
|
display_unit: "표시 단위"
|
||||||
|
relays: "릴레이"
|
||||||
|
nostr_relays: "Nostr 릴레이"
|
||||||
|
node: "노드"
|
||||||
|
min_conf: "최소 확인 횟수"
|
||||||
|
integrated_node: "통합 노드 설정"
|
||||||
|
node_advanced: "고급"
|
||||||
|
slatepacks: "Slatepack"
|
||||||
|
slatepacks_value: "수동 거래"
|
||||||
|
trusted_sites: "신뢰하는 사이트"
|
||||||
|
lock_wallet: "지갑 잠그기"
|
||||||
|
switch_wallet: "지갑 전환"
|
||||||
|
advanced: "고급"
|
||||||
|
privacy: "프라이버시"
|
||||||
|
tor_routing: "Tor 라우팅"
|
||||||
|
messages_lookups: "메시지 및 조회"
|
||||||
|
tor_on: "켜짐"
|
||||||
|
tor_off: "꺼짐"
|
||||||
|
auto_accept: "자동 수락"
|
||||||
|
pairing: "기준 통화"
|
||||||
|
accept_anyone: "누구나"
|
||||||
|
accept_contacts: "연락처만"
|
||||||
|
accept_ask: "항상 확인"
|
||||||
|
requests: "요청"
|
||||||
|
incoming_requests: "받은 요청"
|
||||||
|
incoming_requests_sub: "다른 사람이 나에게 돈을 요청할 수 있도록 허용"
|
||||||
|
hide_amounts: "금액 숨기기"
|
||||||
|
hide_amounts_sub: "알림에서 받은 금액 숨기기"
|
||||||
|
language: "언어"
|
||||||
|
update_available: "업데이트 가능"
|
||||||
|
appearance: "화면"
|
||||||
|
theme: "테마"
|
||||||
|
theme_light: "라이트"
|
||||||
|
theme_dark: "다크"
|
||||||
|
theme_yellow: "옐로우"
|
||||||
|
archive: "보관함"
|
||||||
|
export_archive: "보관함 내보내기"
|
||||||
|
export_archive_caption: "연락처, 결제 내역, 요청을 JSON 형식으로 클립보드에 복사합니다."
|
||||||
|
wipe_history: "결제 내역 지우기"
|
||||||
|
wipe_history_confirm: "지우려면 다시 탭하세요 — 되돌릴 수 없습니다"
|
||||||
|
about: "정보"
|
||||||
|
goblin: "Goblin"
|
||||||
|
build: "빌드 %{build}"
|
||||||
|
network: "네트워크"
|
||||||
|
network_value: "MW + Tor + nostr"
|
||||||
|
third_party: "타사"
|
||||||
|
grim: "GRIM(원본 지갑)"
|
||||||
|
grin_node: "Grin 노드"
|
||||||
|
sp_intro: "고급 — GRIM처럼 원시 slatepack을 직접 주고받습니다. 사용자 이름으로 결제하거나 받을 수 없을 때만 사용하세요."
|
||||||
|
sp_receive_group: "받기 또는 완료"
|
||||||
|
sp_receive_blurb: "누군가에게 받은 slatepack을 붙여넣으세요. Goblin이 결제를 받거나, 청구서를 지불하거나, 완료해서 게시합니다."
|
||||||
|
sp_process: "slatepack 처리"
|
||||||
|
sp_paste_first: "먼저 slatepack을 붙여넣으세요."
|
||||||
|
sp_reply_ready: "응답 준비 완료 — 보낸 사람에게 다시 보내세요."
|
||||||
|
sp_finalizing: "완료하고 체인에 게시하는 중…"
|
||||||
|
sp_create_group: "결제 만들기"
|
||||||
|
sp_create_blurb: "누군가에게 건넬 slatepack을 만드세요. 상대방이 받아 응답을 다시 보내면, 위에서 완료하세요."
|
||||||
|
sp_amount_hint: "grin 단위 금액"
|
||||||
|
sp_addr_hint: "받는 사람 주소(선택 사항)"
|
||||||
|
sp_create: "slatepack 만들기"
|
||||||
|
sp_ready: "slatepack 준비 완료 — 받는 사람에게 건네세요."
|
||||||
|
sp_amount_gt_zero: "0보다 큰 금액을 입력하세요."
|
||||||
|
sp_to_send: "보낼 slatepack"
|
||||||
|
sp_copy: "slatepack 복사"
|
||||||
|
cancel: "취소"
|
||||||
|
continue: "계속"
|
||||||
|
final_confirmation: "최종 확인"
|
||||||
|
type_reset: "RESET 입력"
|
||||||
|
wallet_password: "지갑 비밀번호"
|
||||||
|
done: "완료"
|
||||||
|
close: "닫기"
|
||||||
|
import_nsec_hint: "nsec1… 또는 붙여넣은 백업"
|
||||||
|
backup_password_hint: "백업 비밀번호(다른 곳에서 내보낸 경우에만)"
|
||||||
|
import_btn: "가져오기"
|
||||||
|
importing: "가져오는 중…"
|
||||||
|
import_failed: "가져오기 실패"
|
||||||
|
name_authority: "이름 기관"
|
||||||
|
name_authority_title: "이름 기관 변경"
|
||||||
|
name_authority_blurb: "이름을 등록하고 검증하는 서버입니다. 다른 인스턴스를 지정하면 그곳에서 호스팅하는 이름을 사용하고 결제할 수 있습니다."
|
||||||
|
name_authority_invalid: "전체 URL을 입력하세요(https://…)."
|
||||||
|
reset: "재설정"
|
||||||
|
save: "저장"
|
||||||
|
backup_file: "파일로 백업"
|
||||||
|
choose_backup_file: ".backup 파일 선택"
|
||||||
|
backup_read_failed: "해당 파일을 읽을 수 없습니다."
|
||||||
|
backup_saved: "백업 저장됨"
|
||||||
|
backup_saved_sub: ".backup 파일을 안전하게 보관하세요. 이 파일과 비밀번호를 모두 가진 사람은 지갑 전체를 복원할 수 있습니다."
|
||||||
|
backup_file_title: "지갑 백업"
|
||||||
|
backup_file_blurb: "지갑 시드와 모든 신원이 담긴 암호화된 .backup 파일을 만듭니다. 지갑 비밀번호를 입력해 봉인하세요."
|
||||||
|
backup_write_failed: "파일을 저장할 수 없습니다."
|
||||||
|
create_backup: "백업 만들기"
|
||||||
|
registered: "%{name} 등록됨"
|
||||||
|
released_msg: "해제됨 — 이 이름을 다른 사람이 가져갈 수 있습니다"
|
||||||
|
release_confirm: "%{name}을(를) 해제할까요?"
|
||||||
|
release_blurb: "해제되는 즉시 사용 가능해집니다 — 다음에 교체할 키를 포함해 누구나 가져갈 수 있습니다. 10분 동안은 다른 사용자 이름을 등록할 수 없습니다."
|
||||||
|
releasing: "해제하는 중…"
|
||||||
|
keep_it: "유지하기"
|
||||||
|
release_it: "해제하기"
|
||||||
|
username: "사용자 이름"
|
||||||
|
username_note: "내 이름으로 표시됩니다. goblin.st에 공개됩니다. 결제 내용은 계속 암호화됩니다."
|
||||||
|
release_username: "사용자 이름 해제"
|
||||||
|
pick_username: "사용자 이름 선택 — 선택 사항"
|
||||||
|
working: "처리하는 중…"
|
||||||
|
claim: "등록"
|
||||||
|
err_just_taken: "그 사용자 이름은 방금 다른 사람이 등록했습니다"
|
||||||
|
err_cooldown: "최근에 사용자 이름을 해제했습니다 — 10분 이내에 새로 등록할 수 있습니다."
|
||||||
|
err_unreachable: "goblin.st에 연결할 수 없습니다 — 연결 문제입니다. 다시 시도하세요."
|
||||||
|
err_release: "해제할 수 없습니다: %{err}"
|
||||||
|
avail_available: "사용 가능합니다!"
|
||||||
|
avail_taken: "이미 사용 중"
|
||||||
|
avail_reserved: "예약됨"
|
||||||
|
avail_invalid: "이름은 3~20자여야 합니다: a–z, 0–9, _ 또는 -"
|
||||||
|
avail_quarantined: "사용할 수 없음"
|
||||||
|
avail_unknown: "확인할 수 없습니다 — 연결 문제입니다. 다시 시도하세요."
|
||||||
|
username_none: "설정되지 않음"
|
||||||
|
advanced_privacy: "고급 프라이버시"
|
||||||
|
tap_reveal: "탭해서 표시"
|
||||||
|
notif_someone: "누군가"
|
||||||
|
notif_private_received: "결제를 받았습니다. Goblin을 열어 확인하세요."
|
||||||
|
notif_private_requested: "새 결제 요청이 있습니다. Goblin을 열어 확인하세요."
|
||||||
|
username:
|
||||||
|
title: "사용자 이름"
|
||||||
|
authority: "이름 기관"
|
||||||
|
authority_blurb: "기본값으로 두거나, 다른 인스턴스를 가리켜 그곳에서 호스팅하는 이름을 사용하세요."
|
||||||
|
learn_more: "자세히 보기"
|
||||||
|
custom: "사용자 지정 기관"
|
||||||
|
advprivacy:
|
||||||
|
intro: "알림이 무엇을 드러내는지 제어하고, 이 기기에서 잔액과 기록을 숨기세요."
|
||||||
|
notifications: "알림"
|
||||||
|
hide_names: "이름 숨기기"
|
||||||
|
hide_names_sub: "알림에서 누가 결제했는지 생략"
|
||||||
|
hide_details: "모든 세부정보 숨기기"
|
||||||
|
hide_details_sub: "이름이나 금액 없이 비공개 알림만 표시"
|
||||||
|
anon: "익명 모드"
|
||||||
|
anon_toggle: "잔액과 활동 흐리게 표시"
|
||||||
|
anon_sub: "탭해서 표시할 때까지 잔액과 활동이 점으로 표시됩니다"
|
||||||
|
advanced:
|
||||||
|
title: "고급"
|
||||||
|
intro: "GRIM의 저수준 지갑 도구입니다. 보통은 필요하지 않습니다."
|
||||||
|
own_node_desc: "공개 노드를 신뢰하는 대신 이 기기에서 완전한 Grin 노드를 동기화합니다."
|
||||||
|
own_node_active: "내 노드를 실행하는 중"
|
||||||
|
repair: "지갑 복구"
|
||||||
|
repair_desc: "체인을 다시 스캔하고 누락된 출력을 복구합니다. 시간이 걸릴 수 있습니다."
|
||||||
|
repair_unavailable: "먼저 동기화된 노드 연결이 필요합니다."
|
||||||
|
repairing: "복구하는 중… %{pct}%"
|
||||||
|
restore: "지갑 복원"
|
||||||
|
restore_desc: "로컬 데이터를 삭제하고 복구 문구로 다시 만듭니다. 복구로 해결되지 않을 때 사용하세요 — 이후 지갑을 다시 열어야 합니다."
|
||||||
|
restore_confirm: "다시 눌러서 복원"
|
||||||
|
show_phrase: "복구 문구"
|
||||||
|
phrase_desc: "24개의 grin 복구 단어입니다 — 자금을 복구할 수 있는 유일한 방법입니다. 오프라인에 비공개로 보관하세요."
|
||||||
|
reveal: "문구 표시"
|
||||||
|
hide: "숨기기"
|
||||||
|
password: "지갑 비밀번호"
|
||||||
|
wrong_password: "비밀번호가 틀렸습니다."
|
||||||
|
delete: "지갑 삭제"
|
||||||
|
delete_desc: "이 기기에서 이 지갑과 모든 신원을 영구적으로 삭제합니다. 먼저 백업하세요. 복구 문구가 없으면 자금을 복구할 수 없습니다."
|
||||||
|
delete_confirm: "다시 눌러서 삭제"
|
||||||
|
manage_node: "노드 연결 관리"
|
||||||
|
repair_confirm: "예, 지금 복구"
|
||||||
|
repair_confirm_note: "복구는 체인을 다시 스캔하며 몇 분 정도 걸릴 수 있습니다."
|
||||||
|
restore_confirm_note: "로컬 데이터를 지우고 복구 문구로 다시 만듭니다 — 몇 분 정도 걸릴 수 있습니다."
|
||||||
|
nostr_key: "Nostr 키"
|
||||||
|
nostr_key_desc: "nostr 신원의 비밀 키인 nsec입니다. 복사하거나 QR 코드를 표시해 magick.market 같은 nostr 앱에 로그인하세요. 이 키를 가진 사람은 누구나 내 신원을 통제할 수 있으니 비공개로 보관하세요."
|
||||||
|
reveal_nsec: "키 표시"
|
||||||
|
copy_nsec: "nsec 복사"
|
||||||
|
show_qr: "QR 표시"
|
||||||
|
hide_qr: "QR 숨기기"
|
||||||
|
nostr_section: "고급 nostr 설정"
|
||||||
|
backup_caption: "지갑과 모든 신원이 포함됩니다."
|
||||||
|
danger_zone: "위험 구역"
|
||||||
|
delete_warning: "이 기기의 복구 문구와 모든 신원이 삭제됩니다. 백업하지 않은 항목은 영구적으로 사라집니다."
|
||||||
|
download_backup: "백업 다운로드"
|
||||||
|
delete_final: "영구 삭제"
|
||||||
|
privacy:
|
||||||
|
title: "네트워크 프라이버시"
|
||||||
|
intro: "Goblin은 네트워크로 아주 적은 정보만 전송하며, 각 정보는 종단 간 암호화로 봉인되어 릴레이가 내용을 읽거나 나와 연결 지을 수 없습니다."
|
||||||
|
payments: "결제"
|
||||||
|
payments_blurb: "slatepack을 담은 모든 nostr 메시지입니다."
|
||||||
|
usernames: "사용자 이름"
|
||||||
|
usernames_blurb: "이름 기관과 주고받는 NIP-05 이름 조회입니다."
|
||||||
|
price_avatars: "가격"
|
||||||
|
price_avatars_blurb: "금액 옆에 표시되는 실시간 환율입니다."
|
||||||
|
grin_node: "Grin 노드"
|
||||||
|
grin_node_blurb: "블록을 동기화하고 거래를 네트워크에 전파합니다. 모두에게 동일한 공개 체인 데이터이며 신원과 절대 연결되지 않습니다."
|
||||||
|
how_privacy: "프라이버시가 작동하는 방식"
|
||||||
|
how_privacy_blurb: "결제와 메시지는 인증된 암호화로 봉인되어 비공개 메시지로 전달되므로, 릴레이가 이를 읽거나 결제를 나와 연결 지을 수 없습니다."
|
||||||
|
always_direct: "항상 직접 연결"
|
||||||
|
tor_off: "Tor 꺼짐"
|
||||||
|
tor_on: "Tor 켜짐"
|
||||||
|
tor_switch: "Tor로 라우팅"
|
||||||
|
tor_switch_sub: "릴레이로부터 IP를 숨깁니다."
|
||||||
|
vpn_note: "Tor가 꺼져 있으면 릴레이가 내 IP 주소를 볼 수 있습니다. 최소한 VPN 사용을 권장합니다."
|
||||||
|
pairing:
|
||||||
|
title: "기준 통화"
|
||||||
|
intro: "내 잔액과 금액이 어떤 통화로 표시되는지입니다."
|
||||||
|
pair_with: "기준 통화 선택"
|
||||||
|
rates_note: "환율은 기준 통화가 설정된 동안에만 Tor를 통해 가져옵니다 — 꺼두면 어떤 환율 요청도 기기 밖으로 나가지 않습니다."
|
||||||
|
relays:
|
||||||
|
title: "릴레이"
|
||||||
|
intro: "결제 메시지는 아래의 모든 릴레이에 복제됩니다. 하나만 연결되어도 받을 수 있습니다."
|
||||||
|
your_relays: "내 릴레이"
|
||||||
|
add_relay: "릴레이 추가"
|
||||||
|
add_relay_btn: "릴레이 추가"
|
||||||
|
save_reconnect: "저장하고 다시 연결"
|
||||||
|
none: "없음"
|
||||||
|
count: "릴레이 %{n}개"
|
||||||
|
node:
|
||||||
|
title: "노드"
|
||||||
|
connection: "연결"
|
||||||
|
integrated: "통합 노드"
|
||||||
|
applies_after: "지갑을 잠그고 다시 잠금 해제한 후에 적용됩니다."
|
||||||
|
add_external: "외부 노드 추가"
|
||||||
|
api_secret_hint: "API 시크릿(선택 사항)"
|
||||||
|
add_node: "노드 추가"
|
||||||
|
integrated_host: "통합 노드"
|
||||||
|
summary_syncing: "%{conn} · 동기화하는 중"
|
||||||
|
summary_block: "블록 %{height} · %{conn}"
|
||||||
|
nips:
|
||||||
|
title: "nostr와 NIP"
|
||||||
|
intro1: "Goblin은 nostr를 사용합니다 — 간단한 릴레이 서버를 통해 서명된 메시지를 전달하는 개방형 프로토콜입니다. 내 지갑은 자체 nostr 신원을 가지고 있습니다: 자금과 복구 문구와는 의도적으로 독립된, 별도의 무작위 키입니다. 모든 결제는 신원 간의 종단 간 암호화된 다이렉트 메시지로 전송되며, 그 안에 slatepack이 담겨 있습니다."
|
||||||
|
intro2: "goblin.st는 Goblin의 이름 서비스입니다. 사용자 이름을 등록하면 이름 → 키 매핑이 그곳에 게시되어(NIP-05), 사람들이 긴 npub 대신 나에게 결제할 수 있습니다. 사용자 이름은 공개되지만, 결제 내용은 절대 공개되지 않습니다. NIP는 이 프로토콜의 구성 요소입니다 — 하나를 탭해 사양을 읽어보세요."
|
||||||
|
n05_title: "이름"
|
||||||
|
n05_blurb: "사용자이름@goblin.st를 내 키에 매핑해, 핸들이 주소처럼 작동하게 합니다."
|
||||||
|
n17_title: "비공개 메시지"
|
||||||
|
n17_blurb: "모든 결제가 이동하는 암호화된 DM 봉투입니다."
|
||||||
|
n44_title: "암호화"
|
||||||
|
n44_blurb: "이 메시지 안에서 사용되는 인증된 암호 방식입니다."
|
||||||
|
n49_title: "키 암호화"
|
||||||
|
n49_blurb: "비밀 키가 저장 상태에서 비밀번호로 잠긴 채 보관되는 방식입니다."
|
||||||
|
n59_title: "기프트 랩"
|
||||||
|
n59_blurb: "메시지를 감싸서 릴레이가 누가 누구와 대화하는지 볼 수 없게 합니다."
|
||||||
|
n98_title: "HTTP 인증"
|
||||||
|
n98_blurb: "goblin.st에 대한 사용자 이름 등록 요청에 서명합니다."
|
||||||
|
batch:
|
||||||
|
title: "청구서 %{n}건 승인"
|
||||||
|
blurb: "%{name}님에게 각각 %{amount}씩 %{n}건의 결제 요청을 보냅니다 — 총 %{total}. 각 요청은 고유한 새 결제 주소를 받습니다."
|
||||||
|
approve: "승인"
|
||||||
|
login:
|
||||||
|
title: "로그인"
|
||||||
|
headline: "%{domain}에 로그인할까요?"
|
||||||
|
identity: "다음으로 로그인:"
|
||||||
|
explain: "이는 일회성 로그인 요청에 서명합니다. 키를 공유하지 않으며, 사이트가 나를 대신해 행동할 수도 없습니다."
|
||||||
|
pass_prompt: "로그인하려면 지갑 비밀번호를 입력하세요."
|
||||||
|
confirm: "로그인"
|
||||||
|
sent: "%{domain}에 로그인했습니다"
|
||||||
|
failed: "%{domain}에 연결할 수 없습니다. 로그인이 전달되지 않았습니다."
|
||||||
|
authorize:
|
||||||
|
title: "승인"
|
||||||
|
headline: "%{domain}을(를) 승인할까요?"
|
||||||
|
kind_post: "공개 게시물"
|
||||||
|
kind_repost: "리포스트"
|
||||||
|
kind_reaction: "반응"
|
||||||
|
kind_article: "글(장문)"
|
||||||
|
kind_unknown: "종류 %{n}(인식할 수 없음)"
|
||||||
|
unknown_caution: "Goblin이 이 이벤트 유형을 인식하지 못합니다. %{domain}을(를) 신뢰하는 경우에만 승인하세요."
|
||||||
|
identity: "다음으로 서명:"
|
||||||
|
explain: "승인하면 이 이벤트 하나에 서명해 %{domain}에 전달합니다. Goblin은 아무것도 게시하지 않고, 키도 공유되지 않으며, 사이트는 세션이나 향후 권한을 얻지 못합니다."
|
||||||
|
pass_prompt: "승인하려면 지갑 비밀번호를 입력하세요."
|
||||||
|
confirm: "승인"
|
||||||
|
sent: "%{domain}을(를) 승인했습니다"
|
||||||
|
failed: "%{domain}에 연결할 수 없습니다. 아무것도 전달되지 않았습니다."
|
||||||
|
truncated: "잘림, %{n}자 더 있음"
|
||||||
|
show_full: "전체 보기"
|
||||||
|
show_less: "간략히 보기"
|
||||||
|
replying_to: "%{id}에 답글"
|
||||||
|
mentions: "%{id} 언급"
|
||||||
|
repost_of: "%{id} 리포스트"
|
||||||
|
by_author: "%{id} 작성"
|
||||||
|
reacts_to: "%{id}에 반응"
|
||||||
|
article_title: "제목: %{title}"
|
||||||
|
trust:
|
||||||
|
title: "신뢰"
|
||||||
|
headline: "%{domain}을(를) 신뢰할까요?"
|
||||||
|
identity: "다음으로 로그인:"
|
||||||
|
lead: "%{domain}은(는) 매번 확인하지 않아도 이 세션 동안 나를 대신해 행동할 수 있습니다. 돈이 오가는 작업은 항상 확인을 요청합니다."
|
||||||
|
grant_intro: "확인 없이 서명할 수 있는 항목:"
|
||||||
|
cat_social: "게시물과 반응"
|
||||||
|
cat_dm: "다이렉트 메시지"
|
||||||
|
cat_market: "판매 목록"
|
||||||
|
cat_identity: "프로필과 목록"
|
||||||
|
cat_delete: "삭제"
|
||||||
|
cat_http: "업로드 및 HTTP 인증"
|
||||||
|
cat_unknown: "기타 이벤트 유형(종류 %{n})"
|
||||||
|
login_excluded: "로그인 권한은 절대 사이트에 부여되지 않습니다."
|
||||||
|
money_line: "돈이 오갈 때는 항상 비밀번호를 확인합니다. 어떤 것도 조용히 지출되거나 판매되지 않습니다."
|
||||||
|
duration: "이 세션 동안만 유지됩니다. 설정의 신뢰하는 사이트에서 언제든 종료할 수 있습니다."
|
||||||
|
pass_prompt: "이 사이트를 신뢰하려면 지갑 비밀번호를 입력하세요."
|
||||||
|
confirm_hold: "길게 눌러 이 세션 동안 신뢰"
|
||||||
|
sent: "이 세션 동안 %{domain}을(를) 신뢰합니다"
|
||||||
|
failed: "%{domain}에 연결할 수 없습니다. 세션이 시작되지 않았습니다."
|
||||||
|
announce_failed: "신뢰했지만 %{domain}이(가) 아직 세션을 확인하지 않았습니다."
|
||||||
|
notice_volume: "신뢰하는 사이트가 서명을 많이 하고 있습니다."
|
||||||
|
notice_decrypt: "신뢰하는 사이트가 내 메시지를 읽고 있습니다."
|
||||||
|
money:
|
||||||
|
title: "확인"
|
||||||
|
headline: "%{domain}에서 확인할까요?"
|
||||||
|
identity: "%{id}(으)로 서명"
|
||||||
|
explain: "이는 가치를 이동하거나 확정하므로 항상 확인을 요청합니다. 승인하면 이 동작 하나에 서명합니다."
|
||||||
|
pass_prompt: "확인하려면 지갑 비밀번호를 입력하세요."
|
||||||
|
confirm_hold: "길게 눌러서 확인"
|
||||||
|
encrypt_desc: "결제를 확정하는 암호화된 주문 메시지입니다."
|
||||||
|
trusted_sites:
|
||||||
|
title: "신뢰하는 사이트"
|
||||||
|
intro: "이번 세션에 신뢰한 사이트는 확인 없이 위험도가 낮은 작업에 서명할 수 있습니다. 돈이 오갈 때는 항상 확인합니다. 여기에서 언제든 세션을 종료할 수 있습니다."
|
||||||
|
empty: "신뢰하는 사이트가 없습니다. 로그인할 때 사이트를 신뢰하면 서명 세션이 시작됩니다."
|
||||||
|
none: "없음"
|
||||||
|
can_sign: "조용히 서명 가능: %{cats}"
|
||||||
|
time_left: "%{mins}분 후 종료"
|
||||||
|
paused: "일시 중지됨, 서명이 많음"
|
||||||
|
resume: "재개"
|
||||||
|
end: "세션 종료"
|
||||||
|
identities:
|
||||||
|
title: "신원"
|
||||||
|
switch_hint: "신원 전환"
|
||||||
|
blurb: "내 신원은 결제를 받을 때 사용하는 이름입니다. 모두 이 지갑 하나로 들어옵니다. 새 신원을 추가하거나, 전환하거나, 필요 없는 것을 삭제하세요."
|
||||||
|
privacy_note: "이 신원들은 하나의 지갑과 하나의 잔액을 공유합니다 — 서로 다른 자금이 아니라 서로 다른 입구일 뿐입니다. 진짜로 분리된 자금이 필요하면 별도의 지갑을 사용하세요."
|
||||||
|
held: "보유한 신원"
|
||||||
|
add: "신원 추가"
|
||||||
|
add_title: "신원 추가"
|
||||||
|
nsec_hint: "nsec 붙여넣기"
|
||||||
|
generate_note: "이 지갑을 위한 새롭고 익명인 키가 생성됩니다."
|
||||||
|
generate: "생성"
|
||||||
|
import: "가져오기"
|
||||||
|
choose_backup: ".backup 파일 선택"
|
||||||
|
backup_selected: "백업 파일 선택됨"
|
||||||
|
delete_short: "신원 삭제"
|
||||||
|
delete_title: "%{name}을(를) 삭제할까요?"
|
||||||
|
delete_blurb: "이 지갑에서 신원을 영구적으로 삭제합니다. 이미 받은 돈은 잔액에 그대로 남습니다."
|
||||||
|
delete_backup_note: "먼저 백업하세요 — nsec이나 .backup 파일이 없으면 복구할 수 없습니다."
|
||||||
|
delete_confirm: "삭제"
|
||||||
|
manage_title: "신원 관리"
|
||||||
|
tag_note: "비공개 이름을 지정하세요. 나만 볼 수 있으며 절대 공유되지 않습니다."
|
||||||
|
tag_hint: "비공개 이름"
|
||||||
|
tag_save: "저장"
|
||||||
|
pass_prompt: "지갑 비밀번호로 각 신원을 암호화하고 잠금 해제합니다. 계속하려면 입력하세요."
|
||||||
|
working: "처리하는 중…"
|
||||||
|
onboarding:
|
||||||
|
privacy:
|
||||||
|
kicker: "네트워크 프라이버시"
|
||||||
|
continue: "계속"
|
||||||
|
intro:
|
||||||
|
private_money_head: "프라이빗한 돈"
|
||||||
|
private_money_body: "Goblin은 grin을 위한 지갑입니다 — 체인에 금액도 주소도 남지 않는 디지털 현금입니다."
|
||||||
|
send_like_message_head: "메시지처럼 보내기"
|
||||||
|
send_like_message_body: "사용자 이름이나 npub으로 결제하면 nostr와 Tor를 통해 종단 간 암호화된 메시지로 전달됩니다 — 중간의 누구도 금액이나 당사자를 볼 수 없습니다."
|
||||||
|
yours_alone_head: "오직 나만의 것"
|
||||||
|
yours_alone_body: "키, 이름, 기록은 모두 이 기기에 남습니다. GRIM 지갑을 기반으로 만들어졌습니다."
|
||||||
|
get_started: "시작하기"
|
||||||
|
footnote: "약 1분 정도 걸립니다. 나중에 모든 것을 바꿀 수 있습니다."
|
||||||
|
node:
|
||||||
|
kicker: "1/3단계 · 네트워크"
|
||||||
|
title: "Goblin이 체인을\n어떻게 지켜볼까요?"
|
||||||
|
own_title: "내 노드 직접 운영"
|
||||||
|
own_badge: "프라이빗"
|
||||||
|
own_body: "누구도 신뢰하지 않습니다 — 지갑이 직접 체인을 확인합니다. 설정을 마치는 동안 백그라운드에서 동기화됩니다."
|
||||||
|
connect_title: "노드에 연결"
|
||||||
|
connect_badge: "즉시"
|
||||||
|
connect_body: "동기화를 기다릴 필요가 없습니다. 선택한 노드가 내 지갑의 조회 내용을 볼 수 있습니다."
|
||||||
|
changeable: "설정 → 노드에서 언제든 변경할 수 있습니다."
|
||||||
|
continue: "계속"
|
||||||
|
url_invalid: "노드 URL은 http:// 또는 https://로 시작해야 합니다"
|
||||||
|
wallet:
|
||||||
|
kicker: "2/3단계 · 지갑"
|
||||||
|
title: "지갑을 설정하세요"
|
||||||
|
create_new: "새로 만들기"
|
||||||
|
restore_from_seed: "복구 문구로 복원"
|
||||||
|
import_identity: "신원 가져오기"
|
||||||
|
import_identity_hint: "이 단계 이후에 기존 키(.backup 파일 또는 nsec)를 가져오게 됩니다."
|
||||||
|
name_hint: "지갑 이름"
|
||||||
|
password_hint: "비밀번호"
|
||||||
|
repeat_password_hint: "비밀번호 확인"
|
||||||
|
restore_hint: "복구 문구를 미리 준비하세요 — 다음 단계에서 입력합니다."
|
||||||
|
create_hint: "다음으로 적어 둘 24개의 복구 단어를 받게 됩니다. 이것이 곧 돈입니다 — 이 단어를 가진 사람이 자금을 가집니다."
|
||||||
|
continue: "계속"
|
||||||
|
passwords_no_match: "비밀번호가 일치하지 않습니다"
|
||||||
|
words:
|
||||||
|
kicker: "2/3단계 · 지갑"
|
||||||
|
title_restore: "복구 단어를 입력하세요"
|
||||||
|
title_create: "이 단어들을 적어 두세요"
|
||||||
|
write_down_hint: "종이에 순서대로 적으세요. 이 단어를 가진 사람은 누구나 자금을 가져갈 수 있습니다. 단어 없이 기기를 잃어버리면 자금도 잃게 됩니다."
|
||||||
|
paste: "붙여넣기"
|
||||||
|
scan_qr: "QR 스캔"
|
||||||
|
copy_clipboard: "클립보드에 복사(권장하지 않음)"
|
||||||
|
restore_wallet: "지갑 복원"
|
||||||
|
wrote_them_down: "다 적었습니다"
|
||||||
|
fill_every_word: "모든 단어를 채우세요 — 단어를 탭해서 수정하거나, 문구를 붙여넣으세요."
|
||||||
|
unlock_backup: "백업 잠금 해제"
|
||||||
|
backup_unlocked: "백업이 잠금 해제되었습니다. 지갑을 열면 신원이 복원됩니다."
|
||||||
|
confirm:
|
||||||
|
kicker: "2/3단계 · 지갑"
|
||||||
|
title: "이제 확인해 보세요"
|
||||||
|
enter_hint: "방금 적은 단어를 입력하세요. 단어를 탭해서 입력하세요."
|
||||||
|
paste: "붙여넣기"
|
||||||
|
create_wallet: "지갑 만들기"
|
||||||
|
keep_going: "계속하세요 — 모든 단어를, 순서대로."
|
||||||
|
identity:
|
||||||
|
kicker: "3/3단계 · 신원"
|
||||||
|
title: "나의 결제 신원"
|
||||||
|
key_being_made: "키를 만드는 중…"
|
||||||
|
connected_tor: "Tor로 연결됨"
|
||||||
|
connecting_tor: "Tor로 연결하는 중…"
|
||||||
|
connected_direct: "연결됨 (직접)"
|
||||||
|
connecting_direct: "연결 중 (직접)…"
|
||||||
|
fresh_key_blurb: "복구 문구에 속하지 않는 결제 키입니다 — 자금은 그대로 둔 채 언제든 교체해 프라이버시를 지키세요."
|
||||||
|
restoring: "신원을 복원하는 중…"
|
||||||
|
clean_slate_blurb: "새 출발을 원하시나요? 언제든 완전히 새로운 키로 바꾸세요 — 새로운 나는 이전과 연결되지 않습니다. 같은 지갑, 새로운 얼굴입니다."
|
||||||
|
pick_username: "사용자 이름 선택 — 선택 사항"
|
||||||
|
username_blurb: "친구들이 긴 키 대신 내 이름으로 결제합니다. 선택 사항이며 — 언제든 등록할 수 있습니다."
|
||||||
|
username_field_hint: "내이름"
|
||||||
|
working: "처리하는 중…"
|
||||||
|
claim_username: "사용자 이름 등록"
|
||||||
|
available_when_connected: "Tor가 연결되면 사용할 수 있습니다 — 또는 건너뛰고 나중에 등록하세요."
|
||||||
|
youre: "%{name}님입니다"
|
||||||
|
claimed_title: "%{name}이(가) 내 것이 되었습니다"
|
||||||
|
claimed_blurb: "이제 친구들이 이름으로 결제할 수 있습니다. 모든 준비가 끝났습니다 — 지갑을 열어 보세요."
|
||||||
|
open_wallet: "내 지갑 열기"
|
||||||
|
skip_for_now: "지금은 건너뛰기"
|
||||||
|
import_existing: "이미 Goblin 신원이 있나요? 가져오기"
|
||||||
|
import_title: "신원 가져오기"
|
||||||
|
import_blurb: "nsec을 붙여넣거나 .backup 파일을 선택해서, 새 신원 대신 기존 키와 사용자 이름을 유지하세요."
|
||||||
|
errors:
|
||||||
|
cant_open: "지갑을 열 수 없습니다: %{err}"
|
||||||
|
cant_create: "지갑을 만들 수 없습니다: %{err}"
|
||||||
|
send:
|
||||||
|
scan_to_request: "스캔해서 요청"
|
||||||
|
scan_to_pay: "스캔해서 결제"
|
||||||
|
tab_scan: "스캔"
|
||||||
|
tab_my_code: "내 코드"
|
||||||
|
request_from: "요청 대상"
|
||||||
|
send_to: "받는 사람"
|
||||||
|
search_hint: "핸들, npub 또는 이름"
|
||||||
|
suggested: "%{icon} 추천"
|
||||||
|
no_contacts: "아직 연락처가 없습니다. 핸들로 사람을 찾아보세요."
|
||||||
|
no_profile: "프로필 없음"
|
||||||
|
tag_contact: "연락처"
|
||||||
|
tag_on_nostr: "nostr에서"
|
||||||
|
searching_nostr: "nostr에서 검색하는 중…"
|
||||||
|
unverified_title: "검증되지 않은 키에 결제할까요?"
|
||||||
|
unverified_body: "이 키에 대해 게시된 nostr 프로필이 없습니다 — 새로 만든 키이거나, 익명이거나, 잘못 입력했을 수 있습니다. 보내기 전에 올바른 상대인지 다시 한번 확인하세요."
|
||||||
|
keep_looking: "계속 찾기"
|
||||||
|
pay_anyway: "그래도 결제"
|
||||||
|
scan_not_recipient: "이 QR 코드는 goblin 수신자가 아닙니다 — npub 또는 핸들이 필요합니다"
|
||||||
|
scan_prompt: "goblin 코드를 화면에 비추면 활성화됩니다"
|
||||||
|
scan_to_pay_me: "스캔해서 나에게 결제"
|
||||||
|
share_btn: "%{icon} 공유"
|
||||||
|
share_message: "Goblin에서 저에게 결제하세요 — %{handle}\n%{link}\nnpub: %{npub}"
|
||||||
|
none_found: "%{label}에 대한 결과가 없습니다"
|
||||||
|
enter_recipient: "핸들, npub 또는 이름을 입력하세요"
|
||||||
|
amount_title: "금액"
|
||||||
|
to_name: "%{name}님에게"
|
||||||
|
not_enough: "grin이 충분하지 않습니다"
|
||||||
|
max: "최대"
|
||||||
|
note_label: "메모"
|
||||||
|
note_hint: "메모 추가…"
|
||||||
|
add_note: "메모 추가"
|
||||||
|
edit_note: "메모 수정"
|
||||||
|
note_cancel: "취소"
|
||||||
|
note_save: "저장"
|
||||||
|
review_btn: "검토"
|
||||||
|
confirm_request: "요청 확인"
|
||||||
|
review_title: "검토"
|
||||||
|
requesting_from: "%{name}님에게 요청 중"
|
||||||
|
youre_sending: "%{name}님에게 보내는 중"
|
||||||
|
row_from: "보낸 사람"
|
||||||
|
row_to: "받는 사람"
|
||||||
|
row_note: "메모"
|
||||||
|
row_proof: "결제 증명"
|
||||||
|
row_proof_val: "포함됨"
|
||||||
|
row_proof_shared: "증명 공유 대상"
|
||||||
|
row_they_pay: "상대방 결제"
|
||||||
|
row_they_pay_val: "승인할 때만"
|
||||||
|
row_delivery: "전달 방식"
|
||||||
|
row_delivery_val: "NIP-44 암호화, Tor 경유"
|
||||||
|
row_network_fee: "네트워크 수수료"
|
||||||
|
row_network_fee_val: "잔액에서 차감됨"
|
||||||
|
row_privacy: "프라이버시"
|
||||||
|
row_privacy_val: "Mimblewimble + Tor"
|
||||||
|
send_request_btn: "요청 보내기"
|
||||||
|
request_approve_hint: "상대방이 승인할 요청을 받게 됩니다"
|
||||||
|
hold_to_send: "길게 눌러서 보내기"
|
||||||
|
lower_amount: "돌아가서 금액을 낮추세요"
|
||||||
|
hold_confirm_hint: "길게 눌러서 확인"
|
||||||
|
requesting: "요청하는 중…"
|
||||||
|
sending: "보내는 중…"
|
||||||
|
they: "상대방"
|
||||||
|
request_blocked: "%{who}님은 요청을 받지 않습니다. 대신 grin을 보내 달라고 요청하세요."
|
||||||
|
failed_request_title: "요청할 수 없음"
|
||||||
|
failed_send_title: "보낼 수 없음"
|
||||||
|
failed_request_body: "요청을 전달하지 못했습니다. 대신 grin을 보내 달라고 요청하세요."
|
||||||
|
failed_send_body: "결제가 전달되지 않았습니다. grin은 안전합니다 — 다시 시도하세요."
|
||||||
|
try_again_btn: "다시 시도"
|
||||||
|
close_btn: "닫기"
|
||||||
|
success:
|
||||||
|
requested: "요청됨"
|
||||||
|
sent: "보냄"
|
||||||
|
from: "보낸 사람"
|
||||||
|
to: "받는 사람"
|
||||||
|
subtitle: "%{dir}: %{who} · 방금"
|
||||||
|
done_btn: "완료"
|
||||||
|
receipt_btn: "영수증"
|
||||||
@@ -44,7 +44,7 @@ wallets:
|
|||||||
await_fin_amount: Ожидает завершения
|
await_fin_amount: Ожидает завершения
|
||||||
locked_amount: Заблокировано
|
locked_amount: Заблокировано
|
||||||
txs_empty: 'Для получения средств вручную или через транспорт используйте кнопки %{message} или %{transport} внизу экрана, для изменения настроек кошелька нажмите кнопку %{settings}.'
|
txs_empty: 'Для получения средств вручную или через транспорт используйте кнопки %{message} или %{transport} внизу экрана, для изменения настроек кошелька нажмите кнопку %{settings}.'
|
||||||
title: Кошельки
|
title: Goblin
|
||||||
create_desc: Создайте или импортируйте существующий кошелёк из сохранённой фразы восстановления.
|
create_desc: Создайте или импортируйте существующий кошелёк из сохранённой фразы восстановления.
|
||||||
add: Добавить кошелёк
|
add: Добавить кошелёк
|
||||||
name: 'Название:'
|
name: 'Название:'
|
||||||
@@ -95,6 +95,7 @@ wallets:
|
|||||||
tx_finalizing: Завершение
|
tx_finalizing: Завершение
|
||||||
tx_posting: Публикация
|
tx_posting: Публикация
|
||||||
tx_confirmed: Подтверждено
|
tx_confirmed: Подтверждено
|
||||||
|
tx_stale: Слишком долго в ожидании
|
||||||
txs: Транзакции
|
txs: Транзакции
|
||||||
tx: Транзакция
|
tx: Транзакция
|
||||||
messages: Сообщения
|
messages: Сообщения
|
||||||
@@ -355,4 +356,616 @@ keyboard:
|
|||||||
m: ь
|
m: ь
|
||||||
m1: б
|
m1: б
|
||||||
m2: ю
|
m2: ю
|
||||||
m3: ё
|
m3: ё
|
||||||
|
goblin:
|
||||||
|
home:
|
||||||
|
anonymous: "Аноним"
|
||||||
|
connected_tor: "Подключено через Tor"
|
||||||
|
tor_ready: "Tor готов · реле…"
|
||||||
|
connecting_tor: "Подключение к Tor…"
|
||||||
|
connected_direct: "Подключено (напрямую)"
|
||||||
|
connecting_direct: "Подключение (напрямую)…"
|
||||||
|
cant_reach_node: "Нет связи с узлом"
|
||||||
|
node_synced: "Узел синхронизирован"
|
||||||
|
syncing: "Синхронизация…"
|
||||||
|
balance_updating: "Баланс обновляется…"
|
||||||
|
balance_stale: "Узел недоступен · последний известный баланс"
|
||||||
|
fiat_unavailable: "Курс недоступен"
|
||||||
|
listening: "Ожидание платежей"
|
||||||
|
block: "Блок %{height}"
|
||||||
|
waiting_for_chain: "Ожидание цепочки…"
|
||||||
|
nav_wallet: "Кошелёк"
|
||||||
|
nav_pay: "Оплатить"
|
||||||
|
nav_activity: "Действия"
|
||||||
|
nav_receive: "Получить"
|
||||||
|
nav_settings: "Настройки"
|
||||||
|
back_again: "Нажмите «назад» ещё раз, чтобы сменить кошелёк"
|
||||||
|
activity: "Действия"
|
||||||
|
news: "Новости"
|
||||||
|
empty_title: "Пока нет действий"
|
||||||
|
empty_sub: "Отправьте или получите grin, чтобы начать."
|
||||||
|
recent: "Недавние"
|
||||||
|
scan_to_pay: "Сканируйте для оплаты"
|
||||||
|
type_amount: "Введите сумму"
|
||||||
|
request: "Запросить"
|
||||||
|
pay: "Оплатить"
|
||||||
|
enter_amount: "Введите сумму для оплаты или запроса"
|
||||||
|
activity:
|
||||||
|
canceled: "отменено"
|
||||||
|
pending: "в ожидании"
|
||||||
|
earlier: "Ранее"
|
||||||
|
today: "Сегодня"
|
||||||
|
yesterday: "Вчера"
|
||||||
|
title: "Действия"
|
||||||
|
requests: "Запросы"
|
||||||
|
empty_title: "Пока нет действий"
|
||||||
|
empty_sub: "Здесь появятся ваши платежи."
|
||||||
|
pending_header: "В ожидании"
|
||||||
|
receipt:
|
||||||
|
title: "Квитанция"
|
||||||
|
not_found: "Транзакция не найдена"
|
||||||
|
for_note: "За %{note}"
|
||||||
|
details: "Детали транзакции"
|
||||||
|
canceled: "Отменено"
|
||||||
|
expired: "Истекло"
|
||||||
|
funds_returned: "Средства возвращены"
|
||||||
|
complete: "Завершено"
|
||||||
|
payment_received: "Платёж получен"
|
||||||
|
payment_sent: "Платёж успешно отправлен"
|
||||||
|
pending: "В ожидании"
|
||||||
|
confs: "%{c}/%{r} подтверждений"
|
||||||
|
waiting_to_confirm: "Ожидание подтверждения"
|
||||||
|
paying: "Оплата…"
|
||||||
|
you: "Вы"
|
||||||
|
to: "Кому"
|
||||||
|
from: "От"
|
||||||
|
nostr: "nostr"
|
||||||
|
identity: "Личность"
|
||||||
|
fee_none: "Нет"
|
||||||
|
network_fee: "Сетевая комиссия"
|
||||||
|
privacy: "Приватность"
|
||||||
|
privacy_value: "Mimblewimble + Tor"
|
||||||
|
transaction: "Транзакция"
|
||||||
|
cancel_request: "Отменить запрос"
|
||||||
|
cancel_send: "Отменить платёж"
|
||||||
|
stale_note: "Этот платёж уже некоторое время в ожидании."
|
||||||
|
cancel_send_confirm: "Нажмите ещё раз для отмены — он ещё может его получить"
|
||||||
|
cancel_send_done: "Платёж отменён — ваши средства снова доступны"
|
||||||
|
cancel_send_too_late: "Этот платёж уже прошёл и не может быть отменён"
|
||||||
|
waiting_to_receive: "Ожидание, пока %{name} получит…"
|
||||||
|
request:
|
||||||
|
title: "%{name} запрашивает"
|
||||||
|
approve: "Принять"
|
||||||
|
decline: "Отклонить"
|
||||||
|
review_title: "Проверить запрос"
|
||||||
|
hold_to_accept: "Удерживайте, чтобы принять"
|
||||||
|
hold_accept_hint: "Нажмите и удерживайте, чтобы оплатить запрос"
|
||||||
|
receive:
|
||||||
|
title: "Получить"
|
||||||
|
requesting: "Запрос %{amt}%{tsu} — поделитесь, чтобы получить оплату"
|
||||||
|
clear_request: "Очистить запрос"
|
||||||
|
share_handle: "Поделитесь именем, чтобы получить оплату"
|
||||||
|
share_npub: "Поделитесь своим npub, чтобы получить оплату"
|
||||||
|
copied: "Скопировано"
|
||||||
|
copy_nostr_id: "Копировать nostr ID"
|
||||||
|
copy_address: "Копировать адрес"
|
||||||
|
copy: "Копировать"
|
||||||
|
share_message: "Заплатите мне в Goblin (goblin.st) — %{npub}"
|
||||||
|
privacy_note: "Ваше имя публично. Содержимое платежей остаётся зашифрованным в сети."
|
||||||
|
privacy_note_npub: "Ваш npub публичен. Содержимое платежей остаётся зашифрованным в сети."
|
||||||
|
profile:
|
||||||
|
title: "Профиль"
|
||||||
|
activity: "Действия"
|
||||||
|
no_activity: "Пока нет действий с ними."
|
||||||
|
unblock: "Разблокировать"
|
||||||
|
block: "Заблокировать"
|
||||||
|
blocked_blurb: "Заблокирован — их платежи и запросы отклоняются."
|
||||||
|
block_blurb: "Блокировка отклоняет их входящие платежи и запросы."
|
||||||
|
settings:
|
||||||
|
title: "Настройки"
|
||||||
|
connected_nostr: "Подключено к nostr"
|
||||||
|
connecting_relays: "Подключение к реле…"
|
||||||
|
identity: "Личность"
|
||||||
|
copy_npub: "Копировать npub (публичный)"
|
||||||
|
backup_note: "Меняете устройство? Сохраните ОБА: seed-фразу (средства) и файл .backup личности (имя + ключ)."
|
||||||
|
wallet: "Кошелёк"
|
||||||
|
display_unit: "Единица отображения"
|
||||||
|
relays: "Реле"
|
||||||
|
nostr_relays: "Реле Nostr"
|
||||||
|
node: "Узел"
|
||||||
|
min_conf: "Минимум подтверждений"
|
||||||
|
integrated_node: "Настройки встроенного узла"
|
||||||
|
node_advanced: "Дополнительно"
|
||||||
|
slatepacks: "Slatepacks"
|
||||||
|
slatepacks_value: "Ручная транзакция"
|
||||||
|
trusted_sites: "Доверенные сайты"
|
||||||
|
lock_wallet: "Заблокировать кошелёк"
|
||||||
|
switch_wallet: "Сменить кошелёк"
|
||||||
|
advanced: "Дополнительно"
|
||||||
|
privacy: "Приватность"
|
||||||
|
tor_routing: "Маршрутизация через Tor"
|
||||||
|
messages_lookups: "Сообщения и поиск"
|
||||||
|
tor_on: "Вкл"
|
||||||
|
tor_off: "Выкл"
|
||||||
|
auto_accept: "Автоприём"
|
||||||
|
pairing: "Валюта цены"
|
||||||
|
accept_anyone: "Любой"
|
||||||
|
accept_contacts: "Только контакты"
|
||||||
|
accept_ask: "Всегда спрашивать"
|
||||||
|
requests: "Запросы"
|
||||||
|
incoming_requests: "Входящие запросы"
|
||||||
|
incoming_requests_sub: "Разрешить другим запрашивать у вас деньги"
|
||||||
|
hide_amounts: "Скрыть суммы"
|
||||||
|
hide_amounts_sub: "Скрывать полученные суммы в уведомлениях"
|
||||||
|
language: "Язык"
|
||||||
|
update_available: "Доступно обновление"
|
||||||
|
appearance: "Внешний вид"
|
||||||
|
theme: "Тема"
|
||||||
|
theme_light: "Светлая"
|
||||||
|
theme_dark: "Тёмная"
|
||||||
|
theme_yellow: "Жёлтая"
|
||||||
|
archive: "Архив"
|
||||||
|
export_archive: "Экспорт архива"
|
||||||
|
export_archive_caption: "Копирует ваши контакты, историю платежей и запросы в буфер обмена в формате JSON."
|
||||||
|
wipe_history: "Стереть историю платежей"
|
||||||
|
wipe_history_confirm: "Нажмите ещё раз, чтобы стереть — это нельзя отменить"
|
||||||
|
about: "О приложении"
|
||||||
|
goblin: "Goblin"
|
||||||
|
build: "Сборка %{build}"
|
||||||
|
network: "Сеть"
|
||||||
|
network_value: "MW + Tor + nostr"
|
||||||
|
third_party: "Сторонние"
|
||||||
|
grim: "GRIM (исходный кошелёк)"
|
||||||
|
grin_node: "Узел Grin"
|
||||||
|
sp_intro: "Для опытных — обмен сырыми slatepacks вручную, как в GRIM. Используйте только если не можете платить или получать через username."
|
||||||
|
sp_receive_group: "Получить или завершить"
|
||||||
|
sp_receive_blurb: "Вставьте slatepack, который вам дали. Goblin получит платёж, оплатит счёт или завершит и опубликует его."
|
||||||
|
sp_process: "Обработать slatepack"
|
||||||
|
sp_paste_first: "Сначала вставьте slatepack."
|
||||||
|
sp_reply_ready: "Ответ готов — отправьте его обратно отправителю."
|
||||||
|
sp_finalizing: "Завершение и публикация в цепочку…"
|
||||||
|
sp_create_group: "Создать платёж"
|
||||||
|
sp_create_blurb: "Создайте slatepack для передачи кому-то. Они получат его, отправят ответ, а вы завершите его выше."
|
||||||
|
sp_amount_hint: "Сумма в grin"
|
||||||
|
sp_addr_hint: "Адрес получателя (необязательно)"
|
||||||
|
sp_create: "Создать slatepack"
|
||||||
|
sp_ready: "Slatepack готов — передайте его получателю."
|
||||||
|
sp_amount_gt_zero: "Введите сумму больше нуля."
|
||||||
|
sp_to_send: "Slatepack для отправки"
|
||||||
|
sp_copy: "Копировать slatepack"
|
||||||
|
cancel: "Отмена"
|
||||||
|
continue: "Продолжить"
|
||||||
|
final_confirmation: "Финальное подтверждение"
|
||||||
|
type_reset: "Введите RESET"
|
||||||
|
wallet_password: "Пароль кошелька"
|
||||||
|
done: "Готово"
|
||||||
|
close: "Закрыть"
|
||||||
|
import_nsec_hint: "nsec1… или вставленная копия"
|
||||||
|
backup_password_hint: "Пароль резерва (только если экспортирован в другом месте)"
|
||||||
|
import_btn: "Импорт"
|
||||||
|
importing: "Импорт…"
|
||||||
|
import_failed: "Импорт не удался"
|
||||||
|
name_authority: "Сервер имён"
|
||||||
|
name_authority_title: "Сменить сервер имён"
|
||||||
|
name_authority_blurb: "Сервер, который регистрирует и проверяет имена. Укажите другой инстанс, чтобы использовать и оплачивать имена оттуда."
|
||||||
|
name_authority_invalid: "Введите полный URL (https://…)."
|
||||||
|
reset: "Сброс"
|
||||||
|
save: "Сохранить"
|
||||||
|
backup_file: "Сохранить в файл"
|
||||||
|
choose_backup_file: "Выбрать файл .backup"
|
||||||
|
backup_read_failed: "Не удалось прочитать файл."
|
||||||
|
backup_saved: "Резервная копия сохранена"
|
||||||
|
backup_saved_sub: "Храните файл .backup в безопасности. Любой, у кого есть он И ваш пароль, может восстановить весь ваш кошелёк."
|
||||||
|
backup_file_title: "Резервная копия кошелька"
|
||||||
|
backup_file_blurb: "Создаёт один зашифрованный файл .backup с seed-фразой кошелька и всеми личностями. Введите пароль кошелька, чтобы запечатать его."
|
||||||
|
backup_write_failed: "Не удалось сохранить файл."
|
||||||
|
create_backup: "Создать копию"
|
||||||
|
registered: "Зарегистрировано %{name}"
|
||||||
|
released_msg: "Освобождено — имя свободно для занятия"
|
||||||
|
release_confirm: "Освободить %{name}?"
|
||||||
|
release_blurb: "Как только оно свободно, его можно занять — кто угодно, включая ваш следующий ключ. Вы не сможете зарегистрировать другое имя в течение 10 минут."
|
||||||
|
releasing: "Освобождение…"
|
||||||
|
keep_it: "Оставить"
|
||||||
|
release_it: "Освободить"
|
||||||
|
username: "Имя пользователя"
|
||||||
|
username_note: "Отображается как ваше имя. Публично на goblin.st. Платежи остаются зашифрованными."
|
||||||
|
release_username: "Освободить имя"
|
||||||
|
pick_username: "Выберите имя — необязательно"
|
||||||
|
working: "Обработка…"
|
||||||
|
claim: "Занять"
|
||||||
|
err_just_taken: "Это имя только что заняли"
|
||||||
|
err_cooldown: "Вы недавно освободили имя — можно зарегистрировать новое в течение 10 минут."
|
||||||
|
err_unreachable: "Не удалось связаться с goblin.st — сбой соединения. Попробуйте снова."
|
||||||
|
err_release: "Не удалось освободить: %{err}"
|
||||||
|
avail_available: "Доступно!"
|
||||||
|
avail_taken: "Занято"
|
||||||
|
avail_reserved: "Зарезервировано"
|
||||||
|
avail_invalid: "Имена 3–20 символов: a–z, 0–9, _ или -"
|
||||||
|
avail_quarantined: "Недоступно"
|
||||||
|
avail_unknown: "Не удалось проверить — сбой соединения. Попробуйте снова."
|
||||||
|
username_none: "Не задано"
|
||||||
|
advanced_privacy: "Расширенная приватность"
|
||||||
|
tap_reveal: "Нажмите, чтобы показать"
|
||||||
|
notif_someone: "Кто-то"
|
||||||
|
notif_private_received: "Вам заплатили. Откройте Goblin, чтобы увидеть."
|
||||||
|
notif_private_requested: "Новый запрос платежа. Откройте Goblin, чтобы увидеть."
|
||||||
|
username:
|
||||||
|
title: "Имя пользователя"
|
||||||
|
authority: "Центр имён"
|
||||||
|
authority_blurb: "Оставьте по умолчанию или укажите другой сервер, чтобы использовать размещённые там имена."
|
||||||
|
learn_more: "Подробнее"
|
||||||
|
custom: "Свой центр"
|
||||||
|
advprivacy:
|
||||||
|
intro: "Управляйте тем, что раскрывают уведомления, и скрывайте баланс и историю на этом устройстве."
|
||||||
|
notifications: "Уведомления"
|
||||||
|
hide_names: "Скрывать имена"
|
||||||
|
hide_names_sub: "Не показывать в уведомлениях, кто заплатил"
|
||||||
|
hide_details: "Скрывать все данные"
|
||||||
|
hide_details_sub: "Показывать только приватное оповещение, без имени и суммы"
|
||||||
|
anon: "Анонимный режим"
|
||||||
|
anon_toggle: "Скрывать баланс и активность"
|
||||||
|
anon_sub: "Баланс и активность показываются точками, пока вы не нажмёте, чтобы показать"
|
||||||
|
advanced:
|
||||||
|
title: "Дополнительно"
|
||||||
|
intro: "Низкоуровневые инструменты кошелька из GRIM. Обычно они вам не нужны."
|
||||||
|
own_node_desc: "Синхронизируйте полный узел Grin на этом устройстве вместо доверия публичному."
|
||||||
|
own_node_active: "Ваш узел запущен"
|
||||||
|
repair: "Починить кошелёк"
|
||||||
|
repair_desc: "Повторно просканировать цепочку и восстановить недостающие выходы. Это может занять время."
|
||||||
|
repair_unavailable: "Сначала нужно синхронизированное подключение к узлу."
|
||||||
|
repairing: "Починка… %{pct}%"
|
||||||
|
restore: "Восстановить кошелёк"
|
||||||
|
restore_desc: "Удалить локальные данные и пересоздать из seed-фразы. Используйте, если починка не помогла — после этого откройте кошелёк заново."
|
||||||
|
restore_confirm: "Нажмите ещё раз для восстановления"
|
||||||
|
show_phrase: "Фраза восстановления"
|
||||||
|
phrase_desc: "Ваши 24 seed-слова grin — единственный способ восстановить средства. Храните их офлайн и в тайне."
|
||||||
|
reveal: "Показать фразу"
|
||||||
|
hide: "Скрыть"
|
||||||
|
password: "Пароль кошелька"
|
||||||
|
wrong_password: "Неверный пароль."
|
||||||
|
delete: "Удалить кошелёк"
|
||||||
|
delete_desc: "Безвозвратно удалить этот кошелёк и все его личности с этого устройства. Сначала сделайте резервную копию. Без seed-фразы средства не восстановить."
|
||||||
|
delete_confirm: "Нажмите ещё раз для удаления"
|
||||||
|
manage_node: "Управление подключением к узлу"
|
||||||
|
repair_confirm: "Да, восстановить сейчас"
|
||||||
|
repair_confirm_note: "Восстановление повторно сканирует цепочку и может занять несколько минут."
|
||||||
|
restore_confirm_note: "Это стирает локальные данные и восстанавливает их из seed-фразы — может занять несколько минут."
|
||||||
|
nostr_key: "Ключ Nostr"
|
||||||
|
nostr_key_desc: "Ваш nsec, секретный ключ вашей личности Nostr. Скопируйте его или покажите QR-код, чтобы войти в приложения Nostr, такие как magick.market. Любой, у кого он есть, управляет вашей личностью, держите его в секрете."
|
||||||
|
reveal_nsec: "Показать ключ"
|
||||||
|
copy_nsec: "Копировать nsec"
|
||||||
|
show_qr: "Показать QR"
|
||||||
|
hide_qr: "Скрыть QR"
|
||||||
|
nostr_section: "Расширенные настройки nostr"
|
||||||
|
backup_caption: "Содержит ваш кошелёк и все личности."
|
||||||
|
danger_zone: "Опасная зона"
|
||||||
|
delete_warning: "Это удалит вашу фразу восстановления и все личности на этом устройстве. Всё, что не сохранено в резервной копии, будет потеряно навсегда."
|
||||||
|
download_backup: "Скачать резервную копию"
|
||||||
|
delete_final: "Удалить навсегда"
|
||||||
|
privacy:
|
||||||
|
title: "Сетевая приватность"
|
||||||
|
intro: "Goblin отправляет по сети лишь немногое, и всё это запечатано сквозным шифрованием, поэтому реле не могут это прочитать или связать с вами."
|
||||||
|
payments: "Платежи"
|
||||||
|
payments_blurb: "Каждое nostr-сообщение, несущее slatepack."
|
||||||
|
usernames: "Имена пользователей"
|
||||||
|
usernames_blurb: "Поиск имён NIP-05 к серверам имён и обратно."
|
||||||
|
price_avatars: "Цена"
|
||||||
|
price_avatars_blurb: "Текущий курс рядом с суммами."
|
||||||
|
grin_node: "Узел Grin"
|
||||||
|
grin_node_blurb: "Синхронизирует блоки и транслирует ваши транзакции в сеть. Публичные данные цепочки, одинаковые для всех, никогда не связаны с вашей личностью."
|
||||||
|
how_privacy: "Как работает ваша приватность"
|
||||||
|
how_privacy_blurb: "Платежи и сообщения запечатываются аутентифицированным шифрованием и доставляются как приватные сообщения, поэтому реле не может их прочитать или связать платёж с вами."
|
||||||
|
always_direct: "Всегда напрямую"
|
||||||
|
tor_off: "Tor выключен"
|
||||||
|
tor_on: "Tor включён"
|
||||||
|
tor_switch: "Маршрутизировать через Tor"
|
||||||
|
tor_switch_sub: "Скройте свой IP от реле."
|
||||||
|
vpn_note: "Если Tor выключен, реле могут видеть ваш IP-адрес. Рекомендуем использовать хотя бы VPN."
|
||||||
|
pairing:
|
||||||
|
title: "Привязка"
|
||||||
|
intro: "К чему привязаны отображаемые баланс и суммы."
|
||||||
|
pair_with: "Привязать к"
|
||||||
|
rates_note: "Курсы загружаются через Tor только при включённой привязке — выключено означает, что запрос курса не покидает устройство."
|
||||||
|
relays:
|
||||||
|
title: "Реле"
|
||||||
|
intro: "Сообщения о платежах дублируются на каждое реле ниже; для получения достаточно одного доступного реле."
|
||||||
|
your_relays: "Ваши реле"
|
||||||
|
add_relay: "Добавить реле"
|
||||||
|
add_relay_btn: "Добавить реле"
|
||||||
|
save_reconnect: "Сохранить и переподключить"
|
||||||
|
none: "нет"
|
||||||
|
count: "%{n} реле"
|
||||||
|
node:
|
||||||
|
title: "Узел"
|
||||||
|
connection: "Соединение"
|
||||||
|
integrated: "Встроенный узел"
|
||||||
|
applies_after: "Применяется после блокировки и повторной разблокировки кошелька."
|
||||||
|
add_external: "Добавить внешний узел"
|
||||||
|
api_secret_hint: "API-секрет (необязательно)"
|
||||||
|
add_node: "Добавить узел"
|
||||||
|
integrated_host: "встроенный узел"
|
||||||
|
summary_syncing: "%{conn} · синхронизация"
|
||||||
|
summary_block: "Блок %{height} · %{conn}"
|
||||||
|
nips:
|
||||||
|
title: "nostr и NIPs"
|
||||||
|
intro1: "Goblin говорит на nostr — открытом протоколе подписанных сообщений, передаваемых через простые реле-серверы. Ваш кошелёк несёт собственную nostr-личность: отдельный случайный ключ, намеренно независимый от ваших средств и seed. Каждый платёж идёт как сквозно зашифрованное личное сообщение между личностями, со slatepack внутри."
|
||||||
|
intro2: "goblin.st — это служба имён Goblin: занятие имени публикует там сопоставление имя → ключ (NIP-05), чтобы вам платили на you вместо длинного npub. Имя публично; содержимое платежей — никогда. NIPs — это строительные блоки протокола; коснитесь одного, чтобы прочитать спецификацию."
|
||||||
|
n05_title: "Имена"
|
||||||
|
n05_blurb: "Сопоставляет username@goblin.st с вашим ключом, чтобы имена работали как адреса."
|
||||||
|
n17_title: "Личные сообщения"
|
||||||
|
n17_blurb: "Зашифрованный конверт DM, в котором идёт каждый платёж."
|
||||||
|
n44_title: "Шифрование"
|
||||||
|
n44_blurb: "Аутентифицированный шифр, используемый внутри этих сообщений."
|
||||||
|
n49_title: "Шифрование ключа"
|
||||||
|
n49_blurb: "Как секретный ключ хранится в покое, защищённый вашим паролем."
|
||||||
|
n59_title: "Подарочная обёртка"
|
||||||
|
n59_blurb: "Оборачивает сообщения, чтобы реле не видели, кто с кем общается."
|
||||||
|
n98_title: "HTTP-авторизация"
|
||||||
|
n98_blurb: "Подписывает запрос регистрации имени на goblin.st."
|
||||||
|
batch:
|
||||||
|
title: "Подтвердить %{n} счетов"
|
||||||
|
blurb: "Выставить %{n} запросов на оплату по %{amount} каждый для %{name} — всего %{total}. Каждый запрос получает свой новый платёжный адрес."
|
||||||
|
approve: "Подтвердить"
|
||||||
|
login:
|
||||||
|
title: "Вход"
|
||||||
|
headline: "Войти на %{domain}?"
|
||||||
|
identity: "Войти как"
|
||||||
|
explain: "Будет подписан одноразовый запрос на вход. Ваш ключ не передаётся, и сайт не сможет действовать от вашего имени."
|
||||||
|
pass_prompt: "Введите пароль кошелька, чтобы войти."
|
||||||
|
confirm: "Войти"
|
||||||
|
sent: "Выполнен вход на %{domain}"
|
||||||
|
failed: "Не удалось связаться с %{domain}. Вход не был выполнен."
|
||||||
|
authorize:
|
||||||
|
title: "Авторизовать"
|
||||||
|
headline: "Авторизовать %{domain}?"
|
||||||
|
kind_post: "Публичная запись"
|
||||||
|
kind_repost: "Репост"
|
||||||
|
kind_reaction: "Реакция"
|
||||||
|
kind_article: "Статья (лонгрид)"
|
||||||
|
kind_unknown: "Тип %{n} (не распознан)"
|
||||||
|
unknown_caution: "Goblin не распознаёт этот тип события. Подтверждайте, только если доверяете %{domain}."
|
||||||
|
identity: "Подписать как"
|
||||||
|
explain: "Подтверждение подпишет это одно событие и передаст его %{domain}. Goblin ничего не публикует, ключ не передаётся, и сайт не получает ни сессии, ни будущих полномочий."
|
||||||
|
pass_prompt: "Введите пароль кошелька, чтобы авторизовать."
|
||||||
|
confirm: "Авторизовать"
|
||||||
|
sent: "%{domain} авторизован"
|
||||||
|
failed: "Не удалось связаться с %{domain}. Ничего не было передано."
|
||||||
|
truncated: "обрезано, ещё %{n} символов"
|
||||||
|
show_full: "Показать полностью"
|
||||||
|
show_less: "Свернуть"
|
||||||
|
replying_to: "в ответ на %{id}"
|
||||||
|
mentions: "упоминает %{id}"
|
||||||
|
repost_of: "репост %{id}"
|
||||||
|
by_author: "автор %{id}"
|
||||||
|
reacts_to: "реакция на %{id}"
|
||||||
|
article_title: "Заголовок: %{title}"
|
||||||
|
trust:
|
||||||
|
title: "Доверие"
|
||||||
|
headline: "Доверять %{domain}?"
|
||||||
|
identity: "Вход как"
|
||||||
|
lead: "%{domain} может действовать за вас в этой сессии, не спрашивая каждый раз. Всё, что тратит деньги, всегда спрашивает."
|
||||||
|
grant_intro: "Может подписывать без запроса:"
|
||||||
|
cat_social: "Публикации и реакции"
|
||||||
|
cat_dm: "Личные сообщения"
|
||||||
|
cat_market: "Объявления"
|
||||||
|
cat_identity: "Профиль и списки"
|
||||||
|
cat_delete: "Удаления"
|
||||||
|
cat_http: "Загрузки и HTTP-аутентификация"
|
||||||
|
cat_unknown: "Другой тип события (вид %{n})"
|
||||||
|
login_excluded: "Вход в систему сайту никогда не предоставляется."
|
||||||
|
money_line: "Деньги всегда требуют ваш пароль. Ничего не тратится и не продаётся молча."
|
||||||
|
duration: "Действует только в этой сессии. Завершить можно в любой момент в Настройках, Доверенные сайты."
|
||||||
|
pass_prompt: "Введите пароль кошелька, чтобы доверять этому сайту."
|
||||||
|
confirm_hold: "Удерживайте, чтобы доверять на эту сессию"
|
||||||
|
sent: "Доверие %{domain} на эту сессию"
|
||||||
|
failed: "Не удалось связаться с %{domain}. Сессия не начата."
|
||||||
|
announce_failed: "Доверие оформлено, но %{domain} ещё не подтвердил сессию."
|
||||||
|
notice_volume: "Доверенный сайт подписывает очень много."
|
||||||
|
notice_decrypt: "Доверенный сайт читает ваши сообщения."
|
||||||
|
money:
|
||||||
|
title: "Подтвердить"
|
||||||
|
headline: "Подтвердить в %{domain}?"
|
||||||
|
identity: "Подпись как %{id}"
|
||||||
|
explain: "Это перемещает или фиксирует ценность, поэтому запрос выводится всегда. Одобрение подписывает это одно действие."
|
||||||
|
pass_prompt: "Введите пароль кошелька для подтверждения."
|
||||||
|
confirm_hold: "Удерживайте для подтверждения"
|
||||||
|
encrypt_desc: "Зашифрованное сообщение заказа, фиксирующее платёж."
|
||||||
|
trusted_sites:
|
||||||
|
title: "Доверенные сайты"
|
||||||
|
intro: "Сайты, которым вы доверились в этой сессии, могут подписывать действия с низким риском без запроса. Деньги всегда запрашивают подтверждение. Завершите любую сессию здесь."
|
||||||
|
empty: "Нет доверенных сайтов. Доверьте сайту при входе, чтобы дать ему сессию подписи."
|
||||||
|
none: "ничего"
|
||||||
|
can_sign: "Может подписывать без запроса: %{cats}"
|
||||||
|
time_left: "Завершится через %{mins} мин"
|
||||||
|
paused: "Приостановлено, подписывает много"
|
||||||
|
resume: "Возобновить"
|
||||||
|
end: "Завершить сессию"
|
||||||
|
identities:
|
||||||
|
title: "Личности"
|
||||||
|
switch_hint: "Сменить личность"
|
||||||
|
blurb: "Ваши личности — это имена, под которыми вам платят. Все они пополняют этот один кошелёк. Добавьте новую, переключайтесь между ними или удалите ненужную."
|
||||||
|
privacy_note: "Эти личности используют один кошелёк и один баланс — отдельные двери, а не отдельные средства. Для действительно раздельных денег используйте отдельный кошелёк."
|
||||||
|
held: "Имеющиеся личности"
|
||||||
|
add: "Добавить личность"
|
||||||
|
add_title: "Добавить личность"
|
||||||
|
nsec_hint: "Вставьте nsec"
|
||||||
|
generate_note: "Для этого кошелька будет создан новый анонимный ключ."
|
||||||
|
generate: "Создать"
|
||||||
|
import: "Импортировать"
|
||||||
|
choose_backup: "Выбрать файл .backup"
|
||||||
|
backup_selected: "Файл резервной копии выбран"
|
||||||
|
delete_short: "Удалить личность"
|
||||||
|
delete_title: "Удалить %{name}?"
|
||||||
|
delete_blurb: "Это навсегда удалит личность из этого кошелька. Уже полученные деньги останутся на балансе."
|
||||||
|
delete_backup_note: "Сначала сделайте резервную копию — без её nsec или файла .backup её невозможно восстановить."
|
||||||
|
delete_confirm: "Удалить"
|
||||||
|
manage_title: "Управление личностью"
|
||||||
|
tag_note: "Дайте ей приватное имя. Его видите только вы — оно никогда не передаётся."
|
||||||
|
tag_hint: "Приватное имя"
|
||||||
|
tag_save: "Сохранить"
|
||||||
|
pass_prompt: "Пароль кошелька шифрует и разблокирует каждую личность. Введите его, чтобы продолжить."
|
||||||
|
working: "Обработка…"
|
||||||
|
onboarding:
|
||||||
|
privacy:
|
||||||
|
kicker: "СЕТЕВАЯ ПРИВАТНОСТЬ"
|
||||||
|
continue: "Продолжить"
|
||||||
|
intro:
|
||||||
|
private_money_head: "Приватные деньги"
|
||||||
|
private_money_body: "Goblin — кошелёк для grin: цифровая наличность без сумм и адресов в её цепочке."
|
||||||
|
send_like_message_head: "Отправляйте как сообщение"
|
||||||
|
send_like_message_body: "Заплатите на username или npub, и платёж придёт как сквозно зашифрованное сообщение через nostr и Tor — никто посередине не увидит сумму или участников."
|
||||||
|
yours_alone_head: "Только ваше"
|
||||||
|
yours_alone_body: "Ключи, имена и история живут на этом устройстве. На базе кошелька GRIM."
|
||||||
|
get_started: "Начать"
|
||||||
|
footnote: "Займёт около минуты. Всё можно изменить позже."
|
||||||
|
node:
|
||||||
|
kicker: "ШАГ 1 ИЗ 3 · СЕТЬ"
|
||||||
|
title: "Как Goblin должен\nследить за цепочкой?"
|
||||||
|
own_title: "Запустить свой узел"
|
||||||
|
own_badge: "Приватно"
|
||||||
|
own_body: "Никому не доверяет — ваш кошелёк проверяет цепочку сам. Синхронизируется в фоне, пока вы завершаете настройку."
|
||||||
|
connect_title: "Подключиться к узлу"
|
||||||
|
connect_badge: "Мгновенно"
|
||||||
|
connect_body: "Без ожидания синхронизации. Выбранный узел может видеть запросы вашего кошелька."
|
||||||
|
changeable: "Меняется в любой момент в Настройки → Узел."
|
||||||
|
continue: "Продолжить"
|
||||||
|
url_invalid: "URL узла должен начинаться с http:// или https://"
|
||||||
|
wallet:
|
||||||
|
kicker: "ШАГ 2 ИЗ 3 · КОШЕЛЁК"
|
||||||
|
title: "Настройте кошелёк"
|
||||||
|
create_new: "Создать новый"
|
||||||
|
restore_from_seed: "Восстановить из seed"
|
||||||
|
import_identity: "Импорт личности"
|
||||||
|
import_identity_hint: "После этого шага вы добавите свой существующий ключ (файл .backup или nsec)."
|
||||||
|
name_hint: "Имя кошелька"
|
||||||
|
password_hint: "Пароль"
|
||||||
|
repeat_password_hint: "Повторите пароль"
|
||||||
|
restore_hint: "Подготовьте seed-слова — вы введёте их далее."
|
||||||
|
create_hint: "Далее вы получите 24 seed-слова для записи. Они — это деньги: кто владеет ими, владеет вашими средствами."
|
||||||
|
continue: "Продолжить"
|
||||||
|
passwords_no_match: "Пароли не совпадают"
|
||||||
|
words:
|
||||||
|
kicker: "ШАГ 2 ИЗ 3 · КОШЕЛЁК"
|
||||||
|
title_restore: "Введите seed-слова"
|
||||||
|
title_create: "Запишите эти слова"
|
||||||
|
write_down_hint: "На бумаге, по порядку. Любой с этими словами может забрать ваши средства; без них потеря устройства означает потерю средств."
|
||||||
|
paste: "Вставить"
|
||||||
|
scan_qr: "Сканировать QR"
|
||||||
|
copy_clipboard: "Копировать в буфер (избегайте этого)"
|
||||||
|
restore_wallet: "Восстановить кошелёк"
|
||||||
|
wrote_them_down: "Я записал их"
|
||||||
|
fill_every_word: "Заполните каждое слово — коснитесь слова для редактирования или вставьте фразу."
|
||||||
|
unlock_backup: "Разблокировать копию"
|
||||||
|
backup_unlocked: "Копия разблокирована. Ваши личности восстановятся при открытии кошелька."
|
||||||
|
confirm:
|
||||||
|
kicker: "ШАГ 2 ИЗ 3 · КОШЕЛЁК"
|
||||||
|
title: "Теперь подтвердите"
|
||||||
|
enter_hint: "Введите слова, которые только что записали. Коснитесь слова, чтобы ввести его."
|
||||||
|
paste: "Вставить"
|
||||||
|
create_wallet: "Создать кошелёк"
|
||||||
|
keep_going: "Продолжайте — каждое слово, по порядку."
|
||||||
|
identity:
|
||||||
|
kicker: "ШАГ 3 ИЗ 3 · ЛИЧНОСТЬ"
|
||||||
|
title: "Ваша платёжная личность"
|
||||||
|
key_being_made: "ключ создаётся…"
|
||||||
|
connected_tor: "подключено через Tor"
|
||||||
|
connecting_tor: "подключение через Tor…"
|
||||||
|
connected_direct: "подключено (напрямую)"
|
||||||
|
connecting_direct: "подключение (напрямую)…"
|
||||||
|
fresh_key_blurb: "Платёжный ключ, не связанный с seed-фразой — меняйте его в любой момент, не трогая средства."
|
||||||
|
restoring: "Восстановление ваших личностей…"
|
||||||
|
clean_slate_blurb: "Хотите начать с чистого листа? Подставьте совершенно новый ключ в любой момент — новый вы не связан со старым. Тот же кошелёк, новое лицо."
|
||||||
|
pick_username: "Выберите имя — необязательно"
|
||||||
|
username_blurb: "Друзья платят на ваше имя, а не на длинный ключ. Необязательно — можно занять в любой момент."
|
||||||
|
username_field_hint: "вашеимя"
|
||||||
|
working: "Обработка…"
|
||||||
|
claim_username: "Занять имя"
|
||||||
|
available_when_connected: "Доступно после подключения Tor — или пропустите и займите позже."
|
||||||
|
youre: "Вы %{name}"
|
||||||
|
claimed_title: "%{name} теперь ваше"
|
||||||
|
claimed_blurb: "Друзья теперь могут платить вам по имени. Всё готово — откройте кошелёк."
|
||||||
|
open_wallet: "Открыть кошелёк"
|
||||||
|
skip_for_now: "Пропустить пока"
|
||||||
|
import_existing: "Уже есть личность Goblin? Импортируйте её"
|
||||||
|
import_title: "Импорт личности"
|
||||||
|
import_blurb: "Вставьте свой nsec или выберите файл .backup, чтобы сохранить существующий ключ и имя вместо нового."
|
||||||
|
errors:
|
||||||
|
cant_open: "Не удалось открыть кошелёк: %{err}"
|
||||||
|
cant_create: "Не удалось создать кошелёк: %{err}"
|
||||||
|
send:
|
||||||
|
scan_to_request: "Сканируйте для запроса"
|
||||||
|
scan_to_pay: "Сканируйте для оплаты"
|
||||||
|
tab_scan: "Сканировать"
|
||||||
|
tab_my_code: "Мой код"
|
||||||
|
request_from: "Запросить у"
|
||||||
|
send_to: "Отправить"
|
||||||
|
search_hint: "handle, npub или имя"
|
||||||
|
suggested: "%{icon} Рекомендуемые"
|
||||||
|
no_contacts: "Пока нет контактов. Найдите кого-то по их handle."
|
||||||
|
no_profile: "нет профиля"
|
||||||
|
tag_contact: "контакт"
|
||||||
|
tag_on_nostr: "в nostr"
|
||||||
|
searching_nostr: "Поиск в nostr…"
|
||||||
|
unverified_title: "Заплатить непроверенному ключу?"
|
||||||
|
unverified_body: "Для этого ключа не опубликован nostr-профиль — он может быть совсем новым, анонимным или с опечаткой. Дважды проверьте перед отправкой."
|
||||||
|
keep_looking: "Продолжить поиск"
|
||||||
|
pay_anyway: "Всё равно оплатить"
|
||||||
|
scan_not_recipient: "Этот QR — не получатель goblin; ожидался npub или handle"
|
||||||
|
scan_prompt: "Наведите на код goblin, чтобы активировать"
|
||||||
|
scan_to_pay_me: "Сканируйте, чтобы заплатить мне"
|
||||||
|
share_btn: "%{icon} Поделиться"
|
||||||
|
share_message: "Заплатите мне в Goblin — %{handle}\n%{link}\nnpub: %{npub}"
|
||||||
|
none_found: "Никого не найдено по %{label}"
|
||||||
|
enter_recipient: "Введите handle, npub или имя"
|
||||||
|
amount_title: "Сумма"
|
||||||
|
to_name: "Кому %{name}"
|
||||||
|
not_enough: "Недостаточно grin"
|
||||||
|
max: "Макс"
|
||||||
|
note_label: "Заметка"
|
||||||
|
note_hint: "Добавить заметку…"
|
||||||
|
add_note: "Добавить заметку"
|
||||||
|
edit_note: "Изменить заметку"
|
||||||
|
note_cancel: "Отмена"
|
||||||
|
note_save: "Сохранить"
|
||||||
|
review_btn: "Проверить"
|
||||||
|
confirm_request: "Подтвердить запрос"
|
||||||
|
review_title: "Проверка"
|
||||||
|
requesting_from: "Запрос у %{name}"
|
||||||
|
youre_sending: "Вы отправляете %{name}"
|
||||||
|
row_from: "От"
|
||||||
|
row_to: "Кому"
|
||||||
|
row_note: "Заметка"
|
||||||
|
row_proof: "Подтверждение платежа"
|
||||||
|
row_proof_val: "Включено"
|
||||||
|
row_proof_shared: "Подтверждение получит"
|
||||||
|
row_they_pay: "Они платят"
|
||||||
|
row_they_pay_val: "Только если они одобрят"
|
||||||
|
row_delivery: "Доставка"
|
||||||
|
row_delivery_val: "Зашифровано NIP-44, через Tor"
|
||||||
|
row_network_fee: "Сетевая комиссия"
|
||||||
|
row_network_fee_val: "Списывается с вашего баланса"
|
||||||
|
row_privacy: "Приватность"
|
||||||
|
row_privacy_val: "Mimblewimble + Tor"
|
||||||
|
send_request_btn: "Отправить запрос"
|
||||||
|
request_approve_hint: "Они получат запрос на одобрение"
|
||||||
|
hold_to_send: "Удерживайте для отправки"
|
||||||
|
lower_amount: "Вернуться и уменьшить сумму"
|
||||||
|
hold_confirm_hint: "Нажмите и удерживайте для подтверждения"
|
||||||
|
requesting: "Запрос…"
|
||||||
|
sending: "Отправка…"
|
||||||
|
they: "Они"
|
||||||
|
request_blocked: "%{who} не принимает запросы. Попросите их отправить вам grin вместо этого."
|
||||||
|
failed_request_title: "Не удалось запросить"
|
||||||
|
failed_send_title: "Не удалось отправить"
|
||||||
|
failed_request_body: "Не удалось доставить запрос. Попросите их отправить вам grin вместо этого."
|
||||||
|
failed_send_body: "Платёж не доставлен. Ваш grin в безопасности — попробуйте снова."
|
||||||
|
try_again_btn: "Попробовать снова"
|
||||||
|
close_btn: "Закрыть"
|
||||||
|
success:
|
||||||
|
requested: "Запрошено"
|
||||||
|
sent: "Отправлено"
|
||||||
|
from: "от"
|
||||||
|
to: "кому"
|
||||||
|
subtitle: "%{dir} %{who} · только что"
|
||||||
|
done_btn: "Готово"
|
||||||
|
receipt_btn: "Квитанция"
|
||||||
@@ -44,7 +44,7 @@ wallets:
|
|||||||
await_fin_amount: Tamamlanma bekleniyor
|
await_fin_amount: Tamamlanma bekleniyor
|
||||||
locked_amount: Kilitli
|
locked_amount: Kilitli
|
||||||
txs_empty: 'Koinleri al/gonder icin ekranin altinda bulunan %{receive} / %{send} sekmeleri, cuzdan ayarlar icin %{settings} sekmesini kullanin.'
|
txs_empty: 'Koinleri al/gonder icin ekranin altinda bulunan %{receive} / %{send} sekmeleri, cuzdan ayarlar icin %{settings} sekmesini kullanin.'
|
||||||
title: Cuzdanlar
|
title: Goblin
|
||||||
create_desc: Yeni cuzdan olustur veya var olan bakiyeli cuzdani kurtarma kelimelerinizle canlandirin.
|
create_desc: Yeni cuzdan olustur veya var olan bakiyeli cuzdani kurtarma kelimelerinizle canlandirin.
|
||||||
add: Cuzdan ekle
|
add: Cuzdan ekle
|
||||||
name: 'Ad:'
|
name: 'Ad:'
|
||||||
@@ -95,6 +95,7 @@ wallets:
|
|||||||
tx_finalizing: Islem tamamlaniyor
|
tx_finalizing: Islem tamamlaniyor
|
||||||
tx_posting: Islem kaydetme
|
tx_posting: Islem kaydetme
|
||||||
tx_confirmed: Onaylandi
|
tx_confirmed: Onaylandi
|
||||||
|
tx_stale: Çok uzun süredir bekliyor
|
||||||
txs: Islemler
|
txs: Islemler
|
||||||
tx: Islem
|
tx: Islem
|
||||||
messages: Mesajlar
|
messages: Mesajlar
|
||||||
@@ -355,4 +356,616 @@ keyboard:
|
|||||||
m: m
|
m: m
|
||||||
m1: ','
|
m1: ','
|
||||||
m2: .
|
m2: .
|
||||||
m3: /
|
m3: /
|
||||||
|
goblin:
|
||||||
|
home:
|
||||||
|
anonymous: "Anonim"
|
||||||
|
connected_tor: "Tor üzerinden bağlı"
|
||||||
|
tor_ready: "Tor hazır · relaylar…"
|
||||||
|
connecting_tor: "Tor'a bağlanılıyor…"
|
||||||
|
connected_direct: "Bağlandı (doğrudan)"
|
||||||
|
connecting_direct: "Bağlanıyor (doğrudan)…"
|
||||||
|
cant_reach_node: "Düğüme ulaşılamıyor"
|
||||||
|
node_synced: "Düğüm eşitlendi"
|
||||||
|
syncing: "Eşitleniyor…"
|
||||||
|
balance_updating: "Bakiye güncelleniyor…"
|
||||||
|
balance_stale: "Düğüme ulaşılamıyor · son bilinen bakiye"
|
||||||
|
fiat_unavailable: "Kur mevcut değil"
|
||||||
|
listening: "Ödemeler bekleniyor"
|
||||||
|
block: "Blok %{height}"
|
||||||
|
waiting_for_chain: "Zincir bekleniyor…"
|
||||||
|
nav_wallet: "Cüzdan"
|
||||||
|
nav_pay: "Öde"
|
||||||
|
nav_activity: "Etkinlik"
|
||||||
|
nav_receive: "Al"
|
||||||
|
nav_settings: "Ayarlar"
|
||||||
|
back_again: "Cüzdan değiştirmek için tekrar geri bas"
|
||||||
|
activity: "Etkinlik"
|
||||||
|
news: "Haberler"
|
||||||
|
empty_title: "Henüz etkinlik yok"
|
||||||
|
empty_sub: "Başlamak için grin gönder ya da al."
|
||||||
|
recent: "Son işlemler"
|
||||||
|
scan_to_pay: "Ödemek için tara"
|
||||||
|
type_amount: "Bir tutar gir"
|
||||||
|
request: "İste"
|
||||||
|
pay: "Öde"
|
||||||
|
enter_amount: "Ödemek ya da istemek için bir tutar gir"
|
||||||
|
activity:
|
||||||
|
canceled: "iptal edildi"
|
||||||
|
pending: "beklemede"
|
||||||
|
earlier: "Daha önce"
|
||||||
|
today: "Bugün"
|
||||||
|
yesterday: "Dün"
|
||||||
|
title: "Etkinlik"
|
||||||
|
requests: "İstekler"
|
||||||
|
empty_title: "Henüz etkinlik yok"
|
||||||
|
empty_sub: "Ödemelerin burada görünecek."
|
||||||
|
pending_header: "Beklemede"
|
||||||
|
receipt:
|
||||||
|
title: "Makbuz"
|
||||||
|
not_found: "İşlem bulunamadı"
|
||||||
|
for_note: "%{note} için"
|
||||||
|
details: "İşlem ayrıntıları"
|
||||||
|
canceled: "İptal edildi"
|
||||||
|
expired: "Süresi doldu"
|
||||||
|
funds_returned: "Para iade edildi"
|
||||||
|
complete: "Tamamlandı"
|
||||||
|
payment_received: "Ödeme alındı"
|
||||||
|
payment_sent: "Ödeme başarıyla gönderildi"
|
||||||
|
pending: "Beklemede"
|
||||||
|
confs: "%{c}/%{r} onay"
|
||||||
|
waiting_to_confirm: "Onay bekleniyor"
|
||||||
|
paying: "Ödeniyor…"
|
||||||
|
you: "Sen"
|
||||||
|
to: "Alıcı"
|
||||||
|
from: "Gönderen"
|
||||||
|
nostr: "nostr"
|
||||||
|
identity: "Kimlik"
|
||||||
|
fee_none: "Yok"
|
||||||
|
network_fee: "Ağ ücreti"
|
||||||
|
privacy: "Gizlilik"
|
||||||
|
privacy_value: "Mimblewimble + Tor"
|
||||||
|
transaction: "İşlem"
|
||||||
|
cancel_request: "İsteği iptal et"
|
||||||
|
cancel_send: "Ödemeyi iptal et"
|
||||||
|
stale_note: "Bu ödeme bir süredir bekliyor."
|
||||||
|
cancel_send_confirm: "İptal için tekrar dokun — hâlâ alabilir"
|
||||||
|
cancel_send_done: "Ödeme iptal edildi — paranız yeniden kullanılabilir"
|
||||||
|
cancel_send_too_late: "Bu ödeme zaten geçti ve iptal edilemez"
|
||||||
|
waiting_to_receive: "%{name} alana kadar bekleniyor…"
|
||||||
|
request:
|
||||||
|
title: "%{name} istiyor"
|
||||||
|
approve: "Onayla"
|
||||||
|
decline: "Reddet"
|
||||||
|
review_title: "İsteği incele"
|
||||||
|
hold_to_accept: "Kabul için basılı tut"
|
||||||
|
hold_accept_hint: "Bu isteği ödemek için basılı tutun"
|
||||||
|
receive:
|
||||||
|
title: "Al"
|
||||||
|
requesting: "%{amt}%{tsu} isteniyor — ödeme almak için paylaş"
|
||||||
|
clear_request: "İsteği temizle"
|
||||||
|
share_handle: "Ödeme almak için kullanıcı adını paylaş"
|
||||||
|
share_npub: "Ödeme almak için npub'ını paylaş"
|
||||||
|
copied: "Kopyalandı"
|
||||||
|
copy_nostr_id: "nostr kimliğini kopyala"
|
||||||
|
copy_address: "Adresi kopyala"
|
||||||
|
copy: "Kopyala"
|
||||||
|
share_message: "Goblin'de bana öde (goblin.st) — %{npub}"
|
||||||
|
privacy_note: "Kullanıcı adın herkese açıktır. Ödeme içeriği ağ üzerinde şifreli kalır."
|
||||||
|
privacy_note_npub: "npub'ın herkese açıktır. Ödeme içeriği ağ üzerinde şifreli kalır."
|
||||||
|
profile:
|
||||||
|
title: "Profil"
|
||||||
|
activity: "Etkinlik"
|
||||||
|
no_activity: "Henüz onlarla etkinlik yok."
|
||||||
|
unblock: "Engeli kaldır"
|
||||||
|
block: "Engelle"
|
||||||
|
blocked_blurb: "Engellendi — ödemeleri ve istekleri reddediliyor."
|
||||||
|
block_blurb: "Engellemek, gelen ödeme ve isteklerini düşürür."
|
||||||
|
settings:
|
||||||
|
title: "Ayarlar"
|
||||||
|
connected_nostr: "nostr'a bağlı"
|
||||||
|
connecting_relays: "Relaylara bağlanılıyor…"
|
||||||
|
identity: "Kimlik"
|
||||||
|
copy_npub: "npub kopyala (genel)"
|
||||||
|
backup_note: "Cihaz mı değiştiriyorsun? İKİSİNİ de yedekle: seed ifaden (bakiye) ve kimlik .backup dosyan (ad + anahtar)."
|
||||||
|
wallet: "Cüzdan"
|
||||||
|
display_unit: "Görüntüleme birimi"
|
||||||
|
relays: "Relaylar"
|
||||||
|
nostr_relays: "Nostr Relayları"
|
||||||
|
node: "Düğüm"
|
||||||
|
min_conf: "Asgari onay sayısı"
|
||||||
|
integrated_node: "Tümleşik düğüm ayarları"
|
||||||
|
node_advanced: "Gelişmiş"
|
||||||
|
slatepacks: "Slatepackler"
|
||||||
|
slatepacks_value: "Manuel işlem"
|
||||||
|
trusted_sites: "Güvenilen Siteler"
|
||||||
|
lock_wallet: "Cüzdanı kilitle"
|
||||||
|
switch_wallet: "Cüzdan değiştir"
|
||||||
|
advanced: "Gelişmiş"
|
||||||
|
privacy: "Gizlilik"
|
||||||
|
tor_routing: "Tor yönlendirme"
|
||||||
|
messages_lookups: "Mesajlar ve aramalar"
|
||||||
|
tor_on: "Açık"
|
||||||
|
tor_off: "Kapalı"
|
||||||
|
auto_accept: "Otomatik kabul"
|
||||||
|
pairing: "Fiyat para birimi"
|
||||||
|
accept_anyone: "Herkes"
|
||||||
|
accept_contacts: "Yalnızca kişiler"
|
||||||
|
accept_ask: "Her zaman sor"
|
||||||
|
requests: "İstekler"
|
||||||
|
incoming_requests: "Gelen istekler"
|
||||||
|
incoming_requests_sub: "Başkalarının senden para istemesine izin ver"
|
||||||
|
hide_amounts: "Tutarları gizle"
|
||||||
|
hide_amounts_sub: "Bildirimlerde alınan tutarları gizle"
|
||||||
|
language: "Dil"
|
||||||
|
update_available: "Güncelleme mevcut"
|
||||||
|
appearance: "Görünüm"
|
||||||
|
theme: "Tema"
|
||||||
|
theme_light: "Açık"
|
||||||
|
theme_dark: "Koyu"
|
||||||
|
theme_yellow: "Sarı"
|
||||||
|
archive: "Arşiv"
|
||||||
|
export_archive: "Arşivi dışa aktar"
|
||||||
|
export_archive_caption: "Kişilerinizi, ödeme geçmişinizi ve istekleri JSON olarak panoya kopyalar."
|
||||||
|
wipe_history: "Ödeme geçmişini sil"
|
||||||
|
wipe_history_confirm: "Silmek için tekrar dokun — geri alınamaz"
|
||||||
|
about: "Hakkında"
|
||||||
|
goblin: "Goblin"
|
||||||
|
build: "Sürüm %{build}"
|
||||||
|
network: "Ağ"
|
||||||
|
network_value: "MW + Tor + nostr"
|
||||||
|
third_party: "Üçüncü taraf"
|
||||||
|
grim: "GRIM (üst kaynak cüzdan)"
|
||||||
|
grin_node: "Grin düğümü"
|
||||||
|
sp_intro: "Gelişmiş — GRIM'in yaptığı gibi ham slatepackleri elle değiş tokuş et. Bunu yalnızca bir username üzerinden ödeme yapamadığında ya da alamadığında kullan."
|
||||||
|
sp_receive_group: "Al ya da tamamla"
|
||||||
|
sp_receive_blurb: "Birinin sana verdiği bir slatepack'i yapıştır. Goblin ödemeyi alır, faturayı öder ya da tamamlayıp zincire gönderir."
|
||||||
|
sp_process: "Slatepack işle"
|
||||||
|
sp_paste_first: "Önce bir slatepack yapıştır."
|
||||||
|
sp_reply_ready: "Yanıt hazır — gönderene geri yolla."
|
||||||
|
sp_finalizing: "Tamamlanıp zincire gönderiliyor…"
|
||||||
|
sp_create_group: "Ödeme oluştur"
|
||||||
|
sp_create_blurb: "Birine vermek için bir slatepack oluştur. Onlar alır, yanıtı geri gönderir, sen de yukarıda tamamlarsın."
|
||||||
|
sp_amount_hint: "Grin cinsinden tutar"
|
||||||
|
sp_addr_hint: "Alıcı adresi (isteğe bağlı)"
|
||||||
|
sp_create: "Slatepack oluştur"
|
||||||
|
sp_ready: "Slatepack hazır — alıcıya ver."
|
||||||
|
sp_amount_gt_zero: "Sıfırdan büyük bir tutar gir."
|
||||||
|
sp_to_send: "Gönderilecek slatepack"
|
||||||
|
sp_copy: "Slatepack kopyala"
|
||||||
|
cancel: "İptal"
|
||||||
|
continue: "Devam"
|
||||||
|
final_confirmation: "Son onay"
|
||||||
|
type_reset: "RESET yaz"
|
||||||
|
wallet_password: "Cüzdan parolası"
|
||||||
|
done: "Bitti"
|
||||||
|
close: "Kapat"
|
||||||
|
import_nsec_hint: "nsec1… veya yapıştırılan yedek"
|
||||||
|
backup_password_hint: "Yedek parolası (yalnızca başka yerde dışa aktarıldıysa)"
|
||||||
|
import_btn: "İçe aktar"
|
||||||
|
importing: "İçe aktarılıyor…"
|
||||||
|
import_failed: "İçe aktarma başarısız"
|
||||||
|
name_authority: "İsim otoritesi"
|
||||||
|
name_authority_title: "İsim otoritesini değiştir"
|
||||||
|
name_authority_blurb: "Adları kaydeden ve doğrulayan sunucu. Başka bir örneğe yönlendirerek oradaki adları kullan ve öde."
|
||||||
|
name_authority_invalid: "Tam bir URL gir (https://…)."
|
||||||
|
reset: "Sıfırla"
|
||||||
|
save: "Kaydet"
|
||||||
|
backup_file: "Dosyaya yedekle"
|
||||||
|
choose_backup_file: "Bir .backup dosyası seç"
|
||||||
|
backup_read_failed: "Dosya okunamadı."
|
||||||
|
backup_saved: "Yedek kaydedildi"
|
||||||
|
backup_saved_sub: ".backup dosyasını güvende tut. Hem ona hem de parolana sahip olan tüm cüzdanını geri yükleyebilir."
|
||||||
|
backup_file_title: "Cüzdanı yedekle"
|
||||||
|
backup_file_blurb: "Cüzdan tohumun ve tüm kimliklerinle tek bir şifreli .backup dosyası oluşturur. Mühürlemek için cüzdan parolanı gir."
|
||||||
|
backup_write_failed: "Dosya kaydedilemedi."
|
||||||
|
create_backup: "Yedek oluştur"
|
||||||
|
registered: "%{name} kaydedildi"
|
||||||
|
released_msg: "Bırakıldı — ad artık alınabilir"
|
||||||
|
release_confirm: "%{name} bırakılsın mı?"
|
||||||
|
release_blurb: "Serbest kalır kalmaz herkes alabilir — döndüğün bir sonraki anahtar dahil. 10 dakika boyunca başka bir kullanıcı adı kaydedemezsin."
|
||||||
|
releasing: "Bırakılıyor…"
|
||||||
|
keep_it: "Vazgeç"
|
||||||
|
release_it: "Bırak"
|
||||||
|
username: "Kullanıcı adı"
|
||||||
|
username_note: "Adınız olarak gösterilir. goblin.st'de herkese açık. Ödemeler şifreli kalır."
|
||||||
|
release_username: "Kullanıcı adını bırak"
|
||||||
|
pick_username: "Bir kullanıcı adı seç — isteğe bağlı"
|
||||||
|
working: "Çalışıyor…"
|
||||||
|
claim: "Al"
|
||||||
|
err_just_taken: "O kullanıcı adı az önce alındı"
|
||||||
|
err_cooldown: "Yakın zamanda bir kullanıcı adı bıraktın — 10 dakika içinde yenisini kaydedebilirsin."
|
||||||
|
err_unreachable: "goblin.st'ye ulaşılamadı — bağlantı sorunu. Tekrar dene."
|
||||||
|
err_release: "Bırakılamadı: %{err}"
|
||||||
|
avail_available: "Müsait!"
|
||||||
|
avail_taken: "Alınmış"
|
||||||
|
avail_reserved: "Ayrılmış"
|
||||||
|
avail_invalid: "Adlar 3–20 karakter: a–z, 0–9, _ ya da -"
|
||||||
|
avail_quarantined: "Müsait değil"
|
||||||
|
avail_unknown: "Kontrol edilemedi — bağlantı sorunu. Tekrar dene."
|
||||||
|
username_none: "Ayarlı değil"
|
||||||
|
advanced_privacy: "Gelişmiş Gizlilik"
|
||||||
|
tap_reveal: "Göstermek için dokun"
|
||||||
|
notif_someone: "Biri"
|
||||||
|
notif_private_received: "Ödeme aldın. Görmek için Goblin'i aç."
|
||||||
|
notif_private_requested: "Yeni ödeme isteği. Görmek için Goblin'i aç."
|
||||||
|
username:
|
||||||
|
title: "Kullanıcı adı"
|
||||||
|
authority: "İsim otoritesi"
|
||||||
|
authority_blurb: "Varsayılan bırak ya da orada barındırılan isimleri kullanmak için başka bir sunucuya yönlendir."
|
||||||
|
learn_more: "Daha fazla bilgi"
|
||||||
|
custom: "Özel otorite"
|
||||||
|
advprivacy:
|
||||||
|
intro: "Bildirimlerinin neyi açığa çıkardığını denetle ve bu cihazda bakiyeni ve geçmişini gizle."
|
||||||
|
notifications: "Bildirimler"
|
||||||
|
hide_names: "İsimleri gizle"
|
||||||
|
hide_names_sub: "Kimin ödediğini bildirimlerde gösterme"
|
||||||
|
hide_details: "Tüm ayrıntıları gizle"
|
||||||
|
hide_details_sub: "Yalnızca özel bir uyarı göster, isim ya da tutar olmadan"
|
||||||
|
anon: "Anonim mod"
|
||||||
|
anon_toggle: "Bakiyeyi ve etkinliği bulanıklaştır"
|
||||||
|
anon_sub: "Bakiyen ve etkinliğin, göstermek için dokunana kadar noktalar olarak görünür"
|
||||||
|
advanced:
|
||||||
|
title: "Gelişmiş"
|
||||||
|
intro: "GRIM'den düşük seviyeli cüzdan araçları. Bunlara normalde ihtiyacın olmaz."
|
||||||
|
own_node_desc: "Herkese açık bir düğüme güvenmek yerine bu cihazda tam bir Grin düğümü senkronize edin."
|
||||||
|
own_node_active: "Kendi düğümünüz çalışıyor"
|
||||||
|
repair: "Cüzdanı onar"
|
||||||
|
repair_desc: "Zinciri yeniden tara ve eksik çıktıları geri yükle. Bu biraz zaman alabilir."
|
||||||
|
repair_unavailable: "Önce senkronize bir düğüm bağlantısı gerekir."
|
||||||
|
repairing: "Onarılıyor… %{pct}%"
|
||||||
|
restore: "Cüzdanı geri yükle"
|
||||||
|
restore_desc: "Yerel verileri sil ve tohumundan yeniden oluştur. Onarım işe yaramadıysa bunu kullan — sonra cüzdanı yeniden açarsın."
|
||||||
|
restore_confirm: "Geri yüklemek için tekrar dokun"
|
||||||
|
show_phrase: "Kurtarma ifadesi"
|
||||||
|
phrase_desc: "24 grin tohum kelimen — fonları kurtarmanın tek yolu. Onları çevrimdışı ve gizli tut."
|
||||||
|
reveal: "İfadeyi göster"
|
||||||
|
hide: "Gizle"
|
||||||
|
password: "Cüzdan parolası"
|
||||||
|
wrong_password: "Yanlış parola."
|
||||||
|
delete: "Cüzdanı sil"
|
||||||
|
delete_desc: "Bu cüzdanı ve tüm kimliklerini bu cihazdan kalıcı olarak kaldır. Önce yedek al. Tohumun olmadan fonlar kurtarılamaz."
|
||||||
|
delete_confirm: "Silmek için tekrar dokun"
|
||||||
|
manage_node: "Düğüm bağlantısını yönet"
|
||||||
|
repair_confirm: "Evet, şimdi onar"
|
||||||
|
repair_confirm_note: "Onarım zinciri yeniden tarar ve birkaç dakika sürebilir."
|
||||||
|
restore_confirm_note: "Bu, yerel verileri siler ve seed'inizden yeniden oluşturur — birkaç dakika sürebilir."
|
||||||
|
nostr_key: "Nostr anahtarı"
|
||||||
|
nostr_key_desc: "nsec'iniz, Nostr kimliğinizin gizli anahtarı. magick.market gibi Nostr uygulamalarında oturum açmak için kopyalayın veya QR kodunu gösterin. Ona sahip olan herkes kimliğinizi kontrol eder, gizli tutun."
|
||||||
|
reveal_nsec: "Anahtarı göster"
|
||||||
|
copy_nsec: "nsec'i kopyala"
|
||||||
|
show_qr: "QR göster"
|
||||||
|
hide_qr: "QR gizle"
|
||||||
|
nostr_section: "Gelişmiş nostr ayarları"
|
||||||
|
backup_caption: "Cüzdanını ve tüm kimliklerini içerir."
|
||||||
|
danger_zone: "Tehlikeli bölge"
|
||||||
|
delete_warning: "Bu, kurtarma kelimelerinizi ve bu cihazdaki tüm kimlikleri siler. Yedeklemediğiniz her şey kalıcı olarak kaybolur."
|
||||||
|
download_backup: "Yedeği indir"
|
||||||
|
delete_final: "Kalıcı olarak sil"
|
||||||
|
privacy:
|
||||||
|
title: "Ağ gizliliği"
|
||||||
|
intro: "Goblin ağ üzerinden yalnızca birkaç şey gönderir; her biri uçtan uca şifrelemeyle mühürlenir, böylece relaylar bunları okuyamaz veya seninle ilişkilendiremez."
|
||||||
|
payments: "Ödemeler"
|
||||||
|
payments_blurb: "Slatepack taşıyan her nostr mesajı."
|
||||||
|
usernames: "Kullanıcı adları"
|
||||||
|
usernames_blurb: "İsim otoriteleriyle yapılan NIP-05 ad aramaları."
|
||||||
|
price_avatars: "Fiyat"
|
||||||
|
price_avatars_blurb: "Tutarların yanında gösterilen anlık kur."
|
||||||
|
grin_node: "Grin düğümü"
|
||||||
|
grin_node_blurb: "Blokları eşitler ve işlemlerini ağa yayar. Herkes için aynı olan genel zincir verisidir, kimliğinle asla ilişkilendirilmez."
|
||||||
|
how_privacy: "Gizliliğin nasıl çalıştığı"
|
||||||
|
how_privacy_blurb: "Ödemeler ve mesajlar kimlik doğrulamalı şifrelemeyle mühürlenip özel mesajlar olarak iletilir; böylece bir relay bunları okuyamaz veya bir ödemeyi sana bağlayamaz."
|
||||||
|
always_direct: "Her zaman doğrudan"
|
||||||
|
tor_off: "Tor kapalı"
|
||||||
|
tor_on: "Tor açık"
|
||||||
|
tor_switch: "Tor üzerinden yönlendir"
|
||||||
|
tor_switch_sub: "IP adresini relaylardan gizle."
|
||||||
|
vpn_note: "Tor kapalıyken relaylar IP adresini görebilir. En azından bir VPN kullanmanı öneririz."
|
||||||
|
pairing:
|
||||||
|
title: "Eşleştirme"
|
||||||
|
intro: "Bakiyenin ve tutarların neye göre gösterildiği."
|
||||||
|
pair_with: "Eşleştir"
|
||||||
|
rates_note: "Kurlar yalnızca bir eşleştirme açıkken Tor üzerinden alınır — kapalıysa cihazından hiçbir kur isteği çıkmaz."
|
||||||
|
relays:
|
||||||
|
title: "Relaylar"
|
||||||
|
intro: "Ödeme mesajları aşağıdaki her relay'e yansıtılır; almak için ulaşılabilir tek bir relay yeterlidir."
|
||||||
|
your_relays: "Relaylarn"
|
||||||
|
add_relay: "Relay ekle"
|
||||||
|
add_relay_btn: "Relay ekle"
|
||||||
|
save_reconnect: "Kaydet ve yeniden bağlan"
|
||||||
|
none: "yok"
|
||||||
|
count: "%{n} relay"
|
||||||
|
node:
|
||||||
|
title: "Düğüm"
|
||||||
|
connection: "Bağlantı"
|
||||||
|
integrated: "Tümleşik düğüm"
|
||||||
|
applies_after: "Cüzdan kilitlenip yeniden açıldıktan sonra geçerli olur."
|
||||||
|
add_external: "Harici düğüm ekle"
|
||||||
|
api_secret_hint: "API gizli anahtarı (isteğe bağlı)"
|
||||||
|
add_node: "Düğüm ekle"
|
||||||
|
integrated_host: "tümleşik düğüm"
|
||||||
|
summary_syncing: "%{conn} · eşitleniyor"
|
||||||
|
summary_block: "Blok %{height} · %{conn}"
|
||||||
|
nips:
|
||||||
|
title: "nostr ve NIPler"
|
||||||
|
intro1: "Goblin nostr konuşur — basit relay sunucuları üzerinden geçen imzalı mesajların açık bir protokolü. Cüzdanın kendi nostr kimliğini taşır: bağımsız rastgele bir anahtar, paran ve tohumundan kasıtlı olarak ayrı tutulur. Her ödeme, slatepack içinde olacak şekilde, kimlikler arasında uçtan uca şifreli bir doğrudan mesaj olarak gider."
|
||||||
|
intro2: "goblin.st, Goblin'in ad servisidir: bir kullanıcı adı almak orada bir ad → anahtar eşlemesi yayımlar (NIP-05), böylece insanlar uzun bir npub yerine you'ya ödeme yapabilir. Kullanıcı adı herkese açıktır; ödeme içeriği asla değil. NIPler protokolün yapı taşlarıdır — özelliği okumak için birine dokun."
|
||||||
|
n05_title: "Adlar"
|
||||||
|
n05_blurb: "username@goblin.st'yi anahtarına eşler, böylece kullanıcı adları adres gibi çalışır."
|
||||||
|
n17_title: "Özel mesajlar"
|
||||||
|
n17_blurb: "Her ödemenin içinde gittiği şifreli DM zarfı."
|
||||||
|
n44_title: "Şifreleme"
|
||||||
|
n44_blurb: "Bu mesajların içinde kullanılan kimlik doğrulamalı şifreleme."
|
||||||
|
n49_title: "Anahtar şifreleme"
|
||||||
|
n49_blurb: "Gizli anahtarın parolanla kilitli olarak nasıl depolandığı."
|
||||||
|
n59_title: "Hediye paketi"
|
||||||
|
n59_blurb: "Mesajları sarar, böylece relaylar kimin kiminle konuştuğunu göremez."
|
||||||
|
n98_title: "HTTP kimlik doğrulama"
|
||||||
|
n98_blurb: "goblin.st'ye gönderilen kullanıcı adı kayıt isteğini imzalar."
|
||||||
|
batch:
|
||||||
|
title: "%{n} faturayı onayla"
|
||||||
|
blurb: "%{name} için her biri %{amount} olan %{n} ödeme isteği düzenle — toplam %{total}. Her istek kendi yeni ödeme adresini alır."
|
||||||
|
approve: "Onayla"
|
||||||
|
login:
|
||||||
|
title: "Giriş yap"
|
||||||
|
headline: "%{domain} sitesine giriş yapılsın mı?"
|
||||||
|
identity: "Giriş yapılacak kimlik"
|
||||||
|
explain: "Bu, tek seferlik bir giriş isteğini imzalar. Anahtarın paylaşılmaz ve site senin adına işlem yapamaz."
|
||||||
|
pass_prompt: "Giriş yapmak için cüzdan parolanı gir."
|
||||||
|
confirm: "Giriş yap"
|
||||||
|
sent: "%{domain} girişi yapıldı"
|
||||||
|
failed: "%{domain} adresine ulaşılamadı. Giriş iletilemedi."
|
||||||
|
authorize:
|
||||||
|
title: "Yetkilendir"
|
||||||
|
headline: "%{domain} yetkilendirilsin mi?"
|
||||||
|
kind_post: "Herkese açık gönderi"
|
||||||
|
kind_repost: "Yeniden paylaşım"
|
||||||
|
kind_reaction: "Tepki"
|
||||||
|
kind_article: "Makale (uzun biçim)"
|
||||||
|
kind_unknown: "Tür %{n} (tanınmıyor)"
|
||||||
|
unknown_caution: "Goblin bu olay türünü tanımıyor. Yalnızca %{domain} adresine güveniyorsan onayla."
|
||||||
|
identity: "İmzalayan kimlik"
|
||||||
|
explain: "Onaylamak bu tek olayı imzalar ve %{domain} adresine verir. Goblin hiçbir şey yayımlamaz, hiçbir anahtar paylaşılmaz ve site oturum ya da gelecekteki bir yetki elde etmez."
|
||||||
|
pass_prompt: "Yetkilendirmek için cüzdan parolanı gir."
|
||||||
|
confirm: "Yetkilendir"
|
||||||
|
sent: "%{domain} yetkilendirildi"
|
||||||
|
failed: "%{domain} adresine ulaşılamadı. Hiçbir şey iletilmedi."
|
||||||
|
truncated: "kısaltıldı, %{n} karakter daha"
|
||||||
|
show_full: "Tamamını göster"
|
||||||
|
show_less: "Daha az göster"
|
||||||
|
replying_to: "%{id} yanıtlanıyor"
|
||||||
|
mentions: "%{id} anılıyor"
|
||||||
|
repost_of: "%{id} yeniden paylaşılıyor"
|
||||||
|
by_author: "yazan %{id}"
|
||||||
|
reacts_to: "%{id} tepki veriliyor"
|
||||||
|
article_title: "Başlık: %{title}"
|
||||||
|
trust:
|
||||||
|
title: "Güven"
|
||||||
|
headline: "%{domain} sitesine güvenilsin mi?"
|
||||||
|
identity: "Giriş yapan"
|
||||||
|
lead: "%{domain} bu oturumda her seferinde sormadan sizin adınıza işlem yapabilir. Para harcayan her şey her zaman sorar."
|
||||||
|
grant_intro: "Sormadan imzalayabilir:"
|
||||||
|
cat_social: "Gönderiler ve tepkiler"
|
||||||
|
cat_dm: "Doğrudan mesajlar"
|
||||||
|
cat_market: "İlanlar"
|
||||||
|
cat_identity: "Profil ve listeler"
|
||||||
|
cat_delete: "Silmeler"
|
||||||
|
cat_http: "Yüklemeler ve HTTP kimlik doğrulama"
|
||||||
|
cat_unknown: "Diğer olay türü (tür %{n})"
|
||||||
|
login_excluded: "Bir siteye asla giriş izni verilmez."
|
||||||
|
money_line: "Para her zaman parolanızı ister. Hiçbir şey sessizce harcanmaz veya satılmaz."
|
||||||
|
duration: "Yalnızca bu oturum için geçerlidir. İstediğiniz zaman Ayarlar, Güvenilen Siteler'den sonlandırabilirsiniz."
|
||||||
|
pass_prompt: "Bu siteye güvenmek için cüzdan parolanızı girin."
|
||||||
|
confirm_hold: "Bu oturum için güvenmek üzere basılı tutun"
|
||||||
|
sent: "%{domain} bu oturum için güveniliyor"
|
||||||
|
failed: "%{domain} sitesine ulaşılamadı. Oturum başlatılmadı."
|
||||||
|
announce_failed: "Güvenildi, ancak %{domain} oturumu henüz onaylamadı."
|
||||||
|
notice_volume: "Güvenilen bir site çok fazla imzalıyor."
|
||||||
|
notice_decrypt: "Güvenilen bir site mesajlarınızı okuyor."
|
||||||
|
money:
|
||||||
|
title: "Onayla"
|
||||||
|
headline: "%{domain} içinde onaylansın mı?"
|
||||||
|
identity: "%{id} olarak imzalanıyor"
|
||||||
|
explain: "Bu, değeri taşır veya bağlar, bu yüzden her zaman sorar. Onaylamak bu tek eylemi imzalar."
|
||||||
|
pass_prompt: "Onaylamak için cüzdan parolanızı girin."
|
||||||
|
confirm_hold: "Onaylamak için basılı tutun"
|
||||||
|
encrypt_desc: "Bir ödemeyi bağlayan şifreli sipariş mesajı."
|
||||||
|
trusted_sites:
|
||||||
|
title: "Güvenilen Siteler"
|
||||||
|
intro: "Bu oturumda güvendiğiniz siteler, düşük riskli eylemleri sormadan imzalayabilir. Para her zaman sorar. Herhangi bir oturumu buradan sonlandırın."
|
||||||
|
empty: "Güvenilen site yok. Ona bir imza oturumu vermek için giriş yaparken bir siteye güvenin."
|
||||||
|
none: "hiçbir şey"
|
||||||
|
can_sign: "Sessizce imzalayabilir: %{cats}"
|
||||||
|
time_left: "%{mins} dk içinde sona erer"
|
||||||
|
paused: "Duraklatıldı, çok imzalıyor"
|
||||||
|
resume: "Devam et"
|
||||||
|
end: "Oturumu sonlandır"
|
||||||
|
identities:
|
||||||
|
title: "Kimlikler"
|
||||||
|
switch_hint: "Kimlik değiştir"
|
||||||
|
blurb: "Kimliklerin, ödeme aldığın adlardır. Hepsi bu tek cüzdana öder. Yeni bir tane ekle, aralarında geçiş yap veya ihtiyacın olmayanı kaldır."
|
||||||
|
privacy_note: "Bu kimlikler tek cüzdanı ve tek bakiyeyi paylaşır — ayrı kapılar, ayrı paralar değil. Gerçekten ayrı para için ayrı bir cüzdan kullan."
|
||||||
|
held: "Mevcut kimlikler"
|
||||||
|
add: "Kimlik ekle"
|
||||||
|
add_title: "Bir kimlik ekle"
|
||||||
|
nsec_hint: "Bir nsec yapıştır"
|
||||||
|
generate_note: "Bu cüzdan için yeni, anonim bir anahtar oluşturulacak."
|
||||||
|
generate: "Oluştur"
|
||||||
|
import: "İçe aktar"
|
||||||
|
choose_backup: "Bir .backup dosyası seç"
|
||||||
|
backup_selected: "Yedek dosyası seçildi"
|
||||||
|
delete_short: "Kimliği sil"
|
||||||
|
delete_title: "%{name} silinsin mi?"
|
||||||
|
delete_blurb: "Bu, kimliği bu cüzdandan kalıcı olarak kaldırır. Zaten alınan para bakiyende kalır."
|
||||||
|
delete_backup_note: "Önce yedeğini al — nsec’i veya .backup dosyası olmadan geri getirilemez."
|
||||||
|
delete_confirm: "Sil"
|
||||||
|
manage_title: "Kimliği yönet"
|
||||||
|
tag_note: "Ona özel bir ad ver. Sadece sen görürsün — asla paylaşılmaz."
|
||||||
|
tag_hint: "Özel ad"
|
||||||
|
tag_save: "Kaydet"
|
||||||
|
pass_prompt: "Cüzdan parolan her kimliği şifreler ve açar. Devam etmek için gir."
|
||||||
|
working: "Çalışıyor…"
|
||||||
|
onboarding:
|
||||||
|
privacy:
|
||||||
|
kicker: "AĞ GİZLİLİĞİ"
|
||||||
|
continue: "Devam"
|
||||||
|
intro:
|
||||||
|
private_money_head: "Özel para"
|
||||||
|
private_money_body: "Goblin, grin için bir cüzdan — zincirinde tutar ya da adres bulunmayan dijital nakit."
|
||||||
|
send_like_message_head: "Mesaj gibi gönder"
|
||||||
|
send_like_message_body: "Bir username ya da npub'a öde, nostr ve Tor üzerinden uçtan uca şifreli bir mesaj olarak ulaşır — aradaki hiç kimse tutarı ya da kimlerin dahil olduğunu göremez."
|
||||||
|
yours_alone_head: "Yalnızca senin"
|
||||||
|
yours_alone_body: "Anahtarlar, adlar ve geçmiş bu cihazda yaşar. GRIM cüzdanı üzerine kuruludur."
|
||||||
|
get_started: "Başla"
|
||||||
|
footnote: "Yaklaşık bir dakika sürer. Her şeyi sonradan değiştirebilirsin."
|
||||||
|
node:
|
||||||
|
kicker: "ADIM 1 / 3 · AĞ"
|
||||||
|
title: "Goblin zinciri nasıl\nizlesin?"
|
||||||
|
own_title: "Kendi düğümümü çalıştır"
|
||||||
|
own_badge: "Özel"
|
||||||
|
own_body: "Kimseye güvenmez — cüzdanın zinciri kendisi kontrol eder. Sen kurulumu bitirirken arka planda eşitlenir."
|
||||||
|
connect_title: "Bir düğüme bağlan"
|
||||||
|
connect_badge: "Anında"
|
||||||
|
connect_body: "Eşitleme beklemesi yok. Seçtiğin düğüm cüzdanının sorgularını görebilir."
|
||||||
|
changeable: "Ayarlar → Düğüm'den istediğin zaman değiştirilebilir."
|
||||||
|
continue: "Devam"
|
||||||
|
url_invalid: "Düğüm URL'si http:// ya da https:// ile başlamalı"
|
||||||
|
wallet:
|
||||||
|
kicker: "ADIM 2 / 3 · CÜZDAN"
|
||||||
|
title: "Cüzdanını kur"
|
||||||
|
create_new: "Yeni oluştur"
|
||||||
|
restore_from_seed: "Tohumdan geri yükle"
|
||||||
|
import_identity: "Kimliği içe aktar"
|
||||||
|
import_identity_hint: "Bu adımdan sonra mevcut anahtarını (bir .backup dosyası veya nsec) getireceksin."
|
||||||
|
name_hint: "Cüzdan adı"
|
||||||
|
password_hint: "Parola"
|
||||||
|
repeat_password_hint: "Parolayı tekrarla"
|
||||||
|
restore_hint: "Tohum kelimelerini hazır tut — onları sonra gireceksin."
|
||||||
|
create_hint: "Sırada yazman için 24 tohum kelimesi var. Onlar paradır — onları elinde tutan paranı elinde tutar."
|
||||||
|
continue: "Devam"
|
||||||
|
passwords_no_match: "Parolalar eşleşmiyor"
|
||||||
|
words:
|
||||||
|
kicker: "ADIM 2 / 3 · CÜZDAN"
|
||||||
|
title_restore: "Tohum kelimelerini gir"
|
||||||
|
title_create: "Bu kelimeleri yaz"
|
||||||
|
write_down_hint: "Kâğıda, sırayla. Bu kelimelere sahip olan paranı alabilir; onlar olmadan kaybolan bir cihaz kaybolan para demektir."
|
||||||
|
paste: "Yapıştır"
|
||||||
|
scan_qr: "QR tara"
|
||||||
|
copy_clipboard: "Panoya kopyala (bundan kaçın)"
|
||||||
|
restore_wallet: "Cüzdanı geri yükle"
|
||||||
|
wrote_them_down: "Onları yazdım"
|
||||||
|
fill_every_word: "Her kelimeyi doldur — düzenlemek için bir kelimeye dokun ya da ifadeyi yapıştır."
|
||||||
|
unlock_backup: "Yedeği aç"
|
||||||
|
backup_unlocked: "Yedek açıldı. Kimliklerin cüzdan açıldığında geri yüklenir."
|
||||||
|
confirm:
|
||||||
|
kicker: "ADIM 2 / 3 · CÜZDAN"
|
||||||
|
title: "Şimdi kanıtla"
|
||||||
|
enter_hint: "Az önce yazdığın kelimeleri gir. Yazmak için bir kelimeye dokun."
|
||||||
|
paste: "Yapıştır"
|
||||||
|
create_wallet: "Cüzdan oluştur"
|
||||||
|
keep_going: "Devam et — her kelime, sırayla."
|
||||||
|
identity:
|
||||||
|
kicker: "ADIM 3 / 3 · KİMLİK"
|
||||||
|
title: "Ödeme kimliğin"
|
||||||
|
key_being_made: "anahtar oluşturuluyor…"
|
||||||
|
connected_tor: "Tor üzerinden bağlı"
|
||||||
|
connecting_tor: "Tor üzerinden bağlanılıyor…"
|
||||||
|
connected_direct: "bağlandı (doğrudan)"
|
||||||
|
connecting_direct: "bağlanıyor (doğrudan)…"
|
||||||
|
fresh_key_blurb: "Seed'inin parçası olmayan bir ödeme anahtarı — paranı hiç ellemeden istediğin an döndür."
|
||||||
|
restoring: "Kimliklerin geri yükleniyor…"
|
||||||
|
clean_slate_blurb: "Temiz bir sayfa mı istiyorsun? İstediğin zaman yepyeni bir anahtar tak — yeni sen eskisine bağlı değil. Aynı cüzdan, yeni yüz."
|
||||||
|
pick_username: "Bir kullanıcı adı seç — isteğe bağlı"
|
||||||
|
username_blurb: "Arkadaşların uzun bir anahtar yerine adına öder. İsteğe bağlı — istediğin an al."
|
||||||
|
username_field_hint: "adınız"
|
||||||
|
working: "Çalışıyor…"
|
||||||
|
claim_username: "Kullanıcı adı al"
|
||||||
|
available_when_connected: "Tor bağlandığında müsait — ya da atla ve sonra al."
|
||||||
|
youre: "Sen %{name}'sin"
|
||||||
|
claimed_title: "%{name} artık senin"
|
||||||
|
claimed_blurb: "Arkadaşların artık sana adınla ödeme yapabilir. Her şey hazır — cüzdanını aç."
|
||||||
|
open_wallet: "Cüzdanımı aç"
|
||||||
|
skip_for_now: "Şimdilik atla"
|
||||||
|
import_existing: "Zaten bir Goblin kimliğin var mı? İçe aktar"
|
||||||
|
import_title: "Kimliğini içe aktar"
|
||||||
|
import_blurb: "Bu yeni anahtar yerine mevcut anahtarını ve kullanıcı adını korumak için nsec'ini yapıştır veya bir .backup dosyası seç."
|
||||||
|
errors:
|
||||||
|
cant_open: "Cüzdan açılamadı: %{err}"
|
||||||
|
cant_create: "Cüzdan oluşturulamadı: %{err}"
|
||||||
|
send:
|
||||||
|
scan_to_request: "İstemek için tara"
|
||||||
|
scan_to_pay: "Ödemek için tara"
|
||||||
|
tab_scan: "Tara"
|
||||||
|
tab_my_code: "Kodum"
|
||||||
|
request_from: "Şundan iste"
|
||||||
|
send_to: "Şuna gönder"
|
||||||
|
search_hint: "handle, npub ya da ad"
|
||||||
|
suggested: "%{icon} Önerilen"
|
||||||
|
no_contacts: "Henüz kişi yok. Birini handle ile bul."
|
||||||
|
no_profile: "profil yok"
|
||||||
|
tag_contact: "kişi"
|
||||||
|
tag_on_nostr: "nostr'da"
|
||||||
|
searching_nostr: "nostr aranıyor…"
|
||||||
|
unverified_title: "Doğrulanmamış bir anahtara ödeme yapılsın mı?"
|
||||||
|
unverified_body: "Bu anahtar için yayımlanmış bir nostr profili yok — yepyeni, anonim ya da yanlış yazılmış olabilir. Göndermeden önce doğru olduğunu iki kez kontrol et."
|
||||||
|
keep_looking: "Aramaya devam et"
|
||||||
|
pay_anyway: "Yine de öde"
|
||||||
|
scan_not_recipient: "O QR bir goblin alıcısı değil — bir npub ya da handle bekleniyordu"
|
||||||
|
scan_prompt: "Etkinleştirmek için bir goblin kodunu görüntüye getir"
|
||||||
|
scan_to_pay_me: "Bana ödemek için tara"
|
||||||
|
share_btn: "%{icon} Paylaş"
|
||||||
|
share_message: "Goblin'de bana öde — %{handle}\n%{link}\nnpub: %{npub}"
|
||||||
|
none_found: "%{label} için kimse bulunamadı"
|
||||||
|
enter_recipient: "Bir handle, npub ya da ad gir"
|
||||||
|
amount_title: "Tutar"
|
||||||
|
to_name: "%{name} için"
|
||||||
|
not_enough: "Yeterli grin yok"
|
||||||
|
max: "Maks"
|
||||||
|
note_label: "Not"
|
||||||
|
note_hint: "Bir not ekle…"
|
||||||
|
add_note: "Not ekle"
|
||||||
|
edit_note: "Notu düzenle"
|
||||||
|
note_cancel: "İptal"
|
||||||
|
note_save: "Kaydet"
|
||||||
|
review_btn: "İncele"
|
||||||
|
confirm_request: "İsteği onayla"
|
||||||
|
review_title: "İncele"
|
||||||
|
requesting_from: "%{name} kişisinden isteniyor"
|
||||||
|
youre_sending: "%{name} kişisine gönderiyorsun"
|
||||||
|
row_from: "Gönderen"
|
||||||
|
row_to: "Alıcı"
|
||||||
|
row_note: "Not"
|
||||||
|
row_proof: "Ödeme kanıtı"
|
||||||
|
row_proof_val: "Dahil"
|
||||||
|
row_proof_shared: "Kanıt paylaşılır"
|
||||||
|
row_they_pay: "Onlar öder"
|
||||||
|
row_they_pay_val: "Yalnızca onaylarlarsa"
|
||||||
|
row_delivery: "Teslimat"
|
||||||
|
row_delivery_val: "NIP-44 şifreli, Tor üzerinden"
|
||||||
|
row_network_fee: "Ağ ücreti"
|
||||||
|
row_network_fee_val: "Bakiyenden düşülür"
|
||||||
|
row_privacy: "Gizlilik"
|
||||||
|
row_privacy_val: "Mimblewimble + Tor"
|
||||||
|
send_request_btn: "İstek gönder"
|
||||||
|
request_approve_hint: "Onaylamaları için bir istek alacaklar"
|
||||||
|
hold_to_send: "Göndermek için basılı tut"
|
||||||
|
lower_amount: "Geri dön ve tutarı düşür"
|
||||||
|
hold_confirm_hint: "Onaylamak için basılı tut"
|
||||||
|
requesting: "İsteniyor…"
|
||||||
|
sending: "Gönderiliyor…"
|
||||||
|
they: "Onlar"
|
||||||
|
request_blocked: "%{who} istek kabul etmiyor. Bunun yerine sana grin göndermesini iste."
|
||||||
|
failed_request_title: "İstenemedi"
|
||||||
|
failed_send_title: "Gönderilemedi"
|
||||||
|
failed_request_body: "İsteği teslim edemedik. Bunun yerine sana grin göndermesini iste."
|
||||||
|
failed_send_body: "Ödeme teslim edilemedi. Grin'in güvende — tekrar dene."
|
||||||
|
try_again_btn: "Tekrar dene"
|
||||||
|
close_btn: "Kapat"
|
||||||
|
success:
|
||||||
|
requested: "İstendi"
|
||||||
|
sent: "Gönderildi"
|
||||||
|
from: "şuradan"
|
||||||
|
to: "şuraya"
|
||||||
|
subtitle: "%{dir} %{who} · az önce"
|
||||||
|
done_btn: "Bitti"
|
||||||
|
receipt_btn: "Makbuz"
|
||||||
@@ -0,0 +1,971 @@
|
|||||||
|
lang_name: 繁體中文
|
||||||
|
copy: 複製
|
||||||
|
paste: 貼上
|
||||||
|
continue: 繼續
|
||||||
|
complete: 完成
|
||||||
|
error: 錯誤
|
||||||
|
retry: 重試
|
||||||
|
close: 關閉
|
||||||
|
change: 變更
|
||||||
|
show: 顯示
|
||||||
|
delete: 刪除
|
||||||
|
clear: 清除
|
||||||
|
create: 建立
|
||||||
|
id: 識別碼
|
||||||
|
kernel: 核心
|
||||||
|
settings: 設定
|
||||||
|
language: 語言
|
||||||
|
scan: 掃描
|
||||||
|
qr_code: QR 條碼
|
||||||
|
scan_qr: 掃描 QR 條碼
|
||||||
|
repeat: 重複
|
||||||
|
scan_result: 掃描結果
|
||||||
|
back: 返回
|
||||||
|
share: 分享
|
||||||
|
theme: '主題:'
|
||||||
|
dark: 深色
|
||||||
|
light: 淺色
|
||||||
|
file: 檔案
|
||||||
|
choose_file: 選擇檔案
|
||||||
|
choose_folder: 選擇資料夾
|
||||||
|
crash_report: 當機報告
|
||||||
|
crash_report_warning: 應用程式上次意外關閉,你可以將當機報告分享給開發者。
|
||||||
|
confirmation: 確認
|
||||||
|
enter_url: 輸入網址
|
||||||
|
max_short: 最大
|
||||||
|
files_location: 檔案位置
|
||||||
|
moving_files: 正在移動檔案
|
||||||
|
wrong_path_error: 指定的路徑錯誤
|
||||||
|
check_updates: 啟動時檢查更新
|
||||||
|
update_available: 有可用更新!
|
||||||
|
changelog: '更新日誌:'
|
||||||
|
wallets:
|
||||||
|
await_conf_amount: 等待確認
|
||||||
|
await_fin_amount: 等待最終確認
|
||||||
|
locked_amount: 已鎖定
|
||||||
|
txs_empty: '要手動或透過傳輸方式接收資金,請使用畫面底部的 %{message} 或 %{transport} 按鈕;要變更錢包設定,請按 %{settings} 按鈕。'
|
||||||
|
title: 錢包
|
||||||
|
create_desc: 建立新錢包或從已儲存的助記詞匯入現有錢包。
|
||||||
|
add: 新增錢包
|
||||||
|
name: '名稱:'
|
||||||
|
pass: '密碼:'
|
||||||
|
pass_empty: 輸入錢包密碼
|
||||||
|
current_pass: '目前密碼:'
|
||||||
|
new_pass: '新密碼:'
|
||||||
|
min_tx_conf_count: '交易的最少確認數:'
|
||||||
|
recover: 復原
|
||||||
|
recovery_phrase: 助記詞
|
||||||
|
words_count: '單字數量:'
|
||||||
|
enter_word: '輸入第 %{number} 個單字:'
|
||||||
|
not_valid_word: 輸入的單字無效
|
||||||
|
not_valid_phrase: 輸入的助記詞無效
|
||||||
|
create_phrase_desc: 請安全地寫下並儲存你的助記詞。
|
||||||
|
restore_phrase_desc: 輸入你儲存的助記詞中的單字。
|
||||||
|
setup_conn_desc: 選擇錢包連接網路的方式。
|
||||||
|
conn_method: 連線方式
|
||||||
|
ext_conn: '外部連線:'
|
||||||
|
add_node: 新增節點
|
||||||
|
node_url: '節點網址:'
|
||||||
|
node_secret: 'API 金鑰(選擇性):'
|
||||||
|
invalid_url: 輸入的網址無效
|
||||||
|
open: 開啟錢包
|
||||||
|
wrong_pass: 輸入的密碼錯誤
|
||||||
|
locked: 已鎖定
|
||||||
|
unlocked: 已解鎖
|
||||||
|
enable_node: '請啟用整合節點以使用錢包,或透過選擇畫面底部的 %{settings} 變更連線設定。'
|
||||||
|
node_loading: '錢包將在整合節點同步完成後載入,你可以透過選擇畫面底部的 %{settings} 變更連線方式。'
|
||||||
|
loading: 載入中
|
||||||
|
closing: 關閉中
|
||||||
|
checking: 檢查中
|
||||||
|
default_wallet: 預設錢包
|
||||||
|
new_account_desc: '輸入新帳戶名稱:'
|
||||||
|
wallet_loading: 正在載入錢包
|
||||||
|
wallet_closing: 正在關閉錢包
|
||||||
|
wallet_checking: 正在檢查錢包
|
||||||
|
tx_loading: 正在載入交易
|
||||||
|
default_account: 預設帳戶
|
||||||
|
accounts: 帳戶
|
||||||
|
tx_sent: 已傳送
|
||||||
|
tx_received: 已接收
|
||||||
|
tx_sending: 傳送中
|
||||||
|
tx_receiving: 接收中
|
||||||
|
tx_confirming: 等待確認
|
||||||
|
tx_canceled: 已取消
|
||||||
|
tx_cancelling: 取消中
|
||||||
|
tx_finalizing: 最終確認中
|
||||||
|
tx_posting: 提交中
|
||||||
|
tx_confirmed: 已確認
|
||||||
|
tx_stale: 等待時間過長
|
||||||
|
txs: 交易記錄
|
||||||
|
tx: 交易
|
||||||
|
messages: 訊息
|
||||||
|
transport: 傳輸
|
||||||
|
input_slatepack_desc: '輸入收到的 Slatepack 訊息以建立回應或完成請求:'
|
||||||
|
parse_slatepack_err: '讀取訊息時發生錯誤,請檢查輸入內容:'
|
||||||
|
pay_balance_error: '帳戶餘額不足以支付 %{amount} ツ 和網路手續費。'
|
||||||
|
parse_i1_slatepack_desc: '要支付 %{amount} ツ,請將此訊息傳送給接收方:'
|
||||||
|
parse_i2_slatepack_desc: '完成交易以接收 %{amount} ツ:'
|
||||||
|
parse_i3_slatepack_desc: '提交交易以完成 %{amount} ツ 的接收:'
|
||||||
|
parse_s1_slatepack_desc: '要接收 %{amount} ツ,請將此訊息傳送給發送方:'
|
||||||
|
parse_s2_slatepack_desc: '完成交易以傳送 %{amount} ツ:'
|
||||||
|
parse_s3_slatepack_desc: '提交交易以完成 %{amount} ツ 的傳送:'
|
||||||
|
resp_slatepack_err: '建立回應時發生錯誤,請檢查輸入資料或重試:'
|
||||||
|
resp_exists_err: 該交易已存在。
|
||||||
|
resp_canceled_err: 該交易已被取消。
|
||||||
|
create_request_desc: '建立傳送或接收資金的請求:'
|
||||||
|
send_request_desc: '你已建立傳送 %{amount} ツ 的請求。請將此訊息傳送給接收方:'
|
||||||
|
send_slatepack_err: 建立傳送資金請求時發生錯誤,請檢查輸入資料或重試。
|
||||||
|
invoice_desc: '你已建立接收 %{amount} ツ 的請求。請將此訊息傳送給發送方:'
|
||||||
|
invoice_slatepack_err: 開立發票時發生錯誤,請檢查輸入資料或重試。
|
||||||
|
finalize_slatepack_err: '最終確認時發生錯誤,請檢查輸入資料或重試:'
|
||||||
|
finalize: 最終確認
|
||||||
|
use_dandelion: 使用蒲公英(Dandelion)
|
||||||
|
enter_amount_send: '你有 %{amount} ツ。輸入傳送金額:'
|
||||||
|
enter_amount_receive: '輸入接收金額:'
|
||||||
|
recovery: 復原
|
||||||
|
repair_wallet: 修復錢包
|
||||||
|
repair_desc: 檢查錢包,必要時修復和還原遺失的輸出。此操作需要一些時間。
|
||||||
|
repair_unavailable: 你需要活躍的節點連線並完成錢包同步。
|
||||||
|
delete: 刪除錢包
|
||||||
|
delete_conf: 確定要刪除錢包嗎?
|
||||||
|
delete_desc: 請確保你已儲存助記詞,以便日後存取資金。
|
||||||
|
wallet_loading_err: '錢包同步時發生錯誤,你可以重試或透過選擇畫面底部的 %{settings} 變更連線設定。'
|
||||||
|
wallet: 錢包
|
||||||
|
send: 傳送
|
||||||
|
receive: 接收
|
||||||
|
settings: 錢包設定
|
||||||
|
tx_send_cancel_conf: '確定要取消傳送 %{amount} ツ 嗎?'
|
||||||
|
tx_receive_cancel_conf: '確定要取消接收 %{amount} ツ 嗎?'
|
||||||
|
rec_phrase_not_found: 找不到助記詞。
|
||||||
|
restore_wallet_desc: 如果一般修復無效,可透過刪除所有檔案來還原錢包,你需要重新開啟錢包。
|
||||||
|
fee_base_desc: '手續費(基礎值%{value}):'
|
||||||
|
payment_proof: 支付證明
|
||||||
|
payment_proof_desc: '輸入收到的支付證明以驗證交易:'
|
||||||
|
payment_proof_valid: '輸入的支付證明有效:'
|
||||||
|
payment_proof_error: '輸入的支付證明無效:'
|
||||||
|
tx_delete_confirmation: 確定要從歷史記錄中刪除該交易嗎?
|
||||||
|
transport:
|
||||||
|
desc: '使用傳輸方式同步收發訊息:'
|
||||||
|
tor_network: Tor 網路
|
||||||
|
connected: 已連線
|
||||||
|
connecting: 連線中
|
||||||
|
disconnecting: 中斷中
|
||||||
|
conn_error: 連線錯誤
|
||||||
|
disconnected: 已中斷
|
||||||
|
receiver_address: '接收方位址:'
|
||||||
|
incorrect_addr_err: '輸入的位址不正確:'
|
||||||
|
tor_send_error: 透過 Tor 傳送時發生錯誤,請確認接收方在線上,交易已取消。
|
||||||
|
tor_autorun_desc: 是否在開啟錢包時啟動 Tor 服務以同步接收交易。
|
||||||
|
tor_sending: 正在透過 Tor 傳送
|
||||||
|
tor_settings: Tor 設定
|
||||||
|
bridges: 橋接
|
||||||
|
bridges_desc: 如果一般連線無法使用,設定橋接以繞過 Tor 網路審查。
|
||||||
|
bin_file: '二進位檔案:'
|
||||||
|
conn_line: '連線線路:'
|
||||||
|
bridges_disabled: 橋接已停用
|
||||||
|
bridge_name: '橋接 %{b}'
|
||||||
|
network:
|
||||||
|
self: 網路
|
||||||
|
type: '網路類型:'
|
||||||
|
mainnet: 主網路
|
||||||
|
testnet: 測試網路
|
||||||
|
connections: 連線
|
||||||
|
node: 整合節點
|
||||||
|
metrics: 指標
|
||||||
|
mining: 挖礦
|
||||||
|
settings: 節點設定
|
||||||
|
enable_node: 啟用節點
|
||||||
|
autorun: 自動執行
|
||||||
|
disabled_server: '請啟用整合節點,或按畫面左上角的 %{dots} 新增其他連線方式。'
|
||||||
|
no_ips: 你的系統上沒有可用的 IP 位址,無法啟動伺服器,請檢查網路連線。
|
||||||
|
available: 可用
|
||||||
|
not_available: 不可用
|
||||||
|
availability_check: 可用性檢查
|
||||||
|
android_warning: Android 使用者請注意。要成功同步整合節點,你必須在手機系統設定中允許 Grim 應用程式傳送通知,並移除電池使用限制。這是應用程式在背景正常運作所必需的操作。
|
||||||
|
sync_status:
|
||||||
|
node_restarting: 節點正在重新啟動
|
||||||
|
node_down: 節點已停止
|
||||||
|
initial: 節點正在啟動
|
||||||
|
no_sync: 節點正在執行
|
||||||
|
awaiting_peers: 等待對等節點
|
||||||
|
header_sync: 正在下載區塊頭
|
||||||
|
header_sync_percent: '正在下載區塊頭:%{percent}%'
|
||||||
|
tx_hashset_pibd: 正在下載狀態(PIBD)
|
||||||
|
tx_hashset_pibd_percent: '正在下載狀態(PIBD):%{percent}%'
|
||||||
|
tx_hashset_download: 正在下載狀態
|
||||||
|
tx_hashset_download_percent: '正在下載狀態:%{percent}%'
|
||||||
|
tx_hashset_setup_history: '正在準備狀態(歷史):%{percent}%'
|
||||||
|
tx_hashset_setup_position: '正在準備狀態(位置):%{percent}%'
|
||||||
|
tx_hashset_setup: 正在準備狀態
|
||||||
|
tx_hashset_range_proofs_validation: '正在驗證狀態(範圍證明):%{percent}%'
|
||||||
|
tx_hashset_kernels_validation: '正在驗證狀態(核心):%{percent}%'
|
||||||
|
tx_hashset_save: 正在最終確認鏈狀態
|
||||||
|
body_sync: 正在下載區塊
|
||||||
|
body_sync_percent: '正在下載區塊:%{percent}%'
|
||||||
|
shutdown: 節點正在關閉
|
||||||
|
network_node:
|
||||||
|
header: 區塊頭
|
||||||
|
block: 區塊
|
||||||
|
hash: 雜湊
|
||||||
|
height: 高度
|
||||||
|
difficulty: 難度
|
||||||
|
time: 時間
|
||||||
|
main_pool: 主池
|
||||||
|
stem_pool: 莖池
|
||||||
|
data: 資料
|
||||||
|
size: 大小(GB)
|
||||||
|
peers: 對等節點
|
||||||
|
error_clean: 節點資料已損毀,需要重新同步。
|
||||||
|
resync: 重新同步
|
||||||
|
error_p2p_api: '%{p2p_api} 伺服器初始化時發生錯誤,請透過選擇畫面底部的 %{settings} 檢查 %{p2p_api} 設定。'
|
||||||
|
error_config: '設定初始化時發生錯誤,請透過選擇畫面底部的 %{settings} 檢查設定。'
|
||||||
|
error_unknown: '初始化時發生未知錯誤,請透過選擇畫面底部的 %{settings} 檢查整合節點設定,或重新同步。'
|
||||||
|
network_metrics:
|
||||||
|
loading: 指標將在同步完成後可用
|
||||||
|
emission: 發行量
|
||||||
|
inflation: 通膨率
|
||||||
|
supply: 供應量
|
||||||
|
block_time: 出塊時間
|
||||||
|
reward: 獎勵
|
||||||
|
difficulty_window: '難度視窗 %{size}'
|
||||||
|
network_mining:
|
||||||
|
loading: 挖礦將在同步完成後可用
|
||||||
|
info: '挖礦伺服器已啟用,你可以透過選擇畫面底部的 %{settings} 變更其設定。資料在裝置連線時更新。'
|
||||||
|
restart_server_required: 需要重新啟動伺服器以套用變更。
|
||||||
|
rewards_wallet: 獎勵接收錢包
|
||||||
|
server: Stratum 伺服器
|
||||||
|
address: 位址
|
||||||
|
miners: 礦工
|
||||||
|
devices: 裝置
|
||||||
|
blocks_found: 已找到區塊
|
||||||
|
hashrate: '算力(C%{bits})'
|
||||||
|
connected: 已連線
|
||||||
|
disconnected: 已中斷
|
||||||
|
network_settings:
|
||||||
|
change_value: 變更值
|
||||||
|
stratum_ip: 'Stratum IP 位址:'
|
||||||
|
stratum_port: 'Stratum 連接埠:'
|
||||||
|
port_unavailable: 指定的連接埠無法使用
|
||||||
|
restart_node_required: 需要重新啟動節點以套用變更。
|
||||||
|
choose_wallet: 選擇錢包
|
||||||
|
stratum_wallet_warning: 必須開啟錢包才能接收獎勵。
|
||||||
|
enable: 啟用
|
||||||
|
disable: 停用
|
||||||
|
restart: 重新啟動
|
||||||
|
server: 伺服器
|
||||||
|
api_ip: 'API IP 位址:'
|
||||||
|
api_port: 'API 連接埠:'
|
||||||
|
api_secret: 'Rest API 和 V2 擁有者 API 權杖:'
|
||||||
|
foreign_api_secret: '外部 API 權杖:'
|
||||||
|
disabled: 已停用
|
||||||
|
enabled: 已啟用
|
||||||
|
ftl: '未來時間限制(FTL):'
|
||||||
|
ftl_description: 相對於節點本地時間(秒),新區塊時間戳可以超前的最大限制,超過則區塊不被接受。
|
||||||
|
not_valid_value: 輸入的值無效
|
||||||
|
full_validation: 完整驗證
|
||||||
|
full_validation_description: 是否在處理每個區塊時執行完整鏈驗證(同步期間除外)。
|
||||||
|
archive_mode: 封存模式
|
||||||
|
archive_mode_desc: 以完整封存模式執行節點(同步需要更多磁碟空間和時間)。
|
||||||
|
attempt_time: '挖礦嘗試時間(秒):'
|
||||||
|
attempt_time_desc: 在停止並重新從交易池收集交易之前,嘗試在特定區塊頭上挖礦的時間量
|
||||||
|
min_share_diff: '可接受的最低份額難度:'
|
||||||
|
reset_settings_desc: 將節點設定重設為預設值
|
||||||
|
reset_settings: 重設設定
|
||||||
|
reset: 重設
|
||||||
|
tx_pool: 交易池
|
||||||
|
pool_fee: '交易池接受的基礎手續費:'
|
||||||
|
reorg_period: '重組快取保留期(分鐘):'
|
||||||
|
max_tx_pool: '交易池中最大交易數:'
|
||||||
|
max_tx_stempool: '莖池中最大交易數:'
|
||||||
|
max_tx_weight: '可被選中建構區塊的最大交易總權重:'
|
||||||
|
epoch_duration: '紀元持續時間(秒):'
|
||||||
|
embargo_timer: 'Embargo 計時器(秒):'
|
||||||
|
aggregation_period: '彙總週期(秒):'
|
||||||
|
stem_probability: '莖階段機率:'
|
||||||
|
stem_txs: 莖階段交易
|
||||||
|
p2p_server: P2P 伺服器
|
||||||
|
p2p_port: 'P2P 連接埠:'
|
||||||
|
add_seed: 新增 DNS 種子節點
|
||||||
|
seed_address: 'DNS 種子節點位址:'
|
||||||
|
add_peer: 新增對等節點
|
||||||
|
peer_address: '對等節點位址:'
|
||||||
|
peer_address_error: '請輸入正確格式的 IP 位址或 DNS 名稱(確保指定主機可用),例如:192.168.0.1:1234 或 example.com:5678'
|
||||||
|
default: 預設
|
||||||
|
allow_list: 允許清單
|
||||||
|
allow_list_desc: 僅連線到此清單中的對等節點。
|
||||||
|
deny_list: 拒絕清單
|
||||||
|
deny_list_desc: 永不連線到此清單中的對等節點。
|
||||||
|
favourites: 我的最愛
|
||||||
|
favourites_desc: 優先連線的對等節點清單。
|
||||||
|
ban_window: '被封鎖對等節點的封鎖時長(秒):'
|
||||||
|
ban_window_desc: 封鎖決定由節點根據從對等節點接收資料的正確性做出。
|
||||||
|
max_inbound_count: '最大入站對等連線數:'
|
||||||
|
max_outbound_count: '最大出站對等連線數:'
|
||||||
|
reset_data_desc: 重設節點資料。請謹慎使用,僅在同步發生問題時使用。
|
||||||
|
reset_data: 重設資料
|
||||||
|
modal:
|
||||||
|
cancel: 取消
|
||||||
|
save: 儲存
|
||||||
|
add: 新增
|
||||||
|
modal_exit:
|
||||||
|
description: 確定要離開應用程式嗎?
|
||||||
|
exit: 離開
|
||||||
|
app_settings:
|
||||||
|
proxy: 代理
|
||||||
|
proxy_desc: 是否為應用程式的網路請求使用代理。
|
||||||
|
keyboard:
|
||||||
|
1: 1
|
||||||
|
2: 2
|
||||||
|
3: 3
|
||||||
|
4: 4
|
||||||
|
5: 5
|
||||||
|
6: 6
|
||||||
|
7: 7
|
||||||
|
8: 8
|
||||||
|
9: 9
|
||||||
|
0: 0
|
||||||
|
01: '-'
|
||||||
|
q: 手
|
||||||
|
w: 田
|
||||||
|
e: 水
|
||||||
|
r: 口
|
||||||
|
t: 廿
|
||||||
|
y: 卜
|
||||||
|
u: 山
|
||||||
|
i: 戈
|
||||||
|
o: 人
|
||||||
|
p: 心
|
||||||
|
p1: '"'
|
||||||
|
a: 日
|
||||||
|
s: 尸
|
||||||
|
d: 木
|
||||||
|
f: 火
|
||||||
|
g: 土
|
||||||
|
h: 竹
|
||||||
|
j: 十
|
||||||
|
k: 大
|
||||||
|
l: 中
|
||||||
|
l1: \
|
||||||
|
l2: ':'
|
||||||
|
z: 重
|
||||||
|
x: 難
|
||||||
|
c: 金
|
||||||
|
v: 女
|
||||||
|
b: 月
|
||||||
|
n: 弓
|
||||||
|
m: 一
|
||||||
|
m1: ','
|
||||||
|
m2: .
|
||||||
|
m3: /
|
||||||
|
goblin:
|
||||||
|
home:
|
||||||
|
anonymous: "匿名"
|
||||||
|
connected_tor: "已透過 Tor 連線"
|
||||||
|
tor_ready: "Tor 就緒 · 連線中繼…"
|
||||||
|
connecting_tor: "正在連線 Tor…"
|
||||||
|
connected_direct: "已連線(直連)"
|
||||||
|
connecting_direct: "正在連線(直連)…"
|
||||||
|
cant_reach_node: "無法連線節點"
|
||||||
|
node_synced: "節點已同步"
|
||||||
|
syncing: "同步中…"
|
||||||
|
balance_updating: "餘額更新中…"
|
||||||
|
balance_stale: "無法連線節點 · 上次已知餘額"
|
||||||
|
fiat_unavailable: "匯率不可用"
|
||||||
|
listening: "正在監聽付款"
|
||||||
|
block: "區塊 %{height}"
|
||||||
|
waiting_for_chain: "等待鏈資料…"
|
||||||
|
nav_wallet: "錢包"
|
||||||
|
nav_pay: "支付"
|
||||||
|
nav_activity: "動態"
|
||||||
|
nav_receive: "收款"
|
||||||
|
nav_settings: "設定"
|
||||||
|
back_again: "再按一次返回以切換錢包"
|
||||||
|
activity: "動態"
|
||||||
|
news: "新聞"
|
||||||
|
empty_title: "暫無動態"
|
||||||
|
empty_sub: "收發 grin 即可開始。"
|
||||||
|
recent: "最近"
|
||||||
|
scan_to_pay: "掃碼支付"
|
||||||
|
type_amount: "輸入金額"
|
||||||
|
request: "請求"
|
||||||
|
pay: "支付"
|
||||||
|
enter_amount: "輸入要支付或請求的金額"
|
||||||
|
activity:
|
||||||
|
canceled: "已取消"
|
||||||
|
pending: "待處理"
|
||||||
|
earlier: "更早"
|
||||||
|
today: "今天"
|
||||||
|
yesterday: "昨天"
|
||||||
|
title: "動態"
|
||||||
|
requests: "請求"
|
||||||
|
empty_title: "暫無動態"
|
||||||
|
empty_sub: "你的付款將顯示在這裡。"
|
||||||
|
pending_header: "待處理"
|
||||||
|
receipt:
|
||||||
|
title: "收據"
|
||||||
|
not_found: "未找到交易"
|
||||||
|
for_note: "用於 %{note}"
|
||||||
|
details: "交易詳情"
|
||||||
|
canceled: "已取消"
|
||||||
|
expired: "已過期"
|
||||||
|
funds_returned: "資金已退回"
|
||||||
|
complete: "已完成"
|
||||||
|
payment_received: "已收到付款"
|
||||||
|
payment_sent: "付款傳送成功"
|
||||||
|
pending: "待處理"
|
||||||
|
confs: "%{c}/%{r} 次確認"
|
||||||
|
waiting_to_confirm: "等待確認"
|
||||||
|
paying: "支付中…"
|
||||||
|
you: "你"
|
||||||
|
to: "收款方"
|
||||||
|
from: "付款方"
|
||||||
|
nostr: "nostr"
|
||||||
|
identity: "身份"
|
||||||
|
fee_none: "無"
|
||||||
|
network_fee: "網路手續費"
|
||||||
|
privacy: "隱私"
|
||||||
|
privacy_value: "Mimblewimble + Tor"
|
||||||
|
transaction: "交易"
|
||||||
|
cancel_request: "取消請求"
|
||||||
|
cancel_send: "取消付款"
|
||||||
|
stale_note: "此付款已等待一段時間。"
|
||||||
|
cancel_send_confirm: "再次點按以取消 — 對方可能仍會收到"
|
||||||
|
cancel_send_done: "付款已取消 — 你的資金已重新可用"
|
||||||
|
cancel_send_too_late: "這筆付款已經完成,無法取消"
|
||||||
|
waiting_to_receive: "等待 %{name} 接收…"
|
||||||
|
request:
|
||||||
|
title: "%{name} 發起請求"
|
||||||
|
approve: "同意"
|
||||||
|
decline: "拒絕"
|
||||||
|
review_title: "審核請求"
|
||||||
|
hold_to_accept: "按住以接受"
|
||||||
|
hold_accept_hint: "按住以支付此請求"
|
||||||
|
receive:
|
||||||
|
title: "收款"
|
||||||
|
requesting: "正在請求 %{amt}%{tsu} — 分享以收款"
|
||||||
|
clear_request: "清除請求"
|
||||||
|
share_handle: "分享你的使用者名稱以收款"
|
||||||
|
share_npub: "分享你的 npub 以收款"
|
||||||
|
copied: "已複製"
|
||||||
|
copy_nostr_id: "複製 nostr ID"
|
||||||
|
copy_address: "複製地址"
|
||||||
|
copy: "複製"
|
||||||
|
share_message: "在 Goblin 上向我付款 (goblin.st) — %{npub}"
|
||||||
|
privacy_note: "你的使用者名稱是公開的。付款內容在網路中保持加密。"
|
||||||
|
privacy_note_npub: "你的 npub 是公開的。付款內容在網路中保持加密。"
|
||||||
|
profile:
|
||||||
|
title: "資料"
|
||||||
|
activity: "動態"
|
||||||
|
no_activity: "尚無往來記錄。"
|
||||||
|
unblock: "解除封鎖"
|
||||||
|
block: "封鎖"
|
||||||
|
blocked_blurb: "已封鎖 — 其付款和請求會被丟棄。"
|
||||||
|
block_blurb: "封鎖後將丟棄對方發來的付款和請求。"
|
||||||
|
settings:
|
||||||
|
title: "設定"
|
||||||
|
connected_nostr: "已連線 nostr"
|
||||||
|
connecting_relays: "正在連線中繼…"
|
||||||
|
identity: "身份"
|
||||||
|
copy_npub: "複製 npub(公開)"
|
||||||
|
backup_note: "更換裝置?兩者都要備份:你的助記詞(資金)和身份 .backup 檔案(名稱 + 金鑰)。"
|
||||||
|
wallet: "錢包"
|
||||||
|
display_unit: "顯示單位"
|
||||||
|
relays: "中繼"
|
||||||
|
nostr_relays: "Nostr 中繼"
|
||||||
|
node: "節點"
|
||||||
|
min_conf: "最少確認數"
|
||||||
|
integrated_node: "整合節點設定"
|
||||||
|
node_advanced: "進階"
|
||||||
|
slatepacks: "Slatepack"
|
||||||
|
slatepacks_value: "手動交易"
|
||||||
|
trusted_sites: "受信任網站"
|
||||||
|
lock_wallet: "鎖定錢包"
|
||||||
|
switch_wallet: "切換錢包"
|
||||||
|
advanced: "進階"
|
||||||
|
privacy: "隱私"
|
||||||
|
tor_routing: "Tor 路由"
|
||||||
|
messages_lookups: "訊息和查詢"
|
||||||
|
tor_on: "開啟"
|
||||||
|
tor_off: "關閉"
|
||||||
|
auto_accept: "自動接受"
|
||||||
|
pairing: "價格貨幣"
|
||||||
|
accept_anyone: "任何人"
|
||||||
|
accept_contacts: "僅聯絡人"
|
||||||
|
accept_ask: "每次詢問"
|
||||||
|
requests: "請求"
|
||||||
|
incoming_requests: "收到的請求"
|
||||||
|
incoming_requests_sub: "允許他人向你請求付款"
|
||||||
|
hide_amounts: "隱藏金額"
|
||||||
|
hide_amounts_sub: "在通知中隱藏收到的金額"
|
||||||
|
language: "語言"
|
||||||
|
update_available: "有可用更新"
|
||||||
|
appearance: "外觀"
|
||||||
|
theme: "主題"
|
||||||
|
theme_light: "淺色"
|
||||||
|
theme_dark: "深色"
|
||||||
|
theme_yellow: "黃色"
|
||||||
|
archive: "存檔"
|
||||||
|
export_archive: "匯出存檔"
|
||||||
|
export_archive_caption: "將你的聯絡人、付款記錄和請求以 JSON 格式複製到剪貼簿。"
|
||||||
|
wipe_history: "清除付款記錄"
|
||||||
|
wipe_history_confirm: "再次點按以清除 — 無法撤銷"
|
||||||
|
about: "關於"
|
||||||
|
goblin: "Goblin"
|
||||||
|
build: "構建 %{build}"
|
||||||
|
network: "網路"
|
||||||
|
network_value: "MW + Tor + nostr"
|
||||||
|
third_party: "第三方"
|
||||||
|
grim: "GRIM(上游錢包)"
|
||||||
|
grin_node: "Grin 節點"
|
||||||
|
sp_intro: "進階功能 — 像 GRIM 那樣手動交換原始 slatepack。僅在無法透過使用者名稱收付款時使用。"
|
||||||
|
sp_receive_group: "接收或確認"
|
||||||
|
sp_receive_blurb: "貼上別人給你的 slatepack。Goblin 會接收付款、支付帳單,或確認並廣播到鏈上。"
|
||||||
|
sp_process: "處理 slatepack"
|
||||||
|
sp_paste_first: "請先貼上 slatepack。"
|
||||||
|
sp_reply_ready: "回覆已就緒 — 發回給傳送方。"
|
||||||
|
sp_finalizing: "正在確認並廣播到鏈上…"
|
||||||
|
sp_create_group: "建立付款"
|
||||||
|
sp_create_blurb: "生成一個 slatepack 交給他人。對方接收後將回復發回,你在上方確認即可。"
|
||||||
|
sp_amount_hint: "金額(grin)"
|
||||||
|
sp_addr_hint: "收款地址(可選)"
|
||||||
|
sp_create: "建立 slatepack"
|
||||||
|
sp_ready: "slatepack 已就緒 — 交給收款方。"
|
||||||
|
sp_amount_gt_zero: "請輸入大於零的金額。"
|
||||||
|
sp_to_send: "待發送的 slatepack"
|
||||||
|
sp_copy: "複製 slatepack"
|
||||||
|
cancel: "取消"
|
||||||
|
continue: "繼續"
|
||||||
|
final_confirmation: "最終確認"
|
||||||
|
type_reset: "輸入 RESET"
|
||||||
|
wallet_password: "錢包密碼"
|
||||||
|
done: "完成"
|
||||||
|
close: "關閉"
|
||||||
|
import_nsec_hint: "nsec1… 或貼上的備份"
|
||||||
|
backup_password_hint: "備份密碼(僅當在他處匯出時需要)"
|
||||||
|
import_btn: "匯入"
|
||||||
|
importing: "正在匯入…"
|
||||||
|
import_failed: "匯入失敗"
|
||||||
|
name_authority: "名稱授權方"
|
||||||
|
name_authority_title: "更改名稱授權方"
|
||||||
|
name_authority_blurb: "註冊和驗證名稱的伺服器。指向另一個實例即可使用並支付那裡託管的名稱。"
|
||||||
|
name_authority_invalid: "請輸入完整網址(https://…)。"
|
||||||
|
reset: "重置"
|
||||||
|
save: "儲存"
|
||||||
|
backup_file: "備份到檔案"
|
||||||
|
choose_backup_file: "選擇 .backup 檔案"
|
||||||
|
backup_read_failed: "無法讀取該檔案。"
|
||||||
|
backup_saved: "備份已儲存"
|
||||||
|
backup_saved_sub: "妥善保管 .backup 檔案。同時擁有它和你密碼的人都能恢復你的整個錢包。"
|
||||||
|
backup_file_title: "備份錢包"
|
||||||
|
backup_file_blurb: "建立一個包含你的錢包助記詞和所有身份的加密 .backup 檔案。輸入錢包密碼以封存它。"
|
||||||
|
backup_write_failed: "無法儲存檔案。"
|
||||||
|
create_backup: "建立備份"
|
||||||
|
registered: "已註冊 %{name}"
|
||||||
|
released_msg: "已釋放 — 使用者名稱可被搶注"
|
||||||
|
release_confirm: "釋放 %{name}?"
|
||||||
|
release_blurb: "一旦釋放即可被認領——任何人都可以,包括你接下來輪換到的金鑰。10 分鐘內你無法註冊另一個使用者名稱。"
|
||||||
|
releasing: "正在釋放…"
|
||||||
|
keep_it: "保留"
|
||||||
|
release_it: "釋放"
|
||||||
|
username: "使用者名稱"
|
||||||
|
username_note: "顯示為你的名字。在 goblin.st 上公開。付款保持加密。"
|
||||||
|
release_username: "釋放使用者名稱"
|
||||||
|
pick_username: "選擇使用者名稱 — 可選"
|
||||||
|
working: "處理中…"
|
||||||
|
claim: "註冊"
|
||||||
|
err_just_taken: "該使用者名稱剛被佔用"
|
||||||
|
err_cooldown: "你剛釋放了一個使用者名稱 — 10 分鐘內無法註冊新使用者名稱。"
|
||||||
|
err_unreachable: "無法連線 goblin.st — 連線中斷。請重試。"
|
||||||
|
err_release: "無法釋放:%{err}"
|
||||||
|
avail_available: "可用!"
|
||||||
|
avail_taken: "已被佔用"
|
||||||
|
avail_reserved: "已保留"
|
||||||
|
avail_invalid: "使用者名稱為 3–20 個字元:a–z、0–9、_ 或 -"
|
||||||
|
avail_quarantined: "不可用"
|
||||||
|
avail_unknown: "無法檢查 — 連線中斷。請重試。"
|
||||||
|
username_none: "未設定"
|
||||||
|
advanced_privacy: "進階隱私"
|
||||||
|
tap_reveal: "點按以顯示"
|
||||||
|
notif_someone: "某人"
|
||||||
|
notif_private_received: "你收到了付款。開啟 Goblin 檢視。"
|
||||||
|
notif_private_requested: "新的付款請求。開啟 Goblin 檢視。"
|
||||||
|
username:
|
||||||
|
title: "使用者名稱"
|
||||||
|
authority: "名稱授權方"
|
||||||
|
authority_blurb: "保持預設,或指向其他實例以使用其託管的名稱。"
|
||||||
|
learn_more: "瞭解更多"
|
||||||
|
custom: "自定義授權方"
|
||||||
|
advprivacy:
|
||||||
|
intro: "控制通知會透露的內容,並在此裝置上隱藏你的餘額和歷史記錄。"
|
||||||
|
notifications: "通知"
|
||||||
|
hide_names: "隱藏名稱"
|
||||||
|
hide_names_sub: "通知中不顯示是誰付款"
|
||||||
|
hide_details: "隱藏全部詳情"
|
||||||
|
hide_details_sub: "僅顯示私密提醒,不含名稱或金額"
|
||||||
|
anon: "匿名模式"
|
||||||
|
anon_toggle: "模糊餘額和動態"
|
||||||
|
anon_sub: "餘額和動態顯示為圓點,點按後才會顯示"
|
||||||
|
advanced:
|
||||||
|
title: "進階"
|
||||||
|
intro: "來自 GRIM 的底層錢包工具。通常你用不到這些。"
|
||||||
|
own_node_desc: "在本裝置上同步完整的 Grin 節點,而不是信任公共節點。"
|
||||||
|
own_node_active: "正在執行你自己的節點"
|
||||||
|
repair: "修復錢包"
|
||||||
|
repair_desc: "重新掃描鏈並恢復任何遺失的輸出。這可能需要一些時間。"
|
||||||
|
repair_unavailable: "需要先有已同步的節點連線。"
|
||||||
|
repairing: "修復中… %{pct}%"
|
||||||
|
restore: "恢復錢包"
|
||||||
|
restore_desc: "刪除本地資料並從助記詞重建。如果修復無效,請使用此功能 — 之後需重新開啟錢包。"
|
||||||
|
restore_confirm: "再次點選以恢復"
|
||||||
|
show_phrase: "恢復助記詞"
|
||||||
|
phrase_desc: "你的 24 個 grin 助記詞 — 恢復資金的唯一方式。請離線且私密儲存。"
|
||||||
|
reveal: "顯示助記詞"
|
||||||
|
hide: "隱藏"
|
||||||
|
password: "錢包密碼"
|
||||||
|
wrong_password: "密碼錯誤。"
|
||||||
|
delete: "刪除錢包"
|
||||||
|
delete_desc: "從此裝置永久移除該錢包及其所有身份。請先備份。沒有助記詞,資金將無法找回。"
|
||||||
|
delete_confirm: "再次點選以刪除"
|
||||||
|
manage_node: "管理節點連線"
|
||||||
|
repair_confirm: "是的,立即修復"
|
||||||
|
repair_confirm_note: "修復會重新掃描鏈,可能需要幾分鐘。"
|
||||||
|
restore_confirm_note: "這會清除本地資料並從助記詞重建——可能需要幾分鐘。"
|
||||||
|
nostr_key: "Nostr 金鑰"
|
||||||
|
nostr_key_desc: "你的 nsec,即 nostr 身份的私鑰。複製它或顯示二維碼,即可登入 magick.market 等 nostr 應用。持有它的人即可控制你的身份,請妥善保管。"
|
||||||
|
reveal_nsec: "顯示金鑰"
|
||||||
|
copy_nsec: "複製 nsec"
|
||||||
|
show_qr: "顯示二維碼"
|
||||||
|
hide_qr: "隱藏二維碼"
|
||||||
|
nostr_section: "進階 nostr 設定"
|
||||||
|
backup_caption: "包含你的錢包和所有身份。"
|
||||||
|
danger_zone: "危險區域"
|
||||||
|
delete_warning: "這將刪除此裝置上的恢復助記詞和所有身份。未備份的內容將永久丟失。"
|
||||||
|
download_backup: "下載備份"
|
||||||
|
delete_final: "永久刪除"
|
||||||
|
privacy:
|
||||||
|
title: "網路隱私"
|
||||||
|
intro: "Goblin 只在網路上傳送極少量資訊,且每一項都經過端到端加密封裝,因此中繼無法讀取內容,也無法將其關聯到你。"
|
||||||
|
payments: "付款"
|
||||||
|
payments_blurb: "每條攜帶 slatepack 的 nostr 訊息。"
|
||||||
|
usernames: "使用者名稱"
|
||||||
|
usernames_blurb: "往返名稱授權方的 NIP-05 名稱查詢。"
|
||||||
|
price_avatars: "價格"
|
||||||
|
price_avatars_blurb: "金額旁顯示的即時法幣匯率。"
|
||||||
|
grin_node: "Grin 節點"
|
||||||
|
grin_node_blurb: "同步區塊並向網路廣播你的交易。這是對所有人都一樣的公開鏈上資料,絕不與你的身份關聯。"
|
||||||
|
how_privacy: "你的隱私如何運作"
|
||||||
|
how_privacy_blurb: "付款和訊息都用認證加密進行封裝,並以私密訊息的形式送達,因此中繼既無法讀取內容,也無法將付款關聯到你。"
|
||||||
|
always_direct: "始終直連"
|
||||||
|
tor_off: "Tor 已關閉"
|
||||||
|
tor_on: "Tor 已開啟"
|
||||||
|
tor_switch: "透過 Tor 路由"
|
||||||
|
tor_switch_sub: "向中繼隱藏你的 IP。"
|
||||||
|
vpn_note: "關閉 Tor 後,中繼可以看到你的 IP 位址。我們建議你至少使用 VPN。"
|
||||||
|
pairing:
|
||||||
|
title: "配對"
|
||||||
|
intro: "你的餘額和金額以何種貨幣顯示。"
|
||||||
|
pair_with: "配對貨幣"
|
||||||
|
rates_note: "匯率僅在開啟配對時透過 Tor 獲取 — 關閉後不會有任何匯率請求離開你的裝置。"
|
||||||
|
relays:
|
||||||
|
title: "中繼"
|
||||||
|
intro: "付款訊息會映象到下方每個中繼;只要有一個可達的中繼即可收款。"
|
||||||
|
your_relays: "你的中繼"
|
||||||
|
add_relay: "新增中繼"
|
||||||
|
add_relay_btn: "新增中繼"
|
||||||
|
save_reconnect: "儲存並重新連線"
|
||||||
|
none: "無"
|
||||||
|
count: "%{n} 箇中繼"
|
||||||
|
node:
|
||||||
|
title: "節點"
|
||||||
|
connection: "連線"
|
||||||
|
integrated: "整合節點"
|
||||||
|
applies_after: "在錢包鎖定並再次解鎖後生效。"
|
||||||
|
add_external: "新增外部節點"
|
||||||
|
api_secret_hint: "API 金鑰(可選)"
|
||||||
|
add_node: "新增節點"
|
||||||
|
integrated_host: "整合節點"
|
||||||
|
summary_syncing: "%{conn} · 同步中"
|
||||||
|
summary_block: "區塊 %{height} · %{conn}"
|
||||||
|
nips:
|
||||||
|
title: "nostr 與 NIPs"
|
||||||
|
intro1: "Goblin 使用 nostr — 一種透過簡單中繼伺服器傳遞簽名訊息的開放協議。你的錢包擁有自己的 nostr 身份:一個獨立的隨機金鑰,刻意與你的資金和助記詞保持獨立。每筆付款都作為身份之間的端對端加密私信傳輸,slatepack 就包含在其中。"
|
||||||
|
intro2: "goblin.st 是 Goblin 的名稱服務:註冊使用者名會在此釋出名稱 → 金鑰的對應(NIP-05),讓人們可以付款給你而不必使用冗長的 npub。使用者名稱是公開的;付款內容則永不公開。NIPs 是該協議的構建模組 — 點選任意一項可閱讀規範。"
|
||||||
|
n05_title: "名稱"
|
||||||
|
n05_blurb: "將 username@goblin.st 對應到你的金鑰,讓使用者名稱像地址一樣使用。"
|
||||||
|
n17_title: "私密訊息"
|
||||||
|
n17_blurb: "每筆付款傳輸所用的加密私信信封。"
|
||||||
|
n44_title: "加密"
|
||||||
|
n44_blurb: "這些訊息內部使用的認證加密演算法。"
|
||||||
|
n49_title: "金鑰加密"
|
||||||
|
n49_blurb: "私鑰靜態儲存的方式,由你的密碼鎖定。"
|
||||||
|
n59_title: "禮物包裝"
|
||||||
|
n59_blurb: "包裝訊息,使中繼無法看到通訊雙方是誰。"
|
||||||
|
n98_title: "HTTP 認證"
|
||||||
|
n98_blurb: "為向 goblin.st 註冊使用者名的請求籤名。"
|
||||||
|
batch:
|
||||||
|
title: "批准 %{n} 張發票"
|
||||||
|
blurb: "向 %{name} 發出 %{n} 個付款請求,每個 %{amount},共 %{total}。每個請求都有自己全新的收款地址。"
|
||||||
|
approve: "批准"
|
||||||
|
login:
|
||||||
|
title: "登入"
|
||||||
|
headline: "登入 %{domain}?"
|
||||||
|
identity: "登入身份"
|
||||||
|
explain: "這將簽署一次性的登入請求。不會分享你的金鑰,網站也無法以你的身份行事。"
|
||||||
|
pass_prompt: "輸入錢包密碼以登入。"
|
||||||
|
confirm: "登入"
|
||||||
|
sent: "已登入 %{domain}"
|
||||||
|
failed: "無法連線 %{domain},登入未送達。"
|
||||||
|
authorize:
|
||||||
|
title: "授權"
|
||||||
|
headline: "授權 %{domain}?"
|
||||||
|
kind_post: "公開貼文"
|
||||||
|
kind_repost: "轉發"
|
||||||
|
kind_reaction: "回應"
|
||||||
|
kind_article: "文章(長文)"
|
||||||
|
kind_unknown: "型別 %{n}(無法識別)"
|
||||||
|
unknown_caution: "Goblin 無法識別此事件型別。僅在你信任 %{domain} 時才批准。"
|
||||||
|
identity: "簽名身份"
|
||||||
|
explain: "批准將簽署這一個事件並交給 %{domain}。Goblin 不會發布任何內容,不會分享金鑰,網站也不會獲得會話或未來的許可權。"
|
||||||
|
pass_prompt: "輸入錢包密碼以授權。"
|
||||||
|
confirm: "授權"
|
||||||
|
sent: "已授權 %{domain}"
|
||||||
|
failed: "無法連線 %{domain},未送達任何內容。"
|
||||||
|
truncated: "已截斷,還有 %{n} 個字元"
|
||||||
|
show_full: "顯示全部"
|
||||||
|
show_less: "收起"
|
||||||
|
replying_to: "回覆 %{id}"
|
||||||
|
mentions: "提及 %{id}"
|
||||||
|
repost_of: "轉發 %{id}"
|
||||||
|
by_author: "作者 %{id}"
|
||||||
|
reacts_to: "回應 %{id}"
|
||||||
|
article_title: "標題:%{title}"
|
||||||
|
trust:
|
||||||
|
title: "信任"
|
||||||
|
headline: "信任 %{domain}?"
|
||||||
|
identity: "登入身份"
|
||||||
|
lead: "本次會話中 %{domain} 可代表你操作,無需每次詢問。任何花錢的操作始終會詢問。"
|
||||||
|
grant_intro: "可無需詢問簽署:"
|
||||||
|
cat_social: "貼文和互動"
|
||||||
|
cat_dm: "私信"
|
||||||
|
cat_market: "商品列表"
|
||||||
|
cat_identity: "資料和列表"
|
||||||
|
cat_delete: "刪除"
|
||||||
|
cat_http: "上傳和 HTTP 認證"
|
||||||
|
cat_unknown: "其他事件型別(類型 %{n})"
|
||||||
|
login_excluded: "登入許可權絕不會授予網站。"
|
||||||
|
money_line: "涉及資金時始終需要你的密碼。絕不會靜默花費或出售。"
|
||||||
|
duration: "僅本次會話有效。可隨時在設定,受信任網站中結束。"
|
||||||
|
pass_prompt: "輸入錢包密碼以信任此網站。"
|
||||||
|
confirm_hold: "長按以在本次會話中信任"
|
||||||
|
sent: "已在本次會話中信任 %{domain}"
|
||||||
|
failed: "無法連線 %{domain}。會話未啟動。"
|
||||||
|
announce_failed: "已信任,但 %{domain} 尚未確認會話。"
|
||||||
|
notice_volume: "某個受信任網站簽署量過大。"
|
||||||
|
notice_decrypt: "某個受信任網站正在讀取你的訊息。"
|
||||||
|
money:
|
||||||
|
title: "確認"
|
||||||
|
headline: "在 %{domain} 中確認?"
|
||||||
|
identity: "以 %{id} 簽署"
|
||||||
|
explain: "此操作轉移或鎖定價值,因此每次都會詢問。批准將簽署這一操作。"
|
||||||
|
pass_prompt: "輸入錢包密碼以確認。"
|
||||||
|
confirm_hold: "長按以確認"
|
||||||
|
encrypt_desc: "一條鎖定付款的加密訂單訊息。"
|
||||||
|
trusted_sites:
|
||||||
|
title: "受信任網站"
|
||||||
|
intro: "本次會話中你信任的網站可在不詢問的情況下籤署低風險操作。資金操作始終詢問。可在此結束任意會話。"
|
||||||
|
empty: "暫無受信任網站。登入時信任某個網站即可為其開啟簽署會話。"
|
||||||
|
none: "無"
|
||||||
|
can_sign: "可靜默簽署:%{cats}"
|
||||||
|
time_left: "%{mins} 分鐘後結束"
|
||||||
|
paused: "已暫停,簽署量過大"
|
||||||
|
resume: "恢復"
|
||||||
|
end: "結束會話"
|
||||||
|
identities:
|
||||||
|
title: "身份"
|
||||||
|
switch_hint: "切換身份"
|
||||||
|
blurb: "你的身份就是你收款時使用的名稱。它們都匯入這一個錢包。新增新身份、在它們之間切換,或移除不再需要的身份。"
|
||||||
|
privacy_note: "這些身份共用一個錢包和一個餘額——是不同的入口,而不是不同的資金。若需真正分離的資金,請使用另一個錢包。"
|
||||||
|
held: "已持有的身份"
|
||||||
|
add: "新增身份"
|
||||||
|
add_title: "新增一個身份"
|
||||||
|
nsec_hint: "貼上 nsec"
|
||||||
|
generate_note: "將為此錢包建立一個全新的匿名金鑰。"
|
||||||
|
generate: "建立"
|
||||||
|
import: "匯入"
|
||||||
|
choose_backup: "選擇 .backup 檔案"
|
||||||
|
backup_selected: "已選擇備份檔案"
|
||||||
|
delete_short: "刪除身份"
|
||||||
|
delete_title: "刪除 %{name}?"
|
||||||
|
delete_blurb: "這將從此錢包中永久移除該身份。已收到的錢仍保留在你的餘額中。"
|
||||||
|
delete_backup_note: "請先備份——沒有它的 nsec 或 .backup 檔案將無法恢復。"
|
||||||
|
delete_confirm: "刪除"
|
||||||
|
manage_title: "管理身份"
|
||||||
|
tag_note: "給它起一個私密名稱。只有你能看到——絕不會被分享。"
|
||||||
|
tag_hint: "私密名稱"
|
||||||
|
tag_save: "儲存"
|
||||||
|
pass_prompt: "錢包密碼用於加密和解鎖每個身份。輸入以繼續。"
|
||||||
|
working: "處理中…"
|
||||||
|
onboarding:
|
||||||
|
privacy:
|
||||||
|
kicker: "網路隱私"
|
||||||
|
continue: "繼續"
|
||||||
|
intro:
|
||||||
|
private_money_head: "私密貨幣"
|
||||||
|
private_money_body: "Goblin 是一個 grin 錢包 — 鏈上無金額、無地址的數字現金。"
|
||||||
|
send_like_message_head: "像發訊息一樣付款"
|
||||||
|
send_like_message_body: "向使用者名稱或 npub 付款,款項會作為端對端加密訊息透過 nostr 和 Tor 送達 — 中間任何人都看不到金額或參與者。"
|
||||||
|
yours_alone_head: "完全屬於你"
|
||||||
|
yours_alone_body: "金鑰、使用者名稱和歷史記錄都存於本裝置。基於 GRIM 錢包構建。"
|
||||||
|
get_started: "開始使用"
|
||||||
|
footnote: "約需一分鐘。之後一切均可更改。"
|
||||||
|
node:
|
||||||
|
kicker: "步驟 1 / 3 · 網路"
|
||||||
|
title: "Goblin 該如何\n監視鏈?"
|
||||||
|
own_title: "執行我自己的節點"
|
||||||
|
own_badge: "私密"
|
||||||
|
own_body: "無需信任任何人 — 錢包自行驗證鏈。完成設定時在後臺同步。"
|
||||||
|
connect_title: "連線到節點"
|
||||||
|
connect_badge: "即時"
|
||||||
|
connect_body: "無需等待同步。你選擇的節點可看到錢包的查詢。"
|
||||||
|
changeable: "隨時可在 設定 → 節點 中更改。"
|
||||||
|
continue: "繼續"
|
||||||
|
url_invalid: "節點 URL 必須以 http:// 或 https:// 開頭"
|
||||||
|
wallet:
|
||||||
|
kicker: "步驟 2 / 3 · 錢包"
|
||||||
|
title: "設定你的錢包"
|
||||||
|
create_new: "新建"
|
||||||
|
restore_from_seed: "從助記詞恢復"
|
||||||
|
import_identity: "匯入身份"
|
||||||
|
import_identity_hint: "此步驟之後,你將匯入現有金鑰(.backup 檔案或 nsec)。"
|
||||||
|
name_hint: "錢包名稱"
|
||||||
|
password_hint: "密碼"
|
||||||
|
repeat_password_hint: "重複密碼"
|
||||||
|
restore_hint: "準備好你的助記詞 — 下一步將輸入。"
|
||||||
|
create_hint: "接下來你會得到 24 個助記詞以供抄寫。它們就是錢 — 誰持有它們,誰就掌握你的資金。"
|
||||||
|
continue: "繼續"
|
||||||
|
passwords_no_match: "密碼不一致"
|
||||||
|
words:
|
||||||
|
kicker: "步驟 2 / 3 · 錢包"
|
||||||
|
title_restore: "輸入你的助記詞"
|
||||||
|
title_create: "抄下這些詞"
|
||||||
|
write_down_hint: "按順序寫在紙上。任何持有這些詞的人都能取走你的資金;丟失這些詞又丟失裝置,資金將無法找回。"
|
||||||
|
paste: "貼上"
|
||||||
|
scan_qr: "掃描二維碼"
|
||||||
|
copy_clipboard: "複製到剪貼簿(不建議)"
|
||||||
|
restore_wallet: "恢復錢包"
|
||||||
|
wrote_them_down: "我已抄好"
|
||||||
|
fill_every_word: "填寫每個詞 — 點選某個詞進行編輯,或貼上整個短語。"
|
||||||
|
unlock_backup: "解鎖備份"
|
||||||
|
backup_unlocked: "備份已解鎖。錢包開啟時將恢復你的身份。"
|
||||||
|
confirm:
|
||||||
|
kicker: "步驟 2 / 3 · 錢包"
|
||||||
|
title: "現在來驗證"
|
||||||
|
enter_hint: "輸入你剛抄下的詞。點選某個詞進行輸入。"
|
||||||
|
paste: "貼上"
|
||||||
|
create_wallet: "建立錢包"
|
||||||
|
keep_going: "繼續 — 每個詞,按順序。"
|
||||||
|
identity:
|
||||||
|
kicker: "步驟 3 / 3 · 身份"
|
||||||
|
title: "你的付款身份"
|
||||||
|
key_being_made: "正在生成金鑰…"
|
||||||
|
connected_tor: "已透過 Tor 連線"
|
||||||
|
connecting_tor: "正在透過 Tor 連線…"
|
||||||
|
connected_direct: "已連線(直連)"
|
||||||
|
connecting_direct: "正在連線(直連)…"
|
||||||
|
fresh_key_blurb: "一個不屬於助記詞的支付金鑰——可隨時輪換以保護隱私,且不影響你的資金。"
|
||||||
|
restoring: "正在恢復你的身份…"
|
||||||
|
clean_slate_blurb: "想要全新開始?隨時換上一個全新金鑰 — 新的你與舊的毫無關聯。同一個錢包,煥然一新。"
|
||||||
|
pick_username: "選擇使用者名稱 — 可選"
|
||||||
|
username_blurb: "朋友支付給你的名稱,而不是一長串金鑰。可選——隨時認領。"
|
||||||
|
username_field_hint: "你的使用者名稱"
|
||||||
|
working: "處理中…"
|
||||||
|
claim_username: "註冊使用者名"
|
||||||
|
available_when_connected: "Tor 連線後可用 — 或跳過,稍後註冊。"
|
||||||
|
youre: "你是 %{name}"
|
||||||
|
claimed_title: "%{name} 已歸你所有"
|
||||||
|
claimed_blurb: "朋友現在可以用你的使用者名稱向你付款。一切就緒——開啟錢包吧。"
|
||||||
|
open_wallet: "開啟我的錢包"
|
||||||
|
skip_for_now: "暫時跳過"
|
||||||
|
import_existing: "已有 Goblin 身份?匯入它"
|
||||||
|
import_title: "匯入你的身份"
|
||||||
|
import_blurb: "貼上你的 nsec 或選擇一個 .backup 檔案,保留你現有的金鑰和使用者名稱,而不是這個新的。"
|
||||||
|
errors:
|
||||||
|
cant_open: "無法開啟錢包:%{err}"
|
||||||
|
cant_create: "無法建立錢包:%{err}"
|
||||||
|
send:
|
||||||
|
scan_to_request: "掃碼請求"
|
||||||
|
scan_to_pay: "掃碼支付"
|
||||||
|
tab_scan: "掃描"
|
||||||
|
tab_my_code: "我的二維碼"
|
||||||
|
request_from: "向誰請求"
|
||||||
|
send_to: "傳送給"
|
||||||
|
search_hint: "使用者名稱、npub 或名稱"
|
||||||
|
suggested: "%{icon} 建議"
|
||||||
|
no_contacts: "暫無聯絡人。透過使用者名稱查詢某人。"
|
||||||
|
no_profile: "無資料"
|
||||||
|
tag_contact: "聯絡人"
|
||||||
|
tag_on_nostr: "在 nostr 上"
|
||||||
|
searching_nostr: "正在搜尋 nostr…"
|
||||||
|
unverified_title: "向未驗證的金鑰付款?"
|
||||||
|
unverified_body: "此金鑰未釋出 nostr 資料 — 它可能是全新的、匿名的或輸錯的。傳送前請仔細核對是否正確。"
|
||||||
|
keep_looking: "繼續查詢"
|
||||||
|
pay_anyway: "仍然付款"
|
||||||
|
scan_not_recipient: "該二維碼不是 goblin 收款方 — 應為 npub 或使用者名稱"
|
||||||
|
scan_prompt: "將 goblin 二維碼對準取景框以啟用"
|
||||||
|
scan_to_pay_me: "掃碼向我付款"
|
||||||
|
share_btn: "%{icon} 分享"
|
||||||
|
share_message: "在 Goblin 上向我付款 — %{handle}\n%{link}\nnpub:%{npub}"
|
||||||
|
none_found: "未找到與 %{label} 匹配的人"
|
||||||
|
enter_recipient: "輸入使用者名稱、npub 或名稱"
|
||||||
|
amount_title: "金額"
|
||||||
|
to_name: "傳送給 %{name}"
|
||||||
|
not_enough: "你的 grin 餘額不足"
|
||||||
|
max: "最大"
|
||||||
|
note_label: "備註"
|
||||||
|
note_hint: "新增備註…"
|
||||||
|
add_note: "新增備註"
|
||||||
|
edit_note: "編輯備註"
|
||||||
|
note_cancel: "取消"
|
||||||
|
note_save: "儲存"
|
||||||
|
review_btn: "檢視"
|
||||||
|
confirm_request: "確認請求"
|
||||||
|
review_title: "檢視"
|
||||||
|
requesting_from: "向 %{name} 請求"
|
||||||
|
youre_sending: "你正在傳送給 %{name}"
|
||||||
|
row_from: "付款方"
|
||||||
|
row_to: "收款方"
|
||||||
|
row_note: "備註"
|
||||||
|
row_proof: "支付證明"
|
||||||
|
row_proof_val: "已包含"
|
||||||
|
row_proof_shared: "證明分享給"
|
||||||
|
row_they_pay: "對方支付"
|
||||||
|
row_they_pay_val: "僅當對方同意時"
|
||||||
|
row_delivery: "傳輸"
|
||||||
|
row_delivery_val: "NIP-44 加密,經由 Tor"
|
||||||
|
row_network_fee: "網路手續費"
|
||||||
|
row_network_fee_val: "從你的餘額中扣除"
|
||||||
|
row_privacy: "隱私"
|
||||||
|
row_privacy_val: "Mimblewimble + Tor"
|
||||||
|
send_request_btn: "傳送請求"
|
||||||
|
request_approve_hint: "對方將收到一條待同意的請求"
|
||||||
|
hold_to_send: "長按傳送"
|
||||||
|
lower_amount: "返回並降低金額"
|
||||||
|
hold_confirm_hint: "按住以確認"
|
||||||
|
requesting: "正在請求…"
|
||||||
|
sending: "正在傳送…"
|
||||||
|
they: "對方"
|
||||||
|
request_blocked: "%{who} 不接受請求。請對方改為向你傳送 grin。"
|
||||||
|
failed_request_title: "請求失敗"
|
||||||
|
failed_send_title: "傳送失敗"
|
||||||
|
failed_request_body: "我們無法送達請求。請對方改為向你傳送 grin。"
|
||||||
|
failed_send_body: "付款未送達。你的 grin 是安全的 — 請重試。"
|
||||||
|
try_again_btn: "重試"
|
||||||
|
close_btn: "關閉"
|
||||||
|
success:
|
||||||
|
requested: "已請求"
|
||||||
|
sent: "已傳送"
|
||||||
|
from: "來自"
|
||||||
|
to: "發往"
|
||||||
|
subtitle: "%{dir} %{who} · 剛剛"
|
||||||
|
done_btn: "完成"
|
||||||
|
receipt_btn: "收據"
|
||||||
@@ -5,9 +5,9 @@
|
|||||||
<key>CFBundleDevelopmentRegion</key>
|
<key>CFBundleDevelopmentRegion</key>
|
||||||
<string>en</string>
|
<string>en</string>
|
||||||
<key>CFBundleDisplayName</key>
|
<key>CFBundleDisplayName</key>
|
||||||
<string>Grim</string>
|
<string>Goblin</string>
|
||||||
<key>CFBundleExecutable</key>
|
<key>CFBundleExecutable</key>
|
||||||
<string>grim</string>
|
<string>goblin</string>
|
||||||
<key>CFBundleIconFile</key>
|
<key>CFBundleIconFile</key>
|
||||||
<string>AppIcon</string>
|
<string>AppIcon</string>
|
||||||
<key>CFBundleIconName</key>
|
<key>CFBundleIconName</key>
|
||||||
@@ -17,7 +17,7 @@
|
|||||||
<key>CFBundleInfoDictionaryVersion</key>
|
<key>CFBundleInfoDictionaryVersion</key>
|
||||||
<string>6.0</string>
|
<string>6.0</string>
|
||||||
<key>CFBundleName</key>
|
<key>CFBundleName</key>
|
||||||
<string>Grim</string>
|
<string>Goblin</string>
|
||||||
<key>CFBundlePackageType</key>
|
<key>CFBundlePackageType</key>
|
||||||
<string>APPL</string>
|
<string>APPL</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
@@ -29,7 +29,7 @@
|
|||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>1</string>
|
<string>1</string>
|
||||||
<key>NSCameraUsageDescription</key>
|
<key>NSCameraUsageDescription</key>
|
||||||
<string>Grim needs an access to your camera to scan QR code.</string>
|
<string>Goblin needs an access to your camera to scan QR code.</string>
|
||||||
<key>CFBundleDocumentTypes</key>
|
<key>CFBundleDocumentTypes</key>
|
||||||
<array>
|
<array>
|
||||||
<dict>
|
<dict>
|
||||||
@@ -57,6 +57,19 @@
|
|||||||
<string>Document</string>
|
<string>Document</string>
|
||||||
</dict>
|
</dict>
|
||||||
</array>
|
</array>
|
||||||
|
<key>CFBundleURLTypes</key>
|
||||||
|
<array>
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleURLName</key>
|
||||||
|
<string>st.goblin.pay</string>
|
||||||
|
<key>CFBundleTypeRole</key>
|
||||||
|
<string>Viewer</string>
|
||||||
|
<key>CFBundleURLSchemes</key>
|
||||||
|
<array>
|
||||||
|
<string>goblin</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
</array>
|
||||||
<key>LSApplicationCategoryType</key>
|
<key>LSApplicationCategoryType</key>
|
||||||
<string>public.app-category.finance</string>
|
<string>public.app-category.finance</string>
|
||||||
<key>NSHumanReadableCopyright</key>
|
<key>NSHumanReadableCopyright</key>
|
||||||
@@ -1,128 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
||||||
<plist version="1.0">
|
|
||||||
<dict>
|
|
||||||
<key>files</key>
|
|
||||||
<dict>
|
|
||||||
<key>Resources/AppIcon.icns</key>
|
|
||||||
<data>
|
|
||||||
F0XBdu5xI+eXrj78HQf2Qr9SKio=
|
|
||||||
</data>
|
|
||||||
</dict>
|
|
||||||
<key>files2</key>
|
|
||||||
<dict>
|
|
||||||
<key>Resources/AppIcon.icns</key>
|
|
||||||
<dict>
|
|
||||||
<key>hash2</key>
|
|
||||||
<data>
|
|
||||||
ZjAn1LaNzSeTeUtKbWKWE7W2ELzhYyrHjKYOXUkQvcI=
|
|
||||||
</data>
|
|
||||||
</dict>
|
|
||||||
</dict>
|
|
||||||
<key>rules</key>
|
|
||||||
<dict>
|
|
||||||
<key>^Resources/</key>
|
|
||||||
<true/>
|
|
||||||
<key>^Resources/.*\.lproj/</key>
|
|
||||||
<dict>
|
|
||||||
<key>optional</key>
|
|
||||||
<true/>
|
|
||||||
<key>weight</key>
|
|
||||||
<real>1000</real>
|
|
||||||
</dict>
|
|
||||||
<key>^Resources/.*\.lproj/locversion.plist$</key>
|
|
||||||
<dict>
|
|
||||||
<key>omit</key>
|
|
||||||
<true/>
|
|
||||||
<key>weight</key>
|
|
||||||
<real>1100</real>
|
|
||||||
</dict>
|
|
||||||
<key>^Resources/Base\.lproj/</key>
|
|
||||||
<dict>
|
|
||||||
<key>weight</key>
|
|
||||||
<real>1010</real>
|
|
||||||
</dict>
|
|
||||||
<key>^version.plist$</key>
|
|
||||||
<true/>
|
|
||||||
</dict>
|
|
||||||
<key>rules2</key>
|
|
||||||
<dict>
|
|
||||||
<key>.*\.dSYM($|/)</key>
|
|
||||||
<dict>
|
|
||||||
<key>weight</key>
|
|
||||||
<real>11</real>
|
|
||||||
</dict>
|
|
||||||
<key>^(.*/)?\.DS_Store$</key>
|
|
||||||
<dict>
|
|
||||||
<key>omit</key>
|
|
||||||
<true/>
|
|
||||||
<key>weight</key>
|
|
||||||
<real>2000</real>
|
|
||||||
</dict>
|
|
||||||
<key>^(Frameworks|SharedFrameworks|PlugIns|Plug-ins|XPCServices|Helpers|MacOS|Library/(Automator|Spotlight|LoginItems))/</key>
|
|
||||||
<dict>
|
|
||||||
<key>nested</key>
|
|
||||||
<true/>
|
|
||||||
<key>weight</key>
|
|
||||||
<real>10</real>
|
|
||||||
</dict>
|
|
||||||
<key>^.*</key>
|
|
||||||
<true/>
|
|
||||||
<key>^Info\.plist$</key>
|
|
||||||
<dict>
|
|
||||||
<key>omit</key>
|
|
||||||
<true/>
|
|
||||||
<key>weight</key>
|
|
||||||
<real>20</real>
|
|
||||||
</dict>
|
|
||||||
<key>^PkgInfo$</key>
|
|
||||||
<dict>
|
|
||||||
<key>omit</key>
|
|
||||||
<true/>
|
|
||||||
<key>weight</key>
|
|
||||||
<real>20</real>
|
|
||||||
</dict>
|
|
||||||
<key>^Resources/</key>
|
|
||||||
<dict>
|
|
||||||
<key>weight</key>
|
|
||||||
<real>20</real>
|
|
||||||
</dict>
|
|
||||||
<key>^Resources/.*\.lproj/</key>
|
|
||||||
<dict>
|
|
||||||
<key>optional</key>
|
|
||||||
<true/>
|
|
||||||
<key>weight</key>
|
|
||||||
<real>1000</real>
|
|
||||||
</dict>
|
|
||||||
<key>^Resources/.*\.lproj/locversion.plist$</key>
|
|
||||||
<dict>
|
|
||||||
<key>omit</key>
|
|
||||||
<true/>
|
|
||||||
<key>weight</key>
|
|
||||||
<real>1100</real>
|
|
||||||
</dict>
|
|
||||||
<key>^Resources/Base\.lproj/</key>
|
|
||||||
<dict>
|
|
||||||
<key>weight</key>
|
|
||||||
<real>1010</real>
|
|
||||||
</dict>
|
|
||||||
<key>^[^/]+$</key>
|
|
||||||
<dict>
|
|
||||||
<key>nested</key>
|
|
||||||
<true/>
|
|
||||||
<key>weight</key>
|
|
||||||
<real>10</real>
|
|
||||||
</dict>
|
|
||||||
<key>^embedded\.provisionprofile$</key>
|
|
||||||
<dict>
|
|
||||||
<key>weight</key>
|
|
||||||
<real>20</real>
|
|
||||||
</dict>
|
|
||||||
<key>^version\.plist$</key>
|
|
||||||
<dict>
|
|
||||||
<key>weight</key>
|
|
||||||
<real>20</real>
|
|
||||||
</dict>
|
|
||||||
</dict>
|
|
||||||
</dict>
|
|
||||||
</plist>
|
|
||||||
@@ -35,14 +35,14 @@ cargo zigbuild --release --target ${arch}
|
|||||||
|
|
||||||
rm -f .intentionally-empty-file.o
|
rm -f .intentionally-empty-file.o
|
||||||
|
|
||||||
yes | cp -rf target/${arch}/release/grim macos/Grim.app/Contents/MacOS
|
yes | cp -rf target/${arch}/release/goblin macos/Goblin.app/Contents/MacOS
|
||||||
|
|
||||||
# Sign .app resources on change:
|
# Sign .app resources on change:
|
||||||
#rcodesign generate-self-signed-certificate
|
#rcodesign generate-self-signed-certificate
|
||||||
#rcodesign sign --pem-file cert.pem macos/Grim.app
|
#rcodesign sign --pem-file cert.pem macos/Goblin.app
|
||||||
|
|
||||||
# Create release package
|
# Create release package
|
||||||
FILE_NAME=grim-v$2-macos-$1.zip
|
FILE_NAME=goblin-v$2-macos-$1.zip
|
||||||
cd macos
|
cd macos
|
||||||
zip -r ${FILE_NAME} Grim.app
|
zip -r ${FILE_NAME} Goblin.app
|
||||||
mv ${FILE_NAME} ../target/${arch}/release
|
mv ${FILE_NAME} ../target/${arch}/release
|
||||||
|
|||||||
@@ -24,6 +24,11 @@ BASEDIR=$(cd "$(dirname "$0")" && pwd)
|
|||||||
cd "${BASEDIR}" || exit 1
|
cd "${BASEDIR}" || exit 1
|
||||||
cd ..
|
cd ..
|
||||||
|
|
||||||
|
# Prefer the GRIM-canonical toolchain: the custom NDK r29 (rebuilt LLVM, 16 KB
|
||||||
|
# page-aligned) + Android SDK from code.gri.mw/DEV. scripts/toolchain.sh fetches
|
||||||
|
# them and writes this env; falls back to whatever NDK/SDK is on the system.
|
||||||
|
[ -f .toolchains/env.sh ] && source .toolchains/env.sh
|
||||||
|
|
||||||
# Install platforms and tools
|
# Install platforms and tools
|
||||||
rustup target add armv7-linux-androideabi
|
rustup target add armv7-linux-androideabi
|
||||||
rustup target add aarch64-linux-android
|
rustup target add aarch64-linux-android
|
||||||
@@ -48,12 +53,15 @@ function build_lib() {
|
|||||||
|
|
||||||
sed -i -e 's/"cdylib","rlib"]/"rlib"]/g' Cargo.toml
|
sed -i -e 's/"cdylib","rlib"]/"rlib"]/g' Cargo.toml
|
||||||
rm -f Cargo.toml-e
|
rm -f Cargo.toml-e
|
||||||
|
|
||||||
|
# The Tor transport (embedded arti) is linked INTO libgrim.so, so there is no
|
||||||
|
# separate sidecar binary to cross-build or bundle into jniLibs.
|
||||||
}
|
}
|
||||||
|
|
||||||
### Build application
|
### Build application
|
||||||
function build_apk() {
|
function build_apk() {
|
||||||
flavor=$3
|
flavor=$3
|
||||||
[[ flavor == "" ]] && flavor="local"
|
[[ -z "$flavor" ]] && flavor="local"
|
||||||
cd android || exit 1
|
cd android || exit 1
|
||||||
./gradlew clean
|
./gradlew clean
|
||||||
# Build signed apk if keystore exists
|
# Build signed apk if keystore exists
|
||||||
@@ -72,12 +80,15 @@ function build_apk() {
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ $1 == "" ]] && [ $success -eq 1 ]; then
|
if [[ $1 == "" ]] && [ $success -eq 1 ]; then
|
||||||
# Launch application at all connected devices.
|
# Launch application at all connected devices. The installed application id
|
||||||
|
# (st.goblin.wallet) differs from the Java namespace (mw.gri.android), so
|
||||||
|
# derive it from build.gradle and launch the fully-qualified activity.
|
||||||
|
app_id=$(grep -m 1 -Po 'applicationId "\K[^"]*' app/build.gradle)
|
||||||
for SERIAL in $(adb devices | grep -v List | cut -f 1);
|
for SERIAL in $(adb devices | grep -v List | cut -f 1);
|
||||||
do
|
do
|
||||||
adb -s "$SERIAL" install ${apk_path}
|
adb -s "$SERIAL" install ${apk_path}
|
||||||
sleep 1s
|
sleep 1s
|
||||||
adb -s "$SERIAL" shell am start -n mw.gri.android/.MainActivity;
|
adb -s "$SERIAL" shell am start -n "${app_id}/mw.gri.android.MainActivity";
|
||||||
done
|
done
|
||||||
elif [ $success -eq 1 ]; then
|
elif [ $success -eq 1 ]; then
|
||||||
# Get version
|
# Get version
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Regenerate EVERY platform's app icon from two canonical sources:
|
||||||
|
# img/goblin-icon.png the gradient app icon (yellow gradient + black mascot)
|
||||||
|
# img/goblin-mark-black.svg the black mascot mark, vector, transparent bg
|
||||||
|
# (mirror of site/assets/goblin-mark-black.svg)
|
||||||
|
#
|
||||||
|
# Square icons (desktop window, Linux AppImage, Android launcher, Windows .ico,
|
||||||
|
# macOS .icns) come from the gradient PNG. The Android *adaptive* foreground is
|
||||||
|
# the black mascot on transparency, composited by the OS over the yellow
|
||||||
|
# background color (res/values/ic_launcher_background.xml = #FFD60A) — which
|
||||||
|
# reproduces the gradient icon's look.
|
||||||
|
#
|
||||||
|
# Requires ImageMagick (magick) and python3 (for the .icns container).
|
||||||
|
set -euo pipefail
|
||||||
|
cd "$(dirname "$0")/.."
|
||||||
|
|
||||||
|
ICON=img/goblin-icon.png
|
||||||
|
MARK=img/goblin-mark-black.svg
|
||||||
|
RES=android/app/src/main/res
|
||||||
|
|
||||||
|
# --- Desktop window icon (egui, src/main.rs) + Linux AppImage AppDir icon ---
|
||||||
|
magick "$ICON" -resize 256x256 PNG32:img/icon.png
|
||||||
|
cp img/icon.png linux/Goblin.AppDir/goblin.png
|
||||||
|
|
||||||
|
# --- Android launcher icons (gradient square) + adaptive foreground (mascot) ---
|
||||||
|
declare -A SIZES=( [mdpi]=48 [hdpi]=72 [xhdpi]=96 [xxhdpi]=144 [xxxhdpi]=192 )
|
||||||
|
declare -A FG_SIZES=( [mdpi]=108 [hdpi]=162 [xhdpi]=216 [xxhdpi]=324 [xxxhdpi]=432 )
|
||||||
|
for d in mdpi hdpi xhdpi xxhdpi xxxhdpi; do
|
||||||
|
s=${SIZES[$d]}; fg=${FG_SIZES[$d]}
|
||||||
|
# Mascot fills ~60% of the adaptive canvas — inside the ~61% safe zone, so no
|
||||||
|
# launcher mask (circle/squircle) ever clips it.
|
||||||
|
art=$(( fg * 60 / 100 ))
|
||||||
|
magick "$ICON" -resize "${s}x${s}" PNG32:"$RES/mipmap-$d/ic_launcher.png"
|
||||||
|
magick "$ICON" -resize "${s}x${s}" PNG32:"$RES/mipmap-$d/ic_launcher_round.png"
|
||||||
|
magick -background none "$MARK" -resize "${art}x${art}" \
|
||||||
|
-gravity center -extent "${fg}x${fg}" PNG32:"$RES/mipmap-$d/ic_launcher_foreground.png"
|
||||||
|
done
|
||||||
|
|
||||||
|
# --- Android notification (status-bar) icon: white-on-transparent mascot ---
|
||||||
|
# Android renders ic_stat_name as an alpha-only silhouette, so the RGB channels
|
||||||
|
# are forced to pure white; ~90% of the canvas matches the old asset's padding.
|
||||||
|
declare -A STAT_SIZES=( [mdpi]=24 [hdpi]=36 [xhdpi]=48 [xxhdpi]=72 [xxxhdpi]=96 )
|
||||||
|
for d in mdpi hdpi xhdpi xxhdpi xxxhdpi; do
|
||||||
|
s=${STAT_SIZES[$d]}
|
||||||
|
art=$(( s * 9 / 10 ))
|
||||||
|
magick -background none img/goblin-logo2.svg -resize "${art}x${art}" \
|
||||||
|
-gravity center -extent "${s}x${s}" \
|
||||||
|
-channel RGB -evaluate set 100% +channel PNG32:"$RES/drawable-$d/ic_stat_name.png"
|
||||||
|
done
|
||||||
|
|
||||||
|
# --- Windows installer + file-type icon (WiX wix/Product.ico) ---
|
||||||
|
magick "$ICON" -define icon:auto-resize=256,128,64,48,32,24,16 wix/Product.ico
|
||||||
|
|
||||||
|
# --- macOS app bundle icon (Goblin.app) ---
|
||||||
|
python3 scripts/make-icns.py "$ICON" macos/Goblin.app/Contents/Resources/AppIcon.icns
|
||||||
|
|
||||||
|
echo "icons generated from $ICON + $MARK"
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Build a macOS .icns from a square PNG, dependency-free.
|
||||||
|
|
||||||
|
macOS has `iconutil` and Linux distros have `png2icns`, but neither is reliably
|
||||||
|
present, and ImageMagick's own .icns writer only emits a single size. So we
|
||||||
|
assemble the multi-resolution PNG-payload .icns container by hand (the format
|
||||||
|
macOS 10.7+ accepts): the `icns` magic + big-endian length, then one entry per
|
||||||
|
OSType, each carrying an 8-bit PNG. ImageMagick (`magick`) does the resizing.
|
||||||
|
|
||||||
|
Usage: make-icns.py <source.png> <out.icns>
|
||||||
|
"""
|
||||||
|
|
||||||
|
import struct
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
|
||||||
|
# OSType -> pixel size. PNG-payload entries. Sizes above the source are
|
||||||
|
# Lanczos-upscaled (soft but acceptable for the few large Dock/Finder slots).
|
||||||
|
SLOTS = [
|
||||||
|
(b"icp4", 16),
|
||||||
|
(b"icp5", 32),
|
||||||
|
(b"icp6", 64),
|
||||||
|
(b"ic07", 128),
|
||||||
|
(b"ic08", 256),
|
||||||
|
(b"ic11", 32), # 16@2x
|
||||||
|
(b"ic12", 64), # 32@2x
|
||||||
|
(b"ic13", 256), # 128@2x
|
||||||
|
(b"ic09", 512), # 512
|
||||||
|
(b"ic14", 512), # 256@2x
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def render(src, size):
|
||||||
|
out = "/tmp/_icns_%d.png" % size
|
||||||
|
subprocess.run(
|
||||||
|
["magick", src, "-resize", "%dx%d" % (size, size),
|
||||||
|
"-filter", "Lanczos", "-depth", "8", "PNG32:%s" % out],
|
||||||
|
check=True,
|
||||||
|
)
|
||||||
|
return open(out, "rb").read()
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
if len(sys.argv) != 3:
|
||||||
|
sys.exit("usage: make-icns.py <source.png> <out.icns>")
|
||||||
|
src, out = sys.argv[1], sys.argv[2]
|
||||||
|
cache, entries = {}, []
|
||||||
|
for ostype, size in SLOTS:
|
||||||
|
if size not in cache:
|
||||||
|
cache[size] = render(src, size)
|
||||||
|
data = cache[size]
|
||||||
|
entries.append(ostype + struct.pack(">I", 8 + len(data)) + data)
|
||||||
|
body = b"".join(entries)
|
||||||
|
with open(out, "wb") as f:
|
||||||
|
f.write(b"icns" + struct.pack(">I", 8 + len(body)) + body)
|
||||||
|
print("wrote %s (%d entries)" % (out, len(entries)))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,126 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
#
|
||||||
|
# Fetch the canonical GRIM build toolchains (code.gri.mw/DEV) into .toolchains/.
|
||||||
|
#
|
||||||
|
# These mirror exactly what upstream GRIM's CI uses, so Goblin cross-builds every
|
||||||
|
# platform from one Linux box the same way GRIM does — instead of relying on
|
||||||
|
# whatever NDK/zig/appimagetool happens to be installed on the machine.
|
||||||
|
#
|
||||||
|
# Idempotent: each tool is skipped if already present. Linux x86_64 host only
|
||||||
|
# (the box we build releases on). Writes .toolchains/env.sh with the exports the
|
||||||
|
# build scripts source; run nothing else by hand.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# scripts/toolchain.sh # core: ndk zig appimage (what desktop+android need)
|
||||||
|
# scripts/toolchain.sh sdk gradle # add the Android SDK + Gradle
|
||||||
|
# scripts/toolchain.sh osxcross # build the macOS cross-toolchain (heavy)
|
||||||
|
# scripts/toolchain.sh all # everything
|
||||||
|
#
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
BASEDIR=$(cd "$(dirname "$0")/.." && pwd)
|
||||||
|
TC="${BASEDIR}/.toolchains"
|
||||||
|
DEV="https://code.gri.mw/DEV"
|
||||||
|
mkdir -p "${TC}"
|
||||||
|
|
||||||
|
dl() { echo " ↓ $(basename "$2")"; curl -fSL --retry 3 -o "$2" "$1"; }
|
||||||
|
|
||||||
|
# Pinned versions — bump here to track GRIM's DEV releases.
|
||||||
|
NDK_TAG="r29"; NDK_ARCHIVE="android-ndk-${NDK_TAG}-x86_64-linux-musl.tar.xz"; NDK_DIR="${TC}/android-ndk-${NDK_TAG}"
|
||||||
|
ZIG_VER="0.12.1"; ZIG_DIR="${TC}/zig"
|
||||||
|
AT_VER="1.9.1"; RT_TAG="20251108"
|
||||||
|
SDK_TAG="r36"; SDK_DIR="${TC}/android-sdk"
|
||||||
|
GRADLE_VER="8.13"; GRADLE_DIR="${TC}/gradle-${GRADLE_VER}"
|
||||||
|
SDK_VER="12.3"; OSX_DIR="${TC}/osxcross"
|
||||||
|
|
||||||
|
fetch_ndk() {
|
||||||
|
[ -e "${NDK_DIR}/source.properties" ] && { echo "ndk r29: present"; return; }
|
||||||
|
echo "ndk: fetching custom NDK ${NDK_TAG} (rebuilt LLVM, 16 KB-aligned)…"
|
||||||
|
dl "${DEV}/android-ndk-custom/releases/download/${NDK_TAG}/${NDK_ARCHIVE}" "${TC}/ndk.tar.xz"
|
||||||
|
tar -xJf "${TC}/ndk.tar.xz" -C "${TC}"; rm -f "${TC}/ndk.tar.xz"
|
||||||
|
}
|
||||||
|
|
||||||
|
fetch_zig() {
|
||||||
|
[ -x "${ZIG_DIR}/zig" ] && { echo "zig ${ZIG_VER}: present"; return; }
|
||||||
|
echo "zig: fetching ${ZIG_VER} (linker for cargo-zigbuild)…"
|
||||||
|
dl "${DEV}/zig/releases/download/${ZIG_VER}/zig-linux-x86_64-${ZIG_VER}.tar.xz" "${TC}/zig.tar.xz"
|
||||||
|
tar -xJf "${TC}/zig.tar.xz" -C "${TC}"; rm -f "${TC}/zig.tar.xz"
|
||||||
|
rm -rf "${ZIG_DIR}"; mv "${TC}/zig-linux-x86_64-${ZIG_VER}" "${ZIG_DIR}"
|
||||||
|
}
|
||||||
|
|
||||||
|
fetch_appimage() {
|
||||||
|
[ -x "${TC}/appimagetool" ] && [ -e "${TC}/runtime-x86_64" ] && [ -e "${TC}/runtime-aarch64" ] && { echo "appimagetool ${AT_VER}: present"; return; }
|
||||||
|
echo "appimage: fetching appimagetool ${AT_VER} + type2 runtimes (x86_64 + aarch64)…"
|
||||||
|
dl "${DEV}/appimagetool/releases/download/${AT_VER}/appimagetool-x86_64.AppImage" "${TC}/appimagetool"
|
||||||
|
dl "${DEV}/appimage-type2-runtime/releases/download/${RT_TAG}/runtime-x86_64" "${TC}/runtime-x86_64"
|
||||||
|
dl "${DEV}/appimage-type2-runtime/releases/download/${RT_TAG}/runtime-aarch64" "${TC}/runtime-aarch64"
|
||||||
|
chmod +x "${TC}/appimagetool" "${TC}/runtime-x86_64" "${TC}/runtime-aarch64"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Assemble a minimal Android SDK (build-tools + platform + platform-tools) from
|
||||||
|
# the DEV mirror so gradle has an SDK without a system install.
|
||||||
|
fetch_sdk() {
|
||||||
|
[ -d "${SDK_DIR}/platform-tools" ] && { echo "android-sdk ${SDK_TAG}: present"; return; }
|
||||||
|
echo "android-sdk: fetching build-tools + platform-36 + platform-tools (${SDK_TAG})…"
|
||||||
|
local base="${DEV}/android-platform-tools/releases/download/${SDK_TAG}"
|
||||||
|
mkdir -p "${SDK_DIR}/build-tools" "${SDK_DIR}/platforms"
|
||||||
|
dl "${base}/build-tools_r36.1_linux.zip" "${TC}/bt.zip"
|
||||||
|
dl "${base}/platform-36_r02.zip" "${TC}/pf.zip"
|
||||||
|
dl "${base}/platform-tools_r36.0.0-linux.zip" "${TC}/pt.zip"
|
||||||
|
# build-tools zip unzips to android-NN/ → rename to the version dir gradle wants.
|
||||||
|
local tmp; tmp=$(mktemp -d)
|
||||||
|
unzip -q "${TC}/bt.zip" -d "${tmp}"; mv "${tmp}"/*/ "${SDK_DIR}/build-tools/36.1.0"
|
||||||
|
unzip -q "${TC}/pf.zip" -d "${tmp}"; mv "${tmp}"/*/ "${SDK_DIR}/platforms/android-36"
|
||||||
|
unzip -q "${TC}/pt.zip" -d "${SDK_DIR}"
|
||||||
|
rm -rf "${tmp}" "${TC}/bt.zip" "${TC}/pf.zip" "${TC}/pt.zip"
|
||||||
|
}
|
||||||
|
|
||||||
|
fetch_gradle() {
|
||||||
|
[ -x "${GRADLE_DIR}/bin/gradle" ] && { echo "gradle ${GRADLE_VER}: present"; return; }
|
||||||
|
echo "gradle: fetching ${GRADLE_VER}…"
|
||||||
|
dl "${DEV}/gradle/releases/download/v${GRADLE_VER}/gradle-${GRADLE_VER}-bin.zip" "${TC}/gradle.zip"
|
||||||
|
unzip -q "${TC}/gradle.zip" -d "${TC}"; rm -f "${TC}/gradle.zip"
|
||||||
|
}
|
||||||
|
|
||||||
|
# osxcross: build the macOS cross-toolchain from source + the DEV macOS SDK.
|
||||||
|
# Heavy (compiles cctools/ld64); enables building macOS binaries off-Mac. CI also
|
||||||
|
# builds macOS natively, so this is the local/offline path — experimental.
|
||||||
|
fetch_osxcross() {
|
||||||
|
[ -x "${OSX_DIR}/target/bin/o64-clang" ] && { echo "osxcross: present"; return; }
|
||||||
|
command -v clang >/dev/null || { echo "osxcross: needs system clang/cmake — skipping"; return; }
|
||||||
|
echo "osxcross: cloning + building with macOS SDK ${SDK_VER} (slow)…"
|
||||||
|
[ -d "${OSX_DIR}/.git" ] || git clone --depth 1 "${DEV}/osxcross" "${OSX_DIR}"
|
||||||
|
dl "${DEV}/macosx-sdks/releases/download/${SDK_VER}/MacOSX${SDK_VER}.sdk.tar.xz" "${OSX_DIR}/tarballs/MacOSX${SDK_VER}.sdk.tar.xz"
|
||||||
|
( cd "${OSX_DIR}" && UNATTENDED=1 ./build.sh )
|
||||||
|
}
|
||||||
|
|
||||||
|
write_env() {
|
||||||
|
{
|
||||||
|
echo "# Auto-generated by scripts/toolchain.sh — source me for GRIM-canonical builds."
|
||||||
|
[ -e "${NDK_DIR}/source.properties" ] && { echo "export ANDROID_NDK_HOME=\"${NDK_DIR}\""; echo "export ANDROID_NDK_ROOT=\"${NDK_DIR}\""; }
|
||||||
|
[ -d "${SDK_DIR}/platform-tools" ] && echo "export ANDROID_HOME=\"${SDK_DIR}\""
|
||||||
|
local p="${TC}"
|
||||||
|
[ -x "${ZIG_DIR}/zig" ] && p="${ZIG_DIR}:${p}"
|
||||||
|
[ -x "${GRADLE_DIR}/bin/gradle" ] && p="${GRADLE_DIR}/bin:${p}"
|
||||||
|
[ -x "${OSX_DIR}/target/bin/o64-clang" ] && p="${OSX_DIR}/target/bin:${p}"
|
||||||
|
echo "export PATH=\"${p}:\$PATH\""
|
||||||
|
echo "export GOBLIN_APPIMAGETOOL=\"${TC}/appimagetool\""
|
||||||
|
echo "export GOBLIN_APPIMAGE_RUNTIME=\"${TC}/runtime-x86_64\""
|
||||||
|
} > "${TC}/env.sh"
|
||||||
|
}
|
||||||
|
|
||||||
|
tools=("$@"); [ ${#tools[@]} -eq 0 ] && tools=(ndk zig appimage)
|
||||||
|
[ "${tools[0]:-}" = "all" ] && tools=(ndk zig appimage sdk gradle osxcross)
|
||||||
|
for t in "${tools[@]}"; do
|
||||||
|
case "$t" in
|
||||||
|
ndk) fetch_ndk ;;
|
||||||
|
zig) fetch_zig ;;
|
||||||
|
appimage) fetch_appimage ;;
|
||||||
|
sdk) fetch_sdk ;;
|
||||||
|
gradle) fetch_gradle ;;
|
||||||
|
osxcross) fetch_osxcross ;;
|
||||||
|
*) echo "unknown tool: $t (ndk|zig|appimage|sdk|gradle|osxcross|all)" >&2; exit 1 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
write_env
|
||||||
|
echo "toolchain ready → ${TC}/env.sh"
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
@echo off
|
|
||||||
setlocal enabledelayedexpansion
|
|
||||||
|
|
||||||
:: Change directory to the script's location
|
|
||||||
cd /d "%~dp0"
|
|
||||||
|
|
||||||
:: Skip if Go not found.
|
|
||||||
where go >nul 2>nul
|
|
||||||
if %ERRORLEVEL% neq 0 (
|
|
||||||
echo Go could not be found
|
|
||||||
exit /b 0
|
|
||||||
)
|
|
||||||
|
|
||||||
set "go_os=%~1"
|
|
||||||
set "go_arch=%~2"
|
|
||||||
set "output_path=%~3"
|
|
||||||
|
|
||||||
echo Go build for os: %go_os%, arch: %go_arch%
|
|
||||||
|
|
||||||
:: Setup vars for Android.
|
|
||||||
if "%go_os%"=="android" (
|
|
||||||
|
|
||||||
:: Setup NDK root path env.
|
|
||||||
if "%ANDROID_NDK_HOME%"=="" (
|
|
||||||
:: Extract ndkVersion from build.gradle
|
|
||||||
:: Equivalent to: cat ../android/app/build.gradle | grep 'ndkVersion' | cut -d ' -f 2
|
|
||||||
for /f "tokens=2 delims='" %%a in ('findstr "ndkVersion" ..\android\app\build.gradle') do (
|
|
||||||
set "NDK_VERSION=%%a"
|
|
||||||
)
|
|
||||||
set "ANDROID_NDK_HOME=%ANDROID_HOME%\ndk\!NDK_VERSION!"
|
|
||||||
)
|
|
||||||
|
|
||||||
:: Setup NDK host path.
|
|
||||||
:: Since this is a Batch script, the host is Windows.
|
|
||||||
set "arch_host=windows-x86_64"
|
|
||||||
|
|
||||||
:: Setup NDK target arch.
|
|
||||||
if "%go_arch%"=="arm64" (
|
|
||||||
set "arch_bin_prefix=aarch64-linux-android"
|
|
||||||
) else if "%go_arch%"=="arm" (
|
|
||||||
set "arch_bin_prefix=armv7a-linux-androideabi"
|
|
||||||
) else (
|
|
||||||
set "arch_bin_prefix=x86_64-linux-android"
|
|
||||||
)
|
|
||||||
|
|
||||||
:: Build for current target.
|
|
||||||
set "CGO_ENABLED=1"
|
|
||||||
set "GOOS=%go_os%"
|
|
||||||
set "GOARCH=%go_arch%"
|
|
||||||
|
|
||||||
:: Define CC and CXX paths
|
|
||||||
set "CC=!ANDROID_NDK_HOME!\toolchains\llvm\prebuilt\!arch_host!\bin\!arch_bin_prefix!35-clang"
|
|
||||||
set "CXX=!ANDROID_NDK_HOME!\toolchains\llvm\prebuilt\!arch_host!\bin\!arch_bin_prefix!35-clang++"
|
|
||||||
|
|
||||||
go build -C "../tor/webtunnel" -ldflags="-s -w" -o "%output_path%" code.gri.mw/WEB/webtunnel/main/client
|
|
||||||
|
|
||||||
) else (
|
|
||||||
set "extra_flag="
|
|
||||||
if "%go_os%"=="windows" (
|
|
||||||
set "extra_flag=-H=windowsgui"
|
|
||||||
)
|
|
||||||
|
|
||||||
set "GOOS=%go_os%"
|
|
||||||
set "GOARCH=%go_arch%"
|
|
||||||
|
|
||||||
:: Build for non-android targets
|
|
||||||
go build -C "../tor/webtunnel" -ldflags="-s -w !extra_flag!" -o "%output_path%" code.gri.mw/WEB/webtunnel/main/client
|
|
||||||
)
|
|
||||||
|
|
||||||
endlocal
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
cd "$(dirname "$0")"
|
|
||||||
|
|
||||||
# Skip if Go not found.
|
|
||||||
if ! command -v go >/dev/null 2>&1
|
|
||||||
then
|
|
||||||
echo "Go could not be found"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
go_os=$1
|
|
||||||
go_arch=$2
|
|
||||||
|
|
||||||
echo "Go build for os: $go_os, arch: $go_arch"
|
|
||||||
|
|
||||||
# Setup vars for Android.
|
|
||||||
if [[ "$go_os" == "android" ]]; then
|
|
||||||
# Setup NDK root path env.
|
|
||||||
if [[ -z "$ANDROID_NDK_HOME" ]]; then
|
|
||||||
NDK_VERSION=$(cat ../android/app/build.gradle | grep 'ndkVersion' | cut -d \' -f 2)
|
|
||||||
ANDROID_NDK_HOME=$ANDROID_HOME/ndk/$NDK_VERSION
|
|
||||||
fi
|
|
||||||
# Setup NDK host path.
|
|
||||||
if [[ "$(uname)" == "Darwin" ]]; then
|
|
||||||
arch_host=darwin-x86_64
|
|
||||||
else
|
|
||||||
if [[ "$(uname -m)" == "aarch64" ]]; then
|
|
||||||
arch_host=linux-arm64
|
|
||||||
else
|
|
||||||
arch_host=linux-x86_64
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
# Setup NDK target arch.
|
|
||||||
if [[ "$go_arch" == "arm64" ]]; then
|
|
||||||
arch_bin_prefix=aarch64-linux-android
|
|
||||||
elif [[ "$go_arch" == "arm" ]]; then
|
|
||||||
arch_bin_prefix=armv7a-linux-androideabi
|
|
||||||
else
|
|
||||||
arch_bin_prefix=x86_64-linux-android
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Build for current target.
|
|
||||||
CGO_ENABLED=1 GOOS=$1 GOARCH=$2 CC="${ANDROID_NDK_HOME}/toolchains/llvm/prebuilt/${arch_host}/bin/${arch_bin_prefix}35-clang" CXX="${ANDROID_NDK_HOME}/toolchains/llvm/prebuilt/${arch_path}/bin/${arch_bin_prefix}35-clang++" go build -C "../tor/webtunnel" -ldflags="-s -w" -o "$3" code.gri.mw/WEB/webtunnel/main/client
|
|
||||||
else
|
|
||||||
if [[ "$go_os" == "windows" ]]; then
|
|
||||||
extra_flag="-H=windowsgui"
|
|
||||||
fi
|
|
||||||
GOOS=$1 GOARCH=$2 go build -C "../tor/webtunnel" -ldflags="-s -w ${extra_flag}" -o "$3" code.gri.mw/WEB/webtunnel/main/client
|
|
||||||
fi
|
|
||||||
|
|
||||||
@@ -44,6 +44,8 @@ pub struct App<Platform> {
|
|||||||
resize_direction: Option<ResizeDirection>,
|
resize_direction: Option<ResizeDirection>,
|
||||||
/// Flag to check if it's first draw.
|
/// Flag to check if it's first draw.
|
||||||
first_draw: bool,
|
first_draw: bool,
|
||||||
|
/// Last status-bar icon state pushed to the platform (Android).
|
||||||
|
status_bar_white: Option<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<Platform: PlatformCallbacks> App<Platform> {
|
impl<Platform: PlatformCallbacks> App<Platform> {
|
||||||
@@ -53,6 +55,7 @@ impl<Platform: PlatformCallbacks> App<Platform> {
|
|||||||
content: Content::default(),
|
content: Content::default(),
|
||||||
resize_direction: None,
|
resize_direction: None,
|
||||||
first_draw: true,
|
first_draw: true,
|
||||||
|
status_bar_white: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,6 +77,21 @@ impl<Platform: PlatformCallbacks> App<Platform> {
|
|||||||
self.first_draw = false;
|
self.first_draw = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Heartbeat for "is the app on-screen": stamp this frame, and keep a light
|
||||||
|
// ~2s repaint cadence so the stamp stays fresh while visible. When the app
|
||||||
|
// is backgrounded eframe stops calling this, the stamp goes stale, and
|
||||||
|
// background workers (the @name re-verify sweep) pause until we're back.
|
||||||
|
crate::mark_frame();
|
||||||
|
ctx.request_repaint_after(std::time::Duration::from_secs(2));
|
||||||
|
|
||||||
|
// Keep the Android status-bar icons readable against the in-app theme
|
||||||
|
// (the app draws edge-to-edge over the status bar). Only on change.
|
||||||
|
let white_icons = crate::gui::theme::status_bar_white_icons();
|
||||||
|
if self.status_bar_white != Some(white_icons) {
|
||||||
|
self.platform.set_status_bar_white_icons(white_icons);
|
||||||
|
self.status_bar_white = Some(white_icons);
|
||||||
|
}
|
||||||
|
|
||||||
// Handle Esc keyboard key event and platform Back button key event.
|
// Handle Esc keyboard key event and platform Back button key event.
|
||||||
let back_pressed = BACK_BUTTON_PRESSED.load(Ordering::Relaxed);
|
let back_pressed = BACK_BUTTON_PRESSED.load(Ordering::Relaxed);
|
||||||
if back_pressed
|
if back_pressed
|
||||||
@@ -167,6 +185,28 @@ impl<Platform: PlatformCallbacks> App<Platform> {
|
|||||||
|
|
||||||
/// Draw custom desktop window frame content.
|
/// Draw custom desktop window frame content.
|
||||||
fn custom_frame_ui(&mut self, ui: &mut egui::Ui, is_fullscreen: bool) {
|
fn custom_frame_ui(&mut self, ui: &mut egui::Ui, is_fullscreen: bool) {
|
||||||
|
// Paint the window area inside the frame margin with the theme
|
||||||
|
// background first: surface gaps are otherwise transparent, which X11
|
||||||
|
// without a compositor renders as black (strip under the sidebar in
|
||||||
|
// light/yellow themes). The margin ring itself must STAY transparent —
|
||||||
|
// painting it gives the window a visible border under a compositor.
|
||||||
|
let fill_rect = if is_fullscreen {
|
||||||
|
ui.max_rect()
|
||||||
|
} else {
|
||||||
|
ui.max_rect().shrink(Content::WINDOW_FRAME_MARGIN)
|
||||||
|
};
|
||||||
|
let fill_rounding = if is_fullscreen {
|
||||||
|
CornerRadius::ZERO
|
||||||
|
} else {
|
||||||
|
CornerRadius {
|
||||||
|
nw: 8,
|
||||||
|
ne: 8,
|
||||||
|
sw: 0,
|
||||||
|
se: 0,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
ui.painter()
|
||||||
|
.rect_filled(fill_rect, fill_rounding, Colors::fill());
|
||||||
let content_bg_rect = {
|
let content_bg_rect = {
|
||||||
let mut r = ui.max_rect();
|
let mut r = ui.max_rect();
|
||||||
if !is_fullscreen {
|
if !is_fullscreen {
|
||||||
@@ -290,14 +330,38 @@ impl<Platform: PlatformCallbacks> App<Platform> {
|
|||||||
ui.ctx().send_viewport_cmd(ViewportCommand::StartDrag);
|
ui.ctx().send_viewport_cmd(ViewportCommand::StartDrag);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Paint the title.
|
// Paint the title. Centering on the full rect runs the tail of the
|
||||||
let title_text = format!("Grim {} ツ", crate::VERSION);
|
// string under the right-side window buttons at narrow widths (the
|
||||||
painter.text(
|
// "Build NNN" digits collide with the minimize caret at 390px), so
|
||||||
title_rect.center(),
|
// when the centered galley would reach the button cluster, center it
|
||||||
egui::Align2::CENTER_CENTER,
|
// in the free span between the theme toggle and the buttons instead,
|
||||||
title_text,
|
// eliding if even that span is too tight.
|
||||||
egui::FontId::proportional(15.0),
|
let title_text = format!("Goblin ツ · Build {}", crate::BUILD);
|
||||||
Colors::title(true),
|
let title_font = egui::FontId::proportional(15.0);
|
||||||
|
let title_ink = Colors::title(true);
|
||||||
|
const BUTTONS_LEFT_INSET: f32 = 60.0; // theme toggle
|
||||||
|
const BUTTONS_RIGHT_INSET: f32 = 168.0; // minimize + fullscreen + close
|
||||||
|
let free_left = title_rect.min.x + BUTTONS_LEFT_INSET;
|
||||||
|
let free_right = title_rect.max.x - BUTTONS_RIGHT_INSET;
|
||||||
|
let mut galley = painter.layout_no_wrap(title_text.clone(), title_font.clone(), title_ink);
|
||||||
|
let mut center_x = title_rect.center().x;
|
||||||
|
if center_x + galley.size().x / 2.0 > free_right {
|
||||||
|
center_x = (free_left + free_right) / 2.0;
|
||||||
|
if galley.size().x > free_right - free_left {
|
||||||
|
let mut job =
|
||||||
|
egui::text::LayoutJob::simple_singleline(title_text, title_font, title_ink);
|
||||||
|
job.wrap =
|
||||||
|
egui::text::TextWrapping::truncate_at_width((free_right - free_left).max(0.0));
|
||||||
|
galley = painter.layout_job(job);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
painter.galley(
|
||||||
|
egui::pos2(
|
||||||
|
center_x - galley.size().x / 2.0,
|
||||||
|
title_rect.center().y - galley.size().y / 2.0,
|
||||||
|
),
|
||||||
|
galley,
|
||||||
|
title_ink,
|
||||||
);
|
);
|
||||||
|
|
||||||
ui.scope_builder(UiBuilder::new().max_rect(title_rect), |ui| {
|
ui.scope_builder(UiBuilder::new().max_rect(title_rect), |ui| {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
// Copyright 2023 The Grim Developers
|
// Copyright 2023 The Grim Developers
|
||||||
|
// Copyright 2026 The Goblin Developers
|
||||||
//
|
//
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
// you may not use this file except in compliance with the License.
|
// you may not use this file except in compliance with the License.
|
||||||
@@ -12,95 +13,39 @@
|
|||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
|
//! Legacy color API mapped onto the Goblin design tokens in [`crate::gui::theme`].
|
||||||
|
//! Existing call sites keep compiling; everything sources from the active theme.
|
||||||
|
|
||||||
use egui::Color32;
|
use egui::Color32;
|
||||||
|
|
||||||
use crate::AppConfig;
|
use crate::gui::theme;
|
||||||
|
|
||||||
/// Provides colors values based on current theme.
|
/// Provides color values based on the current theme tokens.
|
||||||
pub struct Colors;
|
pub struct Colors;
|
||||||
|
|
||||||
const WHITE: Color32 = Color32::from_gray(253);
|
|
||||||
const BLACK: Color32 = Color32::from_gray(12);
|
|
||||||
|
|
||||||
const SEMI_TRANSPARENT: Color32 = Color32::from_black_alpha(100);
|
const SEMI_TRANSPARENT: Color32 = Color32::from_black_alpha(100);
|
||||||
const DARK_SEMI_TRANSPARENT: Color32 = Color32::from_black_alpha(170);
|
const DARK_SEMI_TRANSPARENT: Color32 = Color32::from_black_alpha(170);
|
||||||
|
|
||||||
const GOLD: Color32 = Color32::from_rgb(255, 215, 0);
|
const INK: Color32 = Color32::from_rgb(0x0E, 0x0E, 0x0C);
|
||||||
const GOLD_DARK: Color32 = Color32::from_rgb(240, 203, 1);
|
const PAPER: Color32 = Color32::from_rgb(0xFA, 0xFA, 0xF7);
|
||||||
|
|
||||||
const YELLOW: Color32 = Color32::from_rgb(254, 241, 2);
|
fn dark_base() -> bool {
|
||||||
const YELLOW_DARK: Color32 = Color32::from_rgb(239, 229, 3);
|
theme::tokens().dark_base
|
||||||
|
|
||||||
const GREEN: Color32 = Color32::from_rgb(0, 0x64, 0);
|
|
||||||
const GREEN_DARK: Color32 = Color32::from_rgb(0, (0x64 as f32 * 1.3 + 0.5) as u8, 0);
|
|
||||||
|
|
||||||
const RED: Color32 = Color32::from_rgb(0x8B, 0, 0);
|
|
||||||
const RED_DARK: Color32 = Color32::from_rgb((0x8B as f32 * 1.3 + 0.5) as u8, 50, 30);
|
|
||||||
|
|
||||||
const BLUE: Color32 = Color32::from_rgb(0, 0x66, 0xE4);
|
|
||||||
const BLUE_DARK: Color32 = Color32::from_rgb(
|
|
||||||
0,
|
|
||||||
(0x66 as f32 * 1.3 + 0.5) as u8,
|
|
||||||
(0xE4 as f32 * 1.3 + 0.5) as u8,
|
|
||||||
);
|
|
||||||
|
|
||||||
const FILL: Color32 = Color32::from_gray(244);
|
|
||||||
const FILL_DARK: Color32 = Color32::from_gray(26);
|
|
||||||
|
|
||||||
const FILL_DEEP_DARK: Color32 = Color32::from_gray(32);
|
|
||||||
|
|
||||||
const FILL_LITE: Color32 = Color32::from_gray(249);
|
|
||||||
const FILL_LITE_DARK: Color32 = Color32::from_gray(21);
|
|
||||||
|
|
||||||
const TEXT: Color32 = Color32::from_gray(80);
|
|
||||||
const TEXT_DARK: Color32 = Color32::from_gray(185);
|
|
||||||
|
|
||||||
const CHECKBOX: Color32 = Color32::from_gray(100);
|
|
||||||
const CHECKBOX_DARK: Color32 = Color32::from_gray(175);
|
|
||||||
|
|
||||||
const TEXT_BUTTON: Color32 = Color32::from_gray(70);
|
|
||||||
const TEXT_BUTTON_DARK: Color32 = Color32::from_gray(195);
|
|
||||||
|
|
||||||
const TITLE: Color32 = Color32::from_gray(60);
|
|
||||||
const TITLE_DARK: Color32 = Color32::from_gray(205);
|
|
||||||
|
|
||||||
const GRAY: Color32 = Color32::from_gray(120);
|
|
||||||
const GRAY_DARK: Color32 = Color32::from_gray(145);
|
|
||||||
|
|
||||||
const STROKE_DARK: Color32 = Color32::from_gray(50);
|
|
||||||
|
|
||||||
const INACTIVE_TEXT: Color32 = Color32::from_gray(150);
|
|
||||||
const INACTIVE_TEXT_DARK: Color32 = Color32::from_gray(115);
|
|
||||||
|
|
||||||
const ITEM_BUTTON: Color32 = Color32::from_gray(90);
|
|
||||||
const ITEM_BUTTON_DARK: Color32 = Color32::from_gray(175);
|
|
||||||
|
|
||||||
const ITEM_STROKE: Color32 = Color32::from_gray(220);
|
|
||||||
const ITEM_STROKE_DARK: Color32 = Color32::from_gray(40);
|
|
||||||
|
|
||||||
const ITEM_HOVER: Color32 = Color32::from_gray(205);
|
|
||||||
const ITEM_HOVER_DARK: Color32 = Color32::from_gray(48);
|
|
||||||
|
|
||||||
/// Check if dark theme should be used.
|
|
||||||
fn use_dark() -> bool {
|
|
||||||
AppConfig::dark_theme().unwrap_or(false)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Colors {
|
impl Colors {
|
||||||
pub const FILL_DEEP: Color32 = Color32::from_gray(238);
|
pub const FILL_DEEP: Color32 = Color32::from_rgb(0xF2, 0xF1, 0xEC);
|
||||||
pub const TRANSPARENT: Color32 = Color32::from_rgba_premultiplied(0, 0, 0, 0);
|
pub const TRANSPARENT: Color32 = Color32::from_rgba_premultiplied(0, 0, 0, 0);
|
||||||
pub const STROKE: Color32 = Color32::from_gray(200);
|
pub const STROKE: Color32 = Color32::from_rgba_premultiplied(1, 1, 1, 20);
|
||||||
|
|
||||||
|
/// Ink when `true`, paper when `false` (theme aware: maps to text/bg).
|
||||||
pub fn white_or_black(black_in_white: bool) -> Color32 {
|
pub fn white_or_black(black_in_white: bool) -> Color32 {
|
||||||
if use_dark() {
|
let t = theme::tokens();
|
||||||
if black_in_white { WHITE } else { BLACK }
|
if black_in_white { t.text } else { t.bg }
|
||||||
} else {
|
|
||||||
if black_in_white { BLACK } else { WHITE }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn semi_transparent() -> Color32 {
|
pub fn semi_transparent() -> Color32 {
|
||||||
if use_dark() {
|
if dark_base() {
|
||||||
DARK_SEMI_TRANSPARENT
|
DARK_SEMI_TRANSPARENT
|
||||||
} else {
|
} else {
|
||||||
SEMI_TRANSPARENT
|
SEMI_TRANSPARENT
|
||||||
@@ -108,130 +53,120 @@ impl Colors {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn gold() -> Color32 {
|
pub fn gold() -> Color32 {
|
||||||
if use_dark() {
|
theme::tokens().accent
|
||||||
GOLD.gamma_multiply(0.9)
|
|
||||||
} else {
|
|
||||||
GOLD
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn gold_dark() -> Color32 {
|
pub fn gold_dark() -> Color32 {
|
||||||
if use_dark() {
|
theme::tokens().accent_dark
|
||||||
GOLD_DARK.gamma_multiply(0.9)
|
|
||||||
} else {
|
|
||||||
GOLD_DARK
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn yellow() -> Color32 {
|
pub fn yellow() -> Color32 {
|
||||||
YELLOW
|
theme::tokens().accent
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn yellow_dark() -> Color32 {
|
pub fn yellow_dark() -> Color32 {
|
||||||
YELLOW_DARK
|
theme::tokens().accent_dark
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Ink color to draw on top of accent fills.
|
||||||
|
pub fn accent_ink() -> Color32 {
|
||||||
|
theme::tokens().accent_ink
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn green() -> Color32 {
|
pub fn green() -> Color32 {
|
||||||
if use_dark() { GREEN_DARK } else { GREEN }
|
theme::tokens().pos
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn red() -> Color32 {
|
pub fn red() -> Color32 {
|
||||||
if use_dark() { RED_DARK } else { RED }
|
theme::tokens().neg
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Blueviolet ("tor purple") accent for the Tor-on privacy state.
|
||||||
|
pub fn tor_purple() -> Color32 {
|
||||||
|
theme::tokens().tor_purple
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn blue() -> Color32 {
|
pub fn blue() -> Color32 {
|
||||||
if use_dark() { BLUE_DARK } else { BLUE }
|
if dark_base() {
|
||||||
|
Color32::from_rgb(0x7B, 0xA7, 0xFF)
|
||||||
|
} else {
|
||||||
|
Color32::from_rgb(0x0E, 0x62, 0xD0)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn fill() -> Color32 {
|
pub fn fill() -> Color32 {
|
||||||
if use_dark() { FILL_DARK } else { FILL }
|
theme::tokens().bg
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn fill_deep() -> Color32 {
|
pub fn fill_deep() -> Color32 {
|
||||||
if use_dark() {
|
theme::tokens().surface2
|
||||||
FILL_DEEP_DARK
|
|
||||||
} else {
|
|
||||||
Self::FILL_DEEP
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn fill_lite() -> Color32 {
|
pub fn fill_lite() -> Color32 {
|
||||||
if use_dark() {
|
theme::tokens().surface
|
||||||
FILL_LITE_DARK
|
|
||||||
} else {
|
|
||||||
FILL_LITE
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn checkbox() -> Color32 {
|
pub fn checkbox() -> Color32 {
|
||||||
if use_dark() { CHECKBOX_DARK } else { CHECKBOX }
|
theme::tokens().text_dim
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn text(always_light: bool) -> Color32 {
|
pub fn text(always_light: bool) -> Color32 {
|
||||||
if use_dark() && !always_light {
|
if always_light {
|
||||||
TEXT_DARK
|
// Forced light-theme ink, used over always-light surfaces like QR cards.
|
||||||
|
Color32::from_rgb(0x6B, 0x6A, 0x63)
|
||||||
} else {
|
} else {
|
||||||
TEXT
|
theme::tokens().text_dim
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn text_button() -> Color32 {
|
pub fn text_button() -> Color32 {
|
||||||
if use_dark() {
|
theme::tokens().text
|
||||||
TEXT_BUTTON_DARK
|
|
||||||
} else {
|
|
||||||
TEXT_BUTTON
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn title(always_light: bool) -> Color32 {
|
pub fn title(always_light: bool) -> Color32 {
|
||||||
if use_dark() && !always_light {
|
if always_light {
|
||||||
TITLE_DARK
|
INK
|
||||||
} else {
|
} else {
|
||||||
TITLE
|
theme::tokens().text
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn gray() -> Color32 {
|
pub fn gray() -> Color32 {
|
||||||
if use_dark() { GRAY_DARK } else { GRAY }
|
theme::tokens().text_mute
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn stroke() -> Color32 {
|
pub fn stroke() -> Color32 {
|
||||||
if use_dark() {
|
theme::tokens().line
|
||||||
STROKE_DARK
|
|
||||||
} else {
|
|
||||||
Self::STROKE
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn inactive_text() -> Color32 {
|
pub fn inactive_text() -> Color32 {
|
||||||
if use_dark() {
|
theme::tokens().text_mute
|
||||||
INACTIVE_TEXT_DARK
|
|
||||||
} else {
|
|
||||||
INACTIVE_TEXT
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn item_button_text() -> Color32 {
|
pub fn item_button_text() -> Color32 {
|
||||||
if use_dark() {
|
theme::tokens().text_dim
|
||||||
ITEM_BUTTON_DARK
|
|
||||||
} else {
|
|
||||||
ITEM_BUTTON
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn item_stroke() -> Color32 {
|
pub fn item_stroke() -> Color32 {
|
||||||
if use_dark() {
|
theme::tokens().line
|
||||||
ITEM_STROKE_DARK
|
|
||||||
} else {
|
|
||||||
ITEM_STROKE
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn item_hover() -> Color32 {
|
pub fn item_hover() -> Color32 {
|
||||||
if use_dark() {
|
theme::tokens().hover
|
||||||
ITEM_HOVER_DARK
|
}
|
||||||
} else {
|
|
||||||
ITEM_HOVER
|
/// Positive amount color.
|
||||||
}
|
pub fn pos() -> Color32 {
|
||||||
|
theme::tokens().pos
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Always-dark ink (brand black).
|
||||||
|
pub const fn ink() -> Color32 {
|
||||||
|
INK
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Always-light paper (brand white).
|
||||||
|
pub const fn paper() -> Color32 {
|
||||||
|
PAPER
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ pub use app::App;
|
|||||||
mod colors;
|
mod colors;
|
||||||
pub use colors::Colors;
|
pub use colors::Colors;
|
||||||
|
|
||||||
|
pub mod theme;
|
||||||
|
|
||||||
pub mod icons;
|
pub mod icons;
|
||||||
pub mod platform;
|
pub mod platform;
|
||||||
pub mod views;
|
pub mod views;
|
||||||
|
|||||||
@@ -27,6 +27,11 @@ use winit::platform::android::activity::AndroidApp;
|
|||||||
|
|
||||||
use crate::gui::platform::PlatformCallbacks;
|
use crate::gui::platform::PlatformCallbacks;
|
||||||
|
|
||||||
|
/// How long a revealed secret (e.g. an nsec) may sit in the clipboard before it
|
||||||
|
/// is auto-cleared, if it is still there. Long enough to paste into another app,
|
||||||
|
/// short enough that it does not linger.
|
||||||
|
const CLIPBOARD_SECRET_TTL_SECS: u64 = 45;
|
||||||
|
|
||||||
/// Android platform implementation.
|
/// Android platform implementation.
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct Android {
|
pub struct Android {
|
||||||
@@ -40,6 +45,12 @@ pub struct Android {
|
|||||||
impl Android {
|
impl Android {
|
||||||
/// Create new Android platform instance from provided [`AndroidApp`].
|
/// Create new Android platform instance from provided [`AndroidApp`].
|
||||||
pub fn new(app: AndroidApp) -> Self {
|
pub fn new(app: AndroidApp) -> Self {
|
||||||
|
// Keep a process-wide handle so non-GUI threads (the nostr service)
|
||||||
|
// can reach Java too (see `notify_payment_received`).
|
||||||
|
{
|
||||||
|
let mut w_app = ANDROID_APP.write();
|
||||||
|
*w_app = Some(app.clone());
|
||||||
|
}
|
||||||
Self {
|
Self {
|
||||||
android_app: app,
|
android_app: app,
|
||||||
ctx: Arc::new(RwLock::new(None)),
|
ctx: Arc::new(RwLock::new(None)),
|
||||||
@@ -80,6 +91,21 @@ impl PlatformCallbacks for Android {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn copy_secret_to_buffer(&self, data: String) {
|
||||||
|
// Copy now, then clear after a delay if the clipboard still holds exactly
|
||||||
|
// this secret (compare-then-clear), so a revealed nsec does not linger.
|
||||||
|
// Runs on a detached thread that reaches Java via the cloned app handle,
|
||||||
|
// the same non-GUI JNI path as the payment notifications.
|
||||||
|
self.copy_string_to_buffer(data.clone());
|
||||||
|
let platform = self.clone();
|
||||||
|
std::thread::spawn(move || {
|
||||||
|
std::thread::sleep(std::time::Duration::from_secs(CLIPBOARD_SECRET_TTL_SECS));
|
||||||
|
if platform.get_string_from_buffer() == data {
|
||||||
|
platform.copy_string_to_buffer(String::new());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
fn get_string_from_buffer(&self) -> String {
|
fn get_string_from_buffer(&self) -> String {
|
||||||
let result = self
|
let result = self
|
||||||
.call_java_method("pasteText", "()Ljava/lang/String;", &[])
|
.call_java_method("pasteText", "()Ljava/lang/String;", &[])
|
||||||
@@ -158,6 +184,51 @@ impl PlatformCallbacks for Android {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn save_file(&self, name: String, data: Vec<u8>) -> Result<(), std::io::Error> {
|
||||||
|
// Stage the bytes in the share cache (same dir the FileProvider exposes),
|
||||||
|
// then let Java copy them to the user-chosen Storage Access Framework
|
||||||
|
// document. Mirrors `share_data`, but the Java side uses CREATE_DOCUMENT.
|
||||||
|
let default_cache = OsString::from(dirs::cache_dir().unwrap());
|
||||||
|
let mut file = PathBuf::from(env::var_os("XDG_CACHE_HOME").unwrap_or(default_cache));
|
||||||
|
file.push("share");
|
||||||
|
if !file.exists() {
|
||||||
|
std::fs::create_dir(file.clone())?;
|
||||||
|
}
|
||||||
|
file.push(&name);
|
||||||
|
if file.exists() {
|
||||||
|
std::fs::remove_file(file.clone())?;
|
||||||
|
}
|
||||||
|
let mut f = File::create_new(file.clone())?;
|
||||||
|
f.write_all(data.as_slice())?;
|
||||||
|
f.sync_all()?;
|
||||||
|
let vm = unsafe { jni::JavaVM::from_raw(self.android_app.vm_as_ptr() as _) }.unwrap();
|
||||||
|
let env = vm.attach_current_thread().unwrap();
|
||||||
|
let path_arg = env.new_string(file.to_str().unwrap()).unwrap();
|
||||||
|
let name_arg = env.new_string(&name).unwrap();
|
||||||
|
let _ = self.call_java_method(
|
||||||
|
"saveFile",
|
||||||
|
"(Ljava/lang/String;Ljava/lang/String;)V",
|
||||||
|
&[
|
||||||
|
JValue::Object(&JObject::from(path_arg)),
|
||||||
|
JValue::Object(&JObject::from(name_arg)),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn share_text(&self, text: String) {
|
||||||
|
let vm = unsafe { jni::JavaVM::from_raw(self.android_app.vm_as_ptr() as _) }.unwrap();
|
||||||
|
let env = vm.attach_current_thread().unwrap();
|
||||||
|
let Ok(arg_value) = env.new_string(text) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let _ = self.call_java_method(
|
||||||
|
"shareText",
|
||||||
|
"(Ljava/lang/String;)V",
|
||||||
|
&[JValue::Object(&JObject::from(arg_value))],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
fn pick_file(&self) -> Option<String> {
|
fn pick_file(&self) -> Option<String> {
|
||||||
// Clear previous result.
|
// Clear previous result.
|
||||||
let mut w_path = PICKED_FILE_PATH.write();
|
let mut w_path = PICKED_FILE_PATH.write();
|
||||||
@@ -199,6 +270,28 @@ impl PlatformCallbacks for Android {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn clear_user_attention(&self) {}
|
fn clear_user_attention(&self) {}
|
||||||
|
|
||||||
|
fn set_status_bar_white_icons(&self, white: bool) {
|
||||||
|
self.call_java_method(
|
||||||
|
"setStatusBarWhiteIcons",
|
||||||
|
"(Z)V",
|
||||||
|
&[JValue::Bool(white as u8)],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn vibrate_error(&self) {
|
||||||
|
let _ = self.call_java_method("vibrateError", "()V", &[]);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn vibrate_copy(&self) {
|
||||||
|
let _ = self.call_java_method("vibrateCopy", "()V", &[]);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn return_to_caller(&self) {
|
||||||
|
// Background our task (Activity.moveTaskToBack(true), wrapped Java-side)
|
||||||
|
// so the OS brings the app that deep-linked into us back to the front.
|
||||||
|
let _ = self.call_java_method("returnToCaller", "()V", &[]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
lazy_static! {
|
lazy_static! {
|
||||||
@@ -206,6 +299,89 @@ lazy_static! {
|
|||||||
static ref LAST_CAMERA_IMAGE: Arc<RwLock<Option<(Vec<u8>, u32)>>> = Arc::new(RwLock::new(None));
|
static ref LAST_CAMERA_IMAGE: Arc<RwLock<Option<(Vec<u8>, u32)>>> = Arc::new(RwLock::new(None));
|
||||||
/// Picked file path.
|
/// Picked file path.
|
||||||
static ref PICKED_FILE_PATH: Arc<RwLock<Option<String>>> = Arc::new(RwLock::new(None));
|
static ref PICKED_FILE_PATH: Arc<RwLock<Option<String>>> = Arc::new(RwLock::new(None));
|
||||||
|
/// App handle for JNI calls from threads without a platform reference.
|
||||||
|
static ref ANDROID_APP: Arc<RwLock<Option<AndroidApp>>> = Arc::new(RwLock::new(None));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Show the one-shot "payment received" system notification (Java side
|
||||||
|
/// `BackgroundService.notifyPaymentReceived`, id=2, separate from the
|
||||||
|
/// persistent sync notification id=1). Called by the nostr service on
|
||||||
|
/// slatepack receipt from a non-GUI thread, hence the stored [`AndroidApp`]
|
||||||
|
/// handle instead of a platform reference. Fail-open: a missing handle or
|
||||||
|
/// JNI error just skips the notification, never the payment.
|
||||||
|
pub fn notify_payment_received(name: &str, amount: &str) {
|
||||||
|
let app = {
|
||||||
|
let r_app = ANDROID_APP.read();
|
||||||
|
r_app.clone()
|
||||||
|
};
|
||||||
|
let Some(app) = app else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let platform = Android {
|
||||||
|
android_app: app,
|
||||||
|
ctx: Arc::new(RwLock::new(None)),
|
||||||
|
};
|
||||||
|
let Ok(vm) = (unsafe { jni::JavaVM::from_raw(platform.android_app.vm_as_ptr() as _) }) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Ok(env) = vm.attach_current_thread() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Ok(j_name) = env.new_string(name) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Ok(j_amount) = env.new_string(amount) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let _ = platform.call_java_method(
|
||||||
|
"notifyPaymentReceived",
|
||||||
|
"(Ljava/lang/String;Ljava/lang/String;)V",
|
||||||
|
&[
|
||||||
|
JValue::Object(&JObject::from(j_name)),
|
||||||
|
JValue::Object(&JObject::from(j_amount)),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Show the one-shot "payment requested" system notification (Java side
|
||||||
|
/// `BackgroundService.notifyPaymentRequested`, id=3, separate from both the
|
||||||
|
/// persistent sync notification id=1 and the received-payment one id=2). Called
|
||||||
|
/// by the nostr service when a payment request (Invoice1) is ingested from a
|
||||||
|
/// non-GUI thread, hence the stored [`AndroidApp`] handle instead of a platform
|
||||||
|
/// reference. Fail-open: a missing handle or JNI error just skips the
|
||||||
|
/// notification, never the request. Mirrors [`notify_payment_received`].
|
||||||
|
pub fn notify_payment_requested(name: &str, amount: &str) {
|
||||||
|
let app = {
|
||||||
|
let r_app = ANDROID_APP.read();
|
||||||
|
r_app.clone()
|
||||||
|
};
|
||||||
|
let Some(app) = app else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let platform = Android {
|
||||||
|
android_app: app,
|
||||||
|
ctx: Arc::new(RwLock::new(None)),
|
||||||
|
};
|
||||||
|
let Ok(vm) = (unsafe { jni::JavaVM::from_raw(platform.android_app.vm_as_ptr() as _) }) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Ok(env) = vm.attach_current_thread() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Ok(j_name) = env.new_string(name) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Ok(j_amount) = env.new_string(amount) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let _ = platform.call_java_method(
|
||||||
|
"notifyPaymentRequested",
|
||||||
|
"(Ljava/lang/String;Ljava/lang/String;)V",
|
||||||
|
&[
|
||||||
|
JValue::Object(&JObject::from(j_name)),
|
||||||
|
JValue::Object(&JObject::from(j_amount)),
|
||||||
|
],
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Callback from Java code with last entered character from soft keyboard.
|
/// Callback from Java code with last entered character from soft keyboard.
|
||||||
|
|||||||
@@ -24,6 +24,11 @@ use std::thread;
|
|||||||
|
|
||||||
use crate::gui::platform::PlatformCallbacks;
|
use crate::gui::platform::PlatformCallbacks;
|
||||||
|
|
||||||
|
/// How long a revealed secret (e.g. an nsec) may sit in the clipboard before it
|
||||||
|
/// is auto-cleared, if it is still there. Long enough to paste into another app,
|
||||||
|
/// short enough that it does not linger.
|
||||||
|
const CLIPBOARD_SECRET_TTL_SECS: u64 = 45;
|
||||||
|
|
||||||
/// Desktop platform related actions.
|
/// Desktop platform related actions.
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct Desktop {
|
pub struct Desktop {
|
||||||
@@ -39,6 +44,17 @@ pub struct Desktop {
|
|||||||
|
|
||||||
/// Flag to check if attention required after window focusing.
|
/// Flag to check if attention required after window focusing.
|
||||||
attention_required: Arc<AtomicBool>,
|
attention_required: Arc<AtomicBool>,
|
||||||
|
|
||||||
|
/// Long-lived clipboard owner. On Linux (X11 AND Wayland) the clipboard
|
||||||
|
/// selection is owned by the live `arboard::Clipboard` instance: the prior
|
||||||
|
/// code created a fresh instance per call and dropped it the moment the
|
||||||
|
/// function returned, so the selection ownership was released immediately and
|
||||||
|
/// copied text (e.g. a recovery phrase) vanished before it could be pasted —
|
||||||
|
/// the "Paste does nothing on desktop" bug. Keeping ONE instance alive for
|
||||||
|
/// the app lifetime makes our process the durable selection owner, so a copy
|
||||||
|
/// survives long enough to paste (in-app or into another window). Held behind
|
||||||
|
/// a Mutex because the trait methods take `&self` and arboard's take `&mut`.
|
||||||
|
clipboard: Arc<parking_lot::Mutex<Option<arboard::Clipboard>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Desktop {
|
impl Desktop {
|
||||||
@@ -49,9 +65,28 @@ impl Desktop {
|
|||||||
camera_index: Arc::new(AtomicUsize::new(0)),
|
camera_index: Arc::new(AtomicUsize::new(0)),
|
||||||
stop_camera: Arc::new(AtomicBool::new(false)),
|
stop_camera: Arc::new(AtomicBool::new(false)),
|
||||||
attention_required: Arc::new(AtomicBool::new(false)),
|
attention_required: Arc::new(AtomicBool::new(false)),
|
||||||
|
clipboard: Arc::new(parking_lot::Mutex::new(None)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Run `f` against the process-wide, lazily-created clipboard instance,
|
||||||
|
/// returning `default` if the clipboard backend can't be opened. Reusing one
|
||||||
|
/// instance (rather than `Clipboard::new()` per call) is what keeps a copied
|
||||||
|
/// selection alive on Linux — see the `clipboard` field.
|
||||||
|
fn with_clipboard<R>(&self, f: impl FnOnce(&mut arboard::Clipboard) -> R, default: R) -> R {
|
||||||
|
let mut guard = self.clipboard.lock();
|
||||||
|
if guard.is_none() {
|
||||||
|
match arboard::Clipboard::new() {
|
||||||
|
Ok(c) => *guard = Some(c),
|
||||||
|
Err(e) => {
|
||||||
|
log::error!("clipboard: failed to open: {e}");
|
||||||
|
return default;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
f(guard.as_mut().unwrap())
|
||||||
|
}
|
||||||
|
|
||||||
// #[allow(dead_code)]
|
// #[allow(dead_code)]
|
||||||
#[cfg(not(target_os = "macos"))]
|
#[cfg(not(target_os = "macos"))]
|
||||||
fn start_camera_capture(
|
fn start_camera_capture(
|
||||||
@@ -62,17 +97,22 @@ impl Desktop {
|
|||||||
use nokhwa::Camera;
|
use nokhwa::Camera;
|
||||||
use nokhwa::pixel_format::RgbFormat;
|
use nokhwa::pixel_format::RgbFormat;
|
||||||
use nokhwa::utils::ApiBackend;
|
use nokhwa::utils::ApiBackend;
|
||||||
use nokhwa::utils::{CameraIndex, RequestedFormat, RequestedFormatType};
|
use nokhwa::utils::{FrameFormat, RequestedFormat, RequestedFormatType};
|
||||||
|
|
||||||
let devices = nokhwa::query(ApiBackend::Auto).unwrap();
|
|
||||||
cameras_amount.store(devices.len(), Ordering::Relaxed);
|
|
||||||
let index = camera_index.load(Ordering::Relaxed);
|
|
||||||
if devices.is_empty() || index >= devices.len() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
thread::spawn(move || {
|
thread::spawn(move || {
|
||||||
let index = CameraIndex::Index(camera_index.load(Ordering::Relaxed) as u32);
|
// Device enumeration does IO — keep it off the UI thread, and
|
||||||
|
// treat a backend error the same as "no cameras".
|
||||||
|
let devices = nokhwa::query(ApiBackend::Auto).unwrap_or_default();
|
||||||
|
cameras_amount.store(devices.len(), Ordering::Relaxed);
|
||||||
|
let index = camera_index.load(Ordering::Relaxed);
|
||||||
|
if devices.is_empty() || index >= devices.len() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open by the enumerated device's own index, not the list
|
||||||
|
// position: on v4l they differ whenever /dev/video0 is absent
|
||||||
|
// (first camera at video1, loopback-only setups, …).
|
||||||
|
let index = devices[index].index().clone();
|
||||||
let requested =
|
let requested =
|
||||||
RequestedFormat::new::<RgbFormat>(RequestedFormatType::AbsoluteHighestFrameRate);
|
RequestedFormat::new::<RgbFormat>(RequestedFormatType::AbsoluteHighestFrameRate);
|
||||||
// Create and open camera.
|
// Create and open camera.
|
||||||
@@ -89,9 +129,29 @@ impl Desktop {
|
|||||||
}
|
}
|
||||||
// Get a frame.
|
// Get a frame.
|
||||||
if let Ok(frame) = camera.frame() {
|
if let Ok(frame) = camera.frame() {
|
||||||
// Save image.
|
// Consumers expect an encoded image. MJPEG frames
|
||||||
let mut w_image = LAST_CAMERA_IMAGE.write();
|
// already are; anything else (YUYV, NV12, …) must
|
||||||
*w_image = Some((frame.buffer().to_vec(), 0));
|
// be decoded to RGB and re-encoded, or the readers
|
||||||
|
// fail on the raw buffer and show a spinner forever.
|
||||||
|
let bytes = if frame.source_frame_format() == FrameFormat::MJPEG {
|
||||||
|
Some(frame.buffer().to_vec())
|
||||||
|
} else if let Ok(image) = frame.decode_image::<RgbFormat>() {
|
||||||
|
let mut bytes: Vec<u8> = Vec::new();
|
||||||
|
image
|
||||||
|
.write_to(
|
||||||
|
&mut std::io::Cursor::new(&mut bytes),
|
||||||
|
image::ImageFormat::Jpeg,
|
||||||
|
)
|
||||||
|
.ok()
|
||||||
|
.map(|_| bytes)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
if let Some(bytes) = bytes {
|
||||||
|
// Save image.
|
||||||
|
let mut w_image = LAST_CAMERA_IMAGE.write();
|
||||||
|
*w_image = Some((bytes, 0));
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// Clear image.
|
// Clear image.
|
||||||
let mut w_image = LAST_CAMERA_IMAGE.write();
|
let mut w_image = LAST_CAMERA_IMAGE.write();
|
||||||
@@ -99,7 +159,7 @@ impl Desktop {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
camera.stop_stream().unwrap();
|
let _ = camera.stop_stream();
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -186,13 +246,53 @@ impl PlatformCallbacks for Desktop {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn copy_string_to_buffer(&self, data: String) {
|
fn copy_string_to_buffer(&self, data: String) {
|
||||||
let mut clipboard = arboard::Clipboard::new().unwrap();
|
// Reuse the long-lived instance so the selection survives the call (see
|
||||||
clipboard.set_text(data).unwrap();
|
// the `clipboard` field). A backend error is logged, never a panic — a
|
||||||
|
// failed copy must not crash the wallet.
|
||||||
|
self.with_clipboard(
|
||||||
|
|clipboard| {
|
||||||
|
if let Err(e) = clipboard.set_text(data) {
|
||||||
|
log::error!("clipboard: set_text failed: {e}");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn copy_secret_to_buffer(&self, data: String) {
|
||||||
|
// Copy now, then clear after a delay, but only if the clipboard still
|
||||||
|
// holds exactly this secret, so we never clobber whatever the user copied
|
||||||
|
// since. A revealed nsec must not sit in the clipboard forever.
|
||||||
|
self.copy_string_to_buffer(data.clone());
|
||||||
|
let clip = self.clipboard.clone();
|
||||||
|
thread::spawn(move || {
|
||||||
|
thread::sleep(std::time::Duration::from_secs(CLIPBOARD_SECRET_TTL_SECS));
|
||||||
|
let mut guard = clip.lock();
|
||||||
|
// Open the shared instance lazily (mirrors `with_clipboard`); clearing
|
||||||
|
// through the long-lived owner is what actually relinquishes the
|
||||||
|
// selection on Linux.
|
||||||
|
if guard.is_none() {
|
||||||
|
match arboard::Clipboard::new() {
|
||||||
|
Ok(c) => *guard = Some(c),
|
||||||
|
Err(e) => {
|
||||||
|
log::error!("clipboard: failed to open for secret clear: {e}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(clipboard) = guard.as_mut()
|
||||||
|
&& clipboard.get_text().map(|t| t == data).unwrap_or(false)
|
||||||
|
{
|
||||||
|
let _ = clipboard.set_text(String::new());
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_string_from_buffer(&self) -> String {
|
fn get_string_from_buffer(&self) -> String {
|
||||||
let mut clipboard = arboard::Clipboard::new().unwrap();
|
self.with_clipboard(
|
||||||
clipboard.get_text().unwrap_or("".to_string())
|
|clipboard| clipboard.get_text().unwrap_or_default(),
|
||||||
|
String::new(),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn start_camera(&self) {
|
fn start_camera(&self) {
|
||||||
@@ -267,6 +367,15 @@ impl PlatformCallbacks for Desktop {
|
|||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn pick_image_file(&self) -> Option<String> {
|
||||||
|
let file = FileDialog::new()
|
||||||
|
.set_title(t!("choose_file"))
|
||||||
|
.add_filter("Images", &["png", "jpg", "jpeg", "webp"])
|
||||||
|
.set_directory(dirs::home_dir().unwrap())
|
||||||
|
.pick_file();
|
||||||
|
file.and_then(|f| f.to_str().map(|s| s.to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
fn pick_folder(&self) -> Option<String> {
|
fn pick_folder(&self) -> Option<String> {
|
||||||
let file = FileDialog::new()
|
let file = FileDialog::new()
|
||||||
.set_title(t!("choose_folder"))
|
.set_title(t!("choose_folder"))
|
||||||
@@ -325,3 +434,28 @@ lazy_static! {
|
|||||||
/// Last captured image from started camera.
|
/// Last captured image from started camera.
|
||||||
static ref LAST_CAMERA_IMAGE: Arc<RwLock<Option<(Vec<u8>, u32)>>> = Arc::new(RwLock::new(None));
|
static ref LAST_CAMERA_IMAGE: Arc<RwLock<Option<(Vec<u8>, u32)>>> = Arc::new(RwLock::new(None));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod clipboard_tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::gui::platform::PlatformCallbacks;
|
||||||
|
|
||||||
|
/// Round-trips a copy then paste through the REAL `Desktop` platform impl on
|
||||||
|
/// the live session clipboard. Ignored by default (needs a display/clipboard
|
||||||
|
/// backend); run manually with a Wayland/X11 session:
|
||||||
|
/// cargo test --lib clipboard_roundtrip -- --ignored --nocapture
|
||||||
|
/// Proves the persistent-instance fix: with the old fresh-instance-per-call
|
||||||
|
/// pattern this read back empty on Wayland/X11.
|
||||||
|
#[test]
|
||||||
|
#[ignore]
|
||||||
|
fn clipboard_roundtrip() {
|
||||||
|
let d = Desktop::new();
|
||||||
|
let phrase = "abandon ability able about above absent absorb abstract \
|
||||||
|
absurd abuse access accident account accuse achieve acid acoustic \
|
||||||
|
acquire across act action actor actress";
|
||||||
|
d.copy_string_to_buffer(phrase.to_string());
|
||||||
|
std::thread::sleep(std::time::Duration::from_millis(200));
|
||||||
|
let got = d.get_string_from_buffer();
|
||||||
|
assert_eq!(got, phrase, "clipboard round-trip lost the copied text");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -25,6 +25,14 @@ pub trait PlatformCallbacks {
|
|||||||
fn set_context(&mut self, ctx: &egui::Context);
|
fn set_context(&mut self, ctx: &egui::Context);
|
||||||
fn exit(&self);
|
fn exit(&self);
|
||||||
fn copy_string_to_buffer(&self, data: String);
|
fn copy_string_to_buffer(&self, data: String);
|
||||||
|
/// Copy a SECRET (e.g. a revealed nsec) to the clipboard, then auto-clear it
|
||||||
|
/// after a short delay so it does not linger there forever. The clear is
|
||||||
|
/// compare-then-clear: it only wipes the clipboard if it STILL holds exactly
|
||||||
|
/// this secret, so whatever the user copied since is never clobbered.
|
||||||
|
/// Defaults to a plain copy on platforms without a timed clear.
|
||||||
|
fn copy_secret_to_buffer(&self, data: String) {
|
||||||
|
self.copy_string_to_buffer(data);
|
||||||
|
}
|
||||||
fn get_string_from_buffer(&self) -> String;
|
fn get_string_from_buffer(&self) -> String;
|
||||||
fn start_camera(&self);
|
fn start_camera(&self);
|
||||||
fn stop_camera(&self);
|
fn stop_camera(&self);
|
||||||
@@ -32,10 +40,48 @@ pub trait PlatformCallbacks {
|
|||||||
fn can_switch_camera(&self) -> bool;
|
fn can_switch_camera(&self) -> bool;
|
||||||
fn switch_camera(&self);
|
fn switch_camera(&self);
|
||||||
fn share_data(&self, name: String, data: Vec<u8>) -> Result<(), std::io::Error>;
|
fn share_data(&self, name: String, data: Vec<u8>) -> Result<(), std::io::Error>;
|
||||||
|
|
||||||
|
/// Save bytes to a user-chosen location on the device (a "save as" dialog).
|
||||||
|
/// Desktop already does this via `share_data` (rfd save dialog); Android
|
||||||
|
/// overrides to use the Storage Access Framework (ACTION_CREATE_DOCUMENT)
|
||||||
|
/// instead of the share sheet.
|
||||||
|
fn save_file(&self, name: String, data: Vec<u8>) -> Result<(), std::io::Error> {
|
||||||
|
self.share_data(name, data)
|
||||||
|
}
|
||||||
|
/// Share plain text via the platform's native share sheet (e.g. a payment
|
||||||
|
/// link). Defaults to copying to the clipboard on platforms without a share
|
||||||
|
/// sheet (desktop).
|
||||||
|
fn share_text(&self, text: String) {
|
||||||
|
self.copy_string_to_buffer(text);
|
||||||
|
}
|
||||||
fn pick_file(&self) -> Option<String>;
|
fn pick_file(&self) -> Option<String>;
|
||||||
|
/// Native picker filtered to picture files; defaults to the plain picker
|
||||||
|
/// on platforms without filter support (magic-byte sniffing protects).
|
||||||
|
fn pick_image_file(&self) -> Option<String> {
|
||||||
|
self.pick_file()
|
||||||
|
}
|
||||||
fn pick_folder(&self) -> Option<String>;
|
fn pick_folder(&self) -> Option<String>;
|
||||||
fn picked_file(&self) -> Option<String>;
|
fn picked_file(&self) -> Option<String>;
|
||||||
fn request_user_attention(&self);
|
fn request_user_attention(&self);
|
||||||
fn user_attention_required(&self) -> bool;
|
fn user_attention_required(&self) -> bool;
|
||||||
fn clear_user_attention(&self);
|
fn clear_user_attention(&self);
|
||||||
|
|
||||||
|
/// Set the status-bar icon color to contrast the current theme. `white` =
|
||||||
|
/// light icons (for a dark background). No-op off Android.
|
||||||
|
fn set_status_bar_white_icons(&self, _white: bool) {}
|
||||||
|
|
||||||
|
/// Play a short "error" haptic (e.g. a rejected over-balance payment).
|
||||||
|
/// No-op off Android.
|
||||||
|
fn vibrate_error(&self) {}
|
||||||
|
|
||||||
|
/// Play a tiny "tick" haptic confirming a successful copy. No-op off Android.
|
||||||
|
fn vibrate_copy(&self) {}
|
||||||
|
|
||||||
|
/// Return the user to the app that deep-linked into Goblin (the same-device
|
||||||
|
/// app-switch flow: a browser hands off a `goblin:login`/`goblin:authorize`
|
||||||
|
/// URI, we sign, then hand focus back). On Android this backgrounds our task
|
||||||
|
/// so the OS surfaces the previous app (the polling browser tab). Best-effort
|
||||||
|
/// and a no-op off Android, where there is no clean "return focus" primitive
|
||||||
|
/// (egui/winit); desktop leaves the window where it is.
|
||||||
|
fn return_to_caller(&self) {}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,370 @@
|
|||||||
|
// Copyright 2026 The Goblin Developers
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
//! Goblin design tokens: three themes (light/dark/yellow) and density scales,
|
||||||
|
//! taken verbatim from the Goblin design handoff.
|
||||||
|
|
||||||
|
use std::cell::Cell;
|
||||||
|
|
||||||
|
use egui::Color32;
|
||||||
|
|
||||||
|
use crate::AppConfig;
|
||||||
|
|
||||||
|
/// Available color themes.
|
||||||
|
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||||
|
pub enum ThemeKind {
|
||||||
|
Light,
|
||||||
|
Dark,
|
||||||
|
Yellow,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ThemeKind {
|
||||||
|
pub fn id(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
ThemeKind::Light => "light",
|
||||||
|
ThemeKind::Dark => "dark",
|
||||||
|
ThemeKind::Yellow => "yellow",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_id(id: &str) -> Option<ThemeKind> {
|
||||||
|
match id {
|
||||||
|
"light" => Some(ThemeKind::Light),
|
||||||
|
"dark" => Some(ThemeKind::Dark),
|
||||||
|
"yellow" => Some(ThemeKind::Yellow),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Color tokens for a theme.
|
||||||
|
pub struct ThemeTokens {
|
||||||
|
pub bg: Color32,
|
||||||
|
pub surface: Color32,
|
||||||
|
pub surface2: Color32,
|
||||||
|
pub text: Color32,
|
||||||
|
pub text_dim: Color32,
|
||||||
|
pub text_mute: Color32,
|
||||||
|
/// Text on surface/surface2 fills. Matches `text` in light/dark, but the
|
||||||
|
/// yellow theme has dark surfaces on a bright bg, so on-surface text must
|
||||||
|
/// be light there while `text` stays dark for the bg.
|
||||||
|
pub surface_text: Color32,
|
||||||
|
pub surface_text_dim: Color32,
|
||||||
|
pub surface_text_mute: Color32,
|
||||||
|
pub line: Color32,
|
||||||
|
pub accent: Color32,
|
||||||
|
pub accent_dark: Color32,
|
||||||
|
pub accent_ink: Color32,
|
||||||
|
pub pos: Color32,
|
||||||
|
pub neg: Color32,
|
||||||
|
/// Blueviolet ("tor purple") accent for the Tor-on privacy state (CSS
|
||||||
|
/// `blueviolet` = #8A2BE2). Carries the "private traffic on" color on the
|
||||||
|
/// network-privacy status dots and the active big Tor switch.
|
||||||
|
pub tor_purple: Color32,
|
||||||
|
pub chip: Color32,
|
||||||
|
pub hover: Color32,
|
||||||
|
/// Avatar background palette (initial ink picked by luminance).
|
||||||
|
pub avatar_pairs: [(Color32, Color32); 8],
|
||||||
|
/// Whether egui widgets should use the dark base style.
|
||||||
|
pub dark_base: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Avatar (background, ink) pairs shared by all themes — bright pastels
|
||||||
|
/// carry dark ink, saturated darks carry light ink.
|
||||||
|
const AVATAR_PAIRS: [(Color32, Color32); 8] = [
|
||||||
|
(
|
||||||
|
Color32::from_rgb(0xFF, 0xD6, 0x0A),
|
||||||
|
Color32::from_rgb(0x0E, 0x0E, 0x0C),
|
||||||
|
), // accent yellow / ink
|
||||||
|
(
|
||||||
|
Color32::from_rgb(0xFF, 0x8E, 0x3C),
|
||||||
|
Color32::from_rgb(0x26, 0x10, 0x02),
|
||||||
|
), // orange / deep brown
|
||||||
|
(
|
||||||
|
Color32::from_rgb(0x5B, 0xD2, 0x7A),
|
||||||
|
Color32::from_rgb(0x0E, 0x0E, 0x0C),
|
||||||
|
), // light green / black
|
||||||
|
(
|
||||||
|
Color32::from_rgb(0x7B, 0xA7, 0xFF),
|
||||||
|
Color32::from_rgb(0x0B, 0x14, 0x33),
|
||||||
|
), // periwinkle / navy ink
|
||||||
|
(
|
||||||
|
Color32::from_rgb(0x6B, 0x4F, 0xC8),
|
||||||
|
Color32::from_rgb(0xF4, 0xF0, 0xFF),
|
||||||
|
), // purple / light text
|
||||||
|
(
|
||||||
|
Color32::from_rgb(0xE1, 0x74, 0xD0),
|
||||||
|
Color32::from_rgb(0x32, 0x07, 0x2B),
|
||||||
|
), // pink / dark plum
|
||||||
|
(
|
||||||
|
Color32::from_rgb(0x1F, 0x7A, 0x5C),
|
||||||
|
Color32::from_rgb(0xE7, 0xFF, 0xF4),
|
||||||
|
), // deep teal / light mint
|
||||||
|
(
|
||||||
|
Color32::from_rgb(0xA0, 0xE6, 0x6E),
|
||||||
|
Color32::from_rgb(0x14, 0x22, 0x0A),
|
||||||
|
), // lime / dark moss
|
||||||
|
];
|
||||||
|
|
||||||
|
/// The Goblin brand yellow (#FFD60A). Unlike [`ThemeTokens::accent`] (which
|
||||||
|
/// inverts to dark on the yellow theme so it reads on a bright bg), this is the
|
||||||
|
/// same brand yellow in every theme — used for status dots that must always
|
||||||
|
/// carry the brand color, e.g. the always-direct Grin-node dot.
|
||||||
|
pub const GOBLIN_YELLOW: Color32 = Color32::from_rgb(0xFF, 0xD6, 0x0A);
|
||||||
|
|
||||||
|
pub const LIGHT: ThemeTokens = ThemeTokens {
|
||||||
|
bg: Color32::from_rgb(0xFA, 0xFA, 0xF7),
|
||||||
|
surface: Color32::from_rgb(0xFF, 0xFF, 0xFF),
|
||||||
|
surface2: Color32::from_rgb(0xF2, 0xF1, 0xEC),
|
||||||
|
text: Color32::from_rgb(0x0E, 0x0E, 0x0C),
|
||||||
|
text_dim: Color32::from_rgb(0x6B, 0x6A, 0x63),
|
||||||
|
text_mute: Color32::from_rgb(0xA6, 0xA3, 0x9B),
|
||||||
|
surface_text: Color32::from_rgb(0x0E, 0x0E, 0x0C),
|
||||||
|
surface_text_dim: Color32::from_rgb(0x6B, 0x6A, 0x63),
|
||||||
|
surface_text_mute: Color32::from_rgb(0xA6, 0xA3, 0x9B),
|
||||||
|
// rgba(14,14,12,0.08) premultiplied.
|
||||||
|
line: Color32::from_rgba_premultiplied(1, 1, 1, 20),
|
||||||
|
accent: Color32::from_rgb(0xFF, 0xD6, 0x0A),
|
||||||
|
accent_dark: Color32::from_rgb(0xEF, 0xC8, 0x00),
|
||||||
|
accent_ink: Color32::from_rgb(0x0E, 0x0E, 0x0C),
|
||||||
|
pos: Color32::from_rgb(0x0E, 0x7C, 0x3A),
|
||||||
|
neg: Color32::from_rgb(0xB0, 0x48, 0x1E),
|
||||||
|
tor_purple: Color32::from_rgb(0x8A, 0x2B, 0xE2),
|
||||||
|
chip: Color32::from_rgb(0xF2, 0xF1, 0xEC),
|
||||||
|
hover: Color32::from_rgb(0xE9, 0xE7, 0xE0),
|
||||||
|
avatar_pairs: AVATAR_PAIRS,
|
||||||
|
dark_base: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const DARK: ThemeTokens = ThemeTokens {
|
||||||
|
bg: Color32::from_rgb(0x0E, 0x0E, 0x0C),
|
||||||
|
surface: Color32::from_rgb(0x1A, 0x1A, 0x17),
|
||||||
|
surface2: Color32::from_rgb(0x24, 0x24, 0x20),
|
||||||
|
text: Color32::from_rgb(0xFA, 0xFA, 0xF7),
|
||||||
|
text_dim: Color32::from_rgb(0x9A, 0x98, 0x8F),
|
||||||
|
text_mute: Color32::from_rgb(0x60, 0x5E, 0x58),
|
||||||
|
surface_text: Color32::from_rgb(0xFA, 0xFA, 0xF7),
|
||||||
|
surface_text_dim: Color32::from_rgb(0x9A, 0x98, 0x8F),
|
||||||
|
surface_text_mute: Color32::from_rgb(0x60, 0x5E, 0x58),
|
||||||
|
// rgba(255,255,255,0.08) premultiplied.
|
||||||
|
line: Color32::from_rgba_premultiplied(20, 20, 20, 20),
|
||||||
|
accent: Color32::from_rgb(0xFF, 0xD6, 0x0A),
|
||||||
|
accent_dark: Color32::from_rgb(0xEF, 0xC8, 0x00),
|
||||||
|
accent_ink: Color32::from_rgb(0x0E, 0x0E, 0x0C),
|
||||||
|
pos: Color32::from_rgb(0x5B, 0xD2, 0x7A),
|
||||||
|
neg: Color32::from_rgb(0xFF, 0x8B, 0x5E),
|
||||||
|
tor_purple: Color32::from_rgb(0x8A, 0x2B, 0xE2),
|
||||||
|
chip: Color32::from_rgb(0x24, 0x24, 0x20),
|
||||||
|
hover: Color32::from_rgb(0x2E, 0x2E, 0x29),
|
||||||
|
avatar_pairs: AVATAR_PAIRS,
|
||||||
|
dark_base: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const YELLOW: ThemeTokens = ThemeTokens {
|
||||||
|
bg: Color32::from_rgb(0xFF, 0xD6, 0x0A),
|
||||||
|
surface: Color32::from_rgb(0x0E, 0x0E, 0x0C),
|
||||||
|
surface2: Color32::from_rgb(0x1A, 0x1A, 0x17),
|
||||||
|
text: Color32::from_rgb(0x0E, 0x0E, 0x0C),
|
||||||
|
text_dim: Color32::from_rgb(0x3A, 0x3A, 0x36),
|
||||||
|
// Muted on-bg tier darkened for the bright yellow bg: #6B6A63 was only
|
||||||
|
// 3.85:1 (sub-WCAG-AA); #55534A is 5.5:1 and still the faintest tier.
|
||||||
|
text_mute: Color32::from_rgb(0x55, 0x53, 0x4A),
|
||||||
|
surface_text: Color32::from_rgb(0xFA, 0xFA, 0xF7),
|
||||||
|
surface_text_dim: Color32::from_rgb(0x9A, 0x98, 0x8F),
|
||||||
|
surface_text_mute: Color32::from_rgb(0x60, 0x5E, 0x58),
|
||||||
|
// rgba(14,14,12,0.18) premultiplied.
|
||||||
|
line: Color32::from_rgba_premultiplied(2, 2, 2, 46),
|
||||||
|
accent: Color32::from_rgb(0x0E, 0x0E, 0x0C),
|
||||||
|
accent_dark: Color32::from_rgb(0x24, 0x24, 0x20),
|
||||||
|
accent_ink: Color32::from_rgb(0xFF, 0xD6, 0x0A),
|
||||||
|
pos: Color32::from_rgb(0x0E, 0x7C, 0x3A),
|
||||||
|
neg: Color32::from_rgb(0x9E, 0x2E, 0x0E),
|
||||||
|
tor_purple: Color32::from_rgb(0x8A, 0x2B, 0xE2),
|
||||||
|
chip: Color32::from_rgba_premultiplied(2, 2, 2, 20),
|
||||||
|
hover: Color32::from_rgb(0xEF, 0xC8, 0x00),
|
||||||
|
avatar_pairs: AVATAR_PAIRS,
|
||||||
|
dark_base: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
thread_local! {
|
||||||
|
/// Per-frame theme override (see [`scoped`]). egui renders on one thread, so
|
||||||
|
/// a thread-local Cell scopes a different theme to a single surface without
|
||||||
|
/// touching the persisted app config.
|
||||||
|
static OVERRIDE: Cell<Option<ThemeKind>> = const { Cell::new(None) };
|
||||||
|
}
|
||||||
|
|
||||||
|
/// RAII guard that forces [`kind`]/[`tokens`] to a specific theme for its
|
||||||
|
/// lifetime, restoring the previous value on drop (panic-safe). Used to paint
|
||||||
|
/// one surface — the Pay tab — in the yellow theme regardless of the user's
|
||||||
|
/// chosen theme, à la a modern pay app's brand-colored pay screen.
|
||||||
|
#[must_use = "the override only lasts while the guard is alive"]
|
||||||
|
pub struct ScopedTheme(Option<ThemeKind>);
|
||||||
|
|
||||||
|
impl Drop for ScopedTheme {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
OVERRIDE.with(|c| c.set(self.0.take()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Override the active theme until the returned guard drops.
|
||||||
|
pub fn scoped(kind: ThemeKind) -> ScopedTheme {
|
||||||
|
ScopedTheme(OVERRIDE.with(|c| c.replace(Some(kind))))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Current theme kind: a scoped override if one is active, else app config
|
||||||
|
/// (dark is the product default).
|
||||||
|
pub fn kind() -> ThemeKind {
|
||||||
|
OVERRIDE.with(|c| c.get()).unwrap_or_else(AppConfig::theme)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Current theme tokens.
|
||||||
|
pub fn tokens() -> &'static ThemeTokens {
|
||||||
|
match kind() {
|
||||||
|
ThemeKind::Light => &LIGHT,
|
||||||
|
ThemeKind::Dark => &DARK,
|
||||||
|
ThemeKind::Yellow => &YELLOW,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set each frame by the Pay surface (which paints a bright yellow top under a
|
||||||
|
/// possibly-dark global theme), so the status bar can pick readable icons for it.
|
||||||
|
static YELLOW_SURFACE: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
|
||||||
|
|
||||||
|
/// Flag whether the bright Pay/yellow surface is currently on screen.
|
||||||
|
pub fn set_status_surface_yellow(yellow: bool) {
|
||||||
|
YELLOW_SURFACE.store(yellow, std::sync::atomic::Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether the status bar should use light (white) icons: true on the dark
|
||||||
|
/// theme (dark top), false on the light/yellow themes (bright top). The bright
|
||||||
|
/// Pay surface forces dark icons even when the global theme is dark.
|
||||||
|
pub fn status_bar_white_icons() -> bool {
|
||||||
|
if YELLOW_SURFACE.load(std::sync::atomic::Ordering::Relaxed) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
tokens().dark_base
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Density scales from the design handoff.
|
||||||
|
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||||
|
pub enum DensityKind {
|
||||||
|
Compact,
|
||||||
|
Regular,
|
||||||
|
Comfy,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DensityKind {
|
||||||
|
pub fn id(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
DensityKind::Compact => "compact",
|
||||||
|
DensityKind::Regular => "regular",
|
||||||
|
DensityKind::Comfy => "comfy",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_id(id: &str) -> Option<DensityKind> {
|
||||||
|
match id {
|
||||||
|
"compact" => Some(DensityKind::Compact),
|
||||||
|
"regular" => Some(DensityKind::Regular),
|
||||||
|
"comfy" => Some(DensityKind::Comfy),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Spacing tokens for a density.
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
pub struct DensityTokens {
|
||||||
|
pub pad: f32,
|
||||||
|
pub gap: f32,
|
||||||
|
pub radius: f32,
|
||||||
|
pub row: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const COMPACT: DensityTokens = DensityTokens {
|
||||||
|
pad: 12.0,
|
||||||
|
gap: 10.0,
|
||||||
|
radius: 10.0,
|
||||||
|
row: 56.0,
|
||||||
|
};
|
||||||
|
pub const REGULAR: DensityTokens = DensityTokens {
|
||||||
|
pad: 16.0,
|
||||||
|
gap: 14.0,
|
||||||
|
radius: 16.0,
|
||||||
|
row: 64.0,
|
||||||
|
};
|
||||||
|
pub const COMFY: DensityTokens = DensityTokens {
|
||||||
|
pad: 20.0,
|
||||||
|
gap: 18.0,
|
||||||
|
radius: 22.0,
|
||||||
|
row: 72.0,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Current density tokens from app config (comfy is the product default).
|
||||||
|
pub fn density() -> DensityTokens {
|
||||||
|
match AppConfig::density() {
|
||||||
|
DensityKind::Compact => COMPACT,
|
||||||
|
DensityKind::Regular => REGULAR,
|
||||||
|
DensityKind::Comfy => COMFY,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Font family helpers for the Geist weight stack registered in `setup_fonts`.
|
||||||
|
pub mod fonts {
|
||||||
|
use egui::{FontFamily, FontId};
|
||||||
|
|
||||||
|
pub fn regular() -> FontFamily {
|
||||||
|
FontFamily::Proportional
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn medium() -> FontFamily {
|
||||||
|
FontFamily::Name("geist-medium".into())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn semibold() -> FontFamily {
|
||||||
|
FontFamily::Name("geist-semibold".into())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn bold() -> FontFamily {
|
||||||
|
FontFamily::Name("geist-bold".into())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn mono() -> FontFamily {
|
||||||
|
FontFamily::Monospace
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn mono_semibold() -> FontFamily {
|
||||||
|
FontFamily::Name("geist-mono-sb".into())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Uppercase kicker label size (11px in the design).
|
||||||
|
pub fn kicker() -> FontId {
|
||||||
|
FontId::new(11.0, semibold())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Pick a readable ink (black or white) for the given background by luminance.
|
||||||
|
pub fn ink_for(bg: Color32) -> Color32 {
|
||||||
|
let lum = 0.299 * bg.r() as f32 + 0.587 * bg.g() as f32 + 0.114 * bg.b() as f32;
|
||||||
|
if lum > 140.0 {
|
||||||
|
Color32::from_rgb(0x0E, 0x0E, 0x0C)
|
||||||
|
} else {
|
||||||
|
Color32::from_rgb(0xFA, 0xFA, 0xF7)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Number of avatar color pairs (hue derivation modulus).
|
||||||
|
pub fn avatar_pairs_len() -> usize {
|
||||||
|
tokens().avatar_pairs.len()
|
||||||
|
}
|
||||||