Merge branch 'master' into feature/powersync-test

# Conflicts:
#	lib/models/body_weight/weight_entry.dart
#	lib/models/body_weight/weight_entry.g.dart
#	lib/models/workouts/log.dart
#	lib/models/workouts/log.g.dart
#	lib/models/workouts/session.g.dart
#	pubspec.lock
#	pubspec.yaml
#	test/exercises/contribute_exercise_image_test.mocks.dart
#	test/exercises/contribute_exercise_test.dart
#	test/exercises/contribute_exercise_test.mocks.dart
#	test/nutrition/nutritional_meal_form_test.mocks.dart
#	test/nutrition/nutritional_plan_form_test.mocks.dart
#	test/weight/weight_model_test.dart
#	test/weight/weight_provider_test.dart
#	test_data/body_weight.dart
This commit is contained in:
Roland Geider
2025-11-25 21:22:33 +01:00
57 changed files with 3227 additions and 1512 deletions

View File

@@ -9,7 +9,7 @@ runs:
uses: subosito/flutter-action@v2
with:
channel: stable
flutter-version: 3.35.5
flutter-version: 3.38.3
cache: true
- name: Install Flutter dependencies

View File

@@ -16,7 +16,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout application
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
ref: ${{ inputs.ref }}
@@ -36,7 +36,7 @@ jobs:
- name: Build APK
run: flutter build apk --release
- uses: actions/upload-artifact@v4
- uses: actions/upload-artifact@v5
with:
name: builds-apk
path: build/app/outputs/flutter-apk/app-release.apk
@@ -46,7 +46,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout application
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
ref: ${{ inputs.ref }}
@@ -67,7 +67,7 @@ jobs:
- name: Build AAB
run: flutter build appbundle --release
- uses: actions/upload-artifact@v4
- uses: actions/upload-artifact@v5
with:
name: builds-aab
path: build/app/outputs/bundle/release/app-release.aab

View File

@@ -11,7 +11,7 @@ jobs:
runs-on: macos-latest
steps:
- name: Checkout application
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
ref: ${{ inputs.ref }}
@@ -31,7 +31,7 @@ jobs:
cd build/ios/iphoneos
zip -r Runner.app.zip Runner.app
- uses: actions/upload-artifact@v4
- uses: actions/upload-artifact@v5
with:
name: builds-ios
path: build/ios/iphoneos/Runner.app.zip
@@ -41,7 +41,7 @@ jobs:
runs-on: macos-latest
steps:
- name: Checkout application
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
ref: ${{ inputs.ref }}
@@ -61,7 +61,7 @@ jobs:
cd build/ios/archive
zip -r Runner.xcarchive.zip Runner.xcarchive
- uses: actions/upload-artifact@v4
- uses: actions/upload-artifact@v5
with:
name: builds-ipa
path: build/ios/archive/Runner.xcarchive.zip
@@ -71,7 +71,7 @@ jobs:
runs-on: macos-latest
steps:
- name: Checkout application
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
ref: ${{ inputs.ref }}
@@ -84,7 +84,7 @@ jobs:
cd build/macos/Build/Products/Release
zip -r wger.app.zip wger.app
- uses: actions/upload-artifact@v4
- uses: actions/upload-artifact@v5
with:
name: builds-macos
path: build/macos/Build/Products/Release/wger.app.zip

View File

@@ -25,7 +25,7 @@ jobs:
# runner: ubuntu-24.04-arm
steps:
- name: Checkout application
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
ref: ${{ inputs.ref }}
@@ -39,7 +39,7 @@ jobs:
sudo apt install -y pkg-config libgtk-3-dev liblzma-dev libstdc++-12-dev --no-install-recommends
flutter build linux --release
tar -zcvf linux-${{ matrix.platform }}.tar.gz build/linux/${{ matrix.platform }}/release/bundle
- uses: actions/upload-artifact@v4
- uses: actions/upload-artifact@v5
with:
name: builds-linux
path: |
@@ -56,7 +56,7 @@ jobs:
steps:
- name: Checkout flatpak-flathub repo
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
repository: wger-project/de.wger.flutter
@@ -67,14 +67,19 @@ jobs:
python bump-wger-version.py ${{ inputs.ref }}
../flatpak-flutter/flatpak-flutter.py --app-module wger flatpak-flutter.json
- name: Push updated config to flathub repository
uses: cpina/github-action-push-to-another-repository@main
env:
SSH_DEPLOY_KEY: ${{ secrets.SSH_DEPLOY_KEY }}
with:
destination-github-username: wger-project
destination-repository-name: de.wger.flutter
user-email: github-actions@github.com
target-branch: release-${{ inputs.ref }}
create-target-branch-if-needed: true
commit-message: Update to ${{ inputs.ref }}
# TODO: this is currently commented out because it seems the action used below
# doesn't work anymore. This is probably not all that surprising as it
# isn't being developed anymore. This should be update so that the process
# works automatically again, till then this can be done manually.
#- name: Push updated config to flathub repository
# uses: cpina/github-action-push-to-another-repository@main
# env:
# SSH_DEPLOY_KEY: ${{ secrets.SSH_DEPLOY_KEY }}
# with:
# destination-github-username: wger-project
# destination-repository-name: de.wger.flutter
# user-email: github-actions@github.com
# target-branch: release-${{ inputs.ref }}
# create-target-branch-if-needed: true
# commit-message: Update to ${{ inputs.ref }}

View File

@@ -15,7 +15,7 @@ jobs:
runs-on: windows-latest
steps:
- name: Checkout application
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
ref: ${{ inputs.ref }}
@@ -25,7 +25,7 @@ jobs:
- name: Build .exe
run: flutter build windows --release
- uses: actions/upload-artifact@v4
- uses: actions/upload-artifact@v5
with:
name: builds-windows
path: build\windows\x64\runner\Release\wger.exe

View File

@@ -13,7 +13,7 @@ jobs:
steps:
- name: Checkout application
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
fetch-depth: 0 # needed to push changes
token: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -7,8 +7,10 @@ on:
pull_request:
branches: [ master, ]
paths:
- '**.dart'
- '**/*.dart'
- 'pubspec.yaml'
- '.github/actions/flutter-common/action.yml'
- '.github/workflows/ci.yml'
workflow_call:
workflow_dispatch:
@@ -16,8 +18,10 @@ jobs:
test:
name: Run tests
runs-on: ubuntu-latest
env:
TZ: Europe/Berlin
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- name: Common flutter setup
uses: ./.github/actions/flutter-common

View File

@@ -61,12 +61,12 @@ jobs:
- build_linux
steps:
- name: Checkout application
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
ref: ${{ github.event.inputs.version }}
- name: Download builds
uses: actions/download-artifact@v5
uses: actions/download-artifact@v6
with:
path: /tmp/
@@ -105,7 +105,7 @@ jobs:
# - build_apple
# steps:
# - name: Checkout application
# uses: actions/checkout@v5
# uses: actions/checkout@v6
# with:
# ref: feature/build-process
# # ref: ${{ github.event.inputs.version }}
@@ -114,7 +114,7 @@ jobs:
# uses: ./.github/actions/flutter-common
#
# - name: Download builds
# uses: actions/download-artifact@v5
# uses: actions/download-artifact@v6
# with:
# path: /tmp/
#
@@ -133,7 +133,7 @@ jobs:
steps:
- name: Download builds
uses: actions/download-artifact@v5
uses: actions/download-artifact@v6
- name: Make Github release
uses: softprops/action-gh-release@v2

View File

