Compare commits

...

239 Commits

Author SHA1 Message Date
mealie-commit-bot[bot]
3de4024619 chore: bump version to v3.10.0 2026-02-02 18:33:02 +00:00
renovate[bot]
194771653d chore(deps): pin dependency freezegun to ==1.5.5 (#6991)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-02 18:18:36 +00:00
renovate[bot]
24aa8f3525 fix(deps): update dependency orjson to v3.11.7 (#6989)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-02 17:54:33 +00:00
Michael Genson
fb8e318739 fix: Flaky $NOW tests (#6990) 2026-02-02 11:24:13 -06:00
Michael Genson
6255c71609 docs: Misc. cleanup (#6988) 2026-02-02 10:47:23 -06:00
Hayden
f2d1569488 chore(l10n): New Crowdin updates (#6987) 2026-02-02 11:18:56 +00:00
Michael Genson
987c7209fc feat: Query relative dates (#6984) 2026-02-01 21:36:46 -06:00
Hayden
f6dbd1f1f1 chore(l10n): New Crowdin updates (#6983) 2026-02-01 23:23:28 +00:00
Michael Genson
d30118899d fix: Re-enable some style tags (#6982) 2026-02-01 16:24:57 -06:00
Michael Genson
af241dad57 feat: Add range of dates to shopping list from meal planner (#6981) 2026-02-01 15:58:03 -06:00
renovate[bot]
b86de79c6f chore(deps): update dependency rich to v14.3.2 (#6980)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-01 20:28:33 +00:00
Hayden
86e86f8c81 chore(l10n): New Crowdin updates (#6979) 2026-02-01 10:58:51 +00:00
mealie-actions[bot]
d795f91938 chore(l10n): Crowdin locale sync (#6977)
Co-authored-by: GitHub Action <action@github.com>
2026-02-01 04:42:54 +00:00
Michael Genson
a59511cc81 dev: Switch to Mealie bot for auto-merging (#6978) 2026-01-31 22:42:03 -06:00
Michael Genson
a5d4cae6d0 dev: Switch approver for automated i18n merges (#6976) 2026-01-31 21:46:13 -06:00
Michael Genson
2987cf8ba6 dev: Allow locale sync path in auto merge (#6974) 2026-01-31 21:37:31 -06:00
Michael Genson
46b46978ff dev: Increase locale merge limit to 300 and allow PRs from Mealie bot (#6972) 2026-01-31 21:31:52 -06:00
Michael Genson
12857883a9 dev: Fix token vars (#6970) 2026-01-31 21:24:58 -06:00
Michael Genson
60fff3b5b8 dev: Switch to bot token for locale sync (#6969) 2026-01-31 21:21:07 -06:00
Hayden
b42e888929 chore(l10n): New Crowdin updates (#6967) 2026-02-01 00:47:50 +00:00
Michael Genson
570d6f1433 feat: Migrate OpenAI implementation to use structured outputs (#6964) 2026-01-31 11:57:05 -06:00
Morgan
dcf410739e fix: service-worker precache manifest entries not generated correctly (#6815) 2026-01-31 15:51:11 +00:00
Michael Genson
1929d630a1 fix: Remove deprecated warning from shopping list editor (#6963) 2026-01-31 09:44:34 -06:00
Michael Genson
c4c7bf2aed fix: Disable context hover (#6962) 2026-01-31 09:40:31 -06:00
Hayden
47034d18c5 chore(l10n): New Crowdin updates (#6960) 2026-01-31 15:09:56 +00:00
renovate[bot]
7ebe491f74 fix(deps): update dependency ingredient-parser-nlp to v2.5.0 (#6961)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-31 14:52:15 +00:00
Arsène Reymond
719bd89eb1 feat: Improve recipe assets preview (#6602)
Co-authored-by: Michael Genson <71845777+michael-genson@users.noreply.github.com>
2026-01-31 14:48:42 +00:00
Hayden
9030c7e6b9 chore(l10n): New Crowdin updates (#6959) 2026-01-30 23:15:01 +00:00
renovate[bot]
0202cc7ef8 fix(deps): update dependency pyjwt to v2.11.0 (#6958)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-30 20:53:19 +00:00
Patrick Lehner (he/him)
381ac9bfde dev: Improve caching in taskfile (#6798) 2026-01-30 20:52:44 +00:00
Patrick Lehner (he/him)
e9fe71c1b7 dev: Add tasks for e2e tests (#6797)
Co-authored-by: Michael Genson <genson.michael@gmail.com>
Co-authored-by: Michael Genson <71845777+michael-genson@users.noreply.github.com>
2026-01-30 14:51:40 -06:00
Arsène Reymond
79bbc20cd6 fix: recipe context menu (#6782) 2026-01-30 20:08:49 +00:00
Arsène Reymond
c7be4a452a fix: disable invitations when password login is disabled (#6781) 2026-01-30 20:05:40 +00:00
Imanuel
731ee8ae3d fix: sub-recipes in multi group setup (#6652) (#6663) 2026-01-30 18:50:08 +00:00
Imanuel
c7ae67e7cd feat: Customizable OpenAI prompts (#5146) (#6588)
Co-authored-by: Michael Genson <71845777+michael-genson@users.noreply.github.com>
Co-authored-by: Michael Genson <genson.michael@gmail.com>
2026-01-30 12:00:03 -06:00
Gtt1229
e83891e3ca feat: Added Option to Import Recipe Category During Recipe Import (#6523)
Co-authored-by: Michael Genson <genson.michael@gmail.com>
Co-authored-by: Michael Genson <71845777+michael-genson@users.noreply.github.com>
2026-01-30 11:18:15 -06:00
Michael Genson
e3e45c534e dev: Skip Trivy on merge queue (#6957) 2026-01-30 16:47:30 +00:00
Stevie Howard
279cf65673 fix: Seed data - en-US only - correct [some] plural names and add [some] accented characters (#6405)
Co-authored-by: Michael Genson <71845777+michael-genson@users.noreply.github.com>
2026-01-30 16:30:56 +00:00
Fjodor42
cb44ecf394 feat: Add "sprig" as a unit. (#6934)
Co-authored-by: Hayden <64056131+hay-kot@users.noreply.github.com>
2026-01-30 16:03:41 +00:00
Michael Genson
920eeb26d6 dev: Bunch of GH workflow fixes (#6956) 2026-01-30 15:58:49 +00:00
Hayden
9738d9f363 fix: dispose AlchemyExporter engine after restore completes (#6942) 2026-01-30 15:54:38 +00:00
CodeFaux
37e6123f9e fix: Keep ingredient headers in cook mode (#6946)
Co-authored-by: Michael Genson <71845777+michael-genson@users.noreply.github.com>
2026-01-30 15:36:04 +00:00
Hayden
0a2cabb348 chore(l10n): New Crowdin updates (#6954) 2026-01-30 15:19:02 +00:00
Michael Genson
447a1fb239 dev: Enable CI on merge queues (#6955) 2026-01-30 09:18:52 -06:00
Hayden
b5358896eb fix: use GITHUB_TOKEN for auto-merge to respect CI checks (#6953) 2026-01-30 03:26:10 +00:00
Jérôme
78fbbf0264 fix: correct global scroll strategy to prevent menu fixation (#6577)
Co-authored-by: Jerome <jerome.roth@imt-atlantique.net>
Co-authored-by: Hayden <64056131+hay-kot@users.noreply.github.com>
2026-01-30 02:27:45 +00:00
Hayden
a33d8204df chore(l10n): New Crowdin updates (#6949)
Co-authored-by: Michael Genson <71845777+michael-genson@users.noreply.github.com>
2026-01-30 02:10:24 +00:00
Hayden
c8046bbdf0 chore: add workflow to auto-merge l10n PRs (#6948)
Co-authored-by: Michael Genson <71845777+michael-genson@users.noreply.github.com>
2026-01-30 02:05:56 +00:00
renovate[bot]
329ad4d8ed fix(deps): update dependency alembic to v1.18.3 (#6945)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-30 00:43:47 +00:00
renovate[bot]
4ccf649aa1 chore(deps): update dependency setuptools to v80.10.2 (#6930)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-29 18:32:44 -06:00
renovate[bot]
5994328a8b fix(deps): update dependency orjson to v3.11.6 (#6952)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-29 13:42:53 -06:00
Hayden
15b5917054 feat: add discard confirmation dialog for recipe editor (#6941)
Co-authored-by: Michael Genson <71845777+michael-genson@users.noreply.github.com>
2026-01-28 23:09:32 -06:00
Hayden
e48b150f7c chore(l10n): New Crowdin updates (#6944)
Co-authored-by: Michael Genson <71845777+michael-genson@users.noreply.github.com>
2026-01-28 22:57:04 -06:00
renovate[bot]
adbc66316f chore(deps): update dependency coverage to v7.13.2 (#6927)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-28 22:54:52 -06:00
Hayden
0dc7337972 chore(l10n): New Crowdin updates (#6938) 2026-01-28 19:14:41 +07:00
renovate[bot]
58d4b95a56 fix(deps): update dependency openai to v2.16.0 (#6937)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-27 20:51:07 -06:00
Hayden
0e74bc6cd0 chore(l10n): New Crowdin updates (#6936) 2026-01-27 20:29:06 +00:00
Hayden
4866eec62d chore(l10n): New Crowdin updates (#6935) 2026-01-26 18:34:52 -06:00
github-actions[bot]
c0d659724a chore(auto): Update pre-commit hooks (#6932)
Co-authored-by: boc-the-git <3479092+boc-the-git@users.noreply.github.com>
2026-01-26 16:06:35 +00:00
Hayden
5f0996734a chore(l10n): New Crowdin updates (#6933) 2026-01-26 18:09:28 +07:00
Hayden
8cd0286ca1 chore(l10n): New Crowdin updates (#6929) 2026-01-25 19:49:14 -06:00
renovate[bot]
f214e8843a fix(deps): update dependency python-multipart to v0.0.22 (#6926) 2026-01-25 15:30:40 +00:00
github-actions[bot]
66fea60341 chore(l10n): Crowdin locale sync (#6924) 2026-01-25 15:19:23 +00:00
Hayden
69b4684bce chore(l10n): New Crowdin updates (#6925) 2026-01-25 18:46:24 +07:00
renovate[bot]
b75d6812a3 chore(deps): update dependency rich to v14.3.1 (#6923) 2026-01-24 18:04:27 -06:00
Hayden
ed000c2cc6 chore(l10n): New Crowdin updates (#6922) 2026-01-24 22:45:45 +00:00
Alexandre Eberhardt
d43a2020b3 docs: Add an info callout about client-side cookies. (#6830) 2026-01-24 20:33:19 +00:00
Patrick Lehner (he/him)
ff5e65b323 fix: Make 'auto-search' toggle change color to indicate state (#6809) 2026-01-24 20:21:53 +00:00
Hayden
e1b07a250b chore(l10n): New Crowdin updates (#6912) 2026-01-24 20:07:45 +00:00
renovate[bot]
e68486a0e1 chore(deps): update dependency rich to v14.3.0 (#6919)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-24 13:56:54 -06:00
renovate[bot]
271915ee23 chore(deps): update dependency types-python-dateutil to v2.9.0.20260124 (#6913)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-24 13:43:38 -06:00
renovate[bot]
a3d64c0761 fix(deps): update dependency pillow-heif to v1.2.0 (#6910)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-23 08:50:03 -06:00
Hayden
73c664649d chore(l10n): New Crowdin updates (#6911) 2026-01-23 18:08:07 +07:00
renovate[bot]
d887e68228 chore(deps): update dependency ruff to v0.14.14 (#6909)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-22 19:59:48 -06:00
Hayden
a8d3ed3310 chore(l10n): New Crowdin updates (#6908) 2026-01-22 15:43:48 -06:00
Hayden
00bd45c8f1 chore(l10n): New Crowdin updates (#6905) 2026-01-22 14:35:50 +00:00
renovate[bot]
05003a5c6f fix(deps): update dependency sqlalchemy to v2.0.46 (#6903)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-22 03:50:27 +00:00
renovate[bot]
e2be09b5d3 chore(deps): update dependency setuptools to v80.10.1 (#6901)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-21 21:39:54 -06:00
github-actions[bot]
b81e0ac03b chore(l10n): Crowdin locale sync (#6891)
Co-authored-by: GitHub Action <action@github.com>
2026-01-22 03:24:04 +00:00
renovate[bot]
c5d822cded chore(deps): update node.js to b2b2184 (#6879)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-21 21:13:45 -06:00
Hayden
0fc66fee9a chore(l10n): New Crowdin updates (#6888) 2026-01-21 03:44:18 +00:00
github-actions[bot]
612c07e6f3 chore(auto): Update pre-commit hooks (#6895) 2026-01-21 02:52:58 +00:00
renovate[bot]
a0ac2923d6 fix(deps): update dependency apprise to v1.9.7 (#6898) 2026-01-20 20:42:15 -06:00
Hayden
7107c08021 chore(l10n): New Crowdin updates (#6886) 2026-01-16 17:13:20 -06:00
renovate[bot]
a0e336edcb fix(deps): update dependency alembic to v1.18.1 (#6878)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-16 13:29:38 -06:00
Hayden
3e306638d0 fix: prevent XSS via javascript: URIs in recipe actions (#6885) 2026-01-16 12:19:27 -06:00
Patrick Lehner (he/him)
a72641b32e feat: Use toggle button for switching any/all mode for search filters (#6833) 2026-01-16 15:52:11 +00:00
renovate[bot]
f4ed9d92bf chore(deps): update dependency ruff to v0.14.13 (#6881)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-16 09:38:32 -06:00
Hayden
5ae35c3500 chore(l10n): New Crowdin updates (#6870) 2026-01-14 23:36:27 +00:00
renovate[bot]
08666e6c21 chore(deps): update node.js to 0ab63ca (#6872) 2026-01-14 17:26:34 -06:00
Hayden
5ae530a637 chore(l10n): New Crowdin updates (#6867) 2026-01-12 14:44:13 -06:00
github-actions[bot]
2b07497486 chore(auto): Update pre-commit hooks (#6866)
Co-authored-by: boc-the-git <3479092+boc-the-git@users.noreply.github.com>
2026-01-12 18:34:46 +00:00
github-actions[bot]
3b65642325 chore(l10n): Crowdin locale sync (#6862)
Co-authored-by: GitHub Action <action@github.com>
2026-01-12 18:20:16 +00:00
Hayden
fdd1057e79 chore(l10n): New Crowdin updates (#6864) 2026-01-12 12:10:00 -06:00
Hayden
f1afebcc04 chore(l10n): New Crowdin updates (#6863) 2026-01-11 17:00:15 +07:00
Hayden
e711be7efa chore(l10n): New Crowdin updates (#6861) 2026-01-10 19:51:06 +00:00
renovate[bot]
ec94b8179c fix(deps): update dependency openai to v2.15.0 (#6859)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-09 22:32:27 -06:00
renovate[bot]
a7c1d6f486 fix(deps): update dependency alembic to v1.18.0 (#6858)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-10 03:54:14 +00:00
renovate[bot]
df0b792c52 chore(deps): update dependency ruff to v0.14.11 (#6852)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-09 21:43:04 -06:00
Hayden
1f5054fcbd chore(l10n): New Crowdin updates (#6843) 2026-01-10 03:24:28 +00:00
renovate[bot]
ca483b9cbe chore(deps): update dependency types-requests to v2.32.4.20260107 (#6846) 2026-01-09 21:14:02 -06:00
Mike Nguyen
03dc459162 docs: fix authentik oidc link (#6851) 2026-01-08 20:47:15 +00:00
Michael Genson
cf8f5fe2a2 docs: Fix typo and remove unused tip (#6849) 2026-01-07 14:31:57 -06:00
Hayden
760350ef88 chore(l10n): New Crowdin updates (#6840) 2026-01-05 23:47:44 -06:00
Michael Genson
706d4ee0b5 fix: Coerce null servings into 0 servings (#6839) 2026-01-05 17:49:34 -06:00
Patrick Lehner (he/him)
5fd8545cbe fix: Tags can't be renamed (#6835) 2026-01-05 09:46:48 -06:00
Hayden
3397c06db2 chore(l10n): New Crowdin updates (#6832) 2026-01-04 20:00:39 -06:00
Hayden
22df7a1ec7 chore(l10n): New Crowdin updates (#6827) 2026-01-04 09:58:19 -06:00
renovate[bot]
e87b0c75b6 fix(deps): update dependency pillow to v12.1.0 (#6816)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-03 23:34:36 -06:00
Hayden
b406b7fa16 chore(l10n): New Crowdin updates (#6806)
Co-authored-by: Michael Genson <71845777+michael-genson@users.noreply.github.com>
2026-01-04 03:18:35 +00:00
github-actions[bot]
7114ed1122 chore(l10n): Crowdin locale sync (#6825)
Co-authored-by: GitHub Action <action@github.com>
2026-01-03 21:08:10 -06:00
mealie-commit-bot[bot]
70b5865dce chore: bump version to v3.9.2 2026-01-02 19:40:19 +00:00
Michael Genson
3be7056f2c fix: Exception handling for recipe image reprocessing (#6822) 2026-01-02 13:17:07 -06:00
Arsène Reymond
1b57310535 fix: allow start attribute on ordered lists (SafeMarkdown) (#6820)
Co-authored-by: Michael Genson <71845777+michael-genson@users.noreply.github.com>
2026-01-02 19:02:11 +00:00
Patrick Lehner (he/him)
2b15d9a515 fix: Make quantity input in shopping list item editor visually consistent with other inputs (#6810) 2026-01-02 12:51:53 -06:00
Hayden
adc9c0b970 chore(l10n): New Crowdin updates (#6804) 2025-12-31 00:36:01 +00:00
renovate[bot]
bec1708891 chore(deps): update node.js to b52a8d1 (#6800)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-30 10:33:30 -06:00
Patrick Lehner (he/him)
66bb545454 dev: Small .gitignore fixes (#6796) 2025-12-30 15:42:38 +00:00
renovate[bot]
c1ebf04291 chore(deps): update node.js to 33587cf (#6795)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-30 05:20:34 +00:00
mealie-commit-bot[bot]
3166060644 chore: bump version to v3.9.1 2025-12-30 01:56:10 +00:00
Michael Genson
bde7cf6f9d fix: Revert extended touch on shopping list (#6794) 2025-12-29 19:24:03 -06:00
Hayden
8ea9bb19f6 chore(l10n): New Crowdin updates (#6792) 2025-12-29 17:11:05 -06:00
mealie-commit-bot[bot]
6d0f9b0d35 chore: bump version to v3.9.0 2025-12-29 20:46:19 +00:00
Hayden
df541c1924 chore(l10n): New Crowdin updates (#6789) 2025-12-29 14:34:07 -06:00
Hayden
9af92ff397 chore(l10n): New Crowdin updates (#6788) 2025-12-28 21:54:38 +00:00
renovate[bot]
554d50b079 chore(deps): update dependency coverage to v7.13.1 (#6787) 2025-12-28 15:43:41 -06:00
github-actions[bot]
a00e2e8b68 chore(l10n): Crowdin locale sync (#6786)
Co-authored-by: GitHub Action <action@github.com>
2025-12-28 00:02:46 -06:00
renovate[bot]
4fcfbaff3b fix(deps): update dependency fastapi to v0.128.0 (#6783)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-27 12:56:10 -06:00
renovate[bot]
7792f0504d fix(deps): update dependency fastapi to v0.127.1 (#6779) 2025-12-26 12:13:31 -06:00
Hayden
3ca6c67f25 chore(l10n): New Crowdin updates (#6778) 2025-12-25 14:30:16 +00:00
Arsène Reymond
2eb0fdc863 fix: resize pwa maskable icons (#6777) 2025-12-24 18:20:10 +00:00
Hayden
192d48c4a6 chore(l10n): New Crowdin updates (#6776) 2025-12-24 09:42:26 -06:00
Michael Genson
e4f38685b3 fix(deps): Bump various frontend deps (#6774) 2025-12-23 17:12:52 -06:00
Michael Genson
d02023e12c fix: Only fetch recipes with a household id (#6773) 2025-12-23 16:48:27 -06:00
Michael Genson
64d8786d8f fix: Improve recipe bulk deletion (#6772) 2025-12-23 12:31:53 -06:00
Michael Genson
0971d59fa6 fix: Can't remove organizer (#6771) 2025-12-23 11:32:14 -06:00
Hayden
9b799ca441 chore(l10n): New Crowdin updates (#6768) 2025-12-22 23:36:50 -06:00
Michael Genson
193b823688 feat: Replace number inputs with new v-number-input compontent (#6767) 2025-12-22 18:45:52 -06:00
Michael Genson
c64c2d25e7 fix: Add resiliency to LDAP admin filter (#6766) 2025-12-22 15:37:15 -06:00
onemustpersist
8b4111d68f fix: Imported API keys not working on a new server #6477 (#6496)
Co-authored-by: Michael Genson <71845777+michael-genson@users.noreply.github.com>
Co-authored-by: Michael Genson <genson.michael@gmail.com>
2025-12-22 12:08:44 -06:00
miah
9d601ea4b5 feat: Animate shopping list and increase touch target (#6569)
Co-authored-by: Michael Genson <71845777+michael-genson@users.noreply.github.com>
2025-12-22 11:55:25 -06:00
gitolicious
95e1bbce2b feat: persist selected dates in meal planner (#6512)
Co-authored-by: Michael Genson <genson.michael@gmail.com>
Co-authored-by: Michael Genson <71845777+michael-genson@users.noreply.github.com>
2025-12-22 11:10:23 -06:00
Aurelien
7b32508201 fix: the add_pagination_to_query now always returns the correct count (#6505)
Co-authored-by: Michael Genson <71845777+michael-genson@users.noreply.github.com>
2025-12-22 10:39:35 -06:00
Hayden
6ed85d72d7 chore(l10n): New Crowdin updates (#6765) 2025-12-22 15:59:19 +00:00
github-actions[bot]
cd2a522f25 chore(auto): Update pre-commit hooks (#6760)
Co-authored-by: boc-the-git <3479092+boc-the-git@users.noreply.github.com>
2025-12-22 15:48:53 +00:00
Arsène Reymond
6bd6400aba fix: add loader for create backup button (#6763) 2025-12-22 09:38:44 -06:00
Michael Genson
8b92d6ee04 fix: Backup selection doesn't work sometimes (#6759) 2025-12-21 20:56:51 -06:00
Michael Genson
7cc2ed75e5 fix: Consistant Shopping List Recipe State (#6758) 2025-12-21 20:27:00 -06:00
renovate[bot]
cb7f46c0ad fix(deps): update dependency fastapi to v0.127.0 (#6756)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-21 13:52:34 -06:00
renovate[bot]
cb12aedf72 fix(deps): update dependency openai to v2.14.0 (#6745) 2025-12-21 12:48:06 -06:00
renovate[bot]
8c35a26ab0 chore(deps): update dependency mkdocs-material to v9.7.1 (#6741)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-21 18:05:50 +00:00
github-actions[bot]
b2d0f46dd2 chore(l10n): Crowdin locale sync (#6754)
Co-authored-by: GitHub Action <action@github.com>
Co-authored-by: Michael Genson <71845777+michael-genson@users.noreply.github.com>
2025-12-21 11:53:25 -06:00
Hayden
2c4b7bf611 chore(l10n): New Crowdin updates (#6748)
Co-authored-by: Michael Genson <71845777+michael-genson@users.noreply.github.com>
2025-12-21 11:52:47 -06:00
renovate[bot]
38e542bcd3 fix(deps): update dependency python-multipart to v0.0.21 (#6737)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-21 11:52:15 -06:00
renovate[bot]
e53452c19c fix(deps): update dependency uvicorn to v0.40.0 (#6757)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-21 11:35:48 -06:00
Hayden
13213476d8 docs: add --no-project flags to skip mealie installation (#6755) 2025-12-20 21:23:11 -06:00
Hayden
9925450173 docs: isolate docs dependencies to avoid python-ldap build (#6753) 2025-12-20 20:45:59 -06:00
Hayden
efb9dae681 docs: add GitHub Actions workflow for docs deployment (#6752) 2025-12-20 20:26:14 -06:00
renovate[bot]
cee93d2a87 fix(deps): update dependency fastapi to v0.126.0 (#6750)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-20 16:06:03 -06:00
Arsène Reymond
0d4a8654c1 fix: PWA maskable android icons & enctype shared_target (#6731)
Co-authored-by: Michael Genson <71845777+michael-genson@users.noreply.github.com>
2025-12-19 05:06:27 +00:00
Hayden
95b1be07bb chore(l10n): New Crowdin updates (#6744)
Co-authored-by: Michael Genson <71845777+michael-genson@users.noreply.github.com>
2025-12-19 04:53:09 +00:00
mealie-commit-bot[bot]
a6fc98fc82 chore: bump version to v3.8.0 2025-12-19 01:37:16 +00:00
Michael Genson
6f03010f6c fix: Security Patches (#6743) 2025-12-18 22:54:16 +00:00
renovate[bot]
69397c91b8 fix(deps): update dependency openai to v2.13.0 (#6726)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-18 21:45:42 +00:00
Hayden
798792dcdc chore(l10n): New Crowdin updates (#6736)
Co-authored-by: Michael Genson <71845777+michael-genson@users.noreply.github.com>
2025-12-18 15:32:22 -06:00
renovate[bot]
cc32dd9fa6 chore(deps): update dependency ruff to v0.14.10 (#6742)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-18 15:32:00 -06:00
renovate[bot]
0c64eb29f9 fix(deps): update dependency fastapi to v0.125.0 (#6740)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-17 17:37:43 -06:00
renovate[bot]
8baa5cc315 chore(deps): update dependency pre-commit to v4.5.1 (#6734)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-17 00:07:49 +00:00
Hayden
6f3a5c6c8f chore(l10n): New Crowdin updates (#6733) 2025-12-16 17:42:13 +00:00
Hayden
778078590b chore(l10n): New Crowdin updates (#6729) 2025-12-15 22:54:37 -06:00
github-actions[bot]
53c82e5491 chore(auto): Update pre-commit hooks (#6724) 2025-12-15 18:16:59 +00:00
Hayden
fef114d97f chore(l10n): New Crowdin updates (#6725) 2025-12-15 08:55:48 -06:00
Hayden
e80cbfad7f chore(l10n): New Crowdin updates (#6722)
Co-authored-by: Michael Genson <71845777+michael-genson@users.noreply.github.com>
2025-12-14 23:31:48 -06:00
renovate[bot]
99527ce738 chore(deps): update dependency mypy to v1.19.1 (#6723)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-14 23:31:34 -06:00
Arsène Reymond
08ccced734 fix: localize text validators message (#6719) 2025-12-14 09:56:11 -06:00
github-actions[bot]
43c2c9552b chore(l10n): Crowdin locale sync (#6716)
Co-authored-by: GitHub Action <action@github.com>
Co-authored-by: Michael Genson <71845777+michael-genson@users.noreply.github.com>
2025-12-13 22:10:43 -06:00
Hayden
db5741c7ee chore(l10n): New Crowdin updates (#6710) 2025-12-13 22:10:23 -06:00
renovate[bot]
a1e394cf36 fix(deps): update dependency tzdata to v2025.3 (#6713) 2025-12-13 15:21:42 -06:00
Michael Genson
bdbef1ab9e fix: More lenient postgres override parsing (#6712) 2025-12-13 14:21:54 -06:00
Michael Genson
e5276f6c20 fix: Put tooltips behind app bar (#6711) 2025-12-13 10:56:18 -06:00
Michael Genson
20a6e71b31 feat: Optionally include URL when importing via HTML/JSON (#6709) 2025-12-12 23:20:26 -06:00
Michael Genson
24c111af7b chore: Miscellaneous cleanup (#6708) 2025-12-12 22:48:49 -06:00
davidschinkel
ab4559319e fix: Improved bulk deletion by reducing refreshs (#6634)
Co-authored-by: David Schinkel <david@zollsoft.de>
Co-authored-by: Hayden <64056131+hay-kot@users.noreply.github.com>
2025-12-13 04:08:04 +00:00
Michael Genson
2f8625ac44 fix: Disable submit on enter when editing timeline events (#6707) 2025-12-12 21:56:36 -06:00
Hayden
dd146afa57 chore(l10n): New Crowdin updates (#6706) 2025-12-12 21:20:34 -06:00
renovate[bot]
91d15f671e fix(deps): update dependency authlib to v1.6.6 (#6700)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-13 01:54:41 +00:00
renovate[bot]
7008b13246 fix(deps): update dependency fastapi to v0.124.4 (#6702)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-12 19:43:35 -06:00
mealie-commit-bot[bot]
1a1798cd88 chore: bump version to v3.7.0 2025-12-13 01:21:07 +00:00
Michael Genson
64f47c1589 fix: Reprocess script UUID handling for postgres (#6705) 2025-12-12 19:18:52 -06:00
Michael Genson
326bb1eb8e feat: Reprocess image user script (#6704) 2025-12-12 18:30:49 -06:00
Hayden
80dc2ecfb7 chore(l10n): New Crowdin updates (#6701) 2025-12-12 13:53:06 +00:00
renovate[bot]
b72082663f chore(deps): update node.js to 20988bc (#6698) 2025-12-12 05:24:05 +00:00
renovate[bot]
f46ae423d3 chore(deps): update dependency ruff to v0.14.9 (#6699) 2025-12-11 23:13:36 -06:00
Hayden
05cdff8ae7 chore(l10n): New Crowdin updates (#6697) 2025-12-12 03:57:19 +01:00
Hayden
0facdf73be chore(l10n): New Crowdin updates (#6694) 2025-12-11 21:38:11 +00:00
renovate[bot]
cbad569134 fix(deps): update dependency openai to v2.11.0 (#6696)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-11 15:27:38 -06:00
Hayden
1063433aa9 chore(l10n): New Crowdin updates (#6693) 2025-12-10 19:47:30 -06:00
renovate[bot]
0ba22c81e7 fix(deps): update dependency fastapi to v0.124.2 (#6688) 2025-12-10 18:59:49 +00:00
renovate[bot]
0667177a2e fix(deps): update dependency recipe-scrapers to v15.11.0 (#6691) 2025-12-10 18:48:05 +00:00
Hayden
6fcf22869b chore(l10n): New Crowdin updates (#6689) 2025-12-10 12:37:02 -06:00
renovate[bot]
20b45e57e0 fix(deps): update dependency sqlalchemy to v2.0.45 (#6687)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-09 20:53:09 -06:00
Hayden
7a38a52158 chore(l10n): New Crowdin updates (#6686) 2025-12-09 19:45:24 -06:00
renovate[bot]
e27eca5571 chore(deps): update node.js to 9a2ed90 (#6684)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Michael Genson <71845777+michael-genson@users.noreply.github.com>
2025-12-09 12:53:03 -06:00
renovate[bot]
a90b2ccafd chore(deps): update dependency coverage to v7.13.0 (#6683)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-09 12:52:45 -06:00
Michael Genson
e0d8104643 feat: Suggest HTML importer on URL importer failure (#6685) 2025-12-09 11:05:34 -06:00
Hayden
53ee64828b chore(l10n): New Crowdin updates (#6678)
Co-authored-by: Michael Genson <71845777+michael-genson@users.noreply.github.com>
2025-12-09 10:27:21 -06:00
Arsène Reymond
6f7fba5ac1 feat: Add user QueryFilter and improve UI on mobile (#6235)
Co-authored-by: Michael Genson <71845777+michael-genson@users.noreply.github.com>
Co-authored-by: Michael Genson <genson.michael@gmail.com>
2025-12-09 09:49:12 -06:00
github-actions[bot]
89aed15905 chore(auto): Update pre-commit hooks (#6680)
Co-authored-by: boc-the-git <3479092+boc-the-git@users.noreply.github.com>
2025-12-08 14:55:38 +00:00
renovate[bot]
aac48287a4 fix(deps): update dependency apprise to v1.9.6 (#6677)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-08 08:45:21 -06:00
Hayden
34daaa0476 chore(l10n): New Crowdin updates (#6675) 2025-12-07 09:43:47 -06:00
Henri Cook
af56a3e69d fix: improve password manager autofill compatibility on login page (#6662)
Co-authored-by: Henri Cook <henri.cook@linklaters.com>
Co-authored-by: Michael Genson <71845777+michael-genson@users.noreply.github.com>
2025-12-06 23:35:16 -06:00
github-actions[bot]
0908812b47 chore(l10n): Crowdin locale sync (#6672)
Co-authored-by: GitHub Action <action@github.com>
2025-12-06 22:58:22 -06:00
renovate[bot]
d910fbafe8 chore(deps): update dependency pytest to v9.0.2 (#6670) 2025-12-07 00:15:15 +00:00
renovate[bot]
c7692426d5 fix(deps): update dependency orjson to v3.11.5 (#6667) 2025-12-07 00:04:01 +00:00
renovate[bot]
b7a615add9 fix(deps): update dependency fastapi to v0.124.0 (#6664) 2025-12-06 17:52:57 -06:00
Hayden
3167e23b6b chore(l10n): New Crowdin updates (#6671) 2025-12-06 17:21:05 -06:00
Hayden
8b582f8682 feat: autofill default credentials on first login (#6666) 2025-12-06 17:47:33 +00:00
Hayden
05f648d7fb fix: clear cached store data on logout to prevent user data leakage (#6665) 2025-12-06 11:36:39 -06:00
Nathan Winspear
1f19133870 docs: add theming examples to backend configuration guide (#6443)
Co-authored-by: Michael Genson <genson.michael@gmail.com>
Co-authored-by: Michael Genson <71845777+michael-genson@users.noreply.github.com>
2025-12-05 19:35:33 -06:00
Hayden
98273da16e chore(l10n): New Crowdin updates (#6661)
Co-authored-by: Michael Genson <71845777+michael-genson@users.noreply.github.com>
2025-12-05 16:29:47 -06:00
miah
f857ca18da feat: Improve startup workflow UI (#6342)
Co-authored-by: Michael Genson <71845777+michael-genson@users.noreply.github.com>
2025-12-05 22:18:32 +00:00
renovate[bot]
22a0e6d608 fix(deps): update dependency fastapi to v0.123.10 (#6660)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-05 15:58:01 -06:00
Tempest
ed806b9fec feat: Improve Image Minification Logic and Efficiency (#5883)
Co-authored-by: Kuchenpirat <24235032+Kuchenpirat@users.noreply.github.com>
Co-authored-by: Michael Genson <71845777+michael-genson@users.noreply.github.com>
Co-authored-by: Michael Genson <genson.michael@gmail.com>
2025-12-05 15:45:53 -06:00
Michael Genson
ae8b489f97 dev: Add copilot-instructions.md (#6659) 2025-12-05 13:00:45 -06:00
Noneangel
71732d4766 feat: frontend autocomplete is diacritics/ligatures insensitive (#6169)
Co-authored-by: Pierre <pierre@debian.zabi.ovh>
Co-authored-by: Michael Genson <genson.michael@gmail.com>
Co-authored-by: Michael Genson <71845777+michael-genson@users.noreply.github.com>
2025-12-05 12:44:37 -06:00
Cash Prokop-Weaver
6695314588 feat: Add snack, drink, and dessert (#6149)
Co-authored-by: Michael Genson <71845777+michael-genson@users.noreply.github.com>
Co-authored-by: Michael Genson <genson.michael@gmail.com>
2025-12-05 11:54:00 -06:00
Nico Hirsch
c115e6d83f feat: Put calendar directly in the date picker dialogs (#6110)
Co-authored-by: Michael Genson <71845777+michael-genson@users.noreply.github.com>
Co-authored-by: Michael Genson <genson.michael@gmail.com>
2025-12-05 10:05:47 -06:00
renovate[bot]
e3e970213c fix(deps): update dependency fastapi to v0.123.9 (#6657)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-04 23:29:52 -06:00
Hayden
7fe358e5e7 chore(l10n): New Crowdin updates (#6653) 2025-12-04 21:54:37 +00:00
renovate[bot]
c7f3334479 fix(deps): update dependency openai to v2.9.0 (#6656)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-04 15:43:47 -06:00
renovate[bot]
d4467f65fb fix(deps): update dependency fastapi to v0.123.8 (#6640)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-04 21:23:23 +00:00
renovate[bot]
27e61ec6b1 chore(deps): update dependency ruff to v0.14.8 (#6655)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-04 15:12:20 -06:00
Hayden
6c6dc8103d chore(l10n): New Crowdin updates (#6649) 2025-12-03 10:25:28 +01:00
Hayden
35963dad2e fix: change log rotation size from 10kb to 10mb (#6648) 2025-12-02 19:06:51 -06:00
mealie-commit-bot[bot]
acd0c2cb3e chore: bump version to v3.6.1 2025-12-02 23:08:41 +00:00
Michael Genson
28d00f7dd5 fix: Bump version before building release (#6647) 2025-12-02 16:51:46 -06:00
Michael Genson
fdd3d4b37a fix: Remove Auth Refresh (#6646) 2025-12-02 15:55:01 -06:00
Hayden
b09a85dfab chore(l10n): New Crowdin updates (#6643) 2025-12-02 10:39:36 -06:00
github-actions[bot]
b6ceece901 docs(auto): Update image tag, for release v3.6.0 (#6639) 2025-12-01 23:35:45 -06:00
337 changed files with 59117 additions and 55637 deletions

View File

@@ -1,9 +1,10 @@
# See here for image contents: https://github.com/microsoft/vscode-dev-containers/tree/v0.224.2/containers/python-3/.devcontainer/base.Dockerfile
# [Choice] Python version (use -bullseye variants on local arm64/Apple Silicon): 3, 3.10, 3.9, 3.8, 3.7, 3.6, 3-bullseye, 3.10-bullseye, 3.9-bullseye, 3.8-bullseye, 3.7-bullseye, 3.6-bullseye, 3-buster, 3.10-buster, 3.9-buster, 3.8-buster, 3.7-buster, 3.6-buster
ARG VARIANT="3.12-bullseye"
FROM mcr.microsoft.com/devcontainers/python:${VARIANT}
# Remove outdated yarn GPG key, if it exists
RUN rm -f /etc/apt/sources.list.d/yarn.list /usr/share/keyrings/yarn-archive-keyring.gpg || true
# [Choice] Node.js version: none, lts/*, 16, 14, 12, 10
ARG NODE_VERSION="none"
RUN if [ "${NODE_VERSION}" != "none" ]; then su vscode -c "umask 0002 && . /usr/local/share/nvm/nvm.sh && nvm install ${NODE_VERSION} 2>&1"; fi
@@ -13,6 +14,7 @@ RUN echo "export PROMPT_COMMAND='history -a'" >> /home/vscode/.bashrc \
&& chown vscode:vscode -R /home/vscode/
RUN npm install -g @go-task/cli
RUN npm install -g json-schema-to-typescript
# Install additional OS packages
RUN apt-get update \

View File

@@ -23,7 +23,6 @@
"settings": {
"python.defaultInterpreterPath": "/usr/local/bin/python",
"python.formatting.autopep8Path": "/usr/local/py-utils/bin/autopep8",
"python.formatting.blackPath": "/usr/local/py-utils/bin/black",
"python.formatting.yapfPath": "/usr/local/py-utils/bin/yapf",
"mypy.runUsingActiveInterpreter": true
},
@@ -31,10 +30,10 @@
"charliermarsh.ruff",
"dbaeumer.vscode-eslint",
"matangover.mypy",
"ms-python.black-formatter",
"ms-python.pylint",
"ms-python.python",
"ms-python.vscode-pylance",
"streetsidesoftware.code-spell-checker-cspell-bundled-dictionaries",
"Vue.volar"
]
}
@@ -42,6 +41,7 @@
// Use 'forwardPorts' to make a list of ports inside the container available locally.
"forwardPorts": [
3000,
8000, // used by mkdocs
9000,
9091, // used by docker production
24678 // used by nuxt when hot-reloading using polling

240
.github/copilot-instructions.md vendored Normal file
View File

@@ -0,0 +1,240 @@
# Mealie Development Guide for AI Agents
## Project Overview
Mealie is a self-hosted recipe manager, meal planner, and shopping list application with a FastAPI backend (Python 3.12) and Nuxt 3 frontend (Vue 3 + TypeScript). It uses SQLAlchemy ORM with support for SQLite and PostgreSQL databases.
**Development vs Production:**
- **Development:** Frontend (port 3000) and backend (port 9000) run as separate processes
- **Production:** Frontend is statically generated and served via FastAPI's SPA module (`mealie/routes/spa/`) in a single container
## Architecture & Key Patterns
### Backend Architecture (mealie/)
**Repository-Service-Controller Pattern:**
- **Controllers** (`mealie/routes/**/controller_*.py`): Inherit from `BaseUserController` or `BaseAdminController`, handle HTTP concerns, delegate to services
- **Services** (`mealie/services/`): Business logic layer, inherit from `BaseService`, coordinate repos and external dependencies
- **Repositories** (`mealie/repos/`): Data access layer using SQLAlchemy, accessed via `AllRepositories` factory
- Get repos via dependency injection: `repos: AllRepositories = Depends(get_repositories)`
- All repos scoped to group/household context automatically
**Route Organization:**
- Routes in `mealie/routes/` organized by domain (auth, recipe, groups, households, admin)
- Use `APIRouter` with FastAPI dependency injection
- Apply `@router.get/post/put/delete` decorators with Pydantic response models
- Route controllers use `HttpRepo` mixin for common CRUD operations (see `mealie/routes/_base/mixins.py`)
**Schemas & Type Generation:**
- Pydantic schemas in `mealie/schema/` with strict separation: `*In`, `*Out`, `*Create`, `*Update` suffixes
- Auto-exported from submodules via `__init__.py` files (generated by `task dev:generate`)
- TypeScript types auto-generated from Pydantic schemas - **never manually edit** `frontend/lib/api/types/`
**Database & Sessions:**
- Session management via `Depends(generate_session)` in FastAPI routes
- Use `session_context()` context manager in services/scripts
- SQLAlchemy models in `mealie/db/models/`, migrations in `mealie/alembic/`
- Create migrations: `task py:migrate -- "description"`
### Frontend Architecture (frontend/)
**Component Organization (strict naming conventions):**
- **Domain Components** (`components/Domain/`): Feature-specific, prefix with domain (e.g., `AdminDashboard`)
- **Global Components** (`components/global/`): Reusable primitives, prefix with `Base` (e.g., `BaseButton`)
- **Layout Components** (`components/Layout/`): Layout-only, prefix with `App` if props or `The` if singleton
- **Page Components** (`components/` with page prefix): Last resort for breaking up complex pages
**API Client Pattern:**
- API clients in `frontend/lib/api/` extend `BaseAPI`, `BaseCRUDAPI`, or `BaseCRUDAPIReadOnly`
- Types imported from auto-generated `frontend/lib/api/types/` (DO NOT EDIT MANUALLY)
- Composables in `frontend/composables/` for shared state and API logic (e.g., `use-mealie-auth.ts`)
- Use `useAuthBackend()` for authentication state, `useMealieAuth()` for user management
**State Management:**
- Nuxt 3 composables for state (no Vuex)
- Auth state via `use-mealie-auth.ts` composable
- Prefer composables over global state stores
## Essential Commands (via Task/Taskfile.yml)
**Development workflow:**
```bash
task setup # Install all dependencies (Python + Node)
task dev:services # Start Postgres & Mailpit containers
task py # Start FastAPI backend (port 9000)
task ui # Start Nuxt frontend (port 3000)
task docs # Start MkDocs documentation server
```
**Code generation (REQUIRED after schema changes):**
```bash
task dev:generate # Generate TypeScript types, schema exports, test helpers
```
**Testing & Quality:**
```bash
task py:test # Run pytest (supports args: task py:test -- -k test_name)
task py:check # Format + lint + type-check + test (full validation)
task py:format # Ruff format
task py:lint # Ruff check
task py:mypy # Type checking
task ui:test # Vitest frontend tests
task ui:check # Frontend lint + test
```
**Database:**
```bash
task py:migrate -- "description" # Generate Alembic migration
task py:postgres # Run backend with PostgreSQL config
```
**Docker:**
```bash
task docker:prod # Build and run production Docker compose
```
## Critical Development Practices
### Python Backend
1. **Always use `uv` for Python commands** (not `python` or `pip`):
```bash
uv run python mealie/app.py
uv run pytest tests/
```
2. **Type hints are mandatory:** Use mypy-compatible annotations, handle Optional types explicitly
3. **Dependency injection pattern:**
```python
from fastapi import Depends
from mealie.repos.all_repositories import get_repositories, AllRepositories
def my_route(
repos: AllRepositories = Depends(get_repositories),
user: PrivateUser = Depends(get_current_user)
):
recipe = repos.recipes.get_one(recipe_id)
```
4. **Settings & Configuration:**
- Get settings: `settings = get_app_settings()` (cached singleton)
- Get directories: `dirs = get_app_dirs()`
- Never instantiate `AppSettings()` directly
5. **Testing:**
- Fixtures in `tests/fixtures/`
- Use `api_client` fixture for integration tests
- Follow existing patterns in `tests/integration_tests/` and `tests/unit_tests/`
### Frontend
1. **Run code generation after backend schema changes:** `task dev:generate`
2. **TypeScript strict mode:** All code must pass type checking
3. **Component naming:** Follow strict conventions (see Architecture section above)
4. **API calls pattern:**
```typescript
const api = useUserApi();
const recipe = await api.recipes.getOne(recipeId);
```
5. **Composables for shared logic:** Prefer composables in `composables/` over inline code duplication
6. **Translations:** Only modify `en-US` locale files when adding new translation strings - other locales are managed via Crowdin and **must never be modified** (PRs modifying non-English locales will be rejected)
### Cross-Cutting Concerns
1. **Code generation is source of truth:** After Pydantic schema changes, run `task dev:generate` to update:
- TypeScript types (`frontend/lib/api/types/`)
- Schema exports (`mealie/schema/*/__init__.py`)
- Test data paths and routes
2. **Multi-tenancy:** All data scoped to **groups** and **households**:
- Groups contain multiple households
- Households contain recipes, meal plans, shopping lists
- Repositories automatically filter by group/household context
3. **Pre-commit hooks:** Install via `task setup:py`, enforces Ruff formatting/linting
4. **Testing before PRs:** Run `task py:check` and `task ui:check` before submitting PRs
## Pull Request Best Practices
### Before Submitting a PR
1. **Draft PRs are optional:** Create a draft PR early if you want feedback while working, or open directly as ready when complete
2. **Verify code generation:** If you modified Pydantic schemas, ensure `task dev:generate` was run
3. **Follow Conventional Commits:** Title your PR according to the conventional commits format (see PR template)
4. **Add release notes:** Include user-facing changes in the PR description
### What to Review
**Architecture & Patterns:**
- Does the code follow the repository-service-controller pattern?
- Are controllers delegating business logic to services?
- Are services coordinating repositories, not accessing the database directly?
- Is dependency injection used properly (`Depends(get_repositories)`, `Depends(get_current_user)`)?
**Data Scoping:**
- Are repositories correctly scoped to group/household context?
- Do route handlers properly validate group/household ownership before operations?
- Are multi-tenant boundaries enforced (users can't access other groups' data)?
**Type Safety:**
- Are type hints present on all functions and methods?
- Are Pydantic schemas using correct suffixes (`*In`, `*Out`, `*Create`, `*Update`)?
- For frontend, does TypeScript code pass strict type checking?
**Generated Files:**
- Verify `frontend/lib/api/types/` files weren't manually edited (they're auto-generated)
- Check that `mealie/schema/*/__init__.py` exports match actual schema files (auto-generated)
- If schemas changed, confirm generated files were updated via `task dev:generate`
**Code Quality:**
- Is the code readable and well-organized?
- Are complex operations documented with clear comments?
- Do component names follow the strict naming conventions (Domain/Global/Layout/Page prefixes)?
- Are composables used for shared frontend logic instead of duplication?
**Translations:**
- Were only `en-US` locale files modified for new translation strings?
- Verify no other locale files (managed by Crowdin) were touched
**Database Changes:**
- Are Alembic migrations included for schema changes?
- Are migrations tested against both SQLite and PostgreSQL?
### Review Etiquette
- Be constructive and specific in feedback
- Suggest code examples when proposing changes
- Focus on architecture and logic - formatting/linting is handled by CI
- Use "Approve" when ready to merge, "Request Changes" for blocking issues, "Comment" for non-blocking suggestions
## Common Gotchas
- **Don't manually edit generated files:** `frontend/lib/api/types/`, schema `__init__.py` files
- **Repository context:** Repos are group/household-scoped - passing wrong IDs causes 404s
- **Session handling:** Don't create sessions manually, use dependency injection or `session_context()`
- **Schema changes require codegen:** After changing Pydantic models, run `task dev:generate`
- **Translation files:** Only modify `en-US` locale files - all other locales are managed by Crowdin
- **Dev containers:** This project uses VS Code dev containers - leverage the pre-configured environment
- **Task commands:** Use `task` commands instead of direct tool invocation for consistency
## Key Files to Reference
- `Taskfile.yml` - All development commands and workflows
- `mealie/routes/_base/base_controllers.py` - Controller base classes and patterns
- `mealie/repos/repository_factory.py` - Repository factory and available repos
- `frontend/lib/api/base/base-clients.ts` - API client base classes
- `tests/conftest.py` - Test fixtures and setup
- `dev/code-generation/main.py` - Code generation entry point
## Additional Resources
- [Documentation](https://docs.mealie.io/)
- [Contributors Guide](https://nightly.mealie.io/contributors/developers-guide/code-contributions/)
- [Discord](https://discord.gg/QuStdQGSGK)

113
.github/workflows/auto-merge-l10n.yml vendored Normal file
View File

@@ -0,0 +1,113 @@
name: Auto-merge l10n PRs
on:
pull_request:
types: [opened, synchronize, labeled]
permissions:
contents: write
pull-requests: write
jobs:
auto-merge:
runs-on: ubuntu-latest
if: contains(github.event.pull_request.labels.*.name, 'l10n')
steps:
- name: Validate PR author
env:
AUTHOR: ${{ github.event.pull_request.user.login }}
run: |
if [[
"$AUTHOR" != "hay-kot" &&
"$AUTHOR" != "github-actions[bot]" &&
"$AUTHOR" != "mealie-actions[bot]"
]]; then
echo "::error::PR author must be hay-kot, github-actions[bot], or mealie-actions[bot] for auto-merge (got: $AUTHOR)"
exit 1
fi
echo "Author validated: $AUTHOR"
- name: Validate PR size
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR_NUMBER: ${{ github.event.pull_request.number }}
REPO: ${{ github.repository }}
run: |
ADDITIONS=$(gh pr view "$PR_NUMBER" --repo "$REPO" --json additions --jq '.additions')
DELETIONS=$(gh pr view "$PR_NUMBER" --repo "$REPO" --json deletions --jq '.deletions')
TOTAL=$((ADDITIONS + DELETIONS))
echo "PR changes: +$ADDITIONS -$DELETIONS (total: $TOTAL lines)"
if [ "$TOTAL" -gt 400 ]; then
echo "::error::PR exceeds 400 line change limit ($TOTAL lines)"
exit 1
fi
- name: Validate file paths
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR_NUMBER: ${{ github.event.pull_request.number }}
REPO: ${{ github.repository }}
run: |
FILES=$(gh pr view "$PR_NUMBER" --repo "$REPO" --json files --jq '.files[].path')
for file in $FILES; do
# Check if file matches any allowed path
if [[ "$file" == "frontend/composables/use-locales/available-locales.ts" ]] || \
[[ "$file" =~ ^frontend/lang/ ]] || \
[[ "$file" =~ ^mealie/repos/seed/resources/[^/]+/locales/ ]]; then
continue
fi
# File doesn't match allowed paths
echo "::error::Invalid file path: $file"
echo "Only the following paths are allowed:"
echo " - frontend/composables/use-locales/available-locales.ts"
echo " - frontend/lang/"
echo " - mealie/repos/seed/resources/*/locales/"
exit 1
done
echo "All files are in allowed paths"
- name: Approve PR
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR_NUMBER: ${{ github.event.pull_request.number }}
REPO: ${{ github.repository }}
run: |
APPROVED=$(gh pr view "$PR_NUMBER" \
--repo "$REPO" \
--json reviews \
--jq '.reviews[] | select(.state == "APPROVED") | .id' \
| wc -l)
if [ "$APPROVED" -gt 0 ]; then
echo "PR already approved"
exit 0
fi
gh pr review "$PR_NUMBER" \
--repo "$REPO" \
--approve \
--body "Auto-approved: l10n PR from trusted author with valid file paths"
- name: Generate GitHub App Token
id: app-token
uses: actions/create-github-app-token@v1
with:
app-id: ${{ secrets.COMMIT_BOT_APP_ID }}
private-key: ${{ secrets.COMMIT_BOT_APP_PRIVATE_KEY }}
- name: Enable auto-merge
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
PR_NUMBER: ${{ github.event.pull_request.number }}
REPO: ${{ github.repository }}
run: |
gh pr merge "$PR_NUMBER" \
--repo "$REPO" \
--auto \
--squash

50
.github/workflows/docs.yml vendored Normal file
View File

@@ -0,0 +1,50 @@
name: Deploy Documentation
on:
push:
branches: [mealie-next]
paths:
- 'docs/**'
- '.github/workflows/docs.yml'
workflow_dispatch:
permissions:
contents: read
pages: write
id-token: write
concurrency:
group: pages
cancel-in-progress: false
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install uv
uses: astral-sh/setup-uv@v4
- name: Install dependencies
run: uv sync --only-group docs --no-install-project
- name: Build docs
run: uv run --no-project mkdocs build -d site
working-directory: docs
- name: Upload artifact
uses: actions/upload-pages-artifact@v3
with:
path: docs/site
deploy:
needs: build
runs-on: ubuntu-latest
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4

View File

@@ -15,10 +15,17 @@ jobs:
sync-locales:
runs-on: ubuntu-latest
steps:
- name: Generate GitHub App Token
id: app-token
uses: actions/create-github-app-token@v1
with:
app-id: ${{ secrets.COMMIT_BOT_APP_ID }}
private-key: ${{ secrets.COMMIT_BOT_APP_PRIVATE_KEY }}
- name: Checkout repository
uses: actions/checkout@v4
with:
token: ${{ secrets.GITHUB_TOKEN }}
token: ${{ steps.app-token.outputs.token }}
- name: Set up Python
uses: actions/setup-python@v5
@@ -105,7 +112,7 @@ jobs:
- Updated frontend locale files
- Generated from latest translation sources" \
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GH_TOKEN: ${{ steps.app-token.outputs.token }}
- name: No changes detected
if: steps.changes.outputs.has_changes == 'false'

View File

@@ -4,14 +4,19 @@ on:
pull_request:
branches:
- mealie-next
merge_group:
types: [checks_requested]
branches:
- mealie-next
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.event.merge_group.head_ref }}
cancel-in-progress: true
jobs:
pull-request-lint:
name: "Lint PR"
if: github.event_name == 'pull_request'
uses: ./.github/workflows/pull-request-lint.yml
backend-tests:
@@ -24,6 +29,7 @@ jobs:
container-scanning:
name: "Trivy Container Scanning"
if: github.event_name == 'pull_request'
uses: ./.github/workflows/partial-trivy-container-scanning.yml
code-ql:
@@ -47,7 +53,10 @@ jobs:
publish-image:
name: "Publish PR Image"
if: contains(github.event.pull_request.labels.*.name, 'build-image') && github.repository == 'mealie-recipes/mealie'
if: |
github.event_name == 'pull_request' &&
contains(github.event.pull_request.labels.*.name, 'build-image') &&
github.repository == 'mealie-recipes/mealie'
permissions:
contents: read
packages: write

View File

@@ -5,17 +5,73 @@ on:
types: [published]
jobs:
commit-version-bump:
name: Commit version bump to repository
runs-on: ubuntu-latest
permissions:
contents: write
outputs:
commit-sha: ${{ steps.commit.outputs.commit-sha }}
steps:
- name: Generate GitHub App Token
id: app-token
uses: actions/create-github-app-token@v1
with:
app-id: ${{ secrets.COMMIT_BOT_APP_ID }}
private-key: ${{ secrets.COMMIT_BOT_APP_PRIVATE_KEY }}
- name: Checkout 🛎
uses: actions/checkout@v4
with:
token: ${{ steps.app-token.outputs.token }}
- name: Extract Version From Tag Name
run: echo "VERSION_NUM=$(echo ${{ github.event.release.tag_name }} | sed 's/^v//')" >> $GITHUB_ENV
- name: Configure Git
run: |
git config user.name "mealie-commit-bot[bot]"
git config user.email "mealie-commit-bot[bot]@users.noreply.github.com"
- name: Update all version strings
run: |
sed -i 's/^version = "[^"]*"/version = "${{ env.VERSION_NUM }}"/' pyproject.toml
sed -i '/^name = "mealie"$/,/^version = / s/^version = "[^"]*"/version = "${{ env.VERSION_NUM }}"/' uv.lock
sed -i 's/\("version": "\)[^"]*"/\1${{ env.VERSION_NUM }}"/' frontend/package.json
sed -i 's/:v[0-9]*\.[0-9]*\.[0-9]*/:v${{ env.VERSION_NUM }}/' docs/docs/documentation/getting-started/installation/installation-checklist.md
sed -i 's/:v[0-9]*\.[0-9]*\.[0-9]*/:v${{ env.VERSION_NUM }}/' docs/docs/documentation/getting-started/installation/sqlite.md
sed -i 's/:v[0-9]*\.[0-9]*\.[0-9]*/:v${{ env.VERSION_NUM }}/' docs/docs/documentation/getting-started/installation/postgres.md
- name: Commit and push changes
id: commit
run: |
git add pyproject.toml frontend/package.json uv.lock docs/
git commit -m "chore: bump version to ${{ github.event.release.tag_name }}"
git push origin HEAD:${{ github.event.repository.default_branch }}
echo "commit-sha=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT
- name: Move release tag to new commit
run: |
git tag -f ${{ github.event.release.tag_name }}
git push -f origin ${{ github.event.release.tag_name }}
backend-tests:
name: "Backend Server Tests"
uses: ./.github/workflows/test-backend.yml
needs:
- commit-version-bump
frontend-tests:
name: "Frontend Tests"
uses: ./.github/workflows/test-frontend.yml
needs:
- commit-version-bump
build-package:
name: Build Package
uses: ./.github/workflows/build-package.yml
needs:
- commit-version-bump
with:
tag: ${{ github.event.release.tag_name }}
@@ -43,10 +99,48 @@ jobs:
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }}
rollback-on-failure:
name: Rollback version commit if deployment fails
needs:
- commit-version-bump
- publish
if: always() && needs.publish.result == 'failure'
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Generate GitHub App Token
id: app-token
uses: actions/create-github-app-token@v1
with:
app-id: ${{ secrets.COMMIT_BOT_APP_ID }}
private-key: ${{ secrets.COMMIT_BOT_APP_PRIVATE_KEY }}
- name: Checkout 🛎
uses: actions/checkout@v4
with:
token: ${{ steps.app-token.outputs.token }}
fetch-depth: 0
- name: Configure Git
run: |
git config user.name "mealie-commit-bot[bot]"
git config user.email "mealie-commit-bot[bot]@users.noreply.github.com"
- name: Delete release tag
run: |
git push --delete origin ${{ github.event.release.tag_name }}
- name: Revert version bump commit
run: |
git revert --no-edit ${{ needs.commit-version-bump.outputs.commit-sha }}
git push origin HEAD:${{ github.event.repository.default_branch }}
notify-discord:
name: Notify Discord
needs:
- publish
if: success()
runs-on: ubuntu-latest
steps:
- name: Discord notification
@@ -55,41 +149,3 @@ jobs:
uses: Ilshidur/action-discord@0.3.2
with:
args: "🚀 Version {{ EVENT_PAYLOAD.release.tag_name }} of Mealie has been released. See the release notes https://github.com/mealie-recipes/mealie/releases/tag/{{ EVENT_PAYLOAD.release.tag_name }}"
update-image-tags:
name: Update image tag in sample docker-compose files
needs:
- publish
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
steps:
- name: Checkout 🛎
uses: actions/checkout@v4
- name: Extract Version From Tag Name
run: echo "VERSION_NUM=$(echo ${{ github.event.release.tag_name }} | sed 's/^v//')" >> $GITHUB_ENV
- name: Modify version strings
run: |
sed -i 's/:v[0-9]*.[0-9]*.[0-9]*/:v${{ env.VERSION_NUM }}/' docs/docs/documentation/getting-started/installation/installation-checklist.md
sed -i 's/:v[0-9]*.[0-9]*.[0-9]*/:v${{ env.VERSION_NUM }}/' docs/docs/documentation/getting-started/installation/sqlite.md
sed -i 's/:v[0-9]*.[0-9]*.[0-9]*/:v${{ env.VERSION_NUM }}/' docs/docs/documentation/getting-started/installation/postgres.md
sed -i 's/^version = "[^"]*"/version = "${{ env.VERSION_NUM }}"/' pyproject.toml
sed -i 's/\("version": "\)[^"]*"/\1${{ env.VERSION_NUM }}"/' frontend/package.json
- name: Create Pull Request
uses: peter-evans/create-pull-request@v6
# This doesn't currently work for us because it creates the PR but the workflows don't run.
# TODO: Provide a personal access token as a parameter here, that solves that problem.
# https://github.com/peter-evans/create-pull-request
with:
commit-message: "Update image tag, for release ${{ github.event.release.tag_name }}"
branch: "docs/newrelease-update-version-${{ github.event.release.tag_name }}"
labels: |
documentation
delete-branch: true
base: mealie-next
title: "docs(auto): Update image tag, for release ${{ github.event.release.tag_name }}"
body: "Auto-generated by `.github/workflows/release.yml`, on publish of release ${{ github.event.release.tag_name }}"

View File

@@ -40,12 +40,18 @@ jobs:
shell: bash
run: pre-commit autoupdate --color=always
- name: Create Pull Request
uses: peter-evans/create-pull-request@v6
# This doesn't currently work for us because it creates the PR but the workflows don't run.
# TODO: Provide a personal access token as a parameter here, that solves that problem.
# https://github.com/peter-evans/create-pull-request
- name: Generate GitHub App Token
id: app-token
uses: actions/create-github-app-token@v1
with:
app-id: ${{ secrets.COMMIT_BOT_APP_ID }}
private-key: ${{ secrets.COMMIT_BOT_APP_PRIVATE_KEY }}
- name: Create Pull Request
id: create-pr
uses: peter-evans/create-pull-request@v6
with:
token: ${{ steps.app-token.outputs.token }}
commit-message: "Update pre-commit hooks"
branch: "fix/update-pre-commit-hooks"
labels: |
@@ -54,3 +60,38 @@ jobs:
base: mealie-next
title: "chore(auto): Update pre-commit hooks"
body: "Auto-generated by `.github/workflows/scheduled-checks.yml`"
- name: Approve PR
if: steps.create-pr.outputs.pull-request-number
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
PR_NUMBER: ${{ steps.create-pr.outputs.pull-request-number }}
REPO: ${{ github.repository }}
run: |
APPROVED=$(gh pr view "$PR_NUMBER" \
--repo "$REPO" \
--json reviews \
--jq '.reviews[] | select(.state == "APPROVED") | .id' \
| wc -l)
if [ "$APPROVED" -gt 0 ]; then
echo "PR already approved"
exit 0
fi
gh pr review "$PR_NUMBER" \
--repo "$REPO" \
--approve \
--body "Auto-approved: Pre-commit hook updates"
- name: Enable auto-merge
if: steps.create-pr.outputs.pull-request-number
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR_NUMBER: ${{ steps.create-pr.outputs.pull-request-number }}
REPO: ${{ github.repository }}
run: |
gh pr merge "$PR_NUMBER" \
--repo "$REPO" \
--auto \
--squash

6
.gitignore vendored
View File

@@ -20,6 +20,7 @@ dev/data/backups/*
dev/data/debug/*
dev/data/img/*
dev/data/migration/*
dev/data/templates/*
dev/data/users/*
dev/data/groups/*
@@ -69,8 +70,11 @@ wheels/
.installed.cfg
*.egg
# packaged output - temporarily written here by `uv build`
/mealie-*
# frontend copied into Python module for packaging purposes
/mealie/frontend/
/mealie/frontend
# PyInstaller
# Usually these files are written by a python script from a template

View File

@@ -12,7 +12,7 @@ repos:
exclude: ^tests/data/
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.14.7
rev: v0.14.14
hooks:
- id: ruff
- id: ruff-format

View File

@@ -47,8 +47,6 @@ tasks:
sources:
- package.json
- yarn.lock
generates:
- node_modules/**
setup:py:
desc: setup python dependencies
@@ -61,6 +59,18 @@ tasks:
- pyproject.toml
- .pre-commit-config.yaml
setup:e2e:
desc: setup e2e test dependencies
dir: tests/e2e
run: once
cmds:
- yarn install
- yarn playwright install --with-deps
sources:
- package.json
- playwright.config.ts
- yarn.lock
setup:
desc: setup all dependencies
deps:
@@ -179,12 +189,21 @@ tasks:
status:
- '{{ .SKIP_PACKAGE_DEPS | default "false"}}'
py:package:
desc: builds Python packages (sdist and wheel) in top-level dist directory
py:package:build:
internal: true
deps:
- py:package:deps
cmds:
- uv build --out-dir dist
sources:
- uv.lock
- pyproject.toml
- mealie/**
py:package:
desc: builds Python packages (sdist and wheel) in top-level dist directory
cmds:
- task: py:package:build
- task: py:package:generate-requirements
py:
@@ -215,6 +234,12 @@ tasks:
dir: frontend
cmds:
- yarn build
sources:
- "**"
- exclude: .nuxt/**
- exclude: .output/**
- exclude: dist/**
- exclude: node_modules/.cache/**
ui:generate:
desc: generates a static version of the frontend in frontend/dist
@@ -223,18 +248,36 @@ tasks:
- setup:ui
cmds:
- yarn generate
sources:
- "**"
- exclude: .nuxt/**
- exclude: .output/**
- exclude: dist/**
- exclude: node_modules/.cache/**
ui:lint:
desc: runs the frontend linter
dir: frontend
cmds:
- yarn lint --max-warnings=0
sources:
- "**"
- exclude: .nuxt/**
- exclude: .output/**
- exclude: dist/**
- exclude: node_modules/.cache/**
ui:test:
desc: runs the frontend tests
dir: frontend
cmds:
- yarn test
sources:
- "**"
- exclude: .nuxt/**
- exclude: .output/**
- exclude: dist/**
- exclude: node_modules/.cache/**
ui:check:
desc: runs all frontend checks
@@ -263,3 +306,48 @@ tasks:
dir: docker
cmds:
- docker compose -f docker-compose.yml -p mealie up -d --build
e2e:build-image:
desc: builds the e2e test docker image
deps:
- py:package
cmds:
- docker build --tag mealie:e2e --file docker/Dockerfile --build-context packages=dist .
sources:
- docker/Dockerfile
- dist/**
e2e:start-server:
desc: Builds the image and starts the containers for e2e testing
dir: tests/e2e/docker
deps:
- e2e:build-image
vars:
WAIT_UNTIL_HEALTHY: '{{if .WAIT_UNTIL_HEALTHY}}--wait{{else}}{{end}}'
cmds:
- docker compose up -d {{.WAIT_UNTIL_HEALTHY}}
e2e:stop-server:
desc: Shuts down the e2e testing containers
dir: tests/e2e/docker
cmds:
- docker compose down --volumes
e2e:test:
desc: runs the e2e tests
dir: tests/e2e
deps:
- setup:e2e
vars:
PREVENT_REPORT_OPEN: '{{if .PREVENT_REPORT_OPEN}}PLAYWRIGHT_HTML_OPEN=never{{else}}{{end}}'
cmds:
- '{{.PREVENT_REPORT_OPEN}} yarn playwright test'
e2e:
desc: runs the full e2e test suite
cmds:
- task: e2e:start-server
vars: { WAIT_UNTIL_HEALTHY: true }
- defer: { task: e2e:stop-server }
- task: e2e:test
vars: { PREVENT_REPORT_OPEN: true }

View File

@@ -1,7 +1,7 @@
###############################################
# Frontend Build
###############################################
FROM node:24@sha256:aa648b387728c25f81ff811799bbf8de39df66d7e2d9b3ab55cc6300cb9175d9 \
FROM node:24@sha256:b2b2184ba9b78c022e1d6a7924ec6fba577adf28f15c9d9c457730cc4ad3807a \
AS frontend-builder
WORKDIR /frontend

View File

@@ -6,7 +6,7 @@ While this guide aims to simplify the migration process for developers, it's not
## V1 → V2
The biggest change between V1 and V2 is the introduction of Households. For more information on how households work in relation to groups/users, check out the [Groups and Households](./features.md#groups-and-households) section in the Features guide.
The biggest change between V1 and V2 is the introduction of Households. For more information on how households work in relation to groups/users, check out the [Groups and Households](../../documentation/getting-started/features.md#groups-and-households) section in the Features guide.
### `updateAt` is now `updatedAt`

View File

@@ -16,7 +16,7 @@ Recipes extras are a key feature of the Mealie API. They allow you to create cus
For example you could add `{"message": "Remember to thaw the chicken"}` to a recipe and use the webhooks built into mealie to send that message payload to a destination to be processed.
#### Shopping List and Food Extras
Similarly to recipes, extras are supported on shopping lists, shopping list items, and foods. At this time they are only accessible through the API. Extras for these objects allow for rich integrations between the Mealie shopping list and your favorite list manager, such as Alexa, ToDoist, Trello, or any other list manager with an API.
Similarly to recipes, extras are supported on shopping lists, shopping list items, and foods. At this time they are only accessible through the API. Extras for these objects allow for rich integrations between the Mealie shopping list and your favorite list manager, such as Todoist, Trello, or any other list manager with an API.
To keep shopping lists in sync, for instance, you can store your Trello list id on your Mealie shopping list: <br />
`{"trello_list_id": "5abbe4b7ddc1b351ef961414"}`
@@ -52,6 +52,7 @@ Many applications will keep track of the query and adjust the page parameter app
Notice that the route does not contain the baseurl (e.g. `https://mymealieapplication.com/api`).
There are a few shorthands available to reduce the number of calls for certain common requests:
- if you want to return _all_ results, effectively disabling pagination, set `perPage = -1` (and fetch the first page)
- if you want to fetch the _last_ page, set `page = -1`
@@ -89,6 +90,28 @@ This filter will find all recipes that don't start with the word "Test": <br>
This filter will find all recipes that have particular slugs: <br>
`slug IN ["pasta-fagioli", "delicious-ramen"]`
##### Placeholder Keywords
You can use placeholders to insert dynamic values as opposed to static values. Currently the only supported placeholder keyword is `$NOW`, to insert the current time.
`$NOW` can optionally be paired with basic offsets. Here is an example of a filter which gives you recipes not made within the past 30 days: <br>
`lastMade <= "$NOW-30d"`
Supported offsets operations include:
- `-` for subtracting a time (i.e. in the past)
- `+` for adding a time (i.e. in the future)
Supported offset intervals include:
- `y` for years
- `m` for months
- `d` for days
- `H` for hours
- `M` for minutes
- `S` for seconds
Note that intervals are _case sensitive_ (e.g. `s` is an invalid interval).
##### Nested Property filters
When querying tables with relationships, you can filter properties on related tables. For instance, if you want to query all recipes owned by a particular user: <br>
`user.username = "SousChef20220320"`
@@ -96,7 +119,7 @@ When querying tables with relationships, you can filter properties on related ta
This timeline event filter will return all timeline events for recipes that were created after a particular date: <br>
`recipe.createdAt >= "2023-02-25"`
This recipe filter will return all recipes that contains a particular set of tags: <br>
This recipe filter will return all recipes that contain a particular set of tags: <br>
`tags.name CONTAINS ALL ["Easy", "Cajun"]`
##### Compound Filters

View File

@@ -9,7 +9,7 @@
Mealie supports 3rd party authentication via [OpenID Connect (OIDC)](https://openid.net/connect/), an identity layer built on top of OAuth2. OIDC is supported by many Identity Providers (IdP), including:
- [Authentik](https://goauthentik.io/integrations/sources/oauth/#openid-connect)
- [Authentik](https://integrations.goauthentik.io/documentation/mealie/)
- [Authelia](https://www.authelia.com/integration/openid-connect/mealie/)
- [Keycloak](https://www.keycloak.org/docs/latest/securing_apps/#_oidc)
- [Okta](https://www.okta.com/openid-connect/)
@@ -68,7 +68,6 @@ Example configurations for several Identity Providers have been provided by the
If you don't see your provider and have successfully set it up, please consider [creating your own example](https://github.com/mealie-recipes/mealie/discussions/new?category=oauth-provider-example) so that others can have a smoother setup.
## Migration from Mealie v1.x
**High level changes**

View File

@@ -85,13 +85,13 @@ The meal planner has the concept of plan rules. These offer a flexible way to us
The shopping lists feature is a great way to keep track of what you need to buy for your next meal. You can add items directly to the shopping list or link a recipe and all of it's ingredients to track meals during the week.
Managing shopping lists can be done from the Sidebar > Shopping Lists.
Managing shopping lists can be done from the Sidebar > Shopping Lists.
Here you will be able to:
- See items already on the Shopping List
- See linked recipes with ingredients
- Toggling via the 'Pot' icon will show you the linked recipe, allowing you to click to access it.
- Toggling via the 'Pot' icon will show you the linked recipe, allowing you to click to access it.
- Check off an item
- Add / Change / Remove / Sort Items via the grid icon
- Be sure if you are modifying an ingredient to click the 'Save' icon.
@@ -103,13 +103,10 @@ Here you will be able to:
!!! tip
You can use Labels to categorize your ingredients. You may want to Label by Food Type (Frozen, Fresh, etc), by Store, Tool, Recipe, or more. Play around with this to see what works best for you.
!!! tip
You can toggle 'Food' on items so that if you add multiple of the same food / ingredient, Mealie will automatically combine them together. Do this by editing an item in the Shopping List and clicking the 'Apple' icon. If you then have recipes that contain "1 | cup | cheese" and "2 | cup | cheese" this would be combined to show "3 cups of cheese."
[See FAQ for more information](../getting-started/faq.md)
[Shopping List Demo](https://demo.mealie.io/shopping-lists){ .md-button .md-button--primary }
## Integrations
@@ -198,7 +195,7 @@ Mealie lets you fully customize how you organize your users. You can use Groups
### Groups
Groups are fully isolated instances of Mealie. Think of a goup as a completely separate, fully self-contained site. There is no data shared between groups. Each group has its own users, recipes, tags, categories, etc. A user logged-in to one group cannot make any changes to another.
Groups are fully isolated instances of Mealie. Think of a group as a completely separate, fully self-contained site. There is no data shared between groups. Each group has its own users, recipes, tags, categories, etc. A user logged-in to one group cannot make any changes to another.
Common use cases for groups include:

View File

@@ -122,17 +122,18 @@ For usage, see [Usage - OpenID Connect](../authentication/oidc-v2.md)
Mealie supports various integrations using OpenAI. For more information, check out our [OpenAI documentation](./open-ai.md).
For custom mapping variables (e.g. OPENAI_CUSTOM_HEADERS) you should pass values as JSON encoded strings (e.g. `OPENAI_CUSTOM_PARAMS='{"k1": "v1", "k2": "v2"}'`)
| Variables | Default | Description |
| ------------------------------------------------- | :-----: | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| OPENAI_BASE_URL<super>[&dagger;][secrets]</super> | None | The base URL for the OpenAI API. If you're not sure, leave this empty to use the standard OpenAI platform |
| OPENAI_API_KEY<super>[&dagger;][secrets]</super> | None | Your OpenAI API Key. Enables OpenAI-related features |
| OPENAI_MODEL | gpt-4o | Which OpenAI model to use. If you're not sure, leave this empty |
| OPENAI_CUSTOM_HEADERS | None | Custom HTTP headers to add to all OpenAI requests. This should generally be left empty unless your custom service requires them |
| OPENAI_CUSTOM_PARAMS | None | Custom HTTP query params to add to all OpenAI requests. This should generally be left empty unless your custom service requires them |
| OPENAI_ENABLE_IMAGE_SERVICES | True | Whether to enable OpenAI image services, such as creating recipes via image. Leave this enabled unless your custom model doesn't support it, or you want to reduce costs |
| OPENAI_WORKERS | 2 | Number of OpenAI workers per request. Higher values may increase processing speed, but will incur additional API costs |
| OPENAI_SEND_DATABASE_DATA | True | Whether to send Mealie data to OpenAI to improve request accuracy. This will incur additional API costs |
| OPENAI_REQUEST_TIMEOUT | 300 | The number of seconds to wait for an OpenAI request to complete before cancelling the request. Leave this empty unless you're running into timeout issues on slower hardware |
| Variables | Default | Description |
|---------------------------------------------------|:-------:|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| OPENAI_BASE_URL<super>[&dagger;][secrets]</super> | None | The base URL for the OpenAI API. If you're not sure, leave this empty to use the standard OpenAI platform |
| OPENAI_API_KEY<super>[&dagger;][secrets]</super> | None | Your OpenAI API Key. Enables OpenAI-related features |
| OPENAI_MODEL | gpt-4o | Which OpenAI model to use. If you're not sure, leave this empty |
| OPENAI_CUSTOM_HEADERS | None | Custom HTTP headers to add to all OpenAI requests. This should generally be left empty unless your custom service requires them |
| OPENAI_CUSTOM_PARAMS | None | Custom HTTP query params to add to all OpenAI requests. This should generally be left empty unless your custom service requires them |
| OPENAI_ENABLE_IMAGE_SERVICES | True | Whether to enable OpenAI image services, such as creating recipes via image. Leave this enabled unless your custom model doesn't support it, or you want to reduce costs |
| OPENAI_WORKERS | 2 | Number of OpenAI workers per request. Higher values may increase processing speed, but will incur additional API costs |
| OPENAI_SEND_DATABASE_DATA | True | Whether to send Mealie data to OpenAI to improve request accuracy. This will incur additional API costs |
| OPENAI_REQUEST_TIMEOUT | 300 | The number of seconds to wait for an OpenAI request to complete before cancelling the request. Leave this empty unless you're running into timeout issues on slower hardware |
| OPENAI_CUSTOM_PROMPT_DIR | None | Path to custom prompt files. Only existing files in your custom directory will override the defaults; any missing or empty custom files will automatically fall back to the system defaults. See https://github.com/mealie-recipes/mealie/tree/mealie-next/mealie/services/openai/prompts for expected file names. |
### Theming
@@ -145,22 +146,99 @@ Setting the following environmental variables will change the theme of the front
If using YAML sequence syntax, don't include any quotes:<br>`THEME_LIGHT_PRIMARY=#E58325` or `THEME_LIGHT_PRIMARY=E58325`
| Variables | Default | Description |
| --------------------- | :-----: | --------------------------- |
| THEME_LIGHT_PRIMARY | #E58325 | Light Theme Config Variable |
| THEME_LIGHT_ACCENT | #007A99 | Light Theme Config Variable |
| THEME_LIGHT_SECONDARY | #973542 | Light Theme Config Variable |
| THEME_LIGHT_SUCCESS | #43A047 | Light Theme Config Variable |
| THEME_LIGHT_INFO | #1976D2 | Light Theme Config Variable |
| THEME_LIGHT_WARNING | #FF6D00 | Light Theme Config Variable |
| THEME_LIGHT_ERROR | #EF5350 | Light Theme Config Variable |
| THEME_DARK_PRIMARY | #E58325 | Dark Theme Config Variable |
| THEME_DARK_ACCENT | #007A99 | Dark Theme Config Variable |
| THEME_DARK_SECONDARY | #973542 | Dark Theme Config Variable |
| THEME_DARK_SUCCESS | #43A047 | Dark Theme Config Variable |
| THEME_DARK_INFO | #1976D2 | Dark Theme Config Variable |
| THEME_DARK_WARNING | #FF6D00 | Dark Theme Config Variable |
| THEME_DARK_ERROR | #EF5350 | Dark Theme Config Variable |
| Variables | Default | Description |
| --------------------- | :-----: | ---------------------------------- |
| THEME_LIGHT_PRIMARY | #E58325 | Main brand color and headers |
| THEME_LIGHT_ACCENT | #007A99 | Buttons and interactive elements |
| THEME_LIGHT_SECONDARY | #973542 | Navigation and sidebar backgrounds |
| THEME_LIGHT_SUCCESS | #43A047 | Success messages and confirmations |
| THEME_LIGHT_INFO | #1976D2 | Information alerts and tooltips |
| THEME_LIGHT_WARNING | #FF6D00 | Warning notifications |
| THEME_LIGHT_ERROR | #EF5350 | Error messages and alerts |
| THEME_DARK_PRIMARY | #E58325 | Main brand color and headers |
| THEME_DARK_ACCENT | #007A99 | Buttons and interactive elements |
| THEME_DARK_SECONDARY | #973542 | Navigation and sidebar backgrounds |
| THEME_DARK_SUCCESS | #43A047 | Success messages and confirmations |
| THEME_DARK_INFO | #1976D2 | Information alerts and tooltips |
| THEME_DARK_WARNING | #FF6D00 | Warning notifications |
| THEME_DARK_ERROR | #EF5350 | Error messages and alerts |
#### Theming Examples
The examples below provide copy-ready Docker Compose environment configurations for three different color palettes. Copy and paste the desired theme into your `docker-compose.yml` file's environment section.
!!! info
These themes are functional and ready to use, but they are provided primarily as examples. The color palettes can be adjusted or refined to better suit your preferences.
=== "Blue Theme"
```yaml
environment:
# Light mode colors
THEME_LIGHT_PRIMARY: '#5E9BD1'
THEME_LIGHT_ACCENT: '#A3C9E8'
THEME_LIGHT_SECONDARY: '#4F89BA'
THEME_LIGHT_SUCCESS: '#4CAF50'
THEME_LIGHT_INFO: '#4A9ED8'
THEME_LIGHT_WARNING: '#EAC46B'
THEME_LIGHT_ERROR: '#E57373'
# Dark mode colors
THEME_DARK_PRIMARY: '#5A8FBF'
THEME_DARK_ACCENT: '#90B8D9'
THEME_DARK_SECONDARY: '#406D96'
THEME_DARK_SUCCESS: '#81C784'
THEME_DARK_INFO: '#78B2C0'
THEME_DARK_WARNING: '#EBC86E'
THEME_DARK_ERROR: '#E57373'
```
=== "Green Theme"
```yaml
environment:
# Light mode colors
THEME_LIGHT_PRIMARY: '#75A86C'
THEME_LIGHT_ACCENT: '#A8D0A6'
THEME_LIGHT_SECONDARY: '#638E5E'
THEME_LIGHT_SUCCESS: '#4CAF50'
THEME_LIGHT_INFO: '#4A9ED8'
THEME_LIGHT_WARNING: '#EAC46B'
THEME_LIGHT_ERROR: '#E57373'
# Dark mode colors
THEME_DARK_PRIMARY: '#739B7A'
THEME_DARK_ACCENT: '#9FBE9D'
THEME_DARK_SECONDARY: '#56775E'
THEME_DARK_SUCCESS: '#81C784'
THEME_DARK_INFO: '#78B2C0'
THEME_DARK_WARNING: '#EBC86E'
THEME_DARK_ERROR: '#E57373'
```
=== "Pink Theme"
```yaml
environment:
# Light mode colors
THEME_LIGHT_PRIMARY: '#D97C96'
THEME_LIGHT_ACCENT: '#E891A7'
THEME_LIGHT_SECONDARY: '#C86C88'
THEME_LIGHT_SUCCESS: '#4CAF50'
THEME_LIGHT_INFO: '#2196F3'
THEME_LIGHT_WARNING: '#FFC107'
THEME_LIGHT_ERROR: '#E57373'
# Dark mode colors
THEME_DARK_PRIMARY: '#C2185B'
THEME_DARK_ACCENT: '#FF80AB'
THEME_DARK_SECONDARY: '#AD1457'
THEME_DARK_SUCCESS: '#81C784'
THEME_DARK_INFO: '#64B5F6'
THEME_DARK_WARNING: '#FFD54F'
THEME_DARK_ERROR: '#E57373'
```
!!! info
Browser cookies may cause the client to keep outdated settings.
Clearing the cookies can be required for the change to take effect.
### Docker Secrets

View File

@@ -31,7 +31,7 @@ To deploy mealie on your local network, it is highly recommended to use Docker t
We've gone through a few versions of Mealie v1 deployment targets. We have settled on a single container deployment, and we've begun publishing the nightly container on github containers. If you're looking to move from the old nightly (split containers _or_ the omni image) to the new nightly, there are a few things you need to do:
1. Take a backup just in case!
2. Replace the image for the API container with `ghcr.io/mealie-recipes/mealie:v3.5.0`
2. Replace the image for the API container with `ghcr.io/mealie-recipes/mealie:v3.10.0`
3. Take the external port from the frontend container and set that as the port mapped to port `9000` on the new container. The frontend is now served on port 9000 from the new container, so it will need to be mapped for you to have access.
4. Restart the container

View File

@@ -10,7 +10,7 @@ PostgreSQL might be considered if you need to support many concurrent users. In
```yaml
services:
mealie:
image: ghcr.io/mealie-recipes/mealie:v3.5.0 # (3)
image: ghcr.io/mealie-recipes/mealie:v3.10.0 # (3)
container_name: mealie
restart: always
ports:

View File

@@ -11,7 +11,7 @@ SQLite is a popular, open source, self-contained, zero-configuration database th
```yaml
services:
mealie:
image: ghcr.io/mealie-recipes/mealie:v3.5.0 # (3)
image: ghcr.io/mealie-recipes/mealie:v3.10.0 # (3)
container_name: mealie
restart: always
ports:

View File

@@ -9,6 +9,23 @@
- Create a Backup and Download from the UI
- Upgrade
!!! info "Improved Image Processing"
Starting with :octicons-tag-24: v3.7.0, we updated our image processing algorithm to improve image quality and compression. New image processing can be up to 40%-50% smaller on disk while providing higher resolution thumbnails. To take advantage of these improvements on older recipes, you can run our image-processing script:
```shell
docker exec -it mealie bash
python /opt/mealie/lib64/python3.12/site-packages/mealie/scripts/reprocess_images.py
```
### Options
- `--workers N`: Number of worker threads (default: 2, safe for low-powered devices)
- `--force-all`: Reprocess all recipes regardless of current image state
### Example
```shell
python /opt/mealie/lib64/python3.12/site-packages/mealie/scripts/reprocess_images.py --workers 8
```
## Upgrading to Mealie v1 or later
If you are upgrading from pre-v1.0.0 to v1.0.0 or later (v2.0.0, etc.), make sure you read [Migrating to Mealie v1](./migrating-to-mealie-v1.md)!

View File

File diff suppressed because one or more lines are too long

View File

@@ -32,8 +32,8 @@ theme:
markdown_extensions:
- pymdownx.emoji:
emoji_index: !!python/name:materialx.emoji.twemoji
emoji_generator: !!python/name:materialx.emoji.to_svg
emoji_index: !!python/name:material.extensions.emoji.twemoji
emoji_generator: !!python/name:material.extensions.emoji.to_svg
- def_list
- pymdownx.highlight
- pymdownx.superfences

View File

@@ -0,0 +1,121 @@
<template>
<v-container max-width="880" class="end-page-content">
<div class="d-flex flex-column ga-6">
<div>
<v-card-title class="text-h4 justify-center">
{{ $t('admin.setup.setup-complete') }}
</v-card-title>
<v-card-subtitle class="justify-center">
{{ $t('admin.setup.here-are-a-few-things-to-help-you-get-started') }}
</v-card-subtitle>
</div>
<div
v-for="section, idx in sections"
:key="idx"
class="d-flex flex-column ga-3"
>
<v-card-title class="text-h6 pl-0">
{{ section.title }}
</v-card-title>
<div class="sections d-flex flex-column ga-2">
<v-card
v-for="link, linkIdx in section.links"
:key="linkIdx"
clas="link-card"
:href="link.to"
:title="link.text"
:subtitle="link.description"
:append-icon="$globals.icons.chevronRight"
>
<template #prepend>
<v-avatar :icon="link.icon || undefined" variant="tonal" :color="section.color" />
</template>
</v-card>
</div>
</div>
</div>
</v-container>
</template>
<script lang="ts">
export default defineNuxtComponent({
setup() {
const i18n = useI18n();
const $auth = useMealieAuth();
const groupSlug = computed(() => $auth.user.value?.groupSlug);
const { $globals } = useNuxtApp();
const sections = ref([
{
title: i18n.t("profile.data-migrations"),
color: "info",
links: [
{
icon: $globals.icons.backupRestore,
to: "/admin/backups",
text: i18n.t("settings.backup.backup-restore"),
description: i18n.t("admin.setup.restore-from-v1-backup"),
},
{
icon: $globals.icons.import,
to: "/group/migrations",
text: i18n.t("migration.recipe-migration"),
description: i18n.t("migration.coming-from-another-application-or-an-even-older-version-of-mealie"),
},
],
},
{
title: i18n.t("recipe.create-recipes"),
color: "success",
links: [
{
icon: $globals.icons.createAlt,
to: computed(() => `/g/${groupSlug.value || ""}/r/create/new`),
text: i18n.t("recipe.create-recipe"),
description: i18n.t("recipe.create-recipe-description"),
},
{
icon: $globals.icons.link,
to: computed(() => `/g/${groupSlug.value || ""}/r/create/url`),
text: i18n.t("recipe.import-with-url"),
description: i18n.t("recipe.scrape-recipe-description"),
},
],
},
{
title: i18n.t("user.manage-users"),
color: "primary",
links: [
{
icon: $globals.icons.group,
to: "/admin/manage/users",
text: i18n.t("user.manage-users"),
description: i18n.t("user.manage-users-description"),
},
{
icon: $globals.icons.user,
to: "/user/profile",
text: i18n.t("profile.manage-user-profile"),
description: i18n.t("admin.setup.manage-profile-or-get-invite-link"),
},
],
},
]);
return { sections };
},
});
</script>
<style>
.v-container {
.v-card-title,
.v-card-subtitle {
padding: 0;
white-space: unset;
}
.v-card-item {
gap: 0.5rem;
}
}
</style>

View File

@@ -83,6 +83,11 @@ const fieldDefs: FieldDefinition[] = [
label: i18n.t("household.households"),
type: Organizer.Household,
},
{
name: "user_id",
label: i18n.t("user.users"),
type: Organizer.User,
},
{
name: "created_at",
label: i18n.t("general.date-created"),

View File

@@ -14,7 +14,7 @@
<BaseButton
download
size="small"
:download-url="`/api/recipes/bulk-actions/export/download?path=${item.path}`"
:download-url="`/api/recipes/bulk-actions/export/${item.id}/download`"
/>
</template>
</v-data-table>

View File

@@ -15,7 +15,6 @@
:nudge-top="menuTop ? '5' : '0'"
allow-overflow
close-delay="125"
:open-on-hover="mdAndUp"
content-class="d-print-none"
>
<template #activator="{ props: activatorProps }">
@@ -83,8 +82,6 @@ const emit = defineEmits<{
[key: string]: [];
}>();
const { mdAndUp } = useDisplay();
const i18n = useI18n();
const { $globals } = useNuxtApp();
const api = useUserApi();
@@ -94,7 +91,7 @@ const state = reactive({
shoppingListDialog: false,
menuItems: [
{
title: i18n.t("recipe.add-to-list"),
title: i18n.t("meal-plan.add-day-to-list"),
icon: $globals.icons.cartCheck,
color: undefined,
event: "shoppingList",
@@ -126,8 +123,8 @@ async function getShoppingLists() {
// eslint-disable-next-line @typescript-eslint/no-invalid-void-type
const eventHandlers: { [key: string]: () => void | Promise<any> } = {
shoppingList: () => {
getShoppingLists();
shoppingList: async () => {
await getShoppingLists();
state.shoppingListDialog = true;
},
};

View File

@@ -36,7 +36,7 @@
import QueryFilterBuilder from "~/components/Domain/QueryFilterBuilder.vue";
import type { FieldDefinition } from "~/composables/use-query-filter-builder";
import { Organizer } from "~/lib/api/types/non-generated";
import type { QueryFilterJSON } from "~/lib/api/types/response";
import type { QueryFilterJSON } from "~/lib/api/types/non-generated";
interface Props {
queryFilter?: QueryFilterJSON | null;
@@ -58,6 +58,9 @@ const MEAL_TYPE_OPTIONS = [
{ title: i18n.t("meal-plan.lunch"), value: "lunch" },
{ title: i18n.t("meal-plan.dinner"), value: "dinner" },
{ title: i18n.t("meal-plan.side"), value: "side" },
{ title: i18n.t("meal-plan.snack"), value: "snack" },
{ title: i18n.t("meal-plan.drink"), value: "drink" },
{ title: i18n.t("meal-plan.dessert"), value: "dessert" },
{ title: i18n.t("meal-plan.type-any"), value: "unset" },
];
@@ -103,6 +106,11 @@ const fieldDefs: FieldDefinition[] = [
label: i18n.t("household.households"),
type: Organizer.Household,
},
{
name: "user_id",
label: i18n.t("user.users"),
type: Organizer.User,
},
{
name: "last_made",
label: i18n.t("general.last-made"),

View File

@@ -1,283 +1,299 @@
<template>
<v-card class="ma-0" style="overflow-x: auto;">
<v-card class="ma-0" flat fluid>
<v-card-text class="ma-0 pa-0">
<v-container fluid class="ma-0 pa-0">
<VueDraggable
v-model="fields"
handle=".handle"
:delay="250"
:delay-on-touch-only="true"
v-bind="{
animation: 200,
group: 'recipe-instructions',
ghostClass: 'ghost',
}"
@start="drag = true"
@end="onDragEnd"
<VueDraggable
v-model="fields"
handle=".handle"
:delay="250"
:delay-on-touch-only="true"
v-bind="{
animation: 200,
group: 'recipe-instructions',
ghostClass: 'ghost',
}"
@start="drag = true"
@end="onDragEnd"
>
<v-row
v-for="(field, index) in fields"
:key="field.id"
class="d-flex flex-row flex-wrap mx-auto pb-2"
:class="$vuetify.display.xs ? (Math.floor(index / 1) % 2 === 0 ? 'bg-dark' : 'bg-light') : ''"
style="max-width: 100%;"
>
<v-row
v-for="(field, index) in fields"
:key="field.id"
class="d-flex flex-nowrap"
style="max-width: 100%;"
<!-- drag handle -->
<v-col
:cols="config.items.icon.cols(index)"
:sm="config.items.icon.sm(index)"
:class="$vuetify.display.smAndDown ? 'd-flex pa-0' : 'd-flex justify-end pr-6'"
>
<!-- drag handle -->
<v-col
:cols="config.items.icon.cols"
:class="config.col.class"
:style="config.items.icon.style"
<v-icon class="handle my-auto" :size="28" style="cursor: move;">
{{ $globals.icons.arrowUpDown }}
</v-icon>
</v-col>
<!-- and / or -->
<v-col
v-if="index != 0 || $vuetify.display.smAndUp"
:cols="config.items.logicalOperator.cols(index)"
:sm="config.items.logicalOperator.sm(index)"
:class="config.col.class"
>
<v-select
v-if="index"
:model-value="field.logicalOperator"
:items="[logOps.AND, logOps.OR]"
item-title="label"
item-value="value"
variant="underlined"
@update:model-value="setLogicalOperatorValue(field, index, $event as unknown as LogicalOperator)"
>
<v-icon
class="handle"
:size="24"
style="cursor: move;margin: auto;"
>
{{ $globals.icons.arrowUpDown }}
</v-icon>
</v-col>
<!-- and / or -->
<v-col
:cols="config.items.logicalOperator.cols"
:class="config.col.class"
:style="config.items.logicalOperator.style"
<template #chip="{ item }">
<span :class="config.select.textClass" style="width: 100%;">
{{ item.raw.label }}
</span>
</template>
</v-select>
</v-col>
<!-- left parenthesis -->
<v-col
v-if="showAdvanced"
:cols="config.items.leftParens.cols(index)"
:sm="config.items.leftParens.sm(index)"
:class="config.col.class"
>
<v-select
:model-value="field.leftParenthesis"
:items="['', '(', '((', '(((']"
variant="underlined"
@update:model-value="setLeftParenthesisValue(field, index, $event)"
>
<v-select
v-if="index"
:model-value="field.logicalOperator"
:items="[logOps.AND, logOps.OR]"
item-title="label"
item-value="value"
variant="underlined"
@update:model-value="setLogicalOperatorValue(field, index, $event as unknown as LogicalOperator)"
>
<template #chip="{ item }">
<span :class="config.select.textClass" style="width: 100%;">
{{ item.raw.label }}
</span>
</template>
</v-select>
</v-col>
<!-- left parenthesis -->
<v-col
v-if="showAdvanced"
:cols="config.items.leftParens.cols"
:class="config.col.class"
:style="config.items.leftParens.style"
<template #chip="{ item }">
<span :class="config.select.textClass" style="width: 100%;">
{{ item.raw }}
</span>
</template>
</v-select>
</v-col>
<!-- field name -->
<v-col
:cols="config.items.fieldName.cols(index)"
:sm="config.items.fieldName.sm(index)"
:class="config.col.class"
>
<v-select
chips
:model-value="field.label"
:items="fieldDefs"
variant="underlined"
item-title="label"
@update:model-value="setField(index, $event)"
>
<v-select
:model-value="field.leftParenthesis"
:items="['', '(', '((', '(((']"
variant="underlined"
@update:model-value="setLeftParenthesisValue(field, index, $event)"
>
<template #chip="{ item }">
<span :class="config.select.textClass" style="width: 100%;">
{{ item.raw }}
</span>
</template>
</v-select>
</v-col>
<!-- field name -->
<v-col
:cols="config.items.fieldName.cols"
:class="config.col.class"
:style="config.items.fieldName.style"
<template #chip="{ item }">
<span :class="config.select.textClass" style="width: 100%;">
{{ item.raw.label }}
</span>
</template>
</v-select>
</v-col>
<!-- relational operator -->
<v-col
:cols="config.items.relationalOperator.cols(index)"
:sm="config.items.relationalOperator.sm(index)"
:class="config.col.class"
>
<v-select
v-if="field.type !== 'boolean'"
:model-value="field.relationalOperatorValue"
:items="field.relationalOperatorOptions"
item-title="label"
item-value="value"
variant="underlined"
@update:model-value="setRelationalOperatorValue(field, index, $event as unknown as RelationalKeyword | RelationalOperator)"
>
<v-select
chips
:model-value="field.label"
:items="fieldDefs"
variant="underlined"
item-title="label"
@update:model-value="setField(index, $event)"
>
<template #chip="{ item }">
<span :class="config.select.textClass" style="width: 100%;">
{{ item.raw.label }}
</span>
</template>
</v-select>
</v-col>
<!-- relational operator -->
<v-col
:cols="config.items.relationalOperator.cols"
:class="config.col.class"
:style="config.items.relationalOperator.style"
<template #chip="{ item }">
<span :class="config.select.textClass" style="width: 100%;">
{{ item.raw.label }}
</span>
</template>
</v-select>
</v-col>
<!-- field value -->
<v-col
:cols="config.items.fieldValue.cols(index)"
:sm="config.items.fieldValue.sm(index)"
:class="config.col.class"
>
<v-select
v-if="field.fieldOptions"
:model-value="field.values"
:items="field.fieldOptions"
item-title="label"
item-value="value"
multiple
variant="underlined"
@update:model-value="setFieldValues(field, index, $event)"
/>
<v-text-field
v-else-if="field.type === 'string'"
:model-value="field.value"
variant="underlined"
@update:model-value="setFieldValue(field, index, $event)"
/>
<v-number-input
v-else-if="field.type === 'number'"
:model-value="field.value"
variant="underlined"
control-variant="stacked"
inset
:precision="null"
@update:model-value="setFieldValue(field, index, $event)"
/>
<v-checkbox
v-else-if="field.type === 'boolean'"
:model-value="field.value"
@update:model-value="setFieldValue(field, index, $event!)"
/>
<v-menu
v-else-if="field.type === 'date'"
v-model="datePickers[index]"
:close-on-content-click="false"
transition="scale-transition"
offset-y
max-width="290px"
min-width="auto"
>
<v-select
v-if="field.type !== 'boolean'"
:model-value="field.relationalOperatorValue"
:items="field.relationalOperatorOptions"
item-title="label"
item-value="value"
variant="underlined"
@update:model-value="setRelationalOperatorValue(field, index, $event as unknown as RelationalKeyword | RelationalOperator)"
>
<template #chip="{ item }">
<span :class="config.select.textClass" style="width: 100%;">
{{ item.raw.label }}
</span>
</template>
</v-select>
</v-col>
<!-- field value -->
<v-col
:cols="config.items.fieldValue.cols"
:class="config.col.class"
:style="config.items.fieldValue.style"
>
<v-select
v-if="field.fieldOptions"
:model-value="field.values"
:items="field.fieldOptions"
item-title="label"
item-value="value"
multiple
variant="underlined"
@update:model-value="setFieldValues(field, index, $event)"
/>
<v-text-field
v-else-if="field.type === 'string'"
:model-value="field.value"
variant="underlined"
@update:model-value="setFieldValue(field, index, $event)"
/>
<v-text-field
v-else-if="field.type === 'number'"
:model-value="field.value"
type="number"
variant="underlined"
@update:model-value="setFieldValue(field, index, $event)"
/>
<v-checkbox
v-else-if="field.type === 'boolean'"
:model-value="field.value"
@update:model-value="setFieldValue(field, index, $event!)"
/>
<v-menu
v-else-if="field.type === 'date'"
v-model="datePickers[index]"
:close-on-content-click="false"
transition="scale-transition"
offset-y
max-width="290px"
min-width="auto"
>
<template #activator="{ props: activatorProps }">
<v-text-field
:model-value="field.value ? $d(new Date(field.value + 'T00:00:00')) : null"
persistent-hint
:prepend-icon="$globals.icons.calendar"
variant="underlined"
color="primary"
v-bind="activatorProps"
readonly
/>
</template>
<v-date-picker
:model-value="field.value ? new Date(field.value + 'T00:00:00') : null"
hide-header
:first-day-of-week="firstDayOfWeek"
:local="$i18n.locale"
@update:model-value="val => setFieldValue(field, index, val ? val.toISOString().slice(0, 10) : '')"
<template #activator="{ props: activatorProps }">
<v-text-field
:model-value="field.value ? $d(new Date(field.value + 'T00:00:00')) : null"
persistent-hint
:prepend-icon="$globals.icons.calendar"
variant="underlined"
color="primary"
v-bind="activatorProps"
readonly
/>
</v-menu>
<RecipeOrganizerSelector
v-else-if="field.type === Organizer.Category"
v-model="field.organizers"
:selector-type="Organizer.Category"
:show-add="false"
:show-label="false"
:show-icon="false"
variant="underlined"
@update:model-value="setFieldOrganizers(field, index, $event)"
</template>
<v-date-picker
:model-value="field.value ? new Date(field.value + 'T00:00:00') : null"
hide-header
:first-day-of-week="firstDayOfWeek"
:local="$i18n.locale"
@update:model-value="val => setFieldValue(field, index, val ? val.toISOString().slice(0, 10) : '')"
/>
<RecipeOrganizerSelector
v-else-if="field.type === Organizer.Tag"
v-model="field.organizers"
:selector-type="Organizer.Tag"
:show-add="false"
:show-label="false"
:show-icon="false"
variant="underlined"
@update:model-value="setFieldOrganizers(field, index, $event)"
/>
<RecipeOrganizerSelector
v-else-if="field.type === Organizer.Tool"
v-model="field.organizers"
:selector-type="Organizer.Tool"
:show-add="false"
:show-label="false"
:show-icon="false"
variant="underlined"
@update:model-value="setFieldOrganizers(field, index, $event)"
/>
<RecipeOrganizerSelector
v-else-if="field.type === Organizer.Food"
v-model="field.organizers"
:selector-type="Organizer.Food"
:show-add="false"
:show-label="false"
:show-icon="false"
variant="underlined"
@update:model-value="setFieldOrganizers(field, index, $event)"
/>
<RecipeOrganizerSelector
v-else-if="field.type === Organizer.Household"
v-model="field.organizers"
:selector-type="Organizer.Household"
:show-add="false"
:show-label="false"
:show-icon="false"
variant="underlined"
@update:model-value="setFieldOrganizers(field, index, $event)"
/>
</v-col>
<!-- right parenthesis -->
<v-col
v-if="showAdvanced"
:cols="config.items.rightParens.cols"
:class="config.col.class"
:style="config.items.rightParens.style"
</v-menu>
<RecipeOrganizerSelector
v-else-if="field.type === Organizer.Category"
v-model="field.organizers"
:selector-type="Organizer.Category"
:show-add="false"
:show-label="false"
:show-icon="false"
variant="underlined"
@update:model-value="val => setFieldOrganizers(field, index, (val || []) as OrganizerBase[])"
/>
<RecipeOrganizerSelector
v-else-if="field.type === Organizer.Tag"
v-model="field.organizers"
:selector-type="Organizer.Tag"
:show-add="false"
:show-label="false"
:show-icon="false"
variant="underlined"
@update:model-value="val => setFieldOrganizers(field, index, (val || []) as OrganizerBase[])"
/>
<RecipeOrganizerSelector
v-else-if="field.type === Organizer.Tool"
v-model="field.organizers"
:selector-type="Organizer.Tool"
:show-add="false"
:show-label="false"
:show-icon="false"
variant="underlined"
@update:model-value="val => setFieldOrganizers(field, index, (val || []) as OrganizerBase[])"
/>
<RecipeOrganizerSelector
v-else-if="field.type === Organizer.Food"
v-model="field.organizers"
:selector-type="Organizer.Food"
:show-add="false"
:show-label="false"
:show-icon="false"
variant="underlined"
@update:model-value="val => setFieldOrganizers(field, index, (val || []) as OrganizerBase[])"
/>
<RecipeOrganizerSelector
v-else-if="field.type === Organizer.Household"
v-model="field.organizers"
:selector-type="Organizer.Household"
:show-add="false"
:show-label="false"
:show-icon="false"
variant="underlined"
@update:model-value="val => setFieldOrganizers(field, index, (val || []) as OrganizerBase[])"
/>
<RecipeOrganizerSelector
v-else-if="field.type === Organizer.User"
v-model="field.organizers"
:selector-type="Organizer.User"
:show-add="false"
:show-label="false"
:show-icon="false"
variant="underlined"
@update:model-value="val => setFieldOrganizers(field, index, (val || []) as OrganizerBase[])"
/>
</v-col>
<!-- right parenthesis -->
<v-col
v-if="showAdvanced"
:cols="config.items.rightParens.cols(index)"
:sm="config.items.rightParens.sm(index)"
:class="config.col.class"
>
<v-select
:model-value="field.rightParenthesis"
:items="['', ')', '))', ')))']"
variant="underlined"
@update:model-value="setRightParenthesisValue(field, index, $event)"
>
<v-select
:model-value="field.rightParenthesis"
:items="['', ')', '))', ')))']"
variant="underlined"
@update:model-value="setRightParenthesisValue(field, index, $event)"
>
<template #chip="{ item }">
<span :class="config.select.textClass" style="width: 100%;">
{{ item.raw }}
</span>
</template>
</v-select>
</v-col>
<!-- field actions -->
<v-col
:cols="config.items.fieldActions.cols"
:class="config.col.class"
:style="config.items.fieldActions.style"
>
<BaseButtonGroup
:buttons="[
{
icon: $globals.icons.delete,
text: $t('general.delete'),
event: 'delete',
disabled: fields.length === 1,
},
]"
class="my-auto"
@delete="removeField(index)"
/>
</v-col>
</v-row>
</VueDraggable>
</v-container>
<template #chip="{ item }">
<span :class="config.select.textClass" style="width: 100%;">
{{ item.raw }}
</span>
</template>
</v-select>
</v-col>
<!-- field actions -->
<v-col
v-if="!$vuetify.display.smAndDown || index === fields.length - 1"
:cols="config.items.fieldActions.cols(index)"
:sm="config.items.fieldActions.sm(index)"
:class="config.col.class"
>
<BaseButtonGroup
:buttons="[
{
icon: $globals.icons.delete,
text: $t('general.delete'),
event: 'delete',
disabled: fields.length === 1,
},
]"
class="my-auto"
@delete="removeField(index)"
/>
</v-col>
</v-row>
</VueDraggable>
</v-card-text>
<v-card-actions>
<v-row fluid class="d-flex justify-end pa-0 mx-2">
<v-row fluid class="d-flex justify-end ma-2">
<v-spacer />
<v-checkbox
v-model="showAdvanced"
@@ -303,8 +319,9 @@ import { useDebounceFn } from "@vueuse/core";
import { useHouseholdSelf } from "~/composables/use-households";
import RecipeOrganizerSelector from "~/components/Domain/Recipe/RecipeOrganizerSelector.vue";
import { Organizer } from "~/lib/api/types/non-generated";
import type { LogicalOperator, QueryFilterJSON, QueryFilterJSONPart, RelationalKeyword, RelationalOperator } from "~/lib/api/types/response";
import type { LogicalOperator, QueryFilterJSON, QueryFilterJSONPart, RelationalKeyword, RelationalOperator } from "~/lib/api/types/non-generated";
import { useCategoryStore, useFoodStore, useHouseholdStore, useTagStore, useToolStore } from "~/composables/store";
import { useUserStore } from "~/composables/store/use-user-store";
import { type Field, type FieldDefinition, type FieldValue, type OrganizerBase, useQueryFilterBuilder } from "~/composables/use-query-filter-builder";
const props = defineProps({
@@ -344,6 +361,7 @@ const storeMap = {
[Organizer.Tool]: useToolStore(),
[Organizer.Food]: useFoodStore(),
[Organizer.Household]: useHouseholdStore(),
[Organizer.User]: useUserStore(),
};
function onDragEnd(event: any) {
@@ -602,46 +620,56 @@ function buildQueryFilterJSON(): QueryFilterJSON {
}
const config = computed(() => {
const baseColMaxWidth = 55;
const multiple = fields.value.length > 1;
const adv = state.showAdvanced;
return {
col: {
class: "d-flex justify-center align-end field-col pa-1",
class: "d-flex justify-center align-end py-0",
},
select: {
textClass: "d-flex justify-center text-center",
},
items: {
icon: {
cols: 1,
cols: (_index: number) => 2,
sm: (_index: number) => 1,
style: "width: fit-content;",
},
leftParens: {
cols: state.showAdvanced ? 1 : 0,
style: `min-width: ${state.showAdvanced ? baseColMaxWidth : 0}px;`,
cols: (index: number) => (adv ? (index === 0 ? 2 : 0) : 0),
sm: (_index: number) => (adv ? 1 : 0),
},
logicalOperator: {
cols: 1,
style: `min-width: ${baseColMaxWidth}px;`,
cols: (_index: number) => 0,
sm: (_index: number) => (multiple ? 1 : 0),
},
fieldName: {
cols: state.showAdvanced ? 2 : 3,
style: `min-width: ${state.showAdvanced ? baseColMaxWidth * 2 : baseColMaxWidth * 3}px;`,
cols: (index: number) => {
if (adv) return index === 0 ? 8 : 12;
return index === 0 ? 10 : 12;
},
sm: (_index: number) => (adv ? 2 : 3),
},
relationalOperator: {
cols: 2,
style: `min-width: ${baseColMaxWidth * 2}px;`,
cols: (_index: number) => 12,
sm: (_index: number) => 2,
},
fieldValue: {
cols: state.showAdvanced ? 3 : 4,
style: `min-width: ${state.showAdvanced ? baseColMaxWidth * 2 : baseColMaxWidth * 3}px;`,
cols: (index: number) => {
const last = index === fields.value.length - 1;
if (adv) return last ? 8 : 10;
return last ? 10 : 12;
},
sm: (_index: number) => (adv ? 3 : 4),
},
rightParens: {
cols: state.showAdvanced ? 1 : 0,
style: `min-width: ${state.showAdvanced ? baseColMaxWidth : 0}px;`,
cols: (index: number) => (adv ? (index === fields.value.length - 1 ? 2 : 0) : 0),
sm: (_index: number) => (adv ? 1 : 0),
},
fieldActions: {
cols: 1,
style: `min-width: ${baseColMaxWidth}px;`,
cols: (index: number) => (index === fields.value.length - 1 ? 2 : 0),
sm: (_index: number) => 1,
},
},
};
@@ -651,5 +679,14 @@ const config = computed(() => {
<style scoped>
* {
font-size: 1em;
--bg-opactity: calc(var(--v-hover-opacity) * var(--v-theme-overlay-multiplier));
}
.bg-dark {
background-color: rgba(0, 0, 0, var(--bg-opactity));
}
.bg-light {
background-color: rgba(255, 255, 255, var(--bg-opactity));
}
</style>

View File

@@ -79,7 +79,7 @@
@print="$emit('print')"
/>
</div>
<div v-if="open" class="custom-btn-group gapped">
<div v-if="open" class="custom-btn-group gapped ma-1">
<v-btn
v-for="(btn, index) in editorButtons"
:key="index"
@@ -126,7 +126,7 @@ withDefaults(defineProps<Props>(), {
canEdit: false,
});
const emit = defineEmits(["print", "input", "delete", "close", "edit"]);
const emit = defineEmits(["print", "input", "save", "delete", "close", "json", "edit"]);
const deleteDialog = ref(false);

View File

@@ -1,60 +1,97 @@
<template>
<div v-if="model.length > 0 || edit">
<v-card class="mt-4">
<v-card-title class="py-2">
{{ $t("asset.assets") }}
</v-card-title>
<v-list-item class="pr-2 pl-0">
<v-card-title>
{{ $t("asset.assets") }}
</v-card-title>
<template #append>
<v-btn
v-if="edit"
variant="plain"
:icon="$globals.icons.create"
@click="state.newAssetDialog = true"
/>
</template>
</v-list-item>
<v-divider class="mx-2" />
<v-list
v-if="model.length > 0"
lines="two"
:flat="!edit"
>
<v-list-item
v-for="(item, i) in model"
:key="i"
:href="!edit ? assetURL(item.fileName ?? '') : ''"
target="_blank"
class="pr-2"
>
<template #prepend>
<div class="ma-auto">
<v-tooltip location="bottom">
<template #activator="{ props: tooltipProps }">
<v-icon v-bind="tooltipProps">
{{ getIconDefinition(item.icon).icon }}
</v-icon>
</template>
<span>{{ getIconDefinition(item.icon).title }}</span>
</v-tooltip>
</div>
<v-avatar size="48" rounded="lg" class="elevation-1">
<v-img
v-if="isImage(item.fileName)"
:src="assetURL(item.fileName ?? '')"
:alt="item.name"
loading="lazy"
cover
/>
<v-icon v-else size="large">
{{ getIconDefinition(item.icon).icon }}
</v-icon>
</v-avatar>
</template>
<v-list-item-title class="pl-2">
<v-list-item-title>
{{ item.name }}
</v-list-item-title>
<template #append>
<v-menu v-if="edit" location="bottom end">
<template #activator="{ props: menuProps }">
<v-btn
v-bind="menuProps"
icon
variant="plain"
>
<v-icon :icon="$globals.icons.dotsVertical" />
</v-btn>
</template>
<v-list density="compact" min-width="220">
<v-list-item
:href="assetURL(item.fileName ?? '')"
:prepend-icon="$globals.icons.eye"
:title="$t('general.view')"
target="_blank"
/>
<v-list-item
:href="assetURL(item.fileName ?? '')"
:prepend-icon="$globals.icons.download"
:title="$t('general.download')"
download
/>
<v-list-item
v-if="edit"
:prepend-icon="$globals.icons.contentCopy"
:title="$t('general.copy')"
@click="copyText(assetEmbed(item.fileName ?? ''))"
/>
<v-list-item
v-if="edit"
:prepend-icon="$globals.icons.delete"
:title="$t('general.delete')"
@click="model.splice(i, 1)"
/>
</v-list>
</v-menu>
<v-btn
v-if="!edit"
color="primary"
icon
size="small"
variant="plain"
:href="assetURL(item.fileName ?? '')"
target="_blank"
top
download
>
<v-icon> {{ $globals.icons.download }} </v-icon>
</v-btn>
<div v-else>
<v-btn
color="error"
icon
size="small"
top
@click="model.splice(i, 1)"
>
<v-icon>{{ $globals.icons.delete }}</v-icon>
</v-btn>
<AppButtonCopy
color=""
:copy-text="assetEmbed(item.fileName ?? '')"
/>
</div>
</template>
</v-list-item>
</v-list>
@@ -68,18 +105,9 @@
can-submit
@submit="addAsset"
>
<template #activator>
<BaseButton
v-if="edit"
size="small"
create
@click="state.newAssetDialog = true"
/>
</template>
<v-card-text class="pt-4">
<v-text-field
v-model="state.newAsset.name"
density="compact"
:label="$t('general.name')"
/>
<div class="d-flex justify-space-between">
@@ -92,10 +120,14 @@
item-value="name"
class="mr-2"
>
<template #item="{ item, props: itemProps }">
<template #item="{ props: itemProps, item }">
<v-list-item v-bind="itemProps">
<template #prepend>
<v-icon>{{ item.raw.icon }}</v-icon>
<v-avatar>
<v-icon>
{{ item.raw.icon }}
</v-icon>
</v-avatar>
</template>
</v-list-item>
</template>
@@ -107,7 +139,6 @@
@uploaded="setFileObject"
/>
</div>
{{ state.fileObject.name }}
</v-card-text>
</BaseDialog>
</div>
@@ -118,6 +149,7 @@
import { useStaticRoutes, useUserApi } from "~/composables/api";
import { alert } from "~/composables/use-toast";
import type { RecipeAsset } from "~/lib/api/types/recipe";
import { useCopy } from "~/composables/use-copy";
const props = defineProps({
slug: {
@@ -149,6 +181,7 @@ const state = reactive({
const i18n = useI18n();
const { $globals } = useNuxtApp();
const { copyText } = useCopy();
const iconOptions = [
{
@@ -184,21 +217,31 @@ function getIconDefinition(icon: string) {
return iconOptions.find(item => item.name === icon) || iconOptions[0];
}
function isImage(fileName?: string | null) {
if (!fileName) return false;
return /\.(png|jpe?g|gif|webp|bmp|avif)$/i.test(fileName);
}
const { recipeAssetPath } = useStaticRoutes();
function assetURL(assetName: string) {
return recipeAssetPath(props.recipeId, assetName);
}
function assetEmbed(name: string) {
return `<img src="${serverBase}${assetURL(name)}" height="100%" width="100%"> </img>`;
return `<img src="${serverBase}${assetURL(name)}" height="100%" width="100%" />`;
}
function setFileObject(fileObject: File) {
state.fileObject = fileObject;
// If the user didn't provide a name, default to the file base name
if (!state.newAsset.name?.trim()) {
state.newAsset.name = fileObject.name.substring(0, fileObject.name.lastIndexOf("."));
}
}
function validFields() {
return state.newAsset.name.length > 0 && state.fileObject.name.length > 0;
// Only require a file; name will fall back to the file name if empty
return Boolean(state.fileObject?.name);
}
async function addAsset() {
@@ -207,8 +250,10 @@ async function addAsset() {
return;
}
const nameToUse = state.newAsset.name?.trim() || state.fileObject.name;
const { data } = await api.recipes.createAsset(props.slug, {
name: state.newAsset.name,
name: nameToUse,
icon: state.newAsset.icon,
file: state.fileObject,
extension: state.fileObject.name.split(".").pop() || "",

View File

@@ -15,11 +15,11 @@
@click.self="$emit('click')"
>
<RecipeCardImage
small
:icon-size="imageHeight"
:height="imageHeight"
:slug="slug"
:recipe-id="recipeId"
size="small"
:image-version="image"
>
<v-expand-transition v-if="description">

View File

@@ -19,10 +19,10 @@
cover
>
<RecipeCardImage
tiny
:icon-size="100"
:slug="slug"
:recipe-id="recipeId"
size="small"
:image-version="image"
:height="height"
/>
@@ -41,11 +41,11 @@
name="avatar"
>
<RecipeCardImage
tiny
:icon-size="100"
:slug="slug"
:recipe-id="recipeId"
:image-version="image"
size="small"
width="125"
:height="height"
/>

View File

@@ -10,7 +10,6 @@
:nudge-top="menuTop ? '5' : '0'"
allow-overflow
close-delay="125"
:open-on-hover="$vuetify.display.mdAndUp"
content-class="d-print-none"
@update:model-value="onMenuToggle"
>
@@ -24,7 +23,6 @@
:fab="fab"
v-bind="activatorProps"
@click.prevent
@mouseenter="onHover"
>
<v-icon
:size="!fab ? undefined : 'x-large'"
@@ -127,12 +125,6 @@ const contentProps = computed(() => {
return rest;
});
function onHover() {
if (!isMenuContentLoaded.value) {
isMenuContentLoaded.value = true;
}
}
function onMenuToggle(isOpen: boolean) {
if (isOpen && !isMenuContentLoaded.value) {
isMenuContentLoaded.value = true;

View File

@@ -45,31 +45,15 @@
@confirm="addRecipeToPlan()"
>
<v-card-text>
<v-menu
v-model="pickerMenu"
:close-on-content-click="false"
transition="scale-transition"
offset-y
max-width="290px"
min-width="auto"
>
<template #activator="{ props: activatorProps }">
<v-text-field
:model-value="$d(newMealdate)"
:label="$t('general.date')"
:prepend-icon="$globals.icons.calendar"
v-bind="activatorProps"
readonly
/>
</template>
<v-date-picker
v-model="newMealdate"
hide-header
:first-day-of-week="firstDayOfWeek"
:local="$i18n.locale"
@update:model-value="pickerMenu = false"
/>
</v-menu>
<v-date-picker
v-model="newMealdate"
class="mx-auto mb-3"
hide-header
show-adjacent-months
color="primary"
:first-day-of-week="firstDayOfWeek"
:local="$i18n.locale"
/>
<v-select
v-model="newMealType"
:return-object="false"
@@ -192,6 +176,7 @@ const props = withDefaults(defineProps<Props>(), {
const emit = defineEmits<{
[key: string]: any;
deleted: [slug: string];
print: [];
}>();
const api = useUserApi();
@@ -207,7 +192,6 @@ const loading = ref(false);
const menuItems = ref<ContextMenuItem[]>([]);
const newMealdate = ref(new Date());
const newMealType = ref<PlanEntryType>("dinner");
const pickerMenu = ref(false);
const newMealdateString = computed(() => {
// Format the date to YYYY-MM-DD in the same timezone as newMealdate

View File

@@ -6,7 +6,7 @@
:title="$t('recipe.add-to-list')"
:icon="$globals.icons.cartCheck"
>
<v-container v-if="!shoppingListChoices.length">
<v-container v-if="!filteredShoppingLists.length">
<BasePageTitle>
<template #title>
{{ $t('shopping-list.no-shopping-lists-found') }}
@@ -15,7 +15,7 @@
</v-container>
<v-card-text>
<v-card
v-for="list in shoppingListChoices"
v-for="list in filteredShoppingLists"
:key="list.id"
hover
class="my-2 left-border"
@@ -222,39 +222,38 @@ const api = useUserApi();
const preferences = useShoppingListPreferences();
const ready = ref(false);
// Capture values at initialization to avoid reactive updates
const currentHouseholdSlug = ref("");
const filteredShoppingLists = ref<ShoppingListSummary[]>([]);
const state = reactive({
shoppingListDialog: true,
shoppingListDialog: false,
shoppingListIngredientDialog: false,
shoppingListShowAllToggled: false,
});
const { shoppingListDialog, shoppingListIngredientDialog, shoppingListShowAllToggled: _shoppingListShowAllToggled } = toRefs(state);
const userHousehold = computed(() => {
return $auth.user.value?.householdSlug || "";
});
const shoppingListChoices = computed(() => {
return props.shoppingLists.filter(list => preferences.value.viewAllLists || list.userId === $auth.user.value?.id);
});
const recipeIngredientSections = ref<ShoppingListRecipeIngredientSection[]>([]);
const selectedShoppingList = ref<ShoppingListSummary | null>(null);
watchEffect(
() => {
if (shoppingListChoices.value.length === 1 && !state.shoppingListShowAllToggled) {
selectedShoppingList.value = shoppingListChoices.value[0];
watch([dialog, () => preferences.value.viewAllLists], () => {
if (dialog.value) {
currentHouseholdSlug.value = $auth.user.value?.householdSlug || "";
filteredShoppingLists.value = props.shoppingLists.filter(
list => preferences.value.viewAllLists || list.userId === $auth.user.value?.id,
);
if (filteredShoppingLists.value.length === 1 && !state.shoppingListShowAllToggled) {
selectedShoppingList.value = filteredShoppingLists.value[0];
openShoppingListIngredientDialog(selectedShoppingList.value);
}
else {
state.shoppingListDialog = true;
ready.value = true;
}
},
);
watch(dialog, (val) => {
if (!val) {
}
else if (!dialog.value) {
initState();
}
});
@@ -274,22 +273,26 @@ async function consolidateRecipesIntoSections(recipes: RecipeWithScale[]) {
continue;
}
if (!(recipe.id && recipe.name && recipe.recipeIngredient)) {
const { data } = await api.recipes.getOne(recipe.slug);
// Create a local copy to avoid mutating props
let recipeData = { ...recipe };
if (!(recipeData.id && recipeData.name && recipeData.recipeIngredient)) {
const { data } = await api.recipes.getOne(recipeData.slug);
if (!data?.recipeIngredient?.length) {
continue;
}
recipe.id = data.id || "";
recipe.name = data.name || "";
recipe.recipeIngredient = data.recipeIngredient;
recipeData = {
...recipeData,
id: data.id || "",
name: data.name || "",
recipeIngredient: data.recipeIngredient,
};
}
else if (!recipe.recipeIngredient.length) {
else if (!recipeData.recipeIngredient.length) {
continue;
}
const shoppingListIngredients: ShoppingListIngredient[] = [];
function flattenRecipeIngredients(ing: RecipeIngredient, parentTitle = ""): ShoppingListIngredient[] {
const householdsWithFood = ing.food?.householdsWithIngredientFood || [];
if (ing.referencedRecipe) {
// Recursively flatten all ingredients in the referenced recipe
return (ing.referencedRecipe.recipeIngredient ?? []).flatMap((subIng) => {
@@ -303,8 +306,9 @@ async function consolidateRecipesIntoSections(recipes: RecipeWithScale[]) {
}
else {
// Regular ingredient
const householdsWithFood = ing.food?.householdsWithIngredientFood || [];
return [{
checked: !householdsWithFood.includes(userHousehold.value),
checked: !householdsWithFood.includes(currentHouseholdSlug.value),
ingredient: {
...ing,
title: ing.title || parentTitle,
@@ -313,7 +317,7 @@ async function consolidateRecipesIntoSections(recipes: RecipeWithScale[]) {
}
}
recipe.recipeIngredient.forEach((ing) => {
recipeData.recipeIngredient.forEach((ing) => {
const flattened = flattenRecipeIngredients(ing, "");
shoppingListIngredients.push(...flattened);
});
@@ -343,7 +347,7 @@ async function consolidateRecipesIntoSections(recipes: RecipeWithScale[]) {
// Store the on-hand ingredients for later
const householdsWithFood = (ing.ingredient?.food?.householdsWithIngredientFood || []);
if (householdsWithFood.includes(userHousehold.value)) {
if (householdsWithFood.includes(currentHouseholdSlug.value)) {
onHandIngs.push(ing);
return sections;
}
@@ -357,9 +361,9 @@ async function consolidateRecipesIntoSections(recipes: RecipeWithScale[]) {
shoppingListIngredientSections[shoppingListIngredientSections.length - 1].ingredients.push(...onHandIngs);
recipeSectionMap.set(recipe.slug, {
recipeId: recipe.id,
recipeName: recipe.name,
recipeScale: recipe.scale,
recipeId: recipeData.id,
recipeName: recipeData.name,
recipeScale: recipeData.scale,
ingredientSections: shoppingListIngredientSections,
});
}
@@ -368,7 +372,7 @@ async function consolidateRecipesIntoSections(recipes: RecipeWithScale[]) {
}
function initState() {
state.shoppingListDialog = true;
state.shoppingListDialog = false;
state.shoppingListIngredientDialog = false;
state.shoppingListShowAllToggled = false;
recipeIngredientSections.value = [];

View File

@@ -100,6 +100,7 @@
v-model="state.auto"
:label="$t('search.auto-search')"
single-line
color="primary"
/>
<v-btn
block

View File

@@ -101,4 +101,14 @@ const { store: tags } = isOwnGroup.value ? useTagStore() : usePublicTagStore(gro
const { store: tools } = isOwnGroup.value ? useToolStore() : usePublicToolStore(groupSlug.value);
const { store: foods } = isOwnGroup.value ? useFoodStore() : usePublicFoodStore(groupSlug.value);
const { store: households } = isOwnGroup.value ? useHouseholdStore() : usePublicHouseholdStore(groupSlug.value);
watch(
households,
() => {
// if exactly one household exists, then we shouldn't be filtering by household
if (households.value.length == 1) {
selectedHouseholds.value = [];
}
},
);
</script>

View File

@@ -22,12 +22,15 @@
cols="12"
class="flex-grow-0 flex-shrink-0"
>
<v-text-field
<v-number-input
v-model="model.quantity"
variant="solo"
:precision="null"
:min="0"
hide-details
control-variant="stacked"
inset
density="compact"
type="number"
:placeholder="$t('recipe.quantity')"
@keypress="quantityFilter"
>
@@ -38,7 +41,7 @@
{{ $globals.icons.arrowUpDown }}
</v-icon>
</template>
</v-text-field>
</v-number-input>
</v-col>
<v-col
v-if="!state.isRecipe"
@@ -56,6 +59,7 @@
variant="solo"
return-object
:items="units || []"
:custom-filter="normalizeFilter"
item-title="name"
class="mx-1"
:placeholder="$t('recipe.choose-unit')"
@@ -114,6 +118,7 @@
variant="solo"
return-object
:items="foods || []"
:custom-filter="normalizeFilter"
item-title="name"
class="mx-1 py-0"
:placeholder="$t('recipe.choose-food')"
@@ -171,6 +176,7 @@
variant="solo"
return-object
:items="search.data.value || []"
:custom-filter="normalizeFilter"
item-title="name"
class="mx-1 py-0"
:placeholder="$t('search.type-to-search')"
@@ -225,6 +231,7 @@ import { ref, computed, reactive, toRefs } from "vue";
import { useDisplay } from "vuetify";
import { useI18n } from "vue-i18n";
import { useFoodStore, useFoodData, useUnitStore, useUnitData } from "~/composables/store";
import { normalizeFilter } from "~/composables/use-utils";
import { useNuxtApp } from "#app";
import type { RecipeIngredient } from "~/lib/api/types/recipe";
import { usePublicExploreApi, useUserApi } from "~/composables/api";

View File

@@ -17,15 +17,13 @@
v-for="(ingredient, index) in value"
:key="'ingredient' + index"
>
<template v-if="!isCookMode">
<h3
v-if="showTitleEditor[index]"
class="mt-2"
>
{{ ingredient.title }}
</h3>
<v-divider v-if="showTitleEditor[index]" />
</template>
<h3
v-if="showTitleEditor[index]"
class="mt-2"
>
{{ ingredient.title }}
</h3>
<v-divider v-if="showTitleEditor[index]" />
<v-list-item
density="compact"
class="pa-0"

View File

@@ -10,14 +10,17 @@
v-for="(item, key, index) in modelValue"
:key="index"
>
<v-text-field
density="compact"
<v-number-input
:model-value="modelValue[key]"
:label="labels[key].label"
:suffix="labels[key].suffix"
type="number"
density="compact"
autocomplete="off"
variant="underlined"
control-variant="stacked"
inset
:precision="null"
:min="0"
@update:model-value="updateValue(key, $event)"
/>
</div>

View File

@@ -4,17 +4,19 @@
v-bind="inputAttrs"
v-model:search="searchInput"
:items="items"
:custom-filter="normalizeFilter"
:label="label"
chips
closable-chips
item-title="name"
:item-title="itemTitle"
item-value="name"
multiple
:variant="variant"
:prepend-inner-icon="icon"
:append-icon="showAdd ? $globals.icons.create : undefined"
return-object
auto-select-first
class="pa-0"
class="pa-0 ma-0"
@update:model-value="resetSearchInput"
@click:append="dialog = true"
>
@@ -32,7 +34,6 @@
{{ item.value }}
</v-chip>
</template>
<template
v-if="showAdd"
#append
@@ -47,16 +48,17 @@
</template>
<script setup lang="ts">
import type { IngredientFood, RecipeCategory, RecipeTag } from "~/lib/api/types/recipe";
import type { RecipeTool } from "~/lib/api/types/admin";
import type { IngredientFood, RecipeCategory, RecipeTag, RecipeTool } from "~/lib/api/types/recipe";
import { Organizer, type RecipeOrganizer } from "~/lib/api/types/non-generated";
import type { HouseholdSummary } from "~/lib/api/types/household";
import { useCategoryStore, useFoodStore, useHouseholdStore, useTagStore, useToolStore } from "~/composables/store";
import { useUserStore } from "~/composables/store/use-user-store";
import { normalizeFilter } from "~/composables/use-utils";
import type { UserSummary } from "~/lib/api/types/user";
interface Props {
selectorType: RecipeOrganizer;
inputAttrs?: Record<string, any>;
returnObject?: boolean;
showAdd?: boolean;
showLabel?: boolean;
showIcon?: boolean;
@@ -65,7 +67,6 @@ interface Props {
const props = withDefaults(defineProps<Props>(), {
inputAttrs: () => ({}),
returnObject: true,
showAdd: true,
showLabel: true,
showIcon: true,
@@ -78,7 +79,7 @@ const selected = defineModel<(
| RecipeCategory
| RecipeTool
| IngredientFood
| string
| UserSummary
)[] | undefined>({ required: true });
onMounted(() => {
@@ -106,6 +107,8 @@ const label = computed(() => {
return i18n.t("general.foods");
case Organizer.Household:
return i18n.t("household.households");
case Organizer.User:
return i18n.t("user.users");
default:
return i18n.t("general.organizer");
}
@@ -127,11 +130,19 @@ const icon = computed(() => {
return $globals.icons.foods;
case Organizer.Household:
return $globals.icons.household;
case Organizer.User:
return $globals.icons.user;
default:
return $globals.icons.tags;
}
});
const itemTitle = computed(() =>
props.selectorType === Organizer.User
? (i: any) => i?.fullName ?? i?.name ?? ""
: "name",
);
// ===========================================================================
// Store & Items Setup
@@ -141,24 +152,24 @@ const storeMap = {
[Organizer.Tool]: useToolStore(),
[Organizer.Food]: useFoodStore(),
[Organizer.Household]: useHouseholdStore(),
[Organizer.User]: useUserStore(),
};
const store = computed(() => {
const activeStore = computed(() => {
const { store } = storeMap[props.selectorType];
return store.value;
});
const items = computed(() => {
if (!props.returnObject) {
return store.value.map(item => item.name);
}
return store.value;
const items = computed<any[]>(() => {
const list = (activeStore.value as unknown as any[]) ?? [];
return list;
});
function removeByIndex(index: number) {
if (selected.value === undefined) {
return;
}
const newSelected = selected.value.filter((_, i) => i !== index);
selected.value = [...newSelected];
}

View File

@@ -1,5 +1,18 @@
<template>
<div>
<BaseDialog
v-model="discardDialog"
:title="$t('general.discard-changes')"
color="warning"
:icon="$globals.icons.alertCircle"
can-confirm
@confirm="confirmDiscard"
@cancel="cancelDiscard"
>
<v-card-text>
{{ $t("general.discard-changes-description") }}
</v-card-text>
</BaseDialog>
<RecipePageParseDialog
:model-value="isParsing"
:ingredients="recipe.recipeIngredient"
@@ -15,6 +28,7 @@
:landscape="landscape"
@save="saveRecipe"
@delete="deleteRecipe"
@close="closeEditor"
/>
<RecipeJsonEditor
v-if="isEditJSON"
@@ -174,6 +188,7 @@
<script setup lang="ts">
import { invoke, until } from "@vueuse/core";
import type { RouteLocationNormalized } from "vue-router";
import RecipeIngredients from "../RecipeIngredients.vue";
import RecipePageEditorToolbar from "./RecipePageParts/RecipePageEditorToolbar.vue";
import RecipePageFooter from "./RecipePageParts/RecipePageFooter.vue";
@@ -205,7 +220,6 @@ import { useNavigationWarning } from "~/composables/use-navigation-warning";
const recipe = defineModel<NoUndefinedField<Recipe>>({ required: true });
const display = useDisplay();
const i18n = useI18n();
const $auth = useMealieAuth();
const route = useRoute();
const { isOwnGroup } = useLoggedInState();
@@ -231,26 +245,68 @@ const notLinkedIngredients = computed(() => {
* and prompts the user to save if they have unsaved changes.
*/
const originalRecipe = ref<Recipe | null>(null);
const discardDialog = ref(false);
const pendingRoute = ref<RouteLocationNormalized | null>(null);
invoke(async () => {
await until(recipe.value).not.toBeNull();
originalRecipe.value = deepCopy(recipe.value);
});
onUnmounted(async () => {
const isSame = JSON.stringify(recipe.value) === JSON.stringify(originalRecipe.value);
if (isEditMode.value && !isSame && recipe.value?.slug !== undefined) {
const save = window.confirm(i18n.t("general.unsaved-changes"));
if (save) {
await api.recipes.updateOne(recipe.value.slug, recipe.value);
}
function hasUnsavedChanges(): boolean {
if (originalRecipe.value === null) {
return false;
}
return JSON.stringify(recipe.value) !== JSON.stringify(originalRecipe.value);
}
function restoreOriginalRecipe() {
if (originalRecipe.value) {
recipe.value = deepCopy(originalRecipe.value) as NoUndefinedField<Recipe>;
}
}
function closeEditor() {
if (hasUnsavedChanges()) {
pendingRoute.value = null;
discardDialog.value = true;
}
else {
setMode(PageMode.VIEW);
}
}
function confirmDiscard() {
restoreOriginalRecipe();
discardDialog.value = false;
if (pendingRoute.value) {
const destination = pendingRoute.value;
pendingRoute.value = null;
router.push(destination);
}
else {
setMode(PageMode.VIEW);
}
}
function cancelDiscard() {
discardDialog.value = false;
pendingRoute.value = null;
}
onBeforeRouteLeave((to) => {
if (isEditMode.value && hasUnsavedChanges()) {
pendingRoute.value = to;
discardDialog.value = true;
return false;
}
});
onUnmounted(() => {
deactivateNavigationWarning();
toggleCookMode();
clearPageState(recipe.value.slug || "");
console.debug("reset RecipePage state during unmount");
});
const hasLinkedIngredients = computed(() => {
return recipe.value.recipeInstructions.some(
@@ -300,6 +356,8 @@ async function saveRecipe() {
if (data?.slug) {
router.push(`/g/${groupSlug.value}/r/` + data.slug);
recipe.value = data as NoUndefinedField<Recipe>;
// Update the snapshot after successful save
originalRecipe.value = deepCopy(recipe.value);
}
}

View File

@@ -16,7 +16,7 @@
:open="isEditMode"
:recipe-id="recipe.id"
class="ml-auto mt-n7 pb-4"
@close="setMode(PageMode.VIEW)"
@close="$emit('close')"
@json="toggleEditMode()"
@edit="setMode(PageMode.EDIT)"
@save="$emit('save')"
@@ -47,7 +47,7 @@ const props = withDefaults(defineProps<Props>(), {
landscape: false,
});
defineEmits(["save", "delete"]);
defineEmits(["save", "delete", "print", "close"]);
const { recipeImage } = useStaticRoutes();
const { imageKey, setMode, toggleEditMode, isEditMode } = usePageState(props.recipe.slug);

View File

@@ -28,7 +28,7 @@ const props = withDefaults(defineProps<Props>(), {
});
const display = useDisplay();
const { recipeImage } = useStaticRoutes();
const { recipeImage, recipeSmallImage } = useStaticRoutes();
const { imageKey } = usePageState(props.recipe.slug);
const { user } = usePageUser();
@@ -46,7 +46,9 @@ const imageHeight = computed(() => {
});
const recipeImageUrl = computed(() => {
return recipeImage(props.recipe.id, props.recipe.image, imageKey.value);
return display.smAndDown.value
? recipeSmallImage(props.recipe.id, props.recipe.image, imageKey.value)
: recipeImage(props.recipe.id, props.recipe.image, imageKey.value);
});
watch(

View File

@@ -11,27 +11,27 @@
<v-container class="ma-0 pa-0">
<v-row>
<v-col cols="3">
<v-text-field
:model-value="recipeServings"
type="number"
<v-number-input
:model-value="recipe.recipeServings"
:min="0"
hide-spin-buttons
:precision="null"
density="compact"
:label="$t('recipe.servings')"
variant="underlined"
@update:model-value="validateInput($event, 'recipeServings')"
control-variant="hidden"
@update:model-value="recipe.recipeServings = $event"
/>
</v-col>
<v-col cols="3">
<v-text-field
:model-value="recipeYieldQuantity"
type="number"
<v-number-input
:model-value="recipe.recipeYieldQuantity"
:min="0"
hide-spin-buttons
:precision="null"
density="compact"
:label="$t('recipe.yield')"
variant="underlined"
@update:model-value="validateInput($event, 'recipeYieldQuantity')"
control-variant="hidden"
@update:model-value="recipe.recipeYieldQuantity = $event"
/>
</v-col>
<v-col cols="6">
@@ -85,37 +85,4 @@ import type { NoUndefinedField } from "~/lib/api/types/non-generated";
import type { Recipe } from "~/lib/api/types/recipe";
const recipe = defineModel<NoUndefinedField<Recipe>>({ required: true });
const recipeServings = computed<number>({
get() {
return recipe.value.recipeServings;
},
set(val) {
validateInput(val.toString(), "recipeServings");
},
});
const recipeYieldQuantity = computed<number>({
get() {
return recipe.value.recipeYieldQuantity;
},
set(val) {
validateInput(val.toString(), "recipeYieldQuantity");
},
});
function validateInput(value: string | null, property: "recipeServings" | "recipeYieldQuantity") {
if (!value) {
recipe.value[property] = 0;
return;
}
const number = parseFloat(value.replace(/[^0-9.]/g, ""));
if (isNaN(number) || number <= 0) {
recipe.value[property] = 0;
return;
}
recipe.value[property] = number;
}
</script>

View File

@@ -65,13 +65,13 @@
</v-card-title>
<v-card-text class="mt-n5">
<div class="mt-4 d-flex align-center">
<v-text-field
<v-number-input
:model-value="yieldQuantity"
type="number"
:precision="null"
:min="0"
variant="underlined"
hide-spin-buttons
@update:model-value="recalculateScale(parseFloat($event) || 0)"
control-variant="hidden"
@update:model-value="recalculateScale($event || 0)"
/>
<v-tooltip
location="end"

View File

@@ -5,6 +5,7 @@
:title="$t('recipe.edit-timeline-event')"
:icon="$globals.icons.edit"
can-submit
disable-submit-on-enter
:submit-text="$t('general.save')"
@submit="submitEdit"
>
@@ -38,7 +39,6 @@
:nudge-top="props.menuTop ? '5' : '0'"
allow-overflow
close-delay="125"
:open-on-hover="!props.useMobileFormat"
content-class="d-print-none"
>
<template #activator="{ props: btnProps }">
@@ -98,7 +98,6 @@ const props = defineProps<{
color?: string;
event: RecipeTimelineEventOut;
menuIcon?: string | null;
useMobileFormat?: boolean;
}>();
const emit = defineEmits(["delete", "update"]);

View File

@@ -35,7 +35,6 @@
:menu-top="false"
:event="event"
:menu-icon="$globals.icons.dotsVertical"
:use-mobile-format="useMobileFormat"
color="transparent"
:elevation="0"
:card-menu="false"
@@ -119,7 +118,7 @@ defineEmits<{
const { $globals } = useNuxtApp();
const display = useDisplay();
const { recipeTimelineEventImage } = useStaticRoutes();
const { recipeTimelineEventSmallImage } = useStaticRoutes();
const { eventTypeOptions } = useTimelineEventTypes();
const { user: currentUser } = useMealieAuth();
@@ -173,7 +172,7 @@ const eventImageUrl = computed<string>(() => {
return "";
}
return recipeTimelineEventImage(props.event.recipeId, props.event.id);
return recipeTimelineEventSmallImage(props.event.recipeId, props.event.id);
});
</script>

View File

@@ -37,21 +37,27 @@
:label="$t('search.search')"
clearable
/>
<div class="d-flex py-4 px-1">
<v-switch
<div />
<div class="d-flex py-4 px-1 align-center">
<v-btn-toggle
v-if="requireAll != undefined"
v-model="requireAllValue"
v-model="combinator"
mandatory
density="compact"
hide-details
class="my-auto"
variant="outlined"
color="primary"
:label="requireAllValue ? $t('search.has-all') : $t('search.has-any')"
/>
>
<v-btn value="hasAll">
{{ $t('search.has-all') }}
</v-btn>
<v-btn value="hasAny">
{{ $t('search.has-any') }}
</v-btn>
</v-btn-toggle>
<v-spacer />
<v-btn
size="small"
color="accent"
class="mr-2 my-auto"
@click="clearSelection"
>
{{ $t("search.clear-selection") }}
@@ -174,10 +180,10 @@ export default defineNuxtComponent({
// Use shallowRef for better performance with arrays
const debouncedSearch = shallowRef("");
const requireAllValue = computed({
get: () => props.requireAll,
const combinator = computed({
get: () => (props.requireAll ? "hasAll" : "hasAny"),
set: (value) => {
context.emit("update:requireAll", value);
context.emit("update:requireAll", value === "hasAll");
},
});
@@ -246,7 +252,7 @@ export default defineNuxtComponent({
}
return {
requireAllValue,
combinator,
state,
selected,
selectedRadio,

View File

@@ -15,7 +15,10 @@
density="compact"
class="mt-0 flex-shrink-0"
color="null"
@change="$emit('checked', listItem)"
@click="() => {
listItem.checked = !listItem.checked
$emit('checked', listItem)
}"
/>
<div
class="ml-2 text-truncate"

View File

@@ -4,7 +4,16 @@
<v-card-text class="pb-3 pt-1">
<div class="d-md-flex align-center mb-2" style="gap: 20px">
<div>
<InputQuantity v-model="listItem.quantity" />
<v-number-input
v-model="listItem.quantity"
hide-details
:label="$t('form.quantity-label-abbreviated')"
:min="0"
:precision="null"
control-variant="stacked"
inset
style="width: 100px;"
/>
</div>
<InputLabelType
v-model="listItem.unit"
@@ -47,25 +56,6 @@
width="250"
/>
</div>
<v-menu
v-if="listItem.recipeReferences && listItem.recipeReferences.length > 0"
open-on-hover
offset-y
start
top
>
<template #activator="{ props }">
<v-icon class="mt-auto" :icon="$globals.icons.alert" v-bind="props" color="warning">
{{ $globals.icons.alert }}
</v-icon>
</template>
<v-card max-width="350px" class="left-warning-border">
<v-card-text>
{{ $t("shopping-list.linked-item-warning") }}
</v-card-text>
</v-card>
</v-menu>
</div>
<BaseButton
v-if="listItem.labelId && listItem.food && listItem.labelId !== listItem.food.labelId"
@@ -158,6 +148,15 @@ export default defineNuxtComponent({
},
});
watch(
() => props.modelValue.quantity,
() => {
if (!props.modelValue.quantity) {
listItem.value.quantity = 0;
}
},
);
watch(
() => props.modelValue.food,
(newFood) => {

View File

@@ -149,6 +149,6 @@ export default defineNuxtComponent({
<style scoped>
.v-toolbar {
z-index: 1010 !important;
z-index: 2010 !important;
}
</style>

View File

@@ -8,7 +8,6 @@
:nudge-top="menuTop ? '5' : '0'"
allow-overflow
close-delay="125"
open-on-hover
content-class="d-print-none"
>
<template #activator="{ props }">

View File

@@ -7,6 +7,7 @@
item-title="name"
return-object
:items="items"
:custom-filter="normalizeFilter"
:prepend-icon="icon || $globals.icons.tags"
auto-select-first
clearable
@@ -52,6 +53,7 @@
import type { MultiPurposeLabelSummary } from "~/lib/api/types/labels";
import type { IngredientFood, IngredientUnit } from "~/lib/api/types/recipe";
import { normalizeFilter } from "~/composables/use-utils";
export default defineNuxtComponent({
props: {
@@ -122,6 +124,7 @@ export default defineNuxtComponent({
itemIdVal,
searchInput,
emitCreate,
normalizeFilter,
};
},
});

View File

@@ -1,61 +0,0 @@
<template>
<div
class="d-flex align-center"
style="max-width: 60px"
>
<v-text-field
v-model.number="quantity"
hide-details
:label="$t('form.quantity-label-abbreviated')"
:min="min"
:max="max"
type="number"
variant="plain"
density="compact"
style="width: 60px;"
/>
</div>
</template>
<script lang="ts">
export default defineNuxtComponent({
name: "VInputNumber",
props: {
min: {
type: Number,
default: 0,
},
max: {
type: Number,
default: 9999,
},
rules: {
type: Array,
default: () => [],
},
step: {
type: Number,
default: 1,
},
modelValue: {
type: Number,
default: 0,
},
},
emits: ["update:modelValue"],
setup(props, context) {
const quantity = computed({
get: () => {
return Number(props.modelValue);
},
set: (val) => {
context.emit("update:modelValue", val);
},
});
return {
quantity,
};
},
});
</script>

View File

@@ -9,6 +9,7 @@
<v-autocomplete
v-model="selectedLocale"
:items="locales"
:custom-filter="normalizeFilter"
item-title="name"
item-value="value"
class="my-3"
@@ -44,6 +45,7 @@
<script lang="ts">
import { useLocales } from "~/composables/use-locales";
import { normalizeFilter } from "~/composables/use-utils";
export default defineNuxtComponent({
props: {
@@ -83,6 +85,7 @@ export default defineNuxtComponent({
locale,
selectedLocale,
onLocaleSelect,
normalizeFilter,
};
},
});

View File

@@ -7,6 +7,10 @@
import DOMPurify from "isomorphic-dompurify";
import { marked } from "marked";
enum DOMPurifyHook {
UponSanitizeAttribute = "uponSanitizeAttribute",
}
export default defineNuxtComponent({
props: {
source: {
@@ -15,26 +19,42 @@ export default defineNuxtComponent({
},
},
setup(props) {
const ALLOWED_STYLE_TAGS = [
"background-color", "color", "font-style", "font-weight", "text-decoration", "text-align",
];
function sanitizeMarkdown(rawHtml: string | null | undefined): string {
if (!rawHtml) {
return "";
}
DOMPurify.addHook(DOMPurifyHook.UponSanitizeAttribute, (node, data) => {
if (data.attrName === "style") {
const styles = data.attrValue.split(";").filter((style) => {
const [property] = style.split(":");
return ALLOWED_STYLE_TAGS.includes(property.trim().toLowerCase());
});
data.attrValue = styles.join(";");
}
});
const sanitized = DOMPurify.sanitize(rawHtml, {
// List based on
// https://support.zendesk.com/hc/en-us/articles/4408824584602-Allowing-unsafe-HTML-in-help-center-articles
ALLOWED_TAGS: [
"strong", "em", "b", "i", "u", "p", "code", "pre", "samp", "kbd", "var", "sub", "sup", "dfn", "cite",
"small", "address", "hr", "br", "id", "div", "span", "h1", "h2", "h3", "h4", "h5", "h6",
"ul", "ol", "li", "dl", "dt", "dd", "abbr", "a", "img", "blockquote", "iframe",
"del", "ins", "table", "thead", "tbody", "tfoot", "tr", "th", "td", "colgroup",
],
ADD_ATTR: [
ALLOWED_ATTR: [
"href", "src", "alt", "height", "width", "class", "allow", "title", "allowfullscreen", "frameborder",
"scrolling", "cite", "datetime", "name", "abbr", "target", "border",
"scrolling", "cite", "datetime", "name", "abbr", "target", "border", "start", "style",
],
});
Object.values(DOMPurifyHook).forEach((hook) => {
DOMPurify.removeHook(hook);
});
return sanitized;
}

View File

@@ -0,0 +1,58 @@
import { describe, expect, test, vi } from "vitest";
import { ref } from "vue";
import { useStoreActions } from "./use-actions-factory";
import type { BaseCRUDAPI } from "~/lib/api/base/base-clients";
describe("useStoreActions", () => {
const mockApi = {
getAll: vi.fn(),
createOne: vi.fn(),
updateOne: vi.fn(),
deleteOne: vi.fn(),
} as unknown as BaseCRUDAPI<unknown, unknown, unknown>;
const mockStore = ref([]);
const mockLoading = ref(false);
test("deleteMany calls deleteOne for each ID and refreshes once", async () => {
const actions = useStoreActions("test-store", mockApi, mockStore, mockLoading);
mockApi.deleteOne = vi.fn().mockResolvedValue({ response: { data: {} } });
mockApi.getAll = vi.fn().mockResolvedValue({ data: { items: [] } });
const ids = ["1", "2", "3"];
await actions.deleteMany(ids);
expect(mockApi.deleteOne).toHaveBeenCalledTimes(3);
expect(mockApi.deleteOne).toHaveBeenCalledWith("1");
expect(mockApi.deleteOne).toHaveBeenCalledWith("2");
expect(mockApi.deleteOne).toHaveBeenCalledWith("3");
expect(mockApi.getAll).toHaveBeenCalledTimes(1);
});
test("deleteMany handles empty array", async () => {
const actions = useStoreActions("test-store", mockApi, mockStore, mockLoading);
mockApi.deleteOne = vi.fn();
mockApi.getAll = vi.fn().mockResolvedValue({ data: { items: [] } });
await actions.deleteMany([]);
expect(mockApi.deleteOne).not.toHaveBeenCalled();
expect(mockApi.getAll).toHaveBeenCalledTimes(1);
});
test("deleteMany sets loading state", async () => {
const actions = useStoreActions("test-store", mockApi, mockStore, mockLoading);
mockApi.deleteOne = vi.fn().mockResolvedValue({});
mockApi.getAll = vi.fn().mockResolvedValue({ data: { items: [] } });
const promise = actions.deleteMany(["1"]);
expect(mockLoading.value).toBe(true);
await promise;
expect(mockLoading.value).toBe(false);
});
});

View File

@@ -12,6 +12,7 @@ interface StoreActions<T extends BoundT> extends ReadOnlyStoreActions<T> {
createOne(createData: T): Promise<T | null>;
updateOne(updateData: T): Promise<T | null>;
deleteOne(id: string | number): Promise<T | null>;
deleteMany(ids: (string | number)[]): Promise<void>;
}
/**
@@ -165,11 +166,23 @@ export function useStoreActions<T extends BoundT>(
return response?.data || null;
}
async function deleteMany(ids: (string | number)[]) {
loading.value = true;
for (const id of ids) {
await api.deleteOne(id);
}
if (allRef?.value) {
await refresh();
}
loading.value = false;
}
return {
getAll,
refresh,
createOne,
updateOne,
deleteOne,
deleteMany,
};
}

View File

@@ -52,7 +52,7 @@ export const useStore = function <T extends BoundT>(
return await storeActions.refresh(1, -1, params);
},
flushStore() {
store = ref([]);
store.value = [];
},
};

View File

@@ -1,7 +1,31 @@
export { useCategoryStore, usePublicCategoryStore, useCategoryData } from "./use-category-store";
export { useFoodStore, usePublicFoodStore, useFoodData } from "./use-food-store";
export { useHouseholdStore, usePublicHouseholdStore } from "./use-household-store";
export { useLabelStore, useLabelData } from "./use-label-store";
export { useTagStore, usePublicTagStore, useTagData } from "./use-tag-store";
export { useToolStore, usePublicToolStore, useToolData } from "./use-tool-store";
export { useUnitStore, useUnitData } from "./use-unit-store";
import { resetCategoryStore } from "./use-category-store";
import { resetFoodStore } from "./use-food-store";
import { resetHouseholdStore } from "./use-household-store";
import { resetLabelStore } from "./use-label-store";
import { resetTagStore } from "./use-tag-store";
import { resetToolStore } from "./use-tool-store";
import { resetUnitStore } from "./use-unit-store";
import { resetCookbookStore } from "./use-cookbook-store";
import { resetUserStore } from "./use-user-store";
export { useCategoryStore, usePublicCategoryStore, useCategoryData, resetCategoryStore } from "./use-category-store";
export { useFoodStore, usePublicFoodStore, useFoodData, resetFoodStore } from "./use-food-store";
export { useHouseholdStore, usePublicHouseholdStore, resetHouseholdStore } from "./use-household-store";
export { useLabelStore, useLabelData, resetLabelStore } from "./use-label-store";
export { useTagStore, usePublicTagStore, useTagData, resetTagStore } from "./use-tag-store";
export { useToolStore, usePublicToolStore, useToolData, resetToolStore } from "./use-tool-store";
export { useUnitStore, useUnitData, resetUnitStore } from "./use-unit-store";
export { useCookbookStore, usePublicCookbookStore, resetCookbookStore } from "./use-cookbook-store";
export { useUserStore, resetUserStore } from "./use-user-store";
export function clearAllStores() {
resetCategoryStore();
resetFoodStore();
resetHouseholdStore();
resetLabelStore();
resetTagStore();
resetToolStore();
resetUnitStore();
resetCookbookStore();
resetUserStore();
}

View File

@@ -7,6 +7,12 @@ const store: Ref<RecipeCategory[]> = ref([]);
const loading = ref(false);
const publicLoading = ref(false);
export function resetCategoryStore() {
store.value = [];
loading.value = false;
publicLoading.value = false;
}
export const useCategoryData = function () {
return useData<RecipeCategory>({
id: "",

View File

@@ -7,6 +7,12 @@ const cookbooks: Ref<ReadCookBook[]> = ref([]);
const loading = ref(false);
const publicLoading = ref(false);
export function resetCookbookStore() {
cookbooks.value = [];
loading.value = false;
publicLoading.value = false;
}
export const useCookbookStore = function (i18n?: Composer) {
const api = useUserApi(i18n);
const store = useStore<ReadCookBook>("cookbook", cookbooks, loading, api.cookbooks);

View File

@@ -7,6 +7,12 @@ const store: Ref<IngredientFood[]> = ref([]);
const loading = ref(false);
const publicLoading = ref(false);
export function resetFoodStore() {
store.value = [];
loading.value = false;
publicLoading.value = false;
}
export const useFoodData = function () {
return useData<IngredientFood>({
id: "",

View File

@@ -7,6 +7,12 @@ const store: Ref<HouseholdSummary[]> = ref([]);
const loading = ref(false);
const publicLoading = ref(false);
export function resetHouseholdStore() {
store.value = [];
loading.value = false;
publicLoading.value = false;
}
export const useHouseholdStore = function (i18n?: Composer) {
const api = useUserApi(i18n);
return useReadOnlyStore<HouseholdSummary>("household", store, loading, api.households);

View File

@@ -6,6 +6,11 @@ import { useUserApi } from "~/composables/api";
const store: Ref<MultiPurposeLabelOut[]> = ref([]);
const loading = ref(false);
export function resetLabelStore() {
store.value = [];
loading.value = false;
}
export const useLabelData = function () {
return useData<MultiPurposeLabelOut>({
groupId: "",

View File

@@ -7,6 +7,12 @@ const store: Ref<RecipeTag[]> = ref([]);
const loading = ref(false);
const publicLoading = ref(false);
export function resetTagStore() {
store.value = [];
loading.value = false;
publicLoading.value = false;
}
export const useTagData = function () {
return useData<RecipeTag>({
id: "",

View File

@@ -11,6 +11,12 @@ const store: Ref<RecipeTool[]> = ref([]);
const loading = ref(false);
const publicLoading = ref(false);
export function resetToolStore() {
store.value = [];
loading.value = false;
publicLoading.value = false;
}
export const useToolData = function () {
return useData<RecipeToolWithOnHand>({
id: "",

View File

@@ -6,6 +6,11 @@ import { useUserApi } from "~/composables/api";
const store: Ref<IngredientUnit[]> = ref([]);
const loading = ref(false);
export function resetUnitStore() {
store.value = [];
loading.value = false;
}
export const useUnitData = function () {
return useData<IngredientUnit>({
id: "",

View File

@@ -7,6 +7,11 @@ import { BaseCRUDAPIReadOnly } from "~/lib/api/base/base-clients";
const store: Ref<UserSummary[]> = ref([]);
const loading = ref(false);
export function resetUserStore() {
store.value = [];
loading.value = false;
}
class GroupUserAPIReadOnly extends BaseCRUDAPIReadOnly<UserSummary> {
baseRoute = "/api/groups/members";
itemRoute = (idOrUsername: string | number) => `/groups/members/${idOrUsername}`;

View File

@@ -1,5 +1,6 @@
import { ref, computed } from "vue";
import type { UserOut } from "~/lib/api/types/user";
import { clearAllStores } from "~/composables/store";
interface AuthData {
value: UserOut | null;
@@ -101,6 +102,13 @@ export const useAuthBackend = function (): AuthState {
setToken(null);
authUser.value = null;
authStatus.value = "unauthenticated";
// Clear all cached store data to prevent data leakage between users
clearAllStores();
// Clear Nuxt's useAsyncData cache
clearNuxtData();
await router.push(callbackUrl || "/login");
}
}
@@ -120,30 +128,6 @@ export const useAuthBackend = function (): AuthState {
}
}
// Auto-refresh user data periodically when authenticated
if (import.meta.client) {
let refreshInterval: NodeJS.Timeout | null = null;
watch(() => authStatus.value, (status) => {
if (status === "authenticated") {
refreshInterval = setInterval(() => {
if (tokenCookie.value) {
getSession().catch(() => {
// Ignore errors in background refresh
});
}
}, 5 * 60 * 1000); // 5 minutes
}
else {
// Clear interval when not authenticated
if (refreshInterval) {
clearInterval(refreshInterval);
refreshInterval = null;
}
}
}, { immediate: true });
}
return {
data: computed(() => authUser.value),
status: computed(() => authStatus.value),

View File

@@ -15,6 +15,9 @@ export function usePlanTypeOptions() {
{ text: i18n.t("meal-plan.lunch"), value: "lunch" },
{ text: i18n.t("meal-plan.dinner"), value: "dinner" },
{ text: i18n.t("meal-plan.side"), value: "side" },
{ text: i18n.t("meal-plan.snack"), value: "snack" },
{ text: i18n.t("meal-plan.drink"), value: "drink" },
{ text: i18n.t("meal-plan.dessert"), value: "dessert" },
] as PlanOption[];
}

View File

@@ -15,25 +15,25 @@ export const LOCALES = [
{
name: "Tiếng Việt (Vietnamese)",
value: "vi-VN",
progress: 1,
progress: 2,
dir: "ltr",
},
{
name: "Українська (Ukrainian)",
value: "uk-UA",
progress: 93,
progress: 83,
dir: "ltr",
},
{
name: "Türkçe (Turkish)",
value: "tr-TR",
progress: 35,
progress: 40,
dir: "ltr",
},
{
name: "Svenska (Swedish)",
value: "sv-SE",
progress: 67,
progress: 61,
dir: "ltr",
},
{
@@ -45,31 +45,31 @@ export const LOCALES = [
{
name: "Slovenščina (Slovenian)",
value: "sl-SI",
progress: 41,
progress: 40,
dir: "ltr",
},
{
name: "Slovenčina (Slovak)",
value: "sk-SK",
progress: 46,
progress: 47,
dir: "ltr",
},
{
name: "Pусский (Russian)",
value: "ru-RU",
progress: 46,
progress: 44,
dir: "ltr",
},
{
name: "Română (Romanian)",
value: "ro-RO",
progress: 41,
progress: 44,
dir: "ltr",
},
{
name: "Português (Portuguese)",
value: "pt-PT",
progress: 40,
progress: 39,
dir: "ltr",
},
{
@@ -81,13 +81,13 @@ export const LOCALES = [
{
name: "Polski (Polish)",
value: "pl-PL",
progress: 46,
progress: 49,
dir: "ltr",
},
{
name: "Norsk (Norwegian)",
value: "no-NO",
progress: 41,
progress: 42,
dir: "ltr",
},
{
@@ -99,19 +99,19 @@ export const LOCALES = [
{
name: "Latviešu (Latvian)",
value: "lv-LV",
progress: 36,
progress: 35,
dir: "ltr",
},
{
name: "Lietuvių (Lithuanian)",
value: "lt-LT",
progress: 27,
progress: 30,
dir: "ltr",
},
{
name: "한국어 (Korean)",
value: "ko-KR",
progress: 9,
progress: 38,
dir: "ltr",
},
{
@@ -123,73 +123,73 @@ export const LOCALES = [
{
name: "Italiano (Italian)",
value: "it-IT",
progress: 47,
progress: 49,
dir: "ltr",
},
{
name: "Íslenska (Icelandic)",
value: "is-IS",
progress: 44,
progress: 43,
dir: "ltr",
},
{
name: "Magyar (Hungarian)",
value: "hu-HU",
progress: 47,
progress: 46,
dir: "ltr",
},
{
name: "Hrvatski (Croatian)",
value: "hr-HR",
progress: 29,
progress: 30,
dir: "ltr",
},
{
name: "עברית (Hebrew)",
value: "he-IL",
progress: 73,
progress: 64,
dir: "rtl",
},
{
name: "Galego (Galician)",
value: "gl-ES",
progress: 39,
progress: 38,
dir: "ltr",
},
{
name: "Français (French)",
value: "fr-FR",
progress: 69,
progress: 67,
dir: "ltr",
},
{
name: "Français canadien (Canadian French)",
value: "fr-CA",
progress: 100,
progress: 83,
dir: "ltr",
},
{
name: "Belge (Belgian)",
value: "fr-BE",
progress: 40,
progress: 39,
dir: "ltr",
},
{
name: "Suomi (Finnish)",
value: "fi-FI",
progress: 41,
progress: 40,
dir: "ltr",
},
{
name: "Eesti (Estonian)",
value: "et-EE",
progress: 47,
progress: 44,
dir: "ltr",
},
{
name: "Español (Spanish)",
value: "es-ES",
progress: 46,
progress: 45,
dir: "ltr",
},
{
@@ -201,7 +201,7 @@ export const LOCALES = [
{
name: "British English",
value: "en-GB",
progress: 44,
progress: 42,
dir: "ltr",
},
{
@@ -213,37 +213,37 @@ export const LOCALES = [
{
name: "Deutsch (German)",
value: "de-DE",
progress: 97,
progress: 83,
dir: "ltr",
},
{
name: "Dansk (Danish)",
value: "da-DK",
progress: 47,
progress: 63,
dir: "ltr",
},
{
name: "Čeština (Czech)",
value: "cs-CZ",
progress: 42,
progress: 43,
dir: "ltr",
},
{
name: "Català (Catalan)",
value: "ca-ES",
progress: 38,
progress: 40,
dir: "ltr",
},
{
name: "Български (Bulgarian)",
value: "bg-BG",
progress: 47,
progress: 49,
dir: "ltr",
},
{
name: "العربية (Arabic)",
value: "ar-SA",
progress: 23,
progress: 25,
dir: "rtl",
},
{

View File

@@ -1,5 +1,5 @@
import { ref, watch, computed } from "vue";
import { useAuthBackend } from "~/composables/useAuthBackend";
import { useAuthBackend } from "~/composables/use-auth-backend";
import type { UserOut } from "~/lib/api/types/user";
export const useMealieAuth = function () {

View File

@@ -2,6 +2,7 @@ import { useRecipeCreatePreferences } from "~/composables/use-users/preferences"
export interface UseNewRecipeOptionsProps {
enableImportKeywords?: boolean;
enableImportCategories?: boolean;
enableStayInEditMode?: boolean;
enableParseRecipe?: boolean;
}
@@ -9,6 +10,7 @@ export interface UseNewRecipeOptionsProps {
export function useNewRecipeOptions(props: UseNewRecipeOptionsProps = {}) {
const {
enableImportKeywords = true,
enableImportCategories = true,
enableStayInEditMode = true,
enableParseRecipe = true,
} = props;
@@ -27,6 +29,17 @@ export function useNewRecipeOptions(props: UseNewRecipeOptionsProps = {}) {
},
});
const importCategories = computed({
get() {
if (!enableImportCategories) return false;
return recipeCreatePreferences.value.importCategories;
},
set(v: boolean) {
if (!enableImportCategories) return;
recipeCreatePreferences.value.importCategories = v;
},
});
const stayInEditMode = computed({
get() {
if (!enableStayInEditMode) return false;
@@ -71,6 +84,7 @@ export function useNewRecipeOptions(props: UseNewRecipeOptionsProps = {}) {
return {
// Computed properties for the checkboxes
importKeywordsAsTags,
importCategories,
stayInEditMode,
parseRecipe,
@@ -79,6 +93,7 @@ export function useNewRecipeOptions(props: UseNewRecipeOptionsProps = {}) {
// Props for conditional rendering
enableImportKeywords,
enableImportCategories,
enableStayInEditMode,
enableParseRecipe,
};

View File

@@ -1,5 +1,5 @@
import { Organizer, type RecipeOrganizer } from "~/lib/api/types/non-generated";
import type { LogicalOperator, RelationalKeyword, RelationalOperator } from "~/lib/api/types/response";
import { Organizer } from "~/lib/api/types/non-generated";
import type { LogicalOperator, RecipeOrganizer, RelationalKeyword, RelationalOperator } from "~/lib/api/types/non-generated";
export interface FieldLogicalOperator {
label: string;
@@ -168,6 +168,7 @@ export function useQueryFilterBuilder() {
|| type === Organizer.Tool
|| type === Organizer.Food
|| type === Organizer.Household
|| type === Organizer.User
);
};

View File

@@ -1,7 +1,7 @@
import { useLocalStorage, useSessionStorage } from "@vueuse/core";
import { ActivityKey } from "~/lib/api/types/activity";
import type { RegisteredParser, TimelineEventType } from "~/lib/api/types/recipe";
import type { QueryFilterJSON } from "~/lib/api/types/response";
import type { QueryFilterJSON } from "~/lib/api/types/non-generated";
export interface UserPrintPreferences {
imagePosition: string;
@@ -63,6 +63,7 @@ export interface UserRecipeFinderPreferences {
export interface UserRecipeCreatePreferences {
importKeywordsAsTags: boolean;
importCategories: boolean;
stayInEditMode: boolean;
parseRecipe: boolean;
}
@@ -233,6 +234,7 @@ export function useRecipeCreatePreferences(): Ref<UserRecipeCreatePreferences> {
"recipe-create-preferences",
{
importKeywordsAsTags: false,
importCategories: false,
stayInEditMode: false,
parseRecipe: true,
},

View File

@@ -0,0 +1,34 @@
import { describe, expect, test } from "vitest";
import { normalize, normalizeFilter } from "./use-utils";
describe("test normalize", () => {
test("base case", () => {
expect(normalize("banana")).not.toEqual(normalize("Potatoes"));
});
test("diacritics", () => {
expect(normalize("Rátàtôuile")).toEqual("ratatouile");
});
test("ligatures", () => {
expect(normalize("IJ")).toEqual("ij");
expect(normalize("æ")).toEqual("ae");
expect(normalize("œ")).toEqual("oe");
expect(normalize("ff")).toEqual("ff");
expect(normalize("fi")).toEqual("fi");
expect(normalize("st")).toEqual("st");
});
});
describe("test normalize filter", () => {
test("base case", () => {
const patternA = "Escargots persillés";
const patternB = "persillés";
expect(normalizeFilter(patternA, patternB)).toBeTruthy();
expect(normalizeFilter(patternB, patternA)).toBeFalsy();
});
test("normalize", () => {
const value = "Cœur de bœuf";
const query = "coeur";
expect(normalizeFilter(value, query)).toBeTruthy();
});
});

View File

@@ -1,4 +1,5 @@
import { useDark, useToggle } from "@vueuse/core";
import type { FilterFunction } from "vuetify";
export const useToggleDarkMode = () => {
const isDark = useDark();
@@ -18,6 +19,38 @@ export const titleCase = function (str: string) {
.join(" ");
};
const replaceAllBuilder = (map: Map<string, string>): ((str: string) => string) => {
const re = new RegExp(Array.from(map.keys()).join("|"), "gi");
return str => str.replace(re, matched => map.get(matched)!);
};
const normalizeLigatures = replaceAllBuilder(new Map([
["œ", "oe"],
["æ", "ae"],
["ij", "ij"],
["ff", "ff"],
["fi", "fi"],
["fl", "fl"],
["st", "st"],
]));
export const normalize = (str: string) => {
if (!str) {
return "";
}
let normalized = str.normalize("NFKD").toLowerCase();
normalized = normalized.replace(/\p{Diacritic}/gu, "");
normalized = normalizeLigatures(normalized);
return normalized;
};
export const normalizeFilter: FilterFunction = (value: string, query: string) => {
const normalizedValue = normalize(value);
const normalizeQuery = normalize(query);
return normalizedValue.includes(normalizeQuery);
};
export function uuid4() {
return "10000000-1000-4000-8000-100000000000".replace(/[018]/g, c =>
(parseInt(c) ^ (crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (parseInt(c) / 4)))).toString(16),

View File

@@ -1,12 +1,13 @@
import type { RequestResponse } from "~/lib/api/types/non-generated";
import type { ValidationResponse } from "~/lib/api/types/response";
import { required, email, whitespace, url, minLength, maxLength } from "~/lib/validators";
import { required, email, whitespace, url, urlOptional, minLength, maxLength } from "~/lib/validators";
export const validators = {
required,
email,
whitespace,
url,
urlOptional,
minLength,
maxLength,
};

View File

@@ -4,10 +4,10 @@
"about-mealie": "Meer oor Mealie",
"api-docs": "API Dokumentasie",
"api-port": "API Poort",
"application-mode": "Applikasie modues",
"application-mode": "Applikasie Modus",
"database-type": "Databasis Tipe",
"database-url": "Databasis URL",
"default-group": "Standaard groep",
"default-group": "Standaard Groep",
"default-household": "Default Household",
"demo": "Demonstrasie",
"demo-status": "Demonstrasie Status",
@@ -65,7 +65,7 @@
"something-went-wrong": "Iets het verkeerd geloop!",
"subscribed-events": "Ingetekende Gebeure",
"test-message-sent": "Toets Boodskap Gestuur",
"message-sent": "Message Sent",
"message-sent": "Boodskap Gestuur",
"new-notification": "Nuwe kennisgewing",
"event-notifiers": "Gebeurteniskennisgewers",
"apprise-url-skipped-if-blank": "Apprise URL (oorgeslaan indien leeg)",
@@ -84,12 +84,12 @@
"label-events": "Label Events"
},
"general": {
"add": "Add",
"add": "Voeg by",
"cancel": "Kanselleer",
"clear": "Maak skoon",
"close": "Maak toe",
"confirm": "Bevestig",
"confirm-how-does-everything-look": "How does everything look?",
"confirm-how-does-everything-look": "Hoe lyk alles?",
"confirm-delete-generic": "Is jy seker jy wil dit uitvee?",
"copied_message": "Gekopieër!",
"create": "Skep",
@@ -120,9 +120,9 @@
"json": "JSON",
"keyword": "Sleutelwoord",
"link-copied": "Skakel gekopieer",
"loading": "Loading",
"loading": "Laai tans",
"loading-events": "Besig om gebeurtenisse te laai",
"loading-recipe": "Loading recipe...",
"loading-recipe": "Laai tans resep...",
"loading-ocr-data": "Loading OCR data...",
"loading-recipes": "Besig om resepte te laai",
"message": "Boodskap",
@@ -134,7 +134,7 @@
"no-recipe-found": "Geen resep gevind nie",
"ok": "OK",
"options": "Opsies:",
"plural-name": "Plural Name",
"plural-name": "Meervoudsnaam",
"print": "Druk",
"print-preferences": "Drukvoorkeure",
"random": "Willekeurig",
@@ -148,23 +148,23 @@
"save": "Stoor",
"settings": "Verstellings",
"share": "Deel",
"show-all": "Show All",
"show-all": "Wys Alles",
"shuffle": "Skommel",
"sort": "Sorteer",
"sort-ascending": "Sort Ascending",
"sort-descending": "Sort Descending",
"sort-ascending": "Sorteer Oplopend",
"sort-descending": "Sorteer Aflopend",
"sort-alphabetically": "Alfabeties",
"status": "Status",
"subject": "Onderwerp",
"submit": "Dien in",
"success-count": "Sukses: {count}",
"sunday": "Sondag",
"system": "System",
"system": "Sisteem",
"templates": "Sjablone:",
"test": "Toets",
"themes": "Temas",
"thursday": "Donderdag",
"title": "Title",
"title": "Titel",
"token": "Token",
"tuesday": "Dinsdag",
"type": "Tipe",
@@ -179,12 +179,12 @@
"units": "Eenhede",
"back": "Terug",
"next": "Volgende",
"start": "Start",
"start": "Begin",
"toggle-view": "Wissel aansig",
"date": "Datum",
"id": "Id",
"owner": "Eienaar",
"change-owner": "Change Owner",
"change-owner": "Verander Eienaar",
"date-added": "Datum bygevoeg",
"none": "Geen",
"run": "Hardloop",
@@ -212,14 +212,16 @@
"upload-file": "Laai dokument op",
"created-on-date": "Geskep op: {0}",
"unsaved-changes": "You have unsaved changes. Do you want to save before leaving? Okay to save, Cancel to discard changes.",
"clipboard-copy-failure": "Failed to copy to the clipboard.",
"confirm-delete-generic-items": "Are you sure you want to delete the following items?",
"organizers": "Organizers",
"caution": "Caution",
"show-advanced": "Show Advanced",
"add-field": "Add Field",
"date-created": "Date Created",
"date-updated": "Date Updated"
"discard-changes": "Discard Changes",
"discard-changes-description": "You have unsaved changes. Are you sure you want to discard them?",
"clipboard-copy-failure": "Kon nie kopieer na die knipbord toe nie.",
"confirm-delete-generic-items": "Is jy seker jy wil die volgende items verwyder?",
"organizers": "Organiseerders",
"caution": "Versigtig",
"show-advanced": "Wys uitgebreide",
"add-field": "Voeg veld by",
"date-created": "Datum Geskep",
"date-updated": "Datum Opgedateer"
},
"group": {
"are-you-sure-you-want-to-delete-the-group": "Is jy seker jy wil <b>{groupName}<b/> uitvee?",
@@ -326,11 +328,11 @@
"mealplan-households-description": "If no household is selected, recipes can be added from any household",
"any-category": "Any Category",
"any-tag": "Any Tag",
"any-household": "Any Household",
"any-household": "Enide Huishouding",
"no-meal-plan-defined-yet": "Nog geen maaltydplan opgestel nie",
"no-meal-planned-for-today": "Geen maaltyd beplan vir vandag nie",
"numberOfDays-hint": "Number of days on page load",
"numberOfDays-label": "Default Days",
"numberOfDays-label": "Standaard dae",
"only-recipes-with-these-categories-will-be-used-in-meal-plans": "Slegs resepte met hierdie kategorieë sal in maaltydplanne gebruik word",
"planner": "Beplanner",
"quick-week": "Vinnige week",
@@ -342,6 +344,9 @@
"breakfast": "Ontbyt",
"lunch": "Middagete",
"dinner": "Aandete",
"snack": "Snack",
"drink": "Drank",
"dessert": "Nagereg",
"type-any": "Enige",
"day-any": "Enige",
"editor": "Editor",
@@ -364,7 +369,9 @@
"recipe-rules": "Resepreëls",
"applies-to-all-days": "Van toepassing op alle dae",
"applies-on-days": "Van toepassing op {0}s",
"meal-plan-settings": "Maaltydplan verstellings"
"meal-plan-settings": "Maaltydplan verstellings",
"add-all-to-list": "Add All to List",
"add-day-to-list": "Add Day to List"
},
"migration": {
"migration-data-removed": "Migrasiedata is uitgevee",
@@ -438,10 +445,11 @@
"paste-in-your-recipe-data-each-line-will-be-treated-as-an-item-in-a-list": "Plak jou resepdata in. Elke reël sal as 'n item in 'n lys hanteer word",
"recipe-markup-specification": "Resep formaat spesifikasie",
"recipe-url": "Resep URL",
"recipe-html-or-json": "Recipe HTML or JSON",
"recipe-html-or-json": "Resep HTML of JSON",
"upload-a-recipe": "Laai 'n resep op",
"upload-individual-zip-file": "Laai 'n .zip-lêer op wat vanaf 'n ander Mealie-instansie uitgevoer is.",
"url-form-hint": "Kopieer en plak 'n skakel vanaf jou gunstelingresepwebwerf",
"copy-and-paste-the-source-url-of-your-data-optional": "Copy and paste the source URL of your data (optional)",
"view-scraped-data": "Bekyk opgespoorde data",
"trim-whitespace-description": "Knip voorste en agterste witspasie sowel as leë reëls",
"trim-prefix-description": "Knip die eerste karakter van elke reël af",
@@ -449,8 +457,8 @@
"import-by-url": "Voer 'n resep vanaf 'n webwerf in",
"create-manually": "Skep 'n resep met die hand",
"make-recipe-image": "Maak dit die prentjie vir hierdie resep",
"add-food": "Add Food",
"add-recipe": "Add Recipe"
"add-food": "Voeg Voedsel",
"add-recipe": "Voeg Resep By"
},
"page": {
"404-page-not-found": "404 Bladsy nie gevind nie",
@@ -517,8 +525,8 @@
"recipe-deleted": "Resep uitgevee",
"recipe-image": "Resep foto",
"recipe-image-updated": "Resep foto is opgedateer",
"delete-image": "Delete Recipe Image",
"delete-image-confirmation": "Are you sure you want to delete this recipe image?",
"delete-image": "Verwyder Resep Beeld",
"delete-image-confirmation": "Is jy seker jy wil dié beeld van die resep verwyder?",
"recipe-image-deleted": "Recipe image deleted",
"recipe-name": "Resepnaam",
"recipe-settings": "Resep verstellings",
@@ -552,7 +560,7 @@
"add-to-plan": "Voeg by plan",
"add-to-timeline": "Voeg by tydlyn",
"recipe-added-to-list": "Resep by lys gevoeg",
"recipes-added-to-list": "Recipes added to list",
"recipes-added-to-list": "Resepte toegevoeg tot lys",
"successfully-added-to-list": "Successfully added to list",
"recipe-added-to-mealplan": "Resep is by die maaltydplan gevoeg",
"failed-to-add-recipes-to-list": "Failed to add recipe to list",
@@ -565,7 +573,7 @@
"choose-unit": "Kies 'n eenheid",
"press-enter-to-create": "Druk Enter om te skep",
"choose-food": "Keuse van kos",
"choose-recipe": "Choose Recipe",
"choose-recipe": "Kies Resep",
"notes": "Notas",
"toggle-section": "Wissel afdeling",
"see-original-text": "Sien oorspronklike teks",
@@ -633,7 +641,10 @@
"scrape-recipe-suggest-bulk-importer": "Try out the bulk importer",
"scrape-recipe-have-raw-html-or-json-data": "Have raw HTML or JSON data?",
"scrape-recipe-you-can-import-from-raw-data-directly": "You can import from raw data directly",
"scrape-recipe-website-being-blocked": "Website being blocked?",
"scrape-recipe-try-importing-raw-html-instead": "Try importing the raw HTML instead.",
"import-original-keywords-as-tags": "Voer oorspronklike sleutelwoorde as merkers in",
"import-original-categories": "Import original categories",
"stay-in-edit-mode": "Bly in redigeer modus",
"parse-recipe-ingredients-after-import": "Parse recipe ingredients after import",
"import-from-zip": "Voer vanaf zip in",
@@ -654,7 +665,7 @@
"report-deletion-failed": "Kon nie verslag uitvee nie",
"recipe-debugger": "Resep debugger",
"recipe-debugger-description": "Gryp die URL van die resep wat jy wil debug en plak dit hier. Die URL sal deur die resepskraper geskraap word en die resultate sal vertoon word. As jy nie enige data terugstuur sien nie, word die webwerf wat jy probeer skraap nie deur Mealie of sy skraperbiblioteek ondersteun nie.",
"use-openai": "Use OpenAI",
"use-openai": "Gebruik OpenAI",
"recipe-debugger-use-openai-description": "Use OpenAI to parse the results instead of relying on the scraper library. When creating a recipe via URL, this is done automatically if the scraper library fails, but you may test it manually here.",
"debug": "Debug",
"tree-view": "Boomstruktuur",
@@ -665,8 +676,8 @@
"upload-image": "Laai prent",
"screen-awake": "Hou die skerm aan",
"remove-image": "Verwyder prent",
"nextStep": "Next step",
"recipe-actions": "Recipe Actions",
"nextStep": "Volgende stap",
"recipe-actions": "Resep Aksies",
"parser": {
"ingredient-parser": "Ingredient Parser",
"explanation": "To use the ingredient parser, click the 'Parse All' button to start the process. Once the processed ingredients are available, you can review the items and verify that they were parsed correctly. The model's confidence score is displayed on the right of the item title. This score is an average of all the individual scores and may not always be completely accurate.",
@@ -676,7 +687,7 @@
"brute-parser": "Brute Parser",
"openai-parser": "OpenAI Parser",
"parse-all": "Parse All",
"no-unit": "No unit",
"no-unit": "Geen eenheid",
"missing-unit": "Create missing unit: {unit}",
"missing-food": "Create missing food: {food}",
"this-unit-could-not-be-parsed-automatically": "This unit could not be parsed automatically",
@@ -687,12 +698,12 @@
"ingredient-parser-description": "Your ingredients have been successfully parsed. Please review the ingredients we're not sure about.",
"ingredient-parser-final-review-description": "Once all ingredients have been reviewed, you'll have one more chance to review all ingredients before applying the changes to your recipe.",
"add-text-as-alias-for-item": "Add \"{text}\" as alias for {item}",
"delete-item": "Delete Item"
"delete-item": "Verwyder Item"
},
"reset-servings-count": "Reset Servings Count",
"not-linked-ingredients": "Additional Ingredients",
"not-linked-ingredients": "Bykomende Bestanddele",
"upload-another-image": "Upload another image",
"upload-images": "Upload images",
"upload-images": "Laai beelde op",
"upload-more-images": "Upload more images",
"set-as-cover-image": "Set as recipe cover image",
"cover-image": "Cover image",
@@ -884,7 +895,7 @@
"oidc-ready": "OIDC Klar",
"oidc-ready-error-text": "Ikke alle OIDC værdier er konfigureret. Dette kan ignoreres hvis du ikke bruger OIDC godkendelse.",
"oidc-ready-success-text": "Krævede OIDC variabler er udfyldt.",
"openai-ready": "OpenAI Ready",
"openai-ready": "OpenAI Gereed",
"openai-ready-error-text": "Not all OpenAI Values are configured. This can be ignored if you are not using OpenAI features.",
"openai-ready-success-text": "Required OpenAI variables are all set."
},
@@ -1422,5 +1433,13 @@
"is-like": "is like",
"is-not-like": "is not like"
}
},
"validators": {
"required": "This Field is Required",
"invalid-email": "Email Must Be Valid",
"invalid-url": "Must Be A Valid URL",
"no-whitespace": "No Whitespace Allowed",
"min-length": "Must Be At Least {min} Characters",
"max-length": "Must Be At Most {max} Characters"
}
}

View File

@@ -212,6 +212,8 @@
"upload-file": "تحميل الملف",
"created-on-date": "تم الإنشاء في {0}",
"unsaved-changes": "لديك تغييرات غير محفوظة. هل تريد الحفظ قبل المغادرة؟ حسنًا للحفظ، قم بإلغاء تجاهل التغييرات.",
"discard-changes": "إلغاء التغييرات",
"discard-changes-description": "لديك تغييرات غير محفوظة. هل أنت متأكد من أنك تريد تجاهلها؟",
"clipboard-copy-failure": "فشل في النسخ إلى الحافظة.",
"confirm-delete-generic-items": "هل أنت متأكد أنك تريد حذف المجموعات التالية؟",
"organizers": "المنظمون",
@@ -342,6 +344,9 @@
"breakfast": "الإفطار",
"lunch": "الغداء",
"dinner": "العشاء",
"snack": "وجبة خفيفة",
"drink": "مشروب",
"dessert": "حلوى",
"type-any": "أي",
"day-any": "أي",
"editor": "المحرر",
@@ -364,7 +369,9 @@
"recipe-rules": "قواعد الوصفات",
"applies-to-all-days": "ينطبق على جميع الأيام",
"applies-on-days": "يطبق على أيام {0}",
"meal-plan-settings": "إعدادات خِطَّة الوجبات الغذائية"
"meal-plan-settings": "إعدادات خِطَّة الوجبات الغذائية",
"add-all-to-list": "Add All to List",
"add-day-to-list": "Add Day to List"
},
"migration": {
"migration-data-removed": "حذف بيانات الهجرة",
@@ -435,22 +442,23 @@
"github-issues": "مشاكل GitHub",
"google-ld-json-info": "معرف Google + معلومات json",
"must-be-a-valid-url": "يجب أن يكون عنوان URL صالحًا",
"paste-in-your-recipe-data-each-line-will-be-treated-as-an-item-in-a-list": "Paste in your recipe data. Each line will be treated as an item in a list",
"paste-in-your-recipe-data-each-line-will-be-treated-as-an-item-in-a-list": "لصق بيانات الوصفة الخاصة بك. سيتم التعامل مع كل سطر كعنصر في قائمة",
"recipe-markup-specification": "Recipe Markup Specification",
"recipe-url": "رابط الوصفة",
"recipe-html-or-json": "وصفة HTML أو JSON",
"upload-a-recipe": "تحميل وصفة",
"upload-individual-zip-file": "تحميل مِلَفّ zip فردي تم تصديره من مثيل Malie آخر.",
"url-form-hint": "نسخ ولصق رابط من موقعك المفضل للوصفة",
"copy-and-paste-the-source-url-of-your-data-optional": "نسخ ولصق عنوان URL المصدر لبياناتك (اختياري)",
"view-scraped-data": "عرض البيانات المحللة",
"trim-whitespace-description": "قص المسافات البيضاء البادئة واللاحقة وكذلك الأسطر الفارغة",
"trim-prefix-description": "قص الحرف الأول من كل سطر",
"split-by-numbered-line-description": "Attempts to split a paragraph by matching '1)' or '1.' patterns",
"split-by-numbered-line-description": "محاولات تقسيم فقرة عن طريق مطابقة أنماط '1)' أو '1.'",
"import-by-url": "استيراد وصفة عن طريق عنوان URL",
"create-manually": "إنشاء وصفة يدوياً",
"make-recipe-image": "اجعل هذه صورة الوصفة",
"add-food": "Add Food",
"add-recipe": "Add Recipe"
"add-food": "إضافة طعام",
"add-recipe": "إضافة وصفة"
},
"page": {
"404-page-not-found": "404: لم يتم العثور على الصفحة",
@@ -480,7 +488,7 @@
"comment": "أضف تعليق ",
"comments": "التعليقات",
"delete-confirmation": "هل انت متأكد من رغبتك بحذف هذه الوصفة؟",
"admin-delete-confirmation": "You're about to delete a recipe that isn't yours using admin permissions. Are you sure?",
"admin-delete-confirmation": "أنت على وشك حذف وصفة ليست لك استخدام أذونات المشرف. هل أنت متأكد؟",
"delete-recipe": "حذف الوصفة",
"description": "الوصف",
"disable-amount": "إيقاف إظهار كميات المكونات",
@@ -517,9 +525,9 @@
"recipe-deleted": "تم حذف الوصفة",
"recipe-image": "صورة الوصفة",
"recipe-image-updated": "تم تحديث صورة الوصفة",
"delete-image": "Delete Recipe Image",
"delete-image-confirmation": "Are you sure you want to delete this recipe image?",
"recipe-image-deleted": "Recipe image deleted",
"delete-image": "حذف صورة الوصفة",
"delete-image-confirmation": "هل أنت متأكد أنك تريد حذف صورة الوصفة هذه؟",
"recipe-image-deleted": "تم حذف صورة الوصفة",
"recipe-name": "اسم الوصفة",
"recipe-settings": "إعدادات الوصفة",
"recipe-update-failed": "فشل تحديث الوصفة",
@@ -552,10 +560,10 @@
"add-to-plan": "أضف إلى الخُطة",
"add-to-timeline": "إضافة إلى الخط الزمني",
"recipe-added-to-list": "تم إضافة الوصفة إلى القائمة",
"recipes-added-to-list": "Recipes added to list",
"successfully-added-to-list": "Successfully added to list",
"recipe-added-to-mealplan": "Recipe added to mealplan",
"failed-to-add-recipes-to-list": "Failed to add recipe to list",
"recipes-added-to-list": "تم إضافة الوصفات إلى القائمة",
"successfully-added-to-list": "تمت الإضافة إلى القائمة بنجاح",
"recipe-added-to-mealplan": "تم إضافة الوصفة إلى خطة الوجبات",
"failed-to-add-recipes-to-list": "فشل في إضافة وصفة إلى القائمة",
"failed-to-add-recipe-to-mealplan": "فشل في إضافة الوصفة إلى خطة الوجبة",
"failed-to-add-to-list": "فشل في الإضافة إلى القائمة",
"yield": "العائد",
@@ -565,13 +573,13 @@
"choose-unit": "اختر الوحدة",
"press-enter-to-create": "",
"choose-food": "اختيار الطعام",
"choose-recipe": "Choose Recipe",
"choose-recipe": "اختر وصفة",
"notes": "ملاحظات",
"toggle-section": "",
"see-original-text": "عرض النص الأصلي",
"original-text-with-value": "النص الأصلي: {originalText}",
"ingredient-linker": "رابط المكون",
"unlinked": "Not linked yet",
"unlinked": "غير مرتبط بعد",
"linked-to-other-step": "مرتبط بخطوة أخرى",
"auto": "تلقائي",
"cook-mode": "وضع الطبخ",
@@ -593,23 +601,23 @@
"made-this": "لقد طبخت هذا",
"how-did-it-turn-out": "كيف كانت النتيجة؟",
"user-made-this": "{user} طبخ هذه",
"made-for-recipe": "Made for {recipe}",
"added-to-timeline": "Added to timeline",
"failed-to-add-to-timeline": "Failed to add to timeline",
"failed-to-update-recipe": "Failed to update recipe",
"added-to-timeline-but-failed-to-add-image": "Added to timeline, but failed to add image",
"made-for-recipe": "صُنع لـ {recipe}",
"added-to-timeline": "تمت الإضافة إلى الجدول الزمني",
"failed-to-add-to-timeline": "فشلت الإضافة إلى الجدول الزمني",
"failed-to-update-recipe": "فشل تحديث الوصفة",
"added-to-timeline-but-failed-to-add-image": "تمت الإضافة إلى الجدول الزمني، ولكن فشل في إضافة صورة",
"api-extras-description": "Recipes extras are a key feature of the Mealie API. They allow you to create custom JSON key/value pairs within a recipe, to reference from 3rd party applications. You can use these keys to provide information, for example to trigger automations or custom messages to relay to your desired device.",
"message-key": "مفتاح الرساله",
"parse": "تحليل",
"ingredients-not-parsed-description": "It looks like your ingredients aren't parsed yet. Click the \"{parse}\" button below to parse your ingredients into structured foods.",
"attach-images-hint": "Attach images by dragging & dropping them into the editor",
"attach-images-hint": "إرفاق الصور عن طريق سحبها وإسقاطها في المحرر",
"drop-image": "وضع الصورة",
"enable-ingredient-amounts-to-use-this-feature": "Enable ingredient amounts to use this feature",
"recipes-with-units-or-foods-defined-cannot-be-parsed": "Recipes with units or foods defined cannot be parsed.",
"parse-ingredients": "تحليل المكونات",
"edit-markdown": "تعديل Markdown",
"recipe-creation": "إنشاء الوصفة",
"select-one-of-the-various-ways-to-create-a-recipe": "Select one of the various ways to create a recipe",
"select-one-of-the-various-ways-to-create-a-recipe": "اختر واحدة من الطرق المختلفة لإنشاء وصفة",
"looking-for-migrations": "هل تبحث عن نقل المعلومات؟",
"import-with-url": "الاستيراد باستخدام URL",
"create-recipe": "إنشاء وصفة",
@@ -619,21 +627,24 @@
"create-recipe-from-an-image": "إنشاء وصفة عن طريق صورة",
"create-recipe-from-an-image-description": "Create a recipe by uploading an image of it. Mealie will attempt to extract the text from the image using AI and create a recipe from it.",
"crop-and-rotate-the-image": "Crop and rotate the image so that only the text is visible, and it's in the correct orientation.",
"create-from-images": "Create from Images",
"should-translate-description": "Translate the recipe into my language",
"please-wait-image-procesing": "Please wait, the image is processing. This may take some time.",
"please-wait-images-processing": "Please wait, the images are processing. This may take some time.",
"create-from-images": "إنشاء عن طريق صور",
"should-translate-description": "ترجمة الوصفة إلى لغتي",
"please-wait-image-procesing": "الرجاء الانتظار، تتم معالجة الصورة. قد يستغرق هذا بعض الوقت.",
"please-wait-images-processing": "الرجاء الانتظار، يتم معالجة الصور. قد يستغرق هذا بعض الوقت.",
"bulk-url-import": "الاستيراد باستخدام أكثر من URL ",
"debug-scraper": "تصحيح أخطاء المحلل\n",
"create-a-recipe-by-providing-the-name-all-recipes-must-have-unique-names": "Create a recipe by providing the name. All recipes must have unique names.",
"new-recipe-names-must-be-unique": "New recipe names must be unique",
"create-a-recipe-by-providing-the-name-all-recipes-must-have-unique-names": "قم بإنشاء وصفة عن طريق تقديم الاسم. يجب أن يكون لجميع الوصفات أسماء فريدة.",
"new-recipe-names-must-be-unique": "يجب أن تكون أسماء الوصفات فريدة",
"scrape-recipe": "تحليل الوصفة",
"scrape-recipe-description": "أضف الوصفة عن طريق الرابط. قم بتوفير عنوان الURL للموقع الذي تريد أخذ الوصفة منه، وسيحاول Mealie إستخراج الوصفة من ذلك الموقع وإضافتها إلى مجموعتك.",
"scrape-recipe-have-a-lot-of-recipes": "هل لديك الكثير من الوصفات التي تريد أن تحللها في نفس الوقت؟",
"scrape-recipe-suggest-bulk-importer": "جرب الإضافة بالجملة",
"scrape-recipe-have-raw-html-or-json-data": "هل لديك بيانات HTML أو JSON خام؟",
"scrape-recipe-you-can-import-from-raw-data-directly": "يمكنك الإضافة مباشرة باستخدام بيانات خام",
"scrape-recipe-website-being-blocked": "Website being blocked?",
"scrape-recipe-try-importing-raw-html-instead": "Try importing the raw HTML instead.",
"import-original-keywords-as-tags": "استيراد الكلمات المفتاحية الأصلية كوسوم",
"import-original-categories": "Import original categories",
"stay-in-edit-mode": "البقاء في وضع التعديل",
"parse-recipe-ingredients-after-import": "Parse recipe ingredients after import",
"import-from-zip": "استيراد من ملف Zip",
@@ -687,15 +698,15 @@
"ingredient-parser-description": "Your ingredients have been successfully parsed. Please review the ingredients we're not sure about.",
"ingredient-parser-final-review-description": "Once all ingredients have been reviewed, you'll have one more chance to review all ingredients before applying the changes to your recipe.",
"add-text-as-alias-for-item": "Add \"{text}\" as alias for {item}",
"delete-item": "Delete Item"
"delete-item": "حذف العنصر"
},
"reset-servings-count": "إعادة تعيين عدد الحصص",
"not-linked-ingredients": "مكونات إضافية",
"upload-another-image": "Upload another image",
"upload-images": "Upload images",
"upload-more-images": "Upload more images",
"set-as-cover-image": "Set as recipe cover image",
"cover-image": "Cover image",
"upload-another-image": "رفع صورة أخرى",
"upload-images": "رفع الصور",
"upload-more-images": "رفع المزيد من الصور",
"set-as-cover-image": "تعيين كصورة غلاف الوصفة",
"cover-image": "صورة الغلاف",
"include-linked-recipes": "Include Linked Recipes",
"include-linked-recipe-ingredients": "Include Linked Recipe Ingredients",
"toggle-recipe": "Toggle Recipe"
@@ -736,7 +747,7 @@
"advanced": "الإعدادات المتقدمة",
"auto-search": "البحث التلقائي",
"no-results": "لم يتم العثور على نتائج",
"type-to-search": "Type to search..."
"type-to-search": "اكتب للبحث ..."
},
"settings": {
"add-a-new-theme": "إضافة سمة جديدة",
@@ -770,8 +781,8 @@
"custom-pages": "الصفحات الخاصة",
"edit-page": "تعديل الصفحة",
"events": "الأحداث",
"first-day-of-week": "First day of the week",
"group-settings-updated": "Group Settings Updated",
"first-day-of-week": "اليوم الأول من الأسبوع",
"group-settings-updated": "تم تحديث إعدادات المجموعة",
"homepage": {
"all-categories": "جميع التصنيفات",
"card-per-section": "Card Per Section",
@@ -793,29 +804,29 @@
"remove-existing-entries-matching-imported-entries": "Remove existing entries matching imported entries",
"set-new-time": "تعيين وقت جديد",
"settings-update-failed": "فشل تحديث الإعدادات",
"settings-updated": "Settings updated",
"settings-updated": "تم تحديث الإعدادات",
"site-settings": "إعدادات الموقع",
"theme": {
"accent": "Accent",
"accent": "لون التمييز",
"dark": "الوضع الليلي",
"default-to-system": "مطابقة وضع الجهاز",
"error": "حدث خطأ",
"error-creating-theme-see-log-file": "Error creating theme. See log file.",
"error-deleting-theme": "Error deleting theme",
"error-updating-theme": "Error updating theme",
"info": "Info",
"error-creating-theme-see-log-file": "خطأ في إنشاء السمة. راجع ملف السجل.",
"error-deleting-theme": "خطأ في حذف السمة",
"error-updating-theme": "خطأ في تحديث السمة",
"info": "معلومات",
"light": "الوضع النهاري",
"primary": "رئيسي",
"secondary": "ثانوي",
"success": "Success",
"success": "تم بنجاح",
"switch-to-dark-mode": "التحويل إلى الوضع الليلي",
"switch-to-light-mode": "التحويل إلى الوضع النهاري",
"theme-deleted": "Theme deleted",
"theme-name": "Theme Name",
"theme-name-is-required": "Theme Name is required.",
"theme-saved": "Theme Saved",
"theme-updated": "Theme updated",
"warning": "Warning",
"theme-deleted": "تم حذف السمة",
"theme-name": "اسم السمة",
"theme-name-is-required": "اسم السمة مطلوب.",
"theme-saved": "تم حفظ السمة",
"theme-updated": "تم تحديث السمة",
"warning": "تحذير",
"light-mode": "الوضع النهاري",
"dark-mode": "الوضع الليلي"
},
@@ -852,7 +863,7 @@
"bug-report": "الإبلاغ عن الأخطاء البرمجية",
"bug-report-information": "Use this information to report a bug. Providing details of your instance to developers is the best way to get your issues resolved quickly.",
"tracker": "Tracker",
"configuration": "Configuration",
"configuration": "الإعدادات",
"docker-volume": "Docker Volume",
"docker-volume-help": "Mealie requires that the frontend container and the backend share the same docker volume or storage. This ensures that the frontend container can properly access the images and assets stored on disk.",
"volumes-are-misconfigured": "Volumes are misconfigured.",
@@ -1107,21 +1118,21 @@
"seed-dialog-text": "Seed the database with common units based on your local language.",
"combine-unit-description": "Combining the selected units will merge the Source Unit and Target Unit into a single unit. The {source-unit-will-be-deleted} and all of the references to the Source Unit will be updated to point to the Target Unit.",
"combine-unit": "دمج الوحدة",
"source-unit": "Source Unit",
"target-unit": "Target Unit",
"merging-unit-into-unit": "Merging {0} into {1}",
"source-unit": "الوحدة المصدر",
"target-unit": "الوحدة الهدف",
"merging-unit-into-unit": "دمج {0} مع {1}",
"create-unit": "إنشاء وحدة",
"abbreviation": "Abbreviation",
"abbreviation": "الاختصار",
"plural-abbreviation": "Plural Abbreviation",
"description": "Description",
"description": "الوصف",
"display-as-fraction": "Display as Fraction",
"use-abbreviation": "Use Abbreviation",
"edit-unit": "تعديل الوحدة",
"unit-data": "بيانات الوحدة",
"use-abbv": "Use Abbv.",
"fraction": "Fraction",
"example-unit-singular": "ex: Tablespoon",
"example-unit-plural": "ex: Tablespoons",
"example-unit-singular": "مثال: ملعقة",
"example-unit-plural": "مثال: ملاعق",
"example-unit-abbreviation-singular": "ex: Tbsp",
"example-unit-abbreviation-plural": "ex: Tbsps"
},
@@ -1139,18 +1150,18 @@
"the-following-recipes-selected-length-will-be-exported": "The following recipes ({0}) will be exported.",
"settings-chosen-explanation": "Settings chosen here, excluding the locked option, will be applied to all selected recipes.",
"selected-length-recipe-s-settings-will-be-updated": "{count} recipe(s) settings will be updated.",
"recipe-data": "Recipe Data",
"recipe-data": "بيانات الوصفة",
"recipe-data-description": "استخدم هذا القسم لإدارة البيانات المرتبطة بوصفاتك. يمكنك تنفيذ العديد من الإجراءات بالجملة على وصفاتك بما في ذلك التصدير والحذف وتعيين الوسوم وتعيين التصنيفات.",
"recipe-columns": "Recipe Columns",
"recipe-columns": "أعمدة الوصفة",
"data-exports-description": "This section provides links to available exports that are ready to download. These exports do expire, so be sure to grab them while they're still available.",
"data-exports": "Data Exports",
"data-exports": "صادرات البيانات",
"tag": "وسم",
"categorize": "Categorize",
"update-settings": "Update Settings",
"tag-recipes": "وسم الوصفات",
"categorize-recipes": "Categorize Recipes",
"export-recipes": "Export Recipes",
"delete-recipes": "Delete Recipes",
"categorize-recipes": "تصنيف الوصفات",
"export-recipes": "تصدير الوصفات",
"delete-recipes": "حذف الوصفات",
"source-unit-will-be-deleted": "Source Unit will be deleted"
},
"recipe-actions": {
@@ -1160,15 +1171,15 @@
"action-type": "Action Type"
},
"create-alias": "Create Alias",
"manage-aliases": "Manage Aliases",
"manage-aliases": "إدارة الأسماء المستعارة",
"seed-data": "Seed Data",
"seed": "Seed",
"data-management": "Data Management",
"data-management-description": "Select which data set you want to make changes to.",
"select-data": "Select Data",
"select-language": "Select Language",
"columns": "Columns",
"combine": "Combine",
"select-data": " اختر البيانات",
"select-language": "اختر اللغة",
"columns": "الأعمدة",
"combine": "دمج",
"categories": {
"edit-category": "تعديل التصنيف",
"new-category": "تصنيف جديد",
@@ -1198,15 +1209,15 @@
"account-details": "تفاصيل الحساب"
},
"validation": {
"group-name-is-taken": "Group name is taken",
"group-name-is-taken": "اسم المجموعة مأخوذ",
"username-is-taken": "اسم المستخدم مستعمل",
"email-is-taken": "Email is taken",
"this-field-is-required": "This Field is Required"
"email-is-taken": "هذا البريد الإلكتروني مأخوذ",
"this-field-is-required": "هذا الحقل مطلوب"
},
"export": {
"export": "Export",
"file-name": "File Name",
"size": "Size",
"export": "تصدير",
"file-name": "اسم الملف",
"size": "الحجم",
"link-expires": "Link Expires"
},
"recipe-share": {
@@ -1335,12 +1346,12 @@
"profile": {
"welcome-user": "مرحبًا 👋، {0}!",
"description": "Manage your profile, recipes, and group settings.",
"invite-link": "Invite Link",
"get-invite-link": "Get Invite Link",
"invite-link": "رابط الدعوة",
"get-invite-link": "الحصول على رابط الدعوة",
"get-public-link": "Get Public Link",
"account-summary": "Account Summary",
"account-summary-description": "Here's a summary of your group's information.",
"group-statistics": "Group Statistics",
"account-summary": "ملخص الحساب",
"account-summary-description": "إليك ملخص لمعلومات مجموعتك.",
"group-statistics": "إحصائيات المجموعة",
"group-statistics-description": "Your Group Statistics provide some insight how you're using Mealie.",
"household-statistics": "Household Statistics",
"household-statistics-description": "Your Household Statistics provide some insight how you're using Mealie.",
@@ -1352,32 +1363,32 @@
"user-settings-description": "إدارة تفضيلاتك، وتغيير كلمة المرور الخاصة بك، وتحديث بريدك الإلكتروني.",
"api-tokens-description": "Manage your API Tokens for access from external applications.",
"group-description": "These items are shared within your group. Editing one of them will change it for the whole group!",
"group-settings": "Group Settings",
"group-settings": "إعدادات المجموعة",
"group-settings-description": "Manage your common group settings, like privacy settings.",
"household-description": "These items are shared within your household. Editing one of them will change it for the whole household!",
"household-settings": "Household Settings",
"household-settings": "إعدادات الأسرة",
"household-settings-description": "Manage your household settings, like mealplan and privacy settings.",
"cookbooks-description": "إدارة مجموعة من تصنيفات الوصفات وإنشاء صفحات لها.",
"members": "Members",
"members": "الأعضاء",
"members-description": "See who's in your household and manage their permissions.",
"webhooks-description": "Setup webhooks that trigger on days that you have have mealplan scheduled.",
"notifiers": "Notifiers",
"notifiers-description": "Setup email and push notifications that trigger on specific events.",
"manage-data": "Manage Data",
"manage-data": "إدارة البيانات",
"manage-data-description": "إدارة بيانات Mealie الخاصة بك؛ الأطعمة، الوحدات، التصنيفات، الوسوم وأكثر من ذلك.",
"data-migrations": "Data Migrations",
"data-migrations-description": "Migrate your existing data from other applications like Nextcloud Recipes and Chowdown.",
"email-sent": "Email Sent",
"error-sending-email": "Error Sending Email",
"personal-information": "Personal Information",
"preferences": "Preferences",
"email-sent": "تم إرسال البريد الإلكتروني",
"error-sending-email": "خطأ في إرسال البريد الإلكتروني",
"personal-information": "المعلومات الشخصية",
"preferences": "التفضيلات",
"show-advanced-description": "Show advanced features (API Keys, Webhooks, and Data Management)",
"back-to-profile": "Back to Profile",
"back-to-profile": "العودة إلى الملف الشخصي",
"looking-for-privacy-settings": "Looking for Privacy Settings?",
"manage-your-api-tokens": "Manage Your API Tokens",
"manage-user-profile": "إدارة الملف الشخصي للمستخدم",
"manage-cookbooks": "إدارة كتب الطبخ",
"manage-members": "Manage Members",
"manage-members": "إدارة الأعضاء",
"manage-webhooks": "Manage Webhooks",
"manage-notifiers": "Manage Notifiers",
"manage-data-migrations": "Manage Data Migrations"
@@ -1422,5 +1433,13 @@
"is-like": "هو مثل",
"is-not-like": "ليس مثل"
}
},
"validators": {
"required": "هذا الحقل مطلوب",
"invalid-email": "يجب أن يكون البريد الإلكتروني صالحاً",
"invalid-url": "يجب أن يكون عنوان URL صالحًا",
"no-whitespace": "لا يسمح باستخدام المسافات",
"min-length": "يجب أن يكون على الأقل {min} أحرف",
"max-length": "يجب أن يكون على الأكثر {max} أحرف"
}
}

View File

@@ -156,7 +156,7 @@
"sort-alphabetically": "По азбучен ред",
"status": "състояние",
"subject": "Относно",
"submit": "Изпрати",
"submit": "Потвърди",
"success-count": "Успешни: {count}",
"sunday": "Неделя",
"system": "В хронологичен ред",
@@ -212,11 +212,13 @@
"upload-file": "Качване на файл",
"created-on-date": "Добавена на {0}",
"unsaved-changes": "Имате незапазени промени. Желаете ли да ги запазите преди да излезете? Натиснете Ок за запазване и Отказ за отхвърляне на промените.",
"discard-changes": "Отхвърляне на промените",
"discard-changes-description": "Имате незаписани промени, сигурни ли сте, че искате да ги отмените?",
"clipboard-copy-failure": "Линкът към рецептата е копиран в клипборда.",
"confirm-delete-generic-items": "Сигурни ли сте, че желаете да изтриете следните елементи?",
"organizers": "Органайзер",
"caution": "Внимание",
"show-advanced": "Покажи разширени",
"show-advanced": "Разширени настройки",
"add-field": "Добави поле",
"date-created": "Дата на създаване",
"date-updated": "Дата на актуализация"
@@ -342,6 +344,9 @@
"breakfast": "Закуска",
"lunch": "Обяд",
"dinner": "Вечеря",
"snack": "Закуска",
"drink": "Питие",
"dessert": "Десерт",
"type-any": "Всички",
"day-any": "Всички",
"editor": "Редактор",
@@ -364,7 +369,9 @@
"recipe-rules": "Правила на рецептата",
"applies-to-all-days": "Прилага се за всички дни",
"applies-on-days": "Всеки/всяка {0}",
"meal-plan-settings": "Настройки на плана за хранене"
"meal-plan-settings": "Настройки на плана за хранене",
"add-all-to-list": "Add All to List",
"add-day-to-list": "Add Day to List"
},
"migration": {
"migration-data-removed": "Данните за мигриране са премахнати",
@@ -442,6 +449,7 @@
"upload-a-recipe": "Качи рецепта",
"upload-individual-zip-file": "Качи като индивидуален .zip файлов формат от друга инстанция на Mealie.",
"url-form-hint": "Копирай и постави линк от твоя любим сайт за рецепти",
"copy-and-paste-the-source-url-of-your-data-optional": "Копирайте и поставете URL адреса на източника на вашите данни (по избор)",
"view-scraped-data": "Виж събраните данни",
"trim-whitespace-description": "Премахни интервалите в началото и края на текста, също така и празните редове",
"trim-prefix-description": "Премахни първия символ от всеки ред",
@@ -633,7 +641,10 @@
"scrape-recipe-suggest-bulk-importer": "Пробвайте масовото импорторане",
"scrape-recipe-have-raw-html-or-json-data": "Имате ли сурови HTML или JSON данни?",
"scrape-recipe-you-can-import-from-raw-data-directly": "Можете да импортирате директно от сурови данни",
"scrape-recipe-website-being-blocked": "Блокиран ли е уебсайтът?",
"scrape-recipe-try-importing-raw-html-instead": "Опитайте вместо това да импортирате суровия HTML код.",
"import-original-keywords-as-tags": "Добави оригиналните ключови думи като етикети",
"import-original-categories": "Import original categories",
"stay-in-edit-mode": "Остани в режим на редакция",
"parse-recipe-ingredients-after-import": "Анализиране на съставките на рецептата след импортиране",
"import-from-zip": "Импортирай от Zip",
@@ -698,7 +709,7 @@
"cover-image": "Изображение на корицата",
"include-linked-recipes": "Влючване на свързаните рецепти",
"include-linked-recipe-ingredients": "Включване на съставките от свързаните рецепти",
"toggle-recipe": "Превключване на рецептата"
"toggle-recipe": "Вмъкни рецепта"
},
"recipe-finder": {
"recipe-finder": "Търсачка на рецепти",
@@ -1172,17 +1183,17 @@
"categories": {
"edit-category": "Редактиране на категория",
"new-category": "Нова категория",
"category-data": "Категория за данните"
"category-data": "Категории"
},
"tags": {
"new-tag": "Нов етикет",
"edit-tag": "Редакция на етикет",
"tag-data": "Данни на етикета"
"tag-data": "Етикети"
},
"tools": {
"new-tool": "Нов инструмент",
"edit-tool": "Редактирай инструмента",
"tool-data": "Данни на инструмента"
"tool-data": "Прибори"
}
},
"user-registration": {
@@ -1422,5 +1433,13 @@
"is-like": "е като",
"is-not-like": "не е като"
}
},
"validators": {
"required": "Това поле е задължително",
"invalid-email": "Email адресът трябва да бъде валиден",
"invalid-url": "Линкът трябва да е валиден",
"no-whitespace": "Не са позволени интервали",
"min-length": "Трябва да съдържа поне {min} знака",
"max-length": "Трябва да съдържа най-много {max} знака"
}
}

View File

@@ -69,7 +69,7 @@
"new-notification": "Nova notificació",
"event-notifiers": "Notificacions d'esdeveniments",
"apprise-url-skipped-if-blank": "Apprise URL (si es deixa buit, s'ignorarà)",
"apprise-url-is-left-intentionally-blank": "Since Apprise URLs typically contain sensitive information, this field is left intentionally blank while editing. If you wish to update the URL, please enter the new one here, otherwise leave it blank to keep the current URL.",
"apprise-url-is-left-intentionally-blank": "Com que les URLs de Apprise contenen informació sensible, aquest camp es deixa intencionadament en blanc a l'editar. Si vols actualitzar la URL, per favor, introdueix-ne una nova ací, si no, deixa-ho en blanc per a mantenir la URL actual.",
"enable-notifier": "Habilita la notificació",
"what-events": "Què esdeveniments vols que utilitzen aquest notificador?",
"user-events": "Esdeveniments d'usuari",
@@ -81,7 +81,7 @@
"category-events": "Esdeveniments de les categories",
"when-a-new-user-joins-your-group": "Quan un nou usuari s'afegeix al grup",
"recipe-events": "Esdeveniments de receptes",
"label-events": "Label Events"
"label-events": "Etiquetar Esdeveniments"
},
"general": {
"add": "Afegeix",
@@ -212,6 +212,8 @@
"upload-file": "Puja un fitxer",
"created-on-date": "Creat el: {0}",
"unsaved-changes": "Tens canvis que no estan guardats. Vols guardar-los abans de sortir? Clica d'acord per guardar-los o cancel·lar per descartar els canvis.",
"discard-changes": "Discard Changes",
"discard-changes-description": "You have unsaved changes. Are you sure you want to discard them?",
"clipboard-copy-failure": "No s'ha pogut copiar al porta-retalls.",
"confirm-delete-generic-items": "Are you sure you want to delete the following items?",
"organizers": "Organitzadors",
@@ -342,6 +344,9 @@
"breakfast": "Esmorzar",
"lunch": "Dinar",
"dinner": "Sopar",
"snack": "Piscolabis",
"drink": "Beguda",
"dessert": "Postres",
"type-any": "Qualsevol",
"day-any": "Qualsevol",
"editor": "Editor",
@@ -364,7 +369,9 @@
"recipe-rules": "Normes per la recepta",
"applies-to-all-days": "Aplica a tots els dies",
"applies-on-days": "S'aplicarà en {0}s",
"meal-plan-settings": "Opcions de planificació de menús"
"meal-plan-settings": "Opcions de planificació de menús",
"add-all-to-list": "Add All to List",
"add-day-to-list": "Add Day to List"
},
"migration": {
"migration-data-removed": "S'han suprimit les dades migrades",
@@ -400,7 +407,7 @@
"title": "Tandoor Recipes"
},
"cookn": {
"description-long": "Mealie can import recipes from DVO Cook'n X3. Export a cookbook or menu in the \"Cook'n\" format, rename the export extension to .zip, then upload the .zip below.",
"description-long": "Mealie pot importar receptes des de DVO Cook'n K3. Exporta un receptari o menú en el format \"Cook'n\", canvia la extensió d'exportació a .zip, i després puja el .zip a sota.",
"title": "DVO Cook'n X3"
},
"recipe-data-migrations": "Migració de receptes",
@@ -442,6 +449,7 @@
"upload-a-recipe": "Puja una recepta",
"upload-individual-zip-file": "Puja només un arxiu zip, exportat d'altre Mealie.",
"url-form-hint": "Copia i enganxa l'enllaç del teu lloc web de receptes preferit",
"copy-and-paste-the-source-url-of-your-data-optional": "Copia i enganxa la URL font a les teues dades (opcional)",
"view-scraped-data": "Visualitza les dades recuperades",
"trim-whitespace-description": "Elimina els espais a principi i final; i elimina les línies buides",
"trim-prefix-description": "Elimina el primer caràcter de cada línia",
@@ -449,8 +457,8 @@
"import-by-url": "Importa per URL",
"create-manually": "Crea una recepta manualment",
"make-recipe-image": "Fes-la la imatge de la recepta",
"add-food": "Add Food",
"add-recipe": "Add Recipe"
"add-food": "Afegeix Aliment",
"add-recipe": "Afegeix Recepta"
},
"page": {
"404-page-not-found": "404 - Pàgina no trobada",
@@ -480,7 +488,7 @@
"comment": "Comentari",
"comments": "Comentaris",
"delete-confirmation": "Estàs segur que vols suprimir-la?",
"admin-delete-confirmation": "You're about to delete a recipe that isn't yours using admin permissions. Are you sure?",
"admin-delete-confirmation": "Estàs a punt d'eliminar una recepta que no és teva utilitzant permisos d'administrador. N'estàs segur?",
"delete-recipe": "Suprimeix la recepta",
"description": "Descripció",
"disable-amount": "Oculta les quantitats",
@@ -517,9 +525,9 @@
"recipe-deleted": "S'ha suprimit la recepta",
"recipe-image": "Imatge de la recepta",
"recipe-image-updated": "S'ha actualitzat la imatge de la recepta",
"delete-image": "Delete Recipe Image",
"delete-image-confirmation": "Are you sure you want to delete this recipe image?",
"recipe-image-deleted": "Recipe image deleted",
"delete-image": "Suprimir la imatge de la recepta",
"delete-image-confirmation": "Estàs segur que vols suprimir la imatge d'aquesta recepta?",
"recipe-image-deleted": "S'ha suprimit la imatge de la recepta",
"recipe-name": "Nom de la recepta",
"recipe-settings": "Opcions de la recepta",
"recipe-update-failed": "S'ha produït un error a l'actualitzar la recepta",
@@ -565,7 +573,7 @@
"choose-unit": "Tria el tipus d'unitat",
"press-enter-to-create": "Premeu enter per a crear-lo",
"choose-food": "Tria un aliment",
"choose-recipe": "Choose Recipe",
"choose-recipe": "Tria la recepta",
"notes": "Notes",
"toggle-section": "Nova secció",
"see-original-text": "Mostra el text original",
@@ -593,15 +601,15 @@
"made-this": "Ho he fet",
"how-did-it-turn-out": "Com ha sortit?",
"user-made-this": "{user} ha fet això",
"made-for-recipe": "Made for {recipe}",
"added-to-timeline": "Added to timeline",
"failed-to-add-to-timeline": "Failed to add to timeline",
"failed-to-update-recipe": "Failed to update recipe",
"added-to-timeline-but-failed-to-add-image": "Added to timeline, but failed to add image",
"made-for-recipe": "Fet per a {recipe}",
"added-to-timeline": "Afegit a la cronologia",
"failed-to-add-to-timeline": "No s'ha pogut afegir a la cronologia",
"failed-to-update-recipe": "No s'ha pogut actualitzar la recepta",
"added-to-timeline-but-failed-to-add-image": "S'ha afegit a la línia de temps, però no s'ha pogut afegir la imatge",
"api-extras-description": "Els extres de receptes són una funcionalitat clau de l'API de Mealie. Permeten crear parells clau/valor JSON personalitzats dins una recepta, per referenciar-los des d'aplicacions de tercers. Pots emprar aquestes claus per proveir informació, per exemple per a desencadenar automatitzacions o missatges personlitzats per a propagar al teu dispositiu desitjat.",
"message-key": "Clau del missatge",
"parse": "Analitzar",
"ingredients-not-parsed-description": "It looks like your ingredients aren't parsed yet. Click the \"{parse}\" button below to parse your ingredients into structured foods.",
"ingredients-not-parsed-description": "Sembla que els teus ingredients encara no s'han analitzat. Feu clic al botó \"{parse}\" de sota per transformar els vostres ingredients en aliments estructurats.",
"attach-images-hint": "Afegeix imatges arrossegant i deixant anar la imatge a l'editor",
"drop-image": "Deixa anar la imatge",
"enable-ingredient-amounts-to-use-this-feature": "Habilita les quantitats d'ingredients per a poder fer servir aquesta característica",
@@ -619,10 +627,10 @@
"create-recipe-from-an-image": "Crear una recepta a partir d'una imatge",
"create-recipe-from-an-image-description": "Crear una recepta pujant una imatge d'ella. Mealie intentarà extreure el text de la imatge mitjançant IA i crear-ne la recepta.",
"crop-and-rotate-the-image": "Retalla i rota la imatge, per tal que només el text sigui visible, i estigui orientat correctament.",
"create-from-images": "Create from Images",
"create-from-images": "Crear una recepta a partir d'una imatge",
"should-translate-description": "Tradueix la recepta a la meva llengua",
"please-wait-image-procesing": "Si us plau, esperi, la imatge s'està processant. Això pot tardar un temps.",
"please-wait-images-processing": "Please wait, the images are processing. This may take some time.",
"please-wait-images-processing": "Espereu, les imatges s'estan processant. Això pot trigar una estona.",
"bulk-url-import": "Importació d'URL en massa",
"debug-scraper": "Rastrejador de depuració",
"create-a-recipe-by-providing-the-name-all-recipes-must-have-unique-names": "Crea la recepta proporcionant-ne un nom. Totes les receptes han de tenir un nom únic.",
@@ -633,9 +641,12 @@
"scrape-recipe-suggest-bulk-importer": "Prova l'importador a granel",
"scrape-recipe-have-raw-html-or-json-data": "Teniu dades HTML o JSON pla?",
"scrape-recipe-you-can-import-from-raw-data-directly": "Podeu importar directament des de les dades planes",
"scrape-recipe-website-being-blocked": "S'està bloquejant el lloc web?",
"scrape-recipe-try-importing-raw-html-instead": "Prova important l'HTML directament.",
"import-original-keywords-as-tags": "Importa les paraules clau originals com a tags",
"import-original-categories": "Import original categories",
"stay-in-edit-mode": "Segueix en el mode d'edició",
"parse-recipe-ingredients-after-import": "Parse recipe ingredients after import",
"parse-recipe-ingredients-after-import": "Analitza els ingredients de la recepta després d'importar",
"import-from-zip": "Importa des d'un ZIP",
"import-from-zip-description": "Importa una sola recepta que ha estat importada d'una altra instància de Mealie.",
"import-from-html-or-json": "Importar des d'un HTML o JSON",
@@ -679,26 +690,26 @@
"no-unit": "Sense unitat",
"missing-unit": "Crear unitat que manca: {unit}",
"missing-food": "Crear menjar que manca: {food}",
"this-unit-could-not-be-parsed-automatically": "This unit could not be parsed automatically",
"this-food-could-not-be-parsed-automatically": "This food could not be parsed automatically",
"this-unit-could-not-be-parsed-automatically": "Aquesta unitat no s'ha pogut analitzar automàticament",
"this-food-could-not-be-parsed-automatically": "Aquest aliment no s'ha pogut analitzar automàticament",
"no-food": "Sense menjar",
"review-parsed-ingredients": "Review parsed ingredients",
"confidence-score": "Confidence Score",
"ingredient-parser-description": "Your ingredients have been successfully parsed. Please review the ingredients we're not sure about.",
"ingredient-parser-final-review-description": "Once all ingredients have been reviewed, you'll have one more chance to review all ingredients before applying the changes to your recipe.",
"add-text-as-alias-for-item": "Add \"{text}\" as alias for {item}",
"delete-item": "Delete Item"
"review-parsed-ingredients": "Revisió d'ingredients analitzats",
"confidence-score": "Puntuació de confiança",
"ingredient-parser-description": "Els teus ingredients s'han analitzat correctament. Si us plau, revisa els ingredients dels quals no estem segurs.",
"ingredient-parser-final-review-description": "Un cop revisats tots els ingredients, tindràs una oportunitat més de revisar tots els ingredients abans d'aplicar els canvis a la teva recepta.",
"add-text-as-alias-for-item": "Afegeix \"{text}\" com a àlies de {item}",
"delete-item": "Eliminar element"
},
"reset-servings-count": "Reiniciar racions servides",
"not-linked-ingredients": "Ingredients addicionals",
"upload-another-image": "Upload another image",
"upload-images": "Upload images",
"upload-more-images": "Upload more images",
"set-as-cover-image": "Set as recipe cover image",
"cover-image": "Cover image",
"include-linked-recipes": "Include Linked Recipes",
"include-linked-recipe-ingredients": "Include Linked Recipe Ingredients",
"toggle-recipe": "Toggle Recipe"
"upload-another-image": "Puja una altra imatge",
"upload-images": "Puja imatges",
"upload-more-images": "Puja més imatges",
"set-as-cover-image": "Estableix com a imatge de portada de recepta",
"cover-image": "Imatge de portada",
"include-linked-recipes": "Inclou les receptes enllaçades",
"include-linked-recipe-ingredients": "Inclou els ingredients de la recepta enllaçada",
"toggle-recipe": "Alternar recepta"
},
"recipe-finder": {
"recipe-finder": "Cercador de receptes",
@@ -736,7 +747,7 @@
"advanced": "Avançat",
"auto-search": "Cerca automàtica",
"no-results": "No s'han trobat resultats",
"type-to-search": "Type to search..."
"type-to-search": "Escriviu per cercar..."
},
"settings": {
"add-a-new-theme": "Afegiu un nou tema",
@@ -1075,8 +1086,8 @@
"forgot-password": "Contrasenya oblidada",
"forgot-password-text": "Introdueix siusplau la teva adreça de correu electrònic i t'enviarem un enllaç per restablir la teva contrassenya.",
"changes-reflected-immediately": "Els canvis en aquest usuari s'actualitzaran immediatament.",
"default-activity": "Default Activity",
"default-activity-hint": "Select which page you'd like to navigate to upon logging in from this device"
"default-activity": "Activitat per defecte",
"default-activity-hint": "Seleccioneu a quina pàgina voleu navegar en iniciar sessió des d'aquest dispositiu"
},
"language-dialog": {
"translated": "traduït",
@@ -1194,7 +1205,7 @@
"group-details": "Detalls del grup",
"group-details-description": "Abans de crear un compte heu de crear un grup. Al grup només hi serà vostè, però després podeu convidar d'altres. Els membres d'un grup poden compartir menús, llistes de la compra, receptes i molt més!",
"use-seed-data": "Afegiu dades predeterminades",
"use-seed-data-description": "Mealie ships with a collection of Foods, Units, and Labels that can be used to populate your group with helpful data for organizing your recipes. These are translated into the language you currently have selected. You can always add to or modify this data later.",
"use-seed-data-description": "Mealie disposa d'una col·lecció d'aliments, unitats i etiquetes que es poden utilitzar per omplir el vostre grup amb dades útils per organitzar les vostres receptes. Aquests es tradueixen a l'idioma que heu seleccionat actualment. Sempre podeu afegir o modificar aquestes dades més endavant.",
"account-details": "Detalls del compte"
},
"validation": {
@@ -1422,5 +1433,13 @@
"is-like": "és com",
"is-not-like": "no és com"
}
},
"validators": {
"required": "Aquest camp és obligatori",
"invalid-email": "El correu electrònic ha de ser vàlid",
"invalid-url": "La URL ha de ser vàlida",
"no-whitespace": "No es permeten espais en blanc",
"min-length": "Ha de tenir almenys {min} caràcters",
"max-length": "Ha de tenir com a màxim {max} caràcters"
}
}

View File

@@ -5,8 +5,8 @@
"api-docs": "Dokumentace API",
"api-port": "Port API",
"application-mode": "Režim aplikace",
"database-type": "Database Type",
"database-url": "Adresa URL databáze",
"database-type": "Typ databáze",
"database-url": "URL databáze",
"default-group": "Výchozí skupina",
"default-household": "Výchozí domácnost",
"demo": "Demo",
@@ -102,7 +102,7 @@
"duplicate": "Duplikovat",
"edit": "Upravit",
"enabled": "Povoleno",
"exception": "Vyjimka",
"exception": "Výjimka",
"failed-count": "Chyba: {count}",
"failure-uploading-file": "Nahrávání souboru se nezdařilo",
"favorites": "Oblíbené",
@@ -192,7 +192,7 @@
"a-name-is-required": "Název je povinný",
"delete-with-name": "Odstranit {name}",
"confirm-delete-generic-with-name": "Opravdu chcete smazat {name}?",
"confirm-delete-own-admin-account": "Prosím, vemte na vědomí, že se pokoušíte odstranit svůj vlastní účet správce! Tato akce nemůže být odvolána a trvale smaže váš účet?",
"confirm-delete-own-admin-account": "Prosím, vezměte na vědomí, že se pokoušíte odstranit svůj vlastní účet správce! Tato akce nemůže být odvolána a trvale smaže váš účet?",
"organizer": "Organizátor",
"transfer": "Přenos",
"copy": "Kopírovat",
@@ -212,6 +212,8 @@
"upload-file": "Nahrát soubor",
"created-on-date": "Vytvořeno dne: {0}",
"unsaved-changes": "Máte neuložené změny. Chcete je uložit před odchodem? Klikněte Okay pro uložení, Cancel pro smazání změn.",
"discard-changes": "Discard Changes",
"discard-changes-description": "You have unsaved changes. Are you sure you want to discard them?",
"clipboard-copy-failure": "Zkopírování do schránky se nezdařilo.",
"confirm-delete-generic-items": "Opravdu chcete smazat následující položky?",
"organizers": "Organizace",
@@ -292,7 +294,7 @@
"manage-households": "Spravovat domácnosti",
"admin-household-management": "Administrátorská správa domácnosti",
"admin-household-management-text": "Změny v této domácnosti budou okamžitě zohledněny.",
"household-id-value": "Id domácnosti: {0}",
"household-id-value": "ID domácnosti: {0}",
"private-household": "Soukromá domácnost",
"private-household-description": "Nastavení domácnosti na soukromou zakáže všechny možnosti veřejného zobrazení. To má přednost před individuálním nastavením veřejného zobrazení",
"lock-recipe-edits-from-other-households": "Uzamknout úpravy receptů z ostatních domácností",
@@ -342,6 +344,9 @@
"breakfast": "Snídaně",
"lunch": "Oběd",
"dinner": "Večeře",
"snack": "Svačina",
"drink": "Nápoje",
"dessert": "Dezerty",
"type-any": "Libovolné",
"day-any": "Libovolný",
"editor": "Editor",
@@ -364,7 +369,9 @@
"recipe-rules": "Pravidla receptu",
"applies-to-all-days": "Použije se na všechny dny",
"applies-on-days": "Platí pro {0}",
"meal-plan-settings": "Nastavení jídelníčku"
"meal-plan-settings": "Nastavení jídelníčku",
"add-all-to-list": "Add All to List",
"add-day-to-list": "Add Day to List"
},
"migration": {
"migration-data-removed": "Data z migrace byla smazána",
@@ -400,7 +407,7 @@
"title": "Recepty Tandoor"
},
"cookn": {
"description-long": "Mealie can import recipes from DVO Cook'n X3. Export a cookbook or menu in the \"Cook'n\" format, rename the export extension to .zip, then upload the .zip below.",
"description-long": "Mealie může importovat recept z DVO Cook'n X3. Exportujte kuchařku nebo menu ve formátu \"Cook'n\", přejmenujte rozšíření exportu na .zip, poté nahrajte .zip níže.",
"title": "DVO Cook'n X3"
},
"recipe-data-migrations": "Migrace dat receptů",
@@ -432,7 +439,7 @@
"error-details": "Pouze webové stránky obsahující Id+json nebo mikrodata mohou být přeneseny do Mealie. Většina hlavních webových stránek s recepty tuto datovou strukturu podporuje. Pokud vaše stránka nemůže být převedena ale json data jsou uvedena v logu, prosím nahlašte nám chybu do githubu a přiložte URL a data.",
"error-title": "Vypadá to, že se nám nic nepodařilo najít",
"from-url": "Přenést recept",
"github-issues": "Hlášení chyb na Githubu",
"github-issues": "Hlášení chyb na GitHubu",
"google-ld-json-info": "Google ld+json Info",
"must-be-a-valid-url": "Musí být validní URL",
"paste-in-your-recipe-data-each-line-will-be-treated-as-an-item-in-a-list": "Vložte data receptu. Každý řádek bude považován za položku v seznamu",
@@ -442,6 +449,7 @@
"upload-a-recipe": "Nahrát recept",
"upload-individual-zip-file": "Nahrát individuální .zip soubor exportovaný z jiné instance Mealie.",
"url-form-hint": "Zkopírujte a vložte odkaz z vaší oblíbené stránky s recepty",
"copy-and-paste-the-source-url-of-your-data-optional": "Zkopírujte a vložte zdrojovou adresu URL vašich dat (volitelné)",
"view-scraped-data": "Zobrazit scrapovaná data",
"trim-whitespace-description": "Oříznout počáteční a koncové mezery stejně jako prázdné řádky",
"trim-prefix-description": "Oříznout první znak z každé řádky",
@@ -449,8 +457,8 @@
"import-by-url": "Importovat recept podle URL",
"create-manually": "Vytvořit recept ručně",
"make-recipe-image": "Nastavit jako obrázek receptu",
"add-food": "Add Food",
"add-recipe": "Add Recipe"
"add-food": "Přidat jídlo",
"add-recipe": "Přidat recept"
},
"page": {
"404-page-not-found": "404 Stránka nebyla nalezena",
@@ -499,7 +507,7 @@
"insert-below": "Vložit pod",
"instructions": "Postup",
"key-name-required": "Je vyžadován název klíče",
"landscape-view-coming-soon": "Landscape View (Coming Soon)",
"landscape-view-coming-soon": "Horizontální orientace (Coming Soon)",
"milligrams": "miligramy",
"new-key-name": "Nový název klíče",
"no-white-space-allowed": "Prázdná místa nejsou povolena",
@@ -517,9 +525,9 @@
"recipe-deleted": "Recept smazán",
"recipe-image": "Obrázek receptu",
"recipe-image-updated": "Obrázek receptu aktualizován",
"delete-image": "Delete Recipe Image",
"delete-image-confirmation": "Are you sure you want to delete this recipe image?",
"recipe-image-deleted": "Recipe image deleted",
"delete-image": "Smazat recept",
"delete-image-confirmation": "Opravdu chcete smazat tento recept?",
"recipe-image-deleted": "Recept smazán",
"recipe-name": "Název receptu",
"recipe-settings": "Nastavení receptu",
"recipe-update-failed": "Aktualizace receptu se nezdařila",
@@ -546,7 +554,7 @@
"join-the-conversation": "Připojit se ke konverzaci",
"add-recipe-to-mealplan": "Přidat recept do jídelníčku",
"entry-type": "Typ položky",
"date-format-hint": "Formát data MM/DD/YYYY",
"date-format-hint": "Formát data MM/DD/RRRR",
"date-format-hint-yyyy-mm-dd": "Formát RRRR-MM-DD",
"add-to-list": "Přidat na seznam",
"add-to-plan": "Přidat do jídelníčku",
@@ -565,7 +573,7 @@
"choose-unit": "Vybrat jednotku",
"press-enter-to-create": "Stiskněte enter pro vytvoření",
"choose-food": "Zvolte jídlo",
"choose-recipe": "Choose Recipe",
"choose-recipe": "Vybrat recept",
"notes": "Poznámky",
"toggle-section": "Přidat/odebrat název sekce",
"see-original-text": "Zobrazit původní text",
@@ -593,7 +601,7 @@
"made-this": "Toto jsem uvařil",
"how-did-it-turn-out": "Jak to dopadlo?",
"user-made-this": "{user} udělal toto",
"made-for-recipe": "Made for {recipe}",
"made-for-recipe": "Vytvořeno pro {recipe}",
"added-to-timeline": "Přidáno na časovou osu",
"failed-to-add-to-timeline": "Přidání na časovou osu selhalo",
"failed-to-update-recipe": "Aktualizace receptu selhala",
@@ -628,14 +636,17 @@
"create-a-recipe-by-providing-the-name-all-recipes-must-have-unique-names": "Vytvořte recept zadáním názvu. Všechny recepty musí mít jedinečná jména.",
"new-recipe-names-must-be-unique": "Názvy receptů musí být jedinečné",
"scrape-recipe": "Zpracovat recept",
"scrape-recipe-description": "Zpracovat recept na url. Uveďte adresu url pro str8nku, kterou chcete zpracovat a Mealie se pokusí zpracovat recept z tohoto webu a přidat jej do vaší sbírky.",
"scrape-recipe-description": "Zpracovat recept z URL. Uveďte adresu URL pro stránku, kterou chcete zpracovat a Mealie se pokusí zpracovat recept z tohoto webu a přidat jej do vaší sbírky.",
"scrape-recipe-have-a-lot-of-recipes": "Máte spoustu receptů, které chcete zpracovat najednou?",
"scrape-recipe-suggest-bulk-importer": "Vyzkoušejte hromadný import",
"scrape-recipe-have-raw-html-or-json-data": "Máte surová data HTML nebo JSON?",
"scrape-recipe-you-can-import-from-raw-data-directly": "Můžete importovat přímo ze surových dat",
"scrape-recipe-website-being-blocked": "Webové stránky jsou blokovány?",
"scrape-recipe-try-importing-raw-html-instead": "Zkuste namísto toho importovat raw HTML.",
"import-original-keywords-as-tags": "Importovat původní klíčová slova jako štítky",
"import-original-categories": "Import original categories",
"stay-in-edit-mode": "Zůstat v režimu úprav",
"parse-recipe-ingredients-after-import": "Parse recipe ingredients after import",
"parse-recipe-ingredients-after-import": "Po importu analyzovat ingredience receptu",
"import-from-zip": "Importovat ze zipu",
"import-from-zip-description": "Importovat jeden recept, který byl exportován z jiné instance Mealie.",
"import-from-html-or-json": "Importovat z HTML nebo JSON",
@@ -682,12 +693,12 @@
"this-unit-could-not-be-parsed-automatically": "Tuto jednotku nelze analyzovat automaticky",
"this-food-could-not-be-parsed-automatically": "Toto jídlo nelze analyzovat automaticky",
"no-food": "Žádné jídlo",
"review-parsed-ingredients": "Review parsed ingredients",
"confidence-score": "Confidence Score",
"ingredient-parser-description": "Your ingredients have been successfully parsed. Please review the ingredients we're not sure about.",
"ingredient-parser-final-review-description": "Once all ingredients have been reviewed, you'll have one more chance to review all ingredients before applying the changes to your recipe.",
"add-text-as-alias-for-item": "Add \"{text}\" as alias for {item}",
"delete-item": "Delete Item"
"review-parsed-ingredients": "Zkontrolovat analyzované ingredience",
"confidence-score": "Skóre spolehlivosti",
"ingredient-parser-description": "Vaše suroviny byly úspěšně analyzovány. Prosím zkontrolujte ingredience, o kterých si nejsme jisti.",
"ingredient-parser-final-review-description": "Jakmile budou všechny ingredience zkontrolovány, budete mít ještě jednu šanci zkontrolovat všechny ingredience před použitím změn ve vašem receptu.",
"add-text-as-alias-for-item": "Přidat \"{text}\" jako alias pro {item}",
"delete-item": "Odstranit položku"
},
"reset-servings-count": "Resetovat počet porcí",
"not-linked-ingredients": "Další ingredience",
@@ -696,9 +707,9 @@
"upload-more-images": "Nahrát více obrázků",
"set-as-cover-image": "Nastavit recept jako úvodní obrázek",
"cover-image": "Úvodní obrázek",
"include-linked-recipes": "Include Linked Recipes",
"include-linked-recipe-ingredients": "Include Linked Recipe Ingredients",
"toggle-recipe": "Toggle Recipe"
"include-linked-recipes": "Zahrnout připojené recepty",
"include-linked-recipe-ingredients": "Zahrnout připojené ingredience",
"toggle-recipe": "Přepnout na recept/jídlo"
},
"recipe-finder": {
"recipe-finder": "Vyhledávač receptů",
@@ -736,7 +747,7 @@
"advanced": "Pokročilé",
"auto-search": "Automatické vyhledávání",
"no-results": "Nebyly nalezeny žádné výsledky",
"type-to-search": "Type to search..."
"type-to-search": "Zadejte hledaný výraz..."
},
"settings": {
"add-a-new-theme": "Přidat nový motiv",
@@ -748,7 +759,7 @@
"restore-success": "Obnovení bylo úspěšné",
"restore-fail": "Obnovení se nezdařilo. Pro více informací zkontrolujte protokoly serveru",
"backup-tag": "Štítek zálohy",
"create-heading": "Create a Backup",
"create-heading": "Vytvořit zálohy",
"delete-backup": "Smazat zálohu",
"error-creating-backup-see-log-file": "Chyba při vytváření zálohy. Viz log soubor",
"full-backup": "Úplná záloha",
@@ -1036,7 +1047,7 @@
"users-header": "UŽIVATELÉ",
"users": "Uživatelé",
"user-not-found": "Uživatel nebyl nalezen",
"webhook-time": "Čas Webhooku",
"webhook-time": "Čas webhooku",
"webhooks-enabled": "Povolené webhooky",
"you-are-not-allowed-to-create-a-user": "Nemáte oprávnění k vytvoření uživatele",
"you-are-not-allowed-to-delete-this-user": "Nemáte oprávnění k odstranění tohoto uživatele",
@@ -1062,7 +1073,7 @@
"user-details": "Detaily uživatele",
"user-name": "Uživatelské jméno",
"authentication-method": "Metoda ověření",
"authentication-method-hint": "This specifies how a user will authenticate with Mealie. If you're not sure, choose 'Mealie",
"authentication-method-hint": "Toto specifikuje, jak se uživatel přihlásí do Mealie. Pokud si nejste jistí, vyberte 'Mealie'",
"permissions": "Oprávnění",
"administrator": "Správce",
"user-can-invite-other-to-group": "Uživatel může pozvat ostatní do skupiny",
@@ -1071,12 +1082,12 @@
"user-can-organize-group-data": "Uživatel může organizovat data skupiny",
"enable-advanced-features": "Povolit pokročilé funkce",
"it-looks-like-this-is-your-first-time-logging-in": "Vypadá to, že toto je vaše první přihlášení.",
"dont-want-to-see-this-anymore-be-sure-to-change-your-email": "Už to nechcete vidět? Nezapomeňte si změnit svůj e-mail v uživatelském nastavení!",
"dont-want-to-see-this-anymore-be-sure-to-change-your-email": "Už toto nechcete vidět? Nezapomeňte si změnit svůj e-mail v uživatelském nastavení!",
"forgot-password": "Zapomenuté heslo",
"forgot-password-text": "Zadejte prosím svou e-mailovou adresu a my vám zašleme odkaz pro obnovení hesla.",
"changes-reflected-immediately": "Změny tohoto uživatele budou okamžitě zohledněny.",
"default-activity": "Default Activity",
"default-activity-hint": "Select which page you'd like to navigate to upon logging in from this device"
"default-activity": "Výchozí aktivita",
"default-activity-hint": "Vyberte stránku, na kterou chcete přejít po přihlášení z tohoto zařízení"
},
"language-dialog": {
"translated": "přeloženo",
@@ -1089,7 +1100,7 @@
"foods": {
"merge-dialog-text": "Zkombinování zvolených potravin způsobí smazání zdrojové potraviny a veškeré odkazy na ni budou přesměrovány do cílové potraviny.",
"merge-food-example": "Sloučení {food1} do {food2}",
"seed-dialog-text": "Naplňte databázi potravinami z vašeho jazyka. Tímto vytvoříte přes 200 běžných potravin, které můžete použít k organizaci vaší databáze. Potravny jsou přeloženy skrze komunitní úsilí.",
"seed-dialog-text": "Naplňte databázi potravinami z vašeho jazyka. Tímto vytvoříte přes 200 běžných potravin, které můžete použít k organizaci vaší databáze. Potraviny jsou přeloženy skrze komunitní úsilí.",
"seed-dialog-warning": "Již v databázi máte nějaká data. Tato akce neodstraní duplicity, budete je muset odstranit ručně.",
"combine-food": "Kombinovat jídlo",
"source-food": "Zdrojové jídlo",
@@ -1302,7 +1313,7 @@
},
"ingredients-natural-language-processor": "Zpracování přirozeného jazyka přísad",
"ingredients-natural-language-processor-explanation": "Mealie používá podmíněná náhodná pole (CRF) pro rozbor a zpracování ingrediencí. Model používaný pro ingredience je založen na datovém souboru s více než 100 000 ingrediencemi sestaveného New York Times. Vzhledem k tomu, že model je natrénován pouze v angličtině, můžete mít při používání variabilní výsledky. Tato stránka slouží jako hřiště pro testování tohoto modelu.",
"ingredients-natural-language-processor-explanation-2": "Není to dokonalé, ale obecně to přináší skvělé výsledky a je dobrým výchozím bodem pro ruční zpracování ingrediencí do jednotlivých polí. Alternativně můžete také použít procesor \"Brute\", který používá metodu porovnávání vzorců pro idenfikaci ingrediencí.",
"ingredients-natural-language-processor-explanation-2": "Není to dokonalé, ale obecně to přináší skvělé výsledky a je dobrým výchozím bodem pro ruční zpracování ingrediencí do jednotlivých polí. Alternativně můžete také použít procesor \"Brute\", který používá metodu porovnávání vzorců pro identifikaci ingrediencí.",
"nlp": "ZPJ",
"brute": "Brute",
"openai": "OpenAI",
@@ -1374,9 +1385,9 @@
"show-advanced-description": "Zobrazit pokročilé funkce (API klíče, Webhooky a správa dat)",
"back-to-profile": "Zpět na profil",
"looking-for-privacy-settings": "Hledáte nastavení ochrany soukromí?",
"manage-your-api-tokens": "Správa API tokenů",
"manage-user-profile": "Správa uživatelského profilu",
"manage-cookbooks": "Správa kuchařek",
"manage-your-api-tokens": "Spravovat API tokeny",
"manage-user-profile": "Spravovat uživatelský profil",
"manage-cookbooks": "Spravovat kuchařky",
"manage-members": "Spravovat členy",
"manage-webhooks": "Spravovat webhooky",
"manage-notifiers": "Spravovat oznámení",
@@ -1422,5 +1433,13 @@
"is-like": "je jako",
"is-not-like": "není jako"
}
},
"validators": {
"required": "Toto pole je povinné",
"invalid-email": "E-mail musí být platný",
"invalid-url": "Musí být platná URL adresa",
"no-whitespace": "Mezery nejsou povoleny",
"min-length": "Musí být alespoň {min} znaků",
"max-length": "Musí být nejvíce {max} znaků"
}
}

View File

@@ -2,38 +2,38 @@
"about": {
"about": "Om",
"about-mealie": "Om Mealie",
"api-docs": "API dokumentation",
"api-port": "API port",
"api-docs": "API-dokumentation",
"api-port": "API-port",
"application-mode": "Applikationstilstand",
"database-type": "Database-type",
"database-url": "Database-url",
"database-url": "Database-URL",
"default-group": "Standardgruppe",
"default-household": "Standard Husstand",
"default-household": "Standardhusstand",
"demo": "Demo",
"demo-status": "Demo status",
"demo-status": "Demo-status",
"development": "Udvikling",
"docs": "Dokumenter",
"docs": "Dokumentation",
"download-log": "Download log",
"download-recipe-json": "Senest hentede JSON",
"github": "GitHub",
"log-lines": "Log-linjer",
"log-lines": "Loglinjer",
"not-demo": "Ikke demo",
"portfolio": "Portefølje",
"production": "Produktion",
"support": "Hjælp",
"version": "Version",
"unknown-version": "ukendt",
"sponsor": "Sponsor"
"sponsor": "Sponsorér"
},
"asset": {
"assets": "Ekstramateriale",
"assets": "Aktiver",
"code": "Kode",
"file": "Fil",
"image": "Billede",
"new-asset": "Nyt aktiv",
"pdf": "PDF",
"recipe": "Opskrift",
"show-assets": "Vis ekstramateriale",
"show-assets": "Vis aktiver",
"error-submitting-form": "Fejl ved indsendelse af data"
},
"category": {
@@ -51,11 +51,11 @@
"category": "Kategori"
},
"events": {
"apprise-url": "Apprise URL",
"apprise-url": "Apprise-URL",
"database": "Database",
"delete-event": "Slet hændelse",
"delete-event": "Slet begivenhed",
"event-delete-confirmation": "Er du sikker på, at du vil slette denne begivenhed?",
"event-deleted": "Hændelse slettet",
"event-deleted": "Begivenhed slettet",
"event-updated": "Hændelse opdateret",
"new-notification-form-description": "Mealie bruger Apprise biblioteket for at generere notifikationer. De giver mange muligheder for notifikationer til tjenester. Kig i deres wiki for en gennemgående guide til, hvordan en URL oprettes i din situation. Hvis muligt, kan valget af din type af notifikation omfatte flere ekstrafunktioner.",
"new-version": "Ny opdatering er tilgængelig!",
@@ -212,6 +212,8 @@
"upload-file": "Upload fil",
"created-on-date": "Oprettet den: {0}",
"unsaved-changes": "Du har ændringer som ikke er gemt. Vil du gemme før du forlader? Vælg \"Okay\" for at gemme, eller \"Annullér\" for at kassere ændringer.",
"discard-changes": "Discard Changes",
"discard-changes-description": "You have unsaved changes. Are you sure you want to discard them?",
"clipboard-copy-failure": "Kopiering til udklipsholderen mislykkedes.",
"confirm-delete-generic-items": "Er du sikker på at du ønsker at slette de valgte emner?",
"organizers": "Organisatorer",
@@ -342,6 +344,9 @@
"breakfast": "Morgenmad",
"lunch": "Frokost",
"dinner": "Aftensmad",
"snack": "Snack",
"drink": "Drik",
"dessert": "Dessert",
"type-any": "Alle",
"day-any": "Alle",
"editor": "Redigeringsværktøj",
@@ -364,7 +369,9 @@
"recipe-rules": "Opskriftsregler",
"applies-to-all-days": "Gælder for alle dage",
"applies-on-days": "Gælder for {0}e",
"meal-plan-settings": "Indstillinger for madplanlægning"
"meal-plan-settings": "Indstillinger for madplanlægning",
"add-all-to-list": "Add All to List",
"add-day-to-list": "Add Day to List"
},
"migration": {
"migration-data-removed": "Migreringsdata fjernet",
@@ -442,6 +449,7 @@
"upload-a-recipe": "Upload en opskrift",
"upload-individual-zip-file": "Upload en individuel .zip-fil, eksporteret fra en anden Mealie-instans.",
"url-form-hint": "Kopiér og indsæt et link fra din foretrukne opskrifts hjemmeside",
"copy-and-paste-the-source-url-of-your-data-optional": "Kopiér og indsæt kilde-URL'en til dine data (valgfrit)",
"view-scraped-data": "Vis dataudtræk",
"trim-whitespace-description": "Fjern indledende og efterfølgende mellemrum samt blanke linjer",
"trim-prefix-description": "Beskær første tegn fra hver linje",
@@ -558,9 +566,9 @@
"failed-to-add-recipes-to-list": "Kunne ikke tilføje opskrift til listen",
"failed-to-add-recipe-to-mealplan": "Kunne ikke tilføje opskrift til madplanen",
"failed-to-add-to-list": "Kunne ikke tilføje opskrift til listen",
"yield": "Portioner",
"yields-amount-with-text": "{amount} {text}",
"yield-text": "Portionsenhed (eks. pers./stk.)",
"yield": "Mængde",
"yields-amount-with-text": "Mængde {amount} {text}",
"yield-text": "Hvor mange enheder giver opskriften?",
"quantity": "Antal",
"choose-unit": "Vælg enhed",
"press-enter-to-create": "Tryk enter for at oprette",
@@ -633,7 +641,10 @@
"scrape-recipe-suggest-bulk-importer": "Prøv masseimport",
"scrape-recipe-have-raw-html-or-json-data": "Har rå HTML- eller JSON-data?",
"scrape-recipe-you-can-import-from-raw-data-directly": "Du kan importere direkte fra rå data",
"scrape-recipe-website-being-blocked": "Bliver hjemmesiden blokeret?",
"scrape-recipe-try-importing-raw-html-instead": "Forsøg at importere den rå HTML i stedet.",
"import-original-keywords-as-tags": "Importér originale nøgleord som mærker",
"import-original-categories": "Import original categories",
"stay-in-edit-mode": "Bliv i redigeringstilstand",
"parse-recipe-ingredients-after-import": "Fortolk opskrift ingredienser efter import",
"import-from-zip": "Importer fra zip-fil",
@@ -653,14 +664,14 @@
"bulk-import-process-has-failed": "Der opstod en fejl ved import af opskrifter",
"report-deletion-failed": "Sletning af rapport mislykkedes",
"recipe-debugger": "Fejlsøgning af opskrifter",
"recipe-debugger-description": "Indsæt URL'en på hjemmesiden, der indeholder den opskrift, du vil fejlsøge. URL-adressen vil blive læst og resultaterne vil blive vist. Hvis ingen data bliver vist, er indhentning af opskrifter fra hjemmesiden endnu ikke understøttet af Mealie.",
"recipe-debugger-description": "Indsæt URL'en på hjemmesiden, der indeholder den opskrift, du vil fejlsøge. URL-adressen vil blive læst og resultaterne vil blive vist. Hvis ingen data bliver vist, er indhentning af opskrifter fra hjemmesiden endnu ikke understøttet af Mealie.",
"use-openai": "Brug OpenAI",
"recipe-debugger-use-openai-description": "Brug OpenAI til at fortolke resultaterne i stedet for at stole på scraper biblioteket. Når du opretter en opskrift via URL, gøres dette automatisk, hvis skraberbiblioteket fejler, men du kan teste det manuelt her.",
"recipe-debugger-use-openai-description": "Brug OpenAI til at fortolke resultaterne i stedet for at stole på scraperbiblioteket. Når du opretter en opskrift via URL, gøres dette automatisk, hvis skraberbiblioteket fejler, men du kan teste det manuelt her.",
"debug": "Fejlsøgning",
"tree-view": "Træ visning",
"recipe-servings": "Opskrift Servinger",
"recipe-yield": "Udbytte af opskrift",
"recipe-yield-text": "Portioner",
"recipe-yield-text": "Mængde",
"unit": "Enhed",
"upload-image": "Upload billede",
"screen-awake": "Hold skærmen tændt",
@@ -1302,7 +1313,7 @@
},
"ingredients-natural-language-processor": "Ingredienser Naturlig Sprogprocessor",
"ingredients-natural-language-processor-explanation": "Mealie bruger Conditional Random Fields felter (CRF'er) til berarbejdning af ingredienser. Den model, der anvendes til ingredienser er baseret ud fra et datasæt på over 100.000 ingredienser fra et datasæt udarbejdet af New York Times. Bemærk, at da modellen kun er trænet på engelsk, kan du have forskellige resultater, når du bruger modellen på andre sprog. På denne side kan du teste modellen.",
"ingredients-natural-language-processor-explanation-2": "Det er ikke perfekt, men giver generelt gode resultater og er et godt udgangspunkt for manuel redigering af ingredienser i individuelle felter. Alternativt kan du også bruge \"Brute\" metoden, der bruger en mønstermatchende teknik til at identificere ingredienser.",
"ingredients-natural-language-processor-explanation-2": "Det er ikke perfekt, men giver generelt gode resultater og er et godt udgangspunkt for manuel redigering af ingredienser i individuelle felter. Alternativt kan du også bruge \"Brute\"-metoden, der bruger en mønstermatchende teknik til at identificere ingredienser.",
"nlp": "NLP",
"brute": "Brute",
"openai": "OpenAI",
@@ -1422,5 +1433,13 @@
"is-like": "er ligesom",
"is-not-like": "er ikke som"
}
},
"validators": {
"required": "Dette felt er påkrævet",
"invalid-email": "E-mailen skal være gyldig",
"invalid-url": "URL'en skal være gyldig",
"no-whitespace": "Mellemrum er ikke tilladt",
"min-length": "Der skal mindst være {min} tegn",
"max-length": "Der må højst være {max} tegn"
}
}

View File

@@ -51,7 +51,7 @@
"category": "Kategorie"
},
"events": {
"apprise-url": "Apprise-URL",
"apprise-url": "Apprise URL",
"database": "Datenbank",
"delete-event": "Ereignis löschen",
"event-delete-confirmation": "Bist du dir sicher, dass du dieses Ereignis löschen möchtest?",
@@ -69,7 +69,7 @@
"new-notification": "Neue Benachrichtigung",
"event-notifiers": "Ereignis-Benachrichtigungen",
"apprise-url-skipped-if-blank": "Apprise-URL (wird übersprungen, wenn leer)",
"apprise-url-is-left-intentionally-blank": "Da Apprise-URLs normalerweise sensible Informationen enthalten, wird dieses Feld während der Bearbeitung absichtlich leer gelassen. Wenn Sie die URL aktualisieren möchten, geben Sie hier die neue ein. Andernfalls lassen Sie diese leer, um die aktuelle URL zu behalten.",
"apprise-url-is-left-intentionally-blank": "Da Apprise-URLs normalerweise sensible Informationen enthalten, wird dieses Feld während der Bearbeitung absichtlich leer gelassen. Wenn du die URL aktualisieren möchtest, gib hier die neue ein. Andernfalls lasse diese leer, um die aktuelle URL zu behalten.",
"enable-notifier": "Benachrichtigen aktivieren",
"what-events": "Welche Ereignisse soll diese Benachrichtigung abonnieren?",
"user-events": "Benutzer-Ereignisse",
@@ -191,7 +191,7 @@
"menu": "Menü",
"a-name-is-required": "Ein Name wird benötigt",
"delete-with-name": "{name} löschen",
"confirm-delete-generic-with-name": "Bist du dir sicher, dass du dies löschen möchtest?",
"confirm-delete-generic-with-name": "Bist du dir sicher, dass du {name} löschen möchtest?",
"confirm-delete-own-admin-account": "Bitte beachte, dass du versuchst, dein eigenes Administrator-Konto zu löschen! Diese Aktion kann nicht rückgängig gemacht werden und wird dein Konto dauerhaft löschen?",
"organizer": "Organisator",
"transfer": "Übertragen",
@@ -212,6 +212,8 @@
"upload-file": "Datei hochladen",
"created-on-date": "Erstellt am: {0}",
"unsaved-changes": "Du hast ungespeicherte Änderungen. Möchtest du vor dem Verlassen speichern? OK um zu speichern, Cancel um Änderungen zu verwerfen.",
"discard-changes": "Discard Changes",
"discard-changes-description": "You have unsaved changes. Are you sure you want to discard them?",
"clipboard-copy-failure": "Fehler beim Kopieren in die Zwischenablage.",
"confirm-delete-generic-items": "Bist du dir sicher, dass du die folgenden Einträge löschen möchtest?",
"organizers": "Organisieren",
@@ -279,7 +281,7 @@
"admin-group-management-text": "Änderungen an dieser Gruppe sind sofort wirksam.",
"group-id-value": "Gruppen ID: {0}",
"total-households": "Haushalte insgesamt",
"you-must-select-a-group-before-selecting-a-household": "Sie müssen eine Gruppe auswählen, bevor Sie einen Haushalt auswählen"
"you-must-select-a-group-before-selecting-a-household": "Du musst eine Gruppe auswählen, bevor du einen Haushalt auswählst"
},
"household": {
"household": "Haushalt",
@@ -342,6 +344,9 @@
"breakfast": "Frühstück",
"lunch": "Mittagessen",
"dinner": "Abendessen",
"snack": "Zwischenmahlzeit ",
"drink": "Getränk",
"dessert": "Nachspeise",
"type-any": "Alle",
"day-any": "Alle",
"editor": "Bearbeiten",
@@ -364,7 +369,9 @@
"recipe-rules": "Rezeptregeln",
"applies-to-all-days": "Gilt an allen Tagen",
"applies-on-days": "Gilt {0}s",
"meal-plan-settings": "Essensplan Einstellungen"
"meal-plan-settings": "Essensplan Einstellungen",
"add-all-to-list": "Add All to List",
"add-day-to-list": "Add Day to List"
},
"migration": {
"migration-data-removed": "Migrationsdaten entfernt",
@@ -400,7 +407,7 @@
"title": "Tandoor Rezepte"
},
"cookn": {
"description-long": "Mealie kann Rezepte von DVO Cook'n X3 importieren. Exportieren Sie ein Kochbuch oder ein Menü im \"Cook'n\"-Format, benennen Sie die Export-Erweiterung in .zip um, dann laden Sie die .zip unten hoch.",
"description-long": "Mealie kann Rezepte von DVO Cook'n X3 importieren. Exportiere ein Kochbuch oder ein Menü im \"Cook'n\"-Format, benenne die Export-Erweiterung in .zip um, dann lade die .zip unten hoch.",
"title": "DVO Cook'n X3"
},
"recipe-data-migrations": "Rezeptdatenmigration",
@@ -442,6 +449,7 @@
"upload-a-recipe": "Rezept hochladen",
"upload-individual-zip-file": "Lade eine individuelle .zip-Datei hoch, die von einer anderen Mealie-Instanz exportiert wird.",
"url-form-hint": "Kopiere einen Link von deiner Lieblingsrezept-Website und füge ihn ein",
"copy-and-paste-the-source-url-of-your-data-optional": "Kopiere und füge die Quell-URL deiner Daten ein (optional)",
"view-scraped-data": "Gesammelte Daten anzeigen",
"trim-whitespace-description": "Leerzeichen am Anfang und Ende sowie leere Zeilen entfernen",
"trim-prefix-description": "Erste Zeichen aus jeder Zeile entfernen",
@@ -518,7 +526,7 @@
"recipe-image": "Rezeptbild",
"recipe-image-updated": "Rezeptbild aktualisiert",
"delete-image": "Rezeptbild löschen",
"delete-image-confirmation": "Bist du dir sicher, dass du dieses Rezept löschen möchtest?",
"delete-image-confirmation": "Bist du dir sicher, dass du dieses Rezeptbild löschen möchtest?",
"recipe-image-deleted": "Rezeptbild gelöscht",
"recipe-name": "Rezeptname",
"recipe-settings": "Rezepteinstellungen",
@@ -565,7 +573,7 @@
"choose-unit": "Einheit wählen",
"press-enter-to-create": "Zum Erstellen Eingabetaste drücken",
"choose-food": "Lebensmittel wählen",
"choose-recipe": "Choose Recipe",
"choose-recipe": "Rezept wählen",
"notes": "Notizen",
"toggle-section": "Überschrift ein-/ausblenden",
"see-original-text": "Originaltext anzeigen",
@@ -633,7 +641,10 @@
"scrape-recipe-suggest-bulk-importer": "Probiere den Massenimporter aus",
"scrape-recipe-have-raw-html-or-json-data": "Hast du Roh-HTML oder JSON Daten?",
"scrape-recipe-you-can-import-from-raw-data-directly": "Du kannst direkt von Rohdaten importieren",
"scrape-recipe-website-being-blocked": "Die Website wird blockiert?",
"scrape-recipe-try-importing-raw-html-instead": "Versuche stattdessen das reine HTML zu importieren.",
"import-original-keywords-as-tags": "Importiere ursprüngliche Stichwörter als Schlagwörter",
"import-original-categories": "Import original categories",
"stay-in-edit-mode": "Im Bearbeitungsmodus bleiben",
"parse-recipe-ingredients-after-import": "Zutaten nach dem Import parsen",
"import-from-zip": "Von Zip importieren",
@@ -736,7 +747,7 @@
"advanced": "Erweitert",
"auto-search": "Automatische Suche",
"no-results": "Keine Ergebnisse gefunden",
"type-to-search": "Type to search..."
"type-to-search": "Suchbegriff eingeben..."
},
"settings": {
"add-a-new-theme": "Neues Thema hinzufügen",
@@ -759,7 +770,7 @@
"backup-restore": "Wiederherstellen aus Sicherung",
"back-restore-description": "Das Wiederherstellen dieser Sicherung wird alle vorhandenen Daten in deiner Datenbank und im Datenverzeichnis überschreiben und durch den Inhalt dieser Sicherung ersetzen. {cannot-be-undone} Wenn die Wiederherstellung erfolgreich war, wirst du abgemeldet.",
"cannot-be-undone": "Diese Aktion kann nicht rückgängig gemacht werden - verwende sie mit Vorsicht.",
"postgresql-note": "Falls Sie PostgreSQL verwenden, überprüfen Sie bitte den {backup-restore-process} vor dem Wiederherstellen.",
"postgresql-note": "Falls du PostgreSQL verwendest, überprüfe bitte den {backup-restore-process} vor dem Wiederherstellen.",
"backup-restore-process-in-the-documentation": "Sichern- und Wiederherstellen-Prozess in der Dokumentation",
"irreversible-acknowledgment": "Ich verstehe, dass diese Maßnahme unumkehrbar und destruktiv ist und Datenverlust verursachen kann",
"restore-backup": "Sicherung wiederherstellen"
@@ -1076,7 +1087,7 @@
"forgot-password-text": "Bitte gib Deine E-Mail-Adresse ein. Wir werden Dir eine E-Mail zusenden, damit Du Dein Passwort zurücksetzen kannst.",
"changes-reflected-immediately": "Änderungen an diesem Benutzer sind sofort wirksam.",
"default-activity": "Standardaktivität",
"default-activity-hint": "Wählen Sie die Seite, auf die Sie navigieren möchten, wenn Sie sich von diesem Gerät aus anmelden"
"default-activity-hint": "Wähle die Seite, auf die du navigieren möchtest, wenn du dich von diesem Gerät aus anmeldest"
},
"language-dialog": {
"translated": "übersetzt",
@@ -1090,7 +1101,7 @@
"merge-dialog-text": "Zusammenführen der ausgewählten Lebensmittel führt diese zusammen in ein einzelnes Lebensmittel. Die Ausgangslebensmittel werden gelöscht und alle Verweise werden auf das zusammengeführte Lebensmittel angepasst.",
"merge-food-example": "{food1} wird zu {food2} zusammengeführt",
"seed-dialog-text": "Füllt die Datenbank mit Lebensmitteln basierend auf deiner Landessprache. Hierdurch werden mehr als 200 gängige Lebensmittel eingetragen, die verwendet werden können, um die Datenbank zu organisieren. Die Lebensmittel werden von der Community übersetzt.",
"seed-dialog-warning": "Sie haben bereits einige Einträge in der Datenbank. Diese Aktion wird Doppelungen nicht berücksichtigen. Sie müssen diese selbst beheben.",
"seed-dialog-warning": "Du hast bereits einige Einträge in der Datenbank. Diese Aktion wird Doppelungen nicht berücksichtigen. Du musst diese selbst beheben.",
"combine-food": "Lebensmittel zusammenführen",
"source-food": "Quell-Lebensmittel",
"target-food": "Ziel-Lebensmittel",
@@ -1129,7 +1140,7 @@
"seed-dialog-text": "Füllt die Datenbank mit gängigen Kategorien basierend auf deiner Sprache.",
"edit-label": "Kategorie bearbeiten",
"new-label": "Neue Kategorie",
"labels": "Kategorien",
"labels": "Bezeichnungen",
"assign-label": "Kategorie zuweisen"
},
"recipes": {
@@ -1422,5 +1433,13 @@
"is-like": "ist wie",
"is-not-like": "ist nicht wie"
}
},
"validators": {
"required": "Dieses Feld ist erforderlich",
"invalid-email": "E-Mail muss gültig sein",
"invalid-url": "Muss eine gültige URL sein",
"no-whitespace": "Kein Leerzeichen erlaubt",
"min-length": "Muss mindestens {min} Zeichen haben",
"max-length": "Darf mindestens {max} Zeichen haben"
}
}

View File

@@ -212,6 +212,8 @@
"upload-file": "Μεταφόρτωση αρχείου",
"created-on-date": "Δημιουργήθηκε στις: {0}",
"unsaved-changes": "Εχετε μη αποθηκευμένες αλλαγές. Θέλετε να κάνετε αποθήκευση πριν από την αποχώρηση; Εντάξει για αποθήκευση, Ακυρο για απόρριψη των αλλαγών.",
"discard-changes": "Απόρριψη αλλαγών",
"discard-changes-description": "Εχετε μη αποθηκευμένες αλλαγές, θέλετε σίγουρα να τις απορρίψετε;",
"clipboard-copy-failure": "Η αντιγραφή στο πρόχειρο απέτυχε.",
"confirm-delete-generic-items": "Θέλετε σίγουρα να διαγράψετε τα ακόλουθα αντικείμενα;",
"organizers": "Οργανωτές",
@@ -334,7 +336,7 @@
"only-recipes-with-these-categories-will-be-used-in-meal-plans": "Μόνο συνταγές με αυτές τις κατηγορίες θα χρησιμοποιηθούν στα προγράμματα γευμάτων",
"planner": "Προγραμματισμός",
"quick-week": "Γρήγορη προβολή",
"side": "Πλευρά",
"side": "Συνοδευτικό",
"sides": "Πλευρές",
"start-date": "Ημερομηνία έναρξης",
"rule-day": "Ημέρα/ες Κανόνα",
@@ -342,6 +344,9 @@
"breakfast": "Πρωινό",
"lunch": "Μεσημεριανό",
"dinner": "Βραδινό",
"snack": "Σνακ",
"drink": "Ποτό",
"dessert": "Επιδόρπιο",
"type-any": "Οτιδήποτε",
"day-any": "Οποιαδήποτε",
"editor": "Επεξεργαστής κειμένου",
@@ -359,12 +364,14 @@
"for-type-meal-types": "για γεύματα τύπου {0}",
"meal-plan-rules": "Κανόνες Προγράμματος Γευμάτων",
"new-rule": "Νέος κανόνας",
"meal-plan-rules-description": "Μπορείτε να δημιουργήσετε κανόνες για την αυτόματη επιλογή συνταγών για τα προγράμματα γευμάτων. Αυτοί οι κανόνες χρησιμοποιούνται από το διακομιστή για τον προσδιορισμό της τυχαίας δεξαμενής συνταγών από τις οποίες μπορείτε να επιλέξετε κατά τη δημιουργία προγραμμάτων γευμάτων. Σημειώστε ότι αν οι κανόνες έχουν τους ίδιους περιορισμούς ημέρας/τύπου τότε τα φίλτρα κανόνων θα συγχωνευθούν. Στην πράξη, είναι περιττή η δημιουργία διπλότυπων κανόνων, είναι όμως εφικτή.",
"meal-plan-rules-description": "Μπορείτε να δημιουργήσετε κανόνες για την αυτόματη επιλογή συνταγών για τα προγράμματα γευμάτων. Αυτοί οι κανόνες χρησιμοποιούνται από το διακομιστή για τον προσδιορισμό της δεξαμενής τυχαίας επιλογής συνταγής, κατά τη δημιουργία προγραμμάτων γευμάτων. Σημειώστε ότι αν οι κανόνες έχουν τους ίδιους περιορισμούς ημέρας/τύπου τότε τα φίλτρα κανόνων θα συγχωνευθούν. Στην πράξη, είναι περιττή η δημιουργία διπλότυπων κανόνων, είναι όμως εφικτή.",
"new-rule-description": "Κατά τη δημιουργία ενός νέου κανόνα για ένα σχέδιο γεύματος, μπορείτε να περιορίσετε τον κανόνα ώστε να ισχύει για μια συγκεκριμένη ημέρα της εβδομάδας ή/και ένα συγκεκριμένο τύπο γεύματος. Για να εφαρμόσετε έναν κανόνα σε όλες τις ημέρες ή σε όλους τους τύπους γεύματος μπορείτε να ορίσετε τον κανόνα σε \"Ολα\" που θα τον εφαρμόσει σε όλες τις πιθανές τιμές για την ημέρα ή/και τον τύπο γεύματος.",
"recipe-rules": "Κανόνες Συνταγής",
"applies-to-all-days": "Εφαρμόζεται για όλες τις ημέρες",
"applies-on-days": "Εφαρμόζεται κάθε {0}",
"meal-plan-settings": "Ρυθμίσεις προγράμματος γευμάτων"
"meal-plan-settings": "Ρυθμίσεις προγράμματος γευμάτων",
"add-all-to-list": "Προσθήκη όλων στη λίστα",
"add-day-to-list": "Προσθήκη ημέρας στη λίστα"
},
"migration": {
"migration-data-removed": "Τα δεδομένα μετεγκατάστασης καταργήθηκαν",
@@ -442,6 +449,7 @@
"upload-a-recipe": "Ανεβάστε μια συνταγή",
"upload-individual-zip-file": "Ανεβάστε ένα μεμονωμένο αρχείο .zip που εξάγεται από μια άλλη περίπτωση Mealie.",
"url-form-hint": "Αντιγράψτε και επικολλήστε έναν σύνδεσμο από την αγαπημένη σας ιστοσελίδα συνταγών",
"copy-and-paste-the-source-url-of-your-data-optional": "Αντιγράψτε και επικολλήστε το πηγαίο URL των δεδομένων σας (προαιρετικό)",
"view-scraped-data": "Προβολή Παραγόμενων Δεδομένων",
"trim-whitespace-description": "Περικοπή κενών στην αρχή και το τέλος καθώς και των κενών γραμμών",
"trim-prefix-description": "Περικοπή πρώτου χαρακτήρα από κάθε γραμμή",
@@ -633,7 +641,10 @@
"scrape-recipe-suggest-bulk-importer": "Δοκιμάστε τον μαζικό εισαγωγέα συνταγών μας",
"scrape-recipe-have-raw-html-or-json-data": "Εχουν ακατέργαστα δεδομένα HTML ή JSON;",
"scrape-recipe-you-can-import-from-raw-data-directly": "Μπορείτε να κάνετε εισαγωγή απευθείας από ακατέργαστα δεδομένα",
"scrape-recipe-website-being-blocked": "Η ιστοσελίδα μπλοκάρεται;",
"scrape-recipe-try-importing-raw-html-instead": "Δοκιμάστε να εισάγετε τον ακατέργαστο κώδικα HTML.",
"import-original-keywords-as-tags": "Εισαγωγή αρχικών λέξεων-κλειδιών ως ετικέτες",
"import-original-categories": "Import original categories",
"stay-in-edit-mode": "Παραμονή σε λειτουργία επεξεργασίας",
"parse-recipe-ingredients-after-import": "Ανάλυση συστατικών συνταγής μετά την εισαγωγή",
"import-from-zip": "Εισαγωγή μέσω zip",
@@ -873,9 +884,9 @@
"secure-site": "Ασφαλής Ιστοσελίδα",
"secure-site-error-text": "Παροχή μέσω localhost ή ασφάλεια με https. Το πρόχειρο και τα πρόσθετα API προγράμματος περιήγησης μπορεί να μην λειτουργούν.",
"secure-site-success-text": "Ο ιστότοπος έχει πρόσβαση από localhost ή https",
"server-side-base-url": "Βασική Διεύθυνση URL Πλευράς Διακομιστή",
"server-side-base-url": "Βασική διεύθυνση URL πλευράς διακομιστή",
"server-side-base-url-error-text": "Το `BASE_URL` εξακολουθεί να είναι η προεπιλεγμένη τιμή στο διακομιστή API. Αυτό θα προκαλέσει προβλήματα με τις συνδέσεις ειδοποιήσεων που δημιουργούνται στο διακομιστή για email, κλπ.",
"server-side-base-url-success-text": "Το URL Πλευράς Διακομιστή δεν ταιριάζει με την προεπιλογή",
"server-side-base-url-success-text": "Η διεύθυνση URL πλευράς διακομιστή δεν ταιριάζει με την προεπιλεγμένη",
"ldap-ready": "Ετοιμο για LDAP",
"ldap-ready-error-text": "Δεν έχουν ρυθμιστεί όλες οι τιμές LDAP. Αυτό μπορεί να αγνοηθεί αν δεν χρησιμοποιείτε έλεγχο ταυτότητας LDAP.",
"ldap-ready-success-text": "Ολες οι απαιτούμενες μεταβλητές LDAP έχουν οριστεί.",
@@ -1422,5 +1433,13 @@
"is-like": "είναι όμοιο με",
"is-not-like": "δεν είναι όμοιο με"
}
},
"validators": {
"required": "Αυτό το πεδίο είναι υποχρεωτικό",
"invalid-email": "Το e-mail πρέπει να είναι έγκυρο",
"invalid-url": "Πρέπει να είναι μια έγκυρη διεύθυνση URL",
"no-whitespace": "Δεν επιτρέπονται κενοί χαρακτήρες",
"min-length": "Πρέπει να αποτελείται από τουλάχιστον {min} χαρακτήρες",
"max-length": "Πρέπει να αποτελείται το πολύ από {max} χαρακτήρες"
}
}

View File

@@ -212,6 +212,8 @@
"upload-file": "Upload File",
"created-on-date": "Created on: {0}",
"unsaved-changes": "You have unsaved changes. Do you want to save before leaving? Okay to save, Cancel to discard changes.",
"discard-changes": "Discard Changes",
"discard-changes-description": "You have unsaved changes. Are you sure you want to discard them?",
"clipboard-copy-failure": "Failed to copy to the clipboard.",
"confirm-delete-generic-items": "Are you sure you want to delete the following items?",
"organizers": "Organisers",
@@ -342,6 +344,9 @@
"breakfast": "Breakfast",
"lunch": "Lunch",
"dinner": "Dinner",
"snack": "Snack",
"drink": "Drink",
"dessert": "Dessert",
"type-any": "Any",
"day-any": "Any",
"editor": "Editor",
@@ -364,7 +369,9 @@
"recipe-rules": "Recipe Rules",
"applies-to-all-days": "Applies to all days",
"applies-on-days": "Applies on {0}s",
"meal-plan-settings": "Meal Plan Settings"
"meal-plan-settings": "Meal Plan Settings",
"add-all-to-list": "Add All to List",
"add-day-to-list": "Add Day to List"
},
"migration": {
"migration-data-removed": "Migration data removed",
@@ -442,6 +449,7 @@
"upload-a-recipe": "Upload a Recipe",
"upload-individual-zip-file": "Upload an individual .zip file exported from another Mealie instance.",
"url-form-hint": "Copy and paste a link from your favourite recipe website",
"copy-and-paste-the-source-url-of-your-data-optional": "Copy and paste the source URL of your data (optional)",
"view-scraped-data": "View Scraped Data",
"trim-whitespace-description": "Trim leading and trailing whitespace as well as blank lines",
"trim-prefix-description": "Trim first character from each line",
@@ -633,7 +641,10 @@
"scrape-recipe-suggest-bulk-importer": "Try out the bulk importer",
"scrape-recipe-have-raw-html-or-json-data": "Have raw HTML or JSON data?",
"scrape-recipe-you-can-import-from-raw-data-directly": "You can import from raw data directly",
"scrape-recipe-website-being-blocked": "Website being blocked?",
"scrape-recipe-try-importing-raw-html-instead": "Try importing the raw HTML instead.",
"import-original-keywords-as-tags": "Import original keywords as tags",
"import-original-categories": "Import original categories",
"stay-in-edit-mode": "Stay in Edit mode",
"parse-recipe-ingredients-after-import": "Parse recipe ingredients after import",
"import-from-zip": "Import from Zip",
@@ -1422,5 +1433,13 @@
"is-like": "is like",
"is-not-like": "is not like"
}
},
"validators": {
"required": "This Field is Required",
"invalid-email": "Email Must Be Valid",
"invalid-url": "Must Be A Valid URL",
"no-whitespace": "No Whitespace Allowed",
"min-length": "Must Be At Least {min} Characters",
"max-length": "Must Be At Most {max} Characters"
}
}

View File

@@ -212,6 +212,8 @@
"upload-file": "Upload File",
"created-on-date": "Created on: {0}",
"unsaved-changes": "You have unsaved changes. Do you want to save before leaving? Okay to save, Cancel to discard changes.",
"discard-changes": "Discard Changes",
"discard-changes-description": "You have unsaved changes. Are you sure you want to discard them?",
"clipboard-copy-failure": "Failed to copy to the clipboard.",
"confirm-delete-generic-items": "Are you sure you want to delete the following items?",
"organizers": "Organizers",
@@ -342,6 +344,9 @@
"breakfast": "Breakfast",
"lunch": "Lunch",
"dinner": "Dinner",
"snack": "Snack",
"drink": "Drink",
"dessert": "Dessert",
"type-any": "Any",
"day-any": "Any",
"editor": "Editor",
@@ -364,7 +369,9 @@
"recipe-rules": "Recipe Rules",
"applies-to-all-days": "Applies to all days",
"applies-on-days": "Applies on {0}s",
"meal-plan-settings": "Meal Plan Settings"
"meal-plan-settings": "Meal Plan Settings",
"add-all-to-list": "Add All to List",
"add-day-to-list": "Add Day to List"
},
"migration": {
"migration-data-removed": "Migration data removed",
@@ -442,6 +449,7 @@
"upload-a-recipe": "Upload a Recipe",
"upload-individual-zip-file": "Upload an individual .zip file exported from another Mealie instance.",
"url-form-hint": "Copy and paste a link from your favorite recipe website",
"copy-and-paste-the-source-url-of-your-data-optional": "Copy and paste the source URL of your data (optional)",
"view-scraped-data": "View Scraped Data",
"trim-whitespace-description": "Trim leading and trailing whitespace as well as blank lines",
"trim-prefix-description": "Trim first character from each line",
@@ -633,7 +641,10 @@
"scrape-recipe-suggest-bulk-importer": "Try out the bulk importer",
"scrape-recipe-have-raw-html-or-json-data": "Have raw HTML or JSON data?",
"scrape-recipe-you-can-import-from-raw-data-directly": "You can import from raw data directly",
"scrape-recipe-website-being-blocked": "Website being blocked?",
"scrape-recipe-try-importing-raw-html-instead": "Try importing the raw HTML instead.",
"import-original-keywords-as-tags": "Import original keywords as tags",
"import-original-categories": "Import original categories",
"stay-in-edit-mode": "Stay in Edit mode",
"parse-recipe-ingredients-after-import": "Parse recipe ingredients after import",
"import-from-zip": "Import from Zip",
@@ -1422,5 +1433,13 @@
"is-like": "is like",
"is-not-like": "is not like"
}
},
"validators": {
"required": "This Field is Required",
"invalid-email": "Email Must Be Valid",
"invalid-url": "Must Be A Valid URL",
"no-whitespace": "No Whitespace Allowed",
"min-length": "Must Be At Least {min} Characters",
"max-length": "Must Be At Most {max} Characters"
}
}

View File

@@ -212,6 +212,8 @@
"upload-file": "Subir Archivo",
"created-on-date": "Creado el {0}",
"unsaved-changes": "Tienes cambios sin guardar. ¿Quieres guardar antes de salir? Aceptar para guardar, Cancelar para descartar cambios.",
"discard-changes": "Discard Changes",
"discard-changes-description": "You have unsaved changes. Are you sure you want to discard them?",
"clipboard-copy-failure": "No se pudo copiar al portapapeles.",
"confirm-delete-generic-items": "¿Estás seguro que quieres eliminar los siguientes elementos?",
"organizers": "Organizadores",
@@ -342,6 +344,9 @@
"breakfast": "Desayuno",
"lunch": "Comida principal",
"dinner": "Cena",
"snack": "Snack",
"drink": "Bebida",
"dessert": "Postre",
"type-any": "Cualquiera",
"day-any": "Cualquier",
"editor": "Editor",
@@ -364,7 +369,9 @@
"recipe-rules": "Reglas de Recetas",
"applies-to-all-days": "Aplica para todos los días",
"applies-on-days": "Se aplica en {0}s",
"meal-plan-settings": "Configuración del Plan de Comidas"
"meal-plan-settings": "Configuración del Plan de Comidas",
"add-all-to-list": "Add All to List",
"add-day-to-list": "Add Day to List"
},
"migration": {
"migration-data-removed": "Datos de migración eliminados",
@@ -442,6 +449,7 @@
"upload-a-recipe": "Subir una receta",
"upload-individual-zip-file": "Sube un archivo .zip individual exportado desde otra instancia de Mealie.",
"url-form-hint": "Copia y pega un enlace desde tu página web favorita",
"copy-and-paste-the-source-url-of-your-data-optional": "Copia y pega la URL de origen de tus datos (opcional)",
"view-scraped-data": "Ver información recuperada",
"trim-whitespace-description": "Eliminar espacios en blanco iniciales y finales así como líneas en blanco",
"trim-prefix-description": "Eliminar el primer carácter de cada línea",
@@ -449,7 +457,7 @@
"import-by-url": "Importar una receta desde URL",
"create-manually": "Crear receta manualmente",
"make-recipe-image": "Haz de esta la imagen de la receta",
"add-food": "Add Food",
"add-food": "Agregar comida",
"add-recipe": "Agregar receta"
},
"page": {
@@ -519,7 +527,7 @@
"recipe-image-updated": "Imagen de la receta actualizada",
"delete-image": "Borra la imagen de la receta",
"delete-image-confirmation": "¿Estás seguro de que quieres borrar esta imagen de la receta?",
"recipe-image-deleted": "Recipe image deleted",
"recipe-image-deleted": "Imagen de receta eliminada",
"recipe-name": "Nombre de la receta",
"recipe-settings": "Ajustes de la receta",
"recipe-update-failed": "Error al actualizar la receta",
@@ -565,7 +573,7 @@
"choose-unit": "Elija unidad",
"press-enter-to-create": "Presione Intro para crear",
"choose-food": "Elija comida",
"choose-recipe": "Choose Recipe",
"choose-recipe": "Elige la receta",
"notes": "Notas",
"toggle-section": "Activar sección",
"see-original-text": "Mostrar Texto Original",
@@ -593,7 +601,7 @@
"made-this": "Lo hice",
"how-did-it-turn-out": "¿Cómo resultó esto?",
"user-made-this": "{user} hizo esto",
"made-for-recipe": "Made for {recipe}",
"made-for-recipe": "Hecha para {recipe}",
"added-to-timeline": "Añadido a la línea de tiempo",
"failed-to-add-to-timeline": "No se pudo agregar a la línea de tiempo",
"failed-to-update-recipe": "Error al actualizar la receta",
@@ -633,7 +641,10 @@
"scrape-recipe-suggest-bulk-importer": "Prueba el importador masivo",
"scrape-recipe-have-raw-html-or-json-data": "¿Tiene datos HTML o JSON?",
"scrape-recipe-you-can-import-from-raw-data-directly": "Puede importar directamente desde datos brutos",
"scrape-recipe-website-being-blocked": "¿Sitio web bloqueado?",
"scrape-recipe-try-importing-raw-html-instead": "Intenta importar el HTML en bruto.",
"import-original-keywords-as-tags": "Importar palabras clave originales como etiquetas",
"import-original-categories": "Import original categories",
"stay-in-edit-mode": "Permanecer en modo edición",
"parse-recipe-ingredients-after-import": "Analizar los ingredientes de la receta después de importarla",
"import-from-zip": "Importar desde zip",
@@ -696,8 +707,8 @@
"upload-more-images": "Subir más imágenes",
"set-as-cover-image": "Establecer como imagen de portada de receta",
"cover-image": "Imagen de portada",
"include-linked-recipes": "Include Linked Recipes",
"include-linked-recipe-ingredients": "Include Linked Recipe Ingredients",
"include-linked-recipes": "Incluye recetas vinculadas",
"include-linked-recipe-ingredients": "Incluye ingredientes de receta vinculados",
"toggle-recipe": "Alternar Receta"
},
"recipe-finder": {
@@ -736,7 +747,7 @@
"advanced": "Avanzado",
"auto-search": "Búsqueda automática",
"no-results": "No se encontraron resultados",
"type-to-search": "Type to search..."
"type-to-search": "Escribe para buscar..."
},
"settings": {
"add-a-new-theme": "Añadir un nuevo tema",
@@ -1422,5 +1433,13 @@
"is-like": "es como",
"is-not-like": "no es como"
}
},
"validators": {
"required": "Este campo es requerido",
"invalid-email": "El email debe ser válido",
"invalid-url": "Debe ser una URL válida",
"no-whitespace": "No se permiten espacios en blanco",
"min-length": "Debe ser como mínimo {min} caracteres",
"max-length": "Debe ser como máximo {max} caracteres"
}
}

View File

@@ -212,6 +212,8 @@
"upload-file": "Lae fail üles",
"created-on-date": "Loodud: {0}",
"unsaved-changes": "Sul on salvestamata muudatusi. Kas sa tahad salvestada enne lehelt lahkumist? Vajuta OK salvestamiseks või Tühista, et muudatused tühistada.",
"discard-changes": "Discard Changes",
"discard-changes-description": "You have unsaved changes. Are you sure you want to discard them?",
"clipboard-copy-failure": "Lõikepuhvrisse kopeerimine ebaõnnestus.",
"confirm-delete-generic-items": "Kas oled kindel, et tahad kustutada järgnevad asjad?",
"organizers": "Korraldajad",
@@ -342,6 +344,9 @@
"breakfast": "Hommikusöök",
"lunch": "Lõuna",
"dinner": "Õhtusöök",
"snack": "Snack",
"drink": "Drink",
"dessert": "Dessert",
"type-any": "Kõik",
"day-any": "Kõik",
"editor": "Editor",
@@ -364,7 +369,9 @@
"recipe-rules": "Retsepti reeglid",
"applies-to-all-days": "Kehtib kõikide päevade kohta",
"applies-on-days": "Kehtib {0}l",
"meal-plan-settings": "Toitumisplaani sätted"
"meal-plan-settings": "Toitumisplaani sätted",
"add-all-to-list": "Add All to List",
"add-day-to-list": "Add Day to List"
},
"migration": {
"migration-data-removed": "Ületoomiste andmed eemaldatud",
@@ -442,6 +449,7 @@
"upload-a-recipe": "Retsepti üleslaadimne",
"upload-individual-zip-file": "Lae üles üksik .zip fail, mis eksporditi teisest Mealie ekspemplarist.",
"url-form-hint": "Kopeeri ja kleebi link oma lemmikust retsepti leheküljest",
"copy-and-paste-the-source-url-of-your-data-optional": "Copy and paste the source URL of your data (optional)",
"view-scraped-data": "Kuva omandatud andmed",
"trim-whitespace-description": "Eemalda alguses ning lõpus olevad tühikud ning tühjad read",
"trim-prefix-description": "Eemalda esimene tähemärk igast reast",
@@ -633,7 +641,10 @@
"scrape-recipe-suggest-bulk-importer": "Proovi hulgiimportimist.",
"scrape-recipe-have-raw-html-or-json-data": "Sul on töötlemata HTMLi või JSONi andmed?",
"scrape-recipe-you-can-import-from-raw-data-directly": "Sa võid otse importida töötlemata andmetest",
"scrape-recipe-website-being-blocked": "Website being blocked?",
"scrape-recipe-try-importing-raw-html-instead": "Try importing the raw HTML instead.",
"import-original-keywords-as-tags": "Impordi originaal võtmesõnad siltidena",
"import-original-categories": "Import original categories",
"stay-in-edit-mode": "Püsige redigeerimisrežiimis",
"parse-recipe-ingredients-after-import": "Tuvasta retsepti koostisosad pärast importimist",
"import-from-zip": "Impordi .zip-st",
@@ -1422,5 +1433,13 @@
"is-like": "on nagu",
"is-not-like": "ei ole nagu"
}
},
"validators": {
"required": "This Field is Required",
"invalid-email": "Email Must Be Valid",
"invalid-url": "Must Be A Valid URL",
"no-whitespace": "No Whitespace Allowed",
"min-length": "Must Be At Least {min} Characters",
"max-length": "Must Be At Most {max} Characters"
}
}

View File

@@ -2,7 +2,7 @@
"about": {
"about": "Tietoja",
"about-mealie": "Tietoja Mealiestä",
"api-docs": "API-dokumentit",
"api-docs": "API-dokumentaatio",
"api-port": "API-portti",
"application-mode": "Sovellustila",
"database-type": "Tietokannan tyyppi",
@@ -54,7 +54,7 @@
"apprise-url": "Apprise URL",
"database": "Tietokanta",
"delete-event": "Poista tapahtuma",
"event-delete-confirmation": "Oletko varma että haluat poistaa tämän tapahtuman?",
"event-delete-confirmation": "Haluatko varmasti poistaa tämän tapahtuman?",
"event-deleted": "Tapahtuma poistettu",
"event-updated": "Tapahtuma päivitetty",
"new-notification-form-description": "Mealie käyttää Apprise-kirjastoa ilmoitusten luomiseen. Se tarjoaa monia ilmoituspalvelveluvaihtoehtoja. Lisää tietoa siitä, kuinka luoda URL palvelua varten, löydät Apprisen wikistä. Joihinkin ilmoitustyyppeihin voi sisältyä lisäominaisuuksia.",
@@ -212,6 +212,8 @@
"upload-file": "Tuo tiedosto",
"created-on-date": "Luotu {0}",
"unsaved-changes": "Et ole tallentanut tekemiäsi muutoksia. Tallennetaanko ne? Paina \"ok\" tallentaaksesi ja \"peruuta\", jos et halua tallentaa.",
"discard-changes": "Discard Changes",
"discard-changes-description": "You have unsaved changes. Are you sure you want to discard them?",
"clipboard-copy-failure": "Kopioiminen leikepöydälle epäonnistui.",
"confirm-delete-generic-items": "Haluatko varmasti poistaa seuraavat kohteet?",
"organizers": "Järjestäjät",
@@ -342,6 +344,9 @@
"breakfast": "Aamiainen",
"lunch": "Lounas",
"dinner": "Päivällinen",
"snack": "Välipala",
"drink": "Juoma",
"dessert": "Jälkiruoka",
"type-any": "Mikä tahansa",
"day-any": "Koska tahansa",
"editor": "Editori",
@@ -364,7 +369,9 @@
"recipe-rules": "Reseptimääritykset",
"applies-to-all-days": "Sovelletaan kaikkiin päiviin",
"applies-on-days": "Käytetään {0}",
"meal-plan-settings": "Ateriasuunnitelman asetukset"
"meal-plan-settings": "Ateriasuunnitelman asetukset",
"add-all-to-list": "Add All to List",
"add-day-to-list": "Add Day to List"
},
"migration": {
"migration-data-removed": "Tuodut tiedot poistettu",
@@ -442,6 +449,7 @@
"upload-a-recipe": "Lataa resepti",
"upload-individual-zip-file": "Tuo yksittäinen pakattu kansio toisesta Mealie instanssista.",
"url-form-hint": "Liitä linkki lempireseptiverkkosivultasi",
"copy-and-paste-the-source-url-of-your-data-optional": "Copy and paste the source URL of your data (optional)",
"view-scraped-data": "Näytä hankittu data",
"trim-whitespace-description": "Leikkaa alussa ja lopussa olevat välilyönnit sekä tyhjät rivit",
"trim-prefix-description": "Poista joka rivin ensimmäinen merkki",
@@ -633,7 +641,10 @@
"scrape-recipe-suggest-bulk-importer": "Kokeile massasiirtotyökalua",
"scrape-recipe-have-raw-html-or-json-data": "Onko sinulla raakaa HTML- tai JSON-dataa?",
"scrape-recipe-you-can-import-from-raw-data-directly": "Voit tuoda raakadatan suoraan",
"scrape-recipe-website-being-blocked": "Onko sivusto estetty?",
"scrape-recipe-try-importing-raw-html-instead": "Try importing the raw HTML instead.",
"import-original-keywords-as-tags": "Tuo alkuperäiset avainsanat tunnisteiksi",
"import-original-categories": "Import original categories",
"stay-in-edit-mode": "Pysy muokkaustilassa",
"parse-recipe-ingredients-after-import": "Jäsennä reseptin ainesosat tuonnin jälkeen",
"import-from-zip": "Tuo zip-arkistosta",
@@ -1075,7 +1086,7 @@
"forgot-password": "Unohditko salasanasi",
"forgot-password-text": "Syötä sähköpostiosoitteesi, niin voit muuttaa salasanaasi linkin kautta.",
"changes-reflected-immediately": "Muutokset tähän käyttäjään astuvat välittömästi voimaan.",
"default-activity": "Oletus Toiminta",
"default-activity": "Oletusnäkymä",
"default-activity-hint": "Valitse haluamasi sivu, johon haluat navigoida kirjautuessasi tältä laitteelta"
},
"language-dialog": {
@@ -1422,5 +1433,13 @@
"is-like": "on kuin",
"is-not-like": "ei ole kuin"
}
},
"validators": {
"required": "Tämä kenttä on pakollinen",
"invalid-email": "Sähköpostiosoite ei ole kelvollinen",
"invalid-url": "URL ei ole kelvollinen",
"no-whitespace": "No Whitespace Allowed",
"min-length": "Vähimmäispituus on {min} merkkiä",
"max-length": "Enimmäispituus on {max} merkkiä"
}
}

View File

@@ -212,6 +212,8 @@
"upload-file": "Transférer un fichier",
"created-on-date": "Créé le {0}",
"unsaved-changes": "Vous avez des modifications non enregistrées. Voulez-vous enregistrer avant de partir? OK pour enregistrer, Annuler pour ignorer les modifications.",
"discard-changes": "Discard Changes",
"discard-changes-description": "You have unsaved changes. Are you sure you want to discard them?",
"clipboard-copy-failure": "Échec de la copie dans le presse-papiers.",
"confirm-delete-generic-items": "Êtes-vous sûr de vouloir supprimer les éléments suivants ?",
"organizers": "Classification",
@@ -342,6 +344,9 @@
"breakfast": "Petit-déjeuner",
"lunch": "Déjeuner",
"dinner": "Souper",
"snack": "Goûter",
"drink": "Boissons",
"dessert": "Dessert",
"type-any": "Tous",
"day-any": "Tous",
"editor": "Éditeur",
@@ -364,7 +369,9 @@
"recipe-rules": "Règles de recette",
"applies-to-all-days": "S'applique à tous les jours",
"applies-on-days": "S'applique les {0}s",
"meal-plan-settings": "Paramètres des menus"
"meal-plan-settings": "Paramètres des menus",
"add-all-to-list": "Add All to List",
"add-day-to-list": "Add Day to List"
},
"migration": {
"migration-data-removed": "Données de migration supprimées",
@@ -442,6 +449,7 @@
"upload-a-recipe": "Télécharger une recette",
"upload-individual-zip-file": "Chargez un fichier .zip exporté depuis une autre instance Mealie.",
"url-form-hint": "Copiez et collez un lien depuis votre site de recettes favori",
"copy-and-paste-the-source-url-of-your-data-optional": "Copiez et collez l'URL source de vos données (facultatif)",
"view-scraped-data": "Voir les données récupérées",
"trim-whitespace-description": "Ajuster les espaces de début et de fin ainsi que les lignes vides",
"trim-prefix-description": "Couper le premier caractère de chaque ligne",
@@ -565,7 +573,7 @@
"choose-unit": "Choisissez une unité",
"press-enter-to-create": "Clique sur Entrer pour créer",
"choose-food": "Choisissez un aliment",
"choose-recipe": "Choose Recipe",
"choose-recipe": "Choisissez la recette",
"notes": "Notes",
"toggle-section": "Activer/Désactiver la section",
"see-original-text": "Afficher le texte original",
@@ -633,7 +641,10 @@
"scrape-recipe-suggest-bulk-importer": "Essayez limportateur de masse",
"scrape-recipe-have-raw-html-or-json-data": "Vous avez des données brutes en HTML ou JSON ?",
"scrape-recipe-you-can-import-from-raw-data-directly": "Vous pouvez directement importer des données brutes",
"scrape-recipe-website-being-blocked": "Le site web est bloqué ?",
"scrape-recipe-try-importing-raw-html-instead": "Essayez plutôt d'importer le code HTML brut.",
"import-original-keywords-as-tags": "Importer les mots-clés d'origine en tant que tags",
"import-original-categories": "Import original categories",
"stay-in-edit-mode": "Rester en mode édition",
"parse-recipe-ingredients-after-import": "Analyser les ingrédients de la recette après l'import",
"import-from-zip": "Importer depuis un zip",
@@ -736,7 +747,7 @@
"advanced": "Avancé",
"auto-search": "Recherche automatique",
"no-results": "Aucun résultat trouvé",
"type-to-search": "Type to search..."
"type-to-search": "Tapez pour chercher..."
},
"settings": {
"add-a-new-theme": "Ajouter un nouveau thème",
@@ -1422,5 +1433,13 @@
"is-like": "est comme",
"is-not-like": "n'est pas similaire à"
}
},
"validators": {
"required": "Ce champ est obligatoire",
"invalid-email": "Le-mail doit être valide",
"invalid-url": "Doit être une URL valide",
"no-whitespace": "Aucun espace n'est autorisé",
"min-length": "Doit contenir au moins {min} caractères",
"max-length": "Doit contenir au maximum {max} caractères"
}
}

View File

@@ -212,6 +212,8 @@
"upload-file": "Téléverser un fichier",
"created-on-date": "Créé le {0}",
"unsaved-changes": "Vous avez des modifications non enregistrées. Voulez-vous les enregistrer? Ok pour enregistrer, annuler pour ignorer les modifications.",
"discard-changes": "Discard Changes",
"discard-changes-description": "You have unsaved changes. Are you sure you want to discard them?",
"clipboard-copy-failure": "Échec de la copie vers le presse-papiers.",
"confirm-delete-generic-items": "Êtes-vous sûr de vouloir supprimer les éléments suivants ?",
"organizers": "Classification",
@@ -342,6 +344,9 @@
"breakfast": "Petit déjeuner",
"lunch": "Dîner",
"dinner": "Souper",
"snack": "Goûter",
"drink": "Boissons",
"dessert": "Dessert",
"type-any": "Tous",
"day-any": "Tous",
"editor": "Éditeur",
@@ -364,7 +369,9 @@
"recipe-rules": "Règles de recette",
"applies-to-all-days": "S'applique à tous les jours",
"applies-on-days": "S'applique les {0}s",
"meal-plan-settings": "Paramètres des menus"
"meal-plan-settings": "Paramètres des menus",
"add-all-to-list": "Add All to List",
"add-day-to-list": "Add Day to List"
},
"migration": {
"migration-data-removed": "Données de migration supprimées",
@@ -442,6 +449,7 @@
"upload-a-recipe": "Télécharger une recette",
"upload-individual-zip-file": "Téléverser un fichier .zip exporté depuis une autre instance Mealie.",
"url-form-hint": "Copiez et collez un lien depuis votre site de recettes favori",
"copy-and-paste-the-source-url-of-your-data-optional": "Copiez et collez l'URL source de vos données (facultatif)",
"view-scraped-data": "Voir les données récupérées",
"trim-whitespace-description": "Ajuster les espaces de début et de fin ainsi que les lignes vides",
"trim-prefix-description": "Couper le premier caractère de chaque ligne",
@@ -633,7 +641,10 @@
"scrape-recipe-suggest-bulk-importer": "Essayez limportateur de masse",
"scrape-recipe-have-raw-html-or-json-data": "Vous avez des données brutes en HTML ou JSON ?",
"scrape-recipe-you-can-import-from-raw-data-directly": "Vous pouvez directement importer des données brutes",
"scrape-recipe-website-being-blocked": "Le site web est bloqué ?",
"scrape-recipe-try-importing-raw-html-instead": "Essayez plutôt d'importer le code HTML brut.",
"import-original-keywords-as-tags": "Importer les mots-clés d'origine en tant que tags",
"import-original-categories": "Import original categories",
"stay-in-edit-mode": "Rester en mode édition",
"parse-recipe-ingredients-after-import": "Analyser les ingrédients de la recette après l'import",
"import-from-zip": "Importer depuis un zip",
@@ -1422,5 +1433,13 @@
"is-like": "est similaire à",
"is-not-like": "n'est pas similaire à"
}
},
"validators": {
"required": "Ce champ est obligatoire",
"invalid-email": "Le-mail doit être valide",
"invalid-url": "Doit être une URL valide",
"no-whitespace": "Aucun espace n'est autorisé",
"min-length": "",
"max-length": "Doit contenir au maximum {max} caractères"
}
}

View File

@@ -212,6 +212,8 @@
"upload-file": "Téléverser un fichier",
"created-on-date": "Créé le {0}",
"unsaved-changes": "Vous avez des modifications non enregistrées. Voulez-vous enregistrer avant de partir? OK pour enregistrer, Annuler pour ignorer les modifications.",
"discard-changes": "Discard Changes",
"discard-changes-description": "You have unsaved changes. Are you sure you want to discard them?",
"clipboard-copy-failure": "Échec de la copie dans le presse-papiers.",
"confirm-delete-generic-items": "Êtes-vous sûr de vouloir supprimer les éléments suivants ?",
"organizers": "Classification",
@@ -337,11 +339,14 @@
"side": "Accompagnement",
"sides": "Accompagnements",
"start-date": "Date de début",
"rule-day": "Jour de la Règle",
"rule-day": "Jour de la règle",
"meal-type": "Type de repas",
"breakfast": "Petit-déjeuner",
"lunch": "Déjeuner",
"dinner": "Dîner",
"snack": "Goûter",
"drink": "Boissons",
"dessert": "Dessert",
"type-any": "Tous",
"day-any": "Tous",
"editor": "Éditeur",
@@ -364,7 +369,9 @@
"recipe-rules": "Règles de recette",
"applies-to-all-days": "S'applique à tous les jours",
"applies-on-days": "S'applique les {0}s",
"meal-plan-settings": "Paramètres des menus"
"meal-plan-settings": "Paramètres des menus",
"add-all-to-list": "Add All to List",
"add-day-to-list": "Add Day to List"
},
"migration": {
"migration-data-removed": "Données de migration supprimées",
@@ -442,6 +449,7 @@
"upload-a-recipe": "Télécharger une recette",
"upload-individual-zip-file": "Chargez un fichier .zip exporté depuis une autre instance Mealie.",
"url-form-hint": "Copiez et collez un lien depuis votre site de recettes favori",
"copy-and-paste-the-source-url-of-your-data-optional": "Copiez et collez l'URL source de vos données (facultatif)",
"view-scraped-data": "Voir les données récupérées",
"trim-whitespace-description": "Ajuster les espaces de début et de fin ainsi que les lignes vides",
"trim-prefix-description": "Couper le premier caractère de chaque ligne",
@@ -633,7 +641,10 @@
"scrape-recipe-suggest-bulk-importer": "Essayez limportateur de masse",
"scrape-recipe-have-raw-html-or-json-data": "Vous avez des données brutes en HTML ou JSON ?",
"scrape-recipe-you-can-import-from-raw-data-directly": "Vous pouvez directement importer des données brutes",
"scrape-recipe-website-being-blocked": "Le site web est bloqué ?",
"scrape-recipe-try-importing-raw-html-instead": "Essayez plutôt d'importer le code HTML brut.",
"import-original-keywords-as-tags": "Importer les mots-clés d'origine en tant que tags",
"import-original-categories": "Import original categories",
"stay-in-edit-mode": "Rester en mode édition",
"parse-recipe-ingredients-after-import": "Analyser les ingrédients de la recette après l'import",
"import-from-zip": "Importer depuis un zip",
@@ -1422,5 +1433,13 @@
"is-like": "est comme",
"is-not-like": "n'est pas similaire à"
}
},
"validators": {
"required": "Ce champ est obligatoire",
"invalid-email": "Le-mail doit être valide",
"invalid-url": "Doit être une URL valide",
"no-whitespace": "Aucun espace n'est autorisé",
"min-length": "Doit contenir au moins {min} caractères",
"max-length": "Doit contenir au maximum {max} caractères"
}
}

View File

@@ -212,6 +212,8 @@
"upload-file": "Subir Arquivo",
"created-on-date": "Creado o: {0}",
"unsaved-changes": "Tes cambios sen gardar. Queres gardar antes de saír? OK para gardar, Cancelar para descartar cambios.",
"discard-changes": "Discard Changes",
"discard-changes-description": "You have unsaved changes. Are you sure you want to discard them?",
"clipboard-copy-failure": "Produciuse un erro ao copiar contido no portapapeis.",
"confirm-delete-generic-items": "Estás seguro de que queres eliminar os seguintes elementos?",
"organizers": "Organizadores",
@@ -342,6 +344,9 @@
"breakfast": "Almorzo",
"lunch": "Xantar",
"dinner": "Cea",
"snack": "Snack",
"drink": "Drink",
"dessert": "Dessert",
"type-any": "Calquera",
"day-any": "Calquera",
"editor": "Editor",
@@ -364,7 +369,9 @@
"recipe-rules": "Regras da Receita",
"applies-to-all-days": "Aplícase a todos os días",
"applies-on-days": "Aplícase en {0}s",
"meal-plan-settings": "Axustes do Menú"
"meal-plan-settings": "Axustes do Menú",
"add-all-to-list": "Add All to List",
"add-day-to-list": "Add Day to List"
},
"migration": {
"migration-data-removed": "Elimináronse os datos de migración",
@@ -442,6 +449,7 @@
"upload-a-recipe": "Cargar unha Receita",
"upload-individual-zip-file": "Cargar un ficheiro .zip individual, exportado de outra instancia do Mealie.",
"url-form-hint": "Copie e pegue un link do seu site de receitas favorito",
"copy-and-paste-the-source-url-of-your-data-optional": "Copy and paste the source URL of your data (optional)",
"view-scraped-data": "Ver datos recollidos",
"trim-whitespace-description": "Eliminar os espazos en branco no início e no fin, asi como as liñas en branco",
"trim-prefix-description": "Eliminar o primeiro caracter de cada liña",
@@ -633,7 +641,10 @@
"scrape-recipe-suggest-bulk-importer": "Prove o importador en masa",
"scrape-recipe-have-raw-html-or-json-data": "Ten datos HTML ou JSON en bruto?",
"scrape-recipe-you-can-import-from-raw-data-directly": "É posível importar diretamente a partir de datos en bruto",
"scrape-recipe-website-being-blocked": "Website being blocked?",
"scrape-recipe-try-importing-raw-html-instead": "Try importing the raw HTML instead.",
"import-original-keywords-as-tags": "Importar palavras-chave orixinais como etiquetas",
"import-original-categories": "Import original categories",
"stay-in-edit-mode": "Permanecer no modo de edición",
"parse-recipe-ingredients-after-import": "Parse recipe ingredients after import",
"import-from-zip": "Importar de Zip",
@@ -1422,5 +1433,13 @@
"is-like": "é como",
"is-not-like": "non é como"
}
},
"validators": {
"required": "This Field is Required",
"invalid-email": "Email Must Be Valid",
"invalid-url": "Must Be A Valid URL",
"no-whitespace": "No Whitespace Allowed",
"min-length": "Must Be At Least {min} Characters",
"max-length": "Must Be At Most {max} Characters"
}
}

View File

@@ -212,6 +212,8 @@
"upload-file": "העלאת קבצים",
"created-on-date": "נוצר ב-{0}",
"unsaved-changes": "יש שינויים שלא נשמרו. לצאת לפני שמירה? אשר לשמירה, בטל למחיקת שינויים.",
"discard-changes": "Discard Changes",
"discard-changes-description": "You have unsaved changes. Are you sure you want to discard them?",
"clipboard-copy-failure": "כשלון בהעתקה ללוח ההדבקה.",
"confirm-delete-generic-items": "למחוק את הפריטים שנבחרו?",
"organizers": "מארגנים",
@@ -342,6 +344,9 @@
"breakfast": "ארוחת בוקר",
"lunch": "ארוחת צהריים",
"dinner": "ארוחת ערב",
"snack": "Snack",
"drink": "Drink",
"dessert": "Dessert",
"type-any": "הכל",
"day-any": "הכל",
"editor": "עורך",
@@ -364,7 +369,9 @@
"recipe-rules": "חוקי מתכון",
"applies-to-all-days": "החל על כל הימים",
"applies-on-days": "חל על {0}",
"meal-plan-settings": "הגדרות תכנון ארוחות"
"meal-plan-settings": "הגדרות תכנון ארוחות",
"add-all-to-list": "Add All to List",
"add-day-to-list": "Add Day to List"
},
"migration": {
"migration-data-removed": "מידע ממוגרץ נמחק",
@@ -442,6 +449,7 @@
"upload-a-recipe": "העלאת מתכון",
"upload-individual-zip-file": "העלאת קובץ זיפ שיוצא ממילי אחר.",
"url-form-hint": "העתק והדבק קישור מאתר המתכונים המועדף עליך",
"copy-and-paste-the-source-url-of-your-data-optional": "Copy and paste the source URL of your data (optional)",
"view-scraped-data": "צפייה במידע שנאסף",
"trim-whitespace-description": "הסר רווחים מתחילת / סוף שורה ושורות ריקות",
"trim-prefix-description": "חתוך תו ראשון מכל שורה",
@@ -633,7 +641,10 @@
"scrape-recipe-suggest-bulk-importer": "נסה את יכולת קריאת רשימה",
"scrape-recipe-have-raw-html-or-json-data": "יש לך מידע גולמי ב-HTML או JSON?",
"scrape-recipe-you-can-import-from-raw-data-directly": "ניתן לייבא ישירות ממידע גולמי",
"scrape-recipe-website-being-blocked": "Website being blocked?",
"scrape-recipe-try-importing-raw-html-instead": "Try importing the raw HTML instead.",
"import-original-keywords-as-tags": "ייבוא שמות מפתח מקוריות כתגיות",
"import-original-categories": "Import original categories",
"stay-in-edit-mode": "השאר במצב עריכה",
"parse-recipe-ingredients-after-import": "Parse recipe ingredients after import",
"import-from-zip": "ייבא מקובץ",
@@ -1422,5 +1433,13 @@
"is-like": "דומה ל-",
"is-not-like": "לא דומה לא-"
}
},
"validators": {
"required": "This Field is Required",
"invalid-email": "Email Must Be Valid",
"invalid-url": "Must Be A Valid URL",
"no-whitespace": "No Whitespace Allowed",
"min-length": "Must Be At Least {min} Characters",
"max-length": "Must Be At Most {max} Characters"
}
}

Some files were not shown because too many files have changed in this diff Show More