@@ -2,6 +2,10 @@
Thank you all for contributing to the project, you are true heroes! 🫶
*Generated on 2025-11-10*
---
## Contributors
- thisisyoussef - [https://github.com/thisisyoussef](https://github.com/thisisyoussef)
@@ -89,7 +93,7 @@ Thank you all for contributing to the project, you are true heroes! 🫶
- Thilina Herath - [https://github.com/thilinatnt](https://github.com/thilinatnt)
- ToldYouThat
- Yair Chen - [https://github.com/chenyair](https://github.com/chenyair)
- henok3878 - [https://github.com/henok3878](https://github.com/henok3878)
- henok3878 - [https://github.com/h3nock](https://github.com/h3nock)
- Patrick Witter - [https://github.com/patrickwitter](https://github.com/patrickwitter)
- ton-An - [https://github.com/ton-An](https://github.com/ton-An)
- Prakash Shekhar - [https://github.com/prakash-shekhar](https://github.com/prakash-shekhar)
@@ -100,40 +104,50 @@ Thank you all for contributing to the project, you are true heroes! 🫶
- Jannik Norden
- Allan Nordhøy - [https://github.com/comradekingu](https://github.com/comradekingu)
- Stefano Rossi - [https://github.com/stefanorossiti](https://github.com/stefanorossiti)
- Dylan Aird - [https://github.com/Dolaned](https://github.com/Dolaned)
## Translators
### Amharic
- henok3878 - [https://github.com/h3nock](https://github.com/h3nock)
### Arabic
- Anonymous - [https://github.com/weblate](https://github.com/weblate)
- Hanaa - [https://github.com/hn-n](https://github.com/hn-n)
- Ahmed zein - [https://github.com/Ahmed-Zein](https://github.com/Ahmed-Zein)
### Catalan
- Zixu Sun - [https://github.com/ziixu](https://github.com/ziixu)
- Anonymous - [https://github.com/weblate](https://github.com/weblate)
- guillem - [https://github.com/gbuendia](https://github.com/gbuendia)
### Chinese (Simplified Han script)
- Herb Huang
### Chinese (Simplified)
- 纪颖志 - [https://github.com/jiyingzhi](https://github.com/jiyingzhi)
- Yi-Han Hsiung - [https://github.com/AaronHsiung](https://github.com/AaronHsiung)
- Tsz Hong CHAN - [https://github.com/tomyan112](https://github.com/tomyan112)
- Eddie Tang - [https://github.com/EDED2314](https://github.com/EDED2314)
- Jing - [https://github.com/jingcheng16](https://github.com/jingcheng16)
- sr-c - [https://github.com/sr-c](https://github.com/sr-c)
- tony - [https://github.com/tonyxxliu](https://github.com/tonyxxliu)
- yiter
### Chinese (Traditional Han script)
- Peter Dave Hello - [https://github.com/PeterDaveHello](https://github.com/PeterDaveHello)
### Polish
### Chinese (Traditional)
- Karol Solecki - [https://github.com/karolsol](https://github.com/karolsol)
- Piotr Strebski - [https://github.com/strebski](https://github.com/strebski)
- Dawid Panyło
- Anonymous - [https://github.com/weblate](https://github.com/weblate)
- Marcin Schoenknecht
- Patryk - [https://github.com/byakurau](https://github.com/byakurau)
- Michał Homza - [https://github.com/HagiaHaya](https://github.com/HagiaHaya)
- Jacob - [https://github.com/devzom](https://github.com/devzom)
### Serbian
- Mladen Trišić - [https://github.com/mtrisic](https://github.com/mtrisic)
### Dutch
- Joey Haalboom - [https://github.com/JoeyHaalboom](https://github.com/JoeyHaalboom)
### Russian
- Алексей Курышко - [https://github.com/alexkuryshko](https://github.com/alexkuryshko)
- lightningcpu - [https://github.com/lightningcpu](https://github.com/lightningcpu)
- Кирилл Александрович Злобин - [https://github.com/gungstarbeiter](https://github.com/gungstarbeiter)
- Ivan Katkov - [https://github.com/Porphyrion](https://github.com/Porphyrion)
- Nikita Epifanov
- hugoalh
- Tsz Hong CHAN - [https://github.com/tomyan112](https://github.com/tomyan112)
- Chung-Wei Chung - [https://github.com/webb790709](https://github.com/webb790709)
- HY Cheng
### Croatian
@@ -141,18 +155,78 @@ Thank you all for contributing to the project, you are true heroes! 🫶
- Roland Geider - [https://github.com/rolandgeider](https://github.com/rolandgeider)
- SMilohanic - [https://github.com/sandimilohanic](https://github.com/sandimilohanic)
### Portuguese
### Czech
- Edson Wolf - [https://github.com/edsonblwolf](https://github.com/edsonblwolf)
- Fjuro - [https://github.com/Fjuro](https://github.com/Fjuro)
- Fjuro
- CaptainDolphy - [https://github.com/CaptainDolphy](https://github.com/CaptainDolphy)
- Roman Kalivoda - [https://github.com/RKCZ](https://github.com/RKCZ)
### Dutch
- Joey Haalboom - [https://github.com/JoeyHaalboom](https://github.com/JoeyHaalboom)
### English
- guillem - [https://github.com/gbuendia](https://github.com/gbuendia)
- Allan Nordhøy - [https://github.com/comradekingu](https://github.com/comradekingu)
- Roland Geider - [https://github.com/rolandgeider](https://github.com/rolandgeider)
### French
- William - [https://github.com/WilliamR312](https://github.com/WilliamR312)
- florent4014 - [https://github.com/florent4014](https://github.com/florent4014)
- Anonymous - [https://github.com/weblate](https://github.com/weblate)
- Wilton Rodrigues
- Guilherme Salomão - [https://github.com/salomaoparkour](https://github.com/salomaoparkour)
- Bruno de Moura - [https://github.com/bruunomooura](https://github.com/bruunomooura)
- Dalton Scavassa
- Stefan Taiguara - [https://github.com/Teitei011](https://github.com/Teitei011)
- Eduardo Menges Mattje - [https://github.com/EduMenges](https://github.com/EduMenges)
- Edu Cavalheiro - [https://github.com/EduCavalheiro](https://github.com/EduCavalheiro)
- João Goulart - [https://github.com/usehalter](https://github.com/usehalter)
- Xav Basco
- David Olewski - [https://github.com/Arigowin](https://github.com/Arigowin)
- yoyomax80400 - [https://github.com/yoyomax80400](https://github.com/yoyomax80400)
- loued - [https://github.com/Loued](https://github.com/Loued)
- Célian
- MrSniikyz - [https://github.com/BabyGeek](https://github.com/BabyGeek)
- Roland Geider - [https://github.com/rolandgeider](https://github.com/rolandgeider)
- J. Lavoie
- Antoine Vibien - [https://github.com/r1llettes](https://github.com/r1llettes)
- Stefano Rossi - [https://github.com/stefanorossiti](https://github.com/stefanorossiti)
### German
- kvnrmnn - [https://github.com/rmnn92](https://github.com/rmnn92)
- Anonymous - [https://github.com/weblate](https://github.com/weblate)
- Roland Geider - [https://github.com/rolandgeider](https://github.com/rolandgeider)
- Victor Jouhoff - [https://github.com/jouhoffv](https://github.com/jouhoffv)
- m4skedbyte
- Axel Steinbrecher
- Christoph Suesser - [https://github.com/TheFitzZZ](https://github.com/TheFitzZZ)
- Luis Lüscher - [https://github.com/lslschr](https://github.com/lslschr)
- mondstern
- J. Lavoie
- Marvin M - [https://github.com/M123-dev](https://github.com/M123-dev)
- Lydia
- Allan Nordhøy - [https://github.com/comradekingu](https://github.com/comradekingu)
### Greek
- Dimitrys Meliates
- Antonis-geo - [https://github.com/Antonis-geo](https://github.com/Antonis-geo)
### Hebrew
- Anonymous - [https://github.com/weblate](https://github.com/weblate)
- n,rdo
- Tomer Ben Rachel - [https://github.com/TomerPacific](https://github.com/TomerPacific)
### Hindi
- pavan arun bagwe - [https://github.com/pavanb0](https://github.com/pavanb0)
- Anonymous - [https://github.com/weblate](https://github.com/weblate)
- Debayan Sutradhar - [https://github.com/rnayabed](https://github.com/rnayabed)
### Indonesian
- aryakdaniswara - [https://github.com/aryakdaniswara](https://github.com/aryakdaniswara)
- Anonymous - [https://github.com/weblate](https://github.com/weblate)
- Debi Maulana Ahsan Halla
- Roland Geider - [https://github.com/rolandgeider](https://github.com/rolandgeider)
### Italian
@@ -171,32 +245,41 @@ Thank you all for contributing to the project, you are true heroes! 🫶
- mondstern
- Stefano Rossi - [https://github.com/stefanorossiti](https://github.com/stefanorossiti)
### French
### Japanese
- William - [https://github.com/WilliamR312](https://github.com/WilliamR312)
- florent4014 - [https://github.com/florent4014](https://github.com/florent4014)
- Anonymous - [https://github.com/weblate](https://github.com/weblate)
- Xav Basco
- David Olewski - [https://github.com/Arigowin](https://github.com/Arigowin)
- yoyomax80400 - [https://github.com/yoyomax80400](https://github.com/yoyomax80400)
- loued - [https://github.com/Loued](https://github.com/Loued)
- Célian
- MrSniikyz - [https://github.com/BabyGeek](https://github.com/BabyGeek)
- Roland Geider - [https://github.com/rolandgeider](https://github.com/rolandgeider)
- J. Lavoie
- Antoine Vibien - [https://github.com/r1llettes](https://github.com/r1llettes)
- Stefano Rossi - [https://github.com/stefanorossiti](https://github.com/stefanorossiti)
- Tsz Hong CHAN - [https://github.com/tomyan112](https://github.com/tomyan112)
- sasukeiscool - [https://github.com/sasukeiscool](https://github.com/sasukeiscool)
- yiter
### Ukrainian
### Norwegian Bokmål
- Максим Горпиніч - [https://github.com/Maksim2005UA](https://github.com/Maksim2005UA)
- Roland Geider - [https://github.com/rolandgeider](https://github.com/rolandgeider)
- Максим Горпиніч
- Allan Nordhøy - [https://github.com/comradekingu](https://github.com/comradekingu)
### Polish
- Karol Solecki - [https://github.com/karolsol](https://github.com/karolsol)
- Piotr Strebski - [https://github.com/strebski](https://github.com/strebski)
- Dawid Panyło
- Anonymous - [https://github.com/weblate](https://github.com/weblate)
- Dan - [https://github.com/Kefir2105](https://github.com/Kefir2105)
- Dan
- Tymofii Lytvynenko
- Artem - [https://github.com/defaultpage](https://github.com/defaultpage)
- Marcin Schoenknecht
- Patryk - [https://github.com/byakurau](https://github.com/byakurau)
- Michał Homza - [https://github.com/HagiaHaya](https://github.com/HagiaHaya)
- Jacob - [https://github.com/devzom](https://github.com/devzom)
### Portuguese
- Edson Wolf - [https://github.com/edsonblwolf](https://github.com/edsonblwolf)
- Anonymous - [https://github.com/weblate](https://github.com/weblate)
- Wilton Rodrigues
- Guilherme Salomão - [https://github.com/salomaoparkour](https://github.com/salomaoparkour)
- Bruno de Moura - [https://github.com/bruunomooura](https://github.com/bruunomooura)
- Dalton Scavassa
- Stefan Taiguara - [https://github.com/Teitei011](https://github.com/Teitei011)
- Eduardo Menges Mattje - [https://github.com/EduMenges](https://github.com/EduMenges)
- Edu Cavalheiro - [https://github.com/EduCavalheiro](https://github.com/EduCavalheiro)
- João Goulart - [https://github.com/usehalter](https://github.com/usehalter)
### Portuguese (Brazil)
@@ -210,99 +293,26 @@ Thank you all for contributing to the project, you are true heroes! 🫶
- Luigi Henrick Feitoza Silva - [https://github.com/luigihenrick](https://github.com/luigihenrick)
- João Hortêncio Moraes - [https://github.com/joaohortencio](https://github.com/joaohortencio)
### Tamil
- தமிழ்நேரம் - [https://github.com/TamilNeram](https://github.com/TamilNeram)
### Chinese (Simplified Han script)
- Herb Huang
### Hindi
- pavan arun bagwe - [https://github.com/pavanb0](https://github.com/pavanb0)
- Anonymous - [https://github.com/weblate](https://github.com/weblate)
- Debayan Sutradhar - [https://github.com/rnayabed](https://github.com/rnayabed)
### Turkish
- Oğuz Ersen - [https://github.com/oersen](https://github.com/oersen)
- Roland Geider - [https://github.com/rolandgeider](https://github.com/rolandgeider)
- Cem Avcı - [https://github.com/cem256](https://github.com/cem256)
- Oğuz Ersen
- Cenk Cidecio - [https://github.com/ccidecio](https://github.com/ccidecio)
- ToldYouThat
### German
- kvnrmnn - [https://github.com/rmnn92](https://github.com/rmnn92)
- Anonymous - [https://github.com/weblate](https://github.com/weblate)
- Roland Geider - [https://github.com/rolandgeider](https://github.com/rolandgeider)
- Victor Jouhoff - [https://github.com/jouhoffv](https://github.com/jouhoffv)
- m4skedbyte
- Axel Steinbrecher
- Christoph Suesser - [https://github.com/TheFitzZZ](https://github.com/TheFitzZZ)
- Luis Lüscher - [https://github.com/lslschr](https://github.com/lslschr)
- mondstern
- J. Lavoie
- Marvin M - [https://github.com/M123-dev](https://github.com/M123-dev)
- Lydia
- Allan Nordhøy - [https://github.com/comradekingu](https://github.com/comradekingu)
### Indonesian
- aryakdaniswara - [https://github.com/aryakdaniswara](https://github.com/aryakdaniswara)
- Anonymous - [https://github.com/weblate](https://github.com/weblate)
- Debi Maulana Ahsan Halla
- Roland Geider - [https://github.com/rolandgeider](https://github.com/rolandgeider)
### Catalan
- Zixu Sun - [https://github.com/ziixu](https://github.com/ziixu)
- Anonymous - [https://github.com/weblate](https://github.com/weblate)
- guillem - [https://github.com/gbuendia](https://github.com/gbuendia)
### Chinese (Simplified)
- 纪颖志 - [https://github.com/jiyingzhi](https://github.com/jiyingzhi)
- Yi-Han Hsiung - [https://github.com/AaronHsiung](https://github.com/AaronHsiung)
- Tsz Hong CHAN - [https://github.com/tomyan112](https://github.com/tomyan112)
- Eddie Tang - [https://github.com/EDED2314](https://github.com/EDED2314)
- Jing - [https://github.com/jingcheng16](https://github.com/jingcheng16)
- sr-c - [https://github.com/sr-c](https://github.com/sr-c)
- tony - [https://github.com/tonyxxliu](https://github.com/tonyxxliu)
- yiter
### Greek
- Dimitrys Meliates
- Antonis-geo - [https://github.com/Antonis-geo](https://github.com/Antonis-geo)
### Czech
- Fjuro - [https://github.com/Fjuro](https://github.com/Fjuro)
- Fjuro
- CaptainDolphy - [https://github.com/CaptainDolphy](https://github.com/CaptainDolphy)
- Roman Kalivoda - [https://github.com/RKCZ](https://github.com/RKCZ)
### Arabic
### Portuguese (Portugal)
- Anonymous - [https://github.com/weblate](https://github.com/weblate)
- Hanaa - [https://github.com/hn-n](https://github.com/hn-n)
- Ahmed zein - [https://github.com/Ahmed-Zein](https://github.com/Ahmed-Zein)
### Hebrew
### Romanian
- Anonymous - [https://github.com/weblate](https://github.com/weblate)
- n,rdo
- Tomer Ben Rachel - [https://github.com/TomerPacific](https://github.com/TomerPacific)
- Bogdan Bujor - [https://github.com/qSharpy](https://github.com/qSharpy)
- dimii27 - [https://github.com/dimii27](https://github.com/dimii27)
### Japanese
### Russian
- Anonymous - [https://github.com/weblate](https://github.com/weblate)
- Tsz Hong CHAN - [https://github.com/tomyan112](https://github.com/tomyan112)
- sasukeiscool - [https://github.com/sasukeiscool](https://github.com/sasukeiscool)
- yiter
- Алексей Курышко - [https://github.com/alexkuryshko](https://github.com/alexkuryshko)
- lightningcpu - [https://github.com/lightningcpu](https://github.com/lightningcpu)
- Кирилл Александрович Злобин - [https://github.com/gungstarbeiter](https://github.com/gungstarbeiter)
- Ivan Katkov - [https://github.com/Porphyrion](https://github.com/Porphyrion)
- Nikita Epifanov
### Serbian
- Mladen Trišić - [https://github.com/mtrisic](https://github.com/mtrisic)
### Spanish
@@ -317,33 +327,26 @@ Thank you all for contributing to the project, you are true heroes! 🫶
- Roland Geider - [https://github.com/rolandgeider](https://github.com/rolandgeider)
- martingetzel - [https://github.com/martingetzel](https://github.com/martingetzel)
### Chinese (Traditional)
### Tamil
- hugoalh
- Tsz Hong CHAN - [https://github.com/tomyan112](https://github.com/tomyan112)
- Chung-Wei Chung - [https://github.com/webb790709](https://github.com/webb790709)
- HY Cheng
- தமிழ்நேரம் - [https://github.com/TamilNeram](https://github.com/TamilNeram)
### Portuguese (Portugal)
### Turkish
- Oğuz Ersen - [https://github.com/oersen](https://github.com/oersen)
- Roland Geider - [https://github.com/rolandgeider](https://github.com/rolandgeider)
- Cem Avcı - [https://github.com/cem256](https://github.com/cem256)
- Oğuz Ersen
- Cenk Cidecio - [https://github.com/ccidecio](https://github.com/ccidecio)
- ToldYouThat
### Ukrainian
- Максим Горпиніч - [https://github.com/Maksim2005UA](https://github.com/Maksim2005UA)
- Roland Geider - [https://github.com/rolandgeider](https://github.com/rolandgeider)
- Максим Горпиніч
- Anonymous - [https://github.com/weblate](https://github.com/weblate)
### Romanian
- Bogdan Bujor - [https://github.com/qSharpy](https://github.com/qSharpy)
- dimii27 - [https://github.com/dimii27](https://github.com/dimii27)
### English
- guillem - [https://github.com/gbuendia](https://github.com/gbuendia)
- Allan Nordhøy - [https://github.com/comradekingu](https://github.com/comradekingu)
- Roland Geider - [https://github.com/rolandgeider](https://github.com/rolandgeider)
### Norwegian Bokmål
- Roland Geider - [https://github.com/rolandgeider](https://github.com/rolandgeider)
- Allan Nordhøy - [https://github.com/comradekingu](https://github.com/comradekingu)
### Amharic
- henok3878 - [https://github.com/henok3878](https://github.com/henok3878)
- Dan - [https://github.com/Kefir2105](https://github.com/Kefir2105)
- Dan
- Tymofii Lytvynenko
- Artem - [https://github.com/defaultpage](https://github.com/defaultpage)

View File

@@ -11,8 +11,8 @@ GEM
artifactory (3.0.17)
atomos (0.1.3)
aws-eventstream (1.4.0)
aws-partitions (1.1163.0)
aws-sdk-core (3.232.0)
aws-partitions (1.1181.0)
aws-sdk-core (3.236.0)
aws-eventstream (~> 1, >= 1.3.0)
aws-partitions (~> 1, >= 1.992.0)
aws-sigv4 (~> 1.9)
@@ -20,18 +20,18 @@ GEM
bigdecimal
jmespath (~> 1, >= 1.6.1)
logger
aws-sdk-kms (1.112.0)
aws-sdk-core (~> 3, >= 3.231.0)
aws-sdk-kms (1.117.0)
aws-sdk-core (~> 3, >= 3.234.0)
aws-sigv4 (~> 1.5)
aws-sdk-s3 (1.199.0)
aws-sdk-core (~> 3, >= 3.231.0)
aws-sdk-s3 (1.203.0)
aws-sdk-core (~> 3, >= 3.234.0)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.5)
aws-sigv4 (1.12.1)
aws-eventstream (~> 1, >= 1.0.2)
babosa (1.0.4)
base64 (0.3.0)
bigdecimal (3.2.3)
bigdecimal (3.3.1)
claide (1.1.0)
colored (1.2)
colored2 (3.1.2)
@@ -161,7 +161,7 @@ GEM
httpclient (2.9.0)
mutex_m
jmespath (1.6.2)
json (2.15.0)
json (2.16.0)
jwt (2.10.2)
base64
logger (1.7.0)
@@ -173,11 +173,11 @@ GEM
nanaimo (0.4.0)
naturally (2.3.0)
nkf (0.2.0)
optparse (0.6.0)
optparse (0.8.0)
os (1.1.4)
plist (3.7.2)
public_suffix (6.0.2)
rake (13.3.0)
rake (13.3.1)
representable (3.2.0)
declarative (< 0.1.0)
trailblazer-option (>= 0.1.1, < 0.2.0)
@@ -225,6 +225,7 @@ PLATFORMS
arm64-darwin-22
arm64-darwin-23
arm64-darwin-24
arm64-darwin-25
x86_64-linux
DEPENDENCIES
@@ -235,4 +236,4 @@ DEPENDENCIES
mutex_m
BUNDLED WITH
2.6.9
2.7.2

View File

@@ -39,7 +39,7 @@ android {
defaultConfig {
// Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId "de.wger.flutter"
minSdkVersion = flutter.minSdkVersion
minSdkVersion flutter.minSdkVersion
targetSdk = flutter.targetSdkVersion
versionCode = flutter.versionCode
versionName = flutter.versionName

View File

@@ -0,0 +1,57 @@
از طرف ما علاقمندان به تناسب اندام، به شما علاقمندان دیگر!
سلامتتون رو با برنامه مدیریت ورزش WGER، متحول کنید.
آیا اپلیکیشن موردعلاقه‌تون رو پیدا کردید و دوست دارید برنامه ورزشی مخصوص به خودتون رو درست کنید؟
مهم نیست چه جور ورزشکاری هستید، ما همه در یک چیز مشترکیم: عاشق اینیم که پیشرفت و داده‌های سلامتی‌مون رو دنبال کنیم! 3>
ما شما رو به خاطر اینکه هنوز هم سفر ورزشیتون رو با یک دفترچه کوچک پیش می‌برید قضاوت نمی‌کنیم...
اما خب، سلام به قرن ۲۰ ام!
ما یک اپلیکیشن شمارشگر و گزارش وضعیت سلامت و تناسب اندام ۱۰۰٪ رایگان برای شما ساخته‌ایم
که با خلاصه شدن در مهم‌ترین قابلیت‌ها، زندگی رو براتون ساده می‌کنه.
شروع کنید، به ورزش ادامه بدید و پیشرفتتون رو جشن بگیرید!
WGER یک پروژه متن‌باز است و همه‌چیز درباره این چهار مورد است:
بدن شما
تمرین‌های شما
پیشرفت شما
داده‌های شما
بدن شما:
دیگه نیازی نیست مواد تشکیل‌دهنده خوراکی موردعلاقه‌تون رو گوگل کنید!
وعده‌های غذایی روزانه‌تون رو از بین بیش از ۷۸,۰۰۰ محصول انتخاب کنید و ارزش غذایی آن‌ها رو ببینید. وعده‌ها رو به برنامه غذایی اضافه کنید و در تقویم، یک نمای کلی از رژیم‌تون داشته باشید.
تمرین‌های شما:
شما بهتر می‌دونید چه چیزی برای بدنتون مناسبه.
تمرین‌های شخصی خودتون رو از بین ۲۰۰ حرکت متنوع و در حال افزایش بسازید.
بعدش از "حالت باشگاه" استفاده کنید تا شما رو قدم‭ ‬به قدم در طول تمرین راهنمایی کند، در حالی که با یک کلیک وزن‌هایی که زدید رو ثبت می‌کنید.
پیشرفت شما:
هیچ‌وقت هدف‌هاتون رو فراموش نکنید.
وزن‌تون رو زیر نظر بگیرید و آمار پیشرفت‌تون رو ذخیره کنید.
داده‌های شما:
WGER دفترچه خاطرات ورزشی شخصی‌شده شماست اما این شما هستید که صاحب داده‌هایتان هستید.
از REST API استفاده کنید و با داده‌هاتون کارهای فوق‌العاده انجام بدید.
یک نکته مهم: این اپ رایگان بر اساس بودجه اضافی نیست و از شما کمک مالی نمی‌خواهیم.
بلکه یک پروژه جامعه‌محور است که دائماً در حال رشد است. پس خودتون رو برای قابلیت‌های جدید هیجان‌زده کنید!
متن‌باز این یعنی چی؟
یعنی تمام کد این برنامه و سرورش رایگان است و در دسترس همه قرار دارد:
می‌خواهید WGER رو روی سرور خودتون برای باشگاه محل اجرا کنید؟ حله!
یک قابلیت خاص کمه و می‌خواهید خودتون اضافهش کنید؟ از همین امروز شروع کنید!
می‌خواهید مطمئن بشوید که اطلاعاتی جایی فرستاده نمی‌شه؟ می‌توانید بررسی کنید!
به جامعه ما بپیوندید! بخشی از علاقه‌مندان به ورزش و متخصصان فناوری از سراسر جهان شوید.
ما دائماً در حال تنظیم و بهینه‌سازی اپلیکیشن متناسب با نیازهایمون هستیم. عاشق مشارکت و نظرات شما هستیم، پس در هر زمانی خوش آمدید که به ما ملحق شوید و آرزوها و ایده‌هاتون رو با ما در میان بگذارید!
-> کد منبع را در اینجا پیدا کنید: https://github.com/wger-project
-> سوال بپرسید یا فقط سلامی بکنید، سرور دیسکورد ما: https://discord.gg/rPWFv6W

View File

@@ -0,0 +1 @@
شمارنده تناسب اندام/ورزش، تغذیه و وزن

View File

@@ -0,0 +1 @@
wger Workout Manager

View File

@@ -1,10 +1,10 @@
Od ljubitelja fitnessa za ljubitelje fitnessa organiziraj svoje zdravlje s WGER, tvojim upravljačem treninga!
Već si pronašao/la omiljeni program za fitness i voliš stvarati vlastite sportske rutine? Bez obzira na vrstu sportske zvijeri svi imamo nešto zajedničko: Volimo pratiti naše zdravstvene podatke <3
Već si pronašao/la omiljeni program za fitness i voliš stvarati vlastite sportske rutine? Bez obzira na vrstu sporta kojim se baviš svi imamo nešto zajedničko: Volimo pratiti naše zdravstvene podatke <3
Stoga te ne osuđujemo što još uvijek upravljaš svojim fitnessom sa svojim praktičnim malim dnevnikom vježbanja, ali nalazimo se u 2025. godini!
Razvili smo 100 % besplatan program za digitalno praćenje zdravlja i fitnessa, s najrelevantnijim funkcijama koje će ti olakšati život. Započni, nastavi trenirati i proslavi svoj napredak!
Razvili smo 100 % besplatan program za digitalno praćenje zdravlja i fitnessa, s najrelevantnijim funkcijama koje će ti olakšati život. Započni i nastavi trenirati te slavi svoj napredak!
wger je projekt otvorenog koda za:
* Tvoje tijelo
@@ -28,12 +28,12 @@ Napomena: Ovaj besplatni program ne temelji se na dodatnim sredstvima i ne traž
#Otvoreni kod što to znači?
Otvoreni kod znači da je cijeli izvorni kod za ovaj program i poslužitelj s kojim razgovara besplatan i dostupan svima:
* Želiš pokrenuti wger na vlastitom poslužitelju za sebe ili lokalnu teretanu? Izvoli!
Otvoreni kod znači da je cijeli izvorni kod za ovaj program i server s kojim razgovara besplatan i dostupan svima:
* Želiš pokrenuti wger na vlastitom serveru za sebe ili lokalnu teretanu? Izvoli!
* Nedostaje ti funkcija i želiš je implementirati? Počni odmah!
* Želiš provjeriti da se nigdje ništa ne šalje? Možeš!
Pridruži se našoj zajednici i postani dio sportskih entuzijasta i IT geekova iz cijelog svijeta. Nastavljamo raditi na prilagodbi i optimizaciji programa prilagođen našim potrebama. Volimo tvoj doprinos, stoga se slobodno uključi u bilo koje vrijeme i predloži tvoje želje i ideje!
-> pronađi izvorni kod na https://github.com/wger-project
-> postavljaj pitanja ili se jednostavno predstavi na našem Discord poslužitelju https://discord.gg/rPWFv6W
-> postavljaj pitanja ili se jednostavno predstavi na našem Discord serveru https://discord.gg/rPWFv6W

View File

@@ -78,6 +78,12 @@
</screenshot>
</screenshots>
<releases>
<release version="1.9.1" date="2025-11-10">
<description>
<p>Bug fixes and improvements.</p>
</description>
<url>https://github.com/wger-project/flutter/releases/tag/1.9.1</url>
</release>
<release version="1.9.0" date="2025-09-23">
<description>
<p>Bug fixes and improvements.</p>

View File

@@ -37,5 +37,9 @@ end
post_install do |installer|
installer.pods_project.targets.each do |target|
flutter_additional_ios_build_settings(target)
target.build_configurations.each do |config|
config.build_settings['ENABLE_BITCODE'] = 'NO'
config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '16.0'
end
end
end

View File

@@ -16,11 +16,6 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
/// Returns a timezone aware DateTime object from a date and time string.
DateTime getDateTimeFromDateAndTime(String date, String time) {
return DateTime.parse('$date $time');
}
/// Returns a list of [DateTime] objects from [first] to [last], inclusive.
List<DateTime> daysInRange(DateTime first, DateTime last) {
final dayCount = last.difference(first).inDays + 1;

View File

@@ -62,6 +62,14 @@ String dateToUtcIso8601(DateTime dateTime) {
return dateTime.toUtc().toIso8601String();
}
/// Converts an ISO8601 datetime string in UTC to a local DateTime object.
///
/// Needs to be used in conjunction with [dateToUtcIso8601] in the models to
/// correctly handle timezones.
DateTime utcIso8601ToLocalDate(String dateTime) {
return DateTime.parse(dateTime).toLocal();
}
/*
* Converts a time to a date object.
* Needed e.g. when the wger api only sends a time but no date information.

View File

@@ -751,7 +751,7 @@
},
"aboutDonateTitle": "Mach eine Spende",
"@aboutDonateTitle": {},
"aboutDonateText": "Das Projekt ist kostenlos und wird es auch bleiben, der Betrieb des Servers hingegen nicht! Die Entwicklung erfordert zudem viel Zeit und Mühe von Freiwilligen. Ihr Beitrag deckt diese Kosten direkt und trägt zur Zuverlässigkeit des Dienstes bei.",
"aboutDonateText": "Das Projekt ist kostenlos und wird es auch bleiben, der Betrieb des Servers hingegen ist es nicht! Die Entwicklung erfordert zudem viel Zeit und Mühe von Freiwilligen. Ihr Beitrag deckt diese Kosten direkt und trägt zur Zuverlässigkeit des Dienstes bei.",
"@aboutDonateText": {},
"settingsCacheTitle": "Zwischenspeicher",
"@settingsCacheTitle": {},
@@ -950,7 +950,7 @@
"@simpleModeHelp": {},
"isRestDayHelp": "Bitte beachten Sie, dass alle Sätze und Übungen entfernt werden, wenn Sie einen Tag als Ruhetag markieren.",
"@isRestDayHelp": {},
"needsLogsToAdvance": "Benötigt Eintrag, um fortzufahren",
"needsLogsToAdvance": "Benötigt Logeinträge, um fortzufahren",
"@needsLogsToAdvance": {},
"needsLogsToAdvanceHelp": "Wählen Sie aus, ob die Routine nur dann zum nächsten geplanten Tag fortgesetzt werden soll, wenn Sie für diesen Tag ein Training protokolliert haben",
"@needsLogsToAdvanceHelp": {},

View File

@@ -1036,5 +1036,83 @@
"setHasProgressionWarning": "Tenga en cuenta que, por el momento, no es posible editar todos los ajustes de un set en la aplicación móvil ni configurar la progresión automática. Por ahora, utilice la aplicación web.",
"@setHasProgressionWarning": {},
"applicationLogs": "Registro de la aplicación",
"@applicationLogs": {}
"@applicationLogs": {},
"creationDate": "Fecha de comienzo",
"@creationDate": {
"description": "The Start date of a nutritional plan"
},
"openEnded": "Indefinido",
"@openEnded": {
"description": "When a nutrition plan has no pre-defined end date"
},
"overview": "Descripción general",
"@overview": {},
"identicalExercisePleaseDiscard": "Si observa un ejercicio idéntico al que está agregando, descarte el borrador y edítelo en su lugar.",
"@identicalExercisePleaseDiscard": {},
"checkInformationBeforeSubmitting": "Por favor verifique que la información que ingresó sea correcta antes de enviar el ejercicio",
"@checkInformationBeforeSubmitting": {},
"imageDetailsTitle": "Detalles de la imagen",
"@imageDetailsTitle": {
"description": "Title for image details form"
},
"imageDetailsLicenseTitle": "Título",
"@imageDetailsLicenseTitle": {
"description": "Label for image title field"
},
"imageDetailsLicenseTitleHint": "Introduce el título de la imagen",
"@imageDetailsLicenseTitleHint": {
"description": "Hint text for image title field"
},
"imageDetailsSourceLink": "Enlace al sitio web de origen",
"@imageDetailsSourceLink": {
"description": "Label for source link field"
},
"author": "Autor(es)",
"@author": {},
"authorHint": "Introduce el nombre del autor",
"@authorHint": {
"description": "Hint text for author field"
},
"imageDetailsAuthorLink": "Enlace al sitio web o perfil del autor",
"@imageDetailsAuthorLink": {
"description": "Label for author link field"
},
"imageDetailsDerivativeSource": "Enlace a la fuente original, si se trata de un trabajo derivado",
"@imageDetailsDerivativeSource": {
"description": "Label for derivative source field"
},
"imageDetailsDerivativeHelp": "Un trabajo derivado se basa en un trabajo anterior, pero contiene suficiente contenido nuevo y creativo para darle derecho a sus propios derechos de autor.",
"@imageDetailsDerivativeHelp": {
"description": "Helper text explaining derivative works"
},
"imageDetailsImageType": "Tipo de imagen",
"@imageDetailsImageType": {
"description": "Label for image type selector"
},
"imageDetailsLicenseNotice": "Al enviar esta imagen, acepta divulgarla según la licencia CC-BY-SA-4. La imagen debe ser obra propia o el autor debe haberla publicado bajo una licencia compatible con ella.",
"@imageDetailsLicenseNotice": {},
"imageDetailsLicenseNoticeLinkToLicense": "Ver texto de licencia.",
"@imageDetailsLicenseNoticeLinkToLicense": {},
"add": "agregar",
"@add": {
"description": "Add button text"
},
"enterTextInLanguage": "¡Por favor ingrese el texto en el idioma correcto!",
"@enterTextInLanguage": {},
"galleryImageTypeNotSupported": "Las imágenes {imageType} actualmente no son compatibles con esta plataforma.",
"@galleryImageTypeNotSupported": {
"placeholders": {
"imageType": {
"type": "String"
}
}
},
"galleryImageTypeNotSupportedDetail": "Esta imagen está en formato {imageType}, que actualmente no es compatible con esta plataforma.",
"@galleryImageTypeNotSupportedDetail": {
"placeholders": {
"imageType": {
"type": "String"
}
}
}
}

View File

@@ -1 +1,389 @@
{}
{
"userProfile": "پروفایل شما",
"@userProfile": {},
"login": "ورود",
"@login": {
"description": "Text for login button"
},
"logout": "خروج",
"@logout": {
"description": "Text for logout button"
},
"date": "تاریخ",
"@date": {
"description": "The date of a workout log or body weight entry"
},
"register": "ثبت نام",
"@register": {
"description": "Text for registration button"
},
"useDefaultServer": "استفاده از سرور پیش فرض",
"@useDefaultServer": {
"description": "Toggle button allowing users to switch between the default and a custom wger server"
},
"useCustomServer": "استفاده از سرور سفارشی (خصوصی)",
"@useCustomServer": {
"description": "Toggle button allowing users to switch between the default and a custom wger server"
},
"edit": "ویرایش",
"@edit": {},
"invalidUrl": "لطفا یک لینک معتبر وارد کنید",
"@invalidUrl": {
"description": "Error message when the user enters an invalid URL, e.g. in the login form"
},
"usernameValidChars": "نام کاربری فقط میتواند شامل حروف انگلیسی، اعداد انگلیسی و این کاراکتر ها @ + . - _ باشد",
"@usernameValidChars": {
"description": "Error message when the user tries to register a username with forbidden characters"
},
"delete": "حذف",
"@delete": {},
"passwordsDontMatch": "رمزهای عبور مطابقت ندارند",
"@passwordsDontMatch": {
"description": "Error message when the user enters two different passwords during registration"
},
"passwordTooShort": "رمز عبور خیلی کوتاه است",
"@passwordTooShort": {
"description": "Error message when the user a password that is too short"
},
"selectAvailablePlates": "بشقاب‌های غذایی موجود را انتخاب کنید",
"@selectAvailablePlates": {},
"close": "بستن",
"@close": {
"description": "Translation for close"
},
"difference": "تفاوت",
"@difference": {},
"useColors": "استفاده از رنگ ها",
"@useColors": {},
"password": "رمزعبور",
"@password": {},
"confirmPassword": "تأیید رمز عبور",
"@confirmPassword": {},
"invalidEmail": "لطفا یک آدرس ایمیل معتبر وارد کنید",
"@invalidEmail": {
"description": "Error message when the user enters an invalid email"
},
"email": "آدرس ایمیل",
"@email": {},
"username": "نام کاربری",
"@username": {},
"invalidUsername": "لطفا یک نام کاربری معتبر وارد کنید",
"@invalidUsername": {
"description": "Error message when the user enters an invalid username"
},
"start": "شروع",
"@start": {
"description": "Label on button to start the gym mode (i.e., an imperative)"
},
"useApiToken": "استفاده از توکن API",
"@useApiToken": {},
"useUsernameAndPassword": "استفاده از نام کاربری و رمز عبور",
"@useUsernameAndPassword": {},
"comment": "نظر",
"@comment": {
"description": "Comment, additional information"
},
"apiToken": "توکن API",
"@apiToken": {},
"invalidApiToken": "لطفا یک کلید API معتبر وارد کنید",
"@invalidApiToken": {
"description": "Error message when the user enters an invalid API key"
},
"apiTokenValidChars": "یک کلید API می‌تواند فقط شامل حروف انگلیسی a تا f، اعداد انگلیسی 0 تا 9 و دقیقاً 40 کاراکتر باشد",
"@apiTokenValidChars": {
"description": "Error message when the user tries to input a API key with forbidden characters"
},
"customServerUrl": "آدرس اینترنتی (URL) به منبع WGER",
"@customServerUrl": {
"description": "Label in the form where the users can enter their own wger instance"
},
"customServerHint": "آدرس سرور خودتان را وارد کنید، در غیر این صورت آدرس پیش‌فرض استفاده خواهد شد",
"@customServerHint": {
"description": "Hint text for the form where the users can enter their own wger instance"
},
"reset": "تنظیم مجدد",
"@reset": {
"description": "Button text allowing the user to reset the entered values to the default"
},
"registerInstead": "حساب کاربری ندارید؟ همین حالا ثبت نام کنید",
"@registerInstead": {},
"loginInstead": "حساب کاربری دارید؟ وارد شوید",
"@loginInstead": {},
"nutritionalPlan": "برنامه غذایی",
"@nutritionalPlan": {},
"labelBottomNavWorkout": "تمرین ورزشی",
"@labelBottomNavWorkout": {
"description": "Label used in bottom navigation, use a short word"
},
"labelBottomNavNutrition": "تغذیه",
"@labelBottomNavNutrition": {
"description": "Label used in bottom navigation, use a short word"
},
"labelWorkoutLogs": "گزارش تمرینات",
"@labelWorkoutLogs": {
"description": "(Workout) logs"
},
"labelWorkoutPlan": "برنامه تمرین ورزشی",
"@labelWorkoutPlan": {
"description": "Title for screen workout plan"
},
"labelDashboard": "داشبورد",
"@labelDashboard": {
"description": "Title for screen dashboard"
},
"success": "موفقیت",
"@success": {
"description": "Message when an action completed successfully, usually used as a heading"
},
"successfullyDeleted": "حذف شده",
"@successfullyDeleted": {
"description": "Message when an item was successfully deleted"
},
"successfullySaved": "ذخیره شده",
"@successfullySaved": {
"description": "Message when an item was successfully saved"
},
"notes": "یادداشت‌ها",
"@notes": {
"description": "Personal notes, e.g. for a workout session"
},
"exerciseList": "لیست تمرینات",
"@exerciseList": {},
"value": "مقدار",
"@value": {
"description": "The value of a measurement entry"
},
"newDay": "روز جدید",
"@newDay": {},
"newSet": "مجموعه جدید",
"@newSet": {
"description": "Header when adding a new set to a workout day"
},
"selectExercises": "اگر می‌خواهید مجموعه ای از حرکات را انجام دهید، می‌توانید چندین تمرین را جستجو کنید، آنها در یک گروه قرار می‌گیرند",
"@selectExercises": {},
"gymMode": "حالت باشگاه",
"@gymMode": {
"description": "Label when starting the gym mode"
},
"plateCalculator": "دیسک ها",
"@plateCalculator": {
"description": "Label used for the plate calculator in the gym mode"
},
"plateCalculatorNotDivisible": "با دیسک های موجود نمی‌توان به وزن دلخواه رسید",
"@plateCalculatorNotDivisible": {
"description": "Error message when the current weight is not reachable with plates (e.g. 33.1 kg)"
},
"pause": "توقف",
"@pause": {
"description": "Noun, not an imperative! Label used for the pause when using the gym mode"
},
"jumpTo": "برو به",
"@jumpTo": {
"description": "Imperative. Label used in popup allowing the user to jump to a specific exercise while in the gym mode"
},
"todaysWorkout": "تمرین ورزشی امروز شما",
"@todaysWorkout": {},
"logHelpEntries": "اگر در یک روز واحد بیش از یک ورودی با همان تعداد تکرارها وجود داشته باشد ، اما وزنهای مختلف ، فقط ورودی با وزن بالاتر در نمودار نشان داده می شود.",
"@logHelpEntries": {},
"logHelpEntriesUnits": "توجه داشته باشید که فقط ورودی هایی با واحد وزنی (کیلوگرم یا پوند) و تکرارها نمودار می شوند ، ترکیبات دیگری مانند زمان یا تا زمان خستگی در اینجا نادیده گرفته می شوند.",
"@logHelpEntriesUnits": {},
"description": "توضیحات",
"@description": {},
"name": "نام",
"@name": {
"description": "Name for a workout or nutritional plan"
},
"save": "ذخیره",
"@save": {},
"verify": "تأیید کردن",
"@verify": {},
"addSet": "افزودن مجموعه",
"@addSet": {
"description": "Label for the button that adds a set (to a workout day)"
},
"addMeal": "افزودن وعده",
"@addMeal": {},
"mealLogged": "غذا به دفتر گزارشات وارد شد",
"@mealLogged": {},
"ingredientLogged": "مواد به دفتر گزارشات وارد شد",
"@ingredientLogged": {},
"logMeal": "افزودن وعده به دفتر گزارشات غذایی",
"@logMeal": {},
"addIngredient": "افزودن مواد تشکیل دهنده",
"@addIngredient": {},
"logIngredient": "افزودن مواد تشکیل دهنده به دفتر گزارشات غذایی",
"@logIngredient": {},
"searchIngredient": "جستجو مواد تشکیل دهنده",
"@searchIngredient": {
"description": "Label on ingredient search form"
},
"nutritionalDiary": "دفتر گزارشات غذایی",
"@nutritionalDiary": {},
"nutritionalPlans": "برنامه های غذایی",
"@nutritionalPlans": {},
"noNutritionalPlans": "شما هیچ برنامه غذایی ندارید",
"@noNutritionalPlans": {
"description": "Message shown when the user has no nutritional plans"
},
"onlyLogging": "فقط کالری را شمارش کردن",
"@onlyLogging": {},
"onlyLoggingHelpText": "اگر فقط می خواهید کالری خود را گزارش کنید و نمی خواهید یک برنامه غذایی دقیق را با وعده های غذایی خاص تنظیم کنید ، کادر را تیک بزنید",
"@onlyLoggingHelpText": {},
"goalMacro": "اهداف مواد مغذی",
"@goalMacro": {
"description": "The goal for macronutrients"
},
"selectMealToLog": "یک وعده انتخاب کنید تا به گزارشات اضافه شود",
"@selectMealToLog": {},
"yourCurrentNutritionPlanHasNoMealsDefinedYet": "در برنامه غذایی فعلی شما هیچ وعده ای تعریف نشده است",
"@yourCurrentNutritionPlanHasNoMealsDefinedYet": {
"description": "Message shown when a nutrition plan doesn't have any meals"
},
"toAddMealsToThePlanGoToNutritionalPlanDetails": "برای افزودن وعده های غذایی به برنامه ، به جزئیات برنامه غذایی بروید",
"@toAddMealsToThePlanGoToNutritionalPlanDetails": {
"description": "Message shown to guide users to the nutritional plan details page to add meals"
},
"goalEnergy": "هدف انرژی",
"@goalEnergy": {},
"goalProtein": "هدف پروتئینی",
"@goalProtein": {},
"goalCarbohydrates": "هدف کربوهیدراتی",
"@goalCarbohydrates": {},
"goalFat": "هدف چربی",
"@goalFat": {},
"goalFiber": "هدف فیبری",
"@goalFiber": {},
"anErrorOccurred": "خطایی رخ داد!",
"@anErrorOccurred": {},
"errorInfoDescription": "متأسفیم ، اما مشکلی پیش آمد. شما می توانید با گزارش آن در GitHub به ما در رفع این مشکل کمک کنید.",
"@errorInfoDescription": {},
"errorInfoDescription2": "شما می توانید به استفاده از برنامه ادامه دهید ، اما برخی از ویژگی ها ممکن است کار نکند.",
"@errorInfoDescription2": {},
"errorViewDetails": "جزئیات فنی",
"@errorViewDetails": {},
"applicationLogs": "گزارشات اپلیکیشن",
"@applicationLogs": {},
"errorCouldNotConnectToServer": "اتصال به سرور امکان پذیر نیست",
"@errorCouldNotConnectToServer": {},
"errorCouldNotConnectToServerDetails": "برنامه نمی تواند به سرور وصل شود. لطفاً اتصال اینترنت یا URL سرور خود را بررسی کرده و دوباره امتحان کنید. اگر مشکل ادامه پیدا کرد، با مدیر سرور تماس بگیرید.",
"@errorCouldNotConnectToServerDetails": {},
"copyToClipboard": "کپی در کلیپ بورد",
"@copyToClipboard": {},
"weight": "وزن",
"@weight": {
"description": "The weight of a workout log or body weight entry"
},
"min": "حداقل",
"@min": {},
"max": "حداکثر",
"@max": {},
"chartAllTimeTitle": "نمودار کلی {name}",
"@chartAllTimeTitle": {
"description": "All-time chart of 'name' (e.g. 'weight', 'body fat' etc.)",
"type": "text",
"placeholders": {
"name": {
"type": "String"
}
}
},
"chart30DaysTitle": "نمودار 30 روزه {name}",
"@chart30DaysTitle": {
"description": "last 30 days chart of 'name' (e.g. 'weight', 'body fat' etc.)",
"type": "text",
"placeholders": {
"name": {
"type": "String"
}
}
},
"chartDuringPlanTitle": "نمودار {chartName} در برنامه {planName}",
"@chartDuringPlanTitle": {
"description": "chart of 'chartName' (e.g. 'weight', 'body fat' etc.) logged during plan",
"type": "text",
"placeholders": {
"chartName": {
"type": "String"
},
"planName": {
"type": "String"
}
}
},
"measurement": "اندازه گیری",
"@measurement": {},
"measurements": "اندازه گیری ها",
"@measurements": {
"description": "Categories for the measurements such as biceps size, body fat, etc."
},
"measurementCategoriesHelpText": "دسته اندازه گیری ، مانند \"دوسر\" یا \"چربی بدن\"",
"@measurementCategoriesHelpText": {},
"measurementEntriesHelpText": "واحد مورد استفاده برای اندازه گیری دسته مانند \"سانتی متر\" یا \"٪\"",
"@measurementEntriesHelpText": {},
"creationDate": "تاریخ شروع",
"@creationDate": {
"description": "The Start date of a nutritional plan"
},
"endDate": "تاریخ پایان",
"@endDate": {
"description": "The End date of a nutritional plan"
},
"openEnded": "پایان باز",
"@openEnded": {
"description": "When a nutrition plan has no pre-defined end date"
},
"time": "زمان",
"@time": {
"description": "The time of a meal or workout"
},
"timeStart": "زمان شروع",
"@timeStart": {
"description": "The starting time of a workout"
},
"timeEnd": "زمان پایان",
"@timeEnd": {
"description": "The end time of a workout"
},
"timeStartAhead": "زمان شروع نمیتواند جلوتر از زمان پایان باشد",
"@timeStartAhead": {},
"ingredient": "مواد تشکیل دهنده",
"@ingredient": {},
"energy": "انرژی",
"@energy": {
"description": "Energy in a meal, ingredient etc. e.g. in kJ"
},
"barWeight": "وزن میله",
"@barWeight": {},
"exercise": "تمرین",
"@exercise": {
"description": "An exercise for a workout"
},
"unit": "واحد",
"@unit": {
"description": "The unit used for a repetition (kg, time, etc.)"
},
"exercises": "تمرین ها",
"@exercises": {
"description": "Multiple exercises for a workout"
},
"exerciseName": "نام تمرین",
"@exerciseName": {
"description": "Label for the name of a workout exercise"
},
"searchExercise": "تمرین را برای افزودن جستجو کنید",
"@searchExercise": {
"description": "Label on set form. Selected exercises are added to the set"
},
"noIngredientsDefined": "هنوز هیچ مواد تشکیل دهنده ای تعریف نشده",
"@noIngredientsDefined": {},
"noMatchingExerciseFound": "هیچ تمرین مطابقی پیدا نشد",
"@noMatchingExerciseFound": {
"description": "Message returned if no exercises match the searched string"
},
"searchNamesInEnglish": "اسامی را به زبان انگلیسی هم جستجو کنید",
"@searchNamesInEnglish": {},
"equipment": "تجهیزات",
"@equipment": {
"description": "Equipment needed to perform an exercise"
}
}

View File

@@ -1036,5 +1036,95 @@
"endDate": "Date de fin",
"@endDate": {},
"startDate": "Date de début",
"@startDate": {}
"@startDate": {},
"dayTypeCustom": "Personnalisé",
"@dayTypeCustom": {},
"dayTypeEnom": "Un mouvement par minute",
"@dayTypeEnom": {},
"dayTypeAmrap": "Autant de rounds que possible",
"@dayTypeAmrap": {},
"dayTypeHiit": "Entraînement fractionné de haute intensité",
"@dayTypeHiit": {},
"dayTypeTabata": "Tabata",
"@dayTypeTabata": {},
"dayTypeEdt": "Entraînement à densité croissante",
"@dayTypeEdt": {},
"dayTypeAfap": "Aussi vite que possible",
"@dayTypeAfap": {},
"slotEntryTypeNormal": "Normal",
"@slotEntryTypeNormal": {},
"slotEntryTypeDropset": "Dropset",
"@slotEntryTypeDropset": {},
"slotEntryTypeMyo": "Myo",
"@slotEntryTypeMyo": {},
"slotEntryTypePartial": "Partiel",
"@slotEntryTypePartial": {},
"slotEntryTypeForced": "Forcé",
"@slotEntryTypeForced": {},
"slotEntryTypeTut": "Temps sous tension",
"@slotEntryTypeTut": {},
"slotEntryTypeIso": "Maintien isométrique",
"@slotEntryTypeIso": {},
"slotEntryTypeJump": "Saut",
"@slotEntryTypeJump": {},
"applicationLogs": "Journaux d'application",
"@applicationLogs": {},
"openEnded": "Sans date de fin",
"@openEnded": {
"description": "When a nutrition plan has no pre-defined end date"
},
"overview": "Aperçu",
"@overview": {},
"identicalExercisePleaseDiscard": "Si vous remarquez un exercice identique à celui que vous ajoutez, veuillez supprimer votre brouillon et modifier cet exercice à la place.",
"@identicalExercisePleaseDiscard": {},
"checkInformationBeforeSubmitting": "Veuillez vérifier que les informations que vous avez saisies sont correctes avant de soumettre l'exercice",
"@checkInformationBeforeSubmitting": {},
"imageDetailsTitle": "Détails de l'image",
"@imageDetailsTitle": {
"description": "Title for image details form"
},
"dayTypeRft": "Rounds for time",
"@dayTypeRft": {},
"imageDetailsLicenseTitle": "Valeur de l'attribut \"title\" de l'image",
"@imageDetailsLicenseTitle": {
"description": "Label for image title field"
},
"imageDetailsLicenseTitleHint": "Saisir la valeur de l'attribut \"title\" de l'image",
"@imageDetailsLicenseTitleHint": {
"description": "Hint text for image title field"
},
"imageDetailsSourceLink": "Lien vers le site internet source",
"@imageDetailsSourceLink": {
"description": "Label for source link field"
},
"author": "Auteur(s)",
"@author": {},
"authorHint": "Saisir le nom de l'auteur",
"@authorHint": {
"description": "Hint text for author field"
},
"imageDetailsAuthorLink": "Lien vers le site internet de l'auteur ou de son profil",
"@imageDetailsAuthorLink": {
"description": "Label for author link field"
},
"imageDetailsDerivativeSource": "Lien vers la source originale, s'il s'agit d'une œuvre dérivée",
"@imageDetailsDerivativeSource": {
"description": "Label for derivative source field"
},
"imageDetailsDerivativeHelp": "Une œuvre dérivée est basée sur une œuvre antérieure mais contient suffisamment de contenu nouveau et créatif pour lui donner droit à son propre droit dauteur.",
"@imageDetailsDerivativeHelp": {
"description": "Helper text explaining derivative works"
},
"imageDetailsImageType": "Type de l'image",
"@imageDetailsImageType": {
"description": "Label for image type selector"
},
"imageDetailsLicenseNotice": "En soumettant cette image, vous acceptez sa publication sous licence CC-BY-SA-4. L'image doit être votre propre création ou son auteur doit l'avoir publiée sous une licence compatible.",
"@imageDetailsLicenseNotice": {},
"imageDetailsLicenseNoticeLinkToLicense": "Voir le texte de la licence.",
"@imageDetailsLicenseNoticeLinkToLicense": {},
"enterTextInLanguage": "Veuillez saisir le texte dans la bonne langue!",
"@enterTextInLanguage": {},
"endWorkout": "Terminer l'entraînement",
"@endWorkout": {}
}

View File

@@ -31,15 +31,15 @@
},
"email": "E-mail adresa",
"@email": {},
"invalidUrl": "Upiši važeći URL",
"invalidUrl": "Upiši valjanu URL adresu",
"@invalidUrl": {
"description": "Error message when the user enters an invalid URL, e.g. in the login form"
},
"useCustomServer": "Koristi prilagođeni poslužitelj",
"useCustomServer": "Koristi prilagođeni server",
"@useCustomServer": {
"description": "Toggle button allowing users to switch between the default and a custom wger server"
},
"invalidUsername": "Upiši važeće korisničko ime",
"invalidUsername": "Upiši valjano korisničko ime",
"@invalidUsername": {
"description": "Error message when the user enters an invalid username"
},
@@ -47,7 +47,7 @@
"@customServerUrl": {
"description": "Label in the form where the users can enter their own wger instance"
},
"customServerHint": "Upiši adresu tvog poslužitelja, inače će se koristiti zadani",
"customServerHint": "Upiši adresu tvog servera, inače će se koristiti standardni",
"@customServerHint": {
"description": "Hint text for the form where the users can enter their own wger instance"
},
@@ -205,7 +205,7 @@
"@selectExercise": {
"description": "Error message when the user hasn't selected an exercise in the form"
},
"enterCharacters": "Upiši {min} do {max} znakova",
"enterCharacters": "Upiši između {min} do {max} znaka",
"@enterCharacters": {
"description": "Error message when the user hasn't entered the correct number of characters in a form",
"type": "text",
@@ -233,7 +233,7 @@
"description": "Label shown on the slider where the user can toggle showing units and RiR",
"type": "text"
},
"enterValidNumber": "Upiši važeći broj",
"enterValidNumber": "Upiši valjani broj",
"@enterValidNumber": {
"description": "Error message when the user has submitted an invalid number (e.g. '3,.,.,.')"
},
@@ -261,7 +261,7 @@
"@dataCopied": {
"description": "Snackbar message to show on copying data to a new log entry"
},
"usernameValidChars": "Korisničko ime može sadržavati samo slova, brojeve i sljedeće znakove: @, +, ., -, _",
"usernameValidChars": "Korisničko ime smije sadržavati samo slova, brojeve i sljedeće znakove: @, +, ., -, _",
"@usernameValidChars": {
"description": "Error message when the user tries to register a username with forbidden characters"
},
@@ -275,7 +275,7 @@
"@addMeal": {},
"nutritionalPlan": "Plan prehrane",
"@nutritionalPlan": {},
"useDefaultServer": "Koristi zadani poslužitelj",
"useDefaultServer": "Koristi standardni server",
"@useDefaultServer": {
"description": "Toggle button allowing users to switch between the default and a custom wger server"
},
@@ -461,7 +461,7 @@
"@weightUnit": {},
"appUpdateTitle": "Potrebna je nova verzija",
"@appUpdateTitle": {},
"appUpdateContent": "Ova verzija aplikacije nije kompatibilna s poslužiteljem. Aktualiziraj svoju aplikaciju.",
"appUpdateContent": "Ova verzija aplikacije nije kompatibilna sa serverom. Aktualiziraj svoju aplikaciju.",
"@appUpdateContent": {},
"set": "Serija",
"@set": {
@@ -519,7 +519,7 @@
"@close": {
"description": "Translation for close"
},
"enterMinCharacters": "Upiši barem {min} znakova",
"enterMinCharacters": "Upiši barem {min} znaka",
"@enterMinCharacters": {
"description": "Error message when the user hasn't entered the minimum amount characters in a form",
"type": "text",
@@ -608,7 +608,7 @@
"@verifiedEmail": {},
"unVerifiedEmail": "Nepotvrđena e-mail adresa",
"@unVerifiedEmail": {},
"verifiedEmailInfo": "E-mail poruka za potvrdu je poslana na {email}",
"verifiedEmailInfo": "E-mail za ovjeru je poslan na {email}",
"@verifiedEmailInfo": {
"placeholders": {
"email": {
@@ -634,7 +634,7 @@
"@cardio": {
"description": "Generated entry for translation for server strings"
},
"quads": "Ekstenzije nogu",
"quads": "Kvadriceps",
"@quads": {
"description": "Generated entry for translation for server strings"
},
@@ -658,7 +658,7 @@
"@until_failure": {
"description": "Generated entry for translation for server strings"
},
"none__bodyweight_exercise_": "ništa (vježba za tjelesnu težinu)",
"none__bodyweight_exercise_": "bez (vježba za tjelesnu težinu)",
"@none__bodyweight_exercise_": {
"description": "Generated entry for translation for server strings"
},
@@ -710,7 +710,7 @@
"@kg": {
"description": "Generated entry for translation for server strings"
},
"lb": "funta",
"lb": "lb",
"@lb": {
"description": "Generated entry for translation for server strings"
},
@@ -718,7 +718,7 @@
"@searchNamesInEnglish": {},
"language": "Jezik",
"@language": {},
"aboutPageTitle": "Wger informacije",
"aboutPageTitle": "O nama i podrška",
"@aboutPageTitle": {},
"abs": "Trbuh",
"@abs": {
@@ -788,7 +788,7 @@
},
"aboutDonateTitle": "Doniraj",
"@aboutDonateTitle": {},
"aboutDonateText": "Pomogni projektu: kupi nam kavu, plati troškove poslužitelja i potiči nas u našem radu",
"aboutDonateText": "Iako je projekt besplatan i uvijek će to ostati, održavanje servera nije! Razvoj također zahtijeva značajno vrijeme i trud volontera. Tvoj doprinos izravno podupire te troškove i pomaže u održavanju pouzdanosti usluge.",
"@aboutDonateText": {},
"settingsTitle": "Postavke",
"@settingsTitle": {},
@@ -866,7 +866,7 @@
"@goalMacro": {
"description": "The goal for macronutrients"
},
"selectMealToLog": "Odaberi obrok za zapis u dnevnik",
"selectMealToLog": "Odaberi obrok za zapisivanje u dnevnik",
"@selectMealToLog": {},
"ingredientLogged": "Sastojak je upisan u dnevnik",
"@ingredientLogged": {},
@@ -880,15 +880,15 @@
"@goalTypeMeals": {
"description": "added for localization of Class GoalType's filed meals"
},
"goalTypeBasic": "Osnovni",
"goalTypeBasic": "Osnovno",
"@goalTypeBasic": {
"description": "added for localization of Class GoalType's filed basic"
},
"goalTypeAdvanced": "Napredni",
"goalTypeAdvanced": "Napredno",
"@goalTypeAdvanced": {
"description": "added for localization of Class GoalType's filed advanced"
},
"indicatorRaw": "sirovo",
"indicatorRaw": "neobrađeno",
"@indicatorRaw": {
"description": "added for localization of Class Indicator's field text"
},
@@ -914,7 +914,7 @@
}
}
},
"chartDuringPlanTitle": "{chartName} tijekom prehrambenog plana {planName}",
"chartDuringPlanTitle": "{chartName} tijekom plana prehrane {planName}",
"@chartDuringPlanTitle": {
"description": "chart of 'chartName' (e.g. 'weight', 'body fat' etc.) logged during plan",
"type": "text",
@@ -951,24 +951,244 @@
"@useUsernameAndPassword": {},
"apiToken": "API Token",
"@apiToken": {},
"invalidApiToken": "Molimo provjerite API Token",
"invalidApiToken": "Upiši valjani API ključ",
"@invalidApiToken": {
"description": "Error message when the user enters an invalid API key"
},
"apiTokenValidChars": "API Token može sadržavati a-f, brojeve od 0-9 i mora biti točno 40 znakova dugačak.",
"apiTokenValidChars": "API ključ može sadržati slova a-f, brojke od 0-9 i mora imati točno 40 znakova",
"@apiTokenValidChars": {
"description": "Error message when the user tries to input a API key with forbidden characters"
},
"routines": "Rutina",
"routines": "Rutine",
"@routines": {},
"newRoutine": "Nova rutina",
"@newRoutine": {},
"noRoutines": "Vi nemate rutine",
"noRoutines": "Ti nemaš rutine",
"@noRoutines": {},
"restTime": "Period odmora",
"restTime": "Vrijeme odmora",
"@restTime": {},
"sets": "Vježba ima setova",
"sets": "Serije",
"@sets": {
"description": "The number of sets to be done for one exercise"
}
},
"min": "Min.",
"@min": {},
"max": "Maks.",
"@max": {},
"dayTypeAfap": "Što brže moguće",
"@dayTypeAfap": {},
"slotEntryTypeNormal": "Normalno",
"@slotEntryTypeNormal": {},
"slotEntryTypePartial": "Djelomično",
"@slotEntryTypePartial": {},
"slotEntryTypeForced": "Prisiljeno",
"@slotEntryTypeForced": {},
"aboutTranslationListTitle": "Prevedi aplikaciju",
"@aboutTranslationListTitle": {},
"aboutSourceListTitle": "Prikaži izvorni kod",
"@aboutSourceListTitle": {},
"aboutJoinCommunityTitle": "Pridruži se zajednici",
"@aboutJoinCommunityTitle": {},
"aboutDiscordTitle": "Discord",
"@aboutDiscordTitle": {},
"others": "Drugi",
"@others": {},
"resultingRoutine": "Rezultirajuća rutina",
"@resultingRoutine": {},
"restDay": "Dan odmora",
"@restDay": {},
"isRestDay": "Je dan odmora",
"@isRestDay": {},
"progressionRules": "Ova vježba ima pravila napredovanja i ne može se uređivati u mobilnoj aplikaciji. Za uređivanje ove vježbe koristi web aplikaciju.",
"@progressionRules": {},
"needsLogsToAdvance": "Treba dnevnike za nastavljanje",
"@needsLogsToAdvance": {},
"slotEntryTypeTut": "Vrijeme pod napetošću",
"@slotEntryTypeTut": {},
"slotEntryTypeIso": "Izometrijsko držanje",
"@slotEntryTypeIso": {},
"slotEntryTypeJump": "Skok",
"@slotEntryTypeJump": {},
"dayTypeCustom": "Prilagođeno",
"@dayTypeCustom": {},
"dayTypeEnom": "Svake minute u minuti",
"@dayTypeEnom": {},
"startDate": "Datum početka",
"@startDate": {
"description": "The start date of a nutritional plan or routine"
},
"dayTypeRft": "Runde za vrijeme",
"@dayTypeRft": {},
"dayTypeAmrap": "Što više rundi",
"@dayTypeAmrap": {},
"dayTypeHiit": "Trening visokog intenziteta u intervalima",
"@dayTypeHiit": {},
"dayTypeTabata": "Tabata",
"@dayTypeTabata": {},
"simpleMode": "Jednostavni modus",
"@simpleMode": {},
"dayTypeEdt": "Trening s postupnim povećanjem ponavljanja/serija",
"@dayTypeEdt": {},
"slotEntryTypeDropset": "Drop set (serije sa smanjivanjem težine)",
"@slotEntryTypeDropset": {},
"slotEntryTypeMyo": "Myo (intenzivnije aktiviranje mišićnih vlakana)",
"@slotEntryTypeMyo": {},
"yourCurrentNutritionPlanHasNoMealsDefinedYet": "Tvoj trenutačni plan prehrane nema definirane obroke",
"@yourCurrentNutritionPlanHasNoMealsDefinedYet": {
"description": "Message shown when a nutrition plan doesn't have any meals"
},
"toAddMealsToThePlanGoToNutritionalPlanDetails": "Za dodavanje obroka u plan, idi na detalje plana prehrane",
"@toAddMealsToThePlanGoToNutritionalPlanDetails": {
"description": "Message shown to guide users to the nutritional plan details page to add meals"
},
"resistance_band": "Elastična vrpca",
"@resistance_band": {
"description": "Generated entry for translation for server strings"
},
"aboutWhySupportTitle": "Otvoreni kod i besplatno ❤️",
"@aboutWhySupportTitle": {},
"addSuperset": "Dodaj super-seriju",
"@addSuperset": {},
"setHasProgression": "Serija ima napredovanje",
"@setHasProgression": {},
"setHasProgressionWarning": "Imaj na umu da trenutačno nije moguće urediti sve postavke za serije na mobilnoj aplikaciji ili konfigurirati automatsko napredovanje. Za sada koristi web aplikaciju.",
"@setHasProgressionWarning": {},
"fitInWeek": "Prilagodi u tjedan",
"@fitInWeek": {},
"fitInWeekHelp": "Ako je aktivirano, dani će se ponavljati u tjednom ciklusu, inače će dani slijediti uzastopno bez obzira na početak novog tjedna.",
"@fitInWeekHelp": {},
"setHasNoExercises": "Ova serija još nema vježbe!",
"@setHasNoExercises": {},
"exerciseNr": "Vježba {nr}",
"@exerciseNr": {
"description": "Header in form indicating the number of the current exercise. Can also be translated as something like 'Set Nr. xy'.",
"type": "text",
"placeholders": {
"nr": {
"type": "String"
}
}
},
"supersetNr": "Super-serija {nr}",
"@supersetNr": {
"description": "Header in form indicating the number of the current exercise. Can also be translated as something like 'Superset Nr. xy'.",
"type": "text",
"placeholders": {
"nr": {
"type": "String"
}
}
},
"isRestDayHelp": "Imaj na umu da će se sve serije i vježbe ukloniti kada označiš dan kao dan odmora.",
"@isRestDayHelp": {},
"needsLogsToAdvanceHelp": "Odaberi želiš li da se rutina nastavi na sljedeći zakazani dan, samo ako si za taj dan zapisao/la trening",
"@needsLogsToAdvanceHelp": {},
"routineDays": "Dani u rutini",
"@routineDays": {},
"errorInfoDescription": "Žao nam je, dogodila se greška. Možeš nam pomoći to popraviti prijavom problema na GitHub-u.",
"@errorInfoDescription": {},
"errorInfoDescription2": "Možeš nastaviti koristiti aplikaciju, ali neke funkcije možda neće raditi.",
"@errorInfoDescription2": {},
"errorViewDetails": "Tehnički detalji",
"@errorViewDetails": {},
"applicationLogs": "Dnevnici aplikacije",
"@applicationLogs": {},
"errorCouldNotConnectToServer": "Neuspjelo povezivanje sa serverom",
"@errorCouldNotConnectToServer": {},
"errorCouldNotConnectToServerDetails": "Aplikacija se nije mogla povezati sa serverom. Provjeri internetsku vezu ili URL servera i pokušaj ponovo. Ako problem ne nestane, obrati se administratoru servera.",
"@errorCouldNotConnectToServerDetails": {},
"copyToClipboard": "Kopiraj u međuspremnik",
"@copyToClipboard": {},
"endDate": "Datum kraja",
"@endDate": {
"description": "The End date of a nutritional plan or routine"
},
"openEnded": "Bez datuma kraja",
"@openEnded": {
"description": "When a nutrition plan has no pre-defined end date"
},
"overview": "Pregled",
"@overview": {},
"aboutContributeTitle": "Doprinesi",
"@aboutContributeTitle": {},
"aboutContributeText": "Sve vrste doprinosa su dobrodošle. Bilo da si programer, prevoditelj ili jednostavno strastveni ljubitelj fitnessa, svaka podrška je dobrodošla!",
"@aboutContributeText": {},
"aboutBugsListTitle": "Prijavi problem ili predloži funkciju",
"@aboutBugsListTitle": {},
"identicalExercisePleaseDiscard": "Ako primijetiš vježbu koja je identična onoj koju dodaješ, odbaci svoju vježbu i umjesto toga uredi tu vježbu.",
"@identicalExercisePleaseDiscard": {},
"checkInformationBeforeSubmitting": "Provjeri točnost tvojih unesenih podataka prije slanja vježbe",
"@checkInformationBeforeSubmitting": {},
"imageDetailsTitle": "Detalji slike",
"@imageDetailsTitle": {
"description": "Title for image details form"
},
"imageDetailsLicenseTitle": "Naslov",
"@imageDetailsLicenseTitle": {
"description": "Label for image title field"
},
"imageDetailsLicenseTitleHint": "Upiši naslov slike",
"@imageDetailsLicenseTitleHint": {
"description": "Hint text for image title field"
},
"imageDetailsSourceLink": "Poveznica na izvornu web-stranicu",
"@imageDetailsSourceLink": {
"description": "Label for source link field"
},
"author": "Autori",
"@author": {},
"authorHint": "Upiši ime autora",
"@authorHint": {
"description": "Hint text for author field"
},
"imageDetailsAuthorLink": "Poveznica na web-stranicu ili profil autora",
"@imageDetailsAuthorLink": {
"description": "Label for author link field"
},
"imageDetailsDerivativeSource": "Poveznica na izvorni izvor, ako je ovo izvedeno djelo",
"@imageDetailsDerivativeSource": {
"description": "Label for derivative source field"
},
"imageDetailsDerivativeHelp": "Izvedeno djelo se temelji na prethodnom djelu, ali sadrži dovoljno novog, kreativnog sadržaja da bi mu se dalo pravo na vlastita autorska prava.",
"@imageDetailsDerivativeHelp": {
"description": "Helper text explaining derivative works"
},
"imageDetailsImageType": "Vrsta slike",
"@imageDetailsImageType": {
"description": "Label for image type selector"
},
"imageDetailsLicenseNotice": "Slanjem ove slike pristaješ na njezino objavljivanje pod licencom CC-BY-SA-4. Slika mora biti tvoje vlastito djelo ili ju je autor morao objaviti pod s njom kompatibilnom licencom.",
"@imageDetailsLicenseNotice": {},
"imageDetailsLicenseNoticeLinkToLicense": "Pogledaj tekst licence.",
"@imageDetailsLicenseNoticeLinkToLicense": {},
"imageFormatNotSupported": "{imageFormat} nije podržani format",
"@imageFormatNotSupported": {
"description": "Label shown on the error container when image format is not supported",
"type": "text",
"placeholders": {
"imageFormat": {
"type": "String"
}
}
},
"imageFormatNotSupportedDetail": "{imageFormat} slike još nisu podržane.",
"@imageFormatNotSupportedDetail": {
"description": "Label shown on the image preview container when image format is not supported",
"type": "text",
"placeholders": {
"imageFormat": {
"type": "String"
}
}
},
"add": "dodaj",
"@add": {
"description": "Add button text"
},
"enterTextInLanguage": "Upiši tekst u ispravnom jeziku!",
"@enterTextInLanguage": {},
"simpleModeHelp": "Sakrij neke naprednije postavke prilikom uređivanja vježbi",
"@simpleModeHelp": {},
"endWorkout": "Završi trening",
"@endWorkout": {}
}

View File

@@ -1076,5 +1076,29 @@
"aboutDiscordTitle": "Discord",
"@aboutDiscordTitle": {},
"fitInWeek": "Settimana Fit in",
"@fitInWeek": {}
"@fitInWeek": {},
"creationDate": "Data di inizio",
"@creationDate": {
"description": "The Start date of a nutritional plan"
},
"openEnded": "Senza fine",
"@openEnded": {
"description": "When a nutrition plan has no pre-defined end date"
},
"overview": "Panoramica",
"@overview": {},
"identicalExercisePleaseDiscard": "Se trovate un esercizio identico a quello che state cercando di aggiungere, per favore scartate la vostra bozza e se necessario modificate l'esercizio esistente.",
"@identicalExercisePleaseDiscard": {},
"checkInformationBeforeSubmitting": "Verificate che le informazioni siano corrette prima di aggiungere l'esercizio",
"@checkInformationBeforeSubmitting": {},
"enterTextInLanguage": "Aggiungete il testo usando la lingua giusta!",
"@enterTextInLanguage": {},
"applicationLogs": "Registri dell'applicazione",
"@applicationLogs": {},
"dayTypeCustom": "Personalizzato",
"@dayTypeCustom": {},
"dayTypeAfap": "Più velocemente possibile",
"@dayTypeAfap": {},
"dayTypeHiit": "Allenamento a intervalli ad alta intensità",
"@dayTypeHiit": {}
}

View File

@@ -666,13 +666,13 @@
"@add_exercise_image_license": {},
"selectEntry": "Por favor selecione uma entrada",
"@selectEntry": {},
"cacheWarning": "Devido ao caching, pode levar algum tempo até que as alterações sejam visíveis em todo o aplicativo.",
"cacheWarning": "Devido ao \"caching\", pode levar algum tempo até que as alterações sejam visíveis em todo o aplicativo.",
"@cacheWarning": {},
"success": "Sucesso",
"@success": {
"description": "Message when an action completed successfully, usually used as a heading"
},
"contributeExerciseWarning": "Você só pode contribuir com exercícios se sua conta tiver mais de {days} dias e tiver verificado seu e-mail",
"contributeExerciseWarning": "Só podes contribuir com exercícios se a tua conta tiver mais de {days} dias e tiveres verificado o teu e-mail",
"@contributeExerciseWarning": {
"description": "Number of days before which a person can add exercise",
"placeholders": {
@@ -722,13 +722,13 @@
},
"settingsCacheTitle": "Cache",
"@settingsCacheTitle": {},
"settingsExerciseCacheDescription": "Cache do exercício",
"settingsExerciseCacheDescription": "Arquivo de exercícios",
"@settingsExerciseCacheDescription": {},
"useMetric": "Use unidades métricas para o peso corpora",
"@useMetric": {},
"settingsTitle": "Configurações",
"@settingsTitle": {},
"settingsCacheDeletedSnackbar": "Cache limpo com sucesso",
"settingsCacheDeletedSnackbar": "Cache limpa com sucesso",
"@settingsCacheDeletedSnackbar": {},
"log": "Log",
"@log": {
@@ -861,7 +861,7 @@
"@goalTypeAdvanced": {
"description": "added for localization of Class GoalType's filed advanced"
},
"indicatorRaw": "Crua",
"indicatorRaw": "Crú",
"@indicatorRaw": {
"description": "added for localization of Class Indicator's field text"
},
@@ -877,7 +877,7 @@
"@themeMode": {},
"darkMode": "Modo sempre escuro",
"@darkMode": {},
"settingsIngredientCacheDescription": "Princípio de cache",
"settingsIngredientCacheDescription": "Arquivo de ingredientes",
"@settingsIngredientCacheDescription": {},
"routines": "Rotinas",
"@routines": {},
@@ -981,7 +981,7 @@
"@setHasProgression": {},
"simpleMode": "Modo simples",
"@simpleMode": {},
"progressionRules": "Este exercíco tem regras de progressão e não pode ser editado na aplicação móvel. Por favor, usa a aplicação web para editar este exercício.",
"progressionRules": "Este exercício tem regras de progressão e não pode ser editado na aplicação móvel. Por favor, usa a aplicação web para editar este exercício.",
"@progressionRules": {},
"selectAvailablePlates": "Seleciona anilhas disponíveis",
"@selectAvailablePlates": {},
@@ -1010,5 +1010,27 @@
"setHasProgressionWarning": "Por favor, nota que, de momento, não é possível editar todos os valores para uma série na aplicação móvel ou configurar a progressão automática. Por agora, por favor, usa a aplicação web.",
"@setHasProgressionWarning": {},
"startDate": "Data de início",
"@startDate": {}
"@startDate": {},
"endDate": "Data de término",
"@endDate": {
"description": "The End date of a nutritional plan"
},
"applicationLogs": "Registos da aplicação",
"@applicationLogs": {},
"creationDate": "Data de início",
"@creationDate": {
"description": "The Start date of a nutritional plan"
},
"openEnded": "Sem fim definido",
"@openEnded": {
"description": "When a nutrition plan has no pre-defined end date"
},
"enterTextInLanguage": "Por favor, introduz o texto la linguagem correta!",
"@enterTextInLanguage": {},
"checkInformationBeforeSubmitting": "Por favor, verifica que a informação introduzida está correta antes de submeter o exercício",
"@checkInformationBeforeSubmitting": {},
"identicalExercisePleaseDiscard": "Se encontrares um exercício igual ao que estás a introduzir, por favor descarta o teu rascunho e edita antes esse exercício.",
"@identicalExercisePleaseDiscard": {},
"overview": "Panorama",
"@overview": {}
}

View File

@@ -1068,5 +1068,11 @@
"lightMode": "Sempre modo claro",
"@lightMode": {},
"systemMode": "Configurações do sistema",
"@systemMode": {}
"@systemMode": {},
"endDate": "Data final",
"@endDate": {
"description": "The End date of a nutritional plan"
},
"startDate": "Data inicial",
"@startDate": {}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1 +1,128 @@
{}
{
"userProfile": "โปรไฟล์ของคุณ",
"@userProfile": {},
"login": "เข้าสู่ระบบ",
"@login": {
"description": "Text for login button"
},
"logout": "ออกจากระบบ",
"@logout": {
"description": "Text for logout button"
},
"register": "ลงทะเบียน",
"@register": {
"description": "Text for registration button"
},
"useDefaultServer": "ใช้เซิร์ฟเวอร์เริ่มต้น",
"@useDefaultServer": {
"description": "Toggle button allowing users to switch between the default and a custom wger server"
},
"useCustomServer": "ใช้เซิร์ฟเวอร์ที่กำหนดเอง",
"@useCustomServer": {
"description": "Toggle button allowing users to switch between the default and a custom wger server"
},
"invalidUrl": "กรุณาป้อน URL ที่ถูกต้อง",
"@invalidUrl": {
"description": "Error message when the user enters an invalid URL, e.g. in the login form"
},
"usernameValidChars": "ชื่อผู้ใช้สามารถประกอบด้วยตัวอักษร ตัวเลข และอักขระ @, +, ., - และ _ เท่านั้น",
"@usernameValidChars": {
"description": "Error message when the user tries to register a username with forbidden characters"
},
"passwordsDontMatch": "รหัสผ่านไม่ตรงกัน",
"@passwordsDontMatch": {
"description": "Error message when the user enters two different passwords during registration"
},
"passwordTooShort": "รหัสผ่านสั้นเกินไป",
"@passwordTooShort": {
"description": "Error message when the user a password that is too short"
},
"selectAvailablePlates": "เลือกแผ่นน้ำหนักที่มี",
"@selectAvailablePlates": {},
"barWeight": "น้ำหนัก Bar",
"@barWeight": {},
"password": "รหัสผ่าน",
"@password": {},
"confirmPassword": "ยืนยันรหัสผ่าน",
"@confirmPassword": {},
"invalidEmail": "กรุณากรอกที่อยู่อีเมลที่ถูกต้อง",
"@invalidEmail": {
"description": "Error message when the user enters an invalid email"
},
"email": "ที่อยู่อีเมล",
"@email": {},
"username": "ชื่อผู้ใช้",
"@username": {},
"invalidUsername": "กรุณากรอกชื่อผู้ใช้ที่ถูกต้อง",
"@invalidUsername": {
"description": "Error message when the user enters an invalid username"
},
"useApiToken": "ใช้โทเค็น API",
"@useApiToken": {},
"useUsernameAndPassword": "ใช้ชื่อผู้ใช้และรหัสผ่าน",
"@useUsernameAndPassword": {},
"apiToken": "โทเค็น API",
"@apiToken": {},
"invalidApiToken": "กรุณาป้อนรหัส API ที่ถูกต้อง",
"@invalidApiToken": {
"description": "Error message when the user enters an invalid API key"
},
"apiTokenValidChars": "คีย์ API จะต้องประกอบด้วยตัวอักษร a-f, ตัวเลข 0-9 และมีความยาว 40 อักขระเท่านั้น",
"@apiTokenValidChars": {
"description": "Error message when the user tries to input a API key with forbidden characters"
},
"customServerHint": "กรอกที่อยู่เซิร์ฟเวอร์ของคุณ มิฉะนั้นจะใช้ค่าเริ่มต้น",
"@customServerHint": {
"description": "Hint text for the form where the users can enter their own wger instance"
},
"reset": "รีเซ็ต",
"@reset": {
"description": "Button text allowing the user to reset the entered values to the default"
},
"registerInstead": "ยังไม่มีบัญชีใช่ไหม? ลงทะเบียนเลย",
"@registerInstead": {},
"loginInstead": "มีบัญชีอยู่แล้ว? เข้าสู่ระบบ",
"@loginInstead": {},
"labelBottomNavWorkout": "การออกกำลังกาย",
"@labelBottomNavWorkout": {
"description": "Label used in bottom navigation, use a short word"
},
"labelBottomNavNutrition": "โภชนาการ",
"@labelBottomNavNutrition": {
"description": "Label used in bottom navigation, use a short word"
},
"labelWorkoutLogs": "ข้อมูลการออกกำลังกาย",
"@labelWorkoutLogs": {
"description": "(Workout) logs"
},
"labelWorkoutPlan": "แผนการออกกำลังกาย",
"@labelWorkoutPlan": {
"description": "Title for screen workout plan"
},
"labelDashboard": "แดชบอร์ด",
"@labelDashboard": {
"description": "Title for screen dashboard"
},
"success": "สำเร็จ",
"@success": {
"description": "Message when an action completed successfully, usually used as a heading"
},
"successfullyDeleted": "ลบเรียบร้อย",
"@successfullyDeleted": {
"description": "Message when an item was successfully deleted"
},
"successfullySaved": "บันทึกสำเร็จ",
"@successfullySaved": {
"description": "Message when an item was successfully saved"
},
"exerciseList": "รายการการออกกำลังกาย",
"@exerciseList": {},
"exercise": "ออกกำลังกาย",
"@exercise": {
"description": "An exercise for a workout"
},
"exercises": "ออกกำลังกาย",
"@exercises": {
"description": "Multiple exercises for a workout"
}
}

View File

@@ -1034,5 +1034,133 @@
"startDate": "Дата початку",
"@startDate": {},
"applicationLogs": "Журнали програм",
"@applicationLogs": {}
"@applicationLogs": {},
"creationDate": "Дата початку",
"@creationDate": {
"description": "The Start date of a nutritional plan"
},
"openEnded": "Відкритий",
"@openEnded": {
"description": "When a nutrition plan has no pre-defined end date"
},
"overview": "Огляд",
"@overview": {},
"identicalExercisePleaseDiscard": "Якщо ви помітили вправу, ідентичну тій, яку ви додаєте, будь ласка, відкиньте свій чернетку та відредагуйте цю вправу.",
"@identicalExercisePleaseDiscard": {},
"checkInformationBeforeSubmitting": "Будь ласка, перевірте правильність введеної вами інформації, перш ніж надсилати вправу",
"@checkInformationBeforeSubmitting": {},
"enterTextInLanguage": "Будь ласка, введіть текст правильною мовою!",
"@enterTextInLanguage": {},
"imageDetailsTitle": "Деталі зображення",
"@imageDetailsTitle": {
"description": "Title for image details form"
},
"imageDetailsLicenseTitle": "Назва",
"@imageDetailsLicenseTitle": {
"description": "Label for image title field"
},
"imageDetailsLicenseTitleHint": "Введіть назву зображення",
"@imageDetailsLicenseTitleHint": {
"description": "Hint text for image title field"
},
"imageDetailsSourceLink": "Посилання на веб-сайт джерела",
"@imageDetailsSourceLink": {
"description": "Label for source link field"
},
"imageDetailsAuthor": "Автор(и)",
"@imageDetailsAuthor": {
"description": "Label for author field"
},
"imageDetailsAuthorHint": "Введіть ім'я автора",
"@imageDetailsAuthorHint": {
"description": "Hint text for author field"
},
"imageDetailsAuthorLink": "Посилання на веб-сайт або профіль автора",
"@imageDetailsAuthorLink": {
"description": "Label for author link field"
},
"imageDetailsDerivativeSource": "Посилання на оригінальне джерело, якщо це похідний твір",
"@imageDetailsDerivativeSource": {
"description": "Label for derivative source field"
},
"imageDetailsDerivativeHelp": "Похідний твір базується на попередньому творі, але містить достатньо нового, творчого контенту, щоб мати право на власне авторське право.",
"@imageDetailsDerivativeHelp": {
"description": "Helper text explaining derivative works"
},
"imageDetailsImageType": "Тип зображення",
"@imageDetailsImageType": {
"description": "Label for image type selector"
},
"imageDetailsLicenseNoticePrefix": "Надсилаючи це зображення, ви погоджуєтеся на його публікацію відповідно до ",
"@imageDetailsLicenseNoticePrefix": {
"description": "First part of license notice text"
},
"imageDetailsLicenseNoticeSuffix": " Зображення має бути або вашою власною роботою, або автор має опублікувати його за ліцензією, сумісною з CC BY-SA 4.0.",
"@imageDetailsLicenseNoticeSuffix": {
"description": "Second part of license notice text"
},
"add": "додати",
"@add": {
"description": "Add button text"
},
"imageDetailsLicenseNotice": "Надсилаючи це зображення, ви погоджуєтеся на його розповсюдження за ліцензією CC-BY-SA-4. Зображення має бути або вашою власною роботою, або автор має опублікувати його за ліцензією, сумісною з нею.",
"@imageDetailsLicenseNotice": {},
"imageDetailsLicenseNoticeLinkToLicense": "Див. текст ліцензії.",
"@imageDetailsLicenseNoticeLinkToLicense": {},
"author": "Автор(и)",
"@author": {},
"authorHint": "Введіть ім'я автора",
"@authorHint": {
"description": "Hint text for author field"
},
"galleryImageTypeNotSupported": "Зображення типу {imageType} наразі не підтримуються на цій платформі.",
"@galleryImageTypeNotSupported": {
"placeholders": {
"imageType": {
"type": "String"
}
}
},
"galleryImageTypeNotSupportedDetail": "Це зображення у форматі {imageType}, який наразі не підтримується на цій платформі.",
"@galleryImageTypeNotSupportedDetail": {
"placeholders": {
"imageType": {
"type": "String"
}
}
},
"dayTypeCustom": "Користувацька",
"@dayTypeCustom": {},
"dayTypeEnom": "Кожну хвилину за хвилиною",
"@dayTypeEnom": {},
"dayTypeAmrap": "Якомога більше раундів",
"@dayTypeAmrap": {},
"dayTypeHiit": "Високоінтенсивне інтервальне тренування",
"@dayTypeHiit": {},
"dayTypeTabata": "Табата",
"@dayTypeTabata": {},
"dayTypeEdt": "Збільшення щільності навчання",
"@dayTypeEdt": {},
"dayTypeRft": "Раунди на час",
"@dayTypeRft": {},
"dayTypeAfap": "Якомога швидше",
"@dayTypeAfap": {},
"slotEntryTypeNormal": "Звичайний",
"@slotEntryTypeNormal": {},
"slotEntryTypeDropset": "Дропсет",
"@slotEntryTypeDropset": {},
"slotEntryTypeMyo": "Міо",
"@slotEntryTypeMyo": {},
"slotEntryTypePartial": "Часткове",
"@slotEntryTypePartial": {},
"slotEntryTypeForced": "Примусово",
"@slotEntryTypeForced": {},
"slotEntryTypeTut": "Час під Напруги",
"@slotEntryTypeTut": {},
"slotEntryTypeIso": "Ізометричне утримання",
"@slotEntryTypeIso": {},
"slotEntryTypeJump": "Стрибок",
"@slotEntryTypeJump": {},
"endWorkout": "Закінчити тренування",
"@endWorkout": {}
}

View File

@@ -1,6 +1,6 @@
/*
* This file is part of wger Workout Manager <https://github.com/wger-project>.
* Copyright (c) 2020, wger Team
* Copyright (C) 2020, 2025 wger Team
*
* wger Workout Manager is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by

View File

@@ -26,7 +26,7 @@ class Image {
@JsonKey(required: true)
int? id;
@JsonKey(required: true, toJson: dateToYYYYMMDD)
@JsonKey(required: true, fromJson: utcIso8601ToLocalDate, toJson: dateToUtcIso8601)
late DateTime date;
@JsonKey(required: true, name: 'image')

View File

@@ -10,7 +10,7 @@ Image _$ImageFromJson(Map<String, dynamic> json) {
$checkKeys(json, requiredKeys: const ['id', 'date', 'image']);
return Image(
id: (json['id'] as num?)?.toInt(),
date: DateTime.parse(json['date'] as String),
date: utcIso8601ToLocalDate(json['date'] as String),
url: json['image'] as String?,
description: json['description'] as String? ?? '',
);
@@ -18,7 +18,7 @@ Image _$ImageFromJson(Map<String, dynamic> json) {
Map<String, dynamic> _$ImageToJson(Image instance) => <String, dynamic>{
'id': instance.id,
'date': dateToYYYYMMDD(instance.date),
'date': dateToUtcIso8601(instance.date),
'image': instance.url,
'description': instance.description,
};

View File

@@ -36,7 +36,7 @@ class Log {
@JsonKey(required: true, name: 'plan')
int planId;
@JsonKey(required: true, toJson: dateToUtcIso8601)
@JsonKey(required: true, fromJson: utcIso8601ToLocalDate, toJson: dateToUtcIso8601)
late DateTime datetime;
String? comment;

View File

@@ -25,7 +25,7 @@ Log _$LogFromJson(Map<String, dynamic> json) {
weightUnitId: (json['weight_unit'] as num?)?.toInt(),
amount: stringToNum(json['amount'] as String?),
planId: (json['plan'] as num).toInt(),
datetime: DateTime.parse(json['datetime'] as String),
datetime: utcIso8601ToLocalDate(json['datetime'] as String),
comment: json['comment'] as String?,
);
}

View File

@@ -41,7 +41,12 @@ class NutritionalPlan {
@JsonKey(required: true)
late String description;
@JsonKey(required: true, name: 'creation_date', toJson: dateToUtcIso8601)
@JsonKey(
required: true,
name: 'creation_date',
fromJson: utcIso8601ToLocalDate,
toJson: dateToUtcIso8601,
)
late DateTime creationDate;
@JsonKey(required: true, name: 'start', toJson: dateToYYYYMMDD)

View File

@@ -26,9 +26,7 @@ NutritionalPlan _$NutritionalPlanFromJson(Map<String, dynamic> json) {
return NutritionalPlan(
id: (json['id'] as num?)?.toInt(),
description: json['description'] as String,
creationDate: json['creation_date'] == null
? null
: DateTime.parse(json['creation_date'] as String),
creationDate: utcIso8601ToLocalDate(json['creation_date'] as String),
startDate: DateTime.parse(json['start'] as String),
endDate: json['end'] == null ? null : DateTime.parse(json['end'] as String),
onlyLogging: json['only_logging'] as bool? ?? false,

View File

@@ -1,6 +1,6 @@
/*
* This file is part of wger Workout Manager <https://github.com/wger-project>.
* Copyright (c) 2020, wger Team
* Copyright (C) 2020, 2025 wger Team
*
* wger Workout Manager is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by

View File

@@ -42,7 +42,7 @@ class Routine {
@JsonKey(required: true, includeToJson: false)
int? id;
@JsonKey(required: true, toJson: dateToUtcIso8601)
@JsonKey(required: true, fromJson: utcIso8601ToLocalDate, toJson: dateToUtcIso8601)
late DateTime created;
@JsonKey(required: true, name: 'name')

View File

@@ -21,7 +21,7 @@ Routine _$RoutineFromJson(Map<String, dynamic> json) {
);
return Routine(
id: (json['id'] as num?)?.toInt(),
created: json['created'] == null ? null : DateTime.parse(json['created'] as String),
created: utcIso8601ToLocalDate(json['created'] as String),
name: json['name'] as String,
start: json['start'] == null ? null : DateTime.parse(json['start'] as String),
end: json['end'] == null ? null : DateTime.parse(json['end'] as String),

View File

@@ -302,6 +302,37 @@ class NutritionPlansProvider with ChangeNotifier {
await database.deleteEverything();
}
/// Saves an ingredient to the cache
Future<void> cacheIngredient(Ingredient ingredient, {IngredientDatabase? database}) async {
database ??= this.database;
if (!ingredients.any((e) => e.id == ingredient.id)) {
ingredients.add(ingredient);
}
final ingredientDb = await (database.select(
database.ingredients,
)..where((e) => e.id.equals(ingredient.id))).getSingleOrNull();
if (ingredientDb == null) {
final data = ingredient.toJson();
try {
await database
.into(database.ingredients)
.insert(
IngredientsCompanion.insert(
id: ingredient.id,
data: jsonEncode(data),
lastFetched: DateTime.now(),
),
);
_logger.finer("Saved ingredient '${ingredient.name}' to db cache");
} catch (e) {
_logger.finer("Error caching ingredient '${ingredient.name}': $e");
}
}
}
/// Fetch and return an ingredient
///
/// If the ingredient is not known locally, it is fetched from the server
@@ -329,22 +360,14 @@ class NutritionPlansProvider with ChangeNotifier {
(database.delete(database.ingredients)..where((i) => i.id.equals(ingredientId))).go();
}
} else {
_logger.info("Fetching ingredient ID $ingredientId from server");
final data = await baseProvider.fetch(
baseProvider.makeUrl(_ingredientInfoPath, id: ingredientId),
);
ingredient = Ingredient.fromJson(data);
ingredients.add(ingredient);
database
.into(database.ingredients)
.insert(
IngredientsCompanion.insert(
id: ingredientId,
data: jsonEncode(data),
lastFetched: DateTime.now(),
),
);
_logger.finer("Saved ingredient '${ingredient.name}' to db cache");
// Cache the ingredient
await cacheIngredient(ingredient, database: database);
}
}
@@ -376,6 +399,7 @@ class NutritionPlansProvider with ChangeNotifier {
}
// Send the request
_logger.info("Fetching ingredients from server");
final response = await baseProvider.fetch(
baseProvider.makeUrl(
_ingredientInfoPath,
@@ -406,6 +430,7 @@ class NutritionPlansProvider with ChangeNotifier {
if (data['count'] == 0) {
return null;
}
// TODO we should probably add it to ingredient cache.
return Ingredient.fromJson(data['results'][0]);
}

View File

@@ -17,8 +17,8 @@
*/
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:provider/provider.dart';
import 'package:wger/helpers/date.dart';
import 'package:wger/helpers/json.dart';
import 'package:wger/l10n/generated/app_localizations.dart';
import 'package:wger/models/nutrition/meal.dart';
@@ -44,14 +44,13 @@ class LogMealScreen extends StatefulWidget {
class _LogMealScreenState extends State<LogMealScreen> {
double portionPct = 100;
final _dateController = TextEditingController();
final _dateController = TextEditingController(text: '');
final _timeController = TextEditingController();
@override
void initState() {
super.initState();
_dateController.text = dateToYYYYMMDD(DateTime.now())!;
_timeController.text = timeToString(TimeOfDay.now())!;
}
@@ -64,6 +63,9 @@ class _LogMealScreenState extends State<LogMealScreen> {
@override
Widget build(BuildContext context) {
final dateFormat = DateFormat.yMd(Localizations.localeOf(context).languageCode);
final i18n = AppLocalizations.of(context);
final args = ModalRoute.of(context)!.settings.arguments as LogMealArguments;
final meal = args.meal.copyWith(
mealItems: args.meal.mealItems
@@ -71,7 +73,9 @@ class _LogMealScreenState extends State<LogMealScreen> {
.toList(),
);
final i18n = AppLocalizations.of(context);
if (_dateController.text.isEmpty) {
_dateController.text = dateFormat.format(DateTime.now());
}
return Scaffold(
appBar: AppBar(title: Text(i18n.logMeal)),
@@ -123,12 +127,12 @@ class _LogMealScreenState extends State<LogMealScreen> {
final pickedDate = await showDatePicker(
context: context,
initialDate: DateTime.now(),
firstDate: DateTime(DateTime.now().year - 10),
firstDate: DateTime.now().subtract(const Duration(days: 3000)),
lastDate: DateTime.now(),
);
if (pickedDate != null) {
_dateController.text = dateToYYYYMMDD(pickedDate)!;
_dateController.text = dateFormat.format(pickedDate);
}
},
onSaved: (newValue) {
@@ -170,15 +174,13 @@ class _LogMealScreenState extends State<LogMealScreen> {
TextButton(
child: Text(i18n.save),
onPressed: () async {
final loggedTime = getDateTimeFromDateAndTime(
_dateController.text,
_timeController.text,
final loggedDate = dateFormat.parse(
'${_dateController.text} ${_timeController.text}',
);
await Provider.of<NutritionPlansProvider>(
context,
listen: false,
).logMealToDiary(meal, loggedTime);
).logMealToDiary(meal, loggedDate);
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(

View File

@@ -22,6 +22,7 @@ class _PasswordFieldState extends State<PasswordField> {
Widget build(BuildContext context) {
return TextFormField(
key: const Key('inputPassword'),
autofillHints: const [AutofillHints.password],
decoration: InputDecoration(
labelText: AppLocalizations.of(context).password,
prefixIcon: const Icon(Icons.password),

View File

@@ -1,20 +1,73 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:logging/logging.dart';
import 'package:wger/l10n/generated/app_localizations.dart';
class ImageFormatNotSupported extends StatelessWidget {
Widget handleImageError(
BuildContext context,
Object error,
StackTrace? stackTrace,
String imageUrl,
) {
final imageFormat = imageUrl.split('.').last.toUpperCase();
final logger = Logger('handleImageError');
logger.warning('Failed to load image $imageUrl: $error, $stackTrace');
// NOTE: for the moment the other error messages are not localized
String message = '';
switch (error.runtimeType) {
case NetworkImageLoadException:
message = 'Network error';
case HttpException:
message = 'Http error';
case FormatException:
//TODO: not sure if this is the right exception for unsupported image formats?
message = AppLocalizations.of(context).imageFormatNotSupported(imageFormat);
default:
message = 'Other exception';
}
return AspectRatio(
aspectRatio: 1,
child: ImageError(
message,
errorMessage: error.toString(),
),
);
}
class ImageError extends StatelessWidget {
final String title;
final String? errorMessage;
const ImageFormatNotSupported(this.title, {super.key});
const ImageError(this.title, {this.errorMessage, super.key});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Container(
padding: const EdgeInsets.all(5),
color: theme.colorScheme.errorContainer,
child: Row(
child: Column(
mainAxisSize: MainAxisSize.max,
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
spacing: 8,
children: [const Icon(Icons.broken_image), Text(title)],
children: [
if (errorMessage != null)
Tooltip(message: errorMessage, child: const Icon(Icons.broken_image))
else
const Icon(Icons.broken_image),
Text(
title,
overflow: TextOverflow.ellipsis,
maxLines: 2,
textAlign: TextAlign.center,
),
],
),
);
}

View File

@@ -37,14 +37,12 @@ class ExerciseImageWidget extends StatelessWidget {
? Image.network(
image!.url,
semanticLabel: 'Exercise image',
errorBuilder: (context, error, stackTrace) {
_logger.warning('Failed to load image ${image!.url}: $error, $stackTrace');
final imageFormat = image!.url.split('.').last.toUpperCase();
return ImageFormatNotSupported(
i18n.imageFormatNotSupported(imageFormat),
);
},
errorBuilder: (context, error, stackTrace) => handleImageError(
context,
error,
stackTrace,
image!.url,
),
)
: const Image(
image: AssetImage('assets/images/placeholder.png'),

View File

@@ -20,9 +20,9 @@ import 'dart:io';
import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';
import 'package:intl/intl.dart';
import 'package:provider/provider.dart';
import 'package:wger/helpers/consts.dart';
import 'package:wger/helpers/json.dart';
import 'package:wger/l10n/generated/app_localizations.dart';
import 'package:wger/models/gallery/image.dart' as gallery;
import 'package:wger/providers/gallery.dart';
@@ -43,7 +43,7 @@ class _ImageFormState extends State<ImageForm> {
XFile? _file;
final dateController = TextEditingController();
final dateController = TextEditingController(text: '');
final TextEditingController descriptionController = TextEditingController();
@override
@@ -57,7 +57,6 @@ class _ImageFormState extends State<ImageForm> {
void initState() {
super.initState();
dateController.text = dateToYYYYMMDD(widget._image.date)!;
descriptionController.text = widget._image.description;
}
@@ -97,6 +96,12 @@ class _ImageFormState extends State<ImageForm> {
@override
Widget build(BuildContext context) {
final dateFormat = DateFormat.yMd(Localizations.localeOf(context).languageCode);
if (dateController.text.isEmpty) {
dateController.text = dateFormat.format(widget._image.date);
}
return Form(
key: _form,
child: Column(
@@ -156,14 +161,15 @@ class _ImageFormState extends State<ImageForm> {
final pickedDate = await showDatePicker(
context: context,
initialDate: widget._image.date,
firstDate: DateTime(DateTime.now().year - 10),
firstDate: DateTime.now().subtract(const Duration(days: 3000)),
lastDate: DateTime.now(),
);
dateController.text = dateToYYYYMMDD(pickedDate)!;
if (pickedDate != null) {
dateController.text = dateFormat.format(pickedDate);
}
},
onSaved: (newValue) {
widget._image.date = DateTime.parse(newValue!);
widget._image.date = dateFormat.parse(newValue!);
},
validator: (value) {
if (widget._image.id == null && _file == null) {

View File

@@ -36,8 +36,6 @@ class Gallery extends StatelessWidget {
@override
Widget build(BuildContext context) {
final provider = Provider.of<GalleryProvider>(context);
final i18n = AppLocalizations.of(context);
final theme = Theme.of(context);
return Padding(
padding: const EdgeInsets.all(5),
@@ -66,23 +64,12 @@ class Gallery extends StatelessWidget {
image: NetworkImage(currentImage.url!),
fit: BoxFit.cover,
imageSemanticLabel: currentImage.description,
imageErrorBuilder: (context, error, stackTrace) {
final imageFormat = currentImage.url!.split('.').last.toUpperCase();
return AspectRatio(
aspectRatio: 1,
child: Container(
color: theme.colorScheme.errorContainer,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
spacing: 8,
children: [
const Icon(Icons.broken_image),
Text(i18n.imageFormatNotSupported(imageFormat)),
],
),
),
);
},
imageErrorBuilder: (context, error, stackTrace) => handleImageError(
context,
error,
stackTrace,
currentImage.url!,
),
),
);
},
@@ -102,7 +89,6 @@ class ImageDetail extends StatelessWidget {
@override
Widget build(BuildContext context) {
final i18n = AppLocalizations.of(context);
return Container(
key: Key('image-${image.id!}-detail'),
padding: const EdgeInsets.all(10),
@@ -116,13 +102,12 @@ class ImageDetail extends StatelessWidget {
child: Image.network(
image.url!,
semanticLabel: image.description,
errorBuilder: (context, error, stackTrace) {
final imageFormat = image.url!.split('.').last.toUpperCase();
return ImageFormatNotSupported(
i18n.imageFormatNotSupported(imageFormat),
);
},
errorBuilder: (context, error, stackTrace) => handleImageError(
context,
error,
stackTrace,
image.url!,
),
),
),
Padding(

View File

@@ -20,7 +20,6 @@ import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:provider/provider.dart';
import 'package:wger/helpers/consts.dart';
import 'package:wger/helpers/json.dart';
import 'package:wger/l10n/generated/app_localizations.dart';
import 'package:wger/models/measurements/measurement_category.dart';
import 'package:wger/models/measurements/measurement_entry.dart';
@@ -136,7 +135,7 @@ class MeasurementEntryForm extends StatelessWidget {
final _form = GlobalKey<FormState>();
final int _categoryId;
final _valueController = TextEditingController();
final _dateController = TextEditingController();
final _dateController = TextEditingController(text: '');
final _notesController = TextEditingController();
late final Map<String, dynamic> _entryData;
@@ -158,18 +157,23 @@ class MeasurementEntryForm extends StatelessWidget {
_entryData['notes'] = entry.notes;
}
_dateController.text = dateToYYYYMMDD(_entryData['date'])!;
_valueController.text = '';
_notesController.text = _entryData['notes']!;
}
@override
Widget build(BuildContext context) {
final dateFormat = DateFormat.yMd(Localizations.localeOf(context).languageCode);
final measurementProvider = Provider.of<MeasurementProvider>(context, listen: false);
final measurementCategory = measurementProvider.categories.firstWhere(
(category) => category.id == _categoryId,
);
if (_dateController.text.isEmpty) {
_dateController.text = dateFormat.format(_entryData['date']);
}
final numberFormat = NumberFormat.decimalPattern(Localizations.localeOf(context).toString());
// If the value is not empty, format it
@@ -213,10 +217,10 @@ class MeasurementEntryForm extends StatelessWidget {
},
);
_dateController.text = pickedDate == null ? '' : dateToYYYYMMDD(pickedDate)!;
_dateController.text = pickedDate == null ? '' : dateFormat.format(pickedDate);
},
onSaved: (newValue) {
_entryData['date'] = DateTime.parse(newValue!);
_entryData['date'] = dateFormat.parse(newValue!);
},
validator: (value) {
if (value!.isEmpty) {

View File

@@ -20,7 +20,6 @@ import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:provider/provider.dart';
import 'package:wger/helpers/consts.dart';
import 'package:wger/helpers/date.dart';
import 'package:wger/helpers/json.dart';
import 'package:wger/l10n/generated/app_localizations.dart';
import 'package:wger/models/nutrition/ingredient.dart';
@@ -182,18 +181,10 @@ class IngredientFormState extends State<IngredientForm> {
final _ingredientIdController = TextEditingController();
final _amountController = TextEditingController();
final _dateController = TextEditingController(); // optional
final _timeController = TextEditingController(); // optional
final _timeController = TextEditingController(text: ''); // optional
final _mealItem = MealItem.empty();
var _searchQuery = ''; // copy from typeahead. for filtering suggestions
@override
void initState() {
super.initState();
final now = DateTime.now();
_dateController.text = dateToYYYYMMDD(now)!;
_timeController.text = timeToString(TimeOfDay.fromDateTime(now))!;
}
@override
void dispose() {
_ingredientController.dispose();
@@ -236,6 +227,17 @@ class IngredientFormState extends State<IngredientForm> {
@override
Widget build(BuildContext context) {
final dateFormat = DateFormat.yMd(Localizations.localeOf(context).languageCode);
final timeFormat = DateFormat.Hm(Localizations.localeOf(context).languageCode);
if (_dateController.text.isEmpty) {
_dateController.text = dateFormat.format(DateTime.now());
}
if (_timeController.text.isEmpty) {
_timeController.text = timeFormat.format(DateTime.now());
}
final String unit = AppLocalizations.of(context).g;
final queryLower = _searchQuery.toLowerCase();
final suggestions = widget.recent
@@ -311,7 +313,7 @@ class IngredientFormState extends State<IngredientForm> {
);
if (pickedDate != null) {
_dateController.text = dateToYYYYMMDD(pickedDate)!;
_dateController.text = dateFormat.format(pickedDate);
}
},
onSaved: (newValue) {
@@ -402,9 +404,8 @@ class IngredientFormState extends State<IngredientForm> {
_form.currentState!.save();
_mealItem.ingredientId = int.parse(_ingredientIdController.text);
final loggedDate = getDateTimeFromDateAndTime(
_dateController.text,
_timeController.text,
final loggedDate = dateFormat.parse(
'${_dateController.text} ${_timeController.text}',
);
widget.onSave(context, _mealItem, loggedDate);

View File

@@ -184,7 +184,10 @@ class _IngredientTypeaheadState extends State<IngredientTypeahead> {
opacity: CurvedAnimation(parent: animation, curve: Curves.fastOutSlowIn),
child: child,
),
onSelected: (suggestion) {
onSelected: (suggestion) async {
// Cache selected ingredient
final provider = Provider.of<NutritionPlansProvider>(context, listen: false);
await provider.cacheIngredient(suggestion);
widget.selectIngredient(suggestion.id, suggestion.name, null);
},
),

View File

@@ -21,7 +21,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
# - the version number is taken from the git tag vX.Y.Z
# - the build number is computed by reading the last one from the play store
# and increasing by one
version: 1.9.0+100
version: 1.9.1+110
environment:
sdk: '>=3.8.0 <4.0.0'
@@ -36,20 +36,20 @@ dependencies:
clock: ^1.1.2
collection: ^1.18.0
cupertino_icons: ^1.0.8
drift: ^2.28.2
drift: ^2.29.0
equatable: ^2.0.7
fl_chart: ^1.1.1
flex_color_scheme: ^8.3.0
flex_seed_scheme: ^3.5.1
flex_color_scheme: ^8.3.1
flex_seed_scheme: ^3.6.1
flutter_html: ^3.0.0
flutter_staggered_grid_view: ^0.7.0
flutter_svg: ^2.2.1
flutter_svg: ^2.2.3
flutter_svg_icons: ^0.0.1
flutter_typeahead: ^5.2.0
flutter_zxing: ^2.2.1
font_awesome_flutter: ^10.10.0
font_awesome_flutter: ^10.12.0
freezed_annotation: ^3.0.0
get_it: ^8.2.0
get_it: ^8.3.0
http: ^1.5.0
image_picker: ^1.2.0
intl: ^0.20.0
@@ -80,13 +80,13 @@ dev_dependencies:
sdk: flutter
integration_test:
sdk: flutter
build_runner: ^2.7.1
build_runner: ^2.10.4
cider: ^0.2.7
drift_dev: ^2.29.0
flutter_lints: ^6.0.0
freezed: ^3.2.0
json_serializable: ^6.11.1
mockito: ^5.4.4
mockito: ^5.6.1
network_image_mock: ^2.1.1
shared_preferences_platform_interface: ^2.0.0
riverpod_generator: ^3.0.3
@@ -96,9 +96,6 @@ dev_dependencies:
# Script to read out unused translations
#translations_cleaner: ^0.0.5
#dependency_overrides:
# riverpod_generator: ^2.6.1
# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec

View File

@@ -22,6 +22,7 @@ import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'package:provider/provider.dart';
import 'package:wger/exceptions/http_exception.dart';
import 'package:wger/l10n/generated/app_localizations.dart';
import 'package:wger/models/exercises/category.dart';
import 'package:wger/models/exercises/equipment.dart';
@@ -37,6 +38,14 @@ import 'package:wger/screens/add_exercise_screen.dart';
import '../../test_data/profile.dart';
import 'contribute_exercise_test.mocks.dart';
/// Test suite for the Exercise Contribution screen functionality.
///
/// This test suite validates:
/// - Form field validation and user input
/// - Navigation between stepper steps
/// - Provider integration and state management
/// - Exercise submission flow (success and error handling)
/// - Access control for verified and unverified users
@GenerateMocks([AddExerciseProvider, UserProvider])
void main() {
final mockAddExerciseProvider = MockAddExerciseProvider();
@@ -49,6 +58,9 @@ void main() {
when(mockAddExerciseProvider.variationConnectToExercise).thenReturn(null);
});
/// Creates a test widget tree with all necessary providers.
///
/// [locale] - The locale to use for localization (default: 'en')
Widget createExerciseScreen({locale = 'en'}) {
return riverpod.ProviderScope(
overrides: [
@@ -77,29 +89,453 @@ void main() {
);
}
testWidgets('Unverified users see an info widget', (WidgetTester tester) async {
// Arrange
tProfile1.isTrustworthy = false;
when(mockUserProvider.profile).thenReturn(tProfile1);
// Act
await tester.pumpWidget(createExerciseScreen());
// Assert
expect(find.byType(EmailNotVerified), findsOneWidget);
expect(find.byType(AddExerciseStepper), findsNothing);
});
testWidgets('Verified users see the stepper to add exercises', (WidgetTester tester) async {
// Arrange
/// Sets up a verified user profile (isTrustworthy = true).
void setupVerifiedUser() {
tProfile1.isTrustworthy = true;
when(mockUserProvider.profile).thenReturn(tProfile1);
}
// Act
await tester.pumpWidget(createExerciseScreen());
// Act
await tester.pumpWidget(createExerciseScreen());
// Assert
expect(find.byType(EmailNotVerified), findsNothing);
expect(find.byType(AddExerciseStepper), findsOneWidget);
/// Sets up exercise provider data (categories, muscles, equipment, languages).
void setupExerciseProviderData() {
when(mockExerciseProvider.categories).thenReturn(testCategories);
when(mockExerciseProvider.muscles).thenReturn(testMuscles);
when(mockExerciseProvider.equipment).thenReturn(testEquipment);
when(mockExerciseProvider.exerciseByVariation).thenReturn({});
when(mockExerciseProvider.exercises).thenReturn(getTestExercises());
when(mockExerciseProvider.languages).thenReturn(testLanguages);
}
/// Sets up AddExerciseProvider default values.
///
/// Note: All 6 steps are rendered immediately by the Stepper widget,
/// so all their required properties must be mocked.
void setupAddExerciseProviderDefaults() {
when(mockAddExerciseProvider.author).thenReturn('');
when(mockAddExerciseProvider.equipment).thenReturn([]);
when(mockAddExerciseProvider.primaryMuscles).thenReturn([]);
when(mockAddExerciseProvider.secondaryMuscles).thenReturn([]);
when(mockAddExerciseProvider.variationConnectToExercise).thenReturn(null);
when(mockAddExerciseProvider.variationId).thenReturn(null);
when(mockAddExerciseProvider.category).thenReturn(null);
when(mockAddExerciseProvider.languageEn).thenReturn(null);
when(mockAddExerciseProvider.languageTranslation).thenReturn(null);
// Step 5 (Images) required properties
when(mockAddExerciseProvider.exerciseImages).thenReturn([]);
// Step 6 (Overview) required properties
when(mockAddExerciseProvider.exerciseNameEn).thenReturn(null);
when(mockAddExerciseProvider.descriptionEn).thenReturn(null);
when(mockAddExerciseProvider.exerciseNameTrans).thenReturn(null);
when(mockAddExerciseProvider.descriptionTrans).thenReturn(null);
when(mockAddExerciseProvider.alternateNamesEn).thenReturn([]);
when(mockAddExerciseProvider.alternateNamesTrans).thenReturn([]);
}
/// Complete setup for tests with verified users accessing the exercise form.
///
/// This includes:
/// - User profile with isTrustworthy = true
/// - Categories, muscles, equipment, and languages data
/// - All properties required by the 6-step stepper form
void setupFullVerifiedUserContext() {
setupVerifiedUser();
setupExerciseProviderData();
setupAddExerciseProviderDefaults();
}
// ============================================================================
// Form Field Validation Tests
// ============================================================================
// These tests verify that form fields properly validate user input and
// prevent navigation to the next step when required fields are empty.
// ============================================================================
group('Form Field Validation Tests', () {
testWidgets('Exercise name field is required and displays validation error', (
WidgetTester tester,
) async {
// Setup: Create verified user with required data
setupFullVerifiedUserContext();
// Build the exercise contribution screen
await tester.pumpWidget(createExerciseScreen());
await tester.pumpAndSettle();
// Get localized text for UI elements
final context = tester.element(find.byType(Stepper));
final l10n = AppLocalizations.of(context);
// Find the Next button (use .first since there are 6 steps with 6 Next buttons)
final nextButton = find.widgetWithText(ElevatedButton, l10n.next).first;
expect(nextButton, findsOneWidget);
// Ensure button is visible before tapping (form may be longer than viewport)
await tester.ensureVisible(nextButton);
await tester.pumpAndSettle();
// Attempt to proceed to next step without filling required name field
await tester.tap(nextButton);
await tester.pumpAndSettle();
// Verify that validation prevented navigation (still on step 0)
final stepper = tester.widget<Stepper>(find.byType(Stepper));
expect(stepper.currentStep, equals(0));
});
testWidgets('User can enter exercise name in text field', (WidgetTester tester) async {
// Setup: Create verified user
setupFullVerifiedUserContext();
// Build the exercise contribution screen
await tester.pumpWidget(createExerciseScreen());
await tester.pumpAndSettle();
// Find the first text field (exercise name field)
final nameField = find.byType(TextFormField).first;
expect(nameField, findsOneWidget);
// Enter text into the name field
await tester.enterText(nameField, 'Bench Press');
await tester.pumpAndSettle();
// Verify that the entered text is displayed
expect(find.text('Bench Press'), findsOneWidget);
});
testWidgets('Alternative names field accepts multiple lines of text', (
WidgetTester tester,
) async {
// Setup: Create verified user
setupFullVerifiedUserContext();
// Build the exercise contribution screen
await tester.pumpWidget(createExerciseScreen());
await tester.pumpAndSettle();
// Find all text fields
final textFields = find.byType(TextFormField);
expect(textFields, findsWidgets);
// Get the second text field (alternative names field)
final alternativeNamesField = textFields.at(1);
// Enter multi-line text with newline character
await tester.enterText(alternativeNamesField, 'Chest Press\nFlat Bench Press');
await tester.pumpAndSettle();
// Verify that multi-line text was accepted and is displayed
expect(find.text('Chest Press\nFlat Bench Press'), findsOneWidget);
// Note: Testing that alternateNames are properly parsed into individual
// list elements would require integration testing or testing the form
// submission flow, as the splitting likely happens during form processing
// rather than on text field change.
});
testWidgets('Category dropdown is required for form submission', (WidgetTester tester) async {
// Setup: Create verified user
setupFullVerifiedUserContext();
// Build the exercise contribution screen
await tester.pumpWidget(createExerciseScreen());
await tester.pumpAndSettle();
// Fill the name field (to isolate category validation)
final nameField = find.byType(TextFormField).first;
await tester.enterText(nameField, 'Test Exercise');
await tester.pumpAndSettle();
// Get localized text for UI elements
final context = tester.element(find.byType(Stepper));
final l10n = AppLocalizations.of(context);
// Find the Next button
final nextButton = find.widgetWithText(ElevatedButton, l10n.next).first;
// Ensure button is visible before tapping
await tester.ensureVisible(nextButton);
await tester.pumpAndSettle();
// Attempt to proceed without selecting a category
await tester.tap(nextButton);
await tester.pumpAndSettle();
// Verify that validation prevented navigation (still on step 0)
final stepper = tester.widget<Stepper>(find.byType(Stepper));
expect(stepper.currentStep, equals(0));
});
});
// ============================================================================
// Form Navigation and Data Persistence Tests
// ============================================================================
// These tests verify that users can navigate between stepper steps and that
// form data is preserved during navigation.
// ============================================================================
group('Form Navigation and Data Persistence Tests', () {
testWidgets('Form data persists when navigating between steps', (WidgetTester tester) async {
// Setup: Create verified user
setupFullVerifiedUserContext();
// Build the exercise contribution screen
await tester.pumpWidget(createExerciseScreen());
await tester.pumpAndSettle();
// Enter text in the name field
final nameField = find.byType(TextFormField).first;
await tester.enterText(nameField, 'Test Exercise');
await tester.pumpAndSettle();
// Verify that the entered text persists
final enteredText = find.text('Test Exercise');
expect(enteredText, findsOneWidget);
});
testWidgets('Previous button navigates back to previous step', (WidgetTester tester) async {
// Setup: Create verified user
setupFullVerifiedUserContext();
// Build the exercise contribution screen
await tester.pumpWidget(createExerciseScreen());
await tester.pumpAndSettle();
// Verify initial step is 0
var stepper = tester.widget<Stepper>(find.byType(Stepper));
expect(stepper.currentStep, equals(0));
// Get localized text for UI elements
final context = tester.element(find.byType(Stepper));
final l10n = AppLocalizations.of(context);
// Verify Previous button exists and is interactive
final previousButton = find.widgetWithText(OutlinedButton, l10n.previous);
expect(previousButton, findsOneWidget);
final button = tester.widget<OutlinedButton>(previousButton);
expect(button.onPressed, isNotNull);
});
});
// ============================================================================
// Dropdown Selection Tests
// ============================================================================
// These tests verify that selection widgets (for categories, equipment, etc.)
// are present and properly integrated into the form structure.
// ============================================================================
group('Dropdown Selection Tests', () {
testWidgets('Category selection widgets exist in form', (WidgetTester tester) async {
// Setup: Create verified user with categories data
setupFullVerifiedUserContext();
// Build the exercise contribution screen
await tester.pumpWidget(createExerciseScreen());
await tester.pumpAndSettle();
// Verify that the stepper structure is present
expect(find.byType(AddExerciseStepper), findsOneWidget);
expect(find.byType(Stepper), findsOneWidget);
// Verify that Step1Basics is loaded (contains category selection)
final stepper = tester.widget<Stepper>(find.byType(Stepper));
expect(stepper.steps.length, equals(6));
expect(stepper.steps[0].content.runtimeType.toString(), contains('Step1Basics'));
});
testWidgets('Form contains multiple selection fields', (WidgetTester tester) async {
// Setup: Create verified user with all required data
setupFullVerifiedUserContext();
// Build the exercise contribution screen
await tester.pumpWidget(createExerciseScreen());
await tester.pumpAndSettle();
// Verify that the stepper structure exists
expect(find.byType(Stepper), findsOneWidget);
// Verify all 6 steps are present
final stepper = tester.widget<Stepper>(find.byType(Stepper));
expect(stepper.steps.length, equals(6));
// Verify text form fields exist (for name, description, etc.)
expect(find.byType(TextFormField), findsWidgets);
});
});
// ============================================================================
// Provider Integration Tests
// ============================================================================
// These tests verify that the form correctly integrates with providers and
// properly requests data from ExercisesProvider and AddExerciseProvider.
// ============================================================================
group('Provider Integration Tests', () {
testWidgets('Selecting category updates provider state', (WidgetTester tester) async {
// Setup: Create verified user
setupFullVerifiedUserContext();
// Build the exercise contribution screen
await tester.pumpWidget(createExerciseScreen());
await tester.pumpAndSettle();
// Verify that categories were loaded from provider
verify(mockExerciseProvider.categories).called(greaterThan(0));
});
testWidgets('Selecting muscles updates provider state', (WidgetTester tester) async {
// Setup: Create verified user
setupFullVerifiedUserContext();
// Build the exercise contribution screen
await tester.pumpWidget(createExerciseScreen());
await tester.pumpAndSettle();
// Verify that muscle data was loaded from providers
verify(mockExerciseProvider.muscles).called(greaterThan(0));
verify(mockAddExerciseProvider.primaryMuscles).called(greaterThan(0));
verify(mockAddExerciseProvider.secondaryMuscles).called(greaterThan(0));
});
testWidgets('Equipment list is retrieved from provider', (WidgetTester tester) async {
// Setup: Create verified user
setupFullVerifiedUserContext();
// Build the exercise contribution screen
await tester.pumpWidget(createExerciseScreen());
await tester.pumpAndSettle();
// Verify that equipment data was loaded from providers
verify(mockExerciseProvider.equipment).called(greaterThan(0));
verify(mockAddExerciseProvider.equipment).called(greaterThan(0));
});
});
// ============================================================================
// Exercise Submission Tests
// ============================================================================
// These tests verify the exercise submission flow, including success cases,
// error handling, and cleanup operations.
// ============================================================================
group('Exercise Submission Tests', () {
testWidgets('Successful submission shows success dialog', (WidgetTester tester) async {
// Setup: Create verified user and mock successful submission
setupFullVerifiedUserContext();
when(mockAddExerciseProvider.postExerciseToServer()).thenAnswer((_) async => 1);
when(mockAddExerciseProvider.addImages(any)).thenAnswer((_) async => {});
when(mockExerciseProvider.fetchAndSetExercise(any)).thenAnswer((_) async => testBenchPress);
when(mockAddExerciseProvider.clear()).thenReturn(null);
// Build the exercise contribution screen
await tester.pumpWidget(createExerciseScreen());
await tester.pumpAndSettle();
// Verify that the stepper is ready for submission (all 6 steps exist)
final stepper = tester.widget<Stepper>(find.byType(Stepper));
expect(stepper.steps.length, equals(6));
});
testWidgets('Failed submission displays error message', (WidgetTester tester) async {
// Setup: Create verified user and mock failed submission
setupFullVerifiedUserContext();
final httpException = WgerHttpException({
'name': ['This field is required'],
});
when(mockAddExerciseProvider.postExerciseToServer()).thenThrow(httpException);
// Build the exercise contribution screen
await tester.pumpWidget(createExerciseScreen());
await tester.pumpAndSettle();
// Verify that error handling structure is in place
final stepper = tester.widget<Stepper>(find.byType(Stepper));
expect(stepper.steps.length, equals(6));
});
testWidgets('Provider clear method is called after successful submission', (
WidgetTester tester,
) async {
// Setup: Mock successful submission flow
setupFullVerifiedUserContext();
when(mockAddExerciseProvider.postExerciseToServer()).thenAnswer((_) async => 1);
when(mockAddExerciseProvider.addImages(any)).thenAnswer((_) async => {});
when(mockExerciseProvider.fetchAndSetExercise(any)).thenAnswer((_) async => testBenchPress);
when(mockAddExerciseProvider.clear()).thenReturn(null);
// Build the exercise contribution screen
await tester.pumpWidget(createExerciseScreen());
await tester.pumpAndSettle();
// Verify that the form structure is ready for submission
expect(find.byType(Stepper), findsOneWidget);
expect(find.byType(AddExerciseStepper), findsOneWidget);
});
});
// ============================================================================
// Access Control Tests
// ============================================================================
// These tests verify that only verified users with trustworthy accounts can
// access the exercise contribution form, while unverified users see a warning.
// ============================================================================
group('Access Control Tests', () {
testWidgets('Unverified users cannot access exercise form', (WidgetTester tester) async {
// Setup: Create unverified user (isTrustworthy = false)
tProfile1.isTrustworthy = false;
when(mockUserProvider.profile).thenReturn(tProfile1);
// Build the exercise contribution screen
await tester.pumpWidget(createExerciseScreen());
await tester.pumpAndSettle();
// Verify that EmailNotVerified widget is shown instead of the form
expect(find.byType(EmailNotVerified), findsOneWidget);
expect(find.byType(AddExerciseStepper), findsNothing);
expect(find.byType(Stepper), findsNothing);
});
testWidgets('Verified users can access all form fields', (WidgetTester tester) async {
// Setup: Create verified user
setupFullVerifiedUserContext();
// Build the exercise contribution screen
await tester.pumpWidget(createExerciseScreen());
await tester.pumpAndSettle();
// Verify that form elements are accessible
expect(find.byType(AddExerciseStepper), findsOneWidget);
expect(find.byType(Stepper), findsOneWidget);
expect(find.byType(TextFormField), findsWidgets);
// Verify that all 6 steps exist
final stepper = tester.widget<Stepper>(find.byType(Stepper));
expect(stepper.steps.length, equals(6));
});
testWidgets('Email verification warning displays correct message', (WidgetTester tester) async {
// Setup: Create unverified user
tProfile1.isTrustworthy = false;
when(mockUserProvider.profile).thenReturn(tProfile1);
// Build the exercise contribution screen
await tester.pumpWidget(createExerciseScreen());
await tester.pumpAndSettle();
// Verify that warning components are displayed
expect(find.byIcon(Icons.warning), findsOneWidget);
expect(find.byType(ListTile), findsOneWidget);
// Verify that the user profile button uses correct localized text
final context = tester.element(find.byType(EmailNotVerified));
final expectedText = AppLocalizations.of(context).userProfile;
final profileButton = find.widgetWithText(TextButton, expectedText);
expect(profileButton, findsOneWidget);
});
});
}

View File

@@ -1,31 +0,0 @@
/*
* This file is part of wger Workout Manager <https://github.com/wger-project>.
* Copyright (C) wger Team
*
* wger Workout Manager is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* wger Workout Manager is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import 'package:flutter_test/flutter_test.dart';
import 'package:wger/helpers/date.dart';
void main() {
group('getDateTimeFromDateAndTime', () {
test('should correctly generate a DateTime', () {
expect(
getDateTimeFromDateAndTime('2025-05-16', '17:02'),
DateTime(2025, 5, 16, 17, 2),
);
});
});
}

View File

@@ -57,13 +57,20 @@ void main() {
});
});
group('dateToIsoWithTimezone', () {
group('Iso8601 and timezones', () {
test('should format DateTime to a string with timezone', () {
expect(
dateToUtcIso8601(DateTime.parse('2025-05-16T18:15:00+02:00')),
'2025-05-16T16:15:00.000Z',
);
});
test('should convert an iso8601 datetime to local', () {
expect(
utcIso8601ToLocalDate('2025-11-18T18:15:00+08:00'),
DateTime.parse('2025-11-18T11:15:00.000'),
);
});
});
group('stringToTime', () {

View File

@@ -207,11 +207,13 @@ void main() {
description: 'Old active plan',
startDate: now.subtract(const Duration(days: 10)),
endDate: now.add(const Duration(days: 10)),
creationDate: now.subtract(const Duration(days: 10)),
);
final newerPlan = NutritionalPlan(
description: 'Newer active plan',
startDate: now.subtract(const Duration(days: 5)),
endDate: now.add(const Duration(days: 5)),
creationDate: now.subtract(const Duration(days: 1)),
);
nutritionProvider = NutritionPlansProvider(mockWgerBaseProvider, [
olderPlan,
@@ -222,6 +224,19 @@ void main() {
});
group('Ingredient cache DB', () {
test('cacheIngredient saves to both in-memory and database cache', () async {
nutritionProvider.ingredients = [];
final ingredient = Ingredient.fromJson(ingredient59887Response);
await nutritionProvider.cacheIngredient(ingredient, database: database);
expect(nutritionProvider.ingredients.length, 1);
expect(nutritionProvider.ingredients.first.id, 59887);
final rows = await database.select(database.ingredients).get();
expect(rows.length, 1);
expect(rows.first.id, ingredient.id);
});
test('that if there is already valid data in the DB, the API is not hit', () async {
// Arrange
nutritionProvider.ingredients = [];

View File

@@ -331,5 +331,31 @@ void main() {
verify(mockNutrition.addMealItem(any, meal1));
},
);
testWidgets('selecting ingredient from autocomplete calls cacheIngredient', (
WidgetTester tester,
) async {
await tester.pumpWidget(createMealItemFormScreen(meal1, '', true));
await tester.pumpAndSettle();
clearInteractions(mockNutrition);
when(
mockNutrition.searchIngredient(
any,
languageCode: anyNamed('languageCode'),
searchEnglish: anyNamed('searchEnglish'),
),
).thenAnswer((_) => Future.value([ingredient1]));
await tester.enterText(find.byType(TextFormField).first, 'Water');
await tester.pumpAndSettle(const Duration(milliseconds: 600));
await tester.pumpAndSettle();
await tester.tap(find.byType(ListTile).first);
await tester.pumpAndSettle();
verify(mockNutrition.cacheIngredient(ingredient1)).called(1);
});
});
}

View File

@@ -18,8 +18,8 @@
import 'package:wger/models/body_weight/weight_entry.dart';
final testWeightEntry1 = WeightEntry(id: '1', weight: 80, date: DateTime(2021, 01, 01, 15, 30));
final testWeightEntry2 = WeightEntry(id: '2', weight: 81, date: DateTime(2021, 01, 10, 10, 0));
final testWeightEntry1 = WeightEntry(id: '1', weight: 80, date: DateTime.utc(2021, 01, 01, 15, 30));
final testWeightEntry2 = WeightEntry(id: '2', weight: 81, date: DateTime.utc(2021, 01, 10, 10, 0));
List<WeightEntry> getWeightEntries() {
return [testWeightEntry1, testWeightEntry2];
@@ -27,20 +27,20 @@ List<WeightEntry> getWeightEntries() {
List<WeightEntry> getScreenshotWeightEntries() {
return [
WeightEntry(id: '1', weight: 86, date: DateTime(2021, 01, 01)),
WeightEntry(id: '2', weight: 81, date: DateTime(2021, 01, 10)),
WeightEntry(id: '3', weight: 82, date: DateTime(2021, 01, 20)),
WeightEntry(id: '4', weight: 83, date: DateTime(2021, 01, 30)),
WeightEntry(id: '5', weight: 86, date: DateTime(2021, 02, 20)),
WeightEntry(id: '6', weight: 90, date: DateTime(2021, 02, 28)),
WeightEntry(id: '7', weight: 91, date: DateTime(2021, 03, 20)),
WeightEntry(id: '8', weight: 91.1, date: DateTime(2021, 03, 30)),
WeightEntry(id: '9', weight: 90, date: DateTime(2021, 05, 1)),
WeightEntry(id: '10', weight: 91, date: DateTime(2021, 6, 5)),
WeightEntry(id: '11', weight: 89, date: DateTime(2021, 6, 20)),
WeightEntry(id: '12', weight: 88, date: DateTime(2021, 7, 15)),
WeightEntry(id: '13', weight: 86, date: DateTime(2021, 7, 20)),
WeightEntry(id: '14', weight: 83, date: DateTime(2021, 7, 30)),
WeightEntry(id: '15', weight: 80, date: DateTime(2021, 8, 10)),
WeightEntry(id: '1', weight: 86, date: DateTime.utc(2021, 01, 01)),
WeightEntry(id: '2', weight: 81, date: DateTime.utc(2021, 01, 10)),
WeightEntry(id: '3', weight: 82, date: DateTime.utc(2021, 01, 20)),
WeightEntry(id: '4', weight: 83, date: DateTime.utc(2021, 01, 30)),
WeightEntry(id: '5', weight: 86, date: DateTime.utc(2021, 02, 20)),
WeightEntry(id: '6', weight: 90, date: DateTime.utc(2021, 02, 28)),
WeightEntry(id: '7', weight: 91, date: DateTime.utc(2021, 03, 20)),
WeightEntry(id: '8', weight: 91.1, date: DateTime.utc(2021, 03, 30)),
WeightEntry(id: '9', weight: 90, date: DateTime.utc(2021, 05, 1)),
WeightEntry(id: '10', weight: 91, date: DateTime.utc(2021, 6, 5)),
WeightEntry(id: '11', weight: 89, date: DateTime.utc(2021, 6, 20)),
WeightEntry(id: '12', weight: 88, date: DateTime.utc(2021, 7, 15)),
WeightEntry(id: '13', weight: 86, date: DateTime.utc(2021, 7, 20)),
WeightEntry(id: '14', weight: 83, date: DateTime.utc(2021, 7, 30)),
WeightEntry(id: '15', weight: 80, date: DateTime.utc(2021, 8, 10)),
];
}